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"
25 int bank = pc_addr / 0x8000;
26 int addr = (pc_addr % 0x8000) + 0x8000;
27 return absl::StrFormat(
"%02X:%04X", bank, addr);
31 if (addr < 0x8000)
return -1;
32 int pc_addr = ((bank & 0x7F) * 0x8000) + (addr - 0x8000);
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");
78 return absl::InvalidArgumentError(
"Missing ROM path.");
81 return absl::InvalidArgumentError(
"Missing offset.");
83 return absl::OkStatus();
94 return absl::InvalidArgumentError(absl::StrFormat(
"Invalid size: %s", parser.
GetPositional()[2]));
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);
109 if (absl::StrContains(offset_str,
':')) {
110 std::vector<std::string> parts = absl::StrSplit(offset_str,
':');
111 if (parts.size() == 2) {
114 if (absl::SimpleAtoi(parts[0], &bank) && absl::SimpleAtoi(parts[1], &addr)) {
117 bank = std::stoi(parts[0],
nullptr, 16);
118 addr = std::stoi(parts[1],
nullptr, 16);
119 offset = SnesToPcLoRom(bank, addr);
121 return absl::InvalidArgumentError(
"Invalid LoROM SNES address (addr < 0x8000)");
125 mode = AddressMode::kSnes;
128 return absl::InvalidArgumentError(
"Invalid SNES address format (expected HEX:HEX)");
132 }
else if (offset_str.size() > 2 && offset_str.substr(0, 2) ==
"0x") {
134 offset = std::stoi(offset_str,
nullptr, 16);
136 return absl::InvalidArgumentError(absl::StrFormat(
"Invalid hex offset: %s", offset_str));
139 if (!absl::SimpleAtoi(offset_str, &offset)) {
140 return absl::InvalidArgumentError(absl::StrFormat(
"Invalid offset: %s", offset_str));
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()));
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));
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];
165 PrintHexDump(buffer, offset, size, mode, formatter);
166 return absl::OkStatus();
176 return absl::InvalidArgumentError(
177 "Missing ROM paths. Usage: hex-compare <rom1> <rom2>");
179 return absl::OkStatus();
189 int start_offset = 0;
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);
196 if (!absl::SimpleAtoi(*start_str, &start_offset)) {
197 return absl::InvalidArgumentError(
"Invalid start offset: " +
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);
207 if (!absl::SimpleAtoi(*end_str, &end_offset)) {
208 return absl::InvalidArgumentError(
"Invalid end offset: " + *end_str);
213 AddressMode mode = AddressMode::kPc;
214 if (
auto mode_arg = parser.
GetString(
"mode")) {
215 if (*mode_arg ==
"snes") {
216 mode = AddressMode::kSnes;
224 return absl::InvalidArgumentError(
"Failed to load ROM 1: " +
225 std::string(status1.message()));
230 return absl::InvalidArgumentError(
"Failed to load ROM 2: " +
231 std::string(status2.message()));
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;
240 if (
static_cast<size_t>(start_offset) >= compare_size) {
241 return absl::InvalidArgumentError(
"Start offset beyond ROM size");
245 std::vector<uint32_t> diff_offsets;
247 const int max_diff_display = 20;
249 const auto& data1 = rom1.
vector();
250 const auto& data2 = rom2.
vector();
252 for (
size_t i = start_offset; i < compare_size; ++i) {
253 if (data1[i] != data2[i]) {
255 if (diff_offsets.size() < max_diff_display) {
256 diff_offsets.push_back(
static_cast<uint32_t
>(i));
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);
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,
276 std::cout << absl::StrFormat(
"ROM 2: %s (%zu bytes)\n", rom2_path,
278 std::cout << absl::StrFormat(
"Range: 0x%06X - 0x%06zX\n", start_offset,
280 std::cout << absl::StrFormat(
"Total differences: %d\n\n", total_diffs);
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) +
" ";
290 std::cout << absl::StrFormat(
" %s0x%06X: 0x%02X 0x%02X\n", addr_str,
291 offset, data1[offset], data2[offset]);
293 if (total_diffs > max_diff_display) {
294 std::cout << absl::StrFormat(
" ... and %d more differences\n",
295 total_diffs - max_diff_display);
300 return absl::OkStatus();
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"},
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"},
365 {
"Y Position", 0, 1,
"decimal"},
366 {
"X/Subtype", 1, 1,
"hex"},
367 {
"Sprite ID", 2, 1,
"hex"},
375 {
"Tile TL", 0, 2,
"hex"},
376 {
"Tile TR", 2, 2,
"hex"},
377 {
"Tile BL", 4, 2,
"hex"},
378 {
"Tile BR", 6, 2,
"hex"},
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,
387 for (
const auto& field : structure.
fields) {
388 std::cout << absl::StrFormat(
" +%02X %-20s: ", field.offset, field.name);
390 if (field.format ==
"ascii") {
391 std::string ascii_val;
392 for (
int i = 0; i < field.size && field.offset + i < (int)data.size();
394 char c =
static_cast<char>(data[field.offset + i]);
395 if (c >= 32 && c < 127) {
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);
409 }
else if (field.format ==
"decimal") {
410 if (field.offset < (
int)data.size()) {
411 std::cout << static_cast<int>(data[field.offset]) <<
"\n";
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");
438 return absl::InvalidArgumentError(
"Missing ROM path.");
441 return absl::InvalidArgumentError(
"Missing offset.");
443 return absl::OkStatus();
454 if (offset_str.size() > 2 && offset_str.substr(0, 2) ==
"0x") {
456 offset = std::stoi(offset_str,
nullptr, 16);
458 return absl::InvalidArgumentError(
"Invalid hex offset: " + offset_str);
460 }
else if (absl::StrContains(offset_str,
':')) {
462 std::vector<std::string> parts = absl::StrSplit(offset_str,
':');
463 if (parts.size() == 2) {
465 int bank = std::stoi(parts[0],
nullptr, 16);
466 int addr = std::stoi(parts[1],
nullptr, 16);
467 offset = SnesToPcLoRom(bank, addr);
469 return absl::InvalidArgumentError(
470 "Invalid LoROM SNES address (addr < 0x8000)");
473 return absl::InvalidArgumentError(
474 "Invalid SNES address format (expected HEX:HEX)");
478 if (!absl::SimpleAtoi(offset_str, &offset)) {
479 return absl::InvalidArgumentError(
"Invalid offset: " + offset_str);
484 std::string type =
"auto";
485 if (
auto type_arg = parser.
GetString(
"type")) {
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()));
502 if (type ==
"auto") {
503 if (offset == 0x7FC0 || (offset >= 0x7FC0 && offset < 0x7FE0)) {
504 type =
"snes_header";
505 }
else if (offset >= 0x0 && offset < 0x10000) {
507 type =
"room_header";
509 type =
"room_header";
514 const DataStructure* structure =
nullptr;
515 if (type ==
"snes_header") {
516 structure = &kSnesHeaderStructure;
517 if (offset != 0x7FC0) {
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;
527 return absl::InvalidArgumentError(
"Unknown structure type: " + type);
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];
540 formatter.
AddField(
"rom_path", rom_path);
542 formatter.
AddField(
"structure_type", type);
543 formatter.
AddField(
"structure_name", structure->name);
544 formatter.
AddField(
"structure_size", structure->total_size);
546 if (!formatter.
IsJson()) {
547 PrintAnnotatedStructure(*structure, buffer, offset);
551 for (
const auto& field : structure->fields) {
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();
565 char c =
static_cast<char>(buffer[field.offset + i]);
566 if (c >= 32 && c < 127) value += c;
568 }
else if (field.format ==
"flags" && field.offset < (
int)buffer.size()) {
569 value = absl::StrFormat(
"0x%02X", buffer[field.offset]);
573 R
"({"name":"%s","offset":%d,"size":%d,"format":"%s","value":"%s"})",
574 field.name, field.offset, field.size, field.format, value));
579 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