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};
28
30 {"Map32 Ptr Low", 0x1794D, 0x17B2D, true},
31 {"Map32 Ptr High", 0x17B2D, 0x17D0D, true},
32 {"Tile16 Vanilla", 0x78000, 0x78000 + (3752 * 8), false},
33 {"Tile16 Expanded", 0x1E8000, 0x1F0000, false},
34 {"Tile32 BL Expanded", 0x1F0000, 0x1F8000, false},
35 {"Tile32 BR Expanded", 0x1F8000, 0x200000, false},
36 {"ZSCustom Tables", 0x140000, 0x142000, false},
37 {"Overlay Space", 0x120000, 0x130000, false},
38};
39
40// =============================================================================
41// Checksum Calculation
42// =============================================================================
43
44uint32_t CalculateChecksum(const std::vector<uint8_t>& data) {
45 uint32_t sum = 0;
46 for (size_t i = 0; i < data.size(); ++i) {
47 sum += data[i];
48 }
49 return sum;
50}
51
52// =============================================================================
53// ROM Analysis
54// =============================================================================
55
56RomCompareResult::RomInfo AnalyzeRom(const std::vector<uint8_t>& data,
57 const std::string& name) {
59 info.filename = name;
60 info.size = data.size();
61 info.checksum = CalculateChecksum(data);
62
63 if (kZSCustomVersionPos < data.size()) {
65 }
66
67 bool is_vanilla = (info.zs_version == 0xFF || info.zs_version == 0x00);
68
69 if (!is_vanilla) {
70 if (kMap16ExpandedFlagPos < data.size()) {
71 info.has_expanded_tile16 = (data[kMap16ExpandedFlagPos] != 0x0F);
72 }
73
74 if (kMap32ExpandedFlagPos < data.size()) {
75 info.has_expanded_tile32 = (data[kMap32ExpandedFlagPos] != 0x04);
76 }
77 }
78
79 return info;
80}
81
82std::string GetVersionString(uint8_t version) {
83 if (version == 0xFF || version == 0x00) {
84 return "Vanilla";
85 }
86 return absl::StrFormat("v%d", version);
87}
88
89void FindDiffRegions(const std::vector<uint8_t>& target,
90 const std::vector<uint8_t>& baseline,
91 RomCompareResult& result, bool smart_diff) {
92 size_t min_size = std::min(target.size(), baseline.size());
93
94 for (const auto& region : kCriticalRegions) {
95 if (region.start >= min_size) {
96 continue;
97 }
98
99 uint32_t end = std::min(region.end, static_cast<uint32_t>(min_size));
100 size_t diff_count = 0;
101
102 for (uint32_t i = region.start; i < end; ++i) {
103 // Smart diff: Ignore checksum bytes
104 if (smart_diff) {
107 continue;
108 // Ignore ZSCustom timestamp/version if needed (optional)
109 }
110
111 if (target[i] != baseline[i]) {
112 diff_count++;
113 }
114 }
115
116 if (diff_count > 0) {
118 diff.start = region.start;
119 diff.end = end;
120 diff.diff_count = diff_count;
121 diff.region_name = region.name;
122 diff.critical = region.critical;
123 result.diff_regions.push_back(diff);
124 result.total_diff_bytes += diff_count;
125 }
126 }
127}
128
129// =============================================================================
130// Output Helpers
131// =============================================================================
132
134 const std::string& prefix,
135 const RomCompareResult::RomInfo& info) {
136 formatter.AddField(prefix + "_filename", info.filename);
137 formatter.AddField(prefix + "_size", static_cast<int>(info.size));
138 formatter.AddField(prefix + "_version", GetVersionString(info.zs_version));
139 formatter.AddField(prefix + "_expanded_tile16", info.has_expanded_tile16);
140 formatter.AddField(prefix + "_expanded_tile32", info.has_expanded_tile32);
141 formatter.AddHexField(prefix + "_checksum", info.checksum, 8);
142}
143
144void OutputTextBanner(bool is_json) {
145 if (is_json) {
146 return;
147 }
148 std::cout << "\n";
149 std::cout
150 << "╔═══════════════════════════════════════════════════════════════╗\n";
151 std::cout
152 << "║ ROM COMPARE ║\n";
153 std::cout
154 << "║ Baseline Comparison Tool ║\n";
155 std::cout
156 << "╚═══════════════════════════════════════════════════════════════╝\n";
157}
158
160 std::cout << "\n=== ROM Information ===\n";
161 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "", "Baseline", "Target");
162 std::cout << std::string(80, '-') << "\n";
163 std::cout << absl::StrFormat("%-20s %-30zu %-30zu\n", "Size (bytes)",
164 result.baseline.size, result.target.size);
165 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "ZSCustom Version",
168 std::cout << absl::StrFormat(
169 "%-20s %-30s %-30s\n", "Expanded Tile16",
170 result.baseline.has_expanded_tile16 ? "YES" : "NO",
171 result.target.has_expanded_tile16 ? "YES" : "NO");
172 std::cout << absl::StrFormat(
173 "%-20s %-30s %-30s\n", "Expanded Tile32",
174 result.baseline.has_expanded_tile32 ? "YES" : "NO",
175 result.target.has_expanded_tile32 ? "YES" : "NO");
176 std::cout << absl::StrFormat("%-20s 0x%08X%21s 0x%08X\n", "Checksum",
177 result.baseline.checksum, "",
178 result.target.checksum);
179}
180
182 std::cout << "\n=== Difference Summary ===\n";
183
184 if (result.diff_regions.empty()) {
185 std::cout << "No differences found in critical regions.\n";
186 return;
187 }
188
189 std::cout << absl::StrFormat(
190 "Found differences in %zu regions (%zu bytes total):\n",
191 result.diff_regions.size(), result.total_diff_bytes);
192
193 for (const auto& diff : result.diff_regions) {
194 std::string marker = diff.critical ? "[CRITICAL] " : "";
195 std::cout << absl::StrFormat(" %s%-25s 0x%06X-0x%06X %6zu bytes differ\n",
196 marker, diff.region_name, diff.start, diff.end,
197 diff.diff_count);
198 }
199}
200
201void OutputTextDetailedDiff(const std::vector<uint8_t>& target,
202 const std::vector<uint8_t>& baseline,
203 const RomCompareResult::DiffRegion& region,
204 int max_samples) {
205 std::cout << absl::StrFormat("\n Differences in %s (0x%06X-0x%06X):\n",
206 region.region_name, region.start, region.end);
207
208 int samples_shown = 0;
209 for (uint32_t i = region.start; i < region.end && samples_shown < max_samples;
210 ++i) {
211 if (target[i] != baseline[i]) {
212 std::cout << absl::StrFormat(
213 " 0x%06X: baseline=0x%02X target=0x%02X\n", i, baseline[i],
214 target[i]);
215 samples_shown++;
216 }
217 }
218
219 if (region.diff_count > static_cast<size_t>(max_samples)) {
220 std::cout << absl::StrFormat(" ... and %zu more differences\n",
221 region.diff_count - max_samples);
222 }
223}
224
226 std::cout << "\n";
227 std::cout
228 << "╔═══════════════════════════════════════════════════════════════╗\n";
229 std::cout
230 << "║ ASSESSMENT ║\n";
231 std::cout
232 << "╠═══════════════════════════════════════════════════════════════╣\n";
233
234 bool has_issues = false;
235
236 if (!result.sizes_match) {
237 std::cout
238 << "║ SIZE MISMATCH: ROMs have different sizes ║\n";
239 has_issues = true;
240 }
241
242 for (const auto& diff : result.diff_regions) {
243 if (diff.critical) {
244 std::cout << absl::StrFormat(
245 "║ WARNING: %s modified (%zu bytes)%-14s ║\n", diff.region_name,
246 diff.diff_count, "");
247 has_issues = true;
248 }
249 }
250
251 if (!has_issues && result.diff_regions.empty()) {
252 std::cout
253 << "║ ROMs are identical in all critical regions ║\n";
254 } else if (!has_issues) {
255 std::cout
256 << "║ ROMs have expected differences (version upgrade, etc.) ║\n";
257 }
258
259 std::cout
260 << "╚═══════════════════════════════════════════════════════════════╝\n";
261}
262
263} // namespace
264
266 Rom* rom, const resources::ArgumentParser& parser,
267 resources::OutputFormatter& formatter) {
268 auto baseline_path = parser.GetString("baseline");
269 bool verbose = parser.HasFlag("verbose");
270 bool show_diff = parser.HasFlag("show-diff");
271 bool smart_diff = parser.HasFlag("smart");
272 bool is_json = formatter.IsJson();
273
274 if (!baseline_path.has_value()) {
275 return absl::InvalidArgumentError(
276 "Missing required --baseline <path> argument.\n"
277 "Usage: rom-compare --rom <target> --baseline <baseline.sfc>");
278 }
279
280 OutputTextBanner(is_json);
281
282 // Load baseline ROM
283 if (!is_json) {
284 std::cout << "Loading baseline ROM: " << baseline_path.value() << "\n";
285 }
286
287 std::ifstream baseline_file(baseline_path.value(), std::ios::binary);
288 if (!baseline_file) {
289 return absl::NotFoundError(
290 absl::StrFormat("Cannot open baseline ROM: %s", baseline_path.value()));
291 }
292
293 std::vector<uint8_t> baseline_data(
294 (std::istreambuf_iterator<char>(baseline_file)),
295 std::istreambuf_iterator<char>());
296 baseline_file.close();
297
298 // Get target ROM data
299 const std::vector<uint8_t>& target_data = rom->vector();
300
301 // Analyze both ROMs
302 RomCompareResult result;
303 result.baseline = AnalyzeRom(baseline_data, baseline_path.value());
304 result.target = AnalyzeRom(target_data, rom->filename());
305
306 result.sizes_match = (result.target.size == result.baseline.size);
307 result.versions_match =
308 (result.target.zs_version == result.baseline.zs_version);
309 result.features_match = (result.target.has_expanded_tile16 ==
311 (result.target.has_expanded_tile32 ==
313
314 // Find differences
315 FindDiffRegions(target_data, baseline_data, result, smart_diff);
316
317 // JSON output
318 OutputRomInfoJson(formatter, "baseline", result.baseline);
319 OutputRomInfoJson(formatter, "target", result.target);
320 formatter.AddField("sizes_match", result.sizes_match);
321 formatter.AddField("versions_match", result.versions_match);
322 formatter.AddField("features_match", result.features_match);
323 formatter.AddField("total_diff_bytes",
324 static_cast<int>(result.total_diff_bytes));
325
326 formatter.BeginArray("diff_regions");
327 for (const auto& diff : result.diff_regions) {
328 std::string json = absl::StrFormat(
329 R"({"name":"%s","start":"0x%06X","end":"0x%06X","diff_count":%zu,"critical":%s})",
330 diff.region_name, diff.start, diff.end, diff.diff_count,
331 diff.critical ? "true" : "false");
332 formatter.AddArrayItem(json);
333 }
334 formatter.EndArray();
335
336 // Check for critical issues
337 bool has_critical = false;
338 for (const auto& diff : result.diff_regions) {
339 if (diff.critical) {
340 has_critical = true;
341 break;
342 }
343 }
344 formatter.AddField("has_critical_differences", has_critical);
345 formatter.AddField("assessment", has_critical
346 ? "warning"
347 : (result.diff_regions.empty()
348 ? "identical"
349 : "expected_differences"));
350
351 // Text output
352 if (!is_json) {
353 OutputTextRomInfo(result);
354 OutputTextDiffSummary(result);
355
356 if (show_diff && !result.diff_regions.empty()) {
357 std::cout << "\n=== Detailed Differences ===\n";
358 for (const auto& diff : result.diff_regions) {
359 OutputTextDetailedDiff(target_data, baseline_data, diff,
360 verbose ? 10 : 5);
361 }
362 }
363
364 OutputTextAssessment(result);
365 }
366
367 return absl::OkStatus();
368}
369
370} // 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:24
auto filename() const
Definition rom.h:141
const auto & vector() const
Definition rom.h:139
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.
void FindDiffRegions(const std::vector< uint8_t > &target, const std::vector< uint8_t > &baseline, RomCompareResult &result, bool smart_diff)
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 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