yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <chrono>
6#include <cmath>
7
8#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
15#include "app/platform/window.h"
16#include "imgui/imgui.h"
19
20namespace yaze {
21namespace zelda3 {
22
23namespace {
24
25bool IsRepresentableRoomObjectId(int object_id) {
26 return (object_id >= 0x000 && object_id <= 0x0F7) ||
27 (object_id >= 0x100 && object_id <= 0x13F) ||
28 (object_id >= 0xF80 && object_id <= 0xFFF);
29}
30
31} // namespace
32
34
36 if (rom_ == nullptr) {
37 return absl::InvalidArgumentError("ROM is null");
38 }
39
40 // Set default configuration
41 config_.snap_to_grid = true;
42 config_.grid_size = 16;
43 config_.show_grid = true;
44 config_.show_preview = true;
45 config_.auto_save = false;
49
50 // Set default editing state
53 editing_state_.current_object_type = 0x10; // Default to wall
56
57 // Initialize empty room
58 owned_room_ = std::make_unique<Room>(0, rom_);
60
61 // Load templates
62 // TODO: Make this path configurable or platform-aware
63 template_manager_.LoadTemplates("assets/templates/dungeon");
64
65 return absl::OkStatus();
66}
67
68absl::Status DungeonObjectEditor::LoadRoom(int room_id) {
69 if (rom_ == nullptr) {
70 return absl::InvalidArgumentError("ROM is null");
71 }
72
73 if (room_id < 0 || room_id >= kNumberOfRooms) {
74 return absl::InvalidArgumentError("Invalid room ID");
75 }
76
77 // Create undo point before loading
78 auto status = CreateUndoPoint();
79 if (!status.ok()) {
80 // Continue anyway, but log the issue
81 }
82
83 // Load room from ROM
84 owned_room_ = std::make_unique<Room>(room_id, rom_);
86
87 // Clear selection
89
90 // Reset editing state
94
95 // Notify callbacks
98 }
99
100 return absl::OkStatus();
101}
102
104 if (current_room_ == nullptr) {
105 return absl::FailedPreconditionError("No room loaded");
106 }
107
108 // Validate room before saving
110 auto validation_result = ValidateRoom();
111 if (!validation_result.is_valid) {
112 std::string error_msg = "Validation failed";
113 if (!validation_result.errors.empty()) {
114 error_msg += ": " + validation_result.errors[0];
115 }
116 return absl::FailedPreconditionError(error_msg);
117 }
118 }
119
120 // Save room objects back to ROM (Phase 1, Task 1.3)
121 return current_room_->SaveObjects();
122}
123
125 if (current_room_ == nullptr) {
126 return absl::FailedPreconditionError("No room loaded");
127 }
128
129 // Create undo point before clearing
130 auto status = CreateUndoPoint();
131 if (!status.ok()) {
132 return status;
133 }
134
135 // Clear all objects
137
138 // Clear selection
140
141 // Notify callbacks
144 }
145
146 return absl::OkStatus();
147}
148
149absl::Status DungeonObjectEditor::InsertObject(int x, int y, int object_type,
150 int size, int layer) {
151 if (current_room_ == nullptr) {
152 return absl::FailedPreconditionError("No room loaded");
153 }
154
155 // Validate parameters
156 if (!IsRepresentableRoomObjectId(object_type)) {
157 return absl::InvalidArgumentError(absl::StrFormat(
158 "Object ID 0x%03X is not representable; expected 0x000..0x0F7, "
159 "0x100..0x13F, or 0xF80..0xFFF",
160 object_type));
161 }
162
163 if (size < kMinObjectSize || size > kMaxObjectSize) {
164 return absl::InvalidArgumentError("Invalid object size");
165 }
166
167 if (layer < kMinLayer || layer > kMaxLayer) {
168 return absl::InvalidArgumentError("Invalid layer");
169 }
170
171 // Snap coordinates to grid if enabled
172 if (config_.snap_to_grid) {
173 x = SnapToGrid(x);
174 y = SnapToGrid(y);
175 }
176
177 // Create undo point
178 auto status = CreateUndoPoint();
179 if (!status.ok()) {
180 return status;
181 }
182
183 // Create new object with the family-specific canonical size. Type 1 owns a
184 // four-bit size field, Type 2 has no size field, and Type 3 derives those
185 // bits from its ID.
186 const uint8_t canonical_size =
187 CanonicalRoomObjectSize(object_type, static_cast<uint8_t>(size));
188 RoomObject new_object(object_type, x, y, canonical_size, layer);
189 new_object.SetRom(rom_);
190 new_object.EnsureTilesLoaded();
191
192 // Check for collisions if validation is enabled
194 for (const auto& existing_obj : current_room_->GetTileObjects()) {
195 if (ObjectsCollide(new_object, existing_obj)) {
196 return absl::FailedPreconditionError(
197 "Object placement would cause collision");
198 }
199 }
200 }
201
202 // Add object to room using new method (Phase 3)
203 auto add_status = current_room_->AddObject(new_object);
204 if (!add_status.ok()) {
205 return add_status;
206 }
207
208 // Select the new object
212
213 // Notify callbacks
216 new_object);
217 }
218
221 }
222
225 }
226
227 return absl::OkStatus();
228}
229
230absl::Status DungeonObjectEditor::DeleteObject(size_t object_index) {
231 if (current_room_ == nullptr) {
232 return absl::FailedPreconditionError("No room loaded");
233 }
234
235 if (object_index >= current_room_->GetTileObjectCount()) {
236 return absl::OutOfRangeError("Object index out of range");
237 }
238
239 // Create undo point
240 auto status = CreateUndoPoint();
241 if (!status.ok()) {
242 return status;
243 }
244
245 // Remove object from room using new method (Phase 3)
246 auto remove_status = current_room_->RemoveObject(object_index);
247 if (!remove_status.ok()) {
248 return remove_status;
249 }
250
251 // Update selection indices
252 for (auto& selected_index : selection_state_.selected_objects) {
253 if (selected_index > object_index) {
254 selected_index--;
255 } else if (selected_index == object_index) {
256 // Remove the deleted object from selection
258 std::remove(selection_state_.selected_objects.begin(),
259 selection_state_.selected_objects.end(), object_index),
261 }
262 }
263
264 // Notify callbacks
267 }
268
271 }
272
273 return absl::OkStatus();
274}
275
277 if (current_room_ == nullptr) {
278 return absl::FailedPreconditionError("No room loaded");
279 }
280
282 return absl::FailedPreconditionError("No objects selected");
283 }
284
285 // Create undo point
286 auto status = CreateUndoPoint();
287 if (!status.ok()) {
288 return status;
289 }
290
291 // Sort selected indices in descending order to avoid index shifting issues
292 std::vector<size_t> sorted_selection = selection_state_.selected_objects;
293 std::sort(sorted_selection.begin(), sorted_selection.end(),
294 std::greater<size_t>());
295
296 // Delete objects in reverse order
297 for (size_t index : sorted_selection) {
298 if (index < current_room_->GetTileObjectCount()) {
300 }
301 }
302
303 // Clear selection
305
306 // Notify callbacks
309 }
310
311 return absl::OkStatus();
312}
313
314absl::Status DungeonObjectEditor::MoveObject(size_t object_index, int new_x,
315 int new_y) {
316 if (current_room_ == nullptr) {
317 return absl::FailedPreconditionError("No room loaded");
318 }
319
320 if (object_index >= current_room_->GetTileObjectCount()) {
321 return absl::OutOfRangeError("Object index out of range");
322 }
323
324 // Snap coordinates to grid if enabled
325 if (config_.snap_to_grid) {
326 new_x = SnapToGrid(new_x);
327 new_y = SnapToGrid(new_y);
328 }
329
330 // Create undo point
331 auto status = CreateUndoPoint();
332 if (!status.ok()) {
333 return status;
334 }
335
336 // Get the object
337 auto& object = current_room_->GetTileObject(object_index);
338
339 // Check for collisions if validation is enabled
341 RoomObject test_object = object;
342 test_object.set_x(new_x);
343 test_object.set_y(new_y);
344
345 for (size_t i = 0; i < current_room_->GetTileObjects().size(); i++) {
346 if (i != object_index &&
347 ObjectsCollide(test_object, current_room_->GetTileObjects()[i])) {
348 return absl::FailedPreconditionError(
349 "Object move would cause collision");
350 }
351 }
352 }
353
354 // Move the object
355 const bool changed = object.x() != new_x || object.y() != new_y;
356 if (changed) {
358 }
359 object.set_x(new_x);
360 object.set_y(new_y);
361 if (changed) {
363 }
364
365 // Notify callbacks
367 object_changed_callback_(object_index, object);
368 }
369
372 }
373
374 return absl::OkStatus();
375}
376
377absl::Status DungeonObjectEditor::ResizeObject(size_t object_index,
378 int new_size) {
379 if (current_room_ == nullptr) {
380 return absl::FailedPreconditionError("No room loaded");
381 }
382
383 if (object_index >= current_room_->GetTileObjectCount()) {
384 return absl::OutOfRangeError("Object index out of range");
385 }
386
387 if (new_size < kMinObjectSize || new_size > kMaxObjectSize) {
388 return absl::InvalidArgumentError("Invalid object size");
389 }
390
391 auto& object = current_room_->GetTileObject(object_index);
392 if (!IsRoomObjectSizeEditable(object.id_)) {
393 return absl::OkStatus();
394 }
395
396 const uint8_t canonical_size =
397 CanonicalRoomObjectSize(object.id_, static_cast<uint8_t>(new_size));
398 if (object.size() == canonical_size) {
399 return absl::OkStatus();
400 }
401
402 // Create undo point
403 auto status = CreateUndoPoint();
404 if (!status.ok()) {
405 return status;
406 }
407
408 // Resize the object
410 object.set_size(canonical_size);
412
413 // Notify callbacks
415 object_changed_callback_(object_index, object);
416 }
417
420 }
421
422 return absl::OkStatus();
423}
424
426 const std::vector<size_t>& indices, int dx, int dy) {
427 if (current_room_ == nullptr) {
428 return absl::FailedPreconditionError("No room loaded");
429 }
430
431 if (indices.empty()) {
432 return absl::OkStatus();
433 }
434
435 // Create single undo point for the batch operation
436 auto status = CreateUndoPoint();
437 if (!status.ok()) {
438 return status;
439 }
440
441 // Apply moves
442 for (size_t index : indices) {
443 if (index >= current_room_->GetTileObjectCount())
444 continue;
445
446 auto& object = current_room_->GetTileObject(index);
447 int new_x = object.x() + dx;
448 int new_y = object.y() + dy;
449
450 // Clamp to room bounds
451 new_x = std::max(0, std::min(63, new_x));
452 new_y = std::max(0, std::min(63, new_y));
453
454 const bool changed = object.x() != new_x || object.y() != new_y;
455 if (changed) {
457 }
458 object.set_x(new_x);
459 object.set_y(new_y);
460 if (changed) {
462 }
463
465 object_changed_callback_(index, object);
466 }
467 }
468
471 }
472
473 return absl::OkStatus();
474}
475
477 const std::vector<size_t>& indices, int new_layer) {
478 if (current_room_ == nullptr) {
479 return absl::FailedPreconditionError("No room loaded");
480 }
481
482 if (new_layer < kMinLayer || new_layer > kMaxLayer) {
483 return absl::InvalidArgumentError("Invalid layer");
484 }
485
486 const std::vector<size_t> requested_indices = indices;
487 auto& objects = current_room_->GetTileObjects();
488 bool change_needed = false;
489 for (size_t index : requested_indices) {
490 if (index >= objects.size()) {
491 continue;
492 }
493 if (new_layer == 2 && UsesSpecialLayerSelector(objects[index])) {
494 return absl::InvalidArgumentError(
495 "Torches and pushable blocks only support upper/lower draw-layer "
496 "selector values 0/1");
497 }
498 change_needed |= objects[index].GetLayerValue() != new_layer;
499 }
500 if (!change_needed) {
501 return absl::OkStatus();
502 }
503
504 // Create undo point
505 auto status = CreateUndoPoint();
506 if (!status.ok()) {
507 return status;
508 }
509
510 const std::vector<RoomObject> before = objects;
511 auto mutation = ReassignObjectStorage(objects, requested_indices, new_layer);
512 if (!mutation.ok()) {
513 return mutation.status();
514 }
515
516 for (size_t old_index : mutation->changed_old_indices) {
517 const size_t new_index = mutation->old_to_new_index[old_index];
518 current_room_->MarkSaveDirtyForTileObject(before[old_index]);
519 current_room_->MarkSaveDirtyForTileObject(objects[new_index]);
520 }
521
522 std::vector<size_t> remapped_selection;
523 remapped_selection.reserve(selection_state_.selected_objects.size());
524 for (size_t old_index : selection_state_.selected_objects) {
525 if (old_index < mutation->old_to_new_index.size()) {
526 remapped_selection.push_back(mutation->old_to_new_index[old_index]);
527 }
528 }
529 std::sort(remapped_selection.begin(), remapped_selection.end());
530 remapped_selection.erase(
531 std::unique(remapped_selection.begin(), remapped_selection.end()),
532 remapped_selection.end());
533 if (remapped_selection != selection_state_.selected_objects) {
534 selection_state_.selected_objects = std::move(remapped_selection);
537 }
538 }
539
541 for (size_t old_index : mutation->changed_old_indices) {
542 const size_t new_index = mutation->old_to_new_index[old_index];
543 object_changed_callback_(new_index, objects[new_index]);
544 }
545 }
546
549 }
550
551 return absl::OkStatus();
552}
553
555 const std::vector<size_t>& indices, int new_size) {
556 if (current_room_ == nullptr) {
557 return absl::FailedPreconditionError("No room loaded");
558 }
559
560 if (new_size < kMinObjectSize || new_size > kMaxObjectSize) {
561 return absl::InvalidArgumentError("Invalid object size");
562 }
563
564 const uint8_t requested_size = static_cast<uint8_t>(new_size);
565 bool change_needed = false;
566 for (size_t index : indices) {
567 if (index >= current_room_->GetTileObjectCount()) {
568 continue;
569 }
570 const auto& object = current_room_->GetTileObject(index);
571 if (IsRoomObjectSizeEditable(object.id_) &&
572 object.size() != CanonicalRoomObjectSize(object.id_, requested_size)) {
573 change_needed = true;
574 break;
575 }
576 }
577 if (!change_needed) {
578 return absl::OkStatus();
579 }
580
581 // Create undo point
582 auto status = CreateUndoPoint();
583 if (!status.ok()) {
584 return status;
585 }
586
587 for (size_t index : indices) {
588 if (index >= current_room_->GetTileObjectCount())
589 continue;
590
591 auto& object = current_room_->GetTileObject(index);
592 if (!IsRoomObjectSizeEditable(object.id_)) {
593 continue;
594 }
595
596 const uint8_t canonical_size =
597 CanonicalRoomObjectSize(object.id_, requested_size);
598 if (object.size() == canonical_size) {
599 continue;
600 }
602 object.set_size(canonical_size);
604
606 object_changed_callback_(index, object);
607 }
608 }
609
612 }
613
614 return absl::OkStatus();
615}
616
617std::optional<size_t> DungeonObjectEditor::DuplicateObject(size_t object_index,
618 int offset_x,
619 int offset_y) {
620 if (current_room_ == nullptr) {
621 return std::nullopt;
622 }
623
624 if (object_index >= current_room_->GetTileObjectCount()) {
625 return std::nullopt;
626 }
627
628 // Create undo point
630
631 auto object =
633
634 // Offset position
635 int new_x = object.x() + offset_x;
636 int new_y = object.y() + offset_y;
637
638 // Clamp
639 new_x = std::max(0, std::min(63, new_x));
640 new_y = std::max(0, std::min(63, new_y));
641
642 object.set_x(new_x);
643 object.set_y(new_y);
644
645 // Add object
646 if (current_room_->AddObject(object).ok()) {
647 size_t new_index = current_room_->GetTileObjectCount() - 1;
648
651 }
652
653 return new_index;
654 }
655
656 return std::nullopt;
657}
658
660 const std::vector<size_t>& indices) {
661 if (current_room_ == nullptr)
662 return;
663
664 clipboard_.clear();
665
666 for (size_t index : indices) {
667 if (index < current_room_->GetTileObjectCount()) {
668 clipboard_.push_back(current_room_->GetTileObject(index));
669 }
670 }
671}
672
674 if (current_room_ == nullptr || clipboard_.empty()) {
675 return {};
676 }
677
678 // Create undo point
680
681 std::vector<size_t> new_indices;
682 size_t start_index = current_room_->GetTileObjectCount();
683
684 for (const auto& obj : clipboard_) {
685 // Paste with slight offset to make it visible
686 RoomObject new_obj = obj.CopyForNewPlacement();
687
688 // Logic to ensure it stays in bounds if we were to support mouse-position pasting
689 // For now, just paste at original location + offset, or perhaps center of screen
690 // Let's do original + 1,1 for now to match duplicate behavior if we just copy/paste
691 // But better might be to keep relative positions if we had a "cursor" position.
692
693 int new_x = std::min(63, new_obj.x() + 1);
694 int new_y = std::min(63, new_obj.y() + 1);
695 new_obj.set_x(new_x);
696 new_obj.set_y(new_y);
697
698 if (current_room_->AddObject(new_obj).ok()) {
699 new_indices.push_back(start_index++);
700 }
701 }
702
705 }
706
707 return new_indices;
708}
709
710absl::Status DungeonObjectEditor::ChangeObjectType(size_t object_index,
711 int new_type) {
712 if (current_room_ == nullptr) {
713 return absl::FailedPreconditionError("No room loaded");
714 }
715
716 if (object_index >= current_room_->GetTileObjectCount()) {
717 return absl::OutOfRangeError("Object index out of range");
718 }
719
720 if (!IsRepresentableRoomObjectId(new_type)) {
721 return absl::InvalidArgumentError(absl::StrFormat(
722 "Object ID 0x%03X is not representable; expected 0x000..0x0F7, "
723 "0x100..0x13F, or 0xF80..0xFFF",
724 new_type));
725 }
726
727 auto& object = current_room_->GetTileObject(object_index);
728 if (object.id_ == new_type) {
729 return absl::OkStatus();
730 }
731
732 const uint8_t canonical_size =
733 CanonicalRoomObjectSize(new_type, object.size());
734
735 // Create undo point
736 auto status = CreateUndoPoint();
737 if (!status.ok()) {
738 return status;
739 }
740
742 object.set_id(static_cast<int16_t>(new_type));
743 object.set_size(canonical_size);
745
747 object_changed_callback_(object_index, object);
748 }
749
752 }
753
754 return absl::OkStatus();
755}
756
758 int x, int y) {
759 if (current_room_ == nullptr) {
760 return absl::FailedPreconditionError("No room loaded");
761 }
762
763 // Snap coordinates to grid if enabled
764 if (config_.snap_to_grid) {
765 x = SnapToGrid(x);
766 y = SnapToGrid(y);
767 }
768
769 // Create undo point
770 auto status = CreateUndoPoint();
771 if (!status.ok()) {
772 return status;
773 }
774
775 // Instantiate template objects
776 std::vector<RoomObject> new_objects =
778
779 // Check for collisions if enabled
781 for (const auto& new_obj : new_objects) {
782 for (const auto& existing_obj : current_room_->GetTileObjects()) {
783 if (ObjectsCollide(new_obj, existing_obj)) {
784 return absl::FailedPreconditionError(
785 "Template placement would cause collision");
786 }
787 }
788 }
789 }
790
791 // Add objects to room
792 for (const auto& obj : new_objects) {
794 }
795
796 // Select the new objects
798 size_t count = current_room_->GetTileObjectCount();
799 size_t added_count = new_objects.size();
800 for (size_t i = 0; i < added_count; ++i) {
801 selection_state_.selected_objects.push_back(count - added_count + i);
802 }
803 if (!selection_state_.selected_objects.empty()) {
805 }
806
809 }
810
811 return absl::OkStatus();
812}
813
815 const std::string& name, const std::string& description) {
817 return absl::FailedPreconditionError("No objects selected");
818 }
819
820 std::vector<RoomObject> objects;
821 int min_x = 64, min_y = 64;
822
823 // Collect selected objects and find bounds
824 for (size_t index : selection_state_.selected_objects) {
825 if (index < current_room_->GetTileObjectCount()) {
826 const auto& obj = current_room_->GetTileObject(index);
827 objects.push_back(obj);
828 if (obj.x() < min_x)
829 min_x = obj.x();
830 if (obj.y() < min_y)
831 min_y = obj.y();
832 }
833 }
834
835 // Create template
837 name, description, objects, min_x, min_y);
838
839 // Save template
840 return template_manager_.SaveTemplate(tmpl, "assets/templates/dungeon");
841}
842
843const std::vector<ObjectTemplate>& DungeonObjectEditor::GetTemplates() const {
845}
846
848 if (current_room_ == nullptr) {
849 return absl::FailedPreconditionError("No room loaded");
850 }
851
852 if (selection_state_.selected_objects.size() < 2) {
853 return absl::OkStatus(); // Nothing to align
854 }
855
856 // Create undo point
857 auto status = CreateUndoPoint();
858 if (!status.ok()) {
859 return status;
860 }
861
862 // Find reference value (min/max/avg)
863 int ref_val = 0;
864 const auto& indices = selection_state_.selected_objects;
865
866 if (alignment == Alignment::Left || alignment == Alignment::Top) {
867 ref_val = 64; // Max possible
868 } else if (alignment == Alignment::Right || alignment == Alignment::Bottom) {
869 ref_val = 0; // Min possible
870 }
871
872 // First pass: calculate reference
873 int sum = 0;
874 int count = 0;
875
876 for (size_t index : indices) {
877 if (index >= current_room_->GetTileObjectCount())
878 continue;
879 const auto& obj = current_room_->GetTileObject(index);
880
881 switch (alignment) {
882 case Alignment::Left:
883 if (obj.x() < ref_val)
884 ref_val = obj.x();
885 break;
886 case Alignment::Right:
887 if (obj.x() > ref_val)
888 ref_val = obj.x();
889 break;
890 case Alignment::Top:
891 if (obj.y() < ref_val)
892 ref_val = obj.y();
893 break;
895 if (obj.y() > ref_val)
896 ref_val = obj.y();
897 break;
899 sum += obj.x();
900 count++;
901 break;
903 sum += obj.y();
904 count++;
905 break;
906 }
907 }
908
909 if (alignment == Alignment::CenterX || alignment == Alignment::CenterY) {
910 if (count > 0)
911 ref_val = sum / count;
912 }
913
914 // Second pass: apply alignment
915 for (size_t index : indices) {
916 if (index >= current_room_->GetTileObjectCount())
917 continue;
918 auto& obj = current_room_->GetTileObject(index);
919
920 const bool changes_x = alignment == Alignment::Left ||
921 alignment == Alignment::Right ||
922 alignment == Alignment::CenterX;
923 const bool changed = changes_x ? obj.x() != ref_val : obj.y() != ref_val;
924 if (changed) {
926 }
927
928 switch (alignment) {
929 case Alignment::Left:
930 case Alignment::Right:
932 obj.set_x(ref_val);
933 break;
934 case Alignment::Top:
937 obj.set_y(ref_val);
938 break;
939 }
940
941 if (changed) {
943 }
944
946 object_changed_callback_(index, obj);
947 }
948 }
949
952 }
953
954 return absl::OkStatus();
955}
956
957absl::Status DungeonObjectEditor::ChangeObjectLayer(size_t object_index,
958 int new_layer) {
959 if (current_room_ == nullptr) {
960 return absl::FailedPreconditionError("No room loaded");
961 }
962
963 if (object_index >= current_room_->GetTileObjectCount()) {
964 return absl::OutOfRangeError("Object index out of range");
965 }
966
967 if (new_layer < kMinLayer || new_layer > kMaxLayer) {
968 return absl::InvalidArgumentError("Invalid layer");
969 }
970
971 return BatchChangeObjectLayer({object_index}, new_layer);
972}
973
974absl::Status DungeonObjectEditor::HandleScrollWheel(int delta, int x, int y,
975 bool ctrl_pressed) {
976 if (current_room_ == nullptr) {
977 return absl::FailedPreconditionError("No room loaded");
978 }
979
980 // Convert screen coordinates to room coordinates
981 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
982
983 // Handle size editing with scroll wheel
987 return HandleSizeEdit(delta, room_x, room_y);
988 }
989
990 // Handle layer switching with Ctrl+scroll
991 if (ctrl_pressed) {
992 int layer_delta = delta > 0 ? 1 : -1;
993 int new_layer = editing_state_.current_layer + layer_delta;
994 new_layer = std::max(kMinLayer, std::min(kMaxLayer, new_layer));
995
996 if (new_layer != editing_state_.current_layer) {
997 SetCurrentLayer(new_layer);
998 }
999
1000 return absl::OkStatus();
1001 }
1002
1003 return absl::OkStatus();
1004}
1005
1006absl::Status DungeonObjectEditor::HandleSizeEdit(int delta, int x, int y) {
1007 // Handle size editing for preview object
1009 const int new_size = ResizeRoomObjectByDelta(
1011 if (new_size != editing_state_.preview_size) {
1012 editing_state_.preview_size = new_size;
1014 }
1015 return absl::OkStatus();
1016 }
1017
1018 // Handle size editing for selected objects
1021 for (size_t object_index : selection_state_.selected_objects) {
1022 if (object_index < current_room_->GetTileObjectCount()) {
1023 auto& object = current_room_->GetTileObject(object_index);
1024 const int new_size =
1025 ResizeRoomObjectByDelta(object.id_, object.size_, delta);
1026 if (new_size != object.size_) {
1027 auto status = ResizeObject(object_index, new_size);
1028 if (!status.ok()) {
1029 return status;
1030 }
1031 }
1032 }
1033 }
1034 return absl::OkStatus();
1035 }
1036
1037 return absl::OkStatus();
1038}
1039
1041 bool left_button,
1042 bool right_button,
1043 bool shift_pressed) {
1044 if (current_room_ == nullptr) {
1045 return absl::FailedPreconditionError("No room loaded");
1046 }
1047
1048 // Convert screen coordinates to room coordinates
1049 auto [room_x, room_y] = ScreenToRoomCoordinates(x, y);
1050
1051 if (left_button) {
1052 switch (editing_state_.current_mode) {
1053 case Mode::kSelect:
1054 if (shift_pressed) {
1055 // Add to selection
1056 auto object_index = FindObjectAt(room_x, room_y);
1057 if (object_index.has_value()) {
1058 return AddToSelection(object_index.value());
1059 }
1060 } else {
1061 // Select object
1062 return SelectObject(x, y);
1063 }
1064 break;
1065
1066 case Mode::kInsert:
1067 // Insert object at clicked position
1068 return InsertObject(room_x, room_y, editing_state_.current_object_type,
1071
1072 case Mode::kDelete:
1073 // Delete object at clicked position
1074 {
1075 auto object_index = FindObjectAt(room_x, room_y);
1076 if (object_index.has_value()) {
1077 return DeleteObject(object_index.value());
1078 }
1079 }
1080 break;
1081
1082 case Mode::kEdit:
1083 // Select object for editing
1084 return SelectObject(x, y);
1085
1086 default:
1087 break;
1088 }
1089 }
1090
1091 if (right_button) {
1092 // Context menu or alternate action
1093 switch (editing_state_.current_mode) {
1094 case Mode::kSelect:
1095 // Show context menu for object
1096 {
1097 auto object_index = FindObjectAt(room_x, room_y);
1098 if (object_index.has_value()) {
1099 // TODO: Show context menu
1100 }
1101 }
1102 break;
1103
1104 default:
1105 break;
1106 }
1107 }
1108
1109 return absl::OkStatus();
1110}
1111
1112absl::Status DungeonObjectEditor::HandleMouseDrag(int start_x, int start_y,
1113 int current_x,
1114 int current_y) {
1115 if (current_room_ == nullptr) {
1116 return absl::FailedPreconditionError("No room loaded");
1117 }
1118
1119 // Enable dragging if not already (Phase 4)
1125
1126 // Create undo point before drag
1127 auto undo_status = CreateUndoPoint();
1128 if (!undo_status.ok()) {
1129 return undo_status;
1130 }
1131 }
1132
1133 // Handle the drag operation (Phase 4)
1134 return HandleDragOperation(current_x, current_y);
1135}
1136
1138 if (current_room_ == nullptr) {
1139 return absl::FailedPreconditionError("No room loaded");
1140 }
1141
1142 // End dragging operation (Phase 4)
1145
1146 // Notify callbacks about the final positions
1149 }
1150 }
1151
1152 return absl::OkStatus();
1153}
1154
1155absl::Status DungeonObjectEditor::SelectObject(int screen_x, int screen_y) {
1156 if (current_room_ == nullptr) {
1157 return absl::FailedPreconditionError("No room loaded");
1158 }
1159
1160 // Convert screen coordinates to room coordinates
1161 auto [room_x, room_y] = ScreenToRoomCoordinates(screen_x, screen_y);
1162
1163 // Find object at position
1164 auto object_index = FindObjectAt(room_x, room_y);
1165
1166 if (object_index.has_value()) {
1167 // Select the found object
1169 selection_state_.selected_objects.push_back(object_index.value());
1170
1171 // Notify callbacks
1174 }
1175
1176 return absl::OkStatus();
1177 } else {
1178 // Clear selection if no object found
1179 return ClearSelection();
1180 }
1181}
1182
1187
1188 // Notify callbacks
1191 }
1192
1193 return absl::OkStatus();
1194}
1195
1196absl::Status DungeonObjectEditor::AddToSelection(size_t object_index) {
1197 if (current_room_ == nullptr) {
1198 return absl::FailedPreconditionError("No room loaded");
1199 }
1200
1201 if (object_index >= current_room_->GetTileObjectCount()) {
1202 return absl::OutOfRangeError("Object index out of range");
1203 }
1204
1205 // Check if already selected
1206 auto it = std::find(selection_state_.selected_objects.begin(),
1207 selection_state_.selected_objects.end(), object_index);
1208
1209 if (it == selection_state_.selected_objects.end()) {
1210 selection_state_.selected_objects.push_back(object_index);
1212
1213 // Notify callbacks
1216 }
1217 }
1218
1219 return absl::OkStatus();
1220}
1221
1224
1225 // Update preview object based on mode
1227}
1228
1230 if (layer >= kMinLayer && layer <= kMaxLayer) {
1233 }
1234}
1235
1237 if (IsRepresentableRoomObjectId(object_type)) {
1238 editing_state_.current_object_type = object_type;
1242 }
1243}
1244
1245std::optional<size_t> DungeonObjectEditor::FindObjectAt(int room_x,
1246 int room_y) {
1247 if (current_room_ == nullptr) {
1248 return std::nullopt;
1249 }
1250
1251 // Search from back to front (last objects are on top)
1252 for (int i = static_cast<int>(current_room_->GetTileObjectCount()) - 1;
1253 i >= 0; i--) {
1254 if (IsObjectAtPosition(current_room_->GetTileObject(i), room_x, room_y)) {
1255 return static_cast<size_t>(i);
1256 }
1257 }
1258
1259 return std::nullopt;
1260}
1261
1263 int y) {
1264 // Coordinates are in room tiles.
1265 int obj_x = object.x_;
1266 int obj_y = object.y_;
1267
1268 // Simplified bounds: default to 1x1 tile, grow to 2x2 for large objects.
1269 int obj_width = 1;
1270 int obj_height = 1;
1271 if (object.size_ > 0x80) {
1272 obj_width = 2;
1273 obj_height = 2;
1274 }
1275
1276 return (x >= obj_x && x < obj_x + obj_width && y >= obj_y &&
1277 y < obj_y + obj_height);
1278}
1279
1281 const RoomObject& obj2) {
1282 // Simple bounding box collision detection
1283 // In practice, you'd use the actual tile data for more accurate collision
1284
1285 int obj1_x = obj1.x_ * 16;
1286 int obj1_y = obj1.y_ * 16;
1287 int obj1_w = 16;
1288 int obj1_h = 16;
1289
1290 int obj2_x = obj2.x_ * 16;
1291 int obj2_y = obj2.y_ * 16;
1292 int obj2_w = 16;
1293 int obj2_h = 16;
1294
1295 // Adjust sizes based on object size values
1296 if (obj1.size_ > 0x80) {
1297 obj1_w *= 2;
1298 obj1_h *= 2;
1299 }
1300
1301 if (obj2.size_ > 0x80) {
1302 obj2_w *= 2;
1303 obj2_h *= 2;
1304 }
1305
1306 return !(obj1_x + obj1_w <= obj2_x || obj2_x + obj2_w <= obj1_x ||
1307 obj1_y + obj1_h <= obj2_y || obj2_y + obj2_h <= obj1_y);
1308}
1309
1310std::pair<int, int> DungeonObjectEditor::ScreenToRoomCoordinates(int screen_x,
1311 int screen_y) {
1312 // Convert screen coordinates to room tile coordinates
1313 // This is a simplified implementation - in practice, you'd account for
1314 // camera position, zoom level, etc.
1315
1316 int room_x = screen_x / 16; // 16 pixels per tile
1317 int room_y = screen_y / 16;
1318
1319 return {room_x, room_y};
1320}
1321
1323 int room_y) {
1324 // Convert room tile coordinates to screen coordinates
1325 int screen_x = room_x * 16;
1326 int screen_y = room_y * 16;
1327
1328 return {screen_x, screen_y};
1329}
1330
1332 if (!config_.snap_to_grid) {
1333 return coordinate;
1334 }
1335
1336 int grid_size = config_.grid_size;
1337 if (grid_size <= 0) {
1338 return coordinate;
1339 }
1340
1341 // Coordinates are in room tiles; map pixel grid size to tile steps.
1342 int tile_step = std::max(1, grid_size / 16);
1343 return (coordinate / tile_step) * tile_step;
1344}
1345
1363
1365 if (current_room_ == nullptr) {
1366 return absl::FailedPreconditionError("No room loaded");
1367 }
1368
1369 // Create undo point
1370 UndoPoint undo_point;
1371 undo_point.objects = current_room_->GetTileObjects();
1372 undo_point.selection = selection_state_;
1373 undo_point.editing = editing_state_;
1374 undo_point.timestamp = std::chrono::steady_clock::now();
1375
1376 // Add to undo history
1377 undo_history_.push_back(undo_point);
1378
1379 // Limit undo history size
1380 if (undo_history_.size() > kMaxUndoHistory) {
1381 undo_history_.erase(undo_history_.begin());
1382 }
1383
1384 // Clear redo history when new action is performed
1385 redo_history_.clear();
1386
1387 return absl::OkStatus();
1388}
1389
1391 if (!CanUndo()) {
1392 return absl::FailedPreconditionError("Nothing to undo");
1393 }
1394
1395 // Move current state to redo history
1396 UndoPoint current_state;
1397 current_state.objects = current_room_->GetTileObjects();
1398 current_state.selection = selection_state_;
1399 current_state.editing = editing_state_;
1400 current_state.timestamp = std::chrono::steady_clock::now();
1401
1402 redo_history_.push_back(current_state);
1403
1404 // Apply undo point
1405 UndoPoint undo_point = undo_history_.back();
1406 undo_history_.pop_back();
1407
1408 return ApplyUndoPoint(undo_point);
1409}
1410
1412 if (!CanRedo()) {
1413 return absl::FailedPreconditionError("Nothing to redo");
1414 }
1415
1416 // Move current state to undo history
1417 UndoPoint current_state;
1418 current_state.objects = current_room_->GetTileObjects();
1419 current_state.selection = selection_state_;
1420 current_state.editing = editing_state_;
1421 current_state.timestamp = std::chrono::steady_clock::now();
1422
1423 undo_history_.push_back(current_state);
1424
1425 // Apply redo point
1426 UndoPoint redo_point = redo_history_.back();
1427 redo_history_.pop_back();
1428
1429 return ApplyUndoPoint(redo_point);
1430}
1431
1432absl::Status DungeonObjectEditor::ApplyUndoPoint(const UndoPoint& undo_point) {
1433 if (current_room_ == nullptr) {
1434 return absl::FailedPreconditionError("No room loaded");
1435 }
1436
1437 // Restore room state
1439
1440 // Restore editor state
1441 selection_state_ = undo_point.selection;
1442 editing_state_ = undo_point.editing;
1443
1444 // Update preview
1446
1447 // Notify callbacks
1450 }
1451
1454 }
1455
1456 return absl::OkStatus();
1457}
1458
1460 return !undo_history_.empty();
1461}
1462
1464 return !redo_history_.empty();
1465}
1466
1468 undo_history_.clear();
1469 redo_history_.clear();
1470}
1471
1472// ============================================================================
1473// Phase 4: Visual Feedback and GUI Methods
1474// ============================================================================
1475
1476// Helper for color blending
1477static uint32_t BlendColors(uint32_t base, uint32_t tint) {
1478 uint8_t a_tint = (tint >> 24) & 0xFF;
1479 if (a_tint == 0)
1480 return base;
1481
1482 uint8_t r_base = (base >> 16) & 0xFF;
1483 uint8_t g_base = (base >> 8) & 0xFF;
1484 uint8_t b_base = base & 0xFF;
1485
1486 uint8_t r_tint = (tint >> 16) & 0xFF;
1487 uint8_t g_tint = (tint >> 8) & 0xFF;
1488 uint8_t b_tint = tint & 0xFF;
1489
1490 float alpha = a_tint / 255.0f;
1491 uint8_t r = r_base * (1.0f - alpha) + r_tint * alpha;
1492 uint8_t g = g_base * (1.0f - alpha) + g_tint * alpha;
1493 uint8_t b = b_base * (1.0f - alpha) + b_tint * alpha;
1494
1495 return 0xFF000000 | (r << 16) | (g << 8) | b;
1496}
1497
1501 return;
1502 }
1503
1504 // Draw highlight rectangles around selected objects
1505 for (size_t obj_idx : selection_state_.selected_objects) {
1506 if (obj_idx >= current_room_->GetTileObjectCount())
1507 continue;
1508
1509 const auto& obj = current_room_->GetTileObject(obj_idx);
1510 int x = obj.x() * 16;
1511 int y = obj.y() * 16;
1512 int w = 16 + (obj.size() * 4); // Approximate width
1513 int h = 16 + (obj.size() * 4); // Approximate height
1514
1515 // Draw yellow selection box (2px border) - using SetPixel
1516 uint8_t r = (config_.selection_color >> 16) & 0xFF;
1517 uint8_t g = (config_.selection_color >> 8) & 0xFF;
1518 uint8_t b = config_.selection_color & 0xFF;
1519 gfx::SnesColor sel_color(r, g, b);
1520
1521 for (int py = y; py < y + h; py++) {
1522 for (int px = x; px < x + w; px++) {
1523 if (px < canvas.width() && py < canvas.height() &&
1524 (px < x + 2 || px >= x + w - 2 || py < y + 2 || py >= y + h - 2)) {
1525 canvas.SetPixel(px, py, sel_color);
1526 }
1527 }
1528 }
1529 }
1530}
1531
1534 return;
1535 }
1536
1537 // Apply subtle color tints based on layer (simplified - just mark with
1538 // colored border)
1539 for (const auto& obj : current_room_->GetTileObjects()) {
1540 int x = obj.x() * 16;
1541 int y = obj.y() * 16;
1542 int w = 16;
1543 int h = 16;
1544
1545 uint32_t tint_color = 0xFF000000;
1546 switch (obj.GetLayerValue()) {
1547 case 0:
1548 tint_color = config_.layer0_color;
1549 break;
1550 case 1:
1551 tint_color = config_.layer1_color;
1552 break;
1553 case 2:
1554 tint_color = config_.layer2_color;
1555 break;
1556 }
1557
1558 // Draw 1px border in layer color
1559 uint8_t r = (tint_color >> 16) & 0xFF;
1560 uint8_t g = (tint_color >> 8) & 0xFF;
1561 uint8_t b = tint_color & 0xFF;
1562 gfx::SnesColor layer_color(r, g, b);
1563
1564 for (int py = y; py < y + h && py < canvas.height(); py++) {
1565 for (int px = x; px < x + w && px < canvas.width(); px++) {
1566 if (px == x || px == x + w - 1 || py == y || py == y + h - 1) {
1567 canvas.SetPixel(px, py, layer_color);
1568 }
1569 }
1570 }
1571 }
1572}
1573
1575 const auto& theme = editor::AgentUI::GetTheme();
1576
1579 return;
1580 }
1581
1582 if (selection_state_.selected_objects.size() == 1) {
1583 size_t obj_idx = selection_state_.selected_objects[0];
1584 if (obj_idx < current_room_->GetTileObjectCount()) {
1585 auto& obj = current_room_->GetTileObject(obj_idx);
1586
1587 // ========== Identity Section ==========
1588 gui::SectionHeader(ICON_MD_TAG, "Identity", theme.text_info);
1589 if (gui::BeginPropertyTable("##IdentityProps")) {
1590 // Object index
1591 gui::PropertyRow("Object #", static_cast<int>(obj_idx));
1592
1593 // Object ID with name
1594 ImGui::TableNextRow();
1595 ImGui::TableNextColumn();
1596 ImGui::Text("ID");
1597 ImGui::TableNextColumn();
1598 std::string obj_name = GetObjectName(obj.id_);
1599 ImGui::Text("0x%03X", obj.id_);
1600 ImGui::SameLine();
1601 ImGui::TextColored(theme.text_secondary_gray, "(%s)", obj_name.c_str());
1602
1603 // Object type/subtype
1604 int subtype = GetObjectSubtype(obj.id_);
1605 ImGui::TableNextRow();
1606 ImGui::TableNextColumn();
1607 ImGui::Text("Type");
1608 ImGui::TableNextColumn();
1609 ImGui::Text("Subtype %d", subtype);
1610
1612 }
1613
1614 ImGui::Spacing();
1615
1616 // ========== Position Section ==========
1617 gui::SectionHeader(ICON_MD_PLACE, "Position", theme.text_info);
1618 if (gui::BeginPropertyTable("##PositionProps")) {
1619 // X Position
1620 ImGui::TableNextRow();
1621 ImGui::TableNextColumn();
1622 ImGui::Text("X");
1623 ImGui::TableNextColumn();
1624 int x = obj.x();
1625 ImGui::SetNextItemWidth(-1);
1626 if (ImGui::InputInt("##X", &x, 1, 4)) {
1627 if (x >= 0 && x < 64 && obj.x() != x) {
1629 obj.set_x(x);
1632 object_changed_callback_(obj_idx, obj);
1633 }
1634 }
1635 }
1636
1637 // Y Position
1638 ImGui::TableNextRow();
1639 ImGui::TableNextColumn();
1640 ImGui::Text("Y");
1641 ImGui::TableNextColumn();
1642 int y = obj.y();
1643 ImGui::SetNextItemWidth(-1);
1644 if (ImGui::InputInt("##Y", &y, 1, 4)) {
1645 if (y >= 0 && y < 64 && obj.y() != y) {
1647 obj.set_y(y);
1650 object_changed_callback_(obj_idx, obj);
1651 }
1652 }
1653 }
1654
1656 }
1657
1658 ImGui::Spacing();
1659
1660 // ========== Appearance Section ==========
1661 gui::SectionHeader(ICON_MD_PALETTE, "Appearance", theme.text_info);
1662 if (gui::BeginPropertyTable("##AppearanceProps")) {
1663 // Size (for Type 1 objects only)
1664 if (IsRoomObjectSizeEditable(obj.id_)) {
1665 ImGui::TableNextRow();
1666 ImGui::TableNextColumn();
1667 ImGui::Text("Size");
1668 ImGui::TableNextColumn();
1669 int size = obj.size();
1670 ImGui::SetNextItemWidth(-1);
1671 if (ImGui::SliderInt("##Size", &size, 0, 15, "0x%02X")) {
1672 (void)ResizeObject(obj_idx, size);
1673 }
1674 }
1675
1676 // Object ID (editable)
1677 ImGui::TableNextRow();
1678 ImGui::TableNextColumn();
1679 ImGui::Text("Change ID");
1680 ImGui::TableNextColumn();
1681 int id = obj.id_;
1682 ImGui::SetNextItemWidth(-1);
1683 if (ImGui::InputInt("##ID", &id, 1, 16,
1684 ImGuiInputTextFlags_CharsHexadecimal)) {
1685 if (id >= 0 && id <= 0xFFF && obj.id_ != id) {
1686 (void)ChangeObjectType(obj_idx, id);
1687 }
1688 }
1689
1691 }
1692
1693 ImGui::Spacing();
1694
1695 const bool uses_room_stream = UsesRoomObjectStream(obj);
1696 const auto semantics = GetObjectLayerSemantics(obj);
1697 const bool has_routine_defined_route =
1698 uses_room_stream &&
1699 semantics.render_routing != ObjectRenderRouting::kStoredPlacement;
1701 uses_room_stream ? "Object Stream" : "Special Layer",
1702 theme.text_info);
1703 bool storage_changed = false;
1704 if (gui::BeginPropertyTable("##LayerProps")) {
1705 if (uses_room_stream) {
1706 ImGui::TableNextRow();
1707 ImGui::TableNextColumn();
1708 ImGui::Text("Draws To");
1709 ImGui::TableNextColumn();
1710 if (has_routine_defined_route) {
1711 ImGui::TextColored(theme.text_warning_yellow, "%s",
1713 } else {
1714 ImGui::Text("%s", ObjectRenderRoutingDisplayLabel(semantics));
1715 }
1716 }
1717
1718 ImGui::TableNextRow();
1719 ImGui::TableNextColumn();
1720 ImGui::Text("%s", uses_room_stream ? "Stream" : "Selector");
1721 ImGui::TableNextColumn();
1722 int layer = obj.GetLayerValue();
1723 ImGui::SetNextItemWidth(-1);
1724 const char* choices = uses_room_stream
1725 ? "Primary\0BG2 overlay\0BG1 overlay\0"
1726 : "Upper layer (BG1)\0Lower layer (BG2)\0";
1727 if (ImGui::Combo("##Layer", &layer, choices)) {
1728 if (obj.GetLayerValue() != layer) {
1729 storage_changed = ChangeObjectLayer(obj_idx, layer).ok();
1730 }
1731 }
1732 if (has_routine_defined_route) {
1733 ImGui::SameLine();
1735 "This object's draw routine defines its render route. Its object "
1736 "stream still controls draw order and ROM serialization.");
1737 }
1738
1740 }
1741 if (storage_changed) {
1742 return;
1743 }
1744
1745 ImGui::Spacing();
1746 ImGui::Separator();
1747 ImGui::Spacing();
1748
1749 // ========== Actions Section ==========
1750 float button_width = (ImGui::GetContentRegionAvail().x - 8) / 2;
1751
1752 gui::StyleColorGuard delete_btn_guard(
1753 {{ImGuiCol_Button,
1754 ImVec4(theme.status_error.x * 0.7f, theme.status_error.y * 0.7f,
1755 theme.status_error.z * 0.7f, 1.0f)},
1756 {ImGuiCol_ButtonHovered, theme.status_error}});
1757 if (ImGui::Button(ICON_MD_DELETE " Delete", ImVec2(button_width, 0))) {
1758 auto status = DeleteObject(obj_idx);
1759 (void)status;
1760 }
1761
1762 ImGui::SameLine();
1763
1764 if (ImGui::Button(ICON_MD_CONTENT_COPY " Duplicate",
1765 ImVec2(button_width, 0))) {
1766 (void)DuplicateObject(obj_idx, /*offset_x=*/1, /*offset_y=*/0);
1767 }
1768 }
1769 } else {
1770 // ========== Multiple Selection Mode ==========
1771 ImGui::TextColored(theme.text_warning_yellow,
1772 ICON_MD_SELECT_ALL " %zu objects selected",
1774
1775 ImGui::Spacing();
1776
1777 bool has_room_stream_object = false;
1778 bool has_special_table_object = false;
1779 bool has_editable_size_object = false;
1780 for (size_t index : selection_state_.selected_objects) {
1781 if (index >= current_room_->GetTileObjectCount()) {
1782 continue;
1783 }
1784 const auto& object = current_room_->GetTileObject(index);
1785 if (UsesRoomObjectStream(object)) {
1786 has_room_stream_object = true;
1787 } else {
1788 has_special_table_object = true;
1789 }
1790 has_editable_size_object |= IsRoomObjectSizeEditable(object.id_);
1791 }
1792
1793 if (has_special_table_object) {
1796 has_room_stream_object ? "Batch Placement" : "Batch Special Layer",
1797 theme.text_info);
1798 if (has_room_stream_object) {
1799 ImGui::TextWrapped(
1800 "Mixed selection: values apply as object streams to room objects "
1801 "and special draw-layer selectors to torches/blocks.");
1802 }
1803 static int batch_special_layer = 0;
1804 batch_special_layer = std::clamp(batch_special_layer, 0, 1);
1805 ImGui::SetNextItemWidth(-1);
1806 const char* choices = has_room_stream_object
1807 ? "Primary / Upper layer (BG1)\0BG2 overlay / "
1808 "Lower layer (BG2)\0"
1809 : "Upper layer (BG1)\0Lower layer (BG2)\0";
1810 if (ImGui::Combo("##BatchSpecialLayer", &batch_special_layer, choices)) {
1812 batch_special_layer);
1813 }
1814 } else {
1815 gui::SectionHeader(ICON_MD_LAYERS, "Batch Object Stream",
1816 theme.text_info);
1817 static int batch_stream = 0;
1818 ImGui::SetNextItemWidth(-1);
1819 if (ImGui::Combo("##BatchStream", &batch_stream,
1820 "Primary\0BG2 overlay\0BG1 overlay\0")) {
1822 }
1823 }
1824
1825 ImGui::Spacing();
1826
1827 // ========== Batch Size ==========
1828 gui::SectionHeader(ICON_MD_ASPECT_RATIO, "Batch Size", theme.text_info);
1829 static int batch_size = 0x02;
1830 ImGui::SetNextItemWidth(-1);
1831 ImGui::BeginDisabled(!has_editable_size_object);
1832 if (ImGui::InputInt("##BatchSize", &batch_size, 1, 16,
1833 ImGuiInputTextFlags_CharsHexadecimal)) {
1834 batch_size = std::clamp(batch_size, 0, 15);
1836 }
1837 ImGui::EndDisabled();
1838
1839 ImGui::Spacing();
1840
1841 // ========== Nudge Section ==========
1842 gui::SectionHeader(ICON_MD_OPEN_WITH, "Nudge", theme.text_info);
1843 float nudge_btn_size = (ImGui::GetContentRegionAvail().x - 24) / 4;
1844 if (ImGui::Button(ICON_MD_ARROW_BACK, ImVec2(nudge_btn_size, 0))) {
1846 }
1847 ImGui::SameLine();
1848 if (ImGui::Button(ICON_MD_ARROW_UPWARD, ImVec2(nudge_btn_size, 0))) {
1850 }
1851 ImGui::SameLine();
1852 if (ImGui::Button(ICON_MD_ARROW_DOWNWARD, ImVec2(nudge_btn_size, 0))) {
1854 }
1855 ImGui::SameLine();
1856 if (ImGui::Button(ICON_MD_ARROW_FORWARD, ImVec2(nudge_btn_size, 0))) {
1858 }
1859
1860 ImGui::Spacing();
1861 ImGui::Separator();
1862 ImGui::Spacing();
1863
1864 // ========== Actions ==========
1865 float button_width = (ImGui::GetContentRegionAvail().x - 8) / 2;
1866
1867 gui::StyleColorGuard delete_all_btn_guard(
1868 {{ImGuiCol_Button,
1869 ImVec4(theme.status_error.x * 0.7f, theme.status_error.y * 0.7f,
1870 theme.status_error.z * 0.7f, 1.0f)},
1871 {ImGuiCol_ButtonHovered, theme.status_error}});
1872 if (ImGui::Button(ICON_MD_DELETE_SWEEP " Delete All",
1873 ImVec2(button_width, 0))) {
1874 auto status = DeleteSelectedObjects();
1875 (void)status;
1876 }
1877
1878 ImGui::SameLine();
1879
1880 if (ImGui::Button(ICON_MD_DESELECT " Clear Selection",
1881 ImVec2(button_width, 0))) {
1882 auto status = ClearSelection();
1883 (void)status;
1884 }
1885 }
1886}
1887
1889 ImGui::Begin("Layer Controls");
1890
1891 // Current layer selection
1892 ImGui::Text("Current Layer:");
1893 ImGui::RadioButton("Layer 0", &editing_state_.current_layer, 0);
1894 ImGui::SameLine();
1895 ImGui::RadioButton("Layer 1", &editing_state_.current_layer, 1);
1896 ImGui::SameLine();
1897 ImGui::RadioButton("Layer 2", &editing_state_.current_layer, 2);
1898
1899 ImGui::Separator();
1900
1901 // Layer visibility toggles
1902 static bool layer_visible[3] = {true, true, true};
1903 ImGui::Text("Layer Visibility:");
1904 ImGui::Checkbox("Show Layer 0", &layer_visible[0]);
1905 ImGui::Checkbox("Show Layer 1", &layer_visible[1]);
1906 ImGui::Checkbox("Show Layer 2", &layer_visible[2]);
1907
1908 ImGui::Separator();
1909
1910 // Layer colors
1911 ImGui::Checkbox("Show Layer Colors", &config_.show_layer_colors);
1913 ImGui::ColorEdit4("Layer 0 Tint", (float*)&config_.layer0_color);
1914 ImGui::ColorEdit4("Layer 1 Tint", (float*)&config_.layer1_color);
1915 ImGui::ColorEdit4("Layer 2 Tint", (float*)&config_.layer2_color);
1916 }
1917
1918 ImGui::Separator();
1919
1920 // Object counts per layer
1921 if (current_room_) {
1922 int count0 = 0, count1 = 0, count2 = 0;
1923 for (const auto& obj : current_room_->GetTileObjects()) {
1924 switch (obj.GetLayerValue()) {
1925 case 0:
1926 count0++;
1927 break;
1928 case 1:
1929 count1++;
1930 break;
1931 case 2:
1932 count2++;
1933 break;
1934 }
1935 }
1936 ImGui::Text("Layer 0: %d objects", count0);
1937 ImGui::Text("Layer 1: %d objects", count1);
1938 ImGui::Text("Layer 2: %d objects", count2);
1939 }
1940
1941 ImGui::End();
1942}
1943
1945 int current_y) {
1948 return absl::OkStatus();
1949 }
1950
1951 // Calculate delta from drag start
1952 int dx = current_x - selection_state_.drag_start_x;
1953 int dy = current_y - selection_state_.drag_start_y;
1954
1955 // Convert pixel delta to grid delta
1956 int grid_dx = dx / config_.grid_size;
1957 int grid_dy = dy / config_.grid_size;
1958
1959 if (grid_dx == 0 && grid_dy == 0) {
1960 return absl::OkStatus(); // No meaningful movement yet
1961 }
1962
1963 // Move all selected objects
1964 for (size_t obj_idx : selection_state_.selected_objects) {
1965 if (obj_idx >= current_room_->GetTileObjectCount())
1966 continue;
1967
1968 auto& obj = current_room_->GetTileObject(obj_idx);
1969 int new_x = obj.x() + grid_dx;
1970 int new_y = obj.y() + grid_dy;
1971
1972 // Clamp to valid range
1973 new_x = std::max(0, std::min(63, new_x));
1974 new_y = std::max(0, std::min(63, new_y));
1975
1976 const bool changed = obj.x() != new_x || obj.y() != new_y;
1977 if (changed) {
1979 }
1980 obj.set_x(new_x);
1981 obj.set_y(new_y);
1982 if (changed) {
1984 }
1985
1987 object_changed_callback_(obj_idx, obj);
1988 }
1989 }
1990
1991 // Update drag start position
1992 selection_state_.drag_start_x = current_x;
1993 selection_state_.drag_start_y = current_y;
1994
1995 return absl::OkStatus();
1996}
1997
1999 if (current_room_ == nullptr) {
2000 return {false, {}, {"No room loaded"}};
2001 }
2002
2003 // Use the dedicated validator
2005
2006 // Validate objects don't overlap if collision checking is enabled
2008 const auto& objects = current_room_->GetTileObjects();
2009 for (size_t i = 0; i < objects.size(); i++) {
2010 for (size_t j = i + 1; j < objects.size(); j++) {
2011 if (ObjectsCollide(objects[i], objects[j])) {
2012 result.errors.push_back(
2013 absl::StrFormat("Objects at indices %d and %d collide", i, j));
2014 result.is_valid = false;
2015 }
2016 }
2017 }
2018 }
2019
2020 return result;
2021}
2022
2024 auto result = ValidateRoom();
2025 std::vector<std::string> all_issues = result.errors;
2026 all_issues.insert(all_issues.end(), result.warnings.begin(),
2027 result.warnings.end());
2028 return all_issues;
2029}
2030
2035
2039
2044
2046 config_ = config;
2047}
2048
2050 rom_ = rom;
2051 // Reinitialize editor with new ROM
2053}
2054
2056 // Set the current room pointer to the external room
2057 current_room_ = room;
2058
2059 // Reset editing state for new room
2063
2064 // Clear selection as it's invalid for the new room
2066
2067 // Clear undo history as it applies to the previous room
2068 ClearHistory();
2069
2070 // Notify callbacks
2073 }
2074}
2075
2076// Factory function
2077std::unique_ptr<DungeonObjectEditor> CreateDungeonObjectEditor(Rom* rom) {
2078 return std::make_unique<DungeonObjectEditor>(rom);
2079}
2080
2081// Object Categories implementation
2082namespace ObjectCategories {
2083
2084namespace {
2085
2086const std::vector<ObjectCategory>& CategoryTable() {
2087 // Explicit memberships follow Type1/2/3RoomObjectNames in room_object.h.
2088 // These are browsing categories, not collision or gameplay metadata. Keep
2089 // names/custom labels out of classification so translations cannot change
2090 // which objects a filter exposes. Oracle dispatch families remain in the
2091 // selector's separate Custom Assets browser.
2092 static const std::vector<ObjectCategory> categories = [] {
2093 std::vector<ObjectCategory> result = {
2094 {"Walls",
2095 {0x000, 0x001, 0x002, 0x003, 0x004, 0x005, 0x006, 0x007, 0x008, 0x009,
2096 0x00A, 0x00B, 0x00C, 0x00D, 0x00E, 0x00F, 0x010, 0x011, 0x012, 0x013,
2097 0x014, 0x015, 0x016, 0x017, 0x018, 0x019, 0x01A, 0x01B, 0x01C, 0x01D,
2098 0x01E, 0x01F, 0x020, 0x02F, 0x030, 0x060, 0x061, 0x062, 0x063, 0x064,
2099 0x065, 0x066, 0x067, 0x068, 0x06C, 0x06D, 0x0A0, 0x0A1, 0x0A2, 0x0A3,
2100 0x0C0, 0x0CD, 0x0CE, 0x100, 0x101, 0x102, 0x103, 0x104, 0x105, 0x106,
2101 0x107, 0x108, 0x109, 0x10A, 0x10B, 0x10C, 0x10D, 0x10E, 0x10F, 0x110,
2102 0x111, 0x112, 0x113, 0x114, 0x115, 0x116, 0x117, 0x118, 0x119, 0x11A,
2103 0x11B, 0x13C, 0xFA2, 0xFA3, 0xFA4, 0xFA5},
2104 "Walls, wall corners, and ceilings"},
2105 {"Floors",
2106 {0x023, 0x024, 0x025, 0x026, 0x027, 0x028, 0x029, 0x02A, 0x02B,
2107 0x02C, 0x02D, 0x02E, 0x033, 0x034, 0x03F, 0x040, 0x041, 0x042,
2108 0x043, 0x044, 0x045, 0x046, 0x047, 0x048, 0x049, 0x04A, 0x06A,
2109 0x06B, 0x070, 0x071, 0x079, 0x07A, 0x08B, 0x08C, 0x08D, 0x08E,
2110 0x094, 0x0A4, 0x0B0, 0x0B1, 0x0B2, 0x0B3, 0x0B4, 0x0BA, 0x0C1,
2111 0x0C4, 0x0C5, 0x0C7, 0x0C8, 0x0C9, 0x0CA, 0x0D1, 0x0D2, 0x0D8,
2112 0x0DA, 0x0DB, 0x0DC, 0x0DF, 0x0E0, 0x0E1, 0x0E2, 0x0E3, 0x0E4,
2113 0x0E5, 0x0E6, 0x0E7, 0x0E8, 0xFC7, 0xFC8, 0xFE6, 0xFF8},
2114 "Floor surfaces, carpet, pits, water, ice, and conveyors"},
2115 {"Decorations",
2116 {0x036, 0x037, 0x038, 0x039, 0x03A, 0x03B, 0x03C, 0x03D, 0x03E, 0x04B,
2117 0x04C, 0x04D, 0x04E, 0x04F, 0x051, 0x052, 0x055, 0x056, 0x05B, 0x05C,
2118 0x073, 0x074, 0x075, 0x076, 0x077, 0x078, 0x07B, 0x07F, 0x080, 0x081,
2119 0x082, 0x083, 0x084, 0x085, 0x086, 0x087, 0x08F, 0x090, 0x091, 0x095,
2120 0x0B5, 0x0B6, 0x0B7, 0x0BB, 0x0BC, 0x0DD, 0x11C, 0x11D, 0x120, 0x121,
2121 0x122, 0x123, 0x124, 0x125, 0x126, 0x127, 0x128, 0x129, 0x12A, 0x12B,
2122 0x12C, 0x13D, 0x13E, 0x13F, 0xFAA, 0xFAB, 0xFAD, 0xFB0, 0xFB7, 0xFB8,
2123 0xFB9, 0xFC9, 0xFCB, 0xFCC, 0xFCD, 0xFCE, 0xFD1, 0xFD5, 0xFD6, 0xFD7,
2124 0xFD8, 0xFD9, 0xFDA, 0xFDB, 0xFDC, 0xFDD, 0xFDE, 0xFDF, 0xFE0, 0xFE1,
2125 0xFE3, 0xFEB, 0xFEC, 0xFED, 0xFEE, 0xFEF, 0xFF0, 0xFF1, 0xFF7, 0xFF9,
2126 0xFFA},
2127 "Furniture, ornaments, torches, and room scenery"},
2128 {"Interactive",
2129 {0x05E, 0x089, 0x092, 0x093, 0x096, 0x0B8, 0x0B9, 0x0BD,
2130 0x0DE, 0x11E, 0x11F, 0x134, 0x137, 0xF92, 0xF93, 0xF96,
2131 0xF98, 0xFAC, 0xFAF, 0xFCA, 0xFCF, 0xFD0, 0xFD2, 0xFD3},
2132 "Blocks, pegs, switches, locks, and other interaction tiles"},
2133 {"Stairs",
2134 {0x021, 0x12D, 0x12E, 0x12F, 0x130, 0x131, 0x132, 0x133, 0x135, 0x136,
2135 0x138, 0x139, 0x13A, 0x13B, 0xF9B, 0xF9C, 0xF9D, 0xF9E, 0xF9F, 0xFA0,
2136 0xFA1, 0xFA6, 0xFA7, 0xFA8, 0xFA9, 0xFB3, 0xFB4, 0xFB5, 0xFB6},
2137 "Platform, interroom, intraroom, water-hop stairs, and ladders"},
2138 {"Doors",
2139 {0x035, 0xFF4, 0xFF6},
2140 "Doorway tile objects; ordinary room doors use the Door Editor"},
2141 {"Chests",
2142 {0xF99, 0xF9A, 0xFB1, 0xFB2, 0xFF5},
2143 "Small, big, open, and minigame chest objects"},
2144 {"Special", {}, "Other, logic, unused, and unclassified tile objects"}};
2145
2146 std::array<bool, 0x1000> categorized{};
2147 for (const auto& category : result) {
2148 for (const int id : category.object_ids) {
2149 categorized[id] = true;
2150 }
2151 }
2152 for (int id = 0; id < static_cast<int>(categorized.size()); ++id) {
2153 if (IsRepresentableRoomObjectId(id) && !categorized[id]) {
2154 result.back().object_ids.push_back(id);
2155 }
2156 }
2157 return result;
2158 }();
2159 return categories;
2160}
2161
2162} // namespace
2163
2164std::vector<ObjectCategory> GetObjectCategories() {
2165 return CategoryTable();
2166}
2167
2168absl::StatusOr<std::vector<int>> GetObjectsInCategory(
2169 const std::string& category_name) {
2170 for (const auto& category : CategoryTable()) {
2171 if (category.name == category_name) {
2172 return category.object_ids;
2173 }
2174 }
2175
2176 return absl::NotFoundError("Category not found");
2177}
2178
2179absl::StatusOr<std::string> GetObjectCategory(int object_id) {
2180 for (const auto& category : CategoryTable()) {
2181 for (int id : category.object_ids) {
2182 if (id == object_id) {
2183 return category.name;
2184 }
2185 }
2186 }
2187
2188 return absl::NotFoundError("Object category not found");
2189}
2190
2191absl::StatusOr<ObjectInfo> GetObjectInfo(int object_id) {
2192 ObjectInfo info;
2193 info.id = object_id;
2194
2195 // This is a simplified implementation - in practice, you'd have
2196 // a comprehensive database of object information
2197
2198 if (object_id >= 0x10 && object_id <= 0x1F) {
2199 info.name = "Wall";
2200 info.description = "Basic wall object";
2201 info.valid_sizes = {{0x12, 0x12}};
2202 info.valid_layers = {0, 1, 2};
2203 info.is_interactive = false;
2204 info.is_collidable = true;
2205 } else if (object_id >= 0x20 && object_id <= 0x2F) {
2206 info.name = "Floor";
2207 info.description = "Floor tile object";
2208 info.valid_sizes = {{0x12, 0x12}};
2209 info.valid_layers = {0, 1, 2};
2210 info.is_interactive = false;
2211 info.is_collidable = false;
2212 } else if (object_id == 0xF9) {
2213 info.name = "Small Chest";
2214 info.description = "Small treasure chest";
2215 info.valid_sizes = {{0x12, 0x12}};
2216 info.valid_layers = {0, 1};
2217 info.is_interactive = true;
2218 info.is_collidable = true;
2219 } else {
2220 info.name = "Unknown Object";
2221 info.description = "Unknown object type";
2222 info.valid_sizes = {{0x12, 0x12}};
2223 info.valid_layers = {0};
2224 info.is_interactive = false;
2225 info.is_collidable = true;
2226 }
2227
2228 return info;
2229}
2230
2231} // namespace ObjectCategories
2232
2233} // namespace zelda3
2234} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
int height() const
Definition bitmap.h:397
void SetPixel(int x, int y, const SnesColor &color)
Set a pixel at the given x,y coordinates with SNES color.
Definition bitmap.cc:737
int width() const
Definition bitmap.h:396
SNES Color container.
Definition snes_color.h:110
RAII guard for ImGui style colors.
Definition style_guard.h:27
std::function< void(const SelectionState &)> SelectionChangedCallback
absl::Status HandleMouseDrag(int start_x, int start_y, int current_x, int current_y)
absl::Status HandleScrollWheel(int delta, int x, int y, bool ctrl_pressed)
std::pair< int, int > ScreenToRoomCoordinates(int screen_x, int screen_y)
void SetRoomChangedCallback(RoomChangedCallback callback)
std::pair< int, int > RoomToScreenCoordinates(int room_x, int room_y)
const std::vector< ObjectTemplate > & GetTemplates() const
absl::Status BatchMoveObjects(const std::vector< size_t > &indices, int dx, int dy)
absl::Status ChangeObjectLayer(size_t object_index, int new_layer)
absl::Status BatchChangeObjectLayer(const std::vector< size_t > &indices, int new_layer)
absl::Status DeleteObject(size_t object_index)
void CopySelectedObjects(const std::vector< size_t > &indices)
void SetSelectionChangedCallback(SelectionChangedCallback callback)
absl::Status SelectObject(int screen_x, int screen_y)
std::optional< size_t > DuplicateObject(size_t object_index, int offset_x=1, int offset_y=1)
absl::Status AlignSelectedObjects(Alignment alignment)
absl::Status InsertObject(int x, int y, int object_type, int size=0x02, int layer=0)
std::optional< size_t > FindObjectAt(int room_x, int room_y)
absl::Status AddToSelection(size_t object_index)
void RenderLayerVisualization(gfx::Bitmap &canvas)
std::vector< std::string > GetValidationErrors()
void SetConfig(const EditorConfig &config)
std::function< void(size_t object_index, const RoomObject &object)> ObjectChangedCallback
absl::Status HandleDragOperation(int current_x, int current_y)
absl::Status HandleSizeEdit(int delta, int x, int y)
bool ObjectsCollide(const RoomObject &obj1, const RoomObject &obj2)
bool IsObjectAtPosition(const RoomObject &object, int x, int y)
void SetObjectChangedCallback(ObjectChangedCallback callback)
absl::Status HandleMouseRelease(int x, int y)
absl::Status InsertTemplate(const ObjectTemplate &tmpl, int x, int y)
absl::Status MoveObject(size_t object_index, int new_x, int new_y)
absl::Status ResizeObject(size_t object_index, int new_size)
std::optional< RoomObject > preview_object_
absl::Status BatchResizeObjects(const std::vector< size_t > &indices, int new_size)
absl::Status ChangeObjectType(size_t object_index, int new_type)
SelectionChangedCallback selection_changed_callback_
absl::Status ApplyUndoPoint(const UndoPoint &undo_point)
absl::Status HandleMouseClick(int x, int y, bool left_button, bool right_button, bool shift_pressed)
void RenderSelectionHighlight(gfx::Bitmap &canvas)
absl::Status CreateTemplateFromSelection(const std::string &name, const std::string &description)
ValidationResult ValidateRoom(const Room &room)
static ObjectTemplate CreateFromObjects(const std::string &name, const std::string &description, const std::vector< RoomObject > &objects, int origin_x, int origin_y)
const std::vector< ObjectTemplate > & GetTemplates() const
std::vector< RoomObject > InstantiateTemplate(const ObjectTemplate &tmpl, int x, int y, Rom *rom)
absl::Status LoadTemplates(const std::string &directory_path)
absl::Status SaveTemplate(const ObjectTemplate &tmpl, const std::string &directory_path)
void set_x(uint8_t x)
Definition room_object.h:84
void SetRom(Rom *rom)
Definition room_object.h:78
RoomObject CopyForNewPlacement() const
void set_y(uint8_t y)
Definition room_object.h:85
void ClearTileObjects()
Definition room.h:411
void MarkSaveDirtyForTileObject(const RoomObject &object)
Definition room.h:440
absl::Status RemoveObject(size_t index)
Definition room.cc:2530
size_t GetTileObjectCount() const
Definition room.h:490
RoomObject & GetTileObject(size_t index)
Definition room.h:491
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2152
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:405
void SetTileObjects(const std::vector< RoomObject > &objects)
Definition room.h:670
absl::Status AddObject(const RoomObject &object)
Definition room.cc:2516
void RemoveTileObject(size_t index)
Definition room.h:482
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_PLACE
Definition icons.h:1477
#define ICON_MD_OPEN_WITH
Definition icons.h:1356
#define ICON_MD_ARROW_DOWNWARD
Definition icons.h:180
#define ICON_MD_ASPECT_RATIO
Definition icons.h:192
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_ARROW_UPWARD
Definition icons.h:189
#define ICON_MD_ARROW_BACK
Definition icons.h:173
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_DESELECT
Definition icons.h:540
#define ICON_MD_TAG
Definition icons.h:1940
#define ICON_MD_DELETE_SWEEP
Definition icons.h:533
const AgentUITheme & GetTheme()
void EndPropertyTable()
void PropertyRow(const char *label, const char *value)
void SectionHeader(const char *icon, const char *label, const ImVec4 &color)
bool BeginPropertyTable(const char *id, int columns, ImGuiTableFlags extra_flags)
void HelpMarker(const char *desc)
std::vector< ObjectCategory > GetObjectCategories()
Get all available object categories.
absl::StatusOr< ObjectInfo > GetObjectInfo(int object_id)
absl::StatusOr< std::string > GetObjectCategory(int object_id)
Get category for a specific object.
absl::StatusOr< std::vector< int > > GetObjectsInCategory(const std::string &category_name)
Get objects in a specific category.
ObjectLayerSemantics GetObjectLayerSemantics(const RoomObject &object)
uint8_t ResizeRoomObjectByDelta(int object_id, uint8_t size, int delta, bool horizontal)
uint8_t DefaultRoomObjectSizeForPlacement(int object_id)
bool IsRoomObjectSizeEditable(int object_id)
std::unique_ptr< DungeonObjectEditor > CreateDungeonObjectEditor(Rom *rom)
Factory function to create dungeon object editor.
int GetObjectSubtype(int object_id)
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
const char * ObjectRenderRoutingDisplayLabel(const ObjectLayerSemantics &semantics)
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
bool UsesSpecialLayerSelector(const RoomObject &object)
absl::StatusOr< ObjectStorageMutationResult > ReassignObjectStorage(std::vector< RoomObject > &objects, const std::vector< size_t > &indices, int target_value)
std::chrono::steady_clock::time_point timestamp
std::vector< std::pair< int, int > > valid_sizes
std::vector< std::string > errors