diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index a4b77f40a..9d6ddfd6d 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -313,6 +313,8 @@ file(GLOB MAIN_SOURCES Source/CommonFramework/Exceptions/ProgramFinishedException.h Source/CommonFramework/Exceptions/ScreenshotException.cpp Source/CommonFramework/Exceptions/ScreenshotException.h + Source/CommonFramework/Exceptions/UnexpectedBattleException.cpp + Source/CommonFramework/Exceptions/UnexpectedBattleException.h Source/CommonFramework/GlobalServices.cpp Source/CommonFramework/GlobalServices.h Source/CommonFramework/GlobalSettingsPanel.cpp diff --git a/SerialPrograms/SerialPrograms.pro b/SerialPrograms/SerialPrograms.pro index 6fb8ba213..a7f5683f7 100644 --- a/SerialPrograms/SerialPrograms.pro +++ b/SerialPrograms/SerialPrograms.pro @@ -182,6 +182,7 @@ SOURCES += \ Source/CommonFramework/Exceptions/OperationFailedException.cpp \ Source/CommonFramework/Exceptions/ProgramFinishedException.cpp \ Source/CommonFramework/Exceptions/ScreenshotException.cpp \ + Source/CommonFramework/Exceptions/UnexpectedBattleException.cpp \ Source/CommonFramework/GlobalServices.cpp \ Source/CommonFramework/GlobalSettingsPanel.cpp \ Source/CommonFramework/Globals.cpp \ @@ -1230,6 +1231,7 @@ HEADERS += \ Source/CommonFramework/Exceptions/OperationFailedException.h \ Source/CommonFramework/Exceptions/ProgramFinishedException.h \ Source/CommonFramework/Exceptions/ScreenshotException.h \ + Source/CommonFramework/Exceptions/UnexpectedBattleException.h \ Source/CommonFramework/GlobalServices.h \ Source/CommonFramework/GlobalSettingsPanel.h \ Source/CommonFramework/Globals.h \ diff --git a/SerialPrograms/Source/CommonFramework/Exceptions/UnexpectedBattleException.cpp b/SerialPrograms/Source/CommonFramework/Exceptions/UnexpectedBattleException.cpp new file mode 100644 index 000000000..18aa05a60 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Exceptions/UnexpectedBattleException.cpp @@ -0,0 +1,63 @@ +/* Unexpected Battle Exception + * + * From: https://github.com/PokemonAutomation/Arduino-Source + * + */ + +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/Tools/ErrorDumper.h" +#include "CommonFramework/Tools/ProgramEnvironment.h" +#include "CommonFramework/Tools/ConsoleHandle.h" +#include "UnexpectedBattleException.h" + +namespace PokemonAutomation{ + + +UnexpectedBattleException::UnexpectedBattleException(ErrorReport error_report, Logger& logger, std::string message) + : ScreenshotException(error_report, std::move(message)) +{ + logger.log(std::string(UnexpectedBattleException::name()) + ": " + m_message, COLOR_RED); +} +UnexpectedBattleException::UnexpectedBattleException(ErrorReport error_report, Logger& logger, std::string message, std::shared_ptr screenshot) + : ScreenshotException(error_report, std::move(message), std::move(screenshot)) +{ + logger.log(std::string(UnexpectedBattleException::name()) + ": " + m_message, COLOR_RED); +} +UnexpectedBattleException::UnexpectedBattleException(ErrorReport error_report, ConsoleHandle& console, std::string message, bool take_screenshot) + : ScreenshotException(error_report, console, std::move(message), take_screenshot) +{ + console.log(std::string(UnexpectedBattleException::name()) + ": " + m_message, COLOR_RED); +} + + +void UnexpectedBattleException::send_notification(ProgramEnvironment& env, EventNotificationOption& notification) const{ + std::vector> embeds; + if (!m_message.empty()){ + embeds.emplace_back(std::pair("Message:", m_message)); + } + if (m_send_error_report == ErrorReport::SEND_ERROR_REPORT && m_screenshot){ + std::string label = name(); + std::string filename = dump_image_alone(env.logger(), env.program_info(), label, *m_screenshot); + send_program_telemetry( + env.logger(), true, COLOR_RED, + env.program_info(), + label, + embeds, + filename + ); + } + send_program_notification( + env, notification, + COLOR_RED, + "Program Error", + std::move(embeds), "", + screenshot() + ); +} + + + + + +} diff --git a/SerialPrograms/Source/CommonFramework/Exceptions/UnexpectedBattleException.h b/SerialPrograms/Source/CommonFramework/Exceptions/UnexpectedBattleException.h new file mode 100644 index 000000000..994b89977 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Exceptions/UnexpectedBattleException.h @@ -0,0 +1,37 @@ +/* Unexpected Battle Exception + * + * From: https://github.com/PokemonAutomation/Arduino-Source + * + */ + +#ifndef PokemonAutomation_UnexpectedBattleException_H +#define PokemonAutomation_UnexpectedBattleException_H + +#include +#include "ScreenshotException.h" + +namespace PokemonAutomation{ + +class FatalProgramException; + + +// Thrown by subroutines if caught in an wild battle in-game unexpectedly. +// These include recoverable errors which can be consumed by the program. +class UnexpectedBattleException : public ScreenshotException{ +public: + explicit UnexpectedBattleException(ErrorReport error_report, Logger& logger, std::string message); + explicit UnexpectedBattleException(ErrorReport error_report, Logger& logger, std::string message, std::shared_ptr screenshot); + explicit UnexpectedBattleException(ErrorReport error_report, ConsoleHandle& console, std::string message, bool take_screenshot); + + virtual const char* name() const override{ return "UnexpectedBattleException"; } + virtual std::string message() const override{ return m_message; } + + virtual void send_notification(ProgramEnvironment& env, EventNotificationOption& notification) const override; +}; + + + + + +} +#endif diff --git a/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.cpp b/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.cpp index 7380f194d..8ad27cd56 100644 --- a/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.cpp +++ b/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.cpp @@ -39,12 +39,32 @@ class DestinationMarkerMatcher : public ImageMatch::WaterfillTemplateMatcher{ } }; +class DestinationMarkerYellowMatcher : public ImageMatch::WaterfillTemplateMatcher{ +public: + + DestinationMarkerYellowMatcher() : WaterfillTemplateMatcher( + "PokemonSV/Map/DestinationMarkerIcon-Yellow.png", Color(180,80,0), Color(255, 200, 50), 50 + ){ + 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 ImageMatch::WaterfillTemplateMatcher& instance(){ + static DestinationMarkerYellowMatcher matcher; + return matcher; + } +}; + DestinationMarkerDetector::~DestinationMarkerDetector() = default; -DestinationMarkerDetector::DestinationMarkerDetector(Color color, const ImageFloatBox& box) +DestinationMarkerDetector::DestinationMarkerDetector(Color color, const ImageFloatBox& box, bool check_yellow) : m_color(color) , m_box(box) + , m_check_yellow(check_yellow) {} void DestinationMarkerDetector::make_overlays(VideoOverlaySet& items) const{ @@ -53,7 +73,12 @@ void DestinationMarkerDetector::make_overlays(VideoOverlaySet& items) const{ bool DestinationMarkerDetector::detect(const ImageViewRGB32& screen) const{ std::vector hits = detect_all(screen); - return !hits.empty(); + if (!m_check_yellow){ + return !hits.empty(); + }else{ + std::vector hits_yellow = detect_all_yellow(screen); + return !hits.empty() || !hits_yellow.empty(); + } } std::vector DestinationMarkerDetector::detect_all(const ImageViewRGB32& screen) const{ @@ -64,7 +89,6 @@ std::vector DestinationMarkerDetector::detect_all(const ImageView const double rmsd_threshold = 80.0; // from my testing: RMSD is 15 at 1080p, 60 at 720p - const double min_object_size = 150.0; const double screen_rel_size = (screen.height() / 1080.0); @@ -93,6 +117,48 @@ std::vector DestinationMarkerDetector::detect_all(const ImageView +std::vector DestinationMarkerDetector::detect_all_yellow(const ImageViewRGB32& screen) const{ + const std::vector> filters = { + {combine_rgb(180, 100, 0), combine_rgb(255, 190, 50)}, // to detect the marker within the minimap, when the radar beam is covering the marker + }; + + + const double rmsd_threshold = 80.0; // from my testing, RMSD ranges from 15-50, even at 720p + /* + - min object size restrictions also helps to filter out false positives + at pokemon centers + */ + const double min_object_size = 150.0; + + const double screen_rel_size = (screen.height() / 1080.0); + const size_t min_size = size_t(screen_rel_size * screen_rel_size * min_object_size); + + std::vector found_locations; + + ImagePixelBox pixel_search_area = floatbox_to_pixelbox(screen.width(), screen.height(), m_box); + match_template_by_waterfill( + extract_box_reference(screen, m_box), + DestinationMarkerYellowMatcher::instance(), + filters, + {min_size, SIZE_MAX}, + rmsd_threshold, + [&](Kernels::Waterfill::WaterfillObject& object) -> bool { + ImagePixelBox found_box( + object.min_x + pixel_search_area.min_x, object.min_y + pixel_search_area.min_y, + object.max_x + pixel_search_area.min_x, object.max_y + pixel_search_area.min_y); + found_locations.emplace_back(pixelbox_to_floatbox(screen.width(), screen.height(), found_box)); + return true; + } + ); + + return found_locations; +} + + + + + + } } } diff --git a/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.h b/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.h index 96702a5d4..eb03d6472 100644 --- a/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.h +++ b/SerialPrograms/Source/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.h @@ -25,7 +25,7 @@ namespace PokemonSV{ class DestinationMarkerDetector : public StaticScreenDetector{ public: - DestinationMarkerDetector(Color color, const ImageFloatBox& box); + DestinationMarkerDetector(Color color, const ImageFloatBox& box, bool check_yellow); virtual ~DestinationMarkerDetector(); virtual void make_overlays(VideoOverlaySet& items) const override; @@ -33,9 +33,14 @@ class DestinationMarkerDetector : public StaticScreenDetector{ std::vector detect_all(const ImageViewRGB32& screen) const; + // for detecting the marker within the minimap + // ImageFloatBox for detecting within the minimap {0.815, 0.645, 0.180, 0.320}. needs to be slightly higher up than the minimap since the marker will stick up past the minimap + std::vector detect_all_yellow(const ImageViewRGB32& screen) const; + protected: Color m_color; ImageFloatBox m_box; + bool m_check_yellow; }; @@ -45,9 +50,10 @@ class DestinationMarkerWatcher : public DetectorToFinder @@ -48,29 +51,81 @@ using namespace Pokemon; void run_battle_press_A( ConsoleHandle& console, BotBaseContext& context, - BattleStopCondition stop_condition + BattleStopCondition stop_condition, + std::vector enum_optional_callbacks, + bool detect_wipeout ){ int16_t num_times_seen_overworld = 0; + size_t consecutive_move_select = 0; while (true){ NormalBattleMenuWatcher battle(COLOR_BLUE); - // SwapMenuWatcher fainted(COLOR_PURPLE); + SwapMenuWatcher fainted(COLOR_PURPLE); OverworldWatcher overworld(console, COLOR_CYAN); AdvanceDialogWatcher dialog(COLOR_RED); + DialogArrowWatcher dialog_arrow(COLOR_RED, console.overlay(), {0.850, 0.820, 0.020, 0.050}, 0.8365, 0.846); + GradientArrowWatcher next_pokemon(COLOR_BLUE, GradientArrowType::RIGHT, {0.50, 0.51, 0.30, 0.10}); + MoveSelectWatcher move_select_menu(COLOR_YELLOW); + + std::vector callbacks; + // mandatory callbacks: Battle, Overworld, Advance Dialog, Swap menu, Move select + std::vector enum_all_callbacks{CallbackEnum::BATTLE, CallbackEnum::OVERWORLD, CallbackEnum::ADVANCE_DIALOG, CallbackEnum::SWAP_MENU, CallbackEnum::MOVE_SELECT}; // mandatory callbacks + enum_all_callbacks.insert(enum_all_callbacks.end(), enum_optional_callbacks.begin(), enum_optional_callbacks.end()); // append the mandatory and optional callback vectors together + for (const CallbackEnum& enum_callback : enum_all_callbacks){ + switch(enum_callback){ + case CallbackEnum::ADVANCE_DIALOG: + callbacks.emplace_back(dialog); + break; + case CallbackEnum::OVERWORLD: + callbacks.emplace_back(overworld); + break; + case CallbackEnum::DIALOG_ARROW: + callbacks.emplace_back(dialog_arrow); + break; + case CallbackEnum::BATTLE: + callbacks.emplace_back(battle); + break; + case CallbackEnum::GRADIENT_ARROW: + callbacks.emplace_back(next_pokemon); + break; + case CallbackEnum::SWAP_MENU: + callbacks.emplace_back(fainted); + break; + case CallbackEnum::MOVE_SELECT: + callbacks.emplace_back(move_select_menu); + break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "run_battle_press_A: Unknown callback requested."); + } + } context.wait_for_all_requests(); int ret = wait_until( console, context, std::chrono::seconds(90), - {battle, overworld, dialog} + callbacks ); context.wait_for(std::chrono::milliseconds(100)); + if (ret < 0){ + throw OperationFailedException( + ErrorReport::SEND_ERROR_REPORT, console, + "run_battle_press_A(): Timed out. Did not detect expected stop condition.", + true + ); + } - switch (ret){ - case 0: // battle - console.log("Detected battle menu, spam first move."); - pbf_mash_button(context, BUTTON_A, 3 * TICKS_PER_SECOND); + CallbackEnum enum_callback = enum_all_callbacks[ret]; + switch (enum_callback){ + case CallbackEnum::BATTLE: // battle + console.log("Detected battle menu."); + consecutive_move_select = 0; + pbf_press_button(context, BUTTON_A, 20, 105); + break; + case CallbackEnum::MOVE_SELECT: + console.log("Detected move select. Spam first move"); + consecutive_move_select++; + select_top_move(console, context, consecutive_move_select); break; - case 1: // overworld + case CallbackEnum::OVERWORLD: // overworld console.log("Detected overworld, battle over."); num_times_seen_overworld++; if (stop_condition == BattleStopCondition::STOP_OVERWORLD){ @@ -84,9 +139,10 @@ void run_battle_press_A( ); } break; - case 2: // advance dialog + case CallbackEnum::ADVANCE_DIALOG: // advance dialog console.log("Detected dialog."); - { + + if (detect_wipeout){ context.wait_for_all_requests(); WipeoutDetector wipeout; VideoSnapshot screen = console.video().snapshot(); @@ -105,16 +161,37 @@ void run_battle_press_A( } pbf_press_button(context, BUTTON_A, 20, 105); break; - default: // timeout + case CallbackEnum::DIALOG_ARROW: // dialog arrow + console.log("run_battle_press_A: Detected dialog arrow."); + pbf_press_button(context, BUTTON_A, 20, 105); + break; + case CallbackEnum::GRADIENT_ARROW: + console.log("run_battle_press_A: Detected prompt for bringing in next pokemon. Keep current pokemon."); + pbf_mash_button(context, BUTTON_B, 100); + break; + case CallbackEnum::SWAP_MENU: throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, - "run_battle_press_A(): Timed out. Did not detect expected stop condition.", + "run_battle_press_A(): Lead pokemon fainted.", true - ); + ); + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "run_battle_press_A: Unknown callback triggered."); + } } } +void select_top_move(ConsoleHandle& console, BotBaseContext& context, size_t consecutive_move_select){ + if (consecutive_move_select > 3){ + // to handle case where move is disabled/out of PP/taunted + console.log("Failed to select a move 3 times. Choosing a different move.", COLOR_RED); + pbf_press_dpad(context, DPAD_DOWN, 20, 40); + } + pbf_mash_button(context, BUTTON_A, 100); + +} + void clear_tutorial(ConsoleHandle& console, BotBaseContext& context, uint16_t seconds_timeout){ bool seen_tutorial = false; while (true){ @@ -202,6 +279,8 @@ void clear_dialog(ConsoleHandle& console, BotBaseContext& context, case CallbackEnum::BLACK_DIALOG_BOX: callbacks.emplace_back(black_dialog_box); break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "clear_dialog: Unknown callback requested."); } } @@ -279,6 +358,7 @@ void clear_dialog(ConsoleHandle& console, BotBaseContext& context, break; case CallbackEnum::BLACK_DIALOG_BOX: console.log("clear_dialog: Detected black dialog box."); + seen_dialog = true; pbf_press_button(context, BUTTON_A, 20, 105); break; default: @@ -289,6 +369,38 @@ void clear_dialog(ConsoleHandle& console, BotBaseContext& context, } } +bool confirm_marker_present( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context +){ + while (true){ + DestinationMarkerWatcher marker(COLOR_RED, {0.815, 0.645, 0.180, 0.320}, true); + NormalBattleMenuWatcher battle(COLOR_BLUE); + + int ret = wait_until( + console, context, + std::chrono::seconds(10), + {marker, battle} + ); + switch (ret){ + case 0: // marker + console.log("Confirmed that marker is still present."); + return true; + case 1: // battle + throw UnexpectedBattleException( + ErrorReport::SEND_ERROR_REPORT, console, + "confirm_marker_present(): Unexpectedly detected battle.", + false + ); + default: + console.log("Destination marker not detected."); + return false; + } + } + +} + void overworld_navigation( const ProgramInfo& info, ConsoleHandle& console, @@ -297,7 +409,8 @@ void overworld_navigation( NavigationMovementMode movement_mode, uint8_t x, uint8_t y, uint16_t seconds_timeout, uint16_t seconds_realign, - bool auto_heal + bool auto_heal, + bool detect_wipeout ){ bool should_realign = true; if (seconds_timeout <= seconds_realign){ @@ -307,8 +420,8 @@ void overworld_navigation( uint16_t forward_ticks = seconds_realign * TICKS_PER_SECOND; // WallClock start = current_time(); - if (movement_mode == NavigationMovementMode::CLEAR_WITH_LETS_GO){ - context.wait_for(Milliseconds(3000)); // for some reason, the "Destination arrived" notification reappears when you re-assign the marker. + if (stop_condition == NavigationStopCondition::STOP_MARKER){ + context.wait_for(Milliseconds(2000)); // the "Destination arrived" notification can sometimes reappear if you opened the map too quickly after you reached the previous marker. } @@ -342,7 +455,12 @@ void overworld_navigation( } } if (should_realign){ - realign_player(info, console, context, PlayerRealignMode::REALIGN_OLD_MARKER); + try { + realign_player(info, console, context, PlayerRealignMode::REALIGN_OLD_MARKER); + }catch (UnexpectedBattleException&){ + pbf_wait(context, 30 * TICKS_PER_SECOND); // catch exception to allow the battle callback to take over. + } + } } }, @@ -353,13 +471,27 @@ void overworld_navigation( switch (ret){ case 0: // battle console.log("overworld_navigation: Detected start of battle."); - run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + if (stop_condition == NavigationStopCondition::STOP_BATTLE){ + return; + } + + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD, {}, detect_wipeout); if (auto_heal){ auto_heal_from_menu_or_overworld(info, console, context, 0, true); } - realign_player(info, console, context, PlayerRealignMode::REALIGN_OLD_MARKER); - break; + try { + realign_player(info, console, context, PlayerRealignMode::REALIGN_OLD_MARKER); + + if (!confirm_marker_present(info, console, context)){ + // if marker not present, don't keep walking forward. + return; + } + + break; + }catch (UnexpectedBattleException&){ + break; + } case 1: // dialog console.log("overworld_navigation: Detected dialog."); if (stop_condition == NavigationStopCondition::STOP_DIALOG){ @@ -371,7 +503,8 @@ void overworld_navigation( "overworld_navigation(): Unexpectedly detected dialog.", true ); - } + } + pbf_press_button(context, BUTTON_A, 20, 20); break; case 2: // marker console.log("overworld_navigation: Detected marker."); @@ -381,6 +514,9 @@ void overworld_navigation( break; default: console.log("overworld_navigation(): Timed out."); + if (stop_condition == NavigationStopCondition::STOP_TIME){ + return; + } throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, "overworld_navigation(): Timed out. Did not detect expected stop condition.", @@ -527,11 +663,11 @@ void change_settings(SingleSwitchProgramEnvironment& env, BotBaseContext& contex } void do_action_and_monitor_for_battles( - SingleSwitchProgramEnvironment& env, + const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context, std::function< - void(SingleSwitchProgramEnvironment& env, + void(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context) >&& action @@ -541,7 +677,7 @@ void do_action_and_monitor_for_battles( console, context, [&](BotBaseContext& context){ context.wait_for_all_requests(); - action(env, console, context); + action(info, console, context); }, {battle_menu} ); @@ -552,16 +688,78 @@ void do_action_and_monitor_for_battles( true ); - // console.log("Detected battle. Now running away.", COLOR_PURPLE); - // console.overlay().add_log("Detected battle. Now running away."); - // try{ - // run_from_battle(env.program_info(), console, context); - // }catch (OperationFailedException& e){ - // throw FatalProgramException(std::move(e)); - // } } } + +void handle_unexpected_battles( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + std::function< + void(const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context) + >&& action +){ + while (true){ + try { + context.wait_for_all_requests(); + action(info, console, context); + return; + }catch (UnexpectedBattleException&){ + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + } + } +} + +// void handle_when_stationary_in_overworld( +// const ProgramInfo& info, +// ConsoleHandle& console, +// BotBaseContext& context, +// std::function< +// void(const ProgramInfo& info, +// ConsoleHandle& console, +// BotBaseContext& context) +// >&& action, +// std::function< +// void(const ProgramInfo& info, +// ConsoleHandle& console, +// BotBaseContext& context) +// >&& recovery_action, +// size_t seconds_stationary, +// uint16_t minutes_timeout +// ){ +// StationaryOverworldWatcher stationary_overworld(COLOR_RED, {0.865, 0.82, 0.08, 0.1}, seconds_stationary); +// WallClock start = current_time(); +// while (true){ +// if (current_time() - start > std::chrono::minutes(minutes_timeout)){ +// throw OperationFailedException( +// ErrorReport::SEND_ERROR_REPORT, console, +// "handle_when_stationary_in_overworld(): Failed to complete action after 5 minutes.", +// true +// ); +// } + +// int ret = run_until( +// console, context, +// [&](BotBaseContext& context){ +// context.wait_for_all_requests(); +// action(info, console, context); +// }, +// {stationary_overworld} +// ); +// if (ret < 0){ +// // successfully completed action without being stuck in a position where the overworld is stationary. +// return; +// }else if (ret == 0){ +// // if stationary in overworld, run recovery action then try action again +// context.wait_for_all_requests(); +// recovery_action(info, console, context); +// } +// } +// } + void wait_for_gradient_arrow( const ProgramInfo& info, ConsoleHandle& console, @@ -587,6 +785,31 @@ void wait_for_gradient_arrow( } } +void wait_for_overworld( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + uint16_t seconds_timeout +){ + context.wait_for_all_requests(); + OverworldWatcher overworld(console, COLOR_CYAN); + int ret = wait_until( + console, context, + Milliseconds(seconds_timeout * 1000), + { overworld } + ); + if (ret == 0){ + console.log("Overworld detected."); + }else{ + throw OperationFailedException( + ErrorReport::SEND_ERROR_REPORT, console, + "Failed to detect overworld.", + true + ); + } + +} + void press_A_until_dialog(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context, uint16_t seconds_between_button_presses){ context.wait_for_all_requests(); AdvanceDialogWatcher advance_dialog(COLOR_RED); @@ -612,20 +835,28 @@ void press_A_until_dialog(const ProgramInfo& info, ConsoleHandle& console, BotBa } bool check_ride_active(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context){ - // open main menu - enter_menu_from_overworld(info, console, context, -1, MenuSide::NONE, true); - context.wait_for_all_requests(); - ImageStats ride_indicator = image_stats(extract_box_reference(console.video().snapshot(), ImageFloatBox(0.05, 0.995, 0.25, 0.003))); + while (true){ + try { + // open main menu + enter_menu_from_overworld(info, console, context, -1, MenuSide::NONE, true); + context.wait_for_all_requests(); + ImageStats ride_indicator = image_stats(extract_box_reference(console.video().snapshot(), ImageFloatBox(0.05, 0.995, 0.25, 0.003))); - bool is_ride_active = !is_black(ride_indicator); // ride is active if the ride indicator isn't black. - pbf_press_button(context, BUTTON_B, 30, 100); - press_Bs_to_back_to_overworld(info, console, context, 7); - if (is_ride_active){ - console.log("Ride is active."); - }else{ - console.log("Ride is not active."); + bool is_ride_active = !is_black(ride_indicator); // ride is active if the ride indicator isn't black. + pbf_press_button(context, BUTTON_B, 30, 100); + press_Bs_to_back_to_overworld(info, console, context, 7); + if (is_ride_active){ + console.log("Ride is active."); + }else{ + console.log("Ride is not active."); + } + return is_ride_active; + + }catch(UnexpectedBattleException&){ + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + } } - return is_ride_active; + } void get_on_ride(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context){ @@ -651,8 +882,187 @@ void checkpoint_save(SingleSwitchProgramEnvironment& env, BotBaseContext& contex } +void realign_player_from_landmark( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + MoveCursor move_cursor_near_landmark, + MoveCursor move_cursor_to_target +){ + + console.log("Realigning player direction, using a landmark..."); + WallClock start = current_time(); + + while (true){ + if (current_time() - start > std::chrono::minutes(5)){ + throw OperationFailedException( + ErrorReport::SEND_ERROR_REPORT, console, + "realign_player_from_landmark(): Failed to realign player after 5 minutes.", + true + ); + } + + try { + open_map_from_overworld(info, console, context, false); + + // move cursor near landmark (pokecenter) + switch(move_cursor_near_landmark.zoom_change){ + case ZoomChange::ZOOM_IN: + pbf_press_button(context, BUTTON_ZR, 20, 105); + break; + case ZoomChange::ZOOM_IN_TWICE: + pbf_press_button(context, BUTTON_ZR, 20, 105); + pbf_press_button(context, BUTTON_ZR, 20, 105); + break; + case ZoomChange::ZOOM_OUT: + pbf_press_button(context, BUTTON_ZL, 20, 105); + break; + case ZoomChange::ZOOM_OUT_TWICE: + pbf_press_button(context, BUTTON_ZL, 20, 105); + pbf_press_button(context, BUTTON_ZL, 20, 105); + break; + case ZoomChange::KEEP_ZOOM: + break; + } + uint8_t move_x1 = move_cursor_near_landmark.move_x; + uint8_t move_y1 = move_cursor_near_landmark.move_y; + uint16_t move_duration1 = move_cursor_near_landmark.move_duration; + pbf_move_left_joystick(context, move_x1, move_y1, move_duration1, 1 * TICKS_PER_SECOND); + + // move cursor to pokecenter + if (!detect_closest_pokecenter_and_move_map_cursor_there(info, console, context, 0.29)){ + throw OperationFailedException( + ErrorReport::SEND_ERROR_REPORT, console, + "realign_player_from_landmark(): No visible pokecenter found on map.", + true + ); + } + + confirm_cursor_centered_on_pokecenter(info, console, context); // throws exception if fails + + // move cursor from landmark to target + switch(move_cursor_to_target.zoom_change){ + case ZoomChange::ZOOM_IN: + pbf_press_button(context, BUTTON_ZR, 20, 105); + break; + case ZoomChange::ZOOM_IN_TWICE: + pbf_press_button(context, BUTTON_ZR, 20, 105); + pbf_press_button(context, BUTTON_ZR, 20, 105); + break; + case ZoomChange::ZOOM_OUT: + pbf_press_button(context, BUTTON_ZL, 20, 105); + break; + case ZoomChange::ZOOM_OUT_TWICE: + pbf_press_button(context, BUTTON_ZL, 20, 105); + pbf_press_button(context, BUTTON_ZL, 20, 105); + break; + case ZoomChange::KEEP_ZOOM: + break; + } + uint8_t move_x2 = move_cursor_to_target.move_x; + uint8_t move_y2 = move_cursor_to_target.move_y; + uint16_t move_duration2 = move_cursor_to_target.move_duration; + pbf_move_left_joystick(context, move_x2, move_y2, move_duration2, 1 * TICKS_PER_SECOND); + + // place down marker + pbf_press_button(context, BUTTON_A, 20, 105); + pbf_press_button(context, BUTTON_A, 20, 105); + leave_phone_to_overworld(info, console, context); + + return; + + }catch (OperationFailedException&){ + // reset to overworld if failed to center on the pokecenter, and re-try + leave_phone_to_overworld(info, console, context); + }catch (UnexpectedBattleException&){ + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + } + } + + +} + + +void confirm_cursor_centered_on_pokecenter(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context){ + context.wait_for_all_requests(); + context.wait_for(Milliseconds(500)); + ImageFloatBox center_cursor{0.484, 0.472, 0.030, 0.053}; + MapPokeCenterIconDetector pokecenter(COLOR_RED, center_cursor); + if (!pokecenter.detect(console.video().snapshot())){ + throw OperationFailedException( + ErrorReport::SEND_ERROR_REPORT, console, + "confirm_cursor_centered_on_pokecenter(): Cursor is not centered on a pokecenter.", + true + ); + } + + console.log("Confirmed that the cursor is centered on a pokecenter."); +} + +void move_cursor_towards_flypoint_and_go_there( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + MoveCursor move_cursor_near_flypoint +){ + WallClock start = current_time(); + + while (true){ + if (current_time() - start > std::chrono::minutes(5)){ + throw OperationFailedException( + ErrorReport::SEND_ERROR_REPORT, console, + "move_cursor_towards_flypoint_and_go_there(): Failed to fly after 5 minutes.", + true + ); + } + + try { + open_map_from_overworld(info, console, context, false); + // move cursor near landmark (pokecenter) + switch(move_cursor_near_flypoint.zoom_change){ + case ZoomChange::ZOOM_IN: + pbf_press_button(context, BUTTON_ZR, 20, 105); + break; + case ZoomChange::ZOOM_IN_TWICE: + pbf_press_button(context, BUTTON_ZR, 20, 105); + pbf_press_button(context, BUTTON_ZR, 20, 105); + break; + case ZoomChange::ZOOM_OUT: + pbf_press_button(context, BUTTON_ZL, 20, 105); + break; + case ZoomChange::ZOOM_OUT_TWICE: + pbf_press_button(context, BUTTON_ZL, 20, 105); + pbf_press_button(context, BUTTON_ZL, 20, 105); + break; + case ZoomChange::KEEP_ZOOM: + break; + } + uint8_t move_x1 = move_cursor_near_flypoint.move_x; + uint8_t move_y1 = move_cursor_near_flypoint.move_y; + uint16_t move_duration1 = move_cursor_near_flypoint.move_duration; + pbf_move_left_joystick(context, move_x1, move_y1, move_duration1, 1 * TICKS_PER_SECOND); + + if (!fly_to_visible_closest_pokecenter_cur_zoom_level(info, console, context)){ + throw OperationFailedException( + ErrorReport::SEND_ERROR_REPORT, console, + "move_cursor_towards_flypoint_and_go_there(): No visible pokecenter found on map.", + true + ); + } + return; + + }catch (OperationFailedException&){ + // reset to overworld if failed to center on the pokecenter, and re-try + leave_phone_to_overworld(info, console, context); + }catch (UnexpectedBattleException&){ + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + } + } + + +} } diff --git a/SerialPrograms/Source/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.h b/SerialPrograms/Source/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.h index 3d3f0952b..02cc8970a 100644 --- a/SerialPrograms/Source/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.h +++ b/SerialPrograms/Source/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.h @@ -10,7 +10,7 @@ #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "CommonFramework/Tools/StatsTracking.h" #include "CommonFramework/Language.h" -#include "PokemonSV/Programs/PokemonSV_Navigation.h" +// #include "PokemonSV/Programs/PokemonSV_Navigation.h" namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonSV{ @@ -54,7 +54,10 @@ enum class CallbackEnum{ DIALOG_ARROW, BATTLE, TUTORIAL, - BLACK_DIALOG_BOX + BLACK_DIALOG_BOX, + GRADIENT_ARROW, + SWAP_MENU, + MOVE_SELECT, }; enum class StartPoint{ @@ -74,6 +77,25 @@ enum class StarterChoice{ QUAXLY, }; +enum class PlayerRealignMode{ + REALIGN_NEW_MARKER, + REALIGN_OLD_MARKER, + REALIGN_NO_MARKER, +}; + +enum class NavigationStopCondition{ + STOP_DIALOG, + STOP_MARKER, + STOP_TIME, + STOP_BATTLE, +}; + +enum class NavigationMovementMode{ + DIRECTIONAL_ONLY, + DIRECTIONAL_SPAM_A, + CLEAR_WITH_LETS_GO, +}; + struct AutoStoryOptions{ Language language; StarterChoice starter_choice; @@ -97,9 +119,13 @@ class AutoStory_Segment { void run_battle_press_A( ConsoleHandle& console, BotBaseContext& context, - BattleStopCondition stop_condition + BattleStopCondition stop_condition, + std::vector optional_callbacks = {}, + bool detect_wipeout = false ); +void select_top_move(ConsoleHandle& console, BotBaseContext& context, size_t consecutive_move_select); + // press A to clear tutorial screens // throw exception if tutorial screen never detected void clear_tutorial(ConsoleHandle& console, BotBaseContext& context, uint16_t seconds_timeout = 5); @@ -113,6 +139,14 @@ void clear_dialog(ConsoleHandle& console, BotBaseContext& context, std::vector optional_callbacks = {} ); + +// return true if the destination marker is present within the minimap area +bool confirm_marker_present( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context +); + // move character with ssf left joystick, as per given x, y, until // stop_condition is met (e.g. Dialog detected). // throw exception if reaches timeout before detecting stop condition @@ -121,7 +155,8 @@ void overworld_navigation(const ProgramInfo& info, ConsoleHandle& console, BotBa NavigationMovementMode movement_mode, uint8_t x, uint8_t y, uint16_t seconds_timeout = 60, uint16_t seconds_realign = 60, - bool auto_heal = true + bool auto_heal = false, + bool detect_wipeout = false ); void config_option(BotBaseContext& context, int change_option_value); @@ -131,16 +166,49 @@ void swap_starter_moves(const ProgramInfo& info, ConsoleHandle& console, BotBase // run the given `action`. if detect a battle, stop the action, and throw exception void do_action_and_monitor_for_battles( - SingleSwitchProgramEnvironment& env, + const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context, std::function< - void(SingleSwitchProgramEnvironment& env, + void(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context) >&& action ); +// catch any UnexpectedBattle exceptions from `action`. then use run_battle_press_A until overworld, and re-try the `action`. +void handle_unexpected_battles( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + std::function< + void(const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context) + >&& action +); + +// if stationary in overworld for an amount of time (seconds_stationary), run `recovery_action` then try `action` again +// return once successfully completed `action` +// throw exception if fails to complete `action` within a certain amount of time (minutes_timeout). +void handle_when_stationary_in_overworld( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + std::function< + void(const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context) + >&& action, + std::function< + void(const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context) + >&& recovery_action, + size_t seconds_stationary = 5, + uint16_t minutes_timeout = 5 +); + void wait_for_gradient_arrow( const ProgramInfo& info, ConsoleHandle& console, @@ -149,6 +217,13 @@ void wait_for_gradient_arrow( uint16_t seconds_timeout ); +void wait_for_overworld( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + uint16_t seconds_timeout = 30 +); + void press_A_until_dialog(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context, uint16_t seconds_between_button_presses); bool check_ride_active(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context); @@ -166,7 +241,47 @@ void change_settings(SingleSwitchProgramEnvironment& env, BotBaseContext& contex void checkpoint_save(SingleSwitchProgramEnvironment& env, BotBaseContext& context, EventNotificationOption& notif_status_update); +enum class ZoomChange{ + ZOOM_IN, + ZOOM_IN_TWICE, + ZOOM_OUT, + ZOOM_OUT_TWICE, + KEEP_ZOOM, +}; + +struct MoveCursor{ + ZoomChange zoom_change; + uint8_t move_x; + uint8_t move_y; + uint16_t move_duration; +}; +// place a marker on the map, not relative to the current player position, but based on a fixed landmark, such as a pokecenter +// How this works: +// - cursor is moved to a point near the landmark, as per `move_cursor_near_landmark` +// - move the cursor onto the landmark using `detect_closest_pokecenter_and_move_map_cursor_there`. +// - confirm that the pokecenter is centered within cursor. If not, close map app, and re-try. +// - cursor is moved to target location, as per `move_cursor_to_target`. A marker is placed down here. +void realign_player_from_landmark( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + MoveCursor move_cursor_near_landmark, + MoveCursor move_cursor_to_target +); + +// confirm that the cursor is centered on the pokecenter, within the map app +// else throw exception +void confirm_cursor_centered_on_pokecenter(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context); + +// open map, then move cursor to a point near a flypoint as per `move_cursor_near_flypoint` +// then fly to the closest pokecenter near the cursor +void move_cursor_towards_flypoint_and_go_there( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + MoveCursor move_cursor_near_flypoint +); } } diff --git a/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.cpp b/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.cpp index 44f6466fd..388fd3b6c 100644 --- a/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.cpp +++ b/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.cpp @@ -4,6 +4,7 @@ */ #include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/UnexpectedBattleException.h" #include "CommonFramework/InferenceInfra/InferenceRoutines.h" #include "NintendoSwitch/NintendoSwitch_Settings.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" @@ -71,6 +72,7 @@ void day_skip_from_overworld(ConsoleHandle& console, BotBaseContext& context){ void press_Bs_to_back_to_overworld(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context, uint16_t seconds_between_b_presses){ context.wait_for_all_requests(); OverworldWatcher overworld(console, COLOR_RED); + NormalBattleMenuWatcher battle(COLOR_BLUE); int ret = run_until( console, context, [seconds_between_b_presses](BotBaseContext& context){ @@ -79,9 +81,15 @@ void press_Bs_to_back_to_overworld(const ProgramInfo& info, ConsoleHandle& conso pbf_press_button(context, BUTTON_B, 20, seconds_between_b_presses * TICKS_PER_SECOND); } }, - {overworld} + {overworld, battle} ); - if (ret < 0){ + if (ret == 1){ + throw UnexpectedBattleException( + ErrorReport::SEND_ERROR_REPORT, console, + "press_Bs_to_back_to_overworld(): Unexpectedly detected battle.", + false + ); + }else if (ret < 0){ throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, "press_Bs_to_back_to_overworld(): Unable to detect overworld after 10 button B presses.", @@ -98,16 +106,23 @@ void open_map_from_overworld( ){ { OverworldWatcher overworld(console, COLOR_CYAN); + NormalBattleMenuWatcher battle(COLOR_RED); context.wait_for_all_requests(); int ret = wait_until( console, context, std::chrono::seconds(10), - {overworld} + {overworld, battle} ); context.wait_for(std::chrono::milliseconds(100)); if (ret == 0){ console.log("Detected overworld."); pbf_press_button(context, BUTTON_Y, 20, 105); // open map + }else if (ret == 1){ + throw UnexpectedBattleException( + ErrorReport::SEND_ERROR_REPORT, console, + "open_map_from_overworld(): Unexpectedly detected battle.", + false + ); }else{ throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, @@ -119,10 +134,10 @@ void open_map_from_overworld( WallClock start = current_time(); while (true){ - if (current_time() - start > std::chrono::minutes(1)){ + if (current_time() - start > std::chrono::minutes(2)){ throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, - "open_map_from_overworld(): Failed to open map after 1 minute.", + "open_map_from_overworld(): Failed to open map after 2 minutes.", true ); } @@ -131,12 +146,13 @@ void open_map_from_overworld( AdvanceDialogWatcher advance_dialog(COLOR_YELLOW); PromptDialogWatcher prompt_dialog(COLOR_GREEN); MapWatcher map(COLOR_RED); + NormalBattleMenuWatcher battle(COLOR_RED); context.wait_for_all_requests(); int ret = wait_until( console, context, std::chrono::seconds(30), - {overworld, advance_dialog, prompt_dialog, map} + {overworld, advance_dialog, prompt_dialog, map, battle} ); context.wait_for(std::chrono::milliseconds(100)); switch (ret){ @@ -166,6 +182,13 @@ void open_map_from_overworld( pbf_press_button(context, BUTTON_RCLICK, 20, 105); continue; } + case 4: + console.log("Detected battle."); + throw UnexpectedBattleException( + ErrorReport::SEND_ERROR_REPORT, console, + "open_map_from_overworld(): Unexpectedly detected battle.", + false + ); default: throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, @@ -495,6 +518,8 @@ void open_recently_battled_from_pokedex(const ProgramInfo& info, ConsoleHandle& void leave_phone_to_overworld(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context){ console.log("Exiting to overworld from Rotom Phone..."); OverworldWatcher overworld(console, COLOR_CYAN); + NormalBattleMenuWatcher battle(COLOR_BLUE); + GradientArrowWatcher arrow(COLOR_RED, GradientArrowType::DOWN, {0.475, 0.465, 0.05, 0.085}); context.wait_for_all_requests(); int ret = run_until( @@ -504,15 +529,33 @@ void leave_phone_to_overworld(const ProgramInfo& info, ConsoleHandle& console, B pbf_press_button(context, BUTTON_Y, 20, 1000); } }, - {overworld} + {overworld, battle, arrow} ); - if (ret < 0){ + switch (ret){ + case 0: + return; + case 1: + throw UnexpectedBattleException( + ErrorReport::SEND_ERROR_REPORT, console, + "leave_phone_to_overworld(): Unexpectedly detected battle.", + false + ); + case 2: + console.log("Stuck in battle status screen."); + pbf_mash_button(context, BUTTON_B, 200); + throw UnexpectedBattleException( + ErrorReport::SEND_ERROR_REPORT, console, + "leave_phone_to_overworld(): Unexpectedly detected battle.", + false + ); + default: throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, "leave_phone_to_overworld(): Unknown state after 10 button Y presses.", true ); } + } // While in the current map zoom level, detect pokecenter icons and move the map cursor there. @@ -730,16 +773,22 @@ void jump_off_wall_until_map_open(const ProgramInfo& info, ConsoleHandle& consol // Open map and teleport back to town pokecenter to reset the hunting path. void reset_to_pokecenter(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context){ - open_map_from_overworld(info, console, context); - fly_to_closest_pokecenter_on_map(info, console, context); + while (true){ + try { + open_map_from_overworld(info, console, context); + fly_to_closest_pokecenter_on_map(info, console, context); + }catch (UnexpectedBattleException&){ + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + } + } + } void realign_player(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context, PlayerRealignMode realign_mode, - uint8_t move_x, uint8_t move_y, uint8_t move_duration + uint8_t move_x, uint8_t move_y, uint16_t move_duration ){ console.log("Realigning player direction..."); - switch (realign_mode){ case PlayerRealignMode::REALIGN_NEW_MARKER: console.log("Setting new map marker..."); @@ -749,17 +798,18 @@ void realign_player(const ProgramInfo& info, ConsoleHandle& console, BotBaseCont pbf_press_button(context, BUTTON_A, 20, 105); pbf_press_button(context, BUTTON_A, 20, 105); leave_phone_to_overworld(info, console, context); - break; + return; case PlayerRealignMode::REALIGN_OLD_MARKER: - open_map_from_overworld(info, console, context); + open_map_from_overworld(info, console, context, false); leave_phone_to_overworld(info, console, context); pbf_press_button(context, BUTTON_L, 20, 105); - break; + return; case PlayerRealignMode::REALIGN_NO_MARKER: pbf_move_left_joystick(context, move_x, move_y, move_duration, 1 * TICKS_PER_SECOND); pbf_press_button(context, BUTTON_L, 20, 105); - break; - } + return; + } + } @@ -769,6 +819,7 @@ void walk_forward_until_dialog( BotBaseContext& context, NavigationMovementMode movement_mode, uint16_t seconds_timeout, + uint8_t x, uint8_t y ){ @@ -843,6 +894,7 @@ void mash_button_till_overworld( uint16_t button, uint16_t seconds_run ){ OverworldWatcher overworld(console, COLOR_CYAN); + NormalBattleMenuWatcher battle(COLOR_BLUE); context.wait_for_all_requests(); int ret = run_until( @@ -851,10 +903,12 @@ void mash_button_till_overworld( ssf_mash1_button(context, button, seconds_run * TICKS_PER_SECOND); pbf_wait(context, seconds_run * TICKS_PER_SECOND); }, - {overworld} + {overworld, battle} ); - if (ret < 0){ + if (ret == 1){ + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + }else if (ret < 0){ throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, "mash_button_till_overworld(): Timed out, no recognized state found.", @@ -869,11 +923,19 @@ bool attempt_fly_to_overlapping_flypoint( ConsoleHandle& console, BotBaseContext& context ){ - open_map_from_overworld(info, console, context); - context.wait_for_all_requests(); - pbf_press_button(context, BUTTON_ZL, 40, 100); + while (true){ + try { + open_map_from_overworld(info, console, context, false); + context.wait_for_all_requests(); + pbf_press_button(context, BUTTON_ZL, 40, 100); + + return fly_to_overworld_from_map(info, console, context, true); + + }catch (UnexpectedBattleException&){ + run_battle_press_A(console, context, BattleStopCondition::STOP_OVERWORLD); + } + } - return fly_to_overworld_from_map(info, console, context, true); } void fly_to_overlapping_flypoint(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context){ @@ -919,6 +981,7 @@ void enter_menu_from_overworld(const ProgramInfo& info, ConsoleHandle& console, OverworldWatcher overworld(console, COLOR_CYAN); MainMenuWatcher main_menu(COLOR_RED); + NormalBattleMenuWatcher battle(COLOR_RED); context.wait_for_all_requests(); int ret = run_until( @@ -932,7 +995,7 @@ void enter_menu_from_overworld(const ProgramInfo& info, ConsoleHandle& console, } } }, - {overworld, main_menu} + {overworld, main_menu, battle} ); context.wait_for(std::chrono::milliseconds(100)); @@ -957,6 +1020,12 @@ void enter_menu_from_overworld(const ProgramInfo& info, ConsoleHandle& console, } pbf_press_button(context, BUTTON_A, 20, 105); return; + case 2: + throw UnexpectedBattleException( + ErrorReport::SEND_ERROR_REPORT, console, + "enter_menu_from_overworld(): Unexpectedly detected battle.", + false + ); default: throw OperationFailedException( ErrorReport::SEND_ERROR_REPORT, console, @@ -1107,7 +1176,7 @@ void heal_at_pokecenter( case 3: // tutorial console.log("heal_at_pokecenter: Detected tutorial."); pbf_press_button(context, BUTTON_A, 20, 105); - break; + break; default: console.log("heal_at_pokecenter: Timed out."); throw OperationFailedException( diff --git a/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.h b/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.h index 5095f853b..9c0ca16cb 100644 --- a/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.h +++ b/SerialPrograms/Source/PokemonSV/Programs/PokemonSV_Navigation.h @@ -13,6 +13,7 @@ #include #include #include "PokemonSV/Inference/PokemonSV_MainMenuDetector.h" +#include "PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.h" namespace PokemonAutomation{ struct ProgramInfo; @@ -69,6 +70,20 @@ void open_recently_battled_from_pokedex(const ProgramInfo& info, ConsoleHandle& // From any of the rotom phone apps (Map/Pokédex/Profile) go to overworld. void leave_phone_to_overworld(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context); +bool detect_closest_pokecenter_and_move_map_cursor_there( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + double push_scale = 0.29 +); + +bool fly_to_visible_closest_pokecenter_cur_zoom_level( + const ProgramInfo& info, + ConsoleHandle& console, + BotBaseContext& context, + double push_scale = 0.29 +); + // While on map (default zoom), move to the closest PokeCenter and fly there. // The PokeCenter must be already visited before (so having the little wing icon with it) and not occluded // by other map icons on the most zoomed-in level of the map. @@ -81,13 +96,9 @@ void jump_off_wall_until_map_open(const ProgramInfo& info, ConsoleHandle& consol void reset_to_pokecenter(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context); -enum class PlayerRealignMode{ - REALIGN_NEW_MARKER, - REALIGN_OLD_MARKER, - REALIGN_NO_MARKER, -}; // align player orientation based on the alignment mode +// if battle detected, propagates UnexpectedBattleException to the calling function // The direction is specified by (x, y): // x = 0 : left // x = 128 : neutral @@ -103,19 +114,9 @@ enum class PlayerRealignMode{ // then re-align the camera void realign_player(const ProgramInfo& info, ConsoleHandle& console, BotBaseContext& context, PlayerRealignMode realign_mode, - uint8_t move_x = 0, uint8_t move_y = 0, uint8_t move_duration = 0 + uint8_t move_x = 0, uint8_t move_y = 0, uint16_t move_duration = 0 ); -enum class NavigationStopCondition{ - STOP_DIALOG, - STOP_MARKER, -}; - -enum class NavigationMovementMode{ - DIRECTIONAL_ONLY, - DIRECTIONAL_SPAM_A, - CLEAR_WITH_LETS_GO, -}; void walk_forward_until_dialog( @@ -124,6 +125,7 @@ void walk_forward_until_dialog( BotBaseContext& context, NavigationMovementMode movement_mode, uint16_t seconds_timeout = 10, + uint8_t x = 128, uint8_t y = 0 );