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 <utility>
7
11#include "core/features.h"
12#include "util/macro.h"
13
14namespace yaze::editor {
15namespace {
16
17class DungeonSelectionAction final : public UndoAction {
18 public:
19 using Restore = std::function<absl::Status(
20 const std::vector<DungeonSelectionEditPlan>&, bool)>;
21 DungeonSelectionAction(std::vector<DungeonSelectionEditPlan> plans,
22 std::string description, Restore restore)
23 : plans_(std::move(plans)),
24 description_(std::move(description)),
25 restore_(std::move(restore)) {}
26 absl::Status Undo() override { return restore_(plans_, false); }
27 absl::Status Redo() override { return restore_(plans_, true); }
28 std::string Description() const override { return description_; }
29 size_t MemoryUsage() const override {
30 auto bytes = [](const DungeonSelectionEditState& state) {
31 return state.objects.size() * sizeof(zelda3::RoomObject) +
32 state.chests.size() * sizeof(chest_data) +
33 state.doors.size() * sizeof(zelda3::Room::Door) +
34 state.sprites.size() * sizeof(DungeonSpriteSnapshot) +
35 state.items.size() * sizeof(zelda3::PotItem) +
36 state.selected_objects.size() * sizeof(size_t) +
37 state.selected_entities.size() * sizeof(SelectedEntity);
38 };
39 size_t total = description_.capacity();
40 for (const auto& plan : plans_) {
41 total += bytes(plan.before) + bytes(plan.after);
42 }
43 return total;
44 }
45
46 private:
47 std::vector<DungeonSelectionEditPlan> plans_;
48 std::string description_;
50};
51
52} // namespace
53
55 int room_id, const std::vector<zelda3::Room::Door>& doors) {
56 const auto* room = rooms_.GetIfLoaded(room_id);
57 if (!room || room->rom() != rom_) {
58 return absl::FailedPreconditionError("Door room is not loaded");
59 }
60 auto candidate = CaptureDungeonSelectionEditState(*room);
61 candidate.doors = doors;
62 return PreflightStreamMutation(room_id, candidate, kSelectionDoors);
63}
64
66 int room_id, const std::vector<zelda3::Sprite>& sprites) {
67 const auto* room = rooms_.GetIfLoaded(room_id);
68 if (!room || room->rom() != rom_) {
69 return absl::FailedPreconditionError("Sprite room is not loaded");
70 }
71 auto candidate = CaptureDungeonSelectionEditState(*room);
72 candidate.sprites.clear();
73 for (const auto& sprite : sprites) {
74 candidate.sprites.push_back({sprite.id(), sprite.x(), sprite.y(),
75 sprite.subtype(), sprite.layer(),
76 sprite.key_drop(), sprite.deleted()});
77 }
78 return PreflightStreamMutation(room_id, candidate, kSelectionSprites);
79}
80
82 int room_id, const DungeonSelectionEditState& candidate, uint8_t domains) {
83 const auto* original = rooms_.GetIfLoaded(room_id);
84 if (!original || original->rom() != rom_) {
85 return absl::FailedPreconditionError("Stream room is not loaded");
86 }
87 constexpr uint8_t streams =
89 if (!(domains & streams)) {
90 return absl::OkStatus();
91 }
92 // Compare encoded bytes: torches and blocks live outside the object stream,
93 // while doors and hidden sprite key records contribute to its true size.
94 zelda3::Room probe(room_id, rom_);
96 probe, CaptureDungeonSelectionEditState(*original), streams);
97 ApplyDungeonSelectionEditState(probe, candidate, domains & streams);
98 const bool objects_changed =
99 (domains & (kSelectionObjects | kSelectionDoors)) &&
100 probe.EncodeObjects() != original->EncodeObjects();
101 const bool sprites_changed =
102 (domains & kSelectionSprites) &&
103 probe.EncodeSprites() != original->EncodeSprites();
104 if (!objects_changed && !sprites_changed) {
105 return absl::OkStatus();
106 }
107
108 uint8_t changed = objects_changed ? kSelectionObjects | kSelectionDoors : 0;
109 if (sprites_changed)
110 changed |= kSelectionSprites;
112 edit.room_id = room_id;
113 edit.after = candidate;
114 edit.domains = changed;
115 ASSIGN_OR_RETURN(auto ranges, PlanPendingRoomStreamRanges({edit}));
120 "dungeon stream authoring", "DungeonEditorV2", nullptr));
121 }
122 return absl::OkStatus();
123}
124
125absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
127 const std::vector<DungeonSelectionEditPlan>& candidates) const {
128 uint8_t domains = 0;
129 std::set<int> candidate_rooms;
130 for (const auto& candidate : candidates) {
131 if (!candidate.domains)
132 continue;
133 const auto* room = rooms_.GetIfLoaded(candidate.room_id);
134 if (!room || room->rom() != rom_ ||
135 !candidate_rooms.insert(candidate.room_id).second) {
136 return absl::FailedPreconditionError(
137 "Stream candidates must identify distinct loaded rooms");
138 }
139 domains |= candidate.domains;
140 }
141 constexpr uint8_t streams =
143 const auto& flags = core::FeatureFlags::get().dungeon;
144 const bool objects_enabled =
145 flags.kSaveObjects || (domains & (kSelectionObjects | kSelectionDoors));
146 const bool sprites_enabled =
147 flags.kSaveSprites || (domains & kSelectionSprites);
148 bool objects_pending = domains & (kSelectionObjects | kSelectionDoors);
149 bool sprites_pending = domains & kSelectionSprites;
150 rooms_.ForEachLoaded([&](int, const zelda3::Room& room) {
151 objects_pending |= objects_enabled && (room.object_stream_dirty() ||
153 sprites_pending |= sprites_enabled && room.sprites_dirty();
154 });
155 std::vector<std::pair<uint32_t, uint32_t>> writes;
156 if (!objects_pending && !sprites_pending)
157 return writes;
158 if (!rom_ || !rom_->is_loaded())
159 return absl::FailedPreconditionError("ROM is not loaded");
160 std::optional<zelda3::DungeonStreamLayout> object_layout, sprite_layout;
161 const auto* project = dependencies_.project;
162 const auto load_layout = [&](core::DungeonStreamType type, bool pending,
163 auto* result) -> absl::Status {
164 if (!pending || !project || !project->hack_manifest.loaded())
165 return absl::OkStatus();
166 const auto* layout = project->hack_manifest.GetDungeonStreamLayout(type);
167 if (layout &&
168 layout->strategy == core::DungeonWriteStrategy::kCopyOnWrite) {
169 ASSIGN_OR_RETURN(*result,
171 }
172 return absl::OkStatus();
173 };
175 objects_pending, &object_layout));
177 sprites_pending, &sprite_layout));
178 const auto append = [&](const auto& ranges) {
179 writes.insert(writes.end(), ranges.begin(), ranges.end());
180 };
181 // Save visits loaded rooms in ascending order. Simulate every pending stream
182 // in that same order so object/header and sprite relocations share the actual
183 // remaining space. Detached authored state never loads graphics or touches Undo.
184 Rom scratch(*rom_);
185 absl::Status status;
186 rooms_.ForEachLoaded([&](int id, const zelda3::Room& source) {
187 if (!status.ok())
188 return;
189 const auto check_room = [&]() -> absl::Status {
190 auto dirty = source.CaptureSaveDirtySnapshot();
191 const auto candidate = std::find_if(
192 candidates.begin(), candidates.end(), [id](const auto& edit) {
193 return edit.room_id == id && edit.domains;
194 });
195 const bool has_candidate = candidate != candidates.end();
196 if (has_candidate) {
197 dirty.object_stream |=
198 (candidate->domains & (kSelectionObjects | kSelectionDoors)) != 0;
199 dirty.sprites |= (candidate->domains & kSelectionSprites) != 0;
200 }
201 if (!(objects_enabled &&
202 (dirty.object_stream || dirty.object_stream_header)) &&
203 !(sprites_enabled && dirty.sprites))
204 return absl::OkStatus();
205 zelda3::Room staged(id, &scratch);
208 staged, CaptureDungeonSelectionEditState(source), streams);
209 if (has_candidate)
210 ApplyDungeonSelectionEditState(staged, candidate->after,
211 candidate->domains);
212 staged.RestoreSaveDirtySnapshot(dirty);
213 const auto* objects = object_layout ? &*object_layout : nullptr;
214 const auto* sprites = sprite_layout ? &*sprite_layout : nullptr;
215 if (objects_enabled && dirty.object_stream) {
216 ASSIGN_OR_RETURN(auto ranges, staged.GetObjectSaveWriteRanges(objects));
217 append(ranges);
218 RETURN_IF_ERROR(staged.SaveObjects(objects));
219 }
220 if (objects_enabled && dirty.object_stream_header) {
221 ASSIGN_OR_RETURN(auto ranges,
223 append(ranges);
225 }
226 if (sprites_enabled && dirty.sprites) {
227 ASSIGN_OR_RETURN(auto ranges, staged.GetSpriteSaveWriteRanges(sprites));
228 append(ranges);
229 RETURN_IF_ERROR(staged.SaveSprites(sprites));
230 }
231 return absl::OkStatus();
232 };
233 status = check_room();
234 });
235 RETURN_IF_ERROR(status);
236 return writes;
237}
238
240 const DungeonSelectionEditPlan& plan, bool continuous) {
241 auto* room = rooms_.GetIfLoaded(plan.room_id);
242 if (!room || room->rom() != rom_) {
243 return absl::FailedPreconditionError("Selection room is not loaded");
244 }
245 if (!plan.changed()) {
246 return absl::OkStatus();
247 }
248 if (plan.domains & kSelectionObjects) {
250 plan.after.chests, false));
251 }
253 PreflightStreamMutation(plan.room_id, plan.after, plan.domains));
254 if (continuous && pending_selection_undo_.plan &&
255 pending_selection_undo_.plan->room_id != plan.room_id) {
256 return absl::FailedPreconditionError(
257 "Finish the current selection drag before editing another room");
258 }
259 if (!continuous) {
261 } else if (!pending_selection_undo_.plan) {
262 // Close older history before capturing the group gesture. Releasing viewers
263 // here would cancel the coordinator's newly started continuous drag.
264 if (pending_undo_.room_id >= 0) {
266 }
269 }
272 }
274 }
275 // A release may commit a single-entity preview. Never overwrite that newer
276 // state with a candidate planned before its gesture completed.
277 const auto current = CaptureDungeonSelectionEditState(*room);
278 if (ChangedDungeonSelectionDomains(current, plan.before) & plan.domains) {
279 return absl::FailedPreconditionError(
280 "Selection changed while finishing the previous gesture; repeat the "
281 "edit");
282 }
283
284 if (continuous && !pending_selection_undo_.plan) {
286 pending_selection_undo_.dirty_before = room->CaptureSaveDirtySnapshot();
287 }
288 // Publish every affected collection before rendering or updating selection.
289 // The planner has already validated every new or changed authored record.
291 if (continuous) {
292 auto& pending = *pending_selection_undo_.plan;
293 pending.after = plan.after;
294 pending.domains |= plan.domains;
295 } else {
297 }
298 RefreshSelectionEditViews(plan.room_id, nullptr);
299 return absl::OkStatus();
300}
301
303 const auto verb = [&]() {
304 switch (plan.kind) {
306 return "Delete";
308 return "Duplicate";
310 return "Move";
312 return "Paste";
313 }
314 return "Edit";
315 }();
316 const auto description =
317 absl::StrFormat("%s room %03X selection", verb, plan.room_id);
318 PushSelectionUndoBatch({std::move(plan)}, description);
319}
320
322 std::vector<DungeonSelectionEditPlan> plans, std::string description) {
323 undo_manager_.Push(std::make_unique<DungeonSelectionAction>(
324 std::move(plans), std::move(description),
325 [this](const std::vector<DungeonSelectionEditPlan>& states, bool after) {
326 return RestoreSelectionEditBatch(states, after);
327 }));
328}
329
332 return;
333 }
334 auto pending = std::move(pending_selection_undo_);
336 auto plan = std::move(*pending.plan);
337 plan.domains &= ChangedDungeonSelectionDomains(plan.before, plan.after);
338 if (!plan.changed()) {
339 // Returning a drag to its original position is a no-op. Retain existing
340 // redo history and restore the original save-dirty state, including any
341 // unrelated unsaved work that predated this gesture.
342 if (auto* room = rooms_.GetIfLoaded(plan.room_id);
343 room && room->rom() == rom_) {
344 room->RestoreSaveDirtySnapshot(pending.dirty_before);
345 }
346 return;
347 }
348 PushSelectionUndoAction(std::move(plan));
349}
350
352 const std::vector<DungeonSelectionEditPlan>& plans, bool after) {
353 // Validate every endpoint before restoring any. A missing second room must
354 // not leave the first room changed by a failed Undo or Redo.
355 for (const auto& plan : plans) {
356 const auto* room = rooms_.GetIfLoaded(plan.room_id);
357 if (!room || room->rom() != rom_ || room->id() != plan.room_id) {
358 return absl::FailedPreconditionError("Selection undo room is not loaded");
359 }
360 }
361 for (const auto& plan : plans) {
363 after ? plan.after : plan.before,
364 plan.domains);
365 }
366 for (const auto& plan : plans) {
367 RefreshSelectionEditViews(plan.room_id, after ? &plan.after : &plan.before);
368 }
370 return absl::OkStatus();
371}
372
374 int room_id, const DungeonSelectionEditState* selection) {
375 auto* room = rooms_.GetIfLoaded(room_id);
376 if (!room)
377 return;
378 room->MarkObjectsDirty();
379 if (rom_ && room->rom() == rom_ && rom_->is_loaded()) {
380 room->RenderRoomGraphics();
381 }
382 auto refresh = [&](DungeonCanvasViewer* viewer) {
383 if (!viewer)
384 return;
385 // A changed door may affect a connected-room graph centered elsewhere.
386 viewer->InvalidateConnectedRoomGraph();
387 if (viewer->current_room_id() != room_id)
388 return;
389 if (selection) {
390 auto& interaction = viewer->object_interaction();
391 interaction.CancelPlacement();
392 interaction.SetSelectedObjects(selection->selected_objects);
393 interaction.entity_coordinator().SetSelectedEntities(
394 selection->selected_entities);
395 }
396 viewer->TriggerChangePing();
397 };
398 room_viewers_.ForEach([&](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
399 refresh(viewer.get());
400 });
401 refresh(workbench_viewer_.get());
402 refresh(workbench_compare_viewer_.get());
405 }
406}
407
408} // 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:49
bool is_loaded() const
Definition rom.h:187
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.
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)
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_
PendingSelectionUndo pending_selection_undo_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
absl::Status PreflightStreamMutation(int room_id, const DungeonSelectionEditState &candidate, uint8_t domains)
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:371
project::YazeProject * project() const
Definition editor.h:358
EditorDependencies dependencies_
Definition editor.h:370
EditorType type() const
Definition editor.h:343
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:2437
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:2557
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2442
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:2667
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:2067
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2564
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2672
std::vector< uint8_t > EncodeSprites() const
Definition room.cc:2155
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)
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< zelda3::RoomObject > objects
project::YazeProject * project
Definition editor.h:174
Represents a selected entity in the dungeon editor.
RomWritePolicy write_policy
Definition project.h:225
core::HackManifest hack_manifest
Definition project.h:360
Represents a door in a dungeon room.
Definition room.h:339