640absl::Status DungeonObjectValidateCommandHandler::Execute(
643 auto object_arg = parser.
GetInt(
"object");
644 auto size_arg = parser.
GetInt(
"size");
645 auto room_arg = parser.
GetInt(
"room");
646 auto report_arg = parser.
GetString(
"report");
647 auto trace_out_arg = parser.
GetString(
"trace-out");
648 const bool verbose = parser.
HasFlag(
"verbose");
649 const bool all_sizes = parser.
HasFlag(
"all-sizes");
650 const bool all_states = parser.
HasFlag(
"all-states");
651 if (all_sizes && size_arg.ok()) {
652 return absl::InvalidArgumentError(
653 "--all-sizes cannot be combined with --size");
655 if (size_arg.ok() && (size_arg.value() < 0 || size_arg.value() > 0x0F)) {
656 return absl::InvalidArgumentError(
"Object size must be between 0 and 15");
658 const bool room_mode = room_arg.ok();
659 const int room_id = room_mode ? room_arg.value() : -1;
660 if (room_mode && (room_id < 0 || room_id >= kNumRooms)) {
661 return absl::InvalidArgumentError(
662 absl::StrFormat(
"Room ID must be between 0 and %d", kNumRooms - 1));
664 if (room_mode && (size_arg.ok() || all_sizes)) {
665 return absl::InvalidArgumentError(
666 "--room cannot be combined with --size or --all-sizes");
669 (!object_arg.ok() || !IsType1ObjectId(object_arg.value()))) {
670 return absl::InvalidArgumentError(
671 "--size requires a Type 1 --object (0x000-0x0F7)");
675 auto& dimension_table = zelda3::ObjectDimensionTable::Get();
676 auto load_status = dimension_table.LoadFromRom(rom);
677 if (!load_status.ok()) {
680 std::vector<int> object_ids = BuildObjectIds(object_arg);
681 ActiveValidationState active_state;
682 const std::vector<StateProfile> state_profiles =
683 BuildStateProfiles(all_states, &active_state);
685 std::vector<zelda3::ObjectDrawer::TileTrace> trace;
686 std::vector<std::unique_ptr<zelda3::ObjectDrawer>> profile_drawers;
687 profile_drawers.reserve(state_profiles.size());
688 for (
size_t i = 0; i < state_profiles.size(); ++i) {
689 auto drawer = std::make_unique<zelda3::ObjectDrawer>(
690 rom, room_mode ? room_id : 0,
nullptr);
691 drawer->SetTraceCollector(&trace,
true);
692 profile_drawers.push_back(std::move(drawer));
700 int mismatch_count = 0;
701 int empty_trace_count = 0;
702 int expected_empty_trace_count = 0;
703 int negative_offset_count = 0;
704 int skipped_nothing = 0;
705 int room_object_count = 0;
706 std::vector<ValidationResult> mismatches;
707 std::vector<ValidationResult> empty_traces;
708 std::vector<TraceDumpCase> trace_cases;
710 int size_case_count = room_mode ? 1 : 0;
712 for (
int object_id : object_ids) {
713 size_case_count = std::max(
716 BuildSizesForObject(object_id, size_arg, all_sizes).size()));
720 ReportPaths report_paths = ResolveReportPaths(
721 report_arg.has_value() ? report_arg.value() : std::string());
722 const bool write_trace_dump = trace_out_arg.has_value();
723 if (write_trace_dump) {
724 trace_cases.reserve(object_ids.size() * size_case_count *
725 state_profiles.size());
728 ScopedCustomObjectsDisabled custom_objects_disabled;
731 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
734 room_object_count =
static_cast<int>(room_objects.size());
735 if (write_trace_dump) {
736 trace_cases.reserve(room_objects.size());
739 for (
size_t idx = 0; idx < room_objects.size(); ++idx) {
740 const auto& room_obj = room_objects[idx];
741 int object_id = room_obj.id_;
743 zelda3::DrawRoutineRegistry::Get().GetRoutineIdForObject(object_id);
744 if (routine_id == zelda3::DrawRoutineIds::kNothing) {
748 for (
size_t profile_index = 0; profile_index < state_profiles.size();
750 const auto& profile = state_profiles[profile_index];
751 auto& drawer = *profile_drawers[profile_index];
757 const bool expected_empty = IsExpectedEmptyState(object_id, profile);
758 const bool expected_has_tiles = !expected_empty;
759 auto expected_bounds =
760 ResolveExpectedBounds(obj, profile.state, expected_empty);
761 if (expected_has_tiles) {
762 expected_bounds = detail::ClipSelectionBoundsToRoom(
763 object_id, obj.
size_, expected_bounds, obj.
x_, obj.
y_);
767 drawer.DrawObject(obj, bg1, bg2, palette_group, profile.state);
768 if (!draw_status.ok()) {
772 TraceBounds bounds = ComputeBounds(trace);
773 if (write_trace_dump) {
774 TraceDumpCase trace_case{};
775 trace_case.object_id = object_id;
776 trace_case.size = obj.
size_;
777 trace_case.state_profile = profile.name;
778 trace_case.expected_has_tiles = expected_has_tiles;
779 trace_case.has_room_context =
true;
780 trace_case.room_id = room_id;
781 trace_case.object_index =
static_cast<int>(idx);
782 trace_case.object_x = obj.
x_;
783 trace_case.object_y = obj.
y_;
785 trace_case.tiles = NormalizeTrace(trace);
786 trace_cases.push_back(std::move(trace_case));
789 ValidationResult result{};
790 result.object_id = object_id;
791 result.size = obj.
size_;
792 result.state_profile = profile.name;
793 result.expected_has_tiles = expected_has_tiles;
794 result.has_room_context =
true;
795 result.room_id = room_id;
796 result.object_index =
static_cast<int>(idx);
797 result.object_x = obj.
x_;
798 result.object_y = obj.
y_;
800 result.expected_width = expected_bounds.width_tiles;
801 result.expected_height = expected_bounds.height_tiles;
802 result.expected_offset_x = expected_bounds.offset_x_tiles;
803 result.expected_offset_y = expected_bounds.offset_y_tiles;
805 if (!bounds.has_tiles) {
806 result.has_tiles =
false;
807 if (!expected_has_tiles) {
808 expected_empty_trace_count++;
812 result.size_mismatch =
true;
814 mismatches.push_back(result);
816 empty_traces.push_back(result);
821 result.has_tiles =
true;
822 result.trace_width = bounds.width;
823 result.trace_height = bounds.height;
824 result.trace_min_x = bounds.min_x;
825 result.trace_min_y = bounds.min_y;
826 result.trace_offset_x = bounds.min_x - obj.
x_;
827 result.trace_offset_y = bounds.min_y - obj.
y_;
828 result.size_mismatch = !expected_has_tiles ||
829 bounds.width != expected_bounds.width_tiles ||
830 bounds.height != expected_bounds.height_tiles;
831 result.offset_mismatch =
832 expected_has_tiles &&
833 (result.trace_offset_x != expected_bounds.offset_x_tiles ||
834 result.trace_offset_y != expected_bounds.offset_y_tiles);
836 if (expected_has_tiles && (expected_bounds.offset_x_tiles < 0 ||
837 expected_bounds.offset_y_tiles < 0)) {
838 negative_offset_count++;
841 if (result.size_mismatch || result.offset_mismatch) {
843 mismatches.push_back(result);
848 for (
int object_id : object_ids) {
850 zelda3::DrawRoutineRegistry::Get().GetRoutineIdForObject(object_id);
851 if (routine_id == zelda3::DrawRoutineIds::kNothing) {
855 const std::vector<int> object_sizes =
856 BuildSizesForObject(object_id, size_arg, all_sizes);
857 for (
int size : object_sizes) {
858 for (
size_t profile_index = 0; profile_index < state_profiles.size();
860 const auto& profile = state_profiles[profile_index];
861 auto& drawer = *profile_drawers[profile_index];
862 drawer.ResetChestIndex();
869 static_cast<uint8_t
>(size), 0);
870 const bool expected_empty = IsExpectedEmptyState(object_id, profile);
871 const bool expected_has_tiles = !expected_empty;
872 auto expected_bounds =
873 ResolveExpectedBounds(temp_obj, profile.state, expected_empty);
874 const auto [origin_x, origin_y] =
876 ? ChooseOriginForExpectedBounds(expected_bounds)
877 : std::pair<int, int>{0, 0};
880 static_cast<uint8_t
>(size), 0);
881 obj.
layer_ = zelda3::RoomObject::LayerType::BG1;
884 ResolveExpectedBounds(obj, profile.state, expected_empty);
885 if (expected_has_tiles) {
886 expected_bounds = detail::ClipSelectionBoundsToRoom(
887 object_id, size, expected_bounds, obj.
x_, obj.
y_);
890 if (expected_has_tiles && (expected_bounds.offset_x_tiles < 0 ||
891 expected_bounds.offset_y_tiles < 0)) {
892 negative_offset_count++;
896 drawer.DrawObject(obj, bg1, bg2, palette_group, profile.state);
897 if (!draw_status.ok()) {
901 TraceBounds bounds = ComputeBounds(trace);
902 if (write_trace_dump) {
903 TraceDumpCase trace_case{};
904 trace_case.object_id = object_id;
905 trace_case.size = size;
906 trace_case.state_profile = profile.name;
907 trace_case.expected_has_tiles = expected_has_tiles;
908 trace_case.tiles = NormalizeTrace(trace);
909 trace_cases.push_back(std::move(trace_case));
912 ValidationResult result{};
913 result.object_id = object_id;
915 result.state_profile = profile.name;
916 result.expected_has_tiles = expected_has_tiles;
917 result.expected_width = expected_bounds.width_tiles;
918 result.expected_height = expected_bounds.height_tiles;
919 result.expected_offset_x = expected_bounds.offset_x_tiles;
920 result.expected_offset_y = expected_bounds.offset_y_tiles;
922 if (!bounds.has_tiles) {
923 result.has_tiles =
false;
924 if (!expected_has_tiles) {
925 expected_empty_trace_count++;
929 result.size_mismatch =
true;
931 mismatches.push_back(result);
933 empty_traces.push_back(result);
938 result.has_tiles =
true;
939 result.trace_width = bounds.width;
940 result.trace_height = bounds.height;
941 result.trace_min_x = bounds.min_x;
942 result.trace_min_y = bounds.min_y;
943 result.trace_offset_x = bounds.min_x - obj.
x_;
944 result.trace_offset_y = bounds.min_y - obj.
y_;
945 result.size_mismatch = !expected_has_tiles ||
946 bounds.width != expected_bounds.width_tiles ||
947 bounds.height != expected_bounds.height_tiles;
948 result.offset_mismatch =
949 expected_has_tiles &&
950 (result.trace_offset_x != expected_bounds.offset_x_tiles ||
951 result.trace_offset_y != expected_bounds.offset_y_tiles);
953 if (result.size_mismatch || result.offset_mismatch) {
955 mismatches.push_back(result);
962 const int object_count =
963 room_mode ? room_object_count :
static_cast<int>(object_ids.size());
964 auto json_status = WriteJsonReport(
965 report_paths, room_mode, object_count, size_case_count,
966 static_cast<int>(state_profiles.size()), total_tests, mismatch_count,
967 empty_trace_count, expected_empty_trace_count, negative_offset_count,
968 skipped_nothing, mismatches);
969 if (!json_status.ok()) {
973 auto csv_status = WriteCsvReport(report_paths, room_mode, mismatches);
974 if (!csv_status.ok()) {
978 if (write_trace_dump) {
980 WriteTraceDump(trace_out_arg.value(), room_mode, empty_trace_count,
981 expected_empty_trace_count, trace_cases);
982 if (!trace_status.ok()) {
985 formatter.
AddField(
"trace_dump", trace_out_arg.value());
988 formatter.
AddField(
"object_count", object_count);
989 formatter.
AddField(
"size_cases", size_case_count);
990 formatter.
AddField(
"state_cases",
static_cast<int>(state_profiles.size()));
991 formatter.
AddField(
"test_cases", total_tests);
992 formatter.
AddField(
"mismatch_count", mismatch_count);
993 formatter.
AddField(
"empty_traces", empty_trace_count);
994 formatter.
AddField(
"expected_empty_traces", expected_empty_trace_count);
995 formatter.
AddField(
"negative_offsets", negative_offset_count);
996 formatter.
AddField(
"skipped_nothing", skipped_nothing);
998 formatter.
AddField(
"room_id", room_id);
1000 formatter.
AddField(
"report_json", report_paths.json_path);
1001 formatter.
AddField(
"report_csv", report_paths.csv_path);
1004 for (
const auto& result : mismatches) {
1006 : result.FormatText(room_mode));
1012 for (
const auto& result : empty_traces) {
1014 : result.FormatText(room_mode));
1019 return absl::OkStatus();