yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2_undo.cc
Go to the documentation of this file.
1// Undo/redo, clipboard, and room-state restore implementation for
2// DungeonEditorV2. Split out of dungeon_editor_v2.cc to keep that
3// translation unit within the editor source-size guardrail. Class
4// declaration lives in dungeon_editor_v2.h.
5
6#include <algorithm>
7#include <cctype>
8#include <cstdio>
9#include <exception>
10#include <iterator>
11#include <memory>
12#include <string>
13#include <unordered_set>
14#include <utility>
15#include <vector>
16#include "absl/status/status.h"
17#include "absl/strings/str_format.h"
18#include "absl/types/span.h"
55#include "app/gui/core/icons.h"
57#include "core/features.h"
58#include "core/project.h"
59#include "dungeon_editor_v2.h"
60#include "imgui/imgui.h"
61#include "rom/snes.h"
62#include "util/log.h"
63#include "util/macro.h"
69#include "zelda3/dungeon/room.h"
73
74namespace yaze::editor {
75
76namespace {
77
78class RoomGraphicsSheetAction final : public UndoAction {
79 public:
80 using Apply = std::function<absl::Status(uint8_t, uint8_t)>;
81 RoomGraphicsSheetAction(int group, int slot, uint8_t before, uint8_t after,
82 Apply apply)
83 : group_(group),
84 slot_(slot),
85 before_(before),
86 after_(after),
87 apply_(std::move(apply)) {}
88 absl::Status Undo() override { return apply_(after_, before_); }
89 absl::Status Redo() override { return apply_(before_, after_); }
90 std::string Description() const override {
91 return absl::StrFormat("Room graphics group %02X slot %d", group_, slot_);
92 }
93 size_t MemoryUsage() const override { return sizeof(*this); }
94
95 private:
96 int group_;
97 int slot_;
98 uint8_t before_;
99 uint8_t after_;
101};
102
103} // namespace
104
105absl::Status DungeonEditorV2::EditRoomGraphicsSheet(int room_id, int slot,
106 int sheet_id) {
107 if (!game_data_ || !rom_ || !rom_->is_loaded()) {
108 return absl::FailedPreconditionError("No loaded graphics session");
109 }
110 auto* room = rooms_.GetIfMaterialized(room_id);
111 if (!room || slot < 3 || slot > 6 || sheet_id < 0 ||
113 return absl::InvalidArgumentError("Choose a roomset slot (3-6) and sheet");
114 }
115 if (sheet_id != 0 && (!game_data_->sheet_store.HasSheet(sheet_id) ||
118 return absl::InvalidArgumentError(
119 "Room backgrounds require a loaded 3bpp sheet");
120 }
121 const int group = room->blockset();
122 if (group >= static_cast<int>(game_data_->room_blockset_ids.size())) {
123 return absl::OutOfRangeError("Room graphics group is unavailable");
124 }
125 const uint8_t before = game_data_->room_blockset_ids[group][slot - 3];
126 const auto after = static_cast<uint8_t>(sheet_id);
127 if (before == after) {
128 return absl::OkStatus();
129 }
131 auto apply = [this, owner = game_data_, source_rom = rom_, group, slot](
132 uint8_t expected, uint8_t value) -> absl::Status {
133 if (game_data_ != owner || rom_ != source_rom) {
134 return absl::FailedPreconditionError(
135 "Graphics assignment belongs to another session");
136 }
137 auto& assigned = game_data_->room_blockset_ids[group][slot - 3];
138 if (assigned != expected) {
139 return absl::FailedPreconditionError(
140 "Graphics assignment changed elsewhere; Undo was not applied");
141 }
142 assigned = value;
143 rooms_.ForEachMaterialized([group](int, zelda3::Room& affected) {
144 if (affected.blockset() == group) {
145 affected.MarkGraphicsDirty();
146 }
147 });
148 rom_->set_dirty(true);
149 return absl::OkStatus();
150 };
151 RETURN_IF_ERROR(apply(before, after));
152 undo_manager_.Push(std::make_unique<RoomGraphicsSheetAction>(
153 group, slot, before, after, std::move(apply)));
154 return absl::OkStatus();
155}
156
158 // Closing history alone leaves a handler's drag/paint gesture running, so its
159 // next movement could bypass the before-snapshot. End gestures first.
160 room_viewers_.ForEach([](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
161 if (viewer) {
162 viewer->CancelMinecartGesture();
163 viewer->object_interaction().HandleMouseRelease();
164 }
165 });
166 for (auto* viewer :
168 if (viewer) {
169 viewer->CancelMinecartGesture();
170 viewer->object_interaction().HandleMouseRelease();
171 }
172 }
173 if (pending_undo_.room_id >= 0) {
175 }
178 }
181 }
184}
185
186absl::Status DungeonEditorV2::Undo() {
188 const std::string description = undo_manager_.GetUndoDescription();
190 auto status = undo_manager_.Undo();
191 if (status.ok()) {
193 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
194 viewer->TriggerChangePing();
195 }
196 }
199 description.empty() ? "Undid last dungeon edit"
200 : absl::StrFormat("Undid: %s", description),
201 ToastType::kInfo, 2.0f);
202 }
203 }
205 return status;
206}
207
208absl::Status DungeonEditorV2::Redo() {
210 const std::string description = undo_manager_.GetRedoDescription();
212 auto status = undo_manager_.Redo();
213 if (status.ok()) {
215 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
216 viewer->TriggerChangePing();
217 }
218 }
221 description.empty() ? "Redid last dungeon edit"
222 : absl::StrFormat("Redid: %s", description),
223 ToastType::kInfo, 2.0f);
224 }
225 }
227 return status;
228}
229
230absl::Status DungeonEditorV2::Cut() {
231 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
232 RETURN_IF_ERROR(viewer->object_interaction().HandleCopySelected());
233 return viewer->object_interaction().HandleDeleteSelected();
234 }
235 return absl::OkStatus();
236}
237
238absl::Status DungeonEditorV2::Copy() {
239 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
240 return viewer->object_interaction().HandleCopySelected();
241 }
242 return absl::OkStatus();
243}
244
246 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
247 return viewer->object_interaction().HandlePasteObjects();
248 }
249 return absl::OkStatus();
250}
251
252namespace {
253
255 return domain == MutationDomain::kDoors ||
257}
258
260 return static_cast<size_t>(domain) -
261 static_cast<size_t>(MutationDomain::kDoors);
262}
263
265 const DungeonEntitySnapshot& b) {
266 return a.sprites == b.sprites && a.doors.size() == b.doors.size() &&
267 std::equal(a.doors.begin(), a.doors.end(), b.doors.begin(),
268 [](const auto& x, const auto& y) {
269 return x.position == y.position && x.type == y.type &&
270 x.direction == y.direction && x.byte1 == y.byte1 &&
271 x.byte2 == y.byte2;
272 }) &&
273 a.items.size() == b.items.size() &&
274 std::equal(a.items.begin(), a.items.end(), b.items.begin(),
275 [](const auto& x, const auto& y) {
276 return x.position == y.position && x.item == y.item;
277 });
278}
279
280std::vector<SelectedEntity> ValidEntitySelection(
281 const zelda3::Room& room, const std::vector<SelectedEntity>& selected) {
282 std::vector<SelectedEntity> valid;
283 for (const auto entity : selected) {
284 if ((entity.type == EntityType::Door &&
285 entity.index < room.GetDoors().size()) ||
286 (entity.type == EntityType::Sprite &&
287 entity.index < room.GetSprites().size()) ||
288 (entity.type == EntityType::Item &&
289 entity.index < room.GetPotItems().size())) {
290 valid.push_back(entity);
291 }
292 }
293 return valid;
294}
295
296} // namespace
297
302 [this](int id) { return ExportRoomDocument(id); },
303 [this](int target, int source, const std::string& json,
304 const DungeonRoomTransferOptions& options) {
305 return PreviewRoomTransfer(target, source, json, options);
306 },
307 [this](const DungeonRoomTransferPlan& plan) {
308 return ApplyRoomTransfer(plan);
309 });
311 [this](int room_id, size_t index) {
312 const auto* room = rooms_.GetIfLoaded(room_id);
313 if (!room || room->rom() != rom_ || index >= room->GetDoors().size()) {
314 return;
315 }
316 const auto door = room->GetDoors()[index];
317 OnRoomSelected(room_id, true);
318 if (auto* target = GetViewerForRoom(room_id)) {
319 // Workbench navigation draws the new room on the following frame. Bind
320 // its editing context now so the exact return selection survives that draw.
321 target->object_interaction().SetCurrentRoom(&rooms_, room_id);
322 target->object_interaction().ClearSelection();
323 target->object_interaction().SelectEntity(EntityType::Door, index);
324 const auto [x, y, width, height] = door.GetEditorBounds();
325 target->ScrollToTile((x + width / 2) / 8, (y + height / 2) / 8);
326 target->TriggerCanvasPingRect(x, y, width, height);
327 }
328 });
330 [this](const DungeonConnectionRequest& request) {
331 return PreviewDoorConnection(request);
332 },
333 [this](const DungeonConnectionPlan& plan) {
334 return ApplyDoorConnection(plan);
335 });
337 [this](const DungeonSelectionEditPlan& plan, bool continuous) {
338 return CommitSelectionEdit(plan, continuous);
339 },
340 [this]() { FinalizeSelectionUndoAction(); },
341 [this](const absl::Status& status) {
344 absl::StrFormat("Edit not applied: %s", status.message()),
345 ToastType::kWarning, 4.0f);
346 }
347 });
349 [this](int room_id, const RoomMetadataEdit& edit) {
350 return EditRoomMetadata(room_id, edit);
351 });
353 [this](const std::vector<RoomMetadataRequest>& requests) {
354 return EditRoomMetadataBatch(requests);
355 });
356 viewer->SetChestEditCallback(
357 [this](int room_id, size_t index, uint8_t item_id, bool big_chest) {
358 return EditChest(room_id, index, item_id, big_chest);
359 });
360 viewer->SetChestDeleteCallback([this](int room_id, size_t index) {
361 return DeleteChest(room_id, index);
362 });
363 viewer->object_interaction()
365 .tile_handler()
367 [this](int room_id, const std::vector<zelda3::RoomObject>& objects,
368 const std::vector<chest_data>& chests) {
369 return PreflightObjectMutation(room_id, objects, chests);
370 });
371 viewer->object_interaction()
373 .tile_handler()
375 [this]() { FinalizePendingUndoActions(); });
376 viewer->object_interaction()
378 .tile_handler()
379 .SetMutationErrorCallback([this](const absl::Status& status) {
382 absl::StrFormat("Edit not applied: %s", status.message()),
383 ToastType::kWarning, 4.0f);
384 }
385 });
386 auto& door_handler =
388 door_handler.SetDoorMutationPreflight(
389 [this](int id, const std::vector<zelda3::Room::Door>& doors) {
390 return PreflightDoorMutation(id, doors);
391 });
392 door_handler.SetMutationErrorCallback([this](const absl::Status& status) {
395 absl::StrFormat("Edit not applied: %s", status.message()),
396 ToastType::kWarning, 4.0f);
397 }
398 });
399 auto& sprite_handler =
401 sprite_handler.SetSpriteMutationPreflight(
402 [this](int id, const std::vector<zelda3::Sprite>& sprites) {
403 return PreflightSpriteMutation(id, sprites);
404 });
405 sprite_handler.SetMutationErrorCallback([this](const absl::Status& status) {
408 absl::StrFormat("Edit not applied: %s", status.message()),
409 ToastType::kWarning, 4.0f);
410 }
411 });
412 auto& item_handler =
414 item_handler.SetItemPlacementPreflight(
415 [this](int id, const std::vector<zelda3::PotItem>& items) {
416 return PreflightPotItemPlacement(id, items);
417 });
418 item_handler.SetMutationErrorCallback([this](const absl::Status& status) {
421 absl::StrFormat("Edit not applied: %s", status.message()),
422 ToastType::kWarning, 4.0f);
423 }
424 });
426 [this](const std::vector<zelda3::CustomCollisionRoomEntry>& entries) {
427 return ApplyCustomCollisionImportBatch(entries);
428 });
429 // The interaction context is the mutation source of truth. A retained viewer
430 // can change rooms, so do not capture the room ID from its creation time.
431 viewer->object_interaction().SetMutationCallback([this, viewer]() {
433 const auto* ctx = viewer->object_interaction()
435 .tile_handler()
436 .context();
437 const int rid = ctx ? ctx->current_room_id : -1;
438 if (!IsValidRoomId(rid)) {
439 return;
440 }
441 const auto domain = ctx->last_mutation_domain;
442 if (domain == MutationDomain::kTileObjects) {
444 } else if (IsEntityMutation(domain)) {
445 BeginEntityUndoSnapshot(rid, domain);
446 } else if (domain == MutationDomain::kCustomCollision) {
448 } else if (domain == MutationDomain::kWaterFill) {
450 }
451 });
452 viewer->object_interaction().SetCacheInvalidationCallback([this, viewer]() {
453 const auto* ctx = viewer->object_interaction()
455 .tile_handler()
456 .context();
457 const int rid = ctx ? ctx->current_room_id : -1;
458 if (!IsValidRoomId(rid)) {
459 return;
460 }
461 auto& interaction = viewer->object_interaction();
462 const auto domain = ctx->last_invalidation_domain;
463 const auto mode = interaction.mode_manager().GetMode();
464 if (domain == MutationDomain::kTileObjects) {
465 rooms_[rid].MarkObjectsDirty();
466 rooms_[rid].RenderRoomGraphics();
469 }
470 } else if (IsEntityMutation(domain)) {
471 // Entity drags publish their invalidation only at release. Property,
472 // placement and keyboard edits each publish a single completion.
473 // Doors, pot indicators and sprite key drops all contribute pixels to
474 // the object buffers. Save dirtiness alone does not refresh those pixels.
475 auto& room = rooms_[rid];
476 room.MarkObjectsDirty();
477 if (room.rom() == rom_ && rom_ && rom_->is_loaded()) {
478 room.RenderRoomGraphics();
479 }
480 FinalizeEntityUndoAction(rid, domain);
481 } else if (domain == MutationDomain::kCustomCollision) {
482 if (!(mode == InteractionMode::PaintCollision &&
483 interaction.mode_manager().GetModeState().is_painting)) {
485 }
486 } else if (domain == MutationDomain::kWaterFill) {
487 if (!(mode == InteractionMode::PaintWaterFill &&
488 interaction.mode_manager().GetModeState().is_painting)) {
490 }
491 }
492 });
493}
494
496 int room_id, MutationDomain domain) {
498 const auto& room = rooms_[room_id];
499 if (domain == MutationDomain::kDoors) {
500 state.doors = room.GetDoors();
501 } else if (domain == MutationDomain::kSprites) {
502 for (const auto& sprite : room.GetSprites()) {
503 state.sprites.push_back({sprite.id(), sprite.x(), sprite.y(),
504 sprite.subtype(), sprite.layer(),
505 sprite.key_drop(), sprite.deleted()});
506 }
507 } else if (domain == MutationDomain::kItems) {
508 state.items = room.GetPotItems();
509 }
510 if (auto* viewer = GetViewerForRoom(room_id);
511 viewer &&
512 (!IsWorkbenchWorkflowEnabled() || viewer->object_interaction()
513 .entity_coordinator()
514 .tile_handler()
515 .context()
516 ->current_room_id == room_id)) {
517 auto& interaction = viewer->object_interaction();
518 state.entities = interaction.entity_coordinator().GetSelectedEntities();
519 if (state.entities.empty() && interaction.HasEntitySelection()) {
520 state.entities.push_back(interaction.GetSelectedEntity());
521 }
522 state.entities = ValidEntitySelection(room, state.entities);
523 state.objects = interaction.GetSelectedObjectIndices();
524 }
525 return state;
526}
527
529 MutationDomain domain) {
530 if (!IsValidRoomId(room_id) || !IsEntityMutation(domain)) {
531 return;
532 }
533 auto& pending = pending_entity_undo_[EntityDomainIndex(domain)];
534 if (pending.room_id >= 0) {
535 FinalizeEntityUndoAction(pending.room_id, domain);
536 }
537 pending.room_id = room_id;
538 pending.before = CaptureRoomEntities(room_id, domain);
539}
540
542 MutationDomain domain) {
543 if (!IsValidRoomId(room_id) || !IsEntityMutation(domain)) {
544 return;
545 }
546 auto& pending = pending_entity_undo_[EntityDomainIndex(domain)];
547 if (pending.room_id != room_id) {
548 return;
549 }
550 auto after = CaptureRoomEntities(room_id, domain);
551 if (SameEntityData(pending.before, after)) {
552 pending = {};
553 return;
554 }
555 // Deletion shifts vector indices. Do not let an old selected index silently
556 // pick the next entity; undo restores the original selection explicitly.
557 const bool removed = after.doors.size() < pending.before.doors.size() ||
558 after.sprites.size() < pending.before.sprites.size() ||
559 after.items.size() < pending.before.items.size();
560 if (removed) {
561 const auto type = domain == MutationDomain::kDoors ? EntityType::Door
562 : domain == MutationDomain::kSprites ? EntityType::Sprite
563 : EntityType::Item;
564 std::erase_if(after.entities,
565 [type](const auto& entity) { return entity.type == type; });
566 if (auto* viewer = GetViewerForRoom(room_id);
567 viewer && viewer->object_interaction()
568 .entity_coordinator()
569 .tile_handler()
570 .context()
571 ->current_room_id == room_id) {
572 viewer->object_interaction().entity_coordinator().SetSelectedEntities(
573 after.entities);
574 }
575 }
576 undo_manager_.Push(std::make_unique<DungeonEntitiesAction>(
577 room_id, domain, std::move(pending.before), std::move(after),
578 [this](int rid, MutationDomain restored_domain,
579 const DungeonEntitySnapshot& snapshot) {
580 return RestoreRoomEntities(rid, restored_domain, snapshot);
581 }));
582 pending = {};
583}
584
586 for (const auto domain : {MutationDomain::kDoors, MutationDomain::kSprites,
588 const auto& pending = pending_entity_undo_[EntityDomainIndex(domain)];
589 if (pending.room_id >= 0) {
590 FinalizeEntityUndoAction(pending.room_id, domain);
591 }
592 }
593}
594
596 int room_id, MutationDomain domain, const DungeonEntitySnapshot& snapshot) {
597 if (!IsValidRoomId(room_id) || !IsEntityMutation(domain)) {
598 return absl::InvalidArgumentError("Invalid dungeon entity undo target");
599 }
600 auto* room = rooms_.GetIfMaterialized(room_id);
601 if (!room) {
602 return absl::FailedPreconditionError(
603 "Dungeon entity undo room is not loaded");
604 }
605 if (domain == MutationDomain::kDoors) {
606 room->GetDoors() = snapshot.doors;
607 room->MarkObjectStreamDirty();
608 } else if (domain == MutationDomain::kSprites) {
609 std::vector<zelda3::Sprite> restored;
610 restored.reserve(snapshot.sprites.size());
611 for (const auto& sprite : snapshot.sprites) {
612 restored.emplace_back(sprite.id, sprite.x, sprite.y, sprite.subtype,
613 sprite.layer);
614 restored.back().set_key_drop(sprite.key_drop);
615 restored.back().set_deleted(sprite.deleted);
616 }
617 room->GetSprites() = std::move(restored);
618 room->MarkSpritesDirty();
619 } else {
620 room->GetPotItems() = snapshot.items;
621 room->MarkPotItemsDirty();
622 }
623 room->MarkObjectsDirty();
624 if (room->rom() == rom_ && rom_ && rom_->is_loaded()) {
625 room->RenderRoomGraphics();
626 }
627 // An offscreen restore must not flash the currently displayed room.
629 if (auto* viewer = GetViewerForRoom(room_id)) {
630 if (viewer->object_interaction()
631 .entity_coordinator()
632 .tile_handler()
633 .context()
634 ->current_room_id == room_id) {
635 auto& interaction = viewer->object_interaction();
636 interaction.CancelPlacement();
637 std::vector<size_t> objects;
638 for (size_t index : snapshot.objects) {
639 if (index < room->GetTileObjects().size()) {
640 objects.push_back(index);
641 }
642 }
643 interaction.SetSelectedObjects(objects);
644 interaction.entity_coordinator().SetSelectedEntities(
645 ValidEntitySelection(*room, snapshot.entities));
646 viewer->TriggerChangePing();
648 }
649 }
650 return absl::OkStatus();
651}
652
654 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
655 return;
656
657 // Detect leaked undo snapshots (double-Begin without Finalize).
658 if (has_pending_undo_) {
659 LOG_ERROR("DungeonEditor",
660 "BeginUndoSnapshot called twice without FinalizeUndoAction. "
661 "Previous snapshot for room %d is being leaked. Finalizing now.",
663 // Auto-finalize the leaked snapshot to prevent silent state loss.
664 if (pending_undo_.room_id >= 0) {
666 }
667 }
668
669 pending_undo_.room_id = room_id;
670 pending_undo_.before_objects = rooms_[room_id].GetTileObjects();
671 pending_undo_.before_chests = rooms_[room_id].GetChests();
672 pending_undo_.dirty_before = rooms_[room_id].CaptureSaveDirtySnapshot();
674 if (auto* viewer = GetViewerForRoom(room_id);
675 viewer &&
676 (!IsWorkbenchWorkflowEnabled() || viewer->current_room_id() == room_id)) {
678 viewer->object_interaction().GetSelectedObjectIndices();
679 }
680 has_pending_undo_ = true;
681}
682
684 if (pending_undo_.room_id < 0 || pending_undo_.room_id != room_id)
685 return;
686 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
687 return;
688
689 auto after_objects = rooms_[room_id].GetTileObjects();
690 std::vector<size_t> after_selection;
691 if (auto* viewer = GetViewerForRoom(room_id);
692 viewer &&
693 (!IsWorkbenchWorkflowEnabled() || viewer->current_room_id() == room_id)) {
694 after_selection = viewer->object_interaction().GetSelectedObjectIndices();
695 }
696
697 auto action = std::make_unique<DungeonObjectsAction>(
699 after_objects, std::move(after_selection), pending_undo_.before_chests,
700 rooms_[room_id].GetChests(),
701 [this](int rid, const std::vector<zelda3::RoomObject>& objects,
702 const std::vector<size_t>& selected_indices,
703 const std::vector<chest_data>& chests) {
704 return RestoreRoomObjects(rid, objects, selected_indices, chests);
705 });
706 auto paired = PairMinecartObjectCollision(
707 room_id, pending_undo_.before_objects, after_objects, std::move(action));
708 if (paired.ok()) {
709 undo_manager_.Push(std::move(*paired));
710 } else {
711 // The normal handlers preflight before mutation. Retain atomic rollback
712 // for callers that finish an edit after its collision context changed.
713 const auto restored = RestoreRoomObjects(
716 if (restored.ok()) {
717 rooms_[room_id].RestoreSaveDirtySnapshot(pending_undo_.dirty_before);
718 }
721 std::string(paired.status().message()));
722 }
723 LOG_WARN("DungeonEditorV2", "Track collision edit refused: %s",
724 paired.status().message());
725 }
728 }
729
734 has_pending_undo_ = false;
735}
736
738 int room_id, const std::vector<zelda3::RoomObject>& objects,
739 const std::vector<size_t>& selected_indices,
740 const std::vector<chest_data>& chests) {
741 auto* loaded_room = rooms_.GetIfLoaded(room_id);
742 if (!loaded_room || loaded_room->rom() != rom_) {
743 return absl::FailedPreconditionError("Room objects are not loaded");
744 }
745 auto& room = *loaded_room;
746 if (room.GetChests().size() != chests.size() ||
747 !std::equal(
748 chests.begin(), chests.end(), room.GetChests().begin(),
749 [](auto a, auto b) { return a.id == b.id && a.size == b.size; })) {
750 room.GetChests() = chests;
751 room.MarkChestsDirty();
752 }
753 const auto previous_objects = room.GetTileObjects();
754 room.SetTileObjects(objects);
755 room.RenderRoomGraphics();
756 if (auto* viewer = GetViewerForRoom(room_id);
757 viewer &&
758 (!IsWorkbenchWorkflowEnabled() || viewer->current_room_id() == room_id)) {
759 std::vector<size_t> valid_selection;
760 for (size_t index : selected_indices) {
761 if (index < objects.size()) {
762 valid_selection.push_back(index);
763 }
764 }
765 viewer->object_interaction().SetSelectedObjects(valid_selection);
766 viewer->TriggerObjectChangePing(previous_objects, objects);
768 }
771 }
773 return absl::OkStatus();
774}
775
777 // A transport can target any open graph. Clear existing viewers only;
778 // refreshing derived connections must not create viewers or navigate.
779 room_viewers_.ForEach([](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
780 if (viewer) {
781 viewer->InvalidateConnectedRoomGraph();
782 }
783 });
784 for (auto* viewer :
786 if (viewer) {
787 viewer->InvalidateConnectedRoomGraph();
788 }
789 }
790}
791
793 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
794 return;
795
798 }
799
801 pending_collision_undo_.before = rooms_[room_id].custom_collision();
802}
803
806 pending_collision_undo_.room_id != room_id) {
807 return;
808 }
809 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
810 return;
811
812 auto after = rooms_[room_id].custom_collision();
813 if (pending_collision_undo_.before == after) {
816 return;
817 }
818
819 auto action = std::make_unique<DungeonCustomCollisionAction>(
820 room_id, std::move(pending_collision_undo_.before), std::move(after),
821 [this](int rid, const zelda3::CustomCollisionMap& map) {
822 RestoreRoomCustomCollision(rid, map);
823 });
824 undo_manager_.Push(std::move(action));
828 }
829
832}
833
835 int room_id, const zelda3::CustomCollisionMap& map) {
836 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
837 return;
838
839 auto& room = rooms_[room_id];
840 room.custom_collision() = map;
841 room.MarkCustomCollisionDirty();
845 }
846}
847
848namespace {
849
851 return map.has_data || map.HasOverrides();
852}
853
855 const zelda3::CustomCollisionMap& rhs) {
856 return lhs == rhs;
857}
858
860 zelda3::Room* room = nullptr;
862 bool dirty = false;
863};
864
866 public:
867 explicit CollisionBatchRollback(std::vector<CollisionRollbackEntry> entries)
868 : entries_(std::move(entries)) {}
869
871 if (committed_) {
872 return;
873 }
874 for (const auto& entry : entries_) {
875 entry.room->custom_collision() = entry.map;
876 if (entry.dirty) {
877 entry.room->MarkCustomCollisionDirty();
878 } else {
879 entry.room->ClearCustomCollisionDirty();
880 }
881 }
882 }
883
884 void Commit() { committed_ = true; }
885
886 private:
887 std::vector<CollisionRollbackEntry> entries_;
888 bool committed_ = false;
889};
890
891} // namespace
892
894 int room_id, const zelda3::CustomCollisionMap& expected,
895 const MinecartCollisionStamp& stamp) {
896 // Ending a gesture can generate rail collision and can retire the canvas
897 // tool's original-map storage. Copy both inputs before that boundary.
898 const auto expected_snapshot = expected;
899 const auto stamp_snapshot = stamp;
901 auto* room = rooms_.GetIfMaterialized(room_id);
902 if (!room || !rom_ ||
904 return absl::FailedPreconditionError(
905 "Load a room with expanded collision support");
906 }
907 if (!CollisionMapsEqual(room->custom_collision(), expected_snapshot)) {
908 return absl::FailedPreconditionError(
909 "Collision changed; preview the stamp again");
910 }
911 if (stamp_snapshot.source &&
912 (stamp_snapshot.size == 1 || stamp_snapshot.size == 2)) {
913 const int x = stamp_snapshot.source->x;
914 const int y = stamp_snapshot.source->y;
915 if (x >= 0 && y >= 0 && x <= 64 - stamp_snapshot.size &&
916 y <= 64 - stamp_snapshot.size) {
917 for (const auto& sprite : room->GetSprites()) {
918 if (sprite.deleted() || sprite.layer() != 0 || sprite.x() < 0 ||
919 sprite.y() < 0 || sprite.x() > 31 || sprite.y() > 31)
920 continue;
921 const int actor_x = sprite.x() * 2;
922 const int actor_y = sprite.y() * 2;
923 if (actor_x < x || actor_y < y || actor_x >= x + stamp_snapshot.size ||
924 actor_y >= y + stamp_snapshot.size)
925 continue;
926 const size_t offset = actor_y * 64 + actor_x;
927 if (!expected_snapshot.HasOverride(offset))
928 continue;
929 const auto value = expected_snapshot.tiles[offset];
930 if (sprite.id() == 0xA3 && value >= 0xB7 && value <= 0xBA)
931 return absl::FailedPreconditionError(
932 "Move this cart and its stop with the Cart Starts marker");
933 if (sprite.id() == 0xB0 && value >= 0xD0 && value <= 0xD3)
934 return absl::FailedPreconditionError(
935 "Move this switch track with Tracks > Nodes");
936 }
937 }
938 }
939 ASSIGN_OR_RETURN(auto after, PreviewMinecartCollisionStamp(expected_snapshot,
940 stamp_snapshot));
941 if (CollisionMapsEqual(expected_snapshot, after))
942 return absl::OkStatus();
943 // Finalizing another tool may invalidate overlays, so keep value snapshots.
944 const auto before = expected_snapshot;
945 auto action = std::make_unique<DungeonCustomCollisionAction>(
946 room_id, before, after,
947 [this](int rid, const zelda3::CustomCollisionMap& map) {
949 });
950 CollisionBatchRollback rollback(
951 {{room, before, room->custom_collision_dirty()}});
952 room->custom_collision() = after;
953 room->MarkCustomCollisionDirty();
954 try {
955 undo_manager_.Push(std::move(action));
956 } catch (const std::exception& error) {
957 return absl::InternalError(
958 absl::StrFormat("Could not record rail undo: %s", error.what()));
959 }
960 rollback.Commit();
964 return absl::OkStatus();
965}
966
968 const std::vector<zelda3::TrackCollisionResult>& preview,
969 const zelda3::GeneratorOptions& options) {
970 if (preview.empty()) {
971 return absl::InvalidArgumentError("Minecart collision preview is empty");
972 }
973
974 std::unordered_set<int> seen_room_ids;
975 std::vector<DungeonCustomCollisionSnapshot> before;
976 std::vector<DungeonCustomCollisionSnapshot> after;
977 std::vector<CollisionRollbackEntry> rollback_entries;
978 before.reserve(preview.size());
979 after.reserve(preview.size());
980 rollback_entries.reserve(preview.size());
981
982 // Validate the complete batch before changing rooms or undo history.
983 for (const auto& expected : preview) {
984 const int room_id = expected.room_id;
985 if (!IsValidRoomId(room_id)) {
986 return absl::OutOfRangeError(absl::StrFormat(
987 "Minecart collision room 0x%03X is out of range", room_id));
988 }
989 if (!seen_room_ids.insert(room_id).second) {
990 return absl::InvalidArgumentError(absl::StrFormat(
991 "Minecart collision preview contains duplicate room 0x%03X",
992 room_id));
993 }
994
995 zelda3::Room* room = rooms_.GetIfMaterialized(room_id);
996 if (room == nullptr) {
997 return absl::FailedPreconditionError(absl::StrFormat(
998 "Minecart collision room 0x%03X is not loaded", room_id));
999 }
1000 if (HasAnyCustomCollision(room->custom_collision())) {
1001 return absl::FailedPreconditionError(absl::StrFormat(
1002 "Room 0x%03X already has custom collision; generation will not "
1003 "replace it",
1004 room_id));
1005 }
1006
1007 ASSIGN_OR_RETURN(auto current,
1008 zelda3::GenerateTrackCollision(room, options));
1009 current.room_id = room_id;
1010 if (!current.collision_map.has_data || current.tiles_generated <= 0) {
1011 return absl::FailedPreconditionError(absl::StrFormat(
1012 "Room 0x%03X no longer contains supported minecart track pieces",
1013 room_id));
1014 }
1015 if (!CollisionMapsEqual(current.collision_map, expected.collision_map) ||
1016 current.tiles_generated != expected.tiles_generated ||
1017 current.stop_count != expected.stop_count ||
1018 current.corner_count != expected.corner_count ||
1019 current.switch_count != expected.switch_count) {
1020 return absl::FailedPreconditionError(absl::StrFormat(
1021 "Minecart collision preview for room 0x%03X is stale; preview "
1022 "again before applying",
1023 room_id));
1024 }
1025
1026 before.push_back({room_id, room->custom_collision()});
1027 after.push_back({room_id, current.collision_map});
1028 rollback_entries.push_back(
1029 {room, room->custom_collision(), room->custom_collision_dirty()});
1030 }
1031
1035 std::any_of(pending_entity_undo_.begin(), pending_entity_undo_.end(),
1036 [](const auto& pending) { return pending.room_id >= 0; })) {
1037 return absl::FailedPreconditionError(
1038 "Finish the current dungeon edit before applying minecart collision");
1039 }
1040
1041 auto action = std::make_unique<DungeonCustomCollisionBatchAction>(
1042 before, after,
1043 [this](const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
1044 return RestoreRoomCustomCollisionBatch(snapshots);
1045 });
1046
1047 CollisionBatchRollback rollback(std::move(rollback_entries));
1048 for (const auto& snapshot : after) {
1049 auto* room = rooms_.GetIfMaterialized(snapshot.room_id);
1050 room->custom_collision() = snapshot.map;
1051 room->MarkCustomCollisionDirty();
1052 }
1053 try {
1054 undo_manager_.Push(std::move(action));
1055 } catch (const std::exception& error) {
1056 return absl::InternalError(absl::StrFormat(
1057 "Could not record minecart collision undo: %s", error.what()));
1058 }
1059 rollback.Commit();
1063 }
1064
1065 for (const auto& snapshot : after) {
1066 if (auto* viewer = GetViewerForRoom(snapshot.room_id)) {
1067 viewer->TriggerChangePing();
1068 }
1069 }
1070 return absl::OkStatus();
1071}
1072
1074 const std::vector<zelda3::CustomCollisionRoomEntry>& entries) {
1075 if (!rom_ || !rom_->is_loaded() ||
1077 return absl::FailedPreconditionError(
1078 "Load a room with expanded collision support");
1079 }
1081
1082 std::unordered_set<int> seen_room_ids;
1083 for (const auto& entry : entries) {
1084 if (!IsValidRoomId(entry.room_id)) {
1085 return absl::OutOfRangeError(absl::StrFormat(
1086 "Collision import room 0x%03X is out of range", entry.room_id));
1087 }
1088 if (!seen_room_ids.insert(entry.room_id).second) {
1089 return absl::InvalidArgumentError(absl::StrFormat(
1090 "Collision import contains duplicate room 0x%03X", entry.room_id));
1091 }
1092 }
1093
1094 std::vector<DungeonCustomCollisionSnapshot> before;
1095 std::vector<DungeonCustomCollisionSnapshot> after;
1096 std::vector<CollisionRollbackEntry> rollback_entries;
1097 before.reserve(entries.size());
1098 after.reserve(entries.size());
1099 rollback_entries.reserve(entries.size());
1100
1101 for (const auto& entry : entries) {
1102 const int room_id = entry.room_id;
1103 zelda3::Room& room = rooms_[room_id];
1104 if (!room.IsLoaded() && !room.custom_collision_dirty() &&
1105 !room.has_custom_collision()) {
1106 auto map_or = zelda3::LoadCustomCollisionMap(rom_, room_id);
1107 if (!map_or.ok()) {
1108 return absl::Status(map_or.status().code(),
1109 absl::StrFormat("Room 0x%03X: %s", room_id,
1110 map_or.status().message()));
1111 }
1112 room.custom_collision() = std::move(*map_or);
1113 }
1114
1115 zelda3::CustomCollisionMap imported_map;
1116 for (const auto& tile : entry.tiles) {
1117 if (tile.offset >= imported_map.tiles.size()) {
1118 continue;
1119 }
1120 imported_map.SetOverride(tile.offset, tile.value);
1121 }
1122
1123 if (CollisionMapsEqual(room.custom_collision(), imported_map)) {
1124 continue;
1125 }
1126
1127 before.push_back({room_id, room.custom_collision()});
1128 after.push_back({room_id, imported_map});
1129 rollback_entries.push_back(
1130 {&room, room.custom_collision(), room.custom_collision_dirty()});
1131 }
1132
1133 if (after.empty()) {
1134 return absl::OkStatus();
1135 }
1136
1137 auto action = std::make_unique<DungeonCustomCollisionBatchAction>(
1138 before, after,
1139 [this](const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
1140 return RestoreRoomCustomCollisionBatch(snapshots);
1141 });
1142
1143 CollisionBatchRollback rollback(std::move(rollback_entries));
1144 for (const auto& snapshot : after) {
1145 auto& room = rooms_[snapshot.room_id];
1146 room.custom_collision() = snapshot.map;
1147 room.MarkCustomCollisionDirty();
1148 }
1149 try {
1150 undo_manager_.Push(std::move(action));
1151 } catch (const std::exception& error) {
1152 return absl::InternalError(absl::StrFormat(
1153 "Could not record collision import undo: %s", error.what()));
1154 }
1155 rollback.Commit();
1159 }
1160
1161 for (const auto& snapshot : after) {
1162 if (auto* viewer = GetViewerForRoom(snapshot.room_id)) {
1163 viewer->TriggerChangePing();
1164 }
1165 }
1166 return absl::OkStatus();
1167}
1168
1170 const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
1171 std::unordered_set<int> seen_room_ids;
1172 for (const auto& snapshot : snapshots) {
1173 if (!IsValidRoomId(snapshot.room_id)) {
1174 return absl::OutOfRangeError("Collision undo room ID is out of range");
1175 }
1176 if (!seen_room_ids.insert(snapshot.room_id).second) {
1177 return absl::InvalidArgumentError(
1178 "Collision undo contains a duplicate room ID");
1179 }
1180 if (rooms_.GetIfMaterialized(snapshot.room_id) == nullptr) {
1181 return absl::FailedPreconditionError(
1182 "Collision undo target room is not loaded");
1183 }
1184 }
1185
1186 for (const auto& snapshot : snapshots) {
1187 auto& room = *rooms_.GetIfMaterialized(snapshot.room_id);
1188 room.custom_collision() = snapshot.map;
1189 room.MarkCustomCollisionDirty();
1190 if (auto* viewer = GetViewerForRoom(snapshot.room_id)) {
1191 viewer->TriggerChangePing();
1193 }
1194 }
1198 }
1199 return absl::OkStatus();
1200}
1201
1202namespace {
1203
1205 WaterFillSnapshot snap;
1207
1208 const auto& zone = room.water_fill_zone();
1209 // Preserve deterministic ordering (ascending offsets) for stable diffs.
1210 for (size_t i = 0; i < zone.tiles.size(); ++i) {
1211 if (zone.tiles[i] != 0) {
1212 snap.offsets.push_back(static_cast<uint16_t>(i));
1213 }
1214 }
1215 return snap;
1216}
1217
1218} // namespace
1219
1221 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
1222 return;
1223
1226 }
1227
1229 pending_water_fill_undo_.before = MakeWaterFillSnapshot(rooms_[room_id]);
1230}
1231
1234 pending_water_fill_undo_.room_id != room_id) {
1235 return;
1236 }
1237 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
1238 return;
1239
1240 auto after = MakeWaterFillSnapshot(rooms_[room_id]);
1241 if (pending_water_fill_undo_.before.sram_bit_mask == after.sram_bit_mask &&
1242 pending_water_fill_undo_.before.offsets == after.offsets) {
1245 return;
1246 }
1247
1248 auto action = std::make_unique<DungeonWaterFillAction>(
1249 room_id, std::move(pending_water_fill_undo_.before), std::move(after),
1250 [this](int rid, const WaterFillSnapshot& snap) {
1251 RestoreRoomWaterFill(rid, snap);
1252 });
1253 undo_manager_.Push(std::move(action));
1254
1257}
1258
1260 const WaterFillSnapshot& snap) {
1261 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
1262 return;
1263
1264 auto& room = rooms_[room_id];
1265 room.ClearWaterFillZone();
1266 room.set_water_fill_sram_bit_mask(snap.sram_bit_mask);
1267 for (uint16_t off : snap.offsets) {
1268 const int x = static_cast<int>(off % 64);
1269 const int y = static_cast<int>(off / 64);
1270 room.SetWaterFillTile(x, y, /*filled=*/true);
1271 }
1272 room.MarkWaterFillDirty();
1273}
1274
1275} // namespace yaze::editor
void set_dirty(bool dirty)
Definition rom.h:201
const auto & vector() const
Definition rom.h:217
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
InteractionContext * context() const
Get the interaction context.
void SetDoorMutationPreflight(DoorMutationPreflight callback)
void SetChestEditCallback(ChestEditCallback callback)
void SetMetadataBatchEditCallback(MetadataBatchEditCallback callback)
void SetDoorConnectionCallbacks(DoorConnectionPreviewCallback preview, DoorConnectionApplyCallback apply)
void SetRoomTransferCallbacks(RoomDocumentExportCallback export_document, RoomTransferPreviewCallback preview, RoomTransferApplyCallback apply)
DungeonObjectInteraction & object_interaction()
void SetChestDeleteCallback(ChestDeleteCallback callback)
void SetMetadataEditCallback(MetadataEditCallback callback)
void SetDoorConnectionNavigationCallback(std::function< void(int, size_t)> callback)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
absl::Status ApplyCustomCollisionImportBatch(const std::vector< zelda3::CustomCollisionRoomEntry > &entries)
absl::Status EditRoomGraphicsSheet(int room_id, int slot, int sheet_id)
void ConfigureViewerUndoHooks(DungeonCanvasViewer *viewer)
absl::Status PreflightObjectMutation(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< chest_data > &chests, bool check_streams=true)
void RestoreRoomWaterFill(int room_id, const WaterFillSnapshot &snap)
absl::Status RestoreRoomEntities(int room_id, MutationDomain domain, const DungeonEntitySnapshot &snapshot)
void BeginEntityUndoSnapshot(int room_id, MutationDomain domain)
absl::Status ApplyRoomTransfer(const DungeonRoomTransferPlan &plan)
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
DungeonEntitySnapshot CaptureRoomEntities(int room_id, MutationDomain domain)
absl::Status CommitSelectionEdit(const DungeonSelectionEditPlan &plan, bool continuous)
absl::Status PreflightPotItemPlacement(int room_id, const std::vector< zelda3::PotItem > &items)
absl::Status ApplyDoorConnection(const DungeonConnectionPlan &plan)
absl::Status EditRoomMetadataBatch(const std::vector< RoomMetadataRequest > &requests)
void OnRoomSelected(int room_id, bool request_focus=true)
absl::Status ApplyMinecartCollisionStamp(int room_id, const zelda3::CustomCollisionMap &expected, const MinecartCollisionStamp &stamp)
void FinalizeEntityUndoAction(int room_id, MutationDomain domain)
PendingCollisionUndo pending_collision_undo_
absl::Status PreflightDoorMutation(int room_id, const std::vector< zelda3::Room::Door > &doors)
ObjectSelectorContent * object_selector_panel_
absl::Status RestoreRoomObjects(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< size_t > &selected_indices, const std::vector< chest_data > &chests)
absl::Status PreflightSpriteMutation(int room_id, const std::vector< zelda3::Sprite > &sprites)
absl::Status EditRoomMetadata(int room_id, const RoomMetadataEdit &edit)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
PendingWaterFillUndo pending_water_fill_undo_
std::array< PendingEntityUndo, 3 > pending_entity_undo_
static bool IsValidRoomId(int room_id)
absl::StatusOr< std::unique_ptr< UndoAction > > PairMinecartObjectCollision(int room_id, const std::vector< zelda3::RoomObject > &before, const std::vector< zelda3::RoomObject > &after, std::unique_ptr< UndoAction > object_action)
DungeonCanvasViewer * GetViewerForRoom(int room_id)
PendingSelectionUndo pending_selection_undo_
absl::StatusOr< std::string > ExportRoomDocument(int room_id)
absl::Status RestoreRoomCustomCollisionBatch(const std::vector< DungeonCustomCollisionSnapshot > &snapshots)
absl::Status DeleteChest(int room_id, size_t index)
absl::StatusOr< DungeonRoomTransferPlan > PreviewRoomTransfer(int target_room_id, int source_room_id, const std::string &json, const DungeonRoomTransferOptions &options)
DungeonSelectionClipboard selection_clipboard_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
void RestoreRoomCustomCollision(int room_id, const zelda3::CustomCollisionMap &map)
absl::Status EditChest(int room_id, size_t index, uint8_t item_id, bool big_chest)
absl::Status ApplyMinecartCollisionBatch(const std::vector< zelda3::TrackCollisionResult > &preview, const zelda3::GeneratorOptions &options)
absl::StatusOr< DungeonConnectionPlan > PreviewDoorConnection(const DungeonConnectionRequest &request)
void SetCacheInvalidationCallback(std::function< void()> callback)
void SetSelectionEditCallbacks(std::function< absl::Status(const DungeonSelectionEditPlan &, bool)> edit, std::function< void()> finish, std::function< void(const absl::Status &)> error={})
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SetMutationCallback(std::function< void()> callback)
void SetCollisionBatchImportCallback(std::function< absl::Status(const std::vector< zelda3::CustomCollisionRoomEntry > &)> callback)
void SetSharedSelectionClipboard(DungeonSelectionClipboard *clipboard)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
EditorContext context() const
Definition editor.h:376
UndoManager undo_manager_
Definition editor.h:383
EditorDependencies dependencies_
Definition editor.h:382
EditorType type() const
Definition editor.h:355
SpriteInteractionHandler & sprite_handler()
void SetItemPlacementPreflight(ItemPlacementPreflight preflight)
void SetPlacementError(const std::string &message)
void SetSpriteMutationPreflight(SpriteMutationPreflight callback)
void SetObjectMutationPreflight(ObjectMutationPreflight callback)
void SetMutationErrorCallback(std::function< void(const absl::Status &)> callback)
void SetBeforeObjectCommandCallback(std::function< void()> callback)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo()
Redo the top action. Returns error if stack is empty.
std::string GetRedoDescription() const
Description of the action that would be redone (for UI)
std::string GetUndoDescription() const
Description of the action that would be undone (for UI)
absl::Status Undo()
Undo the top action. Returns error if stack is empty.
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
RoomGraphicsSheetAction(int group, int slot, uint8_t before, uint8_t after, Apply apply)
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
bool HasSheet(uint16_t sheet) const
True when the buffer holds all 4096 bytes of sheet.
static constexpr uint16_t kSheetCount
const CustomCollisionMap & custom_collision() const
Definition room.h:559
bool custom_collision_dirty() const
Definition room.h:596
uint8_t blockset() const
Definition room.h:1015
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:601
const std::vector< Door > & GetDoors() const
Definition room.h:413
bool IsLoaded() const
Definition room.h:970
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
bool has_custom_collision() const
Definition room.h:563
const std::vector< PotItem > & GetPotItems() const
Definition room.h:429
uint8_t water_fill_sram_bit_mask() const
Definition room.h:644
Locates graphics sheets in the ROM and writes edited sheets back.
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::vector< SelectedEntity > ValidEntitySelection(const zelda3::Room &room, const std::vector< SelectedEntity > &selected)
bool SameEntityData(const DungeonEntitySnapshot &a, const DungeonEntitySnapshot &b)
bool CollisionMapsEqual(const zelda3::CustomCollisionMap &lhs, const zelda3::CustomCollisionMap &rhs)
bool HasAnyCustomCollision(const zelda3::CustomCollisionMap &map)
WaterFillSnapshot MakeWaterFillSnapshot(const zelda3::Room &room)
Editors are the view controllers for the application.
MutationDomain
Domain/type of mutation for undo + invalidation routing.
absl::StatusOr< zelda3::CustomCollisionMap > PreviewMinecartCollisionStamp(const zelda3::CustomCollisionMap &before, const MinecartCollisionStamp &stamp)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::optional< DungeonSelectionEditPlan > plan
zelda3::Room::SaveDirtySnapshot dirty_before
std::vector< zelda3::RoomObject > before_objects
std::vector< DungeonSpriteSnapshot > sprites
std::vector< zelda3::PotItem > items
std::vector< SelectedEntity > entities
std::vector< zelda3::Room::Door > doors
std::array< uint8_t, 64 *64 > tiles
void SetOverride(size_t offset, uint8_t tile)
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98