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"
64absl::StatusOr<std::optional<RoomPropertyManifestContext>>
66 bool require_object_allocator) {
67 const auto manifest_path = parser.
GetString(
"manifest");
68 if (!manifest_path.has_value()) {
74 if (!require_object_allocator) {
75 return std::optional<RoomPropertyManifestContext>(std::move(context));
80 if (manifest_layout ==
nullptr) {
81 return absl::FailedPreconditionError(
82 "Manifest does not define dungeon_stream_regions.objects");
85 return absl::FailedPreconditionError(
86 "dungeon_stream_regions.objects must use copy_on_write");
93 return std::optional<RoomPropertyManifestContext>(std::move(context));
98 const std::vector<std::pair<uint32_t, uint32_t>>& ranges) {
100 return absl::PermissionDeniedError(
"Write conflict with Hack Manifest");
102 return absl::OkStatus();
108 return absl::FailedPreconditionError(
109 "Object-stream header manifest is missing allocator capability");
112 std::vector<std::pair<uint32_t, uint32_t>> ranges;
114 uint32_t pointer_table_snes = 0;
117 const uint32_t pointer_table_pc =
SnesToPc(pointer_table_snes);
118 const uint64_t pointer_slot =
static_cast<uint64_t
>(pointer_table_pc) +
119 static_cast<uint64_t
>(room_id) * 3u;
120 if (pointer_slot + 3u > rom.
size()) {
121 return absl::OutOfRangeError(
122 "Selected object pointer slot is outside the ROM");
125 uint32_t stream_snes = 0;
127 const uint32_t stream_pc =
SnesToPc(stream_snes);
128 if (
static_cast<uint64_t
>(stream_pc) + 2u > rom.
size()) {
129 return absl::OutOfRangeError(
130 "Selected object stream header is outside the ROM");
132 ranges.emplace_back(stream_pc, stream_pc + 2u);
135 ranges.emplace_back(range.begin, range.end);
137 const uint64_t cow_pointer_slot =
139 static_cast<uint64_t
>(room_id) * 3u;
140 if (cow_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
141 return absl::OutOfRangeError(
"Selected COW pointer range overflows");
143 ranges.emplace_back(
static_cast<uint32_t
>(cow_pointer_slot),
144 static_cast<uint32_t
>(cow_pointer_slot + 3u));
146 const uint64_t door_pointer_slot =
148 static_cast<uint64_t
>(room_id) * 3u;
149 if (door_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
150 return absl::OutOfRangeError(
"Selected door pointer range overflows");
152 ranges.emplace_back(
static_cast<uint32_t
>(door_pointer_slot),
153 static_cast<uint32_t
>(door_pointer_slot + 3u));
158absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
160 uint32_t header_pointer_snes = 0;
163 const uint32_t header_pointer_pc =
SnesToPc(header_pointer_snes);
164 const uint64_t table_offset =
static_cast<uint64_t
>(header_pointer_pc) +
165 static_cast<uint64_t
>(room_id) * 2u;
166 if (table_offset + 2u > rom.
size()) {
167 return absl::OutOfRangeError(
168 "Selected room header pointer slot is outside the ROM");
171 uint16_t header_offset = 0;
173 uint8_t header_bank = 0;
175 const uint32_t header_pc =
176 SnesToPc((
static_cast<uint32_t
>(header_bank) << 16) | header_offset);
177 if (
static_cast<uint64_t
>(header_pc) + 14u > rom.
size()) {
178 return absl::OutOfRangeError(
"Selected room header is outside the ROM");
181 const uint64_t message_pc =
183 static_cast<uint64_t
>(room_id) * 2u;
184 if (message_pc + 2u > rom.
size()) {
185 return absl::OutOfRangeError(
"Selected room message ID is outside the ROM");
188 return std::vector<std::pair<uint32_t, uint32_t>>{
189 {header_pc, header_pc + 14u},
190 {
static_cast<uint32_t
>(message_pc),
191 static_cast<uint32_t
>(message_pc + 2u)}};
196 std::vector<std::pair<uint32_t, uint32_t>> ranges;
206 for (
int i = 0; i < static_cast<int>(existing.
tiles.size()); ++i) {
216 if (parser.
HasFlag(
"mock-rom")) {
217 formatter.
AddField(
"save_status",
"mock-rom-skipped");
218 return absl::OkStatus();
223 auto save_status = rom->
SaveToFile(save_settings);
224 if (!save_status.ok()) {
225 formatter.
AddField(
"save_error", std::string(save_status.message()));
229 formatter.
AddField(
"save_status",
"saved");
230 return absl::OkStatus();
235 auto registry_path = parser.
GetString(
"sprite-registry");
236 if (!registry_path.has_value()) {
237 return absl::OkStatus();
240 std::ifstream file(registry_path.value());
241 if (!file.is_open()) {
242 return absl::NotFoundError(absl::StrFormat(
243 "Could not open sprite registry: %s", registry_path.value()));
246 std::stringstream buffer;
247 buffer << file.rdbuf();
255 std::string_view object_title) {
257 return absl::InvalidArgumentError(absl::StrFormat(
258 "Dungeon spawn point ID %d out of range (0x00-0x06).", spawn_id));
262 if (!spawn_or.ok()) {
263 return spawn_or.status();
265 const auto& spawn = spawn_or.value();
267 if (!object_title.empty()) {
272 formatter.
AddField(
"is_spawn_point",
true);
273 formatter.
AddHexField(
"room_id", spawn.room_id, 4);
274 formatter.
AddHexField(
"dungeon_id", spawn.dungeon_id, 2);
275 formatter.
AddHexField(
"quadrant", spawn.quadrant, 2);
276 formatter.
AddHexField(
"overworld_door_tilemap", spawn.overworld_door_tilemap,
278 formatter.
AddHexField(
"entrance_id", spawn.entrance_id, 4);
281 formatter.
AddField(
"x",
static_cast<int>(spawn.x_coordinate));
282 formatter.
AddField(
"y",
static_cast<int>(spawn.y_coordinate));
286 formatter.
AddField(
"horizontal_scroll",
287 static_cast<int>(spawn.horizontal_scroll));
288 formatter.
AddField(
"vertical_scroll",
289 static_cast<int>(spawn.vertical_scroll));
290 formatter.
AddField(
"trigger_x",
static_cast<int>(spawn.camera_trigger_x));
291 formatter.
AddField(
"trigger_y",
static_cast<int>(spawn.camera_trigger_y));
295 formatter.
AddHexField(
"main_gfx", spawn.main_gfx, 2);
299 spawn.camera_scroll_controller, 2);
304 formatter.
AddHexField(
"qn", spawn.camera_scroll_boundaries[0], 2);
305 formatter.
AddHexField(
"fn", spawn.camera_scroll_boundaries[1], 2);
306 formatter.
AddHexField(
"qs", spawn.camera_scroll_boundaries[2], 2);
307 formatter.
AddHexField(
"fs", spawn.camera_scroll_boundaries[3], 2);
308 formatter.
AddHexField(
"qw", spawn.camera_scroll_boundaries[4], 2);
309 formatter.
AddHexField(
"fw", spawn.camera_scroll_boundaries[5], 2);
310 formatter.
AddHexField(
"qe", spawn.camera_scroll_boundaries[6], 2);
311 formatter.
AddHexField(
"fe", spawn.camera_scroll_boundaries[7], 2);
314 if (!object_title.empty()) {
317 return absl::OkStatus();
324 auto registry_status = MaybeLoadSpriteRegistry(parser);
325 if (!registry_status.ok()) {
326 return registry_status;
329 auto room_id_str = parser.
GetString(
"room").value();
332 if (!ParseHexString(room_id_str, &room_id)) {
333 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
337 formatter.
AddField(
"room_id", room_id);
343 formatter.
AddField(
"total_sprites",
static_cast<int>(sprites.size()));
344 formatter.
AddField(
"status",
"success");
347 for (
const auto& sprite : sprites) {
349 formatter.
AddHexField(
"sprite_id", sprite.id(), 2);
351 formatter.
AddField(
"x", sprite.x());
352 formatter.
AddField(
"y", sprite.y());
353 formatter.
AddField(
"subtype", sprite.subtype());
354 formatter.
AddField(
"layer", sprite.layer());
355 if (sprite.key_drop() > 0) {
356 formatter.
AddField(
"key_drop", sprite.key_drop());
363 return absl::OkStatus();
370 auto registry_status = MaybeLoadSpriteRegistry(parser);
371 if (!registry_status.ok()) {
372 return registry_status;
375 auto room_id_str = parser.
GetString(
"room").value();
378 if (!ParseHexString(room_id_str, &room_id)) {
379 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
382 formatter.
AddField(
"room_id", room_id);
386 const bool include_objects = parser.
HasFlag(
"include-objects");
389 formatter.
AddField(
"status",
"success");
390 formatter.
AddField(
"name", absl::StrFormat(
"Room %d", room.
id()));
393 formatter.
AddField(
"room_type",
"Dungeon Room");
404 formatter.
AddField(
"tag1",
static_cast<int>(room.
tag1()));
405 formatter.
AddField(
"tag2",
static_cast<int>(room.
tag2()));
408 const int object_count = include_objects
409 ?
static_cast<int>(std::count_if(
410 tile_objects.begin(), tile_objects.end(),
411 IsEncodedRoomStreamObject))
412 :
static_cast<int>(tile_objects.size());
413 formatter.
AddField(
"object_count", object_count);
417 if (include_objects) {
419 int stream_index = 0;
420 for (
const auto&
object : tile_objects) {
421 if (!IsEncodedRoomStreamObject(
object)) {
425 const auto encoded =
object.EncodeObjectToBytes();
427 formatter.
AddHexField(
"object_id",
static_cast<uint16_t
>(
object.id_), 3);
428 formatter.
AddField(
"entity_kind",
"room_object");
430 formatter.
AddField(
"name_source",
"vanilla_object_catalog");
431 formatter.
AddField(
"coordinate_space",
"room_tile8");
433 encoded.b1, encoded.b3));
434 formatter.
AddField(
"x",
static_cast<int>(
object.x()));
435 formatter.
AddField(
"y",
static_cast<int>(
object.y()));
436 formatter.
AddField(
"size",
static_cast<int>(
object.size()));
437 formatter.
AddField(
"stream_index", stream_index++);
446 for (
const auto& sprite : room.
GetSprites()) {
448 formatter.
AddHexField(
"sprite_id", sprite.id(), 2);
450 formatter.
AddField(
"x", sprite.x());
451 formatter.
AddField(
"y", sprite.y());
452 formatter.
AddField(
"subtype", sprite.subtype());
453 formatter.
AddField(
"layer", sprite.layer());
460 const auto& doors = room.
GetDoors();
461 for (
size_t index = 0; index < doors.size(); ++index) {
462 const auto& door = doors[index];
464 formatter.
AddField(
"index",
static_cast<int>(index));
465 formatter.
AddField(
"entity_kind",
"door");
466 formatter.
AddField(
"position", door.position);
467 formatter.
AddField(
"direction", std::string(door.GetDirectionName()));
468 formatter.
AddField(
"type", std::string(door.GetTypeName()));
469 formatter.
AddHexField(
"type_id",
static_cast<uint8_t
>(door.type), 2);
470 auto [tx, ty] = door.GetTileCoords();
473 const auto [px, py, width, height] = door.GetBounds();
478 formatter.
AddField(
"height", height);
479 formatter.
AddField(
"coordinate_space",
"room_pixels");
487 for (
const auto& stair : room.
GetStairs()) {
492 "tile_y", stair.room);
493 formatter.
AddField(
"label", stair.label);
498 if (parser.
HasFlag(
"include-staircase-resolution")) {
503 input.objects.insert(input.objects.begin(), prefix.begin(), prefix.end());
506 formatter.
AddField(
"model",
"vanilla_collision_preview");
507 formatter.
AddField(
"runtime_qualified",
false);
508 formatter.
AddField(
"layout_loaded", layout_status.ok());
511 for (
const auto& resolution : resolutions) {
512 if (resolution.object_index < prefix.size())
514 const auto index = resolution.object_index - prefix.size();
515 const auto&
object = tile_objects[index];
517 formatter.
AddField(
"object_index",
static_cast<int>(index));
519 formatter.
AddField(
"x",
object.x());
520 formatter.
AddField(
"y",
object.y());
521 formatter.
AddField(
"stream_index",
object.GetLayerValue());
522 const char* status =
"trigger_unavailable";
523 switch (resolution.status) {
530 status =
"overlapping_stairs";
533 status =
"outside_room";
536 status =
"too_many_stairs";
539 formatter.
AddField(
"vanilla_status", status);
540 if (resolution.slot && layout_status.ok()) {
541 formatter.
AddField(
"vanilla_slot", *resolution.slot);
545 formatter.
AddField(
"vanilla_arrival_plane",
548 if (!layout_status.ok())
549 status =
"layout_unavailable";
551 status =
"custom_collision";
552 formatter.
AddField(
"status", status);
553 if (resolution.slot && layout_status.ok() &&
555 formatter.
AddField(
"slot", *resolution.slot);
574 formatter.
AddField(
"item_name_source",
581 return absl::OkStatus();
587 auto room_id_opt = parser.
GetString(
"room");
589 bool has_room_filter = room_id_opt.has_value();
590 int room_filter = -1;
591 if (has_room_filter) {
592 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
593 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
598 int rooms_with_chests = 0;
599 int total_chests = 0;
600 std::map<int, int> item_counts;
603 formatter.
AddField(
"room_filter", has_room_filter
604 ? absl::StrFormat(
"0x%02X", room_filter)
608 int start_room = has_room_filter ? room_filter : 0;
611 for (
int room_id = start_room; room_id <= end_room; ++room_id) {
617 if (chests.empty()) {
622 total_chests +=
static_cast<int>(chests.size());
625 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", room_id));
627 formatter.
AddField(
"chest_count",
static_cast<int>(chests.size()));
631 for (
const auto&
chest : chests) {
633 formatter.
AddField(
"index", chest_index++);
636 formatter.
AddField(
"item_name_source",
641 item_counts[
chest.id]++;
649 formatter.
AddField(
"total_rooms", total_rooms);
650 formatter.
AddField(
"rooms_with_chests", rooms_with_chests);
651 formatter.
AddField(
"total_chests", total_chests);
652 formatter.
AddField(
"unique_items",
static_cast<int>(item_counts.size()));
655 for (
const auto& [item_id, count] : item_counts) {
662 formatter.
AddField(
"item_name_source",
671 return absl::OkStatus();
677 auto entrance_id_str = parser.
GetString(
"entrance").value();
678 bool is_spawn_point = parser.
HasFlag(
"spawn");
681 if (!ParseHexString(entrance_id_str, &entrance_id)) {
682 return absl::InvalidArgumentError(
683 "Invalid entrance ID format. Must be hex.");
686 if (is_spawn_point) {
691 return absl::OutOfRangeError(
"Regular dungeon entrance ID is out of range");
696 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
697 formatter.
AddField(
"is_spawn_point", is_spawn_point);
698 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%04X", entrance.
room_));
699 formatter.
AddField(
"exit_id", absl::StrFormat(
"0x%04X", entrance.
exit_));
715 formatter.
AddField(
"floor", absl::StrFormat(
"0x%02X", entrance.
floor_));
718 formatter.
AddField(
"door", absl::StrFormat(
"0x%02X", entrance.
door_));
720 absl::StrFormat(
"0x%02X", entrance.
ladder_bg_));
722 absl::StrFormat(
"0x%02X", entrance.
scrolling_));
723 formatter.
AddField(
"scroll_quadrant",
725 formatter.
AddField(
"music", absl::StrFormat(
"0x%02X", entrance.
music_));
747 return absl::OkStatus();
753 auto room_id_str = parser.
GetString(
"room").value();
756 if (!ParseHexString(room_id_str, &room_id)) {
757 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
761 formatter.
AddField(
"room_id", room_id);
765 auto room_or = dungeon_editor.
GetRoom(room_id);
767 formatter.
AddField(
"status",
"error");
768 formatter.
AddField(
"error", room_or.status().ToString());
770 return room_or.status();
773 auto& room = room_or.value();
776 formatter.
AddField(
"status",
"success");
777 formatter.
AddField(
"room_width",
"Unknown");
778 formatter.
AddField(
"room_height",
"Unknown");
779 formatter.
AddField(
"room_name", absl::StrFormat(
"Room %d", room.id()));
783 formatter.
AddField(
"tiles",
"Room tile data would be exported here");
784 formatter.
AddField(
"sprites",
"Room sprite data would be exported here");
785 formatter.
AddField(
"doors",
"Room door data would be exported here");
790 return absl::OkStatus();
796 auto room_id_str = parser.
GetString(
"room").value();
799 if (!ParseHexString(room_id_str, &room_id)) {
800 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
804 formatter.
AddField(
"room_id", room_id);
808 auto room_or = dungeon_editor.
GetRoom(room_id);
810 formatter.
AddField(
"status",
"error");
811 formatter.
AddField(
"error", room_or.status().ToString());
813 return room_or.status();
816 auto& room = room_or.value();
821 const auto& objects = room.GetTileObjects();
822 formatter.
AddField(
"total_objects",
static_cast<int>(objects.size()));
823 formatter.
AddField(
"status",
"success");
826 for (
const auto& obj : objects) {
829 formatter.
AddField(
"id_hex", absl::StrFormat(
"0x%04X", obj.id_));
832 formatter.
AddField(
"size", obj.size_);
833 formatter.
AddField(
"layer",
static_cast<int>(obj.layer_));
843 return absl::OkStatus();
849 auto room_id_str = parser.
GetString(
"room").value();
852 if (!ParseHexString(room_id_str, &room_id)) {
853 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
857 formatter.
AddField(
"room_id", room_id);
861 auto room_or = dungeon_editor.
GetRoom(room_id);
863 formatter.
AddField(
"status",
"error");
864 formatter.
AddField(
"error", room_or.status().ToString());
866 return room_or.status();
869 auto& room = room_or.value();
873 formatter.
AddField(
"room_width", 64);
874 formatter.
AddField(
"room_height", 64);
875 formatter.
AddField(
"total_tiles", 64 * 64);
876 formatter.
AddField(
"has_custom_collision", room.has_custom_collision());
878 static_cast<int>(room.GetTileObjects().size()));
879 formatter.
AddField(
"status",
"success");
884 const auto& collision = room.custom_collision().tiles;
885 for (
int y = 0; y < 64; ++y) {
888 for (
int x = 0; x < 64; ++x) {
892 absl::StrAppend(&row, absl::StrFormat(
"%02X", collision[y * 64 + x]));
899 return absl::OkStatus();
905 auto room_id_str = parser.
GetString(
"room").value();
906 auto property = parser.
GetString(
"property").value();
907 auto value = parser.
GetString(
"value").value();
910 if (!ParseHexString(room_id_str, &room_id)) {
911 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
914 formatter.
BeginObject(
"Dungeon Room Property Set");
915 formatter.
AddField(
"room_id", room_id);
916 formatter.
AddField(
"property", property);
920 const absl::Status room_status =
921 absl::InvalidArgumentError(
"Invalid room ID");
922 formatter.
AddField(
"status",
"error");
923 formatter.
AddField(
"error", room_status.ToString());
928 int parsed_value = 0;
929 if (!ParseHexString(value, &parsed_value)) {
930 const absl::Status value_status =
931 absl::InvalidArgumentError(absl::StrFormat(
932 "Invalid value format: %s (expected integer/hex)", value));
933 formatter.
AddField(
"status",
"error");
934 formatter.
AddField(
"error", std::string(value_status.message()));
939 const std::string prop = absl::AsciiStrToLower(property);
940 const bool is_layout = prop ==
"layout" || prop ==
"layout_id";
941 const bool is_floor = prop ==
"floor1" || prop ==
"floor2";
942 const bool is_object_stream_header = is_layout || is_floor;
943 const bool is_room_header = prop ==
"palette" || prop ==
"blockset" ||
944 prop ==
"spriteset" || prop ==
"effect" ||
945 prop ==
"tag1" || prop ==
"tag2" ||
947 const int property_max = is_layout ? 0x07 : (is_floor ? 0x0F : 0xFF);
948 if (is_object_stream_header &&
949 (parsed_value < 0 || parsed_value > property_max)) {
950 const absl::Status property_status =
951 absl::InvalidArgumentError(absl::StrFormat(
952 "Property %s must be in range 0..%d", property, property_max));
953 formatter.
AddField(
"status",
"error");
954 formatter.
AddField(
"error", std::string(property_status.message()));
956 return property_status;
959 std::optional<RoomPropertyManifestContext> manifest_context;
960 if (is_object_stream_header || is_room_header) {
962 LoadRoomPropertyManifestContext(parser, is_object_stream_header);
963 if (!context_or.ok()) {
964 formatter.
AddField(
"status",
"error");
965 formatter.
AddField(
"error", std::string(context_or.status().message()));
967 return context_or.status();
969 manifest_context = std::move(context_or).value();
970 if (manifest_context.has_value()) {
971 const absl::Status manifest_status =
972 is_object_stream_header
973 ? ValidateObjectStreamHeaderManifestConflicts(*rom, room_id,
975 : ValidateRoomHeaderManifestConflicts(*rom, room_id,
976 manifest_context->manifest);
977 if (!manifest_status.ok()) {
978 formatter.
AddField(
"status",
"error");
979 formatter.
AddField(
"error", std::string(manifest_status.message()));
981 return manifest_status;
984 if (is_object_stream_header) {
985 formatter.
AddField(
"allocator_capability",
"copy_on_write");
992 if (prop ==
"palette") {
993 room.
SetPalette(
static_cast<uint8_t
>(parsed_value & 0xFF));
994 }
else if (prop ==
"blockset") {
995 room.
SetBlockset(
static_cast<uint8_t
>(parsed_value & 0xFF));
996 }
else if (prop ==
"spriteset") {
997 room.
SetSpriteset(
static_cast<uint8_t
>(parsed_value & 0xFF));
998 }
else if (is_layout) {
999 room.
SetLayoutId(
static_cast<uint8_t
>(parsed_value));
1000 }
else if (prop ==
"floor1") {
1001 room.
set_floor1(
static_cast<uint8_t
>(parsed_value));
1002 }
else if (prop ==
"floor2") {
1003 room.
set_floor2(
static_cast<uint8_t
>(parsed_value));
1004 }
else if (prop ==
"effect") {
1006 }
else if (prop ==
"tag1") {
1008 }
else if (prop ==
"tag2") {
1010 }
else if (prop ==
"holewarp") {
1011 room.
SetHolewarp(
static_cast<uint8_t
>(parsed_value & 0xFF));
1013 const absl::Status property_status = absl::InvalidArgumentError(
1014 absl::StrFormat(
"Unsupported property: %s", property));
1015 formatter.
AddField(
"status",
"error");
1016 formatter.
AddField(
"error", std::string(property_status.message()));
1018 return property_status;
1021 const absl::Status write_status =
1022 is_object_stream_header
1024 manifest_context.has_value() &&
1025 manifest_context->allocator_layout.has_value()
1026 ? &*manifest_context->allocator_layout
1029 if (!write_status.ok()) {
1030 formatter.
AddField(
"status",
"error");
1031 formatter.
AddField(
"write_error", std::string(write_status.message()));
1033 return write_status;
1036 if (parser.
HasFlag(
"mock-rom")) {
1038 formatter.
AddField(
"save_status",
"mock-rom-skipped");
1042 const absl::Status save_status = rom->
SaveToFile(save_settings);
1043 if (!save_status.ok()) {
1044 formatter.
AddField(
"status",
"error");
1045 formatter.
AddField(
"save_error", std::string(save_status.message()));
1050 formatter.
AddField(
"save_status",
"saved");
1052 formatter.
AddField(
"status",
"success");
1055 return absl::OkStatus();
1061 auto room_id_str = parser.
GetString(
"room").value();
1064 if (!ParseHexString(room_id_str, &room_id)) {
1065 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
1069 formatter.
AddField(
"room_id", room_id);
1083 int header_pointer_pc =
SnesToPc(header_pointer);
1086 formatter.
AddHexField(
"snes_address", header_pointer, 6);
1087 formatter.
AddHexField(
"pc_address", header_pointer_pc, 6);
1091 int table_offset = header_pointer_pc + (room_id * 2);
1093 (rom->
data()[table_offset + 1] << 8) +
1094 rom->
data()[table_offset];
1095 int room_header_pc =
SnesToPc(room_header_addr);
1098 formatter.
AddHexField(
"table_offset_pc", table_offset, 6);
1099 formatter.
AddHexField(
"snes_address", room_header_addr, 6);
1100 formatter.
AddHexField(
"pc_address", room_header_pc, 6);
1105 for (
int i = 0; i < 14; ++i) {
1106 if (room_header_pc + i <
static_cast<int>(rom->
size())) {
1108 absl::StrFormat(
"0x%02X", rom->
data()[room_header_pc + i]));
1114 if (room_header_pc >= 0 &&
1115 room_header_pc + 13 <
static_cast<int>(rom->
size())) {
1116 uint8_t byte0 = rom->
data()[room_header_pc];
1117 uint8_t byte1 = rom->
data()[room_header_pc + 1];
1118 uint8_t byte2 = rom->
data()[room_header_pc + 2];
1119 uint8_t byte3 = rom->
data()[room_header_pc + 3];
1122 formatter.
AddField(
"bg2", (byte0 >> 5) & 0x07);
1123 formatter.
AddField(
"collision", (byte0 >> 2) & 0x07);
1124 formatter.
AddField(
"is_light", (byte0 & 0x01) == 1);
1127 formatter.
AddField(
"palette", byte1);
1128 formatter.
AddField(
"blockset", byte2);
1129 formatter.
AddField(
"spriteset", byte3);
1130 formatter.
AddField(
"effect", rom->
data()[room_header_pc + 4]);
1131 formatter.
AddField(
"tag1", rom->
data()[room_header_pc + 5]);
1132 formatter.
AddField(
"tag2", rom->
data()[room_header_pc + 6]);
1133 formatter.
AddField(
"holewarp", rom->
data()[room_header_pc + 9]);
1134 formatter.
AddField(
"stair1_room", rom->
data()[room_header_pc + 10]);
1135 formatter.
AddField(
"stair2_room", rom->
data()[room_header_pc + 11]);
1136 formatter.
AddField(
"stair3_room", rom->
data()[room_header_pc + 12]);
1137 formatter.
AddField(
"stair4_room", rom->
data()[room_header_pc + 13]);
1140 formatter.
AddHexField(
"holewarp_destination_room",
1142 room_id, rom->
data()[room_header_pc + 9]),
1144 for (
int slot = 0; slot < 4; ++slot) {
1146 absl::StrFormat(
"stair%d_destination_room", slot + 1),
1148 room_id, rom->
data()[room_header_pc + 10 + slot]),
1153 formatter.
AddField(
"error",
"Room header address out of range");
1157 return absl::OkStatus();
1167 auto switch_str = parser.
GetString(
"promote-switch");
1168 if (switch_str.has_value()) {
1169 for (absl::string_view pair :
1170 absl::StrSplit(switch_str.value(),
' ', absl::SkipEmpty())) {
1171 std::vector<std::string> coords =
1172 absl::StrSplit(pair,
',', absl::SkipEmpty());
1173 if (coords.size() == 2) {
1175 if (ParseHexString(coords[0], &sx) && ParseHexString(coords[1], &sy)) {
1182 bool do_write = parser.
HasFlag(
"write");
1183 bool do_preserve_stops = parser.
HasFlag(
"preserve-stops");
1184 bool do_visualize = parser.
HasFlag(
"visualize");
1187 std::vector<int> room_ids;
1188 auto rooms_arg = parser.
GetString(
"rooms");
1189 auto room_arg = parser.
GetString(
"room");
1191 if (rooms_arg.has_value()) {
1193 for (absl::string_view token :
1194 absl::StrSplit(rooms_arg.value(),
',', absl::SkipEmpty())) {
1196 std::string token_str(token);
1197 if (!ParseHexString(token_str, &rid)) {
1198 return absl::InvalidArgumentError(absl::StrFormat(
1199 "Invalid room ID '%s' in --rooms list. Must be hex.", token_str));
1201 room_ids.push_back(rid);
1203 if (room_ids.empty()) {
1204 return absl::InvalidArgumentError(
"--rooms list is empty.");
1206 }
else if (room_arg.has_value()) {
1209 if (!ParseHexString(room_arg.value(), &rid)) {
1210 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
1212 room_ids.push_back(rid);
1214 return absl::InvalidArgumentError(
"Either --room or --rooms is required.");
1217 bool is_batch = room_ids.size() > 1;
1218 std::unique_ptr<ScopedRomTransaction> transaction;
1220 transaction = std::make_unique<ScopedRomTransaction>(*rom);
1225 formatter.
BeginObject(
"Batch Track Collision Generation");
1226 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1227 formatter.
AddField(
"room_count",
static_cast<int>(room_ids.size()));
1229 int total_tiles = 0;
1230 int total_stops = 0;
1231 int total_corners = 0;
1232 int total_switches = 0;
1233 int rooms_succeeded = 0;
1236 for (
int room_id : room_ids) {
1245 formatter.
AddField(
"error", std::string(result.status().message()));
1247 return absl::InternalError(
1248 absl::StrFormat(
"Generation failed for room 0x%03X: %s", room_id,
1249 result.status().message()));
1252 if (do_preserve_stops && do_write) {
1254 if (existing.ok() && existing->has_data) {
1255 MergeStopTiles(*existing, result->collision_map);
1261 formatter.
AddField(
"tiles_generated", result->tiles_generated);
1262 formatter.
AddField(
"stop_count", result->stop_count);
1263 formatter.
AddField(
"corner_count", result->corner_count);
1264 formatter.
AddField(
"switch_count", result->switch_count);
1265 if (do_preserve_stops) {
1266 formatter.
AddField(
"stops_preserved",
true);
1270 formatter.
AddField(
"visualization", result->ascii_visualization);
1276 if (!write_status.ok()) {
1279 std::string(write_status.message()));
1283 return absl::Status(
1284 write_status.code(),
1285 absl::StrFormat(
"Write failed for room 0x%03X: %s", room_id,
1286 write_status.message()));
1288 formatter.
AddField(
"write_status",
"success");
1291 total_tiles += result->tiles_generated;
1292 total_stops += result->stop_count;
1293 total_corners += result->corner_count;
1294 total_switches += result->switch_count;
1303 auto save_status = SaveTrackCollisionRom(rom, parser, formatter);
1304 if (!save_status.ok()) {
1308 transaction->Commit();
1313 formatter.
AddField(
"rooms_succeeded", rooms_succeeded);
1314 formatter.
AddField(
"tiles_generated", total_tiles);
1315 formatter.
AddField(
"stop_count", total_stops);
1316 formatter.
AddField(
"corner_count", total_corners);
1317 formatter.
AddField(
"switch_count", total_switches);
1323 int room_id = room_ids[0];
1330 return result.status();
1333 if (do_preserve_stops && do_write) {
1335 if (existing.ok() && existing->has_data) {
1336 MergeStopTiles(*existing, result->collision_map);
1340 formatter.
BeginObject(
"Track Collision Generation");
1342 formatter.
AddField(
"tiles_generated", result->tiles_generated);
1343 formatter.
AddField(
"stop_count", result->stop_count);
1344 formatter.
AddField(
"corner_count", result->corner_count);
1345 formatter.
AddField(
"switch_count", result->switch_count);
1346 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1347 if (do_preserve_stops) {
1348 formatter.
AddField(
"stops_preserved",
true);
1351 if (do_visualize || !do_write) {
1352 formatter.
AddField(
"visualization", result->ascii_visualization);
1358 if (!write_status.ok()) {
1359 formatter.
AddField(
"write_error", std::string(write_status.message()));
1361 return write_status;
1363 formatter.
AddField(
"write_status",
"success");
1364 auto save_status = SaveTrackCollisionRom(rom, parser, formatter);
1365 if (!save_status.ok()) {
1369 transaction->Commit();
1376 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::StatusOr< uint8_t > ReadByte(int offset) const
absl::Status SaveToFile(const SaveSettings &settings)
absl::StatusOr< uint16_t > ReadWord(int offset) const
absl::StatusOr< uint32_t > ReadLong(int offset) const
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.
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.
Core dungeon editing system.
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.
uint8_t camera_boundary_fw_
uint8_t camera_boundary_qe_
uint8_t camera_boundary_fe_
uint8_t camera_boundary_fs_
uint8_t camera_boundary_qs_
uint8_t camera_boundary_qw_
uint16_t camera_trigger_y_
uint8_t camera_boundary_fn_
uint16_t camera_trigger_x_
uint8_t camera_boundary_qn_
const absl::Status & load_status() const
const std::vector< RoomObject > & GetObjects() const
absl::Status LoadLayout(int layout_id, util::StopToken stop_token={})
static int DetermineObjectType(uint8_t b1, uint8_t b3)
const std::vector< chest_data > & GetChests() const
void set_floor2(uint8_t value)
int staircase_destination_room(int index) const
void set_floor1(uint8_t value)
void SetTag2(TagKey tag2)
const std::vector< Door > & GetDoors() const
void SetLayoutId(uint8_t id)
void SetHolewarp(uint8_t hw)
const std::vector< staircase > & GetStairs() const
absl::Status SaveRoomHeader()
void SetEffect(EffectKey effect)
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
uint8_t spriteset() const
void SetTag1(TagKey tag1)
const std::vector< zelda3::Sprite > & GetSprites() const
const std::vector< RoomObject > & GetTileObjects() const
void SetSpriteset(uint8_t ss)
void SetBlockset(uint8_t bs)
uint8_t staircase_plane(int index) const
void SetPalette(uint8_t pal)
uint8_t layout_id() const
bool has_custom_collision() const
#define ASSIGN_OR_RETURN(type_variable_name, expression)
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)
bool IsStopTile(uint8_t v)
constexpr uint8_t kStopTileMin
constexpr uint8_t kStopTileMax
absl::Status ValidateObjectStreamHeaderManifestConflicts(const Rom &rom, int room_id, const RoomPropertyManifestContext &context)
bool IsEncodedRoomStreamObject(const zelda3::RoomObject &object)
absl::Status SaveTrackCollisionRom(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter)
absl::StatusOr< std::optional< RoomPropertyManifestContext > > LoadRoomPropertyManifestContext(const resources::ArgumentParser &parser, bool require_object_allocator)
void MergeStopTiles(const zelda3::CustomCollisionMap &existing, zelda3::CustomCollisionMap &generated)
absl::Status WriteDungeonSpawnPointReport(Rom *rom, int spawn_id, resources::OutputFormatter &formatter, std::string_view object_title)
void AddSpriteNameFields(resources::OutputFormatter &formatter, int sprite_id)
absl::StatusOr< zelda3::DungeonStreamLayout > ToDungeonStreamAllocatorLayout(DungeonStreamType stream_type, const DungeonStreamLayout &manifest_layout)
bool ParseHexString(absl::string_view str, uint64_t *out)
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)
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)
constexpr int kNumDungeonSpawnPoints
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
RoomCollisionInput MakeRoomCollisionInput(const Room &room, const RoomTilemaps &tilemaps)
std::vector< StaircaseSlotResolution > ResolveVanillaStaircaseSlots(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
constexpr int kRoomObjectPointer
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
std::optional< zelda3::DungeonStreamLayout > allocator_layout
core::HackManifest manifest
std::array< uint8_t, 64 *64 > tiles
std::vector< std::pair< int, int > > switch_promotions