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