yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_commands.cc
Go to the documentation of this file.
2
3#include <fstream>
4#include <sstream>
5
6#include "absl/strings/str_format.h"
7#include "absl/strings/str_split.h"
8#include "cli/util/hex_util.h"
9#include "rom/rom.h"
10#include "rom/snes.h"
14#include "zelda3/dungeon/room.h"
19
20namespace yaze {
21namespace cli {
22namespace handlers {
23
25
26namespace {
27
28constexpr uint8_t kStopTileMin = 0xB7;
29constexpr uint8_t kStopTileMax = 0xBA;
30
31bool IsStopTile(uint8_t v) {
32 return v >= kStopTileMin && v <= kStopTileMax;
33}
34
35// Merge stop tiles from |existing| into |generated| without overwriting
36// track tiles that were just produced. Stop tiles in |existing| that sit on
37// cells that are zero in |generated| are copied across unchanged.
39 zelda3::CustomCollisionMap& generated) {
40 for (int i = 0; i < static_cast<int>(existing.tiles.size()); ++i) {
41 if (IsStopTile(existing.tiles[i]) && generated.tiles[i] == 0) {
42 generated.tiles[i] = existing.tiles[i];
43 }
44 }
45}
46
47// Helper to load sprite registry from file if --sprite-registry flag is provided
49 auto registry_path = parser.GetString("sprite-registry");
50 if (!registry_path.has_value()) {
51 return absl::OkStatus(); // Flag not provided, nothing to load
52 }
53
54 std::ifstream file(registry_path.value());
55 if (!file.is_open()) {
56 return absl::NotFoundError(absl::StrFormat(
57 "Could not open sprite registry: %s", registry_path.value()));
58 }
59
60 std::stringstream buffer;
61 buffer << file.rdbuf();
63}
64
65} // namespace
66
68 Rom* rom, const resources::ArgumentParser& parser,
69 resources::OutputFormatter& formatter) {
70 // Load custom sprite registry if provided (e.g., Oracle of Secrets)
71 auto registry_status = MaybeLoadSpriteRegistry(parser);
72 if (!registry_status.ok()) {
73 return registry_status;
74 }
75
76 auto room_id_str = parser.GetString("room").value();
77
78 int room_id;
79 if (!ParseHexString(room_id_str, &room_id)) {
80 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
81 }
82
83 formatter.BeginObject("Dungeon Room Sprites");
84 formatter.AddField("room_id", room_id);
85
87 room.LoadSprites();
88 const auto& sprites = room.GetSprites();
89
90 formatter.AddField("total_sprites", static_cast<int>(sprites.size()));
91 formatter.AddField("status", "success");
92
93 formatter.BeginArray("sprites");
94 for (const auto& sprite : sprites) {
95 formatter.BeginObject();
96 formatter.AddHexField("sprite_id", sprite.id(), 2);
97 formatter.AddField("name", zelda3::ResolveSpriteName(sprite.id()));
98 formatter.AddField("x", sprite.x());
99 formatter.AddField("y", sprite.y());
100 formatter.AddField("subtype", sprite.subtype());
101 formatter.AddField("layer", sprite.layer());
102 if (sprite.key_drop() > 0) {
103 formatter.AddField("key_drop", sprite.key_drop());
104 }
105 formatter.EndObject();
106 }
107 formatter.EndArray();
108 formatter.EndObject();
109
110 return absl::OkStatus();
111}
112
114 Rom* rom, const resources::ArgumentParser& parser,
115 resources::OutputFormatter& formatter) {
116 // Load custom sprite registry if provided (e.g., Oracle of Secrets)
117 auto registry_status = MaybeLoadSpriteRegistry(parser);
118 if (!registry_status.ok()) {
119 return registry_status;
120 }
121
122 auto room_id_str = parser.GetString("room").value();
123
124 int room_id;
125 if (!ParseHexString(room_id_str, &room_id)) {
126 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
127 }
128
129 formatter.AddField("room_id", room_id);
130
131 // Load full room to get objects, doors, and stairs
132 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
133
134 formatter.AddField("status", "success");
135 formatter.AddField("name", absl::StrFormat("Room %d", room.id()));
136 formatter.AddField("room_id", room.id());
137 formatter.AddField("room_type", "Dungeon Room");
138
139 // Room properties from Room data
140 formatter.BeginObject("properties");
141 formatter.AddField("blockset", room.blockset());
142 formatter.AddField("spriteset", room.spriteset());
143 formatter.AddField("palette", room.palette());
144 formatter.AddField("layout", room.layout_id());
145 formatter.AddField("floor1", room.floor1());
146 formatter.AddField("floor2", room.floor2());
147 formatter.AddField("effect", static_cast<int>(room.effect()));
148 formatter.AddField("tag1", static_cast<int>(room.tag1()));
149 formatter.AddField("tag2", static_cast<int>(room.tag2()));
150
151 // Check object counts for simple heuristics
152 formatter.AddField("object_count",
153 static_cast<int>(room.GetTileObjects().size()));
154
155 formatter.EndObject();
156
157 // Export Doors
158 formatter.BeginArray("doors");
159 for (const auto& door : room.GetDoors()) {
160 formatter.BeginObject();
161 formatter.AddField("position", door.position);
162 formatter.AddField("direction", std::string(door.GetDirectionName()));
163 formatter.AddField("type", std::string(door.GetTypeName()));
164 auto [tx, ty] = door.GetTileCoords();
165 formatter.AddField("tile_x", tx);
166 formatter.AddField("tile_y", ty);
167 formatter.EndObject();
168 }
169 formatter.EndArray();
170
171 // Export Staircases
172 formatter.BeginArray("staircases");
173 for (const auto& stair : room.GetStairs()) {
174 formatter.BeginObject();
175 formatter.AddField("tile_x",
176 stair.id); // 'id' field stores X in struct staircase
177 formatter.AddField(
178 "tile_y", stair.room); // 'room' field stores Y in struct staircase
179 formatter.AddField("label", stair.label);
180 formatter.EndObject();
181 }
182 formatter.EndArray();
183
184 // Export Chests
185 formatter.BeginArray("chests");
186 for (const auto& chest : room.GetChests()) {
187 formatter.BeginObject();
188 formatter.AddHexField("item_id", chest.id, 2);
189 formatter.AddField("item_name", zelda3::GetItemLabel(chest.id));
190 formatter.AddField("is_big_chest", chest.size);
191 formatter.EndObject();
192 }
193 formatter.EndArray();
194
195 return absl::OkStatus();
196}
197
199 Rom* rom, const resources::ArgumentParser& parser,
200 resources::OutputFormatter& formatter) {
201 auto room_id_opt = parser.GetString("room");
202
203 bool has_room_filter = room_id_opt.has_value();
204 int room_filter = -1;
205 if (has_room_filter) {
206 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
207 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
208 }
209 }
210
211 int total_rooms = 0;
212 int rooms_with_chests = 0;
213 int total_chests = 0;
214 std::map<int, int> item_counts;
215
216 formatter.BeginObject("Dungeon Chests");
217 formatter.AddField("room_filter", has_room_filter
218 ? absl::StrFormat("0x%02X", room_filter)
219 : "all");
220
221 formatter.BeginArray("rooms");
222 int start_room = has_room_filter ? room_filter : 0;
223 int end_room = has_room_filter ? room_filter : zelda3::kNumberOfRooms - 1;
224
225 for (int room_id = start_room; room_id <= end_room; ++room_id) {
226 total_rooms++;
227 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
228 room.LoadChests();
229
230 const auto& chests = room.GetChests();
231 if (chests.empty()) {
232 continue;
233 }
234
235 rooms_with_chests++;
236 total_chests += static_cast<int>(chests.size());
237
238 formatter.BeginObject();
239 formatter.AddField("room_id", absl::StrFormat("0x%02X", room_id));
240 formatter.AddField("room_name", zelda3::GetRoomLabel(room_id));
241 formatter.AddField("chest_count", static_cast<int>(chests.size()));
242
243 formatter.BeginArray("chests");
244 int chest_index = 0;
245 for (const auto& chest : chests) {
246 formatter.BeginObject();
247 formatter.AddField("index", chest_index++);
248 formatter.AddHexField("item_id", chest.id, 2);
249 formatter.AddField("item_name", zelda3::GetItemLabel(chest.id));
250 formatter.AddField("is_big_chest", chest.size);
251 formatter.EndObject();
252
253 if (chest.id != 0) {
254 item_counts[chest.id]++;
255 }
256 }
257 formatter.EndArray();
258 formatter.EndObject();
259 }
260 formatter.EndArray();
261
262 formatter.BeginObject("summary");
263 formatter.AddField("total_rooms", total_rooms);
264 formatter.AddField("rooms_with_chests", rooms_with_chests);
265 formatter.AddField("total_chests", total_chests);
266 formatter.AddField("unique_items", static_cast<int>(item_counts.size()));
267
268 formatter.BeginArray("duplicate_items");
269 for (const auto& [item_id, count] : item_counts) {
270 if (count < 2) {
271 continue;
272 }
273 formatter.BeginObject();
274 formatter.AddHexField("item_id", item_id, 2);
275 formatter.AddField("item_name", zelda3::GetItemLabel(item_id));
276 formatter.AddField("count", count);
277 formatter.EndObject();
278 }
279 formatter.EndArray();
280 formatter.EndObject();
281
282 formatter.EndObject();
283 return absl::OkStatus();
284}
285
287 Rom* rom, const resources::ArgumentParser& parser,
288 resources::OutputFormatter& formatter) {
289 auto entrance_id_str = parser.GetString("entrance").value();
290 bool is_spawn_point = parser.HasFlag("spawn");
291
292 int entrance_id;
293 if (!ParseHexString(entrance_id_str, &entrance_id)) {
294 return absl::InvalidArgumentError(
295 "Invalid entrance ID format. Must be hex.");
296 }
297
298 zelda3::RoomEntrance entrance(rom, static_cast<uint8_t>(entrance_id),
299 is_spawn_point);
300
301 formatter.AddField("entrance_id", absl::StrFormat("0x%02X", entrance_id));
302 formatter.AddField("is_spawn_point", is_spawn_point);
303 formatter.AddField("room_id", absl::StrFormat("0x%04X", entrance.room_));
304 formatter.AddField("exit_id", absl::StrFormat("0x%04X", entrance.exit_));
305
306 formatter.BeginObject("position");
307 formatter.AddField("x", entrance.x_position_);
308 formatter.AddField("y", entrance.y_position_);
309 formatter.EndObject();
310
311 formatter.BeginObject("camera");
312 formatter.AddField("x", entrance.camera_x_);
313 formatter.AddField("y", entrance.camera_y_);
314 formatter.AddField("trigger_x", entrance.camera_trigger_x_);
315 formatter.AddField("trigger_y", entrance.camera_trigger_y_);
316 formatter.EndObject();
317
318 formatter.BeginObject("properties");
319 formatter.AddField("blockset", absl::StrFormat("0x%02X", entrance.blockset_));
320 formatter.AddField("floor", absl::StrFormat("0x%02X", entrance.floor_));
321 formatter.AddField("dungeon_id",
322 absl::StrFormat("0x%02X", entrance.dungeon_id_));
323 formatter.AddField("door", absl::StrFormat("0x%02X", entrance.door_));
324 formatter.AddField("ladder_bg",
325 absl::StrFormat("0x%02X", entrance.ladder_bg_));
326 formatter.AddField("scrolling",
327 absl::StrFormat("0x%02X", entrance.scrolling_));
328 formatter.AddField("scroll_quadrant",
329 absl::StrFormat("0x%02X", entrance.scroll_quadrant_));
330 formatter.AddField("music", absl::StrFormat("0x%02X", entrance.music_));
331 formatter.EndObject();
332
333 formatter.BeginObject("camera_boundaries");
334 formatter.AddField("qn",
335 absl::StrFormat("0x%02X", entrance.camera_boundary_qn_));
336 formatter.AddField("fn",
337 absl::StrFormat("0x%02X", entrance.camera_boundary_fn_));
338 formatter.AddField("qs",
339 absl::StrFormat("0x%02X", entrance.camera_boundary_qs_));
340 formatter.AddField("fs",
341 absl::StrFormat("0x%02X", entrance.camera_boundary_fs_));
342 formatter.AddField("qw",
343 absl::StrFormat("0x%02X", entrance.camera_boundary_qw_));
344 formatter.AddField("fw",
345 absl::StrFormat("0x%02X", entrance.camera_boundary_fw_));
346 formatter.AddField("qe",
347 absl::StrFormat("0x%02X", entrance.camera_boundary_qe_));
348 formatter.AddField("fe",
349 absl::StrFormat("0x%02X", entrance.camera_boundary_fe_));
350 formatter.EndObject();
351
352 return absl::OkStatus();
353}
354
356 Rom* rom, const resources::ArgumentParser& parser,
357 resources::OutputFormatter& formatter) {
358 auto room_id_str = parser.GetString("room").value();
359
360 int room_id;
361 if (!ParseHexString(room_id_str, &room_id)) {
362 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
363 }
364
365 formatter.BeginObject("Dungeon Export");
366 formatter.AddField("room_id", room_id);
367
368 // Use existing dungeon system
369 zelda3::DungeonEditorSystem dungeon_editor(rom);
370 auto room_or = dungeon_editor.GetRoom(room_id);
371 if (!room_or.ok()) {
372 formatter.AddField("status", "error");
373 formatter.AddField("error", room_or.status().ToString());
374 formatter.EndObject();
375 return room_or.status();
376 }
377
378 auto& room = room_or.value();
379
380 // Export room data
381 formatter.AddField("status", "success");
382 formatter.AddField("room_width", "Unknown");
383 formatter.AddField("room_height", "Unknown");
384 formatter.AddField("room_name", absl::StrFormat("Room %d", room.id()));
385
386 // Add room data as JSON
387 formatter.BeginObject("room_data");
388 formatter.AddField("tiles", "Room tile data would be exported here");
389 formatter.AddField("sprites", "Room sprite data would be exported here");
390 formatter.AddField("doors", "Room door data would be exported here");
391 formatter.EndObject();
392
393 formatter.EndObject();
394
395 return absl::OkStatus();
396}
397
399 Rom* rom, const resources::ArgumentParser& parser,
400 resources::OutputFormatter& formatter) {
401 auto room_id_str = parser.GetString("room").value();
402
403 int room_id;
404 if (!ParseHexString(room_id_str, &room_id)) {
405 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
406 }
407
408 formatter.BeginObject("Dungeon Room Objects");
409 formatter.AddField("room_id", room_id);
410
411 // Use existing dungeon system
412 zelda3::DungeonEditorSystem dungeon_editor(rom);
413 auto room_or = dungeon_editor.GetRoom(room_id);
414 if (!room_or.ok()) {
415 formatter.AddField("status", "error");
416 formatter.AddField("error", room_or.status().ToString());
417 formatter.EndObject();
418 return room_or.status();
419 }
420
421 auto& room = room_or.value();
422
423 // Load objects if not already loaded (GetTileObjects might be empty otherwise)
424 room.LoadObjects();
425
426 const auto& objects = room.GetTileObjects();
427 formatter.AddField("total_objects", static_cast<int>(objects.size()));
428 formatter.AddField("status", "success");
429
430 formatter.BeginArray("objects");
431 for (const auto& obj : objects) {
432 formatter.BeginObject("");
433 formatter.AddField("id", obj.id_);
434 formatter.AddField("id_hex", absl::StrFormat("0x%04X", obj.id_));
435 formatter.AddField("x", obj.x_);
436 formatter.AddField("y", obj.y_);
437 formatter.AddField("size", obj.size_);
438 formatter.AddField("layer", static_cast<int>(obj.layer_));
439 // Add decoded type info if available
440 int type = zelda3::RoomObject::DetermineObjectType((obj.id_ & 0xFF),
441 (obj.id_ >> 8));
442 formatter.AddField("type", type);
443 formatter.EndObject();
444 }
445 formatter.EndArray();
446 formatter.EndObject();
447
448 return absl::OkStatus();
449}
450
452 Rom* rom, const resources::ArgumentParser& parser,
453 resources::OutputFormatter& formatter) {
454 auto room_id_str = parser.GetString("room").value();
455
456 int room_id;
457 if (!ParseHexString(room_id_str, &room_id)) {
458 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
459 }
460
461 formatter.BeginObject("Dungeon Room Tiles");
462 formatter.AddField("room_id", room_id);
463
464 // Use existing dungeon system
465 zelda3::DungeonEditorSystem dungeon_editor(rom);
466 auto room_or = dungeon_editor.GetRoom(room_id);
467 if (!room_or.ok()) {
468 formatter.AddField("status", "error");
469 formatter.AddField("error", room_or.status().ToString());
470 formatter.EndObject();
471 return room_or.status();
472 }
473
474 auto& room = room_or.value();
475
476 // TODO: Implement tile data retrieval from room
477 formatter.AddField("room_width", "Unknown");
478 formatter.AddField("room_height", "Unknown");
479 formatter.AddField("total_tiles", "Unknown");
480 formatter.AddField("status", "not_implemented");
481 formatter.AddField("message",
482 "Tile data retrieval requires room tile parsing");
483
484 formatter.BeginArray("tiles");
485 formatter.EndArray();
486 formatter.EndObject();
487
488 return absl::OkStatus();
489}
490
492 Rom* rom, const resources::ArgumentParser& parser,
493 resources::OutputFormatter& formatter) {
494 auto room_id_str = parser.GetString("room").value();
495 auto property = parser.GetString("property").value();
496 auto value = parser.GetString("value").value();
497
498 int room_id;
499 if (!ParseHexString(room_id_str, &room_id)) {
500 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
501 }
502
503 formatter.BeginObject("Dungeon Room Property Set");
504 formatter.AddField("room_id", room_id);
505 formatter.AddField("property", property);
506 formatter.AddField("value", value);
507
508 // Use existing dungeon system
509 zelda3::DungeonEditorSystem dungeon_editor(rom);
510 auto room_or = dungeon_editor.GetRoom(room_id);
511 if (!room_or.ok()) {
512 formatter.AddField("status", "error");
513 formatter.AddField("error", room_or.status().ToString());
514 formatter.EndObject();
515 return room_or.status();
516 }
517
518 // TODO: Implement property setting
519 formatter.AddField("status", "not_implemented");
520 formatter.AddField("message",
521 "Property setting requires room property system");
522 formatter.EndObject();
523
524 return absl::OkStatus();
525}
526
528 Rom* rom, const resources::ArgumentParser& parser,
529 resources::OutputFormatter& formatter) {
530 auto room_id_str = parser.GetString("room").value();
531
532 int room_id;
533 if (!ParseHexString(room_id_str, &room_id)) {
534 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
535 }
536
537 formatter.BeginObject("Room Header Debug");
538 formatter.AddField("room_id", room_id);
539 formatter.AddHexField("room_id_hex", room_id, 2);
540
541 // Show ROM address constants
542 formatter.BeginObject("rom_addresses");
543 formatter.AddHexField("kRoomHeaderPointer", zelda3::kRoomHeaderPointer, 4);
544 formatter.AddHexField("kRoomHeaderPointerBank",
546 formatter.EndObject();
547
548 // Read the pointer table address
549 int header_pointer = (rom->data()[zelda3::kRoomHeaderPointer + 2] << 16) +
550 (rom->data()[zelda3::kRoomHeaderPointer + 1] << 8) +
552 int header_pointer_pc = SnesToPc(header_pointer);
553
554 formatter.BeginObject("pointer_table");
555 formatter.AddHexField("snes_address", header_pointer, 6);
556 formatter.AddHexField("pc_address", header_pointer_pc, 6);
557 formatter.EndObject();
558
559 // Read the room's specific header address from the table
560 int table_offset = header_pointer_pc + (room_id * 2);
561 int room_header_addr = (rom->data()[zelda3::kRoomHeaderPointerBank] << 16) +
562 (rom->data()[table_offset + 1] << 8) +
563 rom->data()[table_offset];
564 int room_header_pc = SnesToPc(room_header_addr);
565
566 formatter.BeginObject("room_header_address");
567 formatter.AddHexField("table_offset_pc", table_offset, 6);
568 formatter.AddHexField("snes_address", room_header_addr, 6);
569 formatter.AddHexField("pc_address", room_header_pc, 6);
570 formatter.EndObject();
571
572 // Read and display raw header bytes (14 bytes)
573 formatter.BeginArray("raw_bytes");
574 for (int i = 0; i < 14; ++i) {
575 if (room_header_pc + i < static_cast<int>(rom->size())) {
576 formatter.AddArrayItem(
577 absl::StrFormat("0x%02X", rom->data()[room_header_pc + i]));
578 }
579 }
580 formatter.EndArray();
581
582 // Decode the header bytes
583 if (room_header_pc >= 0 &&
584 room_header_pc + 13 < static_cast<int>(rom->size())) {
585 uint8_t byte0 = rom->data()[room_header_pc];
586 uint8_t byte1 = rom->data()[room_header_pc + 1];
587 uint8_t byte2 = rom->data()[room_header_pc + 2];
588 uint8_t byte3 = rom->data()[room_header_pc + 3];
589
590 formatter.BeginObject("decoded");
591 formatter.AddField("bg2", (byte0 >> 5) & 0x07);
592 formatter.AddField("collision", (byte0 >> 2) & 0x07);
593 formatter.AddField("is_light", (byte0 & 0x01) == 1);
594 formatter.AddField("palette", byte1 & 0x3F);
595 formatter.AddField("blockset", byte2);
596 formatter.AddField("spriteset", byte3);
597 formatter.AddField("effect", rom->data()[room_header_pc + 4]);
598 formatter.AddField("tag1", rom->data()[room_header_pc + 5]);
599 formatter.AddField("tag2", rom->data()[room_header_pc + 6]);
600 formatter.AddField("holewarp", rom->data()[room_header_pc + 9]);
601 formatter.AddField("stair1_room", rom->data()[room_header_pc + 10]);
602 formatter.AddField("stair2_room", rom->data()[room_header_pc + 11]);
603 formatter.AddField("stair3_room", rom->data()[room_header_pc + 12]);
604 formatter.AddField("stair4_room", rom->data()[room_header_pc + 13]);
605 formatter.EndObject();
606 } else {
607 formatter.AddField("error", "Room header address out of range");
608 }
609
610 formatter.EndObject();
611 return absl::OkStatus();
612}
613
615 Rom* rom, const resources::ArgumentParser& parser,
616 resources::OutputFormatter& formatter) {
617 // Build generator options (shared by single and batch modes)
619
620 // Parse --promote-switch X,Y pairs
621 auto switch_str = parser.GetString("promote-switch");
622 if (switch_str.has_value()) {
623 for (absl::string_view pair :
624 absl::StrSplit(switch_str.value(), ' ', absl::SkipEmpty())) {
625 std::vector<std::string> coords =
626 absl::StrSplit(pair, ',', absl::SkipEmpty());
627 if (coords.size() == 2) {
628 int sx, sy;
629 if (ParseHexString(coords[0], &sx) && ParseHexString(coords[1], &sy)) {
630 options.switch_promotions.emplace_back(sx, sy);
631 }
632 }
633 }
634 }
635
636 bool do_write = parser.HasFlag("write");
637 bool do_preserve_stops = parser.HasFlag("preserve-stops");
638 bool do_visualize = parser.HasFlag("visualize");
639
640 // Determine room list: --rooms (batch) or --room (single)
641 std::vector<int> room_ids;
642 auto rooms_arg = parser.GetString("rooms");
643 auto room_arg = parser.GetString("room");
644
645 if (rooms_arg.has_value()) {
646 // Batch mode: parse comma-separated hex room IDs
647 for (absl::string_view token :
648 absl::StrSplit(rooms_arg.value(), ',', absl::SkipEmpty())) {
649 int rid;
650 std::string token_str(token);
651 if (!ParseHexString(token_str, &rid)) {
652 return absl::InvalidArgumentError(absl::StrFormat(
653 "Invalid room ID '%s' in --rooms list. Must be hex.", token_str));
654 }
655 room_ids.push_back(rid);
656 }
657 if (room_ids.empty()) {
658 return absl::InvalidArgumentError("--rooms list is empty.");
659 }
660 } else if (room_arg.has_value()) {
661 // Single room mode (backwards compatible)
662 int rid;
663 if (!ParseHexString(room_arg.value(), &rid)) {
664 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
665 }
666 room_ids.push_back(rid);
667 } else {
668 return absl::InvalidArgumentError("Either --room or --rooms is required.");
669 }
670
671 bool is_batch = room_ids.size() > 1;
672
673 if (is_batch) {
674 // Batch mode: aggregate results with per-room detail
675 formatter.BeginObject("Batch Track Collision Generation");
676 formatter.AddField("mode", do_write ? "write" : "dry-run");
677 formatter.AddField("room_count", static_cast<int>(room_ids.size()));
678
679 int total_tiles = 0;
680 int total_stops = 0;
681 int total_corners = 0;
682 int total_switches = 0;
683 int rooms_succeeded = 0;
684
685 formatter.BeginArray("rooms");
686 for (int room_id : room_ids) {
687 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
688 room.LoadObjects();
689
690 auto result = zelda3::GenerateTrackCollision(&room, options);
691 if (!result.ok()) {
692 // Stop on first error and report which room failed
693 formatter.EndArray();
694 formatter.AddHexField("failed_room", room_id, 3);
695 formatter.AddField("error", std::string(result.status().message()));
696 formatter.EndObject();
697 return absl::InternalError(
698 absl::StrFormat("Generation failed for room 0x%03X: %s", room_id,
699 result.status().message()));
700 }
701
702 if (do_preserve_stops && do_write) {
703 auto existing = zelda3::LoadCustomCollisionMap(rom, room_id);
704 if (existing.ok() && existing->has_data) {
705 MergeStopTiles(*existing, result->collision_map);
706 }
707 }
708
709 formatter.BeginObject();
710 formatter.AddHexField("room_id", room_id, 3);
711 formatter.AddField("tiles_generated", result->tiles_generated);
712 formatter.AddField("stop_count", result->stop_count);
713 formatter.AddField("corner_count", result->corner_count);
714 formatter.AddField("switch_count", result->switch_count);
715 if (do_preserve_stops) {
716 formatter.AddField("stops_preserved", true);
717 }
718
719 if (do_visualize) {
720 formatter.AddField("visualization", result->ascii_visualization);
721 }
722
723 if (do_write) {
724 auto write_status =
725 zelda3::WriteTrackCollision(rom, room_id, result->collision_map);
726 if (!write_status.ok()) {
727 // Stop on first write error
728 formatter.AddField("write_error",
729 std::string(write_status.message()));
730 formatter.EndObject();
731 formatter.EndArray();
732 formatter.EndObject();
733 return absl::InternalError(
734 absl::StrFormat("Write failed for room 0x%03X: %s", room_id,
735 write_status.message()));
736 }
737 formatter.AddField("write_status", "success");
738 }
739
740 total_tiles += result->tiles_generated;
741 total_stops += result->stop_count;
742 total_corners += result->corner_count;
743 total_switches += result->switch_count;
744 rooms_succeeded++;
745
746 formatter.EndObject();
747 }
748 formatter.EndArray();
749
750 // Save ROM once after all rooms are written
751 if (do_write) {
752 Rom::SaveSettings save_settings;
753 save_settings.backup = true;
754 auto save_status = rom->SaveToFile(save_settings);
755 if (!save_status.ok()) {
756 formatter.AddField("save_error", std::string(save_status.message()));
757 } else {
758 formatter.AddField("save_status", "saved");
759 }
760 }
761
762 // Aggregated totals
763 formatter.BeginObject("totals");
764 formatter.AddField("rooms_succeeded", rooms_succeeded);
765 formatter.AddField("tiles_generated", total_tiles);
766 formatter.AddField("stop_count", total_stops);
767 formatter.AddField("corner_count", total_corners);
768 formatter.AddField("switch_count", total_switches);
769 formatter.EndObject();
770
771 formatter.EndObject();
772 } else {
773 // Single room mode (original behavior, backwards compatible)
774 int room_id = room_ids[0];
775
776 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
777 room.LoadObjects();
778
779 auto result = zelda3::GenerateTrackCollision(&room, options);
780 if (!result.ok()) {
781 return result.status();
782 }
783
784 if (do_preserve_stops && do_write) {
785 auto existing = zelda3::LoadCustomCollisionMap(rom, room_id);
786 if (existing.ok() && existing->has_data) {
787 MergeStopTiles(*existing, result->collision_map);
788 }
789 }
790
791 formatter.BeginObject("Track Collision Generation");
792 formatter.AddHexField("room_id", room_id, 3);
793 formatter.AddField("tiles_generated", result->tiles_generated);
794 formatter.AddField("stop_count", result->stop_count);
795 formatter.AddField("corner_count", result->corner_count);
796 formatter.AddField("switch_count", result->switch_count);
797 formatter.AddField("mode", do_write ? "write" : "dry-run");
798 if (do_preserve_stops) {
799 formatter.AddField("stops_preserved", true);
800 }
801
802 if (do_visualize || !do_write) {
803 formatter.AddField("visualization", result->ascii_visualization);
804 }
805
806 if (do_write) {
807 auto write_status =
808 zelda3::WriteTrackCollision(rom, room_id, result->collision_map);
809 if (!write_status.ok()) {
810 formatter.AddField("write_error", std::string(write_status.message()));
811 } else {
812 formatter.AddField("write_status", "success");
813 // Save ROM back to disk
814 Rom::SaveSettings save_settings;
815 save_settings.backup = true;
816 auto save_status = rom->SaveToFile(save_settings);
817 if (!save_status.ok()) {
818 formatter.AddField("save_error", std::string(save_status.message()));
819 } else {
820 formatter.AddField("save_status", "saved");
821 }
822 }
823 }
824
825 formatter.EndObject();
826 }
827
828 return absl::OkStatus();
829}
830
831} // namespace handlers
832} // namespace cli
833} // 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 SaveToFile(const SaveSettings &settings)
Definition rom.cc:291
auto data() const
Definition rom.h:139
auto size() const
Definition rom.h:138
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 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.
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::StatusOr< Room > GetRoom(int room_id)
absl::Status ImportOracleSpriteRegistry(const std::string &csv_content)
Import sprite labels from Oracle of Secrets registry.csv format.
Dungeon Room Entrance or Spawn Point.
static int DetermineObjectType(uint8_t b1, uint8_t b3)
void LoadChests()
Definition room.cc:2036
const std::vector< chest_data > & GetChests() const
Definition room.h:218
uint8_t blockset() const
Definition room.h:569
const std::vector< Door > & GetDoors() const
Definition room.h:294
TagKey tag2() const
Definition room.h:556
uint8_t floor2() const
Definition room.h:586
const std::vector< staircase > & GetStairs() const
Definition room.h:222
uint8_t palette() const
Definition room.h:571
TagKey tag1() const
Definition room.h:555
uint8_t spriteset() const
Definition room.h:570
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:214
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:314
void LoadObjects()
Definition room.cc:1292
EffectKey effect() const
Definition room.h:554
uint8_t floor1() const
Definition room.h:585
void LoadSprites()
Definition room.cc:1980
uint8_t layout_id() const
Definition room.h:572
int id() const
Definition room.h:566
absl::Status MaybeLoadSpriteRegistry(const resources::ArgumentParser &parser)
void MergeStopTiles(const zelda3::CustomCollisionMap &existing, zelda3::CustomCollisionMap &generated)
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:274
std::string GetRoomLabel(int id)
Convenience function to get a room label.
std::string GetItemLabel(int id)
Convenience function to get an item label.
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:253
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
Treasure chest.
Definition zelda.h:425
std::array< uint8_t, 64 *64 > tiles
std::vector< std::pair< int, int > > switch_promotions