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"
22 int bank = pc_addr / 0x8000;
23 int addr = (pc_addr % 0x8000) + 0x8000;
24 return absl::StrFormat(
"%02X:%04X", bank, addr);
30 int pc_addr = ((bank & 0x7F) * 0x8000) + (addr - 0x8000);
34void PrintHexDump(
const std::vector<uint8_t>& data,
int offset,
int size,
38 for (
int i = 0; i < size; i += 16) {
40 if (mode == AddressMode::kSnes) {
43 absl::StrAppend(&output, absl::StrFormat(
"%06X: ", offset + i));
46 for (
int j = 0; j < 16; ++j) {
48 absl::StrAppend(&output, absl::StrFormat(
"%02X ", data[i + j]));
50 absl::StrAppend(&output,
" ");
54 absl::StrAppend(&output,
" |");
57 for (
int j = 0; j < 16; ++j) {
59 unsigned char c = data[i + j];
60 if (c >= 32 && c <= 126) {
61 absl::StrAppend(&output, absl::StrFormat(
"%c", c));
63 absl::StrAppend(&output,
".");
67 absl::StrAppend(&output,
"|\n");
79 return absl::InvalidArgumentError(
"Missing ROM path.");
82 return absl::InvalidArgumentError(
"Missing offset.");
84 return absl::OkStatus();
96 return absl::InvalidArgumentError(
97 absl::StrFormat(
"Invalid size: %s", parser.
GetPositional()[2]));
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);
112 if (absl::StrContains(offset_str,
':')) {
113 std::vector<std::string> parts = absl::StrSplit(offset_str,
':');
114 if (parts.size() == 2) {
117 if (absl::SimpleAtoi(parts[0], &bank) &&
123 bank = std::stoi(parts[0],
nullptr, 16);
124 addr = std::stoi(parts[1],
nullptr, 16);
125 offset = SnesToPcLoRom(bank, addr);
127 return absl::InvalidArgumentError(
128 "Invalid LoROM SNES address (addr < 0x8000)");
132 mode = AddressMode::kSnes;
135 return absl::InvalidArgumentError(
136 "Invalid SNES address format (expected HEX:HEX)");
140 }
else if (offset_str.size() > 2 && offset_str.substr(0, 2) ==
"0x") {
142 offset = std::stoi(offset_str,
nullptr, 16);
144 return absl::InvalidArgumentError(
145 absl::StrFormat(
"Invalid hex offset: %s", offset_str));
148 if (!absl::SimpleAtoi(offset_str, &offset)) {
149 return absl::InvalidArgumentError(
150 absl::StrFormat(
"Invalid offset: %s", offset_str));
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()));
166 if (
static_cast<size_t>(offset) +
static_cast<size_t>(size) >
168 size =
static_cast<int>(local_rom.
size() -
static_cast<size_t>(offset));
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];
177 PrintHexDump(buffer, offset, size, mode, formatter);
178 return absl::OkStatus();
188 return absl::InvalidArgumentError(
189 "Missing ROM paths. Usage: hex-compare <rom1> <rom2>");
191 return absl::OkStatus();
201 int start_offset = 0;
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);
208 if (!absl::SimpleAtoi(*start_str, &start_offset)) {
209 return absl::InvalidArgumentError(
"Invalid start offset: " +
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);
219 if (!absl::SimpleAtoi(*end_str, &end_offset)) {
220 return absl::InvalidArgumentError(
"Invalid end offset: " + *end_str);
225 AddressMode mode = AddressMode::kPc;
226 if (
auto mode_arg = parser.
GetString(
"mode")) {
227 if (*mode_arg ==
"snes") {
228 mode = AddressMode::kSnes;
236 return absl::InvalidArgumentError(
"Failed to load ROM 1: " +
237 std::string(status1.message()));
242 return absl::InvalidArgumentError(
"Failed to load ROM 2: " +
243 std::string(status2.message()));
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;
252 if (
static_cast<size_t>(start_offset) >= compare_size) {
253 return absl::InvalidArgumentError(
"Start offset beyond ROM size");
257 std::vector<uint32_t> diff_offsets;
259 const int max_diff_display = 20;
261 const auto& data1 = rom1.
vector();
262 const auto& data2 = rom2.
vector();
264 for (
size_t i = start_offset; i < compare_size; ++i) {
265 if (data1[i] != data2[i]) {
267 if (diff_offsets.size() < max_diff_display) {
268 diff_offsets.push_back(
static_cast<uint32_t
>(i));
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);
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,
288 std::cout << absl::StrFormat(
"ROM 2: %s (%zu bytes)\n", rom2_path,
290 std::cout << absl::StrFormat(
"Range: 0x%06X - 0x%06zX\n", start_offset,
292 std::cout << absl::StrFormat(
"Total differences: %d\n\n", total_diffs);
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) +
" ";
302 std::cout << absl::StrFormat(
" %s0x%06X: 0x%02X 0x%02X\n", addr_str,
303 offset, data1[offset], data2[offset]);
305 if (total_diffs > max_diff_display) {
306 std::cout << absl::StrFormat(
" ... and %d more differences\n",
307 total_diffs - max_diff_display);
312 return absl::OkStatus();
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"},
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"},
376 {
"Y Position", 0, 1,
"decimal"},
377 {
"X/Subtype", 1, 1,
"hex"},
378 {
"Sprite ID", 2, 1,
"hex"},
385 {
"Tile TL", 0, 2,
"hex"},
386 {
"Tile TR", 2, 2,
"hex"},
387 {
"Tile BL", 4, 2,
"hex"},
388 {
"Tile BR", 6, 2,
"hex"},
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,
397 for (
const auto& field : structure.
fields) {
398 std::cout << absl::StrFormat(
" +%02X %-20s: ", field.offset, field.name);
400 if (field.format ==
"ascii") {
401 std::string ascii_val;
402 for (
int i = 0; i < field.size && field.offset + i < (int)data.size();
404 char c =
static_cast<char>(data[field.offset + i]);
405 if (c >= 32 && c < 127) {
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);
419 }
else if (field.format ==
"decimal") {
420 if (field.offset < (
int)data.size()) {
421 std::cout << static_cast<int>(data[field.offset]) <<
"\n";
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");
448 return absl::InvalidArgumentError(
"Missing ROM path.");
451 return absl::InvalidArgumentError(
"Missing offset.");
453 return absl::OkStatus();
464 if (offset_str.size() > 2 && offset_str.substr(0, 2) ==
"0x") {
466 offset = std::stoi(offset_str,
nullptr, 16);
468 return absl::InvalidArgumentError(
"Invalid hex offset: " + offset_str);
470 }
else if (absl::StrContains(offset_str,
':')) {
472 std::vector<std::string> parts = absl::StrSplit(offset_str,
':');
473 if (parts.size() == 2) {
475 int bank = std::stoi(parts[0],
nullptr, 16);
476 int addr = std::stoi(parts[1],
nullptr, 16);
477 offset = SnesToPcLoRom(bank, addr);
479 return absl::InvalidArgumentError(
480 "Invalid LoROM SNES address (addr < 0x8000)");
483 return absl::InvalidArgumentError(
484 "Invalid SNES address format (expected HEX:HEX)");
488 if (!absl::SimpleAtoi(offset_str, &offset)) {
489 return absl::InvalidArgumentError(
"Invalid offset: " + offset_str);
494 std::string type =
"auto";
495 if (
auto type_arg = parser.
GetString(
"type")) {
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()));
512 if (type ==
"auto") {
513 if (offset == 0x7FC0 || (offset >= 0x7FC0 && offset < 0x7FE0)) {
514 type =
"snes_header";
515 }
else if (offset >= 0x0 && offset < 0x10000) {
517 type =
"room_header";
519 type =
"room_header";
524 const DataStructure* structure =
nullptr;
525 if (type ==
"snes_header") {
526 structure = &kSnesHeaderStructure;
527 if (offset != 0x7FC0) {
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;
537 return absl::InvalidArgumentError(
"Unknown structure type: " + type);
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];
550 formatter.
AddField(
"rom_path", rom_path);
552 formatter.
AddField(
"structure_type", type);
553 formatter.
AddField(
"structure_name", structure->name);
554 formatter.
AddField(
"structure_size", structure->total_size);
556 if (!formatter.
IsJson()) {
557 PrintAnnotatedStructure(*structure, buffer, offset);
561 for (
const auto& field : structure->fields) {
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();
576 char c =
static_cast<char>(buffer[field.offset + i]);
577 if (c >= 32 && c < 127)
580 }
else if (field.format ==
"flags" && field.offset < (
int)buffer.size()) {
581 value = absl::StrFormat(
"0x%02X", buffer[field.offset]);
585 R
"({"name":"%s","offset":%d,"size":%d,"format":"%s","value":"%s"})",
586 field.name, field.offset, field.size, field.format, value));
591 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
const auto & vector() const
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)
void PrintHexDump(const std::vector< uint8_t > &data, int offset, int size, AddressMode mode, resources::OutputFormatter &formatter)
std::string PcToSnesLoRom(int pc_addr)
const DataStructure kSnesHeaderStructure
const DataStructure kSpriteStructure
int SnesToPcLoRom(int bank, int addr)
const DataStructure kTile16Structure
const DataStructure kRoomHeaderStructure
void PrintAnnotatedStructure(const DataStructure &structure, const std::vector< uint8_t > &data, int offset)
std::vector< AnnotatedField > fields