6#include <unordered_set>
7#include "absl/strings/str_format.h"
13#include "imgui/imgui.h"
38 SDL_Surface* surface = source->surface();
40 if (!palette || palette->ncolors <= 0) {
44 std::vector<SDL_Color> colors(256, {0, 0, 0, 0});
45 const int color_count = std::min(palette->ncolors, 256);
46 std::copy_n(palette->colors, color_count, colors.begin());
47 colors[255] = {0, 0, 0, 0};
50 SDL_SetColorKey(ghost.
surface(), SDL_TRUE, 255);
51 SDL_SetSurfaceBlendMode(ghost.
surface(), SDL_BLENDMODE_BLEND);
60 bool selected =
false;
65 return std::clamp(
static_cast<int>(
object.GetLayerValue()), 0, 2);
69 size_t object_count) {
70 std::unordered_set<size_t> index_set;
71 for (
size_t index : indices) {
72 if (index < object_count) {
73 index_set.insert(index);
80 const std::vector<zelda3::RoomObject>& objects,
81 const std::unordered_set<size_t>& selected_indices) {
82 std::array<std::vector<LayerOrderEntry>, 3> buckets;
83 for (
size_t i = 0; i < objects.size(); ++i) {
92 const std::vector<size_t>& selected_indices_after_reorder) {
96 if (selected_indices_after_reorder.empty()) {
100 selection->
SelectObject(selected_indices_after_reorder.front(),
102 for (
size_t ordinal = 1; ordinal < selected_indices_after_reorder.size();
104 selection->
SelectObject(selected_indices_after_reorder[ordinal],
110 std::array<std::vector<LayerOrderEntry>, 3>& buckets,
111 std::vector<std::optional<size_t>>& sources,
112 std::vector<size_t>& selected_indices_after_reorder) {
113 std::vector<zelda3::RoomObject> reordered;
114 reordered.reserve(objects.size());
116 sources.reserve(objects.size());
117 for (
auto& bucket : buckets) {
118 for (
auto& entry : bucket) {
119 if (entry.selected) {
120 selected_indices_after_reorder.push_back(reordered.size());
122 sources.push_back(entry.old_index);
123 reordered.push_back(std::move(entry.object));
126 objects = std::move(reordered);
130 std::vector<std::optional<size_t>> sources;
131 sources.reserve(count);
132 for (
size_t index = 0; index < count; ++index) {
133 sources.push_back(index);
138bool SameChests(
const std::vector<chest_data>& lhs,
139 const std::vector<chest_data>& rhs) {
140 return lhs.size() == rhs.size() &&
141 std::equal(lhs.begin(), lhs.end(), rhs.begin(), [](
auto a,
auto b) {
142 return a.id == b.id && a.size == b.size;
156 const std::vector<zelda3::RoomObject>& rhs) {
157 return lhs.size() == rhs.size() &&
166 const std::vector<zelda3::RoomObject>& original,
167 const std::vector<zelda3::RoomObject>& candidate,
168 const std::vector<std::optional<size_t>>& sources) {
169 if (sources.size() != candidate.size()) {
170 return absl::InvalidArgumentError(
"Object source mapping is incomplete");
172 std::unordered_set<size_t> retained;
173 for (
size_t i = 0; i < candidate.size(); ++i) {
175 const auto source = *sources[i];
176 if (source >= original.size()) {
177 return absl::InvalidArgumentError(
"Object source is unavailable");
179 if (retained.insert(source).second &&
192 return absl::OkStatus();
205 auto [x, y, width, height] =
207 return GuideRect{x, y, x + width, y + height, x + width / 2, y + height / 2};
211 constexpr int kDuplicateTolerancePx = 2;
212 if (std::any_of(guides.begin(), guides.end(), [&](
int guide) {
213 return std::abs(guide - value) <= kDuplicateTolerancePx;
217 guides.push_back(value);
222 ImU32 color,
float thickness) {
223 constexpr float kDash = 6.0f;
224 constexpr float kGap = 4.0f;
225 const bool vertical = std::abs(start.x - end.x) < 0.5f;
227 vertical ? std::abs(end.y - start.y) : std::abs(end.x - start.x);
228 for (
float offset = 0.0f; offset < total; offset += kDash + kGap) {
229 const float segment_end = std::min(offset + kDash, total);
231 const float y0 = start.y + offset;
232 const float y1 = start.y + segment_end;
233 draw_list->AddLineV(start.x, y0, y1, color, thickness);
235 const float x0 = start.x + offset;
236 const float x1 = start.x + segment_end;
237 draw_list->AddLineH(x0, x1, start.y, color, thickness);
247 return GhostCapacityState::kNormal;
250 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
257 if (room_id < 0 || room_id >=
static_cast<int>(
ctx_->
rooms->
size())) {
288 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
300 if (!hovered.has_value())
303 const ImGuiIO& io = ImGui::GetIO();
313 }
else if (io.KeyCtrl || io.KeySuper) {
336 static_cast<int>(start_pos.y));
340 const ImVec2& mouse_pos,
bool mouse_left_down,
bool mouse_left_released,
341 bool shift_down,
bool toggle_down,
bool alt_down,
bool draw_box) {
350 if (mouse_left_down) {
352 static_cast<int>(mouse_pos.y));
361 if (mouse_left_released) {
363 static_cast<int>(mouse_pos.y));
371 constexpr int kMinRectPixels = 6;
380 return ctx_->IsObjectVisibleForSelection(object);
397 const int tile_dx =
static_cast<int>(drag_delta.x) / 8;
398 const int tile_dy =
static_cast<int>(drag_delta.y) / 8;
404 const std::pair<int, int> requested_delta{tile_dx, tile_dy};
409 if (inc_dx != 0 || inc_dy != 0) {
433 if (had_mutation &&
ctx_) {
447 const int resize_delta = (delta > 0.0f) ? 1 : -1;
448 const bool horizontal = ImGui::GetCurrentContext() && ImGui::GetIO().KeyShift;
477 if (!pointer_screen_pos.has_value()) {
482 const auto [canvas_x, canvas_y] =
484 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
495 auto [snap_canvas_x, snap_canvas_y] =
RoomToCanvas(room_x, room_y);
498 ImDrawList* draw_list = ImGui::GetWindowDrawList();
500 static_cast<float>(snap_canvas_x + preview_geometry.offset_x_tiles * 8),
501 static_cast<float>(snap_canvas_y + preview_geometry.offset_y_tiles * 8)));
503 ImVec2(
static_cast<float>(preview_geometry.width_pixels),
504 static_cast<float>(preview_geometry.height_pixels)));
505 const ImVec2 preview_end(preview_start.x + preview_size.x,
506 preview_start.y + preview_size.y);
508 ImVec2(
static_cast<float>(snap_canvas_x -
509 preview_geometry.render_anchor_x_tiles * 8),
510 static_cast<float>(snap_canvas_y -
511 preview_geometry.render_anchor_y_tiles * 8)));
513 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
518 theme, capacity_state, theme.dungeon_selection_primary);
519 bool drew_bitmap =
false;
523 if (bitmap.texture()) {
524 const int crop_width =
525 std::min(preview_geometry.buffer_width_pixels, bitmap.width());
526 const int crop_height =
527 std::min(preview_geometry.buffer_height_pixels, bitmap.height());
529 static_cast<float>(crop_width),
static_cast<float>(crop_height)));
530 const ImVec2 bitmap_end(bitmap_start.x + bitmap_size.x,
531 bitmap_start.y + bitmap_size.y);
532 const ImVec2 uv_end(
static_cast<float>(crop_width) / bitmap.width(),
533 static_cast<float>(crop_height) / bitmap.height());
534 ImVec4 tint = capacity_state == GhostCapacityState::kNormal
539 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), bitmap_start,
540 bitmap_end, ImVec2(0, 0), uv_end,
541 ImGui::GetColorU32(tint));
542 draw_list->AddRect(preview_start, preview_end,
543 ImGui::GetColorU32(outline_color), 0.0f, 2.0f);
549 draw_list->AddRectFilled(
550 preview_start, preview_end,
552 ImVec4(outline_color.x, outline_color.y, outline_color.z, 0.25f)));
553 draw_list->AddRect(preview_start, preview_end,
554 ImGui::GetColorU32(outline_color), 0.0f, 2.0f);
559 draw_list->AddText(ImVec2(preview_start.x + 2, preview_start.y + 1),
560 ImGui::GetColorU32(theme.text_primary), id_text.c_str());
563 if (capacity_state != GhostCapacityState::kNormal &&
564 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
565 ImGui::SetTooltip(tr(
"Objects: %zu/%zu\n%s"), current_obj_count,
570 const std::string badge_text = absl::StrFormat(
573 ImVec2(preview_start.x, preview_end.y + 6.0f),
574 theme, capacity_state, badge_text);
588 auto result = zelda3::DimensionService::Get().GetDimensions(obj);
589 return std::make_tuple(result.offset_x_tiles * 8,
590 result.offset_y_tiles * 8, result.width_pixels(),
591 result.height_pixels());
600 int canvas_x,
int canvas_y)
const {
610 for (
size_t i = objects.size(); i > 0; --i) {
611 size_t index = i - 1;
612 const auto&
object = objects[index];
624 auto [obj_tile_x, obj_tile_y, width_tiles, height_tiles] =
627 int obj_px = obj_tile_x * 8;
628 int obj_py = obj_tile_y * 8;
629 int w_px = width_tiles * 8;
630 int h_px = height_tiles * 8;
632 if (canvas_x >= obj_px && canvas_x < obj_px + w_px && canvas_y >= obj_py &&
633 canvas_y < obj_py + h_px) {
641 const std::vector<zelda3::RoomObject>& objects)
const {
648 if (selected.empty()) {
652 constexpr int kAlignmentTolerancePx = 2;
653 constexpr size_t kMaxGuidesPerAxis = 8;
654 std::vector<int> vertical_guides;
655 std::vector<int> horizontal_guides;
657 for (
size_t selected_index : selected) {
658 if (selected_index >= objects.size()) {
661 const GuideRect selected_rect = GetGuideRect(objects[selected_index]);
662 const std::array<int, 3> selected_x = {
663 selected_rect.left, selected_rect.center_x, selected_rect.right};
664 const std::array<int, 3> selected_y = {
665 selected_rect.top, selected_rect.center_y, selected_rect.bottom};
667 for (
size_t other_index = 0; other_index < objects.size(); ++other_index) {
671 const GuideRect other_rect = GetGuideRect(objects[other_index]);
672 const std::array<int, 3> other_x = {other_rect.left, other_rect.center_x,
674 const std::array<int, 3> other_y = {other_rect.top, other_rect.center_y,
677 for (
int sx : selected_x) {
678 for (
int ox : other_x) {
679 if (std::abs(sx - ox) <= kAlignmentTolerancePx) {
680 AddUniqueGuide(vertical_guides, ox);
684 for (
int sy : selected_y) {
685 for (
int oy : other_y) {
686 if (std::abs(sy - oy) <= kAlignmentTolerancePx) {
687 AddUniqueGuide(horizontal_guides, oy);
691 if (vertical_guides.size() >= kMaxGuidesPerAxis &&
692 horizontal_guides.size() >= kMaxGuidesPerAxis) {
698 if (vertical_guides.empty() && horizontal_guides.empty()) {
703 ImVec4 guide_color = theme.accent_color;
704 guide_color.w = 0.78f;
705 const ImU32 color = ImGui::GetColorU32(guide_color);
706 ImDrawList* draw_list = ImGui::GetWindowDrawList();
712 const size_t vertical_count =
713 std::min(vertical_guides.size(), kMaxGuidesPerAxis);
714 for (
size_t i = 0; i < vertical_count; ++i) {
717 DrawDashedLine(draw_list, ImVec2(x, canvas_pos.y),
718 ImVec2(x, canvas_pos.y + room_size.y), color, 1.2f);
721 const size_t horizontal_count =
722 std::min(horizontal_guides.size(), kMaxGuidesPerAxis);
723 for (
size_t i = 0; i < horizontal_count; ++i) {
726 DrawDashedLine(draw_list, ImVec2(canvas_pos.x, y),
727 ImVec2(canvas_pos.x + room_size.x, y), color, 1.2f);
744 int room_id, std::vector<zelda3::RoomObject> candidate,
745 const std::vector<std::optional<size_t>>& sources,
746 const std::vector<std::optional<chest_data>>& chest_overrides,
747 std::optional<std::vector<size_t>> selection) {
751 return RejectMutation(absl::InvalidArgumentError(
"Room is unavailable"));
755 "Select the destination room before changing its objects"));
758 candidate.size() > room->GetTileObjects().size()) {
760 absl::ResourceExhaustedError(
"Room object limit reached"));
762 const auto admission =
763 ValidateObjectCandidates(room->GetTileObjects(), candidate, sources);
764 if (!admission.ok()) {
769 candidate, sources, chest_overrides);
773 const bool chests_changed = !SameChests(room->GetChests(), *chests);
774 if (!chests_changed &&
775 SameAuthoredObjects(room->GetTileObjects(), candidate)) {
785 const auto* source_context =
ctx_;
786 const auto before_objects = room->GetTileObjects();
787 const auto before_chests = room->GetChests();
793 !SameAuthoredObjects(room->GetTileObjects(), before_objects) ||
794 !SameChests(room->GetChests(), before_chests)) {
796 "Room objects changed while finishing the previous gesture; repeat "
805 room->SetTileObjects(candidate);
806 if (chests_changed) {
807 room->GetChests() = std::move(*chests);
808 room->MarkChestsDirty();
810 if (selection &&
ctx_) {
818 const std::vector<size_t>& indices,
819 int delta_x,
int delta_y,
820 bool notify_mutation) {
823 if (!room || indices.empty())
827 "Select the destination room before changing its objects"));
830 auto& objects = room->GetTileObjects();
831 const auto before = objects;
832 auto candidate = before;
833 for (
size_t index : indices) {
834 if (index < candidate.size()) {
835 candidate[index].x_ =
836 std::clamp(
static_cast<int>(candidate[index].x_) + delta_x, 0, 63);
837 candidate[index].y_ =
838 std::clamp(
static_cast<int>(candidate[index].y_) + delta_y, 0, 63);
841 if (SameAuthoredObjects(objects, candidate)) {
844 const auto admission = ValidateObjectCandidates(
845 objects, candidate, IdentitySources(objects.size()));
846 if (!admission.ok()) {
850 const auto* source_context =
ctx_;
858 !SameAuthoredObjects(room->GetTileObjects(), before)) {
860 "Room objects changed during edit preflight; repeat the edit"));
863 if (notify_mutation &&
ctx_)
865 objects = std::move(candidate);
871 const std::vector<size_t>& indices,
875 if (!room || indices.empty())
877 auto candidate = room->GetTileObjects();
878 for (
size_t index : indices) {
879 if (index >= candidate.size() || candidate[index].id_ == new_id) {
884 "Use the dedicated torch or pushable-block controls to change "
885 "special-table objects"));
888 const uint8_t canonical_size =
890 candidate[index].set_id(new_id);
891 candidate[index].set_size(canonical_size);
892 auto options = candidate[index].options() & ~zelda3::ObjectOption::Chest;
896 candidate[index].set_options(options);
899 IdentitySources(room->GetTileObjects().size()));
903 const std::vector<size_t>& indices,
907 if (!room || indices.empty())
910 auto& objects = room->GetTileObjects();
917 return new_size <= 0x0F &&
918 new_size < manager.GetSubtypeCount(
object.id_) &&
919 !manager.ResolveFilename(
object.id_, new_size).empty();
923 const bool has_change =
924 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
925 if (index >= objects.size() || !can_update_size(objects[index])) {
928 return objects[index].size_ !=
934 auto candidate = objects;
936 for (
size_t index : indices) {
937 if (index < objects.size() && can_update_size(candidate[index])) {
938 const uint8_t canonical_size =
940 if (candidate[index].size_ == canonical_size) {
943 candidate[index].size_ = canonical_size;
944 candidate[index].tiles_loaded_ =
false;
949 if (!admission.ok()) {
950 RejectMutation(admission);
955 objects = std::move(candidate);
959bool TileObjectHandler::UpdateObjectsLayer(
int room_id,
960 const std::vector<size_t>& indices,
962 mutation_status_ = absl::OkStatus();
963 auto* room = GetRoom(room_id);
964 if (!room || indices.empty())
966 if (new_layer < 0 || new_layer > 2) {
968 absl::InvalidArgumentError(
"Invalid object stream target");
970 "Rejected layer update with invalid target layer: %d", new_layer);
973 auto& objects = room->GetTileObjects();
974 std::vector<size_t> deduped_indices;
975 deduped_indices.reserve(indices.size());
976 std::unordered_set<size_t> seen_indices;
977 for (
size_t index : indices) {
978 if (index >= objects.size()) {
981 if (seen_indices.insert(index).second) {
982 deduped_indices.push_back(index);
985 if (deduped_indices.empty()) {
989 if (deduped_indices.size() > kMaxLayerBatchMutation) {
991 absl::ResourceExhaustedError(
"Object stream batch is too large");
993 "Rejected layer batch mutation of %zu objects (max %zu)",
994 deduped_indices.size(), kMaxLayerBatchMutation);
998 auto candidate_objects = objects;
1000 deduped_indices, new_layer);
1001 if (!mutation.ok()) {
1002 mutation_status_ = mutation.status();
1003 LOG_WARN(
"TileObjectHandler",
"Rejected object stream mutation: %s",
1004 std::string(mutation.status().message()).c_str());
1007 if (!mutation->changed) {
1011 std::vector<std::optional<size_t>> sources(objects.size());
1012 for (
size_t old_index = 0; old_index < objects.size(); ++old_index) {
1013 sources[mutation->old_to_new_index[old_index]] = old_index;
1015 return CommitCandidate(room_id, std::move(candidate_objects), sources, {},
1016 mutation->selected_indices);
1019std::vector<size_t> TileObjectHandler::DuplicateObjects(
1020 int room_id,
const std::vector<size_t>& indices,
int delta_x,
int delta_y) {
1021 mutation_status_ = absl::OkStatus();
1022 auto* room = GetRoom(room_id);
1023 if (!room || indices.empty())
1025 const auto& objects = room->GetTileObjects();
1026 auto candidate = objects;
1027 auto sources = IdentitySources(objects.size());
1028 std::vector<size_t> new_indices;
1029 std::unordered_set<size_t> seen;
1030 for (
size_t index : indices) {
1031 if (index < objects.size() && seen.insert(index).second) {
1032 auto clone = objects[index].CopyForNewPlacement();
1037 clone.x_ = std::clamp(
static_cast<int>(clone.x_ + delta_x), 0, 63);
1038 clone.y_ = std::clamp(
static_cast<int>(clone.y_ + delta_y), 0, 63);
1039 new_indices.push_back(candidate.size());
1040 candidate.push_back(std::move(clone));
1041 sources.push_back(index);
1044 if (new_indices.empty() || !CommitCandidate(room_id, std::move(candidate),
1045 sources, {}, new_indices)) {
1051bool TileObjectHandler::DeleteObjects(
int room_id,
1052 std::vector<size_t> indices) {
1053 mutation_status_ = absl::OkStatus();
1054 auto* room = GetRoom(room_id);
1055 if (!room || indices.empty())
1057 const auto& objects = room->GetTileObjects();
1058 const auto removed = MakeValidIndexSet(indices, objects.size());
1059 if (removed.empty()) {
1062 std::vector<zelda3::RoomObject> candidate;
1063 std::vector<std::optional<size_t>> sources;
1064 for (
size_t index = 0; index < objects.size(); ++index) {
1065 if (!removed.contains(index)) {
1066 candidate.push_back(objects[index]);
1067 sources.push_back(index);
1070 return CommitCandidate(room_id, std::move(candidate), sources, {},
1071 std::vector<size_t>{});
1074bool TileObjectHandler::DeleteAllObjects(
int room_id) {
1075 return CommitCandidate(room_id, {}, {}, {}, std::vector<size_t>{});
1078void TileObjectHandler::SendToFront(
int room_id,
1079 const std::vector<size_t>& indices) {
1080 mutation_status_ = absl::OkStatus();
1081 auto* room = GetRoom(room_id);
1082 if (!room || indices.empty())
1084 auto objects = room->GetTileObjects();
1085 auto selected_set = MakeValidIndexSet(indices, objects.size());
1086 if (selected_set.empty()) {
1089 auto buckets = BuildLayerBuckets(objects, selected_set);
1090 for (
auto& bucket : buckets) {
1091 std::vector<LayerOrderEntry> other;
1092 std::vector<LayerOrderEntry> selected;
1093 other.reserve(bucket.size());
1094 selected.reserve(bucket.size());
1095 for (
auto& entry : bucket) {
1096 if (entry.selected) {
1097 selected.push_back(std::move(entry));
1099 other.push_back(std::move(entry));
1102 bucket = std::move(other);
1103 bucket.insert(bucket.end(), std::make_move_iterator(selected.begin()),
1104 std::make_move_iterator(selected.end()));
1106 std::vector<std::optional<size_t>> sources;
1107 std::vector<size_t> selected_indices;
1108 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1109 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1112void TileObjectHandler::SendToBack(
int room_id,
1113 const std::vector<size_t>& indices) {
1114 mutation_status_ = absl::OkStatus();
1115 auto* room = GetRoom(room_id);
1116 if (!room || indices.empty())
1118 auto objects = room->GetTileObjects();
1119 auto selected_set = MakeValidIndexSet(indices, objects.size());
1120 if (selected_set.empty()) {
1123 auto buckets = BuildLayerBuckets(objects, selected_set);
1124 for (
auto& bucket : buckets) {
1125 std::vector<LayerOrderEntry> selected;
1126 std::vector<LayerOrderEntry> other;
1127 selected.reserve(bucket.size());
1128 other.reserve(bucket.size());
1129 for (
auto& entry : bucket) {
1130 if (entry.selected) {
1131 selected.push_back(std::move(entry));
1133 other.push_back(std::move(entry));
1136 bucket = std::move(selected);
1137 bucket.insert(bucket.end(), std::make_move_iterator(other.begin()),
1138 std::make_move_iterator(other.end()));
1140 std::vector<std::optional<size_t>> sources;
1141 std::vector<size_t> selected_indices;
1142 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1143 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1146void TileObjectHandler::MoveForward(
int room_id,
1147 const std::vector<size_t>& indices) {
1148 mutation_status_ = absl::OkStatus();
1149 auto* room = GetRoom(room_id);
1150 if (!room || indices.empty())
1152 auto objects = room->GetTileObjects();
1153 auto selected_set = MakeValidIndexSet(indices, objects.size());
1154 if (selected_set.empty()) {
1157 auto buckets = BuildLayerBuckets(objects, selected_set);
1158 for (
auto& bucket : buckets) {
1159 if (bucket.size() < 2) {
1162 for (
size_t i = bucket.size() - 1; i > 0; --i) {
1163 const size_t previous = i - 1;
1164 if (bucket[previous].selected && !bucket[i].selected) {
1165 std::swap(bucket[previous], bucket[i]);
1169 std::vector<std::optional<size_t>> sources;
1170 std::vector<size_t> selected_indices;
1171 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1172 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1175void TileObjectHandler::MoveBackward(
int room_id,
1176 const std::vector<size_t>& indices) {
1177 mutation_status_ = absl::OkStatus();
1178 auto* room = GetRoom(room_id);
1179 if (!room || indices.empty())
1181 auto objects = room->GetTileObjects();
1182 auto selected_set = MakeValidIndexSet(indices, objects.size());
1183 if (selected_set.empty()) {
1186 auto buckets = BuildLayerBuckets(objects, selected_set);
1187 for (
auto& bucket : buckets) {
1188 if (bucket.size() < 2) {
1191 for (
size_t i = 1; i < bucket.size(); ++i) {
1192 const size_t previous = i - 1;
1193 if (bucket[i].selected && !bucket[previous].selected) {
1194 std::swap(bucket[i], bucket[previous]);
1198 std::vector<std::optional<size_t>> sources;
1199 std::vector<size_t> selected_indices;
1200 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1201 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1204bool TileObjectHandler::ResizeObjects(
int room_id,
1205 const std::vector<size_t>& indices,
1206 int delta,
bool horizontal,
1208 mutation_status_ = absl::OkStatus();
1209 auto* room = GetRoom(room_id);
1210 if (!room || indices.empty())
1212 if (ctx_->current_room_id != room_id) {
1213 return RejectMutation(absl::FailedPreconditionError(
1214 "Select the destination room before changing its objects"));
1216 auto& objects = room->GetTileObjects();
1217 const auto before = objects;
1220 object.id_,
object.size_, delta)
1224 const bool has_change =
1225 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
1226 return index < objects.size() &&
1227 zelda3::IsRoomObjectResizable(objects[index].id_) &&
1228 objects[index].size_ != resized_size(objects[index]);
1233 auto candidate = objects;
1235 for (
size_t index : indices) {
1236 if (index < objects.size() &&
1238 const uint8_t new_size = resized_size(candidate[index]);
1239 if (candidate[index].size_ == new_size) {
1242 candidate[index].size_ = new_size;
1243 candidate[index].tiles_loaded_ =
false;
1246 const auto admission = ValidateObjectCandidates(
1247 objects, candidate, IdentitySources(objects.size()));
1248 if (!admission.ok()) {
1249 RejectMutation(admission);
1252 if (object_mutation_preflight_) {
1253 const auto* source_context = ctx_;
1255 object_mutation_preflight_(room_id, candidate, room->GetChests());
1256 if (!mutation_status_.ok()) {
1257 return RejectMutation(mutation_status_);
1259 if (ctx_ != source_context || !ctx_ || ctx_->current_room_id != room_id ||
1260 GetRoom(room_id) != room ||
1261 !SameAuthoredObjects(room->GetTileObjects(), before)) {
1262 return RejectMutation(absl::FailedPreconditionError(
1263 "Room objects changed during edit preflight; repeat the edit"));
1267 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1268 objects = std::move(candidate);
1273bool TileObjectHandler::PlaceObjectAt(
int room_id,
1276 mutation_status_ = absl::OkStatus();
1277 auto* room = GetRoom(room_id);
1279 placement_block_reason_ = PlacementBlockReason::kInvalidRoom;
1280 return RejectMutation(absl::InvalidArgumentError(
"Room is unavailable"));
1285 placement_block_reason_ = PlacementBlockReason::kObjectLimit;
1286 return RejectMutation(
1287 absl::ResourceExhaustedError(
"Room object limit reached"));
1290 placement_block_reason_ = PlacementBlockReason::kNone;
1291 auto new_obj =
object.CopyForNewPlacement();
1296 new_obj.x_ = std::clamp(x, 0, 63);
1297 new_obj.y_ = std::clamp(y, 0, 63);
1298 const size_t placed_index = room->GetTileObjects().size();
1299 auto candidate = room->GetTileObjects();
1300 auto sources = IdentitySources(candidate.size());
1301 candidate.push_back(new_obj);
1302 sources.push_back(std::nullopt);
1303 if (!CommitCandidate(room_id, std::move(candidate), sources, {},
1304 std::vector<size_t>{placed_index})) {
1305 placement_block_reason_ = PlacementBlockReason::kChestValidation;
1308 if (placement_policy_ == PlacementPolicy::kOnce) {
1311 if (ctx_ && ctx_->selection) {
1312 ctx_->NotifySelectionChanged();
1314 TriggerSuccessToast();
1315 if (placement_callback_) {
1316 placement_callback_(new_obj);
1322 preview_object_ = object;
1323 RefreshPreviewGraphics();
1326bool TileObjectHandler::SetPreviewSize(uint8_t size) {
1327 if (!object_placement_mode_ ||
1333 if (size > 0x0F || size >= manager.GetSubtypeCount(preview_object_.id_) ||
1334 manager.ResolveFilename(preview_object_.id_, size).empty()) {
1338 const uint8_t canonical_size =
1340 if (preview_object_.size_ != canonical_size) {
1341 preview_object_.size_ = canonical_size;
1342 preview_object_.tiles_loaded_ =
false;
1343 RefreshPreviewGraphics();
1348bool TileObjectHandler::SetPreviewLayer(
int layer) {
1349 if (!object_placement_mode_ || layer < 0 || layer > 2 ||
1353 if (preview_object_.GetLayerValue() != layer) {
1355 preview_object_.tiles_loaded_ =
false;
1356 RefreshPreviewGraphics();
1361void TileObjectHandler::RefreshPreviewGraphics() {
1362 if (object_placement_mode_) {
1363 RenderGhostPreviewBitmap();
1367void TileObjectHandler::RenderGhostPreviewBitmap() {
1368 ghost_preview_bitmap_ready_ =
false;
1369 if (!ctx_ || !ctx_->rom || !ctx_->rom->is_loaded())
1372 auto* room = GetRoom(ctx_->current_room_id);
1373 if (!room || !room->IsLoaded())
1376 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
1381 if (!ghost_preview_buffer_) {
1382 ghost_preview_buffer_ = std::make_unique<gfx::BackgroundBuffer>(
1383 kGhostPreviewBufferSize, kGhostPreviewBufferSize);
1385 ghost_preview_buffer_->EnsureBitmapInitialized();
1386 ghost_preview_buffer_->ClearBuffer();
1387 auto& bitmap = ghost_preview_buffer_->bitmap();
1388 ApplyRoomPaletteToGhost(*room, bitmap);
1389 std::fill(bitmap.mutable_data().begin(), bitmap.mutable_data().end(), 255);
1390 bitmap.set_modified(
true);
1391 const uint8_t* gfx_data = room->get_gfx_buffer().data();
1399 auto render_object = preview_object_;
1400 render_object.x_ = preview_geometry.render_anchor_x_tiles;
1401 render_object.y_ = preview_geometry.render_anchor_y_tiles;
1407 drawer.
DrawObject(render_object, *ghost_preview_buffer_,
1408 *ghost_preview_buffer_, ctx_->current_palette_group);
1413 if (bitmap.size() > 0) {
1414 bitmap.UpdateSurfacePixels();
1415 if (bitmap.texture()) {
1416 ghost_preview_create_queued_ =
false;
1419 }
else if (!ghost_preview_create_queued_) {
1422 ghost_preview_create_queued_ =
true;
1425 if (bitmap.texture()) {
1426 ghost_preview_create_queued_ =
false;
1428 ghost_preview_bitmap_ready_ =
true;
1429 ghost_preview_room_id_ = ctx_->current_room_id;
1430 ghost_preview_graphics_revision_ = room->graphics_revision();
1435TileObjectHandler::CalculateGhostPreviewGeometry(
1438 const auto [resolved_anchor_x, resolved_anchor_y] =
1442 const int anchor_x =
1443 std::max({0, resolved_anchor_x, -dimensions.offset_x_tiles});
1444 const int anchor_y =
1445 std::max({0, resolved_anchor_y, -dimensions.offset_y_tiles});
1446 const int leading_x_tiles = anchor_x + dimensions.offset_x_tiles;
1447 const int leading_y_tiles = anchor_y + dimensions.offset_y_tiles;
1450 .render_anchor_y_tiles = anchor_y,
1451 .offset_x_tiles = dimensions.offset_x_tiles,
1452 .offset_y_tiles = dimensions.offset_y_tiles,
1453 .width_pixels = dimensions.width_pixels(),
1454 .height_pixels = dimensions.height_pixels(),
1455 .buffer_width_pixels = (leading_x_tiles + dimensions.width_tiles) * 8,
1456 .buffer_height_pixels = (leading_y_tiles + dimensions.height_tiles) * 8,
1464void TileObjectHandler::CopyObjectsToClipboard(
1465 int room_id,
const std::vector<size_t>& indices) {
1466 auto* room = GetRoom(room_id);
1467 if (!room || indices.empty())
1471 const auto& objects = room->GetTileObjects();
1472 const bool includes_chest =
1473 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
1474 return index < objects.size() &&
1475 zelda3::IsStatefulChestObjectId(objects[index].id_);
1477 if (includes_chest) {
1481 for (
size_t index : indices) {
1482 if (index >= objects.size()) {
1485 clipboard_.push_back(objects[index]);
1486 std::optional<chest_data>
chest;
1489 if (!chest_index || *chest_index >= room->GetChests().size() ||
1490 room->GetChests()[*chest_index].size !=
1491 (objects[index].id_ == 0xFB1)) {
1492 clipboard_status_ = absl::FailedPreconditionError(
1493 "Cannot copy a chest with missing or mismatched contents");
1495 chest = room->GetChests()[*chest_index];
1498 clipboard_chests_.push_back(
chest);
1502std::vector<size_t> TileObjectHandler::PasteFromClipboard(
int room_id,
1505 mutation_status_ = clipboard_status_;
1506 auto* room = GetRoom(room_id);
1507 if (!room || clipboard_.empty())
1509 if (!mutation_status_.ok()) {
1510 RejectMutation(mutation_status_);
1513 auto candidate = room->GetTileObjects();
1514 auto sources = IdentitySources(candidate.size());
1515 std::vector<std::optional<chest_data>> overrides(candidate.size());
1516 std::vector<size_t> new_indices;
1517 for (
size_t index = 0; index < clipboard_.size(); ++index) {
1518 auto object = clipboard_[index].CopyForNewPlacement();
1523 object.x_ = std::clamp(
object.x_ + offset_x, 0, 63);
1524 object.y_ = std::clamp(
object.y_ + offset_y, 0, 63);
1525 object.tiles_loaded_ =
false;
1526 new_indices.push_back(candidate.size());
1527 candidate.push_back(std::move(
object));
1528 sources.push_back(std::nullopt);
1529 overrides.push_back(clipboard_chests_[index]);
1531 if (!CommitCandidate(room_id, std::move(candidate), sources, overrides,
1538std::vector<size_t> TileObjectHandler::PasteFromClipboardAt(
int room_id,
1541 if (clipboard_.empty())
1544 int offset_x = target_x - clipboard_[0].x_;
1545 int offset_y = target_y - clipboard_[0].y_;
1547 return PasteFromClipboard(room_id, offset_x, offset_y);
std::optional< ImVec2 > GetPointerScreenPosition() const
DungeonCanvasTransform GetCanvasTransform() const
std::pair< int, int > CanvasToRoom(int canvas_x, int canvas_y) const
Convert canvas pixel coordinates to room tile coordinates.
InteractionContext * ctx_
bool IsWithinBounds(int canvas_x, int canvas_y) const
Check if coordinates are within room bounds.
zelda3::Room * GetCurrentRoom() const
Get current room (convenience method)
bool HasValidContext() const
Check if context is valid.
std::pair< int, int > RoomToCanvas(int room_x, int room_y) const
Convert room tile coordinates to canvas pixel coordinates.
Manages object selection state and operations for the dungeon editor.
bool IsRectangleSelectionActive() const
Check if a rectangle selection is in progress.
void UpdateRectangleSelection(int canvas_x, int canvas_y)
Update rectangle selection endpoint.
void DrawSelectionHighlights(gui::Canvas *canvas, const std::vector< zelda3::RoomObject > &objects, std::function< std::tuple< int, int, int, int >(const zelda3::RoomObject &)> bounds_calculator)
Draw selection highlights for all selected objects.
bool IsObjectSelected(size_t index) const
Check if an object is selected.
void EndRectangleSelection(const std::vector< zelda3::RoomObject > &objects, SelectionMode mode=SelectionMode::Single, std::function< bool(const zelda3::RoomObject &)> is_object_visible={})
Complete rectangle selection operation.
std::vector< size_t > GetSelectedIndices() const
Get all selected object indices.
bool PassesLayerFilterForObject(const zelda3::RoomObject &object) const
Check if an object passes the current layer filter.
bool IsRectangleLargeEnough(int min_pixels) const
Check if rectangle selection exceeds a minimum pixel size.
void SelectObject(size_t index, SelectionMode mode=SelectionMode::Single)
Select a single object by index.
void ClearSelection()
Clear all selections.
void BeginRectangleSelection(int canvas_x, int canvas_y)
Begin a rectangle selection operation.
void CancelRectangleSelection()
Cancel rectangle selection without modifying selection.
void DrawRectangleSelectionBox(gui::Canvas *canvas)
Draw the active rectangle selection box.
bool HasSelection() const
Check if any objects are selected.
Handles functional mutations and queries for tile objects.
absl::Status mutation_status_
zelda3::RoomObject preview_object_
void DrawGhostPreview() override
Draw ghost preview during placement.
void DrawSmartGuides(const std::vector< zelda3::RoomObject > &objects) const
bool ghost_preview_bitmap_ready_
void HandleMarqueeSelection(const ImVec2 &mouse_pos, bool mouse_left_down, bool mouse_left_released, bool shift_down, bool toggle_down, bool alt_down, bool draw_box=true)
void BeginPlacement() override
Begin placement mode.
bool HandleMouseWheel(float delta) override
std::unique_ptr< gfx::BackgroundBuffer > ghost_preview_buffer_
ObjectMutationPreflight object_mutation_preflight_
void UpdateObjectsSize(int room_id, const std::vector< size_t > &indices, uint8_t new_size)
bool MoveObjects(int room_id, const std::vector< size_t > &indices, int delta_x, int delta_y, bool notify_mutation=true)
Move a set of objects by a tile delta.
int ghost_preview_room_id_
void RenderGhostPreviewBitmap()
bool SetPreviewSize(uint8_t size)
bool CommitCandidate(int room_id, std::vector< zelda3::RoomObject > candidate, const std::vector< std::optional< size_t > > &sources, const std::vector< std::optional< chest_data > > &chest_overrides={}, std::optional< std::vector< size_t > > selection=std::nullopt)
ImVec2 ApplyDragModifiers(const ImVec2 &delta) const
void HandleRelease() override
Handle mouse release.
bool drag_has_duplicated_
uint64_t ghost_preview_graphics_revision_
void HandleDrag(ImVec2 current_pos, ImVec2 delta) override
Handle mouse drag.
std::optional< std::pair< int, int > > drag_rejected_delta_
void DrawSelectionHighlight() override
Draw selection highlight for selected entities.
bool PlaceObjectAt(int room_id, const zelda3::RoomObject &object, int x, int y)
Place a new object. Returns false if blocked by ROM limits.
bool HandleClick(int canvas_x, int canvas_y) override
Handle mouse click at canvas position.
std::function< void()> before_object_command_callback_
bool object_placement_mode_
void BeginMarqueeSelection(const ImVec2 &start_pos)
bool RejectMutation(absl::Status status)
void CancelPlacement() override
Cancel current placement.
bool drag_mutation_started_
static GhostPreviewGeometry CalculateGhostPreviewGeometry(const zelda3::RoomObject &object)
Resolve the render anchor and visual extent used by placement previews.
bool ResizeObjects(int room_id, const std::vector< size_t > &indices, int delta, bool horizontal=false, bool uniform=false)
Resize objects by a delta. Uniform changes both packed axes; otherwise horizontal selects packed-floo...
void UpdateObjectsId(int room_id, const std::vector< size_t > &indices, int16_t new_id)
zelda3::Room * GetRoom(int room_id)
PlacementCapacityState GhostCapacityState
std::function< void(const absl::Status &) mutation_error_callback_)
void NotifyChange(zelda3::Room *room)
GhostCapacityState GetPlacementGhostCapacityState() const
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void InitDrag(const ImVec2 &start_pos)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
void ProcessTextureQueue(IRenderer *renderer)
Represents a bitmap image optimized for SNES ROM hacking.
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
SDL_Surface * surface() const
static CustomObjectManager & Get()
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
DimensionResult GetDimensions(const RoomObject &obj) const
std::tuple< int, int, int, int > GetHitTestBounds(const RoomObject &obj) const
Draws dungeon objects to background buffers using game patterns.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
void SetRoomFloorGraphics(uint8_t floor1, uint8_t floor2)
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, gfx::BackgroundBuffer *layout_bg2=nullptr)
Draw a room object to background buffers.
std::pair< int, int > ResolveAnchor(int16_t object_id, uint8_t size_byte) const
Resolve the canvas anchor (x, y) for a given object's draw routine.
static ObjectGeometry & Get()
uint8_t torch_reserved_bit_
uint8_t block_behavior_layer_
void MarkTileObjectCollectionDirty()
auto & object_bg2_buffer()
uint64_t graphics_revision() const
const std::vector< RoomObject > & GetTileObjects() const
auto & object_bg1_buffer()
struct chest chest
Treasure chest.
#define LOG_WARN(category, format,...)
const AgentUITheme & GetTheme()
absl::Status ValidateObjectCandidates(const std::vector< zelda3::RoomObject > &original, const std::vector< zelda3::RoomObject > &candidate, const std::vector< std::optional< size_t > > &sources)
void DrawDashedLine(ImDrawList *draw_list, ImVec2 start, ImVec2 end, ImU32 color, float thickness)
constexpr int kGhostPreviewBufferSize
void RestoreObjectSelection(ObjectSelection *selection, const std::vector< size_t > &selected_indices_after_reorder)
constexpr size_t kMaxLayerBatchMutation
bool SameAuthoredObject(const zelda3::RoomObject &a, const zelda3::RoomObject &b)
bool ApplyRoomPaletteToGhost(const zelda3::Room &room, gfx::Bitmap &ghost)
std::unordered_set< size_t > MakeValidIndexSet(const std::vector< size_t > &indices, size_t object_count)
void FlattenLayerBuckets(std::vector< zelda3::RoomObject > &objects, std::array< std::vector< LayerOrderEntry >, 3 > &buckets, std::vector< std::optional< size_t > > &sources, std::vector< size_t > &selected_indices_after_reorder)
GuideRect GetGuideRect(const zelda3::RoomObject &object)
int LayerBucketIndex(const zelda3::RoomObject &object)
std::vector< std::optional< size_t > > IdentitySources(size_t count)
bool AddUniqueGuide(std::vector< int > &guides, int value)
bool SameAuthoredObjects(const std::vector< zelda3::RoomObject > &lhs, const std::vector< zelda3::RoomObject > &rhs)
std::array< std::vector< LayerOrderEntry >, 3 > BuildLayerBuckets(const std::vector< zelda3::RoomObject > &objects, const std::unordered_set< size_t > &selected_indices)
constexpr int kRoomPixelHeight
constexpr int kRoomPixelWidth
ImVec2 SnapToTileGrid(const ImVec2 &point)
Snap a point to the 8px tile grid.
Editors are the view controllers for the application.
void DrawPlacementCapacityBadge(ImDrawList *draw_list, const ImVec2 &badge_min, const AgentUITheme &theme, PlacementCapacityState state, std::string_view primary_text)
ImVec4 GetPlacementAccentColor(const AgentUITheme &theme, PlacementCapacityState state, const ImVec4 &normal_color)
std::string_view GetPlacementCapacityTooltipSuffix(PlacementCapacityState state)
PlacementCapacityState GetPlacementCapacityState(size_t current_count, size_t max_count)
absl::Status ValidateChestObjectMapping(const std::vector< RoomObject > &objects, const std::vector< chest_data > &chests)
uint8_t ResizeRoomObjectUniformlyByDelta(int object_id, uint8_t size, int delta)
uint8_t ResizeRoomObjectByDelta(int object_id, uint8_t size, int delta, bool horizontal)
std::optional< size_t > ChestIndexForObject(const std::vector< RoomObject > &objects, size_t object_index)
bool IsRoomObjectSizeEditable(int object_id)
constexpr size_t kMaxTileObjects
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
bool UsesRoomObjectStream(const RoomObject &object)
constexpr bool IsStatefulChestObjectId(int object_id)
bool UsesSpecialLayerSelector(const RoomObject &object)
absl::StatusOr< ObjectStorageMutationResult > ReassignObjectStorage(std::vector< RoomObject > &objects, const std::vector< size_t > &indices, int target_value)
absl::Status ValidateRoomObjectStreamEntryForSave(const RoomObject &object)
bool IsRoomObjectResizable(int object_id)
absl::StatusOr< std::vector< chest_data > > PlanChestObjectEdit(const std::vector< RoomObject > &before_objects, const std::vector< chest_data > &before_chests, const std::vector< RoomObject > &after_objects, const std::vector< std::optional< size_t > > &source_indices, const std::vector< std::optional< chest_data > > &chest_overrides)
SDL2/SDL3 compatibility layer.
bool IsObjectVisibleForSelection(const zelda3::RoomObject &object) const
void NotifyInvalidateCache(MutationDomain domain=MutationDomain::kUnknown) const
Notify that cache invalidation is needed.
ObjectSelection * selection
void NotifyMutation(MutationDomain domain=MutationDomain::kUnknown) const
Notify that a mutation is about to happen.
int render_anchor_x_tiles
zelda3::RoomObject object