yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_inspect.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <map>
5#include <optional>
6#include <string>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/strings/ascii.h"
11#include "absl/strings/numbers.h"
12#include "absl/strings/str_cat.h"
13#include "absl/strings/str_format.h"
14#include "util/macro.h"
15#include "zelda3/common.h"
21
22namespace yaze {
23namespace cli {
24namespace overworld {
25
26namespace {
27
28constexpr int kLightWorldOffset = 0x00;
29constexpr int kDarkWorldOffset = 0x40;
30constexpr int kSpecialWorldOffset = 0x80;
31
32int NormalizeMapId(uint16_t raw_map_id) {
33 return static_cast<int>(raw_map_id & 0x00FF);
34}
35
36int WorldOffset(int world) {
37 switch (world) {
38 case 0:
39 return kLightWorldOffset;
40 case 1:
41 return kDarkWorldOffset;
42 case 2:
43 return kSpecialWorldOffset;
44 default:
45 return 0;
46 }
47}
48
49absl::Status ValidateMapId(int map_id) {
50 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
51 return absl::InvalidArgumentError(
52 absl::StrFormat("Map ID out of range: 0x%02X", map_id));
53 }
54 return absl::OkStatus();
55}
56
58 switch (size) {
60 return "Small";
62 return "Large";
64 return "Wide";
66 return "Tall";
67 default:
68 return "Unknown";
69 }
70}
71
72std::string EntranceLabel(uint8_t id) {
73 constexpr size_t kEntranceCount =
75 if (id < kEntranceCount) {
76 return zelda3::kEntranceNames[id];
77 }
78 return absl::StrFormat("Entrance %d", id);
79}
80
81void PopulateCommonWarpFields(WarpEntry& entry, uint16_t raw_map_id,
82 uint16_t map_pos, int pixel_x, int pixel_y) {
83 entry.raw_map_id = raw_map_id;
84 entry.map_id = NormalizeMapId(raw_map_id);
85 if (entry.map_id >= zelda3::kNumOverworldMaps) {
86 // Some ROM hacks use sentinel values. Clamp to valid range for reporting
88 }
89 entry.world = (entry.map_id >= kSpecialWorldOffset)
90 ? 2
91 : (entry.map_id >= kDarkWorldOffset ? 1 : 0);
92 entry.local_index = entry.map_id - WorldOffset(entry.world);
93 entry.map_x = entry.local_index % 8;
94 entry.map_y = entry.local_index / 8;
95 entry.map_pos = map_pos;
96 entry.pixel_x = pixel_x;
97 entry.pixel_y = pixel_y;
98 int tile_index = static_cast<int>(map_pos >> 1);
99 entry.tile16_x = tile_index & 0x3F;
100 entry.tile16_y = tile_index >> 6;
101}
102
103} // namespace
104
105absl::StatusOr<int> ParseNumeric(std::string_view value, int base) {
106 try {
107 size_t processed = 0;
108 int result = std::stoi(std::string(value), &processed, base);
109 if (processed != value.size()) {
110 return absl::InvalidArgumentError(
111 absl::StrCat("Invalid numeric value: ", std::string(value)));
112 }
113 return result;
114 } catch (const std::exception&) {
115 return absl::InvalidArgumentError(
116 absl::StrCat("Invalid numeric value: ", std::string(value)));
117 }
118}
119
120absl::StatusOr<int> ParseWorldSpecifier(std::string_view value) {
121 std::string lower = absl::AsciiStrToLower(std::string(value));
122 if (lower == "0" || lower == "light") {
123 return 0;
124 }
125 if (lower == "1" || lower == "dark") {
126 return 1;
127 }
128 if (lower == "2" || lower == "special") {
129 return 2;
130 }
131 return absl::InvalidArgumentError(
132 absl::StrCat("Unknown world value: ", std::string(value)));
133}
134
135absl::StatusOr<int> InferWorldFromMapId(int map_id) {
136 RETURN_IF_ERROR(ValidateMapId(map_id));
137 if (map_id < kDarkWorldOffset) {
138 return 0;
139 }
140 if (map_id < kSpecialWorldOffset) {
141 return 1;
142 }
143 return 2;
144}
145
146std::string WorldName(int world) {
147 switch (world) {
148 case 0:
149 return "Light";
150 case 1:
151 return "Dark";
152 case 2:
153 return "Special";
154 default:
155 return absl::StrCat("Unknown(", world, ")");
156 }
157}
158
159std::string WarpTypeName(WarpType type) {
160 switch (type) {
162 return "entrance";
163 case WarpType::kHole:
164 return "hole";
165 case WarpType::kExit:
166 return "exit";
167 default:
168 return "unknown";
169 }
170}
171
172absl::StatusOr<MapSummary> BuildMapSummary(zelda3::Overworld& overworld,
173 int map_id) {
174 RETURN_IF_ERROR(ValidateMapId(map_id));
175 ASSIGN_OR_RETURN(int world, InferWorldFromMapId(map_id));
176
177 // Ensure map data is built before accessing metadata.
178 RETURN_IF_ERROR(overworld.EnsureMapBuilt(map_id));
179
180 const auto* map = overworld.overworld_map(map_id);
181 if (map == nullptr) {
182 return absl::InternalError(
183 absl::StrFormat("Failed to retrieve overworld map 0x%02X", map_id));
184 }
185
186 MapSummary summary;
187 summary.map_id = map_id;
188 summary.world = world;
189 summary.local_index = map_id - WorldOffset(world);
190 summary.map_x = summary.local_index % 8;
191 summary.map_y = summary.local_index / 8;
192 summary.is_large_map = map->is_large_map();
193 summary.parent_map = map->parent();
194 summary.large_quadrant = map->large_index();
195 summary.area_size = AreaSizeToString(map->area_size());
196 summary.message_id = map->message_id();
197 summary.area_graphics = map->area_graphics();
198 summary.area_palette = map->area_palette();
199 summary.main_palette = map->main_palette();
200 summary.animated_gfx = map->animated_gfx();
201 summary.subscreen_overlay = map->subscreen_overlay();
202 summary.area_specific_bg_color = map->area_specific_bg_color();
203
204 summary.sprite_graphics.clear();
205 summary.sprite_palettes.clear();
206 summary.area_music.clear();
207 summary.static_graphics.clear();
208
209 for (int i = 0; i < 3; ++i) {
210 summary.sprite_graphics.push_back(map->sprite_graphics(i));
211 summary.sprite_palettes.push_back(map->sprite_palette(i));
212 }
213
214 for (int i = 0; i < 4; ++i) {
215 summary.area_music.push_back(map->area_music(i));
216 }
217
218 for (int i = 0; i < 16; ++i) {
219 summary.static_graphics.push_back(map->static_graphics(i));
220 }
221
222 summary.has_overlay = map->has_overlay();
223 summary.overlay_id = map->overlay_id();
224
225 return summary;
226}
227
228absl::StatusOr<std::vector<WarpEntry>> CollectWarpEntries(
229 const zelda3::Overworld& overworld, const WarpQuery& query) {
230 std::vector<WarpEntry> entries;
231
232 const auto& entrances = overworld.entrances();
233 for (const auto& entrance : entrances) {
234 WarpEntry entry;
236 entry.deleted = entrance.deleted;
237 entry.is_hole = entrance.is_hole_;
238 entry.entrance_id = entrance.entrance_id_;
239 entry.entrance_name = EntranceLabel(entrance.entrance_id_);
240 PopulateCommonWarpFields(entry, entrance.map_id_, entrance.map_pos_,
241 entrance.x_, entrance.y_);
242
243 if (query.type.has_value() && *query.type != entry.type) {
244 continue;
245 }
246 if (query.world.has_value() && *query.world != entry.world) {
247 continue;
248 }
249 if (query.map_id.has_value() && *query.map_id != entry.map_id) {
250 continue;
251 }
252
253 entries.push_back(std::move(entry));
254 }
255
256 const auto& holes = overworld.holes();
257 for (const auto& hole : holes) {
258 WarpEntry entry;
259 entry.type = WarpType::kHole;
260 entry.deleted = false;
261 entry.is_hole = true;
262 entry.entrance_id = hole.entrance_id_;
263 entry.entrance_name = EntranceLabel(hole.entrance_id_);
264 PopulateCommonWarpFields(entry, hole.map_id_, hole.map_pos_, hole.x_,
265 hole.y_);
266
267 if (query.type.has_value() && *query.type != entry.type) {
268 continue;
269 }
270 if (query.world.has_value() && *query.world != entry.world) {
271 continue;
272 }
273 if (query.map_id.has_value() && *query.map_id != entry.map_id) {
274 continue;
275 }
276
277 entries.push_back(std::move(entry));
278 }
279
280 std::sort(entries.begin(), entries.end(),
281 [](const WarpEntry& a, const WarpEntry& b) {
282 if (a.world != b.world) {
283 return a.world < b.world;
284 }
285 if (a.map_id != b.map_id) {
286 return a.map_id < b.map_id;
287 }
288 if (a.tile16_y != b.tile16_y) {
289 return a.tile16_y < b.tile16_y;
290 }
291 if (a.tile16_x != b.tile16_x) {
292 return a.tile16_x < b.tile16_x;
293 }
294 return static_cast<int>(a.type) < static_cast<int>(b.type);
295 });
296
297 return entries;
298}
299
300std::pair<int, int> AreaScreenSpan(const zelda3::OverworldMap& parent_map) {
301 switch (parent_map.area_size()) {
303 return {2, 2};
305 return {2, 1};
307 return {1, 2};
308 default:
309 // Pre-v3 ROMs only record large vs small.
310 return parent_map.is_large_map() ? std::pair<int, int>{2, 2}
311 : std::pair<int, int>{1, 1};
312 }
313}
314
315absl::StatusOr<AreaTileLocation> ResolveAreaTileLocation(
316 int map_id, int parent_map, int area_columns, int area_rows, int x, int y) {
317 RETURN_IF_ERROR(ValidateMapId(map_id));
318 RETURN_IF_ERROR(ValidateMapId(parent_map));
319 if (area_columns < 1 || area_columns > 2 || area_rows < 1 || area_rows > 2) {
320 return absl::InvalidArgumentError(
321 absl::StrFormat("Area span must be 1 or 2 screens per axis, got %dx%d",
322 area_columns, area_rows));
323 }
324 ASSIGN_OR_RETURN(const int world, InferWorldFromMapId(map_id));
325 ASSIGN_OR_RETURN(const int parent_world, InferWorldFromMapId(parent_map));
326 if (world != parent_world) {
327 return absl::FailedPreconditionError(absl::StrFormat(
328 "Map 0x%02X is in the %s World but its parent 0x%02X is in the %s "
329 "World",
330 map_id, WorldName(world), parent_map, WorldName(parent_world)));
331 }
332
333 const int parent_local = parent_map - WorldOffset(world);
334 const int parent_column = parent_local % 8;
335 const int parent_row = parent_local / 8;
336 const int map_local = map_id - WorldOffset(world);
337 const int map_column = map_local % 8;
338 const int map_row = map_local / 8;
339 if (map_column < parent_column ||
340 map_column >= parent_column + area_columns || map_row < parent_row ||
341 map_row >= parent_row + area_rows) {
342 return absl::FailedPreconditionError(absl::StrFormat(
343 "Map 0x%02X is outside the %dx%d-screen area of its parent 0x%02X",
344 map_id, area_columns, area_rows, parent_map));
345 }
346 if (parent_column + area_columns > 8) {
347 return absl::FailedPreconditionError(absl::StrFormat(
348 "Area 0x%02X (%d screens wide) crosses the world's east edge",
349 parent_map, area_columns));
350 }
351
352 const int width = area_columns * 32;
353 const int height = area_rows * 32;
354 if (x < 0 || y < 0 || x >= width || y >= height) {
355 return absl::OutOfRangeError(absl::StrFormat(
356 "Tile (%d,%d) is outside area 0x%02X: x must be 0-%d and y 0-%d "
357 "(tile16 units relative to the area's top-left; decimal, or hex with "
358 "a 0x prefix)",
359 x, y, parent_map, width - 1, height - 1));
360 }
361
362 AreaTileLocation location;
363 location.map_id = map_id;
364 location.parent_map = parent_map;
365 location.world = world;
366 location.area_width = width;
367 location.area_height = height;
368 location.area_x = x;
369 location.area_y = y;
370 location.screen_id = parent_map + (x / 32) + (y / 32) * 8;
371 location.screen_x = x % 32;
372 location.screen_y = y % 32;
373 location.world_x = parent_column * 32 + x;
374 location.world_y = parent_row * 32 + y;
375 RETURN_IF_ERROR(ValidateMapId(location.screen_id));
376 return location;
377}
378
379absl::StatusOr<AreaTileLocation> ResolveAreaTileForMap(
380 const zelda3::Overworld& overworld, int map_id, int x, int y) {
381 RETURN_IF_ERROR(ValidateMapId(map_id));
382 const auto* map = overworld.overworld_map(map_id);
383 if (map == nullptr) {
384 return absl::FailedPreconditionError(
385 absl::StrFormat("Overworld map 0x%02X is not loaded", map_id));
386 }
387 const int parent = map->parent();
388 RETURN_IF_ERROR(ValidateMapId(parent));
389 const auto* parent_map = overworld.overworld_map(parent);
390 if (parent_map == nullptr) {
391 return absl::FailedPreconditionError(
392 absl::StrFormat("Overworld map 0x%02X is not loaded", parent));
393 }
394 const auto [columns, rows] = AreaScreenSpan(*parent_map);
395 return ResolveAreaTileLocation(map_id, parent, columns, rows, x, y);
396}
397
398absl::StatusOr<AreaTileLocation> LocateScreenTile(
399 const zelda3::Overworld& overworld, int screen_id, int screen_x,
400 int screen_y) {
401 RETURN_IF_ERROR(ValidateMapId(screen_id));
402 if (screen_x < 0 || screen_x >= 32 || screen_y < 0 || screen_y >= 32) {
403 return absl::OutOfRangeError(absl::StrFormat(
404 "Screen tile (%d,%d) is outside 0-31", screen_x, screen_y));
405 }
406 const auto* map = overworld.overworld_map(screen_id);
407 if (map == nullptr) {
408 return absl::FailedPreconditionError(
409 absl::StrFormat("Overworld map 0x%02X is not loaded", screen_id));
410 }
411 const int parent = map->parent();
412 RETURN_IF_ERROR(ValidateMapId(parent));
413 ASSIGN_OR_RETURN(const int world, InferWorldFromMapId(screen_id));
414 const int screen_local = screen_id - WorldOffset(world);
415 const int parent_local = parent - WorldOffset(world);
416 const int x = (screen_local % 8 - parent_local % 8) * 32 + screen_x;
417 const int y = (screen_local / 8 - parent_local / 8) * 32 + screen_y;
418 return ResolveAreaTileForMap(overworld, screen_id, x, y);
419}
420
421absl::StatusOr<uint16_t> ReadAreaTile(zelda3::Overworld& overworld,
422 const AreaTileLocation& location) {
423 const auto& tiles = overworld.GetMapTiles(location.world);
424 if (location.world_x < 0 ||
425 location.world_x >= static_cast<int>(tiles.size()) ||
426 location.world_y < 0 ||
427 location.world_y >= static_cast<int>(tiles[location.world_x].size())) {
428 return absl::FailedPreconditionError(
429 "Overworld tile data is not loaded for this world");
430 }
431 return tiles[location.world_x][location.world_y];
432}
433
434absl::Status WriteAreaTile(zelda3::Overworld& overworld,
435 const AreaTileLocation& location, uint16_t tile_id) {
436 RETURN_IF_ERROR(ReadAreaTile(overworld, location).status());
437 overworld.GetMapTiles(location.world)[location.world_x][location.world_y] =
438 tile_id;
439 return absl::OkStatus();
440}
441
442absl::StatusOr<std::vector<TileMatch>> FindTileMatches(
443 zelda3::Overworld& overworld, uint16_t tile_id,
444 const TileSearchOptions& options) {
445 if (options.map_id.has_value()) {
446 RETURN_IF_ERROR(ValidateMapId(*options.map_id));
447 }
448 if (options.world.has_value()) {
449 if (*options.world < 0 || *options.world > 2) {
450 return absl::InvalidArgumentError(
451 absl::StrFormat("Unknown world index: %d", *options.world));
452 }
453 }
454
455 if (options.map_id.has_value() && options.world.has_value()) {
456 ASSIGN_OR_RETURN(int inferred_world, InferWorldFromMapId(*options.map_id));
457 if (inferred_world != *options.world) {
458 return absl::InvalidArgumentError(absl::StrFormat(
459 "Map 0x%02X belongs to the %s World but --world requested %s",
460 *options.map_id, WorldName(inferred_world),
461 WorldName(*options.world)));
462 }
463 }
464
465 std::vector<int> worlds;
466 if (options.world.has_value()) {
467 worlds.push_back(*options.world);
468 } else if (options.map_id.has_value()) {
469 ASSIGN_OR_RETURN(int inferred_world, InferWorldFromMapId(*options.map_id));
470 worlds.push_back(inferred_world);
471 } else {
472 worlds = {0, 1, 2};
473 }
474
475 std::vector<TileMatch> matches;
476
477 for (int world : worlds) {
478 int world_start = 0;
479 int world_maps = 0;
480 switch (world) {
481 case 0:
482 world_start = 0x00;
483 world_maps = 0x40;
484 break;
485 case 1:
486 world_start = 0x40;
487 world_maps = 0x40;
488 break;
489 case 2:
490 world_start = 0x80;
491 world_maps = 0x20;
492 break;
493 default:
494 return absl::InvalidArgumentError(
495 absl::StrFormat("Unknown world index: %d", world));
496 }
497
498 overworld.set_current_world(world);
499
500 for (int local_map = 0; local_map < world_maps; ++local_map) {
501 int map_id = world_start + local_map;
502 if (options.map_id.has_value() && map_id != *options.map_id) {
503 continue;
504 }
505
506 int map_x_index = local_map % 8;
507 int map_y_index = local_map / 8;
508
509 int global_x_start = map_x_index * 32;
510 int global_y_start = map_y_index * 32;
511
512 for (int local_y = 0; local_y < 32; ++local_y) {
513 for (int local_x = 0; local_x < 32; ++local_x) {
514 int global_x = global_x_start + local_x;
515 int global_y = global_y_start + local_y;
516
517 uint16_t current_tile = overworld.GetTile(global_x, global_y);
518 if (current_tile == tile_id) {
519 matches.push_back(
520 {map_id, world, local_x, local_y, global_x, global_y});
521 }
522 }
523 }
524 }
525 }
526
527 return matches;
528}
529
530const char* SpritePhaseName(int phase) {
531 switch (phase) {
532 case 0:
533 return "beginning";
534 case 1:
535 return "first_part";
536 case 2:
537 return "second_part";
538 }
539 return "unknown";
540}
541
542absl::StatusOr<std::vector<OverworldSprite>> CollectOverworldSprites(
543 const zelda3::Overworld& overworld, const SpriteQuery& query) {
544 std::vector<OverworldSprite> results;
545
546 // Iterate through all 3 game states (beginning, zelda, agahnim)
547 for (int game_state = 0; game_state < 3; ++game_state) {
548 if (query.phase.has_value() && *query.phase != game_state) {
549 continue;
550 }
551 const auto sprites = overworld.sprites(game_state);
552 std::map<int, int> next_index_by_map;
553
554 for (const auto& sprite : sprites) {
555 const int list_index = next_index_by_map[sprite.map_id()]++;
556 // Apply filters
557 if (query.sprite_id.has_value() && sprite.id() != *query.sprite_id) {
558 continue;
559 }
560
561 int map_id = sprite.map_id();
562 if (query.map_id.has_value() && map_id != *query.map_id) {
563 continue;
564 }
565
566 // Determine world from map_id
567 int world = (map_id >= kSpecialWorldOffset)
568 ? 2
569 : (map_id >= kDarkWorldOffset ? 1 : 0);
570
571 if (query.world.has_value() && world != *query.world) {
572 continue;
573 }
574
575 OverworldSprite entry;
576 entry.sprite_id = sprite.id();
577 entry.map_id = map_id;
578 entry.world = world;
579 entry.x = sprite.x();
580 entry.y = sprite.y();
581 entry.phase = game_state;
582 entry.list_index = list_index;
583 entry.local_x = (sprite.x() - (map_id % 8) * 512) / 16;
584 entry.local_y = (sprite.y() - ((map_id % 64) / 8) * 512) / 16;
585 entry.sprite_name = zelda3::ResolveSpriteName(sprite.id());
586
587 results.push_back(entry);
588 }
589 }
590
591 return results;
592}
593
594absl::StatusOr<EntranceDetails> GetEntranceDetails(
595 const zelda3::Overworld& overworld, uint8_t entrance_id) {
596 const auto& entrances = overworld.entrances();
597
598 if (entrance_id >= entrances.size()) {
599 return absl::NotFoundError(absl::StrFormat(
600 "Entrance %d not found (max: %d)", entrance_id, entrances.size() - 1));
601 }
602
603 const auto& entrance = entrances[entrance_id];
604
605 EntranceDetails details;
606 details.entrance_id = entrance_id;
607 details.map_id = entrance.map_id_;
608
609 // Determine world from map_id
610 details.world = (details.map_id >= kSpecialWorldOffset)
611 ? 2
612 : (details.map_id >= kDarkWorldOffset ? 1 : 0);
613
614 details.x = entrance.x_;
615 details.y = entrance.y_;
616 details.area_x = entrance.game_x_;
617 details.area_y = entrance.game_y_;
618 details.map_pos = entrance.map_pos_;
619 details.is_hole = entrance.is_hole_;
620
621 // Get entrance name if available
622 details.entrance_name = EntranceLabel(entrance_id);
623
624 return details;
625}
626
627absl::StatusOr<TileStatistics> AnalyzeTileUsage(
628 zelda3::Overworld& overworld, uint16_t tile_id,
629 const TileSearchOptions& options) {
630 // Use FindTileMatches to get all occurrences
631 ASSIGN_OR_RETURN(auto matches, FindTileMatches(overworld, tile_id, options));
632
633 TileStatistics stats;
634 stats.tile_id = tile_id;
635 stats.count = static_cast<int>(matches.size());
636
637 // If scoped to a specific map, store that info
638 if (options.map_id.has_value()) {
639 stats.map_id = *options.map_id;
640 if (options.world.has_value()) {
641 stats.world = *options.world;
642 } else {
644 }
645 } else {
646 stats.map_id = -1; // Indicates all maps
647 stats.world = -1;
648 }
649
650 // Store positions (convert from TileMatch to pair)
651 stats.positions.reserve(matches.size());
652 for (const auto& match : matches) {
653 stats.positions.emplace_back(match.local_x, match.local_y);
654 }
655
656 return stats;
657}
658
659absl::StatusOr<TileUsageSummary> CollectTileUsage(zelda3::Overworld& overworld,
660 const TileUsageQuery& query) {
661 std::vector<int> worlds = {0, 1, 2};
662 if (query.world.has_value()) {
663 if (*query.world < 0 || *query.world > 2) {
664 return absl::InvalidArgumentError(
665 absl::StrFormat("Unknown world index: %d", *query.world));
666 }
667 worlds = {*query.world};
668 }
669 if (query.map_id.has_value()) {
670 ASSIGN_OR_RETURN(const int map_world, InferWorldFromMapId(*query.map_id));
671 if (query.world.has_value() && *query.world != map_world) {
672 return absl::InvalidArgumentError(absl::StrFormat(
673 "Map 0x%02X belongs to the %s World but --world requested %s",
674 *query.map_id, WorldName(map_world), WorldName(*query.world)));
675 }
676 worlds = {map_world};
677 }
678
679 constexpr int kScreenTiles = 32;
680 constexpr size_t kTileIds = 0x10000;
681 std::vector<int> counts(kTileIds, 0);
682 std::vector<int> screens_using(kTileIds, 0);
683 std::vector<uint16_t> screen_tiles;
684 screen_tiles.reserve(kScreenTiles * kScreenTiles);
685
686 TileUsageSummary summary;
687 for (const int world : worlds) {
688 const auto& grid = overworld.GetMapTiles(world);
689 const int world_screens = world == 2 ? 0x20 : 0x40;
690 std::vector<bool> seen_in_world(kTileIds, false);
691 WorldTileUsage world_usage;
692 world_usage.world = world;
693
694 for (int local = 0; local < world_screens; ++local) {
695 const int map_id = WorldOffset(world) + local;
696 if (query.map_id.has_value() && map_id != *query.map_id) {
697 continue;
698 }
699 const int x0 = (local % 8) * kScreenTiles;
700 const int y0 = (local / 8) * kScreenTiles;
701 screen_tiles.clear();
702 for (int x = x0; x < x0 + kScreenTiles; ++x) {
703 if (x >= static_cast<int>(grid.size()) ||
704 static_cast<int>(grid[x].size()) < y0 + kScreenTiles) {
705 return absl::FailedPreconditionError(
706 absl::StrFormat("Overworld tile data is not loaded for the %s "
707 "World",
708 WorldName(world)));
709 }
710 screen_tiles.insert(screen_tiles.end(), grid[x].begin() + y0,
711 grid[x].begin() + y0 + kScreenTiles);
712 }
713 std::sort(screen_tiles.begin(), screen_tiles.end());
714
715 ScreenTileUsage screen;
716 screen.map_id = map_id;
717 screen.world = world;
718 const auto* map = overworld.overworld_map(map_id);
719 screen.parent_map = map != nullptr ? map->parent() : map_id;
720 screen.total_tiles = static_cast<int>(screen_tiles.size());
721 for (size_t i = 0; i < screen_tiles.size();) {
722 const uint16_t tile_id = screen_tiles[i];
723 size_t end = i;
724 while (end < screen_tiles.size() && screen_tiles[end] == tile_id) {
725 ++end;
726 }
727 const int run = static_cast<int>(end - i);
728 i = end;
729
730 ++screen.unique_tiles;
731 counts[tile_id] += run;
732 ++screens_using[tile_id];
733 if (!seen_in_world[tile_id]) {
734 seen_in_world[tile_id] = true;
735 ++world_usage.unique_tiles;
736 }
737 if (run > screen.top_tile_count) {
738 screen.top_tile = tile_id;
739 screen.top_tile_count = run;
740 }
741 if (query.tile_id.has_value() && tile_id == *query.tile_id) {
742 screen.tile_count = run;
743 }
744 }
745 ++world_usage.screens;
746 world_usage.total_tiles += screen.total_tiles;
747 summary.total_tiles += screen.total_tiles;
748 summary.screens.push_back(screen);
749 }
750 summary.worlds.push_back(world_usage);
751 }
752 summary.screens_scanned = static_cast<int>(summary.screens.size());
753
754 for (size_t tile_id = 0; tile_id < kTileIds; ++tile_id) {
755 if (counts[tile_id] > 0) {
756 summary.tiles.push_back({static_cast<uint16_t>(tile_id), counts[tile_id],
757 screens_using[tile_id]});
758 }
759 }
760 std::stable_sort(summary.tiles.begin(), summary.tiles.end(),
761 [](const TileUsageCount& a, const TileUsageCount& b) {
762 return a.count > b.count;
763 });
764 return summary;
765}
766
767} // namespace overworld
768} // namespace cli
769} // namespace yaze
Represents a single Overworld map screen.
void set_current_world(int world)
Definition overworld.h:810
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:775
auto sprites(int state) const
Definition overworld.h:754
auto overworld_map(int i) const
Definition overworld.h:737
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
uint16_t GetTile(int x, int y) const
Definition overworld.h:813
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:770
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:721
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
void PopulateCommonWarpFields(WarpEntry &entry, uint16_t raw_map_id, uint16_t map_pos, int pixel_x, int pixel_y)
absl::StatusOr< TileUsageSummary > CollectTileUsage(zelda3::Overworld &overworld, const TileUsageQuery &query)
absl::StatusOr< int > InferWorldFromMapId(int map_id)
absl::StatusOr< MapSummary > BuildMapSummary(zelda3::Overworld &overworld, int map_id)
absl::Status WriteAreaTile(zelda3::Overworld &overworld, const AreaTileLocation &location, uint16_t tile_id)
absl::StatusOr< AreaTileLocation > ResolveAreaTileLocation(int map_id, int parent_map, int area_columns, int area_rows, int x, int y)
absl::StatusOr< std::vector< WarpEntry > > CollectWarpEntries(const zelda3::Overworld &overworld, const WarpQuery &query)
absl::StatusOr< int > ParseNumeric(std::string_view value, int base)
absl::StatusOr< TileStatistics > AnalyzeTileUsage(zelda3::Overworld &overworld, uint16_t tile_id, const TileSearchOptions &options)
absl::StatusOr< int > ParseWorldSpecifier(std::string_view value)
absl::StatusOr< AreaTileLocation > ResolveAreaTileForMap(const zelda3::Overworld &overworld, int map_id, int x, int y)
absl::StatusOr< EntranceDetails > GetEntranceDetails(const zelda3::Overworld &overworld, uint8_t entrance_id)
absl::StatusOr< std::vector< OverworldSprite > > CollectOverworldSprites(const zelda3::Overworld &overworld, const SpriteQuery &query)
absl::StatusOr< uint16_t > ReadAreaTile(zelda3::Overworld &overworld, const AreaTileLocation &location)
absl::StatusOr< AreaTileLocation > LocateScreenTile(const zelda3::Overworld &overworld, int screen_id, int screen_x, int screen_y)
absl::StatusOr< std::vector< TileMatch > > FindTileMatches(zelda3::Overworld &overworld, uint16_t tile_id, const TileSearchOptions &options)
std::string WarpTypeName(WarpType type)
const char * SpritePhaseName(int phase)
std::string WorldName(int world)
std::pair< int, int > AreaScreenSpan(const zelda3::OverworldMap &parent_map)
constexpr int kNumOverworldMaps
Definition common.h:85
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr const char * kEntranceNames[]
Definition common.h:91
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:287
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::optional< std::string > entrance_name
std::vector< uint8_t > sprite_graphics
std::vector< uint8_t > static_graphics
std::vector< uint8_t > sprite_palettes
std::vector< uint8_t > area_music
std::optional< std::string > sprite_name
std::optional< uint8_t > sprite_id
std::vector< std::pair< int, int > > positions
std::vector< ScreenTileUsage > screens
std::vector< TileUsageCount > tiles
std::vector< WorldTileUsage > worlds
std::optional< std::string > entrance_name
std::optional< uint8_t > entrance_id
std::optional< WarpType > type