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