13#include <unordered_set>
16#include "absl/status/status.h"
17#include "absl/strings/str_format.h"
18#include "absl/types/span.h"
60#include "imgui/imgui.h"
80 using Apply = std::function<absl::Status(uint8_t, uint8_t)>;
87 apply_(std::move(apply)) {}
88 absl::Status
Undo()
override {
return apply_(after_, before_); }
89 absl::Status
Redo()
override {
return apply_(before_, after_); }
91 return absl::StrFormat(
"Room graphics group %02X slot %d", group_, slot_);
108 return absl::FailedPreconditionError(
"No loaded graphics session");
111 if (!room || slot < 3 || slot > 6 || sheet_id < 0 ||
113 return absl::InvalidArgumentError(
"Choose a roomset slot (3-6) and sheet");
118 return absl::InvalidArgumentError(
119 "Room backgrounds require a loaded 3bpp sheet");
121 const int group = room->blockset();
123 return absl::OutOfRangeError(
"Room graphics group is unavailable");
126 const auto after =
static_cast<uint8_t
>(sheet_id);
127 if (before == after) {
128 return absl::OkStatus();
131 auto apply = [
this, owner =
game_data_, source_rom =
rom_, group, slot](
132 uint8_t expected, uint8_t value) -> absl::Status {
134 return absl::FailedPreconditionError(
135 "Graphics assignment belongs to another session");
138 if (assigned != expected) {
139 return absl::FailedPreconditionError(
140 "Graphics assignment changed elsewhere; Undo was not applied");
145 affected.MarkGraphicsDirty();
149 return absl::OkStatus();
153 group, slot, before, after, std::move(apply)));
154 return absl::OkStatus();
160 room_viewers_.ForEach([](
int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
162 viewer->CancelMinecartGesture();
163 viewer->object_interaction().HandleMouseRelease();
169 viewer->CancelMinecartGesture();
170 viewer->object_interaction().HandleMouseRelease();
194 viewer->TriggerChangePing();
199 description.empty() ?
"Undid last dungeon edit"
200 : absl::StrFormat(
"Undid: %s", description),
216 viewer->TriggerChangePing();
221 description.empty() ?
"Redid last dungeon edit"
222 : absl::StrFormat(
"Redid: %s", description),
233 return viewer->object_interaction().HandleDeleteSelected();
235 return absl::OkStatus();
240 return viewer->object_interaction().HandleCopySelected();
242 return absl::OkStatus();
247 return viewer->object_interaction().HandlePasteObjects();
249 return absl::OkStatus();
260 return static_cast<size_t>(domain) -
268 [](
const auto& x,
const auto& y) {
269 return x.position == y.position && x.type == y.type &&
270 x.direction == y.direction && x.byte1 == y.byte1 &&
275 [](
const auto& x,
const auto& y) {
276 return x.position == y.position && x.item == y.item;
281 const zelda3::Room& room,
const std::vector<SelectedEntity>& selected) {
282 std::vector<SelectedEntity> valid;
283 for (
const auto entity : selected) {
284 if ((entity.type == EntityType::Door &&
285 entity.index < room.
GetDoors().size()) ||
286 (entity.type == EntityType::Sprite &&
288 (entity.type == EntityType::Item &&
290 valid.push_back(entity);
303 [
this](
int target,
int source,
const std::string& json,
311 [
this](
int room_id,
size_t index) {
313 if (!room || room->rom() !=
rom_ || index >= room->GetDoors().
size()) {
316 const auto door = room->
GetDoors()[index];
321 target->object_interaction().SetCurrentRoom(&
rooms_, room_id);
322 target->object_interaction().ClearSelection();
323 target->object_interaction().SelectEntity(EntityType::Door, index);
324 const auto [x, y, width, height] = door.GetEditorBounds();
325 target->ScrollToTile((x + width / 2) / 8, (y + height / 2) / 8);
326 target->TriggerCanvasPingRect(x, y, width, height);
341 [
this](
const absl::Status& status) {
344 absl::StrFormat(
"Edit not applied: %s", status.message()),
353 [
this](
const std::vector<RoomMetadataRequest>& requests) {
357 [
this](
int room_id,
size_t index, uint8_t item_id,
bool big_chest) {
358 return EditChest(room_id, index, item_id, big_chest);
367 [
this](
int room_id,
const std::vector<zelda3::RoomObject>& objects,
368 const std::vector<chest_data>& chests) {
382 absl::StrFormat(
"Edit not applied: %s", status.message()),
389 [
this](
int id,
const std::vector<zelda3::Room::Door>& doors) {
392 door_handler.SetMutationErrorCallback([
this](
const absl::Status& status) {
395 absl::StrFormat(
"Edit not applied: %s", status.message()),
399 auto& sprite_handler =
402 [
this](
int id,
const std::vector<zelda3::Sprite>& sprites) {
405 sprite_handler.SetMutationErrorCallback([
this](
const absl::Status& status) {
408 absl::StrFormat(
"Edit not applied: %s", status.message()),
415 [
this](
int id,
const std::vector<zelda3::PotItem>& items) {
418 item_handler.SetMutationErrorCallback([
this](
const absl::Status& status) {
421 absl::StrFormat(
"Edit not applied: %s", status.message()),
426 [
this](
const std::vector<zelda3::CustomCollisionRoomEntry>& entries) {
441 const auto domain = ctx->last_mutation_domain;
444 }
else if (IsEntityMutation(domain)) {
463 const auto mode = interaction.mode_manager().GetMode();
465 rooms_[rid].MarkObjectsDirty();
466 rooms_[rid].RenderRoomGraphics();
470 }
else if (IsEntityMutation(domain)) {
476 room.MarkObjectsDirty();
478 room.RenderRoomGraphics();
483 interaction.mode_manager().GetModeState().is_painting)) {
488 interaction.mode_manager().GetModeState().is_painting)) {
498 const auto& room =
rooms_[room_id];
500 state.
doors = room.GetDoors();
502 for (
const auto& sprite : room.GetSprites()) {
503 state.
sprites.push_back({sprite.id(), sprite.x(), sprite.y(),
504 sprite.subtype(), sprite.layer(),
505 sprite.key_drop(), sprite.deleted()});
508 state.
items = room.GetPotItems();
513 .entity_coordinator()
516 ->current_room_id == room_id)) {
517 auto& interaction = viewer->object_interaction();
518 state.
entities = interaction.entity_coordinator().GetSelectedEntities();
519 if (state.
entities.empty() && interaction.HasEntitySelection()) {
520 state.
entities.push_back(interaction.GetSelectedEntity());
523 state.
objects = interaction.GetSelectedObjectIndices();
534 if (pending.room_id >= 0) {
537 pending.room_id = room_id;
547 if (pending.room_id != room_id) {
551 if (SameEntityData(pending.before, after)) {
557 const bool removed = after.doors.size() < pending.before.doors.size() ||
558 after.sprites.size() < pending.before.sprites.size() ||
559 after.items.size() < pending.before.items.size();
564 std::erase_if(after.entities,
565 [
type](
const auto& entity) { return entity.type == type; });
568 .entity_coordinator()
572 viewer->object_interaction().entity_coordinator().SetSelectedEntities(
577 room_id, domain, std::move(pending.before), std::move(after),
580 return RestoreRoomEntities(rid, restored_domain, snapshot);
589 if (pending.room_id >= 0) {
598 return absl::InvalidArgumentError(
"Invalid dungeon entity undo target");
602 return absl::FailedPreconditionError(
603 "Dungeon entity undo room is not loaded");
606 room->GetDoors() = snapshot.
doors;
607 room->MarkObjectStreamDirty();
609 std::vector<zelda3::Sprite> restored;
610 restored.reserve(snapshot.
sprites.size());
611 for (
const auto& sprite : snapshot.
sprites) {
612 restored.emplace_back(sprite.id, sprite.x, sprite.y, sprite.subtype,
614 restored.back().set_key_drop(sprite.key_drop);
615 restored.back().set_deleted(sprite.deleted);
617 room->GetSprites() = std::move(restored);
618 room->MarkSpritesDirty();
620 room->GetPotItems() = snapshot.
items;
621 room->MarkPotItemsDirty();
623 room->MarkObjectsDirty();
625 room->RenderRoomGraphics();
630 if (viewer->object_interaction()
631 .entity_coordinator()
634 ->current_room_id == room_id) {
635 auto& interaction = viewer->object_interaction();
636 interaction.CancelPlacement();
637 std::vector<size_t> objects;
638 for (
size_t index : snapshot.
objects) {
639 if (index < room->GetTileObjects().size()) {
640 objects.push_back(index);
643 interaction.SetSelectedObjects(objects);
644 interaction.entity_coordinator().SetSelectedEntities(
645 ValidEntitySelection(*room, snapshot.
entities));
646 viewer->TriggerChangePing();
650 return absl::OkStatus();
654 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
660 "BeginUndoSnapshot called twice without FinalizeUndoAction. "
661 "Previous snapshot for room %d is being leaked. Finalizing now.",
678 viewer->object_interaction().GetSelectedObjectIndices();
686 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
689 auto after_objects =
rooms_[room_id].GetTileObjects();
690 std::vector<size_t> after_selection;
694 after_selection = viewer->object_interaction().GetSelectedObjectIndices();
697 auto action = std::make_unique<DungeonObjectsAction>(
700 rooms_[room_id].GetChests(),
701 [
this](
int rid,
const std::vector<zelda3::RoomObject>& objects,
702 const std::vector<size_t>& selected_indices,
703 const std::vector<chest_data>& chests) {
704 return RestoreRoomObjects(rid, objects, selected_indices, chests);
721 std::string(paired.status().message()));
723 LOG_WARN(
"DungeonEditorV2",
"Track collision edit refused: %s",
724 paired.status().message());
738 int room_id,
const std::vector<zelda3::RoomObject>& objects,
739 const std::vector<size_t>& selected_indices,
740 const std::vector<chest_data>& chests) {
742 if (!loaded_room || loaded_room->rom() !=
rom_) {
743 return absl::FailedPreconditionError(
"Room objects are not loaded");
745 auto& room = *loaded_room;
746 if (room.GetChests().size() != chests.size() ||
748 chests.begin(), chests.end(), room.GetChests().begin(),
749 [](
auto a,
auto b) { return a.id == b.id && a.size == b.size; })) {
750 room.GetChests() = chests;
751 room.MarkChestsDirty();
753 const auto previous_objects = room.GetTileObjects();
754 room.SetTileObjects(objects);
755 room.RenderRoomGraphics();
759 std::vector<size_t> valid_selection;
760 for (
size_t index : selected_indices) {
761 if (index < objects.size()) {
762 valid_selection.push_back(index);
765 viewer->object_interaction().SetSelectedObjects(valid_selection);
766 viewer->TriggerObjectChangePing(previous_objects, objects);
773 return absl::OkStatus();
779 room_viewers_.ForEach([](
int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
781 viewer->InvalidateConnectedRoomGraph();
787 viewer->InvalidateConnectedRoomGraph();
793 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
809 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
812 auto after =
rooms_[room_id].custom_collision();
819 auto action = std::make_unique<DungeonCustomCollisionAction>(
822 RestoreRoomCustomCollision(rid, map);
836 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
839 auto& room =
rooms_[room_id];
840 room.custom_collision() = map;
841 room.MarkCustomCollisionDirty();
868 : entries_(std::move(entries)) {}
874 for (
const auto& entry : entries_) {
875 entry.room->custom_collision() = entry.map;
877 entry.room->MarkCustomCollisionDirty();
879 entry.room->ClearCustomCollisionDirty();
888 bool committed_ =
false;
898 const auto expected_snapshot = expected;
899 const auto stamp_snapshot = stamp;
902 if (!room || !
rom_ ||
904 return absl::FailedPreconditionError(
905 "Load a room with expanded collision support");
907 if (!CollisionMapsEqual(room->custom_collision(), expected_snapshot)) {
908 return absl::FailedPreconditionError(
909 "Collision changed; preview the stamp again");
911 if (stamp_snapshot.source &&
912 (stamp_snapshot.size == 1 || stamp_snapshot.size == 2)) {
913 const int x = stamp_snapshot.source->x;
914 const int y = stamp_snapshot.source->y;
915 if (x >= 0 && y >= 0 && x <= 64 - stamp_snapshot.size &&
916 y <= 64 - stamp_snapshot.size) {
917 for (
const auto& sprite : room->GetSprites()) {
918 if (sprite.deleted() || sprite.layer() != 0 || sprite.x() < 0 ||
919 sprite.y() < 0 || sprite.x() > 31 || sprite.y() > 31)
921 const int actor_x = sprite.x() * 2;
922 const int actor_y = sprite.y() * 2;
923 if (actor_x < x || actor_y < y || actor_x >= x + stamp_snapshot.size ||
924 actor_y >= y + stamp_snapshot.size)
926 const size_t offset = actor_y * 64 + actor_x;
927 if (!expected_snapshot.HasOverride(offset))
929 const auto value = expected_snapshot.tiles[offset];
930 if (sprite.id() == 0xA3 && value >= 0xB7 && value <= 0xBA)
931 return absl::FailedPreconditionError(
932 "Move this cart and its stop with the Cart Starts marker");
933 if (sprite.id() == 0xB0 && value >= 0xD0 && value <= 0xD3)
934 return absl::FailedPreconditionError(
935 "Move this switch track with Tracks > Nodes");
941 if (CollisionMapsEqual(expected_snapshot, after))
942 return absl::OkStatus();
944 const auto before = expected_snapshot;
945 auto action = std::make_unique<DungeonCustomCollisionAction>(
946 room_id, before, after,
950 CollisionBatchRollback rollback(
951 {{room, before, room->custom_collision_dirty()}});
952 room->custom_collision() = after;
953 room->MarkCustomCollisionDirty();
956 }
catch (
const std::exception& error) {
957 return absl::InternalError(
958 absl::StrFormat(
"Could not record rail undo: %s", error.what()));
964 return absl::OkStatus();
968 const std::vector<zelda3::TrackCollisionResult>& preview,
970 if (preview.empty()) {
971 return absl::InvalidArgumentError(
"Minecart collision preview is empty");
974 std::unordered_set<int> seen_room_ids;
975 std::vector<DungeonCustomCollisionSnapshot> before;
976 std::vector<DungeonCustomCollisionSnapshot> after;
977 std::vector<CollisionRollbackEntry> rollback_entries;
978 before.reserve(preview.size());
979 after.reserve(preview.size());
980 rollback_entries.reserve(preview.size());
983 for (
const auto& expected : preview) {
984 const int room_id = expected.room_id;
986 return absl::OutOfRangeError(absl::StrFormat(
987 "Minecart collision room 0x%03X is out of range", room_id));
989 if (!seen_room_ids.insert(room_id).second) {
990 return absl::InvalidArgumentError(absl::StrFormat(
991 "Minecart collision preview contains duplicate room 0x%03X",
996 if (room ==
nullptr) {
997 return absl::FailedPreconditionError(absl::StrFormat(
998 "Minecart collision room 0x%03X is not loaded", room_id));
1001 return absl::FailedPreconditionError(absl::StrFormat(
1002 "Room 0x%03X already has custom collision; generation will not "
1009 current.room_id = room_id;
1010 if (!current.collision_map.has_data || current.tiles_generated <= 0) {
1011 return absl::FailedPreconditionError(absl::StrFormat(
1012 "Room 0x%03X no longer contains supported minecart track pieces",
1015 if (!CollisionMapsEqual(current.collision_map, expected.collision_map) ||
1016 current.tiles_generated != expected.tiles_generated ||
1017 current.stop_count != expected.stop_count ||
1018 current.corner_count != expected.corner_count ||
1019 current.switch_count != expected.switch_count) {
1020 return absl::FailedPreconditionError(absl::StrFormat(
1021 "Minecart collision preview for room 0x%03X is stale; preview "
1022 "again before applying",
1027 after.push_back({room_id, current.collision_map});
1028 rollback_entries.push_back(
1036 [](
const auto& pending) { return pending.room_id >= 0; })) {
1037 return absl::FailedPreconditionError(
1038 "Finish the current dungeon edit before applying minecart collision");
1041 auto action = std::make_unique<DungeonCustomCollisionBatchAction>(
1043 [
this](
const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
1047 CollisionBatchRollback rollback(std::move(rollback_entries));
1048 for (
const auto& snapshot : after) {
1051 room->MarkCustomCollisionDirty();
1055 }
catch (
const std::exception& error) {
1056 return absl::InternalError(absl::StrFormat(
1057 "Could not record minecart collision undo: %s", error.what()));
1065 for (
const auto& snapshot : after) {
1067 viewer->TriggerChangePing();
1070 return absl::OkStatus();
1074 const std::vector<zelda3::CustomCollisionRoomEntry>& entries) {
1077 return absl::FailedPreconditionError(
1078 "Load a room with expanded collision support");
1082 std::unordered_set<int> seen_room_ids;
1083 for (
const auto& entry : entries) {
1085 return absl::OutOfRangeError(absl::StrFormat(
1086 "Collision import room 0x%03X is out of range", entry.room_id));
1088 if (!seen_room_ids.insert(entry.room_id).second) {
1089 return absl::InvalidArgumentError(absl::StrFormat(
1090 "Collision import contains duplicate room 0x%03X", entry.room_id));
1094 std::vector<DungeonCustomCollisionSnapshot> before;
1095 std::vector<DungeonCustomCollisionSnapshot> after;
1096 std::vector<CollisionRollbackEntry> rollback_entries;
1097 before.reserve(entries.size());
1098 after.reserve(entries.size());
1099 rollback_entries.reserve(entries.size());
1101 for (
const auto& entry : entries) {
1102 const int room_id = entry.room_id;
1108 return absl::Status(map_or.status().code(),
1109 absl::StrFormat(
"Room 0x%03X: %s", room_id,
1110 map_or.status().message()));
1116 for (
const auto& tile : entry.tiles) {
1117 if (tile.offset >= imported_map.
tiles.size()) {
1120 imported_map.
SetOverride(tile.offset, tile.value);
1128 after.push_back({room_id, imported_map});
1129 rollback_entries.push_back(
1133 if (after.empty()) {
1134 return absl::OkStatus();
1137 auto action = std::make_unique<DungeonCustomCollisionBatchAction>(
1139 [
this](
const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
1143 CollisionBatchRollback rollback(std::move(rollback_entries));
1144 for (
const auto& snapshot : after) {
1145 auto& room =
rooms_[snapshot.room_id];
1146 room.custom_collision() = snapshot.map;
1147 room.MarkCustomCollisionDirty();
1151 }
catch (
const std::exception& error) {
1152 return absl::InternalError(absl::StrFormat(
1153 "Could not record collision import undo: %s", error.what()));
1161 for (
const auto& snapshot : after) {
1163 viewer->TriggerChangePing();
1166 return absl::OkStatus();
1170 const std::vector<DungeonCustomCollisionSnapshot>& snapshots) {
1171 std::unordered_set<int> seen_room_ids;
1172 for (
const auto& snapshot : snapshots) {
1174 return absl::OutOfRangeError(
"Collision undo room ID is out of range");
1176 if (!seen_room_ids.insert(snapshot.room_id).second) {
1177 return absl::InvalidArgumentError(
1178 "Collision undo contains a duplicate room ID");
1181 return absl::FailedPreconditionError(
1182 "Collision undo target room is not loaded");
1186 for (
const auto& snapshot : snapshots) {
1189 room.MarkCustomCollisionDirty();
1191 viewer->TriggerChangePing();
1199 return absl::OkStatus();
1210 for (
size_t i = 0; i < zone.tiles.size(); ++i) {
1211 if (zone.tiles[i] != 0) {
1212 snap.
offsets.push_back(
static_cast<uint16_t
>(i));
1221 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
1237 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
1240 auto after = MakeWaterFillSnapshot(
rooms_[room_id]);
1248 auto action = std::make_unique<DungeonWaterFillAction>(
1251 RestoreRoomWaterFill(rid, snap);
1261 if (room_id < 0 || room_id >=
static_cast<int>(
rooms_.
size()))
1264 auto& room =
rooms_[room_id];
1265 room.ClearWaterFillZone();
1267 for (uint16_t off : snap.
offsets) {
1268 const int x =
static_cast<int>(off % 64);
1269 const int y =
static_cast<int>(off / 64);
1270 room.SetWaterFillTile(x, y,
true);
1272 room.MarkWaterFillDirty();
void set_dirty(bool dirty)
const auto & vector() const
InteractionContext * context() const
Get the interaction context.
void SetDoorMutationPreflight(DoorMutationPreflight callback)
void SetChestEditCallback(ChestEditCallback callback)
void SetMetadataBatchEditCallback(MetadataBatchEditCallback callback)
void SetDoorConnectionCallbacks(DoorConnectionPreviewCallback preview, DoorConnectionApplyCallback apply)
void SetRoomTransferCallbacks(RoomDocumentExportCallback export_document, RoomTransferPreviewCallback preview, RoomTransferApplyCallback apply)
DungeonObjectInteraction & object_interaction()
void SetChestDeleteCallback(ChestDeleteCallback callback)
void SetMetadataEditCallback(MetadataEditCallback callback)
void SetDoorConnectionNavigationCallback(std::function< void(int, size_t)> callback)
void BeginCollisionUndoSnapshot(int room_id)
class MinecartTrackEditorPanel * minecart_track_editor_panel_
absl::Status ApplyCustomCollisionImportBatch(const std::vector< zelda3::CustomCollisionRoomEntry > &entries)
absl::Status EditRoomGraphicsSheet(int room_id, int slot, int sheet_id)
void ConfigureViewerUndoHooks(DungeonCanvasViewer *viewer)
absl::Status PreflightObjectMutation(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< chest_data > &chests, bool check_streams=true)
void RestoreRoomWaterFill(int room_id, const WaterFillSnapshot &snap)
absl::Status RestoreRoomEntities(int room_id, MutationDomain domain, const DungeonEntitySnapshot &snapshot)
PendingUndo pending_undo_
void FinalizeSelectionUndoAction()
void BeginEntityUndoSnapshot(int room_id, MutationDomain domain)
absl::Status ApplyRoomTransfer(const DungeonRoomTransferPlan &plan)
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
DungeonEntitySnapshot CaptureRoomEntities(int room_id, MutationDomain domain)
absl::Status CommitSelectionEdit(const DungeonSelectionEditPlan &plan, bool continuous)
absl::Status PreflightPotItemPlacement(int room_id, const std::vector< zelda3::PotItem > &items)
void FinalizeWaterFillUndoAction(int room_id)
absl::Status ApplyDoorConnection(const DungeonConnectionPlan &plan)
absl::Status Copy() override
absl::Status EditRoomMetadataBatch(const std::vector< RoomMetadataRequest > &requests)
bool IsWorkbenchWorkflowEnabled() const
void OnRoomSelected(int room_id, bool request_focus=true)
void FinalizePendingUndoActions()
absl::Status ApplyMinecartCollisionStamp(int room_id, const zelda3::CustomCollisionMap &expected, const MinecartCollisionStamp &stamp)
void FinalizeEntityUndoAction(int room_id, MutationDomain domain)
PendingCollisionUndo pending_collision_undo_
absl::Status PreflightDoorMutation(int room_id, const std::vector< zelda3::Room::Door > &doors)
ObjectSelectorContent * object_selector_panel_
absl::Status RestoreRoomObjects(int room_id, const std::vector< zelda3::RoomObject > &objects, const std::vector< size_t > &selected_indices, const std::vector< chest_data > &chests)
absl::Status PreflightSpriteMutation(int room_id, const std::vector< zelda3::Sprite > &sprites)
absl::Status EditRoomMetadata(int room_id, const RoomMetadataEdit &edit)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
PendingWaterFillUndo pending_water_fill_undo_
absl::Status Cut() override
std::array< PendingEntityUndo, 3 > pending_entity_undo_
absl::Status Paste() override
static bool IsValidRoomId(int room_id)
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)
DungeonCanvasViewer * GetViewerForRoom(int room_id)
PendingSelectionUndo pending_selection_undo_
absl::StatusOr< std::string > ExportRoomDocument(int room_id)
void BeginWaterFillUndoSnapshot(int room_id)
absl::Status Undo() override
absl::Status RestoreRoomCustomCollisionBatch(const std::vector< DungeonCustomCollisionSnapshot > &snapshots)
void InvalidateConnectedRoomGraphs()
void FinalizePendingEntityUndoActions()
absl::Status DeleteChest(int room_id, size_t index)
absl::Status Redo() override
absl::StatusOr< DungeonRoomTransferPlan > PreviewRoomTransfer(int target_room_id, int source_room_id, const std::string &json, const DungeonRoomTransferOptions &options)
void BeginUndoSnapshot(int room_id)
zelda3::GameData * game_data_
DungeonSelectionClipboard selection_clipboard_
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
void RestoreRoomCustomCollision(int room_id, const zelda3::CustomCollisionMap &map)
bool undo_restore_triggered_ping_
absl::Status EditChest(int room_id, size_t index, uint8_t item_id, bool big_chest)
int current_room_id() const
absl::Status ApplyMinecartCollisionBatch(const std::vector< zelda3::TrackCollisionResult > &preview, const zelda3::GeneratorOptions &options)
void FinalizeCollisionUndoAction(int room_id)
absl::StatusOr< DungeonConnectionPlan > PreviewDoorConnection(const DungeonConnectionRequest &request)
void SetCacheInvalidationCallback(std::function< void()> callback)
void SetSelectionEditCallbacks(std::function< absl::Status(const DungeonSelectionEditPlan &, bool)> edit, std::function< void()> finish, std::function< void(const absl::Status &)> error={})
MutationDomain last_invalidation_domain() const
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SetMutationCallback(std::function< void()> callback)
void SetCollisionBatchImportCallback(std::function< absl::Status(const std::vector< zelda3::CustomCollisionRoomEntry > &)> callback)
void SetSharedSelectionClipboard(DungeonSelectionClipboard *clipboard)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
void ForEachMaterialized(Fn &&fn)
EditorContext context() const
UndoManager undo_manager_
EditorDependencies dependencies_
ItemInteractionHandler & item_handler()
SpriteInteractionHandler & sprite_handler()
TileObjectHandler & tile_handler()
DoorInteractionHandler & door_handler()
void SetItemPlacementPreflight(ItemPlacementPreflight preflight)
void InvalidateRoomAudit()
void SetPlacementError(const std::string &message)
void SetSpriteMutationPreflight(SpriteMutationPreflight callback)
void SetObjectMutationPreflight(ObjectMutationPreflight callback)
void SetMutationErrorCallback(std::function< void(const absl::Status &)> callback)
void SetBeforeObjectCommandCallback(std::function< void()> callback)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
Abstract base for all undoable actions (Command pattern)
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)
absl::Status Redo() override
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
RoomGraphicsSheetAction(int group, int slot, uint8_t before, uint8_t after, Apply apply)
absl::Status Undo() override
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
std::function< absl::Status(uint8_t, uint8_t)> Apply
bool HasSheet(uint16_t sheet) const
True when the buffer holds all 4096 bytes of sheet.
static constexpr uint16_t kSheetCount
const CustomCollisionMap & custom_collision() const
bool custom_collision_dirty() const
const WaterFillZoneMap & water_fill_zone() const
const std::vector< Door > & GetDoors() const
const std::vector< zelda3::Sprite > & GetSprites() const
bool has_custom_collision() const
const std::vector< PotItem > & GetPotItems() const
uint8_t water_fill_sram_bit_mask() const
Locates graphics sheets in the ROM and writes edited sheets back.
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
bool IsEntityMutation(MutationDomain domain)
std::vector< SelectedEntity > ValidEntitySelection(const zelda3::Room &room, const std::vector< SelectedEntity > &selected)
size_t EntityDomainIndex(MutationDomain domain)
bool SameEntityData(const DungeonEntitySnapshot &a, const DungeonEntitySnapshot &b)
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.
MutationDomain
Domain/type of mutation for undo + invalidation routing.
absl::StatusOr< zelda3::CustomCollisionMap > PreviewMinecartCollisionStamp(const zelda3::CustomCollisionMap &before, const MinecartCollisionStamp &stamp)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
#define RETURN_IF_ERROR(expr)
zelda3::CustomCollisionMap before
std::optional< DungeonSelectionEditPlan > plan
zelda3::Room::SaveDirtySnapshot dirty_before
std::vector< size_t > before_selection
std::vector< chest_data > before_chests
std::vector< zelda3::RoomObject > before_objects
std::vector< DungeonSpriteSnapshot > sprites
std::vector< size_t > objects
std::vector< zelda3::PotItem > items
std::vector< SelectedEntity > entities
std::vector< zelda3::Room::Door > doors
ToastManager * toast_manager
std::vector< uint16_t > offsets
zelda3::CustomCollisionMap map
bool HasOverrides() const
std::array< uint8_t, 64 *64 > tiles
void SetOverride(size_t offset, uint8_t tile)
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
GraphicsSheetStore sheet_store