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 <memory>
10#include <optional>
11#include <string>
12#include <utility>
13#include <vector>
14#include "absl/status/status.h"
15#include "absl/strings/str_format.h"
16#include "absl/strings/string_view.h"
17#include "absl/types/span.h"
56#include "app/gui/core/icons.h"
59#include "core/features.h"
60#include "core/project.h"
61#include "dungeon_editor_v2.h"
62#include "imgui/imgui.h"
63#include "rom/snes.h"
64#include "rom/transaction.h"
65#include "util/log.h"
66#include "util/macro.h"
73#include "zelda3/dungeon/room.h"
76
77namespace yaze::editor {
78
79namespace {
80
82 "Minecart Track Editor has unpublished drafts. ROM Save/Apply never "
83 "publishes ASM source; use Publish Tracks in the Minecart Track Editor "
84 "or discard the drafts first.";
85
86absl::Status SaveWaterFillZones(Rom* rom, DungeonRoomStore& rooms) {
87 if (!rom || !rom->is_loaded()) {
88 return absl::FailedPreconditionError("ROM not loaded");
89 }
90
91 bool any_dirty = false;
92 rooms.ForEachMaterialized([&any_dirty](int, const zelda3::Room& room) {
93 if (room.water_fill_dirty()) {
94 any_dirty = true;
95 }
96 });
97 if (!any_dirty) {
98 return absl::OkStatus();
99 }
100
101 std::vector<zelda3::WaterFillZoneEntry> zones;
102 zones.reserve(8);
103 for (int room_id = 0; room_id < static_cast<int>(rooms.size()); ++room_id) {
104 auto* room = rooms.GetIfMaterialized(room_id);
105 if (room == nullptr) {
106 continue;
107 }
108 if (!room->has_water_fill_zone()) {
109 continue;
110 }
111
112 const int tile_count = room->WaterFillTileCount();
113 if (tile_count <= 0) {
114 continue;
115 }
116 if (tile_count > 255) {
117 return absl::InvalidArgumentError(absl::StrFormat(
118 "Water fill zone in room 0x%02X has %d tiles (max 255)", room_id,
119 tile_count));
120 }
121
123 z.room_id = room_id;
125 z.fill_offsets.reserve(static_cast<size_t>(tile_count));
126
127 const auto& map = room->water_fill_zone().tiles;
128 for (size_t i = 0; i < map.size(); ++i) {
129 if (map[i] != 0) {
130 z.fill_offsets.push_back(static_cast<uint16_t>(i));
131 }
132 }
133 zones.push_back(std::move(z));
134 }
135
136 if (zones.size() > 8) {
137 return absl::InvalidArgumentError(absl::StrFormat(
138 "Too many water fill zones: %zu (max 8 fits in $7EF411 bitfield)",
139 zones.size()));
140 }
141
142 // Canonicalize SRAM mask assignment using the shared normalizer so editor
143 // save behavior matches JSON import/export workflows.
145 for (const auto& z : zones) {
146 if (auto* room = rooms.GetIfMaterialized(z.room_id)) {
147 room->set_water_fill_sram_bit_mask(z.sram_bit_mask);
148 }
149 }
150
153 [](int, zelda3::Room& room) { room.ClearWaterFillDirty(); });
154
155 return absl::OkStatus();
156}
157
158std::vector<std::pair<uint32_t, uint32_t>> CollectDirtyPotItemWriteRanges(
159 const project::YazeProject* project, const Rom* rom,
160 const DungeonRoomStore& rooms) {
161 std::vector<std::pair<uint32_t, uint32_t>> ranges;
162 if (rom == nullptr || !rom->is_loaded()) {
163 return ranges;
164 }
165
166 bool any_dirty = false;
167 rooms.ForEachMaterialized([&](int, const zelda3::Room& room) {
168 if (room.pot_items_dirty()) {
169 any_dirty = true;
170 }
171 });
172 if (!any_dirty) {
173 return ranges;
174 }
175
176 if (project != nullptr && project->hack_manifest.loaded()) {
177 const auto* layout = project->hack_manifest.GetDungeonStreamLayout(
179 if (layout != nullptr &&
180 layout->strategy == core::DungeonWriteStrategy::kRepackAll) {
181 for (const auto& range : layout->allocation_regions) {
182 ranges.emplace_back(yaze::SnesToPc(range.start),
183 yaze::SnesToPc(range.end));
184 }
185 const uint32_t pointer_width =
187 ? 3u
188 : 2u;
189 const uint32_t pointer_table_pc = yaze::SnesToPc(layout->pointer_table);
190 ranges.emplace_back(
191 pointer_table_pc,
192 pointer_table_pc + layout->pointer_count * pointer_width);
193 return ranges;
194 }
195 }
196
197 const auto& rom_data = rom->vector();
198 rooms.ForEachMaterialized([&](int room_id, const zelda3::Room& room) {
199 if (!room.pot_items_dirty()) {
200 return;
201 }
202 const int pointer_slot = zelda3::kRoomItemsPointers + room_id * 2;
203 if (pointer_slot < 0 ||
204 pointer_slot + 1 >= static_cast<int>(rom_data.size())) {
205 return;
206 }
207 const uint16_t pointer =
208 (static_cast<uint16_t>(rom_data[pointer_slot + 1]) << 8) |
209 rom_data[pointer_slot];
210 if (pointer < 0x8000) {
211 return;
212 }
213 const uint32_t stream_pc = yaze::SnesToPc(0x010000u | pointer);
214 const uint32_t encoded_size =
215 static_cast<uint32_t>(room.GetPotItems().size() * 3 + 2);
216 ranges.emplace_back(stream_pc, stream_pc + encoded_size);
217 });
218 return ranges;
219}
220
221absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
226 rom, static_cast<int>(rooms.size()),
227 [&rooms](int room_id) { return rooms.GetIfMaterialized(room_id); }));
228 return plan.write_ranges();
229}
230
231std::vector<std::pair<uint32_t, uint32_t>> CollectDirtyWaterFillWriteRanges(
232 const Rom* rom, const DungeonRoomStore& rooms) {
233 std::vector<std::pair<uint32_t, uint32_t>> ranges;
234 if (rom == nullptr || !rom->is_loaded() ||
235 zelda3::kWaterFillTableEnd > static_cast<int>(rom->size())) {
236 return ranges;
237 }
238 bool any_dirty = false;
239 rooms.ForEachMaterialized([&any_dirty](int, const zelda3::Room& room) {
240 if (room.water_fill_dirty()) {
241 any_dirty = true;
242 }
243 });
244 if (any_dirty) {
245 ranges.emplace_back(zelda3::kWaterFillTableStart,
247 }
248 return ranges;
249}
250
251std::vector<std::pair<uint32_t, uint32_t>>
253 const DungeonRoomStore& rooms) {
254 std::vector<std::pair<uint32_t, uint32_t>> ranges;
255 const int pointer_table_size = zelda3::kNumberOfRooms * 3;
256 if (rom == nullptr || !rom->is_loaded() ||
257 zelda3::kCustomCollisionRoomPointers + pointer_table_size >
258 static_cast<int>(rom->size()) ||
259 zelda3::kCustomCollisionDataSoftEnd > static_cast<int>(rom->size())) {
260 return ranges;
261 }
262 bool any_dirty = false;
263 rooms.ForEachMaterialized([&any_dirty](int, const zelda3::Room& room) {
264 if (room.custom_collision_dirty()) {
265 any_dirty = true;
266 }
267 });
268 if (any_dirty) {
269 ranges.emplace_back(
271 zelda3::kCustomCollisionRoomPointers + pointer_table_size);
272 ranges.emplace_back(zelda3::kCustomCollisionDataPosition,
274 }
275 return ranges;
276}
277
279 Rom* rom, DungeonRoomStore& rooms) {
280 std::optional<zelda3::DungeonStreamLayout> repack_layout;
281 bool any_dirty = false;
282 rooms.ForEachMaterialized([&](int, const zelda3::Room& room) {
283 if (room.pot_items_dirty()) {
284 any_dirty = true;
285 }
286 });
287 if (any_dirty && project != nullptr && project->hack_manifest.loaded()) {
288 const auto* manifest_layout = project->hack_manifest.GetDungeonStreamLayout(
290 if (manifest_layout != nullptr &&
291 manifest_layout->strategy == core::DungeonWriteStrategy::kRepackAll) {
293 zelda3::DungeonStreamLayout allocator_layout,
295 core::DungeonStreamType::kPotItems, *manifest_layout));
296 repack_layout = std::move(allocator_layout);
297 }
298 }
299
301 rom, static_cast<int>(rooms.size()),
302 [&rooms](int room_id) { return rooms.GetIfMaterialized(room_id); },
303 repack_layout.has_value() ? &*repack_layout : nullptr);
304}
305
307 const project::YazeProject* project, Rom* rom,
308 const DungeonRoomStore& rooms, absl::string_view save_scope,
309 ToastManager* toast_manager) {
310 if (project == nullptr || !project->hack_manifest.loaded()) {
311 return absl::OkStatus();
312 }
313 const auto ranges = CollectDirtyPotItemWriteRanges(project, rom, rooms);
314 if (ranges.empty()) {
315 return absl::OkStatus();
316 }
318 project->hack_manifest, project->rom_metadata.write_policy, ranges,
319 save_scope, "DungeonEditorV2", toast_manager);
320}
321
323 const project::YazeProject* project,
324 const gfx::PaletteManager& palette_manager,
325 const zelda3::GameData* game_data, absl::string_view save_scope,
326 ToastManager* toast_manager) {
327 if (project == nullptr || !project->hack_manifest.loaded()) {
328 return absl::OkStatus();
329 }
330 const auto ranges = palette_manager.GetModifiedColorWriteRanges(game_data);
331 if (ranges.empty()) {
332 return absl::OkStatus();
333 }
335 project->hack_manifest, project->rom_metadata.write_policy, ranges,
336 save_scope, "DungeonEditorV2", toast_manager);
337}
338
340 const project::YazeProject* project, Rom* rom,
341 const std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>&
342 entrances,
343 const std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>&
344 spawn_points,
345 absl::string_view save_scope, ToastManager* toast_manager) {
346 if (rom == nullptr || !rom->is_loaded()) {
347 return absl::FailedPreconditionError("ROM not loaded");
348 }
349
350 // Field and ROM-bound validation must happen even without a manifest. This
351 // runs before any editor save domain mutates the shared ROM buffer.
357
358 if (project == nullptr || !project->hack_manifest.loaded()) {
359 return absl::OkStatus();
360 }
361 auto ranges =
363 auto spawn_ranges =
365 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
366 if (ranges.empty()) {
367 return absl::OkStatus();
368 }
370 project->hack_manifest, project->rom_metadata.write_policy, ranges,
371 save_scope, "DungeonEditorV2", toast_manager);
372}
373
375 const zelda3::ChestSavePlan& plan,
376 absl::string_view save_scope,
377 ToastManager* toast_manager) {
378 const auto ranges = plan.write_ranges();
379 if (ranges.empty() || project == nullptr ||
380 !project->hack_manifest.loaded()) {
381 return absl::OkStatus();
382 }
384 project->hack_manifest, project->rom_metadata.write_policy, ranges,
385 save_scope, "DungeonEditorV2", toast_manager);
386}
387
389 const project::YazeProject* project, const Rom* rom,
390 const DungeonRoomStore& rooms, absl::string_view save_scope,
391 ToastManager* toast_manager) {
392 if (project == nullptr || !project->hack_manifest.loaded()) {
393 return absl::OkStatus();
394 }
395 const auto ranges = CollectDirtyWaterFillWriteRanges(rom, rooms);
396 if (ranges.empty()) {
397 return absl::OkStatus();
398 }
400 project->hack_manifest, project->rom_metadata.write_policy, ranges,
401 save_scope, "DungeonEditorV2", toast_manager);
402}
403
405 const project::YazeProject* project, const Rom* rom,
406 const DungeonRoomStore& rooms, absl::string_view save_scope,
407 ToastManager* toast_manager) {
408 if (project == nullptr || !project->hack_manifest.loaded()) {
409 return absl::OkStatus();
410 }
411 const auto ranges = CollectDirtyCustomCollisionWriteRanges(rom, rooms);
412 if (ranges.empty()) {
413 return absl::OkStatus();
414 }
416 project->hack_manifest, project->rom_metadata.write_policy, ranges,
417 save_scope, "DungeonEditorV2", toast_manager);
418}
419
420} // namespace
421
423 if (save_transaction_snapshot_.has_value()) {
424 return absl::FailedPreconditionError(
425 "Dungeon save transaction is already active");
426 }
427
429 auto& palette_manager = gfx::PaletteManager::Get();
430 if (core::FeatureFlags::get().dungeon.kSavePalettes &&
431 palette_manager.HasUnsavedChanges(game_data_)) {
432 if (!palette_manager.IsManaging(game_data_)) {
433 return absl::FailedPreconditionError(
434 "Cannot save dungeon palettes from a different ROM session");
435 }
436 RETURN_IF_ERROR(palette_manager.BeginSaveTransaction());
437 snapshot.has_palette_transaction = true;
438 }
439 rooms_.ForEachMaterialized([&snapshot](int room_id,
440 const zelda3::Room& room) {
441 snapshot.room_states.emplace_back(room_id, room.CaptureSaveDirtySnapshot());
442 });
443 for (size_t i = 0; i < entrances_.size(); ++i) {
444 snapshot.entrance_dirty_states[i] = entrances_[i].dirty();
445 }
446 for (size_t i = 0; i < spawn_points_.size(); ++i) {
447 snapshot.spawn_dirty_states[i] = spawn_points_[i].dirty();
448 }
449 if (game_data_ != nullptr) {
450 snapshot.has_pit_damage_table = true;
452 }
453 save_transaction_snapshot_ = std::move(snapshot);
454 return absl::OkStatus();
455}
456
458 if (!save_transaction_snapshot_.has_value()) {
459 return;
460 }
461
462 for (const auto& [room_id, state] : save_transaction_snapshot_->room_states) {
463 if (auto* room = rooms_.GetIfMaterialized(room_id); room != nullptr) {
464 room->RestoreSaveDirtySnapshot(state);
465 }
466 }
467 for (size_t i = 0; i < entrances_.size(); ++i) {
468 if (save_transaction_snapshot_->entrance_dirty_states[i]) {
469 entrances_[i].MarkDirty();
470 } else {
471 entrances_[i].ClearDirty();
472 }
473 }
474 for (size_t i = 0; i < spawn_points_.size(); ++i) {
475 if (save_transaction_snapshot_->spawn_dirty_states[i]) {
476 spawn_points_[i].MarkDirty();
477 } else {
478 spawn_points_[i].ClearDirty();
479 }
480 }
481 if (save_transaction_snapshot_->has_pit_damage_table &&
482 game_data_ != nullptr) {
483 if (save_transaction_snapshot_->pit_damage_dirty) {
485 } else {
487 }
488 }
489 if (save_transaction_snapshot_->has_palette_transaction) {
491 }
493}
494
502
504 const std::function<absl::Status()>& operation) {
505 if (!rom_ || !rom_->is_loaded()) {
506 return absl::FailedPreconditionError("ROM not loaded");
507 }
508
509 // File > Save already owns a whole-ROM/editor transaction. Direct Apply Room
510 // and Apply All actions use the same guarantees without nesting it.
511 if (save_transaction_snapshot_.has_value()) {
512 return operation();
513 }
514
515 ScopedRomTransaction rom_transaction(*rom_);
517
518 const absl::Status status = operation();
519 if (!status.ok()) {
521 return status;
522 }
523
525 rom_transaction.Commit();
526 return absl::OkStatus();
527}
528
529absl::Status DungeonEditorV2::Save() {
530 if (minecart_track_editor_panel_ != nullptr &&
532 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
533 }
534 if (!rom_ || !rom_->is_loaded()) {
535 return absl::FailedPreconditionError("ROM not loaded");
536 }
537 if (object_tile_editor_panel_ != nullptr &&
539 return absl::FailedPreconditionError(
540 "Apply or explicitly discard Object Tile Editor changes before "
541 "using Save or Apply Room");
542 }
543
544 const auto& flags = core::FeatureFlags::get().dungeon;
545 std::optional<zelda3::ChestSavePlan> chest_save_plan;
546
547 if (flags.kSaveCollision) {
548 RETURN_IF_ERROR(ValidateCustomCollisionManifestConflicts(
549 dependencies_.project, rom_, rooms_, "custom collision",
551 }
552 if (flags.kSaveWaterFillZones) {
553 RETURN_IF_ERROR(ValidateWaterFillManifestConflicts(
554 dependencies_.project, rom_, rooms_, "water fill zones",
556 }
557 if (flags.kSaveEntrances) {
558 RETURN_IF_ERROR(ValidateDungeonEntranceSavePreflight(
560 "dungeon entrances and spawn points", dependencies_.toast_manager));
561 }
562
563 if (flags.kSaveChests) {
565 rom_, static_cast<int>(rooms_.size()),
566 [this](int room_id) {
567 return rooms_.GetIfMaterialized(room_id);
568 }));
569 RETURN_IF_ERROR(ValidateChestManifestConflicts(
570 dependencies_.project, plan, "chest table",
572 chest_save_plan = std::move(plan);
573 }
574 if (flags.kSavePotItems) {
575 RETURN_IF_ERROR(ValidatePotItemManifestConflicts(
576 dependencies_.project, rom_, rooms_, "pot items",
578 }
579
580 auto& palette_manager = gfx::PaletteManager::Get();
581 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(game_data_)) {
582 if (!palette_manager.IsManaging(game_data_)) {
583 return absl::FailedPreconditionError(
584 "Cannot save dungeon palettes from a different ROM session");
585 }
586 RETURN_IF_ERROR(ValidatePaletteManifestConflicts(
587 dependencies_.project, palette_manager, game_data_, "dungeon palettes",
589 const size_t modified_color_count =
590 palette_manager.GetModifiedColorCount(game_data_);
591 auto status = palette_manager.SaveAllToRom();
592 if (!status.ok()) {
593 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
594 status.message().data());
595 return status;
596 }
597 LOG_INFO("DungeonEditorV2", "Saved %zu modified colors to ROM",
598 modified_color_count);
599 }
600
601 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
602 absl::Status save_status = absl::OkStatus();
603 rooms_.ForEachLoaded([&](int room_id, zelda3::Room&) {
604 if (!save_status.ok()) {
605 return;
606 }
607 auto status = SaveRoomData(room_id);
608 if (!status.ok()) {
609 save_status = status;
610 }
611 });
612 if (!save_status.ok()) {
613 return save_status;
614 }
615 }
616
617 if (flags.kSaveTorches) {
618 auto status = zelda3::SaveAllTorches(
619 rom_, static_cast<int>(rooms_.size()),
620 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
621 if (!status.ok()) {
622 LOG_ERROR("DungeonEditorV2", "Failed to save torches: %s",
623 status.message().data());
624 return status;
625 }
626 }
627
628 if (flags.kSavePits) {
629 auto status = zelda3::SaveAllPits(
631 if (!status.ok()) {
632 LOG_ERROR("DungeonEditorV2", "Failed to save pits: %s",
633 status.message().data());
634 return status;
635 }
636 }
637
638 if (flags.kSaveBlocks) {
639 auto status = zelda3::SaveAllBlocks(
640 rom_, static_cast<int>(rooms_.size()),
641 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
642 if (!status.ok()) {
643 LOG_ERROR("DungeonEditorV2", "Failed to save blocks: %s",
644 status.message().data());
645 return status;
646 }
647 }
648
649 if (flags.kSaveCollision) {
650 auto status = zelda3::SaveAllCollision(
651 rom_, static_cast<int>(rooms_.size()),
652 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
653 if (!status.ok()) {
654 LOG_ERROR("DungeonEditorV2", "Failed to save collision: %s",
655 status.message().data());
656 return status;
657 }
658 }
659
660 if (flags.kSaveWaterFillZones) {
661 auto status = SaveWaterFillZones(rom_, rooms_);
662 if (!status.ok()) {
663 LOG_ERROR("DungeonEditorV2", "Failed to save water fill zones: %s",
664 status.message().data());
665 return status;
666 }
667 }
668
669 if (flags.kSaveChests) {
670 auto status = zelda3::ApplyChestSavePlan(
671 rom_, *chest_save_plan,
672 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
673 if (!status.ok()) {
674 LOG_ERROR("DungeonEditorV2", "Failed to save chests: %s",
675 status.message().data());
676 return status;
677 }
678 }
679
680 if (flags.kSavePotItems) {
681 auto status =
682 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_);
683 if (!status.ok()) {
684 LOG_ERROR("DungeonEditorV2", "Failed to save pot items: %s",
685 status.message().data());
686 return status;
687 }
688 }
689
690 if (flags.kSaveEntrances) {
692 if (!status.ok()) {
693 LOG_ERROR("DungeonEditorV2", "Failed to save dungeon entrances: %s",
694 status.message().data());
695 return status;
696 }
698 if (!status.ok()) {
699 LOG_ERROR("DungeonEditorV2", "Failed to save dungeon spawn points: %s",
700 status.message().data());
701 return status;
702 }
703 }
704
705 return absl::OkStatus();
706}
707
708std::vector<std::pair<uint32_t, uint32_t>> DungeonEditorV2::CollectWriteRanges()
709 const {
710 std::vector<std::pair<uint32_t, uint32_t>> ranges;
711
712 if (!rom_ || !rom_->is_loaded()) {
713 return ranges;
714 }
715
716 const auto& flags = core::FeatureFlags::get().dungeon;
717 const auto& rom_data = rom_->vector();
718
719 auto& palette_manager = gfx::PaletteManager::Get();
720 if (flags.kSavePalettes) {
721 auto palette_ranges =
722 palette_manager.GetModifiedColorWriteRanges(game_data_);
723 ranges.insert(ranges.end(), palette_ranges.begin(), palette_ranges.end());
724 }
725
726 if (flags.kSaveEntrances) {
727 auto entrance_ranges =
729 ranges.insert(ranges.end(), entrance_ranges.begin(), entrance_ranges.end());
730 auto spawn_ranges =
732 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
733 }
734
735 auto append_declared_cow_ranges = [&](core::DungeonStreamType stream_type,
736 bool stream_dirty, int room_id) {
737 if (!stream_dirty || dependencies_.project == nullptr ||
739 return;
740 }
741 const auto* layout =
743 stream_type);
744 if (layout == nullptr ||
745 layout->strategy != core::DungeonWriteStrategy::kCopyOnWrite ||
746 room_id < 0 ||
747 static_cast<uint32_t>(room_id) >= layout->pointer_count) {
748 return;
749 }
750
751 for (const auto& range : layout->allocation_regions) {
752 ranges.emplace_back(yaze::SnesToPc(range.start),
753 yaze::SnesToPc(range.end));
754 }
755 const uint32_t pointer_width =
757 : 2u;
758 const uint32_t pointer_slot =
759 yaze::SnesToPc(layout->pointer_table) +
760 static_cast<uint32_t>(room_id) * pointer_width;
761 ranges.emplace_back(pointer_slot, pointer_slot + pointer_width);
762 if (stream_type == core::DungeonStreamType::kObjects) {
763 const uint32_t door_slot =
764 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
765 ranges.emplace_back(door_slot, door_slot + 3u);
766 }
767 };
768
769 // Oracle of Secrets: the water fill table lives in a reserved tail region.
770 // Include it in write-range reporting whenever we have dirty water fill data,
771 // even if no rooms are currently loaded (SaveWaterFillZones() is room-indexed
772 // and independent of room loading state).
773 if (flags.kSaveWaterFillZones) {
774 auto water_fill_ranges = CollectDirtyWaterFillWriteRanges(rom_, rooms_);
775 ranges.insert(ranges.end(), water_fill_ranges.begin(),
776 water_fill_ranges.end());
777 }
778
779 // Custom collision writes update the pointer table and append blobs into the
780 // expanded collision region. SaveAllCollision() is room-indexed, so include
781 // these ranges whenever any room has dirty custom collision data.
782 if (flags.kSaveCollision) {
783 auto collision_ranges =
784 CollectDirtyCustomCollisionWriteRanges(rom_, rooms_);
785 ranges.insert(ranges.end(), collision_ranges.begin(),
786 collision_ranges.end());
787 }
788
789 // Chests use one global fixed-capacity table. Report only the exact byte
790 // deltas from the same planner used by the serializer so unrelated protected
791 // bytes inside the live table do not block a safe edit.
792 if (flags.kSaveChests) {
793 auto chest_ranges = CollectDirtyChestWriteRanges(rom_, rooms_);
794 if (chest_ranges.ok()) {
795 ranges.insert(ranges.end(), chest_ranges->begin(), chest_ranges->end());
796 } else {
797 LOG_WARN("DungeonEditorV2", "Could not predict chest write ranges: %s",
798 chest_ranges.status().message().data());
799 }
800 }
801
802 rooms_.ForEachLoaded([&](int room_id, const zelda3::Room& room) {
803 room_id = room.id();
804
805 // Header range
806 if (flags.kSaveRoomHeaders && room.header_dirty()) {
807 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
808 int header_ptr_table =
809 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
810 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
811 rom_data[zelda3::kRoomHeaderPointer];
812 header_ptr_table = yaze::SnesToPc(header_ptr_table);
813 int table_offset = header_ptr_table + (room_id * 2);
814
815 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
816 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
817 (rom_data[table_offset + 1] << 8) |
818 rom_data[table_offset];
819 int header_location = yaze::SnesToPc(address);
820 ranges.emplace_back(header_location, header_location + 14);
821 }
822 }
823 const int message_address = zelda3::kMessagesIdDungeon + (room_id * 2);
824 if (message_address >= 0 &&
825 message_address + 1 < static_cast<int>(rom_data.size())) {
826 ranges.emplace_back(message_address, message_address + 2);
827 }
828 }
829
830 // Object range
831 if (flags.kSaveObjects && room.object_stream_dirty()) {
832 const uint32_t door_slot =
833 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
834 ranges.emplace_back(door_slot, door_slot + 3u);
835 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
836 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
837 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
839 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
840 int entry_offset = obj_ptr_table + (room_id * 3);
841
842 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
843 int tile_addr = (rom_data[entry_offset + 2] << 16) |
844 (rom_data[entry_offset + 1] << 8) |
845 rom_data[entry_offset];
846 int objects_location = yaze::SnesToPc(tile_addr);
847
848 auto encoded = room.EncodeObjects();
849 ranges.emplace_back(objects_location,
850 objects_location + encoded.size() + 2);
851 }
852 }
853 }
854
855 if (flags.kSaveObjects && room.object_stream_header_dirty() &&
856 zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
857 const uint32_t table_snes =
858 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 2])
859 << 16) |
860 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 1])
861 << 8) |
863 if ((table_snes & 0xFFFFu) >= 0x8000u) {
864 const int table_pc = yaze::SnesToPc(table_snes);
865 const int pointer_slot = table_pc + room_id * 3;
866 if (pointer_slot >= 0 &&
867 pointer_slot + 2 < static_cast<int>(rom_data.size())) {
868 const uint32_t stream_snes =
869 (static_cast<uint32_t>(rom_data[pointer_slot + 2]) << 16) |
870 (static_cast<uint32_t>(rom_data[pointer_slot + 1]) << 8) |
871 rom_data[pointer_slot];
872 if ((stream_snes & 0xFFFFu) >= 0x8000u) {
873 const uint32_t stream_pc = yaze::SnesToPc(stream_snes);
874 if (static_cast<uint64_t>(stream_pc) + 2u <= rom_data.size()) {
875 ranges.emplace_back(stream_pc, stream_pc + 2u);
876 }
877 }
878 }
879 }
880 }
881
882 // Sprite stream (sort byte + encoded sprite records/terminator).
883 if (flags.kSaveSprites && room.sprites_dirty() &&
884 zelda3::kRoomsSpritePointer + 1 < static_cast<int>(rom_data.size())) {
885 const uint32_t table_snes =
886 0x090000u |
887 (static_cast<uint32_t>(rom_data[zelda3::kRoomsSpritePointer + 1])
888 << 8) |
890 const int table_pc = yaze::SnesToPc(table_snes);
891 const int pointer_slot = table_pc + room_id * 2;
892 if (pointer_slot >= 0 &&
893 pointer_slot + 1 < static_cast<int>(rom_data.size())) {
894 const uint16_t pointer =
895 (static_cast<uint16_t>(rom_data[pointer_slot + 1]) << 8) |
896 rom_data[pointer_slot];
897 if (pointer >= 0x8000) {
898 const uint32_t stream_pc = yaze::SnesToPc(0x090000u | pointer);
899 ranges.emplace_back(stream_pc,
900 stream_pc + room.EncodeSprites().size() + 1);
901 }
902 }
903 }
904
905 append_declared_cow_ranges(
907 flags.kSaveObjects &&
909 room_id);
910 append_declared_cow_ranges(core::DungeonStreamType::kSprites,
911 flags.kSaveSprites && room.sprites_dirty(),
912 room_id);
913 });
914
915 // Pot items are saved after room-local streams but still participate in the
916 // same whole-ROM transaction and manifest preflight.
917 if (flags.kSavePotItems) {
918 auto pot_ranges =
919 CollectDirtyPotItemWriteRanges(dependencies_.project, rom_, rooms_);
920 ranges.insert(ranges.end(), pot_ranges.begin(), pot_ranges.end());
921 }
922
923 return ranges;
924}
925
926absl::Status DungeonEditorV2::SaveRoom(int room_id) {
927 if (minecart_track_editor_panel_ != nullptr &&
928 minecart_track_editor_panel_->HasUnpublishedChanges()) {
929 return absl::FailedPreconditionError(kMinecartDraftSaveBlockMessage);
930 }
931 if (object_tile_editor_panel_ != nullptr &&
932 object_tile_editor_panel_->HasUnappliedChanges()) {
933 return absl::FailedPreconditionError(
934 "Apply or explicitly discard Object Tile Editor changes before "
935 "using Save or Apply Room");
936 }
937 return RunWithSaveTransaction([this, room_id]() -> absl::Status {
938 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
939 return absl::InvalidArgumentError("Invalid room ID");
940 }
941
942 const auto& flags = core::FeatureFlags::get().dungeon;
943 std::optional<zelda3::ChestSavePlan> chest_save_plan;
944 if (flags.kSaveCollision) {
945 RETURN_IF_ERROR(ValidateCustomCollisionManifestConflicts(
946 dependencies_.project, rom_, rooms_,
947 absl::StrFormat("custom collision for room 0x%03X", room_id),
948 dependencies_.toast_manager));
949 }
950 if (flags.kSaveWaterFillZones) {
951 RETURN_IF_ERROR(ValidateWaterFillManifestConflicts(
952 dependencies_.project, rom_, rooms_,
953 absl::StrFormat("water fill zones for room 0x%03X", room_id),
954 dependencies_.toast_manager));
955 }
956 if (flags.kSaveEntrances) {
957 RETURN_IF_ERROR(ValidateDungeonEntranceSavePreflight(
958 dependencies_.project, rom_, entrances_, spawn_points_,
959 absl::StrFormat(
960 "dungeon entrances and spawn points (Apply Room 0x%03X)",
961 room_id),
962 dependencies_.toast_manager));
963 }
964 if (flags.kSaveChests) {
967 rom_, static_cast<int>(rooms_.size()), [this](int id) {
968 return rooms_.GetIfMaterialized(id);
969 }));
970 RETURN_IF_ERROR(ValidateChestManifestConflicts(
971 dependencies_.project, plan,
972 absl::StrFormat("chest table for room 0x%03X", room_id),
973 dependencies_.toast_manager));
974 chest_save_plan = std::move(plan);
975 }
976 if (flags.kSavePotItems) {
977 RETURN_IF_ERROR(ValidatePotItemManifestConflicts(
978 dependencies_.project, rom_, rooms_,
979 absl::StrFormat("pot items for room 0x%03X", room_id),
980 dependencies_.toast_manager));
981 }
982 auto& palette_manager = gfx::PaletteManager::Get();
983 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(game_data_)) {
984 if (!palette_manager.IsManaging(game_data_)) {
985 return absl::FailedPreconditionError(
986 "Cannot save dungeon palettes from a different ROM session");
987 }
988 RETURN_IF_ERROR(ValidatePaletteManifestConflicts(
989 dependencies_.project, palette_manager, game_data_,
990 absl::StrFormat("dungeon palettes for room 0x%03X", room_id),
991 dependencies_.toast_manager));
992 auto status = palette_manager.SaveAllToRom();
993 if (!status.ok()) {
994 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
995 status.message().data());
996 return status;
997 }
998 }
999 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
1000 RETURN_IF_ERROR(SaveRoomData(room_id));
1001 }
1002
1003 if (flags.kSaveTorches) {
1005 rom_, static_cast<int>(rooms_.size()),
1006 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1007 }
1008 if (flags.kSavePits) {
1010 rom_, game_data_ ? &game_data_->pit_damage_table : nullptr));
1011 }
1012 if (flags.kSaveBlocks) {
1014 rom_, static_cast<int>(rooms_.size()),
1015 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1016 }
1017 if (flags.kSaveCollision) {
1019 rom_, static_cast<int>(rooms_.size()),
1020 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1021 }
1022 if (flags.kSaveWaterFillZones) {
1023 RETURN_IF_ERROR(SaveWaterFillZones(rom_, rooms_));
1024 }
1025 if (flags.kSaveChests) {
1027 rom_, *chest_save_plan,
1028 [this](int id) { return rooms_.GetIfMaterialized(id); }));
1029 }
1030 if (flags.kSavePotItems) {
1032 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_));
1033 }
1034 if (flags.kSaveEntrances) {
1037 }
1038
1039 return absl::OkStatus();
1040 });
1041}
1042
1043absl::Status DungeonEditorV2::SaveRoomData(int room_id) {
1044 if (!rom_ || !rom_->is_loaded()) {
1045 return absl::FailedPreconditionError("ROM not loaded");
1046 }
1047 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
1048 return absl::InvalidArgumentError("Invalid room ID");
1049 }
1050
1051 auto& room = rooms_[room_id];
1052 if (!room.IsLoaded()) {
1053 return absl::OkStatus();
1054 }
1055
1056 // ROM safety: validate loaded room content before writing any bytes.
1057 {
1058 zelda3::DungeonValidator validator;
1059 const auto result = validator.ValidateRoom(room);
1060 for (const auto& w : result.warnings) {
1061 LOG_WARN("DungeonEditorV2", "Room 0x%03X validation warning: %s", room_id,
1062 w.c_str());
1063 }
1064 if (!result.is_valid) {
1065 for (const auto& e : result.errors) {
1066 LOG_ERROR("DungeonEditorV2", "Room 0x%03X validation error: %s",
1067 room_id, e.c_str());
1068 }
1069 if (dependencies_.toast_manager) {
1070 dependencies_.toast_manager->Show(
1071 absl::StrFormat(
1072 "Save blocked: room 0x%03X failed validation (%zu error(s))",
1073 room_id, result.errors.size()),
1074 ToastType::kError);
1075 }
1076 return absl::FailedPreconditionError(
1077 absl::StrFormat("Room 0x%03X failed validation", room_id));
1078 }
1079 }
1080
1081 const auto& flags = core::FeatureFlags::get().dungeon;
1082
1083 // A present copy-on-write layout is an explicit capability grant from the
1084 // project manifest. Without it, Room retains the Wave 1 fail-closed behavior
1085 // for shared or over-capacity streams.
1086 std::optional<zelda3::DungeonStreamLayout> object_cow_layout;
1087 std::optional<zelda3::DungeonStreamLayout> sprite_cow_layout;
1088 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1089 const auto& manifest = dependencies_.project->hack_manifest;
1090 auto load_cow_layout =
1091 [&](core::DungeonStreamType stream_type, bool stream_dirty,
1092 std::optional<zelda3::DungeonStreamLayout>* out) -> absl::Status {
1093 if (!stream_dirty) {
1094 return absl::OkStatus();
1095 }
1096 const auto* manifest_layout =
1097 manifest.GetDungeonStreamLayout(stream_type);
1098 if (manifest_layout == nullptr ||
1099 manifest_layout->strategy !=
1101 return absl::OkStatus();
1102 }
1104 zelda3::DungeonStreamLayout allocator_layout,
1105 core::ToDungeonStreamAllocatorLayout(stream_type, *manifest_layout));
1106 *out = std::move(allocator_layout);
1107 return absl::OkStatus();
1108 };
1109
1110 RETURN_IF_ERROR(load_cow_layout(
1112 flags.kSaveObjects &&
1113 (room.object_stream_dirty() || room.object_stream_header_dirty()),
1114 &object_cow_layout));
1116 flags.kSaveSprites && room.sprites_dirty(),
1117 &sprite_cow_layout));
1118 }
1119
1120 // HACK MANIFEST VALIDATION
1121 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
1122 std::vector<std::pair<uint32_t, uint32_t>> ranges;
1123 const auto& manifest = dependencies_.project->hack_manifest;
1124 const auto& rom_data = rom_->vector();
1125
1126 // 1. Validate Header Range
1127 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1128 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
1129 int header_ptr_table =
1130 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
1131 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
1133 header_ptr_table = yaze::SnesToPc(header_ptr_table);
1134 int table_offset = header_ptr_table + (room_id * 2);
1135
1136 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1137 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1138 (rom_data[table_offset + 1] << 8) |
1139 rom_data[table_offset];
1140 int header_location = yaze::SnesToPc(address);
1141 ranges.emplace_back(header_location, header_location + 14);
1142 }
1143 }
1144 const int message_address = zelda3::kMessagesIdDungeon + (room_id * 2);
1145 if (message_address >= 0 &&
1146 message_address + 1 < static_cast<int>(rom_data.size())) {
1147 ranges.emplace_back(message_address, message_address + 2);
1148 }
1149 }
1150
1151 // 2. Validate Object Range
1152 if (flags.kSaveObjects && room.object_stream_dirty()) {
1153 const uint32_t door_slot =
1154 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
1155 ranges.emplace_back(door_slot, door_slot + 3u);
1156 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1157 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
1158 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
1160 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
1161 int entry_offset = obj_ptr_table + (room_id * 3);
1162
1163 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
1164 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1165 (rom_data[entry_offset + 1] << 8) |
1166 rom_data[entry_offset];
1167 int objects_location = yaze::SnesToPc(tile_addr);
1168
1169 // Estimate size based on current encoding
1170 // Note: we check the *target* location (where we will write)
1171 // The EncodeObjects() size is what we *will* write.
1172 // We add 2 bytes for the size/header that SaveObjects writes.
1173 auto encoded = room.EncodeObjects();
1174 ranges.emplace_back(objects_location,
1175 objects_location + encoded.size() + 2);
1176 }
1177 }
1178 }
1179
1180 if (flags.kSaveObjects && room.object_stream_header_dirty() &&
1181 zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1182 const uint32_t table_snes =
1183 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 2])
1184 << 16) |
1185 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 1])
1186 << 8) |
1188 if ((table_snes & 0xFFFFu) >= 0x8000u) {
1189 const int table_pc = yaze::SnesToPc(table_snes);
1190 const int pointer_slot = table_pc + room_id * 3;
1191 if (pointer_slot >= 0 &&
1192 pointer_slot + 2 < static_cast<int>(rom_data.size())) {
1193 const uint32_t stream_snes =
1194 (static_cast<uint32_t>(rom_data[pointer_slot + 2]) << 16) |
1195 (static_cast<uint32_t>(rom_data[pointer_slot + 1]) << 8) |
1196 rom_data[pointer_slot];
1197 if ((stream_snes & 0xFFFFu) >= 0x8000u) {
1198 const uint32_t stream_pc = yaze::SnesToPc(stream_snes);
1199 if (static_cast<uint64_t>(stream_pc) + 2u <= rom_data.size()) {
1200 ranges.emplace_back(stream_pc, stream_pc + 2u);
1201 }
1202 }
1203 }
1204 }
1205 }
1206
1207 // 3. Validate the current sprite stream for the in-place fast path.
1208 if (flags.kSaveSprites && room.sprites_dirty() &&
1209 zelda3::kRoomsSpritePointer + 1 < static_cast<int>(rom_data.size())) {
1210 const uint32_t table_snes =
1211 0x090000u |
1212 (static_cast<uint32_t>(rom_data[zelda3::kRoomsSpritePointer + 1])
1213 << 8) |
1215 const int table_pc = yaze::SnesToPc(table_snes);
1216 const int pointer_slot = table_pc + room_id * 2;
1217 if (pointer_slot >= 0 &&
1218 pointer_slot + 1 < static_cast<int>(rom_data.size())) {
1219 const uint16_t pointer =
1220 (static_cast<uint16_t>(rom_data[pointer_slot + 1]) << 8) |
1221 rom_data[pointer_slot];
1222 if (pointer >= 0x8000) {
1223 const uint32_t stream_pc = yaze::SnesToPc(0x090000u | pointer);
1224 ranges.emplace_back(stream_pc,
1225 stream_pc + room.EncodeSprites().size() + 1);
1226 }
1227 }
1228 }
1229
1230 // A COW fallback may write anywhere in its explicitly declared allocator
1231 // arena, followed by the selected room's pointer slot(s). Validate those
1232 // possible writes before Room mutates the ROM. This is intentionally
1233 // conservative; in-place saves still remain a subset of the allowed set.
1234 auto append_cow_ranges = [&](const zelda3::DungeonStreamLayout& layout) {
1235 for (const auto& range : layout.allocation_ranges) {
1236 ranges.emplace_back(range.begin, range.end);
1237 }
1238 const uint32_t pointer_width =
1239 layout.pointer_encoding == zelda3::DungeonPointerEncoding::kLong24
1240 ? 3u
1241 : 2u;
1242 const uint32_t pointer_slot =
1243 layout.pointer_table_pc +
1244 static_cast<uint32_t>(room_id) * pointer_width;
1245 ranges.emplace_back(pointer_slot, pointer_slot + pointer_width);
1246 if (layout.kind == zelda3::DungeonStreamKind::kObject) {
1247 const uint32_t door_slot =
1248 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
1249 ranges.emplace_back(door_slot, door_slot + 3u);
1250 }
1251 };
1252 if (object_cow_layout.has_value()) {
1253 append_cow_ranges(*object_cow_layout);
1254 }
1255 if (sprite_cow_layout.has_value()) {
1256 append_cow_ranges(*sprite_cow_layout);
1257 }
1258
1259 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1260 // address space (LoROM), so convert before analysis.
1261 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1263 manifest, write_policy, ranges, absl::StrFormat("room 0x%03X", room_id),
1264 "DungeonEditorV2", dependencies_.toast_manager));
1265 }
1266
1267 if (flags.kSaveObjects && room.object_stream_dirty()) {
1268 auto status = room.SaveObjects(
1269 object_cow_layout.has_value() ? &*object_cow_layout : nullptr);
1270 if (!status.ok()) {
1271 LOG_ERROR("DungeonEditorV2", "Failed to save room objects: %s",
1272 status.message().data());
1273 return status;
1274 }
1275 }
1276
1277 if (flags.kSaveObjects && room.object_stream_header_dirty()) {
1278 auto status = room.SaveObjectStreamHeader(
1279 object_cow_layout.has_value() ? &*object_cow_layout : nullptr);
1280 if (!status.ok()) {
1281 LOG_ERROR("DungeonEditorV2",
1282 "Failed to save room object-stream header: %s",
1283 status.message().data());
1284 return status;
1285 }
1286 }
1287
1288 if (flags.kSaveSprites && room.sprites_dirty()) {
1289 auto status = room.SaveSprites(
1290 sprite_cow_layout.has_value() ? &*sprite_cow_layout : nullptr);
1291 if (!status.ok()) {
1292 LOG_ERROR("DungeonEditorV2", "Failed to save room sprites: %s",
1293 status.message().data());
1294 return status;
1295 }
1296 }
1297
1298 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1299 auto status = room.SaveRoomHeader();
1300 if (!status.ok()) {
1301 LOG_ERROR("DungeonEditorV2", "Failed to save room header: %s",
1302 status.message().data());
1303 return status;
1304 }
1305 }
1306
1307 return absl::OkStatus();
1308}
1309
1310void DungeonEditorV2::SaveAllRooms() {
1311 auto status = RunWithSaveTransaction([this]() { return Save(); });
1312 if (status.ok()) {
1313 if (dependencies_.toast_manager) {
1314 dependencies_.toast_manager->Show("Applied loaded rooms to ROM buffer",
1315 ToastType::kSuccess);
1316 }
1317 } else {
1318 LOG_ERROR("DungeonEditorV2", "SaveAllRooms failed: %s",
1319 status.message().data());
1320 if (dependencies_.toast_manager) {
1321 dependencies_.toast_manager->Show(
1322 absl::StrFormat("Save all failed: %s", status.message()),
1323 ToastType::kError);
1324 }
1325 }
1326}
1327
1328} // 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:28
const auto & vector() const
Definition rom.h:173
auto size() const
Definition rom.h:168
bool is_loaded() const
Definition rom.h:155
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_
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)
EditorDependencies dependencies_
Definition editor.h:333
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:574
bool custom_collision_dirty() const
Definition room.h:523
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:528
int WaterFillTileCount() const
Definition room.h:569
bool pot_items_dirty() const
Definition room.h:369
bool object_stream_dirty() const
Definition room.h:442
bool sprites_dirty() const
Definition room.h:256
bool header_dirty() const
Definition room.h:585
bool object_stream_header_dirty() const
Definition room.h:444
bool has_water_fill_zone() const
Definition room.h:530
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:1797
void ClearWaterFillDirty()
Definition room.h:582
std::vector< uint8_t > EncodeSprites() const
Definition room.cc:1885
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
Definition room.h:600
bool water_fill_dirty() const
Definition room.h:581
const std::vector< PotItem > & GetPotItems() const
Definition room.h:367
uint8_t water_fill_sram_bit_mask() const
Definition room.h:571
int id() const
Definition room.h:900
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#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 ValidateWaterFillManifestConflicts(const project::YazeProject *project, const Rom *rom, const DungeonRoomStore &rooms, absl::string_view save_scope, ToastManager *toast_manager)
Editors are the view controllers for the application.
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
constexpr int kDoorPointers
constexpr int kWaterFillTableEnd
constexpr int kCustomCollisionDataSoftEnd
absl::StatusOr< ChestSavePlan > BuildChestSavePlan(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3763
constexpr int kWaterFillTableStart
constexpr int kMessagesIdDungeon
absl::Status NormalizeWaterFillZoneMasks(std::vector< WaterFillZoneEntry > *zones)
absl::Status SaveAllPotItems(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:3985
std::vector< DungeonEntranceWriteRange > CollectDirtyRegularDungeonEntranceWriteRanges(const std::array< RoomEntrance, kNumDungeonEntranceSlots > &entrances)
absl::Status SaveAllDungeonSpawnPoints(Rom *rom, std::array< DungeonSpawnPoint, kNumDungeonSpawnPoints > &spawn_points, bool dirty_only=true)
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2916
constexpr int kRoomsSpritePointer
absl::Status SaveAllPits(Rom *rom)
Definition room.cc:2930
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:3074
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:3777
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:3427
absl::Status ValidateDungeonSpawnPointsForSave(const Rom &rom, const std::array< DungeonSpawnPoint, kNumDungeonSpawnPoints > &spawn_points, bool dirty_only=true)
constexpr int kRoomObjectPointer
std::vector< DungeonSpawnPointWriteRange > CollectDirtyDungeonSpawnPointWriteRanges(const std::array< DungeonSpawnPoint, kNumDungeonSpawnPoints > &spawn_points)
absl::Status WriteWaterFillTable(Rom *rom, const std::vector< WaterFillZoneEntry > &zones)
uint32_t SnesToPc(uint32_t addr) noexcept
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:173
RomWritePolicy write_policy
Definition project.h:110
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
core::HackManifest hack_manifest
Definition project.h:212
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
Definition room.h:1218
PitDamageTable pit_damage_table
Definition game_data.h:108
std::vector< uint16_t > fill_offsets
std::array< uint8_t, 64 *64 > tiles
Definition room.h:193