yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <optional>
6#include <string>
7#include <vector>
8
9#include "absl/strings/numbers.h"
10#include "absl/strings/str_format.h"
13#include "cli/util/hex_util.h"
15#include "rom/rom.h"
16#include "rom/transaction.h"
17#include "util/macro.h"
22
23namespace yaze {
24namespace cli {
25namespace handlers {
26
28
29namespace {
30
31absl::Status ValidateMapId(int map_id) {
32 if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) {
33 return absl::InvalidArgumentError(
34 absl::StrFormat("Map ID out of range: 0x%02X", map_id));
35 }
36 return absl::OkStatus();
37}
38
39// IDs (map, screen, tile) are hex like every other overworld command;
40// coordinates are decimal with an optional 0x prefix (ArgumentParser::GetInt),
41// matching the dungeon and overworld-sprite editing commands.
42absl::StatusOr<int> ParseMapIdArg(const resources::ArgumentParser& parser,
43 const std::string& name) {
44 const auto value = parser.GetString(name);
45 int map_id = 0;
46 if (!value.has_value() || !ParseHexString(*value, &map_id)) {
47 return absl::InvalidArgumentError(
48 absl::StrFormat("--%s must be a hex map id (e.g. 0x40 or 40)", name));
49 }
51 return map_id;
52}
53
54absl::StatusOr<int> ParseTileIdArg(const resources::ArgumentParser& parser) {
55 const auto value = parser.GetString("tile");
56 int tile_id = 0;
57 if (!value.has_value() || !ParseHexString(*value, &tile_id)) {
58 return absl::InvalidArgumentError(
59 "--tile must be a hex tile16 id (e.g. 0x0255)");
60 }
61 if (tile_id < 0 || tile_id > 0xFFFF) {
62 return absl::InvalidArgumentError(
63 absl::StrFormat("Tile ID out of range: 0x%X", tile_id));
64 }
65 return tile_id;
66}
67
68absl::StatusOr<int> ParseCoordinateArg(const resources::ArgumentParser& parser,
69 const std::string& name) {
70 auto value = parser.GetInt(name);
71 if (!value.ok()) {
72 return absl::InvalidArgumentError(absl::StrFormat(
73 "--%s must be a decimal tile coordinate (or 0x-prefixed hex): %s", name,
74 value.status().message()));
75 }
76 return *value;
77}
78
80 const overworld::AreaTileLocation& location) {
81 formatter.AddHexField("map_id", location.map_id, 2);
82 formatter.AddField("world", overworld::WorldName(location.world));
83 formatter.AddHexField("parent_area", location.parent_map, 2);
84 formatter.AddField("area_tiles", absl::StrFormat("%dx%d", location.area_width,
85 location.area_height));
86 formatter.AddField("x", location.area_x);
87 formatter.AddField("y", location.area_y);
88 formatter.AddHexField("screen", location.screen_id, 2);
89 formatter.AddField("screen_x", location.screen_x);
90 formatter.AddField("screen_y", location.screen_y);
91 formatter.AddField("world_x", location.world_x);
92 formatter.AddField("world_y", location.world_y);
93}
94
95// Mirrors OverworldEditor::Save's map path: rebuild the tile32 table from the
96// edited tile16 grid, then write tile32 definitions (vanilla or expanded
97// layout) and the compressed screens. Tile16 definitions are untouched.
98absl::Status SaveOverworldTileGrid(Rom& rom, zelda3::Overworld& overworld) {
99 const auto profile = zelda3::DetectOverworldRomProfile(rom);
101 if (profile.has_expanded_tile32) {
103 } else {
104 RETURN_IF_ERROR(overworld.SaveMap32Tiles());
105 }
106 return overworld.SaveOverworldMaps();
107}
108
109// Reload the saved bytes and require every world's tile16 grid to match the
110// edited grid exactly.
111absl::Status VerifyTileGridRoundTrip(const Rom& rom,
112 const zelda3::Overworld& expected) {
113 Rom reloaded;
114 Rom::LoadOptions options;
115 options.load_resource_labels = false;
116 RETURN_IF_ERROR(reloaded.LoadFromData(rom.vector(), options));
117 zelda3::Overworld actual(&reloaded);
118 RETURN_IF_ERROR(actual.Load(&reloaded));
119 const auto want = expected.map_tiles();
120 const auto got = actual.map_tiles();
121 const zelda3::OverworldBlockset* want_worlds[] = {
122 &want.light_world, &want.dark_world, &want.special_world};
123 const zelda3::OverworldBlockset* got_worlds[] = {
124 &got.light_world, &got.dark_world, &got.special_world};
125 for (int world = 0; world < 3; ++world) {
126 const auto& w = *want_worlds[world];
127 const auto& g = *got_worlds[world];
128 if (w.size() != g.size()) {
129 return absl::DataLossError("Reloaded tile grid has a different size");
130 }
131 for (size_t x = 0; x < w.size(); ++x) {
132 if (w[x] == g[x]) {
133 continue;
134 }
135 for (size_t y = 0; y < w[x].size() && y < g[x].size(); ++y) {
136 if (w[x][y] != g[x][y]) {
137 return absl::DataLossError(absl::StrFormat(
138 "Saved overworld does not read back: %s World tile (%d,%d) is "
139 "0x%04X, expected 0x%04X",
140 overworld::WorldName(world), static_cast<int>(x),
141 static_cast<int>(y), g[x][y], w[x][y]));
142 }
143 }
144 return absl::DataLossError("Reloaded tile grid column size differs");
145 }
146 }
147 return absl::OkStatus();
148}
149
150// Dry-run and --write run the same save path on the in-memory ROM and verify
151// that a fresh load reads back the edited grid; only --write commits it.
152absl::Status ApplyTileEdit(Rom& rom, zelda3::Overworld& overworld,
153 const overworld::AreaTileLocation& location,
154 uint16_t tile_id, bool do_write, bool mock_rom,
155 resources::OutputFormatter& formatter) {
156 ASSIGN_OR_RETURN(const uint16_t before,
157 overworld::ReadAreaTile(overworld, location));
158 if (before == tile_id) {
159 formatter.AddField("write_status", "not-needed");
160 return absl::OkStatus();
161 }
162
163 const std::vector<uint8_t> before_rom = rom.vector();
164 ScopedRomTransaction transaction(rom);
165 RETURN_IF_ERROR(overworld::WriteAreaTile(overworld, location, tile_id));
166 RETURN_IF_ERROR(SaveOverworldTileGrid(rom, overworld));
168
169 // Every changed byte must fall inside the regions an overworld map save is
170 // documented to own (tile32 quadrants, tile16 table, compressed map banks
171 // and their pointer tables); anything else fails closed.
172 const auto save_ranges = overworld.GetProjectedWriteRanges();
173 const auto& after_rom = rom.vector();
174 int changed_bytes = 0;
175 int changed_ranges = 0;
176 int outside_bytes = 0;
177 size_t first_outside = 0;
178 bool in_range = false;
179 for (size_t i = 0; i < after_rom.size(); ++i) {
180 const bool differs =
181 i >= before_rom.size() || after_rom[i] != before_rom[i];
182 changed_bytes += differs ? 1 : 0;
183 changed_ranges += (differs && !in_range) ? 1 : 0;
184 in_range = differs;
185 if (!differs) {
186 continue;
187 }
188 bool owned = false;
189 for (const auto& [start, end] : save_ranges) {
190 owned |= i >= start && i < end;
191 }
192 if (!owned && outside_bytes++ == 0) {
193 first_outside = i;
194 }
195 }
196 formatter.AddField("changed_bytes", changed_bytes);
197 formatter.AddField("changed_ranges", changed_ranges);
198 formatter.AddField("bytes_outside_save_ranges", outside_bytes);
199 if (outside_bytes > 0) {
200 return absl::FailedPreconditionError(absl::StrFormat(
201 "Overworld save changed %d byte(s) outside its documented ranges, "
202 "first at PC $%06X; nothing was written",
203 outside_bytes, first_outside));
204 }
205 formatter.AddField("verified_grid", true);
206
207 if (!do_write) {
208 formatter.AddField("write_status", "dry-run");
209 return absl::OkStatus(); // the transaction restores the ROM
210 }
211 if (mock_rom) {
212 formatter.AddField("save_status", "mock-rom-skipped");
213 return absl::OkStatus();
214 }
215
216 Rom::SaveSettings save_settings;
217 save_settings.require_backup = true;
218 RETURN_IF_ERROR(rom.SaveToFile(save_settings));
219 transaction.Commit();
220 formatter.AddField("save_status", "saved");
221
222 // The file on disk must be exactly the verified image.
223 Rom reopened;
224 RETURN_IF_ERROR(reopened.LoadFromFile(rom.filename()));
225 if (reopened.vector() != rom.vector()) {
226 return absl::DataLossError(
227 "Saved ROM file differs from the verified image");
228 }
229 formatter.AddField("verified_reload", true);
230 return absl::OkStatus();
231}
232
233} // namespace
234
236 Rom* rom, const resources::ArgumentParser& parser,
237 resources::OutputFormatter& formatter) {
238 ASSIGN_OR_RETURN(const int map_id, ParseMapIdArg(parser, "map"));
239 zelda3::Overworld overworld(rom);
240 RETURN_IF_ERROR(overworld.Load(rom));
241 const auto inspection =
242 zelda3::InspectSpecialEntrances(*rom, overworld, map_id);
243 formatter.BeginObject("Special Entrances");
244 formatter.AddField("available", inspection.available);
245 formatter.AddField("provenance", inspection.provenance);
246 formatter.AddField("limitation", inspection.limitation);
247 formatter.AddField("overlay_may_replace_tiles",
248 inspection.overlay_may_replace_tiles);
249 formatter.AddHexField("map_id", map_id, 2);
250 formatter.AddHexField("parent_area", inspection.parent_area, 2);
251 formatter.AddField("match_count",
252 static_cast<int>(inspection.matches.size()));
253 formatter.BeginArray("matches");
254 for (const auto& match : inspection.matches) {
255 const auto& row = inspection.rows[match.row_index];
256 formatter.BeginObject();
257 formatter.AddHexField("screen_id", match.screen_id, 2);
258 formatter.AddHexField("source_area", match.parent_area, 2);
259 formatter.AddField("tile_x", match.tile_x);
260 formatter.AddField("tile_y", match.tile_y);
261 formatter.AddField("world_x", match.world_x);
262 formatter.AddField("world_y", match.world_y);
263 formatter.AddHexField("map16_id", match.tile16_id, 3);
264 formatter.AddHexField("first_word", match.first_word, 4);
265 formatter.AddHexField("masked_trigger", match.masked_trigger, 3);
266 formatter.AddHexField("destination_marker", row.destination_marker, 4);
267 formatter.AddHexField("arrival_direction", row.direction, 4);
268 if (row.destination_area >= 0) {
269 formatter.AddHexField("destination_area", row.destination_area, 2);
270 } else {
271 formatter.AddField("destination_area", "unresolved");
272 }
273 formatter.EndObject();
274 }
275 formatter.EndArray();
276 formatter.EndObject();
277 return absl::OkStatus();
278}
279
281 Rom* rom, const resources::ArgumentParser& parser,
282 resources::OutputFormatter& formatter) {
283 ASSIGN_OR_RETURN(const int map_id, ParseMapIdArg(parser, "map"));
284 ASSIGN_OR_RETURN(const int x, ParseCoordinateArg(parser, "x"));
285 ASSIGN_OR_RETURN(const int y, ParseCoordinateArg(parser, "y"));
286
287 zelda3::Overworld overworld(rom);
288 RETURN_IF_ERROR(overworld.Load(rom));
289 ASSIGN_OR_RETURN(const auto location,
290 overworld::ResolveAreaTileForMap(overworld, map_id, x, y));
291 ASSIGN_OR_RETURN(const uint16_t tile,
292 overworld::ReadAreaTile(overworld, location));
293
294 formatter.BeginObject("Overworld Tile");
295 AddTileLocationFields(formatter, location);
296 formatter.AddHexField("tile_id", tile, 4);
297 formatter.EndObject();
298 return absl::OkStatus();
299}
300
302 Rom* rom, const resources::ArgumentParser& parser,
303 resources::OutputFormatter& formatter) {
304 resources::CommandInvocationContext invocation_context;
305 if (rom != nullptr && !rom->filename().empty()) {
306 invocation_context.source_rom_path = std::filesystem::path(rom->filename());
307 invocation_context.active_rom_path = std::filesystem::path(rom->filename());
308 }
309 return ExecuteWithContext(rom, parser, formatter, invocation_context);
310}
311
313 Rom* rom, const resources::ArgumentParser& parser,
315 const resources::CommandInvocationContext& invocation_context) {
316 ASSIGN_OR_RETURN(const int map_id, ParseMapIdArg(parser, "map"));
317 ASSIGN_OR_RETURN(const int x, ParseCoordinateArg(parser, "x"));
318 ASSIGN_OR_RETURN(const int y, ParseCoordinateArg(parser, "y"));
319 ASSIGN_OR_RETURN(const int tile_value, ParseTileIdArg(parser));
320 const bool do_write = parser.HasFlag("write");
321 const bool mock_rom = parser.HasFlag("mock-rom");
322 if (rom == nullptr || !rom->is_loaded()) {
323 return absl::FailedPreconditionError("ROM not loaded");
324 }
325
326 if (auto path = parser.GetString("manifest"); path.has_value()) {
327 core::HackManifest manifest;
328 RETURN_IF_ERROR(manifest.LoadFromFile(*path));
330 }
331
332 // Same guard as the overworld sprite editors: never write a ROM that lives
333 // in an Oracle of Secrets checkout unless the caller opts in explicitly.
334 const auto active_path = invocation_context.active_rom_path.value_or(
335 std::filesystem::path(rom->filename()));
336 if (do_write && !mock_rom && !parser.HasFlag("allow-project-rom") &&
337 IsOracleProjectRomPath(active_path)) {
338 return absl::PermissionDeniedError(absl::StrFormat(
339 "Refusing --write to %s: it is inside an Oracle of Secrets checkout "
340 "(Oracle_main.asm next to its Roms/ folder). Copy the ROM (with "
341 "Data/dungeons/custom_collision.json) and write the copy, or pass "
342 "--allow-project-rom deliberately.",
343 active_path.string()));
344 }
345
346 // Preview through the real serializer on an independent image. This keeps
347 // dry runs usable when the active project ROM has a read-only command scope.
348 std::optional<Rom> preview_rom;
349 if (!do_write) {
350 preview_rom.emplace(*rom);
351 rom = &*preview_rom;
352 }
353 zelda3::Overworld overworld(rom);
354 RETURN_IF_ERROR(overworld.Load(rom));
355 ASSIGN_OR_RETURN(const auto location,
356 overworld::ResolveAreaTileForMap(overworld, map_id, x, y));
357 const int tile16_count = static_cast<int>(overworld.tiles16().size());
358 if (tile_value >= tile16_count) {
359 return absl::OutOfRangeError(absl::StrFormat(
360 "Tile ID 0x%04X is past the last tile16 definition (0x%04X)",
361 tile_value, tile16_count - 1));
362 }
363 ASSIGN_OR_RETURN(const uint16_t before,
364 overworld::ReadAreaTile(overworld, location));
365
366 formatter.BeginObject("Overworld Tile Write");
367 formatter.AddField("mode", do_write ? "write" : "dry-run");
368 AddTileLocationFields(formatter, location);
369 formatter.AddHexField("tile_before", before, 4);
370 formatter.AddHexField("tile_after", tile_value, 4);
371 const absl::Status status = ApplyTileEdit(*rom, overworld, location,
372 static_cast<uint16_t>(tile_value),
373 do_write, mock_rom, formatter);
374 if (!status.ok()) {
375 formatter.AddField("error", std::string(status.message()));
376 }
377 formatter.EndObject();
378 return status;
379}
380
382 Rom* rom, const resources::ArgumentParser& parser,
383 resources::OutputFormatter& formatter) {
384 ASSIGN_OR_RETURN(const int tile_id, ParseTileIdArg(parser));
385
387 if (parser.GetString("map").has_value()) {
388 ASSIGN_OR_RETURN(const int map_id, ParseMapIdArg(parser, "map"));
389 options.map_id = map_id;
390 }
391 if (auto world = parser.GetString("world"); world.has_value()) {
392 ASSIGN_OR_RETURN(const int world_id,
394 options.world = world_id;
395 }
396
397 // Load the Overworld from ROM
398 zelda3::Overworld overworld(rom);
399 auto ow_status = overworld.Load(rom);
400 if (!ow_status.ok()) {
401 return ow_status;
402 }
403
404 // Call the helper function to find tile matches
405 auto matches_or = overworld::FindTileMatches(
406 overworld, static_cast<uint16_t>(tile_id), options);
407 if (!matches_or.ok()) {
408 return matches_or.status();
409 }
410 const auto& matches = matches_or.value();
411
412 // Format the output
413 formatter.BeginObject("Overworld Tile Search");
414 formatter.AddField("tile_id", absl::StrFormat("0x%04X", tile_id));
415 formatter.AddField("matches_found", static_cast<int>(matches.size()));
416 formatter.AddField("coordinates",
417 "x/y are area-relative (overworld-get-tile/set-tile "
418 "input); local_x/local_y are within map_id; "
419 "global_x/global_y index the world grid");
420
421 formatter.BeginArray("matches");
422 for (const auto& match : matches) {
423 formatter.BeginObject();
424 formatter.AddField("map_id", absl::StrFormat("0x%02X", match.map_id));
425 formatter.AddField("world", overworld::WorldName(match.world));
426 if (auto area = overworld::LocateScreenTile(overworld, match.map_id,
427 match.local_x, match.local_y);
428 area.ok()) {
429 formatter.AddField("parent_area",
430 absl::StrFormat("0x%02X", area->parent_map));
431 formatter.AddField("x", area->area_x);
432 formatter.AddField("y", area->area_y);
433 }
434 formatter.AddField("local_x", match.local_x);
435 formatter.AddField("local_y", match.local_y);
436 formatter.AddField("global_x", match.global_x);
437 formatter.AddField("global_y", match.global_y);
438 formatter.EndObject();
439 }
440 formatter.EndArray();
441 formatter.EndObject();
442
443 return absl::OkStatus();
444}
445
447 Rom* rom, const resources::ArgumentParser& parser,
448 resources::OutputFormatter& formatter) {
449 ASSIGN_OR_RETURN(const int screen_id, ParseMapIdArg(parser, "screen"));
450
451 // Load the Overworld from ROM
452 zelda3::Overworld overworld(rom);
453 auto ow_status = overworld.Load(rom);
454 if (!ow_status.ok()) {
455 return ow_status;
456 }
457
458 // Call the helper function to build the map summary
459 auto summary_or = overworld::BuildMapSummary(overworld, screen_id);
460 if (!summary_or.ok()) {
461 return summary_or.status();
462 }
463 const auto& summary = summary_or.value();
464
465 // Format the output using OutputFormatter
466 formatter.AddField("screen_id", absl::StrFormat("0x%02X", summary.map_id));
467 formatter.AddField("world", overworld::WorldName(summary.world));
468
469 formatter.BeginObject("grid");
470 formatter.AddField("x", summary.map_x);
471 formatter.AddField("y", summary.map_y);
472 formatter.AddField("local_index", summary.local_index);
473 formatter.EndObject();
474
475 formatter.BeginObject("size");
476 formatter.AddField("label", summary.area_size);
477 formatter.AddField("is_large", summary.is_large_map);
478 formatter.AddField("parent", absl::StrFormat("0x%02X", summary.parent_map));
479 formatter.AddField("quadrant", summary.large_quadrant);
480 // Coordinate bounds for overworld-get-tile/set-tile on this area.
481 if (const auto* parent = overworld.overworld_map(summary.parent_map);
482 parent != nullptr) {
483 const auto [columns, rows] = overworld::AreaScreenSpan(*parent);
484 formatter.AddField("area_tiles",
485 absl::StrFormat("%dx%d", columns * 32, rows * 32));
486 }
487 formatter.EndObject();
488
489 formatter.AddField("message_id",
490 absl::StrFormat("0x%04X", summary.message_id));
491 formatter.AddField("area_graphics",
492 absl::StrFormat("0x%02X", summary.area_graphics));
493 formatter.AddField("area_palette",
494 absl::StrFormat("0x%02X", summary.area_palette));
495 formatter.AddField("main_palette",
496 absl::StrFormat("0x%02X", summary.main_palette));
497 formatter.AddField("animated_gfx",
498 absl::StrFormat("0x%02X", summary.animated_gfx));
499 formatter.AddField("subscreen_overlay",
500 absl::StrFormat("0x%04X", summary.subscreen_overlay));
501 formatter.AddField("area_specific_bg_color",
502 absl::StrFormat("0x%04X", summary.area_specific_bg_color));
503
504 // Entrances
505 formatter.BeginArray("entrances");
506 auto entrances_or = zelda3::LoadEntrances(rom);
507 if (entrances_or.ok()) {
508 for (const auto& entrance : entrances_or.value()) {
509 if (entrance.map_id_ == screen_id) {
510 formatter.BeginObject();
511 formatter.AddField("entrance_id", entrance.entrance_id_);
512 formatter.AddField("x", entrance.x_);
513 formatter.AddField("y", entrance.y_);
514 formatter.AddField("is_hole", false);
515 formatter.EndObject();
516 }
517 }
518 }
519 // Holes (as entrances)
520 auto holes_or = zelda3::LoadHoles(rom);
521 if (holes_or.ok()) {
522 for (const auto& hole : holes_or.value()) {
523 if (hole.map_id_ == screen_id) {
524 formatter.BeginObject();
525 formatter.AddField("entrance_id", hole.entrance_id_);
526 formatter.AddField("x", hole.x_);
527 formatter.AddField("y", hole.y_);
528 formatter.AddField("is_hole", true);
529 formatter.EndObject();
530 }
531 }
532 }
533 formatter.EndArray();
534
535 // Exits
536 formatter.BeginArray("exits");
537 auto exits_or = zelda3::LoadExits(rom);
538 if (exits_or.ok()) {
539 for (const auto& exit : exits_or.value()) {
540 if (exit.map_id_ == screen_id) {
541 formatter.BeginObject();
542 formatter.AddField("room_id", exit.room_id_);
543 formatter.AddField("x", exit.x_);
544 formatter.AddField("y", exit.y_);
545 formatter.EndObject();
546 }
547 }
548 }
549 formatter.EndArray();
550
551 formatter.BeginArray("sprite_graphics");
552 for (uint8_t gfx : summary.sprite_graphics) {
553 formatter.AddArrayItem(absl::StrFormat("0x%02X", gfx));
554 }
555 formatter.EndArray();
556
557 formatter.BeginArray("sprite_palettes");
558 for (uint8_t pal : summary.sprite_palettes) {
559 formatter.AddArrayItem(absl::StrFormat("0x%02X", pal));
560 }
561 formatter.EndArray();
562
563 formatter.BeginArray("area_music");
564 for (uint8_t music : summary.area_music) {
565 formatter.AddArrayItem(absl::StrFormat("0x%02X", music));
566 }
567 formatter.EndArray();
568
569 formatter.BeginArray("static_graphics");
570 for (uint8_t sgfx : summary.static_graphics) {
571 formatter.AddArrayItem(absl::StrFormat("0x%02X", sgfx));
572 }
573 formatter.EndArray();
574
575 formatter.BeginObject("overlay");
576 formatter.AddField("enabled", summary.has_overlay);
577 formatter.AddField("id", absl::StrFormat("0x%04X", summary.overlay_id));
578 formatter.EndObject();
579
580 formatter.EndObject();
581
582 return absl::OkStatus();
583}
584
586 Rom* rom, const resources::ArgumentParser& parser,
587 resources::OutputFormatter& formatter) {
588 auto screen_id_str = parser.GetString("screen").value_or("all");
589
590 // Load the Overworld from ROM
591 zelda3::Overworld overworld(rom);
592 auto ow_status = overworld.Load(rom);
593 if (!ow_status.ok()) {
594 return ow_status;
595 }
596
597 // Build the query
599 if (screen_id_str != "all") {
600 int map_id;
601 if (!ParseHexString(screen_id_str, &map_id)) {
602 return absl::InvalidArgumentError(
603 "Invalid screen ID format. Must be hex.");
604 }
605 query.map_id = map_id;
606 }
607
608 // Call the helper function to collect warp entries
609 auto warps_or = overworld::CollectWarpEntries(overworld, query);
610 if (!warps_or.ok()) {
611 return warps_or.status();
612 }
613 const auto& warps = warps_or.value();
614
615 // Format the output
616 formatter.BeginObject("Overworld Warps");
617 formatter.AddField("screen_filter", screen_id_str);
618 formatter.AddField("total_warps", static_cast<int>(warps.size()));
619
620 formatter.BeginArray("warps");
621 for (const auto& warp : warps) {
622 formatter.BeginObject();
623 formatter.AddField("type", overworld::WarpTypeName(warp.type));
624 formatter.AddField("map_id", absl::StrFormat("0x%02X", warp.map_id));
625 formatter.AddField("world", overworld::WorldName(warp.world));
626 formatter.AddField("position",
627 absl::StrFormat("(%d,%d)", warp.pixel_x, warp.pixel_y));
628 formatter.AddField("map_pos", absl::StrFormat("0x%04X", warp.map_pos));
629
630 if (warp.entrance_id.has_value()) {
631 formatter.AddField("entrance_id",
632 absl::StrFormat("0x%02X", warp.entrance_id.value()));
633 }
634 if (warp.entrance_name.has_value()) {
635 formatter.AddField("entrance_name", warp.entrance_name.value());
636 }
637 if (warp.room_id.has_value()) {
638 formatter.AddField("room_id",
639 absl::StrFormat("0x%04X", warp.room_id.value()));
640 }
641
642 formatter.AddField("deleted", warp.deleted);
643 formatter.AddField("is_hole", warp.is_hole);
644 formatter.EndObject();
645 }
646 formatter.EndArray();
647 formatter.EndObject();
648
649 return absl::OkStatus();
650}
651
653 Rom* rom, const resources::ArgumentParser& parser,
654 resources::OutputFormatter& formatter) {
655 auto screen_id_str = parser.GetString("screen").value_or("all");
656
658 std::string phase_filter = "all";
659 if (auto phase_str = parser.GetString("phase"); phase_str.has_value()) {
660 int phase = -1;
661 if (!absl::SimpleAtoi(*phase_str, &phase) || phase < 0 || phase > 2) {
662 return absl::InvalidArgumentError("--phase must be 0, 1, or 2");
663 }
664 query.phase = phase;
665 phase_filter = *phase_str;
666 }
667
668 // Load the Overworld from ROM
669 zelda3::Overworld overworld(rom);
670 auto ow_status = overworld.Load(rom);
671 if (!ow_status.ok()) {
672 return ow_status;
673 }
674
675 // Sprites are stored on the parent area; a child screen resolves to it.
676 std::optional<int> requested_screen;
677 if (screen_id_str != "all") {
678 int map_id;
679 if (!ParseHexString(screen_id_str, &map_id)) {
680 return absl::InvalidArgumentError(
681 "Invalid screen ID format. Must be hex.");
682 }
683 const auto* map = overworld.overworld_map(map_id);
684 if (map == nullptr) {
685 return absl::InvalidArgumentError(
686 absl::StrFormat("Screen 0x%02X is out of range", map_id));
687 }
688 requested_screen = map_id;
689 query.map_id = map->parent();
690 }
691
692 auto sprites_or = overworld::CollectOverworldSprites(overworld, query);
693 if (!sprites_or.ok()) {
694 return sprites_or.status();
695 }
696 const auto& sprites = sprites_or.value();
697
698 formatter.BeginObject("Overworld Sprites");
699 formatter.AddField("screen_filter", screen_id_str);
700 if (requested_screen.has_value()) {
701 formatter.AddField("parent_area", absl::StrFormat("0x%02X", *query.map_id));
702 }
703 formatter.AddField("phase_filter", phase_filter);
704 formatter.AddField(
705 "phase_legend",
706 "0=beginning, 1=first_part, 2=second_part (vanilla: game state "
707 "<2 / 2 / >=3; Oracle ZSOW: GameState 0-1 / GameState 2 day / night or "
708 "GameState 3)");
709 formatter.AddField("total_sprites", static_cast<int>(sprites.size()));
710
711 formatter.BeginArray("sprites");
712 for (const auto& sprite : sprites) {
713 formatter.BeginObject();
714 formatter.AddField("phase", sprite.phase);
715 formatter.AddField("phase_name", overworld::SpritePhaseName(sprite.phase));
716 formatter.AddField("list_index", sprite.list_index);
717 formatter.AddField("sprite_id",
718 absl::StrFormat("0x%02X", sprite.sprite_id));
719 formatter.AddField("map_id", absl::StrFormat("0x%02X", sprite.map_id));
720 formatter.AddField("world", overworld::WorldName(sprite.world));
721 formatter.AddField("position",
722 absl::StrFormat("(%d,%d)", sprite.x, sprite.y));
723 formatter.AddField(
724 "tile", absl::StrFormat("(%d,%d)", sprite.local_x, sprite.local_y));
725
726 util::AddSpriteNameFields(formatter, sprite.sprite_id);
727
728 formatter.EndObject();
729 }
730 formatter.EndArray();
731 formatter.EndObject();
732
733 return absl::OkStatus();
734}
735
737 Rom* rom, const resources::ArgumentParser& parser,
738 resources::OutputFormatter& formatter) {
739 auto screen_id_str = parser.GetString("screen").value_or("all");
740
741 // Load the Overworld from ROM
742 zelda3::Overworld overworld(rom);
743 auto ow_status = overworld.Load(rom);
744 if (!ow_status.ok()) {
745 return ow_status;
746 }
747
748 // Optional screen filter
749 std::optional<int> map_filter;
750 if (screen_id_str != "all") {
751 int map_id;
752 if (!ParseHexString(screen_id_str, &map_id)) {
753 return absl::InvalidArgumentError(
754 "Invalid screen ID format. Must be hex.");
755 }
756 map_filter = map_id;
757 }
758
759 auto maps = overworld.overworld_maps();
760 ASSIGN_OR_RETURN(auto items, zelda3::LoadItems(rom, maps));
761
762 const auto& item_names = zelda3::Zelda3Labels::GetItemNames();
763
764 formatter.BeginObject("Overworld Items");
765 formatter.AddField("screen_filter", screen_id_str);
766
767 formatter.BeginArray("items");
768 int total_items = 0;
769 for (const auto& item : items) {
770 if (map_filter.has_value() &&
771 static_cast<int>(item.room_map_id_) != map_filter.value()) {
772 continue;
773 }
774
775 std::string world_name = "Unknown";
776 auto world_or = overworld::InferWorldFromMapId(item.room_map_id_);
777 if (world_or.ok()) {
778 world_name = overworld::WorldName(world_or.value());
779 }
780
781 formatter.BeginObject();
782 formatter.AddField("item_id", absl::StrFormat("0x%02X", item.id_));
783
784 if (item.id_ < item_names.size()) {
785 formatter.AddField("item_name", item_names[item.id_]);
786 }
787
788 formatter.AddField("map_id", absl::StrFormat("0x%02X", item.room_map_id_));
789 formatter.AddField("world", world_name);
790 formatter.AddField("tile_pos",
791 absl::StrFormat("(%d,%d)", item.game_x_, item.game_y_));
792 formatter.AddField("pixel_pos",
793 absl::StrFormat("(%d,%d)", item.x_, item.y_));
794 formatter.EndObject();
795
796 total_items++;
797 }
798 formatter.EndArray();
799 formatter.AddField("total_items", total_items);
800 formatter.EndObject();
801
802 return absl::OkStatus();
803}
804
806 Rom* rom, const resources::ArgumentParser& parser,
807 resources::OutputFormatter& formatter) {
808 auto entrance_id_str = parser.GetString("entrance").value();
809
810 int entrance_id;
811 if (!ParseHexString(entrance_id_str, &entrance_id)) {
812 return absl::InvalidArgumentError(
813 "Invalid entrance ID format. Must be hex.");
814 }
815
816 // Load the Overworld from ROM
817 zelda3::Overworld overworld(rom);
818 auto ow_status = overworld.Load(rom);
819 if (!ow_status.ok()) {
820 return ow_status;
821 }
822
823 // Call the helper function to get entrance details
824 auto details_or = overworld::GetEntranceDetails(overworld, entrance_id);
825 if (!details_or.ok()) {
826 return details_or.status();
827 }
828 const auto& details = details_or.value();
829
830 // Format the output
831 formatter.BeginObject("Overworld Entrance");
832 formatter.AddField("entrance_id",
833 absl::StrFormat("0x%02X", details.entrance_id));
834 formatter.AddField("map_id", absl::StrFormat("0x%02X", details.map_id));
835 formatter.AddField("world", overworld::WorldName(details.world));
836 formatter.AddField("position",
837 absl::StrFormat("(%d,%d)", details.x, details.y));
838 formatter.AddField("area_position", absl::StrFormat("(%d,%d)", details.area_x,
839 details.area_y));
840 formatter.AddField("map_pos", absl::StrFormat("0x%04X", details.map_pos));
841 formatter.AddField("is_hole", details.is_hole);
842
843 if (details.entrance_name.has_value()) {
844 formatter.AddField("name", details.entrance_name.value());
845 }
846
847 formatter.EndObject();
848
849 return absl::OkStatus();
850}
851
853 Rom* rom, const resources::ArgumentParser& parser,
854 resources::OutputFormatter& formatter) {
856 const bool has_screen = parser.GetString("screen").has_value();
857 const bool has_map = parser.GetString("map").has_value();
858 if (has_screen || has_map) {
859 ASSIGN_OR_RETURN(const int map_id,
860 ParseMapIdArg(parser, has_screen ? "screen" : "map"));
861 if (has_screen && has_map) {
862 ASSIGN_OR_RETURN(const int other, ParseMapIdArg(parser, "map"));
863 if (other != map_id) {
864 return absl::InvalidArgumentError(
865 "--screen and --map name different screens; pass one");
866 }
867 }
868 query.map_id = map_id;
869 }
870 if (auto world = parser.GetString("world"); world.has_value()) {
871 ASSIGN_OR_RETURN(const int world_id,
873 query.world = world_id;
874 }
875 if (parser.GetString("tile").has_value()) {
876 ASSIGN_OR_RETURN(const int tile_id, ParseTileIdArg(parser));
877 query.tile_id = static_cast<uint16_t>(tile_id);
878 }
879 int top = 32;
880 if (parser.GetString("top").has_value()) {
881 auto top_or = parser.GetInt("top");
882 if (!top_or.ok() || *top_or < 0) {
883 return absl::InvalidArgumentError(
884 "--top must be a non-negative count (0 lists every tile)");
885 }
886 top = *top_or;
887 }
888
889 zelda3::Overworld overworld(rom);
890 RETURN_IF_ERROR(overworld.Load(rom));
891 ASSIGN_OR_RETURN(const auto summary,
892 overworld::CollectTileUsage(overworld, query));
893
894 formatter.BeginObject("Overworld Tile Statistics");
895 formatter.AddField("screen_filter",
896 query.map_id.has_value()
897 ? absl::StrFormat("0x%02X", *query.map_id)
898 : std::string("all"));
899 formatter.AddField("world_filter", query.world.has_value()
901 : std::string("all"));
902 formatter.AddField("screens_scanned", summary.screens_scanned);
903 formatter.AddField("total_tiles", summary.total_tiles);
904 formatter.AddField("unique_tiles", static_cast<int>(summary.tiles.size()));
905
906 if (query.tile_id.has_value()) {
907 int count = 0;
908 int screens = 0;
909 for (const auto& usage : summary.tiles) {
910 if (usage.tile_id == *query.tile_id) {
911 count = usage.count;
912 screens = usage.screens;
913 break;
914 }
915 }
916 formatter.BeginObject("tile");
917 formatter.AddField("tile_id", absl::StrFormat("0x%04X", *query.tile_id));
918 formatter.AddField("count", count);
919 formatter.AddField("screens", screens);
920 formatter.EndObject();
921 }
922
923 formatter.BeginArray("worlds");
924 for (const auto& world : summary.worlds) {
925 formatter.BeginObject();
926 formatter.AddField("world", overworld::WorldName(world.world));
927 formatter.AddField("screens", world.screens);
928 formatter.AddField("total_tiles", world.total_tiles);
929 formatter.AddField("unique_tiles", world.unique_tiles);
930 formatter.EndObject();
931 }
932 formatter.EndArray();
933
934 // Per screen: each 32x32 block of the world grid (child screens of a
935 // multi-screen area are listed separately with their parent_area).
936 formatter.BeginArray("screens");
937 for (const auto& screen : summary.screens) {
938 formatter.BeginObject();
939 formatter.AddField("map_id", absl::StrFormat("0x%02X", screen.map_id));
940 formatter.AddField("world", overworld::WorldName(screen.world));
941 formatter.AddField("parent_area",
942 absl::StrFormat("0x%02X", screen.parent_map));
943 formatter.AddField("total_tiles", screen.total_tiles);
944 formatter.AddField("unique_tiles", screen.unique_tiles);
945 formatter.AddField("top_tile", absl::StrFormat("0x%04X", screen.top_tile));
946 formatter.AddField("top_tile_count", screen.top_tile_count);
947 if (query.tile_id.has_value()) {
948 formatter.AddField("tile_count", screen.tile_count);
949 }
950 formatter.EndObject();
951 }
952 formatter.EndArray();
953
954 const size_t shown = top == 0 ? summary.tiles.size()
955 : std::min<size_t>(summary.tiles.size(), top);
956 formatter.AddField("tile_counts_shown", static_cast<int>(shown));
957 formatter.BeginArray("tile_counts");
958 for (size_t i = 0; i < shown; ++i) {
959 const auto& usage = summary.tiles[i];
960 formatter.BeginObject();
961 formatter.AddField("tile_id", absl::StrFormat("0x%04X", usage.tile_id));
962 formatter.AddField("count", usage.count);
963 formatter.AddField("screens", usage.screens);
964 formatter.EndObject();
965 }
966 formatter.EndArray();
967 formatter.EndObject();
968
969 return absl::OkStatus();
970}
971
972} // namespace handlers
973} // namespace cli
974} // 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:52
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:632
absl::Status AttachLyraPrivateReservation(const core::HackManifest &manifest)
Definition rom.cc:518
auto filename() const
Definition rom.h:219
const auto & vector() const
Definition rom.h:217
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:893
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:781
bool is_loaded() const
Definition rom.h:199
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 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 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 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 ExecuteWithContext(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter, const resources::CommandInvocationContext &invocation_context) override
Execute with immutable, invocation-scoped ROM path identity.
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.
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)
bool HasFlag(const std::string &name) const
Check if a flag is 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 BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
Loads and queries the hack manifest JSON for yaze-ASM integration.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
absl::Status SaveMap32Expanded()
Save expanded tile32 definitions (v1+ ROMs)
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:158
std::vector< std::pair< uint32_t, uint32_t > > GetProjectedWriteRanges() const
Get the projected write ranges (PC offsets) for overworld map saves.
absl::Status SaveMap32Tiles()
Save tile32 definitions to ROM.
auto map_tiles() const
Definition overworld.h:866
const std::vector< gfx::Tile16 > & tiles16() const
Definition overworld.h:784
absl::Status CreateTile32Tilemap()
Build tile32 tilemap from current tile16 data.
const std::vector< OverworldMap > & overworld_maps() const
Definition overworld.h:767
auto overworld_map(int i) const
Definition overworld.h:770
absl::Status SaveOverworldMaps()
Save compressed map tile data to ROM.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status VerifyTileGridRoundTrip(const Rom &rom, const zelda3::Overworld &expected)
void AddTileLocationFields(resources::OutputFormatter &formatter, const overworld::AreaTileLocation &location)
absl::Status ApplyTileEdit(Rom &rom, zelda3::Overworld &overworld, const overworld::AreaTileLocation &location, uint16_t tile_id, bool do_write, bool mock_rom, resources::OutputFormatter &formatter)
absl::StatusOr< int > ParseCoordinateArg(const resources::ArgumentParser &parser, const std::string &name)
absl::StatusOr< int > ParseMapIdArg(const resources::ArgumentParser &parser, const std::string &name)
absl::Status SaveOverworldTileGrid(Rom &rom, zelda3::Overworld &overworld)
absl::StatusOr< int > ParseTileIdArg(const resources::ArgumentParser &parser)
bool IsOracleProjectRomPath(const std::filesystem::path &rom_path)
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< std::vector< WarpEntry > > CollectWarpEntries(const zelda3::Overworld &overworld, const WarpQuery &query)
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)
void AddSpriteNameFields(resources::OutputFormatter &formatter, int sprite_id)
bool ParseHexString(absl::string_view str, uint64_t *out)
Definition hex.cc:133
absl::StatusOr< std::vector< OverworldEntrance > > LoadEntrances(Rom *rom)
absl::StatusOr< std::vector< OverworldItem > > LoadItems(Rom *rom, std::vector< OverworldMap > &overworld_maps)
constexpr int kNumOverworldMaps
Definition common.h:85
std::vector< std::vector< uint16_t > > OverworldBlockset
Represents tile32 data for the overworld.
SpecialEntranceInspection InspectSpecialEntrances(const Rom &rom, Overworld &overworld, int map_id)
absl::StatusOr< std::vector< OverworldExit > > LoadExits(Rom *rom)
OverworldRomProfile DetectOverworldRomProfile(const Rom &rom)
Definition overworld.h:325
absl::StatusOr< std::vector< OverworldEntrance > > LoadHoles(Rom *rom)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:69
std::optional< std::filesystem::path > active_rom_path
std::optional< std::filesystem::path > source_rom_path
static const std::vector< std::string > & GetItemNames()