yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
visual_analysis_tool.cc
Go to the documentation of this file.
1
7
8#include <algorithm>
9#include <cmath>
10#include <map>
11#include <numeric>
12#include <set>
13#include <sstream>
14#include <vector>
15
16#include "absl/status/status.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
21#include "core/project.h"
22#include "nlohmann/json.hpp"
23#include "rom/rom.h"
25
26namespace yaze {
27namespace cli {
28namespace agent {
29namespace tools {
30
31// =============================================================================
32// VisualAnalysisBase Implementation
33// =============================================================================
34
36 const std::vector<uint8_t>& tile_a,
37 const std::vector<uint8_t>& tile_b) const {
38 if (tile_a.size() != tile_b.size() || tile_a.empty()) {
39 return 0.0;
40 }
41
42 // Compute Mean Absolute Error
43 double total_diff = 0.0;
44 for (size_t i = 0; i < tile_a.size(); ++i) {
45 total_diff +=
46 std::abs(static_cast<int>(tile_a[i]) - static_cast<int>(tile_b[i]));
47 }
48
49 // Normalize to 0-100 scale (max diff per pixel is 255)
50 double max_possible_diff = tile_a.size() * 255.0;
51 double normalized_diff = total_diff / max_possible_diff;
52
53 // Convert to similarity (100 = identical, 0 = completely different)
54 return (1.0 - normalized_diff) * 100.0;
55}
56
58 const std::vector<uint8_t>& tile_a,
59 const std::vector<uint8_t>& tile_b) const {
60 if (tile_a.size() != tile_b.size() || tile_a.empty()) {
61 return 0.0;
62 }
63
64 const size_t n = tile_a.size();
65
66 // Compute means
67 double mean_a = 0.0, mean_b = 0.0;
68 for (size_t i = 0; i < n; ++i) {
69 mean_a += tile_a[i];
70 mean_b += tile_b[i];
71 }
72 mean_a /= n;
73 mean_b /= n;
74
75 // Compute variances and covariance
76 double var_a = 0.0, var_b = 0.0, covar = 0.0;
77 for (size_t i = 0; i < n; ++i) {
78 double diff_a = tile_a[i] - mean_a;
79 double diff_b = tile_b[i] - mean_b;
80 var_a += diff_a * diff_a;
81 var_b += diff_b * diff_b;
82 covar += diff_a * diff_b;
83 }
84 var_a /= n;
85 var_b /= n;
86 covar /= n;
87
88 // SSIM-like formula with stability constants
89 const double c1 = 6.5025; // (0.01 * 255)^2
90 const double c2 = 58.5225; // (0.03 * 255)^2
91
92 double numerator = (2.0 * mean_a * mean_b + c1) * (2.0 * covar + c2);
93 double denominator =
94 (mean_a * mean_a + mean_b * mean_b + c1) * (var_a + var_b + c2);
95
96 double ssim = numerator / denominator;
97
98 // Convert to 0-100 scale
99 return std::max(0.0, std::min(100.0, ssim * 100.0));
100}
101
102absl::StatusOr<std::vector<uint8_t>> VisualAnalysisBase::ExtractTile(
103 Rom* rom, int sheet_index, int tile_index) const {
104 if (!rom) {
105 return absl::InvalidArgumentError("ROM is null");
106 }
107
108 if (sheet_index < 0 || sheet_index >= kMaxSheets) {
109 return absl::InvalidArgumentError(
110 absl::StrCat("Invalid sheet index: ", sheet_index));
111 }
112
113 // Calculate tile position in sheet
114 int tiles_per_sheet =
116 if (tile_index < 0 || tile_index >= tiles_per_sheet) {
117 return absl::InvalidArgumentError(
118 absl::StrCat("Invalid tile index: ", tile_index));
119 }
120
121 int x = (tile_index % kTilesPerRow) * kTileWidth;
122 int y = (tile_index / kTilesPerRow) * kTileHeight;
123
124 return ExtractTileAtPosition(rom, sheet_index, x, y);
125}
126
127absl::StatusOr<std::vector<uint8_t>> VisualAnalysisBase::ExtractTileAtPosition(
128 Rom* rom, int sheet_index, int x, int y) const {
129 if (!rom) {
130 return absl::InvalidArgumentError("ROM is null");
131 }
132
133 // Get graphics sheet data from ROM
134 // Graphics sheets are stored in a compressed format in the ROM
135 // For now, we'll work with the decompressed data if available
136
137 // Calculate the base address for the graphics sheet
138 // ALTTP graphics are stored at various locations depending on the sheet
139 // This is a simplified extraction - in practice, this would need to
140 // interface with the existing graphics loading code
141
142 std::vector<uint8_t> tile_data(kTilePixels, 0);
143
144 // For the agent tool, we should interface with existing graphics
145 // decompression. For now, return a placeholder that indicates
146 // we need ROM context with loaded graphics.
147
148 // Access graphics from Arena if available
149 const auto& gfx_sheets = gfx::Arena::Get().gfx_sheets();
150 if (gfx_sheets.empty() || !gfx_sheets[0].is_active()) {
151 return absl::FailedPreconditionError(
152 "Graphics not loaded. Load ROM with graphics first.");
153 }
154
155 // Check sheet index is valid
156 if (sheet_index >= static_cast<int>(gfx_sheets.size())) {
157 return absl::OutOfRangeError(
158 absl::StrCat("Sheet ", sheet_index, " out of range"));
159 }
160
161 const auto& sheet = gfx_sheets[sheet_index];
162 if (!sheet.is_active()) {
163 return absl::FailedPreconditionError(
164 absl::StrCat("Sheet ", sheet_index, " is not loaded"));
165 }
166
167 const auto& sheet_data = sheet.vector();
168
169 // Extract 8x8 tile from the sheet
170 for (int row = 0; row < kTileHeight; ++row) {
171 size_t src_offset = (y + row) * kSheetWidth + x;
172 for (int col = 0; col < kTileWidth; ++col) {
173 if (src_offset + col < sheet_data.size()) {
174 tile_data[row * kTileWidth + col] = sheet_data[src_offset + col];
175 }
176 }
177 }
178
179 return tile_data;
180}
181
182bool VisualAnalysisBase::IsRegionEmpty(const std::vector<uint8_t>& data) const {
183 // A region is free only when every pixel is palette index 0. Missing data
184 // is not evidence of free space, and 0xFF or "mostly zero" pixels are art.
185 return !data.empty() && std::all_of(data.begin(), data.end(),
186 [](uint8_t b) { return b == 0x00; });
187}
188
190 // Standard SNES sheet is 128x32 pixels = 16x4 = 64 tiles
192}
193
195 const std::vector<TileSimilarityMatch>& matches) const {
196 std::ostringstream json;
197 if (matches.empty()) {
198 json << "{\n \"matches\": [],\n";
199 json << " \"total_matches\": 0\n";
200 json << "}\n";
201 return json.str();
202 }
203
204 json << "{\n \"matches\": [\n";
205
206 for (size_t i = 0; i < matches.size(); ++i) {
207 const auto& m = matches[i];
208 json << " {\n";
209 json << " \"tile_id\": " << m.tile_id << ",\n";
210 json << " \"similarity_score\": "
211 << absl::StrFormat("%.2f", m.similarity_score) << ",\n";
212 json << " \"sheet_index\": " << m.sheet_index << ",\n";
213 json << " \"x_position\": " << m.x_position << ",\n";
214 json << " \"y_position\": " << m.y_position << "\n";
215 json << " }";
216 if (i < matches.size() - 1)
217 json << ",";
218 json << "\n";
219 }
220
221 json << " ],\n";
222 json << " \"total_matches\": " << matches.size() << "\n";
223 json << "}\n";
224
225 return json.str();
226}
227
229 const std::vector<UnusedRegion>& regions) const {
230 std::ostringstream json;
231 if (regions.empty()) {
232 json << "{\n \"unused_regions\": [],\n";
233 json << " \"total_regions\": 0,\n";
234 json << " \"total_free_tiles\": 0\n";
235 json << "}\n";
236 return json.str();
237 }
238
239 json << "{\n \"unused_regions\": [\n";
240
241 for (size_t i = 0; i < regions.size(); ++i) {
242 const auto& r = regions[i];
243 json << " {\n";
244 json << " \"sheet_index\": " << r.sheet_index << ",\n";
245 json << " \"x\": " << r.x << ",\n";
246 json << " \"y\": " << r.y << ",\n";
247 json << " \"width\": " << r.width << ",\n";
248 json << " \"height\": " << r.height << ",\n";
249 json << " \"tile_count\": " << r.tile_count << "\n";
250 json << " }";
251 if (i < regions.size() - 1)
252 json << ",";
253 json << "\n";
254 }
255
256 int total_tiles = 0;
257 for (const auto& r : regions) {
258 total_tiles += r.tile_count;
259 }
260
261 json << " ],\n";
262 json << " \"total_regions\": " << regions.size() << ",\n";
263 json << " \"total_free_tiles\": " << total_tiles << "\n";
264 json << "}\n";
265
266 return json.str();
267}
268
270 const std::vector<PaletteUsageStats>& stats) const {
271 std::ostringstream json;
272 json << "{\n \"palette_usage\": [\n";
273
274 for (size_t i = 0; i < stats.size(); ++i) {
275 const auto& s = stats[i];
276 json << " {\n";
277 json << " \"palette_index\": " << s.palette_index << ",\n";
278 json << " \"usage_count\": " << s.usage_count << ",\n";
279 json << " \"usage_percentage\": "
280 << absl::StrFormat("%.2f", s.usage_percentage) << ",\n";
281 json << " \"used_by_maps\": [";
282 for (size_t j = 0; j < s.used_by_maps.size(); ++j) {
283 json << s.used_by_maps[j];
284 if (j < s.used_by_maps.size() - 1)
285 json << ", ";
286 }
287 json << "]\n";
288 json << " }";
289 if (i < stats.size() - 1)
290 json << ",";
291 json << "\n";
292 }
293
294 json << " ]\n";
295 json << "}\n";
296
297 return json.str();
298}
299
301 const std::vector<TileUsageEntry>& entries) const {
302 std::ostringstream json;
303 json << "{\n \"tile_histogram\": [\n";
304
305 for (size_t i = 0; i < entries.size(); ++i) {
306 const auto& e = entries[i];
307 json << " {\n";
308 json << " \"tile_id\": " << e.tile_id << ",\n";
309 json << " \"usage_count\": " << e.usage_count << ",\n";
310 json << " \"usage_percentage\": "
311 << absl::StrFormat("%.2f", e.usage_percentage) << ",\n";
312 json << " \"locations\": [";
313 for (size_t j = 0; j < std::min(e.locations.size(), size_t(10)); ++j) {
314 json << e.locations[j];
315 if (j < std::min(e.locations.size(), size_t(10)) - 1)
316 json << ", ";
317 }
318 if (e.locations.size() > 10)
319 json << ", ...";
320 json << "]\n";
321 json << " }";
322 if (i < entries.size() - 1)
323 json << ",";
324 json << "\n";
325 }
326
327 json << " ],\n";
328 json << " \"total_entries\": " << entries.size() << "\n";
329 json << "}\n";
330
331 return json.str();
332}
333
334// =============================================================================
335// TileSimilarityTool Implementation
336// =============================================================================
337
339 const resources::ArgumentParser& parser) {
340 return parser.RequireArgs({"tile_id"});
341}
342
344 Rom* rom, const resources::ArgumentParser& parser,
345 resources::OutputFormatter& formatter) {
346 // Parse arguments
347 auto tile_id_or = parser.GetInt("tile_id");
348 if (!tile_id_or.ok()) {
349 return absl::InvalidArgumentError("tile_id is required");
350 }
351 int tile_id = tile_id_or.value();
352 int sheet = parser.GetInt("sheet").value_or(0);
353 int threshold = parser.GetInt("threshold").value_or(80);
354 std::string method = parser.GetString("method").value_or("structural");
355
356 // Validate threshold
357 threshold = std::clamp(threshold, 0, 100);
358
359 // Extract reference tile
360 auto ref_tile_or = ExtractTile(rom, sheet, tile_id);
361 if (!ref_tile_or.ok()) {
362 return ref_tile_or.status();
363 }
364 const auto& ref_tile = ref_tile_or.value();
365
366 // Find similar tiles across all sheets (or specified sheet)
367 std::vector<TileSimilarityMatch> matches;
368
369 bool has_sheet = parser.GetString("sheet").has_value();
370 int start_sheet = has_sheet ? sheet : 0;
371 int end_sheet = has_sheet ? sheet + 1 : kMaxSheets;
372
373 for (int s = start_sheet; s < end_sheet; ++s) {
374 int tile_count = GetTileCountForSheet(s);
375
376 for (int t = 0; t < tile_count; ++t) {
377 // Skip the reference tile itself
378 if (s == sheet && t == tile_id) {
379 continue;
380 }
381
382 auto cmp_tile_or = ExtractTile(rom, s, t);
383 if (!cmp_tile_or.ok()) {
384 continue; // Skip tiles that can't be extracted
385 }
386 const auto& cmp_tile = cmp_tile_or.value();
387
388 // Compute similarity
389 double score;
390 if (method == "pixel") {
391 score = ComputePixelDifference(ref_tile, cmp_tile);
392 } else {
393 score = ComputeStructuralSimilarity(ref_tile, cmp_tile);
394 }
395
396 if (score >= threshold) {
398 match.tile_id = t;
399 match.similarity_score = score;
400 match.sheet_index = s;
401 match.x_position = (t % kTilesPerRow) * kTileWidth;
402 match.y_position = (t / kTilesPerRow) * kTileHeight;
403 matches.push_back(match);
404 }
405 }
406 }
407
408 // Sort by similarity score descending
409 std::sort(matches.begin(), matches.end(), [](const auto& a, const auto& b) {
410 return a.similarity_score > b.similarity_score;
411 });
412
413 // Limit to top 50 matches
414 if (matches.size() > 50) {
415 matches.resize(50);
416 }
417
418 // Output results
419 std::cout << FormatMatchesAsJson(matches);
420
421 return absl::OkStatus();
422}
423
424// =============================================================================
425// SpritesheetAnalysisTool Implementation
426// =============================================================================
427
429 const resources::ArgumentParser& /*parser*/) {
430 return absl::OkStatus();
431}
432
434 Rom* rom, const resources::ArgumentParser& parser,
435 resources::OutputFormatter& formatter) {
436 int tile_size = parser.GetInt("tile_size").value_or(8);
437 if (tile_size != 8 && tile_size != 16) {
438 tile_size = 8;
439 }
440
441 std::vector<UnusedRegion> all_regions;
442
443 auto sheet_arg = parser.GetString("sheet");
444 if (sheet_arg.has_value()) {
445 int sheet = parser.GetInt("sheet").value_or(0);
446 auto regions = FindUnusedRegions(rom, sheet, tile_size);
447 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
448 } else {
449 // Analyze all sheets
450 for (int s = 0; s < kMaxSheets; ++s) {
451 auto regions = FindUnusedRegions(rom, s, tile_size);
452 all_regions.insert(all_regions.end(), regions.begin(), regions.end());
453 }
454 }
455
456 // Merge adjacent regions
457 all_regions = MergeAdjacentRegions(all_regions);
458
459 // Sort by size (largest first)
460 std::sort(
461 all_regions.begin(), all_regions.end(),
462 [](const auto& a, const auto& b) { return a.tile_count > b.tile_count; });
463
464 // Output results through the formatter so callers capture them.
465 if (formatter.IsJson()) {
466 const auto document =
467 nlohmann::json::parse(FormatRegionsAsJson(all_regions));
468 for (const auto& [key, value] : document.items()) {
469 formatter.AddRawJsonField(key, value.dump());
470 }
471 } else {
472 int total_tiles = 0;
473 for (const auto& region : all_regions) {
474 total_tiles += region.tile_count;
475 formatter.AddField(
476 absl::StrFormat("sheet 0x%02X", region.sheet_index),
477 absl::StrFormat("x=%d y=%d %dx%d (%d tiles)", region.x, region.y,
478 region.width, region.height, region.tile_count));
479 }
480 formatter.AddField("total_regions", static_cast<int>(all_regions.size()));
481 formatter.AddField("total_free_tiles", total_tiles);
482 }
483
484 return absl::OkStatus();
485}
486
488 Rom* rom, int sheet_index, int tile_size) const {
489 std::vector<UnusedRegion> regions;
490 if (rom == nullptr || !rom->is_loaded() || sheet_index < 0 ||
491 sheet_index >= kMaxSheets) {
492 return regions;
493 }
494 // Raw sheets (115-126) and 2bpp sheets are loaded by engine code, and
495 // reserved sheets must stay as they are: none of them is free space.
496 const auto sheet = static_cast<uint16_t>(sheet_index);
499 return regions;
500 }
501 std::vector<uint16_t> reserved_blocks;
502 if (project_ != nullptr) {
503 const auto& settings = project_->graphics_sheets;
504 const auto& manifest_reserved =
506 auto contains = [sheet](const std::vector<uint16_t>& ids) {
507 return std::find(ids.begin(), ids.end(), sheet) != ids.end();
508 };
509 if (contains(settings.reserved_sheets) || contains(manifest_reserved)) {
510 return regions;
511 }
512 if (auto it = settings.reserved_blocks.find(sheet);
513 it != settings.reserved_blocks.end()) {
514 reserved_blocks = it->second;
515 }
516 }
517
518 // Read the sheet from the ROM, so the result does not depend on whether
519 // the GUI has loaded graphics.
520 auto data = zelda3::ReadGfxSheetData(*rom, sheet);
521 if (!data.ok() || data->size() < zelda3::kGfxSheet3bppBytes) {
522 return regions;
523 }
524 constexpr size_t kTileBytes = 24; // one 3bpp 8x8 tile
525 auto tile_empty = [&data](int tile) {
526 const auto begin = data->begin() + tile * kTileBytes;
527 return std::all_of(begin, begin + kTileBytes,
528 [](uint8_t b) { return b == 0; });
529 };
530 auto block_reserved = [&reserved_blocks](int block) {
531 return std::find(reserved_blocks.begin(), reserved_blocks.end(), block) !=
532 reserved_blocks.end();
533 };
534
535 if (tile_size == 16) {
536 for (int block = 0; block < zelda3::kGfxSheetBlockCount; ++block) {
537 if (block_reserved(block) ||
538 !zelda3::IsGfxSheetBlockEmpty(*data, block, 3)) {
539 continue;
540 }
541 regions.push_back(
542 {sheet_index, (block % 8) * 16, (block / 8) * 16, 16, 16, 4});
543 }
544 return regions;
545 }
546
547 for (int tile = 0; tile < GetTileCountForSheet(sheet_index); ++tile) {
548 const int block = (tile / 32) * 8 + (tile % 16) / 2;
549 if (block_reserved(block) || !tile_empty(tile)) {
550 continue;
551 }
552 regions.push_back({sheet_index, (tile % 16) * 8, (tile / 16) * 8, 8, 8, 1});
553 }
554 return regions;
555}
556
558 const std::vector<UnusedRegion>& regions) const {
559 if (regions.empty()) {
560 return regions;
561 }
562
563 // Group by sheet
564 std::map<int, std::vector<UnusedRegion>> by_sheet;
565 for (const auto& r : regions) {
566 by_sheet[r.sheet_index].push_back(r);
567 }
568
569 std::vector<UnusedRegion> merged;
570
571 for (auto& [sheet, sheet_regions] : by_sheet) {
572 // Simple horizontal merging for adjacent tiles
573 std::sort(sheet_regions.begin(), sheet_regions.end(),
574 [](const auto& a, const auto& b) {
575 if (a.y != b.y)
576 return a.y < b.y;
577 return a.x < b.x;
578 });
579
580 for (size_t i = 0; i < sheet_regions.size();) {
581 UnusedRegion current = sheet_regions[i];
582
583 // Try to merge with following regions on the same row
584 size_t j = i + 1;
585 while (j < sheet_regions.size() && sheet_regions[j].y == current.y &&
586 sheet_regions[j].x == current.x + current.width) {
587 current.width += sheet_regions[j].width;
588 current.tile_count += sheet_regions[j].tile_count;
589 ++j;
590 }
591
592 merged.push_back(current);
593 i = j;
594 }
595 }
596
597 return merged;
598}
599
600// =============================================================================
601// PaletteUsageTool Implementation
602// =============================================================================
603
605 const resources::ArgumentParser& /*parser*/) {
606 return absl::OkStatus();
607}
608
610 const resources::ArgumentParser& parser,
611 resources::OutputFormatter& formatter) {
612 std::string type = parser.GetString("type").value_or("all");
613
614 std::vector<PaletteUsageStats> stats;
615
616 if (type == "overworld" || type == "all") {
617 auto ow_stats = AnalyzeOverworldPalettes(rom);
618 stats.insert(stats.end(), ow_stats.begin(), ow_stats.end());
619 }
620
621 if (type == "dungeon" || type == "all") {
622 auto dg_stats = AnalyzeDungeonPalettes(rom);
623 // Merge with overworld stats or add new entries
624 for (const auto& ds : dg_stats) {
625 bool found = false;
626 for (auto& s : stats) {
627 if (s.palette_index == ds.palette_index) {
628 s.usage_count += ds.usage_count;
629 s.used_by_maps.insert(s.used_by_maps.end(), ds.used_by_maps.begin(),
630 ds.used_by_maps.end());
631 found = true;
632 break;
633 }
634 }
635 if (!found) {
636 stats.push_back(ds);
637 }
638 }
639 }
640
641 // Calculate percentages
642 int total_usage = 0;
643 for (const auto& s : stats) {
644 total_usage += s.usage_count;
645 }
646 for (auto& s : stats) {
647 s.usage_percentage =
648 total_usage > 0
649 ? (static_cast<double>(s.usage_count) / total_usage) * 100.0
650 : 0.0;
651 }
652
653 // Sort by usage count
654 std::sort(stats.begin(), stats.end(), [](const auto& a, const auto& b) {
655 return a.usage_count > b.usage_count;
656 });
657
658 std::cout << FormatPaletteUsageAsJson(stats);
659
660 return absl::OkStatus();
661}
662
663std::vector<PaletteUsageStats> PaletteUsageTool::AnalyzeOverworldPalettes(
664 Rom* rom) const {
665 std::map<int, PaletteUsageStats> palette_map;
666
667 // Overworld palette indices used by each map are stored in ROM
668 // Light World uses palettes 0-7, Dark World uses 8-15 typically
669 // For now, return basic structure - full implementation would
670 // read actual palette assignments from ROM
671
672 // Initialize with 8 main palettes
673 for (int i = 0; i < 8; ++i) {
674 PaletteUsageStats stats;
675 stats.palette_index = i;
676 stats.usage_count = 0;
677 palette_map[i] = stats;
678 }
679
680 // Count usage across 128 overworld maps (0-63 Light, 64-127 Dark)
681 for (int map_id = 0; map_id < 128; ++map_id) {
682 // In actual implementation, read palette from map header
683 // For now, use a heuristic: Light World maps use lower palettes
684 int palette_idx = (map_id < 64) ? (map_id % 8) : (map_id % 8);
685 palette_map[palette_idx].usage_count++;
686 palette_map[palette_idx].used_by_maps.push_back(map_id);
687 }
688
689 std::vector<PaletteUsageStats> result;
690 for (const auto& [_, stats] : palette_map) {
691 result.push_back(stats);
692 }
693
694 return result;
695}
696
697std::vector<PaletteUsageStats> PaletteUsageTool::AnalyzeDungeonPalettes(
698 Rom* rom) const {
699 std::map<int, PaletteUsageStats> palette_map;
700
701 // Dungeons use a different palette system
702 // Each room can reference one of several dungeon palettes
703
704 // Initialize with dungeon palettes (typically 0-15)
705 for (int i = 0; i < 16; ++i) {
706 PaletteUsageStats stats;
707 stats.palette_index = i + 100; // Offset to distinguish from OW
708 stats.usage_count = 0;
709 palette_map[i + 100] = stats;
710 }
711
712 // Count usage across ~320 dungeon rooms
713 for (int room_id = 0; room_id < 320; ++room_id) {
714 // In actual implementation, read palette from room header
715 int palette_idx = 100 + (room_id % 16);
716 palette_map[palette_idx].usage_count++;
717 palette_map[palette_idx].used_by_maps.push_back(room_id);
718 }
719
720 std::vector<PaletteUsageStats> result;
721 for (const auto& [_, stats] : palette_map) {
722 result.push_back(stats);
723 }
724
725 return result;
726}
727
728// =============================================================================
729// TileHistogramTool Implementation
730// =============================================================================
731
733 const resources::ArgumentParser& /*parser*/) {
734 return absl::OkStatus();
735}
736
738 const resources::ArgumentParser& parser,
739 resources::OutputFormatter& formatter) {
740 std::string type = parser.GetString("type").value_or("overworld");
741 int top_n = parser.GetInt("top").value_or(20);
742
743 std::map<int, TileUsageEntry> usage_map;
744
745 if (type == "overworld") {
746 usage_map = CountOverworldTiles(rom);
747 } else if (type == "dungeon") {
748 usage_map = CountDungeonTiles(rom);
749 } else {
750 // Both
751 auto ow = CountOverworldTiles(rom);
752 auto dg = CountDungeonTiles(rom);
753 usage_map = ow;
754 for (const auto& [tile_id, entry] : dg) {
755 if (usage_map.count(tile_id)) {
756 usage_map[tile_id].usage_count += entry.usage_count;
757 usage_map[tile_id].locations.insert(usage_map[tile_id].locations.end(),
758 entry.locations.begin(),
759 entry.locations.end());
760 } else {
761 usage_map[tile_id] = entry;
762 }
763 }
764 }
765
766 // Convert to vector and calculate percentages
767 std::vector<TileUsageEntry> entries;
768 int total_usage = 0;
769 for (const auto& [_, entry] : usage_map) {
770 total_usage += entry.usage_count;
771 entries.push_back(entry);
772 }
773
774 for (auto& e : entries) {
775 e.usage_percentage =
776 total_usage > 0
777 ? (static_cast<double>(e.usage_count) / total_usage) * 100.0
778 : 0.0;
779 }
780
781 // Sort by usage count descending
782 std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
783 return a.usage_count > b.usage_count;
784 });
785
786 // Limit to top N
787 if (static_cast<int>(entries.size()) > top_n) {
788 entries.resize(top_n);
789 }
790
791 std::cout << FormatHistogramAsJson(entries);
792
793 return absl::OkStatus();
794}
795
796std::map<int, TileUsageEntry> TileHistogramTool::CountOverworldTiles(
797 Rom* rom) const {
798 std::map<int, TileUsageEntry> usage;
799
800 // Overworld uses Tile16 (16x16 metatiles) composed of Tile32 (32x32 blocks)
801 // Each overworld map is 32x32 Tile16s = 1024 tiles per map
802 // There are 160 overworld maps total
803
804 // For demonstration, create synthetic data
805 // In actual implementation, read tilemap data from ROM
806
807 for (int map_id = 0; map_id < 160; ++map_id) {
808 // Simulate reading 1024 tiles per map
809 for (int i = 0; i < 1024; ++i) {
810 // In actual implementation: int tile_id = ReadTileFromMap(rom, map_id, i);
811 int tile_id = (map_id * 7 + i) % 512; // Synthetic distribution
812
813 if (usage.count(tile_id) == 0) {
814 TileUsageEntry entry;
815 entry.tile_id = tile_id;
816 entry.usage_count = 0;
817 usage[tile_id] = entry;
818 }
819 usage[tile_id].usage_count++;
820
821 // Track first 10 locations only to save memory
822 if (usage[tile_id].locations.size() < 10) {
823 usage[tile_id].locations.push_back(map_id);
824 }
825 }
826 }
827
828 return usage;
829}
830
831std::map<int, TileUsageEntry> TileHistogramTool::CountDungeonTiles(
832 Rom* rom) const {
833 std::map<int, TileUsageEntry> usage;
834
835 // Dungeons use different tile arrangements
836 // Each room varies in size but typically uses floor, wall, and object tiles
837
838 for (int room_id = 0; room_id < 320; ++room_id) {
839 // Simulate tile usage per room
840 for (int i = 0; i < 256; ++i) {
841 int tile_id = (room_id * 11 + i) % 256; // Synthetic distribution
842
843 if (usage.count(tile_id) == 0) {
844 TileUsageEntry entry;
845 entry.tile_id = tile_id;
846 entry.usage_count = 0;
847 usage[tile_id] = entry;
848 }
849 usage[tile_id].usage_count++;
850
851 if (usage[tile_id].locations.size() < 10) {
852 usage[tile_id].locations.push_back(room_id +
853 1000); // Offset for dungeons
854 }
855 }
856 }
857
858 return usage;
859}
860
861// =============================================================================
862// OpenCV Integration (Optional)
863// =============================================================================
864
865#ifdef YAZE_WITH_OPENCV
866#include <opencv2/core.hpp>
867#include <opencv2/features2d.hpp>
868#include <opencv2/imgproc.hpp>
869
870namespace opencv {
871
872std::pair<std::pair<int, int>, double> TemplateMatch(
873 const std::vector<uint8_t>& reference,
874 const std::vector<uint8_t>& search_region, int width, int height,
875 int method) {
876 // Convert to cv::Mat
877 cv::Mat ref_mat(8, 8, CV_8UC1, const_cast<uint8_t*>(reference.data()));
878 cv::Mat search_mat(height, width, CV_8UC1,
879 const_cast<uint8_t*>(search_region.data()));
880
881 cv::Mat result;
882 cv::matchTemplate(search_mat, ref_mat, result, method);
883
884 double min_val, max_val;
885 cv::Point min_loc, max_loc;
886 cv::minMaxLoc(result, &min_val, &max_val, &min_loc, &max_loc);
887
888 // For TM_SQDIFF methods, lower is better
889 if (method == cv::TM_SQDIFF || method == cv::TM_SQDIFF_NORMED) {
890 return {{min_loc.x, min_loc.y}, 1.0 - min_val};
891 }
892
893 return {{max_loc.x, max_loc.y}, max_val};
894}
895
896double FeatureMatch(const std::vector<uint8_t>& tile_a,
897 const std::vector<uint8_t>& tile_b) {
898 cv::Mat mat_a(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_a.data()));
899 cv::Mat mat_b(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_b.data()));
900
901 // ORB detector
902 cv::Ptr<cv::ORB> orb = cv::ORB::create();
903
904 std::vector<cv::KeyPoint> kp_a, kp_b;
905 cv::Mat desc_a, desc_b;
906
907 orb->detectAndCompute(mat_a, cv::noArray(), kp_a, desc_a);
908 orb->detectAndCompute(mat_b, cv::noArray(), kp_b, desc_b);
909
910 if (desc_a.empty() || desc_b.empty()) {
911 return 0.0; // No features found
912 }
913
914 // BFMatcher
915 cv::BFMatcher matcher(cv::NORM_HAMMING);
916 std::vector<cv::DMatch> matches;
917 matcher.match(desc_a, desc_b, matches);
918
919 if (matches.empty()) {
920 return 0.0;
921 }
922
923 // Calculate match score based on distances
924 double total_dist = 0.0;
925 for (const auto& m : matches) {
926 total_dist += m.distance;
927 }
928
929 // Normalize: lower distance = higher similarity
930 double avg_dist = total_dist / matches.size();
931 return std::max(0.0, 100.0 - avg_dist);
932}
933
934double ComputeSSIM(const std::vector<uint8_t>& tile_a,
935 const std::vector<uint8_t>& tile_b) {
936 cv::Mat mat_a(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_a.data()));
937 cv::Mat mat_b(8, 8, CV_8UC1, const_cast<uint8_t*>(tile_b.data()));
938
939 const double C1 = 6.5025, C2 = 58.5225;
940
941 cv::Mat I1, I2;
942 mat_a.convertTo(I1, CV_32F);
943 mat_b.convertTo(I2, CV_32F);
944
945 cv::Mat I1_2 = I1.mul(I1);
946 cv::Mat I2_2 = I2.mul(I2);
947 cv::Mat I1_I2 = I1.mul(I2);
948
949 cv::Mat mu1, mu2;
950 cv::GaussianBlur(I1, mu1, cv::Size(3, 3), 1.5);
951 cv::GaussianBlur(I2, mu2, cv::Size(3, 3), 1.5);
952
953 cv::Mat mu1_2 = mu1.mul(mu1);
954 cv::Mat mu2_2 = mu2.mul(mu2);
955 cv::Mat mu1_mu2 = mu1.mul(mu2);
956
957 cv::Mat sigma1_2, sigma2_2, sigma12;
958 cv::GaussianBlur(I1_2, sigma1_2, cv::Size(3, 3), 1.5);
959 sigma1_2 -= mu1_2;
960 cv::GaussianBlur(I2_2, sigma2_2, cv::Size(3, 3), 1.5);
961 sigma2_2 -= mu2_2;
962 cv::GaussianBlur(I1_I2, sigma12, cv::Size(3, 3), 1.5);
963 sigma12 -= mu1_mu2;
964
965 cv::Mat t1 = 2 * mu1_mu2 + C1;
966 cv::Mat t2 = 2 * sigma12 + C2;
967 cv::Mat t3 = t1.mul(t2);
968
969 t1 = mu1_2 + mu2_2 + C1;
970 t2 = sigma1_2 + sigma2_2 + C2;
971 t1 = t1.mul(t2);
972
973 cv::Mat ssim_map;
974 cv::divide(t3, t1, ssim_map);
975
976 cv::Scalar mssim = cv::mean(ssim_map);
977 return mssim[0] * 100.0; // Convert to percentage
978}
979
980} // namespace opencv
981#endif // YAZE_WITH_OPENCV
982
983} // namespace tools
984} // namespace agent
985} // namespace cli
986} // 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:49
bool is_loaded() const
Definition rom.h:187
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::vector< PaletteUsageStats > AnalyzeOverworldPalettes(Rom *rom) const
Analyze overworld palette usage.
std::vector< PaletteUsageStats > AnalyzeDungeonPalettes(Rom *rom) const
Analyze dungeon palette usage.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
std::vector< UnusedRegion > MergeAdjacentRegions(const std::vector< UnusedRegion > &regions) const
Merge adjacent empty regions.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::vector< UnusedRegion > FindUnusedRegions(Rom *rom, int sheet_index, int tile_size) const
Find contiguous empty regions in a sheet.
std::map< int, TileUsageEntry > CountDungeonTiles(Rom *rom) const
Count tile usage in dungeons.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
std::map< int, TileUsageEntry > CountOverworldTiles(Rom *rom) const
Count tile usage in overworld.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
double ComputePixelDifference(const std::vector< uint8_t > &tile_a, const std::vector< uint8_t > &tile_b) const
Compute simple pixel difference (Mean Absolute Error)
int GetTileCountForSheet(int sheet_index) const
Get the number of tiles in a graphics sheet.
absl::StatusOr< std::vector< uint8_t > > ExtractTile(Rom *rom, int sheet_index, int tile_index) const
Extract 8x8 tile data from ROM graphics.
std::string FormatHistogramAsJson(const std::vector< TileUsageEntry > &entries) const
Format tile histogram as JSON.
std::string FormatRegionsAsJson(const std::vector< UnusedRegion > &regions) const
Format unused regions as JSON.
std::string FormatMatchesAsJson(const std::vector< TileSimilarityMatch > &matches) const
Format similarity matches as JSON.
absl::StatusOr< std::vector< uint8_t > > ExtractTileAtPosition(Rom *rom, int sheet_index, int x, int y) const
Extract 8x8 tile at specific pixel coordinates.
bool IsRegionEmpty(const std::vector< uint8_t > &data) const
Check if a tile region is empty (all 0x00 or 0xFF)
double ComputeStructuralSimilarity(const std::vector< uint8_t > &tile_a, const std::vector< uint8_t > &tile_b) const
Compute structural similarity index (SSIM-like)
std::string FormatPaletteUsageAsJson(const std::vector< PaletteUsageStats > &stats) const
Format palette usage as JSON.
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)
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
void AddRawJsonField(const std::string &key, const std::string &json)
Add a field whose value is already-serialized JSON (numbers, nested arrays, null)....
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
const GraphicsSheetLayout & graphics_sheet_layout() const
Graphics-sheet free space and reserved sheets. Empty when the optional graphics_sheet_regions section...
GraphicsSheets & gfx_sheets()
Definition arena.h:178
static Arena & Get()
Definition arena.cc:24
Locates graphics sheets in the ROM and writes edited sheets back.
constexpr size_t kGfxSheet3bppBytes
absl::StatusOr< std::vector< uint8_t > > ReadGfxSheetData(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Returns the sheet's decoded bytes (SNES planar data).
bool IsGfxSheetBlockEmpty(const std::vector< uint8_t > &data, int block, int bpp)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
constexpr int kGfxSheetBlockCount
16x16 blocks per sheet: 8 columns x 2 rows over the 128x32 sheet.
Result of a tile similarity comparison.
Represents a contiguous unused region in a spritesheet.
std::vector< uint16_t > reserved_sheets
core::HackManifest hack_manifest
Definition project.h:360
GraphicsSheetSettings graphics_sheets
Definition project.h:348
Visual analysis tools for AI agents.