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