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>>>
218 bool any_dirty = false;
219 rooms.ForEachMaterialized([&](int, const zelda3::Room& room) {
220 if (room.chests_dirty()) {
221 any_dirty = true;
222 }
223 });
224 if (!any_dirty) {
225 return std::vector<std::pair<uint32_t, uint32_t>>{};
226 }
228}
229
231 Rom* rom, DungeonRoomStore& rooms) {
232 std::optional<zelda3::DungeonStreamLayout> repack_layout;
233 bool any_dirty = false;
234 rooms.ForEachMaterialized([&](int, const zelda3::Room& room) {
235 if (room.pot_items_dirty()) {
236 any_dirty = true;
237 }
238 });
239 if (any_dirty && project != nullptr && project->hack_manifest.loaded()) {
240 const auto* manifest_layout = project->hack_manifest.GetDungeonStreamLayout(
242 if (manifest_layout != nullptr &&
243 manifest_layout->strategy == core::DungeonWriteStrategy::kRepackAll) {
245 zelda3::DungeonStreamLayout allocator_layout,
247 core::DungeonStreamType::kPotItems, *manifest_layout));
248 repack_layout = std::move(allocator_layout);
249 }
250 }
251
253 rom, static_cast<int>(rooms.size()),
254 [&rooms](int room_id) { return rooms.GetIfMaterialized(room_id); },
255 repack_layout.has_value() ? &*repack_layout : nullptr);
256}
257
259 const project::YazeProject* project, Rom* rom,
260 const DungeonRoomStore& rooms, absl::string_view save_scope,
261 ToastManager* toast_manager) {
262 if (project == nullptr || !project->hack_manifest.loaded()) {
263 return absl::OkStatus();
264 }
265 const auto ranges = CollectDirtyPotItemWriteRanges(project, rom, rooms);
266 if (ranges.empty()) {
267 return absl::OkStatus();
268 }
270 project->hack_manifest, project->rom_metadata.write_policy, ranges,
271 save_scope, "DungeonEditorV2", toast_manager);
272}
273
275 const project::YazeProject* project, Rom* rom,
276 const std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>&
277 entrances,
278 const std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>&
279 spawn_points,
280 absl::string_view save_scope, ToastManager* toast_manager) {
281 if (rom == nullptr || !rom->is_loaded()) {
282 return absl::FailedPreconditionError("ROM not loaded");
283 }
284
285 // Field and ROM-bound validation must happen even without a manifest. This
286 // runs before any editor save domain mutates the shared ROM buffer.
292
293 if (project == nullptr || !project->hack_manifest.loaded()) {
294 return absl::OkStatus();
295 }
296 auto ranges =
298 auto spawn_ranges =
300 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
301 if (ranges.empty()) {
302 return absl::OkStatus();
303 }
305 project->hack_manifest, project->rom_metadata.write_policy, ranges,
306 save_scope, "DungeonEditorV2", toast_manager);
307}
308
310 Rom* rom,
311 const DungeonRoomStore& rooms,
312 absl::string_view save_scope,
313 ToastManager* toast_manager) {
314 ASSIGN_OR_RETURN(const auto ranges, CollectDirtyChestWriteRanges(rom, rooms));
315 if (ranges.empty() || project == nullptr ||
316 !project->hack_manifest.loaded()) {
317 return absl::OkStatus();
318 }
320 project->hack_manifest, project->rom_metadata.write_policy, ranges,
321 save_scope, "DungeonEditorV2", toast_manager);
322}
323
324} // namespace
325
327 if (save_transaction_snapshot_.has_value()) {
328 return absl::FailedPreconditionError(
329 "Dungeon save transaction is already active");
330 }
331
333 auto& palette_manager = gfx::PaletteManager::Get();
334 if (core::FeatureFlags::get().dungeon.kSavePalettes &&
335 palette_manager.HasUnsavedChanges(game_data_)) {
336 if (!palette_manager.IsManaging(game_data_)) {
337 return absl::FailedPreconditionError(
338 "Cannot save dungeon palettes from a different ROM session");
339 }
340 RETURN_IF_ERROR(palette_manager.BeginSaveTransaction());
341 snapshot.has_palette_transaction = true;
342 }
343 rooms_.ForEachMaterialized([&snapshot](int room_id,
344 const zelda3::Room& room) {
345 snapshot.room_states.emplace_back(room_id, room.CaptureSaveDirtySnapshot());
346 });
347 for (size_t i = 0; i < entrances_.size(); ++i) {
348 snapshot.entrance_dirty_states[i] = entrances_[i].dirty();
349 }
350 for (size_t i = 0; i < spawn_points_.size(); ++i) {
351 snapshot.spawn_dirty_states[i] = spawn_points_[i].dirty();
352 }
353 if (game_data_ != nullptr) {
354 snapshot.has_pit_damage_table = true;
356 }
357 save_transaction_snapshot_ = std::move(snapshot);
358 return absl::OkStatus();
359}
360
362 if (!save_transaction_snapshot_.has_value()) {
363 return;
364 }
365
366 for (const auto& [room_id, state] : save_transaction_snapshot_->room_states) {
367 if (auto* room = rooms_.GetIfMaterialized(room_id); room != nullptr) {
368 room->RestoreSaveDirtySnapshot(state);
369 }
370 }
371 for (size_t i = 0; i < entrances_.size(); ++i) {
372 if (save_transaction_snapshot_->entrance_dirty_states[i]) {
373 entrances_[i].MarkDirty();
374 } else {
375 entrances_[i].ClearDirty();
376 }
377 }
378 for (size_t i = 0; i < spawn_points_.size(); ++i) {
379 if (save_transaction_snapshot_->spawn_dirty_states[i]) {
380 spawn_points_[i].MarkDirty();
381 } else {
382 spawn_points_[i].ClearDirty();
383 }
384 }
385 if (save_transaction_snapshot_->has_pit_damage_table &&
386 game_data_ != nullptr) {
387 if (save_transaction_snapshot_->pit_damage_dirty) {
389 } else {
391 }
392 }
393 if (save_transaction_snapshot_->has_palette_transaction) {
395 }
397}
398
406
408 const std::function<absl::Status()>& operation) {
409 if (!rom_ || !rom_->is_loaded()) {
410 return absl::FailedPreconditionError("ROM not loaded");
411 }
412
413 // File > Save already owns a whole-ROM/editor transaction. Direct Apply Room
414 // and Apply All actions use the same guarantees without nesting it.
415 if (save_transaction_snapshot_.has_value()) {
416 return operation();
417 }
418
419 ScopedRomTransaction rom_transaction(*rom_);
421
422 const absl::Status status = operation();
423 if (!status.ok()) {
425 return status;
426 }
427
429 rom_transaction.Commit();
430 return absl::OkStatus();
431}
432
433absl::Status DungeonEditorV2::Save() {
434 if (!rom_ || !rom_->is_loaded()) {
435 return absl::FailedPreconditionError("ROM not loaded");
436 }
437
438 const auto& flags = core::FeatureFlags::get().dungeon;
439
440 if (flags.kSaveEntrances) {
441 RETURN_IF_ERROR(ValidateDungeonEntranceSavePreflight(
443 "dungeon entrances and spawn points", dependencies_.toast_manager));
444 }
445
446 if (flags.kSaveChests) {
447 RETURN_IF_ERROR(ValidateChestManifestConflicts(
448 dependencies_.project, rom_, rooms_, "chest table",
450 }
451 if (flags.kSavePotItems) {
452 RETURN_IF_ERROR(ValidatePotItemManifestConflicts(
453 dependencies_.project, rom_, rooms_, "pot items",
455 }
456
457 auto& palette_manager = gfx::PaletteManager::Get();
458 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(game_data_)) {
459 if (!palette_manager.IsManaging(game_data_)) {
460 return absl::FailedPreconditionError(
461 "Cannot save dungeon palettes from a different ROM session");
462 }
463 const size_t modified_color_count =
464 palette_manager.GetModifiedColorCount(game_data_);
465 auto status = palette_manager.SaveAllToRom();
466 if (!status.ok()) {
467 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
468 status.message().data());
469 return status;
470 }
471 LOG_INFO("DungeonEditorV2", "Saved %zu modified colors to ROM",
472 modified_color_count);
473 }
474
475 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
476 absl::Status save_status = absl::OkStatus();
477 rooms_.ForEachLoaded([&](int room_id, zelda3::Room&) {
478 if (!save_status.ok()) {
479 return;
480 }
481 auto status = SaveRoomData(room_id);
482 if (!status.ok()) {
483 save_status = status;
484 }
485 });
486 if (!save_status.ok()) {
487 return save_status;
488 }
489 }
490
491 if (flags.kSaveTorches) {
492 auto status = zelda3::SaveAllTorches(
493 rom_, static_cast<int>(rooms_.size()),
494 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
495 if (!status.ok()) {
496 LOG_ERROR("DungeonEditorV2", "Failed to save torches: %s",
497 status.message().data());
498 return status;
499 }
500 }
501
502 if (flags.kSavePits) {
503 auto status = zelda3::SaveAllPits(
505 if (!status.ok()) {
506 LOG_ERROR("DungeonEditorV2", "Failed to save pits: %s",
507 status.message().data());
508 return status;
509 }
510 }
511
512 if (flags.kSaveBlocks) {
513 auto status = zelda3::SaveAllBlocks(
514 rom_, static_cast<int>(rooms_.size()),
515 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
516 if (!status.ok()) {
517 LOG_ERROR("DungeonEditorV2", "Failed to save blocks: %s",
518 status.message().data());
519 return status;
520 }
521 }
522
523 if (flags.kSaveCollision) {
524 auto status = zelda3::SaveAllCollision(
525 rom_, static_cast<int>(rooms_.size()),
526 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
527 if (!status.ok()) {
528 LOG_ERROR("DungeonEditorV2", "Failed to save collision: %s",
529 status.message().data());
530 return status;
531 }
532 }
533
534 if (flags.kSaveWaterFillZones) {
535 auto status = SaveWaterFillZones(rom_, rooms_);
536 if (!status.ok()) {
537 LOG_ERROR("DungeonEditorV2", "Failed to save water fill zones: %s",
538 status.message().data());
539 return status;
540 }
541 }
542
543 if (flags.kSaveChests) {
544 auto status = zelda3::SaveAllChests(
545 rom_, static_cast<int>(rooms_.size()),
546 [this](int room_id) { return rooms_.GetIfMaterialized(room_id); });
547 if (!status.ok()) {
548 LOG_ERROR("DungeonEditorV2", "Failed to save chests: %s",
549 status.message().data());
550 return status;
551 }
552 }
553
554 if (flags.kSavePotItems) {
555 auto status =
556 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_);
557 if (!status.ok()) {
558 LOG_ERROR("DungeonEditorV2", "Failed to save pot items: %s",
559 status.message().data());
560 return status;
561 }
562 }
563
564 if (flags.kSaveEntrances) {
566 if (!status.ok()) {
567 LOG_ERROR("DungeonEditorV2", "Failed to save dungeon entrances: %s",
568 status.message().data());
569 return status;
570 }
572 if (!status.ok()) {
573 LOG_ERROR("DungeonEditorV2", "Failed to save dungeon spawn points: %s",
574 status.message().data());
575 return status;
576 }
577 }
578
579 return absl::OkStatus();
580}
581
582std::vector<std::pair<uint32_t, uint32_t>> DungeonEditorV2::CollectWriteRanges()
583 const {
584 std::vector<std::pair<uint32_t, uint32_t>> ranges;
585
586 if (!rom_ || !rom_->is_loaded()) {
587 return ranges;
588 }
589
590 const auto& flags = core::FeatureFlags::get().dungeon;
591 const auto& rom_data = rom_->vector();
592
593 auto& palette_manager = gfx::PaletteManager::Get();
594 if (flags.kSavePalettes) {
595 auto palette_ranges =
596 palette_manager.GetModifiedColorWriteRanges(game_data_);
597 ranges.insert(ranges.end(), palette_ranges.begin(), palette_ranges.end());
598 }
599
600 if (flags.kSaveEntrances) {
601 auto entrance_ranges =
603 ranges.insert(ranges.end(), entrance_ranges.begin(), entrance_ranges.end());
604 auto spawn_ranges =
606 ranges.insert(ranges.end(), spawn_ranges.begin(), spawn_ranges.end());
607 }
608
609 auto append_declared_cow_ranges = [&](core::DungeonStreamType stream_type,
610 bool stream_dirty, int room_id) {
611 if (!stream_dirty || dependencies_.project == nullptr ||
613 return;
614 }
615 const auto* layout =
617 stream_type);
618 if (layout == nullptr ||
619 layout->strategy != core::DungeonWriteStrategy::kCopyOnWrite ||
620 room_id < 0 ||
621 static_cast<uint32_t>(room_id) >= layout->pointer_count) {
622 return;
623 }
624
625 for (const auto& range : layout->allocation_regions) {
626 ranges.emplace_back(yaze::SnesToPc(range.start),
627 yaze::SnesToPc(range.end));
628 }
629 const uint32_t pointer_width =
631 : 2u;
632 const uint32_t pointer_slot =
633 yaze::SnesToPc(layout->pointer_table) +
634 static_cast<uint32_t>(room_id) * pointer_width;
635 ranges.emplace_back(pointer_slot, pointer_slot + pointer_width);
636 if (stream_type == core::DungeonStreamType::kObjects) {
637 const uint32_t door_slot =
638 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
639 ranges.emplace_back(door_slot, door_slot + 3u);
640 }
641 };
642
643 // Oracle of Secrets: the water fill table lives in a reserved tail region.
644 // Include it in write-range reporting whenever we have dirty water fill data,
645 // even if no rooms are currently loaded (SaveWaterFillZones() is room-indexed
646 // and independent of room loading state).
647 if (flags.kSaveWaterFillZones &&
648 zelda3::kWaterFillTableEnd <= static_cast<int>(rom_data.size())) {
649 bool any_dirty = false;
650 rooms_.ForEachMaterialized([&](int, const zelda3::Room& room) {
651 if (room.water_fill_dirty()) {
652 any_dirty = true;
653 }
654 });
655 if (any_dirty) {
656 ranges.emplace_back(zelda3::kWaterFillTableStart,
658 }
659 }
660
661 // Custom collision writes update the pointer table and append blobs into the
662 // expanded collision region. SaveAllCollision() is room-indexed, so include
663 // these ranges whenever any room has dirty custom collision data.
664 if (flags.kSaveCollision) {
665 const int ptrs_size = zelda3::kNumberOfRooms * 3;
666 const bool has_ptr_table =
668 static_cast<int>(rom_data.size()));
669 const bool has_data_region = (zelda3::kCustomCollisionDataSoftEnd <=
670 static_cast<int>(rom_data.size()));
671 if (has_ptr_table && has_data_region) {
672 bool any_dirty = false;
673 rooms_.ForEachMaterialized([&](int, const zelda3::Room& room) {
674 if (room.custom_collision_dirty()) {
675 any_dirty = true;
676 }
677 });
678 if (any_dirty) {
679 ranges.emplace_back(zelda3::kCustomCollisionRoomPointers,
681 ranges.emplace_back(zelda3::kCustomCollisionDataPosition,
683 }
684 }
685 }
686
687 // Chests use one global fixed-capacity table. A dirty room may change the
688 // runtime byte length and any slot inside the live pointer target, so report
689 // both complete write ranges before serializers clear dirty state.
690 if (flags.kSaveChests) {
691 auto chest_ranges = CollectDirtyChestWriteRanges(rom_, rooms_);
692 if (chest_ranges.ok()) {
693 ranges.insert(ranges.end(), chest_ranges->begin(), chest_ranges->end());
694 } else {
695 LOG_WARN("DungeonEditorV2", "Could not predict chest write ranges: %s",
696 chest_ranges.status().message().data());
697 }
698 }
699
700 rooms_.ForEachLoaded([&](int room_id, const zelda3::Room& room) {
701 room_id = room.id();
702
703 // Header range
704 if (flags.kSaveRoomHeaders && room.header_dirty()) {
705 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
706 int header_ptr_table =
707 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
708 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
709 rom_data[zelda3::kRoomHeaderPointer];
710 header_ptr_table = yaze::SnesToPc(header_ptr_table);
711 int table_offset = header_ptr_table + (room_id * 2);
712
713 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
714 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
715 (rom_data[table_offset + 1] << 8) |
716 rom_data[table_offset];
717 int header_location = yaze::SnesToPc(address);
718 ranges.emplace_back(header_location, header_location + 14);
719 }
720 }
721 const int message_address = zelda3::kMessagesIdDungeon + (room_id * 2);
722 if (message_address >= 0 &&
723 message_address + 1 < static_cast<int>(rom_data.size())) {
724 ranges.emplace_back(message_address, message_address + 2);
725 }
726 }
727
728 // Object range
729 if (flags.kSaveObjects && room.object_stream_dirty()) {
730 const uint32_t door_slot =
731 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
732 ranges.emplace_back(door_slot, door_slot + 3u);
733 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
734 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
735 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
737 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
738 int entry_offset = obj_ptr_table + (room_id * 3);
739
740 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
741 int tile_addr = (rom_data[entry_offset + 2] << 16) |
742 (rom_data[entry_offset + 1] << 8) |
743 rom_data[entry_offset];
744 int objects_location = yaze::SnesToPc(tile_addr);
745
746 auto encoded = room.EncodeObjects();
747 ranges.emplace_back(objects_location,
748 objects_location + encoded.size() + 2);
749 }
750 }
751 }
752
753 if (flags.kSaveObjects && room.object_stream_header_dirty() &&
754 zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
755 const uint32_t table_snes =
756 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 2])
757 << 16) |
758 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 1])
759 << 8) |
761 if ((table_snes & 0xFFFFu) >= 0x8000u) {
762 const int table_pc = yaze::SnesToPc(table_snes);
763 const int pointer_slot = table_pc + room_id * 3;
764 if (pointer_slot >= 0 &&
765 pointer_slot + 2 < static_cast<int>(rom_data.size())) {
766 const uint32_t stream_snes =
767 (static_cast<uint32_t>(rom_data[pointer_slot + 2]) << 16) |
768 (static_cast<uint32_t>(rom_data[pointer_slot + 1]) << 8) |
769 rom_data[pointer_slot];
770 if ((stream_snes & 0xFFFFu) >= 0x8000u) {
771 const uint32_t stream_pc = yaze::SnesToPc(stream_snes);
772 if (static_cast<uint64_t>(stream_pc) + 2u <= rom_data.size()) {
773 ranges.emplace_back(stream_pc, stream_pc + 2u);
774 }
775 }
776 }
777 }
778 }
779
780 // Sprite stream (sort byte + encoded sprite records/terminator).
781 if (flags.kSaveSprites && room.sprites_dirty() &&
782 zelda3::kRoomsSpritePointer + 1 < static_cast<int>(rom_data.size())) {
783 const uint32_t table_snes =
784 0x090000u |
785 (static_cast<uint32_t>(rom_data[zelda3::kRoomsSpritePointer + 1])
786 << 8) |
788 const int table_pc = yaze::SnesToPc(table_snes);
789 const int pointer_slot = table_pc + room_id * 2;
790 if (pointer_slot >= 0 &&
791 pointer_slot + 1 < static_cast<int>(rom_data.size())) {
792 const uint16_t pointer =
793 (static_cast<uint16_t>(rom_data[pointer_slot + 1]) << 8) |
794 rom_data[pointer_slot];
795 if (pointer >= 0x8000) {
796 const uint32_t stream_pc = yaze::SnesToPc(0x090000u | pointer);
797 ranges.emplace_back(stream_pc,
798 stream_pc + room.EncodeSprites().size() + 1);
799 }
800 }
801 }
802
803 append_declared_cow_ranges(
805 flags.kSaveObjects &&
807 room_id);
808 append_declared_cow_ranges(core::DungeonStreamType::kSprites,
809 flags.kSaveSprites && room.sprites_dirty(),
810 room_id);
811 });
812
813 // Pot items are saved after room-local streams but still participate in the
814 // same whole-ROM transaction and manifest preflight.
815 if (flags.kSavePotItems) {
816 auto pot_ranges =
817 CollectDirtyPotItemWriteRanges(dependencies_.project, rom_, rooms_);
818 ranges.insert(ranges.end(), pot_ranges.begin(), pot_ranges.end());
819 }
820
821 return ranges;
822}
823
824absl::Status DungeonEditorV2::SaveRoom(int room_id) {
825 return RunWithSaveTransaction([this, room_id]() -> absl::Status {
826 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
827 return absl::InvalidArgumentError("Invalid room ID");
828 }
829
830 const auto& flags = core::FeatureFlags::get().dungeon;
831 if (flags.kSaveEntrances) {
832 RETURN_IF_ERROR(ValidateDungeonEntranceSavePreflight(
833 dependencies_.project, rom_, entrances_, spawn_points_,
834 absl::StrFormat(
835 "dungeon entrances and spawn points (Apply Room 0x%03X)",
836 room_id),
837 dependencies_.toast_manager));
838 }
839 if (flags.kSaveChests) {
840 RETURN_IF_ERROR(ValidateChestManifestConflicts(
841 dependencies_.project, rom_, rooms_,
842 absl::StrFormat("chest table for room 0x%03X", room_id),
843 dependencies_.toast_manager));
844 }
845 if (flags.kSavePotItems) {
846 RETURN_IF_ERROR(ValidatePotItemManifestConflicts(
847 dependencies_.project, rom_, rooms_,
848 absl::StrFormat("pot items for room 0x%03X", room_id),
849 dependencies_.toast_manager));
850 }
851 auto& palette_manager = gfx::PaletteManager::Get();
852 if (flags.kSavePalettes && palette_manager.HasUnsavedChanges(game_data_)) {
853 if (!palette_manager.IsManaging(game_data_)) {
854 return absl::FailedPreconditionError(
855 "Cannot save dungeon palettes from a different ROM session");
856 }
857 auto status = palette_manager.SaveAllToRom();
858 if (!status.ok()) {
859 LOG_ERROR("DungeonEditorV2", "Failed to save palette changes: %s",
860 status.message().data());
861 return status;
862 }
863 }
864 if (flags.kSaveObjects || flags.kSaveSprites || flags.kSaveRoomHeaders) {
865 RETURN_IF_ERROR(SaveRoomData(room_id));
866 }
867
868 if (flags.kSaveTorches) {
870 rom_, static_cast<int>(rooms_.size()),
871 [this](int id) { return rooms_.GetIfMaterialized(id); }));
872 }
873 if (flags.kSavePits) {
875 rom_, game_data_ ? &game_data_->pit_damage_table : nullptr));
876 }
877 if (flags.kSaveBlocks) {
879 rom_, static_cast<int>(rooms_.size()),
880 [this](int id) { return rooms_.GetIfMaterialized(id); }));
881 }
882 if (flags.kSaveCollision) {
884 rom_, static_cast<int>(rooms_.size()),
885 [this](int id) { return rooms_.GetIfMaterialized(id); }));
886 }
887 if (flags.kSaveWaterFillZones) {
888 RETURN_IF_ERROR(SaveWaterFillZones(rom_, rooms_));
889 }
890 if (flags.kSaveChests) {
892 rom_, static_cast<int>(rooms_.size()),
893 [this](int id) { return rooms_.GetIfMaterialized(id); }));
894 }
895 if (flags.kSavePotItems) {
897 SaveAllPotItemsForProject(dependencies_.project, rom_, rooms_));
898 }
899 if (flags.kSaveEntrances) {
902 }
903
904 return absl::OkStatus();
905 });
906}
907
908absl::Status DungeonEditorV2::SaveRoomData(int room_id) {
909 if (!rom_ || !rom_->is_loaded()) {
910 return absl::FailedPreconditionError("ROM not loaded");
911 }
912 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size())) {
913 return absl::InvalidArgumentError("Invalid room ID");
914 }
915
916 auto& room = rooms_[room_id];
917 if (!room.IsLoaded()) {
918 return absl::OkStatus();
919 }
920
921 // ROM safety: validate loaded room content before writing any bytes.
922 {
923 zelda3::DungeonValidator validator;
924 const auto result = validator.ValidateRoom(room);
925 for (const auto& w : result.warnings) {
926 LOG_WARN("DungeonEditorV2", "Room 0x%03X validation warning: %s", room_id,
927 w.c_str());
928 }
929 if (!result.is_valid) {
930 for (const auto& e : result.errors) {
931 LOG_ERROR("DungeonEditorV2", "Room 0x%03X validation error: %s",
932 room_id, e.c_str());
933 }
934 if (dependencies_.toast_manager) {
935 dependencies_.toast_manager->Show(
936 absl::StrFormat(
937 "Save blocked: room 0x%03X failed validation (%zu error(s))",
938 room_id, result.errors.size()),
939 ToastType::kError);
940 }
941 return absl::FailedPreconditionError(
942 absl::StrFormat("Room 0x%03X failed validation", room_id));
943 }
944 }
945
946 const auto& flags = core::FeatureFlags::get().dungeon;
947
948 // A present copy-on-write layout is an explicit capability grant from the
949 // project manifest. Without it, Room retains the Wave 1 fail-closed behavior
950 // for shared or over-capacity streams.
951 std::optional<zelda3::DungeonStreamLayout> object_cow_layout;
952 std::optional<zelda3::DungeonStreamLayout> sprite_cow_layout;
953 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
954 const auto& manifest = dependencies_.project->hack_manifest;
955 auto load_cow_layout =
956 [&](core::DungeonStreamType stream_type, bool stream_dirty,
957 std::optional<zelda3::DungeonStreamLayout>* out) -> absl::Status {
958 if (!stream_dirty) {
959 return absl::OkStatus();
960 }
961 const auto* manifest_layout =
962 manifest.GetDungeonStreamLayout(stream_type);
963 if (manifest_layout == nullptr ||
964 manifest_layout->strategy !=
966 return absl::OkStatus();
967 }
969 zelda3::DungeonStreamLayout allocator_layout,
970 core::ToDungeonStreamAllocatorLayout(stream_type, *manifest_layout));
971 *out = std::move(allocator_layout);
972 return absl::OkStatus();
973 };
974
975 RETURN_IF_ERROR(load_cow_layout(
977 flags.kSaveObjects &&
978 (room.object_stream_dirty() || room.object_stream_header_dirty()),
979 &object_cow_layout));
981 flags.kSaveSprites && room.sprites_dirty(),
982 &sprite_cow_layout));
983 }
984
985 // HACK MANIFEST VALIDATION
986 if (dependencies_.project && dependencies_.project->hack_manifest.loaded()) {
987 std::vector<std::pair<uint32_t, uint32_t>> ranges;
988 const auto& manifest = dependencies_.project->hack_manifest;
989 const auto& rom_data = rom_->vector();
990
991 // 1. Validate Header Range
992 if (flags.kSaveRoomHeaders && room.header_dirty()) {
993 if (zelda3::kRoomHeaderPointer + 2 < static_cast<int>(rom_data.size())) {
994 int header_ptr_table =
995 (rom_data[zelda3::kRoomHeaderPointer + 2] << 16) |
996 (rom_data[zelda3::kRoomHeaderPointer + 1] << 8) |
998 header_ptr_table = yaze::SnesToPc(header_ptr_table);
999 int table_offset = header_ptr_table + (room_id * 2);
1000
1001 if (table_offset + 1 < static_cast<int>(rom_data.size())) {
1002 int address = (rom_data[zelda3::kRoomHeaderPointerBank] << 16) |
1003 (rom_data[table_offset + 1] << 8) |
1004 rom_data[table_offset];
1005 int header_location = yaze::SnesToPc(address);
1006 ranges.emplace_back(header_location, header_location + 14);
1007 }
1008 }
1009 const int message_address = zelda3::kMessagesIdDungeon + (room_id * 2);
1010 if (message_address >= 0 &&
1011 message_address + 1 < static_cast<int>(rom_data.size())) {
1012 ranges.emplace_back(message_address, message_address + 2);
1013 }
1014 }
1015
1016 // 2. Validate Object Range
1017 if (flags.kSaveObjects && room.object_stream_dirty()) {
1018 const uint32_t door_slot =
1019 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
1020 ranges.emplace_back(door_slot, door_slot + 3u);
1021 if (zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1022 int obj_ptr_table = (rom_data[zelda3::kRoomObjectPointer + 2] << 16) |
1023 (rom_data[zelda3::kRoomObjectPointer + 1] << 8) |
1025 obj_ptr_table = yaze::SnesToPc(obj_ptr_table);
1026 int entry_offset = obj_ptr_table + (room_id * 3);
1027
1028 if (entry_offset + 2 < static_cast<int>(rom_data.size())) {
1029 int tile_addr = (rom_data[entry_offset + 2] << 16) |
1030 (rom_data[entry_offset + 1] << 8) |
1031 rom_data[entry_offset];
1032 int objects_location = yaze::SnesToPc(tile_addr);
1033
1034 // Estimate size based on current encoding
1035 // Note: we check the *target* location (where we will write)
1036 // The EncodeObjects() size is what we *will* write.
1037 // We add 2 bytes for the size/header that SaveObjects writes.
1038 auto encoded = room.EncodeObjects();
1039 ranges.emplace_back(objects_location,
1040 objects_location + encoded.size() + 2);
1041 }
1042 }
1043 }
1044
1045 if (flags.kSaveObjects && room.object_stream_header_dirty() &&
1046 zelda3::kRoomObjectPointer + 2 < static_cast<int>(rom_data.size())) {
1047 const uint32_t table_snes =
1048 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 2])
1049 << 16) |
1050 (static_cast<uint32_t>(rom_data[zelda3::kRoomObjectPointer + 1])
1051 << 8) |
1053 if ((table_snes & 0xFFFFu) >= 0x8000u) {
1054 const int table_pc = yaze::SnesToPc(table_snes);
1055 const int pointer_slot = table_pc + room_id * 3;
1056 if (pointer_slot >= 0 &&
1057 pointer_slot + 2 < static_cast<int>(rom_data.size())) {
1058 const uint32_t stream_snes =
1059 (static_cast<uint32_t>(rom_data[pointer_slot + 2]) << 16) |
1060 (static_cast<uint32_t>(rom_data[pointer_slot + 1]) << 8) |
1061 rom_data[pointer_slot];
1062 if ((stream_snes & 0xFFFFu) >= 0x8000u) {
1063 const uint32_t stream_pc = yaze::SnesToPc(stream_snes);
1064 if (static_cast<uint64_t>(stream_pc) + 2u <= rom_data.size()) {
1065 ranges.emplace_back(stream_pc, stream_pc + 2u);
1066 }
1067 }
1068 }
1069 }
1070 }
1071
1072 // 3. Validate the current sprite stream for the in-place fast path.
1073 if (flags.kSaveSprites && room.sprites_dirty() &&
1074 zelda3::kRoomsSpritePointer + 1 < static_cast<int>(rom_data.size())) {
1075 const uint32_t table_snes =
1076 0x090000u |
1077 (static_cast<uint32_t>(rom_data[zelda3::kRoomsSpritePointer + 1])
1078 << 8) |
1080 const int table_pc = yaze::SnesToPc(table_snes);
1081 const int pointer_slot = table_pc + room_id * 2;
1082 if (pointer_slot >= 0 &&
1083 pointer_slot + 1 < static_cast<int>(rom_data.size())) {
1084 const uint16_t pointer =
1085 (static_cast<uint16_t>(rom_data[pointer_slot + 1]) << 8) |
1086 rom_data[pointer_slot];
1087 if (pointer >= 0x8000) {
1088 const uint32_t stream_pc = yaze::SnesToPc(0x090000u | pointer);
1089 ranges.emplace_back(stream_pc,
1090 stream_pc + room.EncodeSprites().size() + 1);
1091 }
1092 }
1093 }
1094
1095 // A COW fallback may write anywhere in its explicitly declared allocator
1096 // arena, followed by the selected room's pointer slot(s). Validate those
1097 // possible writes before Room mutates the ROM. This is intentionally
1098 // conservative; in-place saves still remain a subset of the allowed set.
1099 auto append_cow_ranges = [&](const zelda3::DungeonStreamLayout& layout) {
1100 for (const auto& range : layout.allocation_ranges) {
1101 ranges.emplace_back(range.begin, range.end);
1102 }
1103 const uint32_t pointer_width =
1104 layout.pointer_encoding == zelda3::DungeonPointerEncoding::kLong24
1105 ? 3u
1106 : 2u;
1107 const uint32_t pointer_slot =
1108 layout.pointer_table_pc +
1109 static_cast<uint32_t>(room_id) * pointer_width;
1110 ranges.emplace_back(pointer_slot, pointer_slot + pointer_width);
1111 if (layout.kind == zelda3::DungeonStreamKind::kObject) {
1112 const uint32_t door_slot =
1113 zelda3::kDoorPointers + static_cast<uint32_t>(room_id) * 3u;
1114 ranges.emplace_back(door_slot, door_slot + 3u);
1115 }
1116 };
1117 if (object_cow_layout.has_value()) {
1118 append_cow_ranges(*object_cow_layout);
1119 }
1120 if (sprite_cow_layout.has_value()) {
1121 append_cow_ranges(*sprite_cow_layout);
1122 }
1123
1124 // `ranges` are PC offsets (ROM file offsets). The hack manifest is in SNES
1125 // address space (LoROM), so convert before analysis.
1126 const auto write_policy = dependencies_.project->rom_metadata.write_policy;
1128 manifest, write_policy, ranges, absl::StrFormat("room 0x%03X", room_id),
1129 "DungeonEditorV2", dependencies_.toast_manager));
1130 }
1131
1132 if (flags.kSaveObjects && room.object_stream_dirty()) {
1133 auto status = room.SaveObjects(
1134 object_cow_layout.has_value() ? &*object_cow_layout : nullptr);
1135 if (!status.ok()) {
1136 LOG_ERROR("DungeonEditorV2", "Failed to save room objects: %s",
1137 status.message().data());
1138 return status;
1139 }
1140 }
1141
1142 if (flags.kSaveObjects && room.object_stream_header_dirty()) {
1143 auto status = room.SaveObjectStreamHeader(
1144 object_cow_layout.has_value() ? &*object_cow_layout : nullptr);
1145 if (!status.ok()) {
1146 LOG_ERROR("DungeonEditorV2",
1147 "Failed to save room object-stream header: %s",
1148 status.message().data());
1149 return status;
1150 }
1151 }
1152
1153 if (flags.kSaveSprites && room.sprites_dirty()) {
1154 auto status = room.SaveSprites(
1155 sprite_cow_layout.has_value() ? &*sprite_cow_layout : nullptr);
1156 if (!status.ok()) {
1157 LOG_ERROR("DungeonEditorV2", "Failed to save room sprites: %s",
1158 status.message().data());
1159 return status;
1160 }
1161 }
1162
1163 if (flags.kSaveRoomHeaders && room.header_dirty()) {
1164 auto status = room.SaveRoomHeader();
1165 if (!status.ok()) {
1166 LOG_ERROR("DungeonEditorV2", "Failed to save room header: %s",
1167 status.message().data());
1168 return status;
1169 }
1170 }
1171
1172 return absl::OkStatus();
1173}
1174
1175void DungeonEditorV2::SaveAllRooms() {
1176 auto status = RunWithSaveTransaction([this]() { return Save(); });
1177 if (status.ok()) {
1178 if (dependencies_.toast_manager) {
1179 dependencies_.toast_manager->Show("Applied loaded rooms to ROM buffer",
1180 ToastType::kSuccess);
1181 }
1182 } else {
1183 LOG_ERROR("DungeonEditorV2", "SaveAllRooms failed: %s",
1184 status.message().data());
1185 if (dependencies_.toast_manager) {
1186 dependencies_.toast_manager->Show(
1187 absl::StrFormat("Save all failed: %s", status.message()),
1188 ToastType::kError);
1189 }
1190 }
1191}
1192
1193} // 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
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:573
bool custom_collision_dirty() const
Definition room.h:522
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:527
int WaterFillTileCount() const
Definition room.h:568
bool pot_items_dirty() const
Definition room.h:368
bool object_stream_dirty() const
Definition room.h:441
bool sprites_dirty() const
Definition room.h:255
bool header_dirty() const
Definition room.h:584
bool object_stream_header_dirty() const
Definition room.h:443
bool has_water_fill_zone() const
Definition room.h:529
bool chests_dirty() const
Definition room.h:262
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:1795
void ClearWaterFillDirty()
Definition room.h:581
std::vector< uint8_t > EncodeSprites() const
Definition room.cc:1883
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
Definition room.h:599
bool water_fill_dirty() const
Definition room.h:580
const std::vector< PotItem > & GetPotItems() const
Definition room.h:366
uint8_t water_fill_sram_bit_mask() const
Definition room.h:570
int id() const
Definition room.h:899
#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)
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 ValidateChestManifestConflicts(const project::YazeProject *project, Rom *rom, const DungeonRoomStore &rooms, absl::string_view save_scope, ToastManager *toast_manager)
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)
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)
absl::Status SaveAllChests(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:3681
constexpr int kDoorPointers
constexpr int kWaterFillTableEnd
constexpr int kCustomCollisionDataSoftEnd
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:3806
std::vector< DungeonEntranceWriteRange > CollectDirtyRegularDungeonEntranceWriteRanges(const std::array< RoomEntrance, kNumDungeonEntranceSlots > &entrances)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetChestTableWriteRanges(const Rom *rom)
Definition room.cc:3425
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:2902
constexpr int kRoomsSpritePointer
absl::Status SaveAllPits(Rom *rom)
Definition room.cc:2916
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:3060
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
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:3413
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
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:192