From 5ebf6d0511a7c702ef6a3306fa9a61a7228669f0 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 5 Apr 2026 17:02:29 -0500 Subject: [PATCH 01/17] Added type detection for home and lza. Added sorting rules for box sorters --- .../Options/Pokemon_BoxSortingTable.cpp | 1 + .../Pokemon/Options/Pokemon_BoxSortingTable.h | 1 + .../Pokemon/Pokemon_CollectedPokemonInfo.cpp | 16 +- .../Pokemon/Pokemon_CollectedPokemonInfo.h | 3 + .../Inference/PokemonHome_TypeReader.cpp | 269 ++++++++++++++++++ .../Inference/PokemonHome_TypeReader.h | 39 +++ .../Programs/PokemonHome_BoxSorter.cpp | 11 +- .../Programs/PokemonLZA_BoxSorter.cpp | 5 + SerialPrograms/cmake/SourceFiles.cmake | 2 + 9 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp create mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.cpp index ef3b381c1e..7a06bddb33 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.cpp +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.cpp @@ -18,6 +18,7 @@ const EnumDropdownDatabase& SortingRuleType_Database(){ {SortingRuleType::Alpha, "alpha", "Alpha"}, {SortingRuleType::Ball_Slug, "ball_slug", "Ball Type"}, {SortingRuleType::Gender, "gender", "Gender (Male, Female, Genderless)"}, + //{SortingRuleType::Type, "type", "Types"}, Mainly used for testing. The order of the PokemonType Enum is not the most intuitive for sorting. }); return database; } diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h index a0c736adc1..a8a5320169 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_BoxSortingTable.h @@ -23,6 +23,7 @@ enum class SortingRuleType Alpha, Ball_Slug, Gender, + Type }; struct SortingRule diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp index a7fd926038..e8b9e0e24c 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp @@ -27,7 +27,9 @@ bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs lhs.alpha == rhs.alpha && lhs.ball_slug == rhs.ball_slug && lhs.gender == rhs.gender && - lhs.ot_id == rhs.ot_id; + lhs.ot_id == rhs.ot_id && + lhs.type1 == rhs.type1 && + lhs.type2 == rhs.type2; } @@ -73,6 +75,14 @@ bool operator<(const std::optional& lhs, const std::option return (lhs->gender < rhs->gender) != preference.reverse; } break; + case SortingRuleType::Type: + if (lhs->type1 != rhs->type1){ + return (lhs->type1 < rhs->type1) != preference.reverse; + } + if (lhs->type2 != rhs->type2){ + return (lhs->type2 < rhs->type2) != preference.reverse; + } + break; default: throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "unknown SortingRuleType"); } // end switch @@ -94,6 +104,8 @@ std::ostream& operator<<(std::ostream& os, const std::optionalball_slug << " "; os << "gender:" << gender_to_string(pokemon->gender) << " "; os << "ot_id:" << pokemon->ot_id << " "; + os << "type1:" << POKEMON_TYPE_SLUGS().get_string(pokemon->type1) << " "; + os << "type2:" << POKEMON_TYPE_SLUGS().get_string(pokemon->type2) << " "; os << ")"; }else{ os << "(empty)"; @@ -144,6 +156,8 @@ void save_boxes_data_to_json(const std::vectorball_slug; pokemon["gender"] = gender_to_string(current_pokemon->gender); pokemon["ot_id"] = current_pokemon->ot_id; + pokemon["type1"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->type1); + pokemon["type2"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->type2); } pokemon_data.push_back(std::move(pokemon)); } diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h index e430a7f5c8..2e2709d51c 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h @@ -14,6 +14,7 @@ #include #include "Pokemon/Options/Pokemon_StatsHuntFilter.h" #include "Pokemon/Options/Pokemon_BoxSortingTable.h" +#include "Pokemon_Types.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -31,6 +32,8 @@ struct CollectedPokemonInfo{ std::string ball_slug = ""; StatsHuntGenderFilter gender = StatsHuntGenderFilter::Genderless; uint32_t ot_id = 0; // original trainer ID + PokemonType type1 = PokemonType::NONE; + PokemonType type2 = PokemonType::NONE; }; bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs); diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp new file mode 100644 index 0000000000..1d57b23a4e --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp @@ -0,0 +1,269 @@ +/* Type Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Kernels/Waterfill/Kernels_Waterfill.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonTools/Images/BinaryImage_FilterRgb32.h" +#include "CommonTools/ImageMatch/ExactImageMatcher.h" +#include "PokemonHome_TypeReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ + +using namespace Kernels; +using namespace Kernels::Waterfill; +using namespace Pokemon; + + + +class TypeSprite{ +public: + TypeSprite(const std::string& slug) + : m_slug(slug) + { + ImageRGB32 sprite(RESOURCE_PATH() + "PokemonHome/Types/" + slug + ".png"); + + // Clear non-255 alpha pixels. + size_t words = sprite.bytes_per_row() / sizeof(uint32_t); + uint32_t* ptr = sprite.data(); + for (size_t r = 0; r < sprite.height(); r++){ + for (size_t c = 0; c < sprite.width(); c++){ + uint32_t pixel = ptr[c]; + if ((pixel >> 24) != 0xff){ + ptr[c] = 0; + } + } + ptr += words; + } + + PackedBinaryMatrix matrix = compress_rgb32_to_binary_min(sprite, 224, 224, 224); + std::vector objects = find_objects_inplace(matrix, 10); + + WaterfillObject object; + for (const WaterfillObject& item : objects){ + object.merge_assume_no_overlap(item); + } + + m_aspect_ratio = object.aspect_ratio(); + m_matcher = std::make_unique( + sprite.sub_image(object.min_x, object.min_y, object.width(), object.height()).copy(), + ImageMatch::WeightedExactImageMatcher::InverseStddevWeight{1, 64} + ); + } + + const std::string& slug() const{ return m_slug; } + double aspect_ratio() const{ return m_aspect_ratio; } + const ImageMatch::WeightedExactImageMatcher& matcher() const{ return *m_matcher; } + +private: + std::string m_slug; + double m_aspect_ratio; + std::unique_ptr m_matcher; +}; + + +struct TypeSpriteDatabase{ + std::map m_type_map; + + static TypeSpriteDatabase& instance(){ + static TypeSpriteDatabase data; + return data; + } + + TypeSpriteDatabase(){ + for (const auto& item : POKEMON_TYPE_SLUGS()){ + if (item.first == PokemonType::NONE){ + continue; + } + m_type_map.emplace(item.first, TypeSprite(item.second)); + } + } +}; + + + +static size_t box_distance_sqr(const ImagePixelBox& a, const ImagePixelBox& b){ + size_t dist_x = a.distance_x(b); + size_t dist_y = a.distance_y(b); + return dist_x*dist_x + dist_y*dist_y; +} + + +static std::pair match_type_blob(const ImageViewRGB32& image){ + size_t width = image.width(); + size_t height = image.height(); + if (width * height < 100){ + return {1.0, PokemonType::NONE}; + } + if (width > 2 * height || height > 2 * width){ + return {1.0, PokemonType::NONE}; + } + + ImageStats stats = image_stats(image); + if (stats.stddev.sum() < 50){ + return {1.0, PokemonType::NONE}; + } + + double aspect_ratio = (double)width / height; + double best_score = 0.45; + PokemonType best_type = PokemonType::NONE; + + for (const auto& item : TypeSpriteDatabase::instance().m_type_map){ + double expected_ratio = item.second.aspect_ratio(); + double ratio = aspect_ratio / expected_ratio; + if (std::abs(ratio - 1) > 0.2){ + continue; + } + double score = item.second.matcher().diff(image); + if (score < best_score){ + best_score = score; + best_type = item.first; + } + } + + return {best_score, best_type}; +} + + +static void find_type_candidates( + std::multimap>& candidates, + const ImageViewRGB32& image, + PackedBinaryMatrix& matrix, + double max_area_ratio +){ + size_t max_area = (size_t)(image.width() * image.height() * max_area_ratio); + std::vector objects = find_objects_inplace(matrix, 20); + + std::map objmap; + for (size_t c = 0; c < objects.size(); c++){ + if (objects[c].area > max_area){ + continue; + } + objmap[c] = objects[c]; + } + + // Merge nearby objects. + bool changed; + do{ + changed = false; + for (auto iter0 = objmap.begin(); iter0 != objmap.end(); ++iter0){ + for (auto iter1 = objmap.begin(); iter1 != objmap.end();){ + if (iter0->first >= iter1->first){ + ++iter1; + continue; + } + if (box_distance_sqr(ImagePixelBox(iter0->second), ImagePixelBox(iter1->second)) < 5*5){ + iter0->second.merge_assume_no_overlap(iter1->second); + iter1 = objmap.erase(iter1); + changed = true; + }else{ + ++iter1; + } + } + } + }while (changed); + + for (const auto& item : objmap){ + ImageViewRGB32 img = extract_box_reference(image, item.second); + std::pair result = match_type_blob(img); + if (result.second != PokemonType::NONE){ + candidates.emplace( + result.first, + std::pair(result.second, ImagePixelBox(item.second)) + ); + } + } +} + + + +TypeReader::TypeReader(VideoStream& stream, const ImageFloatBox& box) + : m_box(stream.overlay(), box) +{} + +std::pair TypeReader::read_types(const ImageViewRGB32& screen) const{ + ImageViewRGB32 image = extract_box_reference(screen, m_box); + + std::multimap> candidates; + { + std::vector matrices = compress_rgb32_to_binary_range( + image, + { + {0xff808060, 0xffffffff}, + {0xffa0a060, 0xffffffff}, + {0xff606060, 0xffffffff}, + {0xff707070, 0xffffffff}, + {0xff808080, 0xffffffff}, + {0xff909090, 0xffffffff}, + {0xffa0a0a0, 0xffffffff}, + {0xffb0b0b0, 0xffffffff}, + {0xffc0c0c0, 0xffffffff}, + {0xffd0d0d0, 0xffffffff}, + {0xffe0e0e0, 0xffffffff}, + } + ); + for (PackedBinaryMatrix& matrix : matrices){ + find_type_candidates(candidates, image, matrix, 0.20); + } + } + + std::multimap> filtered; + for (const auto& candidate : candidates){ + bool is_dupe = false; + for (const auto& item : filtered){ + if (box_distance_sqr(candidate.second.second, item.second.second) == 0){ + is_dupe = true; + break; + } + } + if (!is_dupe){ + filtered.emplace(candidate); + } + } + + // Re-sort by screen position (left-to-right, top-to-bottom) so that type1/type2 + // assignment is stable regardless of which type was detected with higher confidence. + std::vector> sorted; + sorted.reserve(filtered.size()); + for (const auto& item : filtered){ + sorted.emplace_back(item.second); + } + std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b){ + size_t height = a.second.max_y - a.second.min_y; + size_t diff_y = a.second.min_y > b.second.min_y + ? a.second.min_y - b.second.min_y + : b.second.min_y - a.second.min_y; + if (diff_y > height){ + return a.second.min_y < b.second.min_y; + } + return a.second.min_x < b.second.min_x; + }); + + std::pair result{PokemonType::NONE, PokemonType::NONE}; + for (const auto& item : sorted){ + PokemonType type = item.first; + if (result.first == PokemonType::NONE){ + result.first = type; + }else if (type != result.first){ + result.second = type; + break; + } + } + return result; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h new file mode 100644 index 0000000000..7c60a2b226 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h @@ -0,0 +1,39 @@ +/* Type Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonHome_TypeReader_H +#define PokemonAutomation_PokemonHome_TypeReader_H + +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "Pokemon/Pokemon_Types.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ + +using namespace Pokemon; + + +class TypeReader{ +public: + TypeReader(const ImageFloatBox& box); + + // Returns up to 2 detected types, best match first. + // The second type is PokemonType::NONE for single-type Pokemon. + std::pair read_types(const ImageViewRGB32& screen) const; + +private: + OverlayBoxScope m_box; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index 6385f43d94..79ad65911c 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -49,6 +49,7 @@ language #include "PokemonHome/Inference/PokemonHome_ButtonDetector.h" #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" +#include "PokemonHome/Inference/PokemonHome_TypeReader.h" #include "PokemonHome_BoxSorter.h" namespace PokemonAutomation{ @@ -401,6 +402,8 @@ void read_summary_screen( ImageFloatBox nature_box(0.157, 0.783, 0.212, 0.042); // Nature ImageFloatBox ability_box(0.158, 0.838, 0.213, 0.042); // Ability ImageFloatBox alpha_box(0.787, 0.095, 0.024, 0.046); // Alpha symbol + ImageFloatBox type_box(0.615, 0.240, 0.071, 0.057); // Type symbols + video_overlay_set.add(COLOR_WHITE, national_dex_number_box); video_overlay_set.add(COLOR_BLUE, shiny_symbol_box); @@ -461,7 +464,13 @@ void read_summary_screen( dump_image(env.console, ProgramInfo(), "ReadSummary_OT", screen); } cur_pokemon_info.ot_id = ot_id; - + + TypeReader type_reader(type_box); + auto [type1, type2] = type_reader.read_types(screen); + + cur_pokemon_info.type1 = type1; + cur_pokemon_info.type2 = type2; + env.add_overlay_log(create_overlay_info(cur_pokemon_info)); video_overlay_set.clear(); diff --git a/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp b/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp index 3d564f80e2..2ec5929b77 100644 --- a/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp @@ -28,6 +28,7 @@ #include "Pokemon/Pokemon_CollectedPokemonInfo.h" #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" +#include "PokemonHome/Inference/PokemonHome_TypeReader.h" #include "PokemonLZA/Inference/Boxes/PokemonLZA_BoxDetection.h" #include "PokemonLZA/Inference/Boxes/PokemonLZA_BoxInfoDetector.h" #include "PokemonLZA/Resources/PokemonLZA_AvailablePokemon.h" @@ -255,6 +256,7 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex BoxAlphaDetector alpha_detector(COLOR_RED, &env.console.overlay()); SomethingInBoxCellDetector non_empty_detector(COLOR_BLUE, &env.console.overlay()); BoxDexNumberDetector dex_number_detector(env.console); + PokemonHome::TypeReader type_reader(ImageFloatBox(0.467, 0.256, 0.110, 0.041)); box_detector.make_overlays(video_overlay_set); shiny_detector.make_overlays(video_overlay_set); @@ -319,6 +321,7 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex name_slug = LUMIOSE_DEX_SLUGS()[dex_number-1]; } + auto [type1, type2] = type_reader.read_types(screen); boxes_data.push_back( CollectedPokemonInfo{ .preferences = &sort_preferences, @@ -326,6 +329,8 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex .name_slug = name_slug, .shiny = shiny_detector.detect(screen), .alpha = alpha_detector.detect(screen), + .type1 = type1, + .type2 = type2, } ); ss << "\u2705 " ; // checkbox diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index f0a89d4520..afb4c7cf28 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1487,6 +1487,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonHome/Inference/PokemonHome_BoxGenderDetector.h Source/PokemonHome/Inference/PokemonHome_ButtonDetector.cpp Source/PokemonHome/Inference/PokemonHome_ButtonDetector.h + Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp + Source/PokemonHome/Inference/PokemonHome_TypeReader.h Source/PokemonHome/PokemonHome_Panels.cpp Source/PokemonHome/PokemonHome_Panels.h Source/PokemonHome/PokemonHome_Settings.cpp From fd13d6ccfdc2b329fa49698a279a8ce776e6a1b4 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 5 Apr 2026 19:18:30 -0500 Subject: [PATCH 02/17] cleanup --- .../Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp | 4 ++-- .../Source/PokemonHome/Inference/PokemonHome_TypeReader.h | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp index 1d57b23a4e..d2c572b7cd 100644 --- a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp @@ -187,8 +187,8 @@ static void find_type_candidates( -TypeReader::TypeReader(VideoStream& stream, const ImageFloatBox& box) - : m_box(stream.overlay(), box) +TypeReader::TypeReader(const ImageFloatBox& box) + : m_box(box) {} std::pair TypeReader::read_types(const ImageViewRGB32& screen) const{ diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h index 7c60a2b226..c6497a7d2b 100644 --- a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h +++ b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h @@ -9,7 +9,6 @@ #include #include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "Pokemon/Pokemon_Types.h" namespace PokemonAutomation{ @@ -28,7 +27,7 @@ class TypeReader{ std::pair read_types(const ImageViewRGB32& screen) const; private: - OverlayBoxScope m_box; + ImageFloatBox m_box; }; From 6764b10c9c1808e7dfcb0835d600a51a3b6a63e7 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Fri, 10 Apr 2026 22:40:32 -0500 Subject: [PATCH 03/17] dedupe type detecton. moved to shared pokemon type reader --- .../Pokemon/Inference/Pokemon_TypeReader.cpp | 426 ++++++++++++++++++ .../Pokemon/Inference/Pokemon_TypeReader.h | 43 ++ .../Pokemon/Pokemon_CollectedPokemonInfo.cpp | 21 +- .../Pokemon/Pokemon_CollectedPokemonInfo.h | 4 +- .../Inference/PokemonHome_TypeReader.cpp | 269 ----------- .../Inference/PokemonHome_TypeReader.h | 38 -- .../Programs/PokemonLZA_BoxSorter.cpp | 9 +- .../PokemonSwSh_MaxLair_Detect_BattleMenu.cpp | 9 +- .../PokemonSwSh_MaxLair_Detect_PathMap.cpp | 9 +- SerialPrograms/cmake/SourceFiles.cmake | 2 + 10 files changed, 501 insertions(+), 329 deletions(-) create mode 100644 SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp create mode 100644 SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h delete mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp delete mode 100644 SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h diff --git a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp new file mode 100644 index 0000000000..f2e0b75822 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.cpp @@ -0,0 +1,426 @@ +/* Type Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/CancellableScope.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/ImageMatch/ExactImageMatcher.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/Images/BinaryImage_FilterRgb32.h" +#include "Kernels/Waterfill/Kernels_Waterfill.h" +#include "Pokemon_TypeReader.h" + +#include + +namespace PokemonAutomation { +namespace Pokemon { + +using namespace Kernels; +using namespace Kernels::Waterfill; + + +class TypeSprite { +public: + TypeSprite(const std::string& slug, PokemonTypeGeneration generation) + : m_slug(slug) + { + ImageRGB32 sprite; + switch (generation) + { + case PokemonAutomation::Pokemon::PokemonTypeGeneration::GEN8: + sprite = ImageRGB32(RESOURCE_PATH() + "Pokemon/Types/Gen8/" + slug + ".png"); + break; + case PokemonAutomation::Pokemon::PokemonTypeGeneration::GEN9: + sprite = ImageRGB32(RESOURCE_PATH() + "Pokemon/Types/Gen9/" + slug + ".png"); + break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid enum."); + } + + sprite = filter_rgb32_range( + sprite, + 0xff000000, 0xffffffff, + Color(0), false + ); + + PackedBinaryMatrix matrix = compress_rgb32_to_binary_min(sprite, 224, 224, 224); + std::vector objects = find_objects_inplace(matrix, 10); + + WaterfillObject object; + for (const WaterfillObject& item : objects) { + object.merge_assume_no_overlap(item); + } + + m_aspect_ratio = object.aspect_ratio(); + m_matcher = std::make_unique( + sprite.sub_image(object.min_x, object.min_y, object.width(), object.height()).copy(), + ImageMatch::WeightedExactImageMatcher::InverseStddevWeight{ 1, 64 } + ); + } + + const std::string& slug() const { return m_slug; } + double aspect_ratio() const { return m_aspect_ratio; } + const ImageMatch::WeightedExactImageMatcher& matcher() const { return *m_matcher; } + +private: + std::string m_slug; + double m_aspect_ratio; + std::unique_ptr m_matcher; +}; + +struct Gen8TypeSpriteDatabase { + std::map m_type_map; + + static Gen8TypeSpriteDatabase& instance() { + static Gen8TypeSpriteDatabase data; + return data; + } + + Gen8TypeSpriteDatabase() { + for (const auto& item : POKEMON_TYPE_SLUGS()) { + if (item.first == PokemonType::NONE) { + continue; + } + m_type_map.emplace(item.first, TypeSprite(item.second, PokemonTypeGeneration::GEN8)); + } + } +}; + +struct Gen9TypeSpriteDatabase { + std::map m_type_map; + + static Gen9TypeSpriteDatabase& instance() { + static Gen9TypeSpriteDatabase data; + return data; + } + + Gen9TypeSpriteDatabase() { + for (const auto& item : POKEMON_TYPE_SLUGS()) { + if (item.first == PokemonType::NONE) { + continue; + } + m_type_map.emplace(item.first, TypeSprite(item.second, PokemonTypeGeneration::GEN9)); + } + } +}; + +size_t distance_sqr(const ImagePixelBox& a, const ImagePixelBox& b) { + bool overlap_x = a.min_x <= b.max_x && b.min_x <= a.max_x; + bool overlap_y = a.min_y <= b.max_y && b.min_y <= a.max_y; + if (overlap_x && overlap_y) { + return 0; + } + + size_t dist_x = 0; + if (!overlap_x) { + dist_x = a.max_x < b.min_x + ? b.min_x - a.max_x + : a.min_x - b.max_x; + } + + size_t dist_y = 0; + if (!overlap_y) { + dist_y = a.max_y < b.min_y + ? b.min_y - a.max_y + : a.min_y - b.max_y; + } + + return dist_x * dist_x + dist_y * dist_y; +} + +std::pair match_type_symbol(const ImageViewRGB32& image, PokemonTypeGeneration generation) { + size_t width = image.width(); + size_t height = image.height(); + if (width * height < 100) { + return { 1.0, PokemonType::NONE }; + } + if (width > 2 * height) { + return { 1.0, PokemonType::NONE }; + } + if (height > 2 * width) { + return { 1.0, PokemonType::NONE }; + } + ImageStats stats = image_stats(image); + if (stats.stddev.sum() < 50) { + // if (print){ + // cout << "stats.stddev.sum() = " << stats.stddev.sum() << endl; + // } + return { 1.0, PokemonType::NONE }; + } + + double aspect_ratio = (double)width / height; + + // static int c = 0; + // image.save("test-" + std::to_string(threshold) + "-" + std::to_string(c++) + ".png"); + + // std::map rank; + + const std::map* type_sprite_map; + + switch (generation) { + case PokemonAutomation::Pokemon::PokemonTypeGeneration::GEN8: + type_sprite_map = &Gen8TypeSpriteDatabase::instance().m_type_map; + break; + case PokemonAutomation::Pokemon::PokemonTypeGeneration::GEN9: + type_sprite_map = &Gen9TypeSpriteDatabase::instance().m_type_map; + break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid enum."); + } + + double best_score = 0.45; + PokemonType best_type = PokemonType::NONE; + for (const auto& item : *type_sprite_map) { + // if (threshold != 700 || id != 55){ + // continue; + // } + + double expected_aspect_ratio = item.second.aspect_ratio(); + double ratio = aspect_ratio / expected_aspect_ratio; +#if 0 + if (print) { + cout << item.second.slug() + << " : expected = " << expected_aspect_ratio + << ", actual = " << aspect_ratio + << ", ratio = " << ratio << endl; + } +#endif + if (std::abs(ratio - 1) > 0.2) { + continue; + } + + double rmsd_alpha = item.second.matcher().diff(image); + + // item.second.matcher().m_image.save("sprite.png"); + // if (print){ + // cout << item.second.slug() << ": " << rmsd_alpha << endl; + // } + +#if 0 + // Handicap fairy due to white and pink being too similar in color and + // false positiving on the background. + if (item.first == PokemonType::FAIRY) { + rmsd_ratio *= 1.5; + } + + // Bonus for dark because or large contrast. + if (item.first == PokemonType::DARK) { + rmsd_ratio *= 0.8; + } +#endif + + if (best_score > rmsd_alpha) { + best_score = rmsd_alpha; + best_type = item.first; + // cout << item.second.slug() << ": " << stats.stddev << endl; + } + } + + // if (best_type != PokemonType::NONE){ + // cout << get_type_slug(best_type) << ": " << best_score << endl; + // } + return { best_score, best_type }; +} + +void find_type_symbol_candidates( + std::multimap>& candidates, + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + PackedBinaryMatrix& matrix, + double max_area_ratio, + PokemonTypeGeneration generation +){ + size_t max_area = (size_t)(image.width() * image.height() * max_area_ratio); + std::vector objects = find_objects_inplace( + matrix, + (size_t)(20. * original_screen.total_pixels() / (1920*1080)) + ); + +// static int index = 0; + + std::map objmap; + for (size_t c = 0; c < objects.size(); c++){ + if (objects[c].area > max_area){ + continue; + } + objmap[c] = objects[c]; + +#if 0 + extract_box_reference(image, ImagePixelBox(objects[c])).save("test-" + std::to_string(index++) + ".png"); +#endif + } + +// cout << "begin = " << objmap.size() << endl; + + // Merge nearby objects. + bool changed; + do{ + changed = false; + for (auto iter0 = objmap.begin(); iter0 != objmap.end(); ++iter0){ + for (auto iter1 = objmap.begin(); iter1 != objmap.end();){ + if (iter0->first >= iter1->first){ + ++iter1; + continue; + } + const WaterfillObject& obj0 = iter0->second; + const WaterfillObject& obj1 = iter1->second; + size_t distance = distance_sqr( + ImagePixelBox(obj0.min_x, obj0.min_y, obj0.max_x, obj0.max_y), + ImagePixelBox(obj1.min_x, obj1.min_y, obj1.max_x, obj1.max_y) + ); + if (distance < 5*5){ + iter0->second.merge_assume_no_overlap(iter1->second); + iter1 = objmap.erase(iter1); + changed = true; + }else{ + ++iter1; + } + } + } + }while (changed); + +// cout << "merged = " << objmap.size() << endl; + + // Identify objects. + for (const auto& item : objmap){ + ImageViewRGB32 img = extract_box_reference(image, item.second); + +// print = index == 137; +// img.save("test-" + std::to_string(index++) + ".png"); + + std::pair result = match_type_symbol(img, generation); +// cout << "result = " << POKEMON_TYPE_SLUGS().get_string(result.second) << ": " << result.first << endl; + if (result.second != PokemonType::NONE){ + const WaterfillObject& obj = item.second; + candidates.emplace( + result.first, + std::pair( + result.second, + ImagePixelBox(obj.min_x, obj.min_y, obj.max_x, obj.max_y) + ) + ); + } + } + +// cout << "candidates = " << candidates.size() << endl; +} + + +std::multimap> find_type_symbols( + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + double max_area_ratio, + PokemonTypeGeneration generation +) { + std::multimap> candidates; + + { + std::vector matrices = compress_rgb32_to_binary_range( + image, + { + {0xff808060, 0xffffffff}, + {0xffa0a060, 0xffffffff}, + {0xff606060, 0xffffffff}, + {0xff707070, 0xffffffff}, + {0xff808080, 0xffffffff}, + {0xff909090, 0xffffffff}, + {0xffa0a0a0, 0xffffffff}, + {0xffb0b0b0, 0xffffffff}, + {0xffc0c0c0, 0xffffffff}, + {0xffd0d0d0, 0xffffffff}, + {0xffe0e0e0, 0xffffffff}, + } + ); + for (PackedBinaryMatrix& matrix : matrices) { + find_type_symbol_candidates(candidates, original_screen, image, matrix, max_area_ratio, generation); + } + } + + // cout << "-------------" << endl; + + std::multimap> filtered; + for (const auto& candidate : candidates) { + // cout << POKEMON_TYPE_SLUGS().get_string(candidate.second.first) << ": " << candidate.first << endl; + // hits.emplace_back(overlay, translate_to_parent(screen, box, candidate.second.second.box), COLOR_GREEN); + + bool is_dupe = false; + for (const auto& item : filtered) { + if (distance_sqr(candidate.second.second, item.second.second) == 0) { + is_dupe = true; + break; + } + } + if (!is_dupe) { + filtered.emplace(candidate); + } + } + +#if 0 + static int c = 0; + for (const auto& item : filtered) { + // cout << get_type_slug(item.second.first) << ": " << item.first << " - [" << item.second.second.center_x() << "," << item.second.second.center_y() << "]" << endl; + const ImagePixelBox& box = item.second.second; + ImageViewRGB32 img = image.sub_image( + box.min_x, box.min_y, + box.width(), box.height() + ); + img.save("test-" + std::to_string(c++) + ".png"); + } +#endif + + return filtered; +} + +std::pair read_pokemon_types( + const ImageViewRGB32& original_screen, + const ImageFloatBox& box, + PokemonTypeGeneration generation +) { + + ImageViewRGB32 image = extract_box_reference(original_screen, box); + + std::multimap> filtered = find_type_symbols( + original_screen, + image, + 0.20, + generation + ); + + // Re-sort by screen position (left-to-right, top-to-bottom) so that primary/secondary + // assignment is stable regardless of which type was detected with higher confidence. + std::vector> sorted; + sorted.reserve(filtered.size()); + for (const auto& item : filtered) { + sorted.emplace_back(item.second); + } + std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { + size_t height = a.second.max_y - a.second.min_y; + size_t diff_y = a.second.min_y > b.second.min_y + ? a.second.min_y - b.second.min_y + : b.second.min_y - a.second.min_y; + if (diff_y > height) { + return a.second.min_y < b.second.min_y; + } + return a.second.min_x < b.second.min_x; + }); + + std::pair result{ PokemonType::NONE, PokemonType::NONE }; + for (const auto& item : sorted) { + PokemonType type = item.first; + if (result.first == PokemonType::NONE) { + result.first = type; + } + else if (type != result.first) { + result.second = type; + break; + } + } + return result; +} + +} +} \ No newline at end of file diff --git a/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h new file mode 100644 index 0000000000..bdb1c0d597 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Inference/Pokemon_TypeReader.h @@ -0,0 +1,43 @@ +/* Type Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Pokemon_TypeReader_H +#define PokemonAutomation_Pokemon_TypeReader_H + +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "Pokemon/Pokemon_Types.h" + +namespace PokemonAutomation { +namespace Pokemon { + +// The style of type symbols. +// GEN8: SWSH, Needs testing: LGPE +// GEN9: SV, LZA, HOME(Currently), Needs testing: Champions, LA, BDSP +enum class PokemonTypeGeneration{ + GEN8, + GEN9, +}; + +// Find all type symbols inside the image. +std::multimap> find_type_symbols( + const ImageViewPlanar32& original_screen, + const ImageViewRGB32& image, + double max_area_ratio, + PokemonTypeGeneration generation +); + +// Reads the types of a Pokemon. Second type will be PokemonType::NONE if the Pokemon is single-type +std::pair read_pokemon_types( + const ImageViewRGB32& original_screen, + const ImageFloatBox& box, + PokemonTypeGeneration generation +); + +} +} +#endif \ No newline at end of file diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp index e8b9e0e24c..b2dc725640 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp @@ -11,7 +11,6 @@ #include "Pokemon/Pokemon_Strings.h" #include "Pokemon/Pokemon_BoxCursor.h" #include "Pokemon/Resources/Pokemon_PokemonNames.h" -#include "Pokemon/Resources/Pokemon_PokemonSlugs.h" #include "Pokemon_CollectedPokemonInfo.h" namespace PokemonAutomation{ @@ -28,8 +27,8 @@ bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs lhs.ball_slug == rhs.ball_slug && lhs.gender == rhs.gender && lhs.ot_id == rhs.ot_id && - lhs.type1 == rhs.type1 && - lhs.type2 == rhs.type2; + lhs.primaryType == rhs.primaryType && + lhs.secondaryType == rhs.secondaryType; } @@ -76,11 +75,11 @@ bool operator<(const std::optional& lhs, const std::option } break; case SortingRuleType::Type: - if (lhs->type1 != rhs->type1){ - return (lhs->type1 < rhs->type1) != preference.reverse; + if (lhs->primaryType != rhs->primaryType){ + return (lhs->primaryType < rhs->primaryType) != preference.reverse; } - if (lhs->type2 != rhs->type2){ - return (lhs->type2 < rhs->type2) != preference.reverse; + if (lhs->secondaryType != rhs->secondaryType){ + return (lhs->secondaryType < rhs->secondaryType) != preference.reverse; } break; default: @@ -104,8 +103,8 @@ std::ostream& operator<<(std::ostream& os, const std::optionalball_slug << " "; os << "gender:" << gender_to_string(pokemon->gender) << " "; os << "ot_id:" << pokemon->ot_id << " "; - os << "type1:" << POKEMON_TYPE_SLUGS().get_string(pokemon->type1) << " "; - os << "type2:" << POKEMON_TYPE_SLUGS().get_string(pokemon->type2) << " "; + os << "primaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->primaryType) << " "; + os << "secondaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->secondaryType) << " "; os << ")"; }else{ os << "(empty)"; @@ -156,8 +155,8 @@ void save_boxes_data_to_json(const std::vectorball_slug; pokemon["gender"] = gender_to_string(current_pokemon->gender); pokemon["ot_id"] = current_pokemon->ot_id; - pokemon["type1"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->type1); - pokemon["type2"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->type2); + pokemon["primaryType"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->primaryType); + pokemon["secondaryType"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->secondaryType); } pokemon_data.push_back(std::move(pokemon)); } diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h index 2e2709d51c..7bd6117f0c 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h @@ -32,8 +32,8 @@ struct CollectedPokemonInfo{ std::string ball_slug = ""; StatsHuntGenderFilter gender = StatsHuntGenderFilter::Genderless; uint32_t ot_id = 0; // original trainer ID - PokemonType type1 = PokemonType::NONE; - PokemonType type2 = PokemonType::NONE; + PokemonType primaryType = PokemonType::NONE; + PokemonType secondaryType = PokemonType::NONE; }; bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs); diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp deleted file mode 100644 index d2c572b7cd..0000000000 --- a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp +++ /dev/null @@ -1,269 +0,0 @@ -/* Type Reader - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include -#include "Kernels/Waterfill/Kernels_Waterfill.h" -#include "CommonFramework/Globals.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/ImageTools/ImageStats.h" -#include "CommonTools/Images/BinaryImage_FilterRgb32.h" -#include "CommonTools/ImageMatch/ExactImageMatcher.h" -#include "PokemonHome_TypeReader.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonHome{ - -using namespace Kernels; -using namespace Kernels::Waterfill; -using namespace Pokemon; - - - -class TypeSprite{ -public: - TypeSprite(const std::string& slug) - : m_slug(slug) - { - ImageRGB32 sprite(RESOURCE_PATH() + "PokemonHome/Types/" + slug + ".png"); - - // Clear non-255 alpha pixels. - size_t words = sprite.bytes_per_row() / sizeof(uint32_t); - uint32_t* ptr = sprite.data(); - for (size_t r = 0; r < sprite.height(); r++){ - for (size_t c = 0; c < sprite.width(); c++){ - uint32_t pixel = ptr[c]; - if ((pixel >> 24) != 0xff){ - ptr[c] = 0; - } - } - ptr += words; - } - - PackedBinaryMatrix matrix = compress_rgb32_to_binary_min(sprite, 224, 224, 224); - std::vector objects = find_objects_inplace(matrix, 10); - - WaterfillObject object; - for (const WaterfillObject& item : objects){ - object.merge_assume_no_overlap(item); - } - - m_aspect_ratio = object.aspect_ratio(); - m_matcher = std::make_unique( - sprite.sub_image(object.min_x, object.min_y, object.width(), object.height()).copy(), - ImageMatch::WeightedExactImageMatcher::InverseStddevWeight{1, 64} - ); - } - - const std::string& slug() const{ return m_slug; } - double aspect_ratio() const{ return m_aspect_ratio; } - const ImageMatch::WeightedExactImageMatcher& matcher() const{ return *m_matcher; } - -private: - std::string m_slug; - double m_aspect_ratio; - std::unique_ptr m_matcher; -}; - - -struct TypeSpriteDatabase{ - std::map m_type_map; - - static TypeSpriteDatabase& instance(){ - static TypeSpriteDatabase data; - return data; - } - - TypeSpriteDatabase(){ - for (const auto& item : POKEMON_TYPE_SLUGS()){ - if (item.first == PokemonType::NONE){ - continue; - } - m_type_map.emplace(item.first, TypeSprite(item.second)); - } - } -}; - - - -static size_t box_distance_sqr(const ImagePixelBox& a, const ImagePixelBox& b){ - size_t dist_x = a.distance_x(b); - size_t dist_y = a.distance_y(b); - return dist_x*dist_x + dist_y*dist_y; -} - - -static std::pair match_type_blob(const ImageViewRGB32& image){ - size_t width = image.width(); - size_t height = image.height(); - if (width * height < 100){ - return {1.0, PokemonType::NONE}; - } - if (width > 2 * height || height > 2 * width){ - return {1.0, PokemonType::NONE}; - } - - ImageStats stats = image_stats(image); - if (stats.stddev.sum() < 50){ - return {1.0, PokemonType::NONE}; - } - - double aspect_ratio = (double)width / height; - double best_score = 0.45; - PokemonType best_type = PokemonType::NONE; - - for (const auto& item : TypeSpriteDatabase::instance().m_type_map){ - double expected_ratio = item.second.aspect_ratio(); - double ratio = aspect_ratio / expected_ratio; - if (std::abs(ratio - 1) > 0.2){ - continue; - } - double score = item.second.matcher().diff(image); - if (score < best_score){ - best_score = score; - best_type = item.first; - } - } - - return {best_score, best_type}; -} - - -static void find_type_candidates( - std::multimap>& candidates, - const ImageViewRGB32& image, - PackedBinaryMatrix& matrix, - double max_area_ratio -){ - size_t max_area = (size_t)(image.width() * image.height() * max_area_ratio); - std::vector objects = find_objects_inplace(matrix, 20); - - std::map objmap; - for (size_t c = 0; c < objects.size(); c++){ - if (objects[c].area > max_area){ - continue; - } - objmap[c] = objects[c]; - } - - // Merge nearby objects. - bool changed; - do{ - changed = false; - for (auto iter0 = objmap.begin(); iter0 != objmap.end(); ++iter0){ - for (auto iter1 = objmap.begin(); iter1 != objmap.end();){ - if (iter0->first >= iter1->first){ - ++iter1; - continue; - } - if (box_distance_sqr(ImagePixelBox(iter0->second), ImagePixelBox(iter1->second)) < 5*5){ - iter0->second.merge_assume_no_overlap(iter1->second); - iter1 = objmap.erase(iter1); - changed = true; - }else{ - ++iter1; - } - } - } - }while (changed); - - for (const auto& item : objmap){ - ImageViewRGB32 img = extract_box_reference(image, item.second); - std::pair result = match_type_blob(img); - if (result.second != PokemonType::NONE){ - candidates.emplace( - result.first, - std::pair(result.second, ImagePixelBox(item.second)) - ); - } - } -} - - - -TypeReader::TypeReader(const ImageFloatBox& box) - : m_box(box) -{} - -std::pair TypeReader::read_types(const ImageViewRGB32& screen) const{ - ImageViewRGB32 image = extract_box_reference(screen, m_box); - - std::multimap> candidates; - { - std::vector matrices = compress_rgb32_to_binary_range( - image, - { - {0xff808060, 0xffffffff}, - {0xffa0a060, 0xffffffff}, - {0xff606060, 0xffffffff}, - {0xff707070, 0xffffffff}, - {0xff808080, 0xffffffff}, - {0xff909090, 0xffffffff}, - {0xffa0a0a0, 0xffffffff}, - {0xffb0b0b0, 0xffffffff}, - {0xffc0c0c0, 0xffffffff}, - {0xffd0d0d0, 0xffffffff}, - {0xffe0e0e0, 0xffffffff}, - } - ); - for (PackedBinaryMatrix& matrix : matrices){ - find_type_candidates(candidates, image, matrix, 0.20); - } - } - - std::multimap> filtered; - for (const auto& candidate : candidates){ - bool is_dupe = false; - for (const auto& item : filtered){ - if (box_distance_sqr(candidate.second.second, item.second.second) == 0){ - is_dupe = true; - break; - } - } - if (!is_dupe){ - filtered.emplace(candidate); - } - } - - // Re-sort by screen position (left-to-right, top-to-bottom) so that type1/type2 - // assignment is stable regardless of which type was detected with higher confidence. - std::vector> sorted; - sorted.reserve(filtered.size()); - for (const auto& item : filtered){ - sorted.emplace_back(item.second); - } - std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b){ - size_t height = a.second.max_y - a.second.min_y; - size_t diff_y = a.second.min_y > b.second.min_y - ? a.second.min_y - b.second.min_y - : b.second.min_y - a.second.min_y; - if (diff_y > height){ - return a.second.min_y < b.second.min_y; - } - return a.second.min_x < b.second.min_x; - }); - - std::pair result{PokemonType::NONE, PokemonType::NONE}; - for (const auto& item : sorted){ - PokemonType type = item.first; - if (result.first == PokemonType::NONE){ - result.first = type; - }else if (type != result.first){ - result.second = type; - break; - } - } - return result; -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h b/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h deleted file mode 100644 index c6497a7d2b..0000000000 --- a/SerialPrograms/Source/PokemonHome/Inference/PokemonHome_TypeReader.h +++ /dev/null @@ -1,38 +0,0 @@ -/* Type Reader - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonHome_TypeReader_H -#define PokemonAutomation_PokemonHome_TypeReader_H - -#include -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "Pokemon/Pokemon_Types.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonHome{ - -using namespace Pokemon; - - -class TypeReader{ -public: - TypeReader(const ImageFloatBox& box); - - // Returns up to 2 detected types, best match first. - // The second type is PokemonType::NONE for single-type Pokemon. - std::pair read_types(const ImageViewRGB32& screen) const; - -private: - ImageFloatBox m_box; -}; - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp b/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp index 2ec5929b77..86c29ee9b9 100644 --- a/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonLZA/Programs/PokemonLZA_BoxSorter.cpp @@ -22,13 +22,13 @@ #include "CommonTools/VisualDetectors/FrozenImageDetector.h" #include "CommonTools/StartupChecks/StartProgramChecks.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Inference/Pokemon_TypeReader.h" #include "Pokemon/Pokemon_Strings.h" #include "Pokemon/Resources/Pokemon_PokemonNames.h" #include "Pokemon/Pokemon_BoxCursor.h" #include "Pokemon/Pokemon_CollectedPokemonInfo.h" #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" -#include "PokemonHome/Inference/PokemonHome_TypeReader.h" #include "PokemonLZA/Inference/Boxes/PokemonLZA_BoxDetection.h" #include "PokemonLZA/Inference/Boxes/PokemonLZA_BoxInfoDetector.h" #include "PokemonLZA/Resources/PokemonLZA_AvailablePokemon.h" @@ -256,7 +256,6 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex BoxAlphaDetector alpha_detector(COLOR_RED, &env.console.overlay()); SomethingInBoxCellDetector non_empty_detector(COLOR_BLUE, &env.console.overlay()); BoxDexNumberDetector dex_number_detector(env.console); - PokemonHome::TypeReader type_reader(ImageFloatBox(0.467, 0.256, 0.110, 0.041)); box_detector.make_overlays(video_overlay_set); shiny_detector.make_overlays(video_overlay_set); @@ -321,7 +320,7 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex name_slug = LUMIOSE_DEX_SLUGS()[dex_number-1]; } - auto [type1, type2] = type_reader.read_types(screen); + auto [primaryType, secondaryType] = read_pokemon_types(screen, ImageFloatBox(0.467, 0.256, 0.110, 0.041), PokemonTypeGeneration::GEN9); boxes_data.push_back( CollectedPokemonInfo{ .preferences = &sort_preferences, @@ -329,8 +328,8 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex .name_slug = name_slug, .shiny = shiny_detector.detect(screen), .alpha = alpha_detector.detect(screen), - .type1 = type1, - .type2 = type2, + .primaryType = primaryType, + .secondaryType = secondaryType, } ); ss << "\u2705 " ; // checkbox diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_BattleMenu.cpp b/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_BattleMenu.cpp index e7813ecce3..5219bbe7c9 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_BattleMenu.cpp +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_BattleMenu.cpp @@ -13,8 +13,8 @@ #include "CommonTools/Images/SolidColorTest.h" #include "CommonTools/Images/ColorClustering.h" #include "Pokemon/Inference/Pokemon_ReadHpBar.h" +#include "Pokemon/Inference/Pokemon_TypeReader.h" #include "PokemonSwSh/Resources/PokemonSwSh_MaxLairDatabase.h" -#include "PokemonSwSh/Inference/PokemonSwSh_TypeSymbolFinder.h" #include "PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options.h" #include "PokemonSwSh_MaxLair_Detect_PokemonReader.h" #include "PokemonSwSh_MaxLair_Detect_HPPP.h" @@ -263,7 +263,12 @@ std::set BattleMenuReader::read_opponent_in_summary(Logger& logger, PokemonType type0, type1; { ImageViewRGB32 types = extract_box_reference(screen, m_summary_opponent_types); - std::multimap> candidates = find_type_symbols(screen, types, 0.2); + std::multimap> candidates = find_type_symbols( + screen, + types, + 0.2, + PokemonTypeGeneration::GEN8 + ); std::string type_str = "Type Read Result:\n"; for (const auto& item : candidates){ diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_PathMap.cpp b/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_PathMap.cpp index 6a1e07fa4f..0cab6940e7 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_PathMap.cpp +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_PathMap.cpp @@ -9,7 +9,7 @@ #include "CommonFramework/Tools/ErrorDumper.h" #include "CommonFramework/Tools/ProgramEnvironment.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "PokemonSwSh/Inference/PokemonSwSh_TypeSymbolFinder.h" +#include "Pokemon/Inference/Pokemon_TypeReader.h" //#include "PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options.h" #include "PokemonSwSh_MaxLair_Detect_PathMap.h" @@ -36,7 +36,12 @@ bool read_type_array( type[c] = PokemonType::NONE; } - std::multimap> candidates = find_type_symbols(screen, image, 0.20); + std::multimap> candidates = find_type_symbols( + screen, + image, + 0.20, + PokemonTypeGeneration::GEN8 + ); // cout << candidates.size() << endl; #if 0 diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index afb4c7cf28..612cd6e787 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1239,6 +1239,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Inference/Pokemon_TrainIVCheckerOCR.h Source/Pokemon/Inference/Pokemon_TrainPokemonOCR.cpp Source/Pokemon/Inference/Pokemon_TrainPokemonOCR.h + Source/Pokemon/Inference/Pokemon_TypeReader.cpp + Source/Pokemon/Inference/Pokemon_TypeReader.h Source/Pokemon/Options/Pokemon_BoxSortingTable.cpp Source/Pokemon/Options/Pokemon_BoxSortingTable.h Source/Pokemon/Options/Pokemon_EncounterBotOptions.h From d9cf40874b919e65458d8b9668ce7ce0f4d99dd1 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Fri, 10 Apr 2026 22:43:19 -0500 Subject: [PATCH 04/17] remove from cmake --- SerialPrograms/cmake/SourceFiles.cmake | 2 -- 1 file changed, 2 deletions(-) diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 612cd6e787..6a3599cd02 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1489,8 +1489,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonHome/Inference/PokemonHome_BoxGenderDetector.h Source/PokemonHome/Inference/PokemonHome_ButtonDetector.cpp Source/PokemonHome/Inference/PokemonHome_ButtonDetector.h - Source/PokemonHome/Inference/PokemonHome_TypeReader.cpp - Source/PokemonHome/Inference/PokemonHome_TypeReader.h Source/PokemonHome/PokemonHome_Panels.cpp Source/PokemonHome/PokemonHome_Panels.h Source/PokemonHome/PokemonHome_Settings.cpp From ca5a4f13854445010b4554fb82055d82f0e43c89 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Fri, 10 Apr 2026 22:46:41 -0500 Subject: [PATCH 05/17] initial working living dex sorter --- .../Source/PokemonHome/PokemonHome_Panels.cpp | 2 + .../Programs/PokemonHome_BoxNavigation.cpp | 371 +++++++++++ .../Programs/PokemonHome_BoxNavigation.h | 65 ++ .../Programs/PokemonHome_BoxSorter.cpp | 335 +--------- .../PokemonHome_BoxSorterLivingDex.cpp | 592 ++++++++++++++++++ .../Programs/PokemonHome_BoxSorterLivingDex.h | 49 ++ SerialPrograms/cmake/SourceFiles.cmake | 4 + 7 files changed, 1086 insertions(+), 332 deletions(-) create mode 100644 SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp create mode 100644 SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h create mode 100644 SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp create mode 100644 SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h diff --git a/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp b/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp index 0b03823a4b..de1991c23c 100644 --- a/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp +++ b/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp @@ -10,6 +10,7 @@ #include "Programs/PokemonHome_PageSwap.h" #include "Programs/PokemonHome_BoxSorter.h" +#include "Programs/PokemonHome_BoxSorterLivingDex.h" #include "Programs/PokemonHome_GenerateNameOCR.h" @@ -31,6 +32,7 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back("---- General ----"); ret.emplace_back(make_single_switch_program()); ret.emplace_back(make_single_switch_program()); + ret.emplace_back(make_single_switch_program()); // ret.emplace_back("---- Trading ----"); diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp new file mode 100644 index 0000000000..c49cbeea4e --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp @@ -0,0 +1,371 @@ +/* Home Box Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/Notifications/ProgramInfo.h" +#include "CommonFramework/Tools/ErrorDumper.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/OCR/OCR_NumberReader.h" +#include "CommonTools/VisualDetectors/FrozenImageDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Resources/Pokemon_PokemonSlugs.h" +#include "Pokemon/Inference/Pokemon_TypeReader.h" +#include "PokemonHome/Inference/PokemonHome_BallReader.h" +#include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" +#include "PokemonHome_BoxNavigation.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ +using namespace Pokemon; + + +// Move the red cursor to the first slot of the box +// If the cursor is not add the first slot, move the cursor to the left and up one row at a time until it is at the first slot. +bool go_to_first_slot( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + Milliseconds VIDEO_DELAY +) { + ImageFloatBox first_slot_cursor_box(0.07, 0.15, 0.01, 0.01); //cursor position of the first slot of the box + VideoSnapshot screen = env.console.video().snapshot(); + FloatPixel first_slot_cursor_color = image_stats(extract_box_reference(screen, first_slot_cursor_box)).average; + env.console.log("BoxCursor color detection: " + first_slot_cursor_color.to_string()); + VideoOverlaySet BoxRender(env.console); + BoxRender.add(COLOR_BLUE, first_slot_cursor_box); + + // If the cursor is not at the first slot + if (first_slot_cursor_color.r <= first_slot_cursor_color.g + first_slot_cursor_color.b) { + bool cursor_found = false; + for (uint8_t rows = 0; rows < 7; rows++) { + for (uint8_t column = 0; column < 5; column++) { + pbf_press_dpad(context, DPAD_LEFT, 80ms, VIDEO_DELAY); + context.wait_for_all_requests(); + screen = env.console.video().snapshot(); + first_slot_cursor_color = image_stats(extract_box_reference(screen, first_slot_cursor_box)).average; + env.console.log("BoxCursor color detection: " + first_slot_cursor_color.to_string()); + + if (first_slot_cursor_color.r > first_slot_cursor_color.g + first_slot_cursor_color.b) { + cursor_found = true; + break; + } + } + if (!cursor_found) { + pbf_press_dpad(context, DPAD_UP, 80ms, VIDEO_DELAY); + context.wait_for_all_requests(); + screen = env.console.video().snapshot(); + first_slot_cursor_color = image_stats(extract_box_reference(screen, first_slot_cursor_box)).average; + env.console.log("BoxCursor color detection: " + first_slot_cursor_color.to_string()); + + if (first_slot_cursor_color.r > first_slot_cursor_color.g + first_slot_cursor_color.b) { + cursor_found = true; + break; + } + } + else { + break; + } + } + if (!cursor_found) { + return false; + } + } + BoxRender.clear(); + return true; +} + +//Move the cursor to the given coordinates, knowing current pos via the cursor struct +[[nodiscard]] BoxCursor move_cursor_to( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const BoxCursor& cur_cursor, + const BoxCursor& dest_cursor, + Milliseconds GAME_DELAY +) { + + std::ostringstream ss; + ss << "Moving cursor from " << cur_cursor << " to " << dest_cursor; + env.console.log(ss.str()); + + // TODO: shortest path movement though pages, boxes + for (size_t i = cur_cursor.box; i < dest_cursor.box; ++i) { + pbf_press_button(context, BUTTON_R, 80ms, GAME_DELAY + 240ms); + } + for (size_t i = dest_cursor.box; i < cur_cursor.box; ++i) { + pbf_press_button(context, BUTTON_L, 80ms, GAME_DELAY + 240ms); + } + + + // direct nav up or down through rows + if (!(cur_cursor.row == 0 && dest_cursor.row == 4) && !(dest_cursor.row == 0 && cur_cursor.row == 4)) { + for (size_t i = cur_cursor.row; i < dest_cursor.row; ++i) { + pbf_press_dpad(context, DPAD_DOWN, 80ms, GAME_DELAY); + } + for (size_t i = dest_cursor.row; i < cur_cursor.row; ++i) { + pbf_press_dpad(context, DPAD_UP, 80ms, GAME_DELAY); + } + } + else { // wrap around is faster to move between first or last row + if (cur_cursor.row == 0 && dest_cursor.row == 4) { + for (size_t i = 0; i <= 2; ++i) { + pbf_press_dpad(context, DPAD_UP, 80ms, GAME_DELAY); + } + } + else { + for (size_t i = 0; i <= 2; ++i) { + pbf_press_dpad(context, DPAD_DOWN, 80ms, GAME_DELAY); + } + } + } + + // direct nav forward or backward through columns + if ((dest_cursor.column > cur_cursor.column && dest_cursor.column - cur_cursor.column <= 3) || (cur_cursor.column > dest_cursor.column && cur_cursor.column - dest_cursor.column <= 3)) { + for (size_t i = cur_cursor.column; i < dest_cursor.column; ++i) { + pbf_press_dpad(context, DPAD_RIGHT, 80ms, GAME_DELAY); + } + for (size_t i = dest_cursor.column; i < cur_cursor.column; ++i) { + pbf_press_dpad(context, DPAD_LEFT, 80ms, GAME_DELAY); + } + } + else { // wrap around is faster if direct movement is more than 3 away + if (dest_cursor.column > cur_cursor.column) { + for (size_t i = 0; i < BOX_COLS - (dest_cursor.column - cur_cursor.column); ++i) { + pbf_press_dpad(context, DPAD_LEFT, 80ms, GAME_DELAY); + } + } + if (cur_cursor.column > dest_cursor.column) { + for (size_t i = 0; i < BOX_COLS - (cur_cursor.column - dest_cursor.column); ++i) { + pbf_press_dpad(context, DPAD_RIGHT, 80ms, GAME_DELAY); + } + } + } + + context.wait_for_all_requests(); + return dest_cursor; +} + +// Read current screen to find occupied and empty slots in the box. +// Add a placeholder value for each slot in order into `boxes_data`. For empty slot the value is just std::nullopt, while +// for the occupied slot it is an empty pokemon struct `CollectedPokemonInfo`. +// Return the (row, col) index of the first pokemon (aka non-empty) slot in the box. +std::array find_occupied_slots_in_box( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector>& boxes_data, + const std::vector& sort_preferences) +{ + //BoxSorter_Descriptor::Stats& stats = env.current_stats< BoxSorter_Descriptor::Stats>(); + + VideoSnapshot screen = env.console.video().snapshot(); + + std::ostringstream ss; + ss << "\n"; + + std::array first_pokemon_slot = { SIZE_MAX, SIZE_MAX }; + + int num_empty_slots = 0; + for (size_t row = 0; row < BOX_ROWS; row++) { + for (size_t col = 0; col < BOX_COLS; col++) { + ImageFloatBox slot_box(0.06 + (0.072 * col), 0.2 + (0.1035 * row), 0.03, 0.057); + int current_box_value = (int)image_stddev(extract_box_reference(screen, slot_box)).sum(); + + ss << current_box_value; + + //checking color to know if a pokemon is on the slot or not + if (current_box_value < 10) { + //stats.empty++; + num_empty_slots++; + boxes_data.push_back(std::nullopt); //empty optional to make sorting easier later + ss << "\u274c "; // "X" + } + else { + if (first_pokemon_slot[0] == SIZE_MAX) { + first_pokemon_slot = { row, col }; + } + //stats.pkmn++; + boxes_data.push_back( + CollectedPokemonInfo{ + .preferences = &sort_preferences + } + ); //default initialised pokemon to know there is a pokemon here that needs a value + ss << "\u2705 "; // checkbox + } + } + ss << "\n"; + } + + //env.update_stats(); + env.log(ss.str()); + env.add_overlay_log("Empty: " + std::to_string(num_empty_slots) + "/30"); + + return first_pokemon_slot; +} + +// Read the current summary screen and assign various pokemon info into `cur_pokemon-info` +void read_summary_screen( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + CollectedPokemonInfo& cur_pokemon_info +) { + VideoOverlaySet video_overlay_set(env.console); + + ImageFloatBox national_dex_number_box(0.448, 0.245, 0.049, 0.04); //pokemon national dex number pos + ImageFloatBox shiny_symbol_box(0.702, 0.09, 0.04, 0.06); // shiny symbol pos + // TODO: gmax symbol is at the same location as Tera type symbol! Need better detection to tell apart + // gmax symbol and tera types + ImageFloatBox gmax_symbol_box(0.463, 0.09, 0.04, 0.06); // gmax symbol pos + ImageFloatBox origin_symbol_box(0.623, 0.095, 0.033, 0.05); // origin symbol pos + ImageFloatBox pokemon_box(0.69, 0.18, 0.28, 0.46); // pokemon render pos + ImageFloatBox level_box(0.546, 0.099, 0.044, 0.041); // Level + ImageFloatBox ot_id_box(0.782, 0.719, 0.193, 0.046); // OT ID + ImageFloatBox ot_box(0.492, 0.719, 0.165, 0.049); // OT + ImageFloatBox nature_box(0.157, 0.783, 0.212, 0.042); // Nature + ImageFloatBox ability_box(0.158, 0.838, 0.213, 0.042); // Ability + ImageFloatBox alpha_box(0.787, 0.095, 0.024, 0.046); // Alpha symbol + ImageFloatBox type_box(0.615, 0.240, 0.071, 0.057); // Type symbols + + + video_overlay_set.add(COLOR_WHITE, national_dex_number_box); + video_overlay_set.add(COLOR_BLUE, shiny_symbol_box); + video_overlay_set.add(COLOR_RED, gmax_symbol_box); + video_overlay_set.add(COLOR_RED, alpha_box); + video_overlay_set.add(COLOR_DARKGREEN, origin_symbol_box); + video_overlay_set.add(COLOR_DARK_BLUE, pokemon_box); + video_overlay_set.add(COLOR_RED, level_box); + video_overlay_set.add(COLOR_RED, ot_id_box); + video_overlay_set.add(COLOR_RED, ot_box); + video_overlay_set.add(COLOR_RED, nature_box); + video_overlay_set.add(COLOR_RED, ability_box); + video_overlay_set.add(COLOR_RED, type_box); + BoxGenderDetector::make_overlays(video_overlay_set); + + + // Wait for the summary screen transition to end + FrozenImageDetector frozen_image_detector(COLOR_GREEN, { 0.388, 0.238, 0.109, 0.062 }, Milliseconds(80), 20); + frozen_image_detector.make_overlays(video_overlay_set); + wait_until(env.console, context, 5s, { frozen_image_detector }); + + VideoSnapshot screen = env.console.video().snapshot(); + + const int dex_number = OCR::read_number_waterfill(env.console, extract_box_reference(screen, national_dex_number_box), 0xff808080, 0xffffffff); + if (dex_number <= 0 || dex_number > static_cast(NATIONAL_DEX_SLUGS().size())) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "BoxSorter Check Summary: Unable to read a correct dex number, found: " + std::to_string(dex_number), + env.console + ); + } + cur_pokemon_info.dex_number = (uint16_t)dex_number; + cur_pokemon_info.name_slug = NATIONAL_DEX_SLUGS()[dex_number - 1]; + + const int shiny_stddev_value = (int)image_stddev(extract_box_reference(screen, shiny_symbol_box)).sum(); + const bool is_shiny = shiny_stddev_value > 30; + cur_pokemon_info.shiny = is_shiny; + env.console.log("Shiny detection stddev:" + std::to_string(shiny_stddev_value) + " is shiny:" + std::to_string(is_shiny)); + + const int gmax_stddev_value = (int)image_stddev(extract_box_reference(screen, gmax_symbol_box)).sum(); + const bool is_gmax = gmax_stddev_value > 30; + cur_pokemon_info.gmax = is_gmax; + env.console.log("Gmax detection stddev:" + std::to_string(gmax_stddev_value) + " is gmax:" + std::to_string(is_gmax)); + + const int alpha_stddev_value = (int)image_stddev(extract_box_reference(screen, alpha_box)).sum(); + const bool is_alpha = alpha_stddev_value > 40; + cur_pokemon_info.alpha = is_alpha; + env.console.log("Alpha detection stddev:" + std::to_string(alpha_stddev_value) + " is alpha:" + std::to_string(is_alpha)); + + BallReader ball_reader(env.console); + cur_pokemon_info.ball_slug = ball_reader.read_ball(screen); + + const StatsHuntGenderFilter gender = BoxGenderDetector::detect(screen); + env.console.log("Gender: " + gender_to_string(gender), COLOR_GREEN); + cur_pokemon_info.gender = gender; + + const int ot_id = OCR::read_number_waterfill(env.console, extract_box_reference(screen, ot_id_box), 0xff808080, 0xffffffff); + if (ot_id < 0 || ot_id > 999'999) { + dump_image(env.console, ProgramInfo(), "ReadSummary_OT", screen); + } + cur_pokemon_info.ot_id = ot_id; + + auto [primaryType, secondaryType] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); + + cur_pokemon_info.primaryType = primaryType; + cur_pokemon_info.secondaryType = secondaryType; + + env.add_overlay_log(create_overlay_info(cur_pokemon_info)); + video_overlay_set.clear(); + + // NOTE edit when adding new struct members (detections go here likely) + + // level_box + // ot_box + // nature_box + // ability_box + + // Press button R to go to next summary screen + pbf_press_button(context, BUTTON_R, 80ms, 300ms); + context.wait_for_all_requests(); +} + +void print_boxes_data(const std::vector>& boxes_data, SingleSwitchProgramEnvironment& env) { + std::ostringstream ss; + for (const std::optional& pokemon : boxes_data) { + ss << pokemon << "\n"; + } + env.console.log(ss.str()); +} + +void exit_menus(SingleSwitchProgramEnvironment& env, ProControllerContext& context, std::chrono::milliseconds video_delay) { + VideoSnapshot screen = env.console.video().snapshot(); + VideoOverlaySet video_overlay_set(env.console); + + ImageFloatBox select_check(0.495, 0.0045, 0.01, 0.005); // square color to check which mode is active + FloatPixel image_value = image_stats(extract_box_reference(screen, select_check)).average; + + env.console.log("Color detected from the select square: " + image_value.to_string()); + + //if the correct color is not detected, getting out of every possible menu to make sure the program work no matter where you start it in your pokemon home + video_overlay_set.add(COLOR_BLUE, select_check); + if (image_value.r <= image_value.g + image_value.b) { + for (int var = 0; var < 5; ++var) { + pbf_press_button(context, BUTTON_B, 80ms, video_delay + 80ms); + } + context.wait_for_all_requests(); + context.wait_for(std::chrono::milliseconds(video_delay)); + screen = env.console.video().snapshot(); + image_value = image_stats(extract_box_reference(screen, select_check)).average; + env.console.log("Color detected from the select square: " + image_value.to_string()); + if (image_value.r <= image_value.g + image_value.b) { + for (int i = 0; i < 2; ++i) { + pbf_press_button(context, BUTTON_ZR, 80ms, video_delay + 240ms); //additional delay because this animation is slower than the rest + context.wait_for_all_requests(); + screen = env.console.video().snapshot(); + image_value = image_stats(extract_box_reference(screen, select_check)).average; + env.console.log("Color detected from the select square: " + image_value.to_string()); + if (image_value.r > image_value.g + image_value.b) { + break; + } + else if (i == 1) { + dump_image(env.console, ProgramInfo(), "SelectSquare", screen); + env.console.log("ERROR: Could not find correct color mode please check color logs and timings\n", COLOR_RED); + return; + } + } + } + } + + video_overlay_set.clear(); +} + +} +} +} diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h new file mode 100644 index 0000000000..0ec54b6ba8 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h @@ -0,0 +1,65 @@ +/* Home Box Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonHome_BoxNavigation_H +#define PokemonAutomation_PokemonHome_BoxNavigation_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Pokemon_BoxCursor.h" +#include "Pokemon/Pokemon_CollectedPokemonInfo.h" +#include "Pokemon/Options/Pokemon_BoxSortingTable.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ + +// Move the red cursor to the first slot of the box +bool go_to_first_slot( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + Milliseconds VIDEO_DELAY +); + +// Move the cursor to the given coordinates, knowing current pos via the cursor struct +[[nodiscard]] Pokemon::BoxCursor move_cursor_to( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const Pokemon::BoxCursor& cur_cursor, + const Pokemon::BoxCursor& dest_cursor, + Milliseconds GAME_DELAY +); + +// Read current screen to find occupied and empty slots in the box. +// Returns the (row, col) index of the first pokemon (aka non-empty) slot in the box. +std::array find_occupied_slots_in_box( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector>& boxes_data, + const std::vector& sort_preferences +); + +// Read the current summary screen and assign various pokemon info into cur_pokemon_info +void read_summary_screen( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + Pokemon::CollectedPokemonInfo& cur_pokemon_info +); + +void print_boxes_data( + const std::vector>& boxes_data, + SingleSwitchProgramEnvironment& env +); + +void exit_menus(SingleSwitchProgramEnvironment& env, ProControllerContext& context, std::chrono::milliseconds video_delay); + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index 79ad65911c..51d5457814 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -49,8 +49,8 @@ language #include "PokemonHome/Inference/PokemonHome_ButtonDetector.h" #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" -#include "PokemonHome/Inference/PokemonHome_TypeReader.h" #include "PokemonHome_BoxSorter.h" +#include "PokemonHome_BoxNavigation.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -137,136 +137,6 @@ BoxSorter::BoxSorter() PA_ADD_OPTION(NOTIFICATIONS); } - -// Move the red cursor to the first slot of the box -// If the cursor is not add the first slot, move the cursor to the left and up one row at a time until it is at the first slot. -bool go_to_first_slot( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - Milliseconds VIDEO_DELAY -){ - ImageFloatBox first_slot_cursor_box(0.07, 0.15, 0.01, 0.01); //cursor position of the first slot of the box - VideoSnapshot screen = env.console.video().snapshot(); - FloatPixel first_slot_cursor_color = image_stats(extract_box_reference(screen, first_slot_cursor_box)).average; - env.console.log("BoxCursor color detection: " + first_slot_cursor_color.to_string()); - VideoOverlaySet BoxRender(env.console); - BoxRender.add(COLOR_BLUE, first_slot_cursor_box); - - // If the cursor is not at the first slot - if(first_slot_cursor_color.r <= first_slot_cursor_color.g + first_slot_cursor_color.b){ - bool cursor_found = false; - for (uint8_t rows = 0; rows < 7; rows++){ - for (uint8_t column = 0; column < 5; column++){ - pbf_press_dpad(context, DPAD_LEFT, 80ms, VIDEO_DELAY); - context.wait_for_all_requests(); - screen = env.console.video().snapshot(); - first_slot_cursor_color = image_stats(extract_box_reference(screen, first_slot_cursor_box)).average; - env.console.log("BoxCursor color detection: " + first_slot_cursor_color.to_string()); - - if(first_slot_cursor_color.r > first_slot_cursor_color.g + first_slot_cursor_color.b){ - cursor_found = true; - break; - } - } - if(!cursor_found){ - pbf_press_dpad(context, DPAD_UP, 80ms, VIDEO_DELAY); - context.wait_for_all_requests(); - screen = env.console.video().snapshot(); - first_slot_cursor_color = image_stats(extract_box_reference(screen, first_slot_cursor_box)).average; - env.console.log("BoxCursor color detection: " + first_slot_cursor_color.to_string()); - - if(first_slot_cursor_color.r > first_slot_cursor_color.g + first_slot_cursor_color.b){ - cursor_found = true; - break; - } - }else{ - break; - } - } - if(!cursor_found){ - return false; - } - } - BoxRender.clear(); - return true; -} - -//Move the cursor to the given coordinates, knowing current pos via the cursor struct -[[nodiscard]] BoxCursor move_cursor_to( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - const BoxCursor& cur_cursor, - const BoxCursor& dest_cursor, - Milliseconds GAME_DELAY -){ - - std::ostringstream ss; - ss << "Moving cursor from " << cur_cursor << " to " << dest_cursor; - env.console.log(ss.str()); - - // TODO: shortest path movement though pages, boxes - for (size_t i = cur_cursor.box; i < dest_cursor.box; ++i){ - pbf_press_button(context, BUTTON_R, 80ms, GAME_DELAY + 240ms); - } - for (size_t i = dest_cursor.box; i < cur_cursor.box; ++i){ - pbf_press_button(context, BUTTON_L, 80ms, GAME_DELAY + 240ms); - } - - - // direct nav up or down through rows - if (!(cur_cursor.row == 0 && dest_cursor.row == 4) && !(dest_cursor.row == 0 && cur_cursor.row == 4)){ - for (size_t i = cur_cursor.row; i < dest_cursor.row; ++i){ - pbf_press_dpad(context, DPAD_DOWN, 80ms, GAME_DELAY); - } - for (size_t i = dest_cursor.row; i < cur_cursor.row; ++i){ - pbf_press_dpad(context, DPAD_UP, 80ms, GAME_DELAY); - } - }else{ // wrap around is faster to move between first or last row - if (cur_cursor.row == 0 && dest_cursor.row == 4){ - for (size_t i = 0; i <= 2; ++i){ - pbf_press_dpad(context, DPAD_UP, 80ms, GAME_DELAY); - } - }else{ - for (size_t i = 0; i <= 2; ++i){ - pbf_press_dpad(context, DPAD_DOWN, 80ms, GAME_DELAY); - } - } - } - - // direct nav forward or backward through columns - if ((dest_cursor.column > cur_cursor.column && dest_cursor.column - cur_cursor.column <= 3) || (cur_cursor.column > dest_cursor.column && cur_cursor.column - dest_cursor.column <= 3)){ - for (size_t i = cur_cursor.column; i < dest_cursor.column; ++i){ - pbf_press_dpad(context, DPAD_RIGHT, 80ms, GAME_DELAY); - } - for (size_t i = dest_cursor.column; i < cur_cursor.column; ++i){ - pbf_press_dpad(context, DPAD_LEFT, 80ms, GAME_DELAY); - } - }else{ // wrap around is faster if direct movement is more than 3 away - if (dest_cursor.column > cur_cursor.column){ - for (size_t i = 0; i < BOX_COLS - (dest_cursor.column - cur_cursor.column); ++i){ - pbf_press_dpad(context, DPAD_LEFT, 80ms, GAME_DELAY); - } - } - if (cur_cursor.column > dest_cursor.column){ - for (size_t i = 0; i < BOX_COLS - (cur_cursor.column - dest_cursor.column); ++i){ - pbf_press_dpad(context, DPAD_RIGHT, 80ms, GAME_DELAY); - } - } - } - - context.wait_for_all_requests(); - return dest_cursor; -} - -void print_boxes_data(const std::vector>& boxes_data, SingleSwitchProgramEnvironment& env){ - std::ostringstream ss; - for (const std::optional& pokemon : boxes_data){ - ss << pokemon << "\n"; - } - env.console.log(ss.str()); -} - - void sort( SingleSwitchProgramEnvironment& env, ProControllerContext& context, @@ -326,169 +196,9 @@ void sort( } } -// Read current screen to find occupied and empty slots in the box. -// Add a placeholder value for each slot in order into `boxes_data`. For empty slot the value is just std::nullopt, while -// for the occupied slot it is an empty pokemon struct `CollectedPokemonInfo`. -// Return the (row, col) index of the first pokemon (aka non-empty) slot in the box. -std::array find_occupied_slots_in_box( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - std::vector>& boxes_data, - const std::vector& sort_preferences) -{ - BoxSorter_Descriptor::Stats& stats = env.current_stats< BoxSorter_Descriptor::Stats>(); - - VideoSnapshot screen = env.console.video().snapshot(); - - std::ostringstream ss; - ss << "\n"; - - std::array first_pokemon_slot = {SIZE_MAX, SIZE_MAX}; - - int num_empty_slots = 0; - for (size_t row = 0; row < BOX_ROWS; row++){ - for (size_t col = 0; col < BOX_COLS; col++){ - ImageFloatBox slot_box(0.06 + (0.072 * col), 0.2 + (0.1035 * row), 0.03, 0.057); - int current_box_value = (int)image_stddev(extract_box_reference(screen, slot_box)).sum(); - - ss << current_box_value; - - //checking color to know if a pokemon is on the slot or not - if(current_box_value < 10){ - stats.empty++; - num_empty_slots++; - boxes_data.push_back(std::nullopt); //empty optional to make sorting easier later - ss << "\u274c " ; // "X" - }else{ - if(first_pokemon_slot[0] == SIZE_MAX){ - first_pokemon_slot = {row, col}; - } - stats.pkmn++; - boxes_data.push_back( - CollectedPokemonInfo{ - .preferences = &sort_preferences - } - ); //default initialised pokemon to know there is a pokemon here that needs a value - ss << "\u2705 " ; // checkbox - } - } - ss << "\n"; - } - - env.update_stats(); - env.log(ss.str()); - env.add_overlay_log("Empty: " + std::to_string(num_empty_slots) + "/30"); - - return first_pokemon_slot; -} - -// Read the current summary screen and assign various pokemon info into `cur_pokemon-info` -void read_summary_screen( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, - CollectedPokemonInfo& cur_pokemon_info -){ - VideoOverlaySet video_overlay_set(env.console); - - ImageFloatBox national_dex_number_box(0.448, 0.245, 0.049, 0.04); //pokemon national dex number pos - ImageFloatBox shiny_symbol_box(0.702, 0.09, 0.04, 0.06); // shiny symbol pos - // TODO: gmax symbol is at the same location as Tera type symbol! Need better detection to tell apart - // gmax symbol and tera types - ImageFloatBox gmax_symbol_box(0.463, 0.09, 0.04, 0.06); // gmax symbol pos - ImageFloatBox origin_symbol_box(0.623, 0.095, 0.033, 0.05); // origin symbol pos - ImageFloatBox pokemon_box(0.69, 0.18, 0.28, 0.46); // pokemon render pos - ImageFloatBox level_box(0.546, 0.099, 0.044, 0.041); // Level - ImageFloatBox ot_id_box(0.782, 0.719, 0.193, 0.046); // OT ID - ImageFloatBox ot_box(0.492, 0.719, 0.165, 0.049); // OT - ImageFloatBox nature_box(0.157, 0.783, 0.212, 0.042); // Nature - ImageFloatBox ability_box(0.158, 0.838, 0.213, 0.042); // Ability - ImageFloatBox alpha_box(0.787, 0.095, 0.024, 0.046); // Alpha symbol - ImageFloatBox type_box(0.615, 0.240, 0.071, 0.057); // Type symbols - - - video_overlay_set.add(COLOR_WHITE, national_dex_number_box); - video_overlay_set.add(COLOR_BLUE, shiny_symbol_box); - video_overlay_set.add(COLOR_RED, gmax_symbol_box); - video_overlay_set.add(COLOR_RED, alpha_box); - video_overlay_set.add(COLOR_DARKGREEN, origin_symbol_box); - video_overlay_set.add(COLOR_DARK_BLUE, pokemon_box); - video_overlay_set.add(COLOR_RED, level_box); - video_overlay_set.add(COLOR_RED, ot_id_box); - video_overlay_set.add(COLOR_RED, ot_box); - video_overlay_set.add(COLOR_RED, nature_box); - video_overlay_set.add(COLOR_RED, ability_box); - BoxGenderDetector::make_overlays(video_overlay_set); - - - // Wait for the summary screen transition to end - FrozenImageDetector frozen_image_detector(COLOR_GREEN, {0.388, 0.238, 0.109, 0.062}, Milliseconds(80), 20); - frozen_image_detector.make_overlays(video_overlay_set); - wait_until(env.console, context, 5s, {frozen_image_detector}); - - VideoSnapshot screen = env.console.video().snapshot(); - - const int dex_number = OCR::read_number_waterfill(env.console, extract_box_reference(screen, national_dex_number_box), 0xff808080, 0xffffffff); - if (dex_number <= 0 || dex_number > static_cast(NATIONAL_DEX_SLUGS().size())){ - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, - "BoxSorter Check Summary: Unable to read a correct dex number, found: " + std::to_string(dex_number), - env.console - ); - } - cur_pokemon_info.dex_number = (uint16_t)dex_number; - cur_pokemon_info.name_slug = NATIONAL_DEX_SLUGS()[dex_number-1]; - - const int shiny_stddev_value = (int)image_stddev(extract_box_reference(screen, shiny_symbol_box)).sum(); - const bool is_shiny = shiny_stddev_value > 30; - cur_pokemon_info.shiny = is_shiny; - env.console.log("Shiny detection stddev:" + std::to_string(shiny_stddev_value) + " is shiny:" + std::to_string(is_shiny)); - - const int gmax_stddev_value = (int)image_stddev(extract_box_reference(screen, gmax_symbol_box)).sum(); - const bool is_gmax = gmax_stddev_value > 30; - cur_pokemon_info.gmax = is_gmax; - env.console.log("Gmax detection stddev:" + std::to_string(gmax_stddev_value) + " is gmax:" + std::to_string(is_gmax)); - - const int alpha_stddev_value = (int)image_stddev(extract_box_reference(screen, alpha_box)).sum(); - const bool is_alpha = alpha_stddev_value > 40; - cur_pokemon_info.alpha = is_alpha; - env.console.log("Alpha detection stddev:" + std::to_string(alpha_stddev_value) + " is alpha:" + std::to_string(is_alpha)); - - BallReader ball_reader(env.console); - cur_pokemon_info.ball_slug = ball_reader.read_ball(screen); - - const StatsHuntGenderFilter gender = BoxGenderDetector::detect(screen); - env.console.log("Gender: " + gender_to_string(gender), COLOR_GREEN); - cur_pokemon_info.gender = gender; - - const int ot_id = OCR::read_number_waterfill(env.console, extract_box_reference(screen, ot_id_box), 0xff808080, 0xffffffff); - if (ot_id < 0 || ot_id > 999'999){ - dump_image(env.console, ProgramInfo(), "ReadSummary_OT", screen); - } - cur_pokemon_info.ot_id = ot_id; - - TypeReader type_reader(type_box); - auto [type1, type2] = type_reader.read_types(screen); - - cur_pokemon_info.type1 = type1; - cur_pokemon_info.type2 = type2; - - env.add_overlay_log(create_overlay_info(cur_pokemon_info)); - video_overlay_set.clear(); - - // NOTE edit when adding new struct members (detections go here likely) - - // level_box - // ot_box - // nature_box - // ability_box - - // Press button R to go to next summary screen - pbf_press_button(context, BUTTON_R, 80ms, 300ms); - context.wait_for_all_requests(); -} - void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ StartProgramChecks::check_performance_class_wired_or_wireless(context); - + const std::vector sort_preferences = SORT_TABLE.preferences(); if (sort_preferences.empty()){ throw UserSetupError(env.console, "At least one sorting method selection needs to be made!"); @@ -496,56 +206,17 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex BoxSorter_Descriptor::Stats& stats = env.current_stats< BoxSorter_Descriptor::Stats>(); - ImageFloatBox select_check(0.495, 0.0045, 0.01, 0.005); // square color to check which mode is active - - // vector that will store data for each slot std::vector> boxes_data; BoxCursor cur_cursor{static_cast(BOX_NUMBER-1), 0, 0}; - VideoSnapshot screen = env.console.video().snapshot(); - VideoOverlaySet video_overlay_set(env.console); - FloatPixel image_value = image_stats(extract_box_reference(screen, select_check)).average; - - env.console.log("Color detected from the select square: " + image_value.to_string()); - - //if the correct color is not detected, getting out of every possible menu to make sure the program work no matter where you start it in your pokemon home - video_overlay_set.add(COLOR_BLUE, select_check); - if(image_value.r <= image_value.g + image_value.b){ - for (int var = 0; var < 5; ++var){ - pbf_press_button(context, BUTTON_B, 80ms, GAME_DELAY.get() + 80ms); - } - context.wait_for_all_requests(); - context.wait_for(std::chrono::milliseconds(VIDEO_DELAY)); - screen = env.console.video().snapshot(); - image_value = image_stats(extract_box_reference(screen, select_check)).average; - env.console.log("Color detected from the select square: " + image_value.to_string()); - if(image_value.r <= image_value.g + image_value.b){ - for (int i = 0; i < 2; ++i){ - pbf_press_button(context, BUTTON_ZR, 80ms, VIDEO_DELAY.get() + 240ms); //additional delay because this animation is slower than the rest - context.wait_for_all_requests(); - screen = env.console.video().snapshot(); - image_value = image_stats(extract_box_reference(screen, select_check)).average; - env.console.log("Color detected from the select square: " + image_value.to_string()); - if(image_value.r > image_value.g + image_value.b){ - break; - }else if(i==1){ - dump_image(env.console, ProgramInfo(), "SelectSquare", screen); - env.console.log("ERROR: Could not find correct color mode please check color logs and timings\n", COLOR_RED); - return; - } - } - } - } - - video_overlay_set.clear(); - BoxCursor dest_cursor; BoxCursor nav_cursor = {0, 0, 0}; + exit_menus(env, context, VIDEO_DELAY.get()); BoxViewWatcher box_view_watcher(&env.console.overlay()); SummaryScreenWatcher summary_screen_watcher(&env.console.overlay()); diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp new file mode 100644 index 0000000000..560153eb44 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -0,0 +1,592 @@ +/* Home Box Sorter Living Dex + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "CommonTools/StartupChecks/StartProgramChecks.h" +#include "Pokemon/Pokemon_Strings.h" +#include "Pokemon/Pokemon_BoxCursor.h" +#include "Pokemon/Pokemon_CollectedPokemonInfo.h" +#include "PokemonHome/Inference/PokemonHome_ButtonDetector.h" +#include "PokemonHome_BoxSorterLivingDex.h" +#include "PokemonHome_BoxNavigation.h" +#include "Common/Cpp/Json/JsonValue.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Pokemon/Inference/Pokemon_TypeReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ +using namespace Pokemon; + + +const size_t MAX_HOME_BOXES = 200; + +struct LivingDexEntry{ + std::string species_slug; + std::string form_slug; + uint16_t nat_id; + PokemonType primary_type; + PokemonType secondary_type; + std::string slug; + bool has_gender_difference; +}; + + +BoxSorterLivingDex_Descriptor::BoxSorterLivingDex_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonHome:BoxSorterLivingDex", + STRING_POKEMON + " Home", "Box Sorter Living Dex", + "Programs/PokemonHome/BoxSorterLivingDex.html", + "Arrange boxes of " + STRING_POKEMON + " into living dex order.", + ProgramControllerClass::StandardController_RequiresPrecision, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS + ) +{} + +struct BoxSorterLivingDex_Descriptor::Stats : public StatsTracker{ + Stats() + : pkmn(m_stats["Pokemon"]) + , empty(m_stats["Empty Slots"]) + , swaps(m_stats["Swaps"]) + { + m_display_order.emplace_back(Stat("Pokemon")); + m_display_order.emplace_back(Stat("Empty Slots")); + m_display_order.emplace_back(Stat("Swaps")); + } + std::atomic& pkmn; + std::atomic& empty; + std::atomic& swaps; +}; + +std::unique_ptr BoxSorterLivingDex_Descriptor::make_stats() const{ + return std::unique_ptr(new Stats()); +} + +BoxSorterLivingDex::BoxSorterLivingDex() + : LIVING_DEX_START_BOX( + "Living Dex Starting Box:
Box number where the living dex placement begins (1-indexed).", + LockMode::LOCK_WHILE_RUNNING, + 1, 1, (uint16_t)MAX_HOME_BOXES + ) + , REJECT_BOX_START( + "Reject Box Range Start:
First box of the range reserved for rejected or overflow " + STRING_POKEMON + ".", + LockMode::LOCK_WHILE_RUNNING, + 1, 1, (uint16_t)MAX_HOME_BOXES + ) + , REJECT_BOX_END( + "Reject Box Range End:
Last box of the range reserved for rejected or overflow " + STRING_POKEMON + ".", + LockMode::LOCK_WHILE_RUNNING, + 1, 1, (uint16_t)MAX_HOME_BOXES + ) + , VIDEO_DELAY( + "Capture Card Delay:", + LockMode::LOCK_WHILE_RUNNING, + "400 ms" + ) + , GAME_DELAY( + "" + STRING_POKEMON + " Home App Delay:", + LockMode::LOCK_WHILE_RUNNING, + "240 ms" + ) + , OUTPUT_FILE( + false, + "Output File:
JSON file basename to catalogue box info found by the program.", + LockMode::LOCK_WHILE_RUNNING, + "living_dex_order", + "living_dex_order" + ) + , DRY_RUN( + "Dry Run:
Catalogue and make sorting plan without execution. Check output at .json and -sorted.json)", + LockMode::LOCK_WHILE_RUNNING, + false + ) + , NOTIFICATIONS({ + &NOTIFICATION_PROGRAM_FINISH + }) +{ + PA_ADD_OPTION(LIVING_DEX_START_BOX); + PA_ADD_OPTION(REJECT_BOX_START); + PA_ADD_OPTION(REJECT_BOX_END); + PA_ADD_OPTION(VIDEO_DELAY); + PA_ADD_OPTION(GAME_DELAY); + PA_ADD_OPTION(OUTPUT_FILE); + PA_ADD_OPTION(DRY_RUN); + PA_ADD_OPTION(NOTIFICATIONS); +} + +bool is_viable_for_dex(LivingDexEntry entry, CollectedPokemonInfo pokemonInfo) { + if (entry.nat_id != pokemonInfo.dex_number) { + return false; + } + + if (entry.has_gender_difference) { + if (entry.slug.ends_with("-female") && pokemonInfo.gender != StatsHuntGenderFilter::Female) { + return false; + } + + if (entry.slug.ends_with("-male") && pokemonInfo.gender != StatsHuntGenderFilter::Male) { + return false; + } + } + + if (entry.primary_type != pokemonInfo.primaryType || entry.secondary_type != pokemonInfo.secondaryType) { + return false; + } + + return true; +} + +void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + StartProgramChecks::check_performance_class_wired_or_wireless(context); + + ImageFloatBox type_box(0.615, 0.240, 0.071, 0.057); + + for (size_t i = 0; i < 10; i++) + { + auto [t1, t2] = read_pokemon_types( + env.console.video().snapshot(), + type_box, + PokemonTypeGeneration::GEN9 + ); + + env.log("Type read: " + POKEMON_TYPE_SLUGS().get_string(t1), COLOR_CYAN); + env.log("Type read: " + POKEMON_TYPE_SLUGS().get_string(t2), COLOR_CYAN); + pbf_wait(context, 2000ms); + context.wait_for_all_requests(); + } + + + + std::string path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living.json"; + JsonValue json = load_json_file(path); + const JsonArray& slugs = json.to_array_throw(path); + + std::vector living_dex_order; + living_dex_order.reserve(slugs.size()); + for (const JsonValue& entry_value : slugs){ + const JsonObject& obj = entry_value.to_object_throw(path); + LivingDexEntry entry; + entry.species_slug = obj.get_string_throw("species_slug", path); + entry.form_slug = obj.get_string_throw("form_slug", path); + entry.nat_id = (uint16_t)obj.get_integer_throw("natID", path); + entry.primary_type = POKEMON_TYPE_SLUGS().get_enum(obj.get_string_throw("primaryType", path), PokemonType::NONE); + entry.secondary_type = POKEMON_TYPE_SLUGS().get_enum(obj.get_string_default("secondaryType", "none"), PokemonType::NONE); + entry.slug = obj.get_string_throw("slug", path); + entry.has_gender_difference = obj.get_boolean_throw("has_gender_difference", path); + living_dex_order.emplace_back(std::move(entry)); + } + + // TODO allow users to have rules for the "best" version of a pokemon to be kept in the living dex box + // It would be nice to add OT as most users that care about living dex also like to have it with their OT + const std::vector sort_preferences( + { + { SortingRuleType::DexNo, false } + }); + + BoxSorterLivingDex_Descriptor::Stats& stats = env.current_stats(); + + // vector that will store data for each slot + std::vector> living_dex_boxes_data; + + VideoOverlaySet video_overlay_set(env.console); + + exit_menus(env, context, VIDEO_DELAY.get()); + + BoxCursor dest_cursor; + BoxCursor nav_cursor((LIVING_DEX_START_BOX - 1), 0, 0); + + BoxViewWatcher box_view_watcher(&env.console.overlay()); + SummaryScreenWatcher summary_screen_watcher(&env.console.overlay()); + + size_t living_dex_box_count = (size_t)std::ceil((double)living_dex_order.size() / 30); + + //cycle through each living dex box + for (size_t box_idx = 0; box_idx < living_dex_box_count; box_idx++) { + if (box_idx != 0) { + dest_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + box_idx, 0, 0); + nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + } + else { + // Moving the cursor until it goes to the first slot + if (!go_to_first_slot(env, context, VIDEO_DELAY)) { + throw UserSetupError( + env.logger(), + "ERROR: Could not move cursor to the first slot, please consider adjusting delay." + ); + } + } + + context.wait_for_all_requests(); + const std::string log_msg = "Checking box " + std::to_string(box_idx + 1) + "/" + std::to_string(living_dex_box_count); + env.log(log_msg); + env.console.overlay().add_log(log_msg); + + + // Check box grid color stddev to find occupied slots and fill living_dex_boxes_data with placeholder pokemon info. + const std::array first_pokemon_slot = find_occupied_slots_in_box(env, context, living_dex_boxes_data, sort_preferences); + + //enter the summary screen to read pokemon info + if (first_pokemon_slot[0] != SIZE_MAX) { + // moving cursor to the first pokemon slot + dest_cursor = { LIVING_DEX_START_BOX - 1 + box_idx, first_pokemon_slot[0], first_pokemon_slot[1] }; + nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + + env.add_overlay_log("Checking Summary..."); + + pbf_press_button(context, BUTTON_A, 80ms, GAME_DELAY); + context.wait_for_all_requests(); + video_overlay_set.clear(); + pbf_press_dpad(context, DPAD_DOWN, 80ms, GAME_DELAY); + pbf_press_button(context, BUTTON_A, 80ms, 100ms); + context.wait_for_all_requests(); + int ret = wait_until(env.console, context, Seconds(5), { summary_screen_watcher }); + if (ret != 0) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find summary screen after 5 sec", env.console + ); + } + + + // cycle through each summary of the current box and fill pokemon information + for (size_t row = 0; row < BOX_ROWS; row++) { + for (size_t column = 0; column < BOX_COLS; column++) { + const size_t global_idx = to_global_index(box_idx, row, column); + if (!living_dex_boxes_data[global_idx].has_value()) { + continue; + } + + // Read the summary screen and assign data to living_dex_boxes_data[global_idx] + read_summary_screen(env, context, living_dex_boxes_data[global_idx].value()); + } + } + + // log detailed pokemon infomation of this box + std::ostringstream ss; + ss << std::endl; + for (size_t row = 0; row < BOX_ROWS; row++) { + for (size_t column = 0; column < BOX_COLS; column++) { + ss << "[" << row << ", " << column << "]: " << living_dex_boxes_data[to_global_index(box_idx, row, column)] << std::endl; + } + } + env.log(ss.str()); + + //get out of summary with a lot of delay because it's slow for some reasons + pbf_press_button(context, BUTTON_B, 80ms, 100ms); + context.wait_for_all_requests(); + ret = wait_until(env.console, context, Seconds(5), { box_view_watcher }); + if (ret != 0) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find box view after 5 sec", env.console + ); + } + video_overlay_set.clear(); + } + video_overlay_set.clear(); + } + + dest_cursor = BoxCursor((REJECT_BOX_START - 1), 0, 0); + nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + context.wait_for_all_requests(); + + std::vector> reject_boxes_data; + size_t reject_box_count = REJECT_BOX_END - REJECT_BOX_START + 1; + + //cycle through each reject box + for (size_t box_idx = 0; box_idx < reject_box_count; box_idx++) { + if (box_idx != 0) { + dest_cursor = BoxCursor(REJECT_BOX_START - 1 + box_idx, 0, 0); + nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + } + else { + // Moving the cursor until it goes to the first slot + if (!go_to_first_slot(env, context, VIDEO_DELAY)) { + throw UserSetupError( + env.logger(), + "ERROR: Could not move cursor to the first slot, please consider adjusting delay." + ); + } + } + + context.wait_for_all_requests(); + const std::string log_msg = "Checking box " + std::to_string(box_idx + 1) + "/" + std::to_string(reject_box_count); + env.log(log_msg); + env.console.overlay().add_log(log_msg); + + + // Check box grid color stddev to find occupied slots and fill reject_boxes_data with placeholder pokemon info. + const std::array first_pokemon_slot = find_occupied_slots_in_box(env, context, reject_boxes_data, sort_preferences); + + //enter the summary screen to read pokemon info + if (first_pokemon_slot[0] != SIZE_MAX) { + // moving cursor to the first pokemon slot + dest_cursor = { REJECT_BOX_START - 1 + box_idx, first_pokemon_slot[0], first_pokemon_slot[1] }; + nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + + env.add_overlay_log("Checking Summary..."); + + pbf_press_button(context, BUTTON_A, 80ms, GAME_DELAY); + context.wait_for_all_requests(); + video_overlay_set.clear(); + pbf_press_dpad(context, DPAD_DOWN, 80ms, GAME_DELAY); + pbf_press_button(context, BUTTON_A, 80ms, 100ms); + context.wait_for_all_requests(); + int ret = wait_until(env.console, context, Seconds(5), { summary_screen_watcher }); + if (ret != 0) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find summary screen after 5 sec", env.console + ); + } + + + // cycle through each summary of the current box and fill pokemon information + for (size_t row = 0; row < BOX_ROWS; row++) { + for (size_t column = 0; column < BOX_COLS; column++) { + const size_t global_idx = to_global_index(box_idx, row, column); + if (!reject_boxes_data[global_idx].has_value()) { + continue; + } + + // Read the summary screen and assign data to reject_boxes_data[global_idx] + read_summary_screen(env, context, reject_boxes_data[global_idx].value()); + } + } + + // log detailed pokemon infomation of this box + std::ostringstream ss; + ss << std::endl; + for (size_t row = 0; row < BOX_ROWS; row++) { + for (size_t column = 0; column < BOX_COLS; column++) { + ss << "[" << row << ", " << column << "]: " << reject_boxes_data[to_global_index(box_idx, row, column)] << std::endl; + } + } + env.log(ss.str()); + + //get out of summary with a lot of delay because it's slow for some reasons + pbf_press_button(context, BUTTON_B, 80ms, 100ms); + context.wait_for_all_requests(); + ret = wait_until(env.console, context, Seconds(5), { box_view_watcher }); + if (ret != 0) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find box view after 5 sec", env.console + ); + } + video_overlay_set.clear(); + } + + video_overlay_set.clear(); + } + + + std::ostringstream ss; + + // TODO: implement living dex ordering logic + + for (size_t i = 0; i < living_dex_order.size(); i++) + { + bool found = false; + LivingDexEntry entry = living_dex_order[i]; + env.log("Looking for " + entry.slug + " in the living dex boxes."); + for(size_t j = i; j < living_dex_boxes_data.size(); j++){ + if (!living_dex_boxes_data[j].has_value()) { + continue; + } + + if (!is_viable_for_dex(entry, *living_dex_boxes_data[j])) { + continue; + } + + found = true; + if (i == j) { + // already in the right spot, do nothing + break; + } + else { + ss << "j: " << j << "i:" << i; + env.console.log(ss.str()); + ss.str(""); + BoxCursor pokemon_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + j / 30, (j / 6) % 5, j % 6); + BoxCursor destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); + ss << "Swapping " << living_dex_boxes_data[j] << " at " << pokemon_cursor << " and " << living_dex_boxes_data[i] << " at " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); + + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + + context.wait_for_all_requests(); + + std::swap(living_dex_boxes_data[j], living_dex_boxes_data[i]); + stats.swaps++; + env.update_stats(); + } + + break; + } + + if (found){ + continue; + } + + env.log("Could not find a pokemon for " + entry.slug + " in the living dex boxes, looking in reject boxes for a valid match."); + + for (size_t j = 0; j < reject_boxes_data.size(); j++) { + if (!reject_boxes_data[j].has_value()) { + continue; + } + + if (!is_viable_for_dex(entry, *reject_boxes_data[j])) { + continue; + } + + found = true; + + BoxCursor pokemon_cursor = BoxCursor(REJECT_BOX_START - 1 + j / 30, (j / 6) % 5, j % 6); + BoxCursor destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); + ss << "Swapping " << reject_boxes_data[j] << " at " << pokemon_cursor << " and " << living_dex_boxes_data[i] << " at " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); + + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + + context.wait_for_all_requests(); + + std::swap(living_dex_boxes_data[j], living_dex_boxes_data[i]); + stats.swaps++; + env.update_stats(); + + break; + } + + if (found) { + continue; + } + + if (!living_dex_boxes_data[i].has_value()) { + continue; + } + + // move the pokemon in the current living dex slot to the reject box or later in the living dex + size_t empty_reject_slot = SIZE_MAX; + size_t empty_living_dex_slot = SIZE_MAX; + for (size_t j = 0; j < reject_boxes_data.size(); j++) { + if (!reject_boxes_data[j].has_value()) { + empty_reject_slot = j; + break; + } + } + if (empty_reject_slot == SIZE_MAX) { + env.log("No empty reject box slots available to move pokemon out of the way, placing in open slot in living dex for now."); + + for (size_t j = i + 1; j < living_dex_boxes_data.size(); j++) { + if (!living_dex_boxes_data[j].has_value()) { + empty_living_dex_slot = j; + break; + } + } + } + BoxCursor pokemon_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); + BoxCursor destination_cursor; + + if (empty_reject_slot != SIZE_MAX) { + destination_cursor = BoxCursor(REJECT_BOX_START - 1 + empty_reject_slot / 30, (empty_reject_slot / 6) % 5, empty_reject_slot % 6); + } + else if (empty_living_dex_slot != SIZE_MAX) { + destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + empty_living_dex_slot / 30, (empty_living_dex_slot / 6) % 5, empty_living_dex_slot % 6); + } + else { + throw UserSetupError( + env.logger(), + "ERROR: No empty slots available to move pokemon out of the way, please consider removing some duplicates or increasing the reject box range." + ); + } + ss << "Moving " << living_dex_boxes_data[i] << " at " << pokemon_cursor << " to " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + context.wait_for_all_requests(); + if (empty_living_dex_slot != SIZE_MAX) { + std::swap(living_dex_boxes_data[i], living_dex_boxes_data[empty_living_dex_slot]); + } + else { + std::swap(living_dex_boxes_data[i], reject_boxes_data[empty_reject_slot]); + } + stats.swaps++; + env.update_stats(); + } + + for (size_t i = living_dex_order.size(); i < living_dex_boxes_data.size(); i++) + { + if (!living_dex_boxes_data[i].has_value()) { + continue; + } + + size_t empty_reject_slot = SIZE_MAX; + for (size_t j = 0; j < reject_boxes_data.size(); j++) { + if (!reject_boxes_data[j].has_value()) { + empty_reject_slot = j; + break; + } + } + + if (empty_reject_slot == SIZE_MAX) { + break; + } + + BoxCursor pokemon_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, i % 5, i % 6); + BoxCursor destination_cursor = BoxCursor(REJECT_BOX_START - 1 + empty_reject_slot / 30, (empty_reject_slot / 6) % 5, empty_reject_slot % 6); + + ss << "Moving " << living_dex_boxes_data[i] << " at " << pokemon_cursor << " to " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + context.wait_for_all_requests(); + std::swap(living_dex_boxes_data[i], reject_boxes_data[empty_reject_slot]); + stats.swaps++; + env.update_stats(); + } + + // Loop through living dex order + // Attempt to find pokemon in living_dex_boxes_data then in reject_boxes_data + // If found swap the pokemon into the living dex + // If empty and currently ocupied move to open reject box slot swapping the records + // If no empty reject slots move to the end of the living dex to be moved later when there are open slots + // Add a check to ensure enough reject slots? + + + + send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); +} + +} +} +} diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h new file mode 100644 index 0000000000..e5c2bed21f --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h @@ -0,0 +1,49 @@ +/* Home Box Sorter Living Dex + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonHome_BoxSorterLivingDex_H +#define PokemonAutomation_PokemonHome_BoxSorterLivingDex_H + +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "Common/Cpp/Options/StringOption.h" +#include "Common/Cpp/Options/TimeDurationOption.h" +#include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ + +class BoxSorterLivingDex_Descriptor : public SingleSwitchProgramDescriptor{ +public: + BoxSorterLivingDex_Descriptor(); + + struct Stats; + virtual std::unique_ptr make_stats() const override; +}; + +class BoxSorterLivingDex : public SingleSwitchProgramInstance{ +public: + BoxSorterLivingDex(); + + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + SimpleIntegerOption LIVING_DEX_START_BOX; + SimpleIntegerOption REJECT_BOX_START; + SimpleIntegerOption REJECT_BOX_END; + MillisecondsOption VIDEO_DELAY; + MillisecondsOption GAME_DELAY; + StringOption OUTPUT_FILE; + BooleanCheckBoxOption DRY_RUN; + EventNotificationsOption NOTIFICATIONS; +}; + +} +} +} +#endif // PokemonAutomation_PokemonHome_BoxSorterLivingDex_H diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 6a3599cd02..cfd05899a1 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1493,8 +1493,12 @@ file(GLOB LIBRARY_SOURCES Source/PokemonHome/PokemonHome_Panels.h Source/PokemonHome/PokemonHome_Settings.cpp Source/PokemonHome/PokemonHome_Settings.h + Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp + Source/PokemonHome/Programs/PokemonHome_BoxNavigatino.h Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp Source/PokemonHome/Programs/PokemonHome_BoxSorter.h + Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp + Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h Source/PokemonHome/Programs/PokemonHome_GenerateNameOCR.cpp Source/PokemonHome/Programs/PokemonHome_GenerateNameOCR.h Source/PokemonHome/Programs/PokemonHome_PageSwap.cpp From b6140b7d479bba80edd21af40ffb25f79465ce32 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Wed, 22 Apr 2026 20:40:49 -0500 Subject: [PATCH 06/17] cleanup --- .../Programs/PokemonHome_BoxSorter.cpp | 18 ++------ .../PokemonHome_BoxSorterLivingDex.cpp | 45 +++++++++---------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index 7cf2886dac..d9a8d7151b 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -30,28 +30,18 @@ language #include #include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonFramework/ImageTools/ImageStats.h" #include "CommonFramework/Notifications/ProgramNotifications.h" -#include "CommonFramework/Tools/ErrorDumper.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonFramework/ProgramStats/StatsTracking.h" #include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/FrozenImageDetector.h" -#include "CommonTools/OCR/OCR_NumberReader.h" #include "CommonTools/StartupChecks/StartProgramChecks.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "Pokemon/Inference/Pokemon_TypeReader.h" #include "Pokemon/Pokemon_Strings.h" -#include "Pokemon/Resources/Pokemon_PokemonNames.h" -#include "Pokemon/Resources/Pokemon_PokemonSlugs.h" #include "Pokemon/Pokemon_BoxCursor.h" #include "Pokemon/Pokemon_CollectedPokemonInfo.h" #include "PokemonHome/Inference/PokemonHome_ButtonDetector.h" -#include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" -#include "PokemonHome/Inference/PokemonHome_BallReader.h" -#include "PokemonHome_BoxSorter.h" #include "PokemonHome_BoxNavigation.h" +#include "PokemonHome_BoxSorter.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -217,7 +207,7 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex BoxCursor dest_cursor; BoxCursor nav_cursor = {0, 0, 0}; - exit_menus(env, context, VIDEO_DELAY.get()); + exit_menus(env, context, VIDEO_DELAY.get()); BoxViewWatcher box_view_watcher(&env.console.overlay()); SummaryScreenWatcher summary_screen_watcher(&env.console.overlay()); @@ -267,7 +257,6 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex ); } - // cycle through each summary of the current box and fill pokemon information for (size_t row = 0; row < BOX_ROWS; row++){ for (size_t column = 0; column < BOX_COLS; column++){ @@ -335,5 +324,4 @@ void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContex } } -} - +} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index 560153eb44..dade0f4c1a 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -9,25 +9,24 @@ #include #include #include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonValue.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" #include "CommonFramework/Exceptions/OperationFailedException.h" #include "CommonFramework/ImageTools/ImageStats.h" #include "CommonFramework/Notifications/ProgramNotifications.h" #include "CommonFramework/ProgramStats/StatsTracking.h" #include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/StartupChecks/StartProgramChecks.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" - #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "CommonTools/StartupChecks/StartProgramChecks.h" +#include "Pokemon/Inference/Pokemon_TypeReader.h" #include "Pokemon/Pokemon_Strings.h" #include "Pokemon/Pokemon_BoxCursor.h" #include "Pokemon/Pokemon_CollectedPokemonInfo.h" #include "PokemonHome/Inference/PokemonHome_ButtonDetector.h" -#include "PokemonHome_BoxSorterLivingDex.h" #include "PokemonHome_BoxNavigation.h" -#include "Common/Cpp/Json/JsonValue.h" -#include "Common/Cpp/Json/JsonArray.h" -#include "Common/Cpp/Json/JsonObject.h" -#include "Pokemon/Inference/Pokemon_TypeReader.h" +#include "PokemonHome_BoxSorterLivingDex.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -148,7 +147,7 @@ bool is_viable_for_dex(LivingDexEntry entry, CollectedPokemonInfo pokemonInfo) { if (entry.primary_type != pokemonInfo.primaryType || entry.secondary_type != pokemonInfo.secondaryType) { return false; - } + } return true; } @@ -312,7 +311,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl for (size_t box_idx = 0; box_idx < reject_box_count; box_idx++) { if (box_idx != 0) { dest_cursor = BoxCursor(REJECT_BOX_START - 1 + box_idx, 0, 0); - nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); } else { // Moving the cursor until it goes to the first slot @@ -400,7 +399,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl for (size_t i = 0; i < living_dex_order.size(); i++) { - bool found = false; + bool found = false; LivingDexEntry entry = living_dex_order[i]; env.log("Looking for " + entry.slug + " in the living dex boxes."); for(size_t j = i; j < living_dex_boxes_data.size(); j++){ @@ -408,7 +407,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl continue; } - if (!is_viable_for_dex(entry, *living_dex_boxes_data[j])) { + if (!is_viable_for_dex(entry, *living_dex_boxes_data[j])) { continue; } @@ -440,25 +439,25 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl env.update_stats(); } - break; + break; } if (found){ - continue; + continue; } - env.log("Could not find a pokemon for " + entry.slug + " in the living dex boxes, looking in reject boxes for a valid match."); + env.log("Could not find a pokemon for " + entry.slug + " in the living dex boxes, looking in reject boxes for a valid match."); for (size_t j = 0; j < reject_boxes_data.size(); j++) { if (!reject_boxes_data[j].has_value()) { continue; } - if (!is_viable_for_dex(entry, *reject_boxes_data[j])) { + if (!is_viable_for_dex(entry, *reject_boxes_data[j])) { continue; } - found = true; + found = true; BoxCursor pokemon_cursor = BoxCursor(REJECT_BOX_START - 1 + j / 30, (j / 6) % 5, j % 6); BoxCursor destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); @@ -482,12 +481,12 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl } if (found) { - continue; + continue; } if (!living_dex_boxes_data[i].has_value()) { continue; - } + } // move the pokemon in the current living dex slot to the reject box or later in the living dex size_t empty_reject_slot = SIZE_MAX; @@ -499,7 +498,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl } } if (empty_reject_slot == SIZE_MAX) { - env.log("No empty reject box slots available to move pokemon out of the way, placing in open slot in living dex for now."); + env.log("No empty reject box slots available to move pokemon out of the way, placing in open slot in living dex for now."); for (size_t j = i + 1; j < living_dex_boxes_data.size(); j++) { if (!living_dex_boxes_data[j].has_value()) { @@ -521,8 +520,8 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl throw UserSetupError( env.logger(), "ERROR: No empty slots available to move pokemon out of the way, please consider removing some duplicates or increasing the reject box range." - ); - } + ); + } ss << "Moving " << living_dex_boxes_data[i] << " at " << pokemon_cursor << " to " << destination_cursor; env.console.log(ss.str()); ss.str(""); @@ -531,7 +530,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); context.wait_for_all_requests(); - if (empty_living_dex_slot != SIZE_MAX) { + if (empty_living_dex_slot != SIZE_MAX) { std::swap(living_dex_boxes_data[i], living_dex_boxes_data[empty_living_dex_slot]); } else { @@ -545,7 +544,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl { if (!living_dex_boxes_data[i].has_value()) { continue; - } + } size_t empty_reject_slot = SIZE_MAX; for (size_t j = 0; j < reject_boxes_data.size(); j++) { From f5ffd3dc14596870c9517378008359ed5a259182 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Wed, 22 Apr 2026 21:09:22 -0500 Subject: [PATCH 07/17] Missing include? --- .../PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index dade0f4c1a..b2b6813220 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include From 19c8ed3281c27b5e7b83932d9310a2754090a502 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Fri, 24 Apr 2026 14:29:01 -0500 Subject: [PATCH 08/17] Support Shiny Dex Remove testing code --- .../PokemonHome_BoxSorterLivingDex.cpp | 36 ++++++++----------- .../Programs/PokemonHome_BoxSorterLivingDex.h | 1 + 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index b2b6813220..4e0be349cc 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -95,6 +95,11 @@ BoxSorterLivingDex::BoxSorterLivingDex() LockMode::LOCK_WHILE_RUNNING, 1, 1, (uint16_t)MAX_HOME_BOXES ) + , SHINY_DEX( + "Shiny Dex:
Enable or disable shiny dex mode.", + LockMode::LOCK_WHILE_RUNNING, + false + ) , VIDEO_DELAY( "Capture Card Delay:", LockMode::LOCK_WHILE_RUNNING, @@ -124,6 +129,7 @@ BoxSorterLivingDex::BoxSorterLivingDex() PA_ADD_OPTION(LIVING_DEX_START_BOX); PA_ADD_OPTION(REJECT_BOX_START); PA_ADD_OPTION(REJECT_BOX_END); + PA_ADD_OPTION(SHINY_DEX); PA_ADD_OPTION(VIDEO_DELAY); PA_ADD_OPTION(GAME_DELAY); PA_ADD_OPTION(OUTPUT_FILE); @@ -131,7 +137,11 @@ BoxSorterLivingDex::BoxSorterLivingDex() PA_ADD_OPTION(NOTIFICATIONS); } -bool is_viable_for_dex(LivingDexEntry entry, CollectedPokemonInfo pokemonInfo) { +bool is_viable_for_dex(LivingDexEntry entry, CollectedPokemonInfo pokemonInfo, bool shiny_dex) { + if (shiny_dex != pokemonInfo.shiny){ + return false; + } + if (entry.nat_id != pokemonInfo.dex_number) { return false; } @@ -156,25 +166,7 @@ bool is_viable_for_dex(LivingDexEntry entry, CollectedPokemonInfo pokemonInfo) { void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ StartProgramChecks::check_performance_class_wired_or_wireless(context); - ImageFloatBox type_box(0.615, 0.240, 0.071, 0.057); - - for (size_t i = 0; i < 10; i++) - { - auto [t1, t2] = read_pokemon_types( - env.console.video().snapshot(), - type_box, - PokemonTypeGeneration::GEN9 - ); - - env.log("Type read: " + POKEMON_TYPE_SLUGS().get_string(t1), COLOR_CYAN); - env.log("Type read: " + POKEMON_TYPE_SLUGS().get_string(t2), COLOR_CYAN); - pbf_wait(context, 2000ms); - context.wait_for_all_requests(); - } - - - - std::string path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living.json"; + std::string path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living_form_dex_by_id_no_space.json"; JsonValue json = load_json_file(path); const JsonArray& slugs = json.to_array_throw(path); @@ -408,7 +400,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl continue; } - if (!is_viable_for_dex(entry, *living_dex_boxes_data[j])) { + if (!is_viable_for_dex(entry, *living_dex_boxes_data[j], SHINY_DEX)) { continue; } @@ -454,7 +446,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl continue; } - if (!is_viable_for_dex(entry, *reject_boxes_data[j])) { + if (!is_viable_for_dex(entry, *reject_boxes_data[j], SHINY_DEX)) { continue; } diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h index e5c2bed21f..ece0f8bddd 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h @@ -36,6 +36,7 @@ class BoxSorterLivingDex : public SingleSwitchProgramInstance{ SimpleIntegerOption LIVING_DEX_START_BOX; SimpleIntegerOption REJECT_BOX_START; SimpleIntegerOption REJECT_BOX_END; + BooleanCheckBoxOption SHINY_DEX; MillisecondsOption VIDEO_DELAY; MillisecondsOption GAME_DELAY; StringOption OUTPUT_FILE; From 5461db34dcab1ae8192b988f3ce74e6031a26ba8 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Fri, 24 Apr 2026 16:04:06 -0500 Subject: [PATCH 09/17] support dry run. fix swap bug --- .../PokemonHome_BoxSorterLivingDex.cpp | 127 ++++++++++-------- 1 file changed, 68 insertions(+), 59 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index 4e0be349cc..a9cf8e0066 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonValue.h" #include "Common/Cpp/Json/JsonArray.h" @@ -385,11 +386,13 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl video_overlay_set.clear(); } + if (DRY_RUN){ + save_boxes_data_to_json(living_dex_boxes_data, "living_boxes_unsorted.json"); + save_boxes_data_to_json(reject_boxes_data, "reject_boxes_unsorted.json"); + } std::ostringstream ss; - // TODO: implement living dex ordering logic - for (size_t i = 0; i < living_dex_order.size(); i++) { bool found = false; @@ -410,26 +413,28 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl break; } else { - ss << "j: " << j << "i:" << i; - env.console.log(ss.str()); - ss.str(""); + BoxCursor pokemon_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + j / 30, (j / 6) % 5, j % 6); BoxCursor destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); - ss << "Swapping " << living_dex_boxes_data[j] << " at " << pokemon_cursor << " and " << living_dex_boxes_data[i] << " at " << destination_cursor; - env.console.log(ss.str()); - ss.str(""); - nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + if (!DRY_RUN){ + ss << "Swapping " << living_dex_boxes_data[j] << " at " << pokemon_cursor << " and " << living_dex_boxes_data[i] << " at " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); - nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - context.wait_for_all_requests(); + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - std::swap(living_dex_boxes_data[j], living_dex_boxes_data[i]); - stats.swaps++; - env.update_stats(); + context.wait_for_all_requests(); + + stats.swaps++; + env.update_stats(); + } + + std::swap(living_dex_boxes_data[j], living_dex_boxes_data[i]); } break; @@ -454,21 +459,21 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl BoxCursor pokemon_cursor = BoxCursor(REJECT_BOX_START - 1 + j / 30, (j / 6) % 5, j % 6); BoxCursor destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); - ss << "Swapping " << reject_boxes_data[j] << " at " << pokemon_cursor << " and " << living_dex_boxes_data[i] << " at " << destination_cursor; - env.console.log(ss.str()); - ss.str(""); - - nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - - nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - - context.wait_for_all_requests(); + + if (!DRY_RUN){ + ss << "Swapping " << reject_boxes_data[j] << " at " << pokemon_cursor << " and " << living_dex_boxes_data[i] << " at " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + context.wait_for_all_requests(); + stats.swaps++; + env.update_stats(); + } - std::swap(living_dex_boxes_data[j], living_dex_boxes_data[i]); - stats.swaps++; - env.update_stats(); + std::swap(living_dex_boxes_data[i], reject_boxes_data[j]); break; } @@ -515,22 +520,26 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl "ERROR: No empty slots available to move pokemon out of the way, please consider removing some duplicates or increasing the reject box range." ); } - ss << "Moving " << living_dex_boxes_data[i] << " at " << pokemon_cursor << " to " << destination_cursor; - env.console.log(ss.str()); - ss.str(""); - nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - context.wait_for_all_requests(); - if (empty_living_dex_slot != SIZE_MAX) { + + if (!DRY_RUN){ + ss << "Moving " << living_dex_boxes_data[i] << " at " << pokemon_cursor << " to " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + context.wait_for_all_requests(); + stats.swaps++; + env.update_stats(); + } + + if (empty_living_dex_slot != SIZE_MAX){ std::swap(living_dex_boxes_data[i], living_dex_boxes_data[empty_living_dex_slot]); } else { std::swap(living_dex_boxes_data[i], reject_boxes_data[empty_reject_slot]); } - stats.swaps++; - env.update_stats(); } for (size_t i = living_dex_order.size(); i < living_dex_boxes_data.size(); i++) @@ -554,27 +563,27 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl BoxCursor pokemon_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, i % 5, i % 6); BoxCursor destination_cursor = BoxCursor(REJECT_BOX_START - 1 + empty_reject_slot / 30, (empty_reject_slot / 6) % 5, empty_reject_slot % 6); - ss << "Moving " << living_dex_boxes_data[i] << " at " << pokemon_cursor << " to " << destination_cursor; - env.console.log(ss.str()); - ss.str(""); - nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); - pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); - context.wait_for_all_requests(); + if (!DRY_RUN){ + ss << "Moving " << living_dex_boxes_data[i] << " at " << pokemon_cursor << " to " << destination_cursor; + env.console.log(ss.str()); + ss.str(""); + nav_cursor = move_cursor_to(env, context, nav_cursor, pokemon_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + nav_cursor = move_cursor_to(env, context, nav_cursor, destination_cursor, GAME_DELAY); + pbf_press_button(context, BUTTON_Y, 80ms, GAME_DELAY.get() + 240ms); + context.wait_for_all_requests(); + stats.swaps++; + env.update_stats(); + } + std::swap(living_dex_boxes_data[i], reject_boxes_data[empty_reject_slot]); - stats.swaps++; - env.update_stats(); + } - // Loop through living dex order - // Attempt to find pokemon in living_dex_boxes_data then in reject_boxes_data - // If found swap the pokemon into the living dex - // If empty and currently ocupied move to open reject box slot swapping the records - // If no empty reject slots move to the end of the living dex to be moved later when there are open slots - // Add a check to ensure enough reject slots? - - + if (DRY_RUN){ + save_boxes_data_to_json(living_dex_boxes_data, "living_boxes_sorted.json"); + save_boxes_data_to_json(reject_boxes_data, "reject_boxes_sorted.json"); + } send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); } From 200752b33020c5465ac43c4e061c859f0c4e617e Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Fri, 24 Apr 2026 22:32:18 -0500 Subject: [PATCH 10/17] Move duplicate pokemon summary code to shared method. Support gigantamax --- .../PokemonHome_BoxSorterLivingDex.cpp | 293 +++++++----------- .../Programs/PokemonHome_BoxSorterLivingDex.h | 31 ++ 2 files changed, 137 insertions(+), 187 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index a9cf8e0066..03c4eee1f8 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -38,16 +38,6 @@ using namespace Pokemon; const size_t MAX_HOME_BOXES = 200; -struct LivingDexEntry{ - std::string species_slug; - std::string form_slug; - uint16_t nat_id; - PokemonType primary_type; - PokemonType secondary_type; - std::string slug; - bool has_gender_difference; -}; - BoxSorterLivingDex_Descriptor::BoxSorterLivingDex_Descriptor() : SingleSwitchProgramDescriptor( @@ -138,87 +128,57 @@ BoxSorterLivingDex::BoxSorterLivingDex() PA_ADD_OPTION(NOTIFICATIONS); } -bool is_viable_for_dex(LivingDexEntry entry, CollectedPokemonInfo pokemonInfo, bool shiny_dex) { - if (shiny_dex != pokemonInfo.shiny){ +bool BoxSorterLivingDex::is_viable_for_dex(const LivingDexEntry& entry, const CollectedPokemonInfo& pokemonInfo) { + if (SHINY_DEX != pokemonInfo.shiny){ return false; } - if (entry.nat_id != pokemonInfo.dex_number) { + if (entry.nat_id != pokemonInfo.dex_number){ return false; } if (entry.has_gender_difference) { - if (entry.slug.ends_with("-female") && pokemonInfo.gender != StatsHuntGenderFilter::Female) { + if (entry.slug.ends_with("-female") && pokemonInfo.gender != StatsHuntGenderFilter::Female){ return false; } - if (entry.slug.ends_with("-male") && pokemonInfo.gender != StatsHuntGenderFilter::Male) { + if (entry.slug.ends_with("-male") && pokemonInfo.gender != StatsHuntGenderFilter::Male){ return false; } } - if (entry.primary_type != pokemonInfo.primaryType || entry.secondary_type != pokemonInfo.secondaryType) { + if (entry.primary_type != pokemonInfo.primaryType || entry.secondary_type != pokemonInfo.secondaryType){ return false; } - return true; -} - -void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - StartProgramChecks::check_performance_class_wired_or_wireless(context); - - std::string path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living_form_dex_by_id_no_space.json"; - JsonValue json = load_json_file(path); - const JsonArray& slugs = json.to_array_throw(path); - - std::vector living_dex_order; - living_dex_order.reserve(slugs.size()); - for (const JsonValue& entry_value : slugs){ - const JsonObject& obj = entry_value.to_object_throw(path); - LivingDexEntry entry; - entry.species_slug = obj.get_string_throw("species_slug", path); - entry.form_slug = obj.get_string_throw("form_slug", path); - entry.nat_id = (uint16_t)obj.get_integer_throw("natID", path); - entry.primary_type = POKEMON_TYPE_SLUGS().get_enum(obj.get_string_throw("primaryType", path), PokemonType::NONE); - entry.secondary_type = POKEMON_TYPE_SLUGS().get_enum(obj.get_string_default("secondaryType", "none"), PokemonType::NONE); - entry.slug = obj.get_string_throw("slug", path); - entry.has_gender_difference = obj.get_boolean_throw("has_gender_difference", path); - living_dex_order.emplace_back(std::move(entry)); + if (entry.form_slug == "gigantamax" && !pokemonInfo.gmax){ + return false; } - // TODO allow users to have rules for the "best" version of a pokemon to be kept in the living dex box - // It would be nice to add OT as most users that care about living dex also like to have it with their OT - const std::vector sort_preferences( - { - { SortingRuleType::DexNo, false } - }); - - BoxSorterLivingDex_Descriptor::Stats& stats = env.current_stats(); - - // vector that will store data for each slot - std::vector> living_dex_boxes_data; - - VideoOverlaySet video_overlay_set(env.console); - - exit_menus(env, context, VIDEO_DELAY.get()); + return true; +} +[[nodiscard]] BoxCursor BoxSorterLivingDex::populate_box_data( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector sort_preferences, + std::vector>& boxes_data, + size_t box_count, + BoxCursor& nav_cursor +){ BoxCursor dest_cursor; - BoxCursor nav_cursor((LIVING_DEX_START_BOX - 1), 0, 0); - + size_t starting_box(nav_cursor.box); + VideoOverlaySet video_overlay_set(env.console); BoxViewWatcher box_view_watcher(&env.console.overlay()); SummaryScreenWatcher summary_screen_watcher(&env.console.overlay()); - size_t living_dex_box_count = (size_t)std::ceil((double)living_dex_order.size() / 30); - - //cycle through each living dex box - for (size_t box_idx = 0; box_idx < living_dex_box_count; box_idx++) { - if (box_idx != 0) { - dest_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + box_idx, 0, 0); + for (size_t box_idx = 0; box_idx < box_count; box_idx++){ + if (box_idx != 0){ + dest_cursor = BoxCursor(starting_box + box_idx, 0, 0); nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); - } - else { - // Moving the cursor until it goes to the first slot - if (!go_to_first_slot(env, context, VIDEO_DELAY)) { + } else{ + env.log("Moving the cursor to the first slot on the page."); + if (!go_to_first_slot(env, context, VIDEO_DELAY)){ throw UserSetupError( env.logger(), "ERROR: Could not move cursor to the first slot, please consider adjusting delay." @@ -227,18 +187,17 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl } context.wait_for_all_requests(); - const std::string log_msg = "Checking box " + std::to_string(box_idx + 1) + "/" + std::to_string(living_dex_box_count); + const std::string log_msg = "Checking box " + std::to_string(box_idx + 1) + "/" + std::to_string(box_count); env.log(log_msg); env.console.overlay().add_log(log_msg); - - // Check box grid color stddev to find occupied slots and fill living_dex_boxes_data with placeholder pokemon info. - const std::array first_pokemon_slot = find_occupied_slots_in_box(env, context, living_dex_boxes_data, sort_preferences); + // Check box grid color stddev to find occupied slots and fill boxes_data with placeholder pokemon info. + const std::array first_pokemon_slot = find_occupied_slots_in_box(env, context, boxes_data, sort_preferences); //enter the summary screen to read pokemon info - if (first_pokemon_slot[0] != SIZE_MAX) { + if (first_pokemon_slot[0] != SIZE_MAX){ // moving cursor to the first pokemon slot - dest_cursor = { LIVING_DEX_START_BOX - 1 + box_idx, first_pokemon_slot[0], first_pokemon_slot[1] }; + dest_cursor = { starting_box + box_idx, first_pokemon_slot[0], first_pokemon_slot[1] }; nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); env.add_overlay_log("Checking Summary..."); @@ -250,32 +209,31 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl pbf_press_button(context, BUTTON_A, 80ms, 100ms); context.wait_for_all_requests(); int ret = wait_until(env.console, context, Seconds(5), { summary_screen_watcher }); - if (ret != 0) { + if (ret != 0){ OperationFailedException::fire( ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find summary screen after 5 sec", env.console ); } - // cycle through each summary of the current box and fill pokemon information - for (size_t row = 0; row < BOX_ROWS; row++) { - for (size_t column = 0; column < BOX_COLS; column++) { + for (size_t row = 0; row < BOX_ROWS; row++){ + for (size_t column = 0; column < BOX_COLS; column++){ const size_t global_idx = to_global_index(box_idx, row, column); - if (!living_dex_boxes_data[global_idx].has_value()) { + if (!boxes_data[global_idx].has_value()){ continue; } - // Read the summary screen and assign data to living_dex_boxes_data[global_idx] - read_summary_screen(env, context, living_dex_boxes_data[global_idx].value()); + // Read the summary screen and assign data to boxes_data[global_idx] + read_summary_screen(env, context, boxes_data[global_idx].value()); } } // log detailed pokemon infomation of this box std::ostringstream ss; ss << std::endl; - for (size_t row = 0; row < BOX_ROWS; row++) { - for (size_t column = 0; column < BOX_COLS; column++) { - ss << "[" << row << ", " << column << "]: " << living_dex_boxes_data[to_global_index(box_idx, row, column)] << std::endl; + for (size_t row = 0; row < BOX_ROWS; row++){ + for (size_t column = 0; column < BOX_COLS; column++){ + ss << "[" << row << ", " << column << "]: " << boxes_data[to_global_index(box_idx, row, column)] << std::endl; } } env.log(ss.str()); @@ -284,9 +242,9 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl pbf_press_button(context, BUTTON_B, 80ms, 100ms); context.wait_for_all_requests(); ret = wait_until(env.console, context, Seconds(5), { box_view_watcher }); - if (ret != 0) { + if (ret != 0){ OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find box view after 5 sec", env.console + ErrorReport::SEND_ERROR_REPORT, "BoxSorterLivingDex(): does not find box view after 5 sec", env.console ); } video_overlay_set.clear(); @@ -294,97 +252,60 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl video_overlay_set.clear(); } - dest_cursor = BoxCursor((REJECT_BOX_START - 1), 0, 0); - nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); - context.wait_for_all_requests(); - - std::vector> reject_boxes_data; - size_t reject_box_count = REJECT_BOX_END - REJECT_BOX_START + 1; + return nav_cursor; +} - //cycle through each reject box - for (size_t box_idx = 0; box_idx < reject_box_count; box_idx++) { - if (box_idx != 0) { - dest_cursor = BoxCursor(REJECT_BOX_START - 1 + box_idx, 0, 0); - nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); - } - else { - // Moving the cursor until it goes to the first slot - if (!go_to_first_slot(env, context, VIDEO_DELAY)) { - throw UserSetupError( - env.logger(), - "ERROR: Could not move cursor to the first slot, please consider adjusting delay." - ); - } - } +void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + StartProgramChecks::check_performance_class_wired_or_wireless(context); - context.wait_for_all_requests(); - const std::string log_msg = "Checking box " + std::to_string(box_idx + 1) + "/" + std::to_string(reject_box_count); - env.log(log_msg); - env.console.overlay().add_log(log_msg); + std::string path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living_form_dex_by_id_no_space.json"; + JsonValue json = load_json_file(path); + const JsonArray& slugs = json.to_array_throw(path); + std::vector living_dex_order; + living_dex_order.reserve(slugs.size()); + for (const JsonValue& entry_value : slugs){ + const JsonObject& obj = entry_value.to_object_throw(path); + LivingDexEntry entry; + entry.species_slug = obj.get_string_throw("species_slug", path); + entry.form_slug = obj.get_string_throw("form_slug", path); + entry.nat_id = (uint16_t)obj.get_integer_throw("natID", path); + entry.primary_type = POKEMON_TYPE_SLUGS().get_enum(obj.get_string_throw("primaryType", path), PokemonType::NONE); + entry.secondary_type = POKEMON_TYPE_SLUGS().get_enum(obj.get_string_default("secondaryType", "none"), PokemonType::NONE); + entry.slug = obj.get_string_throw("slug", path); + entry.has_gender_difference = obj.get_boolean_throw("has_gender_difference", path); + living_dex_order.emplace_back(std::move(entry)); + } - // Check box grid color stddev to find occupied slots and fill reject_boxes_data with placeholder pokemon info. - const std::array first_pokemon_slot = find_occupied_slots_in_box(env, context, reject_boxes_data, sort_preferences); + // TODO allow users to have rules for the "best" version of a pokemon to be kept in the living dex box + // It would be nice to add OT as most users that care about living dex also like to have it with their OT + const std::vector sort_preferences( + { + { SortingRuleType::DexNo, false } + }); - //enter the summary screen to read pokemon info - if (first_pokemon_slot[0] != SIZE_MAX) { - // moving cursor to the first pokemon slot - dest_cursor = { REJECT_BOX_START - 1 + box_idx, first_pokemon_slot[0], first_pokemon_slot[1] }; - nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + BoxSorterLivingDex_Descriptor::Stats& stats = env.current_stats(); - env.add_overlay_log("Checking Summary..."); + // vector that will store data for each slot + std::vector> living_dex_boxes_data; - pbf_press_button(context, BUTTON_A, 80ms, GAME_DELAY); - context.wait_for_all_requests(); - video_overlay_set.clear(); - pbf_press_dpad(context, DPAD_DOWN, 80ms, GAME_DELAY); - pbf_press_button(context, BUTTON_A, 80ms, 100ms); - context.wait_for_all_requests(); - int ret = wait_until(env.console, context, Seconds(5), { summary_screen_watcher }); - if (ret != 0) { - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find summary screen after 5 sec", env.console - ); - } + exit_menus(env, context, VIDEO_DELAY.get()); + BoxCursor dest_cursor; + BoxCursor nav_cursor((LIVING_DEX_START_BOX - 1), 0, 0); - // cycle through each summary of the current box and fill pokemon information - for (size_t row = 0; row < BOX_ROWS; row++) { - for (size_t column = 0; column < BOX_COLS; column++) { - const size_t global_idx = to_global_index(box_idx, row, column); - if (!reject_boxes_data[global_idx].has_value()) { - continue; - } + size_t living_dex_box_count = (size_t)std::ceil((double)living_dex_order.size() / 30); - // Read the summary screen and assign data to reject_boxes_data[global_idx] - read_summary_screen(env, context, reject_boxes_data[global_idx].value()); - } - } + nav_cursor = populate_box_data(env, context, sort_preferences, living_dex_boxes_data, living_dex_box_count, nav_cursor); - // log detailed pokemon infomation of this box - std::ostringstream ss; - ss << std::endl; - for (size_t row = 0; row < BOX_ROWS; row++) { - for (size_t column = 0; column < BOX_COLS; column++) { - ss << "[" << row << ", " << column << "]: " << reject_boxes_data[to_global_index(box_idx, row, column)] << std::endl; - } - } - env.log(ss.str()); + dest_cursor = BoxCursor((REJECT_BOX_START - 1), 0, 0); + nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); + context.wait_for_all_requests(); - //get out of summary with a lot of delay because it's slow for some reasons - pbf_press_button(context, BUTTON_B, 80ms, 100ms); - context.wait_for_all_requests(); - ret = wait_until(env.console, context, Seconds(5), { box_view_watcher }); - if (ret != 0) { - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, "HomeBoxSorter(): does not find box view after 5 sec", env.console - ); - } - video_overlay_set.clear(); - } + std::vector> reject_boxes_data; + size_t reject_box_count = REJECT_BOX_END - REJECT_BOX_START + 1; - video_overlay_set.clear(); - } + nav_cursor = populate_box_data(env, context, sort_preferences, reject_boxes_data, reject_box_count, nav_cursor); if (DRY_RUN){ save_boxes_data_to_json(living_dex_boxes_data, "living_boxes_unsorted.json"); @@ -393,26 +314,25 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl std::ostringstream ss; - for (size_t i = 0; i < living_dex_order.size(); i++) - { + for (size_t i = 0; i < living_dex_order.size(); i++){ bool found = false; LivingDexEntry entry = living_dex_order[i]; env.log("Looking for " + entry.slug + " in the living dex boxes."); for(size_t j = i; j < living_dex_boxes_data.size(); j++){ - if (!living_dex_boxes_data[j].has_value()) { + if (!living_dex_boxes_data[j].has_value()){ continue; } - if (!is_viable_for_dex(entry, *living_dex_boxes_data[j], SHINY_DEX)) { + if (!is_viable_for_dex(entry, *living_dex_boxes_data[j])){ continue; } found = true; - if (i == j) { + if (i == j){ // already in the right spot, do nothing break; } - else { + else{ BoxCursor pokemon_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + j / 30, (j / 6) % 5, j % 6); BoxCursor destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); @@ -446,12 +366,12 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl env.log("Could not find a pokemon for " + entry.slug + " in the living dex boxes, looking in reject boxes for a valid match."); - for (size_t j = 0; j < reject_boxes_data.size(); j++) { + for (size_t j = 0; j < reject_boxes_data.size(); j++){ if (!reject_boxes_data[j].has_value()) { continue; } - if (!is_viable_for_dex(entry, *reject_boxes_data[j], SHINY_DEX)) { + if (!is_viable_for_dex(entry, *reject_boxes_data[j])){ continue; } @@ -478,28 +398,28 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl break; } - if (found) { + if (found){ continue; } - if (!living_dex_boxes_data[i].has_value()) { + if (!living_dex_boxes_data[i].has_value()){ continue; } // move the pokemon in the current living dex slot to the reject box or later in the living dex size_t empty_reject_slot = SIZE_MAX; size_t empty_living_dex_slot = SIZE_MAX; - for (size_t j = 0; j < reject_boxes_data.size(); j++) { - if (!reject_boxes_data[j].has_value()) { + for (size_t j = 0; j < reject_boxes_data.size(); j++){ + if (!reject_boxes_data[j].has_value()){ empty_reject_slot = j; break; } } - if (empty_reject_slot == SIZE_MAX) { + if (empty_reject_slot == SIZE_MAX){ env.log("No empty reject box slots available to move pokemon out of the way, placing in open slot in living dex for now."); - for (size_t j = i + 1; j < living_dex_boxes_data.size(); j++) { - if (!living_dex_boxes_data[j].has_value()) { + for (size_t j = i + 1; j < living_dex_boxes_data.size(); j++){ + if (!living_dex_boxes_data[j].has_value()){ empty_living_dex_slot = j; break; } @@ -508,13 +428,13 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl BoxCursor pokemon_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + i / 30, (i / 6) % 5, i % 6); BoxCursor destination_cursor; - if (empty_reject_slot != SIZE_MAX) { + if (empty_reject_slot != SIZE_MAX){ destination_cursor = BoxCursor(REJECT_BOX_START - 1 + empty_reject_slot / 30, (empty_reject_slot / 6) % 5, empty_reject_slot % 6); } - else if (empty_living_dex_slot != SIZE_MAX) { + else if (empty_living_dex_slot != SIZE_MAX){ destination_cursor = BoxCursor(LIVING_DEX_START_BOX - 1 + empty_living_dex_slot / 30, (empty_living_dex_slot / 6) % 5, empty_living_dex_slot % 6); } - else { + else{ throw UserSetupError( env.logger(), "ERROR: No empty slots available to move pokemon out of the way, please consider removing some duplicates or increasing the reject box range." @@ -537,26 +457,25 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl if (empty_living_dex_slot != SIZE_MAX){ std::swap(living_dex_boxes_data[i], living_dex_boxes_data[empty_living_dex_slot]); } - else { + else{ std::swap(living_dex_boxes_data[i], reject_boxes_data[empty_reject_slot]); } } - for (size_t i = living_dex_order.size(); i < living_dex_boxes_data.size(); i++) - { - if (!living_dex_boxes_data[i].has_value()) { + for (size_t i = living_dex_order.size(); i < living_dex_boxes_data.size(); i++){ + if (!living_dex_boxes_data[i].has_value()){ continue; } size_t empty_reject_slot = SIZE_MAX; - for (size_t j = 0; j < reject_boxes_data.size(); j++) { - if (!reject_boxes_data[j].has_value()) { + for (size_t j = 0; j < reject_boxes_data.size(); j++){ + if (!reject_boxes_data[j].has_value()){ empty_reject_slot = j; break; } } - if (empty_reject_slot == SIZE_MAX) { + if (empty_reject_slot == SIZE_MAX){ break; } diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h index ece0f8bddd..b8b240311b 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h @@ -13,10 +13,14 @@ #include "Common/Cpp/Options/TimeDurationOption.h" #include "CommonFramework/Notifications/EventNotificationsTable.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Pokemon_CollectedPokemonInfo.h" +#include "Pokemon/Pokemon_Types.h" +#include "Pokemon/Pokemon_BoxCursor.h" namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonHome{ +using namespace Pokemon; class BoxSorterLivingDex_Descriptor : public SingleSwitchProgramDescriptor{ public: @@ -33,6 +37,33 @@ class BoxSorterLivingDex : public SingleSwitchProgramInstance{ virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; private: + struct LivingDexEntry{ + std::string species_slug; + std::string form_slug; + uint16_t nat_id; + PokemonType primary_type; + PokemonType secondary_type; + std::string slug; + bool has_gender_difference; + }; + + // Checks if the given pokemon is viable for the living dex entry. + // Currently the checks include: + // National Dex Id, Types (for regional forms), Gender (if no gender difference either is fine) + // Shiny (if the user has selected to only include shinies in the living dex). + bool is_viable_for_dex(const LivingDexEntry& entry, const CollectedPokemonInfo& pokemonInfo); + + // Goes through the summary screen of each pokemon in the range of boxes. + // Populates the boxes_data vector with the pokemon info. Returns the final cursor position after reading the boxes. + [[nodiscard]] BoxCursor populate_box_data( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector sort_preferences, + std::vector>& boxes_data, + size_t box_count, + BoxCursor& nav_cursor + ); + SimpleIntegerOption LIVING_DEX_START_BOX; SimpleIntegerOption REJECT_BOX_START; SimpleIntegerOption REJECT_BOX_END; From eb8eb08513e5bd803a061959ae050084e7ef1fae Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Fri, 1 May 2026 20:57:10 -0500 Subject: [PATCH 11/17] Fix merge issues --- .../Programs/PokemonHome_BoxNavigation.cpp | 22 +-- .../Programs/PokemonHome_BoxSorter.cpp | 159 ------------------ .../PokemonHome_BoxSorterLivingDex.cpp | 2 +- 3 files changed, 12 insertions(+), 171 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp index c49cbeea4e..96fcfc67b7 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp @@ -23,6 +23,8 @@ #include "Pokemon/Inference/Pokemon_TypeReader.h" #include "PokemonHome/Inference/PokemonHome_BallReader.h" #include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h" +#include "PokemonHome/Inference/PokemonHome_GigantamaxDetector.h" +#include "PokemonHome/Inference/PokemonHome_TeraTypeReader.h" #include "PokemonHome_BoxNavigation.h" namespace PokemonAutomation{ @@ -221,9 +223,7 @@ void read_summary_screen( ImageFloatBox national_dex_number_box(0.448, 0.245, 0.049, 0.04); //pokemon national dex number pos ImageFloatBox shiny_symbol_box(0.702, 0.09, 0.04, 0.06); // shiny symbol pos - // TODO: gmax symbol is at the same location as Tera type symbol! Need better detection to tell apart - // gmax symbol and tera types - ImageFloatBox gmax_symbol_box(0.463, 0.09, 0.04, 0.06); // gmax symbol pos + ImageFloatBox gmax_tera_symbol_box(0.463, 0.09, 0.04, 0.06); // gmax OR tera symbol pos ImageFloatBox origin_symbol_box(0.623, 0.095, 0.033, 0.05); // origin symbol pos ImageFloatBox pokemon_box(0.69, 0.18, 0.28, 0.46); // pokemon render pos ImageFloatBox level_box(0.546, 0.099, 0.044, 0.041); // Level @@ -237,7 +237,7 @@ void read_summary_screen( video_overlay_set.add(COLOR_WHITE, national_dex_number_box); video_overlay_set.add(COLOR_BLUE, shiny_symbol_box); - video_overlay_set.add(COLOR_RED, gmax_symbol_box); + video_overlay_set.add(COLOR_RED, gmax_tera_symbol_box); video_overlay_set.add(COLOR_RED, alpha_box); video_overlay_set.add(COLOR_DARKGREEN, origin_symbol_box); video_overlay_set.add(COLOR_DARK_BLUE, pokemon_box); @@ -273,10 +273,10 @@ void read_summary_screen( cur_pokemon_info.shiny = is_shiny; env.console.log("Shiny detection stddev:" + std::to_string(shiny_stddev_value) + " is shiny:" + std::to_string(is_shiny)); - const int gmax_stddev_value = (int)image_stddev(extract_box_reference(screen, gmax_symbol_box)).sum(); - const bool is_gmax = gmax_stddev_value > 30; - cur_pokemon_info.gmax = is_gmax; - env.console.log("Gmax detection stddev:" + std::to_string(gmax_stddev_value) + " is gmax:" + std::to_string(is_gmax)); + GigantamaxDetector gmax_detector(COLOR_RED, &env.console.overlay(), gmax_tera_symbol_box); + cur_pokemon_info.gmax = gmax_detector.detect(screen); + + cur_pokemon_info.tera_type = read_pokemon_tera_type(screen, gmax_tera_symbol_box); const int alpha_stddev_value = (int)image_stddev(extract_box_reference(screen, alpha_box)).sum(); const bool is_alpha = alpha_stddev_value > 40; @@ -296,10 +296,10 @@ void read_summary_screen( } cur_pokemon_info.ot_id = ot_id; - auto [primaryType, secondaryType] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); + auto [primary_type, secondary_type] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); - cur_pokemon_info.primaryType = primaryType; - cur_pokemon_info.secondaryType = secondaryType; + cur_pokemon_info.primary_type = primary_type; + cur_pokemon_info.secondary_type = secondary_type; env.add_overlay_log(create_overlay_info(cur_pokemon_info)); video_overlay_set.clear(); diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp index 64f6a16210..d9a8d7151b 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -187,165 +187,6 @@ void sort( } } -// Read current screen to find occupied and empty slots in the box. -// Add a placeholder value for each slot in order into `boxes_data`. For empty slot the value is just std::nullopt, while -// for the occupied slot it is an empty pokemon struct `CollectedPokemonInfo`. -// Return the (row, col) index of the first pokemon (aka non-empty) slot in the box. -std::array find_occupied_slots_in_box( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - std::vector>& boxes_data, - const std::vector& sort_preferences) -{ - BoxSorter_Descriptor::Stats& stats = env.current_stats< BoxSorter_Descriptor::Stats>(); - - VideoSnapshot screen = env.console.video().snapshot(); - - std::ostringstream ss; - ss << "\n"; - - std::array first_pokemon_slot = {SIZE_MAX, SIZE_MAX}; - - int num_empty_slots = 0; - for (size_t row = 0; row < BOX_ROWS; row++){ - for (size_t col = 0; col < BOX_COLS; col++){ - ImageFloatBox slot_box(0.06 + (0.072 * col), 0.2 + (0.1035 * row), 0.03, 0.057); - int current_box_value = (int)image_stddev(extract_box_reference(screen, slot_box)).sum(); - - ss << current_box_value; - - //checking color to know if a pokemon is on the slot or not - if(current_box_value < 10){ - stats.empty++; - num_empty_slots++; - boxes_data.push_back(std::nullopt); //empty optional to make sorting easier later - ss << "\u274c " ; // "X" - }else{ - if(first_pokemon_slot[0] == SIZE_MAX){ - first_pokemon_slot = {row, col}; - } - stats.pkmn++; - boxes_data.push_back( - CollectedPokemonInfo{ - .preferences = &sort_preferences - } - ); //default initialised pokemon to know there is a pokemon here that needs a value - ss << "\u2705 " ; // checkbox - } - } - ss << "\n"; - } - - env.update_stats(); - env.log(ss.str()); - env.add_overlay_log("Empty: " + std::to_string(num_empty_slots) + "/30"); - - return first_pokemon_slot; -} - -// Read the current summary screen and assign various pokemon info into `cur_pokemon-info` -void read_summary_screen( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, - CollectedPokemonInfo& cur_pokemon_info -){ - VideoOverlaySet video_overlay_set(env.console); - - ImageFloatBox national_dex_number_box(0.448, 0.245, 0.049, 0.04); //pokemon national dex number pos - ImageFloatBox shiny_symbol_box(0.702, 0.09, 0.04, 0.06); // shiny symbol pos - // TODO: gmax symbol is at the same location as Tera type symbol! Need better detection to tell apart - // gmax symbol and tera types - ImageFloatBox gmax_symbol_box(0.463, 0.09, 0.04, 0.06); // gmax symbol pos - ImageFloatBox origin_symbol_box(0.623, 0.095, 0.033, 0.05); // origin symbol pos - ImageFloatBox pokemon_box(0.69, 0.18, 0.28, 0.46); // pokemon render pos - ImageFloatBox level_box(0.546, 0.099, 0.044, 0.041); // Level - ImageFloatBox ot_id_box(0.782, 0.719, 0.193, 0.046); // OT ID - ImageFloatBox ot_box(0.492, 0.719, 0.165, 0.049); // OT - ImageFloatBox nature_box(0.157, 0.783, 0.212, 0.042); // Nature - ImageFloatBox ability_box(0.158, 0.838, 0.213, 0.042); // Ability - ImageFloatBox alpha_box(0.787, 0.095, 0.024, 0.046); // Alpha symbol - ImageFloatBox type_box(0.615, 0.240, 0.071, 0.057); // Type symbols - - - video_overlay_set.add(COLOR_WHITE, national_dex_number_box); - video_overlay_set.add(COLOR_BLUE, shiny_symbol_box); - video_overlay_set.add(COLOR_RED, gmax_symbol_box); - video_overlay_set.add(COLOR_RED, alpha_box); - video_overlay_set.add(COLOR_DARKGREEN, origin_symbol_box); - video_overlay_set.add(COLOR_DARK_BLUE, pokemon_box); - video_overlay_set.add(COLOR_RED, level_box); - video_overlay_set.add(COLOR_RED, ot_id_box); - video_overlay_set.add(COLOR_RED, ot_box); - video_overlay_set.add(COLOR_RED, nature_box); - video_overlay_set.add(COLOR_RED, ability_box); - BoxGenderDetector::make_overlays(video_overlay_set); - - - // Wait for the summary screen transition to end - FrozenImageDetector frozen_image_detector(COLOR_GREEN, {0.388, 0.238, 0.109, 0.062}, Milliseconds(80), 20); - frozen_image_detector.make_overlays(video_overlay_set); - wait_until(env.console, context, 5s, {frozen_image_detector}); - - VideoSnapshot screen = env.console.video().snapshot(); - - const int dex_number = OCR::read_number_waterfill(env.console, extract_box_reference(screen, national_dex_number_box), 0xff808080, 0xffffffff); - if (dex_number <= 0 || dex_number > static_cast(NATIONAL_DEX_SLUGS().size())){ - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, - "BoxSorter Check Summary: Unable to read a correct dex number, found: " + std::to_string(dex_number), - env.console - ); - } - cur_pokemon_info.dex_number = (uint16_t)dex_number; - cur_pokemon_info.name_slug = NATIONAL_DEX_SLUGS()[dex_number-1]; - - const int shiny_stddev_value = (int)image_stddev(extract_box_reference(screen, shiny_symbol_box)).sum(); - const bool is_shiny = shiny_stddev_value > 30; - cur_pokemon_info.shiny = is_shiny; - env.console.log("Shiny detection stddev:" + std::to_string(shiny_stddev_value) + " is shiny:" + std::to_string(is_shiny)); - - const int gmax_stddev_value = (int)image_stddev(extract_box_reference(screen, gmax_symbol_box)).sum(); - const bool is_gmax = gmax_stddev_value > 30; - cur_pokemon_info.gmax = is_gmax; - env.console.log("Gmax detection stddev:" + std::to_string(gmax_stddev_value) + " is gmax:" + std::to_string(is_gmax)); - - const int alpha_stddev_value = (int)image_stddev(extract_box_reference(screen, alpha_box)).sum(); - const bool is_alpha = alpha_stddev_value > 40; - cur_pokemon_info.alpha = is_alpha; - env.console.log("Alpha detection stddev:" + std::to_string(alpha_stddev_value) + " is alpha:" + std::to_string(is_alpha)); - - BallReader ball_reader(env.console); - cur_pokemon_info.ball_slug = ball_reader.read_ball(screen); - - const StatsHuntGenderFilter gender = BoxGenderDetector::detect(screen); - env.console.log("Gender: " + gender_to_string(gender), COLOR_GREEN); - cur_pokemon_info.gender = gender; - - const int ot_id = OCR::read_number_waterfill(env.console, extract_box_reference(screen, ot_id_box), 0xff808080, 0xffffffff); - if (ot_id < 0 || ot_id > 999'999){ - dump_image(env.console, ProgramInfo(), "ReadSummary_OT", screen); - } - cur_pokemon_info.ot_id = ot_id; - - auto [primaryType, secondaryType] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); - - cur_pokemon_info.primaryType = primaryType; - cur_pokemon_info.secondaryType = secondaryType; - - env.add_overlay_log(create_overlay_info(cur_pokemon_info)); - video_overlay_set.clear(); - - // NOTE edit when adding new struct members (detections go here likely) - - // level_box - // ot_box - // nature_box - // ability_box - - // Press button R to go to next summary screen - pbf_press_button(context, BUTTON_R, 80ms, 300ms); - context.wait_for_all_requests(); -} - void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ StartProgramChecks::check_performance_class_wired_or_wireless(context); diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index 03c4eee1f8..1d86b2ceb9 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -147,7 +147,7 @@ bool BoxSorterLivingDex::is_viable_for_dex(const LivingDexEntry& entry, const Co } } - if (entry.primary_type != pokemonInfo.primaryType || entry.secondary_type != pokemonInfo.secondaryType){ + if (entry.primary_type != pokemonInfo.primary_type || entry.secondary_type != pokemonInfo.secondary_type){ return false; } From 77cf1da133ec3de4f8b53fa5fa37a46c72311c60 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 3 May 2026 09:36:01 -0500 Subject: [PATCH 12/17] Reading OT Name --- .../Pokemon/Pokemon_CollectedPokemonInfo.cpp | 3 ++ .../Pokemon/Pokemon_CollectedPokemonInfo.h | 1 + .../Programs/PokemonHome_BoxNavigation.cpp | 35 +++++++++++++++++-- .../Programs/PokemonHome_BoxNavigation.h | 3 +- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp index e225914298..4116a6aec2 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.cpp @@ -27,6 +27,7 @@ bool operator==(const CollectedPokemonInfo& lhs, const CollectedPokemonInfo& rhs lhs.ball_slug == rhs.ball_slug && lhs.gender == rhs.gender && lhs.ot_id == rhs.ot_id && + lhs.ot_name == rhs.ot_name && lhs.primary_type == rhs.primary_type && lhs.secondary_type == rhs.secondary_type && lhs.tera_type == rhs.tera_type; @@ -109,6 +110,7 @@ std::ostream& operator<<(std::ostream& os, const std::optionalball_slug << " "; os << "gender:" << gender_to_string(pokemon->gender) << " "; os << "ot_id:" << pokemon->ot_id << " "; + os << "ot_name:" << pokemon->ot_name << " "; os << "primaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->primary_type) << " "; os << "secondaryType:" << POKEMON_TYPE_SLUGS().get_string(pokemon->secondary_type) << " "; os << "teraType:" << POKEMON_TERA_TYPE_SLUGS().get_string(pokemon->tera_type) << " "; @@ -162,6 +164,7 @@ void save_boxes_data_to_json(const std::vectorball_slug; pokemon["gender"] = gender_to_string(current_pokemon->gender); pokemon["ot_id"] = current_pokemon->ot_id; + pokemon["ot_name"] = current_pokemon->ot_name; pokemon["primary_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->primary_type); pokemon["secondary_type"] = POKEMON_TYPE_SLUGS().get_string(current_pokemon->secondary_type); pokemon["tera_type"] = POKEMON_TERA_TYPE_SLUGS().get_string(current_pokemon->tera_type); diff --git a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h index 8e0935e56d..5e2a30c482 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_CollectedPokemonInfo.h @@ -32,6 +32,7 @@ struct CollectedPokemonInfo{ std::string ball_slug = ""; StatsHuntGenderFilter gender = StatsHuntGenderFilter::Genderless; uint32_t ot_id = 0; // original trainer ID + std::string ot_name = ""; // original trainer name PokemonType primary_type = PokemonType::NONE; PokemonType secondary_type = PokemonType::NONE; PokemonTeraType tera_type = PokemonTeraType::NONE; diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp index 96fcfc67b7..900e689fb9 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp @@ -8,10 +8,15 @@ #include #include #include +#include "Common/Cpp/Strings/Unicode.h" #include "CommonFramework/Exceptions/OperationFailedException.h" #include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonFramework/ImageTools/ImageStats.h" #include "CommonFramework/Notifications/ProgramInfo.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/OCR/OCR_RawOCR.h" +#include "CommonTools/OCR/OCR_Routines.h" +#include "CommonTools/OCR/OCR_StringNormalization.h" #include "CommonFramework/Tools/ErrorDumper.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" @@ -217,7 +222,7 @@ std::array find_occupied_slots_in_box( // Read the current summary screen and assign various pokemon info into `cur_pokemon-info` void read_summary_screen( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - CollectedPokemonInfo& cur_pokemon_info + CollectedPokemonInfo& cur_pokemon_info, Language ot_name_language ) { VideoOverlaySet video_overlay_set(env.console); @@ -301,13 +306,39 @@ void read_summary_screen( cur_pokemon_info.primary_type = primary_type; cur_pokemon_info.secondary_type = secondary_type; + if (ot_name_language != Language::None){ + const std::vector& text_filters = OCR::WHITE_TEXT_FILTERS(); + std::vector bw; + bw.reserve(text_filters.size()); + for (const auto& f : text_filters){ + bw.push_back({ true, f.mins, f.maxs }); + } + + auto filtered_images = to_blackwhite_rgb32_range(extract_box_reference(screen, ot_box), bw); + + std::string best_raw; + for (auto& [img, px_count] : filtered_images){ + if (px_count == 0){ + continue; + } + std::string candidate = OCR::ocr_read(ot_name_language, img, OCR::PageSegMode::SINGLE_LINE); + if (!candidate.empty() && best_raw.empty()){ + best_raw = candidate; + } + } + + env.log("Raw trainer name: " + best_raw); + std::string normalized = utf32_to_str(OCR::normalize_utf32(best_raw)); + env.log("Normalized trainer name: " + normalized); + cur_pokemon_info.ot_name = normalized; + } + env.add_overlay_log(create_overlay_info(cur_pokemon_info)); video_overlay_set.clear(); // NOTE edit when adding new struct members (detections go here likely) // level_box - // ot_box // nature_box // ability_box diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h index 0ec54b6ba8..f07b420200 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h @@ -49,7 +49,8 @@ std::array find_occupied_slots_in_box( void read_summary_screen( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - Pokemon::CollectedPokemonInfo& cur_pokemon_info + Pokemon::CollectedPokemonInfo& cur_pokemon_info, + Language ot_name_language = Language::None ); void print_boxes_data( From 9bba9acff01c427c310deebb5fc7cb95d629aefd Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 3 May 2026 09:56:51 -0500 Subject: [PATCH 13/17] Add name input --- .../PokemonHome_BoxSorterLivingDex.cpp | 75 +++++++++++++++++-- .../Programs/PokemonHome_BoxSorterLivingDex.h | 13 +++- 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index 1d86b2ceb9..3c6bc43d81 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -11,6 +11,7 @@ #include #include #include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Strings/Unicode.h" #include "Common/Cpp/Json/JsonValue.h" #include "Common/Cpp/Json/JsonArray.h" #include "Common/Cpp/Json/JsonObject.h" @@ -20,6 +21,7 @@ #include "CommonFramework/ProgramStats/StatsTracking.h" #include "CommonTools/Async/InferenceRoutines.h" #include "CommonTools/StartupChecks/StartProgramChecks.h" +#include "CommonTools/OCR/OCR_StringNormalization.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "Pokemon/Inference/Pokemon_TypeReader.h" @@ -70,6 +72,10 @@ std::unique_ptr BoxSorterLivingDex_Descriptor::make_stats() const{ return std::unique_ptr(new Stats()); } +BoxSorterLivingDex::~BoxSorterLivingDex(){ + OT_NAME_LANGUAGE.remove_listener(*this); +}; + BoxSorterLivingDex::BoxSorterLivingDex() : LIVING_DEX_START_BOX( "Living Dex Starting Box:
Box number where the living dex placement begins (1-indexed).", @@ -91,6 +97,31 @@ BoxSorterLivingDex::BoxSorterLivingDex() LockMode::LOCK_WHILE_RUNNING, false ) + , OT_NAME_LANGUAGE( + "OT Name Language:
Language of the OT name you are filtering for. " + "If set to None, OT names will not be read.", + LanguageSet{ + Language::English, + Language::Japanese, + Language::Spanish, + Language::French, + Language::German, + Language::Italian, + Language::Korean, + Language::ChineseSimplified, + Language::ChineseTraditional, + }, + LockMode::LOCK_WHILE_RUNNING, + false + ) + , OT_NAME( + false, + "OT Name Filter:
Only keep pokemon with this OT name in the living dex. " + "Leave blank to accept any OT name.", + LockMode::LOCK_WHILE_RUNNING, + "", + "Enter OT name here..." + ) , VIDEO_DELAY( "Capture Card Delay:", LockMode::LOCK_WHILE_RUNNING, @@ -121,14 +152,38 @@ BoxSorterLivingDex::BoxSorterLivingDex() PA_ADD_OPTION(REJECT_BOX_START); PA_ADD_OPTION(REJECT_BOX_END); PA_ADD_OPTION(SHINY_DEX); + PA_ADD_OPTION(OT_NAME_LANGUAGE); + PA_ADD_OPTION(OT_NAME); PA_ADD_OPTION(VIDEO_DELAY); PA_ADD_OPTION(GAME_DELAY); PA_ADD_OPTION(OUTPUT_FILE); PA_ADD_OPTION(DRY_RUN); PA_ADD_OPTION(NOTIFICATIONS); + + BoxSorterLivingDex::on_config_value_changed(this); + + OT_NAME_LANGUAGE.add_listener(*this); } -bool BoxSorterLivingDex::is_viable_for_dex(const LivingDexEntry& entry, const CollectedPokemonInfo& pokemonInfo) { +void BoxSorterLivingDex::on_config_value_changed(void* object){ + if (OT_NAME_LANGUAGE == Language::None){ + OT_NAME.set_visibility(ConfigOptionState::HIDDEN); + }else{ + OT_NAME.set_visibility(ConfigOptionState::ENABLED); + } +} + +bool BoxSorterLivingDex::is_viable_for_dex( + const LivingDexEntry& entry, + const CollectedPokemonInfo& pokemonInfo, + const std::string& normalized_ot_name +){ + if (OT_NAME_LANGUAGE != Language::None){ + if (normalized_ot_name != pokemonInfo.ot_name){ + return false; + } + } + if (SHINY_DEX != pokemonInfo.shiny){ return false; } @@ -164,7 +219,8 @@ bool BoxSorterLivingDex::is_viable_for_dex(const LivingDexEntry& entry, const Co std::vector sort_preferences, std::vector>& boxes_data, size_t box_count, - BoxCursor& nav_cursor + BoxCursor& nav_cursor, + Language ot_name_language ){ BoxCursor dest_cursor; size_t starting_box(nav_cursor.box); @@ -224,7 +280,7 @@ bool BoxSorterLivingDex::is_viable_for_dex(const LivingDexEntry& entry, const Co } // Read the summary screen and assign data to boxes_data[global_idx] - read_summary_screen(env, context, boxes_data[global_idx].value()); + read_summary_screen(env, context, boxes_data[global_idx].value(), ot_name_language); } } @@ -277,6 +333,11 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl living_dex_order.emplace_back(std::move(entry)); } + std::string ot_name = static_cast(OT_NAME); + env.log("OT Name user input: " + ot_name); + std::string ot_name_normalized = utf32_to_str(OCR::normalize_utf32(ot_name)); + env.log("OT Name normalized: " + ot_name_normalized); + // TODO allow users to have rules for the "best" version of a pokemon to be kept in the living dex box // It would be nice to add OT as most users that care about living dex also like to have it with their OT const std::vector sort_preferences( @@ -296,7 +357,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl size_t living_dex_box_count = (size_t)std::ceil((double)living_dex_order.size() / 30); - nav_cursor = populate_box_data(env, context, sort_preferences, living_dex_boxes_data, living_dex_box_count, nav_cursor); + nav_cursor = populate_box_data(env, context, sort_preferences, living_dex_boxes_data, living_dex_box_count, nav_cursor, OT_NAME_LANGUAGE); dest_cursor = BoxCursor((REJECT_BOX_START - 1), 0, 0); nav_cursor = move_cursor_to(env, context, nav_cursor, dest_cursor, GAME_DELAY); @@ -305,7 +366,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl std::vector> reject_boxes_data; size_t reject_box_count = REJECT_BOX_END - REJECT_BOX_START + 1; - nav_cursor = populate_box_data(env, context, sort_preferences, reject_boxes_data, reject_box_count, nav_cursor); + nav_cursor = populate_box_data(env, context, sort_preferences, reject_boxes_data, reject_box_count, nav_cursor, OT_NAME_LANGUAGE); if (DRY_RUN){ save_boxes_data_to_json(living_dex_boxes_data, "living_boxes_unsorted.json"); @@ -323,7 +384,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl continue; } - if (!is_viable_for_dex(entry, *living_dex_boxes_data[j])){ + if (!is_viable_for_dex(entry, *living_dex_boxes_data[j], ot_name_normalized)){ continue; } @@ -371,7 +432,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl continue; } - if (!is_viable_for_dex(entry, *reject_boxes_data[j])){ + if (!is_viable_for_dex(entry, *reject_boxes_data[j], ot_name_normalized)){ continue; } diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h index b8b240311b..b238f69951 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h @@ -12,6 +12,7 @@ #include "Common/Cpp/Options/StringOption.h" #include "Common/Cpp/Options/TimeDurationOption.h" #include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "CommonTools/Options/LanguageOCROption.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "Pokemon/Pokemon_CollectedPokemonInfo.h" #include "Pokemon/Pokemon_Types.h" @@ -30,8 +31,9 @@ class BoxSorterLivingDex_Descriptor : public SingleSwitchProgramDescriptor{ virtual std::unique_ptr make_stats() const override; }; -class BoxSorterLivingDex : public SingleSwitchProgramInstance{ +class BoxSorterLivingDex : public SingleSwitchProgramInstance, public ConfigOption::Listener{ public: + ~BoxSorterLivingDex(); BoxSorterLivingDex(); virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; @@ -51,7 +53,7 @@ class BoxSorterLivingDex : public SingleSwitchProgramInstance{ // Currently the checks include: // National Dex Id, Types (for regional forms), Gender (if no gender difference either is fine) // Shiny (if the user has selected to only include shinies in the living dex). - bool is_viable_for_dex(const LivingDexEntry& entry, const CollectedPokemonInfo& pokemonInfo); + bool is_viable_for_dex(const LivingDexEntry& entry, const CollectedPokemonInfo& pokemonInfo, const std::string& normalized_ot_name); // Goes through the summary screen of each pokemon in the range of boxes. // Populates the boxes_data vector with the pokemon info. Returns the final cursor position after reading the boxes. @@ -61,13 +63,18 @@ class BoxSorterLivingDex : public SingleSwitchProgramInstance{ std::vector sort_preferences, std::vector>& boxes_data, size_t box_count, - BoxCursor& nav_cursor + BoxCursor& nav_cursor, + Language ot_name_language ); + virtual void on_config_value_changed(void* object) override; + SimpleIntegerOption LIVING_DEX_START_BOX; SimpleIntegerOption REJECT_BOX_START; SimpleIntegerOption REJECT_BOX_END; BooleanCheckBoxOption SHINY_DEX; + OCR::LanguageOCROption OT_NAME_LANGUAGE; + StringOption OT_NAME; MillisecondsOption VIDEO_DELAY; MillisecondsOption GAME_DELAY; StringOption OUTPUT_FILE; From 7bf8af1679db977c297ac327e5915cadfa56e427 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 3 May 2026 12:24:23 -0500 Subject: [PATCH 14/17] Move behind dev mode --- .../Source/CommonFramework/GlobalSettingsPanel.cpp | 1 + SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp index 0b42b0cc46..a9ccd52285 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp @@ -48,6 +48,7 @@ const std::set TOKENS{ "e8d168bc482e96553ea9f9ecaea5a817474dbccc2a6a228a6bde67f2b2aa2889", // James' token. "7555b7c63481cad42306718c67e7f9def5bfd1da8f6cd299ccd3d7dc95f307ae", // Kuro's token. "3d475b46d121fc24559d100de2426feaa53cd6578aac2817c4857a610ccde2dd", // kichi's token. + "9b41db8175b5f248a78e738c7bd63a36e33b57953cb4e80ccdd13c2a7e892eec", // Dalton's token. }; diff --git a/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp b/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp index de1991c23c..8203746792 100644 --- a/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp +++ b/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp @@ -32,8 +32,9 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back("---- General ----"); ret.emplace_back(make_single_switch_program()); ret.emplace_back(make_single_switch_program()); - ret.emplace_back(make_single_switch_program()); - + if (PreloadSettings::instance().DEVELOPER_MODE){ + ret.emplace_back(make_single_switch_program()); + } // ret.emplace_back("---- Trading ----"); // ret.emplace_back("---- Farming ----"); From 4d56092b0feb698235e748d736881c4451ccd293 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Sun, 3 May 2026 16:50:32 -0500 Subject: [PATCH 15/17] Support different dex styles --- .../PokemonHome_BoxSorterLivingDex.cpp | 27 ++++++++++++++++++- .../Programs/PokemonHome_BoxSorterLivingDex.h | 14 ++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp index 3c6bc43d81..12ba01e773 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -92,6 +92,15 @@ BoxSorterLivingDex::BoxSorterLivingDex() LockMode::LOCK_WHILE_RUNNING, 1, 1, (uint16_t)MAX_HOME_BOXES ) + , LIVING_DEX_STYLE( + "Living Dex Style:
Please see the wiki for a detailed explanation of each style.", + { + {LivingDexStyle::SORTED_BY_FORMS, "sorted_by_forms", "Sorted by Forms"}, + {LivingDexStyle::SORTED_BY_SPECIES, "sorted_by_species", "Sorted by Species"}, + }, + LockMode::LOCK_WHILE_RUNNING, + LivingDexStyle::SORTED_BY_FORMS + ) , SHINY_DEX( "Shiny Dex:
Enable or disable shiny dex mode.", LockMode::LOCK_WHILE_RUNNING, @@ -151,6 +160,7 @@ BoxSorterLivingDex::BoxSorterLivingDex() PA_ADD_OPTION(LIVING_DEX_START_BOX); PA_ADD_OPTION(REJECT_BOX_START); PA_ADD_OPTION(REJECT_BOX_END); + PA_ADD_OPTION(LIVING_DEX_STYLE); PA_ADD_OPTION(SHINY_DEX); PA_ADD_OPTION(OT_NAME_LANGUAGE); PA_ADD_OPTION(OT_NAME); @@ -314,7 +324,21 @@ bool BoxSorterLivingDex::is_viable_for_dex( void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ StartProgramChecks::check_performance_class_wired_or_wireless(context); - std::string path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living_form_dex_by_id_no_space.json"; + std::string path; + + switch (LIVING_DEX_STYLE){ + case LivingDexStyle::SORTED_BY_FORMS: + path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living_dex_by_forms.json"; + break; + case LivingDexStyle::SORTED_BY_SPECIES: + path = RESOURCE_PATH() + "PokemonHome/DexTemplates/living_dex_by_species.json"; + break; + default: + throw InternalProgramError( + &env.logger(), PA_CURRENT_FUNCTION, "Invalid living dex style" + ); + } + JsonValue json = load_json_file(path); const JsonArray& slugs = json.to_array_throw(path); @@ -340,6 +364,7 @@ void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControl // TODO allow users to have rules for the "best" version of a pokemon to be kept in the living dex box // It would be nice to add OT as most users that care about living dex also like to have it with their OT + // Sort preferences isn't really used as the json/options dictate the exact pokemon to be placed in each slot. const std::vector sort_preferences( { { SortingRuleType::DexNo, false } diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h index b238f69951..fcf64c60a2 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h @@ -49,6 +49,12 @@ class BoxSorterLivingDex : public SingleSwitchProgramInstance, public ConfigOpti bool has_gender_difference; }; + + enum class LivingDexStyle{ + SORTED_BY_FORMS, + SORTED_BY_SPECIES + }; + // Checks if the given pokemon is viable for the living dex entry. // Currently the checks include: // National Dex Id, Types (for regional forms), Gender (if no gender difference either is fine) @@ -69,12 +75,20 @@ class BoxSorterLivingDex : public SingleSwitchProgramInstance, public ConfigOpti virtual void on_config_value_changed(void* object) override; + // Working boxes SimpleIntegerOption LIVING_DEX_START_BOX; SimpleIntegerOption REJECT_BOX_START; SimpleIntegerOption REJECT_BOX_END; + + // Living dex style. + EnumDropdownOption LIVING_DEX_STYLE; + + // Dex filters BooleanCheckBoxOption SHINY_DEX; OCR::LanguageOCROption OT_NAME_LANGUAGE; StringOption OT_NAME; + + // Setup options MillisecondsOption VIDEO_DELAY; MillisecondsOption GAME_DELAY; StringOption OUTPUT_FILE; From 5a11d5793c4cfc5f628d667c7f22bac0d36478b4 Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Mon, 4 May 2026 21:33:53 -0500 Subject: [PATCH 16/17] typos --- .../PokemonHome/Programs/PokemonHome_BoxNavigation.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp index 900e689fb9..1221737130 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp @@ -39,7 +39,7 @@ using namespace Pokemon; // Move the red cursor to the first slot of the box -// If the cursor is not add the first slot, move the cursor to the left and up one row at a time until it is at the first slot. +// If the cursor is not at the first slot, move the cursor to the left and up one row at a time until it is at the first slot. bool go_to_first_slot( SingleSwitchProgramEnvironment& env, ProControllerContext& context, @@ -105,7 +105,7 @@ bool go_to_first_slot( ss << "Moving cursor from " << cur_cursor << " to " << dest_cursor; env.console.log(ss.str()); - // TODO: shortest path movement though pages, boxes + // TODO: shortest path movement through pages, boxes for (size_t i = cur_cursor.box; i < dest_cursor.box; ++i) { pbf_press_button(context, BUTTON_R, 80ms, GAME_DELAY + 240ms); } @@ -219,7 +219,7 @@ std::array find_occupied_slots_in_box( return first_pokemon_slot; } -// Read the current summary screen and assign various pokemon info into `cur_pokemon-info` +// Read the current summary screen and assign various pokemon info into `cur_pokemon_info` void read_summary_screen( SingleSwitchProgramEnvironment& env, ProControllerContext& context, CollectedPokemonInfo& cur_pokemon_info, Language ot_name_language From a6925dee50677150c8142312a52790db838a43aa Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Wed, 6 May 2026 07:36:19 -0500 Subject: [PATCH 17/17] typo --- SerialPrograms/cmake/SourceFiles.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index f3f2305004..d279dc6164 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1574,7 +1574,7 @@ file(GLOB LIBRARY_SOURCES Source/PokemonHome/PokemonHome_Settings.cpp Source/PokemonHome/PokemonHome_Settings.h Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp - Source/PokemonHome/Programs/PokemonHome_BoxNavigatino.h + Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp Source/PokemonHome/Programs/PokemonHome_BoxSorter.h Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp