yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2_selection_edits.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <memory>
5#include <set>
6#include <span>
7#include <tuple>
8#include <utility>
9#include <vector>
10
14#include "core/features.h"
15#include "util/macro.h"
16
17namespace yaze::editor {
18namespace {
19
21 std::span<const DungeonSpriteSnapshot> before,
22 std::span<const DungeonSpriteSnapshot> after) {
23 const auto positions = [](std::span<const DungeonSpriteSnapshot> sprites) {
24 std::vector<std::tuple<int, int, int>> result;
25 for (const auto& sprite : sprites) {
26 if (sprite.id == 0xB0 && !sprite.deleted)
27 result.emplace_back(sprite.x, sprite.y, sprite.layer);
28 }
29 std::sort(result.begin(), result.end());
30 return result;
31 };
32 const auto previous = positions(before);
33 const auto next = positions(after);
34 // A multiset subset permits deletions and channel/key edits, but neither a
35 // new anchor nor a duplicate actor can be introduced by a generic transform.
36 if (!std::includes(previous.begin(), previous.end(), next.begin(),
37 next.end()))
38 return absl::FailedPreconditionError(
39 "Place and move switch tracks with Tracks > Nodes");
40 return absl::OkStatus();
41}
42
43class DungeonSelectionAction final : public UndoAction {
44 public:
45 using Restore = std::function<absl::Status(
46 const std::vector<DungeonSelectionEditPlan>&, bool)>;
47 DungeonSelectionAction(std::vector<DungeonSelectionEditPlan> plans,
48 std::string description, Restore restore)
49 : plans_(std::move(plans)),
50 description_(std::move(description)),
51 restore_(std::move(restore)) {}
52 absl::Status Undo() override { return restore_(plans_, false); }
53 absl::Status Redo() override { return restore_(plans_, true); }
54 std::string Description() const override { return description_; }
55 size_t MemoryUsage() const override {
56 auto bytes = [](const DungeonSelectionEditState& state) {
57 return state.objects.size() * sizeof(zelda3::RoomObject) +
58 state.chests.size() * sizeof(chest_data) +
59 state.doors.size() * sizeof(zelda3::Room::Door) +
60 state.sprites.size() * sizeof(DungeonSpriteSnapshot) +
61 state.items.size() * sizeof(zelda3::PotItem) +
62 state.selected_objects.size() * sizeof(size_t) +
63 state.selected_entities.size() * sizeof(SelectedEntity);
64 };
65 size_t total = description_.capacity();
66 for (const auto& plan : plans_) {
67 total += bytes(plan.before) + bytes(plan.after);
68 }
69 return total;
70 }
71
72 private:
73 std::vector<DungeonSelectionEditPlan> plans_;
74 std::string description_;
76};
77
78} // namespace
79
81 int room_id, const std::vector<zelda3::Room::Door>& doors) {
82 const auto* room = rooms_.GetIfLoaded(room_id);
83 if (!room || room->rom() != rom_) {
84 return absl::FailedPreconditionError("Door room is not loaded");
85 }
86 auto candidate = CaptureDungeonSelectionEditState(*room);
87 candidate.doors = doors;
88 return PreflightStreamMutation(room_id, candidate, kSelectionDoors);
89}
90
92 int room_id, const std::vector<zelda3::Sprite>& sprites) {
93 const auto* room = rooms_.GetIfLoaded(room_id);
94 if (!room || room->rom() != rom_) {
95 return absl::FailedPreconditionError("Sprite room is not loaded");
96 }
97 const auto before = CaptureDungeonSelectionEditState(*room);
98 auto candidate = before;
99 candidate.sprites.clear();
100 for (const auto& sprite : sprites) {
101 candidate.sprites.push_back({sprite.id(), sprite.x(), sprite.y(),
102 sprite.subtype(), sprite.layer(),
103 sprite.key_drop(), sprite.deleted()});
104 }
107 ValidateSwitchTransforms(before.sprites, candidate.sprites));
108 return PreflightStreamMutation(room_id, candidate, kSelectionSprites);
109}
110
112 int room_id, const DungeonSelectionEditState& candidate, uint8_t domains) {
113 const auto* original = rooms_.GetIfLoaded(room_id);
114 if (!original || original->rom() != rom_) {
115 return absl::FailedPreconditionError("Stream room is not loaded");
116 }
117 constexpr uint8_t streams =
119 if (!(domains & streams)) {
120 return absl::OkStatus();
121 }
122 // Compare encoded bytes: torches and blocks live outside the object stream,
123 // while doors and hidden sprite key records contribute to its true size.
124 zelda3::Room probe(room_id, rom_);
126 probe, CaptureDungeonSelectionEditState(*original), streams);
127 ApplyDungeonSelectionEditState(probe, candidate, domains & streams);
128 const bool objects_changed =
129 (domains & (kSelectionObjects | kSelectionDoors)) &&
130 probe.EncodeObjects() != original->EncodeObjects();
131 const bool sprites_changed =
132 (domains & kSelectionSprites) &&
133 probe.EncodeSprites() != original->EncodeSprites();
134 if (!objects_changed && !sprites_changed) {
135 return absl::OkStatus();
136 }
137
138 uint8_t changed = objects_changed ? kSelectionObjects | kSelectionDoors : 0;
139 if (sprites_changed)
140 changed |= kSelectionSprites;
142 edit.room_id = room_id;
143 edit.after = candidate;
144 edit.domains = changed;
145 ASSIGN_OR_RETURN(auto ranges, PlanPendingRoomStreamRanges({edit}));
150 "dungeon stream authoring", "DungeonEditorV2", nullptr));
151 }
152 return absl::OkStatus();
153}
154
155absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
157 const std::vector<DungeonSelectionEditPlan>& candidates) const {
158 uint8_t domains = 0;
159 std::set<int> candidate_rooms;
160 for (const auto& candidate : candidates) {
161 if (!candidate.domains)
162 continue;
163 const auto* room = rooms_.GetIfLoaded(candidate.room_id);
164 if (!room || room->rom() != rom_ ||
165 !candidate_rooms.insert(candidate.room_id).second) {
166 return absl::FailedPreconditionError(
167 "Stream candidates must identify distinct loaded rooms");
168 }
169 domains |= candidate.domains;
170 }
171 constexpr uint8_t streams =
173 const auto& flags = core::FeatureFlags::get().dungeon;
174 const bool objects_enabled =
175 flags.kSaveObjects || (domains & (kSelectionObjects | kSelectionDoors));
176 const bool sprites_enabled =
177 flags.kSaveSprites || (domains & kSelectionSprites);
178 bool objects_pending = domains & (kSelectionObjects | kSelectionDoors);
179 bool sprites_pending = domains & kSelectionSprites;
180 rooms_.ForEachLoaded([&](int, const zelda3::Room& room) {
181 objects_pending |= objects_enabled && (room.object_stream_dirty() ||
183 sprites_pending |= sprites_enabled && room.sprites_dirty();
184 });
185 std::vector<std::pair<uint32_t, uint32_t>> writes;
186 if (!objects_pending && !sprites_pending)
187 return writes;
188 if (!rom_ || !rom_->is_loaded())
189 return absl::FailedPreconditionError("ROM is not loaded");
190 std::optional<zelda3::DungeonStreamLayout> object_layout, sprite_layout;
191 const auto* project = dependencies_.project;
192 const auto load_layout = [&](core::DungeonStreamType type, bool pending,
193 auto* result) -> absl::Status {
194 if (!pending || !project || !project->hack_manifest.loaded())
195 return absl::OkStatus();
196 const auto* layout = project->hack_manifest.GetDungeonStreamLayout(type);
197 if (layout &&
198 layout->strategy == core::DungeonWriteStrategy::kCopyOnWrite) {
199 ASSIGN_OR_RETURN(*result,
201 }
202 return absl::OkStatus();
203 };
205 objects_pending, &object_layout));
207 sprites_pending, &sprite_layout));
208 const auto append = [&](const auto& ranges) {
209 writes.insert(writes.end(), ranges.begin(), ranges.end());
210 };
211 // Save visits loaded rooms in ascending order. Simulate every pending stream
212 // in that same order so object/header and sprite relocations share the actual
213 // remaining space. Detached authored state never loads graphics or touches Undo.
214 Rom scratch(*rom_);
215 absl::Status status;
216 rooms_.ForEachLoaded([&](int id, const zelda3::Room& source) {
217 if (!status.ok())
218 return;
219 const auto check_room = [&]() -> absl::Status {
220 auto dirty = source.CaptureSaveDirtySnapshot();
221 const auto candidate = std::find_if(
222 candidates.begin(), candidates.end(), [id](const auto& edit) {
223 return edit.room_id == id && edit.domains;
224 });
225 const bool has_candidate = candidate != candidates.end();
226 if (has_candidate) {
227 dirty.object_stream |=
228 (candidate->domains & (kSelectionObjects | kSelectionDoors)) != 0;
229 dirty.sprites |= (candidate->domains & kSelectionSprites) != 0;
230 }
231 if (!(objects_enabled &&
232 (dirty.object_stream || dirty.object_stream_header)) &&
233 !(sprites_enabled && dirty.sprites))
234 return absl::OkStatus();
235 zelda3::Room staged(id, &scratch);
238 staged, CaptureDungeonSelectionEditState(source), streams);
239 if (has_candidate)
240 ApplyDungeonSelectionEditState(staged, candidate->after,
241 candidate->domains);
242 staged.RestoreSaveDirtySnapshot(dirty);
243 const auto* objects = object_layout ? &*object_layout : nullptr;
244 const auto* sprites = sprite_layout ? &*sprite_layout : nullptr;
245 if (objects_enabled && dirty.object_stream) {
246 ASSIGN_OR_RETURN(auto ranges, staged.GetObjectSaveWriteRanges(objects));
247 append(ranges);
248 RETURN_IF_ERROR(staged.SaveObjects(objects));
249 }
250 if (objects_enabled && dirty.object_stream_header) {
251 ASSIGN_OR_RETURN(auto ranges,
253 append(ranges);
255 }
256 if (sprites_enabled && dirty.sprites) {
257 ASSIGN_OR_RETURN(auto ranges, staged.GetSpriteSaveWriteRanges(sprites));
258 append(ranges);
259 RETURN_IF_ERROR(staged.SaveSprites(sprites));
260 }
261 return absl::OkStatus();
262 };
263 status = check_room();
264 });
265 RETURN_IF_ERROR(status);
266 return writes;
267}
268
270 const DungeonSelectionEditPlan& plan, bool continuous) {
271 auto* room = rooms_.GetIfLoaded(plan.room_id);
272 if (!room || room->rom() != rom_) {
273 return absl::FailedPreconditionError("Selection room is not loaded");
274 }
275 if (!plan.changed()) {
276 return absl::OkStatus();
277 }
278 if (plan.domains & kSelectionObjects) {
280 plan.after.chests, false));
281 }
283 PreflightStreamMutation(plan.room_id, plan.after, plan.domains));
284 if (continuous && pending_selection_undo_.plan &&
285 pending_selection_undo_.plan->room_id != plan.room_id) {
286 return absl::FailedPreconditionError(
287 "Finish the current selection drag before editing another room");
288 }
289 if (!continuous) {
291 } else if (!pending_selection_undo_.plan) {
292 // Close older history before capturing the group gesture. Releasing viewers
293 // here would cancel the coordinator's newly started continuous drag.
294 if (pending_undo_.room_id >= 0) {
296 }
299 }
302 }
304 }
305 // A release may commit a single-entity preview. Never overwrite that newer
306 // state with a candidate planned before its gesture completed.
307 const auto current = CaptureDungeonSelectionEditState(*room);
308 if (ChangedDungeonSelectionDomains(current, plan.before) & plan.domains) {
309 return absl::FailedPreconditionError(
310 "Selection changed while finishing the previous gesture; repeat the "
311 "edit");
312 }
313
316 ValidateSwitchTransforms(current.sprites, plan.after.sprites));
317
318 if (continuous && !pending_selection_undo_.plan) {
320 pending_selection_undo_.dirty_before = room->CaptureSaveDirtySnapshot();
321 }
322 // Publish every affected collection before rendering or updating selection.
323 // The planner has already validated every new or changed authored record.
325 if (continuous) {
326 auto& pending = *pending_selection_undo_.plan;
327 pending.after = plan.after;
328 pending.domains |= plan.domains;
329 } else {
331 }
332 RefreshSelectionEditViews(plan.room_id, nullptr);
333 return absl::OkStatus();
334}
335
337 const auto verb = [&]() {
338 switch (plan.kind) {
340 return "Delete";
342 return "Duplicate";
344 return "Move";
346 return "Paste";
348 return "Complete stone surround for";
349 }
350 return "Edit";
351 }();
352 const auto description =
353 absl::StrFormat("%s room %03X selection", verb, plan.room_id);
354 PushSelectionUndoBatch({std::move(plan)}, description);
355}
356
358 std::vector<DungeonSelectionEditPlan> plans, std::string description) {
359 std::unique_ptr<UndoAction> action = std::make_unique<DungeonSelectionAction>(
360 plans, std::move(description),
361 [this](const std::vector<DungeonSelectionEditPlan>& states, bool after) {
362 return RestoreSelectionEditBatch(states, after);
363 });
364 // Nest collision restores around the existing batch. Every wrapper checks
365 // its expected map before the innermost action changes any room.
366 std::vector<DungeonCustomCollisionSnapshot> collision_before;
367 const auto ownership_before = minecart_collision_ownership_;
368 for (const auto& plan : plans) {
369 if (!(plan.domains & kSelectionObjects)) {
370 continue;
371 }
372 const auto* room = rooms_.GetIfLoaded(plan.room_id);
373 if (!room) {
374 continue;
375 }
376 collision_before.push_back({plan.room_id, room->custom_collision()});
377 auto paired =
378 PairMinecartObjectCollision(plan.room_id, plan.before.objects,
379 plan.after.objects, std::move(action));
380 if (!paired.ok()) {
381 for (const auto& snapshot : collision_before) {
382 RestoreRoomCustomCollision(snapshot.room_id, snapshot.map);
383 }
384 minecart_collision_ownership_ = ownership_before;
385 (void)RestoreSelectionEditBatch(plans, false);
388 std::string(paired.status().message()));
389 }
390 return;
391 }
392 action = std::move(*paired);
393 }
394 undo_manager_.Push(std::move(action));
395}
396
399 return;
400 }
401 auto pending = std::move(pending_selection_undo_);
403 auto plan = std::move(*pending.plan);
404 plan.domains &= ChangedDungeonSelectionDomains(plan.before, plan.after);
405 if (!plan.changed()) {
406 // Returning a drag to its original position is a no-op. Retain existing
407 // redo history and restore the original save-dirty state, including any
408 // unrelated unsaved work that predated this gesture.
409 if (auto* room = rooms_.GetIfLoaded(plan.room_id);
410 room && room->rom() == rom_) {
411 room->RestoreSaveDirtySnapshot(pending.dirty_before);
412 }
413 return;
414 }
415 PushSelectionUndoAction(std::move(plan));
416}
417
419 const std::vector<DungeonSelectionEditPlan>& plans, bool after) {
420 // Validate every endpoint before restoring any. A missing second room must
421 // not leave the first room changed by a failed Undo or Redo.
422 for (const auto& plan : plans) {
423 const auto* room = rooms_.GetIfLoaded(plan.room_id);
424 if (!room || room->rom() != rom_ || room->id() != plan.room_id) {
425 return absl::FailedPreconditionError("Selection undo room is not loaded");
426 }
427 }
428 for (const auto& plan : plans) {
430 after ? plan.after : plan.before,
431 plan.domains);
432 }
433 for (const auto& plan : plans) {
434 RefreshSelectionEditViews(plan.room_id, after ? &plan.after : &plan.before);
435 }
437 return absl::OkStatus();
438}
439
441 int room_id, const DungeonSelectionEditState* selection) {
442 auto* room = rooms_.GetIfLoaded(room_id);
443 if (!room)
444 return;
445 room->MarkObjectsDirty();
446 if (rom_ && room->rom() == rom_ && rom_->is_loaded()) {
447 room->RenderRoomGraphics();
448 }
449 auto refresh = [&](DungeonCanvasViewer* viewer) {
450 if (!viewer)
451 return;
452 // A changed door may affect a connected-room graph centered elsewhere.
453 viewer->InvalidateConnectedRoomGraph();
454 if (viewer->current_room_id() != room_id)
455 return;
456 if (selection) {
457 auto& interaction = viewer->object_interaction();
458 interaction.CancelPlacement();
459 interaction.SetSelectedObjects(selection->selected_objects);
460 interaction.entity_coordinator().SetSelectedEntities(
461 selection->selected_entities);
462 }
463 viewer->TriggerChangePing();
464 };
465 room_viewers_.ForEach([&](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
466 refresh(viewer.get());
467 });
468 refresh(workbench_viewer_.get());
469 refresh(workbench_compare_viewer_.get());
472 }
473}
474
475} // 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:52
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
bool loaded() const
Check if the manifest has been loaded.
class MinecartTrackEditorPanel * minecart_track_editor_panel_
absl::Status PreflightObjectMutation(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< chest_data > &chests, bool check_streams=true)
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
absl::Status CommitSelectionEdit(const DungeonSelectionEditPlan &plan, bool continuous)
void RefreshSelectionEditViews(int room_id, const DungeonSelectionEditState *selection)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > PlanPendingRoomStreamRanges(const std::vector< DungeonSelectionEditPlan > &candidates) const
PendingCollisionUndo pending_collision_undo_
absl::Status PreflightDoorMutation(int room_id, const std::vector< zelda3::Room::Door > &doors)
ObjectSelectorContent * object_selector_panel_
absl::Status PreflightSpriteMutation(int room_id, const std::vector< zelda3::Sprite > &sprites)
absl::Status RestoreSelectionEditBatch(const std::vector< DungeonSelectionEditPlan > &plans, bool after)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
PendingWaterFillUndo pending_water_fill_undo_
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)
PendingSelectionUndo pending_selection_undo_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
void RestoreRoomCustomCollision(int room_id, const zelda3::CustomCollisionMap &map)
absl::Status PreflightStreamMutation(int room_id, const DungeonSelectionEditState &candidate, uint8_t domains)
std::unordered_map< int, zelda3::MinecartTrackCollisionOwnership > minecart_collision_ownership_
void PushSelectionUndoBatch(std::vector< DungeonSelectionEditPlan > plans, std::string description)
void PushSelectionUndoAction(DungeonSelectionEditPlan plan)
zelda3::Room * GetIfLoaded(int room_id)
UndoManager undo_manager_
Definition editor.h:383
project::YazeProject * project() const
Definition editor.h:370
EditorDependencies dependencies_
Definition editor.h:382
EditorType type() const
Definition editor.h:355
void SetPlacementError(const std::string &message)
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
void Push(std::unique_ptr< UndoAction > action)
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
DungeonSelectionAction(std::vector< DungeonSelectionEditPlan > plans, std::string description, Restore restore)
std::function< absl::Status( const std::vector< DungeonSelectionEditPlan > &, bool)> Restore
void MarkObjectsDirty()
Definition room.h:471
void RestoreMetadataSnapshot(const MetadataSnapshot &snapshot)
Definition room.cc:886
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetObjectSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
Definition room.cc:2438
bool object_stream_dirty() const
Definition room.h:504
bool sprites_dirty() const
Definition room.h:318
bool object_stream_header_dirty() const
Definition room.h:506
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetObjectStreamHeaderSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
Definition room.cc:2558
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2443
void RestoreSaveDirtySnapshot(const SaveDirtySnapshot &snapshot)
Definition room.h:698
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetSpriteSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
Definition room.cc:2668
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:2068
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2565
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2673
std::vector< uint8_t > EncodeSprites() const
Definition room.cc:2156
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
Definition room.h:673
MetadataSnapshot CaptureMetadataSnapshot() const
Definition room.cc:861
struct chest_data chest_data
Legacy chest data structure.
#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 ValidateSwitchTransforms(std::span< const DungeonSpriteSnapshot > before, std::span< const DungeonSpriteSnapshot > after)
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)
DungeonSelectionEditState CaptureDungeonSelectionEditState(const zelda3::Room &room)
constexpr uint8_t kSelectionSprites
constexpr uint8_t kSelectionObjects
uint8_t ChangedDungeonSelectionDomains(const DungeonSelectionEditState &a, const DungeonSelectionEditState &b)
constexpr uint8_t kSelectionDoors
void ApplyDungeonSelectionEditState(zelda3::Room &room, const DungeonSelectionEditState &state, uint8_t domains)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::optional< DungeonSelectionEditPlan > plan
std::vector< SelectedEntity > selected_entities
std::vector< DungeonSpriteSnapshot > sprites
std::vector< zelda3::RoomObject > objects
project::YazeProject * project
Definition editor.h:174
Represents a selected entity in the dungeon editor.
RomWritePolicy write_policy
Definition project.h:229
core::HackManifest hack_manifest
Definition project.h:376
Represents a door in a dungeon room.
Definition room.h:339