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"
11#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,
92 bool smart_diff) {
93 size_t min_size = std::min(target.size(), baseline.size());
94
95 for (const auto& region : kCriticalRegions) {
96 if (region.start >= min_size) {
97 continue;
98 }
99
100 uint32_t end = std::min(region.end, static_cast<uint32_t>(min_size));
101 size_t diff_count = 0;
102
103 for (uint32_t i = region.start; i < end; ++i) {
104 // Smart diff: Ignore checksum bytes
105 if (smart_diff) {
106 if (i >= yaze::cli::kChecksumComplementPos && i <= yaze::cli::kChecksumPos + 1) continue;
107 // Ignore ZSCustom timestamp/version if needed (optional)
108 }
109
110 if (target[i] != baseline[i]) {
111 diff_count++;
112 }
113 }
114
115 if (diff_count > 0) {
117 diff.start = region.start;
118 diff.end = end;
119 diff.diff_count = diff_count;
120 diff.region_name = region.name;
121 diff.critical = region.critical;
122 result.diff_regions.push_back(diff);
123 result.total_diff_bytes += diff_count;
124 }
125 }
126}
127
128// =============================================================================
129// Output Helpers
130// =============================================================================
131
133 const std::string& prefix,
134 const RomCompareResult::RomInfo& info) {
135 formatter.AddField(prefix + "_filename", info.filename);
136 formatter.AddField(prefix + "_size", static_cast<int>(info.size));
137 formatter.AddField(prefix + "_version", GetVersionString(info.zs_version));
138 formatter.AddField(prefix + "_expanded_tile16", info.has_expanded_tile16);
139 formatter.AddField(prefix + "_expanded_tile32", info.has_expanded_tile32);
140 formatter.AddHexField(prefix + "_checksum", info.checksum, 8);
141}
142
143void OutputTextBanner(bool is_json) {
144 if (is_json) {
145 return;
146 }
147 std::cout << "\n";
148 std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
149 std::cout << "║ ROM COMPARE ║\n";
150 std::cout << "║ Baseline Comparison Tool ║\n";
151 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
152}
153
155 std::cout << "\n=== ROM Information ===\n";
156 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "", "Baseline", "Target");
157 std::cout << std::string(80, '-') << "\n";
158 std::cout << absl::StrFormat("%-20s %-30zu %-30zu\n", "Size (bytes)",
159 result.baseline.size, result.target.size);
160 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "ZSCustom Version",
163 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "Expanded Tile16",
164 result.baseline.has_expanded_tile16 ? "YES" : "NO",
165 result.target.has_expanded_tile16 ? "YES" : "NO");
166 std::cout << absl::StrFormat("%-20s %-30s %-30s\n", "Expanded Tile32",
167 result.baseline.has_expanded_tile32 ? "YES" : "NO",
168 result.target.has_expanded_tile32 ? "YES" : "NO");
169 std::cout << absl::StrFormat("%-20s 0x%08X%21s 0x%08X\n", "Checksum",
170 result.baseline.checksum, "",
171 result.target.checksum);
172}
173
175 std::cout << "\n=== Difference Summary ===\n";
176
177 if (result.diff_regions.empty()) {
178 std::cout << "No differences found in critical regions.\n";
179 return;
180 }
181
182 std::cout << absl::StrFormat("Found differences in %zu regions (%zu bytes total):\n",
183 result.diff_regions.size(), result.total_diff_bytes);
184
185 for (const auto& diff : result.diff_regions) {
186 std::string marker = diff.critical ? "[CRITICAL] " : "";
187 std::cout << absl::StrFormat(" %s%-25s 0x%06X-0x%06X %6zu bytes differ\n",
188 marker, diff.region_name, diff.start, diff.end,
189 diff.diff_count);
190 }
191}
192
193void OutputTextDetailedDiff(const std::vector<uint8_t>& target,
194 const std::vector<uint8_t>& baseline,
195 const RomCompareResult::DiffRegion& region,
196 int max_samples) {
197 std::cout << absl::StrFormat("\n Differences in %s (0x%06X-0x%06X):\n",
198 region.region_name, region.start, region.end);
199
200 int samples_shown = 0;
201 for (uint32_t i = region.start;
202 i < region.end && samples_shown < max_samples; ++i) {
203 if (target[i] != baseline[i]) {
204 std::cout << absl::StrFormat(" 0x%06X: baseline=0x%02X target=0x%02X\n",
205 i, baseline[i], target[i]);
206 samples_shown++;
207 }
208 }
209
210 if (region.diff_count > static_cast<size_t>(max_samples)) {
211 std::cout << absl::StrFormat(" ... and %zu more differences\n",
212 region.diff_count - max_samples);
213 }
214}
215
217 std::cout << "\n";
218 std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
219 std::cout << "║ ASSESSMENT ║\n";
220 std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
221
222 bool has_issues = false;
223
224 if (!result.sizes_match) {
225 std::cout << "║ SIZE MISMATCH: ROMs have different sizes ║\n";
226 has_issues = true;
227 }
228
229 for (const auto& diff : result.diff_regions) {
230 if (diff.critical) {
231 std::cout << absl::StrFormat(
232 "║ WARNING: %s modified (%zu bytes)%-14s ║\n",
233 diff.region_name, diff.diff_count, "");
234 has_issues = true;
235 }
236 }
237
238 if (!has_issues && result.diff_regions.empty()) {
239 std::cout << "║ ROMs are identical in all critical regions ║\n";
240 } else if (!has_issues) {
241 std::cout << "║ ROMs have expected differences (version upgrade, etc.) ║\n";
242 }
243
244 std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
245}
246
247} // namespace
248
250 Rom* rom, const resources::ArgumentParser& parser,
251 resources::OutputFormatter& formatter) {
252 auto baseline_path = parser.GetString("baseline");
253 bool verbose = parser.HasFlag("verbose");
254 bool show_diff = parser.HasFlag("show-diff");
255 bool smart_diff = parser.HasFlag("smart");
256 bool is_json = formatter.IsJson();
257
258 if (!baseline_path.has_value()) {
259 return absl::InvalidArgumentError(
260 "Missing required --baseline <path> argument.\n"
261 "Usage: rom-compare --rom <target> --baseline <baseline.sfc>");
262 }
263
264 OutputTextBanner(is_json);
265
266 // Load baseline ROM
267 if (!is_json) {
268 std::cout << "Loading baseline ROM: " << baseline_path.value() << "\n";
269 }
270
271 std::ifstream baseline_file(baseline_path.value(), std::ios::binary);
272 if (!baseline_file) {
273 return absl::NotFoundError(
274 absl::StrFormat("Cannot open baseline ROM: %s", baseline_path.value()));
275 }
276
277 std::vector<uint8_t> baseline_data(
278 (std::istreambuf_iterator<char>(baseline_file)),
279 std::istreambuf_iterator<char>());
280 baseline_file.close();
281
282 // Get target ROM data
283 const std::vector<uint8_t>& target_data = rom->vector();
284
285 // Analyze both ROMs
286 RomCompareResult result;
287 result.baseline = AnalyzeRom(baseline_data, baseline_path.value());
288 result.target = AnalyzeRom(target_data, rom->filename());
289
290 result.sizes_match = (result.target.size == result.baseline.size);
291 result.versions_match =
292 (result.target.zs_version == result.baseline.zs_version);
293 result.features_match =
296
297 // Find differences
298 FindDiffRegions(target_data, baseline_data, result, smart_diff);
299
300 // JSON output
301 OutputRomInfoJson(formatter, "baseline", result.baseline);
302 OutputRomInfoJson(formatter, "target", result.target);
303 formatter.AddField("sizes_match", result.sizes_match);
304 formatter.AddField("versions_match", result.versions_match);
305 formatter.AddField("features_match", result.features_match);
306 formatter.AddField("total_diff_bytes", static_cast<int>(result.total_diff_bytes));
307
308 formatter.BeginArray("diff_regions");
309 for (const auto& diff : result.diff_regions) {
310 std::string json = absl::StrFormat(
311 R"({"name":"%s","start":"0x%06X","end":"0x%06X","diff_count":%zu,"critical":%s})",
312 diff.region_name, diff.start, diff.end, diff.diff_count,
313 diff.critical ? "true" : "false");
314 formatter.AddArrayItem(json);
315 }
316 formatter.EndArray();
317
318 // Check for critical issues
319 bool has_critical = false;
320 for (const auto& diff : result.diff_regions) {
321 if (diff.critical) {
322 has_critical = true;
323 break;
324 }
325 }
326 formatter.AddField("has_critical_differences", has_critical);
327 formatter.AddField("assessment",
328 has_critical ? "warning" : (result.diff_regions.empty()
329 ? "identical"
330 : "expected_differences"));
331
332 // Text output
333 if (!is_json) {
334 OutputTextRomInfo(result);
335 OutputTextDiffSummary(result);
336
337 if (show_diff && !result.diff_regions.empty()) {
338 std::cout << "\n=== Detailed Differences ===\n";
339 for (const auto& diff : result.diff_regions) {
340 OutputTextDetailedDiff(target_data, baseline_data, diff, verbose ? 10 : 5);
341 }
342 }
343
344 OutputTextAssessment(result);
345 }
346
347 return absl::OkStatus();
348}
349
350} // 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