diff --git a/SerialPrograms/Source/VideoGames/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HelioptileHunter.cpp b/SerialPrograms/Source/VideoGames/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HelioptileHunter.cpp index f293a7a2d4..cbaa511a8b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HelioptileHunter.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_HelioptileHunter.cpp @@ -4,10 +4,15 @@ * */ +#include +#include +#include #include "Common/Cpp/Options/ConfigOption.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/ProgramStats/StatsTracking.h" #include "CommonFramework/Notifications/ProgramNotifications.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/ImageTools/ImageStats.h" #include "CommonTools/Async/InferenceRoutines.h" #include "CommonTools/StartupChecks/VideoResolutionCheck.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" @@ -16,11 +21,11 @@ #include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" #include "Pokemon/Pokemon_Strings.h" #include "VideoGames/PokemonLZA/Inference/PokemonLZA_ButtonDetector.h" +#include "VideoGames/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.h" #include "VideoGames/PokemonLZA/Inference/PokemonLZA_WeatherDetector.h" -#include "PokemonLZA_ShinyHunt_HelioptileHunter.h" #include "VideoGames/PokemonLZA/Programs/PokemonLZA_BasicNavigation.h" -#include -#include +#include "VideoGames/PokemonLZA/Programs/PokemonLZA_FastTravelNavigation.h" +#include "PokemonLZA_ShinyHunt_HelioptileHunter.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -66,11 +71,14 @@ std::unique_ptr ShinyHunt_HelioptileHunter_Descriptor::make_stats( } ShinyHunt_HelioptileHunter::ShinyHunt_HelioptileHunter() - : END_AFTER_CYCLE("How Many cycles before stopping. 0 for never stop.", + : END_AFTER_CYCLE( + "How many day/night cycles before stopping. 0 for never stop.
" + "
" + "Each day/night cycle is roughly 65 tries.", LockMode::LOCK_WHILE_RUNNING, - 0, 0, 32*30 + 0, 0 ) - , NOTIFICATION_STATUS("Status Update", true, false, ImageAttachmentMode::JPG, {"Notifs"}, std::chrono::seconds(3600)) + , NOTIFICATION_STATUS("Status Update", true, false, std::chrono::seconds(3600)) , NOTIFICATIONS({ &NOTIFICATION_STATUS, &NOTIFICATION_PROGRAM_FINISH, @@ -82,24 +90,6 @@ ShinyHunt_HelioptileHunter::ShinyHunt_HelioptileHunter() PA_ADD_OPTION(NOTIFICATIONS); } -bool proper_weather(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - - open_map(env.console, context, true, true); - - WeatherIconDetector sunnyDetector( - WeatherIconType::Sunny, - &env.console.overlay() - ); - - WeatherIconDetector clearDetector( - WeatherIconType::Clear, - &env.console.overlay() - ); - VideoSnapshot screen = env.console.video().snapshot(); - - return (sunnyDetector.detect(screen) || clearDetector.detect(screen)); -} - void bench_loop(SingleSwitchProgramEnvironment& env, ProControllerContext& context, size_t quantity){ for (size_t i = 0; i < quantity; i++){ sit_on_bench(env.console, context); @@ -107,26 +97,137 @@ void bench_loop(SingleSwitchProgramEnvironment& env, ProControllerContext& conte } } -void find_weather(SingleSwitchProgramEnvironment& env, ProControllerContext& context, bool is_night_time){ +struct WeatherTimeState{ + bool correct_weather; + bool daytime; +}; + +WeatherTimeState get_weather_time_state( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + bool close_map +){ + open_map(env.console, context, false, true); + context.wait_for_all_requests(); + + // zoom fully in + pbf_move_right_joystick(context, {0, 1}, 900ms, 120ms); + context.wait_for_all_requests(); + // hide icons + pbf_press_button(context, BUTTON_MINUS, 80ms, 120ms); context.wait_for_all_requests(); - env.log("Starting weather loop"); - bench_loop(env, context, (is_night_time) ? 1 : 2); - while (!proper_weather(env, context)){ - env.log("Weather not found"); + VideoSnapshot screen = env.console.video().snapshot(); + // Log every weather type so uploaded user logs distinguish unsuitable + // weather from missed or ambiguous detections. These small-region checks + // run between controller sequences; retain them for support troubleshooting. + // Keep the Sunny-or-Clear acceptance rule, including multiple matches. + const std::array types = { + WeatherIconType::Sunny, WeatherIconType::Clear, WeatherIconType::Rain, + WeatherIconType::Cloudy, WeatherIconType::Foggy, WeatherIconType::Rainbow, + }; + const std::array names = { + "Sunny", "Clear", "Rain", "Cloudy", "Foggy", "Rainbow", + }; + std::array matches{}; + std::string detected; + std::string detection_map; + size_t match_count = 0; + for (size_t i = 0; i < types.size(); i++){ + WeatherIconDetector detector(types[i], &env.console.overlay()); + matches[i] = detector.detect(screen); + if (i > 0){ + detection_map += ","; + } + detection_map += std::string(names[i]) + "=" + (matches[i] ? "1" : "0"); + if (matches[i]){ + if (match_count++ > 0){ + detected += ","; + } + detected += names[i]; + } + } + if (match_count == 0){ + detected = "Unknown"; + } + + DayNightStateDetector dayNightDetector(&env.console.overlay()); + dayNightDetector.detect(screen); + bool daytime = dayNightDetector.state() != DayNightState::NIGHT; + bool correct_weather = matches[0] || matches[1]; + + // Mirror the detector's terrain sample so the day/night threshold can be + // checked against capture brightness without changing the detector itself. + const ImageStats terrain = image_stats( + extract_box_reference(screen, ImageFloatBox(0.30, 0.55, 0.15, 0.18)) + ); + const double blue_ratio = terrain.average.b / + (terrain.average.r + terrain.average.g + terrain.average.b); + auto& stats = env.current_stats(); + env.log("[Helioptile][Weather] cycle=" + std::to_string(stats.cycles.load() + 1) + + " completed_loops=" + std::to_string(stats.loops.load()) + + " phase=" + (close_map ? "bench" : "hunt") + + " detected=" + detected + + " ambiguous=" + (match_count > 1 ? "1" : "0") + + " matches={" + detection_map + "}" + + " time=" + (daytime ? "Day" : "Night") + + " blue_ratio=" + std::to_string(blue_ratio) + + " night_threshold=0.36" + + " eligible=" + (correct_weather && daytime ? "1" : "0") + + " frame=" + std::to_string(screen->width()) + "x" + std::to_string(screen->height()) + ); + + if (close_map){ + context.wait_for_all_requests(); pbf_press_button(context, BUTTON_PLUS, 500ms, 500ms); + context.wait_for_all_requests(); + } + + return {correct_weather, daytime}; +} + +void find_weather(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + WeatherTimeState state = get_weather_time_state(env, context, true); + context.wait_for_all_requests(); + env.log("[Helioptile][Decision] action=initial_bench_reroll sits=" + + std::to_string(state.daytime ? 2 : 1)); + bench_loop(env, context, (state.daytime) ? 2 : 1); + + size_t weather_attempt = 0; + while (true){ + env.log("[Helioptile][Bench] attempt=" + std::to_string(++weather_attempt)); + state = get_weather_time_state(env, context, true); + if (state.correct_weather && state.daytime){ + break; + } + env.log("[Helioptile][Decision] action=bench_reroll sits=2 reason=" + + std::string(!state.correct_weather ? "weather" : "night")); bench_loop(env, context, 2); } - env.log("Weather found"); + env.log("[Helioptile][Decision] action=weather_found destination=WildZone14"); context.wait_for_all_requests(); } void reach_bench(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ //Go to poke center - pbf_move_left_joystick(context, {-0.141, +1}, 100ms, 200ms); - pbf_press_button(context, BUTTON_A, 500ms, 500ms); - pbf_press_button(context, BUTTON_A, 500ms, 500ms); - pbf_wait(context, 3000ms); + FastTravelState result = open_map_and_fly_to( + env.console, context, Language::English, + Location::MAGENTA_POKEMON_CENTER, false, true + ); + + if (result != FastTravelState::SUCCESS) { + env.log( + "Fast travel to Magenta failed. State = " + + std::to_string((int)result) + ); + + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to fast travel to Magenta Pokemon Center.", + env.console + ); + } + context.wait_for_all_requests(); //Go to bench pbf_move_left_joystick(context, {-1, 0}, 700ms, 200ms); pbf_move_left_joystick(context, {0, +1}, 500ms, 200ms); @@ -151,11 +252,20 @@ void reach_gate( env.console.overlay().add_log("Detect Entrance"); } -void reach_wild_zone(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - pbf_move_left_joystick(context, {+0.094, -1}, 200ms, 200ms); - pbf_press_button(context, BUTTON_A, 500ms, 500ms); - pbf_press_button(context, BUTTON_A, 500ms, 500ms); - pbf_wait(context, 2000ms); +void warp_wild_zone_14(SingleSwitchProgramEnvironment& env, ProControllerContext& context +) { + FastTravelState result = open_map_and_fly_to( + env.console, context, Language::English, + Location::WILD_ZONE_14, false, true + ); + + if (result != FastTravelState::SUCCESS) { + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to fast travel to Wild Zone 14.", + env.console + ); + } } void execute_fixed_routine(SingleSwitchProgramEnvironment& env, ConsoleHandle& console, ProControllerContext& context, EventNotificationOption& settings){ @@ -190,36 +300,80 @@ void ShinyHunt_HelioptileHunter::program(SingleSwitchProgramEnvironment& env, Pr require_player(env.console, context, BUTTON_L); ShinyHunt_HelioptileHunter_Descriptor::Stats& stats = env.current_stats(); - - // This routine do not care for day/night change as is supposed to stop before time change. + while(true){ - - if (END_AFTER_CYCLE.current_value() > 0 && END_AFTER_CYCLE.current_value() == stats.cycles.load()){ + + if (END_AFTER_CYCLE.current_value() > 0 && + END_AFTER_CYCLE.current_value() == stats.cycles.load(std::memory_order_relaxed) + ){ go_home(env.console, context); send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); break; } int hunt_loops = 0; + std::string cycle_end_reason = "loop_limit"; while (hunt_loops < 65){ - if (!proper_weather(env,context) || (hunt_loops == 0)){ - env.log("Not correct weather"); + // Begin each cycle by re-rolling weather at the bench. A cycle ends + // after 65 completed hunt loops or when the weather/time check fails. + if (hunt_loops == 0){ + env.log("[Helioptile][Decision] action=reach_bench reason=cycle_start"); reach_bench(env, context); - find_weather(env, context, false); - reach_wild_zone(env, context); + find_weather(env, context); + warp_wild_zone_14(env, context); hunt_loops = 0; }else{ - env.log("Correct weather. Continuing"); - move_map_cursor_from_entrance_to_zone(env.console, context, Location::WILD_ZONE_14); - fly_from_map(env.console, context); - } + WeatherTimeState state = get_weather_time_state(env, context, false); + if (state.correct_weather && state.daytime){ + env.log("[Helioptile][Decision] action=continue_hunt"); + // Zoom fully out before moving map cursor for fast travel. + pbf_move_right_joystick(context, {0, -1}, 900ms, 120ms); + // Re-show icons before moving the map cursor to the destination. + pbf_press_button(context, BUTTON_MINUS, 80ms, 120ms); + //these extra waits help prevent early execution of the map move which sometimes fired early during testing + context.wait_for_all_requests(); + pbf_wait(context, 100ms); + move_map_cursor_from_entrance_to_zone(env.console, context, Location::WILD_ZONE_14); + FastTravelState result = fly_from_map(env.console, context); + + switch (result) { + case FastTravelState::SUCCESS: + wait_until_overworld(env.console, context); + break; + + default: + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to fast travel back to Wild Zone 14 after weather check.", + env.console + ); + } + }else{ + cycle_end_reason = !state.correct_weather ? "weather" : "night"; + env.log("[Helioptile][Decision] action=end_cycle reason=" + cycle_end_reason); + pbf_press_button(context, BUTTON_PLUS, 500ms, 500ms); + context.wait_for_all_requests(); + break; + } + } + env.log( + "[Helioptile][Loop] cycle=" + std::to_string(stats.cycles.load(std::memory_order_relaxed) + 1) + + " cycle_loop=" + std::to_string(hunt_loops + 1) + + " total_loop=" + std::to_string(stats.loops.load() + 1) + ); execute_fixed_routine(env,env.console, context, NOTIFICATION_STATUS); stats.loops++; hunt_loops++; env.update_stats(); } + env.log( + "[Helioptile][Decision] action=cycle_complete cycle=" + + std::to_string(stats.cycles.load(std::memory_order_relaxed) + 1) + + " completed_loops=" + std::to_string(hunt_loops) + + " reason=" + cycle_end_reason + ); stats.cycles++; env.update_stats(); }