yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2_persistence.cc
Go to the documentation of this file.
1// Persistence (ROM save) implementation for DungeonEditorV2. Split out of
2// dungeon_editor_v2.cc to keep that translation unit within the editor
3// source-size guardrail. Class declaration lives in dungeon_editor_v2.h.
4
5#include <algorithm>
6#include <cctype>
7#include <cstdio>
8#include <iterator>
9#include <map>
10#include <memory>
11#include <optional>
12#include <string>
13#include <utility>
14#include <vector>
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"
57#include "app/gui/core/icons.h"
60#include "core/features.h"
61#include "core/project.h"
62#include "dungeon_editor_v2.h"
63#include "imgui/imgui.h"
64#include "rom/snes.h"
65#include "rom/transaction.h"
66#include "util/log.h"
67#include "util/macro.h"
74#include "zelda3/dungeon/room.h"
77
78namespace yaze::editor {
79
80namespace {
81
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.";
86
88 int only_room = -1) {
89 const auto& flags = core::FeatureFlags::get().dungeon;
90 absl::Status status;
91 rooms.ForEachMaterialized([&](int id, const zelda3::Room& room) {
92 if (!status.ok() || !room.chests_dirty() || !room.object_stream_dirty())
93 return;
94 const bool writes_objects = flags.kSaveObjects && room.IsLoaded() &&
95 (only_room < 0 || only_room == id);
96 if (writes_objects == flags.kSaveChests)
97 return;
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.",
102 id));
103 });
104 return status;
105}
106
107absl::Status SaveWaterFillZones(Rom* rom, DungeonRoomStore& rooms) {
108 if (!rom || !rom->is_loaded()) {
109 return absl::FailedPreconditionError("ROM not loaded");
110 }
111
112 bool any_dirty = false;
113 rooms.ForEachMaterialized([&any_dirty](int, const zelda3::Room& room) {
114 if (room.water_fill_dirty()) {
115 any_dirty = true;
116 }
117 });
118 if (!any_dirty) {
119 return absl::OkStatus();
120 }
121
122 // Preserve saved zones even when no viewer has materialized their room.
123 ASSIGN_OR_RETURN(auto saved_zones, zelda3::LoadWaterFillTable(rom));
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) {
128 auto* room = rooms.GetIfMaterialized(room_id);
129 if (room == nullptr) {
130 continue;
131 }
132 if (!room->has_water_fill_zone()) {
133 if (room->water_fill_dirty())
134 merged.erase(room_id);
135 continue;
136 }
137
138 const int tile_count = room->WaterFillTileCount();
139 if (tile_count <= 0) {
140 continue;
141 }
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,
145 tile_count));
146 }
147
149 z.room_id = room_id;
151 z.fill_offsets.reserve(static_cast<size_t>(tile_count));
152
153 const auto& map = room->water_fill_zone().tiles;
154 for (size_t i = 0; i < map.size(); ++i) {
155 if (map[i] != 0) {
156 z.fill_offsets.push_back(static_cast<uint16_t>(i));
157 }
158 }
159 merged[room_id] = std::move(z);
160 }
161
162 std::vector<zelda3::WaterFillZoneEntry> zones;
163 for (auto& [id, zone] : merged)
164 zones.push_back(std::move(zone));
165
166 if (zones.size() > 8) {
167 return absl::InvalidArgumentError(absl::StrFormat(
168 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
169 zones.size()));
170 }
171
172 // Canonicalize SRAM mask assignment using the shared normalizer so editor
173 // save behavior matches JSON import/export workflows.
175 for (const auto& z : zones) {
176 if (auto* room = rooms.GetIfMaterialized(z.room_id)) {
177 room->set_water_fill_sram_bit_mask(z.sram_bit_mask);
178 }
179 }
180
183 [](int, zelda3::Room& room) { room.ClearWaterFillDirty(); });
184
185 return absl::OkStatus();
186}
187
188std::vector<std::pair<uint32_t, uint32_t>> CollectDirtyPotItemWriteRanges(
189 const project::YazeProject* project, const Rom* rom,
190 const DungeonRoomStore& rooms) {
191 std::vector<std::pair<uint32_t, uint32_t>> ranges;
192 if (rom == nullptr || !rom->is_loaded()) {
193 return ranges;
194 }
195
196 bool any_dirty = false;
197 rooms.ForEachMaterialized([&](int, const zelda3::Room& room) {
198 if (room.pot_items_dirty()) {
199 any_dirty = true;
200 }
201 });
202 if (!any_dirty) {
203 return ranges;
204 }
205
206 if (project != nullptr && project->hack_manifest.loaded()) {
207 const auto* layout = project->hack_manifest.GetDungeonStreamLayout(
209 if (layout != nullptr &&
210 layout->strategy == core::DungeonWriteStrategy::kRepackAll) {
211 for (const auto& range : layout->allocation_regions) {
212 ranges.emplace_back(yaze::SnesToPc(range.start),
213 yaze::SnesToPc(range.end));
214 }
215 const uint32_t pointer_width =
217 ? 3u
218 : 2u;
219 const uint32_t pointer_table_pc = yaze::SnesToPc(layout->pointer_table);
220 ranges.emplace_back(
221 pointer_table_pc,
222 pointer_table_pc + layout->pointer_count * pointer_width);
223 return ranges;
224 }
225 }
226
227 const auto& rom_data = rom->vector();
228 rooms.ForEachMaterialized([&](int room_id, const zelda3::Room& room) {
229 if (!room.pot_items_dirty()) {
230 return;
231 }
232 const int pointer_slot = zelda3::kRoomItemsPointers + room_id * 2;
233 if (pointer_slot < 0 ||
234 pointer_slot + 1 >= static_cast<int>(rom_data.size())) {
235 return;
236 }
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) {
241 return;
242 }
243 const uint32_t stream_pc = yaze::SnesToPc(0x010000u | pointer);
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);
247 });
248 return ranges;
249}
250
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); }));
258 return plan.write_ranges();
259}
260
261std::vector<std::pair<uint32_t, uint32_t>> CollectDirtyWaterFillWriteRanges(
262 const Rom* rom, const DungeonRoomStore& rooms) {
263 std::vector<std::pair<uint32_t, uint32_t>> ranges;
264 if (rom == nullptr || !rom->is_loaded() ||
265 zelda3::kWaterFillTableEnd > static_cast<int>(rom->size())) {
266 return ranges;
267 }
268 bool any_dirty = false;
269 rooms.ForEachMaterialized([&any_dirty](int, const zelda3::Room& room) {
270 if (room.water_fill_dirty()) {
271 any_dirty = true;
272 }
273 });
274 if (any_dirty) {
275 ranges.emplace_back(zelda3::kWaterFillTableStart,
277 }
278 return ranges;
279}
280
281std::vector<std::pair<uint32_t, uint32_t>>
283 const DungeonRoomStore& rooms) {
284 std::vector<std::pair<uint32_t, uint32_t>> ranges;
285 const int pointer_table_size = zelda3::kNumberOfRooms * 3;
286 if (rom == nullptr || !rom->is_loaded() ||
287 zelda3::kCustomCollisionRoomPointers + pointer_table_size >
288 static_cast<int>(rom->size()) ||
289 zelda3::kCustomCollisionDataSoftEnd > static_cast<int>(rom->size())) {
290 return ranges;
291 }
292 bool any_dirty = false;
293 rooms.ForEachMaterialized([&any_dirty](int, const zelda3::Room& room) {
294 if (room.custom_collision_dirty()) {
295 any_dirty = true;
296 }
297 });
298 if (any_dirty) {
299 ranges.emplace_back(
301 zelda3::kCustomCollisionRoomPointers + pointer_table_size);
302 ranges.emplace_back(zelda3::kCustomCollisionDataPosition,
304 }
305 return ranges;
306}
307
309 Rom* rom, DungeonRoomStore& rooms) {
310 std::optional<zelda3::DungeonStreamLayout> repack_layout;
311 bool any_dirty = false;
312 rooms.ForEachMaterialized([&](int, const zelda3::Room& room) {
313 if (room.pot_items_dirty()) {
314 any_dirty = true;
315 }
316 });
317 if (any_dirty && project != nullptr && project->hack_manifest.loaded()) {
318 const auto* manifest_layout = project->hack_manifest.GetDungeonStreamLayout(
320 if (manifest_layout != nullptr &&
321 manifest_layout->strategy == core::DungeonWriteStrategy::kRepackAll) {
323 zelda3::DungeonStreamLayout allocator_layout,
325 core::DungeonStreamType::kPotItems, *manifest_layout));
326 repack_layout = std::move(allocator_layout);
327 }
328 }
329
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);
334}
335
337 const project::YazeProject* project, Rom* rom,
338 const DungeonRoomStore& rooms, absl::string_view save_scope,
339 ToastManager* toast_manager) {
340 if (project == nullptr || !project->hack_manifest.loaded()) {
341 return absl::OkStatus();
342 }
343 const auto ranges = CollectDirtyPotItemWriteRanges(project, rom, rooms);
344 if (ranges.empty()) {
345 return absl::OkStatus();
346 }
348 project->hack_manifest, project->rom_metadata.write_policy, ranges,
349 save_scope, "DungeonEditorV2", toast_manager);
350}
351
353 const project::YazeProject* project,
354 const gfx::PaletteManager& palette_manager,
355 const zelda3::GameData* game_data, absl::string_view save_scope,
356 ToastManager* toast_manager) {
357 if (project == nullptr || !project->hack_manifest.loaded()) {
358 return absl::OkStatus();
359 }
360 const auto ranges = palette_manager.GetModifiedColorWriteRanges(game_data);
361 if (ranges.empty()) {
362 return absl::OkStatus();
363 }
365 project->hack_manifest, project->rom_metadata.write_policy, ranges,
366 save_scope, "DungeonEditorV2", toast_manager);
367}
368
370 const project::YazeProject* project, Rom* rom,
371 const std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>&
372 entrances,
373 const std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>&
374 spawn_points,
375 absl::string_view save_scope, ToastManager* toast_manager) {
376 if (rom == nullptr || !rom->is_loaded()) {
377 return absl::FailedPreconditionError("ROM not loaded");
378 }
379
380 // Field and ROM-bound validation must happen even without a manifest. This
381 // runs before any editor save domain mutates the shared ROM buffer.
387
388 if (project == nullptr || !project->hack_manifest.loaded()) {
389 return absl::OkStatus();
390 }
392 auto ranges,
394 auto spawn_ranges =
396 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
397 if (ranges.empty()) {
398 return absl::OkStatus();
399 }
401 project->hack_manifest, project->rom_metadata.write_policy, ranges,
402 save_scope, "DungeonEditorV2", toast_manager);
403}
404
406 const zelda3::ChestSavePlan& plan,
407 absl::string_view save_scope,
408 ToastManager* toast_manager) {
409 const auto ranges = plan.write_ranges();
410 if (ranges.empty() || project == nullptr ||
411 !project->hack_manifest.loaded()) {
412 return absl::OkStatus();
413 }
415 project->hack_manifest, project->rom_metadata.write_policy, ranges,
416 save_scope, "DungeonEditorV2", toast_manager);
417}
418
420 const project::YazeProject* project, const Rom* rom,
421 const DungeonRoomStore& rooms, absl::string_view save_scope,
422 ToastManager* toast_manager) {
423 if (project == nullptr || !project->hack_manifest.loaded()) {
424 return absl::OkStatus();
425 }
426 const auto ranges = CollectDirtyWaterFillWriteRanges(rom, rooms);
427 if (ranges.empty()) {
428 return absl::OkStatus();
429 }
431 project->hack_manifest, project->rom_metadata.write_policy, ranges,
432 save_scope, "DungeonEditorV2", toast_manager);
433}
434
436 const project::YazeProject* project, const Rom* rom,
437 const DungeonRoomStore& rooms, absl::string_view save_scope,
438 ToastManager* toast_manager) {
439 if (project == nullptr || !project->hack_manifest.loaded()) {
440 return absl::OkStatus();
441 }
442 const auto ranges = CollectDirtyCustomCollisionWriteRanges(rom, rooms);
443 if (ranges.empty()) {
444 return absl::OkStatus();
445 }
447 project->hack_manifest, project->rom_metadata.write_policy, ranges,
448 save_scope, "DungeonEditorV2", toast_manager);
449}
450
451} // namespace
452
454 int room_id, const std::vector<zelda3::PotItem>& items) {
455 const auto* room = rooms_.GetIfLoaded(room_id);
456 if (!rom_ || !rom_->is_loaded() || !room || room->rom() != rom_) {
457 return absl::FailedPreconditionError("Pot-item room is not loaded");
458 }
459 // Pot streams share one save operation. Include pending edits in other rooms
460 // so an individually small placement cannot overcommit the repack pool.
461 // Only detached rooms can have their dirty flags cleared by this dry run.
462 Rom scratch(*rom_);
463 DungeonRoomStore staged(&scratch);
464 rooms_.ForEachMaterialized([&](int id, const zelda3::Room& source) {
465 if (id != room_id && !source.pot_items_dirty()) {
466 return;
467 }
468 auto& candidate = staged[id];
469 candidate.GetPotItems() = id == room_id ? items : source.GetPotItems();
470 candidate.MarkPotItemsDirty();
471 });
472 RETURN_IF_ERROR(ValidatePotItemManifestConflicts(
473 dependencies_.project, &scratch, staged, "pot-item placement", nullptr));
474 return SaveAllPotItemsForProject(dependencies_.project, &scratch, staged);
475}
476
478 if (save_transaction_snapshot_.has_value()) {
479 return absl::FailedPreconditionError(
480 "Dungeon save transaction is already active");
481 }
482
483 // A save is a history boundary: a later drag increment must not restore
484 // dirty flags captured before the bytes we are about to publish.
487 auto& palette_manager = gfx::PaletteManager::Get();
488 if (core::FeatureFlags::get().dungeon.kSavePalettes &&
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");
493 }
494 RETURN_IF_ERROR(palette_manager.BeginSaveTransaction());
495 snapshot.has_palette_transaction = true;
496 }
497 rooms_.ForEachMaterialized([&snapshot](int room_id,
498 const zelda3::Room& room) {
499 snapshot.room_states.emplace_back(room_id, room.CaptureSaveDirtySnapshot());
500 });
501 for (size_t i = 0; i < entrances_.size(); ++i) {
502 snapshot.entrance_dirty_states[i] = entrances_[i].dirty();
503 }
504 for (size_t i = 0; i < spawn_points_.size(); ++i) {
505 snapshot.spawn_dirty_states[i] = spawn_points_[i].dirty();
506 }
507 if (game_data_ != nullptr) {
508 snapshot.has_pit_damage_table = true;
510 }
511 save_transaction_snapshot_ = std::move(snapshot);
512 return absl::OkStatus();
513}
514
516 if (!save_transaction_snapshot_.has_value()) {
517 return;
518 }
519
520 for (const auto& [room_id, state] : save_transaction_snapshot_->room_states) {
521 if (auto* room = rooms_.GetIfMaterialized(room_id); room != nullptr) {
522 room->RestoreSaveDirtySnapshot(state);
523 }
524 }
525 for (size_t i = 0; i < entrances_.size(); ++i) {
526 if (save_transaction_snapshot_->entrance_dirty_states[i]) {
527 entrances_[i].MarkDirty();
528 } else {
529 entrances_[i].ClearDirty();
530 }
531 }
532 for (size_t i = 0; i < spawn_points_.size(); ++i) {
533 if (save_transaction_snapshot_->spawn_dirty_states[i]) {
534 spawn_points_[i].MarkDirty();
535 } else {
536 spawn_points_[i].ClearDirty();
537 }
538 }
539 if (save_transaction_snapshot_->has_pit_damage_table &&
540 game_data_ != nullptr) {
541 if (save_transaction_snapshot_->pit_damage_dirty) {
543 } else {
545 }
546 }
547 if (save_transaction_snapshot_->has_palette_transaction) {
549 }
551}
552
563
565 const std::function<absl::Status()>& operation) {
566 if (!rom_ || !rom_->is_loaded()) {
567 return absl::FailedPreconditionError("ROM not loaded");
568 }
569
570 // File > Save already owns a whole-ROM/editor transaction. Direct Apply Room
571 // and Apply All actions use the same guarantees without nesting it.
572 if (save_transaction_snapshot_.has_value()) {
573 return operation();
574 }
575
576 ScopedRomTransaction rom_transaction(*rom_);
578
579 const absl::Status status = operation();
580 if (!status.ok()) {
582 return status;
583 }
584
586 rom_transaction.Commit();
587 return absl::OkStatus();
588}
589
590absl::Status DungeonEditorV2::ValidateSaveScope(int only_room,
591 bool room_data_only) const {
592 // A chest object and its reward are one edit. Preserve the more specific
593 // recovery instruction when only one half of that edit would be written.
594 RETURN_IF_ERROR(ValidateChestObjectSaveCoupling(rooms_, only_room));
595 const auto& flags = core::FeatureFlags::get().dungeon;
597 bool collision = false;
598 bool water_fill = false;
599 rooms_.ForEachMaterialized([&](int id, const zelda3::Room& room) {
600 // Apply Room writes local streams for its selected room, but the compact
601 // shared tables below include pending records from every materialized room.
602 if (only_room < 0 || only_room == id) {
603 pending.object_stream |=
605 pending.sprites |= room.sprites_dirty();
606 pending.header |= room.header_dirty();
607 }
608 pending.chests |= room.chests_dirty();
609 pending.pot_items |= room.pot_items_dirty();
610 pending.torches |= room.torches_dirty();
611 pending.blocks |= room.blocks_dirty();
612 collision |= room.custom_collision_dirty();
613 water_fill |= room.water_fill_dirty();
614 });
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);
619 }
620 };
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) {
630 check(game_data_ && game_data_->pit_damage_table.dirty(), flags.kSavePits,
631 "Save Pits");
632 const auto dirty = [](const auto& entrance) {
633 return entrance.dirty();
634 };
635 check(std::any_of(entrances_.begin(), entrances_.end(), dirty) ||
636 std::any_of(spawn_points_.begin(), spawn_points_.end(), dirty),
637 flags.kSaveEntrances, "Save Entrances");
638 }
639 std::string message;
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 "
646 "is needed.");
647 }
648 // Water Fill import/migration is established at project load, so keep its
649 // existing project-only scope contract rather than silently enabling it.
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.");
655 }
656 return message.empty() ? absl::OkStatus()
657 : absl::FailedPreconditionError(message);
658}
659
660absl::Status DungeonEditorV2::Save() {
662 if (minecart_track_editor_panel_ != nullptr &&
664 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
665 }
666 if (!rom_ || !rom_->is_loaded()) {
667 return absl::FailedPreconditionError("ROM not loaded");
668 }
669 if (object_tile_editor_panel_ != nullptr &&
671 return absl::FailedPreconditionError(
672 "Apply or explicitly discard Object Tile Editor changes before "
673 "using Save or Apply Room");
674 }
675
677 // A sprite/item drag can first become dirty when the gesture is released.
678 // Direct Save callers need the same post-finalization check as File > Save.
680 const auto& flags = core::FeatureFlags::get().dungeon;
681 std::optional<zelda3::ChestSavePlan> chest_save_plan;
682
683 // Simulate the pending room streams in save order before any ROM writer.
684 // Earlier relocations can change where later streams and headers will write.
685 ASSIGN_OR_RETURN(const auto stream_ranges, PlanPendingRoomStreamRanges({}));
690 "dungeon room streams", "DungeonEditorV2",
692 }
693
694 if (flags.kSaveCollision) {
695 RETURN_IF_ERROR(ValidateCustomCollisionManifestConflicts(
696 dependencies_.project, rom_, rooms_, "custom collision",
698 }
699 if (flags.kSaveWaterFillZones) {
700 RETURN_IF_ERROR(ValidateWaterFillManifestConflicts(
701 dependencies_.project, rom_, rooms_, "water fill zones",
703 }
704 if (flags.kSaveEntrances) {
705 RETURN_IF_ERROR(ValidateDungeonEntranceSavePreflight(
707 "dungeon entrances and spawn points", dependencies_.toast_manager));
708 }
709
710 if (flags.kSaveChests) {
712 rom_, static_cast<int>(rooms_.size()),
713 [this](int room_id) {
714 return rooms_.GetIfMaterialized(room_id);
715 }));
716 RETURN_IF_ERROR(ValidateChestManifestConflicts(
717 dependencies_.project, plan, "chest table",
719 chest_save_plan = std::move(plan);
720 }
721 if (flags.kSavePotItems) {
722 RETURN_IF_ERROR(ValidatePotItemManifestConflicts(
723 dependencies_.project, rom_, rooms_, "pot items",
725 }
726
727 auto& palette_manager = gfx::PaletteManager::Get();
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");
732 }
733 RETURN_IF_ERROR(ValidatePaletteManifestConflicts(
734 dependencies_.project, palette_manager, game_data_, "dungeon palettes",
736 const size_t modified_color_count =
737 palette_manager.GetModifiedColorCount(game_data_);
738 auto status = palette_manager.SaveAllToRom();
739 if (!status.ok()) {
740 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
741 status.message().data());
742 return status;
743 }
744 LOG_INFO("DungeonEditorV2", "Saved %zu modified colors to ROM",
745 modified_color_count);
746 }
747
748 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
749 absl::Status save_status = absl::OkStatus();
750 rooms_.ForEachLoaded([&](int room_id, zelda3::Room&) {
751 if (!save_status.ok()) {
752 return;
753 }
754 auto status = SaveRoomData(room_id);
755 if (!status.ok()) {
756 save_status = status;
757 }
758 });
759 if (!save_status.ok()) {
760 return save_status;
761 }
762 }
763
764 if (flags.kSaveTorches) {
765 auto status = zelda3::SaveAllTorches(
766 rom_, static_cast<int>(rooms_.size()),
767 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
768 if (!status.ok()) {
769 LOG_ERROR("DungeonEditorV2", "Failed to save torches: %s",
770 status.message().data());
771 return status;
772 }
773 }
774
775 if (flags.kSavePits) {
776 auto status = zelda3::SaveAllPits(
778 if (!status.ok()) {
779 LOG_ERROR("DungeonEditorV2", "Failed to save pits: %s",
780 status.message().data());
781 return status;
782 }
783 }
784
785 if (flags.kSaveBlocks) {
786 auto status = zelda3::SaveAllBlocks(
787 rom_, static_cast<int>(rooms_.size()),
788 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
789 if (!status.ok()) {
790 LOG_ERROR("DungeonEditorV2", "Failed to save blocks: %s",
791 status.message().data());
792 return status;
793 }
794 }
795
796 if (flags.kSaveCollision) {
797 auto status = zelda3::SaveAllCollision(
798 rom_, static_cast<int>(rooms_.size()),
799 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
800 if (!status.ok()) {
801 LOG_ERROR("DungeonEditorV2", "Failed to save collision: %s",
802 status.message().data());
803 return status;
804 }
805 }
806
807 if (flags.kSaveWaterFillZones) {
808 auto status = SaveWaterFillZones(rom_, rooms_);
809 if (!status.ok()) {
810 LOG_ERROR("DungeonEditorV2", "Failed to save water fill zones: %s",
811 status.message().data());
812 return status;
813 }
814 }
815
816 if (flags.kSaveChests) {
817 auto status = zelda3::ApplyChestSavePlan(
818 rom_, *chest_save_plan,
819 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
820 if (!status.ok()) {
821 LOG_ERROR("DungeonEditorV2", "Failed to save chests: %s",
822 status.message().data());
823 return status;
824 }
825 }
826
827 if (flags.kSavePotItems) {
828 auto status =
829 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_);
830 if (!status.ok()) {
831 LOG_ERROR("DungeonEditorV2", "Failed to save pot items: %s",
832 status.message().data());
833 return status;
834 }
835 }
836
837 if (flags.kSaveEntrances) {
839 if (!status.ok()) {
840 LOG_ERROR("DungeonEditorV2", "Failed to save dungeon entrances: %s",
841 status.message().data());
842 return status;
843 }
845 if (!status.ok()) {
846 LOG_ERROR("DungeonEditorV2", "Failed to save dungeon spawn points: %s",
847 status.message().data());
848 return status;
849 }
850 }
851
852 return absl::OkStatus();
853}
854
855std::vector<std::pair<uint32_t, uint32_t>> DungeonEditorV2::CollectWriteRanges()
856 const {
857 std::vector<std::pair<uint32_t, uint32_t>> ranges;
858
859 if (!rom_ || !rom_->is_loaded()) {
860 return ranges;
861 }
862
863 const auto& flags = core::FeatureFlags::get().dungeon;
864 const auto& rom_data = rom_->vector();
865
866 auto& palette_manager = gfx::PaletteManager::Get();
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());
871 }
872
873 if (flags.kSaveEntrances) {
874 auto entrance_ranges =
876 entrances_);
877 if (entrance_ranges.ok()) {
878 ranges.insert(ranges.end(), entrance_ranges->begin(),
879 entrance_ranges->end());
880 }
881 // Invalid layouts are rejected by ValidateDungeonEntranceSavePreflight
882 // before any save domain mutates bytes. Do not predict legacy ranges here.
883 auto spawn_ranges =
885 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
886 }
887
888 const auto stream_ranges = PlanPendingRoomStreamRanges({});
889 if (stream_ranges.ok()) {
890 ranges.insert(ranges.end(), stream_ranges->begin(), stream_ranges->end());
891 } else {
892 // Save repeats this preflight and fails before mutating any ROM bytes.
893 LOG_ERROR("DungeonEditorV2",
894 "Could not predict room stream write ranges: %s",
895 stream_ranges.status().message().data());
896 }
897
898 // Oracle of Secrets: the water fill table lives in a reserved tail region.
899 // Include it in write-range reporting whenever we have dirty water fill data,
900 // even if no rooms are currently loaded (SaveWaterFillZones() is room-indexed
901 // and independent of room loading state).
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());
906 }
907
908 // Custom collision writes update the pointer table and append blobs into the
909 // expanded collision region. SaveAllCollision() is room-indexed, so include
910 // these ranges whenever any room has dirty custom collision data.
911 if (flags.kSaveCollision) {
912 auto collision_ranges =
913 CollectDirtyCustomCollisionWriteRanges(rom_, rooms_);
914 ranges.insert(ranges.end(), collision_ranges.begin(),
915 collision_ranges.end());
916 }
917
918 // Chests use one global fixed-capacity table. Report only the exact byte
919 // deltas from the same planner used by the serializer so unrelated protected
920 // bytes inside the live table do not block a safe edit.
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());
925 } else {
926 LOG_WARN("DungeonEditorV2", "Could not predict chest write ranges: %s",
927 chest_ranges.status().message().data());
928 }
929 }
930
931 rooms_.ForEachLoaded([&](int room_id, const zelda3::Room& room) {
932 room_id = room.id();
933
934 // Header range
935 if (flags.kSaveRoomHeaders && room.header_dirty()) {
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);
943
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);
950 }
951 }
952 const int message_address = zelda3::kMessagesIdDungeon + (room_id * 2);
953 if (message_address >= 0 &&
954 message_address + 1 < static_cast<int>(rom_data.size())) {
955 ranges.emplace_back(message_address, message_address + 2);
956 }
957 }
958 });
959
960 // Pot items are saved after room-local streams but still participate in the
961 // same whole-ROM transaction and manifest preflight.
962 if (flags.kSavePotItems) {
963 auto pot_ranges =
964 CollectDirtyPotItemWriteRanges(dependencies_.project, rom_, rooms_);
965 ranges.insert(ranges.end(), pot_ranges.begin(), pot_ranges.end());
966 }
967
968 return ranges;
969}
970
971absl::Status DungeonEditorV2::SaveRoom(int room_id) {
972 return SaveRoomImpl(room_id, false);
973}
974
975// The room-only path is used exclusively by a detached scratch editor during
976// transfer preflight. It cannot save project-global palettes, pits or entrances.
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);
981 }
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");
987 }
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");
991 }
992
993 RETURN_IF_ERROR(ValidateSaveScope(room_id, room_data_only));
994 const auto& flags = core::FeatureFlags::get().dungeon;
995 std::optional<zelda3::ChestSavePlan> chest_save_plan;
996 if (flags.kSaveCollision) {
997 RETURN_IF_ERROR(ValidateCustomCollisionManifestConflicts(
998 dependencies_.project, rom_, rooms_,
999 absl::StrFormat("custom collision for room 0x%03X", room_id),
1000 dependencies_.toast_manager));
1001 }
1002 if (flags.kSaveWaterFillZones) {
1003 RETURN_IF_ERROR(ValidateWaterFillManifestConflicts(
1004 dependencies_.project, rom_, rooms_,
1005 absl::StrFormat("water fill zones for room 0x%03X", room_id),
1006 dependencies_.toast_manager));
1007 }
1008 if (!room_data_only && flags.kSaveEntrances) {
1009 RETURN_IF_ERROR(ValidateDungeonEntranceSavePreflight(
1010 dependencies_.project, rom_, entrances_, spawn_points_,
1011 absl::StrFormat(
1012 "dungeon entrances and spawn points (Apply Room 0x%03X)",
1013 room_id),
1014 dependencies_.toast_manager));
1015 }
1016 if (flags.kSaveChests) {
1018 auto plan, zelda3::BuildChestSavePlan(
1019 rom_, static_cast<int>(rooms_.size()), [this](int id) {
1020 return rooms_.GetIfMaterialized(id);
1021 }));
1022 RETURN_IF_ERROR(ValidateChestManifestConflicts(
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);
1027 }
1028 if (flags.kSavePotItems) {
1029 RETURN_IF_ERROR(ValidatePotItemManifestConflicts(
1030 dependencies_.project, rom_, rooms_,
1031 absl::StrFormat("pot items for room 0x%03X", room_id),
1032 dependencies_.toast_manager));
1033 }
1034 auto& palette_manager = gfx::PaletteManager::Get();
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");
1040 }
1041 RETURN_IF_ERROR(ValidatePaletteManifestConflicts(
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();
1046 if (!status.ok()) {
1047 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
1048 status.message().data());
1049 return status;
1050 }
1051 }
1052 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1053 RETURN_IF_ERROR(SaveRoomData(room_id));
1054 }
1055
1056 if (flags.kSaveTorches) {
1058 rom_, static_cast<int>(rooms_.size()),
1059 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1060 }
1061 if (!room_data_only && flags.kSavePits) {
1063 rom_, game_data_ ? &game_data_->pit_damage_table : nullptr));
1064 }
1065 if (flags.kSaveBlocks) {
1067 rom_, static_cast<int>(rooms_.size()),
1068 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1069 }
1070 if (flags.kSaveCollision) {
1072 rom_, static_cast<int>(rooms_.size()),
1073 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1074 }
1075 if (flags.kSaveWaterFillZones) {
1076 RETURN_IF_ERROR(SaveWaterFillZones(rom_, rooms_));
1077 }
1078 if (flags.kSaveChests) {
1080 rom_, *chest_save_plan,
1081 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1082 }
1083 if (flags.kSavePotItems) {
1085 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_));
1086 }
1087 if (!room_data_only && flags.kSaveEntrances) {
1090 }
1091
1092 return absl::OkStatus();
1093 };
1094 // Scratch state is discarded after either outcome; no live palette/history
1095 // transaction should be started by a preview.
1096 return room_data_only ? operation() : RunWithSaveTransaction(operation);
1097}
1098
1099absl::Status DungeonEditorV2::SaveRoomData(int room_id) {
1100 if (!rom_ || !rom_->is_loaded()) {
1101 return absl::FailedPreconditionError("ROM not loaded");
1102 }
1103 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1104 return absl::InvalidArgumentError("Invalid room ID");
1105 }
1106
1107 auto& room = rooms_[room_id];
1108 if (!room.IsLoaded()) {
1109 return absl::OkStatus();
1110 }
1111
1112 // ROM safety: validate loaded room content before writing any bytes.
1113 {
1114 zelda3::DungeonValidator validator;
1115 const auto result = validator.ValidateRoom(room);
1116 for (const auto& w : result.warnings) {
1117 LOG_WARN("DungeonEditorV2", "Room 0x%03X validation warning: %s", room_id,
1118 w.c_str());
1119 }
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());
1124 }
1125 if (dependencies_.toast_manager) {
1126 dependencies_.toast_manager->Show(
1127 absl::StrFormat(
1128 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1129 room_id, result.errors.size()),
1130 ToastType::kError);
1131 }
1132 return absl::FailedPreconditionError(
1133 absl::StrFormat("Room 0x%03X failed validation", room_id));
1134 }
1135 }
1136
1137 const auto& flags = core::FeatureFlags::get().dungeon;
1138
1139 // A present copy-on-write layout is an explicit capability grant from the
1140 // project manifest. Without it, Room retains the Wave 1 fail-closed behavior
1141 // for shared or over-capacity streams.
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 =
1147 [&](core::DungeonStreamType stream_type, bool stream_dirty,
1148 std::optional<zelda3::DungeonStreamLayout>* out) -> absl::Status {
1149 if (!stream_dirty) {
1150 return absl::OkStatus();
1151 }
1152 const auto* manifest_layout =
1153 manifest.GetDungeonStreamLayout(stream_type);
1154 if (manifest_layout == nullptr ||
1155 manifest_layout->strategy !=
1157 return absl::OkStatus();
1158 }
1160 zelda3::DungeonStreamLayout allocator_layout,
1161 core::ToDungeonStreamAllocatorLayout(stream_type, *manifest_layout));
1162 *out = std::move(allocator_layout);
1163 return absl::OkStatus();
1164 };
1165
1166 RETURN_IF_ERROR(load_cow_layout(
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));
1174 }
1175
1176 // HACK MANIFEST VALIDATION
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();
1181
1182 // 1. Validate Header Range
1183 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1184 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
1185 int header_ptr_table =
1186 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
1187 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
1189 header_ptr_table = yaze::SnesToPc(header_ptr_table);
1190 int table_offset = header_ptr_table + (room_id * 2);
1191
1192 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1193 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1194 (rom_data[table_offset + 1] << 8) |
1195 rom_data[table_offset];
1196 int header_location = yaze::SnesToPc(address);
1197 ranges.emplace_back(header_location, header_location + 14);
1198 }
1199 }
1200 const int message_address = zelda3::kMessagesIdDungeon + (room_id * 2);
1201 if (message_address >= 0 &&
1202 message_address + 1 < static_cast<int>(rom_data.size())) {
1203 ranges.emplace_back(message_address, message_address + 2);
1204 }
1205 }
1206
1207 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1208 // address space (LoROM), so convert before analysis.
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));
1213 }
1214
1215 const auto validate_stream_ranges =
1216 [&](const std::vector<std::pair<uint32_t, uint32_t>>& ranges)
1217 -> absl::Status {
1218 if (!dependencies_.project ||
1219 !dependencies_.project->hack_manifest.loaded()) {
1220 return absl::OkStatus();
1221 }
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);
1227 };
1228
1229 if (flags.kSaveObjects && room.object_stream_dirty()) {
1230 ASSIGN_OR_RETURN(auto ranges,
1231 room.GetObjectSaveWriteRanges(object_cow_layout.has_value()
1232 ? &*object_cow_layout
1233 : nullptr));
1234 RETURN_IF_ERROR(validate_stream_ranges(ranges));
1235 auto status = room.SaveObjects(
1236 object_cow_layout.has_value() ? &*object_cow_layout : nullptr);
1237 if (!status.ok()) {
1238 LOG_ERROR("DungeonEditorV2", "Failed to save room objects: %s",
1239 status.message().data());
1240 return status;
1241 }
1242 }
1243
1244 if (flags.kSaveObjects && room.object_stream_header_dirty()) {
1245 // Object serialization may just have relocated the stream. Query its
1246 // header destination from the updated ROM immediately before saving it.
1248 auto ranges,
1249 room.GetObjectStreamHeaderSaveWriteRanges(
1250 object_cow_layout.has_value() ? &*object_cow_layout : nullptr));
1251 RETURN_IF_ERROR(validate_stream_ranges(ranges));
1252 auto status = room.SaveObjectStreamHeader(
1253 object_cow_layout.has_value() ? &*object_cow_layout : nullptr);
1254 if (!status.ok()) {
1255 LOG_ERROR("DungeonEditorV2",
1256 "Failed to save room object-stream header: %s",
1257 status.message().data());
1258 return status;
1259 }
1260 }
1261
1262 if (flags.kSaveSprites && room.sprites_dirty()) {
1263 ASSIGN_OR_RETURN(auto ranges,
1264 room.GetSpriteSaveWriteRanges(sprite_cow_layout.has_value()
1265 ? &*sprite_cow_layout
1266 : nullptr));
1267 RETURN_IF_ERROR(validate_stream_ranges(ranges));
1268 auto status = room.SaveSprites(
1269 sprite_cow_layout.has_value() ? &*sprite_cow_layout : nullptr);
1270 if (!status.ok()) {
1271 LOG_ERROR("DungeonEditorV2", "Failed to save room sprites: %s",
1272 status.message().data());
1273 return status;
1274 }
1275 }
1276
1277 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1278 auto status = room.SaveRoomHeader();
1279 if (!status.ok()) {
1280 LOG_ERROR("DungeonEditorV2", "Failed to save room header: %s",
1281 status.message().data());
1282 return status;
1283 }
1284 }
1285
1286 return absl::OkStatus();
1287}
1288
1289void DungeonEditorV2::SaveAllRooms() {
1290 auto status = RunWithSaveTransaction([this]() { return Save(); });
1291 if (status.ok()) {
1292 if (dependencies_.toast_manager) {
1293 dependencies_.toast_manager->Show("Applied loaded rooms to ROM buffer",
1294 ToastType::kSuccess);
1295 }
1296 } else {
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()),
1302 ToastType::kError);
1303 }
1304 }
1305}
1306
1307} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
const auto & vector() const
Definition rom.h:205
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
static Flags & get()
Definition features.h:119
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_
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
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > PlanPendingRoomStreamRanges(const std::vector< DungeonSelectionEditPlan > &candidates) const
ObjectTileEditorPanel * object_tile_editor_panel_
absl::Status RunWithSaveTransaction(const std::function< absl::Status()> &operation)
std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > spawn_points_
std::optional< SaveTransactionSnapshot > save_transaction_snapshot_
std::vector< std::pair< uint32_t, uint32_t > > CollectWriteRanges() const
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
EditorDependencies dependencies_
Definition editor.h:370
Centralized palette management system.
std::vector< std::pair< uint32_t, uint32_t > > GetModifiedColorWriteRanges() const
Get exact, coalesced half-open ROM ranges for modified colors.
static PaletteManager & Get()
Get the singleton instance.
ValidationResult ValidateRoom(const Room &room)
void set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:647
bool custom_collision_dirty() const
Definition room.h:596
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:601
int WaterFillTileCount() const
Definition room.h:642
bool IsLoaded() const
Definition room.h:970
bool pot_items_dirty() const
Definition room.h:431
bool object_stream_dirty() const
Definition room.h:504
bool sprites_dirty() const
Definition room.h:318
bool header_dirty() const
Definition room.h:658
bool object_stream_header_dirty() const
Definition room.h:506
bool has_water_fill_zone() const
Definition room.h:603
bool chests_dirty() const
Definition room.h:325
void ClearWaterFillDirty()
Definition room.h:655
bool blocks_dirty() const
Definition room.h:438
bool torches_dirty() const
Definition room.h:435
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
Definition room.h:673
bool water_fill_dirty() const
Definition room.h:654
const std::vector< PotItem > & GetPotItems() const
Definition room.h:429
uint8_t water_fill_sram_bit_mask() const
Definition room.h:644
int id() const
Definition room.h:1012
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
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)
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 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)
Definition room.cc:4269
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)
Definition room.cc:4491
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)
Definition room.cc:3331
absl::Status SaveAllPits(Rom *rom)
Definition room.cc:3358
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:3543
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)
Definition room.cc:4283
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:3933
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
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
DungeonPointerEncoding pointer_encoding
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::vector< std::pair< int, zelda3::Room::SaveDirtySnapshot > > room_states
std::array< bool, zelda3::kNumDungeonSpawnPoints > spawn_dirty_states
std::array< bool, zelda3::kNumDungeonEntranceSlots > entrance_dirty_states
project::YazeProject * project
Definition editor.h:174
RomWritePolicy write_policy
Definition project.h:225
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
core::HackManifest hack_manifest
Definition project.h:360
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
Definition room.h:1430
PitDamageTable pit_damage_table
Definition game_data.h:114
std::vector< uint16_t > fill_offsets
std::array< uint8_t, 64 *64 > tiles
Definition room.h:223