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 <iterator>
10#include <memory>
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/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"
72
73namespace yaze::editor {
74
75absl::Status DungeonEditorV2::Undo() {
76 // Finalize any in-progress edit before undoing.
77 if (pending_undo_.room_id >= 0) {
79 }
82 }
85 }
86 const std::string description = undo_manager_.GetUndoDescription();
88 auto status = undo_manager_.Undo();
89 if (status.ok()) {
91 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
92 viewer->TriggerChangePing();
93 }
94 }
97 description.empty() ? "Undid last dungeon edit"
98 : absl::StrFormat("Undid: %s", description),
99 ToastType::kInfo, 2.0f);
100 }
101 }
103 return status;
104}
105
106absl::Status DungeonEditorV2::Redo() {
107 // Finalize any in-progress edit before redoing.
108 if (pending_undo_.room_id >= 0) {
110 }
113 }
116 }
117 const std::string description = undo_manager_.GetRedoDescription();
119 auto status = undo_manager_.Redo();
120 if (status.ok()) {
122 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
123 viewer->TriggerChangePing();
124 }
125 }
128 description.empty() ? "Redid last dungeon edit"
129 : absl::StrFormat("Redid: %s", description),
130 ToastType::kInfo, 2.0f);
131 }
132 }
134 return status;
135}
136
137absl::Status DungeonEditorV2::Cut() {
138 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
139 viewer->object_interaction().HandleCopySelected();
140 viewer->object_interaction().HandleDeleteSelected();
141 }
142 return absl::OkStatus();
143}
144
145absl::Status DungeonEditorV2::Copy() {
146 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
147 viewer->object_interaction().HandleCopySelected();
148 }
149 return absl::OkStatus();
150}
151
153 if (auto* viewer = GetViewerForRoom(current_room_id_)) {
154 viewer->object_interaction().HandlePasteObjects();
155 }
156 return absl::OkStatus();
157}
158
160 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
161 return;
162
163 // Detect leaked undo snapshots (double-Begin without Finalize).
164 if (has_pending_undo_) {
165 LOG_ERROR("DungeonEditor",
166 "BeginUndoSnapshot called twice without FinalizeUndoAction. "
167 "Previous snapshot for room %d is being leaked. Finalizing now.",
169 // Auto-finalize the leaked snapshot to prevent silent state loss.
170 if (pending_undo_.room_id >= 0) {
172 }
173 }
174
175 pending_undo_.room_id = room_id;
176 pending_undo_.before_objects = rooms_[room_id].GetTileObjects();
178 if (auto* viewer = GetViewerForRoom(room_id);
179 viewer &&
180 (!IsWorkbenchWorkflowEnabled() || viewer->current_room_id() == room_id)) {
182 viewer->object_interaction().GetSelectedObjectIndices();
183 }
184 has_pending_undo_ = true;
185}
186
188 if (pending_undo_.room_id < 0 || pending_undo_.room_id != room_id)
189 return;
190 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
191 return;
192
193 auto after_objects = rooms_[room_id].GetTileObjects();
194 std::vector<size_t> after_selection;
195 if (auto* viewer = GetViewerForRoom(room_id);
196 viewer &&
197 (!IsWorkbenchWorkflowEnabled() || viewer->current_room_id() == room_id)) {
198 after_selection = viewer->object_interaction().GetSelectedObjectIndices();
199 }
200
201 auto action = std::make_unique<DungeonObjectsAction>(
202 room_id, std::move(pending_undo_.before_objects),
203 std::move(pending_undo_.before_selection), std::move(after_objects),
204 std::move(after_selection),
205 [this](int rid, const std::vector<zelda3::RoomObject>& objects,
206 const std::vector<size_t>& selected_indices) {
207 RestoreRoomObjects(rid, objects, selected_indices);
208 });
209 undo_manager_.Push(std::move(action));
210
214 has_pending_undo_ = false;
215}
216
218 int room_id, const std::vector<zelda3::RoomObject>& objects,
219 const std::vector<size_t>& selected_indices) {
220 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
221 return;
222
223 auto& room = rooms_[room_id];
224 const auto previous_objects = room.GetTileObjects();
225 room.SetTileObjects(objects);
226 room.RenderRoomGraphics();
227 if (auto* viewer = GetViewerForRoom(room_id)) {
228 std::vector<size_t> valid_selection;
229 for (size_t index : selected_indices) {
230 if (index < objects.size()) {
231 valid_selection.push_back(index);
232 }
233 }
234 if (!IsWorkbenchWorkflowEnabled() || viewer->current_room_id() == room_id) {
235 viewer->object_interaction().SetSelectedObjects(valid_selection);
236 }
237 viewer->TriggerObjectChangePing(previous_objects, objects);
239 }
240}
241
243 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
244 return;
245
248 }
249
251 pending_collision_undo_.before = rooms_[room_id].custom_collision();
252}
253
256 pending_collision_undo_.room_id != room_id) {
257 return;
258 }
259 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
260 return;
261
262 auto after = rooms_[room_id].custom_collision();
263 if (pending_collision_undo_.before.has_data == after.has_data &&
264 pending_collision_undo_.before.tiles == after.tiles) {
267 return;
268 }
269
270 auto action = std::make_unique<DungeonCustomCollisionAction>(
271 room_id, std::move(pending_collision_undo_.before), std::move(after),
272 [this](int rid, const zelda3::CustomCollisionMap& map) {
273 RestoreRoomCustomCollision(rid, map);
274 });
275 undo_manager_.Push(std::move(action));
276
279}
280
282 int room_id, const zelda3::CustomCollisionMap& map) {
283 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
284 return;
285
286 auto& room = rooms_[room_id];
287 room.custom_collision() = map;
288 room.MarkCustomCollisionDirty();
289}
290
291namespace {
292
296
297 const auto& zone = room.water_fill_zone();
298 // Preserve deterministic ordering (ascending offsets) for stable diffs.
299 for (size_t i = 0; i < zone.tiles.size(); ++i) {
300 if (zone.tiles[i] != 0) {
301 snap.offsets.push_back(static_cast<uint16_t>(i));
302 }
303 }
304 return snap;
305}
306
307} // namespace
308
310 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
311 return;
312
315 }
316
318 pending_water_fill_undo_.before = MakeWaterFillSnapshot(rooms_[room_id]);
319}
320
324 return;
325 }
326 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
327 return;
328
329 auto after = MakeWaterFillSnapshot(rooms_[room_id]);
330 if (pending_water_fill_undo_.before.sram_bit_mask == after.sram_bit_mask &&
331 pending_water_fill_undo_.before.offsets == after.offsets) {
334 return;
335 }
336
337 auto action = std::make_unique<DungeonWaterFillAction>(
338 room_id, std::move(pending_water_fill_undo_.before), std::move(after),
339 [this](int rid, const WaterFillSnapshot& snap) {
340 RestoreRoomWaterFill(rid, snap);
341 });
342 undo_manager_.Push(std::move(action));
343
346}
347
349 const WaterFillSnapshot& snap) {
350 if (room_id < 0 || room_id >= static_cast<int>(rooms_.size()))
351 return;
352
353 auto& room = rooms_[room_id];
354 room.ClearWaterFillZone();
355 room.set_water_fill_sram_bit_mask(snap.sram_bit_mask);
356 for (uint16_t off : snap.offsets) {
357 const int x = static_cast<int>(off % 64);
358 const int y = static_cast<int>(off / 64);
359 room.SetWaterFillTile(x, y, /*filled=*/true);
360 }
361 room.MarkWaterFillDirty();
362}
363
364} // namespace yaze::editor
void RestoreRoomWaterFill(int room_id, const WaterFillSnapshot &snap)
void RestoreRoomObjects(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< size_t > &selected_indices)
PendingCollisionUndo pending_collision_undo_
PendingWaterFillUndo pending_water_fill_undo_
DungeonCanvasViewer * GetViewerForRoom(int room_id)
void RestoreRoomCustomCollision(int room_id, const zelda3::CustomCollisionMap &map)
UndoManager undo_manager_
Definition editor.h:334
EditorDependencies dependencies_
Definition editor.h:333
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
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.
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:527
uint8_t water_fill_sram_bit_mask() const
Definition room.h:570
#define LOG_ERROR(category, format,...)
Definition log.h:109
WaterFillSnapshot MakeWaterFillSnapshot(const zelda3::Room &room)
Editors are the view controllers for the application.
std::vector< zelda3::RoomObject > before_objects
std::array< uint8_t, 64 *64 > tiles