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 "app/zelda3/common.h"
18#include "util/macro.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(), [](const WarpEntry& a,
279 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,
313 InferWorldFromMapId(*options.map_id));
314 if (inferred_world != *options.world) {
315 return absl::InvalidArgumentError(
316 absl::StrFormat(
317 "Map 0x%02X belongs to the %s World but --world requested %s",
318 *options.map_id, WorldName(inferred_world),
319 WorldName(*options.world)));
320 }
321 }
322
323 std::vector<int> worlds;
324 if (options.world.has_value()) {
325 worlds.push_back(*options.world);
326 } else if (options.map_id.has_value()) {
327 ASSIGN_OR_RETURN(int inferred_world,
328 InferWorldFromMapId(*options.map_id));
329 worlds.push_back(inferred_world);
330 } else {
331 worlds = {0, 1, 2};
332 }
333
334 std::vector<TileMatch> matches;
335
336 for (int world : worlds) {
337 int world_start = 0;
338 int world_maps = 0;
339 switch (world) {
340 case 0:
341 world_start = 0x00;
342 world_maps = 0x40;
343 break;
344 case 1:
345 world_start = 0x40;
346 world_maps = 0x40;
347 break;
348 case 2:
349 world_start = 0x80;
350 world_maps = 0x20;
351 break;
352 default:
353 return absl::InvalidArgumentError(
354 absl::StrFormat("Unknown world index: %d", world));
355 }
356
357 overworld.set_current_world(world);
358
359 for (int local_map = 0; local_map < world_maps; ++local_map) {
360 int map_id = world_start + local_map;
361 if (options.map_id.has_value() && map_id != *options.map_id) {
362 continue;
363 }
364
365 int map_x_index = local_map % 8;
366 int map_y_index = local_map / 8;
367
368 int global_x_start = map_x_index * 32;
369 int global_y_start = map_y_index * 32;
370
371 for (int local_y = 0; local_y < 32; ++local_y) {
372 for (int local_x = 0; local_x < 32; ++local_x) {
373 int global_x = global_x_start + local_x;
374 int global_y = global_y_start + local_y;
375
376 uint16_t current_tile = overworld.GetTile(global_x, global_y);
377 if (current_tile == tile_id) {
378 matches.push_back({map_id, world, local_x, local_y, global_x,
379 global_y});
380 }
381 }
382 }
383 }
384 }
385
386 return matches;
387}
388
389absl::StatusOr<std::vector<OverworldSprite>> CollectOverworldSprites(
390 const zelda3::Overworld& overworld, const SpriteQuery& query) {
391 std::vector<OverworldSprite> results;
392
393 // Iterate through all 3 game states (beginning, zelda, agahnim)
394 for (int game_state = 0; game_state < 3; ++game_state) {
395 const auto& sprites = overworld.sprites(game_state);
396
397 for (const auto& sprite : sprites) {
398 // Apply filters
399 if (query.sprite_id.has_value() && sprite.id() != *query.sprite_id) {
400 continue;
401 }
402
403 int map_id = sprite.map_id();
404 if (query.map_id.has_value() && map_id != *query.map_id) {
405 continue;
406 }
407
408 // Determine world from map_id
409 int world = (map_id >= kSpecialWorldOffset) ? 2
410 : (map_id >= kDarkWorldOffset ? 1 : 0);
411
412 if (query.world.has_value() && world != *query.world) {
413 continue;
414 }
415
416 OverworldSprite entry;
417 entry.sprite_id = sprite.id();
418 entry.map_id = map_id;
419 entry.world = world;
420 entry.x = sprite.x();
421 entry.y = sprite.y();
422 // Sprite names would come from a label system if available
423 // entry.sprite_name = GetSpriteName(sprite.id());
424
425 results.push_back(entry);
426 }
427 }
428
429 return results;
430}
431
432absl::StatusOr<EntranceDetails> GetEntranceDetails(
433 const zelda3::Overworld& overworld, uint8_t entrance_id) {
434 const auto& entrances = overworld.entrances();
435
436 if (entrance_id >= entrances.size()) {
437 return absl::NotFoundError(
438 absl::StrFormat("Entrance %d not found (max: %d)",
439 entrance_id, entrances.size() - 1));
440 }
441
442 const auto& entrance = entrances[entrance_id];
443
444 EntranceDetails details;
445 details.entrance_id = entrance_id;
446 details.map_id = entrance.map_id_;
447
448 // Determine world from map_id
449 details.world = (details.map_id >= kSpecialWorldOffset) ? 2
450 : (details.map_id >= kDarkWorldOffset ? 1 : 0);
451
452 details.x = entrance.x_;
453 details.y = entrance.y_;
454 details.area_x = entrance.area_x_;
455 details.area_y = entrance.area_y_;
456 details.map_pos = entrance.map_pos_;
457 details.is_hole = entrance.is_hole_;
458
459 // Get entrance name if available
460 details.entrance_name = EntranceLabel(entrance_id);
461
462 return details;
463}
464
465absl::StatusOr<TileStatistics> AnalyzeTileUsage(
466 zelda3::Overworld& overworld, uint16_t tile_id,
467 const TileSearchOptions& options) {
468
469 // Use FindTileMatches to get all occurrences
470 ASSIGN_OR_RETURN(auto matches, FindTileMatches(overworld, tile_id, options));
471
472 TileStatistics stats;
473 stats.tile_id = tile_id;
474 stats.count = static_cast<int>(matches.size());
475
476 // If scoped to a specific map, store that info
477 if (options.map_id.has_value()) {
478 stats.map_id = *options.map_id;
479 if (options.world.has_value()) {
480 stats.world = *options.world;
481 } else {
483 }
484 } else {
485 stats.map_id = -1; // Indicates all maps
486 stats.world = -1;
487 }
488
489 // Store positions (convert from TileMatch to pair)
490 stats.positions.reserve(matches.size());
491 for (const auto& match : matches) {
492 stats.positions.emplace_back(match.local_x, match.local_y);
493 }
494
495 return stats;
496}
497
498} // namespace overworld
499} // namespace cli
500} // namespace yaze
Represents the full Overworld data, light and dark world.
Definition overworld.h:135
void set_current_world(int world)
Definition overworld.h:292
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:273
auto sprites(int state) const
Definition overworld.h:265
auto overworld_map(int i) const
Definition overworld.h:258
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
Definition overworld.cc:656
uint16_t GetTile(int x, int y) const
Definition overworld.h:293
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:270
#define RETURN_IF_ERROR(expression)
Definition macro.h:53
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:61
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 overworld.h:119
constexpr const char * kEntranceNames[]
Definition common.h:47
Main namespace for the application.
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