yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_compare_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdint>
5#include <fstream>
6#include <iostream>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/strings/str_format.h"
12#include "rom/rom.h"
13
14namespace yaze::cli {
15
16namespace {
17
18// =============================================================================
19// ROM Region Definitions
20// =============================================================================
21
22struct RomRegion {
23 const char* name;
24 uint32_t start;
25 uint32_t end;
27 const char* category;
28};
29
31 {"Map32 Ptr Low", 0x1794D, 0x17B2D, true, "overworld"},
32 {"Map32 Ptr High", 0x17B2D, 0x17D0D, true, "overworld"},
33 {"Overworld Data", 0x70000, 0x78000, true, "overworld"},
34 {"Tile16 Vanilla", 0x78000, 0x78000 + (3752 * 8), false, "overworld"},
35 {"Tile16 Expanded", 0x1E8000, 0x1F0000, false, "overworld"},
36 {"Tile32 BL Expanded", 0x1F0000, 0x1F8000, false, "overworld"},
37 {"Tile32 BR Expanded", 0x1F8000, 0x200000, false, "overworld"},
38 {"Dungeon Ptr Table", 0x01F800, 0x01FB00, true, "dungeon"},
39 {"Dungeon Room Data", 0x1D8000, 0x1E8000, true, "dungeon"},
40 {"Message Data", 0x1C0000, 0x1D8000, false, "text"},
41 {"ZSCustom Tables", 0x140000, 0x142000, false, "system"},
42 {"Overlay Space", 0x120000, 0x130000, false, "overworld"},
43 {"SNES Header", 0x7FC0, 0x8000, true, "system"},
44 {"Bank 00 Code", 0x000000, 0x008000, true, "code"},
45 {"Bank 01 Code", 0x008000, 0x010000, true, "code"},
46 {"Bank 02 Code", 0x010000, 0x018000, true, "code"},
47};
48
49// =============================================================================
50// Checksum Calculation
51// =============================================================================
52
53uint32_t CalculateChecksum(const std::vector<uint8_t>& data) {
54 uint32_t sum = 0;
55 for (size_t i = 0; i < data.size(); ++i) {
56 sum += data[i];
57 }
58 return sum;
59}
60
61// =============================================================================
62// ROM Analysis
63// =============================================================================
64
65RomCompareResult::RomInfo AnalyzeRom(const std::vector<uint8_t>& data,
66 const std::string& name) {
68 info.filename = name;
69 info.size = data.size();
70 info.checksum = CalculateChecksum(data);
71
72 if (kZSCustomVersionPos < data.size()) {
74 }
75
76 bool is_vanilla = (info.zs_version == 0xFF || info.zs_version == 0x00);
77
78 if (!is_vanilla) {
79 if (kMap16ExpandedFlagPos < data.size()) {
80 info.has_expanded_tile16 = (data[kMap16ExpandedFlagPos] != 0x0F);
81 }
82
83 if (kMap32ExpandedFlagPos < data.size()) {
84 info.has_expanded_tile32 = (data[kMap32ExpandedFlagPos] != 0x04);
85 }
86 }
87
88 return info;
89}
90
91std::string GetVersionString(uint8_t version) {
92 if (version == 0xFF || version == 0x00) {
93 return "Vanilla";
94 }
95 return absl::StrFormat("v%d", version);
96}
97
98void FindDiffRegions(const std::vector<uint8_t>& target,
99 const std::vector<uint8_t>& baseline,
100 RomCompareResult& result, bool smart_diff,
101 const std::string& region_filter, bool scan_all) {
102 size_t min_size = std::min(target.size(), baseline.size());
103
104 auto is_ignored = [&](uint32_t i) {
105 if (!smart_diff)
106 return false;
107 // SNES Checksum region
110 return true;
111 // ZSCustom Version/Flags region
112 if (i >= 0x140141 && i <= 0x140148)
113 return true;
114 return false;
115 };
116
117 if (scan_all) {
118 // Perform full ROM scan for differences
119 uint32_t start = 0;
120 bool in_diff = false;
121 size_t diff_count = 0;
122
123 for (uint32_t i = 0; i < min_size; ++i) {
124 if (is_ignored(i))
125 continue;
126
127 if (target[i] != baseline[i]) {
128 if (!in_diff) {
129 start = i;
130 in_diff = true;
131 diff_count = 0;
132 }
133 diff_count++;
134 } else if (in_diff) {
136 diff.start = start;
137 diff.end = i;
138 diff.diff_count = diff_count;
139 diff.region_name = "Modified Region";
140 diff.critical = false;
141 result.diff_regions.push_back(diff);
142 result.total_diff_bytes += diff_count;
143 in_diff = false;
144 }
145 }
146 return;
147 }
148
149 auto checked = [&](uint32_t i) {
150 for (const auto& region : kCriticalRegions) {
151 if ((region_filter.empty() || region.category == region_filter) &&
152 i >= region.start && i < region.end) {
153 return true;
154 }
155 }
156 return false;
157 };
158 for (uint32_t i = 0; i < min_size; ++i) {
159 if (target[i] != baseline[i] && !is_ignored(i) && !checked(i)) {
160 result.unchecked_diff_bytes++;
161 }
162 }
163
164 for (const auto& region : kCriticalRegions) {
165 if (!region_filter.empty() && region.category != region_filter) {
166 continue;
167 }
168
169 if (region.start >= min_size) {
170 continue;
171 }
172
173 uint32_t end = std::min(region.end, static_cast<uint32_t>(min_size));
174 size_t diff_count = 0;
175
176 for (uint32_t i = region.start; i < end; ++i) {
177 if (is_ignored(i))
178 continue;
179
180 if (target[i] != baseline[i]) {
181 diff_count++;
182 }
183 }
184
185 if (diff_count > 0) {
187 diff.start = region.start;
188 diff.end = end;
189 diff.diff_count = diff_count;
190 diff.region_name = region.name;
191 diff.critical = region.critical;
192 result.diff_regions.push_back(diff);
193 result.total_diff_bytes += diff_count;
194 }
195 }
196}
197
198// =============================================================================
199// Output Helpers
200// =============================================================================
201
203 const std::string& prefix,
204 const RomCompareResult::RomInfo& info) {
205 formatter.AddField(prefix + "_filename", info.filename);
206 formatter.AddField(prefix + "_size", static_cast<int>(info.size));
207 formatter.AddField(prefix + "_version", GetVersionString(info.zs_version));
208 formatter.AddField(prefix + "_expanded_tile16", info.has_expanded_tile16);
209 formatter.AddField(prefix + "_expanded_tile32", info.has_expanded_tile32);
210 formatter.AddHexField(prefix + "_checksum", info.checksum, 8);
211}
212
213void OutputTextBanner(bool is_json) {
214 if (is_json) {
215 return;
216 }
217 std::cout << "\n";
218 std::cout
219 << "╔═══════════════════════════════════════════════════════════════╗\n";
220 std::cout
221 << "║ ROM COMPARE ║\n";
222 std::cout
223 << "║ Baseline Comparison Tool ║\n";
224 std::cout
225 << "╚═══════════════════════════════════════════════════════════════╝\n";
226}
227
229 std::cout << "\n=== ROM Information ===\n";
230 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "", "Baseline", "Target");
231 std::cout << std::string(80, '-') << "\n";
232 std::cout << absl::StrFormat("%-20s %-30zu %-30zu\n", "Size (bytes)",
233 result.baseline.size, result.target.size);
234 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "ZSCustom Version",
237 std::cout << absl::StrFormat(
238 "%-20s %-30s %-30s\n", "Expanded Tile16",
239 result.baseline.has_expanded_tile16 ? "YES" : "NO",
240 result.target.has_expanded_tile16 ? "YES" : "NO");
241 std::cout << absl::StrFormat(
242 "%-20s %-30s %-30s\n", "Expanded Tile32",
243 result.baseline.has_expanded_tile32 ? "YES" : "NO",
244 result.target.has_expanded_tile32 ? "YES" : "NO");
245 std::cout << absl::StrFormat("%-20s 0x%08X%21s 0x%08X\n", "Checksum",
246 result.baseline.checksum, "",
247 result.target.checksum);
248}
249
251 std::cout << "\n=== Difference Summary ===\n";
252
253 if (result.diff_regions.empty()) {
254 std::cout << "No differences found in specified regions.\n";
255 return;
256 }
257
258 std::cout << absl::StrFormat(
259 "Found differences in %zu regions (%zu bytes total):\n",
260 result.diff_regions.size(), result.total_diff_bytes);
261
262 for (const auto& diff : result.diff_regions) {
263 std::string marker = diff.critical ? "[CRITICAL] " : "";
264 std::cout << absl::StrFormat(" %s%-25s 0x%06X-0x%06X %6zu bytes differ\n",
265 marker, diff.region_name, diff.start, diff.end,
266 diff.diff_count);
267 }
268}
269
270void OutputTextDetailedDiff(const std::vector<uint8_t>& target,
271 const std::vector<uint8_t>& baseline,
272 const RomCompareResult::DiffRegion& region,
273 int max_samples) {
274 std::cout << absl::StrFormat("\n Differences in %s (0x%06X-0x%06X):\n",
275 region.region_name, region.start, region.end);
276
277 int samples_shown = 0;
278 for (uint32_t i = region.start; i < region.end && samples_shown < max_samples;
279 ++i) {
280 if (target[i] != baseline[i]) {
281 std::cout << absl::StrFormat(
282 " 0x%06X: base 0x%02X | target 0x%02X\n", i, baseline[i],
283 target[i]);
284 samples_shown++;
285 }
286 }
287
288 if (region.diff_count > static_cast<size_t>(max_samples)) {
289 std::cout << absl::StrFormat(" ... and %zu more differences\n",
290 region.diff_count - max_samples);
291 }
292}
293
295 std::cout << "\n";
296 std::cout
297 << "╔═══════════════════════════════════════════════════════════════╗\n";
298 std::cout
299 << "║ ASSESSMENT ║\n";
300 std::cout
301 << "╠═══════════════════════════════════════════════════════════════╣\n";
302
303 bool has_issues = false;
304
305 if (!result.sizes_match) {
306 std::cout
307 << "║ SIZE MISMATCH: ROMs have different sizes ║\n";
308 has_issues = true;
309 }
310
311 for (const auto& diff : result.diff_regions) {
312 if (diff.critical) {
313 std::cout << absl::StrFormat(
314 "║ WARNING: %s modified (%zu bytes)%-14s ║\n", diff.region_name,
315 diff.diff_count, "");
316 has_issues = true;
317 }
318 }
319
320 if (result.unchecked_diff_bytes > 0) {
321 std::cout << absl::StrFormat(
322 "║ %-7zu byte(s) differ outside the checked regions (--all) ║\n",
323 result.unchecked_diff_bytes);
324 }
325 if (!has_issues && result.diff_regions.empty() &&
326 result.unchecked_diff_bytes == 0) {
327 std::cout
328 << "║ ROMs are identical in all checked regions ║\n";
329 } else if (!has_issues) {
330 std::cout
331 << "║ ROMs have expected differences (modifications detected) ║\n";
332 }
333
334 std::cout
335 << "╚═══════════════════════════════════════════════════════════════╝\n";
336}
337
338} // namespace
339
341 Rom* rom, const resources::ArgumentParser& parser,
342 resources::OutputFormatter& formatter) {
343 auto baseline_path = parser.GetString("baseline");
344 bool verbose = parser.HasFlag("verbose");
345 bool show_diff = parser.HasFlag("show-diff");
346 bool smart_diff = parser.HasFlag("smart");
347 bool scan_all = parser.HasFlag("all");
348 std::string region_filter = parser.GetString("region").value_or("");
349 bool is_json = formatter.IsJson();
350
351 if (!baseline_path.has_value()) {
352 return absl::InvalidArgumentError(
353 "Missing required --baseline <path> argument.\n"
354 "Usage: rom-compare --rom <target> --baseline <baseline.sfc>");
355 }
356
357 OutputTextBanner(is_json);
358
359 // Load baseline ROM
360 if (!is_json) {
361 std::cout << "Loading baseline ROM: " << baseline_path.value() << "\n";
362 }
363
364 std::ifstream baseline_file(baseline_path.value(), std::ios::binary);
365 if (!baseline_file) {
366 return absl::NotFoundError(
367 absl::StrFormat("Cannot open baseline ROM: %s", baseline_path.value()));
368 }
369
370 std::vector<uint8_t> baseline_data(
371 (std::istreambuf_iterator<char>(baseline_file)),
372 std::istreambuf_iterator<char>());
373 baseline_file.close();
374
375 // Get target ROM data
376 const std::vector<uint8_t>& target_data = rom->vector();
377
378 // Analyze both ROMs
379 RomCompareResult result;
380 result.baseline = AnalyzeRom(baseline_data, baseline_path.value());
381 result.target = AnalyzeRom(target_data, rom->filename());
382
383 result.sizes_match = (result.target.size == result.baseline.size);
384 result.versions_match =
385 (result.target.zs_version == result.baseline.zs_version);
386 result.features_match = (result.target.has_expanded_tile16 ==
388 (result.target.has_expanded_tile32 ==
390
391 // Find differences
392 FindDiffRegions(target_data, baseline_data, result, smart_diff, region_filter,
393 scan_all);
394
395 // JSON output
396 OutputRomInfoJson(formatter, "baseline", result.baseline);
397 OutputRomInfoJson(formatter, "target", result.target);
398 formatter.AddField("sizes_match", result.sizes_match);
399 formatter.AddField("versions_match", result.versions_match);
400 formatter.AddField("features_match", result.features_match);
401 formatter.AddField("total_diff_bytes",
402 static_cast<int>(result.total_diff_bytes));
403 formatter.AddField("unchecked_diff_bytes",
404 static_cast<int>(result.unchecked_diff_bytes));
405
406 formatter.BeginArray("diff_regions");
407 for (const auto& diff : result.diff_regions) {
408 std::string json = absl::StrFormat(
409 R"({"name":"%s","start":"0x%06X","end":"0x%06X","diff_count":%zu,"critical":%s})",
410 diff.region_name, diff.start, diff.end, diff.diff_count,
411 diff.critical ? "true" : "false");
412 formatter.AddArrayItem(json);
413 }
414 formatter.EndArray();
415
416 // Check for critical issues
417 bool has_critical = false;
418 for (const auto& diff : result.diff_regions) {
419 if (diff.critical) {
420 has_critical = true;
421 break;
422 }
423 }
424 formatter.AddField("has_critical_differences", has_critical);
425 const char* assessment = "expected_differences";
426 if (has_critical) {
427 assessment = "warning";
428 } else if (result.diff_regions.empty()) {
429 assessment = result.unchecked_diff_bytes > 0
430 ? "differences_outside_checked_regions"
431 : "identical";
432 }
433 formatter.AddField("assessment", assessment);
434
435 // Text output
436 if (!is_json) {
437 OutputTextRomInfo(result);
438 OutputTextDiffSummary(result);
439
440 if (show_diff && !result.diff_regions.empty()) {
441 std::cout << "\n=== Detailed Differences ===\n";
442 for (const auto& diff : result.diff_regions) {
443 OutputTextDetailedDiff(target_data, baseline_data, diff,
444 verbose ? 10 : 5);
445 }
446 }
447
448 OutputTextAssessment(result);
449 }
450
451 return absl::OkStatus();
452}
453
454} // namespace yaze::cli
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
auto filename() const
Definition rom.h:219
const auto & vector() const
Definition rom.h:217
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::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
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.
RomCompareResult::RomInfo AnalyzeRom(const std::vector< uint8_t > &data, const std::string &name)
void OutputTextDetailedDiff(const std::vector< uint8_t > &target, const std::vector< uint8_t > &baseline, const RomCompareResult::DiffRegion &region, int max_samples)
uint32_t CalculateChecksum(const std::vector< uint8_t > &data)
void FindDiffRegions(const std::vector< uint8_t > &target, const std::vector< uint8_t > &baseline, RomCompareResult &result, bool smart_diff, const std::string &region_filter, bool scan_all)
void OutputRomInfoJson(resources::OutputFormatter &formatter, const std::string &prefix, const RomCompareResult::RomInfo &info)
constexpr uint32_t kChecksumPos
constexpr uint32_t kMap32ExpandedFlagPos
constexpr uint32_t kZSCustomVersionPos
constexpr uint32_t kMap16ExpandedFlagPos
constexpr uint32_t kChecksumComplementPos
ROM comparison result for baseline comparisons.
std::vector< DiffRegion > diff_regions