15#include "absl/status/status.h"
16#include "absl/strings/str_cat.h"
17#include "absl/strings/str_format.h"
18#include "absl/strings/str_join.h"
19#include "absl/strings/string_view.h"
20#include "absl/types/span.h"
63#include "imgui/imgui.h"
83 "Minecart Track Editor has unpublished drafts. ROM Save/Apply never "
84 "publishes ASM source; use Publish Tracks in the Minecart Track Editor "
85 "or discard the drafts first.";
94 const bool writes_objects = flags.kSaveObjects && room.
IsLoaded() &&
95 (only_room < 0 || only_room == id);
96 if (writes_objects == flags.kSaveChests)
98 status = absl::FailedPreconditionError(absl::StrFormat(
99 "Room 0x%03X has pending chest object and reward edits. Enable both "
100 "Room Objects and Chests, then apply that room or save all rooms "
101 "together before applying another room.",
109 return absl::FailedPreconditionError(
"ROM not loaded");
112 bool any_dirty =
false;
119 return absl::OkStatus();
124 std::map<int, zelda3::WaterFillZoneEntry> merged;
125 for (
auto& zone : saved_zones)
126 merged[zone.room_id] = std::move(zone);
127 for (
int room_id = 0; room_id < static_cast<int>(rooms.
size()); ++room_id) {
129 if (room ==
nullptr) {
134 merged.erase(room_id);
139 if (tile_count <= 0) {
142 if (tile_count > 255) {
143 return absl::InvalidArgumentError(absl::StrFormat(
144 "Water fill zone in room 0x%02X has %d tiles (max 255)", room_id,
151 z.
fill_offsets.reserve(
static_cast<size_t>(tile_count));
154 for (
size_t i = 0; i < map.size(); ++i) {
159 merged[room_id] = std::move(z);
162 std::vector<zelda3::WaterFillZoneEntry> zones;
163 for (
auto& [
id, zone] : merged)
164 zones.push_back(std::move(zone));
166 if (zones.size() > 8) {
167 return absl::InvalidArgumentError(absl::StrFormat(
168 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
175 for (
const auto& z : zones) {
185 return absl::OkStatus();
191 std::vector<std::pair<uint32_t, uint32_t>> ranges;
192 if (rom ==
nullptr || !rom->
is_loaded()) {
196 bool any_dirty =
false;
209 if (layout !=
nullptr &&
211 for (
const auto& range : layout->allocation_regions) {
215 const uint32_t pointer_width =
219 const uint32_t pointer_table_pc =
yaze::SnesToPc(layout->pointer_table);
222 pointer_table_pc + layout->pointer_count * pointer_width);
227 const auto& rom_data = rom->
vector();
233 if (pointer_slot < 0 ||
234 pointer_slot + 1 >=
static_cast<int>(rom_data.size())) {
237 const uint16_t pointer =
238 (
static_cast<uint16_t
>(rom_data[pointer_slot + 1]) << 8) |
239 rom_data[pointer_slot];
240 if (pointer < 0x8000) {
244 const uint32_t encoded_size =
245 static_cast<uint32_t
>(room.
GetPotItems().size() * 3 + 2);
246 ranges.emplace_back(stream_pc, stream_pc + encoded_size);
251absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
256 rom,
static_cast<int>(rooms.
size()),
257 [&rooms](
int room_id) { return rooms.GetIfMaterialized(room_id); }));
263 std::vector<std::pair<uint32_t, uint32_t>> ranges;
264 if (rom ==
nullptr || !rom->
is_loaded() ||
268 bool any_dirty =
false;
281std::vector<std::pair<uint32_t, uint32_t>>
284 std::vector<std::pair<uint32_t, uint32_t>> ranges;
286 if (rom ==
nullptr || !rom->
is_loaded() ||
288 static_cast<int>(rom->
size()) ||
292 bool any_dirty =
false;
310 std::optional<zelda3::DungeonStreamLayout> repack_layout;
311 bool any_dirty =
false;
320 if (manifest_layout !=
nullptr &&
326 repack_layout = std::move(allocator_layout);
331 rom,
static_cast<int>(rooms.
size()),
332 [&rooms](
int room_id) { return rooms.GetIfMaterialized(room_id); },
333 repack_layout.has_value() ? &*repack_layout :
nullptr);
341 return absl::OkStatus();
344 if (ranges.empty()) {
345 return absl::OkStatus();
349 save_scope,
"DungeonEditorV2", toast_manager);
358 return absl::OkStatus();
361 if (ranges.empty()) {
362 return absl::OkStatus();
366 save_scope,
"DungeonEditorV2", toast_manager);
371 const std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>&
373 const std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>&
375 absl::string_view save_scope,
ToastManager* toast_manager) {
376 if (rom ==
nullptr || !rom->
is_loaded()) {
377 return absl::FailedPreconditionError(
"ROM not loaded");
389 return absl::OkStatus();
396 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
397 if (ranges.empty()) {
398 return absl::OkStatus();
402 save_scope,
"DungeonEditorV2", toast_manager);
407 absl::string_view save_scope,
410 if (ranges.empty() || project ==
nullptr ||
412 return absl::OkStatus();
416 save_scope,
"DungeonEditorV2", toast_manager);
424 return absl::OkStatus();
427 if (ranges.empty()) {
428 return absl::OkStatus();
432 save_scope,
"DungeonEditorV2", toast_manager);
440 return absl::OkStatus();
443 if (ranges.empty()) {
444 return absl::OkStatus();
448 save_scope,
"DungeonEditorV2", toast_manager);
454 int room_id,
const std::vector<zelda3::PotItem>& items) {
457 return absl::FailedPreconditionError(
"Pot-item room is not loaded");
468 auto& candidate = staged[
id];
469 candidate.GetPotItems() =
id == room_id ? items : source.
GetPotItems();
470 candidate.MarkPotItemsDirty();
479 return absl::FailedPreconditionError(
480 "Dungeon save transaction is already active");
489 palette_manager.HasUnsavedChanges(
game_data_)) {
490 if (!palette_manager.IsManaging(
game_data_)) {
491 return absl::FailedPreconditionError(
492 "Cannot save dungeon palettes from a different ROM session");
501 for (
size_t i = 0; i <
entrances_.size(); ++i) {
512 return absl::OkStatus();
522 room->RestoreSaveDirtySnapshot(state);
525 for (
size_t i = 0; i <
entrances_.size(); ++i) {
565 const std::function<absl::Status()>& operation) {
567 return absl::FailedPreconditionError(
"ROM not loaded");
579 const absl::Status status = operation();
587 return absl::OkStatus();
591 bool room_data_only)
const {
597 bool collision =
false;
598 bool water_fill =
false;
602 if (only_room < 0 || only_room ==
id) {
603 pending.object_stream |=
615 std::vector<std::string> excluded;
616 auto check = [&](
bool dirty,
bool enabled,
const char* label) {
617 if (dirty && !enabled) {
618 excluded.emplace_back(label);
621 check(pending.object_stream, flags.kSaveObjects,
"Save Objects");
622 check(pending.sprites, flags.kSaveSprites,
"Save Sprites");
623 check(pending.header, flags.kSaveRoomHeaders,
"Save Room Headers");
624 check(pending.torches, flags.kSaveTorches,
"Save Torches");
625 check(pending.blocks, flags.kSaveBlocks,
"Save Blocks");
626 check(collision, flags.kSaveCollision,
"Save Collision");
627 check(pending.chests, flags.kSaveChests,
"Save Chests");
628 check(pending.pot_items, flags.kSavePotItems,
"Save Pot Items");
629 if (!room_data_only) {
632 const auto dirty = [](
const auto& entrance) {
633 return entrance.dirty();
637 flags.kSaveEntrances,
"Save Entrances");
640 if (!excluded.empty()) {
641 message = absl::StrCat(
642 "Save blocked: pending dungeon edits are excluded by ",
643 absl::StrJoin(excluded,
", "),
644 ". Open File > Save Scope > Dungeon, enable these choices and retry "
645 "Save or Apply Room. Or undo the excluded edits. No project reopen "
650 if (water_fill && !flags.kSaveWaterFillZones) {
651 absl::StrAppend(&message, message.empty() ?
"Save blocked: " :
" ",
652 "Water Fill edits are pending while their project save "
653 "scope is disabled. Undo these edits or restore the "
654 "project's Water Fill save setting before saving.");
656 return message.empty() ? absl::OkStatus()
657 : absl::FailedPreconditionError(message);
664 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
667 return absl::FailedPreconditionError(
"ROM not loaded");
671 return absl::FailedPreconditionError(
672 "Apply or explicitly discard Object Tile Editor changes before "
673 "using Save or Apply Room");
681 std::optional<zelda3::ChestSavePlan> chest_save_plan;
690 "dungeon room streams",
"DungeonEditorV2",
694 if (flags.kSaveCollision) {
699 if (flags.kSaveWaterFillZones) {
704 if (flags.kSaveEntrances) {
710 if (flags.kSaveChests) {
713 [
this](
int room_id) {
714 return rooms_.GetIfMaterialized(room_id);
719 chest_save_plan = std::move(plan);
721 if (flags.kSavePotItems) {
728 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(
game_data_)) {
729 if (!palette_manager.IsManaging(
game_data_)) {
730 return absl::FailedPreconditionError(
731 "Cannot save dungeon palettes from a different ROM session");
736 const size_t modified_color_count =
737 palette_manager.GetModifiedColorCount(
game_data_);
738 auto status = palette_manager.SaveAllToRom();
740 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
741 status.message().data());
744 LOG_INFO(
"DungeonEditorV2",
"Saved %zu modified colors to ROM",
745 modified_color_count);
748 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
749 absl::Status save_status = absl::OkStatus();
751 if (!save_status.ok()) {
756 save_status = status;
759 if (!save_status.ok()) {
764 if (flags.kSaveTorches) {
767 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
769 LOG_ERROR(
"DungeonEditorV2",
"Failed to save torches: %s",
770 status.message().data());
775 if (flags.kSavePits) {
779 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pits: %s",
780 status.message().data());
785 if (flags.kSaveBlocks) {
788 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
790 LOG_ERROR(
"DungeonEditorV2",
"Failed to save blocks: %s",
791 status.message().data());
796 if (flags.kSaveCollision) {
799 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
801 LOG_ERROR(
"DungeonEditorV2",
"Failed to save collision: %s",
802 status.message().data());
807 if (flags.kSaveWaterFillZones) {
808 auto status = SaveWaterFillZones(
rom_,
rooms_);
810 LOG_ERROR(
"DungeonEditorV2",
"Failed to save water fill zones: %s",
811 status.message().data());
816 if (flags.kSaveChests) {
818 rom_, *chest_save_plan,
821 LOG_ERROR(
"DungeonEditorV2",
"Failed to save chests: %s",
822 status.message().data());
827 if (flags.kSavePotItems) {
831 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pot items: %s",
832 status.message().data());
837 if (flags.kSaveEntrances) {
840 LOG_ERROR(
"DungeonEditorV2",
"Failed to save dungeon entrances: %s",
841 status.message().data());
846 LOG_ERROR(
"DungeonEditorV2",
"Failed to save dungeon spawn points: %s",
847 status.message().data());
852 return absl::OkStatus();
857 std::vector<std::pair<uint32_t, uint32_t>> ranges;
867 if (flags.kSavePalettes) {
868 auto palette_ranges =
869 palette_manager.GetModifiedColorWriteRanges(
game_data_);
870 ranges.insert(ranges.end(), palette_ranges.begin(), palette_ranges.end());
873 if (flags.kSaveEntrances) {
874 auto entrance_ranges =
877 if (entrance_ranges.ok()) {
878 ranges.insert(ranges.end(), entrance_ranges->begin(),
879 entrance_ranges->end());
885 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
889 if (stream_ranges.ok()) {
890 ranges.insert(ranges.end(), stream_ranges->begin(), stream_ranges->end());
894 "Could not predict room stream write ranges: %s",
895 stream_ranges.status().message().data());
902 if (flags.kSaveWaterFillZones) {
903 auto water_fill_ranges = CollectDirtyWaterFillWriteRanges(
rom_,
rooms_);
904 ranges.insert(ranges.end(), water_fill_ranges.begin(),
905 water_fill_ranges.end());
911 if (flags.kSaveCollision) {
912 auto collision_ranges =
913 CollectDirtyCustomCollisionWriteRanges(
rom_,
rooms_);
914 ranges.insert(ranges.end(), collision_ranges.begin(),
915 collision_ranges.end());
921 if (flags.kSaveChests) {
922 auto chest_ranges = CollectDirtyChestWriteRanges(
rom_,
rooms_);
923 if (chest_ranges.ok()) {
924 ranges.insert(ranges.end(), chest_ranges->begin(), chest_ranges->end());
926 LOG_WARN(
"DungeonEditorV2",
"Could not predict chest write ranges: %s",
927 chest_ranges.status().message().data());
936 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
937 int header_ptr_table =
938 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
939 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
940 rom_data[zelda3::kRoomHeaderPointer];
941 header_ptr_table = yaze::SnesToPc(header_ptr_table);
942 int table_offset = header_ptr_table + (room_id * 2);
944 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
945 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
946 (rom_data[table_offset + 1] << 8) |
947 rom_data[table_offset];
948 int header_location = yaze::SnesToPc(address);
949 ranges.emplace_back(header_location, header_location + 14);
953 if (message_address >= 0 &&
954 message_address + 1 <
static_cast<int>(rom_data.size())) {
955 ranges.emplace_back(message_address, message_address + 2);
962 if (flags.kSavePotItems) {
964 CollectDirtyPotItemWriteRanges(dependencies_.project, rom_, rooms_);
965 ranges.insert(ranges.end(), pot_ranges.begin(), pot_ranges.end());
971absl::Status DungeonEditorV2::SaveRoom(
int room_id) {
972 return SaveRoomImpl(room_id,
false);
977absl::Status DungeonEditorV2::SaveRoomImpl(
int room_id,
bool room_data_only) {
978 if (minecart_track_editor_panel_ !=
nullptr &&
979 minecart_track_editor_panel_->HasUnpublishedChanges()) {
980 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
982 if (object_tile_editor_panel_ !=
nullptr &&
983 object_tile_editor_panel_->HasUnappliedChanges()) {
984 return absl::FailedPreconditionError(
985 "Apply or explicitly discard Object Tile Editor changes before "
986 "using Save or Apply Room");
988 auto operation = [
this, room_id, room_data_only]() -> absl::Status {
989 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size())) {
990 return absl::InvalidArgumentError(
"Invalid room ID");
995 std::optional<zelda3::ChestSavePlan> chest_save_plan;
996 if (flags.kSaveCollision) {
998 dependencies_.project, rom_, rooms_,
999 absl::StrFormat(
"custom collision for room 0x%03X", room_id),
1000 dependencies_.toast_manager));
1002 if (flags.kSaveWaterFillZones) {
1004 dependencies_.project, rom_, rooms_,
1005 absl::StrFormat(
"water fill zones for room 0x%03X", room_id),
1006 dependencies_.toast_manager));
1008 if (!room_data_only && flags.kSaveEntrances) {
1010 dependencies_.project, rom_, entrances_, spawn_points_,
1012 "dungeon entrances and spawn points (Apply Room 0x%03X)",
1014 dependencies_.toast_manager));
1016 if (flags.kSaveChests) {
1019 rom_,
static_cast<int>(rooms_.size()), [
this](
int id) {
1020 return rooms_.GetIfMaterialized(id);
1023 dependencies_.project, plan,
1024 absl::StrFormat(
"chest table for room 0x%03X", room_id),
1025 dependencies_.toast_manager));
1026 chest_save_plan = std::move(plan);
1028 if (flags.kSavePotItems) {
1030 dependencies_.project, rom_, rooms_,
1031 absl::StrFormat(
"pot items for room 0x%03X", room_id),
1032 dependencies_.toast_manager));
1035 if (!room_data_only && flags.kSavePalettes &&
1036 palette_manager.HasUnsavedChanges(game_data_)) {
1037 if (!palette_manager.IsManaging(game_data_)) {
1038 return absl::FailedPreconditionError(
1039 "Cannot save dungeon palettes from a different ROM session");
1042 dependencies_.project, palette_manager, game_data_,
1043 absl::StrFormat(
"dungeon palettes for room 0x%03X", room_id),
1044 dependencies_.toast_manager));
1045 auto status = palette_manager.SaveAllToRom();
1047 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
1048 status.message().data());
1052 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1056 if (flags.kSaveTorches) {
1058 rom_,
static_cast<int>(rooms_.size()),
1059 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1061 if (!room_data_only && flags.kSavePits) {
1063 rom_, game_data_ ? &game_data_->pit_damage_table :
nullptr));
1065 if (flags.kSaveBlocks) {
1067 rom_,
static_cast<int>(rooms_.size()),
1068 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1070 if (flags.kSaveCollision) {
1072 rom_,
static_cast<int>(rooms_.size()),
1073 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1075 if (flags.kSaveWaterFillZones) {
1078 if (flags.kSaveChests) {
1080 rom_, *chest_save_plan,
1081 [
this](
int id) {
return rooms_.GetIfMaterialized(
id); }));
1083 if (flags.kSavePotItems) {
1085 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_));
1087 if (!room_data_only && flags.kSaveEntrances) {
1092 return absl::OkStatus();
1096 return room_data_only ? operation() : RunWithSaveTransaction(operation);
1099absl::Status DungeonEditorV2::SaveRoomData(
int room_id) {
1100 if (!rom_ || !rom_->is_loaded()) {
1101 return absl::FailedPreconditionError(
"ROM not loaded");
1103 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size())) {
1104 return absl::InvalidArgumentError(
"Invalid room ID");
1107 auto& room = rooms_[room_id];
1108 if (!room.IsLoaded()) {
1109 return absl::OkStatus();
1116 for (
const auto& w : result.warnings) {
1117 LOG_WARN(
"DungeonEditorV2",
"Room 0x%03X validation warning: %s", room_id,
1120 if (!result.is_valid) {
1121 for (
const auto& e : result.errors) {
1122 LOG_ERROR(
"DungeonEditorV2",
"Room 0x%03X validation error: %s",
1123 room_id, e.c_str());
1125 if (dependencies_.toast_manager) {
1126 dependencies_.toast_manager->Show(
1128 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1129 room_id, result.errors.size()),
1132 return absl::FailedPreconditionError(
1133 absl::StrFormat(
"Room 0x%03X failed validation", room_id));
1142 std::optional<zelda3::DungeonStreamLayout> object_cow_layout;
1143 std::optional<zelda3::DungeonStreamLayout> sprite_cow_layout;
1144 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1145 const auto& manifest = dependencies_.project->hack_manifest;
1146 auto load_cow_layout =
1148 std::optional<zelda3::DungeonStreamLayout>* out) -> absl::Status {
1149 if (!stream_dirty) {
1150 return absl::OkStatus();
1152 const auto* manifest_layout =
1153 manifest.GetDungeonStreamLayout(stream_type);
1154 if (manifest_layout ==
nullptr ||
1155 manifest_layout->strategy !=
1157 return absl::OkStatus();
1162 *out = std::move(allocator_layout);
1163 return absl::OkStatus();
1168 flags.kSaveObjects &&
1169 (room.object_stream_dirty() || room.object_stream_header_dirty()),
1170 &object_cow_layout));
1172 flags.kSaveSprites && room.sprites_dirty(),
1173 &sprite_cow_layout));
1177 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1178 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1179 const auto& manifest = dependencies_.project->hack_manifest;
1180 const auto& rom_data = rom_->vector();
1183 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1185 int header_ptr_table =
1190 int table_offset = header_ptr_table + (room_id * 2);
1192 if (table_offset + 1 <
static_cast<int>(rom_data.size())) {
1194 (rom_data[table_offset + 1] << 8) |
1195 rom_data[table_offset];
1197 ranges.emplace_back(header_location, header_location + 14);
1201 if (message_address >= 0 &&
1202 message_address + 1 <
static_cast<int>(rom_data.size())) {
1203 ranges.emplace_back(message_address, message_address + 2);
1209 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1211 manifest, write_policy, ranges, absl::StrFormat(
"room 0x%03X", room_id),
1212 "DungeonEditorV2", dependencies_.toast_manager));
1215 const auto validate_stream_ranges =
1216 [&](
const std::vector<std::pair<uint32_t, uint32_t>>& ranges)
1218 if (!dependencies_.project ||
1219 !dependencies_.project->hack_manifest.loaded()) {
1220 return absl::OkStatus();
1223 dependencies_.project->hack_manifest,
1224 dependencies_.project->rom_metadata.write_policy, ranges,
1225 absl::StrFormat(
"room 0x%03X", room_id),
"DungeonEditorV2",
1226 dependencies_.toast_manager);
1229 if (flags.kSaveObjects && room.object_stream_dirty()) {
1231 room.GetObjectSaveWriteRanges(object_cow_layout.has_value()
1232 ? &*object_cow_layout
1235 auto status = room.SaveObjects(
1236 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr);
1238 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room objects: %s",
1239 status.message().data());
1244 if (flags.kSaveObjects && room.object_stream_header_dirty()) {
1249 room.GetObjectStreamHeaderSaveWriteRanges(
1250 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr));
1252 auto status = room.SaveObjectStreamHeader(
1253 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr);
1256 "Failed to save room object-stream header: %s",
1257 status.message().data());
1262 if (flags.kSaveSprites && room.sprites_dirty()) {
1264 room.GetSpriteSaveWriteRanges(sprite_cow_layout.has_value()
1265 ? &*sprite_cow_layout
1268 auto status = room.SaveSprites(
1269 sprite_cow_layout.has_value() ? &*sprite_cow_layout :
nullptr);
1271 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room sprites: %s",
1272 status.message().data());
1277 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1278 auto status = room.SaveRoomHeader();
1280 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room header: %s",
1281 status.message().data());
1286 return absl::OkStatus();
1289void DungeonEditorV2::SaveAllRooms() {
1290 auto status = RunWithSaveTransaction([
this]() {
return Save(); });
1292 if (dependencies_.toast_manager) {
1293 dependencies_.toast_manager->Show(
"Applied loaded rooms to ROM buffer",
1294 ToastType::kSuccess);
1297 LOG_ERROR(
"DungeonEditorV2",
"SaveAllRooms failed: %s",
1298 status.message().data());
1299 if (dependencies_.toast_manager) {
1300 dependencies_.toast_manager->Show(
1301 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
uint64_t entrance_property_save_generation_
absl::Status SaveRoomData(int room_id)
absl::Status Save() override
std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > entrances_
absl::Status PreflightPotItemPlacement(int room_id, const std::vector< zelda3::PotItem > &items)
absl::Status ValidateSaveScope(int only_room=-1, bool room_data_only=false) const
void FinalizePendingUndoActions()
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > PlanPendingRoomStreamRanges(const std::vector< DungeonSelectionEditPlan > &candidates) const
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)
zelda3::Room * GetIfLoaded(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
bool chests_dirty() const
void ClearWaterFillDirty()
bool blocks_dirty() const
bool torches_dirty() 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 ValidateChestObjectSaveCoupling(const DungeonRoomStore &rooms, int only_room=-1)
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 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::StatusOr< std::vector< DungeonEntranceWriteRange > > CollectDirtyRegularDungeonEntranceWriteRanges(const Rom &rom, const std::array< RoomEntrance, kNumDungeonEntranceSlots > &entrances)
absl::Status SaveAllPotItems(Rom *rom, absl::Span< const Room > rooms)
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)
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)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
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