diff --git a/SerialPrograms/Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.cpp b/SerialPrograms/Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.cpp
new file mode 100644
index 0000000000..d09696bfc4
--- /dev/null
+++ b/SerialPrograms/Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.cpp
@@ -0,0 +1,65 @@
+#include "PokemonLZA_DayNightStateDetector.h"
+
+#include "CommonFramework/ImageTools/ImageStats.h"
+
+namespace PokemonAutomation{
+namespace NintendoSwitch{
+namespace PokemonLZA{
+
+
+DayNightStateDetector::DayNightStateDetector(VideoOverlay* overlay)
+ :
+ // sample terrain area after zooming map fully in and hiding icons
+ // screen-relative coordinates (0–1)
+ m_box(0.30, 0.55, 0.15, 0.18),
+ m_state(DayNightState::DAY)
+{}
+
+
+void DayNightStateDetector::make_overlays(VideoOverlaySet& items) const{
+ items.add(COLOR_GREEN, m_box);
+}
+
+
+bool DayNightStateDetector::detect(const ImageViewRGB32& screen){
+
+ ImageStats stats =
+ image_stats(extract_box_reference(screen, m_box));
+
+
+ // robust against brightness differences between capture cards
+ const double blue_ratio =
+ stats.average.b /
+ (stats.average.r + stats.average.g + stats.average.b);
+
+
+ /*
+ Empirically stable separation:
+
+ DAY ≈ 0.32 – 0.35
+ NIGHT ≈ 0.40 – 0.46
+
+ Threshold chosen conservatively to survive:
+ - HDMI color variance
+ - OBS color space changes
+ - capture card gamma shifts
+ */
+
+ if (blue_ratio > 0.36){
+ m_state = DayNightState::NIGHT;
+ }
+ else{
+ m_state = DayNightState::DAY;
+ }
+
+ return true;
+}
+
+
+DayNightState DayNightStateDetector::state() const{
+ return m_state;
+}
+
+}
+}
+}
\ No newline at end of file
diff --git a/SerialPrograms/Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.h b/SerialPrograms/Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.h
new file mode 100644
index 0000000000..61a3b44b92
--- /dev/null
+++ b/SerialPrograms/Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "Common/Cpp/Color.h"
+#include "CommonFramework/ImageTools/ImageBoxes.h"
+#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h"
+#include "CommonTools/VisualDetector.h"
+
+namespace PokemonAutomation{
+namespace NintendoSwitch{
+namespace PokemonLZA{
+
+
+enum class DayNightState{
+ DAY,
+ NIGHT
+};
+
+
+class DayNightStateDetector : public StaticScreenDetector{
+public:
+
+ DayNightStateDetector(VideoOverlay* overlay = nullptr);
+
+ virtual void make_overlays(VideoOverlaySet& items) const override;
+
+ virtual bool detect(const ImageViewRGB32& screen) override;
+
+ DayNightState state() const;
+
+private:
+
+ ImageFloatBox m_box;
+
+ DayNightState m_state;
+
+
+};
+
+
+}
+}
+}
\ No newline at end of file
diff --git a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.cpp b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.cpp
index aae4ecf654..be7a560749 100644
--- a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.cpp
+++ b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.cpp
@@ -8,6 +8,7 @@
#include "CommonFramework/GlobalSettingsPanel.h"
#include "CommonFramework/ProgramStats/StatsTracking.h"
#include "CommonFramework/Notifications/ProgramNotifications.h"
+#include "CommonFramework/VideoPipeline/VideoFeed.h"
#include "CommonTools/Async/InferenceRoutines.h"
#include "CommonTools/StartupChecks/VideoResolutionCheck.h"
#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h"
@@ -15,6 +16,7 @@
#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h"
#include "Pokemon/Pokemon_Strings.h"
#include "PokemonLA/Inference/Sounds/PokemonLA_ShinySoundDetector.h"
+#include "PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.h"
#include "PokemonLZA/Inference/PokemonLZA_ButtonDetector.h"
#include "PokemonLZA/Programs/PokemonLZA_BasicNavigation.h"
#include "PokemonLZA_ShinyHunt_BenchSit.h"
@@ -92,6 +94,29 @@ ShinyHunt_BenchSit::ShinyHunt_BenchSit()
100,
0
)
+ , DAY_NIGHT_FILTER(
+ "Run Forward Only During Day/Night:",
+ LockMode::UNLOCK_WHILE_RUNNING,
+ GroupOption::EnableMode::DEFAULT_DISABLED
+ )
+ , DAY_FILTER_MODE(
+ "Time filter:",
+ {
+ {0, "day", "Day"},
+ {1, "night", "Night"},
+ },
+ LockMode::UNLOCK_WHILE_RUNNING,
+ 0
+ )
+ , PERIODIC_TIME_CHECK(
+ "Periodically Check Time:
"
+ "How often the program re-checks the actual time of day from the map.
"
+ "0 = check every cycle.
"
+ "Higher values run faster but assume time alternates each bench sit.",
+ LockMode::UNLOCK_WHILE_RUNNING,
+ 100,
+ 0
+ )
, SHINY_DETECTED(
"Shiny Detected", "",
"2000 ms",
@@ -112,6 +137,9 @@ ShinyHunt_BenchSit::ShinyHunt_BenchSit()
}
PA_ADD_OPTION(WALK_FORWARD_DURATION);
PA_ADD_OPTION(PERIODIC_SAVE);
+ PA_ADD_OPTION(DAY_NIGHT_FILTER);
+ DAY_NIGHT_FILTER.add_option(DAY_FILTER_MODE, "FilterMode");
+ DAY_NIGHT_FILTER.add_option(PERIODIC_TIME_CHECK,"PeriodicTimeCheck");
PA_ADD_OPTION(SHINY_DETECTED);
PA_ADD_OPTION(NOTIFICATIONS);
}
@@ -156,7 +184,174 @@ void run_back_until_found_bench(
);
}
}
+bool ShinyHunt_BenchSit::should_run_based_on_day_night(
+ const ImageViewRGB32& frame, VideoOverlay& overlay
+){
+ if ((Milliseconds)WALK_FORWARD_DURATION == Milliseconds::zero()){
+ return true;
+ }
+ if (!DAY_NIGHT_FILTER.enabled()){
+ return true;
+ }
+ if (!m_day_night_detector){
+ m_day_night_detector = std::make_unique(&overlay);
+ }
+ if (!m_day_night_detector->detect(frame)){
+ return true;
+ }
+ DayNightState current_state = m_day_night_detector->state();
+ if (current_state == DayNightState::NIGHT) {
+ overlay.add_log("Day/Night Detector: NIGHT", COLOR_CYAN);
+ } else {
+ overlay.add_log("Day/Night Detector: DAY", COLOR_YELLOW);
+ }
+ size_t filter_mode = DAY_FILTER_MODE.current_value();
+ if (filter_mode == 0){
+ return current_state == DayNightState::DAY;
+ }
+ else if (filter_mode == 1){
+ return current_state == DayNightState::NIGHT;
+ }
+ return true;
+}
+void ShinyHunt_BenchSit::check_daynight(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context
+){
+ open_map(env.console, context, false, true);
+ context.wait_for_all_requests();
+ pbf_wait(context, 180ms);
+ // zoom fully in
+ pbf_move_right_joystick(context, {0, 1}, 900ms, 120ms);
+ context.wait_for_all_requests();
+ pbf_wait(context, 120ms);
+ // hide icons
+ pbf_press_button(context, BUTTON_MINUS, 80ms, 120ms);
+ context.wait_for_all_requests();
+ pbf_wait(context, 160ms);
+ // double snapshot
+ ImageViewRGB32 frame1 = env.console.video().snapshot();
+ pbf_wait(context, 120ms);
+ ImageViewRGB32 frame2 = env.console.video().snapshot();
+ should_run_based_on_day_night(frame1, env.console.overlay());
+ should_run_based_on_day_night(frame2, env.console.overlay());
+ if (m_day_night_detector){
+ m_cached_time = m_day_night_detector->state();
+ m_cached_time_initialized = true;
+ m_rounds_since_time_check = 0;
+ std::string time_string = m_cached_time == DayNightState::DAY
+ ? "Cached Day"
+ : "Cached Night";
+ env.console.overlay().add_log(time_string, COLOR_GREEN);
+ env.log(time_string);
+ }
+
+ // close map
+ pbf_wait(context, 120ms);
+ pbf_press_button(context, BUTTON_PLUS, 500ms, 120ms);
+ context.wait_for_all_requests();
+ pbf_wait(context, 150ms);
+}
+void ShinyHunt_BenchSit::run_rounds(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context,
+ ShinySoundHandler& shiny_sound_handler
+){
+ ShinyHunt_BenchSit_Descriptor::Stats& stats = env.current_stats();
+
+ for (uint32_t rounds_since_last_save = 0;; rounds_since_last_save++){
+ send_program_status_notification(env, NOTIFICATION_STATUS);
+ sit_on_bench(env.console, context);
+ shiny_sound_handler.process_pending(context);
+ stats.resets++;
+ env.update_stats();
+ if (m_cached_time_initialized){
+ m_cached_time = m_cached_time == DayNightState::DAY
+ ? DayNightState::NIGHT
+ : DayNightState::DAY;
+ m_rounds_since_time_check++;
+ }
+ uint32_t periodic_save = PERIODIC_SAVE;
+ if (periodic_save != 0 && rounds_since_last_save >= periodic_save){
+ bool save_successful = save_game_to_menu(env.console, context);
+ pbf_mash_button(context, BUTTON_B, 2000ms);
+ if (save_successful){
+ env.console.overlay().add_log("Game Saved Successfully", COLOR_BLUE);
+ rounds_since_last_save = 0;
+ }else{
+ env.console.overlay().add_log("Game Save Failed. Will attempt to save after the next reset.", COLOR_RED);
+ }
+ }
+
+ Milliseconds duration = WALK_FORWARD_DURATION;
+
+ // Only open map when movement enabled AND day/night filter requires verification
+ bool need_time_check = true;
+
+ uint32_t periodic_time_check = PERIODIC_TIME_CHECK;
+
+ if (periodic_time_check > 0 && m_cached_time_initialized){
+ need_time_check = m_rounds_since_time_check >= periodic_time_check;
+ }
+
+ if (duration > Milliseconds::zero() &&
+ DAY_NIGHT_FILTER.enabled() &&
+ (!m_cached_time_initialized || need_time_check)
+ ){
+ check_daynight(env, context);
+ }
+ // filtering
+ if (DAY_NIGHT_FILTER.enabled()){
+ size_t filter_mode = DAY_FILTER_MODE.current_value();
+ bool predicted_ok = filter_mode == 0
+ ? m_cached_time == DayNightState::DAY
+ : m_cached_time == DayNightState::NIGHT;
+
+ if (!predicted_ok){
+ env.console.overlay().add_log(
+ "Skipping move (predicted wrong time of day)",
+ COLOR_ORANGE
+ );
+ run_back_until_found_bench(env, context);
+ shiny_sound_handler.process_pending(context);
+ continue;
+ }
+ }
+
+ // movement
+ if (duration > Milliseconds::zero()){
+ if (WALK_DIRECTION.current_value() == 0){ // forward
+ env.console.overlay().add_log("Move Forward");
+ ssf_press_button(context, BUTTON_B, 0ms, 2*duration, 0ms);
+ pbf_move_left_joystick(context, {0, +1}, duration, 0ms);
+ // run back
+ pbf_move_left_joystick(context, {0, -1}, duration + 750ms, 0ms);
+ run_back_until_found_bench(env, context);
+ }else if (WALK_DIRECTION.current_value() == 1){ // left
+ env.console.overlay().add_log("Move Left");
+ ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
+ pbf_move_left_joystick(context, {-1, 0}, duration, 0ms);
+ pbf_press_button(context, BUTTON_L, 100ms, 400ms);
+ ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
+ pbf_move_left_joystick(context, {0, -1}, duration, 0ms);
+ pbf_move_left_joystick(context, {-1, 0}, 100ms, 0ms);
+ }else if (WALK_DIRECTION.current_value() == 2){ // right
+ env.console.overlay().add_log("Move Right");
+ ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
+ pbf_move_left_joystick(context, {+1, 0}, duration, 0ms);
+ pbf_press_button(context, BUTTON_L, 100ms, 400ms);
+ ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
+ pbf_move_left_joystick(context, {0, -1}, duration, 0ms);
+ pbf_move_left_joystick(context, {+1, 0}, 100ms, 0ms);
+ }
+ }else{
+ run_back_until_found_bench(env, context);
+ }
+
+ shiny_sound_handler.process_pending(context);
+ }
+}
void ShinyHunt_BenchSit::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){
assert_16_9_720p_min(env.logger(), env.console);
@@ -164,6 +359,10 @@ void ShinyHunt_BenchSit::program(SingleSwitchProgramEnvironment& env, ProControl
ShinySoundHandler shiny_sound_handler(SHINY_DETECTED);
+ m_cached_time = DayNightState::DAY;
+ m_cached_time_initialized = false;
+ m_rounds_since_time_check = 0;
+
PokemonLA::ShinySoundDetector shiny_detector(env.console, [&](float error_coefficient) -> bool{
// Warning: This callback will be run from a different thread than this function.
stats.shinies++;
@@ -179,57 +378,7 @@ void ShinyHunt_BenchSit::program(SingleSwitchProgramEnvironment& env, ProControl
run_until(
env.console, context,
[&](ProControllerContext& context){
- for (uint32_t rounds_since_last_save = 0;; rounds_since_last_save++){
- send_program_status_notification(env, NOTIFICATION_STATUS);
- sit_on_bench(env.console, context);
- shiny_sound_handler.process_pending(context);
- stats.resets++;
- env.update_stats();
-
- uint32_t periodic_save = PERIODIC_SAVE;
- if (periodic_save != 0 && rounds_since_last_save >= periodic_save){
- bool save_successful = save_game_to_menu(env.console, context);
- pbf_mash_button(context, BUTTON_B, 2000ms);
- if (save_successful){
- env.console.overlay().add_log("Game Saved Successfully", COLOR_BLUE);
- rounds_since_last_save = 0;
- }else{
- env.console.overlay().add_log("Game Save Failed. Will attempt to save after the next reset.", COLOR_RED);
- }
- }
-
- Milliseconds duration = WALK_FORWARD_DURATION;
- if (duration > Milliseconds::zero()){
- if (WALK_DIRECTION.current_value() == 0){ // forward
- env.console.overlay().add_log("Move Forward");
- ssf_press_button(context, BUTTON_B, 0ms, 2*duration, 0ms);
- pbf_move_left_joystick(context, {0, +1}, duration, 0ms);
- // run back
- pbf_move_left_joystick(context, {0, -1}, duration + 750ms, 0ms);
- run_back_until_found_bench(env, context);
- }else if (WALK_DIRECTION.current_value() == 1){ // left
- env.console.overlay().add_log("Move Left");
- ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
- pbf_move_left_joystick(context, {-1, 0}, duration, 0ms);
- pbf_press_button(context, BUTTON_L, 100ms, 400ms);
- ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
- pbf_move_left_joystick(context, {0, -1}, duration, 0ms);
- pbf_move_left_joystick(context, {-1, 0}, 100ms, 0ms);
- }else if (WALK_DIRECTION.current_value() == 2){ // right
- env.console.overlay().add_log("Move Right");
- ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
- pbf_move_left_joystick(context, {+1, 0}, duration, 0ms);
- pbf_press_button(context, BUTTON_L, 100ms, 400ms);
- ssf_press_button(context, BUTTON_B, 0ms, duration, 0ms);
- pbf_move_left_joystick(context, {0, -1}, duration, 0ms);
- pbf_move_left_joystick(context, {+1, 0}, 100ms, 0ms);
- }
- }else{
- run_back_until_found_bench(env, context);
- }
-
- shiny_sound_handler.process_pending(context);
- }
+ run_rounds(env, context, shiny_sound_handler);
},
{shiny_detector}
);
diff --git a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.h b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.h
index 700d1e435b..881ad19a82 100644
--- a/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.h
+++ b/SerialPrograms/Source/PokemonLZA/Programs/ShinyHunting/PokemonLZA_ShinyHunt_BenchSit.h
@@ -10,10 +10,13 @@
#include "Common/Cpp/Options/EnumDropdownOption.h"
#include "Common/Cpp/Options/SimpleIntegerOption.h"
#include "Common/Cpp/Options/TimeDurationOption.h"
+#include "Common/Cpp/Options/GroupOption.h"
#include "CommonFramework/Notifications/EventNotificationsTable.h"
+#include "CommonFramework/ImageTools/ImageBoxes.h"
#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h"
#include "PokemonLA/Options/PokemonLA_ShinyDetectedAction.h"
#include "PokemonLZA/Options/PokemonLZA_ShinyDetectedAction.h"
+#include "PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.h"
namespace PokemonAutomation{
namespace NintendoSwitch{
@@ -37,6 +40,17 @@ class ShinyHunt_BenchSit : public SingleSwitchProgramInstance{
virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override;
+private:
+ void check_daynight(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context
+ );
+ void run_rounds(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context,
+ ShinySoundHandler& shiny_sound_handler
+ );
+
private:
PokemonLA::ShinyRequiresAudioText SHINY_REQUIRES_AUDIO;
@@ -44,10 +58,21 @@ class ShinyHunt_BenchSit : public SingleSwitchProgramInstance{
MillisecondsOption WALK_FORWARD_DURATION;
SimpleIntegerOption PERIODIC_SAVE;
+ GroupOption DAY_NIGHT_FILTER;
+ IntegerEnumDropdownOption DAY_FILTER_MODE;
+ SimpleIntegerOption PERIODIC_TIME_CHECK;
+
ShinySoundDetectedActionOption SHINY_DETECTED;
EventNotificationOption NOTIFICATION_STATUS;
EventNotificationsOption NOTIFICATIONS;
+
+ std::unique_ptr m_day_night_detector;
+ bool should_run_based_on_day_night(const ImageViewRGB32& frame, VideoOverlay& overlay);
+
+ DayNightState m_cached_time = DayNightState::DAY;
+ bool m_cached_time_initialized = false;
+ uint32_t m_rounds_since_time_check = 0;
};
diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake
index 4255a98889..ffe3441ea2 100644
--- a/SerialPrograms/cmake/SourceFiles.cmake
+++ b/SerialPrograms/cmake/SourceFiles.cmake
@@ -1817,6 +1817,8 @@ file(GLOB LIBRARY_SOURCES
Source/PokemonLZA/Inference/PokemonLZA_ButtonDetector.h
Source/PokemonLZA/Inference/PokemonLZA_DayNightChangeDetector.cpp
Source/PokemonLZA/Inference/PokemonLZA_DayNightChangeDetector.h
+ Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.cpp
+ Source/PokemonLZA/Inference/PokemonLZA_DayNightStateDetector.h
Source/PokemonLZA/Inference/PokemonLZA_DialogDetector.cpp
Source/PokemonLZA/Inference/PokemonLZA_DialogDetector.h
Source/PokemonLZA/Inference/PokemonLZA_HyperspaceRewardNameReader.cpp