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) return false;
106 // SNES Checksum region
108 return true;
109 // ZSCustom Version/Flags region
110 if (i >= 0x140141 && i <= 0x140148)
111 return true;
112 return false;
113 };
114
115 if (scan_all) {
116 // Perform full ROM scan for differences
117 uint32_t start = 0;
118 bool in_diff = false;
119 size_t diff_count = 0;
120
121 for (uint32_t i = 0; i < min_size; ++i) {
122 if (is_ignored(i)) continue;
123
124 if (target[i] != baseline[i]) {
125 if (!in_diff) {
126 start = i;
127 in_diff = true;
128 diff_count = 0;
129 }
130 diff_count++;
131 } else if (in_diff) {
133 diff.start = start;
134 diff.end = i;
135 diff.diff_count = diff_count;
136 diff.region_name = "Modified Region";
137 diff.critical = false;
138 result.diff_regions.push_back(diff);
139 result.total_diff_bytes += diff_count;
140 in_diff = false;
141 }
142 }
143 return;
144 }
145
146 for (const auto& region : kCriticalRegions) {
147 if (!region_filter.empty() && region.category != region_filter) {
148 continue;
149 }
150
151 if (region.start >= min_size) {
152 continue;
153 }
154
155 uint32_t end = std::min(region.end, static_cast<uint32_t>(min_size));
156 size_t diff_count = 0;
157
158 for (uint32_t i = region.start; i < end; ++i) {
159 if (is_ignored(i)) continue;
160
161 if (target[i] != baseline[i]) {
162 diff_count++;
163 }
164 }
165
166 if (diff_count > 0) {
168 diff.start = region.start;
169 diff.end = end;
170 diff.diff_count = diff_count;
171 diff.region_name = region.name;
172 diff.critical = region.critical;
173 result.diff_regions.push_back(diff);
174 result.total_diff_bytes += diff_count;
175 }
176 }
177}
178
179// =============================================================================
180// Output Helpers
181// =============================================================================
182
184 const std::string& prefix,
185 const RomCompareResult::RomInfo& info) {
186 formatter.AddField(prefix + "_filename", info.filename);
187 formatter.AddField(prefix + "_size", static_cast<int>(info.size));
188 formatter.AddField(prefix + "_version", GetVersionString(info.zs_version));
189 formatter.AddField(prefix + "_expanded_tile16", info.has_expanded_tile16);
190 formatter.AddField(prefix + "_expanded_tile32", info.has_expanded_tile32);
191 formatter.AddHexField(prefix + "_checksum", info.checksum, 8);
192}
193
194void OutputTextBanner(bool is_json) {
195 if (is_json) {
196 return;
197 }
198 std::cout << "\n";
199 std::cout
200 << "╔═══════════════════════════════════════════════════════════════╗\n";
201 std::cout
202 << "║ ROM COMPARE ║\n";
203 std::cout
204 << "║ Baseline Comparison Tool ║\n";
205 std::cout
206 << "╚═══════════════════════════════════════════════════════════════╝\n";
207}
208
210 std::cout << "\n=== ROM Information ===\n";
211 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "", "Baseline", "Target");
212 std::cout << std::string(80, '-') << "\n";
213 std::cout << absl::StrFormat("%-20s %-30zu %-30zu\n", "Size (bytes)",
214 result.baseline.size, result.target.size);
215 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "ZSCustom Version",
218 std::cout << absl::StrFormat(
219 "%-20s %-30s %-30s\n", "Expanded Tile16",
220 result.baseline.has_expanded_tile16 ? "YES" : "NO",
221 result.target.has_expanded_tile16 ? "YES" : "NO");
222 std::cout << absl::StrFormat(
223 "%-20s %-30s %-30s\n", "Expanded Tile32",
224 result.baseline.has_expanded_tile32 ? "YES" : "NO",
225 result.target.has_expanded_tile32 ? "YES" : "NO");
226 std::cout << absl::StrFormat("%-20s 0x%08X%21s 0x%08X\n", "Checksum",
227 result.baseline.checksum, "",
228 result.target.checksum);
229}
230
232 std::cout << "\n=== Difference Summary ===\n";
233
234 if (result.diff_regions.empty()) {
235 std::cout << "No differences found in specified regions.\n";
236 return;
237 }
238
239 std::cout << absl::StrFormat(
240 "Found differences in %zu regions (%zu bytes total):\n",
241 result.diff_regions.size(), result.total_diff_bytes);
242
243 for (const auto& diff : result.diff_regions) {
244 std::string marker = diff.critical ? "[CRITICAL] " : "";
245 std::cout << absl::StrFormat(" %s%-25s 0x%06X-0x%06X %6zu bytes differ\n",
246 marker, diff.region_name, diff.start, diff.end,
247 diff.diff_count);
248 }
249}
250
251void OutputTextDetailedDiff(const std::vector<uint8_t>& target,
252 const std::vector<uint8_t>& baseline,
253 const RomCompareResult::DiffRegion& region,
254 int max_samples) {
255 std::cout << absl::StrFormat("\n Differences in %s (0x%06X-0x%06X):\n",
256 region.region_name, region.start, region.end);
257
258 int samples_shown = 0;
259 for (uint32_t i = region.start; i < region.end && samples_shown < max_samples;
260 ++i) {
261 if (target[i] != baseline[i]) {
262 std::cout << absl::StrFormat(
263 " 0x%06X: base 0x%02X | target 0x%02X\n", i, baseline[i],
264 target[i]);
265 samples_shown++;
266 }
267 }
268
269 if (region.diff_count > static_cast<size_t>(max_samples)) {
270 std::cout << absl::StrFormat(" ... and %zu more differences\n",
271 region.diff_count - max_samples);
272 }
273}
274
276 std::cout << "\n";
277 std::cout
278 << "╔═══════════════════════════════════════════════════════════════╗\n";
279 std::cout
280 << "║ ASSESSMENT ║\n";
281 std::cout
282 << "╠═══════════════════════════════════════════════════════════════╣\n";
283
284 bool has_issues = false;
285
286 if (!result.sizes_match) {
287 std::cout
288 << "║ SIZE MISMATCH: ROMs have different sizes ║\n";
289 has_issues = true;
290 }
291
292 for (const auto& diff : result.diff_regions) {
293 if (diff.critical) {
294 std::cout << absl::StrFormat(
295 "║ WARNING: %s modified (%zu bytes)%-14s ║\n", diff.region_name,
296 diff.diff_count, "");
297 has_issues = true;
298 }
299 }
300
301 if (!has_issues && result.diff_regions.empty()) {
302 std::cout
303 << "║ ROMs are identical in all checked regions ║\n";
304 } else if (!has_issues) {
305 std::cout
306 << "║ ROMs have expected differences (modifications detected) ║\n";
307 }
308
309 std::cout
310 << "╚═══════════════════════════════════════════════════════════════╝\n";
311}
312
313} // namespace
314
316 Rom* rom, const resources::ArgumentParser& parser,
317 resources::OutputFormatter& formatter) {
318 auto baseline_path = parser.GetString("baseline");
319 bool verbose = parser.HasFlag("verbose");
320 bool show_diff = parser.HasFlag("show-diff");
321 bool smart_diff = parser.HasFlag("smart");
322 bool scan_all = parser.HasFlag("all");
323 std::string region_filter = parser.GetString("region").value_or("");
324 bool is_json = formatter.IsJson();
325
326 if (!baseline_path.has_value()) {
327 return absl::InvalidArgumentError(
328 "Missing required --baseline <path> argument.\n"
329 "Usage: rom-compare --rom <target> --baseline <baseline.sfc>");
330 }
331
332 OutputTextBanner(is_json);
333
334 // Load baseline ROM
335 if (!is_json) {
336 std::cout << "Loading baseline ROM: " << baseline_path.value() << "\n";
337 }
338
339 std::ifstream baseline_file(baseline_path.value(), std::ios::binary);
340 if (!baseline_file) {
341 return absl::NotFoundError(
342 absl::StrFormat("Cannot open baseline ROM: %s", baseline_path.value()));
343 }
344
345 std::vector<uint8_t> baseline_data(
346 (std::istreambuf_iterator<char>(baseline_file)),
347 std::istreambuf_iterator<char>());
348 baseline_file.close();
349
350 // Get target ROM data
351 const std::vector<uint8_t>& target_data = rom->vector();
352
353 // Analyze both ROMs
354 RomCompareResult result;
355 result.baseline = AnalyzeRom(baseline_data, baseline_path.value());
356 result.target = AnalyzeRom(target_data, rom->filename());
357
358 result.sizes_match = (result.target.size == result.baseline.size);
359 result.versions_match =
360 (result.target.zs_version == result.baseline.zs_version);
361 result.features_match = (result.target.has_expanded_tile16 ==
363 (result.target.has_expanded_tile32 ==
365
366 // Find differences
367 FindDiffRegions(target_data, baseline_data, result, smart_diff, region_filter, scan_all);
368
369 // JSON output
370 OutputRomInfoJson(formatter, "baseline", result.baseline);
371 OutputRomInfoJson(formatter, "target", result.target);
372 formatter.AddField("sizes_match", result.sizes_match);
373 formatter.AddField("versions_match", result.versions_match);
374 formatter.AddField("features_match", result.features_match);
375 formatter.AddField("total_diff_bytes",
376 static_cast<int>(result.total_diff_bytes));
377
378 formatter.BeginArray("diff_regions");
379 for (const auto& diff : result.diff_regions) {
380 std::string json = absl::StrFormat(
381 R"({"name":"%s","start":"0x%06X","end":"0x%06X","diff_count":%zu,"critical":%s})",
382 diff.region_name, diff.start, diff.end, diff.diff_count,
383 diff.critical ? "true" : "false");
384 formatter.AddArrayItem(json);
385 }
386 formatter.EndArray();
387
388 // Check for critical issues
389 bool has_critical = false;
390 for (const auto& diff : result.diff_regions) {
391 if (diff.critical) {
392 has_critical = true;
393 break;
394 }
395 }
396 formatter.AddField("has_critical_differences", has_critical);
397 formatter.AddField("assessment", has_critical
398 ? "warning"
399 : (result.diff_regions.empty()
400 ? "identical"
401 : "expected_differences"));
402
403 // Text output
404 if (!is_json) {
405 OutputTextRomInfo(result);
406 OutputTextDiffSummary(result);
407
408 if (show_diff && !result.diff_regions.empty()) {
409 std::cout << "\n=== Detailed Differences ===\n";
410 for (const auto& diff : result.diff_regions) {
411 OutputTextDetailedDiff(target_data, baseline_data, diff,
412 verbose ? 10 : 5);
413 }
414 }
415
416 OutputTextAssessment(result);
417 }
418
419 return absl::OkStatus();
420}
421
422} // 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:28
auto filename() const
Definition rom.h:145
const auto & vector() const
Definition rom.h:143
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)
Namespace for the command line interface.
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