diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.cpp new file mode 100644 index 0000000000..52e40624a8 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.cpp @@ -0,0 +1,140 @@ +/* Selection Arrow Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "CommonTools/ImageMatch/WaterfillTemplateMatcher.h" +#include "CommonTools/Images/WaterfillUtilities.h" +#include "PokemonFRLG_SelectionArrowDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +ImageFloatBox SelectionArrowDetector::arrow_box_for_position(SelectionArrowPositionStartMenu position){ + switch (position){ + case SelectionArrowPositionStartMenu::POKEDEX: + return ImageFloatBox(0.685, 0.055, 0.03, 0.075); + case SelectionArrowPositionStartMenu::POKEMON: + return ImageFloatBox(0.685, 0.145, 0.03, 0.075); + case SelectionArrowPositionStartMenu::BAG: + return ImageFloatBox(0.685, 0.235, 0.03, 0.075); + case SelectionArrowPositionStartMenu::TRAINER: + return ImageFloatBox(0.685, 0.330, 0.03, 0.075); + case SelectionArrowPositionStartMenu::SAVE: + return ImageFloatBox(0.685, 0.415, 0.03, 0.075); + case SelectionArrowPositionStartMenu::OPTION: + return ImageFloatBox(0.685, 0.510, 0.03, 0.075); + case SelectionArrowPositionStartMenu::EXIT: + return ImageFloatBox(0.685, 0.6, 0.03, 0.075); + default: + break; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid FRLG Selection Arrow Position"); +} + +ImageFloatBox SelectionArrowDetector::arrow_box_for_position(SelectionArrowPositionConfirmationMenu position) { + switch (position) { + case SelectionArrowPositionConfirmationMenu::YES: + return ImageFloatBox(0.660, 0.450, 0.03, 0.075); + case SelectionArrowPositionConfirmationMenu::NO: + return ImageFloatBox(0.660, 0.535, 0.03, 0.075); + default: + break; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid FRLG Selection Arrow Position"); +} + +class SelectionArrowMatcher : public ImageMatch::WaterfillTemplateMatcher{ +public: + SelectionArrowMatcher(const char* path) + : WaterfillTemplateMatcher( + path, + Color(50, 50, 50), Color(115, 115, 115), 70 + ) + { + m_aspect_ratio_lower = 0.8; + m_aspect_ratio_upper = 1.2; + m_area_ratio_lower = 0.8; + m_area_ratio_upper = 1.2; + } + + static const SelectionArrowMatcher& matcher(){ + static SelectionArrowMatcher matcher("PokemonFRLG/SelectionArrow.png"); + return matcher; + } +}; + +SelectionArrowDetector::SelectionArrowDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box +) + : m_color(color) + , m_overlay(overlay) + , m_arrow_box(box) +{} +SelectionArrowDetector::SelectionArrowDetector( + Color color, + VideoOverlay* overlay, + SelectionArrowPositionStartMenu position +) + : m_color(color) + , m_overlay(overlay) + , m_arrow_box(arrow_box_for_position(position)) +{} +SelectionArrowDetector::SelectionArrowDetector( + Color color, + VideoOverlay* overlay, + SelectionArrowPositionConfirmationMenu position +) + : m_color(color) + , m_overlay(overlay) + , m_arrow_box(arrow_box_for_position(position)) +{ +} +void SelectionArrowDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_arrow_box); +} +bool SelectionArrowDetector::detect(const ImageViewRGB32& screen){ + double screen_rel_size = (screen.height() / 1080.0); + double screen_rel_size_2 = screen_rel_size * screen_rel_size; + + double min_area_1080p = 700; + double rmsd_threshold = 80; + size_t min_area = size_t(screen_rel_size_2 * min_area_1080p); + + const std::vector> FILTERS = { + {0xff464646, 0xff787878} + }; + + bool found = match_template_by_waterfill( + screen.size(), + extract_box_reference(screen, m_arrow_box), + SelectionArrowMatcher::matcher(), + FILTERS, + {min_area, SIZE_MAX}, + rmsd_threshold, + [&](Kernels::Waterfill::WaterfillObject& object) -> bool { + m_last_detected = translate_to_parent(screen, m_arrow_box, object); + return true; + } + ); + + if (m_overlay){ + if (found){ + m_last_detected_box.emplace(*m_overlay, m_last_detected, COLOR_GREEN); + }else{ + m_last_detected_box.reset(); + } + } + + return found; +} + + +} +} +} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h new file mode 100644 index 0000000000..d9f32c682e --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h @@ -0,0 +1,110 @@ +/* Selection Arrow Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_SelectionArrowDetector_H +#define PokemonAutomation_PokemonFRLG_SelectionArrowDetector_H + +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/VisualDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +const int START_MENU_OPTION_COUNT = 7; +// The order of these enums should be the same as the order of options in the game menu, from top to bottom, for ease of use with loops. +enum class SelectionArrowPositionStartMenu{ + POKEDEX, + POKEMON, + BAG, + TRAINER, + SAVE, + OPTION, + EXIT +}; + +enum class SelectionArrowPositionConfirmationMenu { + YES, + NO +}; + +class SelectionArrowDetector : public StaticScreenDetector{ +public: + SelectionArrowDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box + ); + + SelectionArrowDetector( + Color color, + VideoOverlay* overlay, + SelectionArrowPositionStartMenu position + ); + + SelectionArrowDetector( + Color color, + VideoOverlay* overlay, + SelectionArrowPositionConfirmationMenu position + ); + + static ImageFloatBox arrow_box_for_position(SelectionArrowPositionStartMenu position); + + static ImageFloatBox arrow_box_for_position(SelectionArrowPositionConfirmationMenu position); + + const ImageFloatBox& last_detected() const { return m_last_detected; } + + virtual void make_overlays(VideoOverlaySet& items) const override; + + // This is not const so that detectors can save/cache state. + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + friend class SelectionArrowWatcher; + + const Color m_color; + VideoOverlay* m_overlay; + const ImageFloatBox m_arrow_box; + + ImageFloatBox m_last_detected; + std::optional m_last_detected_box; +}; +class SelectionArrowWatcher : public DetectorToFinder{ +public: + SelectionArrowWatcher( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("SelectionArrowWatcher", hold_duration, color, overlay, box) + {} + SelectionArrowWatcher( + Color color, + VideoOverlay* overlay, + SelectionArrowPositionStartMenu position, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("SelectionArrowWatcher", hold_duration, color, overlay, arrow_box_for_position(position)) + { + } + SelectionArrowWatcher( + Color color, + VideoOverlay* overlay, + SelectionArrowPositionConfirmationMenu position, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("SelectionArrowWatcher", hold_duration, color, overlay, arrow_box_for_position(position)) + { + } +}; + + +} +} +} + +#endif \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.cpp index 06e233521b..2793b6f069 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.cpp @@ -14,6 +14,8 @@ #include "PokemonFRLG/Inference/Menus/PokemonFRLG_StartMenuDetector.h" #include "PokemonFRLG/PokemonFRLG_Navigation.h" #include "PokemonFRLG_NuggetBridgeFarmer.h" +#include "PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h" +#include namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -55,17 +57,17 @@ NuggetBridgeFarmer::NuggetBridgeFarmer() "Number of Nuggets:
" "Zero will run until 'Stop after Current Nugget' is pressed or the program is manually stopped.", LockMode::UNLOCK_WHILE_RUNNING, - 120, // About 2 hours of farming. + 100, // About 2 hours of farming. 0 ) , GO_HOME_WHEN_DONE(false) - /*, PERIODIC_SAVE( + , PERIODIC_SAVE( "Periodically Save:
" "Save the game every this many nuggets. This reduces the loss to game crashes. Set to zero to disable.", LockMode::UNLOCK_WHILE_RUNNING, - 10, + 50, 0 - )*/ + ) , NOTIFICATION_STATUS_UPDATE("Status Update", true, false, std::chrono::seconds(3600)) , NOTIFICATIONS({ &NOTIFICATION_STATUS_UPDATE, @@ -77,7 +79,7 @@ NuggetBridgeFarmer::NuggetBridgeFarmer() PA_ADD_OPTION(NUM_NUGGETS); PA_ADD_OPTION(GO_HOME_WHEN_DONE); - //PA_ADD_OPTION(PERIODIC_SAVE); + PA_ADD_OPTION(PERIODIC_SAVE); PA_ADD_OPTION(NOTIFICATIONS); } @@ -89,13 +91,19 @@ void NuggetBridgeFarmer::program(SingleSwitchProgramEnvironment& env, ProControl NuggetBridgeFarmer_Descriptor::Stats& stats = env.current_stats(); DeferredStopButtonOption::ResetOnExit reset_on_exit(STOP_AFTER_CURRENT); - //for (uint32_t nuggets_since_last_save = 0;; nuggets_since_last_save++) { - while (true) { + for (uint32_t nuggets_since_last_save = 0;; nuggets_since_last_save++) { send_program_status_notification(env, NOTIFICATION_STATUS_UPDATE); if (NUM_NUGGETS != 0 && stats.nuggets >= NUM_NUGGETS) { break; } + if (PERIODIC_SAVE != 0 && nuggets_since_last_save >= PERIODIC_SAVE) { + env.console.log("Saving game..."); + + save_game_to_overworld(env.console, context); + nuggets_since_last_save = 0; + } + env.console.log("Exiting Pokemon Center..."); while (true){ BlackScreenWatcher pokemon_ceter_exit(COLOR_RED); @@ -210,41 +218,6 @@ void NuggetBridgeFarmer::program(SingleSwitchProgramEnvironment& env, ProControl } } - //TODO: Implement periodic saving. The Save Menu keeps the last cursor position. Need to implement arrow dectection on the correct option - /*if (PERIODIC_SAVE != 0 && nuggets_since_last_save >= PERIODIC_SAVE) { - StartMenuWatcher start_menu = StartMenuWatcher(COLOR_RED); - - env.console.log("Saving game..."); - - while (true) - { - int ret = run_until( - env.console, context, - [](ProControllerContext& context) { - pbf_press_button(context, BUTTON_PLUS, 320ms, 640ms); - pbf_wait(context, 100ms); - context.wait_for_all_requests(); - }, - { start_menu } - ); - - if (ret == 0) { - break; - } - } - - pbf_press_dpad(context, DPAD_DOWN, 320ms, 320ms); - pbf_press_dpad(context, DPAD_DOWN, 320ms, 320ms); - pbf_press_dpad(context, DPAD_DOWN, 320ms, 320ms); - pbf_press_dpad(context, DPAD_DOWN, 320ms, 320ms); - - pbf_press_button(context, BUTTON_A, 320ms, 320ms); - pbf_press_button(context, BUTTON_A, 320ms, 320ms); - - pbf_mash_button(context, BUTTON_B, 2000ms); - nuggets_since_last_save = 0; - }*/ - stats.nuggets++; env.update_stats(); diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.h b/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.h index e3b9a25c2e..66da2669ef 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.h +++ b/SerialPrograms/Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.h @@ -41,7 +41,7 @@ class NuggetBridgeFarmer : public SingleSwitchProgramInstance { SimpleIntegerOption NUM_NUGGETS; GoHomeWhenDoneOption GO_HOME_WHEN_DONE; - //SimpleIntegerOption PERIODIC_SAVE; + SimpleIntegerOption PERIODIC_SAVE; EventNotificationOption NOTIFICATION_STATUS_UPDATE; EventNotificationsOption NOTIFICATIONS; diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.cpp new file mode 100644 index 0000000000..ca11b61401 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.cpp @@ -0,0 +1,204 @@ +/* Start Menu Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Time.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG/Inference/Menus/PokemonFRLG_StartMenuDetector.h" +#include "PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h" +#include "PokemonFRLG_StartMenuNavigation.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG { + +bool move_cursor_to_position(ConsoleHandle& console, ProControllerContext& context, SelectionArrowPositionStartMenu destination){ + SelectionArrowWatcher pokedex_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionStartMenu::POKEDEX + ); + + SelectionArrowWatcher pokemon_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionStartMenu::POKEMON + ); + + SelectionArrowWatcher bag_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionStartMenu::BAG + ); + + SelectionArrowWatcher trainer_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionStartMenu::TRAINER + ); + + SelectionArrowWatcher save_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionStartMenu::SAVE + ); + + SelectionArrowWatcher option_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionStartMenu::OPTION + ); + + SelectionArrowWatcher exit_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionStartMenu::EXIT + ); + + // The order of these watchers needs to match the order of the SelectionArrowPositionStartMenu enum for the math below to work. + int ret = wait_until( + console, context, + std::chrono::seconds(2), + { + pokedex_arrow, + pokemon_arrow, + bag_arrow, + trainer_arrow, + save_arrow, + option_arrow, + exit_arrow + } + ); + + if (ret < 0) { + console.log("Unable to detect selection arrow. Not moving cursor.", COLOR_RED); + return false; + } + + int destination_index = static_cast(destination); + int forward = (destination_index - ret + START_MENU_OPTION_COUNT) % START_MENU_OPTION_COUNT; + int backward = (ret - destination_index + START_MENU_OPTION_COUNT) % START_MENU_OPTION_COUNT; + if (forward <= backward) { + for (int i = 0; i < forward; i++) { + pbf_press_dpad(context, DPAD_DOWN, 320ms, 400ms); + } + context.wait_for_all_requests(); + } + else { + for (int i = 0; i < backward; i++) + { + pbf_press_dpad(context, DPAD_UP, 320ms, 400ms); + } + context.wait_for_all_requests(); + } + + return true; +} + +void save_game_to_overworld(ConsoleHandle& console, ProControllerContext& context){ + + bool seen_start_menu = false; + + WallClock start = current_time(); + while (true){ + context.wait_for_all_requests(); + + if (current_time() - start > std::chrono::seconds(120)) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "save_game_to_overworld(): Unable to save game after 2 minutes.", + console + ); + } + + StartMenuWatcher start_menu = StartMenuWatcher(COLOR_RED); + + int ret = wait_until( + console, context, + std::chrono::seconds(1), + { + start_menu + } + ); + + if (ret == 0) { + seen_start_menu = true; + } + + while (!seen_start_menu) + { + int ret2 = run_until( + console, context, + [](ProControllerContext& context) { + pbf_press_button(context, BUTTON_PLUS, 320ms, 320ms); + pbf_wait(context, 100ms); + context.wait_for_all_requests(); + }, + { start_menu } + ); + + if (ret2 == 0) { + seen_start_menu = true; + } + } + + if (!move_cursor_to_position(console, context, SelectionArrowPositionStartMenu::SAVE)) { + continue; + } + + pbf_press_button(context, BUTTON_A, 320ms, 400ms); + + bool save_confirmed = false; + // There can be one or two confirmation dialogs depending on whether the game was saved prior to this. + while (true){ + if (current_time() - start > std::chrono::seconds(120)) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "save_game_to_overworld(): Unable to save game after 2 minutes.", + console + ); + } + + SelectionArrowWatcher save_confirm_arrow = SelectionArrowWatcher( + COLOR_RED, + &console.overlay(), + SelectionArrowPositionConfirmationMenu::YES + ); + + int ret4 = wait_until( + console, context, + std::chrono::seconds(1), + { + save_confirm_arrow + } + ); + + if (ret4 == 0) { + console.log("Detected Save Confirmation Arrow. Saving game."); + pbf_press_button(context, BUTTON_A, 320ms, 200ms); + save_confirmed = true; + } + + if (ret4 != 0 && save_confirmed) { + break; + } + + context.wait_for_all_requests(); + } + + context.wait_for_all_requests(); + + return; +} + +} + +} +} +} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h new file mode 100644 index 0000000000..f09b5650f1 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h @@ -0,0 +1,40 @@ +/* Start Menu Navigation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_StartMenuNavigation_H +#define PokemonAutomation_PokemonFRLG_StartMenuNavigation_H + +namespace PokemonAutomation{ + +class ImageRGB32; + +template class ControllerContext; +struct ImageFloatBox; + +namespace NintendoSwitch{ + +class ConsoleHandle; +class ProController; +using ProControllerContext = ControllerContext; + +namespace PokemonFRLG { + +// Starting from the start menu, move the selection arrow to the specified position. +// Return true if successful, false otherwise (e.g. if selection arrow is not detected). +bool move_cursor_to_position( + ConsoleHandle& console, ProControllerContext& context, + SelectionArrowPositionStartMenu destination +); + +// Starting from either the overworld or the main menu, save the game. +// This function returns in the overworld. +void save_game_to_overworld(ConsoleHandle& console, ProControllerContext& context); + +} +} +} + +#endif \ No newline at end of file diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 867e36916d..7f50baea66 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1414,6 +1414,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Inference/Menus/PokemonFRLG_LoadMenuDetector.h Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.cpp Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.h + Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.cpp + Source/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h Source/PokemonFRLG/Inference/PokemonFRLG_ShinySymbolDetector.cpp Source/PokemonFRLG/Inference/PokemonFRLG_ShinySymbolDetector.h Source/PokemonFRLG/PokemonFRLG_Navigation.cpp @@ -1424,6 +1426,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/PokemonFRLG_Settings.h Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.cpp Source/PokemonFRLG/Programs/Farming/PokemonFRLG_NuggetBridgeFarmer.h + Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.cpp + Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h Source/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_GiftReset.cpp Source/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_GiftReset.h Source/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_LegendaryReset.cpp