638absl::Status DungeonObjectValidateCommandHandler::Execute(
641 auto object_arg = parser.
GetInt(
"object");
642 auto size_arg = parser.
GetInt(
"size");
643 auto room_arg = parser.
GetInt(
"room");
644 auto report_arg = parser.
GetString(
"report");
645 auto trace_out_arg = parser.
GetString(
"trace-out");
646 const bool verbose = parser.
HasFlag(
"verbose");
647 const bool all_sizes = parser.
HasFlag(
"all-sizes");
648 const bool all_states = parser.
HasFlag(
"all-states");
649 if (all_sizes && size_arg.ok()) {
650 return absl::InvalidArgumentError(
651 "--all-sizes cannot be combined with --size");
653 if (size_arg.ok() && (size_arg.value() < 0 || size_arg.value() > 0x0F)) {
654 return absl::InvalidArgumentError(
"Object size must be between 0 and 15");
656 const bool room_mode = room_arg.ok();
657 const int room_id = room_mode ? room_arg.value() : -1;
658 if (room_mode && (room_id < 0 || room_id >= kNumRooms)) {
659 return absl::InvalidArgumentError(
660 absl::StrFormat(
"Room ID must be between 0 and %d", kNumRooms - 1));
662 if (room_mode && (size_arg.ok() || all_sizes)) {
663 return absl::InvalidArgumentError(
664 "--room cannot be combined with --size or --all-sizes");
667 (!object_arg.ok() || !IsType1ObjectId(object_arg.value()))) {
668 return absl::InvalidArgumentError(
669 "--size requires a Type 1 --object (0x000-0x0F7)");
671 const bool write_report = report_arg.has_value();
672 if (write_report && report_arg->empty()) {
673 return absl::InvalidArgumentError(
"--report requires a non-empty path");
677 auto& dimension_table = zelda3::ObjectDimensionTable::Get();
678 auto load_status = dimension_table.LoadFromRom(rom);
679 if (!load_status.ok()) {
682 std::vector<int> object_ids = BuildObjectIds(object_arg);
683 ActiveValidationState active_state;
684 const std::vector<StateProfile> state_profiles =
685 BuildStateProfiles(all_states, &active_state);
687 std::vector<zelda3::ObjectDrawer::TileTrace> trace;
688 std::vector<std::unique_ptr<zelda3::ObjectDrawer>> profile_drawers;
689 profile_drawers.reserve(state_profiles.size());
690 for (
size_t i = 0; i < state_profiles.size(); ++i) {
691 auto drawer = std::make_unique<zelda3::ObjectDrawer>(
692 rom, room_mode ? room_id : 0,
nullptr);
693 drawer->SetTraceCollector(&trace,
true);
694 profile_drawers.push_back(std::move(drawer));
702 int mismatch_count = 0;
703 int empty_trace_count = 0;
704 int expected_empty_trace_count = 0;
705 int negative_offset_count = 0;
706 int skipped_nothing = 0;
707 int room_object_count = 0;
708 std::vector<ValidationResult> mismatches;
709 std::vector<ValidationResult> empty_traces;
710 std::vector<TraceDumpCase> trace_cases;
712 int size_case_count = room_mode ? 1 : 0;
714 for (
int object_id : object_ids) {
715 size_case_count = std::max(
718 BuildSizesForObject(object_id, size_arg, all_sizes).size()));
722 ReportPaths report_paths;
724 report_paths = ResolveReportPaths(report_arg.value());
726 const bool write_trace_dump = trace_out_arg.has_value();
727 if (write_trace_dump) {
728 trace_cases.reserve(object_ids.size() * size_case_count *
729 state_profiles.size());
732 ScopedCustomObjectsDisabled custom_objects_disabled;
735 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
737 for (
auto& drawer : profile_drawers) {
738 drawer->SetRoomFloorGraphics(room.
floor1(), room.
floor2());
741 room_object_count =
static_cast<int>(room_objects.size());
742 if (write_trace_dump) {
743 trace_cases.reserve(room_objects.size());
746 for (
size_t idx = 0; idx < room_objects.size(); ++idx) {
747 const auto& room_obj = room_objects[idx];
748 int object_id = room_obj.id_;
750 zelda3::DrawRoutineRegistry::Get().GetRoutineIdForObject(object_id);
751 if (routine_id == zelda3::DrawRoutineIds::kNothing) {
755 for (
size_t profile_index = 0; profile_index < state_profiles.size();
757 const auto& profile = state_profiles[profile_index];
758 auto& drawer = *profile_drawers[profile_index];
764 const bool expected_empty = IsExpectedEmptyState(object_id, profile);
765 const bool expected_has_tiles = !expected_empty;
766 auto expected_bounds =
767 ResolveExpectedBounds(obj, profile.state, expected_empty);
768 if (expected_has_tiles) {
769 expected_bounds = detail::ClipSelectionBoundsToRoom(
770 object_id, obj.
size_, expected_bounds, obj.
x_, obj.
y_);
774 drawer.DrawObject(obj, bg1, bg2, palette_group, profile.state);
775 if (!draw_status.ok()) {
779 TraceBounds bounds = ComputeBounds(trace);
780 if (write_trace_dump) {
781 TraceDumpCase trace_case{};
782 trace_case.object_id = object_id;
783 trace_case.size = obj.
size_;
784 trace_case.state_profile = profile.name;
785 trace_case.expected_has_tiles = expected_has_tiles;
786 trace_case.has_room_context =
true;
787 trace_case.room_id = room_id;
788 trace_case.object_index =
static_cast<int>(idx);
789 trace_case.object_x = obj.
x_;
790 trace_case.object_y = obj.
y_;
792 trace_case.tiles = NormalizeTrace(trace);
793 trace_cases.push_back(std::move(trace_case));
796 ValidationResult result{};
797 result.object_id = object_id;
798 result.size = obj.
size_;
799 result.state_profile = profile.name;
800 result.expected_has_tiles = expected_has_tiles;
801 result.has_room_context =
true;
802 result.room_id = room_id;
803 result.object_index =
static_cast<int>(idx);
804 result.object_x = obj.
x_;
805 result.object_y = obj.
y_;
807 result.expected_width = expected_bounds.width_tiles;
808 result.expected_height = expected_bounds.height_tiles;
809 result.expected_offset_x = expected_bounds.offset_x_tiles;
810 result.expected_offset_y = expected_bounds.offset_y_tiles;
812 if (!bounds.has_tiles) {
813 result.has_tiles =
false;
814 if (!expected_has_tiles) {
815 expected_empty_trace_count++;
819 result.size_mismatch =
true;
821 mismatches.push_back(result);
823 empty_traces.push_back(result);
828 result.has_tiles =
true;
829 result.trace_width = bounds.width;
830 result.trace_height = bounds.height;
831 result.trace_min_x = bounds.min_x;
832 result.trace_min_y = bounds.min_y;
833 result.trace_offset_x = bounds.min_x - obj.
x_;
834 result.trace_offset_y = bounds.min_y - obj.
y_;
835 result.size_mismatch = !expected_has_tiles ||
836 bounds.width != expected_bounds.width_tiles ||
837 bounds.height != expected_bounds.height_tiles;
838 result.offset_mismatch =
839 expected_has_tiles &&
840 (result.trace_offset_x != expected_bounds.offset_x_tiles ||
841 result.trace_offset_y != expected_bounds.offset_y_tiles);
843 if (expected_has_tiles && (expected_bounds.offset_x_tiles < 0 ||
844 expected_bounds.offset_y_tiles < 0)) {
845 negative_offset_count++;
848 if (result.size_mismatch || result.offset_mismatch) {
850 mismatches.push_back(result);
855 for (
int object_id : object_ids) {
857 zelda3::DrawRoutineRegistry::Get().GetRoutineIdForObject(object_id);
858 if (routine_id == zelda3::DrawRoutineIds::kNothing) {
862 const std::vector<int> object_sizes =
863 BuildSizesForObject(object_id, size_arg, all_sizes);
864 for (
int size : object_sizes) {
865 for (
size_t profile_index = 0; profile_index < state_profiles.size();
867 const auto& profile = state_profiles[profile_index];
868 auto& drawer = *profile_drawers[profile_index];
869 drawer.ResetChestIndex();
876 static_cast<uint8_t
>(size), 0);
877 const bool expected_empty = IsExpectedEmptyState(object_id, profile);
878 const bool expected_has_tiles = !expected_empty;
879 auto expected_bounds =
880 ResolveExpectedBounds(temp_obj, profile.state, expected_empty);
881 const auto [origin_x, origin_y] =
883 ? ChooseOriginForExpectedBounds(expected_bounds)
884 : std::pair<int, int>{0, 0};
887 static_cast<uint8_t
>(size), 0);
888 obj.
layer_ = zelda3::RoomObject::LayerType::BG1;
891 ResolveExpectedBounds(obj, profile.state, expected_empty);
892 if (expected_has_tiles) {
893 expected_bounds = detail::ClipSelectionBoundsToRoom(
894 object_id, size, expected_bounds, obj.
x_, obj.
y_);
897 if (expected_has_tiles && (expected_bounds.offset_x_tiles < 0 ||
898 expected_bounds.offset_y_tiles < 0)) {
899 negative_offset_count++;
903 drawer.DrawObject(obj, bg1, bg2, palette_group, profile.state);
904 if (!draw_status.ok()) {
908 TraceBounds bounds = ComputeBounds(trace);
909 if (write_trace_dump) {
910 TraceDumpCase trace_case{};
911 trace_case.object_id = object_id;
912 trace_case.size = size;
913 trace_case.state_profile = profile.name;
914 trace_case.expected_has_tiles = expected_has_tiles;
915 trace_case.tiles = NormalizeTrace(trace);
916 trace_cases.push_back(std::move(trace_case));
919 ValidationResult result{};
920 result.object_id = object_id;
922 result.state_profile = profile.name;
923 result.expected_has_tiles = expected_has_tiles;
924 result.expected_width = expected_bounds.width_tiles;
925 result.expected_height = expected_bounds.height_tiles;
926 result.expected_offset_x = expected_bounds.offset_x_tiles;
927 result.expected_offset_y = expected_bounds.offset_y_tiles;
929 if (!bounds.has_tiles) {
930 result.has_tiles =
false;
931 if (!expected_has_tiles) {
932 expected_empty_trace_count++;
936 result.size_mismatch =
true;
938 mismatches.push_back(result);
940 empty_traces.push_back(result);
945 result.has_tiles =
true;
946 result.trace_width = bounds.width;
947 result.trace_height = bounds.height;
948 result.trace_min_x = bounds.min_x;
949 result.trace_min_y = bounds.min_y;
950 result.trace_offset_x = bounds.min_x - obj.
x_;
951 result.trace_offset_y = bounds.min_y - obj.
y_;
952 result.size_mismatch = !expected_has_tiles ||
953 bounds.width != expected_bounds.width_tiles ||
954 bounds.height != expected_bounds.height_tiles;
955 result.offset_mismatch =
956 expected_has_tiles &&
957 (result.trace_offset_x != expected_bounds.offset_x_tiles ||
958 result.trace_offset_y != expected_bounds.offset_y_tiles);
960 if (result.size_mismatch || result.offset_mismatch) {
962 mismatches.push_back(result);
969 const int object_count =
970 room_mode ? room_object_count :
static_cast<int>(object_ids.size());
972 auto json_status = WriteJsonReport(
973 report_paths, room_mode, object_count, size_case_count,
974 static_cast<int>(state_profiles.size()), total_tests, mismatch_count,
975 empty_trace_count, expected_empty_trace_count, negative_offset_count,
976 skipped_nothing, mismatches);
977 if (!json_status.ok()) {
981 auto csv_status = WriteCsvReport(report_paths, room_mode, mismatches);
982 if (!csv_status.ok()) {
987 if (write_trace_dump) {
989 WriteTraceDump(trace_out_arg.value(), room_mode, empty_trace_count,
990 expected_empty_trace_count, trace_cases);
991 if (!trace_status.ok()) {
994 formatter.
AddField(
"trace_dump", trace_out_arg.value());
997 formatter.
AddField(
"object_count", object_count);
998 formatter.
AddField(
"size_cases", size_case_count);
999 formatter.
AddField(
"state_cases",
static_cast<int>(state_profiles.size()));
1000 formatter.
AddField(
"test_cases", total_tests);
1001 formatter.
AddField(
"mismatch_count", mismatch_count);
1002 formatter.
AddField(
"empty_traces", empty_trace_count);
1003 formatter.
AddField(
"expected_empty_traces", expected_empty_trace_count);
1004 formatter.
AddField(
"negative_offsets", negative_offset_count);
1005 formatter.
AddField(
"skipped_nothing", skipped_nothing);
1007 formatter.
AddField(
"room_id", room_id);
1010 formatter.
AddField(
"report_json", report_paths.json_path);
1011 formatter.
AddField(
"report_csv", report_paths.csv_path);
1015 for (
const auto& result : mismatches) {
1017 : result.FormatText(room_mode));
1023 for (
const auto& result : empty_traces) {
1025 : result.FormatText(room_mode));
1030 return absl::OkStatus();