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 = absl::StrFormat("%zu bytes changed", old_bytes.size());
138 }
139
140 summary.diffs.push_back(diff);
141 summary.changes_by_category[diff.category]++;
142 in_diff = false;
143 }
144 }
145
146 // Handle final diff region if we ended while in a diff
147 if (in_diff) {
148 RomDiff diff;
149 diff.address = diff_start;
150 diff.length = old_bytes.size();
151 diff.old_value = old_bytes;
152 diff.new_value = new_bytes;
153 diff.category = semantic ? CategorizeAddress(diff_start) : "data";
154 diff.description = DescribeChange(diff_start, old_bytes, new_bytes);
155 summary.diffs.push_back(diff);
156 summary.changes_by_category[diff.category]++;
157 }
158
159 // Handle size difference
160 if (rom1.size() != rom2.size()) {
161 summary.changes_by_category["size"] = 1;
162 if (rom2.size() > rom1.size()) {
163 summary.total_bytes_changed += (rom2.size() - rom1.size());
164 }
165 }
166
167 summary.num_regions = summary.diffs.size();
168 return summary;
169}
170
171std::string RomDiffTool::CategorizeAddress(uint32_t address) const {
172 for (const auto& region : kKnownRegions) {
173 if (address >= region.start && address <= region.end) {
174 return region.category;
175 }
176 }
177 return "unknown";
178}
179
180std::string RomDiffTool::DescribeChange(uint32_t address,
181 const std::vector<uint8_t>& old_val,
182 const std::vector<uint8_t>& new_val) const {
183 std::string category = CategorizeAddress(address);
184
185 if (category == "graphics") {
186 return absl::StrFormat("Graphics data changed (%zu bytes)", old_val.size());
187 } else if (category == "palettes") {
188 return absl::StrFormat("Palette data changed (%zu bytes)", old_val.size());
189 } else if (category == "code") {
190 return absl::StrFormat("Code modified (%zu bytes)", old_val.size());
191 } else if (category == "sprites") {
192 return absl::StrFormat("Sprite data changed (%zu bytes)", old_val.size());
193 } else if (category == "messages") {
194 return absl::StrFormat("Message/text data changed (%zu bytes)", old_val.size());
195 } else if (category == "dungeon") {
196 return absl::StrFormat("Dungeon data changed (%zu bytes)", old_val.size());
197 } else if (category == "overworld") {
198 return absl::StrFormat("Overworld data changed (%zu bytes)", old_val.size());
199 }
200
201 return absl::StrFormat("%zu bytes changed", old_val.size());
202}
203
204std::string RomDiffTool::FormatAsJson(const DiffSummary& summary) const {
205 std::ostringstream json;
206
207 json << "{\n";
208 json << " \"total_bytes_changed\": " << summary.total_bytes_changed << ",\n";
209 json << " \"num_regions\": " << summary.num_regions << ",\n";
210
211 // Changes by category
212 json << " \"changes_by_category\": {";
213 bool first = true;
214 for (const auto& [cat, count] : summary.changes_by_category) {
215 if (!first) json << ", ";
216 json << "\"" << cat << "\": " << count;
217 first = false;
218 }
219 json << "},\n";
220
221 // Individual diffs (limit to first 50 for JSON output)
222 json << " \"diffs\": [\n";
223 size_t max_diffs = std::min(size_t(50), summary.diffs.size());
224 for (size_t i = 0; i < max_diffs; ++i) {
225 const auto& diff = summary.diffs[i];
226 json << " {";
227 json << "\"address\": \"" << absl::StrFormat("0x%06X", diff.address) << "\", ";
228 json << "\"length\": " << diff.length << ", ";
229 json << "\"category\": \"" << diff.category << "\", ";
230 json << "\"description\": \"" << diff.description << "\"";
231 json << "}";
232 if (i < max_diffs - 1) json << ",";
233 json << "\n";
234 }
235
236 if (summary.diffs.size() > 50) {
237 json << " // ... and " << (summary.diffs.size() - 50) << " more\n";
238 }
239
240 json << " ]\n";
241 json << "}\n";
242
243 return json.str();
244}
245
246std::string RomDiffTool::FormatAsText(const DiffSummary& summary) const {
247 std::ostringstream text;
248
249 text << "ROM Comparison Summary\n";
250 text << "======================\n\n";
251 text << "Total bytes changed: " << summary.total_bytes_changed << "\n";
252 text << "Number of diff regions: " << summary.num_regions << "\n\n";
253
254 text << "Changes by category:\n";
255 for (const auto& [cat, count] : summary.changes_by_category) {
256 text << " " << cat << ": " << count << " regions\n";
257 }
258 text << "\n";
259
260 text << "Diff regions (first 20):\n";
261 size_t max_diffs = std::min(size_t(20), summary.diffs.size());
262 for (size_t i = 0; i < max_diffs; ++i) {
263 const auto& diff = summary.diffs[i];
264 text << absl::StrFormat(" %06X: %s (%zu bytes)\n",
265 diff.address, diff.description, diff.length);
266 }
267
268 if (summary.diffs.size() > 20) {
269 text << " ... and " << (summary.diffs.size() - 20) << " more\n";
270 }
271
272 return text.str();
273}
274
275// =============================================================================
276// RomChangesTool
277// =============================================================================
278
279absl::Status RomChangesTool::Execute(Rom* /*rom*/,
280 const resources::ArgumentParser& parser,
281 resources::OutputFormatter& formatter) {
282 std::string rom1_path = parser.GetString("rom1").value();
283 std::string rom2_path = parser.GetString("rom2").value();
284 std::string format = parser.GetString("format").value_or("json");
285
286 // Load both ROMs
287 Rom rom1, rom2;
288 auto status1 = rom1.LoadFromFile(rom1_path);
289 if (!status1.ok()) {
290 return absl::InvalidArgumentError("Failed to load ROM 1: " + rom1_path);
291 }
292
293 auto status2 = rom2.LoadFromFile(rom2_path);
294 if (!status2.ok()) {
295 return absl::InvalidArgumentError("Failed to load ROM 2: " + rom2_path);
296 }
297
298 std::ostringstream output;
299
300 if (format == "json") {
301 output << "{\n";
302 output << " \"rom1\": {\"path\": \"" << rom1_path << "\", "
303 << "\"size\": " << rom1.size() << ", "
304 << "\"title\": \"" << rom1.title() << "\"},\n";
305 output << " \"rom2\": {\"path\": \"" << rom2_path << "\", "
306 << "\"size\": " << rom2.size() << ", "
307 << "\"title\": \"" << rom2.title() << "\"},\n";
308
309 // Size comparison
310 if (rom1.size() != rom2.size()) {
311 output << " \"size_change\": "
312 << static_cast<int64_t>(rom2.size()) - static_cast<int64_t>(rom1.size())
313 << ",\n";
314 }
315
316 // Title comparison
317 output << " \"title_changed\": "
318 << (rom1.title() != rom2.title() ? "true" : "false") << ",\n";
319
320 // Basic change detection
321 int total_diffs = 0;
322 size_t min_size = std::min(rom1.size(), rom2.size());
323 for (size_t i = 0; i < min_size; ++i) {
324 if (rom1[i] != rom2[i]) total_diffs++;
325 }
326
327 output << " \"bytes_different\": " << total_diffs << ",\n";
328 output << " \"percent_changed\": "
329 << absl::StrFormat("%.2f", (total_diffs * 100.0) / min_size) << "\n";
330 output << "}\n";
331 } else {
332 output << "ROM Comparison: Changes Analysis\n";
333 output << "================================\n\n";
334 output << "ROM 1: " << rom1_path << " (" << rom1.size() << " bytes)\n";
335 output << "ROM 2: " << rom2_path << " (" << rom2.size() << " bytes)\n\n";
336
337 if (rom1.size() != rom2.size()) {
338 output << "Size change: "
339 << (static_cast<int64_t>(rom2.size()) - static_cast<int64_t>(rom1.size()))
340 << " bytes\n";
341 }
342
343 if (rom1.title() != rom2.title()) {
344 output << "Title changed: '" << rom1.title() << "' -> '" << rom2.title() << "'\n";
345 }
346
347 int total_diffs = 0;
348 size_t min_size = std::min(rom1.size(), rom2.size());
349 for (size_t i = 0; i < min_size; ++i) {
350 if (rom1[i] != rom2[i]) total_diffs++;
351 }
352
353 output << "Bytes different: " << total_diffs << " ("
354 << absl::StrFormat("%.2f%%", (total_diffs * 100.0) / min_size) << ")\n";
355 }
356
357 std::cout << output.str();
358 formatter.AddField("status", "complete");
359 return absl::OkStatus();
360}
361
362} // namespace tools
363} // namespace agent
364} // namespace cli
365} // namespace yaze
366
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:74
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