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