yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
minecart_commands.cc
Go to the documentation of this file.
2
3#include <array>
4#include <cstdint>
5#include <set>
6#include <string>
7#include <unordered_map>
8#include <unordered_set>
9#include <vector>
10
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"
15#include "cli/util/hex_util.h"
16#include "rom/rom.h"
17#include "util/macro.h"
19#include "zelda3/dungeon/room.h"
23
24namespace yaze {
25namespace cli {
26namespace handlers {
27
29
30namespace {
31
32constexpr int kCollisionWidth = 64;
33constexpr int kCollisionHeight = 64;
34
35constexpr std::array<uint8_t, 11> kDefaultTrackTiles = {
36 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xBB, 0xBC, 0xBD, 0xBE};
37constexpr std::array<uint8_t, 4> kDefaultStopTiles = {0xB7, 0xB8, 0xB9, 0xBA};
38constexpr std::array<uint8_t, 4> kDefaultSwitchTiles = {0xD0, 0xD1, 0xD2, 0xD3};
39
40std::unordered_map<uint8_t, bool> MakeTileSet(
41 const std::vector<uint8_t>& tiles) {
42 std::unordered_map<uint8_t, bool> result;
43 for (uint8_t t : tiles) {
44 result[t] = true;
45 }
46 return result;
47}
48
49std::vector<uint8_t> ToVector(const std::array<uint8_t, 11>& a) {
50 return std::vector<uint8_t>(a.begin(), a.end());
51}
52std::vector<uint8_t> ToVector(const std::array<uint8_t, 4>& a) {
53 return std::vector<uint8_t>(a.begin(), a.end());
54}
55
56absl::StatusOr<int> ParseOptionalHexArg(const resources::ArgumentParser& parser,
57 const std::string& name,
58 int default_value) {
59 auto s = parser.GetString(name);
60 if (!s.has_value()) {
61 return default_value;
62 }
63 int v = 0;
64 if (!ParseHexString(s.value(), &v)) {
65 return absl::InvalidArgumentError(
66 absl::StrFormat("Invalid %s format. Must be hex (e.g., 0x31).", name));
67 }
68 return v;
69}
70
71absl::StatusOr<std::vector<int>> ParseRooms(
72 const resources::ArgumentParser& parser) {
73 std::vector<int> rooms;
74
75 if (parser.HasFlag("all")) {
76 rooms.reserve(320);
77 for (int i = 0; i < 320; ++i) {
78 rooms.push_back(i);
79 }
80 return rooms;
81 }
82
83 auto room_opt = parser.GetString("room");
84 if (room_opt.has_value()) {
85 int room = 0;
86 if (!ParseHexString(room_opt.value(), &room)) {
87 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
88 }
89 rooms.push_back(room);
90 return rooms;
91 }
92
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.");
97 }
98
99 for (absl::string_view token :
100 absl::StrSplit(rooms_opt.value(), ',', absl::SkipEmpty())) {
101 std::string t = std::string(absl::StripAsciiWhitespace(token));
102 int room = 0;
103 if (!ParseHexString(t, &room)) {
104 return absl::InvalidArgumentError(
105 absl::StrFormat("Invalid room in --rooms list: %s", t));
106 }
107 rooms.push_back(room);
108 }
109
110 return rooms;
111}
112
114 int sprite_id = 0;
115 int x = 0;
116 int y = 0;
117 int subtype = 0;
118 int layer = 0;
119 int tile_x = 0;
120 int tile_y = 0;
121 bool on_stop_tile = false;
122};
123
125 int room_id = 0;
126 bool has_custom_collision_data = false;
127 int track_collision_tiles = 0;
128 int stop_tiles = 0;
129 int switch_tiles = 0;
131 std::vector<MinecartSpriteAudit> minecart_sprites;
132 std::vector<std::string> issues;
133};
134
135RoomMinecartAudit AuditRoom(Rom* rom, int room_id, int track_object_id,
136 int minecart_sprite_id,
137 bool include_track_objects_without_collision) {
138 RoomMinecartAudit audit;
139 audit.room_id = room_id;
140
141 // Load room header, objects, sprites.
142 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
143 room.LoadObjects();
144 room.LoadSprites();
145
146 for (const auto& obj : room.GetTileObjects()) {
147 if (static_cast<int>(obj.id_) != track_object_id) {
148 continue;
149 }
150 int subtype = obj.size_ & 0x1F;
151 if (!zelda3::IsMinecartTrackGraphicsSubtype(track_object_id, subtype)) {
152 continue;
153 }
154 audit.track_object_subtypes.insert(subtype);
155 }
156
157 // Collision audit.
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 =
164
165 std::unordered_set<int> stop_positions;
166 auto map_or = zelda3::LoadCustomCollisionMap(rom, room_id);
167 if (map_or.ok() && map_or.value().has_data) {
168 audit.has_custom_collision_data = true;
169 const auto& map = map_or.value().tiles;
170 for (int y = 0; y < kCollisionHeight; ++y) {
171 for (int x = 0; x < kCollisionWidth; ++x) {
172 uint8_t tile = map[static_cast<size_t>(y * kCollisionWidth + x)];
173 if (track_tiles[tile]) {
174 ++audit.track_collision_tiles;
175 }
176 if (stop_tiles[tile]) {
177 ++audit.stop_tiles;
178 stop_positions.insert(y * kCollisionWidth + x);
179 }
180 if (switch_tiles[tile]) {
181 ++audit.switch_tiles;
182 }
183 }
184 }
185 }
186
187 // Sprite audit.
188 for (const auto& sprite : room.GetSprites()) {
189 if (static_cast<int>(sprite.id()) != minecart_sprite_id) {
190 continue;
191 }
193 spr.sprite_id = sprite.id();
194 spr.x = sprite.x();
195 spr.y = sprite.y();
196 spr.subtype = sprite.subtype();
197 spr.layer = sprite.layer();
198 spr.tile_x = spr.x * 2;
199 spr.tile_y = spr.y * 2;
200 if (spr.tile_x >= 0 && spr.tile_x < kCollisionWidth && spr.tile_y >= 0 &&
201 spr.tile_y < kCollisionHeight) {
202 spr.on_stop_tile =
203 stop_positions.find(spr.tile_y * kCollisionWidth + spr.tile_x) !=
204 stop_positions.end();
205 }
206 audit.minecart_sprites.push_back(spr);
207 }
208
209 // Issues (heuristics).
210 const bool has_track_collision =
211 (audit.track_collision_tiles + audit.stop_tiles + audit.switch_tiles) > 0;
212 const bool has_track_objects = !audit.track_object_subtypes.empty();
213 const bool has_minecart_sprites = !audit.minecart_sprites.empty();
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;
218 for (const auto& spr : audit.minecart_sprites) {
219 if (spr.on_stop_tile) {
220 any_on_stop = true;
221 break;
222 }
223 }
224
225 if ((track_objects_signal || has_minecart_sprites) &&
227 audit.issues.push_back(
228 "Room uses minecart objects/sprites but has no custom collision data.");
229 }
230 if (has_minecart_sprites && !has_track_collision) {
231 audit.issues.push_back(
232 "Minecart sprite present but room has no minecart collision tiles.");
233 }
234 if (track_objects_signal && !has_track_collision) {
235 audit.issues.push_back(
236 "Track objects present but room has no minecart collision tiles.");
237 }
238 if (has_track_collision && !has_track_objects) {
239 audit.issues.push_back(
240 "Minecart collision tiles present but no track objects (0x31) found.");
241 }
242 if (has_minecart_sprites && audit.stop_tiles > 0 && !any_on_stop) {
243 audit.issues.push_back(
244 "Minecart sprite present but none placed on a stop tile (B7-BA).");
245 }
246 if (has_track_collision && audit.stop_tiles == 0) {
247 audit.issues.push_back(
248 "Minecart collision tiles present but no stop tiles.");
249 }
250
251 return audit;
252}
253
254} // namespace
255
257 const resources::ArgumentParser& parser) {
258 if (parser.HasFlag("all")) {
259 return absl::OkStatus();
260 }
261 if (parser.GetString("room").has_value()) {
262 return absl::OkStatus();
263 }
264 if (parser.GetString("rooms").has_value()) {
265 return absl::OkStatus();
266 }
267 return absl::InvalidArgumentError(
268 "Missing required args. Use --room, --rooms, or --all.");
269}
270
272 Rom* rom, const resources::ArgumentParser& parser,
273 resources::OutputFormatter& formatter) {
274 ASSIGN_OR_RETURN(auto rooms, ParseRooms(parser));
275
276 ASSIGN_OR_RETURN(int track_object_id,
277 ParseOptionalHexArg(parser, "track-object-id", 0x31));
278 ASSIGN_OR_RETURN(int minecart_sprite_id,
279 ParseOptionalHexArg(parser, "minecart-sprite-id", 0xA3));
280
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");
284
285 formatter.BeginObject("Dungeon Minecart Audit");
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);
289
290 int rooms_emitted = 0;
291 int rooms_with_issues = 0;
292
293 formatter.BeginArray("rooms");
294 for (int room_id : rooms) {
295 RoomMinecartAudit audit =
296 AuditRoom(rom, room_id, track_object_id, minecart_sprite_id,
297 include_track_objects);
298
299 const bool has_track_collision =
300 (audit.track_collision_tiles + audit.stop_tiles + audit.switch_tiles) >
301 0;
302 const bool has_track_objects = !audit.track_object_subtypes.empty();
303 const bool has_minecart_sprites = !audit.minecart_sprites.empty();
304
305 if (!audit.issues.empty()) {
306 ++rooms_with_issues;
307 }
308 if (only_issues && audit.issues.empty()) {
309 continue;
310 }
311 if (only_matches && !has_track_collision &&
312 !(include_track_objects && has_track_objects) &&
313 !has_minecart_sprites) {
314 continue;
315 }
316
317 formatter.BeginObject();
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);
325
326 formatter.BeginArray("track_object_subtypes");
327 for (int subtype : audit.track_object_subtypes) {
328 formatter.AddArrayItem(absl::StrFormat("%d", subtype));
329 }
330 formatter.EndArray();
331
332 formatter.BeginArray("minecart_sprites");
333 for (const auto& spr : audit.minecart_sprites) {
334 formatter.BeginObject();
335 formatter.AddHexField("sprite_id", spr.sprite_id, 2);
336 formatter.AddField("x", spr.x);
337 formatter.AddField("y", spr.y);
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);
343 formatter.EndObject();
344 }
345 formatter.EndArray();
346
347 formatter.BeginArray("issues");
348 for (const auto& issue : audit.issues) {
349 formatter.AddArrayItem(issue);
350 }
351 formatter.EndArray();
352
353 formatter.EndObject();
354 ++rooms_emitted;
355 }
356 formatter.EndArray();
357
358 formatter.AddField("rooms_emitted", rooms_emitted);
359 formatter.AddField("rooms_with_issues", rooms_with_issues);
360 formatter.AddField("status", "success");
361 formatter.EndObject();
362 return absl::OkStatus();
363}
364
365namespace {
366
367bool IsTrackTile(uint8_t v) {
368 return (v >= 0xB0 && v <= 0xBE) || (v >= 0xD0 && v <= 0xD3);
369}
370
371std::string TrackTileTypeName(uint8_t v) {
372 using T = zelda3::TrackTileType;
373 switch (static_cast<T>(v)) {
374 case T::HorizStraight:
375 return "HorizStraight";
376 case T::VertStraight:
377 return "VertStraight";
378 case T::CornerTL:
379 return "CornerTL";
380 case T::CornerBL:
381 return "CornerBL";
382 case T::CornerTR:
383 return "CornerTR";
384 case T::CornerBR:
385 return "CornerBR";
386 case T::Intersection:
387 return "Intersection";
388 case T::StopNorth:
389 return "StopNorth";
390 case T::StopSouth:
391 return "StopSouth";
392 case T::StopWest:
393 return "StopWest";
394 case T::StopEast:
395 return "StopEast";
396 case T::TJuncNorth:
397 return "TJuncNorth";
398 case T::TJuncSouth:
399 return "TJuncSouth";
400 case T::TJuncEast:
401 return "TJuncEast";
402 case T::TJuncWest:
403 return "TJuncWest";
404 case T::SwitchTL:
405 return "SwitchTL";
406 case T::SwitchBL:
407 return "SwitchBL";
408 case T::SwitchTR:
409 return "SwitchTR";
410 case T::SwitchBR:
411 return "SwitchBR";
412 default:
413 return absl::StrFormat("unknown(0x%02X)", v);
414 }
415}
416
417char TrackTileChar(uint8_t v) {
418 using T = zelda3::TrackTileType;
419 switch (static_cast<T>(v)) {
420 case T::HorizStraight:
421 return '-';
422 case T::VertStraight:
423 return '|';
424 case T::CornerTL:
425 return '/';
426 case T::CornerBL:
427 return '\\';
428 case T::CornerTR:
429 return '\\';
430 case T::CornerBR:
431 return '/';
432 case T::Intersection:
433 return '+';
434 case T::StopNorth:
435 return 'N';
436 case T::StopSouth:
437 return 's';
438 case T::StopWest:
439 return 'W';
440 case T::StopEast:
441 return 'E';
442 case T::TJuncNorth:
443 case T::TJuncSouth:
444 case T::TJuncEast:
445 case T::TJuncWest:
446 return 'T';
447 case T::SwitchTL:
448 case T::SwitchBL:
449 case T::SwitchTR:
450 case T::SwitchBR:
451 return 'X';
452 default:
453 return '?';
454 }
455}
456
457std::string TrackTileCategory(uint8_t v) {
458 if (v >= 0xB0 && v <= 0xB6)
459 return "track";
460 if (v >= 0xB7 && v <= 0xBA)
461 return "stop";
462 if (v >= 0xBB && v <= 0xBE)
463 return "junction";
464 if (v >= 0xD0 && v <= 0xD3)
465 return "switch";
466 return "unknown";
467}
468
469} // namespace
470
472 Rom* rom, const resources::ArgumentParser& parser,
473 resources::OutputFormatter& formatter) {
474 auto room_str = parser.GetString("room");
475 if (!room_str.has_value()) {
476 return absl::InvalidArgumentError("Missing required argument --room");
477 }
478 int room_id = 0;
479 if (!ParseHexString(room_str.value(), &room_id)) {
480 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
481 }
482
483 auto map_or = zelda3::LoadCustomCollisionMap(rom, room_id);
484 if (!map_or.ok()) {
485 return map_or.status();
486 }
487 const auto& cmap = map_or.value();
488
489 formatter.BeginObject("Dungeon Minecart Map");
490 formatter.AddField("room_id", room_id);
491 formatter.AddHexField("room_id_hex", room_id, 2);
492 formatter.AddField("has_custom_collision_data", cmap.has_data);
493
494 if (!cmap.has_data) {
495 formatter.AddField("tile_count", 0);
496 formatter.EndObject();
497 return absl::OkStatus();
498 }
499
500 // Collect track tiles and compute bounding box.
501 struct TrackTile {
502 int x, y;
503 uint8_t value;
504 };
505 std::vector<TrackTile> tiles;
506 int min_x = 64, max_x = -1, min_y = 64, max_y = -1;
507
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);
517 }
518 }
519 }
520
521 formatter.AddField("tile_count", static_cast<int>(tiles.size()));
522
523 if (tiles.empty()) {
524 formatter.EndObject();
525 return absl::OkStatus();
526 }
527
528 // Bounding box.
529 formatter.BeginObject("bounding_box");
530 formatter.AddField("min_x", min_x);
531 formatter.AddField("max_x", max_x);
532 formatter.AddField("min_y", min_y);
533 formatter.AddField("max_y", max_y);
534 formatter.EndObject();
535
536 // Tile list.
537 formatter.BeginArray("tiles");
538 for (const auto& t : tiles) {
539 formatter.BeginObject();
540 formatter.AddField("x", t.x);
541 formatter.AddField("y", t.y);
542 formatter.AddHexField("value", t.value, 2);
543 formatter.AddField("type", TrackTileTypeName(t.value));
544 formatter.AddField("category", TrackTileCategory(t.value));
545 formatter.EndObject();
546 }
547 formatter.EndArray();
548
549 // Bounded ASCII grid: column header (tens, units) then one row per tile_y.
550 // Each cell is one character wide at 1:1 scale. Agents can cross-reference
551 // (x, y) from the tile list against this grid to reason about adjacency.
552 {
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;
556 }
557
558 const int pad = 4; // "NNN " row label width
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);
563 }
564
565 std::vector<std::string> grid_lines;
566 grid_lines.push_back(tens_hdr);
567 grid_lines.push_back(units_hdr);
568
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) : ' ';
574 }
575 grid_lines.push_back(row);
576 }
577
578 formatter.BeginArray("ascii_grid");
579 for (const auto& line : grid_lines) {
580 formatter.AddArrayItem(line);
581 }
582 formatter.EndArray();
583 }
584
585 formatter.AddField("status", "success");
586 formatter.EndObject();
587 return absl::OkStatus();
588}
589
590} // namespace handlers
591} // namespace cli
592} // 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:28
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.
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.
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:276
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:405
void LoadObjects()
Definition room.cc:1755
void LoadSprites()
Definition room.cc:2629
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
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::unordered_map< uint8_t, bool > MakeTileSet(const std::vector< uint8_t > &tiles)
bool ParseHexString(absl::string_view str, uint64_t *out)
Definition hex.cc:133
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:673
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
bool IsMinecartTrackGraphicsSubtype(int object_id, int subtype)