15#include "absl/status/status.h"
16#include "absl/strings/cord.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
19#include "absl/strings/str_join.h"
20#include "absl/strings/string_view.h"
21#include "absl/types/span.h"
66#include "imgui/imgui.h"
86 "Minecart Track Editor has unpublished drafts. ROM Save/Apply never "
87 "publishes ASM source; use Publish Tracks in the Minecart Track Editor "
88 "or discard the drafts first.";
97 const bool writes_objects = flags.kSaveObjects && room.
IsLoaded() &&
98 (only_room < 0 || only_room == id);
99 if (writes_objects == flags.kSaveChests)
101 status = absl::FailedPreconditionError(absl::StrFormat(
102 "Room 0x%03X has pending chest object and reward edits. Enable both "
103 "Room Objects and Chests, then apply that room or save all rooms "
104 "together before applying another room.",
112 return absl::FailedPreconditionError(
"ROM not loaded");
115 bool any_dirty =
false;
122 return absl::OkStatus();
127 std::map<int, zelda3::WaterFillZoneEntry> merged;
128 for (
auto& zone : saved_zones)
129 merged[zone.room_id] = std::move(zone);
130 for (
int room_id = 0; room_id < static_cast<int>(rooms.
size()); ++room_id) {
132 if (room ==
nullptr) {
137 merged.erase(room_id);
142 if (tile_count <= 0) {
145 if (tile_count > 255) {
146 return absl::InvalidArgumentError(absl::StrFormat(
147 "Water fill zone in room 0x%02X has %d tiles (max 255)", room_id,
154 z.
fill_offsets.reserve(
static_cast<size_t>(tile_count));
157 for (
size_t i = 0; i < map.size(); ++i) {
162 merged[room_id] = std::move(z);
165 std::vector<zelda3::WaterFillZoneEntry> zones;
166 for (
auto& [
id, zone] : merged)
167 zones.push_back(std::move(zone));
169 if (zones.size() > 8) {
170 return absl::InvalidArgumentError(absl::StrFormat(
171 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
178 for (
const auto& z : zones) {
188 return absl::OkStatus();
194 std::vector<std::pair<uint32_t, uint32_t>> ranges;
195 if (rom ==
nullptr || !rom->
is_loaded()) {
199 bool any_dirty =
false;
212 if (layout !=
nullptr &&
214 for (
const auto& range : layout->allocation_regions) {
218 const uint32_t pointer_width =
222 const uint32_t pointer_table_pc =
yaze::SnesToPc(layout->pointer_table);
225 pointer_table_pc + layout->pointer_count * pointer_width);
230 const auto& rom_data = rom->
vector();
236 if (pointer_slot < 0 ||
237 pointer_slot + 1 >=
static_cast<int>(rom_data.size())) {
240 const uint16_t pointer =
241 (
static_cast<uint16_t
>(rom_data[pointer_slot + 1]) << 8) |
242 rom_data[pointer_slot];
243 if (pointer < 0x8000) {
247 const uint32_t encoded_size =
248 static_cast<uint32_t
>(room.
GetPotItems().size() * 3 + 2);
249 ranges.emplace_back(stream_pc, stream_pc + encoded_size);
254absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
259 rom,
static_cast<int>(rooms.
size()),
260 [&rooms](
int room_id) { return rooms.GetIfMaterialized(room_id); }));
266 std::vector<std::pair<uint32_t, uint32_t>> ranges;
267 if (rom ==
nullptr || !rom->
is_loaded() ||
271 bool any_dirty =
false;
284std::vector<std::pair<uint32_t, uint32_t>>
287 std::vector<std::pair<uint32_t, uint32_t>> ranges;
289 if (rom ==
nullptr || !rom->
is_loaded() ||
291 static_cast<int>(rom->
size()) ||
295 bool any_dirty =
false;
313 std::optional<zelda3::DungeonStreamLayout> repack_layout;
314 bool any_dirty =
false;
323 if (manifest_layout !=
nullptr &&
329 repack_layout = std::move(allocator_layout);
334 rom,
static_cast<int>(rooms.
size()),
335 [&rooms](
int room_id) { return rooms.GetIfMaterialized(room_id); },
336 repack_layout.has_value() ? &*repack_layout :
nullptr);
344 return absl::OkStatus();
347 if (ranges.empty()) {
348 return absl::OkStatus();
352 save_scope,
"DungeonEditorV2", toast_manager);
363 return absl::OkStatus();
365 if (ranges.empty()) {
366 return absl::OkStatus();
370 save_scope,
"DungeonEditorV2", toast_manager);
375 const std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>&
377 const std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>&
379 absl::string_view save_scope,
ToastManager* toast_manager) {
380 if (rom ==
nullptr || !rom->
is_loaded()) {
381 return absl::FailedPreconditionError(
"ROM not loaded");
393 return absl::OkStatus();
400 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
401 if (ranges.empty()) {
402 return absl::OkStatus();
406 save_scope,
"DungeonEditorV2", toast_manager);
411 absl::string_view save_scope,
414 if (ranges.empty() || project ==
nullptr ||
416 return absl::OkStatus();
420 save_scope,
"DungeonEditorV2", toast_manager);
428 return absl::OkStatus();
431 if (ranges.empty()) {
432 return absl::OkStatus();
436 save_scope,
"DungeonEditorV2", toast_manager);
444 return absl::OkStatus();
447 if (ranges.empty()) {
448 return absl::OkStatus();
452 save_scope,
"DungeonEditorV2", toast_manager);
458 int room_id, std::optional<std::string> label) {
463 if (editor_manager ==
nullptr ||
464 !editor_manager->IsCurrentProjectContextOwnedBySession(
466 return absl::FailedPreconditionError(
467 "Activate this dungeon project before renaming rooms");
470 if (authoritative ==
nullptr || !authoritative->
project_opened()) {
471 return absl::FailedPreconditionError(
"Room names require an open project");
475 if (label.has_value()) {
481 apply(authoritative);
486 editor_manager->MarkCurrentProjectDirty();
487 editor_manager->RefreshResourceLabelProvider();
488 return absl::OkStatus();
492 int room_id,
const std::vector<zelda3::PotItem>& items) {
495 return absl::FailedPreconditionError(
"Pot-item room is not loaded");
506 auto& candidate = staged[
id];
507 candidate.GetPotItems() =
id == room_id ? items : source.
GetPotItems();
508 candidate.MarkPotItemsDirty();
517 return absl::FailedPreconditionError(
518 "Dungeon save transaction is already active");
527 palette_manager.HasUnsavedChanges(
game_data_)) {
528 if (!palette_manager.IsManaging(
game_data_)) {
529 return absl::FailedPreconditionError(
530 "Cannot save dungeon palettes from a different ROM session");
539 for (
size_t i = 0; i <
entrances_.size(); ++i) {
550 return absl::OkStatus();
560 room->RestoreSaveDirtySnapshot(state);
563 for (
size_t i = 0; i <
entrances_.size(); ++i) {
603 const std::function<absl::Status()>& operation) {
605 return absl::FailedPreconditionError(
"ROM not loaded");
617 const absl::Status status = operation();
625 return absl::OkStatus();
629 bool room_data_only)
const {
635 bool collision =
false;
636 bool water_fill =
false;
640 if (only_room < 0 || only_room ==
id) {
641 pending.object_stream |=
653 std::vector<std::string> excluded;
654 auto check = [&](
bool dirty,
bool enabled,
const char* label) {
655 if (dirty && !enabled) {
656 excluded.emplace_back(label);
659 check(pending.object_stream, flags.kSaveObjects,
"Save Objects");
660 check(pending.sprites, flags.kSaveSprites,
"Save Sprites");
661 check(pending.header, flags.kSaveRoomHeaders,
"Save Room Headers");
662 check(pending.torches, flags.kSaveTorches,
"Save Torches");
663 check(pending.blocks, flags.kSaveBlocks,
"Save Blocks");
664 check(collision, flags.kSaveCollision,
"Save Collision");
665 check(pending.chests, flags.kSaveChests,
"Save Chests");
666 check(pending.pot_items, flags.kSavePotItems,
"Save Pot Items");
667 if (!room_data_only) {
670 const auto dirty = [](
const auto& entrance) {
671 return entrance.dirty();
675 flags.kSaveEntrances,
"Save Entrances");
678 if (!excluded.empty()) {
679 message = absl::StrCat(
680 "Save blocked: pending dungeon edits are excluded by ",
681 absl::StrJoin(excluded,
", "),
682 ". Open File > Save Scope > Dungeon, enable these choices and retry "
683 "Save or Apply Room. Or undo the excluded edits. No project reopen "
688 if (water_fill && !flags.kSaveWaterFillZones) {
689 absl::StrAppend(&message, message.empty() ?
"Save blocked: " :
" ",
690 "Water Fill edits are pending while their project save "
691 "scope is disabled. Undo these edits or restore the "
692 "project's Water Fill save setting before saving.");
694 if (!message.empty()) {
695 return absl::FailedPreconditionError(message);
697 if (!flags.kSaveRoomHeaders) {
698 return absl::OkStatus();
702 absl::Status ownership;
705 (only_room >= 0 && only_room !=
id)) {
709 ownership = absl::FailedPreconditionError(
"ROM not loaded");
710 }
else if (room.
rom() !=
rom_) {
711 ownership = absl::FailedPreconditionError(
712 absl::StrFormat(
"Room 0x%03X belongs to another ROM", id));
714 ownership = ValidateRoomHeaderOwnership(*rom_, room.id());
723 return absl::InvalidArgumentError(
"Invalid room ID");
729 const auto table_bank = (table_snes >> 16) & 0xFF;
730 if ((table_snes & 0xFFFF) < 0x8000 || table_bank == 0x7E ||
731 table_bank == 0x7F || table_bank == 0xFE || table_bank == 0xFF ||
732 bank == 0x7E || bank == 0x7F || bank == 0xFE || bank == 0xFF) {
733 return absl::FailedPreconditionError(
"Invalid room header pointer table");
735 const auto table_pc =
SnesToPc(table_snes);
738 return absl::FailedPreconditionError(
739 "Room header pointer table is truncated");
742 const auto header_pc =
SnesToPc((bank << 16) | header);
743 if (header < 0x8000 ||
static_cast<uint64_t
>(header_pc) + 14 >
rom.
size()) {
744 return absl::FailedPreconditionError(
745 absl::StrFormat(
"Room %03X header pointer is invalid", room_id));
748 if (other_id == room_id)
752 const auto other_pc =
SnesToPc((bank << 16) | other_header);
753 if (other_header < 0x8000 ||
754 static_cast<uint64_t
>(other_pc) + 14 >
rom.
size())
756 if (header_pc < other_pc + 14 && other_pc < header_pc + 14) {
757 auto status = absl::FailedPreconditionError(absl::StrFormat(
758 "Room %03X header overlaps room %03X; writing its room properties "
759 "would also change that room. Undo or exclude the room property "
766 return absl::OkStatus();
773 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
776 return absl::FailedPreconditionError(
"ROM not loaded");
780 return absl::FailedPreconditionError(
781 "Apply or explicitly discard Object Tile Editor changes before "
782 "using Save or Apply Room");
790 std::optional<zelda3::ChestSavePlan> chest_save_plan;
799 "dungeon room streams",
"DungeonEditorV2",
803 if (flags.kSaveCollision) {
808 if (flags.kSaveWaterFillZones) {
813 if (flags.kSaveEntrances) {
819 if (flags.kSaveChests) {
822 [
this](
int room_id) {
823 return rooms_.GetIfMaterialized(room_id);
828 chest_save_plan = std::move(plan);
830 if (flags.kSavePotItems) {
837 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(
game_data_)) {
838 if (!palette_manager.IsManaging(
game_data_)) {
839 return absl::FailedPreconditionError(
840 "Cannot save dungeon palettes from a different ROM session");
845 const size_t modified_color_count =
846 palette_manager.GetModifiedColorCount(
game_data_);
847 auto status = palette_manager.SaveAllToRom();
849 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
850 status.message().data());
853 LOG_INFO(
"DungeonEditorV2",
"Saved %zu modified colors to ROM",
854 modified_color_count);
857 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
858 absl::Status save_status = absl::OkStatus();
860 if (!save_status.ok()) {
865 save_status = status;
868 if (!save_status.ok()) {
873 if (flags.kSaveTorches) {
876 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
878 LOG_ERROR(
"DungeonEditorV2",
"Failed to save torches: %s",
879 status.message().data());
884 if (flags.kSavePits) {
888 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pits: %s",
889 status.message().data());
894 if (flags.kSaveBlocks) {
897 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
899 LOG_ERROR(
"DungeonEditorV2",
"Failed to save blocks: %s",
900 status.message().data());
905 if (flags.kSaveCollision) {
908 [
this](
int room_id) { return rooms_.GetIfMaterialized(room_id); });
910 LOG_ERROR(
"DungeonEditorV2",
"Failed to save collision: %s",
911 status.message().data());
916 if (flags.kSaveWaterFillZones) {
917 auto status = SaveWaterFillZones(
rom_,
rooms_);
919 LOG_ERROR(
"DungeonEditorV2",
"Failed to save water fill zones: %s",
920 status.message().data());
925 if (flags.kSaveChests) {
927 rom_, *chest_save_plan,
930 LOG_ERROR(
"DungeonEditorV2",
"Failed to save chests: %s",
931 status.message().data());
936 if (flags.kSavePotItems) {
940 LOG_ERROR(
"DungeonEditorV2",
"Failed to save pot items: %s",
941 status.message().data());
946 if (flags.kSaveEntrances) {
949 LOG_ERROR(
"DungeonEditorV2",
"Failed to save dungeon entrances: %s",
950 status.message().data());
955 LOG_ERROR(
"DungeonEditorV2",
"Failed to save dungeon spawn points: %s",
956 status.message().data());
961 return absl::OkStatus();
966 std::vector<std::pair<uint32_t, uint32_t>> ranges;
976 if (flags.kSavePalettes) {
977 auto palette_ranges =
978 palette_manager.GetModifiedColorWriteRanges(
game_data_);
979 if (palette_ranges.ok()) {
980 ranges.insert(ranges.end(), palette_ranges->begin(),
981 palette_ranges->end());
983 LOG_ERROR(
"DungeonEditorV2",
"Cannot predict palette write ranges: %s",
984 palette_ranges.status().message().data());
991 if (flags.kSaveEntrances) {
992 auto entrance_ranges =
995 if (entrance_ranges.ok()) {
996 ranges.insert(ranges.end(), entrance_ranges->begin(),
997 entrance_ranges->end());
1003 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
1007 if (stream_ranges.ok()) {
1008 ranges.insert(ranges.end(), stream_ranges->begin(), stream_ranges->end());
1012 "Could not predict room stream write ranges: %s",
1013 stream_ranges.status().message().data());
1020 if (flags.kSaveWaterFillZones) {
1021 auto water_fill_ranges = CollectDirtyWaterFillWriteRanges(
rom_,
rooms_);
1022 ranges.insert(ranges.end(), water_fill_ranges.begin(),
1023 water_fill_ranges.end());
1029 if (flags.kSaveCollision) {
1030 auto collision_ranges =
1031 CollectDirtyCustomCollisionWriteRanges(
rom_,
rooms_);
1032 ranges.insert(ranges.end(), collision_ranges.begin(),
1033 collision_ranges.end());
1039 if (flags.kSaveChests) {
1040 auto chest_ranges = CollectDirtyChestWriteRanges(
rom_,
rooms_);
1041 if (chest_ranges.ok()) {
1042 ranges.insert(ranges.end(), chest_ranges->begin(), chest_ranges->end());
1044 LOG_WARN(
"DungeonEditorV2",
"Could not predict chest write ranges: %s",
1045 chest_ranges.status().message().data());
1050 room_id = room.
id();
1054 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
1055 int header_ptr_table =
1056 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
1057 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
1058 rom_data[zelda3::kRoomHeaderPointer];
1059 header_ptr_table = yaze::SnesToPc(header_ptr_table);
1060 int table_offset = header_ptr_table + (room_id * 2);
1062 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1063 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1064 (rom_data[table_offset + 1] << 8) |
1065 rom_data[table_offset];
1066 int header_location = yaze::SnesToPc(address);
1067 ranges.emplace_back(header_location, header_location + 14);
1071 if (message_address >= 0 &&
1072 message_address + 1 <
static_cast<int>(rom_data.size())) {
1073 ranges.emplace_back(message_address, message_address + 2);
1080 if (flags.kSavePotItems) {
1082 CollectDirtyPotItemWriteRanges(dependencies_.project, rom_, rooms_);
1083 ranges.insert(ranges.end(), pot_ranges.begin(), pot_ranges.end());
1089absl::Status DungeonEditorV2::SaveRoom(
int room_id) {
1090 return SaveRoomImpl(room_id,
false);
1095absl::Status DungeonEditorV2::SaveRoomImpl(
int room_id,
bool room_data_only) {
1096 if (minecart_track_editor_panel_ !=
nullptr &&
1097 minecart_track_editor_panel_->HasUnpublishedChanges()) {
1098 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
1100 if (object_tile_editor_panel_ !=
nullptr &&
1101 object_tile_editor_panel_->HasUnappliedChanges()) {
1102 return absl::FailedPreconditionError(
1103 "Apply or explicitly discard Object Tile Editor changes before "
1104 "using Save or Apply Room");
1106 auto operation = [
this, room_id, room_data_only]() -> absl::Status {
1107 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size())) {
1108 return absl::InvalidArgumentError(
"Invalid room ID");
1113 std::optional<zelda3::ChestSavePlan> chest_save_plan;
1114 if (flags.kSaveCollision) {
1116 dependencies_.project, rom_, rooms_,
1117 absl::StrFormat(
"custom collision for room 0x%03X", room_id),
1118 dependencies_.toast_manager));
1120 if (flags.kSaveWaterFillZones) {
1122 dependencies_.project, rom_, rooms_,
1123 absl::StrFormat(
"water fill zones for room 0x%03X", room_id),
1124 dependencies_.toast_manager));
1126 if (!room_data_only && flags.kSaveEntrances) {
1128 dependencies_.project, rom_, entrances_, spawn_points_,
1130 "dungeon entrances and spawn points (Apply Room 0x%03X)",
1132 dependencies_.toast_manager));
1134 if (flags.kSaveChests) {
1137 rom_,
static_cast<int>(rooms_.size()), [
this](
int id) {
1138 return rooms_.GetIfMaterialized(id);
1141 dependencies_.project, plan,
1142 absl::StrFormat(
"chest table for room 0x%03X", room_id),
1143 dependencies_.toast_manager));
1144 chest_save_plan = std::move(plan);
1146 if (flags.kSavePotItems) {
1148 dependencies_.project, rom_, rooms_,
1149 absl::StrFormat(
"pot items for room 0x%03X", room_id),
1150 dependencies_.toast_manager));
1153 if (!room_data_only && flags.kSavePalettes &&
1154 palette_manager.HasUnsavedChanges(game_data_)) {
1155 if (!palette_manager.IsManaging(game_data_)) {
1156 return absl::FailedPreconditionError(
1157 "Cannot save dungeon palettes from a different ROM session");
1160 dependencies_.project, palette_manager, game_data_,
1161 absl::StrFormat(
"dungeon palettes for room 0x%03X", room_id),
1162 dependencies_.toast_manager));
1163 auto status = palette_manager.SaveAllToRom();
1165 LOG_ERROR(
"DungeonEditorV2",
"Failed to save palette changes: %s",
1166 status.message().data());
1170 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1174 if (flags.kSaveTorches) {
1176 rom_,
static_cast<int>(rooms_.size()),
1177 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1179 if (!room_data_only && flags.kSavePits) {
1181 rom_, game_data_ ? &game_data_->pit_damage_table :
nullptr));
1183 if (flags.kSaveBlocks) {
1185 rom_,
static_cast<int>(rooms_.size()),
1186 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1188 if (flags.kSaveCollision) {
1190 rom_,
static_cast<int>(rooms_.size()),
1191 [
this](
int id) { return rooms_.GetIfMaterialized(id); }));
1193 if (flags.kSaveWaterFillZones) {
1196 if (flags.kSaveChests) {
1198 rom_, *chest_save_plan,
1199 [
this](
int id) {
return rooms_.GetIfMaterialized(
id); }));
1201 if (flags.kSavePotItems) {
1203 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_));
1205 if (!room_data_only && flags.kSaveEntrances) {
1210 return absl::OkStatus();
1214 return room_data_only ? operation() : RunWithSaveTransaction(operation);
1217absl::Status DungeonEditorV2::SaveRoomData(
int room_id) {
1218 if (!rom_ || !rom_->is_loaded()) {
1219 return absl::FailedPreconditionError(
"ROM not loaded");
1221 if (room_id < 0 || room_id >=
static_cast<int>(rooms_.size())) {
1222 return absl::InvalidArgumentError(
"Invalid room ID");
1225 auto& room = rooms_[room_id];
1226 if (!room.IsLoaded()) {
1227 return absl::OkStatus();
1234 for (
const auto& w : result.warnings) {
1235 LOG_WARN(
"DungeonEditorV2",
"Room 0x%03X validation warning: %s", room_id,
1238 if (!result.is_valid) {
1239 for (
const auto& e : result.errors) {
1240 LOG_ERROR(
"DungeonEditorV2",
"Room 0x%03X validation error: %s",
1241 room_id, e.c_str());
1243 if (dependencies_.toast_manager) {
1244 dependencies_.toast_manager->Show(
1246 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1247 room_id, result.errors.size()),
1250 return absl::FailedPreconditionError(
1251 absl::StrFormat(
"Room 0x%03X failed validation", room_id));
1260 std::optional<zelda3::DungeonStreamLayout> object_cow_layout;
1261 std::optional<zelda3::DungeonStreamLayout> sprite_cow_layout;
1262 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1263 const auto& manifest = dependencies_.project->hack_manifest;
1264 auto load_cow_layout =
1266 std::optional<zelda3::DungeonStreamLayout>* out) -> absl::Status {
1267 if (!stream_dirty) {
1268 return absl::OkStatus();
1270 const auto* manifest_layout =
1271 manifest.GetDungeonStreamLayout(stream_type);
1272 if (manifest_layout ==
nullptr ||
1273 manifest_layout->strategy !=
1275 return absl::OkStatus();
1280 *out = std::move(allocator_layout);
1281 return absl::OkStatus();
1286 flags.kSaveObjects &&
1287 (room.object_stream_dirty() || room.object_stream_header_dirty()),
1288 &object_cow_layout));
1290 flags.kSaveSprites && room.sprites_dirty(),
1291 &sprite_cow_layout));
1295 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1296 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1297 const auto& manifest = dependencies_.project->hack_manifest;
1298 const auto& rom_data = rom_->vector();
1301 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1303 int header_ptr_table =
1308 int table_offset = header_ptr_table + (room_id * 2);
1310 if (table_offset + 1 <
static_cast<int>(rom_data.size())) {
1312 (rom_data[table_offset + 1] << 8) |
1313 rom_data[table_offset];
1315 ranges.emplace_back(header_location, header_location + 14);
1319 if (message_address >= 0 &&
1320 message_address + 1 <
static_cast<int>(rom_data.size())) {
1321 ranges.emplace_back(message_address, message_address + 2);
1327 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1329 manifest, write_policy, ranges, absl::StrFormat(
"room 0x%03X", room_id),
1330 "DungeonEditorV2", dependencies_.toast_manager));
1333 const auto validate_stream_ranges =
1334 [&](
const std::vector<std::pair<uint32_t, uint32_t>>& ranges)
1336 if (!dependencies_.project ||
1337 !dependencies_.project->hack_manifest.loaded()) {
1338 return absl::OkStatus();
1341 dependencies_.project->hack_manifest,
1342 dependencies_.project->rom_metadata.write_policy, ranges,
1343 absl::StrFormat(
"room 0x%03X", room_id),
"DungeonEditorV2",
1344 dependencies_.toast_manager);
1347 if (flags.kSaveObjects && room.object_stream_dirty()) {
1349 room.GetObjectSaveWriteRanges(object_cow_layout.has_value()
1350 ? &*object_cow_layout
1353 auto status = room.SaveObjects(
1354 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr);
1356 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room objects: %s",
1357 status.message().data());
1362 if (flags.kSaveObjects && room.object_stream_header_dirty()) {
1367 room.GetObjectStreamHeaderSaveWriteRanges(
1368 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr));
1370 auto status = room.SaveObjectStreamHeader(
1371 object_cow_layout.has_value() ? &*object_cow_layout :
nullptr);
1374 "Failed to save room object-stream header: %s",
1375 status.message().data());
1380 if (flags.kSaveSprites && room.sprites_dirty()) {
1382 room.GetSpriteSaveWriteRanges(sprite_cow_layout.has_value()
1383 ? &*sprite_cow_layout
1386 auto status = room.SaveSprites(
1387 sprite_cow_layout.has_value() ? &*sprite_cow_layout :
nullptr);
1389 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room sprites: %s",
1390 status.message().data());
1395 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1396 auto status = room.SaveRoomHeader();
1398 LOG_ERROR(
"DungeonEditorV2",
"Failed to save room header: %s",
1399 status.message().data());
1404 return absl::OkStatus();
1407void DungeonEditorV2::SaveAllRooms() {
1408 auto status = RunWithSaveTransaction([
this]() {
return Save(); });
1410 if (dependencies_.toast_manager) {
1411 dependencies_.toast_manager->Show(
"Applied loaded rooms to ROM buffer",
1412 ToastType::kSuccess);
1415 LOG_ERROR(
"DungeonEditorV2",
"SaveAllRooms failed: %s",
1416 status.message().data());
1417 if (dependencies_.toast_manager) {
1418 dependencies_.toast_manager->Show(
1419 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...
absl::StatusOr< uint8_t > ReadByte(int offset) const
const auto & vector() const
absl::StatusOr< uint16_t > ReadWord(int offset) const
absl::StatusOr< uint32_t > ReadLong(int offset) 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 ApplyRoomLabelEdit(int room_id, std::optional< std::string > label)
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
static absl::Status ValidateRoomHeaderOwnership(const Rom &rom, int room_id)
absl::Status BeginSaveTransaction() override
static bool IsValidRoomId(int room_id)
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)
The EditorManager controls the main editor window and manages the various editor classes.
project::YazeProject * project() const
EditorDependencies dependencies_
bool HasUnpublishedChanges() const
bool HasUnappliedChanges() const
Centralized palette management system.
void RollbackSaveTransaction()
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetModifiedColorWriteRanges() const
Get exact, coalesced half-open ROM ranges for modified colors. Invalid or unbound addresses return an...
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)
void ClearRoomLabel(project::YazeProject *project, int room_id)
void SetRoomLabel(project::YazeProject *project, int room_id, const std::string &label)
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 char kRoomTransferSharedHeaderPayload[]
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.
bool project_opened() const
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