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 <limits>
6#include <memory>
7#include <optional>
8#include <sstream>
9#include <vector>
10
11#include "absl/strings/ascii.h"
12#include "absl/strings/str_cat.h"
13#include "absl/strings/str_format.h"
14#include "absl/strings/str_split.h"
15#include "cli/util/hex_util.h"
18#include "core/hack_manifest.h"
19#include "rom/rom.h"
20#include "rom/snes.h"
21#include "rom/transaction.h"
22#include "util/macro.h"
27#include "zelda3/dungeon/room.h"
34
35namespace yaze {
36namespace cli {
37namespace handlers {
38
40
41namespace {
42
43constexpr uint8_t kStopTileMin = 0xB7;
44constexpr uint8_t kStopTileMax = 0xBA;
45
46bool IsStopTile(uint8_t v) {
47 return v >= kStopTileMin && v <= kStopTileMax;
48}
49
51 // Lightable torches and pushable blocks are synthesized from separate
52 // global tables after the encoded room object stream has been parsed.
53 return (object.options() & zelda3::ObjectOption::Torch) ==
55 (object.options() & zelda3::ObjectOption::Block) ==
57}
58
61 std::optional<zelda3::DungeonStreamLayout> allocator_layout;
62};
63
64absl::StatusOr<std::optional<RoomPropertyManifestContext>>
66 bool require_object_allocator) {
67 const auto manifest_path = parser.GetString("manifest");
68 if (!manifest_path.has_value()) {
69 return std::nullopt;
70 }
71
73 RETURN_IF_ERROR(context.manifest.LoadFromFile(*manifest_path));
74 if (!require_object_allocator) {
75 return std::optional<RoomPropertyManifestContext>(std::move(context));
76 }
77
78 const auto* manifest_layout = context.manifest.GetDungeonStreamLayout(
80 if (manifest_layout == nullptr) {
81 return absl::FailedPreconditionError(
82 "Manifest does not define dungeon_stream_regions.objects");
83 }
84 if (manifest_layout->strategy != core::DungeonWriteStrategy::kCopyOnWrite) {
85 return absl::FailedPreconditionError(
86 "dungeon_stream_regions.objects must use copy_on_write");
87 }
88 zelda3::DungeonStreamLayout allocator_layout;
89 ASSIGN_OR_RETURN(allocator_layout,
91 core::DungeonStreamType::kObjects, *manifest_layout));
92 context.allocator_layout = std::move(allocator_layout);
93 return std::optional<RoomPropertyManifestContext>(std::move(context));
94}
95
97 const core::HackManifest& manifest,
98 const std::vector<std::pair<uint32_t, uint32_t>>& ranges) {
99 if (!manifest.AnalyzePcWriteRanges(ranges).empty()) {
100 return absl::PermissionDeniedError("Write conflict with Hack Manifest");
101 }
102 return absl::OkStatus();
103}
104
106 const Rom& rom, int room_id, const RoomPropertyManifestContext& context) {
107 if (!context.allocator_layout.has_value()) {
108 return absl::FailedPreconditionError(
109 "Object-stream header manifest is missing allocator capability");
110 }
111
112 std::vector<std::pair<uint32_t, uint32_t>> ranges;
113
114 uint32_t pointer_table_snes = 0;
115 ASSIGN_OR_RETURN(pointer_table_snes,
117 const uint32_t pointer_table_pc = SnesToPc(pointer_table_snes);
118 const uint64_t pointer_slot = static_cast<uint64_t>(pointer_table_pc) +
119 static_cast<uint64_t>(room_id) * 3u;
120 if (pointer_slot + 3u > rom.size()) {
121 return absl::OutOfRangeError(
122 "Selected object pointer slot is outside the ROM");
123 }
124
125 uint32_t stream_snes = 0;
126 ASSIGN_OR_RETURN(stream_snes, rom.ReadLong(static_cast<int>(pointer_slot)));
127 const uint32_t stream_pc = SnesToPc(stream_snes);
128 if (static_cast<uint64_t>(stream_pc) + 2u > rom.size()) {
129 return absl::OutOfRangeError(
130 "Selected object stream header is outside the ROM");
131 }
132 ranges.emplace_back(stream_pc, stream_pc + 2u);
133
134 for (const auto& range : context.allocator_layout->allocation_ranges) {
135 ranges.emplace_back(range.begin, range.end);
136 }
137 const uint64_t cow_pointer_slot =
138 static_cast<uint64_t>(context.allocator_layout->pointer_table_pc) +
139 static_cast<uint64_t>(room_id) * 3u;
140 if (cow_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
141 return absl::OutOfRangeError("Selected COW pointer range overflows");
142 }
143 ranges.emplace_back(static_cast<uint32_t>(cow_pointer_slot),
144 static_cast<uint32_t>(cow_pointer_slot + 3u));
145
146 const uint64_t door_pointer_slot =
147 static_cast<uint64_t>(zelda3::kDoorPointers) +
148 static_cast<uint64_t>(room_id) * 3u;
149 if (door_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
150 return absl::OutOfRangeError("Selected door pointer range overflows");
151 }
152 ranges.emplace_back(static_cast<uint32_t>(door_pointer_slot),
153 static_cast<uint32_t>(door_pointer_slot + 3u));
154
155 return ValidateManifestWriteRanges(context.manifest, ranges);
156}
157
158absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
159CollectRoomHeaderWriteRanges(const Rom& rom, int room_id) {
160 uint32_t header_pointer_snes = 0;
161 ASSIGN_OR_RETURN(header_pointer_snes,
163 const uint32_t header_pointer_pc = SnesToPc(header_pointer_snes);
164 const uint64_t table_offset = static_cast<uint64_t>(header_pointer_pc) +
165 static_cast<uint64_t>(room_id) * 2u;
166 if (table_offset + 2u > rom.size()) {
167 return absl::OutOfRangeError(
168 "Selected room header pointer slot is outside the ROM");
169 }
170
171 uint16_t header_offset = 0;
172 ASSIGN_OR_RETURN(header_offset, rom.ReadWord(static_cast<int>(table_offset)));
173 uint8_t header_bank = 0;
175 const uint32_t header_pc =
176 SnesToPc((static_cast<uint32_t>(header_bank) << 16) | header_offset);
177 if (static_cast<uint64_t>(header_pc) + 14u > rom.size()) {
178 return absl::OutOfRangeError("Selected room header is outside the ROM");
179 }
180
181 const uint64_t message_pc =
182 static_cast<uint64_t>(zelda3::kMessagesIdDungeon) +
183 static_cast<uint64_t>(room_id) * 2u;
184 if (message_pc + 2u > rom.size()) {
185 return absl::OutOfRangeError("Selected room message ID is outside the ROM");
186 }
187
188 return std::vector<std::pair<uint32_t, uint32_t>>{
189 {header_pc, header_pc + 14u},
190 {static_cast<uint32_t>(message_pc),
191 static_cast<uint32_t>(message_pc + 2u)}};
192}
193
195 const Rom& rom, int room_id, const core::HackManifest& manifest) {
196 std::vector<std::pair<uint32_t, uint32_t>> ranges;
197 ASSIGN_OR_RETURN(ranges, CollectRoomHeaderWriteRanges(rom, room_id));
198 return ValidateManifestWriteRanges(manifest, ranges);
199}
200
201// Merge stop tiles from |existing| into |generated| without overwriting
202// track tiles that were just produced. Stop tiles in |existing| that sit on
203// cells that are zero in |generated| are copied across unchanged.
205 zelda3::CustomCollisionMap& generated) {
206 for (int i = 0; i < static_cast<int>(existing.tiles.size()); ++i) {
207 if (IsStopTile(existing.tiles[i]) && generated.tiles[i] == 0) {
208 generated.tiles[i] = existing.tiles[i];
209 }
210 }
211}
212
213absl::Status SaveTrackCollisionRom(Rom* rom,
214 const resources::ArgumentParser& parser,
215 resources::OutputFormatter& formatter) {
216 if (parser.HasFlag("mock-rom")) {
217 formatter.AddField("save_status", "mock-rom-skipped");
218 return absl::OkStatus();
219 }
220
221 Rom::SaveSettings save_settings;
222 save_settings.require_backup = true;
223 auto save_status = rom->SaveToFile(save_settings);
224 if (!save_status.ok()) {
225 formatter.AddField("save_error", std::string(save_status.message()));
226 return save_status;
227 }
228
229 formatter.AddField("save_status", "saved");
230 return absl::OkStatus();
231}
232
233// Helper to load sprite registry from file if --sprite-registry flag is provided
235 auto registry_path = parser.GetString("sprite-registry");
236 if (!registry_path.has_value()) {
237 return absl::OkStatus(); // Flag not provided, nothing to load
238 }
239
240 std::ifstream file(registry_path.value());
241 if (!file.is_open()) {
242 return absl::NotFoundError(absl::StrFormat(
243 "Could not open sprite registry: %s", registry_path.value()));
244 }
245
246 std::stringstream buffer;
247 buffer << file.rdbuf();
249}
250
251} // namespace
252
253absl::Status WriteDungeonSpawnPointReport(Rom* rom, int spawn_id,
255 std::string_view object_title) {
256 if (spawn_id < 0 || spawn_id >= zelda3::kNumDungeonSpawnPoints) {
257 return absl::InvalidArgumentError(absl::StrFormat(
258 "Dungeon spawn point ID %d out of range (0x00-0x06).", spawn_id));
259 }
260
261 auto spawn_or = zelda3::DungeonSpawnPoint::Load(*rom, spawn_id);
262 if (!spawn_or.ok()) {
263 return spawn_or.status();
264 }
265 const auto& spawn = spawn_or.value();
266
267 if (!object_title.empty()) {
268 formatter.BeginObject(std::string(object_title));
269 }
270
271 formatter.AddHexField("spawn_id", spawn_id, 2);
272 formatter.AddField("is_spawn_point", true);
273 formatter.AddHexField("room_id", spawn.room_id, 4);
274 formatter.AddHexField("dungeon_id", spawn.dungeon_id, 2);
275 formatter.AddHexField("quadrant", spawn.quadrant, 2);
276 formatter.AddHexField("overworld_door_tilemap", spawn.overworld_door_tilemap,
277 4);
278 formatter.AddHexField("entrance_id", spawn.entrance_id, 4);
279
280 formatter.BeginObject("position");
281 formatter.AddField("x", static_cast<int>(spawn.x_coordinate));
282 formatter.AddField("y", static_cast<int>(spawn.y_coordinate));
283 formatter.EndObject();
284
285 formatter.BeginObject("camera");
286 formatter.AddField("horizontal_scroll",
287 static_cast<int>(spawn.horizontal_scroll));
288 formatter.AddField("vertical_scroll",
289 static_cast<int>(spawn.vertical_scroll));
290 formatter.AddField("trigger_x", static_cast<int>(spawn.camera_trigger_x));
291 formatter.AddField("trigger_y", static_cast<int>(spawn.camera_trigger_y));
292 formatter.EndObject();
293
294 formatter.BeginObject("properties");
295 formatter.AddHexField("main_gfx", spawn.main_gfx, 2);
296 formatter.AddHexField("floor", spawn.floor, 2);
297 formatter.AddHexField("layer", spawn.layer, 2);
298 formatter.AddHexField("camera_scroll_controller",
299 spawn.camera_scroll_controller, 2);
300 formatter.AddHexField("song", spawn.song, 2);
301 formatter.EndObject();
302
303 formatter.BeginObject("camera_boundaries");
304 formatter.AddHexField("qn", spawn.camera_scroll_boundaries[0], 2);
305 formatter.AddHexField("fn", spawn.camera_scroll_boundaries[1], 2);
306 formatter.AddHexField("qs", spawn.camera_scroll_boundaries[2], 2);
307 formatter.AddHexField("fs", spawn.camera_scroll_boundaries[3], 2);
308 formatter.AddHexField("qw", spawn.camera_scroll_boundaries[4], 2);
309 formatter.AddHexField("fw", spawn.camera_scroll_boundaries[5], 2);
310 formatter.AddHexField("qe", spawn.camera_scroll_boundaries[6], 2);
311 formatter.AddHexField("fe", spawn.camera_scroll_boundaries[7], 2);
312 formatter.EndObject();
313
314 if (!object_title.empty()) {
315 formatter.EndObject();
316 }
317 return absl::OkStatus();
318}
319
321 Rom* rom, const resources::ArgumentParser& parser,
322 resources::OutputFormatter& formatter) {
323 // Load custom sprite registry if provided (e.g., Oracle of Secrets)
324 auto registry_status = MaybeLoadSpriteRegistry(parser);
325 if (!registry_status.ok()) {
326 return registry_status;
327 }
328
329 auto room_id_str = parser.GetString("room").value();
330
331 int room_id;
332 if (!ParseHexString(room_id_str, &room_id)) {
333 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
334 }
335
336 formatter.BeginObject("Dungeon Room Sprites");
337 formatter.AddField("room_id", room_id);
338
339 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
340 room.LoadSprites();
341 const auto& sprites = room.GetSprites();
342
343 formatter.AddField("total_sprites", static_cast<int>(sprites.size()));
344 formatter.AddField("status", "success");
345
346 formatter.BeginArray("sprites");
347 for (const auto& sprite : sprites) {
348 formatter.BeginObject();
349 formatter.AddHexField("sprite_id", sprite.id(), 2);
350 util::AddSpriteNameFields(formatter, sprite.id());
351 formatter.AddField("x", sprite.x());
352 formatter.AddField("y", sprite.y());
353 formatter.AddField("subtype", sprite.subtype());
354 formatter.AddField("layer", sprite.layer());
355 if (sprite.key_drop() > 0) {
356 formatter.AddField("key_drop", sprite.key_drop());
357 }
358 formatter.EndObject();
359 }
360 formatter.EndArray();
361 formatter.EndObject();
362
363 return absl::OkStatus();
364}
365
367 Rom* rom, const resources::ArgumentParser& parser,
368 resources::OutputFormatter& formatter) {
369 // Load custom sprite registry if provided (e.g., Oracle of Secrets)
370 auto registry_status = MaybeLoadSpriteRegistry(parser);
371 if (!registry_status.ok()) {
372 return registry_status;
373 }
374
375 auto room_id_str = parser.GetString("room").value();
376
377 int room_id;
378 if (!ParseHexString(room_id_str, &room_id)) {
379 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
380 }
381
382 formatter.AddField("room_id", room_id);
383
384 // Load full room to get objects, doors, and stairs
385 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
386 const bool include_objects = parser.HasFlag("include-objects");
387 const auto& tile_objects = room.GetTileObjects();
388
389 formatter.AddField("status", "success");
390 formatter.AddField("name", absl::StrFormat("Room %d", room.id()));
391 formatter.AddField("room_name", zelda3::GetRoomLabel(room.id()));
392 formatter.AddField("room_id", room.id());
393 formatter.AddField("room_type", "Dungeon Room");
394
395 // Room properties from Room data
396 formatter.BeginObject("properties");
397 formatter.AddField("blockset", room.blockset());
398 formatter.AddField("spriteset", room.spriteset());
399 formatter.AddField("palette", room.palette());
400 formatter.AddField("layout", room.layout_id());
401 formatter.AddField("floor1", room.floor1());
402 formatter.AddField("floor2", room.floor2());
403 formatter.AddField("effect", static_cast<int>(room.effect()));
404 formatter.AddField("tag1", static_cast<int>(room.tag1()));
405 formatter.AddField("tag2", static_cast<int>(room.tag2()));
406
407 // Check object counts for simple heuristics
408 const int object_count = include_objects
409 ? static_cast<int>(std::count_if(
410 tile_objects.begin(), tile_objects.end(),
411 IsEncodedRoomStreamObject))
412 : static_cast<int>(tile_objects.size());
413 formatter.AddField("object_count", object_count);
414
415 formatter.EndObject();
416
417 if (include_objects) {
418 formatter.BeginArray("objects");
419 int stream_index = 0;
420 for (const auto& object : tile_objects) {
421 if (!IsEncodedRoomStreamObject(object)) {
422 continue;
423 }
424
425 const auto encoded = object.EncodeObjectToBytes();
426 formatter.BeginObject();
427 formatter.AddHexField("object_id", static_cast<uint16_t>(object.id_), 3);
428 formatter.AddField("entity_kind", "room_object");
429 formatter.AddField("name", zelda3::GetObjectName(object.id_));
430 formatter.AddField("name_source", "vanilla_object_catalog");
431 formatter.AddField("coordinate_space", "room_tile8");
433 encoded.b1, encoded.b3));
434 formatter.AddField("x", static_cast<int>(object.x()));
435 formatter.AddField("y", static_cast<int>(object.y()));
436 formatter.AddField("size", static_cast<int>(object.size()));
437 formatter.AddField("stream_index", stream_index++);
438 formatter.EndObject();
439 }
440 formatter.EndArray();
441 }
442
443 // Export Sprites (project names when --project-context renames the slot)
444 room.LoadSprites();
445 formatter.BeginArray("sprites");
446 for (const auto& sprite : room.GetSprites()) {
447 formatter.BeginObject();
448 formatter.AddHexField("sprite_id", sprite.id(), 2);
449 util::AddSpriteNameFields(formatter, sprite.id());
450 formatter.AddField("x", sprite.x());
451 formatter.AddField("y", sprite.y());
452 formatter.AddField("subtype", sprite.subtype());
453 formatter.AddField("layer", sprite.layer());
454 formatter.EndObject();
455 }
456 formatter.EndArray();
457
458 // Export Doors
459 formatter.BeginArray("doors");
460 const auto& doors = room.GetDoors();
461 for (size_t index = 0; index < doors.size(); ++index) {
462 const auto& door = doors[index];
463 formatter.BeginObject();
464 formatter.AddField("index", static_cast<int>(index));
465 formatter.AddField("entity_kind", "door");
466 formatter.AddField("position", door.position);
467 formatter.AddField("direction", std::string(door.GetDirectionName()));
468 formatter.AddField("type", std::string(door.GetTypeName()));
469 formatter.AddHexField("type_id", static_cast<uint8_t>(door.type), 2);
470 auto [tx, ty] = door.GetTileCoords();
471 formatter.AddField("tile_x", tx);
472 formatter.AddField("tile_y", ty);
473 const auto [px, py, width, height] = door.GetBounds();
474 formatter.BeginObject("bounds_px");
475 formatter.AddField("x", px);
476 formatter.AddField("y", py);
477 formatter.AddField("width", width);
478 formatter.AddField("height", height);
479 formatter.AddField("coordinate_space", "room_pixels");
480 formatter.EndObject();
481 formatter.EndObject();
482 }
483 formatter.EndArray();
484
485 // Export Staircases
486 formatter.BeginArray("staircases");
487 for (const auto& stair : room.GetStairs()) {
488 formatter.BeginObject();
489 formatter.AddField("tile_x",
490 stair.id); // 'id' field stores X in struct staircase
491 formatter.AddField(
492 "tile_y", stair.room); // 'room' field stores Y in struct staircase
493 formatter.AddField("label", stair.label);
494 formatter.EndObject();
495 }
496 formatter.EndArray();
497
498 if (parser.HasFlag("include-staircase-resolution")) {
499 zelda3::RoomLayout layout(rom);
500 const auto layout_status = layout.LoadLayout(room.layout_id());
501 auto input = zelda3::MakeRoomCollisionInput(room);
502 const auto& prefix = layout.GetObjects();
503 input.objects.insert(input.objects.begin(), prefix.begin(), prefix.end());
504 const auto resolutions = zelda3::ResolveVanillaStaircaseSlots(*rom, input);
505 formatter.BeginObject("staircase_resolution");
506 formatter.AddField("model", "vanilla_collision_preview");
507 formatter.AddField("runtime_qualified", false);
508 formatter.AddField("layout_loaded", layout_status.ok());
509 formatter.AddField("custom_collision", room.has_custom_collision());
510 formatter.BeginArray("objects");
511 for (const auto& resolution : resolutions) {
512 if (resolution.object_index < prefix.size())
513 continue;
514 const auto index = resolution.object_index - prefix.size();
515 const auto& object = tile_objects[index];
516 formatter.BeginObject();
517 formatter.AddField("object_index", static_cast<int>(index));
518 formatter.AddHexField("object_id", object.id_, 3);
519 formatter.AddField("x", object.x());
520 formatter.AddField("y", object.y());
521 formatter.AddField("stream_index", object.GetLayerValue());
522 const char* status = "trigger_unavailable";
523 switch (resolution.status) {
525 status = "resolved";
526 break;
528 break;
530 status = "overlapping_stairs";
531 break;
533 status = "outside_room";
534 break;
536 status = "too_many_stairs";
537 break;
538 }
539 formatter.AddField("vanilla_status", status);
540 if (resolution.slot && layout_status.ok()) {
541 formatter.AddField("vanilla_slot", *resolution.slot);
542 formatter.AddHexField("vanilla_destination_room",
543 room.staircase_destination_room(*resolution.slot),
544 3);
545 formatter.AddField("vanilla_arrival_plane",
546 room.staircase_plane(*resolution.slot));
547 }
548 if (!layout_status.ok())
549 status = "layout_unavailable";
550 if (room.has_custom_collision())
551 status = "custom_collision";
552 formatter.AddField("status", status);
553 if (resolution.slot && layout_status.ok() &&
554 !room.has_custom_collision()) {
555 formatter.AddField("slot", *resolution.slot);
556 formatter.AddHexField("destination_room",
557 room.staircase_destination_room(*resolution.slot),
558 3);
559 formatter.AddField("arrival_plane",
560 room.staircase_plane(*resolution.slot));
561 }
562 formatter.EndObject();
563 }
564 formatter.EndArray();
565 formatter.EndObject();
566 }
567
568 // Export Chests
569 formatter.BeginArray("chests");
570 for (const auto& chest : room.GetChests()) {
571 formatter.BeginObject();
572 formatter.AddHexField("item_id", chest.id, 2);
573 formatter.AddField("item_name", zelda3::GetItemReceiptLabel(chest.id));
574 formatter.AddField("item_name_source",
576 formatter.AddField("is_big_chest", chest.size);
577 formatter.EndObject();
578 }
579 formatter.EndArray();
580
581 return absl::OkStatus();
582}
583
585 Rom* rom, const resources::ArgumentParser& parser,
586 resources::OutputFormatter& formatter) {
587 auto room_id_opt = parser.GetString("room");
588
589 bool has_room_filter = room_id_opt.has_value();
590 int room_filter = -1;
591 if (has_room_filter) {
592 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
593 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
594 }
595 }
596
597 int total_rooms = 0;
598 int rooms_with_chests = 0;
599 int total_chests = 0;
600 std::map<int, int> item_counts;
601
602 formatter.BeginObject("Dungeon Chests");
603 formatter.AddField("room_filter", has_room_filter
604 ? absl::StrFormat("0x%02X", room_filter)
605 : "all");
606
607 formatter.BeginArray("rooms");
608 int start_room = has_room_filter ? room_filter : 0;
609 int end_room = has_room_filter ? room_filter : zelda3::kNumberOfRooms - 1;
610
611 for (int room_id = start_room; room_id <= end_room; ++room_id) {
612 total_rooms++;
613 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
614 room.LoadChests();
615
616 const auto& chests = room.GetChests();
617 if (chests.empty()) {
618 continue;
619 }
620
621 rooms_with_chests++;
622 total_chests += static_cast<int>(chests.size());
623
624 formatter.BeginObject();
625 formatter.AddField("room_id", absl::StrFormat("0x%02X", room_id));
626 formatter.AddField("room_name", zelda3::GetRoomLabel(room_id));
627 formatter.AddField("chest_count", static_cast<int>(chests.size()));
628
629 formatter.BeginArray("chests");
630 int chest_index = 0;
631 for (const auto& chest : chests) {
632 formatter.BeginObject();
633 formatter.AddField("index", chest_index++);
634 formatter.AddHexField("item_id", chest.id, 2);
635 formatter.AddField("item_name", zelda3::GetItemReceiptLabel(chest.id));
636 formatter.AddField("item_name_source",
638 formatter.AddField("is_big_chest", chest.size);
639 formatter.EndObject();
640
641 item_counts[chest.id]++;
642 }
643 formatter.EndArray();
644 formatter.EndObject();
645 }
646 formatter.EndArray();
647
648 formatter.BeginObject("summary");
649 formatter.AddField("total_rooms", total_rooms);
650 formatter.AddField("rooms_with_chests", rooms_with_chests);
651 formatter.AddField("total_chests", total_chests);
652 formatter.AddField("unique_items", static_cast<int>(item_counts.size()));
653
654 formatter.BeginArray("duplicate_items");
655 for (const auto& [item_id, count] : item_counts) {
656 if (count < 2) {
657 continue;
658 }
659 formatter.BeginObject();
660 formatter.AddHexField("item_id", item_id, 2);
661 formatter.AddField("item_name", zelda3::GetItemReceiptLabel(item_id));
662 formatter.AddField("item_name_source",
664 formatter.AddField("count", count);
665 formatter.EndObject();
666 }
667 formatter.EndArray();
668 formatter.EndObject();
669
670 formatter.EndObject();
671 return absl::OkStatus();
672}
673
675 Rom* rom, const resources::ArgumentParser& parser,
676 resources::OutputFormatter& formatter) {
677 auto entrance_id_str = parser.GetString("entrance").value();
678 bool is_spawn_point = parser.HasFlag("spawn");
679
680 int entrance_id;
681 if (!ParseHexString(entrance_id_str, &entrance_id)) {
682 return absl::InvalidArgumentError(
683 "Invalid entrance ID format. Must be hex.");
684 }
685
686 if (is_spawn_point) {
687 return WriteDungeonSpawnPointReport(rom, entrance_id, formatter, "");
688 }
689
690 if (entrance_id < 0 || entrance_id >= zelda3::kNumRegularDungeonEntrances) {
691 return absl::OutOfRangeError("Regular dungeon entrance ID is out of range");
692 }
693 zelda3::RoomEntrance entrance(rom, static_cast<uint8_t>(entrance_id), false);
694 RETURN_IF_ERROR(entrance.load_status());
695
696 formatter.AddField("entrance_id", absl::StrFormat("0x%02X", entrance_id));
697 formatter.AddField("is_spawn_point", is_spawn_point);
698 formatter.AddField("room_id", absl::StrFormat("0x%04X", entrance.room_));
699 formatter.AddField("exit_id", absl::StrFormat("0x%04X", entrance.exit_));
700
701 formatter.BeginObject("position");
702 formatter.AddField("x", entrance.x_position_);
703 formatter.AddField("y", entrance.y_position_);
704 formatter.EndObject();
705
706 formatter.BeginObject("camera");
707 formatter.AddField("x", entrance.camera_x_);
708 formatter.AddField("y", entrance.camera_y_);
709 formatter.AddField("trigger_x", entrance.camera_trigger_x_);
710 formatter.AddField("trigger_y", entrance.camera_trigger_y_);
711 formatter.EndObject();
712
713 formatter.BeginObject("properties");
714 formatter.AddField("blockset", absl::StrFormat("0x%02X", entrance.blockset_));
715 formatter.AddField("floor", absl::StrFormat("0x%02X", entrance.floor_));
716 formatter.AddField("dungeon_id",
717 absl::StrFormat("0x%02X", entrance.dungeon_id_));
718 formatter.AddField("door", absl::StrFormat("0x%02X", entrance.door_));
719 formatter.AddField("ladder_bg",
720 absl::StrFormat("0x%02X", entrance.ladder_bg_));
721 formatter.AddField("scrolling",
722 absl::StrFormat("0x%02X", entrance.scrolling_));
723 formatter.AddField("scroll_quadrant",
724 absl::StrFormat("0x%02X", entrance.scroll_quadrant_));
725 formatter.AddField("music", absl::StrFormat("0x%02X", entrance.music_));
726 formatter.EndObject();
727
728 formatter.BeginObject("camera_boundaries");
729 formatter.AddField("qn",
730 absl::StrFormat("0x%02X", entrance.camera_boundary_qn_));
731 formatter.AddField("fn",
732 absl::StrFormat("0x%02X", entrance.camera_boundary_fn_));
733 formatter.AddField("qs",
734 absl::StrFormat("0x%02X", entrance.camera_boundary_qs_));
735 formatter.AddField("fs",
736 absl::StrFormat("0x%02X", entrance.camera_boundary_fs_));
737 formatter.AddField("qw",
738 absl::StrFormat("0x%02X", entrance.camera_boundary_qw_));
739 formatter.AddField("fw",
740 absl::StrFormat("0x%02X", entrance.camera_boundary_fw_));
741 formatter.AddField("qe",
742 absl::StrFormat("0x%02X", entrance.camera_boundary_qe_));
743 formatter.AddField("fe",
744 absl::StrFormat("0x%02X", entrance.camera_boundary_fe_));
745 formatter.EndObject();
746
747 return absl::OkStatus();
748}
749
751 Rom* rom, const resources::ArgumentParser& parser,
752 resources::OutputFormatter& formatter) {
753 auto room_id_str = parser.GetString("room").value();
754
755 int room_id;
756 if (!ParseHexString(room_id_str, &room_id)) {
757 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
758 }
759
760 formatter.BeginObject("Dungeon Export");
761 formatter.AddField("room_id", room_id);
762
763 // Use existing dungeon system
764 zelda3::DungeonEditorSystem dungeon_editor(rom);
765 auto room_or = dungeon_editor.GetRoom(room_id);
766 if (!room_or.ok()) {
767 formatter.AddField("status", "error");
768 formatter.AddField("error", room_or.status().ToString());
769 formatter.EndObject();
770 return room_or.status();
771 }
772
773 auto& room = room_or.value();
774
775 // Export room data
776 formatter.AddField("status", "success");
777 formatter.AddField("room_width", "Unknown");
778 formatter.AddField("room_height", "Unknown");
779 formatter.AddField("room_name", absl::StrFormat("Room %d", room.id()));
780
781 // Add room data as JSON
782 formatter.BeginObject("room_data");
783 formatter.AddField("tiles", "Room tile data would be exported here");
784 formatter.AddField("sprites", "Room sprite data would be exported here");
785 formatter.AddField("doors", "Room door data would be exported here");
786 formatter.EndObject();
787
788 formatter.EndObject();
789
790 return absl::OkStatus();
791}
792
794 Rom* rom, const resources::ArgumentParser& parser,
795 resources::OutputFormatter& formatter) {
796 auto room_id_str = parser.GetString("room").value();
797
798 int room_id;
799 if (!ParseHexString(room_id_str, &room_id)) {
800 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
801 }
802
803 formatter.BeginObject("Dungeon Room Objects");
804 formatter.AddField("room_id", room_id);
805
806 // Use existing dungeon system
807 zelda3::DungeonEditorSystem dungeon_editor(rom);
808 auto room_or = dungeon_editor.GetRoom(room_id);
809 if (!room_or.ok()) {
810 formatter.AddField("status", "error");
811 formatter.AddField("error", room_or.status().ToString());
812 formatter.EndObject();
813 return room_or.status();
814 }
815
816 auto& room = room_or.value();
817
818 // Load objects if not already loaded (GetTileObjects might be empty otherwise)
819 room.LoadObjects();
820
821 const auto& objects = room.GetTileObjects();
822 formatter.AddField("total_objects", static_cast<int>(objects.size()));
823 formatter.AddField("status", "success");
824
825 formatter.BeginArray("objects");
826 for (const auto& obj : objects) {
827 formatter.BeginObject("");
828 formatter.AddField("id", obj.id_);
829 formatter.AddField("id_hex", absl::StrFormat("0x%04X", obj.id_));
830 formatter.AddField("x", obj.x_);
831 formatter.AddField("y", obj.y_);
832 formatter.AddField("size", obj.size_);
833 formatter.AddField("layer", static_cast<int>(obj.layer_));
834 // Add decoded type info if available
835 int type = zelda3::RoomObject::DetermineObjectType((obj.id_ & 0xFF),
836 (obj.id_ >> 8));
837 formatter.AddField("type", type);
838 formatter.EndObject();
839 }
840 formatter.EndArray();
841 formatter.EndObject();
842
843 return absl::OkStatus();
844}
845
847 Rom* rom, const resources::ArgumentParser& parser,
848 resources::OutputFormatter& formatter) {
849 auto room_id_str = parser.GetString("room").value();
850
851 int room_id;
852 if (!ParseHexString(room_id_str, &room_id)) {
853 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
854 }
855
856 formatter.BeginObject("Dungeon Room Tiles");
857 formatter.AddField("room_id", room_id);
858
859 // Use existing dungeon system
860 zelda3::DungeonEditorSystem dungeon_editor(rom);
861 auto room_or = dungeon_editor.GetRoom(room_id);
862 if (!room_or.ok()) {
863 formatter.AddField("status", "error");
864 formatter.AddField("error", room_or.status().ToString());
865 formatter.EndObject();
866 return room_or.status();
867 }
868
869 auto& room = room_or.value();
870
871 room.LoadObjects();
872
873 formatter.AddField("room_width", 64);
874 formatter.AddField("room_height", 64);
875 formatter.AddField("total_tiles", 64 * 64);
876 formatter.AddField("has_custom_collision", room.has_custom_collision());
877 formatter.AddField("object_count",
878 static_cast<int>(room.GetTileObjects().size()));
879 formatter.AddField("status", "success");
880
881 // Emit tile rows as compact hex strings to keep output manageable while still
882 // returning deterministic room-tile data.
883 formatter.BeginArray("collision_rows");
884 const auto& collision = room.custom_collision().tiles;
885 for (int y = 0; y < 64; ++y) {
886 std::string row;
887 row.reserve(64 * 3);
888 for (int x = 0; x < 64; ++x) {
889 if (!row.empty()) {
890 row.push_back(' ');
891 }
892 absl::StrAppend(&row, absl::StrFormat("%02X", collision[y * 64 + x]));
893 }
894 formatter.AddArrayItem(row);
895 }
896 formatter.EndArray();
897 formatter.EndObject();
898
899 return absl::OkStatus();
900}
901
903 Rom* rom, const resources::ArgumentParser& parser,
904 resources::OutputFormatter& formatter) {
905 auto room_id_str = parser.GetString("room").value();
906 auto property = parser.GetString("property").value();
907 auto value = parser.GetString("value").value();
908
909 int room_id;
910 if (!ParseHexString(room_id_str, &room_id)) {
911 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
912 }
913
914 formatter.BeginObject("Dungeon Room Property Set");
915 formatter.AddField("room_id", room_id);
916 formatter.AddField("property", property);
917 formatter.AddField("value", value);
918
919 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
920 const absl::Status room_status =
921 absl::InvalidArgumentError("Invalid room ID");
922 formatter.AddField("status", "error");
923 formatter.AddField("error", room_status.ToString());
924 formatter.EndObject();
925 return room_status;
926 }
927
928 int parsed_value = 0;
929 if (!ParseHexString(value, &parsed_value)) {
930 const absl::Status value_status =
931 absl::InvalidArgumentError(absl::StrFormat(
932 "Invalid value format: %s (expected integer/hex)", value));
933 formatter.AddField("status", "error");
934 formatter.AddField("error", std::string(value_status.message()));
935 formatter.EndObject();
936 return value_status;
937 }
938
939 const std::string prop = absl::AsciiStrToLower(property);
940 const bool is_layout = prop == "layout" || prop == "layout_id";
941 const bool is_floor = prop == "floor1" || prop == "floor2";
942 const bool is_object_stream_header = is_layout || is_floor;
943 const bool is_room_header = prop == "palette" || prop == "blockset" ||
944 prop == "spriteset" || prop == "effect" ||
945 prop == "tag1" || prop == "tag2" ||
946 prop == "holewarp";
947 const int property_max = is_layout ? 0x07 : (is_floor ? 0x0F : 0xFF);
948 if (is_object_stream_header &&
949 (parsed_value < 0 || parsed_value > property_max)) {
950 const absl::Status property_status =
951 absl::InvalidArgumentError(absl::StrFormat(
952 "Property %s must be in range 0..%d", property, property_max));
953 formatter.AddField("status", "error");
954 formatter.AddField("error", std::string(property_status.message()));
955 formatter.EndObject();
956 return property_status;
957 }
958
959 std::optional<RoomPropertyManifestContext> manifest_context;
960 if (is_object_stream_header || is_room_header) {
961 auto context_or =
962 LoadRoomPropertyManifestContext(parser, is_object_stream_header);
963 if (!context_or.ok()) {
964 formatter.AddField("status", "error");
965 formatter.AddField("error", std::string(context_or.status().message()));
966 formatter.EndObject();
967 return context_or.status();
968 }
969 manifest_context = std::move(context_or).value();
970 if (manifest_context.has_value()) {
971 const absl::Status manifest_status =
972 is_object_stream_header
973 ? ValidateObjectStreamHeaderManifestConflicts(*rom, room_id,
974 *manifest_context)
975 : ValidateRoomHeaderManifestConflicts(*rom, room_id,
976 manifest_context->manifest);
977 if (!manifest_status.ok()) {
978 formatter.AddField("status", "error");
979 formatter.AddField("error", std::string(manifest_status.message()));
980 formatter.EndObject();
981 return manifest_status;
982 }
983 formatter.AddField("manifest", *parser.GetString("manifest"));
984 if (is_object_stream_header) {
985 formatter.AddField("allocator_capability", "copy_on_write");
986 }
987 }
988 }
989
990 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
991 ScopedRomTransaction transaction(*rom);
992 if (prop == "palette") {
993 room.SetPalette(static_cast<uint8_t>(parsed_value & 0xFF));
994 } else if (prop == "blockset") {
995 room.SetBlockset(static_cast<uint8_t>(parsed_value & 0xFF));
996 } else if (prop == "spriteset") {
997 room.SetSpriteset(static_cast<uint8_t>(parsed_value & 0xFF));
998 } else if (is_layout) {
999 room.SetLayoutId(static_cast<uint8_t>(parsed_value));
1000 } else if (prop == "floor1") {
1001 room.set_floor1(static_cast<uint8_t>(parsed_value));
1002 } else if (prop == "floor2") {
1003 room.set_floor2(static_cast<uint8_t>(parsed_value));
1004 } else if (prop == "effect") {
1005 room.SetEffect(static_cast<zelda3::EffectKey>(parsed_value & 0xFF));
1006 } else if (prop == "tag1") {
1007 room.SetTag1(static_cast<zelda3::TagKey>(parsed_value & 0xFF));
1008 } else if (prop == "tag2") {
1009 room.SetTag2(static_cast<zelda3::TagKey>(parsed_value & 0xFF));
1010 } else if (prop == "holewarp") {
1011 room.SetHolewarp(static_cast<uint8_t>(parsed_value & 0xFF));
1012 } else {
1013 const absl::Status property_status = absl::InvalidArgumentError(
1014 absl::StrFormat("Unsupported property: %s", property));
1015 formatter.AddField("status", "error");
1016 formatter.AddField("error", std::string(property_status.message()));
1017 formatter.EndObject();
1018 return property_status;
1019 }
1020
1021 const absl::Status write_status =
1022 is_object_stream_header
1024 manifest_context.has_value() &&
1025 manifest_context->allocator_layout.has_value()
1026 ? &*manifest_context->allocator_layout
1027 : nullptr)
1028 : room.SaveRoomHeader();
1029 if (!write_status.ok()) {
1030 formatter.AddField("status", "error");
1031 formatter.AddField("write_error", std::string(write_status.message()));
1032 formatter.EndObject();
1033 return write_status;
1034 }
1035
1036 if (parser.HasFlag("mock-rom")) {
1037 transaction.Commit();
1038 formatter.AddField("save_status", "mock-rom-skipped");
1039 } else {
1040 Rom::SaveSettings save_settings;
1041 save_settings.require_backup = true;
1042 const absl::Status save_status = rom->SaveToFile(save_settings);
1043 if (!save_status.ok()) {
1044 formatter.AddField("status", "error");
1045 formatter.AddField("save_error", std::string(save_status.message()));
1046 formatter.EndObject();
1047 return save_status;
1048 }
1049 transaction.Commit();
1050 formatter.AddField("save_status", "saved");
1051 }
1052 formatter.AddField("status", "success");
1053 formatter.EndObject();
1054
1055 return absl::OkStatus();
1056}
1057
1059 Rom* rom, const resources::ArgumentParser& parser,
1060 resources::OutputFormatter& formatter) {
1061 auto room_id_str = parser.GetString("room").value();
1062
1063 int room_id;
1064 if (!ParseHexString(room_id_str, &room_id)) {
1065 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
1066 }
1067
1068 formatter.BeginObject("Room Header Debug");
1069 formatter.AddField("room_id", room_id);
1070 formatter.AddHexField("room_id_hex", room_id, 2);
1071
1072 // Show ROM address constants
1073 formatter.BeginObject("rom_addresses");
1074 formatter.AddHexField("kRoomHeaderPointer", zelda3::kRoomHeaderPointer, 4);
1075 formatter.AddHexField("kRoomHeaderPointerBank",
1077 formatter.EndObject();
1078
1079 // Read the pointer table address
1080 int header_pointer = (rom->data()[zelda3::kRoomHeaderPointer + 2] << 16) +
1081 (rom->data()[zelda3::kRoomHeaderPointer + 1] << 8) +
1083 int header_pointer_pc = SnesToPc(header_pointer);
1084
1085 formatter.BeginObject("pointer_table");
1086 formatter.AddHexField("snes_address", header_pointer, 6);
1087 formatter.AddHexField("pc_address", header_pointer_pc, 6);
1088 formatter.EndObject();
1089
1090 // Read the room's specific header address from the table
1091 int table_offset = header_pointer_pc + (room_id * 2);
1092 int room_header_addr = (rom->data()[zelda3::kRoomHeaderPointerBank] << 16) +
1093 (rom->data()[table_offset + 1] << 8) +
1094 rom->data()[table_offset];
1095 int room_header_pc = SnesToPc(room_header_addr);
1096
1097 formatter.BeginObject("room_header_address");
1098 formatter.AddHexField("table_offset_pc", table_offset, 6);
1099 formatter.AddHexField("snes_address", room_header_addr, 6);
1100 formatter.AddHexField("pc_address", room_header_pc, 6);
1101 formatter.EndObject();
1102
1103 // Read and display raw header bytes (14 bytes)
1104 formatter.BeginArray("raw_bytes");
1105 for (int i = 0; i < 14; ++i) {
1106 if (room_header_pc + i < static_cast<int>(rom->size())) {
1107 formatter.AddArrayItem(
1108 absl::StrFormat("0x%02X", rom->data()[room_header_pc + i]));
1109 }
1110 }
1111 formatter.EndArray();
1112
1113 // Decode the header bytes
1114 if (room_header_pc >= 0 &&
1115 room_header_pc + 13 < static_cast<int>(rom->size())) {
1116 uint8_t byte0 = rom->data()[room_header_pc];
1117 uint8_t byte1 = rom->data()[room_header_pc + 1];
1118 uint8_t byte2 = rom->data()[room_header_pc + 2];
1119 uint8_t byte3 = rom->data()[room_header_pc + 3];
1120
1121 formatter.BeginObject("decoded");
1122 formatter.AddField("bg2", (byte0 >> 5) & 0x07);
1123 formatter.AddField("collision", (byte0 >> 2) & 0x07);
1124 formatter.AddField("is_light", (byte0 & 0x01) == 1);
1125 // This is the full 8-bit palette-set ID. Values 0x40-0x47 are valid;
1126 // masking to six bits silently aliases them to the wrong palette set.
1127 formatter.AddField("palette", byte1);
1128 formatter.AddField("blockset", byte2);
1129 formatter.AddField("spriteset", byte3);
1130 formatter.AddField("effect", rom->data()[room_header_pc + 4]);
1131 formatter.AddField("tag1", rom->data()[room_header_pc + 5]);
1132 formatter.AddField("tag2", rom->data()[room_header_pc + 6]);
1133 formatter.AddField("holewarp", rom->data()[room_header_pc + 9]);
1134 formatter.AddField("stair1_room", rom->data()[room_header_pc + 10]);
1135 formatter.AddField("stair2_room", rom->data()[room_header_pc + 11]);
1136 formatter.AddField("stair3_room", rom->data()[room_header_pc + 12]);
1137 formatter.AddField("stair4_room", rom->data()[room_header_pc + 13]);
1138 // The fields above are raw header bytes. The game keeps the current
1139 // room's high byte ($A1), so report the room each byte leads to.
1140 formatter.AddHexField("holewarp_destination_room",
1142 room_id, rom->data()[room_header_pc + 9]),
1143 3);
1144 for (int slot = 0; slot < 4; ++slot) {
1145 formatter.AddHexField(
1146 absl::StrFormat("stair%d_destination_room", slot + 1),
1148 room_id, rom->data()[room_header_pc + 10 + slot]),
1149 3);
1150 }
1151 formatter.EndObject();
1152 } else {
1153 formatter.AddField("error", "Room header address out of range");
1154 }
1155
1156 formatter.EndObject();
1157 return absl::OkStatus();
1158}
1159
1161 Rom* rom, const resources::ArgumentParser& parser,
1162 resources::OutputFormatter& formatter) {
1163 // Build generator options (shared by single and batch modes)
1165
1166 // Parse --promote-switch X,Y pairs
1167 auto switch_str = parser.GetString("promote-switch");
1168 if (switch_str.has_value()) {
1169 for (absl::string_view pair :
1170 absl::StrSplit(switch_str.value(), ' ', absl::SkipEmpty())) {
1171 std::vector<std::string> coords =
1172 absl::StrSplit(pair, ',', absl::SkipEmpty());
1173 if (coords.size() == 2) {
1174 int sx, sy;
1175 if (ParseHexString(coords[0], &sx) && ParseHexString(coords[1], &sy)) {
1176 options.switch_promotions.emplace_back(sx, sy);
1177 }
1178 }
1179 }
1180 }
1181
1182 bool do_write = parser.HasFlag("write");
1183 bool do_preserve_stops = parser.HasFlag("preserve-stops");
1184 bool do_visualize = parser.HasFlag("visualize");
1185
1186 // Determine room list: --rooms (batch) or --room (single)
1187 std::vector<int> room_ids;
1188 auto rooms_arg = parser.GetString("rooms");
1189 auto room_arg = parser.GetString("room");
1190
1191 if (rooms_arg.has_value()) {
1192 // Batch mode: parse comma-separated hex room IDs
1193 for (absl::string_view token :
1194 absl::StrSplit(rooms_arg.value(), ',', absl::SkipEmpty())) {
1195 int rid;
1196 std::string token_str(token);
1197 if (!ParseHexString(token_str, &rid)) {
1198 return absl::InvalidArgumentError(absl::StrFormat(
1199 "Invalid room ID '%s' in --rooms list. Must be hex.", token_str));
1200 }
1201 room_ids.push_back(rid);
1202 }
1203 if (room_ids.empty()) {
1204 return absl::InvalidArgumentError("--rooms list is empty.");
1205 }
1206 } else if (room_arg.has_value()) {
1207 // Single room mode (backwards compatible)
1208 int rid;
1209 if (!ParseHexString(room_arg.value(), &rid)) {
1210 return absl::InvalidArgumentError("Invalid room ID format. Must be hex.");
1211 }
1212 room_ids.push_back(rid);
1213 } else {
1214 return absl::InvalidArgumentError("Either --room or --rooms is required.");
1215 }
1216
1217 bool is_batch = room_ids.size() > 1;
1218 std::unique_ptr<ScopedRomTransaction> transaction;
1219 if (do_write) {
1220 transaction = std::make_unique<ScopedRomTransaction>(*rom);
1221 }
1222
1223 if (is_batch) {
1224 // Batch mode: aggregate results with per-room detail
1225 formatter.BeginObject("Batch Track Collision Generation");
1226 formatter.AddField("mode", do_write ? "write" : "dry-run");
1227 formatter.AddField("room_count", static_cast<int>(room_ids.size()));
1228
1229 int total_tiles = 0;
1230 int total_stops = 0;
1231 int total_corners = 0;
1232 int total_switches = 0;
1233 int rooms_succeeded = 0;
1234
1235 formatter.BeginArray("rooms");
1236 for (int room_id : room_ids) {
1237 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
1238 room.LoadObjects();
1239
1240 auto result = zelda3::GenerateTrackCollision(&room, options);
1241 if (!result.ok()) {
1242 // Stop on first error and report which room failed
1243 formatter.EndArray();
1244 formatter.AddHexField("failed_room", room_id, 3);
1245 formatter.AddField("error", std::string(result.status().message()));
1246 formatter.EndObject();
1247 return absl::InternalError(
1248 absl::StrFormat("Generation failed for room 0x%03X: %s", room_id,
1249 result.status().message()));
1250 }
1251
1252 if (do_preserve_stops && do_write) {
1253 auto existing = zelda3::LoadCustomCollisionMap(rom, room_id);
1254 if (existing.ok() && existing->has_data) {
1255 MergeStopTiles(*existing, result->collision_map);
1256 }
1257 }
1258
1259 formatter.BeginObject();
1260 formatter.AddHexField("room_id", room_id, 3);
1261 formatter.AddField("tiles_generated", result->tiles_generated);
1262 formatter.AddField("stop_count", result->stop_count);
1263 formatter.AddField("corner_count", result->corner_count);
1264 formatter.AddField("switch_count", result->switch_count);
1265 if (do_preserve_stops) {
1266 formatter.AddField("stops_preserved", true);
1267 }
1268
1269 if (do_visualize) {
1270 formatter.AddField("visualization", result->ascii_visualization);
1271 }
1272
1273 if (do_write) {
1274 auto write_status =
1275 zelda3::WriteTrackCollision(rom, room_id, result->collision_map);
1276 if (!write_status.ok()) {
1277 // Stop on first write error
1278 formatter.AddField("write_error",
1279 std::string(write_status.message()));
1280 formatter.EndObject();
1281 formatter.EndArray();
1282 formatter.EndObject();
1283 return absl::Status(
1284 write_status.code(),
1285 absl::StrFormat("Write failed for room 0x%03X: %s", room_id,
1286 write_status.message()));
1287 }
1288 formatter.AddField("write_status", "success");
1289 }
1290
1291 total_tiles += result->tiles_generated;
1292 total_stops += result->stop_count;
1293 total_corners += result->corner_count;
1294 total_switches += result->switch_count;
1295 rooms_succeeded++;
1296
1297 formatter.EndObject();
1298 }
1299 formatter.EndArray();
1300
1301 // Save ROM once after all rooms are written
1302 if (do_write) {
1303 auto save_status = SaveTrackCollisionRom(rom, parser, formatter);
1304 if (!save_status.ok()) {
1305 formatter.EndObject();
1306 return save_status;
1307 }
1308 transaction->Commit();
1309 }
1310
1311 // Aggregated totals
1312 formatter.BeginObject("totals");
1313 formatter.AddField("rooms_succeeded", rooms_succeeded);
1314 formatter.AddField("tiles_generated", total_tiles);
1315 formatter.AddField("stop_count", total_stops);
1316 formatter.AddField("corner_count", total_corners);
1317 formatter.AddField("switch_count", total_switches);
1318 formatter.EndObject();
1319
1320 formatter.EndObject();
1321 } else {
1322 // Single room mode (original behavior, backwards compatible)
1323 int room_id = room_ids[0];
1324
1325 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
1326 room.LoadObjects();
1327
1328 auto result = zelda3::GenerateTrackCollision(&room, options);
1329 if (!result.ok()) {
1330 return result.status();
1331 }
1332
1333 if (do_preserve_stops && do_write) {
1334 auto existing = zelda3::LoadCustomCollisionMap(rom, room_id);
1335 if (existing.ok() && existing->has_data) {
1336 MergeStopTiles(*existing, result->collision_map);
1337 }
1338 }
1339
1340 formatter.BeginObject("Track Collision Generation");
1341 formatter.AddHexField("room_id", room_id, 3);
1342 formatter.AddField("tiles_generated", result->tiles_generated);
1343 formatter.AddField("stop_count", result->stop_count);
1344 formatter.AddField("corner_count", result->corner_count);
1345 formatter.AddField("switch_count", result->switch_count);
1346 formatter.AddField("mode", do_write ? "write" : "dry-run");
1347 if (do_preserve_stops) {
1348 formatter.AddField("stops_preserved", true);
1349 }
1350
1351 if (do_visualize || !do_write) {
1352 formatter.AddField("visualization", result->ascii_visualization);
1353 }
1354
1355 if (do_write) {
1356 auto write_status =
1357 zelda3::WriteTrackCollision(rom, room_id, result->collision_map);
1358 if (!write_status.ok()) {
1359 formatter.AddField("write_error", std::string(write_status.message()));
1360 formatter.EndObject();
1361 return write_status;
1362 } else {
1363 formatter.AddField("write_status", "success");
1364 auto save_status = SaveTrackCollisionRom(rom, parser, formatter);
1365 if (!save_status.ok()) {
1366 formatter.EndObject();
1367 return save_status;
1368 }
1369 transaction->Commit();
1370 }
1371 }
1372
1373 formatter.EndObject();
1374 }
1375
1376 return absl::OkStatus();
1377}
1378
1379} // namespace handlers
1380} // namespace cli
1381} // 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:52
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:1036
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:893
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:1044
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:1051
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.
Loads and queries the hack manifest JSON for yaze-ASM integration.
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
absl::StatusOr< Room > GetRoom(int room_id)
static absl::StatusOr< DungeonSpawnPoint > Load(const Rom &rom, int spawn_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.
const absl::Status & load_status() const
const std::vector< RoomObject > & GetObjects() const
Definition room_layout.h:34
absl::Status LoadLayout(int layout_id, util::StopToken stop_token={})
static int DetermineObjectType(uint8_t b1, uint8_t b3)
void LoadChests()
Definition room.cc:2947
const std::vector< chest_data > & GetChests() const
Definition room.h:323
uint8_t blockset() const
Definition room.h:1015
void set_floor2(uint8_t value)
Definition room.h:1050
int staircase_destination_room(int index) const
Definition room.h:1004
void set_floor1(uint8_t value)
Definition room.h:1043
void SetTag2(TagKey tag2)
Definition room.h:809
const std::vector< Door > & GetDoors() const
Definition room.h:413
void SetLayoutId(uint8_t id)
Definition room.h:1032
void SetHolewarp(uint8_t hw)
Definition room.h:822
TagKey tag2() const
Definition room.h:988
uint8_t floor2() const
Definition room.h:1042
const std::vector< staircase > & GetStairs() const
Definition room.h:330
uint8_t palette() const
Definition room.h:1019
absl::Status SaveRoomHeader()
Definition room.cc:2682
TagKey tag1() const
Definition room.h:987
void SetEffect(EffectKey effect)
Definition room.h:795
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2565
uint8_t spriteset() const
Definition room.h:1018
void SetTag1(TagKey tag1)
Definition room.h:802
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
void LoadObjects()
Definition room.cc:1891
EffectKey effect() const
Definition room.h:986
void SetSpriteset(uint8_t ss)
Definition room.h:781
uint8_t floor1() const
Definition room.h:1041
void SetBlockset(uint8_t bs)
Definition room.h:773
uint8_t staircase_plane(int index) const
Definition room.h:995
void LoadSprites()
Definition room.cc:2887
void SetPalette(uint8_t pal)
Definition room.h:766
uint8_t layout_id() const
Definition room.h:1026
bool has_custom_collision() const
Definition room.h:563
int id() const
Definition room.h:1012
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status MaybeLoadSpriteRegistry(const resources::ArgumentParser &parser)
absl::Status ValidateManifestWriteRanges(const core::HackManifest &manifest, const std::vector< std::pair< uint32_t, uint32_t > > &ranges)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > CollectRoomHeaderWriteRanges(const Rom &rom, int room_id)
absl::Status ValidateRoomHeaderManifestConflicts(const Rom &rom, int room_id, const core::HackManifest &manifest)
absl::Status ValidateObjectStreamHeaderManifestConflicts(const Rom &rom, int room_id, const RoomPropertyManifestContext &context)
bool IsEncodedRoomStreamObject(const zelda3::RoomObject &object)
absl::Status SaveTrackCollisionRom(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter)
absl::StatusOr< std::optional< RoomPropertyManifestContext > > LoadRoomPropertyManifestContext(const resources::ArgumentParser &parser, bool require_object_allocator)
void MergeStopTiles(const zelda3::CustomCollisionMap &existing, zelda3::CustomCollisionMap &generated)
absl::Status WriteDungeonSpawnPointReport(Rom *rom, int spawn_id, resources::OutputFormatter &formatter, std::string_view object_title)
void AddSpriteNameFields(resources::OutputFormatter &formatter, int sprite_id)
absl::StatusOr< zelda3::DungeonStreamLayout > ToDungeonStreamAllocatorLayout(DungeonStreamType stream_type, const DungeonStreamLayout &manifest_layout)
bool ParseHexString(absl::string_view str, uint64_t *out)
Definition hex.cc:133
constexpr int kDoorPointers
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
const char * GetItemReceiptLabelSource(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
constexpr int ResolveHeaderDestinationRoom(int source_room_id, uint8_t header_byte)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:680
std::string GetRoomLabel(int id)
Convenience function to get a room label.
constexpr int kMessagesIdDungeon
std::string GetItemReceiptLabel(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
constexpr int kNumRegularDungeonEntrances
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
constexpr int kNumDungeonSpawnPoints
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
RoomCollisionInput MakeRoomCollisionInput(const Room &room, const RoomTilemaps &tilemaps)
std::vector< StaircaseSlotResolution > ResolveVanillaStaircaseSlots(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
constexpr int kRoomObjectPointer
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