diff --git a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.cpp b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.cpp index f3b1a1972..de1265ccd 100644 --- a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.cpp +++ b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.cpp @@ -5,9 +5,11 @@ */ #include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/GlobalSettingsPanel.h" #include "CommonFramework/ProgramStats/StatsTracking.h" #include "CommonFramework/Notifications/ProgramNotifications.h" #include "CommonFramework/VideoPipeline/VideoOverlay.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonTools/Async/InferenceRoutines.h" #include "CommonTools/VisualDetectors/BlackScreenDetector.h" #include "CommonTools/StartupChecks/VideoResolutionCheck.h" @@ -15,9 +17,11 @@ #include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" #include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" #include "Pokemon/Pokemon_Strings.h" +#include "PokemonLZA/Inference/PokemonLZA_ButtonDetector.h" #include "PokemonLZA/Inference/PokemonLZA_HyperspaceCalorieDetector.h" #include "PokemonLA/Inference/Sounds/PokemonLA_ShinySoundDetector.h" #include "PokemonLZA/Programs/PokemonLZA_BasicNavigation.h" +#include "PokemonLZA/Programs/PokemonLZA_GameEntry.h" #include "PokemonLZA_ShinyHunt_HyperspaceLegendary.h" namespace PokemonAutomation { @@ -42,15 +46,18 @@ class ShinyHunt_HyperspaceLegendary_Descriptor::Stats : public StatsTracker{ public: Stats() : resets(m_stats["Resets"]) + , rounds(m_stats["Rounds"]) , shinies(m_stats["Shiny Sounds"]) , errors(m_stats["Errors"]) { m_display_order.emplace_back("Resets"); + m_display_order.emplace_back("Rounds"); m_display_order.emplace_back("Shiny Sounds"); m_display_order.emplace_back("Errors", HIDDEN_IF_ZERO); } std::atomic& resets; + std::atomic& rounds; std::atomic& shinies; std::atomic& errors; }; @@ -61,9 +68,13 @@ std::unique_ptr ShinyHunt_HyperspaceLegendary_Descriptor::make_sta ShinyHunt_HyperspaceLegendary::ShinyHunt_HyperspaceLegendary() - : SHINY_DETECTED("Shiny Detected", "", "2000 ms", ShinySoundDetectedAction::NOTIFY_ON_FIRST_ONLY) + : SHINY_DETECTED("Shiny Detected", "", "2000 ms", ShinySoundDetectedAction::STOP_PROGRAM) , LEGENDARY("Hunt Route:", { + // {Legendary::LATIAS, "latias", "Latias"}, + // {Legendary::LATIOS, "latios", "Latios"}, + // {Legendary::COBALION, "cobalion", "Cobalion"}, + {Legendary::TERRAKION, "terrakion", "Terrakion"}, {Legendary::VIRIZION, "virizion", "Virizion"}, }, LockMode::LOCK_WHILE_RUNNING, @@ -83,6 +94,10 @@ ShinyHunt_HyperspaceLegendary::ShinyHunt_HyperspaceLegendary() LockMode::UNLOCK_WHILE_RUNNING, 600, 0, 9999 // default, min, max ) + , SAVE_ON_START( + "Save on Start:
Save the game when starting the program. Each cycle will start at the calorie count at the time of the save", + LockMode::LOCK_WHILE_RUNNING, + true) // default , NOTIFICATION_STATUS("Status Update", true, false, std::chrono::seconds(3600)) , NOTIFICATIONS({ &NOTIFICATION_STATUS, @@ -96,85 +111,181 @@ ShinyHunt_HyperspaceLegendary::ShinyHunt_HyperspaceLegendary() PA_ADD_OPTION(LEGENDARY); PA_ADD_OPTION(MAX_ROUNDS); PA_ADD_OPTION(MIN_CALORIE_REMAINING); + PA_ADD_OPTION(SAVE_ON_START); PA_ADD_OPTION(SHINY_DETECTED); PA_ADD_OPTION(NOTIFICATIONS); } namespace { -// Return if the loop should stop -typedef std::function route_func; - -void route_default( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats){ - // Open map - bool can_fast_travel = open_map(env.console, context); - if (!can_fast_travel){ - stats.errors++; - env.update_stats(); - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, - "route_default(): Cannot open map for fast travel.", - env.console - ); +class LegendaryRoute { +public: + virtual ~LegendaryRoute() = default; + + // A wrapper function to repeatedly run the reset route and exit when min calorie is reached + virtual void reset( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats, + SimpleIntegerOption& MIN_CALORIE_REMAINING){ + + while (true){ + const uint16_t min_calorie = MIN_CALORIE_REMAINING; + HyperspaceCalorieDetector calorie_detector(env.logger()); + + reset_route(env, context, stats); + context.wait_for_all_requests(); + stats.resets++; + env.update_stats(); + + VideoSnapshot screen = env.console.video().snapshot(); + bool valid_calorie_count = calorie_detector.detect(screen); + if (!valid_calorie_count){ + stats.errors++; + env.update_stats(); + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + std::string(typeid(*this).name()) + " reset(): Error detecting calorie count.", + env.console + ); + } + uint16_t calorie_number = calorie_detector.calorie_number(); + + const std::string log_msg = std::format("Calorie: {}/{}", calorie_number, min_calorie); + env.add_overlay_log(log_msg); + env.log(log_msg); + if (calorie_number <= min_calorie){ + env.log("min calorie reached"); + break; + } + } } - // Move map cursor upwards a little bit - pbf_move_left_joystick_old(context, 128, 64, 100ms, 200ms); + // Unique route used to reset each respective legendary, to be implemented by subclasses + virtual void reset_route( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats){ - // Fly from map to reset spawns - FastTravelState travel_status = fly_from_map(env.console, context); - if (travel_status != FastTravelState::SUCCESS){ - stats.errors++; - env.update_stats(); - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, - "route_default(): Cannot fast travel after moving map cursor.", - env.console - ); + return; } -} -bool route_virizion( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats, - SimpleIntegerOption& MIN_CALORIE_REMAINING, - uint8_t& ready_to_stop_counter){ + // The route used to check if the legendary is shiny after resetting + virtual bool check( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats){ - const uint16_t min_calorie = MIN_CALORIE_REMAINING; + return false; // For legendaries that do not have checking implemented + } - // running forward - Milliseconds duration(4400); + void detect_warp_pad(SingleSwitchProgramEnvironment& env, ProControllerContext& context, + ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats){ - HyperspaceCalorieLimitWatcher calorie_watcher(env.logger(), min_calorie); - const int ret = run_until( - env.console, context, - [&](ProControllerContext& context){ - // running forward - ssf_press_button(context, BUTTON_B, 0ms, 2*duration, 0ms); - // Add 30 ms to avoid any drift using the balustrade - pbf_move_left_joystick(context, {0, +1}, duration + 30ms, 0ms); - // run back - pbf_move_left_joystick(context, {0, -1}, duration, 0ms); - pbf_wait(context, 100ms); - }, - {{calorie_watcher}} - ); - uint16_t calorie_number = calorie_watcher.calorie_number(); - const std::string log_msg = std::format("Calorie: {}/{}", calorie_number, min_calorie); - env.add_overlay_log(log_msg); - env.log(log_msg); - if (ret == 0){ - env.log("min calorie reached"); - return true; + ButtonWatcher ButtonA( + COLOR_RED, + ButtonType::ButtonA, + {0.4, 0.1, 0.2, 0.8}, + &env.console.overlay() + ); + + int ret = wait_until( + env.console, context, 10s, + {ButtonA} + ); + if (ret < 0){ + stats.errors++; + env.update_stats(); + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "route_terrakion_reset(): Cannot detect warp pad after 10 seconds", + env.console + ); + }else{ + env.log("Detected warp pad."); + // env.console.overlay().add_log("Warp Pad Detected"); + } } +}; - return false; -} +class TerrakionRoute : public LegendaryRoute { +public: + void reset_route( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats + ) override { + context.wait_for_all_requests(); + + // Warp away from Terrakion to despawn + detect_warp_pad(env, context, stats); + pbf_press_button(context, BUTTON_A, 160ms, 80ms); + + // Warp towards Terrakion + detect_warp_pad(env, context, stats); + pbf_press_button(context, BUTTON_A, 160ms, 80ms); + + // Roll and roll back on Terrakion's roof to respawn + detect_warp_pad(env, context, stats); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + pbf_move_left_joystick(context, {0, -1}, 80ms, 160ms); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + } + bool check( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats + ) override { + context.wait_for_all_requests(); + + // Use warp pads to reset position + detect_warp_pad(env, context, stats); + pbf_press_button(context, BUTTON_A, 160ms, 80ms); + detect_warp_pad(env, context, stats); + pbf_press_button(context, BUTTON_A, 160ms, 80ms); + detect_warp_pad(env, context, stats); + + // Roll to Terrakion to trigger potential shiny sound + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + pbf_move_left_joystick(context, {-1, 1}, 80ms, 160ms); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + pbf_move_left_joystick(context, {0, 1}, 80ms, 500ms); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + pbf_move_left_joystick(context, {1, 1}, 80ms, 160ms); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + pbf_press_button(context, BUTTON_Y, 100ms, 900ms); + + env.add_overlay_log("Checking for Shiny"); + env.log("Checking shiny status of Terrakion."); + context.wait_for_all_requests(); + + go_home(env.console, context); + reset_game_from_home(env, env.console, context); + + return false; + } +}; + +class VirizionRoute : public LegendaryRoute { +public: + void reset_route( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + ShinyHunt_HyperspaceLegendary_Descriptor::Stats& stats + ) override { + context.wait_for_all_requests(); + // running forward + ssf_press_button(context, BUTTON_B, 0ms, 8800ms, 0ms); + // Add 30 ms to avoid any drift using the balustrade + pbf_move_left_joystick(context, {0, +1}, 4430ms, 0ms); + // run back + pbf_move_left_joystick(context, {0, -1}, 4400ms, 0ms); + pbf_wait(context, 100ms); + } +}; } // namespace @@ -201,8 +312,28 @@ void ShinyHunt_HyperspaceLegendary::program(SingleSwitchProgramEnvironment& env, ); }); - uint64_t num_resets = 0; - uint8_t ready_to_stop_counter = 0; + std::unique_ptr legendary_route; + switch(LEGENDARY) { + case Legendary::TERRAKION: + legendary_route = std::make_unique(); + break; + case Legendary::VIRIZION: + legendary_route = std::make_unique(); + break; + default: + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "legendary route not implemented", + env.console + ); + } + + if (SAVE_ON_START){ + save_game_to_menu(env.console, context); + pbf_mash_button(context, BUTTON_B, 2000ms); + } + + int consecutive_failures = 0; run_until( env.console, context, [&](ProControllerContext& context){ @@ -211,27 +342,42 @@ void ShinyHunt_HyperspaceLegendary::program(SingleSwitchProgramEnvironment& env, shiny_sound_handler.process_pending(context); bool should_stop = false; - if (LEGENDARY == Legendary::VIRIZION){ - should_stop = route_virizion(env, context, stats, MIN_CALORIE_REMAINING, ready_to_stop_counter); - } else{ - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, - "legendary hunt not implemented", - env.console - ); + try{ + legendary_route->reset(env, context, stats, MIN_CALORIE_REMAINING); + context.wait_for_all_requests(); + + should_stop = legendary_route->check(env, context, stats); + context.wait_for_all_requests(); + + stats.rounds++; + }catch (OperationFailedException&){ + consecutive_failures++; + env.log("Consecutive failures: " + std::to_string(consecutive_failures), COLOR_RED); + if (consecutive_failures >= 3){ + if (PreloadSettings::instance().DEVELOPER_MODE && GlobalSettings::instance().SAVE_DEBUG_VIDEOS_ON_SWITCH){ + env.log("Saving debug video on Switch..."); + env.console.overlay().add_log("Save Debug Video on Switch"); + pbf_press_button(context, BUTTON_CAPTURE, 2000ms, 0ms); + context.wait_for_all_requests(); + } + go_home(env.console, context); // go Home to preserve game state for debugging + throw; + } + env.log("Error encountered. Resetting...", COLOR_RED); + stats.errors++; + env.console.overlay().add_log("Error Found. Reset Game", COLOR_RED); + go_home(env.console, context); + reset_game_from_home(env, env.console, context); } - num_resets++; - stats.resets++; env.update_stats(); - if (stats.resets.load(std::memory_order_relaxed) % 10 == 0){ + if (stats.rounds.load(std::memory_order_relaxed) % 10 == 0){ send_program_status_notification(env, NOTIFICATION_STATUS); } - if (should_stop){ break; } - if (MAX_ROUNDS > 0 && num_resets >= MAX_ROUNDS){ - env.log(std::format("Reached reset limit {}", static_cast(MAX_ROUNDS))); + if (MAX_ROUNDS > 0 && stats.rounds >= MAX_ROUNDS){ + env.log(std::format("Reached round limit {}", static_cast(MAX_ROUNDS))); break; } diff --git a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.h b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.h index 97d194046..e8363e59b 100644 --- a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.h +++ b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HyperspaceLegendary.h @@ -35,6 +35,10 @@ class ShinyHunt_HyperspaceLegendary : public SingleSwitchProgramInstance{ virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; enum class Legendary{ + // LATIAS, + // LATIOS, + // COBALION, + TERRAKION, VIRIZION }; @@ -44,6 +48,7 @@ class ShinyHunt_HyperspaceLegendary : public SingleSwitchProgramInstance{ EnumDropdownOption LEGENDARY; SimpleIntegerOption MAX_ROUNDS; SimpleIntegerOption MIN_CALORIE_REMAINING; + BooleanCheckBoxOption SAVE_ON_START; EventNotificationOption NOTIFICATION_STATUS; EventNotificationsOption NOTIFICATIONS;