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->AddLine(ImVec2(start.x, y0), ImVec2(start.x, y1), color,
236 const float x0 = start.x + offset;
237 const float x1 = start.x + segment_end;
238 draw_list->AddLine(ImVec2(x0, start.y), ImVec2(x1, start.y), color,
249 return GhostCapacityState::kNormal;
252 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
259 if (room_id < 0 || room_id >=
static_cast<int>(
ctx_->
rooms->
size())) {
290 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
302 if (!hovered.has_value())
305 const ImGuiIO& io = ImGui::GetIO();
315 }
else if (io.KeyCtrl || io.KeySuper) {
338 static_cast<int>(start_pos.y));
342 const ImVec2& mouse_pos,
bool mouse_left_down,
bool mouse_left_released,
343 bool shift_down,
bool toggle_down,
bool alt_down,
bool draw_box) {
352 if (mouse_left_down) {
354 static_cast<int>(mouse_pos.y));
363 if (mouse_left_released) {
365 static_cast<int>(mouse_pos.y));
373 constexpr int kMinRectPixels = 6;
382 return ctx_->IsObjectVisibleForSelection(object);
399 const int tile_dx =
static_cast<int>(drag_delta.x) / 8;
400 const int tile_dy =
static_cast<int>(drag_delta.y) / 8;
406 const std::pair<int, int> requested_delta{tile_dx, tile_dy};
411 if (inc_dx != 0 || inc_dy != 0) {
435 if (had_mutation &&
ctx_) {
449 const int resize_delta = (delta > 0.0f) ? 1 : -1;
450 const bool horizontal = ImGui::GetCurrentContext() && ImGui::GetIO().KeyShift;
479 if (!pointer_screen_pos.has_value()) {
484 const auto [canvas_x, canvas_y] =
486 auto [room_x, room_y] =
CanvasToRoom(canvas_x, canvas_y);
497 auto [snap_canvas_x, snap_canvas_y] =
RoomToCanvas(room_x, room_y);
500 ImDrawList* draw_list = ImGui::GetWindowDrawList();
502 static_cast<float>(snap_canvas_x + preview_geometry.offset_x_tiles * 8),
503 static_cast<float>(snap_canvas_y + preview_geometry.offset_y_tiles * 8)));
505 ImVec2(
static_cast<float>(preview_geometry.width_pixels),
506 static_cast<float>(preview_geometry.height_pixels)));
507 const ImVec2 preview_end(preview_start.x + preview_size.x,
508 preview_start.y + preview_size.y);
510 ImVec2(
static_cast<float>(snap_canvas_x -
511 preview_geometry.render_anchor_x_tiles * 8),
512 static_cast<float>(snap_canvas_y -
513 preview_geometry.render_anchor_y_tiles * 8)));
515 const size_t current_obj_count = room ? room->
GetTileObjects().size() : 0;
520 theme, capacity_state, theme.dungeon_selection_primary);
521 bool drew_bitmap =
false;
525 if (bitmap.texture()) {
526 const int crop_width =
527 std::min(preview_geometry.buffer_width_pixels, bitmap.width());
528 const int crop_height =
529 std::min(preview_geometry.buffer_height_pixels, bitmap.height());
531 static_cast<float>(crop_width),
static_cast<float>(crop_height)));
532 const ImVec2 bitmap_end(bitmap_start.x + bitmap_size.x,
533 bitmap_start.y + bitmap_size.y);
534 const ImVec2 uv_end(
static_cast<float>(crop_width) / bitmap.width(),
535 static_cast<float>(crop_height) / bitmap.height());
536 ImVec4 tint = capacity_state == GhostCapacityState::kNormal
541 draw_list->AddImage((ImTextureID)(intptr_t)bitmap.texture(), bitmap_start,
542 bitmap_end, ImVec2(0, 0), uv_end,
543 ImGui::GetColorU32(tint));
544 draw_list->AddRect(preview_start, preview_end,
545 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
551 draw_list->AddRectFilled(
552 preview_start, preview_end,
554 ImVec4(outline_color.x, outline_color.y, outline_color.z, 0.25f)));
555 draw_list->AddRect(preview_start, preview_end,
556 ImGui::GetColorU32(outline_color), 0.0f, 0, 2.0f);
561 draw_list->AddText(ImVec2(preview_start.x + 2, preview_start.y + 1),
562 ImGui::GetColorU32(theme.text_primary), id_text.c_str());
565 if (capacity_state != GhostCapacityState::kNormal &&
566 ImGui::IsMouseHoveringRect(preview_start, preview_end)) {
567 ImGui::SetTooltip(tr(
"Objects: %zu/%zu\n%s"), current_obj_count,
572 const std::string badge_text = absl::StrFormat(
575 ImVec2(preview_start.x, preview_end.y + 6.0f),
576 theme, capacity_state, badge_text);
590 auto result = zelda3::DimensionService::Get().GetDimensions(obj);
591 return std::make_tuple(result.offset_x_tiles * 8,
592 result.offset_y_tiles * 8, result.width_pixels(),
593 result.height_pixels());
602 int canvas_x,
int canvas_y)
const {
612 for (
size_t i = objects.size(); i > 0; --i) {
613 size_t index = i - 1;
614 const auto&
object = objects[index];
626 auto [obj_tile_x, obj_tile_y, width_tiles, height_tiles] =
629 int obj_px = obj_tile_x * 8;
630 int obj_py = obj_tile_y * 8;
631 int w_px = width_tiles * 8;
632 int h_px = height_tiles * 8;
634 if (canvas_x >= obj_px && canvas_x < obj_px + w_px && canvas_y >= obj_py &&
635 canvas_y < obj_py + h_px) {
643 const std::vector<zelda3::RoomObject>& objects)
const {
650 if (selected.empty()) {
654 constexpr int kAlignmentTolerancePx = 2;
655 constexpr size_t kMaxGuidesPerAxis = 8;
656 std::vector<int> vertical_guides;
657 std::vector<int> horizontal_guides;
659 for (
size_t selected_index : selected) {
660 if (selected_index >= objects.size()) {
663 const GuideRect selected_rect = GetGuideRect(objects[selected_index]);
664 const std::array<int, 3> selected_x = {
665 selected_rect.left, selected_rect.center_x, selected_rect.right};
666 const std::array<int, 3> selected_y = {
667 selected_rect.top, selected_rect.center_y, selected_rect.bottom};
669 for (
size_t other_index = 0; other_index < objects.size(); ++other_index) {
673 const GuideRect other_rect = GetGuideRect(objects[other_index]);
674 const std::array<int, 3> other_x = {other_rect.left, other_rect.center_x,
676 const std::array<int, 3> other_y = {other_rect.top, other_rect.center_y,
679 for (
int sx : selected_x) {
680 for (
int ox : other_x) {
681 if (std::abs(sx - ox) <= kAlignmentTolerancePx) {
682 AddUniqueGuide(vertical_guides, ox);
686 for (
int sy : selected_y) {
687 for (
int oy : other_y) {
688 if (std::abs(sy - oy) <= kAlignmentTolerancePx) {
689 AddUniqueGuide(horizontal_guides, oy);
693 if (vertical_guides.size() >= kMaxGuidesPerAxis &&
694 horizontal_guides.size() >= kMaxGuidesPerAxis) {
700 if (vertical_guides.empty() && horizontal_guides.empty()) {
705 ImVec4 guide_color = theme.accent_color;
706 guide_color.w = 0.78f;
707 const ImU32 color = ImGui::GetColorU32(guide_color);
708 ImDrawList* draw_list = ImGui::GetWindowDrawList();
714 const size_t vertical_count =
715 std::min(vertical_guides.size(), kMaxGuidesPerAxis);
716 for (
size_t i = 0; i < vertical_count; ++i) {
719 DrawDashedLine(draw_list, ImVec2(x, canvas_pos.y),
720 ImVec2(x, canvas_pos.y + room_size.y), color, 1.2f);
723 const size_t horizontal_count =
724 std::min(horizontal_guides.size(), kMaxGuidesPerAxis);
725 for (
size_t i = 0; i < horizontal_count; ++i) {
728 DrawDashedLine(draw_list, ImVec2(canvas_pos.x, y),
729 ImVec2(canvas_pos.x + room_size.x, y), color, 1.2f);
746 int room_id, std::vector<zelda3::RoomObject> candidate,
747 const std::vector<std::optional<size_t>>& sources,
748 const std::vector<std::optional<chest_data>>& chest_overrides,
749 std::optional<std::vector<size_t>> selection) {
753 return RejectMutation(absl::InvalidArgumentError(
"Room is unavailable"));
757 "Select the destination room before changing its objects"));
760 candidate.size() > room->GetTileObjects().size()) {
762 absl::ResourceExhaustedError(
"Room object limit reached"));
764 const auto admission =
765 ValidateObjectCandidates(room->GetTileObjects(), candidate, sources);
766 if (!admission.ok()) {
771 candidate, sources, chest_overrides);
775 const bool chests_changed = !SameChests(room->GetChests(), *chests);
776 if (!chests_changed &&
777 SameAuthoredObjects(room->GetTileObjects(), candidate)) {
787 const auto* source_context =
ctx_;
788 const auto before_objects = room->GetTileObjects();
789 const auto before_chests = room->GetChests();
795 !SameAuthoredObjects(room->GetTileObjects(), before_objects) ||
796 !SameChests(room->GetChests(), before_chests)) {
798 "Room objects changed while finishing the previous gesture; repeat "
807 room->SetTileObjects(candidate);
808 if (chests_changed) {
809 room->GetChests() = std::move(*chests);
810 room->MarkChestsDirty();
812 if (selection &&
ctx_) {
820 const std::vector<size_t>& indices,
821 int delta_x,
int delta_y,
822 bool notify_mutation) {
825 if (!room || indices.empty())
829 "Select the destination room before changing its objects"));
832 auto& objects = room->GetTileObjects();
833 const auto before = objects;
834 auto candidate = before;
835 for (
size_t index : indices) {
836 if (index < candidate.size()) {
837 candidate[index].x_ =
838 std::clamp(
static_cast<int>(candidate[index].x_) + delta_x, 0, 63);
839 candidate[index].y_ =
840 std::clamp(
static_cast<int>(candidate[index].y_) + delta_y, 0, 63);
843 if (SameAuthoredObjects(objects, candidate)) {
846 const auto admission = ValidateObjectCandidates(
847 objects, candidate, IdentitySources(objects.size()));
848 if (!admission.ok()) {
852 const auto* source_context =
ctx_;
860 !SameAuthoredObjects(room->GetTileObjects(), before)) {
862 "Room objects changed during edit preflight; repeat the edit"));
865 if (notify_mutation &&
ctx_)
867 objects = std::move(candidate);
873 const std::vector<size_t>& indices,
877 if (!room || indices.empty())
879 auto candidate = room->GetTileObjects();
880 for (
size_t index : indices) {
881 if (index >= candidate.size() || candidate[index].id_ == new_id) {
886 "Use the dedicated torch or pushable-block controls to change "
887 "special-table objects"));
890 const uint8_t canonical_size =
892 candidate[index].set_id(new_id);
893 candidate[index].set_size(canonical_size);
894 auto options = candidate[index].options() & ~zelda3::ObjectOption::Chest;
898 candidate[index].set_options(options);
901 IdentitySources(room->GetTileObjects().size()));
905 const std::vector<size_t>& indices,
909 if (!room || indices.empty())
912 auto& objects = room->GetTileObjects();
919 return new_size <= 0x0F &&
920 new_size < manager.GetSubtypeCount(
object.id_) &&
921 !manager.ResolveFilename(
object.id_, new_size).empty();
925 const bool has_change =
926 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
927 if (index >= objects.size() || !can_update_size(objects[index])) {
930 return objects[index].size_ !=
936 auto candidate = objects;
938 for (
size_t index : indices) {
939 if (index < objects.size() && can_update_size(candidate[index])) {
940 const uint8_t canonical_size =
942 if (candidate[index].size_ == canonical_size) {
945 candidate[index].size_ = canonical_size;
946 candidate[index].tiles_loaded_ =
false;
951 if (!admission.ok()) {
952 RejectMutation(admission);
957 objects = std::move(candidate);
961bool TileObjectHandler::UpdateObjectsLayer(
int room_id,
962 const std::vector<size_t>& indices,
964 mutation_status_ = absl::OkStatus();
965 auto* room = GetRoom(room_id);
966 if (!room || indices.empty())
968 if (new_layer < 0 || new_layer > 2) {
970 absl::InvalidArgumentError(
"Invalid object stream target");
972 "Rejected layer update with invalid target layer: %d", new_layer);
975 auto& objects = room->GetTileObjects();
976 std::vector<size_t> deduped_indices;
977 deduped_indices.reserve(indices.size());
978 std::unordered_set<size_t> seen_indices;
979 for (
size_t index : indices) {
980 if (index >= objects.size()) {
983 if (seen_indices.insert(index).second) {
984 deduped_indices.push_back(index);
987 if (deduped_indices.empty()) {
991 if (deduped_indices.size() > kMaxLayerBatchMutation) {
993 absl::ResourceExhaustedError(
"Object stream batch is too large");
995 "Rejected layer batch mutation of %zu objects (max %zu)",
996 deduped_indices.size(), kMaxLayerBatchMutation);
1000 auto candidate_objects = objects;
1002 deduped_indices, new_layer);
1003 if (!mutation.ok()) {
1004 mutation_status_ = mutation.status();
1005 LOG_WARN(
"TileObjectHandler",
"Rejected object stream mutation: %s",
1006 std::string(mutation.status().message()).c_str());
1009 if (!mutation->changed) {
1013 std::vector<std::optional<size_t>> sources(objects.size());
1014 for (
size_t old_index = 0; old_index < objects.size(); ++old_index) {
1015 sources[mutation->old_to_new_index[old_index]] = old_index;
1017 return CommitCandidate(room_id, std::move(candidate_objects), sources, {},
1018 mutation->selected_indices);
1021std::vector<size_t> TileObjectHandler::DuplicateObjects(
1022 int room_id,
const std::vector<size_t>& indices,
int delta_x,
int delta_y) {
1023 mutation_status_ = absl::OkStatus();
1024 auto* room = GetRoom(room_id);
1025 if (!room || indices.empty())
1027 const auto& objects = room->GetTileObjects();
1028 auto candidate = objects;
1029 auto sources = IdentitySources(objects.size());
1030 std::vector<size_t> new_indices;
1031 std::unordered_set<size_t> seen;
1032 for (
size_t index : indices) {
1033 if (index < objects.size() && seen.insert(index).second) {
1034 auto clone = objects[index].CopyForNewPlacement();
1039 clone.x_ = std::clamp(
static_cast<int>(clone.x_ + delta_x), 0, 63);
1040 clone.y_ = std::clamp(
static_cast<int>(clone.y_ + delta_y), 0, 63);
1041 new_indices.push_back(candidate.size());
1042 candidate.push_back(std::move(clone));
1043 sources.push_back(index);
1046 if (new_indices.empty() || !CommitCandidate(room_id, std::move(candidate),
1047 sources, {}, new_indices)) {
1053bool TileObjectHandler::DeleteObjects(
int room_id,
1054 std::vector<size_t> indices) {
1055 mutation_status_ = absl::OkStatus();
1056 auto* room = GetRoom(room_id);
1057 if (!room || indices.empty())
1059 const auto& objects = room->GetTileObjects();
1060 const auto removed = MakeValidIndexSet(indices, objects.size());
1061 if (removed.empty()) {
1064 std::vector<zelda3::RoomObject> candidate;
1065 std::vector<std::optional<size_t>> sources;
1066 for (
size_t index = 0; index < objects.size(); ++index) {
1067 if (!removed.contains(index)) {
1068 candidate.push_back(objects[index]);
1069 sources.push_back(index);
1072 return CommitCandidate(room_id, std::move(candidate), sources, {},
1073 std::vector<size_t>{});
1076bool TileObjectHandler::DeleteAllObjects(
int room_id) {
1077 return CommitCandidate(room_id, {}, {}, {}, std::vector<size_t>{});
1080void TileObjectHandler::SendToFront(
int room_id,
1081 const std::vector<size_t>& indices) {
1082 mutation_status_ = absl::OkStatus();
1083 auto* room = GetRoom(room_id);
1084 if (!room || indices.empty())
1086 auto objects = room->GetTileObjects();
1087 auto selected_set = MakeValidIndexSet(indices, objects.size());
1088 if (selected_set.empty()) {
1091 auto buckets = BuildLayerBuckets(objects, selected_set);
1092 for (
auto& bucket : buckets) {
1093 std::vector<LayerOrderEntry> other;
1094 std::vector<LayerOrderEntry> selected;
1095 other.reserve(bucket.size());
1096 selected.reserve(bucket.size());
1097 for (
auto& entry : bucket) {
1098 if (entry.selected) {
1099 selected.push_back(std::move(entry));
1101 other.push_back(std::move(entry));
1104 bucket = std::move(other);
1105 bucket.insert(bucket.end(), std::make_move_iterator(selected.begin()),
1106 std::make_move_iterator(selected.end()));
1108 std::vector<std::optional<size_t>> sources;
1109 std::vector<size_t> selected_indices;
1110 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1111 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1114void TileObjectHandler::SendToBack(
int room_id,
1115 const std::vector<size_t>& indices) {
1116 mutation_status_ = absl::OkStatus();
1117 auto* room = GetRoom(room_id);
1118 if (!room || indices.empty())
1120 auto objects = room->GetTileObjects();
1121 auto selected_set = MakeValidIndexSet(indices, objects.size());
1122 if (selected_set.empty()) {
1125 auto buckets = BuildLayerBuckets(objects, selected_set);
1126 for (
auto& bucket : buckets) {
1127 std::vector<LayerOrderEntry> selected;
1128 std::vector<LayerOrderEntry> other;
1129 selected.reserve(bucket.size());
1130 other.reserve(bucket.size());
1131 for (
auto& entry : bucket) {
1132 if (entry.selected) {
1133 selected.push_back(std::move(entry));
1135 other.push_back(std::move(entry));
1138 bucket = std::move(selected);
1139 bucket.insert(bucket.end(), std::make_move_iterator(other.begin()),
1140 std::make_move_iterator(other.end()));
1142 std::vector<std::optional<size_t>> sources;
1143 std::vector<size_t> selected_indices;
1144 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1145 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1148void TileObjectHandler::MoveForward(
int room_id,
1149 const std::vector<size_t>& indices) {
1150 mutation_status_ = absl::OkStatus();
1151 auto* room = GetRoom(room_id);
1152 if (!room || indices.empty())
1154 auto objects = room->GetTileObjects();
1155 auto selected_set = MakeValidIndexSet(indices, objects.size());
1156 if (selected_set.empty()) {
1159 auto buckets = BuildLayerBuckets(objects, selected_set);
1160 for (
auto& bucket : buckets) {
1161 if (bucket.size() < 2) {
1164 for (
size_t i = bucket.size() - 1; i > 0; --i) {
1165 const size_t previous = i - 1;
1166 if (bucket[previous].selected && !bucket[i].selected) {
1167 std::swap(bucket[previous], bucket[i]);
1171 std::vector<std::optional<size_t>> sources;
1172 std::vector<size_t> selected_indices;
1173 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1174 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1177void TileObjectHandler::MoveBackward(
int room_id,
1178 const std::vector<size_t>& indices) {
1179 mutation_status_ = absl::OkStatus();
1180 auto* room = GetRoom(room_id);
1181 if (!room || indices.empty())
1183 auto objects = room->GetTileObjects();
1184 auto selected_set = MakeValidIndexSet(indices, objects.size());
1185 if (selected_set.empty()) {
1188 auto buckets = BuildLayerBuckets(objects, selected_set);
1189 for (
auto& bucket : buckets) {
1190 if (bucket.size() < 2) {
1193 for (
size_t i = 1; i < bucket.size(); ++i) {
1194 const size_t previous = i - 1;
1195 if (bucket[i].selected && !bucket[previous].selected) {
1196 std::swap(bucket[i], bucket[previous]);
1200 std::vector<std::optional<size_t>> sources;
1201 std::vector<size_t> selected_indices;
1202 FlattenLayerBuckets(objects, buckets, sources, selected_indices);
1203 CommitCandidate(room_id, std::move(objects), sources, {}, selected_indices);
1206bool TileObjectHandler::ResizeObjects(
int room_id,
1207 const std::vector<size_t>& indices,
1208 int delta,
bool horizontal,
1210 mutation_status_ = absl::OkStatus();
1211 auto* room = GetRoom(room_id);
1212 if (!room || indices.empty())
1214 if (ctx_->current_room_id != room_id) {
1215 return RejectMutation(absl::FailedPreconditionError(
1216 "Select the destination room before changing its objects"));
1218 auto& objects = room->GetTileObjects();
1219 const auto before = objects;
1222 object.id_,
object.size_, delta)
1226 const bool has_change =
1227 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
1228 return index < objects.size() &&
1229 zelda3::IsRoomObjectResizable(objects[index].id_) &&
1230 objects[index].size_ != resized_size(objects[index]);
1235 auto candidate = objects;
1237 for (
size_t index : indices) {
1238 if (index < objects.size() &&
1240 const uint8_t new_size = resized_size(candidate[index]);
1241 if (candidate[index].size_ == new_size) {
1244 candidate[index].size_ = new_size;
1245 candidate[index].tiles_loaded_ =
false;
1248 const auto admission = ValidateObjectCandidates(
1249 objects, candidate, IdentitySources(objects.size()));
1250 if (!admission.ok()) {
1251 RejectMutation(admission);
1254 if (object_mutation_preflight_) {
1255 const auto* source_context = ctx_;
1257 object_mutation_preflight_(room_id, candidate, room->GetChests());
1258 if (!mutation_status_.ok()) {
1259 return RejectMutation(mutation_status_);
1261 if (ctx_ != source_context || !ctx_ || ctx_->current_room_id != room_id ||
1262 GetRoom(room_id) != room ||
1263 !SameAuthoredObjects(room->GetTileObjects(), before)) {
1264 return RejectMutation(absl::FailedPreconditionError(
1265 "Room objects changed during edit preflight; repeat the edit"));
1269 ctx_->NotifyMutation(MutationDomain::kTileObjects);
1270 objects = std::move(candidate);
1275bool TileObjectHandler::PlaceObjectAt(
int room_id,
1278 mutation_status_ = absl::OkStatus();
1279 auto* room = GetRoom(room_id);
1281 placement_block_reason_ = PlacementBlockReason::kInvalidRoom;
1282 return RejectMutation(absl::InvalidArgumentError(
"Room is unavailable"));
1287 placement_block_reason_ = PlacementBlockReason::kObjectLimit;
1288 return RejectMutation(
1289 absl::ResourceExhaustedError(
"Room object limit reached"));
1292 placement_block_reason_ = PlacementBlockReason::kNone;
1293 auto new_obj =
object.CopyForNewPlacement();
1298 new_obj.x_ = std::clamp(x, 0, 63);
1299 new_obj.y_ = std::clamp(y, 0, 63);
1300 const size_t placed_index = room->GetTileObjects().size();
1301 auto candidate = room->GetTileObjects();
1302 auto sources = IdentitySources(candidate.size());
1303 candidate.push_back(new_obj);
1304 sources.push_back(std::nullopt);
1305 if (!CommitCandidate(room_id, std::move(candidate), sources, {},
1306 std::vector<size_t>{placed_index})) {
1307 placement_block_reason_ = PlacementBlockReason::kChestValidation;
1310 if (placement_policy_ == PlacementPolicy::kOnce) {
1313 if (ctx_ && ctx_->selection) {
1314 ctx_->NotifySelectionChanged();
1316 TriggerSuccessToast();
1317 if (placement_callback_) {
1318 placement_callback_(new_obj);
1324 preview_object_ = object;
1325 RefreshPreviewGraphics();
1328bool TileObjectHandler::SetPreviewSize(uint8_t size) {
1329 if (!object_placement_mode_ ||
1335 if (size > 0x0F || size >= manager.GetSubtypeCount(preview_object_.id_) ||
1336 manager.ResolveFilename(preview_object_.id_, size).empty()) {
1340 const uint8_t canonical_size =
1342 if (preview_object_.size_ != canonical_size) {
1343 preview_object_.size_ = canonical_size;
1344 preview_object_.tiles_loaded_ =
false;
1345 RefreshPreviewGraphics();
1350bool TileObjectHandler::SetPreviewLayer(
int layer) {
1351 if (!object_placement_mode_ || layer < 0 || layer > 2 ||
1355 if (preview_object_.GetLayerValue() != layer) {
1357 preview_object_.tiles_loaded_ =
false;
1358 RefreshPreviewGraphics();
1363void TileObjectHandler::RefreshPreviewGraphics() {
1364 if (object_placement_mode_) {
1365 RenderGhostPreviewBitmap();
1369void TileObjectHandler::RenderGhostPreviewBitmap() {
1370 ghost_preview_bitmap_ready_ =
false;
1371 if (!ctx_ || !ctx_->rom || !ctx_->rom->is_loaded())
1374 auto* room = GetRoom(ctx_->current_room_id);
1375 if (!room || !room->IsLoaded())
1378 const auto preview_geometry = CalculateGhostPreviewGeometry(preview_object_);
1383 if (!ghost_preview_buffer_) {
1384 ghost_preview_buffer_ = std::make_unique<gfx::BackgroundBuffer>(
1385 kGhostPreviewBufferSize, kGhostPreviewBufferSize);
1387 ghost_preview_buffer_->EnsureBitmapInitialized();
1388 ghost_preview_buffer_->ClearBuffer();
1389 auto& bitmap = ghost_preview_buffer_->bitmap();
1390 ApplyRoomPaletteToGhost(*room, bitmap);
1391 std::fill(bitmap.mutable_data().begin(), bitmap.mutable_data().end(), 255);
1392 bitmap.set_modified(
true);
1393 const uint8_t* gfx_data = room->get_gfx_buffer().data();
1401 auto render_object = preview_object_;
1402 render_object.x_ = preview_geometry.render_anchor_x_tiles;
1403 render_object.y_ = preview_geometry.render_anchor_y_tiles;
1409 drawer.
DrawObject(render_object, *ghost_preview_buffer_,
1410 *ghost_preview_buffer_, ctx_->current_palette_group);
1415 if (bitmap.size() > 0) {
1416 bitmap.UpdateSurfacePixels();
1417 if (bitmap.texture()) {
1418 ghost_preview_create_queued_ =
false;
1421 }
else if (!ghost_preview_create_queued_) {
1424 ghost_preview_create_queued_ =
true;
1427 if (bitmap.texture()) {
1428 ghost_preview_create_queued_ =
false;
1430 ghost_preview_bitmap_ready_ =
true;
1431 ghost_preview_room_id_ = ctx_->current_room_id;
1432 ghost_preview_graphics_revision_ = room->graphics_revision();
1437TileObjectHandler::CalculateGhostPreviewGeometry(
1440 const auto [resolved_anchor_x, resolved_anchor_y] =
1444 const int anchor_x =
1445 std::max({0, resolved_anchor_x, -dimensions.offset_x_tiles});
1446 const int anchor_y =
1447 std::max({0, resolved_anchor_y, -dimensions.offset_y_tiles});
1448 const int leading_x_tiles = anchor_x + dimensions.offset_x_tiles;
1449 const int leading_y_tiles = anchor_y + dimensions.offset_y_tiles;
1452 .render_anchor_y_tiles = anchor_y,
1453 .offset_x_tiles = dimensions.offset_x_tiles,
1454 .offset_y_tiles = dimensions.offset_y_tiles,
1455 .width_pixels = dimensions.width_pixels(),
1456 .height_pixels = dimensions.height_pixels(),
1457 .buffer_width_pixels = (leading_x_tiles + dimensions.width_tiles) * 8,
1458 .buffer_height_pixels = (leading_y_tiles + dimensions.height_tiles) * 8,
1466void TileObjectHandler::CopyObjectsToClipboard(
1467 int room_id,
const std::vector<size_t>& indices) {
1468 auto* room = GetRoom(room_id);
1469 if (!room || indices.empty())
1473 const auto& objects = room->GetTileObjects();
1474 const bool includes_chest =
1475 std::any_of(indices.begin(), indices.end(), [&](
size_t index) {
1476 return index < objects.size() &&
1477 zelda3::IsStatefulChestObjectId(objects[index].id_);
1479 if (includes_chest) {
1483 for (
size_t index : indices) {
1484 if (index >= objects.size()) {
1487 clipboard_.push_back(objects[index]);
1488 std::optional<chest_data>
chest;
1491 if (!chest_index || *chest_index >= room->GetChests().size() ||
1492 room->GetChests()[*chest_index].size !=
1493 (objects[index].id_ == 0xFB1)) {
1494 clipboard_status_ = absl::FailedPreconditionError(
1495 "Cannot copy a chest with missing or mismatched contents");
1497 chest = room->GetChests()[*chest_index];
1500 clipboard_chests_.push_back(
chest);
1504std::vector<size_t> TileObjectHandler::PasteFromClipboard(
int room_id,
1507 mutation_status_ = clipboard_status_;
1508 auto* room = GetRoom(room_id);
1509 if (!room || clipboard_.empty())
1511 if (!mutation_status_.ok()) {
1512 RejectMutation(mutation_status_);
1515 auto candidate = room->GetTileObjects();
1516 auto sources = IdentitySources(candidate.size());
1517 std::vector<std::optional<chest_data>> overrides(candidate.size());
1518 std::vector<size_t> new_indices;
1519 for (
size_t index = 0; index < clipboard_.size(); ++index) {
1520 auto object = clipboard_[index].CopyForNewPlacement();
1525 object.x_ = std::clamp(
object.x_ + offset_x, 0, 63);
1526 object.y_ = std::clamp(
object.y_ + offset_y, 0, 63);
1527 object.tiles_loaded_ =
false;
1528 new_indices.push_back(candidate.size());
1529 candidate.push_back(std::move(
object));
1530 sources.push_back(std::nullopt);
1531 overrides.push_back(clipboard_chests_[index]);
1533 if (!CommitCandidate(room_id, std::move(candidate), sources, overrides,
1540std::vector<size_t> TileObjectHandler::PasteFromClipboardAt(
int room_id,
1543 if (clipboard_.empty())
1546 int offset_x = target_x - clipboard_[0].x_;
1547 int offset_y = target_y - clipboard_[0].y_;
1549 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