13#include <unordered_set>
16#include "absl/status/status.h"
17#include "absl/strings/str_format.h"
18#include "absl/types/span.h"
62#include "imgui/imgui.h"
94 viewer->TriggerChangePing();
99 description.empty() ?
"Undid last dungeon edit"
100 : absl::StrFormat(
"Undid: %s", description),
125 viewer->TriggerChangePing();
130 description.empty() ?
"Redid last dungeon edit"
131 : absl::StrFormat(
"Redid: %s", description),
141 viewer->object_interaction().HandleCopySelected();
142 viewer->object_interaction().HandleDeleteSelected();
144 return absl::OkStatus();
149 viewer->object_interaction().HandleCopySelected();
151 return absl::OkStatus();
156 viewer->object_interaction().HandlePasteObjects();
158 return absl::OkStatus();
162 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
168 "BeginUndoSnapshot called twice without FinalizeUndoAction. "
169 "Previous snapshot for room %d is being leaked. Finalizing now.",
184 viewer->object_interaction().GetSelectedObjectIndices();
192 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
195 auto after_objects =
rooms_[room_id].GetTileObjects();
196 std::vector<size_t> after_selection;
200 after_selection = viewer->object_interaction().GetSelectedObjectIndices();
203 auto action = std::make_unique<DungeonObjectsAction>(
206 std::move(after_selection),
207 [
this](
int rid,
const std::vector<zelda3::RoomObject>& objects,
208 const std::vector<size_t>& selected_indices) {
209 RestoreRoomObjects(rid, objects, selected_indices);
223 int room_id,
const std::vector<zelda3::RoomObject>& objects,
224 const std::vector<size_t>& selected_indices) {
225 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
228 auto& room =
rooms_[room_id];
229 const auto previous_objects = room.GetTileObjects();
230 room.SetTileObjects(objects);
231 room.RenderRoomGraphics();
233 std::vector<size_t> valid_selection;
234 for (
size_t index : selected_indices) {
235 if (index < objects.size()) {
236 valid_selection.push_back(index);
240 viewer->object_interaction().SetSelectedObjects(valid_selection);
242 viewer->TriggerObjectChangePing(previous_objects, objects);
251 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
267 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
270 auto after =
rooms_[room_id].custom_collision();
278 auto action = std::make_unique<DungeonCustomCollisionAction>(
281 RestoreRoomCustomCollision(rid, map);
294 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
297 auto& room =
rooms_[room_id];
298 room.custom_collision() = map;
299 room.MarkCustomCollisionDirty();
309 [](uint8_t tile) { return tile != 0; });
326 : entries_(std::move(entries)) {}
332 for (
const auto& entry : entries_) {
333 entry.room->custom_collision() = entry.map;
335 entry.room->MarkCustomCollisionDirty();
337 entry.room->ClearCustomCollisionDirty();
346 bool committed_ =
false;
352 const std::vector<zelda3::TrackCollisionResult>& preview,
354 if (preview.empty()) {
355 return absl::InvalidArgumentError(
"Minecart collision preview is empty");
358 std::unordered_set<int> seen_room_ids;
359 std::vector<DungeonCustomCollisionSnapshot> before;
360 std::vector<DungeonCustomCollisionSnapshot> after;
361 std::vector<CollisionRollbackEntry> rollback_entries;
362 before.reserve(preview.size());
363 after.reserve(preview.size());
364 rollback_entries.reserve(preview.size());
367 for (
const auto& expected : preview) {
368 const int room_id = expected.room_id;
370 return absl::OutOfRangeError(absl::StrFormat(
371 "Minecart collision room 0x%03X is out of range", room_id));
373 if (!seen_room_ids.insert(room_id).second) {
374 return absl::InvalidArgumentError(absl::StrFormat(
375 "Minecart collision preview contains duplicate room 0x%03X",
380 if (room ==
nullptr) {
381 return absl::FailedPreconditionError(absl::StrFormat(
382 "Minecart collision room 0x%03X is not loaded", room_id));
385 return absl::FailedPreconditionError(absl::StrFormat(
386 "Room 0x%03X already has custom collision; generation will not "
393 current.room_id = room_id;
394 if (!current.collision_map.has_data || current.tiles_generated <= 0) {
395 return absl::FailedPreconditionError(absl::StrFormat(
396 "Room 0x%03X no longer contains supported minecart track pieces",
399 if (!CollisionMapsEqual(current.collision_map, expected.collision_map) ||
400 current.tiles_generated != expected.tiles_generated ||
401 current.stop_count != expected.stop_count ||
402 current.corner_count != expected.corner_count ||
403 current.switch_count != expected.switch_count) {
404 return absl::FailedPreconditionError(absl::StrFormat(
405 "Minecart collision preview for room 0x%03X is stale; preview "
406 "again before applying",
411 after.push_back({room_id, current.collision_map});
412 rollback_entries.push_back(
418 return absl::FailedPreconditionError(
419 "Finish the current dungeon edit before applying minecart collision");
422 auto action = std::make_unique<DungeonCustomCollisionBatchAction>(
424 [
this](
const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
428 CollisionBatchRollback rollback(std::move(rollback_entries));
429 for (
const auto& snapshot : after) {
432 room->MarkCustomCollisionDirty();
436 }
catch (
const std::exception& error) {
437 return absl::InternalError(absl::StrFormat(
438 "Could not record minecart collision undo: %s", error.what()));
445 for (
const auto& snapshot : after) {
447 viewer->TriggerChangePing();
450 return absl::OkStatus();
454 const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
455 std::unordered_set<int> seen_room_ids;
456 for (
const auto& snapshot : snapshots) {
458 return absl::OutOfRangeError(
"Collision undo room ID is out of range");
460 if (!seen_room_ids.insert(snapshot.room_id).second) {
461 return absl::InvalidArgumentError(
462 "Collision undo contains a duplicate room ID");
465 return absl::FailedPreconditionError(
466 "Collision undo target room is not loaded");
470 for (
const auto& snapshot : snapshots) {
473 room.MarkCustomCollisionDirty();
475 viewer->TriggerChangePing();
482 return absl::OkStatus();
493 for (
size_t i = 0; i < zone.tiles.size(); ++i) {
494 if (zone.tiles[i] != 0) {
495 snap.
offsets.push_back(
static_cast<uint16_t
>(i));
504 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
520 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
523 auto after = MakeWaterFillSnapshot(
rooms_[room_id]);
531 auto action = std::make_unique<DungeonWaterFillAction>(
534 RestoreRoomWaterFill(rid, snap);
544 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
547 auto& room =
rooms_[room_id];
548 room.ClearWaterFillZone();
550 for (uint16_t off : snap.
offsets) {
551 const int x =
static_cast<int>(off % 64);
552 const int y =
static_cast<int>(off / 64);
553 room.SetWaterFillTile(x, y,
true);
555 room.MarkWaterFillDirty();
void BeginCollisionUndoSnapshot(int room_id)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
void RestoreRoomWaterFill(int room_id, const WaterFillSnapshot &snap)
PendingUndo pending_undo_
void FinalizeWaterFillUndoAction(int room_id)
absl::Status Copy() override
void RestoreRoomObjects(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< size_t > &selected_indices)
bool IsWorkbenchWorkflowEnabled() const
PendingCollisionUndo pending_collision_undo_
PendingWaterFillUndo pending_water_fill_undo_
absl::Status Cut() override
absl::Status Paste() override
static bool IsValidRoomId(int room_id)
void FinalizeUndoAction(int room_id)
DungeonCanvasViewer * GetViewerForRoom(int room_id)
void BeginWaterFillUndoSnapshot(int room_id)
absl::Status Undo() override
absl::Status RestoreRoomCustomCollisionBatch(const std::vector< DungeonCustomCollisionSnapshot > &snapshots)
absl::Status Redo() override
void BeginUndoSnapshot(int room_id)
void RestoreRoomCustomCollision(int room_id, const zelda3::CustomCollisionMap &map)
bool undo_restore_triggered_ping_
absl::Status ApplyMinecartCollisionBatch(const std::vector< zelda3::TrackCollisionResult > &preview, const zelda3::GeneratorOptions &options)
void FinalizeCollisionUndoAction(int room_id)
zelda3::Room * GetIfMaterialized(int room_id)
UndoManager undo_manager_
EditorDependencies dependencies_
void InvalidateRoomAudit()
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.
~CollisionBatchRollback()
std::vector< CollisionRollbackEntry > entries_
CollisionBatchRollback(std::vector< CollisionRollbackEntry > entries)
const CustomCollisionMap & custom_collision() const
bool custom_collision_dirty() const
const WaterFillZoneMap & water_fill_zone() const
uint8_t water_fill_sram_bit_mask() const
#define LOG_ERROR(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
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.
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
zelda3::CustomCollisionMap before
std::vector< size_t > before_selection
std::vector< zelda3::RoomObject > before_objects
ToastManager * toast_manager
std::vector< uint16_t > offsets
zelda3::CustomCollisionMap map
std::array< uint8_t, 64 *64 > tiles