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);
29 std::sort(result.begin(), result.end());
32 const auto previous = positions(before);
33 const auto next = positions(after);
36 if (!std::includes(previous.begin(), previous.end(), next.begin(),
38 return absl::FailedPreconditionError(
39 "Place and move switch tracks with Tracks > Nodes");
40 return absl::OkStatus();
45 using Restore = std::function<absl::Status(
46 const std::vector<DungeonSelectionEditPlan>&,
bool)>;
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_; }
62 state.selected_objects.size() *
sizeof(size_t) +
65 size_t total = description_.capacity();
66 for (
const auto& plan : plans_) {
67 total += bytes(plan.before) + bytes(plan.after);
73 std::vector<DungeonSelectionEditPlan>
plans_;
81 int room_id,
const std::vector<zelda3::Room::Door>& doors) {
83 if (!room || room->rom() !=
rom_) {
84 return absl::FailedPreconditionError(
"Door room is not loaded");
87 candidate.doors = doors;
92 int room_id,
const std::vector<zelda3::Sprite>& sprites) {
94 if (!room || room->rom() !=
rom_) {
95 return absl::FailedPreconditionError(
"Sprite room is not loaded");
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()});
107 ValidateSwitchTransforms(before.sprites, candidate.sprites));
114 if (!original || original->rom() !=
rom_) {
115 return absl::FailedPreconditionError(
"Stream room is not loaded");
117 constexpr uint8_t streams =
119 if (!(domains & streams)) {
120 return absl::OkStatus();
128 const bool objects_changed =
131 const bool sprites_changed =
134 if (!objects_changed && !sprites_changed) {
135 return absl::OkStatus();
143 edit.
after = candidate;
150 "dungeon stream authoring",
"DungeonEditorV2",
nullptr));
152 return absl::OkStatus();
155absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
157 const std::vector<DungeonSelectionEditPlan>& candidates)
const {
159 std::set<int> candidate_rooms;
160 for (
const auto& candidate : candidates) {
161 if (!candidate.domains)
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");
169 domains |= candidate.domains;
171 constexpr uint8_t streams =
174 const bool objects_enabled =
176 const bool sprites_enabled =
185 std::vector<std::pair<uint32_t, uint32_t>> writes;
186 if (!objects_pending && !sprites_pending)
189 return absl::FailedPreconditionError(
"ROM is not loaded");
190 std::optional<zelda3::DungeonStreamLayout> object_layout, sprite_layout;
193 auto* result) -> absl::Status {
195 return absl::OkStatus();
202 return absl::OkStatus();
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());
219 const auto check_room = [&]() -> absl::Status {
221 const auto candidate = std::find_if(
222 candidates.begin(), candidates.end(), [
id](
const auto& edit) {
223 return edit.room_id == id && edit.domains;
225 const bool has_candidate = candidate != candidates.end();
227 dirty.object_stream |=
231 if (!(objects_enabled &&
232 (dirty.object_stream || dirty.object_stream_header)) &&
233 !(sprites_enabled && dirty.sprites))
234 return absl::OkStatus();
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) {
250 if (objects_enabled && dirty.object_stream_header) {
256 if (sprites_enabled && dirty.sprites) {
261 return absl::OkStatus();
263 status = check_room();
272 if (!room || room->rom() !=
rom_) {
273 return absl::FailedPreconditionError(
"Selection room is not loaded");
276 return absl::OkStatus();
286 return absl::FailedPreconditionError(
287 "Finish the current selection drag before editing another room");
309 return absl::FailedPreconditionError(
310 "Selection changed while finishing the previous gesture; repeat the "
316 ValidateSwitchTransforms(current.sprites, plan.
after.
sprites));
327 pending.after = plan.
after;
328 pending.domains |= plan.
domains;
333 return absl::OkStatus();
337 const auto verb = [&]() {
348 return "Complete stone surround for";
352 const auto description =
353 absl::StrFormat(
"%s room %03X selection", verb, plan.
room_id);
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) {
366 std::vector<DungeonCustomCollisionSnapshot> collision_before;
368 for (
const auto& plan : plans) {
376 collision_before.push_back({plan.room_id, room->custom_collision()});
379 plan.after.objects, std::move(action));
381 for (
const auto& snapshot : collision_before) {
388 std::string(paired.status().message()));
392 action = std::move(*paired);
403 auto plan = std::move(*pending.plan);
405 if (!plan.changed()) {
410 room && room->rom() ==
rom_) {
411 room->RestoreSaveDirtySnapshot(pending.dirty_before);
419 const std::vector<DungeonSelectionEditPlan>& plans,
bool after) {
422 for (
const auto& plan : plans) {
424 if (!room || room->rom() !=
rom_ || room->id() != plan.room_id) {
425 return absl::FailedPreconditionError(
"Selection undo room is not loaded");
428 for (
const auto& plan : plans) {
430 after ? plan.after : plan.before,
433 for (
const auto& plan : plans) {
437 return absl::OkStatus();
447 room->RenderRoomGraphics();
453 viewer->InvalidateConnectedRoomGraph();
454 if (viewer->current_room_id() != room_id)
457 auto& interaction = viewer->object_interaction();
458 interaction.CancelPlacement();
460 interaction.entity_coordinator().SetSelectedEntities(
463 viewer->TriggerChangePing();
465 room_viewers_.ForEach([&](
int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
466 refresh(viewer.get());
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
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)
PendingUndo pending_undo_
void FinalizeSelectionUndoAction()
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
absl::Status CommitSelectionEdit(const DungeonSelectionEditPlan &plan, bool continuous)
void FinalizeWaterFillUndoAction(int room_id)
void RefreshSelectionEditViews(int room_id, const DungeonSelectionEditState *selection)
void FinalizePendingUndoActions()
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)
void FinalizeUndoAction(int room_id)
PendingSelectionUndo pending_selection_undo_
void FinalizePendingEntityUndoActions()
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
void RestoreRoomCustomCollision(int room_id, const zelda3::CustomCollisionMap &map)
bool undo_restore_triggered_ping_
absl::Status PreflightStreamMutation(int room_id, const DungeonSelectionEditState &candidate, uint8_t domains)
std::unordered_map< int, zelda3::MinecartTrackCollisionOwnership > minecart_collision_ownership_
void FinalizeCollisionUndoAction(int room_id)
void PushSelectionUndoBatch(std::vector< DungeonSelectionEditPlan > plans, std::string description)
void PushSelectionUndoAction(DungeonSelectionEditPlan plan)
zelda3::Room * GetIfLoaded(int room_id)
void ForEachLoaded(Fn &&fn)
UndoManager undo_manager_
project::YazeProject * project() const
EditorDependencies dependencies_
void InvalidateRoomAudit()
void SetPlacementError(const std::string &message)
Abstract base for all undoable actions (Command pattern)
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
std::vector< DungeonSelectionEditPlan > plans_
absl::Status Redo() override
absl::Status Undo() override
void RestoreMetadataSnapshot(const MetadataSnapshot &snapshot)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetObjectSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
bool object_stream_dirty() const
bool sprites_dirty() const
bool object_stream_header_dirty() const
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetObjectStreamHeaderSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
void RestoreSaveDirtySnapshot(const SaveDirtySnapshot &snapshot)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetSpriteSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
std::vector< uint8_t > EncodeObjects() const
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
std::vector< uint8_t > EncodeSprites() const
SaveDirtySnapshot CaptureSaveDirtySnapshot() const
MetadataSnapshot CaptureMetadataSnapshot() const
struct chest_data chest_data
Legacy chest data structure.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
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)
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
zelda3::Room::SaveDirtySnapshot dirty_before
std::optional< DungeonSelectionEditPlan > plan
DungeonSelectionEditKind kind
DungeonSelectionEditState before
DungeonSelectionEditState after
std::vector< SelectedEntity > selected_entities
std::vector< DungeonSpriteSnapshot > sprites
std::vector< zelda3::RoomObject > objects
std::vector< chest_data > chests
std::vector< size_t > selected_objects
project::YazeProject * project
Represents a selected entity in the dungeon editor.
core::HackManifest hack_manifest
Represents a door in a dungeon room.