14#include "absl/status/status.h"
15#include "absl/strings/str_format.h"
16#include "absl/strings/string_view.h"
17#include "absl/types/span.h"
62#include "imgui/imgui.h"
82 "Minecart Track Editor has unpublished drafts. ROM Save/Apply never "
83 "publishes ASM source; use Publish Tracks in the Minecart Track Editor "
84 "or discard the drafts first.";
88 return absl::FailedPreconditionError(
"ROM not loaded");
91 bool any_dirty =
false;
98 return absl::OkStatus();
101 std::vector<zelda3::WaterFillZoneEntry> zones;
103 for (
int room_id = 0; room_id < static_cast<int>(rooms.
size()); ++room_id) {
105 if (room ==
nullptr) {
113 if (tile_count <= 0) {
116 if (tile_count > 255) {
117 return absl::InvalidArgumentError(absl::StrFormat(
118 "Water fill zone in room 0x%02X has %d tiles (max 255)", room_id,
125 z.
fill_offsets.reserve(
static_cast<size_t>(tile_count));
128 for (
size_t i = 0; i < map.size(); ++i) {
133 zones.push_back(std::move(z));
136 if (zones.size() > 8) {
137 return absl::InvalidArgumentError(absl::StrFormat(
138 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
145 for (
const auto& z : zones) {
155 return absl::OkStatus();
161 std::vector<std::pair<uint32_t, uint32_t>> ranges;
162 if (rom ==
nullptr || !rom->
is_loaded()) {
166 bool any_dirty =
false;
179 if (layout !=
nullptr &&
181 for (
const auto& range : layout->allocation_regions) {
185 const uint32_t pointer_width =
189 const uint32_t pointer_table_pc =
yaze::SnesToPc(layout->pointer_table);
192 pointer_table_pc + layout->pointer_count * pointer_width);
197 const auto& rom_data = rom->
vector();
203 if (pointer_slot < 0 ||
204 pointer_slot + 1 >=
static_cast<int>(rom_data.size())) {
207 const uint16_t pointer =
208 (
static_cast<uint16_t
>(rom_data[pointer_slot + 1]) << 8) |
209 rom_data[pointer_slot];
210 if (pointer < 0x8000) {
214 const uint32_t encoded_size =
215 static_cast<uint32_t
>(room.
GetPotItems().size() * 3 + 2);
216 ranges.emplace_back(stream_pc, stream_pc + encoded_size);
221absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
226 rom,
static_cast<int>(rooms.
size()),
227 [&rooms](
int room_id) { return rooms.GetIfMaterialized(room_id); }));
233 std::vector<std::pair<uint32_t, uint32_t>> ranges;
234 if (rom ==
nullptr || !rom->
is_loaded() ||
238 bool any_dirty =
false;
251std::vector<std::pair<uint32_t, uint32_t>>
254 std::vector<std::pair<uint32_t, uint32_t>> ranges;
256 if (rom ==
nullptr || !rom->
is_loaded() ||
258 static_cast<int>(rom->
size()) ||
262 bool any_dirty =
false;
280 std::optional<zelda3::DungeonStreamLayout> repack_layout;
281 bool any_dirty =
false;
290 if (manifest_layout !=
nullptr &&
296 repack_layout = std::move(allocator_layout);
301 rom,
static_cast<int>(rooms.
size()),
302 [&rooms](
int room_id) { return rooms.GetIfMaterialized(room_id); },
303 repack_layout.has_value() ? &*repack_layout :
nullptr);
311 return absl::OkStatus();
314 if (ranges.empty()) {
315 return absl::OkStatus();
319 save_scope,
"DungeonEditorV2", toast_manager);
328 return absl::OkStatus();
331 if (ranges.empty()) {
332 return absl::OkStatus();
336 save_scope,
"DungeonEditorV2", toast_manager);
341 const std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>&
343 const std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>&
345 absl::string_view save_scope,
ToastManager* toast_manager) {
346 if (rom ==
nullptr || !rom->
is_loaded()) {
347 return absl::FailedPreconditionError(
"ROM not loaded");
359 return absl::OkStatus();
365 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
366 if (ranges.empty()) {
367 return absl::OkStatus();
371 save_scope,
"DungeonEditorV2", toast_manager);
376 absl::string_view save_scope,
379 if (ranges.empty() || project ==
nullptr ||
381 return absl::OkStatus();
385 save_scope,
"DungeonEditorV2", toast_manager);
393 return absl::OkStatus();
396 if (ranges.empty()) {
397 return absl::OkStatus();
401 save_scope,
"DungeonEditorV2", toast_manager);
409 return absl::OkStatus();
412 if (ranges.empty()) {
413 return absl::OkStatus();
417 save_scope,
"DungeonEditorV2", toast_manager);
424 return absl::FailedPreconditionError(
425 "Dungeon save transaction is already active");
431 palette_manager.HasUnsavedChanges(
game_data_)) {
432 if (!palette_manager.IsManaging(
game_data_)) {
433 return absl::FailedPreconditionError(
434 "Cannot save dungeon palettes from a different ROM session");
443 for (
size_t i = 0; i <
entrances_.size(); ++i) {
454 return absl::OkStatus();
464 room->RestoreSaveDirtySnapshot(state);
467 for (
size_t i = 0; i <
entrances_.size(); ++i) {
504 const std::function<absl::Status()>& operation) {
506 return absl::FailedPreconditionError(
"ROM not loaded");
518 const absl::Status status = operation();
526 return absl::OkStatus();
532 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
535 return absl::FailedPreconditionError(
"ROM not loaded");
539 return absl::FailedPreconditionError(
540 "Apply or explicitly discard Object Tile Editor changes before "
541 "using Save or Apply Room");
545 std::optional<zelda3::ChestSavePlan> chest_save_plan;
547 if (flags.kSaveCollision) {
552 if (flags.kSaveWaterFillZones) {
557 if (flags.kSaveEntrances) {
563 if (flags.kSaveChests) {
566 [
this](
int room_id) {
567 return rooms_.GetIfMaterialized(room_id);
572 chest_save_plan = std::move(plan);
574 if (flags.kSavePotItems) {
581 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(
game_data_)) {
582 if (!palette_manager.IsManaging(
game_data_)) {
583 return absl::FailedPreconditionError(
584 "Cannot save dungeon palettes from a different ROM session");
589 const size_t modified_color_count =
590 palette_manager.GetModifiedColorCount(
game_data_);
591 auto status = palette_manager.SaveAllToRom();
593 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
594 status.message().data());
597 LOG_INFO(
"DungeonEditorV2",
"Saved %zu modified colors to ROM",
598 modified_color_count);
601 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
602 absl::Status save_status = absl::OkStatus();
604 if (!save_status.ok()) {
609 save_status = status;
612 if (!save_status.ok()) {
617 if (flags.kSaveTorches) {
620 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
622 LOG_ERROR(
"DungeonEditorV2",
"Failed to save torches: %s",
623 status.message().data());
628 if (flags.kSavePits) {
632 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pits: %s",
633 status.message().data());
638 if (flags.kSaveBlocks) {
641 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
643 LOG_ERROR(
"DungeonEditorV2",
"Failed to save blocks: %s",
644 status.message().data());
649 if (flags.kSaveCollision) {
652 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
654 LOG_ERROR(
"DungeonEditorV2",
"Failed to save collision: %s",
655 status.message().data());
660 if (flags.kSaveWaterFillZones) {
661 auto status = SaveWaterFillZones(
rom_,
rooms_);
663 LOG_ERROR(
"DungeonEditorV2",
"Failed to save water fill zones: %s",
664 status.message().data());
669 if (flags.kSaveChests) {
671 rom_, *chest_save_plan,
674 LOG_ERROR(
"DungeonEditorV2",
"Failed to save chests: %s",
675 status.message().data());
680 if (flags.kSavePotItems) {
684 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pot items: %s",
685 status.message().data());
690 if (flags.kSaveEntrances) {
693 LOG_ERROR(
"DungeonEditorV2",
"Failed to save dungeon entrances: %s",
694 status.message().data());
699 LOG_ERROR(
"DungeonEditorV2",
"Failed to save dungeon spawn points: %s",
700 status.message().data());
705 return absl::OkStatus();
710 std::vector<std::pair<uint32_t, uint32_t>> ranges;
720 if (flags.kSavePalettes) {
721 auto palette_ranges =
722 palette_manager.GetModifiedColorWriteRanges(
game_data_);
723 ranges.insert(ranges.end(), palette_ranges.begin(), palette_ranges.end());
726 if (flags.kSaveEntrances) {
727 auto entrance_ranges =
729 ranges.insert(ranges.end(), entrance_ranges.begin(), entrance_ranges.end());
732 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
736 bool stream_dirty,
int room_id) {
744 if (layout ==
nullptr ||
747 static_cast<uint32_t
>(room_id) >= layout->pointer_count) {
751 for (
const auto& range : layout->allocation_regions) {
755 const uint32_t pointer_width =
758 const uint32_t pointer_slot =
760 static_cast<uint32_t
>(room_id) * pointer_width;
761 ranges.emplace_back(pointer_slot, pointer_slot + pointer_width);
763 const uint32_t door_slot =
765 ranges.emplace_back(door_slot, door_slot + 3u);
773 if (flags.kSaveWaterFillZones) {
774 auto water_fill_ranges = CollectDirtyWaterFillWriteRanges(
rom_,
rooms_);
775 ranges.insert(ranges.end(), water_fill_ranges.begin(),
776 water_fill_ranges.end());
782 if (flags.kSaveCollision) {
783 auto collision_ranges =
784 CollectDirtyCustomCollisionWriteRanges(
rom_,
rooms_);
785 ranges.insert(ranges.end(), collision_ranges.begin(),
786 collision_ranges.end());
792 if (flags.kSaveChests) {
793 auto chest_ranges = CollectDirtyChestWriteRanges(
rom_,
rooms_);
794 if (chest_ranges.ok()) {
795 ranges.insert(ranges.end(), chest_ranges->begin(), chest_ranges->end());
797 LOG_WARN(
"DungeonEditorV2",
"Could not predict chest write ranges: %s",
798 chest_ranges.status().message().data());
807 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
808 int header_ptr_table =
809 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
810 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
811 rom_data[zelda3::kRoomHeaderPointer];
812 header_ptr_table = yaze::SnesToPc(header_ptr_table);
813 int table_offset = header_ptr_table + (room_id * 2);
815 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
816 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
817 (rom_data[table_offset + 1] << 8) |
818 rom_data[table_offset];
819 int header_location = yaze::SnesToPc(address);
820 ranges.emplace_back(header_location, header_location + 14);
824 if (message_address >= 0 &&
825 message_address + 1 <
static_cast<int>(rom_data.size())) {
826 ranges.emplace_back(message_address, message_address + 2);
832 const uint32_t door_slot =
834 ranges.emplace_back(door_slot, door_slot + 3u);
840 int entry_offset = obj_ptr_table + (room_id * 3);
842 if (entry_offset + 2 <
static_cast<int>(rom_data.size())) {
843 int tile_addr = (rom_data[entry_offset + 2] << 16) |
844 (rom_data[entry_offset + 1] << 8) |
845 rom_data[entry_offset];
849 ranges.emplace_back(objects_location,
850 objects_location + encoded.size() + 2);
857 const uint32_t table_snes =
863 if ((table_snes & 0xFFFFu) >= 0x8000u) {
865 const int pointer_slot = table_pc + room_id * 3;
866 if (pointer_slot >= 0 &&
867 pointer_slot + 2 <
static_cast<int>(rom_data.size())) {
868 const uint32_t stream_snes =
869 (
static_cast<uint32_t
>(rom_data[pointer_slot + 2]) << 16) |
870 (
static_cast<uint32_t
>(rom_data[pointer_slot + 1]) << 8) |
871 rom_data[pointer_slot];
872 if ((stream_snes & 0xFFFFu) >= 0x8000u) {
874 if (
static_cast<uint64_t
>(stream_pc) + 2u <= rom_data.size()) {
875 ranges.emplace_back(stream_pc, stream_pc + 2u);
885 const uint32_t table_snes =
891 const int pointer_slot = table_pc + room_id * 2;
892 if (pointer_slot >= 0 &&
893 pointer_slot + 1 <
static_cast<int>(rom_data.size())) {
894 const uint16_t pointer =
895 (
static_cast<uint16_t
>(rom_data[pointer_slot + 1]) << 8) |
896 rom_data[pointer_slot];
897 if (pointer >= 0x8000) {
899 ranges.emplace_back(stream_pc,
905 append_declared_cow_ranges(
907 flags.kSaveObjects &&
917 if (flags.kSavePotItems) {
919 CollectDirtyPotItemWriteRanges(dependencies_.project, rom_, rooms_);
920 ranges.insert(ranges.end(), pot_ranges.begin(), pot_ranges.end());
926absl::Status DungeonEditorV2::SaveRoom(
int room_id) {
927 if (minecart_track_editor_panel_ !=
nullptr &&
928 minecart_track_editor_panel_->HasUnpublishedChanges()) {
929 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
931 if (object_tile_editor_panel_ !=
nullptr &&
932 object_tile_editor_panel_->HasUnappliedChanges()) {
933 return absl::FailedPreconditionError(
934 "Apply or explicitly discard Object Tile Editor changes before "
935 "using Save or Apply Room");
937 return RunWithSaveTransaction([
this, room_id]() -> absl::Status {
938 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size())) {
939 return absl::InvalidArgumentError(
"Invalid room ID");
943 std::optional<zelda3::ChestSavePlan> chest_save_plan;
944 if (flags.kSaveCollision) {
946 dependencies_.project, rom_, rooms_,
947 absl::StrFormat(
"custom collision for room 0x%03X", room_id),
948 dependencies_.toast_manager));
950 if (flags.kSaveWaterFillZones) {
952 dependencies_.project, rom_, rooms_,
953 absl::StrFormat(
"water fill zones for room 0x%03X", room_id),
954 dependencies_.toast_manager));
956 if (flags.kSaveEntrances) {
958 dependencies_.project, rom_, entrances_, spawn_points_,
960 "dungeon entrances and spawn points (Apply Room 0x%03X)",
962 dependencies_.toast_manager));
964 if (flags.kSaveChests) {
967 rom_,
static_cast<int>(rooms_.size()), [
this](
int id) {
968 return rooms_.GetIfMaterialized(id);
971 dependencies_.project, plan,
972 absl::StrFormat(
"chest table for room 0x%03X", room_id),
973 dependencies_.toast_manager));
974 chest_save_plan = std::move(plan);
976 if (flags.kSavePotItems) {
978 dependencies_.project, rom_, rooms_,
979 absl::StrFormat(
"pot items for room 0x%03X", room_id),
980 dependencies_.toast_manager));
983 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(game_data_)) {
984 if (!palette_manager.IsManaging(game_data_)) {
985 return absl::FailedPreconditionError(
986 "Cannot save dungeon palettes from a different ROM session");
989 dependencies_.project, palette_manager, game_data_,
990 absl::StrFormat(
"dungeon palettes for room 0x%03X", room_id),
991 dependencies_.toast_manager));
992 auto status = palette_manager.SaveAllToRom();
994 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
995 status.message().data());
999 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1003 if (flags.kSaveTorches) {
1005 rom_,
static_cast<int>(rooms_.size()),
1006 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1008 if (flags.kSavePits) {
1010 rom_, game_data_ ? &game_data_->pit_damage_table :
nullptr));
1012 if (flags.kSaveBlocks) {
1014 rom_,
static_cast<int>(rooms_.size()),
1015 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1017 if (flags.kSaveCollision) {
1019 rom_,
static_cast<int>(rooms_.size()),
1020 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1022 if (flags.kSaveWaterFillZones) {
1025 if (flags.kSaveChests) {
1027 rom_, *chest_save_plan,
1028 [
this](
int id) {
return rooms_.GetIfMaterialized(
id); }));
1030 if (flags.kSavePotItems) {
1032 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_));
1034 if (flags.kSaveEntrances) {
1039 return absl::OkStatus();
1043absl::Status DungeonEditorV2::SaveRoomData(
int room_id) {
1044 if (!rom_ || !rom_->is_loaded()) {
1045 return absl::FailedPreconditionError(
"ROM not loaded");
1047 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size())) {
1048 return absl::InvalidArgumentError(
"Invalid room ID");
1051 auto& room = rooms_[room_id];
1052 if (!room.IsLoaded()) {
1053 return absl::OkStatus();
1060 for (
const auto& w : result.warnings) {
1061 LOG_WARN(
"DungeonEditorV2",
"Room 0x%03X validation warning: %s", room_id,
1064 if (!result.is_valid) {
1065 for (
const auto& e : result.errors) {
1066 LOG_ERROR(
"DungeonEditorV2",
"Room 0x%03X validation error: %s",
1067 room_id, e.c_str());
1069 if (dependencies_.toast_manager) {
1070 dependencies_.toast_manager->Show(
1072 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1073 room_id, result.errors.size()),
1076 return absl::FailedPreconditionError(
1077 absl::StrFormat(
"Room 0x%03X failed validation", room_id));
1086 std::optional<zelda3::DungeonStreamLayout> object_cow_layout;
1087 std::optional<zelda3::DungeonStreamLayout> sprite_cow_layout;
1088 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1089 const auto& manifest = dependencies_.project->hack_manifest;
1090 auto load_cow_layout =
1092 std::optional<zelda3::DungeonStreamLayout>* out) -> absl::Status {
1093 if (!stream_dirty) {
1094 return absl::OkStatus();
1096 const auto* manifest_layout =
1097 manifest.GetDungeonStreamLayout(stream_type);
1098 if (manifest_layout ==
nullptr ||
1099 manifest_layout->strategy !=
1101 return absl::OkStatus();
1106 *out = std::move(allocator_layout);
1107 return absl::OkStatus();
1112 flags.kSaveObjects &&
1113 (room.object_stream_dirty() || room.object_stream_header_dirty()),
1114 &object_cow_layout));
1116 flags.kSaveSprites && room.sprites_dirty(),
1117 &sprite_cow_layout));
1121 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1122 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1123 const auto& manifest = dependencies_.project->hack_manifest;
1124 const auto& rom_data = rom_->vector();
1127 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1129 int header_ptr_table =
1134 int table_offset = header_ptr_table + (room_id * 2);
1136 if (table_offset + 1 <
static_cast<int>(rom_data.size())) {
1138 (rom_data[table_offset + 1] << 8) |
1139 rom_data[table_offset];
1141 ranges.emplace_back(header_location, header_location + 14);
1145 if (message_address >= 0 &&
1146 message_address + 1 <
static_cast<int>(rom_data.size())) {
1147 ranges.emplace_back(message_address, message_address + 2);
1152 if (flags.kSaveObjects && room.object_stream_dirty()) {
1153 const uint32_t door_slot =
1155 ranges.emplace_back(door_slot, door_slot + 3u);
1161 int entry_offset = obj_ptr_table + (room_id * 3);
1163 if (entry_offset + 2 <
static_cast<int>(rom_data.size())) {
1164 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1165 (rom_data[entry_offset + 1] << 8) |
1166 rom_data[entry_offset];
1173 auto encoded = room.EncodeObjects();
1174 ranges.emplace_back(objects_location,
1175 objects_location + encoded.size() + 2);
1180 if (flags.kSaveObjects && room.object_stream_header_dirty() &&
1182 const uint32_t table_snes =
1188 if ((table_snes & 0xFFFFu) >= 0x8000u) {
1190 const int pointer_slot = table_pc + room_id * 3;
1191 if (pointer_slot >= 0 &&
1192 pointer_slot + 2 <
static_cast<int>(rom_data.size())) {
1193 const uint32_t stream_snes =
1194 (
static_cast<uint32_t
>(rom_data[pointer_slot + 2]) << 16) |
1195 (
static_cast<uint32_t
>(rom_data[pointer_slot + 1]) << 8) |
1196 rom_data[pointer_slot];
1197 if ((stream_snes & 0xFFFFu) >= 0x8000u) {
1199 if (
static_cast<uint64_t
>(stream_pc) + 2u <= rom_data.size()) {
1200 ranges.emplace_back(stream_pc, stream_pc + 2u);
1208 if (flags.kSaveSprites && room.sprites_dirty() &&
1210 const uint32_t table_snes =
1216 const int pointer_slot = table_pc + room_id * 2;
1217 if (pointer_slot >= 0 &&
1218 pointer_slot + 1 <
static_cast<int>(rom_data.size())) {
1219 const uint16_t pointer =
1220 (
static_cast<uint16_t
>(rom_data[pointer_slot + 1]) << 8) |
1221 rom_data[pointer_slot];
1222 if (pointer >= 0x8000) {
1224 ranges.emplace_back(stream_pc,
1225 stream_pc + room.EncodeSprites().size() + 1);
1235 for (
const auto& range : layout.allocation_ranges) {
1236 ranges.emplace_back(range.begin, range.end);
1238 const uint32_t pointer_width =
1242 const uint32_t pointer_slot =
1243 layout.pointer_table_pc +
1244 static_cast<uint32_t
>(room_id) * pointer_width;
1245 ranges.emplace_back(pointer_slot, pointer_slot + pointer_width);
1247 const uint32_t door_slot =
1249 ranges.emplace_back(door_slot, door_slot + 3u);
1252 if (object_cow_layout.has_value()) {
1253 append_cow_ranges(*object_cow_layout);
1255 if (sprite_cow_layout.has_value()) {
1256 append_cow_ranges(*sprite_cow_layout);
1261 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1263 manifest, write_policy, ranges, absl::StrFormat(
"room 0x%03X", room_id),
1264 "DungeonEditorV2", dependencies_.toast_manager));
1267 if (flags.kSaveObjects && room.object_stream_dirty()) {
1268 auto status = room.SaveObjects(
1269 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr);
1271 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room objects: %s",
1272 status.message().data());
1277 if (flags.kSaveObjects && room.object_stream_header_dirty()) {
1278 auto status = room.SaveObjectStreamHeader(
1279 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr);
1282 "Failed to save room object-stream header: %s",
1283 status.message().data());
1288 if (flags.kSaveSprites && room.sprites_dirty()) {
1289 auto status = room.SaveSprites(
1290 sprite_cow_layout.has_value() ? &*sprite_cow_layout :
nullptr);
1292 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room sprites: %s",
1293 status.message().data());
1298 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1299 auto status = room.SaveRoomHeader();
1301 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room header: %s",
1302 status.message().data());
1307 return absl::OkStatus();
1310void DungeonEditorV2::SaveAllRooms() {
1311 auto status = RunWithSaveTransaction([
this]() {
return Save(); });
1313 if (dependencies_.toast_manager) {
1314 dependencies_.toast_manager->Show(
"Applied loaded rooms to ROM buffer",
1315 ToastType::kSuccess);
1318 LOG_ERROR(
"DungeonEditorV2",
"SaveAllRooms failed: %s",
1319 status.message().data());
1320 if (dependencies_.toast_manager) {
1321 dependencies_.toast_manager->Show(
1322 absl::StrFormat(
"Save all failed: %s", status.message()),
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
bool loaded() const
Check if the manifest has been loaded.
class MinecartTrackEditorPanel * minecart_track_editor_panel_
void CommitSaveTransaction() override
absl::Status SaveRoomData(int room_id)
absl::Status Save() override
std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > entrances_
ObjectTileEditorPanel * object_tile_editor_panel_
void RollbackSaveTransaction() override
absl::Status BeginSaveTransaction() override
absl::Status RunWithSaveTransaction(const std::function< absl::Status()> &operation)
std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > spawn_points_
std::optional< SaveTransactionSnapshot > save_transaction_snapshot_
zelda3::GameData * game_data_
std::vector< std::pair< uint32_t, uint32_t > > CollectWriteRanges() const
zelda3::Room * GetIfMaterialized(int room_id)
void ForEachMaterialized(Fn &&fn)
void ForEachLoaded(Fn &&fn)
EditorDependencies dependencies_
bool HasUnpublishedChanges() const
bool HasUnappliedChanges() const
Centralized palette management system.
std::vector< std::pair< uint32_t, uint32_t > > GetModifiedColorWriteRanges() const
Get exact, coalesced half-open ROM ranges for modified colors.
void RollbackSaveTransaction()
void CommitSaveTransaction()
static PaletteManager & Get()
Get the singleton instance.
ValidationResult ValidateRoom(const Room &room)
void set_water_fill_sram_bit_mask(uint8_t mask)
bool custom_collision_dirty() const
const WaterFillZoneMap & water_fill_zone() const
int WaterFillTileCount() const
bool pot_items_dirty() const
bool object_stream_dirty() const
bool sprites_dirty() const
bool header_dirty() const
bool object_stream_header_dirty() const
bool has_water_fill_zone() const
std::vector< uint8_t > EncodeObjects() const
void ClearWaterFillDirty()
std::vector< uint8_t > EncodeSprites() const
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
bool water_fill_dirty() const
const std::vector< PotItem > & GetPotItems() const
uint8_t water_fill_sram_bit_mask() const
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
DungeonStreamType
Dungeon stream kinds with allocator layouts in the hack manifest.
absl::StatusOr< zelda3::DungeonStreamLayout > ToDungeonStreamAllocatorLayout(DungeonStreamType stream_type, const DungeonStreamLayout &manifest_layout)
absl::Status ValidatePotItemManifestConflicts(const project::YazeProject *project, Rom *rom, const DungeonRoomStore &rooms, absl::string_view save_scope, ToastManager *toast_manager)
std::vector< std::pair< uint32_t, uint32_t > > CollectDirtyWaterFillWriteRanges(const Rom *rom, const DungeonRoomStore &rooms)
absl::Status ValidateCustomCollisionManifestConflicts(const project::YazeProject *project, const Rom *rom, const DungeonRoomStore &rooms, absl::string_view save_scope, ToastManager *toast_manager)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > CollectDirtyChestWriteRanges(const Rom *rom, const DungeonRoomStore &rooms)
std::vector< std::pair< uint32_t, uint32_t > > CollectDirtyPotItemWriteRanges(const project::YazeProject *project, const Rom *rom, const DungeonRoomStore &rooms)
absl::Status SaveAllPotItemsForProject(const project::YazeProject *project, Rom *rom, DungeonRoomStore &rooms)
constexpr char kMinecartDraftSaveBlockMessage[]
absl::Status ValidateDungeonEntranceSavePreflight(const project::YazeProject *project, Rom *rom, const std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > &entrances, const std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > &spawn_points, absl::string_view save_scope, ToastManager *toast_manager)
absl::Status ValidatePaletteManifestConflicts(const project::YazeProject *project, const gfx::PaletteManager &palette_manager, const zelda3::GameData *game_data, absl::string_view save_scope, ToastManager *toast_manager)
std::vector< std::pair< uint32_t, uint32_t > > CollectDirtyCustomCollisionWriteRanges(const Rom *rom, const DungeonRoomStore &rooms)
absl::Status ValidateChestManifestConflicts(const project::YazeProject *project, const zelda3::ChestSavePlan &plan, absl::string_view save_scope, ToastManager *toast_manager)
absl::Status SaveWaterFillZones(Rom *rom, DungeonRoomStore &rooms)
absl::Status ValidateWaterFillManifestConflicts(const project::YazeProject *project, const Rom *rom, const DungeonRoomStore &rooms, absl::string_view save_scope, ToastManager *toast_manager)
Editors are the view controllers for the application.
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
constexpr int kDoorPointers
constexpr int kWaterFillTableEnd
constexpr int kCustomCollisionDataSoftEnd
absl::StatusOr< ChestSavePlan > BuildChestSavePlan(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
constexpr int kWaterFillTableStart
constexpr int kMessagesIdDungeon
absl::Status NormalizeWaterFillZoneMasks(std::vector< WaterFillZoneEntry > *zones)
absl::Status SaveAllPotItems(Rom *rom, absl::Span< const Room > rooms)
std::vector< DungeonEntranceWriteRange > CollectDirtyRegularDungeonEntranceWriteRanges(const std::array< RoomEntrance, kNumDungeonEntranceSlots > &entrances)
absl::Status SaveAllDungeonSpawnPoints(Rom *rom, std::array< DungeonSpawnPoint, kNumDungeonSpawnPoints > &spawn_points, bool dirty_only=true)
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
constexpr int kRoomsSpritePointer
absl::Status SaveAllPits(Rom *rom)
absl::Status SaveAllBlocks(Rom *rom)
constexpr int kRoomItemsPointers
constexpr int kCustomCollisionDataPosition
absl::Status RejectUnsupportedDungeonSpawnPointSaves(const std::array< RoomEntrance, kNumDungeonEntranceSlots > &entrances, bool dirty_only=true)
absl::Status SaveAllDungeonEntrances(Rom *rom, std::array< RoomEntrance, kNumDungeonEntranceSlots > &entrances, bool dirty_only=true)
absl::Status ValidateRegularDungeonEntrancesForSave(const Rom &rom, const std::array< RoomEntrance, kNumDungeonEntranceSlots > &entrances, bool dirty_only=true)
constexpr int kNumberOfRooms
absl::Status ApplyChestSavePlan(Rom *rom, const ChestSavePlan &plan, const std::function< const Room *(int)> &room_lookup)
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
absl::Status ValidateDungeonSpawnPointsForSave(const Rom &rom, const std::array< DungeonSpawnPoint, kNumDungeonSpawnPoints > &spawn_points, bool dirty_only=true)
constexpr int kRoomObjectPointer
std::vector< DungeonSpawnPointWriteRange > CollectDirtyDungeonSpawnPointWriteRanges(const std::array< DungeonSpawnPoint, kNumDungeonSpawnPoints > &spawn_points)
absl::Status WriteWaterFillTable(Rom *rom, const std::vector< WaterFillZoneEntry > &zones)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
DungeonPointerEncoding pointer_encoding
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
bool has_palette_transaction
std::vector< std::pair< int, zelda3::Room::SaveDirtySnapshot > > room_states
std::array< bool, zelda3::kNumDungeonSpawnPoints > spawn_dirty_states
bool has_pit_damage_table
std::array< bool, zelda3::kNumDungeonEntranceSlots > entrance_dirty_states
project::YazeProject * project
ToastManager * toast_manager
Modern project structure with comprehensive settings consolidation.
core::HackManifest hack_manifest
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
PitDamageTable pit_damage_table
std::vector< uint16_t > fill_offsets
std::array< uint8_t, 64 *64 > tiles