21 : plan_(std::move(plan)), restore_(std::move(restore)) {}
22 absl::Status
Undo()
override {
return restore_(plan_.before); }
23 absl::Status
Redo()
override {
return restore_(plan_.after); }
25 return absl::StrFormat(
"Replace room %03X contents", plan_.target_room_id);
28 size_t bytes =
sizeof(plan_);
29 for (
const auto* document : {&plan_.before, &plan_.after}) {
30 const auto& c = document->contents;
57 return absl::FailedPreconditionError(
58 "A changed room domain has saving disabled; enable it before "
61 return absl::OkStatus();
69 constexpr uint16_t kMaxTorchTableBytes = 0x120;
70 if (length > kMaxTorchTableBytes || (length & 1) != 0 ||
72 return absl::FailedPreconditionError(
73 "Torch table has an invalid length or lies outside the ROM");
76 bool found_room =
false;
77 while (offset < length) {
82 if (segment_room == 0xFFFF)
85 return absl::FailedPreconditionError(
86 "Torch table has an invalid room ID");
88 if (segment_room == room_id) {
90 return absl::FailedPreconditionError(absl::StrFormat(
91 "Torch table contains duplicate segments for room %03X", room_id));
95 bool terminated =
false;
96 while (offset < length) {
100 if (entry == 0xFFFF) {
106 return absl::FailedPreconditionError(absl::StrFormat(
107 "Torch segment for room %03X has no end marker", segment_room));
110 return absl::OkStatus();
116 return absl::OkStatus();
118 std::map<int, uint8_t> masks;
119 for (
const auto& zone : saved)
120 masks[zone.room_id] = zone.sram_bit_mask;
128 for (
const auto& [
id, mask] : masks) {
133 if (prior && !(prior & (prior - 1)) && !(used & prior)) {
135 return absl::OkStatus();
137 for (
unsigned bit = 1; bit <= 128; bit <<= 1) {
140 return absl::OkStatus();
143 return absl::ResourceExhaustedError(
144 "No free water-fill save-state bit remains");
151 return absl::InvalidArgumentError(
"Choose a room in the current ROM");
154 if (existing && existing->rom() !=
rom_) {
155 return absl::FailedPreconditionError(
"Room belongs to another ROM");
158 ((!existing->AreTorchesLoaded() && existing->torches_dirty()) ||
159 (!existing->AreBlocksLoaded() && existing->blocks_dirty()))) {
160 return absl::FailedPreconditionError(
161 "Finish loading the room's torch/block edits before copying it");
165 if (!existing || !existing->AreTorchesLoaded()) {
168 if (!existing || !existing->AreSpritesLoaded()) {
169 if (existing && existing->sprites_dirty()) {
170 return absl::FailedPreconditionError(
171 "Finish loading the room's sprite edits first");
175 if (!existing || !existing->ArePotItemsLoaded()) {
176 if (existing && existing->pot_items_dirty()) {
177 return absl::FailedPreconditionError(
178 "Finish loading the room's pot-item edits first");
182 if ((!existing || !existing->AreObjectsLoaded()) &&
190 if (!room.AreSpritesLoaded()) {
191 return absl::FailedPreconditionError(
"Room sprites did not load");
193 if (!room.AreChestsLoaded()) {
194 if (room.chests_dirty()) {
195 return absl::FailedPreconditionError(
196 "Finish loading the room's chest edits first");
200 room.EnsurePotItemsLoaded();
201 if (!room.water_fill_dirty() && !room.has_water_fill_zone()) {
203 for (
const auto& zone : zones) {
204 if (zone.room_id != room_id)
206 room.set_water_fill_sram_bit_mask(zone.sram_bit_mask);
207 for (
const auto offset : zone.fill_offsets)
208 room.SetWaterFillTile(offset % 64, offset / 64,
true);
209 room.ClearWaterFillDirty();
213 if (!room.AreTorchesLoaded())
215 if (!room.AreBlocksLoaded())
217 if (!room.AreChestsLoaded() || !room.ArePotItemsLoaded() ||
218 !room.AreTorchesLoaded() || !room.AreBlocksLoaded()) {
219 return absl::FailedPreconditionError(
220 "Room data did not fully load; repair the source before copying it");
222 return absl::OkStatus();
232 int target_room_id,
int source_room_id,
const std::string& json,
234 if (source_room_id < -1) {
235 return absl::InvalidArgumentError(
"Invalid source room");
237 if (source_room_id == target_room_id) {
238 return absl::InvalidArgumentError(
"Choose a different source room");
241 if (source_room_id >= 0) {
251 if (source_room_id >= 0) {
252 plan.clone_source_room_id = source_room_id;
253 plan.clone_source_before = std::move(source);
256 if (!viewer || viewer->current_room_id() != target_room_id)
258 plan.before.contents.selected_objects =
259 viewer->object_interaction().GetSelectedObjectIndices();
260 plan.before.contents.selected_entities = viewer->object_interaction()
261 .entity_coordinator()
262 .SelectedEntitiesForEdit();
264 room_viewers_.ForEach([&](
int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
265 capture_selection(viewer.get());
282 auto scratch_game = std::make_unique<zelda3::GameData>();
283 auto shadow = std::make_unique<DungeonEditorV2>(&scratch);
286 shadow->SetGameData(scratch_game.get());
289 auto& copy = shadow->rooms_[id];
296 for (
auto&
object : copy.GetTileObjects())
298 copy.SetLoaded(original.
IsLoaded());
305 for (
const auto& zone : saved_zones) {
306 if (shadow->rooms_.GetIfMaterialized(zone.room_id))
308 auto& copy = shadow->rooms_[zone.room_id];
309 copy.set_water_fill_sram_bit_mask(zone.sram_bit_mask);
310 for (
const auto offset : zone.fill_offsets)
311 copy.SetWaterFillTile(offset % 64, offset / 64,
true);
312 copy.ClearWaterFillDirty();
317 const auto prior_dirty = target.CaptureSaveDirtySnapshot();
318 target.ClearSaveDirtyState();
319 target.ClearCustomCollisionDirty();
320 target.ClearWaterFillDirty();
323 const auto changed_dirty = target.CaptureSaveDirtySnapshot();
324 target.RestoreSaveDirtySnapshot(prior_dirty);
325 auto combined = prior_dirty;
326 combined.header |= changed_dirty.header;
327 combined.object_stream |= changed_dirty.object_stream;
328 combined.object_stream_header |= changed_dirty.object_stream_header;
329 combined.sprites |= changed_dirty.sprites;
330 combined.chests |= changed_dirty.chests;
331 combined.pot_items |= changed_dirty.pot_items;
332 combined.torches |= changed_dirty.torches;
333 combined.blocks |= changed_dirty.blocks;
334 combined.custom_collision |= changed_dirty.custom_collision;
335 combined.water_fill |= changed_dirty.water_fill;
337 combined.block_load_orders = changed_dirty.block_load_orders;
338 target.RestoreSaveDirtySnapshot(combined);
340 target.header_dirty()) {
344 if (changed_dirty.water_fill &&
346 return absl::FailedPreconditionError(
347 "Water-fill state bits changed; preview again");
349 bool changed_other_mask =
false;
351 const auto* copy = shadow->rooms_.GetIfMaterialized(
id);
354 changed_other_mask =
true;
356 if (changed_other_mask) {
357 return absl::FailedPreconditionError(
358 "Replacement would reassign another room's water-fill state bit");
363 std::vector<std::pair<uint32_t, uint32_t>> ranges;
365 const auto& after = scratch.
vector();
366 if (before.size() != after.size()) {
367 return absl::FailedPreconditionError(
368 "Room replacement cannot resize the ROM");
370 for (
size_t i = 0; i < before.size();) {
371 if (before[i] == after[i]) {
375 const size_t start = i++;
376 while (i < before.size() && before[i] != after[i])
378 ranges.emplace_back(start, i);
383 "room clone/import",
"DungeonEditorV2",
nullptr));
385 return absl::OkStatus();
390 auto validate = [&]() -> absl::Status {
392 if (plan.
rom !=
rom_ || !target || target->rom() !=
rom_ ||
396 return absl::FailedPreconditionError(
397 "Room changed since preview; preview again");
404 return absl::FailedPreconditionError(
405 "Source room changed; preview again");
412 return absl::InvalidArgumentError(
413 "Room preview does not match its copy options");
415 return absl::OkStatus();
419 return absl::OkStatus();
432 return absl::FailedPreconditionError(
433 "Room history belongs to another ROM");
434 return RestoreRoomTransfer(id, state);
436 return absl::OkStatus();
442 if (!room || room->rom() !=
rom_ || room->id() != room_id) {
443 return absl::FailedPreconditionError(
444 "Room replacement target is not loaded");
447 for (
auto&
object : room->GetTileObjects())
452 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
absl::StatusOr< uint16_t > ReadWord(int offset) const
bool loaded() const
Check if the manifest has been loaded.
absl::Status PreflightRoomTransfer(const DungeonRoomTransferPlan &plan)
absl::Status ApplyRoomTransfer(const DungeonRoomTransferPlan &plan)
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
void RefreshSelectionEditViews(int room_id, const DungeonSelectionEditState *selection)
void FinalizePendingUndoActions()
static absl::Status ValidateRoomHeaderOwnership(const Rom &rom, int room_id)
absl::Status EnsureRoomTransferLoaded(int room_id)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
static bool IsValidRoomId(int room_id)
void RefreshRoomMetadataViews(int room_id)
absl::StatusOr< std::string > ExportRoomDocument(int room_id)
absl::Status EnsureConnectionRoomLoaded(int room_id)
absl::StatusOr< DungeonRoomTransferPlan > PreviewRoomTransfer(int target_room_id, int source_room_id, const std::string &json, const DungeonRoomTransferOptions &options)
zelda3::GameData * game_data_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
bool undo_restore_triggered_ping_
absl::Status RestoreRoomTransfer(int room_id, const DungeonRoomDocument &document)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
void ForEachMaterialized(Fn &&fn)
UndoManager undo_manager_
EditorDependencies dependencies_
Abstract base for all undoable actions (Command pattern)
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo() override
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
std::function< absl::Status(const DungeonRoomDocument &)> Restore
absl::Status Undo() override
RoomTransferAction(DungeonRoomTransferPlan plan, Restore restore)
DungeonRoomTransferPlan plan_
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
void EnsureSpritesLoaded()
bool custom_collision_dirty() const
bool pot_items_dirty() const
bool object_stream_dirty() const
bool sprites_dirty() const
bool header_dirty() const
bool object_stream_header_dirty() const
bool has_water_fill_zone() const
bool chests_dirty() const
bool blocks_dirty() const
bool torches_dirty() const
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
bool water_fill_dirty() const
bool AreTorchesLoaded() const
bool AreBlocksLoaded() const
uint8_t water_fill_sram_bit_mask() const
struct chest_data chest_data
Legacy chest data structure.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::Status CheckTransferSaveFlags(const zelda3::Room &changed)
absl::Status ValidateTransferTorchSource(const Rom &rom, int room_id)
absl::Status AssignTransferredWaterMask(Rom &rom, const DungeonRoomStore &rooms, DungeonRoomTransferPlan &plan)
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::StatusOr< DungeonRoomTransferPlan > PlanDungeonRoomTransfer(const zelda3::Room &target, const DungeonRoomDocument &source, DungeonRoomTransferOptions options)
bool SameDungeonRoomDocument(const DungeonRoomDocument &a, const DungeonRoomDocument &b)
absl::StatusOr< std::string > SerializeDungeonRoomDocument(const DungeonRoomDocument &document)
constexpr uint16_t kTransferWater
void ApplyDungeonRoomDocument(zelda3::Room &room, const DungeonRoomDocument &document)
absl::StatusOr< DungeonRoomDocument > ParseDungeonRoomDocument(const std::string &json)
DungeonRoomDocument CaptureDungeonRoomDocument(const zelda3::Room &room)
constexpr int kTorchesLengthPointer
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
absl::StatusOr< DungeonStreamRecord > ReadDungeonPotItemStream(const Rom &rom, int room_id)
constexpr int kNumberOfRooms
constexpr int kCustomCollisionRoomPointers
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)
absl::StatusOr< DungeonStreamRecord > ReadDungeonSpriteStream(const Rom &rom, int room_id)
#define RETURN_IF_ERROR(expr)
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
DungeonSelectionEditState contents
zelda3::WaterFillZoneMap water
DungeonRoomTransferOptions options
std::optional< DungeonRoomDocument > clone_source_before
DungeonRoomDocument after
DungeonRoomDocument before
project::YazeProject * project
core::HackManifest hack_manifest
Represents a door in a dungeon room.