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/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/PokemonHome_Panels.cpp b/SerialPrograms/Source/PokemonHome/PokemonHome_Panels.cpp index 0b03823a4b..8203746792 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,7 +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()); - + if (PreloadSettings::instance().DEVELOPER_MODE){ + ret.emplace_back(make_single_switch_program()); + } // ret.emplace_back("---- Trading ----"); // ret.emplace_back("---- Farming ----"); diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp new file mode 100644 index 0000000000..1221737130 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.cpp @@ -0,0 +1,402 @@ +/* Home Box Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#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" +#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/Inference/PokemonHome_GigantamaxDetector.h" +#include "PokemonHome/Inference/PokemonHome_TeraTypeReader.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 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, + 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 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); + } + 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, Language ot_name_language +) { + 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 + 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 + 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_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); + 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)); + + 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; + 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 [primary_type, secondary_type] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); + + 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 + // 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..f07b420200 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxNavigation.h @@ -0,0 +1,66 @@ +/* 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, + Language ot_name_language = Language::None +); + +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 72e32d83f5..d9a8d7151b 100644 --- a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorter.cpp @@ -30,28 +30,17 @@ 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/Inference/PokemonHome_GigantamaxDetector.h" -#include "PokemonHome/Inference/PokemonHome_TeraTypeReader.h" +#include "PokemonHome_BoxNavigation.h" #include "PokemonHome_BoxSorter.h" namespace PokemonAutomation{ @@ -139,136 +128,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, @@ -328,168 +187,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 - 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 - 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_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); - 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)); - - GigantamaxDetector gmax_detector(COLOR_RED, &env.console.overlay(), gmax_tera_symbol_box); - bool is_gmax = gmax_detector.detect(screen); - cur_pokemon_info.gmax = is_gmax; - - PokemonTeraType tera_type = read_pokemon_tera_type(screen, gmax_tera_symbol_box); - cur_pokemon_info.tera_type = tera_type; - - 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 [primary_type, secondary_type] = read_pokemon_types(screen, type_box, PokemonTypeGeneration::GEN9); - - 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(); - - // 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!"); @@ -497,56 +197,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()); @@ -596,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++){ @@ -664,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 new file mode 100644 index 0000000000..12ba01e773 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.cpp @@ -0,0 +1,598 @@ +/* Home Box Sorter Living Dex + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#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" +#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 "CommonTools/OCR/OCR_StringNormalization.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.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_BoxNavigation.h" +#include "PokemonHome_BoxSorterLivingDex.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonHome{ +using namespace Pokemon; + + +const size_t MAX_HOME_BOXES = 200; + + +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(){ + 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).", + 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 + ) + , 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, + 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, + "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(LIVING_DEX_STYLE); + 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); +} + +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; + } + + 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.primary_type || entry.secondary_type != pokemonInfo.secondary_type){ + return false; + } + + if (entry.form_slug == "gigantamax" && !pokemonInfo.gmax){ + return false; + } + + 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, + Language ot_name_language +){ + BoxCursor dest_cursor; + 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()); + + 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{ + 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." + ); + } + } + + context.wait_for_all_requests(); + 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 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){ + // moving cursor to the first pokemon slot + 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..."); + + 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 (!boxes_data[global_idx].has_value()){ + continue; + } + + // Read the summary screen and assign data to boxes_data[global_idx] + read_summary_screen(env, context, boxes_data[global_idx].value(), ot_name_language); + } + } + + // 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 << "]: " << 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, "BoxSorterLivingDex(): does not find box view after 5 sec", env.console + ); + } + video_overlay_set.clear(); + } + video_overlay_set.clear(); + } + + return nav_cursor; +} + +void BoxSorterLivingDex::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + StartProgramChecks::check_performance_class_wired_or_wireless(context); + + 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); + + 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)); + } + + 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 + // 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 } + }); + + BoxSorterLivingDex_Descriptor::Stats& stats = env.current_stats(); + + // vector that will store data for each slot + std::vector> living_dex_boxes_data; + + exit_menus(env, context, VIDEO_DELAY.get()); + + BoxCursor dest_cursor; + BoxCursor nav_cursor((LIVING_DEX_START_BOX - 1), 0, 0); + + 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, 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); + context.wait_for_all_requests(); + + 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, OT_NAME_LANGUAGE); + + 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; + + 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], ot_name_normalized)){ + continue; + } + + found = true; + if (i == j){ + // already in the right spot, do nothing + break; + } + 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); + + 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, 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]); + } + + 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], ot_name_normalized)){ + 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); + + 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[i], reject_boxes_data[j]); + + 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." + ); + } + + 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]); + } + } + + 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); + + 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]); + + } + + 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); +} + +} +} +} diff --git a/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h new file mode 100644 index 0000000000..fcf64c60a2 --- /dev/null +++ b/SerialPrograms/Source/PokemonHome/Programs/PokemonHome_BoxSorterLivingDex.h @@ -0,0 +1,102 @@ +/* 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 "CommonTools/Options/LanguageOCROption.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: + BoxSorterLivingDex_Descriptor(); + + struct Stats; + virtual std::unique_ptr make_stats() const override; +}; + +class BoxSorterLivingDex : public SingleSwitchProgramInstance, public ConfigOption::Listener{ +public: + ~BoxSorterLivingDex(); + BoxSorterLivingDex(); + + 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; + }; + + + 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) + // 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, 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. + [[nodiscard]] BoxCursor populate_box_data( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + std::vector sort_preferences, + std::vector>& boxes_data, + size_t box_count, + BoxCursor& nav_cursor, + Language ot_name_language + ); + + 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; + BooleanCheckBoxOption DRY_RUN; + EventNotificationsOption NOTIFICATIONS; +}; + +} +} +} +#endif // PokemonAutomation_PokemonHome_BoxSorterLivingDex_H diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 16fa053fcf..d279dc6164 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1573,8 +1573,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_BoxNavigation.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