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