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;
158 std::unordered_map<uint8_t, bool> track_tiles =
160 std::unordered_map<uint8_t, bool> stop_tiles =
162 std::unordered_map<uint8_t, bool> switch_tiles =
165 std::unordered_set<int> stop_positions;
167 if (map_or.ok() && map_or.value().has_data) {
169 const auto& map = map_or.value().tiles;
173 if (track_tiles[tile]) {
176 if (stop_tiles[tile]) {
180 if (switch_tiles[tile]) {
188 for (
const auto& sprite : room.
GetSprites()) {
189 if (
static_cast<int>(sprite.id()) != minecart_sprite_id) {
196 spr.
subtype = sprite.subtype();
197 spr.
layer = sprite.layer();
204 stop_positions.end();
210 const bool has_track_collision =
214 const bool track_objects_signal =
215 has_track_objects && (include_track_objects_without_collision ||
216 has_track_collision || has_minecart_sprites);
217 bool any_on_stop =
false;
219 if (spr.on_stop_tile) {
225 if ((track_objects_signal || has_minecart_sprites) &&
228 "Room uses minecart objects/sprites but has no custom collision data.");
230 if (has_minecart_sprites && !has_track_collision) {
232 "Minecart sprite present but room has no minecart collision tiles.");
234 if (track_objects_signal && !has_track_collision) {
236 "Track objects present but room has no minecart collision tiles.");
238 if (has_track_collision && !has_track_objects) {
240 "Minecart collision tiles present but no track objects (0x31) found.");
242 if (has_minecart_sprites && audit.
stop_tiles > 0 && !any_on_stop) {
244 "Minecart sprite present but none placed on a stop tile (B7-BA).");
246 if (has_track_collision && audit.
stop_tiles == 0) {
248 "Minecart collision tiles present but no stop tiles.");
259 return absl::OkStatus();
261 if (parser.
GetString(
"room").has_value()) {
262 return absl::OkStatus();
264 if (parser.
GetString(
"rooms").has_value()) {
265 return absl::OkStatus();
267 return absl::InvalidArgumentError(
268 "Missing required args. Use --room, --rooms, or --all.");
277 ParseOptionalHexArg(parser,
"track-object-id", 0x31));
279 ParseOptionalHexArg(parser,
"minecart-sprite-id", 0xA3));
281 const bool only_issues = parser.
HasFlag(
"only-issues");
282 const bool only_matches = parser.
HasFlag(
"only-matches");
283 const bool include_track_objects = parser.
HasFlag(
"include-track-objects");
286 formatter.
AddField(
"total_rooms_requested",
static_cast<int>(rooms.size()));
287 formatter.
AddHexField(
"track_object_id", track_object_id, 2);
288 formatter.
AddHexField(
"minecart_sprite_id", minecart_sprite_id, 2);
290 int rooms_emitted = 0;
291 int rooms_with_issues = 0;
294 for (
int room_id : rooms) {
295 RoomMinecartAudit audit =
296 AuditRoom(rom, room_id, track_object_id, minecart_sprite_id,
297 include_track_objects);
299 const bool has_track_collision =
300 (audit.track_collision_tiles + audit.stop_tiles + audit.switch_tiles) >
302 const bool has_track_objects = !audit.track_object_subtypes.empty();
303 const bool has_minecart_sprites = !audit.minecart_sprites.empty();
305 if (!audit.issues.empty()) {
308 if (only_issues && audit.issues.empty()) {
311 if (only_matches && !has_track_collision &&
312 !(include_track_objects && has_track_objects) &&
313 !has_minecart_sprites) {
318 formatter.
AddField(
"room_id", audit.room_id);
319 formatter.
AddHexField(
"room_id_hex", audit.room_id, 2);
320 formatter.
AddField(
"has_custom_collision_data",
321 audit.has_custom_collision_data);
322 formatter.
AddField(
"track_collision_tiles", audit.track_collision_tiles);
323 formatter.
AddField(
"stop_tiles", audit.stop_tiles);
324 formatter.
AddField(
"switch_tiles", audit.switch_tiles);
326 formatter.
BeginArray(
"track_object_subtypes");
327 for (
int subtype : audit.track_object_subtypes) {
333 for (
const auto& spr : audit.minecart_sprites) {
335 formatter.
AddHexField(
"sprite_id", spr.sprite_id, 2);
338 formatter.
AddField(
"subtype", spr.subtype);
339 formatter.
AddField(
"layer", spr.layer);
340 formatter.
AddField(
"tile_x", spr.tile_x);
341 formatter.
AddField(
"tile_y", spr.tile_y);
342 formatter.
AddField(
"on_stop_tile", spr.on_stop_tile);
348 for (
const auto& issue : audit.issues) {
358 formatter.
AddField(
"rooms_emitted", rooms_emitted);
359 formatter.
AddField(
"rooms_with_issues", rooms_with_issues);
360 formatter.
AddField(
"status",
"success");
362 return absl::OkStatus();
368 return (v >= 0xB0 && v <= 0xBE) || (v >= 0xD0 && v <= 0xD3);
373 switch (
static_cast<T
>(v)) {
374 case T::HorizStraight:
375 return "HorizStraight";
376 case T::VertStraight:
377 return "VertStraight";
386 case T::Intersection:
387 return "Intersection";
413 return absl::StrFormat(
"unknown(0x%02X)", v);
419 switch (
static_cast<T
>(v)) {
420 case T::HorizStraight:
422 case T::VertStraight:
432 case T::Intersection:
458 if (v >= 0xB0 && v <= 0xB6)
460 if (v >= 0xB7 && v <= 0xBA)
462 if (v >= 0xBB && v <= 0xBE)
464 if (v >= 0xD0 && v <= 0xD3)
474 auto room_str = parser.
GetString(
"room");
475 if (!room_str.has_value()) {
476 return absl::InvalidArgumentError(
"Missing required argument --room");
479 if (!ParseHexString(room_str.value(), &room_id)) {
480 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
485 return map_or.status();
487 const auto& cmap = map_or.value();
490 formatter.
AddField(
"room_id", room_id);
492 formatter.
AddField(
"has_custom_collision_data", cmap.has_data);
494 if (!cmap.has_data) {
495 formatter.
AddField(
"tile_count", 0);
497 return absl::OkStatus();
505 std::vector<TrackTile> tiles;
506 int min_x = 64, max_x = -1, min_y = 64, max_y = -1;
508 for (
int y = 0; y < kCollisionHeight; ++y) {
509 for (
int x = 0; x < kCollisionWidth; ++x) {
510 uint8_t v = cmap.tiles[
static_cast<size_t>(y * kCollisionWidth + x)];
511 if (IsTrackTile(v)) {
512 tiles.push_back({x, y, v});
513 min_x = std::min(min_x, x);
514 max_x = std::max(max_x, x);
515 min_y = std::min(min_y, y);
516 max_y = std::max(max_y, y);
521 formatter.
AddField(
"tile_count",
static_cast<int>(tiles.size()));
525 return absl::OkStatus();
538 for (
const auto& t : tiles) {
543 formatter.
AddField(
"type", TrackTileTypeName(t.value));
544 formatter.
AddField(
"category", TrackTileCategory(t.value));
553 std::unordered_map<int, uint8_t> tile_map;
554 for (
const auto& t : tiles) {
555 tile_map[t.y * kCollisionWidth + t.x] = t.value;
559 std::string tens_hdr(pad,
' '), units_hdr(pad,
' ');
560 for (
int x = min_x; x <= max_x; ++x) {
561 tens_hdr += (x % 10 == 0) ?
static_cast<char>(
'0' + (x / 10) % 10) :
' ';
562 units_hdr +=
static_cast<char>(
'0' + x % 10);
565 std::vector<std::string> grid_lines;
566 grid_lines.push_back(tens_hdr);
567 grid_lines.push_back(units_hdr);
569 for (
int y = min_y; y <= max_y; ++y) {
570 std::string row = absl::StrFormat(
"%3d ", y);
571 for (
int x = min_x; x <= max_x; ++x) {
572 auto it = tile_map.find(y * kCollisionWidth + x);
573 row += (it != tile_map.end()) ? TrackTileChar(it->second) :
' ';
575 grid_lines.push_back(row);
579 for (
const auto& line : grid_lines) {
585 formatter.
AddField(
"status",
"success");
587 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, uint64_t *out)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
bool IsMinecartTrackGraphicsSubtype(int object_id, int subtype)
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