7#include <unordered_map>
8#include <unordered_set>
11#include "absl/strings/ascii.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_join.h"
14#include "absl/strings/str_split.h"
36 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xBB, 0xBC, 0xBD, 0xBE};
41 const std::vector<uint8_t>& tiles) {
42 std::unordered_map<uint8_t, bool> result;
43 for (uint8_t t : tiles) {
49std::vector<uint8_t>
ToVector(
const std::array<uint8_t, 11>& a) {
50 return std::vector<uint8_t>(a.begin(), a.end());
52std::vector<uint8_t>
ToVector(
const std::array<uint8_t, 4>& a) {
53 return std::vector<uint8_t>(a.begin(), a.end());
57 const std::string&
name,
64 if (!ParseHexString(s.value(), &v)) {
65 return absl::InvalidArgumentError(
66 absl::StrFormat(
"Invalid %s format. Must be hex (e.g., 0x31).",
name));
73 std::vector<int> rooms;
77 for (
int i = 0; i < 320; ++i) {
84 if (room_opt.has_value()) {
86 if (!ParseHexString(room_opt.value(), &room)) {
87 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
89 rooms.push_back(room);
93 auto rooms_opt = parser.
GetString(
"rooms");
94 if (!rooms_opt.has_value()) {
95 return absl::InvalidArgumentError(
96 "Missing required args. Use --room, --rooms, or --all.");
99 for (absl::string_view token :
100 absl::StrSplit(rooms_opt.value(),
',', absl::SkipEmpty())) {
101 std::string t = std::string(absl::StripAsciiWhitespace(token));
103 if (!ParseHexString(t, &room)) {
104 return absl::InvalidArgumentError(
105 absl::StrFormat(
"Invalid room in --rooms list: %s", t));
107 rooms.push_back(room);
121 bool on_stop_tile =
false;
126 bool has_custom_collision_data =
false;
127 int track_collision_tiles = 0;
129 int switch_tiles = 0;
136 int minecart_sprite_id,
137 bool include_track_objects_without_collision) {
147 if (
static_cast<int>(obj.id_) != track_object_id) {
150 int subtype = obj.size_ & 0x1F;
155 std::unordered_map<uint8_t, bool> track_tiles =
157 std::unordered_map<uint8_t, bool> stop_tiles =
159 std::unordered_map<uint8_t, bool> switch_tiles =
162 std::unordered_set<int> stop_positions;
164 if (map_or.ok() && map_or.value().has_data) {
166 const auto& map = map_or.value().tiles;
170 if (track_tiles[tile]) {
173 if (stop_tiles[tile]) {
177 if (switch_tiles[tile]) {
185 for (
const auto& sprite : room.
GetSprites()) {
186 if (
static_cast<int>(sprite.id()) != minecart_sprite_id) {
193 spr.
subtype = sprite.subtype();
194 spr.
layer = sprite.layer();
201 stop_positions.end();
207 const bool has_track_collision =
211 const bool track_objects_signal =
212 has_track_objects && (include_track_objects_without_collision ||
213 has_track_collision || has_minecart_sprites);
214 bool any_on_stop =
false;
216 if (spr.on_stop_tile) {
222 if ((track_objects_signal || has_minecart_sprites) &&
225 "Room uses minecart objects/sprites but has no custom collision data.");
227 if (has_minecart_sprites && !has_track_collision) {
229 "Minecart sprite present but room has no minecart collision tiles.");
231 if (track_objects_signal && !has_track_collision) {
233 "Track objects present but room has no minecart collision tiles.");
235 if (has_track_collision && !has_track_objects) {
237 "Minecart collision tiles present but no track objects (0x31) found.");
239 if (has_minecart_sprites && audit.
stop_tiles > 0 && !any_on_stop) {
241 "Minecart sprite present but none placed on a stop tile (B7-BA).");
248 absl::StrFormat(
"Minecart sprite subtype %d is not referenced by any "
254 if (has_track_collision && audit.
stop_tiles == 0) {
256 "Minecart collision tiles present but no stop tiles.");
267 return absl::OkStatus();
269 if (parser.
GetString(
"room").has_value()) {
270 return absl::OkStatus();
272 if (parser.
GetString(
"rooms").has_value()) {
273 return absl::OkStatus();
275 return absl::InvalidArgumentError(
276 "Missing required args. Use --room, --rooms, or --all.");
285 ParseOptionalHexArg(parser,
"track-object-id", 0x31));
287 ParseOptionalHexArg(parser,
"minecart-sprite-id", 0xA3));
289 const bool only_issues = parser.
HasFlag(
"only-issues");
290 const bool only_matches = parser.
HasFlag(
"only-matches");
291 const bool include_track_objects = parser.
HasFlag(
"include-track-objects");
294 formatter.
AddField(
"total_rooms_requested",
static_cast<int>(rooms.size()));
295 formatter.
AddHexField(
"track_object_id", track_object_id, 2);
296 formatter.
AddHexField(
"minecart_sprite_id", minecart_sprite_id, 2);
298 int rooms_emitted = 0;
299 int rooms_with_issues = 0;
302 for (
int room_id : rooms) {
303 RoomMinecartAudit audit =
304 AuditRoom(rom, room_id, track_object_id, minecart_sprite_id,
305 include_track_objects);
307 const bool has_track_collision =
308 (audit.track_collision_tiles + audit.stop_tiles + audit.switch_tiles) >
310 const bool has_track_objects = !audit.track_object_subtypes.empty();
311 const bool has_minecart_sprites = !audit.minecart_sprites.empty();
313 if (!audit.issues.empty()) {
316 if (only_issues && audit.issues.empty()) {
319 if (only_matches && !has_track_collision &&
320 !(include_track_objects && has_track_objects) &&
321 !has_minecart_sprites) {
326 formatter.
AddField(
"room_id", audit.room_id);
327 formatter.
AddHexField(
"room_id_hex", audit.room_id, 2);
328 formatter.
AddField(
"has_custom_collision_data",
329 audit.has_custom_collision_data);
330 formatter.
AddField(
"track_collision_tiles", audit.track_collision_tiles);
331 formatter.
AddField(
"stop_tiles", audit.stop_tiles);
332 formatter.
AddField(
"switch_tiles", audit.switch_tiles);
334 formatter.
BeginArray(
"track_object_subtypes");
335 for (
int subtype : audit.track_object_subtypes) {
341 for (
const auto& spr : audit.minecart_sprites) {
343 formatter.
AddHexField(
"sprite_id", spr.sprite_id, 2);
346 formatter.
AddField(
"subtype", spr.subtype);
347 formatter.
AddField(
"layer", spr.layer);
348 formatter.
AddField(
"tile_x", spr.tile_x);
349 formatter.
AddField(
"tile_y", spr.tile_y);
350 formatter.
AddField(
"on_stop_tile", spr.on_stop_tile);
356 for (
const auto& issue : audit.issues) {
366 formatter.
AddField(
"rooms_emitted", rooms_emitted);
367 formatter.
AddField(
"rooms_with_issues", rooms_with_issues);
368 formatter.
AddField(
"status",
"success");
370 return absl::OkStatus();
376 return (v >= 0xB0 && v <= 0xBE) || (v >= 0xD0 && v <= 0xD3);
381 switch (
static_cast<T
>(v)) {
382 case T::HorizStraight:
383 return "HorizStraight";
384 case T::VertStraight:
385 return "VertStraight";
394 case T::Intersection:
395 return "Intersection";
421 return absl::StrFormat(
"unknown(0x%02X)", v);
427 switch (
static_cast<T
>(v)) {
428 case T::HorizStraight:
430 case T::VertStraight:
440 case T::Intersection:
466 if (v >= 0xB0 && v <= 0xB6)
468 if (v >= 0xB7 && v <= 0xBA)
470 if (v >= 0xBB && v <= 0xBE)
472 if (v >= 0xD0 && v <= 0xD3)
482 auto room_str = parser.
GetString(
"room");
483 if (!room_str.has_value()) {
484 return absl::InvalidArgumentError(
"Missing required argument --room");
487 if (!ParseHexString(room_str.value(), &room_id)) {
488 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
493 return map_or.status();
495 const auto& cmap = map_or.value();
498 formatter.
AddField(
"room_id", room_id);
500 formatter.
AddField(
"has_custom_collision_data", cmap.has_data);
502 if (!cmap.has_data) {
503 formatter.
AddField(
"tile_count", 0);
505 return absl::OkStatus();
513 std::vector<TrackTile> tiles;
514 int min_x = 64, max_x = -1, min_y = 64, max_y = -1;
516 for (
int y = 0; y < kCollisionHeight; ++y) {
517 for (
int x = 0; x < kCollisionWidth; ++x) {
518 uint8_t v = cmap.tiles[
static_cast<size_t>(y * kCollisionWidth + x)];
519 if (IsTrackTile(v)) {
520 tiles.push_back({x, y, v});
521 min_x = std::min(min_x, x);
522 max_x = std::max(max_x, x);
523 min_y = std::min(min_y, y);
524 max_y = std::max(max_y, y);
529 formatter.
AddField(
"tile_count",
static_cast<int>(tiles.size()));
533 return absl::OkStatus();
546 for (
const auto& t : tiles) {
551 formatter.
AddField(
"type", TrackTileTypeName(t.value));
552 formatter.
AddField(
"category", TrackTileCategory(t.value));
561 std::unordered_map<int, uint8_t> tile_map;
562 for (
const auto& t : tiles) {
563 tile_map[t.y * kCollisionWidth + t.x] = t.value;
567 std::string tens_hdr(pad,
' '), units_hdr(pad,
' ');
568 for (
int x = min_x; x <= max_x; ++x) {
569 tens_hdr += (x % 10 == 0) ?
static_cast<char>(
'0' + (x / 10) % 10) :
' ';
570 units_hdr +=
static_cast<char>(
'0' + x % 10);
573 std::vector<std::string> grid_lines;
574 grid_lines.push_back(tens_hdr);
575 grid_lines.push_back(units_hdr);
577 for (
int y = min_y; y <= max_y; ++y) {
578 std::string row = absl::StrFormat(
"%3d ", y);
579 for (
int x = min_x; x <= max_x; ++x) {
580 auto it = tile_map.find(y * kCollisionWidth + x);
581 row += (it != tile_map.end()) ? TrackTileChar(it->second) :
' ';
583 grid_lines.push_back(row);
587 for (
const auto& line : grid_lines) {
593 formatter.
AddField(
"status",
"success");
595 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
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.
const std::vector< zelda3::Sprite > & GetSprites() const
const std::vector< RoomObject > & GetTileObjects() const
#define ASSIGN_OR_RETURN(type_variable_name, expression)
std::string TrackTileTypeName(uint8_t v)
constexpr int kCollisionHeight
char TrackTileChar(uint8_t v)
constexpr std::array< uint8_t, 4 > kDefaultStopTiles
constexpr int kCollisionWidth
absl::StatusOr< std::vector< int > > ParseRooms(const resources::ArgumentParser &parser)
absl::StatusOr< int > ParseOptionalHexArg(const resources::ArgumentParser &parser, const std::string &name, int default_value)
RoomMinecartAudit AuditRoom(Rom *rom, int room_id, int track_object_id, int minecart_sprite_id, bool include_track_objects_without_collision)
std::vector< uint8_t > ToVector(const std::array< uint8_t, 11 > &a)
std::string TrackTileCategory(uint8_t v)
bool IsTrackTile(uint8_t v)
std::unordered_map< uint8_t, bool > MakeTileSet(const std::vector< uint8_t > &tiles)
constexpr std::array< uint8_t, 11 > kDefaultTrackTiles
constexpr std::array< uint8_t, 4 > kDefaultSwitchTiles
bool ParseHexString(absl::string_view str, int *out)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
std::vector< std::string > issues
std::vector< MinecartSpriteAudit > minecart_sprites
bool has_custom_collision_data
std::set< int > track_object_subtypes
int track_collision_tiles