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"
29constexpr int kDarkWorldOffset = 0x40;
30constexpr int kSpecialWorldOffset = 0x80;
33 return static_cast<int>(raw_map_id & 0x00FF);
41 return kDarkWorldOffset;
43 return kSpecialWorldOffset;
49absl::Status ValidateMapId(
int map_id) {
51 return absl::InvalidArgumentError(
52 absl::StrFormat(
"Map ID out of range: 0x%02X", map_id));
54 return absl::OkStatus();
73 constexpr size_t kEntranceCount =
75 if (
id < kEntranceCount) {
78 return absl::StrFormat(
"Entrance %d",
id);
82 uint16_t map_pos,
int pixel_x,
int pixel_y) {
91 : (entry.
map_id >= kDarkWorldOffset ? 1 : 0);
98 int tile_index =
static_cast<int>(map_pos >> 1);
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)));
114 }
catch (
const std::exception&) {
115 return absl::InvalidArgumentError(
116 absl::StrCat(
"Invalid numeric value: ", std::string(value)));
121 std::string lower = absl::AsciiStrToLower(std::string(value));
122 if (lower ==
"0" || lower ==
"light") {
125 if (lower ==
"1" || lower ==
"dark") {
128 if (lower ==
"2" || lower ==
"special") {
131 return absl::InvalidArgumentError(
132 absl::StrCat(
"Unknown world value: ", std::string(value)));
137 if (map_id < kDarkWorldOffset) {
140 if (map_id < kSpecialWorldOffset) {
155 return absl::StrCat(
"Unknown(", world,
")");
181 if (map ==
nullptr) {
182 return absl::InternalError(
183 absl::StrFormat(
"Failed to retrieve overworld map 0x%02X", map_id));
188 summary.
world = world;
195 summary.
area_size = AreaSizeToString(map->area_size());
209 for (
int i = 0; i < 3; ++i) {
214 for (
int i = 0; i < 4; ++i) {
215 summary.
area_music.push_back(map->area_music(i));
218 for (
int i = 0; i < 16; ++i) {
230 std::vector<WarpEntry> entries;
232 const auto& entrances = overworld.
entrances();
233 for (
const auto& entrance : entrances) {
236 entry.
deleted = entrance.deleted;
237 entry.
is_hole = entrance.is_hole_;
240 PopulateCommonWarpFields(entry, entrance.map_id_, entrance.map_pos_,
241 entrance.x_, entrance.y_);
243 if (query.
type.has_value() && *query.
type != entry.
type) {
253 entries.push_back(std::move(entry));
256 const auto& holes = overworld.
holes();
257 for (
const auto& hole : holes) {
264 PopulateCommonWarpFields(entry, hole.map_id_, hole.map_pos_, hole.x_,
267 if (query.
type.has_value() && *query.
type != entry.
type) {
277 entries.push_back(std::move(entry));
280 std::sort(entries.begin(), entries.end(),
282 if (a.world != b.world) {
283 return a.world < b.world;
286 return a.map_id < b.map_id;
289 return a.tile16_y < b.tile16_y;
292 return a.tile16_x < b.tile16_x;
294 return static_cast<int>(a.
type) <
static_cast<int>(b.
type);
310 return parent_map.
is_large_map() ? std::pair<int, int>{2, 2}
311 : std::pair<int, int>{1, 1};
316 int map_id,
int parent_map,
int area_columns,
int area_rows,
int x,
int y) {
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));
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 "
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));
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));
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 "
359 x, y, parent_map, width - 1, height - 1));
365 location.
world = world;
370 location.
screen_id = parent_map + (x / 32) + (y / 32) * 8;
373 location.
world_x = parent_column * 32 + x;
374 location.
world_y = parent_row * 32 + y;
383 if (map ==
nullptr) {
384 return absl::FailedPreconditionError(
385 absl::StrFormat(
"Overworld map 0x%02X is not loaded", map_id));
387 const int parent = map->parent();
390 if (parent_map ==
nullptr) {
391 return absl::FailedPreconditionError(
392 absl::StrFormat(
"Overworld map 0x%02X is not loaded", parent));
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));
407 if (map ==
nullptr) {
408 return absl::FailedPreconditionError(
409 absl::StrFormat(
"Overworld map 0x%02X is not loaded", screen_id));
411 const int parent = map->parent();
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;
425 location.
world_x >=
static_cast<int>(tiles.size()) ||
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");
439 return absl::OkStatus();
445 if (options.
map_id.has_value()) {
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));
455 if (options.
map_id.has_value() && options.
world.has_value()) {
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",
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()) {
470 worlds.push_back(inferred_world);
475 std::vector<TileMatch> matches;
477 for (
int world : worlds) {
494 return absl::InvalidArgumentError(
495 absl::StrFormat(
"Unknown world index: %d", world));
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) {
506 int map_x_index = local_map % 8;
507 int map_y_index = local_map / 8;
509 int global_x_start = map_x_index * 32;
510 int global_y_start = map_y_index * 32;
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;
517 uint16_t current_tile = overworld.
GetTile(global_x, global_y);
518 if (current_tile == tile_id) {
520 {map_id, world, local_x, local_y, global_x, global_y});
537 return "second_part";
544 std::vector<OverworldSprite> results;
547 for (
int game_state = 0; game_state < 3; ++game_state) {
548 if (query.
phase.has_value() && *query.
phase != game_state) {
551 const auto sprites = overworld.
sprites(game_state);
552 std::map<int, int> next_index_by_map;
554 for (
const auto& sprite : sprites) {
555 const int list_index = next_index_by_map[sprite.map_id()]++;
561 int map_id = sprite.map_id();
562 if (query.
map_id.has_value() && map_id != *query.
map_id) {
567 int world = (map_id >= kSpecialWorldOffset)
569 : (map_id >= kDarkWorldOffset ? 1 : 0);
571 if (query.
world.has_value() && world != *query.
world) {
579 entry.
x = sprite.x();
580 entry.
y = sprite.y();
581 entry.
phase = game_state;
583 entry.
local_x = (sprite.x() - (map_id % 8) * 512) / 16;
584 entry.
local_y = (sprite.y() - ((map_id % 64) / 8) * 512) / 16;
587 results.push_back(entry);
596 const auto& entrances = overworld.
entrances();
598 if (entrance_id >= entrances.size()) {
599 return absl::NotFoundError(absl::StrFormat(
600 "Entrance %d not found (max: %d)", entrance_id, entrances.size() - 1));
603 const auto& entrance = entrances[entrance_id];
607 details.
map_id = entrance.map_id_;
610 details.
world = (details.
map_id >= kSpecialWorldOffset)
612 : (details.
map_id >= kDarkWorldOffset ? 1 : 0);
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_;
635 stats.
count =
static_cast<int>(matches.size());
638 if (options.
map_id.has_value()) {
640 if (options.
world.has_value()) {
652 for (
const auto& match : matches) {
653 stats.
positions.emplace_back(match.local_x, match.local_y);
661 std::vector<int> worlds = {0, 1, 2};
662 if (query.
world.has_value()) {
664 return absl::InvalidArgumentError(
665 absl::StrFormat(
"Unknown world index: %d", *query.
world));
667 worlds = {*query.
world};
669 if (query.
map_id.has_value()) {
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",
676 worlds = {map_world};
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);
687 for (
const int world : worlds) {
689 const int world_screens = world == 2 ? 0x20 : 0x40;
690 std::vector<bool> seen_in_world(kTileIds,
false);
692 world_usage.
world = world;
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) {
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 "
710 screen_tiles.insert(screen_tiles.end(), grid[x].begin() + y0,
711 grid[x].begin() + y0 + kScreenTiles);
713 std::sort(screen_tiles.begin(), screen_tiles.end());
717 screen.
world = world;
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];
724 while (end < screen_tiles.size() && screen_tiles[end] == tile_id) {
727 const int run =
static_cast<int>(end - i);
731 counts[tile_id] += run;
732 ++screens_using[tile_id];
733 if (!seen_in_world[tile_id]) {
734 seen_in_world[tile_id] =
true;
748 summary.
screens.push_back(screen);
750 summary.
worlds.push_back(world_usage);
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]});
760 std::stable_sort(summary.
tiles.begin(), summary.
tiles.end(),
762 return a.count > b.count;
Represents a single Overworld map screen.
auto is_large_map() const
void set_current_world(int world)
const std::vector< OverworldEntrance > & holes() const
auto sprites(int state) const
auto overworld_map(int i) const
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
const std::vector< OverworldEntrance > & entrances() const
OverworldBlockset & GetMapTiles(int world_type)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
int WorldOffset(int world)
int NormalizeMapId(uint16_t raw_map_id)
constexpr int kLightWorldOffset
void PopulateCommonWarpFields(WarpEntry &entry, uint16_t raw_map_id, uint16_t map_pos, int pixel_x, int pixel_y)
std::string EntranceLabel(uint8_t id)
std::string AreaSizeToString(zelda3::AreaSizeEnum size)
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
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr const char * kEntranceNames[]
const char * ResolveSpriteName(uint16_t id)
#define RETURN_IF_ERROR(expr)
std::optional< std::string > entrance_name
uint16_t area_specific_bg_color
std::vector< uint8_t > sprite_graphics
std::vector< uint8_t > static_graphics
std::vector< uint8_t > sprite_palettes
std::vector< uint8_t > area_music
uint16_t subscreen_overlay
std::optional< std::string > sprite_name
std::optional< uint8_t > sprite_id
std::optional< int > world
std::optional< int > phase
std::optional< int > map_id
std::optional< int > world
std::optional< int > map_id
std::vector< std::pair< int, int > > positions
std::optional< uint16_t > tile_id
std::optional< int > map_id
std::optional< int > world
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< int > map_id
std::optional< WarpType > type
std::optional< int > world