yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom_diff_tool.cc
Go to the documentation of this file.
1
7
8#include <fstream>
9#include <iomanip>
10#include <map>
11#include <sstream>
12#include <vector>
13
14#include "absl/strings/str_format.h"
15#include "rom/rom.h"
16
17namespace yaze {
18namespace cli {
19namespace agent {
20namespace tools {
21
22// Known memory regions for semantic categorization
24 uint32_t start;
25 uint32_t end;
26 const char* category;
27 const char* description;
28};
29
30static const std::vector<MemoryRegion> kKnownRegions = {
31 // Graphics
32 {0x000000, 0x00FFFF, "graphics", "Compressed graphics"},
33 {0x080000, 0x08FFFF, "graphics", "More graphics data"},
34
35 // Overworld
36 {0x020000, 0x027FFF, "overworld", "Overworld tilemap data"},
37 {0x0A8000, 0x0AFFFF, "overworld", "Overworld map data"},
38
39 // Dungeon
40 {0x028000, 0x02FFFF, "dungeon", "Dungeon room data"},
41 {0x0B0000, 0x0B7FFF, "dungeon", "Dungeon tileset data"},
42
43 // Sprites
44 {0x09C800, 0x09DFFF, "sprites", "Sprite graphics"},
45 {0x0D8000, 0x0DFFFF, "sprites", "More sprite data"},
46
47 // Palettes
48 {0x0DD218, 0x0DD4FF, "palettes", "Main palettes"},
49 {0x0DE000, 0x0DEFFF, "palettes", "Sprite palettes"},
50
51 // Text/Messages
52 {0x0E0000, 0x0E7FFF, "messages", "Text and messages"},
53
54 // Code
55 {0x008000, 0x00FFFF, "code", "Bank $00 code"},
56 {0x018000, 0x01FFFF, "code", "Bank $03 code"},
57
58 // Audio
59 {0x0C0000, 0x0CFFFF, "audio", "Music/SPC data"},
60};
61
62// =============================================================================
63// RomDiffTool
64// =============================================================================
65
66absl::Status RomDiffTool::Execute(Rom* /*rom*/,
67 const resources::ArgumentParser& parser,
68 resources::OutputFormatter& formatter) {
69 std::string rom1_path = parser.GetString("rom1").value();
70 std::string rom2_path = parser.GetString("rom2").value();
71 bool semantic = parser.HasFlag("semantic");
72 std::string format = parser.GetString("format").value_or("json");
73
74 // Load both ROMs
75 Rom rom1, rom2;
76 auto status1 = rom1.LoadFromFile(rom1_path);
77 if (!status1.ok()) {
78 return absl::InvalidArgumentError("Failed to load ROM 1: " + rom1_path);
79 }
80
81 auto status2 = rom2.LoadFromFile(rom2_path);
82 if (!status2.ok()) {
83 return absl::InvalidArgumentError("Failed to load ROM 2: " + rom2_path);
84 }
85
86 // Compute differences
87 auto summary = ComputeDiff(rom1.vector(), rom2.vector(), semantic);
88
89 // Output
90 if (format == "json") {
91 std::cout << FormatAsJson(summary);
92 } else {
93 std::cout << FormatAsText(summary);
94 }
95
96 formatter.AddField("status", "complete");
97 return absl::OkStatus();
98}
99
100DiffSummary RomDiffTool::ComputeDiff(const std::vector<uint8_t>& rom1,
101 const std::vector<uint8_t>& rom2,
102 bool semantic) {
103 DiffSummary summary;
104
105 size_t min_size = std::min(rom1.size(), rom2.size());
106 size_t max_size = std::max(rom1.size(), rom2.size());
107
108 // Find contiguous regions of differences
109 bool in_diff = false;
110 uint32_t diff_start = 0;
111 std::vector<uint8_t> old_bytes, new_bytes;
112
113 for (size_t i = 0; i < min_size; ++i) {
114 if (rom1[i] != rom2[i]) {
115 if (!in_diff) {
116 in_diff = true;
117 diff_start = static_cast<uint32_t>(i);
118 old_bytes.clear();
119 new_bytes.clear();
120 }
121 old_bytes.push_back(rom1[i]);
122 new_bytes.push_back(rom2[i]);
123 summary.total_bytes_changed++;
124 } else if (in_diff) {
125 // End of diff region
126 RomDiff diff;
127 diff.address = diff_start;
128 diff.length = old_bytes.size();
129 diff.old_value = old_bytes;
130 diff.new_value = new_bytes;
131
132 if (semantic) {
133 diff.category = CategorizeAddress(diff_start);
134 diff.description = DescribeChange(diff_start, old_bytes, new_bytes);
135 } else {
136 diff.category = "data";
137 diff.description =
138 absl::StrFormat("%zu bytes changed", old_bytes.size());
139 }
140
141 summary.diffs.push_back(diff);
142 summary.changes_by_category[diff.category]++;
143 in_diff = false;
144 }
145 }
146
147 // Handle final diff region if we ended while in a diff
148 if (in_diff) {
149 RomDiff diff;
150 diff.address = diff_start;
151 diff.length = old_bytes.size();
152 diff.old_value = old_bytes;
153 diff.new_value = new_bytes;
154 diff.category = semantic ? CategorizeAddress(diff_start) : "data";
155 diff.description = DescribeChange(diff_start, old_bytes, new_bytes);
156 summary.diffs.push_back(diff);
157 summary.changes_by_category[diff.category]++;
158 }
159
160 // Handle size difference
161 if (rom1.size() != rom2.size()) {
162 summary.changes_by_category["size"] = 1;
163 if (rom2.size() > rom1.size()) {
164 summary.total_bytes_changed += (rom2.size() - rom1.size());
165 }
166 }
167
168 summary.num_regions = summary.diffs.size();
169 return summary;
170}
171
172std::string RomDiffTool::CategorizeAddress(uint32_t address) const {
173 for (const auto& region : kKnownRegions) {
174 if (address >= region.start && address <= region.end) {
175 return region.category;
176 }
177 }
178 return "unknown";
179}
180
182 uint32_t address, const std::vector<uint8_t>& old_val,
183 const std::vector<uint8_t>& new_val) const {
184 std::string category = CategorizeAddress(address);
185
186 if (category == "graphics") {
187 return absl::StrFormat("Graphics data changed (%zu bytes)", old_val.size());
188 } else if (category == "palettes") {
189 return absl::StrFormat("Palette data changed (%zu bytes)", old_val.size());
190 } else if (category == "code") {
191 return absl::StrFormat("Code modified (%zu bytes)", old_val.size());
192 } else if (category == "sprites") {
193 return absl::StrFormat("Sprite data changed (%zu bytes)", old_val.size());
194 } else if (category == "messages") {
195 return absl::StrFormat("Message/text data changed (%zu bytes)",
196 old_val.size());
197 } else if (category == "dungeon") {
198 return absl::StrFormat("Dungeon data changed (%zu bytes)", old_val.size());
199 } else if (category == "overworld") {
200 return absl::StrFormat("Overworld data changed (%zu bytes)",
201 old_val.size());
202 }
203
204 return absl::StrFormat("%zu bytes changed", old_val.size());
205}
206
207std::string RomDiffTool::FormatAsJson(const DiffSummary& summary) const {
208 std::ostringstream json;
209
210 json << "{\n";
211 json << " \"total_bytes_changed\": " << summary.total_bytes_changed << ",\n";
212 json << " \"num_regions\": " << summary.num_regions << ",\n";
213
214 // Changes by category
215 json << " \"changes_by_category\": {";
216 bool first = true;
217 for (const auto& [cat, count] : summary.changes_by_category) {
218 if (!first)
219 json << ", ";
220 json << "\"" << cat << "\": " << count;
221 first = false;
222 }
223 json << "},\n";
224
225 // Individual diffs (limit to first 50 for JSON output)
226 json << " \"diffs\": [\n";
227 size_t max_diffs = std::min(size_t(50), summary.diffs.size());
228 for (size_t i = 0; i < max_diffs; ++i) {
229 const auto& diff = summary.diffs[i];
230 json << " {";
231 json << "\"address\": \"" << absl::StrFormat("0x%06X", diff.address)
232 << "\", ";
233 json << "\"length\": " << diff.length << ", ";
234 json << "\"category\": \"" << diff.category << "\", ";
235 json << "\"description\": \"" << diff.description << "\"";
236 json << "}";
237 if (i < max_diffs - 1)
238 json << ",";
239 json << "\n";
240 }
241
242 if (summary.diffs.size() > 50) {
243 json << " // ... and " << (summary.diffs.size() - 50) << " more\n";
244 }
245
246 json << " ]\n";
247 json << "}\n";
248
249 return json.str();
250}
251
252std::string RomDiffTool::FormatAsText(const DiffSummary& summary) const {
253 std::ostringstream text;
254
255 text << "ROM Comparison Summary\n";
256 text << "======================\n\n";
257 text << "Total bytes changed: " << summary.total_bytes_changed << "\n";
258 text << "Number of diff regions: " << summary.num_regions << "\n\n";
259
260 text << "Changes by category:\n";
261 for (const auto& [cat, count] : summary.changes_by_category) {
262 text << " " << cat << ": " << count << " regions\n";
263 }
264 text << "\n";
265
266 text << "Diff regions (first 20):\n";
267 size_t max_diffs = std::min(size_t(20), summary.diffs.size());
268 for (size_t i = 0; i < max_diffs; ++i) {
269 const auto& diff = summary.diffs[i];
270 text << absl::StrFormat(" %06X: %s (%zu bytes)\n", diff.address,
271 diff.description, diff.length);
272 }
273
274 if (summary.diffs.size() > 20) {
275 text << " ... and " << (summary.diffs.size() - 20) << " more\n";
276 }
277
278 return text.str();
279}
280
281// =============================================================================
282// RomChangesTool
283// =============================================================================
284
285absl::Status RomChangesTool::Execute(Rom* /*rom*/,
286 const resources::ArgumentParser& parser,
287 resources::OutputFormatter& formatter) {
288 std::string rom1_path = parser.GetString("rom1").value();
289 std::string rom2_path = parser.GetString("rom2").value();
290 std::string format = parser.GetString("format").value_or("json");
291
292 // Load both ROMs
293 Rom rom1, rom2;
294 auto status1 = rom1.LoadFromFile(rom1_path);
295 if (!status1.ok()) {
296 return absl::InvalidArgumentError("Failed to load ROM 1: " + rom1_path);
297 }
298
299 auto status2 = rom2.LoadFromFile(rom2_path);
300 if (!status2.ok()) {
301 return absl::InvalidArgumentError("Failed to load ROM 2: " + rom2_path);
302 }
303
304 std::ostringstream output;
305
306 if (format == "json") {
307 output << "{\n";
308 output << " \"rom1\": {\"path\": \"" << rom1_path << "\", "
309 << "\"size\": " << rom1.size() << ", "
310 << "\"title\": \"" << rom1.title() << "\"},\n";
311 output << " \"rom2\": {\"path\": \"" << rom2_path << "\", "
312 << "\"size\": " << rom2.size() << ", "
313 << "\"title\": \"" << rom2.title() << "\"},\n";
314
315 // Size comparison
316 if (rom1.size() != rom2.size()) {
317 output << " \"size_change\": "
318 << static_cast<int64_t>(rom2.size()) -
319 static_cast<int64_t>(rom1.size())
320 << ",\n";
321 }
322
323 // Title comparison
324 output << " \"title_changed\": "
325 << (rom1.title() != rom2.title() ? "true" : "false") << ",\n";
326
327 // Basic change detection
328 int total_diffs = 0;
329 size_t min_size = std::min(rom1.size(), rom2.size());
330 for (size_t i = 0; i < min_size; ++i) {
331 if (rom1[i] != rom2[i])
332 total_diffs++;
333 }
334
335 output << " \"bytes_different\": " << total_diffs << ",\n";
336 output << " \"percent_changed\": "
337 << absl::StrFormat("%.2f", (total_diffs * 100.0) / min_size) << "\n";
338 output << "}\n";
339 } else {
340 output << "ROM Comparison: Changes Analysis\n";
341 output << "================================\n\n";
342 output << "ROM 1: " << rom1_path << " (" << rom1.size() << " bytes)\n";
343 output << "ROM 2: " << rom2_path << " (" << rom2.size() << " bytes)\n\n";
344
345 if (rom1.size() != rom2.size()) {
346 output << "Size change: "
347 << (static_cast<int64_t>(rom2.size()) -
348 static_cast<int64_t>(rom1.size()))
349 << " bytes\n";
350 }
351
352 if (rom1.title() != rom2.title()) {
353 output << "Title changed: '" << rom1.title() << "' -> '" << rom2.title()
354 << "'\n";
355 }
356
357 int total_diffs = 0;
358 size_t min_size = std::min(rom1.size(), rom2.size());
359 for (size_t i = 0; i < min_size; ++i) {
360 if (rom1[i] != rom2[i])
361 total_diffs++;
362 }
363
364 output << "Bytes different: " << total_diffs << " ("
365 << absl::StrFormat("%.2f%%", (total_diffs * 100.0) / min_size)
366 << ")\n";
367 }
368
369 std::cout << output.str();
370 formatter.AddField("status", "complete");
371 return absl::OkStatus();
372}
373
374} // namespace tools
375} // namespace agent
376} // namespace cli
377} // namespace yaze
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
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:75
const auto & vector() const
Definition rom.h:139
auto size() const
Definition rom.h:134
auto title() const
Definition rom.h:133
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::string DescribeChange(uint32_t address, const std::vector< uint8_t > &old_val, const std::vector< uint8_t > &new_val) const
DiffSummary ComputeDiff(const std::vector< uint8_t > &rom1, const std::vector< uint8_t > &rom2, bool semantic)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::string FormatAsJson(const DiffSummary &summary) const
std::string CategorizeAddress(uint32_t address) const
std::string FormatAsText(const DiffSummary &summary) const
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 AddField(const std::string &key, const std::string &value)
Add a key-value pair.
ROM comparison and diff analysis tools for AI agents.
Summary of differences between two ROMs.
std::map< std::string, int > changes_by_category
A single difference between two ROMs.
std::vector< uint8_t > new_value
std::vector< uint8_t > old_value