From de0a227d7f23c0f42053d1afd84b3855173319ea Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Sun, 26 Apr 2026 18:08:57 -0500 Subject: [PATCH 1/8] calibration and other fixes --- SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h b/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h index 6863a4f67d..fbababc1db 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h @@ -138,6 +138,9 @@ Pokemon::NatureAdjustments nature_to_adjustment(AdvNature nature); // returns the appropriate NatureAdjustments for an AdvNature Pokemon::NatureAdjustments nature_to_adjustment(AdvNature nature); +// returns the appropriate NatureAdjustments for an AdvNature +Pokemon::NatureAdjustments nature_to_adjustment(AdvNature nature); + // returns search filters that correspond with observed stats AdvRngFilters observation_to_filters(const AdvObservedPokemon& observation, const BaseStats& basestats, AdvRngMethod method = AdvRngMethod::Method1); From f377815ccb5cf8f366d97a3d2c9681efa5700bd9 Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Tue, 28 Apr 2026 00:39:29 -0500 Subject: [PATCH 2/8] static rng program --- .../Source/PokemonFRLG/PokemonFRLG_Panels.cpp | 2 + .../PokemonFRLG_BlindNavigation.cpp | 9 +- .../PokemonFRLG_BlindNavigation.h | 7 + .../RngManipulation/PokemonFRLG_HardReset.cpp | 13 +- .../PokemonFRLG_RngNavigation.cpp | 7 + .../RngManipulation/PokemonFRLG_StaticRng.cpp | 856 ++++++++++++++++++ .../RngManipulation/PokemonFRLG_StaticRng.h | 94 ++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 8 files changed, 985 insertions(+), 5 deletions(-) create mode 100644 SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp create mode 100644 SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.h diff --git a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Panels.cpp b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Panels.cpp index 01bb6accff..24eb37f241 100644 --- a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Panels.cpp +++ b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Panels.cpp @@ -24,6 +24,7 @@ #include "Programs/RngManipulation/PokemonFRLG_SidHelper.h" #include "Programs/RngManipulation/PokemonFRLG_StarterRng.h" #include "Programs/RngManipulation/PokemonFRLG_GiftRng.h" +#include "Programs/RngManipulation/PokemonFRLG_StaticRng.h" #include "Programs/TestPrograms/PokemonFRLG_SoundListener.h" #include "Programs/TestPrograms/PokemonFRLG_ReadStats.h" #include "Programs/TestPrograms/PokemonFRLG_ReadBattleLevelUp.h" @@ -71,6 +72,7 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back(make_single_switch_program()); ret.emplace_back(make_single_switch_program()); ret.emplace_back(make_single_switch_program()); + ret.emplace_back(make_single_switch_program()); } if (PreloadSettings::instance().DEVELOPER_MODE){ diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp index 1c44a297db..87cc208b64 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp @@ -32,7 +32,7 @@ void set_seed_after_delay(ProControllerContext& context, SeedButton SEED_BUTTON, default: b_button = BUTTON_L; } - Milliseconds blackout_wait = 3600ms; // wait for the copyright text to disappear + Milliseconds blackout_wait = 3800ms; // wait for the copyright text to disappear Milliseconds blackout_delay = std::chrono::milliseconds(SEED_DELAY) - blackout_wait; Milliseconds blackout_hold = 30000ms; // wait for leaves/flames to appear on the title screen. It's okay if this is held over the seed button press ssf_do_nothing(context, blackout_wait); @@ -652,6 +652,13 @@ void perform_blind_sequence( case PokemonFRLG_RngTarget::togepi: collect_togepi_egg_after_delay(context, INGAME_DELAY); return; + case PokemonFRLG_RngTarget::electrode: + case PokemonFRLG_RngTarget::articuno: + case PokemonFRLG_RngTarget::zapdos: + case PokemonFRLG_RngTarget::moltres: + case PokemonFRLG_RngTarget::lugia: + case PokemonFRLG_RngTarget::deoxys_attack: + case PokemonFRLG_RngTarget::deoxys_defense: case PokemonFRLG_RngTarget::staticencounter: encounter_static_after_delay(context, INGAME_DELAY); return; diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h index 8f3dd0c8d0..e8bcb53d9a 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h @@ -37,6 +37,13 @@ namespace PokemonFRLG{ gamecornerpinsir, gamecornerporygon, togepi, + electrode, + articuno, + zapdos, + moltres, + lugia, + deoxys_attack, + deoxys_defense, staticencounter, snorlax, mewtwo, diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp index 0adbb71fb4..496db7b53c 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp @@ -39,7 +39,8 @@ void rng_reset_and_return_home( ConsoleType console_type = console.state().console_type(); // go_home() has already been called, so this should already be detected Milliseconds launch_delay = (console_type == ConsoleType::Switch1) ? 450ms : 950ms; - bool extra_resume_press = false; + bool update_popup = false; + bool connect_popup = false; WallClock deadline = current_time() + std::chrono::minutes(5); while (true){ if (current_time() > deadline){ @@ -111,7 +112,11 @@ void rng_reset_and_return_home( // but we can make sure we've gotten back to the home screen and pause there // open the game and go back home ASAP - if (extra_resume_press){ + if (update_popup){ + pbf_press_button(context, BUTTON_A, 160ms, 840ms); + pbf_press_dpad(context, DPAD_UP, 40ms, 0ms); + } + if (connect_popup){ pbf_press_button(context, BUTTON_A, 160ms, 840ms); } pbf_press_button(context, BUTTON_A, 50ms, launch_delay); @@ -136,10 +141,10 @@ void rng_reset_and_return_home( break; case 1: console.log("Update menu detected.", COLOR_BLUE); - extra_resume_press = true; + update_popup = true; case 2: console.log("Failed to connect detected.", COLOR_BLUE); - extra_resume_press = true; + connect_popup = true; } context.wait_for_all_requests(); diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp index 6111ba371e..6353d192e0 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp @@ -162,6 +162,13 @@ bool check_for_shiny(ConsoleHandle& console, ProControllerContext& context, Poke case PokemonFRLG_RngTarget::gamecornerbug: case PokemonFRLG_RngTarget::gamecornerporygon: return shiny_check_summary(console, context); + case PokemonFRLG_RngTarget::electrode: + case PokemonFRLG_RngTarget::articuno: + case PokemonFRLG_RngTarget::zapdos: + case PokemonFRLG_RngTarget::moltres: + case PokemonFRLG_RngTarget::lugia: + case PokemonFRLG_RngTarget::deoxys_attack: + case PokemonFRLG_RngTarget::deoxys_defense: case PokemonFRLG_RngTarget::staticencounter: case PokemonFRLG_RngTarget::snorlax: case PokemonFRLG_RngTarget::mewtwo: diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp new file mode 100644 index 0000000000..7c5a6e414b --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp @@ -0,0 +1,856 @@ +/* Static RNG + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "CommonTools/Random.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/StartupChecks/StartProgramChecks.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "Pokemon/Pokemon_Strings.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/NintendoSwitch_Settings.h" +#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" +#include "PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" +#include "PokemonFRLG/Inference/Dialogs/PokemonFRLG_PartyDialogs.h" +#include "PokemonFRLG/Inference/Menus/PokemonFRLG_SummaryDetector.h" +#include "PokemonFRLG/Inference/Menus/PokemonFRLG_PartyMenuDetector.h" +#include "PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.h" +#include "PokemonFRLG/Inference/PokemonFRLG_PartyLevelUpReader.h" +#include "PokemonFRLG/Inference/PokemonFRLG_StatsReader.h" +#include "PokemonFRLG/PokemonFRLG_Navigation.h" +#include "PokemonFRLG_BlindNavigation.h" +#include "PokemonFRLG_RngNavigation.h" +#include "PokemonFRLG_HardReset.h" +#include "PokemonFRLG_StaticRng.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +StaticRng_Descriptor::StaticRng_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonFRLG:StaticRng", + Pokemon::STRING_POKEMON + " FRLG", "Static RNG", + "Programs/PokemonFRLG/StaticRng.html", + "Automatically calibrate timings to hit a specific RNG target for FRLG static encounters.", + ProgramControllerClass::StandardController_RequiresPrecision, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS + ) +{} + +struct StaticRng_Descriptor::Stats : public StatsTracker{ + Stats() + : resets(m_stats["Resets"]) + , shinies(m_stats["Shinies"]) + , nonshiny(m_stats["Non-Shiny Hits"]) + , errors(m_stats["Errors"]) + { + m_display_order.emplace_back("Resets"); + m_display_order.emplace_back("Shinies"); + m_display_order.emplace_back("Non-Shiny Hits", HIDDEN_IF_ZERO); + m_display_order.emplace_back("Errors", HIDDEN_IF_ZERO); + } + std::atomic& resets; + std::atomic& shinies; + std::atomic& nonshiny; + std::atomic& errors; +}; +std::unique_ptr StaticRng_Descriptor::make_stats() const{ + return std::unique_ptr(new Stats()); +} + +StaticRng::StaticRng() + : LANGUAGE( + "Game Language:", + { + Language::English, + Language::Japanese, + Language::Spanish, + Language::French, + Language::German, + Language::Italian, + }, + LockMode::LOCK_WHILE_RUNNING, + true + ) + , TARGET( + "Target:
", + { + {PokemonFRLG_RngTarget::electrode, "electrode", "Electrode"}, + {PokemonFRLG_RngTarget::snorlax, "snorlax", "Snorlax"}, + {PokemonFRLG_RngTarget::articuno, "articuno", "Articuno"}, + {PokemonFRLG_RngTarget::zapdos, "zapdos", "Zapdos"}, + {PokemonFRLG_RngTarget::moltres, "moltres", "Moltres"}, + {PokemonFRLG_RngTarget::mewtwo, "mewtwo", "Mewtwo"}, + {PokemonFRLG_RngTarget::hypno, "hypno", "Hypno"}, + {PokemonFRLG_RngTarget::hooh, "hooh", "Ho-oh"}, + {PokemonFRLG_RngTarget::lugia, "lugia", "Lugia"}, + {PokemonFRLG_RngTarget::deoxys_attack, "deoxys_attack", "Deoxys-Attack"}, + {PokemonFRLG_RngTarget::deoxys_defense, "deoxys_defense", "Deoxys-Defense"} + }, + LockMode::LOCK_WHILE_RUNNING, + PokemonFRLG_RngTarget::electrode + ) + , MAX_RESETS( + "Max Resets:
", + LockMode::UNLOCK_WHILE_RUNNING, + 50, 0 // default, min + ) + , MAX_RARE_CANDIES( + "Max Rare Candies:
" + "The number of rare candies in your bag. Make sure these are at the top position of the bag.
" + "Rare candies used during calibration will be restored after resetting.", + LockMode::UNLOCK_WHILE_RUNNING, + 0, 0, 999 // default, min, max + ) + , MAX_BALL_THROWS( + "Max Balls Thrown:
" + "The number of " + STRING_POKEBALL + "s in your bag to attempt to throw. Make sure these are at the top position of the bag.
" + "Balls thrown during calibration will be restored after resetting.", + LockMode::UNLOCK_WHILE_RUNNING, + 20, 1, 999 // default, min, max + ) + , SEED( + false, + "Target Seed:", + LockMode::LOCK_WHILE_RUNNING, + "70FE", "70FE", + true + ) + , SEED_LIST( + "Nearby Seeds:
" + "This box should contain a list of seeds (in order) around and including your target seed, with one seed on each line", + LockMode::LOCK_WHILE_RUNNING, + "D000\n199A\n77A1\nAABC\n280C\n70FE\nB573\n02F2\n8084\nA533\nED1E", + "D000\n199A\n77A1\nAABC\n280C\n70FE\nB573\n02F2\n8084\nA533\nED1E", + true + ) + , SEED_BUTTON( + "Seed Button:
", + { + {SeedButton::A, "A", "A"}, + {SeedButton::Start, "Start", "Start"}, + {SeedButton::L, "L", "L (L=A)"}, + }, + LockMode::LOCK_WHILE_RUNNING, + SeedButton::A + ) + , EXTRA_BUTTON( + "Extra Button:
" + "Additional button presses that affect the seed.", + { + {BlackoutButton::None, "None", "None"}, + {BlackoutButton::L, "L", "Blackout L"}, + {BlackoutButton::R, "R", "Blackout R"}, + }, + LockMode::LOCK_WHILE_RUNNING, + BlackoutButton::None + ) + , SEED_DELAY( + "Seed Delay Time (ms):
The delay between starting the game and advancing past the title screen. Set this to match your target seed.", + LockMode::LOCK_WHILE_RUNNING, + 31338, 28000 // default, min + ) + , ADVANCES( + "Advances:
The total number of RNG advances for your target.
This should be the combined amount of continue screen and in-game advances.", + LockMode::LOCK_WHILE_RUNNING, + 10000, 600, 1000000000 // default, min + ) + // , CONTINUE_SCREEN_FRAMES( + // "Continue Screen Frames:
The number of RNG advances to pass on the continue screen.
This should be less than the total number of advances above.", + // LockMode::LOCK_WHILE_RUNNING, + // 1000, 192 // default, min + // ) + , USE_TEACHY_TV( + "Use Teachy TV:" + "
Opens the Teachy TV to quickly advance the RNG at 313x speed.
" + "Warning: can result in larger misses.", + LockMode::LOCK_WHILE_RUNNING, + false // default + ) + , PROFILE( + "User Profile Position:
" + "The position, from left to right, of the Switch profile with the FRLG save you'd like to use.
" + "If this is set to 0, Switch 1 defaults to the last-used profile, while Switch 2 defaults to the first profile (position 1)", + LockMode::LOCK_WHILE_RUNNING, + 0, 0, 8 // default, min, max + ) + , TAKE_VIDEO( + "Take Video:
Record a video when the shiny is found.", + LockMode::LOCK_WHILE_RUNNING, + true // default + ) + , GO_HOME_WHEN_DONE(true) + , NOTIFICATION_SHINY( + "Shiny found", + true, true, ImageAttachmentMode::JPG, + {"Notifs", "Showcase"} + ) + , NOTIFICATION_STATUS_UPDATE("Status Update", true, false, std::chrono::seconds(3600)) + , NOTIFICATIONS({ + &NOTIFICATION_SHINY, + &NOTIFICATION_STATUS_UPDATE, + &NOTIFICATION_PROGRAM_FINISH, + }) +{ + PA_ADD_OPTION(RNG_FILTERS); + PA_ADD_OPTION(RNG_CALIBRATION); + PA_ADD_OPTION(LANGUAGE); + PA_ADD_OPTION(TARGET); + PA_ADD_OPTION(MAX_RESETS); + PA_ADD_OPTION(MAX_BALL_THROWS); + PA_ADD_OPTION(MAX_RARE_CANDIES); + PA_ADD_OPTION(SEED); + PA_ADD_OPTION(SEED_LIST); + PA_ADD_OPTION(SEED_BUTTON); + PA_ADD_OPTION(EXTRA_BUTTON); + PA_ADD_OPTION(SEED_DELAY); + PA_ADD_OPTION(ADVANCES); + // PA_ADD_OPTION(CONTINUE_SCREEN_FRAMES); + PA_ADD_OPTION(USE_TEACHY_TV); + PA_ADD_OPTION(PROFILE); + PA_ADD_OPTION(TAKE_VIDEO); + PA_ADD_OPTION(GO_HOME_WHEN_DONE); + PA_ADD_OPTION(NOTIFICATIONS); +} + + + +bool StaticRng::have_hit_target(SingleSwitchProgramEnvironment& env, const uint32_t& TARGET_SEED, const AdvRngState& hit){ + return (hit.seed == TARGET_SEED) && (hit.advance == ADVANCES); +} + +bool StaticRng::auto_catch(SingleSwitchProgramEnvironment& env, ProControllerContext& context, const uint64_t& MAX_BALL_THROWS){ + for (int i=0; i= 10){ + return false; + } + count++; + + BattleMenuWatcher battle_menu(COLOR_RED); + PartyMenuWatcher party_menu(COLOR_RED); + BlackScreenWatcher black_screen(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + env.console, context, + [](ProControllerContext& context) { + pbf_mash_button(context, BUTTON_B, 30s); + }, + { battle_menu, party_menu, black_screen } + ); + switch (ret){ + case 0: + env.log("Battle menu detected"); + break; + case 1: + env.log("Party menu detected. Attempting to send out next Pokemon"); + pbf_move_left_joystick(context, {0, -1}, 200ms, 300ms); + pbf_mash_button(context, BUTTON_A, 1000ms); + continue; + case 2: + env.log("Black screen detected. Battle exited."); + return false; + default: + send_program_recoverable_error_notification( + env, NOTIFICATION_ERROR_RECOVERABLE, + "auto_catch(): no recognized state after 30 seconds." + ); + return true; + } + + break; + } + + // select BAG (selection arrow does not wrap around) + pbf_move_left_joystick(context, {+1, 0}, 200ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + pbf_move_left_joystick(context, {+1, 0}, 200ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + + BagWatcher bag_open(COLOR_RED); + int ret2 = run_until( + env.console, context, + [](ProControllerContext& context) { + for (int i=0; i<5; i++){ + pbf_press_button(context, BUTTON_A, 200ms, 1800ms); + } + }, + { bag_open } + ); + if (ret2 < 0){ + send_program_recoverable_error_notification( + env, NOTIFICATION_ERROR_RECOVERABLE, + "auto_catch(): failed to open bag." + ); + return true; + } + + if (i == 0){ + // go to balls pocket (pockets do not wrap around, topmost item will already be selected) + pbf_move_left_joystick(context, {+1, 0}, 200ms, 800ms); + pbf_move_left_joystick(context, {+1, 0}, 200ms, 800ms); + pbf_move_left_joystick(context, {+1, 0}, 200ms, 800ms); + } + + // use ball + pbf_mash_button(context, BUTTON_A, 5s); + + // handle catch detection separately, since the white screen was also triggering the BattleMenuDetector + count = 0; + while (true){ + if (count >= 10){ + return false; + } + count++; + + BlackScreenWatcher black_screen(COLOR_RED); + WhiteScreenOverWatcher white_screen(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + env.console, context, + [](ProControllerContext& context) { + pbf_mash_button(context, BUTTON_B, 20s); + }, + { black_screen, white_screen } + ); + switch (ret){ + case 0: + env.log("Black screen detected. Battle exited."); + return false; + case 1: + env.log("White screen detected. Skipping through Pokedex entry..."); + continue; + default: + break; + } + + break; + } + } + + env.log("auto_catch(): ran out of balls."); + return true; +} + +AdvObservedPokemon StaticRng::read_summary(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + // navigate to the summary page of the last occupied (not necessarily 6th) party slot + open_party_menu_from_overworld(env.console, context); + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + + SummaryWatcher page_one(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + env.console, context, + [](ProControllerContext& context) { + pbf_press_button(context, BUTTON_A, 200ms, 300ms); + for (int i=0; i<5; i++){ + pbf_press_button(context, BUTTON_A, 200ms, 3800ms); + } + }, + { page_one } + ); + + if (ret < 0){ + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "read_summary(): Failed to detect first summary screen.", + env.console + ); + } + + // read stats + PokemonFRLG_Stats stats; + StatsReader reader(COLOR_RED); + + env.log("Reading Page 1 (Name, Level, Nature, Gender)..."); + VideoSnapshot screen1 = env.console.video().snapshot(); + reader.read_page1(env.logger(), LANGUAGE, screen1, stats); + + SummaryPage2Watcher page_two(COLOR_RED); + context.wait_for_all_requests(); + int ret2 = run_until( + env.console, context, + [](ProControllerContext& context) { + for (int i=0; i<5; i++){ + pbf_press_dpad(context, DPAD_RIGHT, 200ms, 1800ms); + } + }, + { page_two } + ); + + if (ret2 < 0){ + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "read_summary(): Failed to detect second summary screen.", + env.console + ); + } + + env.log("Reading Page 2 (Stats)..."); + VideoSnapshot screen2 = env.console.video().snapshot(); + reader.read_page2(env.logger(), screen2, stats); + + StatReads statreads = { + static_cast(stats.hp.value_or(0)), + static_cast(stats.attack.value_or(0)), + static_cast(stats.defense.value_or(0)), + static_cast(stats.sp_attack.value_or(0)), + static_cast(stats.sp_defense.value_or(0)), + static_cast(stats.speed.value_or(0)) + }; + + AdvGender gender; + switch(stats.gender.value_or(SummaryGender::Genderless)){ + case SummaryGender::Male: + gender = AdvGender::Male; + break; + case SummaryGender::Female: + gender = AdvGender::Female; + break; + default: + gender = AdvGender::Any; + break; + } + + AdvObservedPokemon pokemon = { + gender, + string_to_nature(stats.nature), + AdvAbility::Any, + { uint8_t(stats.level.value_or(5)) }, + { statreads }, + { {0,0,0,0,0,0} }, + AdvShinyType::Any + }; + + return pokemon; +} + +bool StaticRng::use_rare_candy( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const BaseStats& BASE_STATS, + bool first +){ + // navigate to the bag (only needed for the first use) + if (first){ + open_bag_from_overworld(env.console, context); + // move left to the correct pocket (in case Teachy TV was used) + pbf_move_left_joystick(context, {-1, 0}, 200ms, 800ms); + pbf_move_left_joystick(context, {-1, 0}, 200ms, 800ms); + pbf_move_left_joystick(context, {-1, 0}, 200ms, 800ms); + } + + // use rare candy and watch for the party screen + PartyMenuWatcher party_menu(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + env.console, context, + [](ProControllerContext& context) { + for (int i=0; i<5; i++){ + pbf_press_button(context, BUTTON_A, 200ms, 2800ms); + } + }, + { party_menu } + ); + if (ret < 0){ + send_program_recoverable_error_notification( + env, NOTIFICATION_ERROR_RECOVERABLE, + "use_rare_candy(): failed to detect party menu." + ); + return true; + } + + // select the last party slot (unknown how full the party is, so we can't detect a particular slot) + // only needed on the first use + if (first){ + context.wait_for_all_requests(); + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + } + + // watch for level up stats + PartyLevelUpWatcher level_up(COLOR_RED, PartyLevelUpDialog::stats); + context.wait_for_all_requests(); + int ret2 = run_until( + env.console, context, + [](ProControllerContext& context) { + for (int i=0; i<30; i++){ + pbf_press_button(context, BUTTON_A, 200ms, 800ms); + } + }, + { level_up } + ); + if (ret2 < 0){ + send_program_recoverable_error_notification( + env, NOTIFICATION_ERROR_RECOVERABLE, + "use_rare_candy(): failed to detect level-up stats." + ); + return true; + } + + PartyLevelUpReader reader(COLOR_RED); + VideoOverlaySet overlays(env.console.overlay()); + reader.make_overlays(overlays); + + env.log("Reading stats..."); + VideoSnapshot screen = env.console.video().snapshot(); + StatReads stats = reader.read_stats(env.logger(), screen); + + update_filters(filters, pokemon, stats, {}, BASE_STATS); + RNG_FILTERS.set(filters); + + // return to the bag (possibly learning a move, but trying to preven evolution) + int attempts = 0; + while (true){ + if (attempts > 5){ + send_program_recoverable_error_notification( + env, NOTIFICATION_ERROR_RECOVERABLE, + "use_rare_candy(): failed to return to bag menu in 5 attempts." + ); + return true; + } + BagWatcher bag_menu(COLOR_RED); + PartyMoveLearnWatcher move_learn(COLOR_RED); + context.wait_for_all_requests(); + int ret3 = run_until( + env.console, context, + [](ProControllerContext& context) { + for (int i=0; i<15; i++){ + pbf_press_button(context, BUTTON_B, 200ms, 1800ms); + } + }, + { bag_menu, move_learn } + ); + attempts++; + switch (ret3){ + case 0: + env.log("Returned to bag."); + return false; + case 1: + env.log("Move learn opportunity detected."); + // don't learn move + pbf_press_button(context, BUTTON_B, 200ms, 1800ms); + pbf_press_button(context, BUTTON_A, 200ms, 1800ms); + continue; + default: + send_program_recoverable_error_notification( + env, NOTIFICATION_ERROR_RECOVERABLE, + "use_rare_candy(): failed to return to bag menu." + ); + return true; + } + } +} + + +void StaticRng::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + /* + * Settings: Text Speed fast + */ + + StaticRng_Descriptor::Stats& stats = env.current_stats(); + + home_black_border_check(env.console, context); + + RNG_FILTERS.reset(); + RNG_CALIBRATION.reset(); + + const uint16_t TARGET_SEED = parse_seed(env.console, SEED); + const std::vector SEED_VALUES = parse_seed_list(env.console, SEED_LIST); + const int16_t SEED_POSITION = seed_position_in_list(TARGET_SEED, SEED_VALUES); + + if (SEED_POSITION == -1){ + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "StaticRng(): Target Seed is missing from the list of nearby seeds.", + env.console + ); + } + + env.log("Target Seed Value: " + std::to_string(TARGET_SEED)); + + BaseStats BASE_STATS; + int16_t GENDER_THRESHOLD = -1; + switch (TARGET){ + case PokemonFRLG_RngTarget::electrode: + BASE_STATS = { 60, 50, 70, 80, 80, 140 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::snorlax: + BASE_STATS = { 160, 110, 65, 65, 110, 30 }; + GENDER_THRESHOLD = 30; + break; + case PokemonFRLG_RngTarget::articuno: + BASE_STATS = { 90, 85, 100, 95, 125, 85 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::zapdos: + BASE_STATS = { 90, 90, 85, 125, 90, 100 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::moltres: + BASE_STATS = { 90, 100, 90, 125, 85, 90 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::mewtwo: + BASE_STATS = { 106, 110, 90, 154, 90, 130 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::hypno: + BASE_STATS = { 85, 73, 70, 73, 115, 67 }; + GENDER_THRESHOLD = 126; + break; + case PokemonFRLG_RngTarget::hooh: + BASE_STATS = { 106, 130, 90, 110, 154, 90 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::lugia: + BASE_STATS = { 106, 90, 130, 90, 154, 110 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::deoxys_attack: + BASE_STATS = { 50, 180, 20, 180, 20, 150 }; + GENDER_THRESHOLD = -1; + break; + case PokemonFRLG_RngTarget::deoxys_defense: + BASE_STATS = { 50, 70, 160, 70, 160, 90 }; + GENDER_THRESHOLD = -1; + break; + default: + break; + } + + const double FRAMERATE = 59.999977; // FPS + const double FRAME_DURATION = 1000 / FRAMERATE; + + uint8_t MAX_HISTORY_LENGTH = USE_TEACHY_TV ? 2 : 10; + double SEED_BUMPS[] = {0, 1, -1, 2, -2}; + + uint64_t CONTINUE_SCREEN_FRAMES = 200; + + const int64_t FIXED_SEED_OFFSET = -845; // milliseconds. approximate; + double SEED_CALIBRATION_FRAMES = RNG_CALIBRATION.seed_calibration / FRAME_DURATION; + double ADVANCES_CALIBRATION = RNG_CALIBRATION.advances_calibration; + double CONTINUE_SCREEN_ADJUSTMENT = RNG_CALIBRATION.csf_calibration; + + AdvRngSearcher searcher(TARGET_SEED, ADVANCES, AdvRngMethod::Method1); + AdvPokemonResult target_result = searcher.generate_pokemon(); + env.log("Target IVs:"); + env.log("HP: " + std::to_string(target_result.ivs.hp)); + env.log("Atk: " + std::to_string(target_result.ivs.attack)); + env.log("Def: " + std::to_string(target_result.ivs.defense)); + env.log("SpA: " + std::to_string(target_result.ivs.spatk)); + env.log("SpD: " + std::to_string(target_result.ivs.spdef)); + env.log("Spe: " + std::to_string(target_result.ivs.speed)); + + RngAdvanceHistory ADVANCE_HISTORY; + RngCalibrationHistory CALIBRATION_HISTORY; + uint64_t INITIAL_ADVANCES_RADIUS = USE_TEACHY_TV ? 8192 : 1024; + uint64_t resets = 0; + bool wildshiny_found = false; + + while (true){ + if (CALIBRATION_HISTORY.results.size() > 0){ + env.log("Checking for nonshiny target hit..."); + if (have_hit_target(env, TARGET_SEED, CALIBRATION_HISTORY.results.back())){ + env.log("Target Hit!"); + stats.nonshiny++; + break; + } + env.log("Missed target."); + } + + if (resets > MAX_RESETS){ + env.log("Max resets reached."); + break; + } + + if (wildshiny_found){ + break; + } + + send_program_status_notification( + env, NOTIFICATION_STATUS_UPDATE, + "Calibrating." + ); + env.update_stats(); + + uint64_t advances_radius = INITIAL_ADVANCES_RADIUS; + for (size_t i=0; i 0){ + SEED_CALIBRATION_FRAMES = get_seed_calibration_frames(CALIBRATION_HISTORY, SEED_VALUES, SEED_POSITION); + ADVANCES_CALIBRATION = get_advances_calibration_frames(CALIBRATION_HISTORY, ADVANCES); + } + + if (CALIBRATION_HISTORY.results.size() > 0){ + AdvRngState prev_hit = CALIBRATION_HISTORY.results.back(); + double prev_csf_calibration = CALIBRATION_HISTORY.continue_screen_adjustments.back(); + int64_t prev_advance_miss = int64_t(prev_hit.advance) - int64_t(ADVANCES); + if (prev_advance_miss != 0 && std::abs(prev_advance_miss) < 2){ + env.log("Attempting to correct for off-by-one miss by modifying continue screen frames."); + if (prev_advance_miss > 0){ + CONTINUE_SCREEN_ADJUSTMENT = prev_csf_calibration - 0.5; + }else{ + CONTINUE_SCREEN_ADJUSTMENT = prev_csf_calibration + 0.5; + } + CONTINUE_SCREEN_ADJUSTMENT = fmod(CONTINUE_SCREEN_ADJUSTMENT, 2); + }else{ + // we're still not that close. Slightly vary the seed to more reliably hone in on advances + double seed_bump = SEED_BUMPS[ADVANCE_HISTORY.results.size() % 5]; + SEED_CALIBRATION_FRAMES += seed_bump; + } + }else{ + double seed_bump = SEED_BUMPS[ADVANCE_HISTORY.results.size() % 5]; + SEED_CALIBRATION_FRAMES += seed_bump; + } + + double CALIBRATED_ADVANCES = ADVANCES + ADVANCES_CALIBRATION; + double INGAME_ADVANCES = CALIBRATED_ADVANCES - CONTINUE_SCREEN_FRAMES - CONTINUE_SCREEN_ADJUSTMENT; + + double TEACHY_ADVANCES = 0; + bool should_use_teachy_tv = USE_TEACHY_TV && (INGAME_ADVANCES > 5000); // don't use Teachy TV for short in-game advance targets + if (should_use_teachy_tv) { + TEACHY_ADVANCES = std::floor((INGAME_ADVANCES - 5000) / 313) * 313; + } + + env.log("Seed calibration (frames): " + std::to_string(SEED_CALIBRATION_FRAMES)); + env.log("Advance calibration (frames / 2): " + std::to_string(ADVANCES_CALIBRATION)); + env.log("Continue screen adjustment (frames): " + std::to_string(CONTINUE_SCREEN_ADJUSTMENT)); + + uint64_t CALIBRATED_SEED_DELAY = uint64_t(std::round(SEED_DELAY + FIXED_SEED_OFFSET + FRAME_DURATION * SEED_CALIBRATION_FRAMES)); + uint64_t CONTINUE_SCREEN_DELAY = uint64_t(std::round(FRAME_DURATION * (CONTINUE_SCREEN_FRAMES + CONTINUE_SCREEN_ADJUSTMENT))); + uint64_t TEACHY_DELAY = uint64_t(TEACHY_ADVANCES * FRAME_DURATION / 313); + uint64_t INGAME_DELAY = uint64_t(std::round(FRAME_DURATION * (INGAME_ADVANCES - TEACHY_ADVANCES) / 2)) - (should_use_teachy_tv ? 13700 : 0); + + env.log("Title screen duration: " + std::to_string(CALIBRATED_SEED_DELAY) + "ms"); + env.log("Continue screen duration: " + std::to_string(CONTINUE_SCREEN_DELAY) + "ms"); + if (should_use_teachy_tv){ + env.log("Teachy TV duration: " + std::to_string(TEACHY_DELAY) + "ms"); + env.log("Non-Teachy TV in-game duration: " + std::to_string(INGAME_DELAY) + "ms"); + }else{ + env.log("In-game duration: " + std::to_string(INGAME_DELAY) + "ms"); + } + + env.log("Resetting Game..."); + reset_and_perform_blind_sequence( + env.console, context, TARGET, + SEED_BUTTON, EXTRA_BUTTON, CALIBRATED_SEED_DELAY, + CONTINUE_SCREEN_DELAY, TEACHY_DELAY, INGAME_DELAY, + false, PROFILE + ); + stats.resets++; + + RNG_FILTERS.reset(); + RNG_CALIBRATION.reset(); + + bool shiny_found = check_for_shiny(env.console, context, TARGET); + + if (shiny_found){ + env.log("Shiny found!"); + stats.shinies++; + send_program_notification( + env, + NOTIFICATION_SHINY, + COLOR_YELLOW, + "Shiny found!", + {}, "", + env.console.video().snapshot(), + true + ); + if (TAKE_VIDEO){ + pbf_press_button(context, BUTTON_CAPTURE, 2000ms, 0ms); + } + break; + } + + bool failed = auto_catch(env, context, MAX_BALL_THROWS); + if (failed){ + env.log("Failed catch."); + stats.errors++; + continue; + } + + AdvObservedPokemon pokemon = read_summary(env, context); + AdvRngFilters filters = observation_to_filters(pokemon, BASE_STATS); + RNG_FILTERS.set(filters); + + std::map search_hits = get_search_results(env.console, searcher, filters, SEED_VALUES, ADVANCES, advances_radius, GENDER_THRESHOLD); + RNG_CALIBRATION.set( + SEED_CALIBRATION_FRAMES * FRAME_DURATION, + CONTINUE_SCREEN_ADJUSTMENT, + ADVANCES_CALIBRATION - CONTINUE_SCREEN_ADJUSTMENT, + search_hits + ); + bool finished = update_history(env.console, ADVANCE_HISTORY, CALIBRATION_HISTORY, MAX_HISTORY_LENGTH, SEED_CALIBRATION_FRAMES, ADVANCES_CALIBRATION, CONTINUE_SCREEN_ADJUSTMENT, search_hits, 1); + if (finished || (MAX_RARE_CANDIES == 0)){ + env.log("RNG search finished."); + continue; + } + + for (uint64_t i=0; i make_stats() const override; +}; + +class StaticRng : public SingleSwitchProgramInstance{ +public: + StaticRng(); + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext &context) override; + virtual void start_program_border_check( + VideoStream& stream, + FeedbackType feedback_type + ) override{} + +private: + + bool have_hit_target(SingleSwitchProgramEnvironment& env, const uint32_t& TARGET_SEED, const AdvRngState& hit); + + AdvObservedPokemon read_summary(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + + bool auto_catch(SingleSwitchProgramEnvironment& env, ProControllerContext& context, const uint64_t& MAX_BALL_THROWS); + + bool use_rare_candy( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const BaseStats& BASE_STATS, + bool first + ); + + OCR::LanguageOCROption LANGUAGE; + + EnumDropdownOption TARGET; + + SimpleIntegerOption MAX_RESETS; + SimpleIntegerOption MAX_RARE_CANDIES; + SimpleIntegerOption MAX_BALL_THROWS; + + RngFilterDisplay RNG_FILTERS; + RngCalibrationDisplay RNG_CALIBRATION; + + StringOption SEED; + TextEditOption SEED_LIST; + EnumDropdownOption SEED_BUTTON; + EnumDropdownOption EXTRA_BUTTON; + SimpleIntegerOption SEED_DELAY; + + SimpleIntegerOptionADVANCES; + + BooleanCheckBoxOption USE_TEACHY_TV; + + SimpleIntegerOption PROFILE; + + BooleanCheckBoxOption TAKE_VIDEO; + GoHomeWhenDoneOption GO_HOME_WHEN_DONE; + EventNotificationOption NOTIFICATION_SHINY; + EventNotificationOption NOTIFICATION_STATUS_UPDATE; + EventNotificationsOption NOTIFICATIONS; +}; + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index e279105a8f..91402740e4 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1545,6 +1545,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.h Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.h + Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp + Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.h Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SoundListener.cpp Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SoundListener.h Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadStats.cpp From f47431fc1a10ad2077f3e6363ba9dcfc4e67e232 Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Tue, 28 Apr 2026 00:48:31 -0500 Subject: [PATCH 3/8] adjust stat boxes --- .../PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp index b34e53cdc0..e57945dbfe 100644 --- a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_StatsReader.cpp @@ -27,15 +27,15 @@ namespace NintendoSwitch { namespace PokemonFRLG { StatsReader::StatsReader(Color color) - : m_color(color), m_box_nature(0.028976, 0.729610, 0.502487, 0.065251), + : m_color(color), m_box_nature(0.028976, 0.729610, 0.502487, 0.066639), m_box_level(0.052000, 0.120140, 0.099000, 0.069416), m_box_name(0.163158, 0.122917, 0.262811, 0.066639), m_box_gender(0.430769, 0.114423, 0.034615, 0.081731), - m_box_hp(0.815558, 0.131247, 0.173049, 0.065251), - m_box_attack(0.891000, 0.245089, 0.097607, 0.063862), + m_box_hp(0.815558, 0.131247, 0.173049, 0.066639), + m_box_attack(0.891000, 0.245089, 0.097607, 0.066639), m_box_defense(0.891000, 0.325612, 0.097607, 0.066639), m_box_sp_attack(0.891000, 0.406134, 0.097607, 0.066639), - m_box_sp_defense(0.891000, 0.486657, 0.097607, 0.063862), + m_box_sp_defense(0.891000, 0.486657, 0.097607, 0.066639), m_box_speed(0.891000, 0.567180, 0.097607, 0.066639){} void StatsReader::make_overlays(VideoOverlaySet &items) const { From 31dbb7634fa508de411ec04aef630236eb4c874a Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Tue, 28 Apr 2026 12:35:15 -0500 Subject: [PATCH 4/8] detector fixes --- .../Dialogs/PokemonFRLG_BattleDialogs.cpp | 6 +- .../PokemonFRLG_DexRegistrationDetector.cpp | 68 +++++++++++++++ .../PokemonFRLG_DexRegistrationDetector.h | 51 +++++++++++ .../RngManipulation/PokemonFRLG_HardReset.cpp | 2 +- .../RngManipulation/PokemonFRLG_StaticRng.cpp | 85 +++++++++---------- SerialPrograms/cmake/SourceFiles.cmake | 2 + 6 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.cpp create mode 100644 SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.cpp index 65814e866f..d045a928b5 100644 --- a/SerialPrograms/Source/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.cpp @@ -88,12 +88,14 @@ bool BattleMenuDetector::detect(const ImageViewRGB32& screen){ if ((is_white(menu_top_image) //All languages except japanese && is_white(menu_right_image) && is_solid(dialog_top_image, { 0.176, 0.357, 0.467 }, 0.25, 20) //40, 81, 106 teal - && is_solid(dialog_right_image, { 0.176, 0.357, 0.467 }, 0.25, 20)) + && is_solid(dialog_right_image, { 0.176, 0.357, 0.467 }, 0.25, 20) + && !is_white(dialog_top_image)) || (is_white(menu_top_jpn_image) //japanese && is_white(menu_right_jpn_image) && is_solid(dialog_top_jpn_image, { 0.176, 0.357, 0.467 }, 0.25, 20) - && is_solid(dialog_right_jpn_image, { 0.176, 0.357, 0.467 }, 0.25, 20)) + && is_solid(dialog_right_jpn_image, { 0.176, 0.357, 0.467 }, 0.25, 20) + && !is_white(dialog_top_jpn_image)) ){ return true; } diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.cpp b/SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.cpp new file mode 100644 index 0000000000..f231030e2b --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.cpp @@ -0,0 +1,68 @@ +/* Dex Registration Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/Images/WaterfillUtilities.h" +#include "PokemonFRLG_DexRegistrationDetector.h" + + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +DexRegistrationDetector::DexRegistrationDetector(Color color) + : m_top_box(0.940865, 0.013942, 0.049519, 0.071394) + , m_header_box(0.037019, 0.147355, 0.928365, 0.015865) + , m_divider_box(0.016827, 0.560577, 0.961827, 0.002885) + , m_body_box(0.016827, 0.582577, 0.961827, 0.002885) + , m_bottom_box(0.007692, 0.912500, 0.688942, 0.075721) +{} +void DexRegistrationDetector::make_overlays(VideoOverlaySet& items) const{ + const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; + items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_top_box)); // Green RGB(189, 205, 0) + items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_header_box)); // White + items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_divider_box)); // Brown RGB(136, 100, 38) + items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_body_box)); // Beige RGB(255, 245, 204) + items.add(COLOR_RED, GAME_BOX.inner_to_outer(m_bottom_box)); // Green + +} +bool DexRegistrationDetector::detect(const ImageViewRGB32& screen){ + ImageViewRGB32 game_screen = extract_box_reference(screen, GameSettings::instance().GAME_BOX); + + ImageViewRGB32 top_image = extract_box_reference(game_screen, m_top_box); + ImageViewRGB32 header_image = extract_box_reference(game_screen, m_header_box); + ImageViewRGB32 divider_image = extract_box_reference(game_screen, m_divider_box); + ImageViewRGB32 body_image = extract_box_reference(game_screen, m_body_box); + ImageViewRGB32 bottom_image = extract_box_reference(game_screen, m_bottom_box); + + + if ( is_solid(top_image, { 0.480, 0.520, 0.000 }, 0.25, 20) + && is_white(header_image) + && is_solid(divider_image, { 0.496, 0.365, 0.139 }, 0.25, 20) + && is_solid(body_image, { 0.362, 0.348, 0.290 }, 0.25, 20) + && is_solid(bottom_image, { 0.480, 0.520, 0.000 }, 0.25, 20) + && !is_white(top_image) + && !is_white(divider_image) + && !is_white(bottom_image) + ){ + return true; + } + return false; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h b/SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h new file mode 100644 index 0000000000..f2e36e3fc7 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h @@ -0,0 +1,51 @@ +/* Dex Registration Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_DexRegistrationDetector_H +#define PokemonAutomation_PokemonFRLG_DexRegistrationDetector_H + +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/VisualDetector.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "PokemonFRLG/PokemonFRLG_Settings.h" + +namespace PokemonAutomation{ + class CancellableScope; + class VideoFeed; +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +class DexRegistrationDetector : public StaticScreenDetector{ +public: + DexRegistrationDetector(Color color); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + ImageFloatBox m_top_box; + ImageFloatBox m_header_box; + ImageFloatBox m_divider_box; + ImageFloatBox m_body_box; + ImageFloatBox m_bottom_box; +}; +class DexRegistrationWatcher : public DetectorToFinder{ +public: + DexRegistrationWatcher(Color color) + : DetectorToFinder("DexRegistrationWatcher", std::chrono::milliseconds(50), color) + {} +}; + + + +} +} +} + +#endif diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp index 496db7b53c..c529ff9eed 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp @@ -129,7 +129,7 @@ void rng_reset_and_return_home( BlackScreenWatcher black_screen(COLOR_BLUE, {0.1, 0.15, 0.8, 0.7}); int ret3 = wait_until( console, context, - std::chrono::seconds(2), + std::chrono::seconds(2 + (update_popup ? 1 : 0) + (connect_popup ? 1 : 0)), { black_screen, update_menu, failed_to_connect }, 1ms ); diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp index 7c5a6e414b..790b933485 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp @@ -25,6 +25,8 @@ #include "PokemonFRLG/Inference/Menus/PokemonFRLG_SummaryDetector.h" #include "PokemonFRLG/Inference/Menus/PokemonFRLG_PartyMenuDetector.h" #include "PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.h" +#include "PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h" +#include "PokemonFRLG/Inference/Menus/PokemonFRLG_StartMenuDetector.h" #include "PokemonFRLG/Inference/PokemonFRLG_PartyLevelUpReader.h" #include "PokemonFRLG/Inference/PokemonFRLG_StatsReader.h" #include "PokemonFRLG/PokemonFRLG_Navigation.h" @@ -243,15 +245,21 @@ bool StaticRng::auto_catch(SingleSwitchProgramEnvironment& env, ProControllerCon BattleMenuWatcher battle_menu(COLOR_RED); PartyMenuWatcher party_menu(COLOR_RED); + DexRegistrationWatcher dex_registration(COLOR_RED); BlackScreenWatcher black_screen(COLOR_RED); context.wait_for_all_requests(); int ret = run_until( env.console, context, [](ProControllerContext& context) { - pbf_mash_button(context, BUTTON_B, 30s); + for (int i=0; i<60; i++){ + pbf_press_button(context, BUTTON_B, 200ms, 300ms); + } }, - { battle_menu, party_menu, black_screen } + { battle_menu, party_menu, black_screen }, + 10ms ); + + int start_ret; switch (ret){ case 0: env.log("Battle menu detected"); @@ -262,24 +270,47 @@ bool StaticRng::auto_catch(SingleSwitchProgramEnvironment& env, ProControllerCon pbf_mash_button(context, BUTTON_A, 1000ms); continue; case 2: + env.log("Dex registration detected. Exiting battle..."); + pbf_mash_button(context, BUTTON_B, 5000ms); + return false; + case 3: env.log("Black screen detected. Battle exited."); return false; default: - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "auto_catch(): no recognized state after 30 seconds." - ); - return true; + env.log("No recognized state. Try checking if in the overworld..."); + StartMenuWatcher start_menu; + context.wait_for_all_requests(); + start_ret = run_until( + env.console, context, + [](ProControllerContext& context) { + for (int i=0; i<3; i++){ + pbf_press_button(context, BUTTON_PLUS, 200ms, 2800ms); + pbf_mash_button(context, BUTTON_B, 500ms); + } + }, + { start_menu } + ); + if (start_ret < 0){ + send_program_recoverable_error_notification( + env, NOTIFICATION_ERROR_RECOVERABLE, + "auto_catch(): no recognized state after 30 seconds." + ); + return true; + } + env.log("Overworld detected."); + pbf_mash_button(context, BUTTON_B, 500ms); + context.wait_for_all_requests(); + return false; } break; } // select BAG (selection arrow does not wrap around) - pbf_move_left_joystick(context, {+1, 0}, 200ms, 300ms); - pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); - pbf_move_left_joystick(context, {+1, 0}, 200ms, 300ms); - pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + pbf_move_left_joystick(context, {+1, 0}, 100ms, 150ms); + pbf_move_left_joystick(context, {0, +1}, 100ms, 150ms); + pbf_move_left_joystick(context, {+1, 0}, 100ms, 150ms); + pbf_move_left_joystick(context, {0, +1}, 100ms, 150ms); BagWatcher bag_open(COLOR_RED); int ret2 = run_until( @@ -308,38 +339,6 @@ bool StaticRng::auto_catch(SingleSwitchProgramEnvironment& env, ProControllerCon // use ball pbf_mash_button(context, BUTTON_A, 5s); - - // handle catch detection separately, since the white screen was also triggering the BattleMenuDetector - count = 0; - while (true){ - if (count >= 10){ - return false; - } - count++; - - BlackScreenWatcher black_screen(COLOR_RED); - WhiteScreenOverWatcher white_screen(COLOR_RED); - context.wait_for_all_requests(); - int ret = run_until( - env.console, context, - [](ProControllerContext& context) { - pbf_mash_button(context, BUTTON_B, 20s); - }, - { black_screen, white_screen } - ); - switch (ret){ - case 0: - env.log("Black screen detected. Battle exited."); - return false; - case 1: - env.log("White screen detected. Skipping through Pokedex entry..."); - continue; - default: - break; - } - - break; - } } env.log("auto_catch(): ran out of balls."); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 91402740e4..66bf335541 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1471,6 +1471,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Inference/Menus/PokemonFRLG_PartyMenuDetector.h Source/PokemonFRLG/Inference/Menus/PokemonFRLG_TrainerCardDetector.cpp Source/PokemonFRLG/Inference/Menus/PokemonFRLG_TrainerCardDetector.h + Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.cpp + Source/PokemonFRLG/Inference/Menus/PokemonFRLG_DexRegistrationDetector.h Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.cpp Source/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.h Source/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp From 83677d3f129c0ea2d83cda4e4e83cf148f520049 Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Tue, 28 Apr 2026 16:33:18 -0500 Subject: [PATCH 5/8] oops --- SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h b/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h index fbababc1db..5b0511cd8c 100644 --- a/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h +++ b/SerialPrograms/Source/Pokemon/Pokemon_AdvRng.h @@ -135,12 +135,6 @@ void level_up_observed_pokemon(AdvObservedPokemon& pokemon, const StatReads& new // returns the appropriate NatureAdjustments for an AdvNature Pokemon::NatureAdjustments nature_to_adjustment(AdvNature nature); -// returns the appropriate NatureAdjustments for an AdvNature -Pokemon::NatureAdjustments nature_to_adjustment(AdvNature nature); - -// returns the appropriate NatureAdjustments for an AdvNature -Pokemon::NatureAdjustments nature_to_adjustment(AdvNature nature); - // returns search filters that correspond with observed stats AdvRngFilters observation_to_filters(const AdvObservedPokemon& observation, const BaseStats& basestats, AdvRngMethod method = AdvRngMethod::Method1); From 8a9d9fec14ba463b44b38d4d62e185605410b0f0 Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Tue, 28 Apr 2026 17:36:20 -0500 Subject: [PATCH 6/8] fix int comparison --- .../Programs/RngManipulation/PokemonFRLG_StaticRng.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp index 790b933485..3e362d0b63 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp @@ -235,7 +235,7 @@ bool StaticRng::have_hit_target(SingleSwitchProgramEnvironment& env, const uint3 } bool StaticRng::auto_catch(SingleSwitchProgramEnvironment& env, ProControllerContext& context, const uint64_t& MAX_BALL_THROWS){ - for (int i=0; i= 10){ From 2bc960987f08605250962e68cef5c44be99b3d22 Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Wed, 29 Apr 2026 09:50:51 -0500 Subject: [PATCH 7/8] stricter minimum seed duration --- .../Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp | 4 ++-- .../Programs/RngManipulation/PokemonFRLG_GiftRng.cpp | 2 +- .../Programs/RngManipulation/PokemonFRLG_RngHelper.cpp | 2 +- .../Programs/RngManipulation/PokemonFRLG_StarterRng.cpp | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp index 87cc208b64..f671ecaff0 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp @@ -365,10 +365,10 @@ void check_timings( console ); } - if (SEED_DELAY < 28000){ + if (SEED_DELAY < 30000){ OperationFailedException::fire( ErrorReport::SEND_ERROR_REPORT, - "The title screen delay cannot be less than 28000ms. Check your seed calibration.", + "The title screen delay cannot be less than 30s. Check your seed calibration.", console ); } diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp index cbe8c4aa64..21eb95821e 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp @@ -156,7 +156,7 @@ GiftRng::GiftRng() , SEED_DELAY( "Seed Delay Time (ms):
The delay between starting the game and advancing past the title screen. Set this to match your target seed.", LockMode::LOCK_WHILE_RUNNING, - 31338, 28000 // default, min + 31338, 30470 // default, min ) , ADVANCES( "Advances:
The total number of RNG advances for your target.
This should be the combined amount of continue screen and in-game advances.", diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngHelper.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngHelper.cpp index 9e64e28cd8..c46dc9263a 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngHelper.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngHelper.cpp @@ -119,7 +119,7 @@ RngHelper::RngHelper() "Seed Delay Time (ms):
" "The delay between starting the game and advancing past the title screen. Set this to match your target seed.", LockMode::LOCK_WHILE_RUNNING, - 35000, 28000 // default, min + 35000, 30470 // default, min ) , SEED_CALIBRATION( "Seed Calibration (ms):" diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp index a57a9b907f..76efaf159c 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp @@ -143,7 +143,7 @@ StarterRng::StarterRng() , SEED_DELAY( "Seed Delay Time (ms):
The delay between starting the game and advancing past the title screen. Set this to match your target seed.", LockMode::LOCK_WHILE_RUNNING, - 31338, 28000 // default, min + 31338, 30470 // default, min ) , ADVANCES( "Advances:
The total number of RNG advances for your target.
This should be the combined amount of continue screen and in-game advances.", From 1f5c81b28ba16e826c11f64d9721c945484d7644 Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Wed, 29 Apr 2026 12:05:20 -0500 Subject: [PATCH 8/8] fix minimum seed delay duration check --- .../Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp index f671ecaff0..6af5e48de7 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp @@ -365,10 +365,10 @@ void check_timings( console ); } - if (SEED_DELAY < 30000){ + if (SEED_DELAY < 29500){ OperationFailedException::fire( ErrorReport::SEND_ERROR_REPORT, - "The title screen delay cannot be less than 30s. Check your seed calibration.", + "The title screen delay cannot be less than 29.5s. Check your seed calibration.", console ); }