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 <optional>
5#include <string>
6#include <vector>
7
8#include "absl/status/status.h"
9#include "absl/strings/ascii.h"
10#include "absl/strings/numbers.h"
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_format.h"
13#include "util/macro.h"
14#include "zelda3/common.h"
19
20namespace yaze {
21namespace cli {
22namespace overworld {
23
24namespace {
25
26constexpr int kLightWorldOffset = 0x00;
27constexpr int kDarkWorldOffset = 0x40;
28constexpr int kSpecialWorldOffset = 0x80;
29
30int NormalizeMapId(uint16_t raw_map_id) {
31 return static_cast<int>(raw_map_id & 0x00FF);
32}
33
34int WorldOffset(int world) {
35 switch (world) {
36 case 0:
37 return kLightWorldOffset;
38 case 1:
39 return kDarkWorldOffset;
40 case 2:
42 default:
43 return 0;
44 }
45}
46
47absl::Status ValidateMapId(int map_id) {
48 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
49 return absl::InvalidArgumentError(
50 absl::StrFormat("Map ID out of range: 0x%02X", map_id));
51 }
52 return absl::OkStatus();
53}
54
56 switch (size) {
58 return "Small";
60 return "Large";
62 return "Wide";
64 return "Tall";
65 default:
66 return "Unknown";
67 }
68}
69
70std::string EntranceLabel(uint8_t id) {
71 constexpr size_t kEntranceCount =
73 if (id < kEntranceCount) {
74 return zelda3::kEntranceNames[id];
75 }
76 return absl::StrFormat("Entrance %d", id);
77}
78
79void PopulateCommonWarpFields(WarpEntry& entry, uint16_t raw_map_id,
80 uint16_t map_pos, int pixel_x, int pixel_y) {
81 entry.raw_map_id = raw_map_id;
82 entry.map_id = NormalizeMapId(raw_map_id);
83 if (entry.map_id >= zelda3::kNumOverworldMaps) {
84 // Some ROM hacks use sentinel values. Clamp to valid range for reporting
86 }
87 entry.world = (entry.map_id >= kSpecialWorldOffset)
88 ? 2
89 : (entry.map_id >= kDarkWorldOffset ? 1 : 0);
90 entry.local_index = entry.map_id - WorldOffset(entry.world);
91 entry.map_x = entry.local_index % 8;
92 entry.map_y = entry.local_index / 8;
93 entry.map_pos = map_pos;
94 entry.pixel_x = pixel_x;
95 entry.pixel_y = pixel_y;
96 int tile_index = static_cast<int>(map_pos >> 1);
97 entry.tile16_x = tile_index & 0x3F;
98 entry.tile16_y = tile_index >> 6;
99}
100
101} // namespace
102
103absl::StatusOr<int> ParseNumeric(std::string_view value, int base) {
104 try {
105 size_t processed = 0;
106 int result = std::stoi(std::string(value), &processed, base);
107 if (processed != value.size()) {
108 return absl::InvalidArgumentError(
109 absl::StrCat("Invalid numeric value: ", std::string(value)));
110 }
111 return result;
112 } catch (const std::exception&) {
113 return absl::InvalidArgumentError(
114 absl::StrCat("Invalid numeric value: ", std::string(value)));
115 }
116}
117
118absl::StatusOr<int> ParseWorldSpecifier(std::string_view value) {
119 std::string lower = absl::AsciiStrToLower(std::string(value));
120 if (lower == "0" || lower == "light") {
121 return 0;
122 }
123 if (lower == "1" || lower == "dark") {
124 return 1;
125 }
126 if (lower == "2" || lower == "special") {
127 return 2;
128 }
129 return absl::InvalidArgumentError(
130 absl::StrCat("Unknown world value: ", std::string(value)));
131}
132
133absl::StatusOr<int> InferWorldFromMapId(int map_id) {
134 RETURN_IF_ERROR(ValidateMapId(map_id));
135 if (map_id < kDarkWorldOffset) {
136 return 0;
137 }
138 if (map_id < kSpecialWorldOffset) {
139 return 1;
140 }
141 return 2;
142}
143
144std::string WorldName(int world) {
145 switch (world) {
146 case 0:
147 return "Light";
148 case 1:
149 return "Dark";
150 case 2:
151 return "Special";
152 default:
153 return absl::StrCat("Unknown(", world, ")");
154 }
155}
156
157std::string WarpTypeName(WarpType type) {
158 switch (type) {
160 return "entrance";
161 case WarpType::kHole:
162 return "hole";
163 case WarpType::kExit:
164 return "exit";
165 default:
166 return "unknown";
167 }
168}
169
170absl::StatusOr<MapSummary> BuildMapSummary(zelda3::Overworld& overworld,
171 int map_id) {
172 RETURN_IF_ERROR(ValidateMapId(map_id));
173 ASSIGN_OR_RETURN(int world, InferWorldFromMapId(map_id));
174
175 // Ensure map data is built before accessing metadata.
176 RETURN_IF_ERROR(overworld.EnsureMapBuilt(map_id));
177
178 const auto* map = overworld.overworld_map(map_id);
179 if (map == nullptr) {
180 return absl::InternalError(
181 absl::StrFormat("Failed to retrieve overworld map 0x%02X", map_id));
182 }
183
184 MapSummary summary;
185 summary.map_id = map_id;
186 summary.world = world;
187 summary.local_index = map_id - WorldOffset(world);
188 summary.map_x = summary.local_index % 8;
189 summary.map_y = summary.local_index / 8;
190 summary.is_large_map = map->is_large_map();
191 summary.parent_map = map->parent();
192 summary.large_quadrant = map->large_index();
193 summary.area_size = AreaSizeToString(map->area_size());
194 summary.message_id = map->message_id();
195 summary.area_graphics = map->area_graphics();
196 summary.area_palette = map->area_palette();
197 summary.main_palette = map->main_palette();
198 summary.animated_gfx = map->animated_gfx();
199 summary.subscreen_overlay = map->subscreen_overlay();
200 summary.area_specific_bg_color = map->area_specific_bg_color();
201
202 summary.sprite_graphics.clear();
203 summary.sprite_palettes.clear();
204 summary.area_music.clear();
205 summary.static_graphics.clear();
206
207 for (int i = 0; i < 3; ++i) {
208 summary.sprite_graphics.push_back(map->sprite_graphics(i));
209 summary.sprite_palettes.push_back(map->sprite_palette(i));
210 }
211
212 for (int i = 0; i < 4; ++i) {
213 summary.area_music.push_back(map->area_music(i));
214 }
215
216 for (int i = 0; i < 16; ++i) {
217 summary.static_graphics.push_back(map->static_graphics(i));
218 }
219
220 summary.has_overlay = map->has_overlay();
221 summary.overlay_id = map->overlay_id();
222
223 return summary;
224}
225
226absl::StatusOr<std::vector<WarpEntry>> CollectWarpEntries(
227 const zelda3::Overworld& overworld, const WarpQuery& query) {
228 std::vector<WarpEntry> entries;
229
230 const auto& entrances = overworld.entrances();
231 for (const auto& entrance : entrances) {
232 WarpEntry entry;
234 entry.deleted = entrance.deleted;
235 entry.is_hole = entrance.is_hole_;
236 entry.entrance_id = entrance.entrance_id_;
237 entry.entrance_name = EntranceLabel(entrance.entrance_id_);
238 PopulateCommonWarpFields(entry, entrance.map_id_, entrance.map_pos_,
239 entrance.x_, entrance.y_);
240
241 if (query.type.has_value() && *query.type != entry.type) {
242 continue;
243 }
244 if (query.world.has_value() && *query.world != entry.world) {
245 continue;
246 }
247 if (query.map_id.has_value() && *query.map_id != entry.map_id) {
248 continue;
249 }
250
251 entries.push_back(std::move(entry));
252 }
253
254 const auto& holes = overworld.holes();
255 for (const auto& hole : holes) {
256 WarpEntry entry;
257 entry.type = WarpType::kHole;
258 entry.deleted = false;
259 entry.is_hole = true;
260 entry.entrance_id = hole.entrance_id_;
261 entry.entrance_name = EntranceLabel(hole.entrance_id_);
262 PopulateCommonWarpFields(entry, hole.map_id_, hole.map_pos_, hole.x_,
263 hole.y_);
264
265 if (query.type.has_value() && *query.type != entry.type) {
266 continue;
267 }
268 if (query.world.has_value() && *query.world != entry.world) {
269 continue;
270 }
271 if (query.map_id.has_value() && *query.map_id != entry.map_id) {
272 continue;
273 }
274
275 entries.push_back(std::move(entry));
276 }
277
278 std::sort(entries.begin(), entries.end(),
279 [](const WarpEntry& a, const WarpEntry& b) {
280 if (a.world != b.world) {
281 return a.world < b.world;
282 }
283 if (a.map_id != b.map_id) {
284 return a.map_id < b.map_id;
285 }
286 if (a.tile16_y != b.tile16_y) {
287 return a.tile16_y < b.tile16_y;
288 }
289 if (a.tile16_x != b.tile16_x) {
290 return a.tile16_x < b.tile16_x;
291 }
292 return static_cast<int>(a.type) < static_cast<int>(b.type);
293 });
294
295 return entries;
296}
297
298absl::StatusOr<std::vector<TileMatch>> FindTileMatches(
299 zelda3::Overworld& overworld, uint16_t tile_id,
300 const TileSearchOptions& options) {
301 if (options.map_id.has_value()) {
302 RETURN_IF_ERROR(ValidateMapId(*options.map_id));
303 }
304 if (options.world.has_value()) {
305 if (*options.world < 0 || *options.world > 2) {
306 return absl::InvalidArgumentError(
307 absl::StrFormat("Unknown world index: %d", *options.world));
308 }
309 }
310
311 if (options.map_id.has_value() && options.world.has_value()) {
312 ASSIGN_OR_RETURN(int inferred_world, InferWorldFromMapId(*options.map_id));
313 if (inferred_world != *options.world) {
314 return absl::InvalidArgumentError(absl::StrFormat(
315 "Map 0x%02X belongs to the %s World but --world requested %s",
316 *options.map_id, WorldName(inferred_world),
317 WorldName(*options.world)));
318 }
319 }
320
321 std::vector<int> worlds;
322 if (options.world.has_value()) {
323 worlds.push_back(*options.world);
324 } else if (options.map_id.has_value()) {
325 ASSIGN_OR_RETURN(int inferred_world, InferWorldFromMapId(*options.map_id));
326 worlds.push_back(inferred_world);
327 } else {
328 worlds = {0, 1, 2};
329 }
330
331 std::vector<TileMatch> matches;
332
333 for (int world : worlds) {
334 int world_start = 0;
335 int world_maps = 0;
336 switch (world) {
337 case 0:
338 world_start = 0x00;
339 world_maps = 0x40;
340 break;
341 case 1:
342 world_start = 0x40;
343 world_maps = 0x40;
344 break;
345 case 2:
346 world_start = 0x80;
347 world_maps = 0x20;
348 break;
349 default:
350 return absl::InvalidArgumentError(
351 absl::StrFormat("Unknown world index: %d", world));
352 }
353
354 overworld.set_current_world(world);
355
356 for (int local_map = 0; local_map < world_maps; ++local_map) {
357 int map_id = world_start + local_map;
358 if (options.map_id.has_value() && map_id != *options.map_id) {
359 continue;
360 }
361
362 int map_x_index = local_map % 8;
363 int map_y_index = local_map / 8;
364
365 int global_x_start = map_x_index * 32;
366 int global_y_start = map_y_index * 32;
367
368 for (int local_y = 0; local_y < 32; ++local_y) {
369 for (int local_x = 0; local_x < 32; ++local_x) {
370 int global_x = global_x_start + local_x;
371 int global_y = global_y_start + local_y;
372
373 uint16_t current_tile = overworld.GetTile(global_x, global_y);
374 if (current_tile == tile_id) {
375 matches.push_back(
376 {map_id, world, local_x, local_y, global_x, global_y});
377 }
378 }
379 }
380 }
381 }
382
383 return matches;
384}
385
386absl::StatusOr<std::vector<OverworldSprite>> CollectOverworldSprites(
387 const zelda3::Overworld& overworld, const SpriteQuery& query) {
388 std::vector<OverworldSprite> results;
389
390 // Iterate through all 3 game states (beginning, zelda, agahnim)
391 for (int game_state = 0; game_state < 3; ++game_state) {
392 const auto& sprites = overworld.sprites(game_state);
393
394 for (const auto& sprite : sprites) {
395 // Apply filters
396 if (query.sprite_id.has_value() && sprite.id() != *query.sprite_id) {
397 continue;
398 }
399
400 int map_id = sprite.map_id();
401 if (query.map_id.has_value() && map_id != *query.map_id) {
402 continue;
403 }
404
405 // Determine world from map_id
406 int world = (map_id >= kSpecialWorldOffset)
407 ? 2
408 : (map_id >= kDarkWorldOffset ? 1 : 0);
409
410 if (query.world.has_value() && world != *query.world) {
411 continue;
412 }
413
414 OverworldSprite entry;
415 entry.sprite_id = sprite.id();
416 entry.map_id = map_id;
417 entry.world = world;
418 entry.x = sprite.x();
419 entry.y = sprite.y();
420 // Sprite names would come from a label system if available
421 // entry.sprite_name = GetSpriteName(sprite.id());
422
423 results.push_back(entry);
424 }
425 }
426
427 return results;
428}
429
430absl::StatusOr<EntranceDetails> GetEntranceDetails(
431 const zelda3::Overworld& overworld, uint8_t entrance_id) {
432 const auto& entrances = overworld.entrances();
433
434 if (entrance_id >= entrances.size()) {
435 return absl::NotFoundError(absl::StrFormat(
436 "Entrance %d not found (max: %d)", entrance_id, entrances.size() - 1));
437 }
438
439 const auto& entrance = entrances[entrance_id];
440
441 EntranceDetails details;
442 details.entrance_id = entrance_id;
443 details.map_id = entrance.map_id_;
444
445 // Determine world from map_id
446 details.world = (details.map_id >= kSpecialWorldOffset)
447 ? 2
448 : (details.map_id >= kDarkWorldOffset ? 1 : 0);
449
450 details.x = entrance.x_;
451 details.y = entrance.y_;
452 details.area_x = entrance.game_x_;
453 details.area_y = entrance.game_y_;
454 details.map_pos = entrance.map_pos_;
455 details.is_hole = entrance.is_hole_;
456
457 // Get entrance name if available
458 details.entrance_name = EntranceLabel(entrance_id);
459
460 return details;
461}
462
463absl::StatusOr<TileStatistics> AnalyzeTileUsage(
464 zelda3::Overworld& overworld, uint16_t tile_id,
465 const TileSearchOptions& options) {
466 // Use FindTileMatches to get all occurrences
467 ASSIGN_OR_RETURN(auto matches, FindTileMatches(overworld, tile_id, options));
468
469 TileStatistics stats;
470 stats.tile_id = tile_id;
471 stats.count = static_cast<int>(matches.size());
472
473 // If scoped to a specific map, store that info
474 if (options.map_id.has_value()) {
475 stats.map_id = *options.map_id;
476 if (options.world.has_value()) {
477 stats.world = *options.world;
478 } else {
480 }
481 } else {
482 stats.map_id = -1; // Indicates all maps
483 stats.world = -1;
484 }
485
486 // Store positions (convert from TileMatch to pair)
487 stats.positions.reserve(matches.size());
488 for (const auto& match : matches) {
489 stats.positions.emplace_back(match.local_x, match.local_y);
490 }
491
492 return stats;
493}
494
495} // namespace overworld
496} // namespace cli
497} // namespace yaze
Represents the full Overworld data, light and dark world.
Definition overworld.h:217
void set_current_world(int world)
Definition overworld.h:535
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:507
auto sprites(int state) const
Definition overworld.h:490
auto overworld_map(int i) const
Definition overworld.h:473
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
Definition overworld.cc:888
uint16_t GetTile(int x, int y) const
Definition overworld.h:536
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:502
#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< int > InferWorldFromMapId(int map_id)
absl::StatusOr< MapSummary > BuildMapSummary(zelda3::Overworld &overworld, int map_id)
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< 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< std::vector< TileMatch > > FindTileMatches(zelda3::Overworld &overworld, uint16_t tile_id, const TileSearchOptions &options)
std::string WarpTypeName(WarpType type)
std::string WorldName(int world)
constexpr int kNumOverworldMaps
Definition common.h:85
AreaSizeEnum
Area size enumeration for v3+ ROMs.
constexpr const char * kEntranceNames[]
Definition common.h:91
#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< uint8_t > sprite_id
std::vector< std::pair< int, int > > positions
std::optional< std::string > entrance_name
std::optional< uint8_t > entrance_id
std::optional< WarpType > type