diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index f71d6b42f3..e1f2ddcfcf 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -46,6 +46,7 @@ #include "Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.h" +#include "Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngTargetSearch_Tests.h" #include "Tests/TestUtils.h" @@ -723,6 +724,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_RngPlan(database); add_tests_RngPlanTracker(database); add_tests_RngPlanLog(database); + add_tests_RngSteps(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.cpp new file mode 100644 index 0000000000..32b1d9d90f --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.cpp @@ -0,0 +1,283 @@ +/* RNG Steps Tests + * + * From: https://github.com/PokemonAutomation/ + * + * SID Helper's intro timing per language, and Starter RNG's per-starter + * tables, as SID Helper and Starter RNG computed them before the steps + * moved out of the programs; and whether a shiny was the target. + * + */ + +#include +#include +#include +#include +#include +#include "CommonFramework/Language.h" +#include "PokemonFRLG_SidSteps.h" +#include "PokemonFRLG_StarterRngAttempts.h" +#include "PokemonFRLG_RngSteps_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +struct SidTimingRow{ + Language language; + uint32_t target_advances; + uint64_t delay_ms; + bool extra_press_at_end; +}; + +// (target_advances - 2 * final text frames + 7) frames of 1000 / 59.999977 ms, +// halved since advances pass 2 by 2. +const std::vector SID_TIMING_ROWS = { + {.language = Language::English, .target_advances = 3001, .delay_ms = 20916, .extra_press_at_end = false}, + {.language = Language::English, .target_advances = 2275, .delay_ms = 14866, .extra_press_at_end = false}, + {.language = Language::Japanese, .target_advances = 3001, .delay_ms = 21833, .extra_press_at_end = true}, + {.language = Language::Italian, .target_advances = 3001, .delay_ms = 21133, .extra_press_at_end = false}, + {.language = Language::French, .target_advances = 3001, .delay_ms = 21650, .extra_press_at_end = true}, + {.language = Language::German, .target_advances = 3001, .delay_ms = 21600, .extra_press_at_end = true}, + {.language = Language::Spanish, .target_advances = 2275, .delay_ms = 15650, .extra_press_at_end = true}, +}; + +class Test_SidTiming : public UnitTest{ +public: + explicit Test_SidTiming(const SidTimingRow& row) + : UnitTest(std::format( + "PokemonFRLG::RngSteps - SID timing {} {}", + language_data(row.language).name, row.target_advances + )) + , m_row(row) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + SidTiming timing = sid_timing(m_row.language, m_row.target_advances); + if (timing.delay_ms != m_row.delay_ms || timing.extra_press_at_end != m_row.extra_press_at_end){ + return std::format( + "Expected delay {} ms, extra press {}; got {} ms, extra press {}", + m_row.delay_ms, m_row.extra_press_at_end, + timing.delay_ms, timing.extra_press_at_end + ); + } + return true; + } + +private: + SidTimingRow m_row; +}; + + +struct StarterRow{ + StarterSpecies starter; + std::string name; + BaseStats base_stats; + EVs rival_ev_yield; + int steps_left; +}; + +const std::vector STARTER_ROWS = { + { + .starter = StarterSpecies::bulbasaur, + .name = "Bulbasaur", + .base_stats = {45, 49, 49, 65, 65, 45}, + .rival_ev_yield = {0, 0, 0, 0, 0, 1}, + .steps_left = 2, + }, + { + .starter = StarterSpecies::squirtle, + .name = "Squirtle", + .base_stats = {44, 48, 65, 50, 64, 43}, + .rival_ev_yield = {0, 0, 0, 1, 0, 0}, + .steps_left = 3, + }, + { + .starter = StarterSpecies::charmander, + .name = "Charmander", + .base_stats = {39, 52, 43, 60, 50, 65}, + .rival_ev_yield = {0, 0, 1, 0, 0, 0}, + .steps_left = 4, + }, +}; + +std::string to_string(const BaseStats& stats){ + return std::format("{}/{}/{}/{}/{}/{}", stats.hp, stats.attack, stats.defense, stats.spatk, stats.spdef, stats.speed); +} +std::string to_string(const EVs& evs){ + return std::format("{}/{}/{}/{}/{}/{}", evs.hp, evs.attack, evs.defense, evs.spatk, evs.spdef, evs.speed); +} + +class Test_StarterTables : public UnitTest{ +public: + explicit Test_StarterTables(const StarterRow& row) + : UnitTest("PokemonFRLG::RngSteps - Starter tables " + row.name) + , m_row(row) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + std::string expected = std::format( + "base stats {}, rival EVs {}, steps left {}", + to_string(m_row.base_stats), to_string(m_row.rival_ev_yield), m_row.steps_left + ); + std::string got = std::format( + "base stats {}, rival EVs {}, steps left {}", + to_string(starter_base_stats(m_row.starter)), + to_string(rival_ev_yield(m_row.starter)), + steps_left_to_lab_door(m_row.starter) + ); + if (expected != got){ + return "Expected " + expected + "; got " + got; + } + return true; + } + +private: + StarterRow m_row; +}; + + + +// Target: seed 0x70FE, advance 1000. +struct ShinyRow{ + std::string name; + // The (seed, advance) of each state the shiny's stats match. + std::vector> hits; + StarterRngOutcome expected; +}; + +const std::vector SHINY_ROWS = { + {.name = "the target alone", .hits = {{0x70fe, 1000}}, .expected = StarterRngOutcome::shiny_on_target}, + {.name = "the target among others", .hits = {{0x70fe, 998}, {0x70fe, 1000}, {0x70ff, 1000}}, .expected = StarterRngOutcome::shiny_on_target}, + {.name = "another advance", .hits = {{0x70fe, 1002}}, .expected = StarterRngOutcome::shiny_off_target}, + {.name = "another seed", .hits = {{0x70fd, 1000}}, .expected = StarterRngOutcome::shiny_off_target}, + {.name = "no match", .hits = {}, .expected = StarterRngOutcome::shiny}, +}; + +const char* outcome_name(StarterRngOutcome outcome){ + switch (outcome){ + case StarterRngOutcome::shiny: return "shiny"; + case StarterRngOutcome::shiny_on_target: return "shiny_on_target"; + case StarterRngOutcome::shiny_off_target: return "shiny_off_target"; + case StarterRngOutcome::target_hit: return "target_hit"; + case StarterRngOutcome::wild_shiny: return "wild_shiny"; + case StarterRngOutcome::out_of_resets: return "out_of_resets"; + case StarterRngOutcome::no_matches: return "no_matches"; + } + return "?"; +} + +class Test_IdentifyShiny : public UnitTest{ +public: + explicit Test_IdentifyShiny(const ShinyRow& row) + : UnitTest("PokemonFRLG::RngSteps - Shiny " + row.name) + , m_row(row) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + std::vector hits; + for (const auto& [seed, advance] : m_row.hits){ + AdvRngState hit{}; + hit.seed = seed; + hit.advance = advance; + hits.push_back(hit); + } + const StarterRngOutcome got = identify_shiny(0x70fe, 1000, hits); + if (got != m_row.expected){ + return std::string("Expected ") + outcome_name(m_row.expected) + ", got " + outcome_name(got); + } + return true; + } + +private: + ShinyRow m_row; +}; + + +AdvRngState state(uint16_t seed, uint64_t advance){ + AdvRngState hit{}; + hit.seed = seed; + hit.advance = advance; + return hit; +} + +// A state's (seed, advance). +using SeedAdvance = std::pair; + +std::string state_text(const std::optional& state){ + return state ? std::format("{:04x}/{}", state->first, state->second) : "none"; +} + +struct ResetHitRow{ + std::string name; + // The reset's hits, and what update_history() left after it. + std::vector hits; + size_t uncertain = 0; + std::vector calibrated; + // The (seed, advance) taken as hit, if any. + std::optional expected; +}; + +const std::vector RESET_HIT_ROWS = { + {.name = "one hit", .hits = {state(0x70fe, 1001)}, .uncertain = 0, .calibrated = {state(0x70fe, 1001)}, .expected = SeedAdvance{0x70fe, 1001}}, + { + .name = "settled by the history", .hits = {state(0x70fe, 998), state(0x70ff, 1003)}, .uncertain = 0, + .calibrated = {state(0x70fd, 1004), state(0x70ff, 1003)}, .expected = SeedAdvance{0x70ff, 1003}, + }, + {.name = "a tie", .hits = {state(0x70fe, 998), state(0x70ff, 1003)}, .uncertain = 2, .calibrated = {state(0x70fd, 1004)}, .expected = std::nullopt}, + {.name = "no hits", .hits = {}, .uncertain = 0, .calibrated = {state(0x70fd, 1004)}, .expected = std::nullopt}, +}; + +class Test_ResetHit : public UnitTest{ +public: + explicit Test_ResetHit(const ResetHitRow& row) + : UnitTest("PokemonFRLG::RngSteps - Reset hit " + row.name) + , m_row(row) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + RngUncertainHistory uncertain; + uncertain.results.resize(m_row.uncertain); + RngCalibrationHistory calibration; + calibration.results = m_row.calibrated; + const std::optional hit = reset_hit(uncertain, calibration, m_row.hits); + std::optional got; + if (hit){ + got = SeedAdvance{hit->seed, hit->advance}; + } + if (got != m_row.expected){ + return "Expected " + state_text(m_row.expected) + ", got " + state_text(got); + } + return true; + } + +private: + ResetHitRow m_row; +}; + +} + + +void add_tests_RngSteps(UnitTestDatabase& database){ + for (const SidTimingRow& row : SID_TIMING_ROWS){ + database.add(row); + } + for (const StarterRow& row : STARTER_ROWS){ + database.add(row); + } + for (const ShinyRow& row : SHINY_ROWS){ + database.add(row); + } + for (const ResetHitRow& row : RESET_HIT_ROWS){ + database.add(row); + } +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.h new file mode 100644 index 0000000000..a5bca3130c --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.h @@ -0,0 +1,24 @@ +/* RNG Steps Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngSteps_Tests_H +#define PokemonAutomation_PokemonFRLG_RngSteps_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The pure parts of the SID steps and Starter RNG attempts. +void add_tests_RngSteps(UnitTestDatabase& database); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp index cb79d9241f..c3b2e0f680 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp @@ -9,8 +9,6 @@ #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/ProgramStats/StatsTracking.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "Pokemon/Pokemon_Strings.h" #include "Pokemon/Pokemon_AdvRng.h" @@ -19,7 +17,7 @@ #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h" -#include "PokemonFRLG_RngCalibration.h" +#include "PokemonFRLG_SidSteps.h" #include "PokemonFRLG_SidHelper.h" namespace PokemonAutomation{ @@ -101,74 +99,6 @@ SidHelper::SidHelper() namespace{ -void set_sid_from_name_screen( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - const uint64_t& SID_DELAY, - bool extra_press_at_end -){ - // this is performed blind to try to maximize consistency of timing - // ensure the OK button is selected - pbf_press_button(context, BUTTON_PLUS, 200ms, 300ms); - // confirm name (SID delay starts now) - pbf_press_button(context, BUTTON_A, 200ms, 1800ms); - // advance to "...about to unfold!", picking the default rival name - pbf_press_button(context, BUTTON_A, 200ms, 2300ms); - pbf_press_button(context, BUTTON_A, 200ms, 1050ms); - pbf_press_button(context, BUTTON_A, 200ms, 1550ms); - pbf_move_left_joystick(context, {0,-1}, 200ms, 300ms); - pbf_press_button(context, BUTTON_A, 200ms, 1050ms); - pbf_press_button(context, BUTTON_A, 200ms, 1550ms); - pbf_press_button(context, BUTTON_A, 200ms, 2300ms); - - // 2000 + 2500 + 1250 + 1750 + 500 + 1250 + 1750 + 2500 + 200 ms - Milliseconds delay(SID_DELAY > 13700 - ? SID_DELAY - 13700 - : 0 - ); - - if (extra_press_at_end){ - pbf_press_button(context, BUTTON_A, 200ms, 1300ms); - delay = delay > 1500ms ? delay - 1500ms : 0ms; - } - - pbf_press_button(context, BUTTON_A, 200ms, delay); - - // finish dialogue and lock in SID - pbf_press_button(context, BUTTON_A, 200ms, 0ms); - context.wait_for_all_requests(); -} - -void finish_intro_animations(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - BlackScreenWatcher black_screen; - context.wait_for_all_requests(); - int ret = wait_until( - env.console, context, 10s, - { black_screen } - ); - - if (ret < 0){ - OperationFailedExceptionWithScreenshot::fire( - ErrorReportMode::SEND_ERROR_REPORT, "SidHelper(): black screen not detected within 10 seconds of setting SID", env.console - ); - } - - BlackScreenOverWatcher black_screen_over; - context.wait_for_all_requests(); - int ret2 = wait_until( - env.console, context, 10s, - { black_screen_over } - ); - - if (ret2 < 0){ - env.log("finish_intro_animations(): end of black screen not detected within 10 seconds. Continuing anyway..."); - // if there's a problem, an error will be thrown when trying to navigate to the trainer card - } - - pbf_wait(context, 2000ms); - context.wait_for_all_requests(); -} - std::vector> get_sid_messages( SingleSwitchProgramEnvironment& env, ProControllerContext& context, @@ -213,44 +143,11 @@ void SidHelper::program(SingleSwitchProgramEnvironment& env, ProControllerContex // SidHelper_Descriptor::Stats& stats = env.current_stats(); - uint64_t FINAL_TEXT_FRAMES; Language lang = LANGUAGE; - switch (lang){ - case Language::English: - FINAL_TEXT_FRAMES = 249; - break; - case Language::Japanese: - FINAL_TEXT_FRAMES = 194; - break; - case Language::Italian: - FINAL_TEXT_FRAMES = 236; - break; - case Language::French: - FINAL_TEXT_FRAMES = 205; - break; - case Language::German: - FINAL_TEXT_FRAMES = 208; - break; - case Language::Spanish: - FINAL_TEXT_FRAMES = 202; - break; - default: - FINAL_TEXT_FRAMES = 249; - } - - static const int64_t FIXED_ADVANCES_OFFSET = 7; // determined empirically. Probably not console/setup dependent - - bool extra_press_at_end = ( - LANGUAGE == Language::German || - LANGUAGE == Language::Japanese || - LANGUAGE == Language::Spanish || - LANGUAGE == Language::French - ); - - const uint64_t SID_DELAY = uint64_t((TARGET_ADVANCES - 2*FINAL_TEXT_FRAMES + FIXED_ADVANCES_OFFSET) * FRLG_FRAME_DURATION / 2); // advances pass 2 by 2 - env.log("Delay: " + std::to_string(SID_DELAY) + "ms"); + const SidTiming timing = sid_timing(lang, TARGET_ADVANCES); + env.log("Delay: " + std::to_string(timing.delay_ms) + "ms"); - set_sid_from_name_screen(env, context, SID_DELAY, extra_press_at_end); + set_sid_from_name_screen(context, timing); finish_intro_animations(env, context); navigate_to_trainer_card(env.console, context); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.cpp new file mode 100644 index 0000000000..701411f827 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.cpp @@ -0,0 +1,124 @@ +/* SID Steps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonFRLG_RngCalibration.h" +#include "PokemonFRLG_SidSteps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +SidTiming sid_timing(Language language, uint32_t target_advances){ + uint64_t FINAL_TEXT_FRAMES; + switch (language){ + case Language::English: + FINAL_TEXT_FRAMES = 249; + break; + case Language::Japanese: + FINAL_TEXT_FRAMES = 194; + break; + case Language::Italian: + FINAL_TEXT_FRAMES = 236; + break; + case Language::French: + FINAL_TEXT_FRAMES = 205; + break; + case Language::German: + FINAL_TEXT_FRAMES = 208; + break; + case Language::Spanish: + FINAL_TEXT_FRAMES = 202; + break; + default: + FINAL_TEXT_FRAMES = 249; + } + + static const int64_t FIXED_ADVANCES_OFFSET = 7; // determined empirically. Probably not console/setup dependent + + bool extra_press_at_end = ( + language == Language::German || + language == Language::Japanese || + language == Language::Spanish || + language == Language::French + ); + + const uint64_t SID_DELAY = uint64_t((target_advances - 2*FINAL_TEXT_FRAMES + FIXED_ADVANCES_OFFSET) * FRLG_FRAME_DURATION / 2); // advances pass 2 by 2 + return SidTiming{.delay_ms = SID_DELAY, .extra_press_at_end = extra_press_at_end}; +} + +void set_sid_from_name_screen(ProControllerContext& context, const SidTiming& timing){ + // this is performed blind to try to maximize consistency of timing + // ensure the OK button is selected + pbf_press_button(context, BUTTON_PLUS, 200ms, 300ms); + // confirm name (SID delay starts now) + pbf_press_button(context, BUTTON_A, 200ms, 1800ms); + // advance to "...about to unfold!", picking the default rival name + pbf_press_button(context, BUTTON_A, 200ms, 2300ms); + pbf_press_button(context, BUTTON_A, 200ms, 1050ms); + pbf_press_button(context, BUTTON_A, 200ms, 1550ms); + pbf_move_left_joystick(context, {0,-1}, 200ms, 300ms); + pbf_press_button(context, BUTTON_A, 200ms, 1050ms); + pbf_press_button(context, BUTTON_A, 200ms, 1550ms); + pbf_press_button(context, BUTTON_A, 200ms, 2300ms); + + // 2000 + 2500 + 1250 + 1750 + 500 + 1250 + 1750 + 2500 + 200 ms + Milliseconds delay(timing.delay_ms > 13700 + ? timing.delay_ms - 13700 + : 0 + ); + + if (timing.extra_press_at_end){ + pbf_press_button(context, BUTTON_A, 200ms, 1300ms); + delay = delay > 1500ms ? delay - 1500ms : 0ms; + } + + pbf_press_button(context, BUTTON_A, 200ms, delay); + + // finish dialogue and lock in SID + pbf_press_button(context, BUTTON_A, 200ms, 0ms); + context.wait_for_all_requests(); +} + +void finish_intro_animations(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + BlackScreenWatcher black_screen; + context.wait_for_all_requests(); + int ret = wait_until( + env.console, context, 10s, + { black_screen } + ); + + if (ret < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, "SidHelper(): black screen not detected within 10 seconds of setting SID", env.console + ); + } + + BlackScreenOverWatcher black_screen_over; + context.wait_for_all_requests(); + int ret2 = wait_until( + env.console, context, 10s, + { black_screen_over } + ); + + if (ret2 < 0){ + env.log("finish_intro_animations(): end of black screen not detected within 10 seconds. Continuing anyway..."); + // if there's a problem, an error will be thrown when trying to navigate to the trainer card + } + + pbf_wait(context, 2000ms); + context.wait_for_all_requests(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.h new file mode 100644 index 0000000000..f1c358fa20 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.h @@ -0,0 +1,50 @@ +/* SID Steps + * + * From: https://github.com/PokemonAutomation/ + * + * SID Helper's steps from the naming screen to the overworld, which set the + * SID by timing the last press of the intro. Shared with AutoStory. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_SidSteps_H +#define PokemonAutomation_PokemonFRLG_SidSteps_H + +#include +#include "CommonFramework/Language.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// When to press the last A of the intro to land on an advance. +struct SidTiming{ + // From confirming the player's name to the last press. + uint64_t delay_ms = 0; + // The language's intro has one more text box before the last press. + bool extra_press_at_end = false; +}; + +// The timing that sets the SID at "target_advances" (odd) in a game in +// "language". +SidTiming sid_timing(Language language, uint32_t target_advances); + +// From the player's naming screen, confirms the name, picks the default +// rival name and presses through the intro at "timing", which sets the SID. +// Runs blind; ends on the last press of the intro. +void set_sid_from_name_screen(ProControllerContext& context, const SidTiming& timing); + +// After set_sid_from_name_screen(): waits out the black screen into the +// overworld. Throws OperationFailedException if no black screen comes +// within 10 seconds. +void finish_intro_animations(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp index 5b22bee42f..63da6e6aa8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp @@ -6,22 +6,15 @@ #include "Common/Cpp/Strings/StringTools.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.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 "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" #include "Pokemon/Pokemon_Strings.h" -#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" -#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" -#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h" -#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "PokemonFRLG_RngNavigation.h" -#include "PokemonFRLG_HardReset.h" +#include "PokemonFRLG_RngCalibration.h" #include "PokemonFRLG_SeedsDatabase.h" +#include "PokemonFRLG_StarterRngAttempts.h" #include "PokemonFRLG_StarterRng.h" namespace PokemonAutomation{ @@ -42,26 +35,6 @@ StarterRng_Descriptor::StarterRng_Descriptor() ) {} -struct StarterRng_Descriptor::Stats : public StatsTracker{ - Stats() - : resets(m_stats["Resets"]) - , shinies(m_stats["Shinies"]) - , nonshiny(m_stats["Non-Shiny Hits"]) - , wildshinies(m_stats["Wild Shinies"]) - , 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("Wild Shinies", HIDDEN_IF_ZERO); - m_display_order.emplace_back("Errors", HIDDEN_IF_ZERO); - } - std::atomic& resets; - std::atomic& shinies; - std::atomic& nonshiny; - std::atomic& wildshinies; - std::atomic& errors; -}; std::unique_ptr StarterRng_Descriptor::make_stats() const{ return std::unique_ptr(new Stats()); } @@ -115,12 +88,12 @@ StarterRng::StarterRng() , STARTER( "Starter Species:", { - {Starter::bulbasaur, "bulbasaur", "Bulbasaur"}, - {Starter::squirtle, "squirtle", "Squirtle"}, - {Starter::charmander, "charmander", "Charmander"}, + {StarterSpecies::bulbasaur, "bulbasaur", "Bulbasaur"}, + {StarterSpecies::squirtle, "squirtle", "Squirtle"}, + {StarterSpecies::charmander, "charmander", "Charmander"}, }, LockMode::LOCK_WHILE_RUNNING, - Starter::bulbasaur + StarterSpecies::bulbasaur ) , SEED( false, @@ -214,352 +187,12 @@ void StarterRng::on_config_value_changed(void* object){ -bool StarterRng::walk_to_rival_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - // return to the overworld - pbf_mash_button(context, BUTTON_B, 5000ms); - int num_steps_to_the_left; - switch(STARTER){ - case Starter::bulbasaur: - num_steps_to_the_left = 2; - break; - case Starter::squirtle: - num_steps_to_the_left = 3; - break; - case Starter::charmander: - num_steps_to_the_left = 4; - break; - default: - num_steps_to_the_left = 2; - } - - // dodge rival - pbf_move_left_joystick(context, {0, -1}, 40ms, 460ms); - pbf_move_left_joystick(context, {0, -1}, 100ms, 400ms); - - // line up with the doorway - pbf_move_left_joystick(context, {-1, 0}, 40ms, 460ms); // pivot left - for (int i=0; i( - env.console, context, - [](ProControllerContext& context){ - for (int i=0; i<5; i++){ - ssf_press_left_joystick(context, {0, -1}, 0ms, 20000ms, 0ms); - ssf_mash1_button(context, BUTTON_B, 20000ms); - } - }, - { black_screen } - ); - - bool failed = ret < 0; - if (failed){ - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "walk_to_rival_battle(): failed to initiate battle." - ); - } - return failed; -} - -bool StarterRng::auto_battle_rival( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AdvObservedPokemon& pokemon, - AdvRngFilters& filters, - const BaseStats& BASE_STATS -){ - Pokemon::EVs evyield = {0, 0, 0, 0, 0, 0}; - switch(STARTER){ - case Starter::bulbasaur: - evyield.speed = 1; // from charmander - break; - case Starter::squirtle: - evyield.spatk = 1; // from bulbasaur - break; - case Starter::charmander: - evyield.defense = 1; // from squirtle - } - - // detect the battle menu - BattleMenuWatcher battle_ready(COLOR_RED); - context.wait_for_all_requests(); - int ret1 = run_until( - env.console, context, - [](ProControllerContext& context){ - pbf_mash_button(context, BUTTON_B, 30s); - }, - { battle_ready } - ); - if (ret1 < 0){ - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "auto_battle_rival(): failed to detect the battle menu." - ); - return true; - } - env.log("Battle started. Using first move..."); - - // perform the first move and get through Oak's dialogue, - // which messes up most detectors when it dims the screen - pbf_mash_button(context, BUTTON_A, 1000ms); // execute first move - context.wait_for_all_requests(); - int ret2 = run_until( - env.console, context, - [](ProControllerContext& context){ - pbf_mash_button(context, BUTTON_B, 30s); - }, - { battle_ready } - ); - if (ret2 < 0){ - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "auto_battle_rival(): failed to detect the battle menu." - ); - return true; - } - env.log("Oak tutorial dialogue finished. Mashing A..."); - - // mash A until somebody faints - BattleOpponentFaintWatcher player_won(COLOR_RED); - BattleFaintWatcher player_lost(COLOR_RED); - context.wait_for_all_requests(); - int ret3 = run_until( - env.console, context, - [](ProControllerContext& context){ - pbf_mash_button(context, BUTTON_A, 300s); - }, - { player_won, player_lost } - ); - - switch(ret3){ - case 0: - env.log("Won battle against rival. Watching for level-up stats..."); - break; - case 1: - env.log("Lost battle against rival."); - pbf_mash_button(context, BUTTON_B, 20s); // exit battle and dialogue - return false; - default: - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "auto_battle_rival(): no fainting detected with 5 minutes." - ); - return true; - } - - // slowly advance dialog until level-up stats are visible - BattleLevelUpWatcher level_up_stats(COLOR_RED, BattleLevelUpDialog::stats, LANGUAGE); - BlackScreenWatcher black_screen(COLOR_RED); - context.wait_for_all_requests(); - int ret4 = run_until( - env.console, context, - [](ProControllerContext& context){ - for(int i=0; i<60; i++){ - pbf_press_button(context, BUTTON_A, 200ms, 1800ms); - } - }, - { level_up_stats, black_screen } - ); - - switch(ret4){ - case 0: - env.log("Level-up stats detected."); - break; - case 1: - env.log("Battle exited without detecting level-up stats"); - return true; // will cause issues with keeping track of level and EVs - default: - env.log("auto_battle_rival(): no recognized state within 2 minutes of winning battle."); - return true; - } - - // read stats - BattleLevelUpReader 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, evyield, BASE_STATS); - RNG_FILTERS.set(filters); - - // exit battle - pbf_mash_button(context, BUTTON_B, 20s); - context.wait_for_all_requests(); - - return false; -} - - - -bool StarterRng::walk_to_route1_from_lab(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - // exit the lab - env.log("Exiting the lab..."); - BlackScreenWatcher black_screen(COLOR_RED); - context.wait_for_all_requests(); - int ret = run_until( - env.console, context, - [](ProControllerContext& context){ - pbf_move_left_joystick(context, {0, -1}, 10s, 0ms); - }, - { black_screen } - ); - - if (ret < 0){ - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "walk_to_route1_from_lab(): failed to exit lab." - ); - return true; - } - - env.log("Lab exited. Walking to Route 1..."); - pbf_wait(context, 5000ms); - pbf_move_left_joystick(context, {-1, 0}, 1280ms, 300ms); - pbf_move_left_joystick(context, {0, +1}, 3150ms, 300ms); - pbf_move_left_joystick(context, {+1, 0}, 330ms, 300ms); - pbf_move_left_joystick(context, {0, +1}, 720ms, 300ms); - context.wait_for_all_requests(); - return false; -} - -bool StarterRng::walk_to_route1_from_home(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - // exit the house - env.log("Exiting the house..."); - BlackScreenWatcher black_screen(COLOR_RED); - context.wait_for_all_requests(); - int ret = run_until( - env.console, context, - [](ProControllerContext& context){ - pbf_move_left_joystick(context, {0, -1}, 1000ms, 300ms); - pbf_move_left_joystick(context, {-1, 0}, 900ms, 300ms); - pbf_move_left_joystick(context, {0, -1}, 1000ms, 300ms); - }, - { black_screen } - ); - - if (ret < 0){ - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "walk_to_route1_from_home(): failed to exit the house." - ); - return true; - } - - env.log("House exited. Walking to Route 1..."); - pbf_wait(context, 5s); - pbf_move_left_joystick(context, {+1, 0}, 1370ms, 300ms); - pbf_move_left_joystick(context, {0, +1}, 1450ms, 300ms); - pbf_move_left_joystick(context, {+1, 0}, 300ms, 300ms); - pbf_move_left_joystick(context, {0, +1}, 1250ms, 300ms); - - return false; -} - -int StarterRng::autolevel_on_route1( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AdvObservedPokemon& pokemon, - AdvRngFilters& filters, - const BaseStats& BASE_STATS -){ - Pokemon::EVs evyield = {0, 0, 0, 0, 0, 0}; - Pokemon::StatReads stats; - - env.log("Arrived at Route 1."); - bool leftright = true; - context.wait_for_all_requests(); - - while (true){ - // trigger encounter - env.log("Triggering wild encounters..."); - int ret = grass_spin(env.console, context, leftright); - if (ret < 0){ - send_program_recoverable_error_notification( - env, NOTIFICATION_ERROR_RECOVERABLE, - "autolevel_on_route1(): failed to trigger encounter." - ); - return -1; - } - - if (ret == 1){ // shiny - return 1; - } - - // auto battle - BattleResult ret2 = spam_first_move(env.console, context); - - BattleLevelUpWatcher level_up(COLOR_RED, BattleLevelUpDialog::stats, LANGUAGE); - BlackScreenWatcher black_screen(COLOR_RED); - VideoSnapshot screen; - int ret3; - bool failed; - BattleLevelUpReader reader; - - switch (ret2){ - case BattleResult::opponentfainted: - env.log("Opponent fainted."); - evyield.speed++; // always rattata or pidgey - leftright = !leftright; - - context.wait_for_all_requests(); - ret3 = run_until( - env.console, context, - [](ProControllerContext& context){ - for (int i=0; i<5; i++){ - pbf_press_button(context, BUTTON_B, 200ms, 1800ms); - } - }, - { level_up, black_screen } - ); - - switch (ret3){ - case 0: - env.log("Level-up stats detected. Reading stats..."); - screen = env.console.video().snapshot(); - stats = reader.read_stats(env.logger(), screen); - update_filters(filters, pokemon, stats, evyield, BASE_STATS); - RNG_FILTERS.set(filters); - exit_wild_battle(env.console, context, false, true); - return 0; - case -1: - exit_wild_battle(env.console, context, false, true); - default: - pbf_wait(context, 1000ms); - context.wait_for_all_requests(); - continue; - } - case BattleResult::playerfainted: - env.log("Pokemon fainted. Mashing B through dialogues..."); - pbf_mash_button(context, BUTTON_B, 30s); // skip through a few transitions and lots of dialogue - failed = walk_to_route1_from_home(env, context); - if (failed){ - return -1; - } - context.wait_for_all_requests(); - leftright = true; - continue; - case BattleResult::outofpp: - case BattleResult::unknown: - default: - return -1; - } - } -} - void StarterRng::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ /* * Settings: Text Speed fast */ - StarterRng_Descriptor::Stats& stats = env.current_stats(); + StarterRngStats& stats = env.current_stats(); home_black_border_check(env.console, context); @@ -572,279 +205,41 @@ void StarterRng::program(SingleSwitchProgramEnvironment& env, ProControllerConte if (!seed_match.found){ throw UserSetupError(env.console, "The target Seed was not found in the seed database for this game version, language, and sound setting."); } - const std::vector SEED_VALUES = seed_match.seed_values; - const int16_t SEED_POSITION = seed_match.seed_position; - const uint64_t SEED_DELAY = seed_match.seed_delay; - const SeedButton SEED_BUTTON = seed_match.seed_button; - const BlackoutButton EXTRA_BUTTON = seed_match.extra_button; - env.log("Seed delay: " + std::to_string(SEED_DELAY) + "ms, button mode: " + seed_match.button_mode + env.log("Seed delay: " + std::to_string(seed_match.seed_delay) + "ms, button mode: " + seed_match.button_mode + ", column: " + seed_match.column_name); - BaseStats BASE_STATS; - switch (STARTER){ - case Starter::bulbasaur: - BASE_STATS = { 45, 49, 49, 65, 65, 45 }; - break; - case Starter::squirtle: - BASE_STATS = { 44, 48, 65, 50, 64, 43 }; - break; - case Starter::charmander: - BASE_STATS = { 39, 52, 43, 60, 50, 65 }; - break; - default: - break; - } - - static const int64_t FIXED_SEED_OFFSET = -845; // milliseconds, approximate - - static const uint64_t CONTINUE_SCREEN_FRAMES = 200; - - static const uint64_t INITIAL_ADVANCES_RADIUS = 1024; - - static const uint8_t MAX_HISTORY_LENGTH = 10; - - static const int16_t GENDER_THRESHOLD = 30; - - static const std::set SPECIES_LIST = { "bulbasaur", "squirtle", "charmander" }; - - env.log("RNG Target: " + std::to_string(STARTER.current_value())); - env.log("Target Seed: " + to_hex_string(TARGET_SEED)); - env.log("Target Advances: " + std::to_string(ADVANCES)); - - AdvRngSearcher searcher(TARGET_SEED, ADVANCES, AdvRngMethod::Method1); - AdvPokemonResult target_result = searcher.generate_pokemon(); - RNG_TARGET.set_target(target_result, GENDER_THRESHOLD); - log_target_pokemon(env.console, target_result); - - RngCalibrations calibrations = { - RNG_CALIBRATION.seed_calibration / FRLG_FRAME_DURATION, - RNG_CALIBRATION.csf_calibration, - RNG_CALIBRATION.advances_calibration - }; - log_calibrations(env.console, calibrations, true); - - Milliseconds launch_delay = INITIAL_LAUNCH_DELAY; - - RngUncertainHistory uncertain_history; - RngCalibrationHistory calibration_history; - - bool wildshiny_found = false; - - uint16_t failed_searches = 0; - - while (true){ - if (calibration_history.results.size() > 0){ - env.log("Checking for nonshiny target hit..."); - if (have_hit_target(TARGET_SEED, ADVANCES, calibration_history.results.back())){ - env.log("Target Hit!"); - stats.nonshiny++; - break; - } - env.log("Missed target."); - } - - if (failed_searches >= 5){ - env.log("Failed to find any matches 5 times in a row"); - OperationFailedExceptionWithScreenshot::fire( - ErrorReportMode::NO_ERROR_REPORT, - "Failed to find any matches 5 times in a row. Check your seed and advances settings.", - env.console - ); - break; - } - - if (stats.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 = get_advances_radius(env.console, calibration_history, INITIAL_ADVANCES_RADIUS); - - if (calibration_history.results.size() > 0){ - calibrations = get_calibrations(env.console, calibration_history, SEED_VALUES, SEED_POSITION, ADVANCES); - } - - // if previous resets had uncertain advances, slightly modify the seed delay to try to hit a different target - apply_seed_bump(calibrations, uncertain_history); - - uint64_t ingame_advances = ADVANCES - CONTINUE_SCREEN_FRAMES; - - RngTimings timings = prepare_timings( - env.console, PokemonFRLG_RngTarget::starters, - SEED_DELAY, CONTINUE_SCREEN_FRAMES, ingame_advances, - false, calibrations, - FIXED_SEED_OFFSET - ); - - env.log("Resetting Game..."); - reset_and_perform_blind_sequence( - env.console, context, - LANGUAGE, PokemonFRLG_RngTarget::starters, - SEED_BUTTON, EXTRA_BUTTON, timings, - launch_delay, false, PROFILE - ); - stats.resets++; - - RNG_FILTERS.reset(); - RNG_CALIBRATION.set_calibrations(calibrations); - RNG_CALIBRATION.reset_hits(); - - bool shiny_found = check_for_shiny(env.console, context, PokemonFRLG_RngTarget::starters); - - if (shiny_found){ - env.log("Shiny found!"); - stats.shinies++; - RNG_CALIBRATION.hits.set("Shiny!"); - 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; - } - - // Stage 1: initial search -- starter received - AdvObservedPokemon pokemon = read_summary(env.console, context, LANGUAGE, SPECIES_LIST); - pokemon.level[0] = 5; - AdvRngFilters filters = observation_to_filters(pokemon, BASE_STATS); - RNG_FILTERS.set(filters); - - std::vector search_hits = get_search_results(env.console, searcher, filters, SEED_VALUES, ADVANCES, advances_radius, GENDER_THRESHOLD); - RNG_CALIBRATION.set_hits(search_hits); - bool finished = update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 1 - ); - if (finished){ - env.log("RNG search finished."); - update_failed_searches(failed_searches, search_hits); - continue; - } - - // Stage 2: first search update -- post-rival-battle - bool failed = walk_to_rival_battle(env, context); - if (failed){ - update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 1, 2, true - ); - stats.errors++; - continue; // reset game - } - - failed = auto_battle_rival(env, context, pokemon, filters, BASE_STATS); - if (failed){ - update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 1, 2, true - ); - stats.errors++; - continue; // reset game - } - if (pokemon.level.size() > 1){ - search_hits = get_search_results(env.console, searcher, filters, SEED_VALUES, ADVANCES, advances_radius, GENDER_THRESHOLD); - RNG_CALIBRATION.set_hits(search_hits); - env.log("Number of search hits: " + std::to_string(search_hits.size())); - finished = update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 5 - ); - if (finished){ - env.log("RNG search finished."); - update_failed_searches(failed_searches, search_hits); - continue; - } - } - - // Stage 3: subsequent search updates -- leveling up from wild encounters - failed = walk_to_route1_from_lab(env, context); - if (failed){ - update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 1, 2, true - ); - stats.errors++; - continue; // reset game - } - - auto num_levels = pokemon.level.size(); - uint16_t MAX_LEVELS = 3; - while(true){ - if (num_levels > MAX_LEVELS){ - env.log("RNG search not complete after 3 level-ups."); - finished = update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 5, 2, true - ); - break; - } - - env.log("Level: " + std::to_string(4 + pokemon.level.size())); - env.log("Speed EVs: " + std::to_string(pokemon.evs.back().speed)); - - int ret2 = autolevel_on_route1(env, context, pokemon, filters, BASE_STATS); - if (ret2 < 0){ - update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 1, 2, true - ); - stats.errors++; - break; - }else if(ret2 == 1){ - env.log("Wild shiny found!"); - stats.wildshinies++; - send_program_notification( - env, - NOTIFICATION_SHINY, - COLOR_YELLOW, - "Wild Shiny found!", - {}, "", - env.console.video().snapshot(), - true - ); - if (TAKE_VIDEO){ - pbf_press_button(context, BUTTON_CAPTURE, 2000ms, 0ms); - } - if (!IGNORE_WILD_SHINIES){ - wildshiny_found = true; - break; - } - } - - // force the search to finish after enough level-ups - if (pokemon.level.size() > num_levels){ - num_levels = pokemon.level.size(); - search_hits = get_search_results(env.console, searcher, filters, SEED_VALUES, ADVANCES, advances_radius, GENDER_THRESHOLD); - RNG_CALIBRATION.set_hits(search_hits); - env.log("Number of search hits: " + std::to_string(search_hits.size())); - update_history( - env.console, uncertain_history, calibration_history, - MAX_HISTORY_LENGTH, calibrations, search_hits, 5, 2, true - ); - env.log("RNG search finished."); - update_failed_searches(failed_searches, search_hits); - break; - } - } - + StarterRngAttempts attempts( + StarterRngTarget{ + .starter = STARTER, + .seed = TARGET_SEED, + .advances = ADVANCES, + .seed_match = seed_match, + }, + StarterRngSettings{ + .language = LANGUAGE, + .profile = PROFILE, + .ignore_wild_shinies = IGNORE_WILD_SHINIES, + .take_video = TAKE_VIDEO, + .identify_shiny = false, + .max_resets = [this]{ return uint64_t(MAX_RESETS); }, + .on_reset = nullptr, + .on_read = nullptr, + }, + StarterRngDisplays{.target = RNG_TARGET, .filters = RNG_FILTERS, .calibration = RNG_CALIBRATION}, + StarterRngNotifications{ + .shiny = NOTIFICATION_SHINY, + .status_update = NOTIFICATION_STATUS_UPDATE, + .error_recoverable = NOTIFICATION_ERROR_RECOVERABLE, + }, + stats + ); + StarterRngResult result = attempts.run(env, context); + if (result.outcome == StarterRngOutcome::no_matches){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::NO_ERROR_REPORT, + "Failed to find any matches 5 times in a row. Check your seed and advances settings.", + env.console + ); } if (GO_HOME_WHEN_DONE){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.h index 4e99f55bda..c4fec01083 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.h @@ -18,6 +18,7 @@ #include "Pokemon/Pokemon_AdvRng.h" #include "PokemonFRLG_RngDisplays.h" #include "PokemonFRLG_SeedsDatabase.h" +#include "PokemonFRLG_StarterRngAttempts.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -26,7 +27,7 @@ namespace PokemonFRLG{ class StarterRng_Descriptor : public SingleSwitchProgramDescriptor{ public: StarterRng_Descriptor(); - struct Stats; + using Stats = StarterRngStats; virtual std::unique_ptr make_stats() const override; }; @@ -44,36 +45,11 @@ class StarterRng : public SingleSwitchProgramInstance, private ConfigOption::Lis private: virtual void on_config_value_changed(void* object) override; - enum class Starter{ - bulbasaur, - squirtle, - charmander - }; - enum class GameVersion{ firered, leafgreen }; - bool walk_to_rival_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - bool auto_battle_rival( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AdvObservedPokemon& pokemon, - AdvRngFilters& filters, - const BaseStats& BASE_STATS - ); - - bool walk_to_route1_from_lab(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - bool walk_to_route1_from_home(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - int autolevel_on_route1( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AdvObservedPokemon& pokemon, - AdvRngFilters& filters, - const BaseStats& BASE_STATS - ); - SectionDividerOption m_calibration_displays; RngTargetDisplay RNG_TARGET; RngFilterDisplay RNG_FILTERS; @@ -85,7 +61,7 @@ class StarterRng : public SingleSwitchProgramInstance, private ConfigOption::Lis EnumDropdownOption SOUND; SectionDividerOption m_target_settings; - EnumDropdownOption STARTER; + EnumDropdownOption STARTER; StringOption SEED; SimpleIntegerOptionADVANCES; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.cpp new file mode 100644 index 0000000000..ed67e21b42 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.cpp @@ -0,0 +1,845 @@ +/* Starter RNG Attempts + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" +#include "PokemonFRLG_HardReset.h" +#include "PokemonFRLG_RngNavigation.h" +#include "PokemonFRLG_StarterRngAttempts.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +const int64_t FIXED_SEED_OFFSET = -845; // milliseconds, approximate + +const uint64_t CONTINUE_SCREEN_FRAMES = 200; + +const uint64_t INITIAL_ADVANCES_RADIUS = 1024; + +const uint8_t MAX_HISTORY_LENGTH = 10; + +const int16_t GENDER_THRESHOLD = 30; + +const std::set& starter_species(){ + static const std::set SPECIES_LIST = { "bulbasaur", "squirtle", "charmander" }; + return SPECIES_LIST; +} + +} + + +BaseStats starter_base_stats(StarterSpecies starter){ + switch (starter){ + case StarterSpecies::bulbasaur: + return { 45, 49, 49, 65, 65, 45 }; + case StarterSpecies::squirtle: + return { 44, 48, 65, 50, 64, 43 }; + case StarterSpecies::charmander: + return { 39, 52, 43, 60, 50, 65 }; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown starter."); +} + +EVs rival_ev_yield(StarterSpecies starter){ + Pokemon::EVs evyield = {0, 0, 0, 0, 0, 0}; + switch(starter){ + case StarterSpecies::bulbasaur: + evyield.speed = 1; // from charmander + break; + case StarterSpecies::squirtle: + evyield.spatk = 1; // from bulbasaur + break; + case StarterSpecies::charmander: + evyield.defense = 1; // from squirtle + } + return evyield; +} + +int steps_left_to_lab_door(StarterSpecies starter){ + switch(starter){ + case StarterSpecies::bulbasaur: + return 2; + case StarterSpecies::squirtle: + return 3; + case StarterSpecies::charmander: + return 4; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown starter."); +} + + +StarterRngOutcome identify_shiny(uint16_t seed, uint64_t advances, const std::vector& hits){ + if (hits.empty()){ + return StarterRngOutcome::shiny; + } + const bool on_target = std::any_of(hits.begin(), hits.end(), [&](const AdvRngState& hit){ + return have_hit_target(seed, advances, hit); + }); + return on_target ? StarterRngOutcome::shiny_on_target : StarterRngOutcome::shiny_off_target; +} + +std::optional reset_hit( + const RngUncertainHistory& uncertain, const RngCalibrationHistory& calibration, const std::vector& hits +){ + // update_history() clears `uncertain` only when it takes a hit, and adds + // the reset's last after the earlier ones it settles. + if (hits.empty() || !uncertain.results.empty() || calibration.results.empty()){ + return std::nullopt; + } + return calibration.results.back(); +} + + +StarterRngStats::StarterRngStats() + : resets(m_stats["Resets"]) + , shinies(m_stats["Shinies"]) + , nonshiny(m_stats["Non-Shiny Hits"]) + , wildshinies(m_stats["Wild Shinies"]) + , 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("Wild Shinies", HIDDEN_IF_ZERO); + m_display_order.emplace_back("Errors", HIDDEN_IF_ZERO); +} + + +StarterRngAttempts::StarterRngAttempts( + StarterRngTarget target, + StarterRngSettings settings, + const StarterRngDisplays& displays, + const StarterRngNotifications& notifications, + StarterRngStats& stats +) + : m_target(std::move(target)) + , m_settings(std::move(settings)) + , m_displays(displays) + , m_notifications(notifications) + , m_stats(stats) + , m_base_stats(starter_base_stats(m_target.starter)) + , m_searcher(m_target.seed, m_target.advances, AdvRngMethod::Method1) + , m_launch_delay(INITIAL_LAUNCH_DELAY) +{} + + +StarterRngResult StarterRngAttempts::run(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("RNG Target: " + std::to_string(static_cast(m_target.starter))); + env.log("Target Seed: " + to_hex_string(m_target.seed)); + env.log("Target Advances: " + std::to_string(m_target.advances)); + + AdvPokemonResult target_result = m_searcher.generate_pokemon(); + m_displays.target.set_target(target_result, GENDER_THRESHOLD); + log_target_pokemon(env.console, target_result); + + m_calibrations = { + m_displays.calibration.seed_calibration / FRLG_FRAME_DURATION, + m_displays.calibration.csf_calibration, + m_displays.calibration.advances_calibration + }; + log_calibrations(env.console, m_calibrations, true); + + while (true){ + std::optional outcome = finished(env); + if (outcome){ + StarterRngResult result{.outcome = *outcome, .pokemon = std::nullopt}; + if (*outcome == StarterRngOutcome::target_hit){ + result.pokemon = target_result; + } + return result; + } + + reset_game(env, context); + + if (check_shiny_starter(env, context)){ + const StarterRngOutcome shiny = m_settings.identify_shiny ? identify_shiny_starter(env, context) : StarterRngOutcome::shiny; + const bool on_target = shiny == StarterRngOutcome::shiny_on_target; + report_read(true); + return StarterRngResult{.outcome = shiny, .pokemon = on_target ? std::optional(target_result) : std::nullopt}; + } + + run_attempt(env, context); + report_read(false); + } +} + +void StarterRngAttempts::report_read(bool shiny){ + if (m_settings.on_read){ + m_settings.on_read(RngResetRead{ + .resets = m_stats.resets.load(), + .hits = m_search_hits, + .hit = shiny ? std::nullopt : reset_hit(m_uncertain_history, m_calibration_history, m_search_hits), + .shiny = shiny, + }); + } +} + +std::optional StarterRngAttempts::finished(SingleSwitchProgramEnvironment& env){ + if (m_calibration_history.results.size() > 0){ + env.log("Checking for nonshiny target hit..."); + if (have_hit_target(m_target.seed, m_target.advances, m_calibration_history.results.back())){ + env.log("Target Hit!"); + m_stats.nonshiny++; + return StarterRngOutcome::target_hit; + } + env.log("Missed target."); + } + + if (m_failed_searches >= 5){ + env.log("Failed to find any matches 5 times in a row"); + return StarterRngOutcome::no_matches; + } + + if (m_stats.resets > m_settings.max_resets()){ + env.log("Max resets reached."); + return StarterRngOutcome::out_of_resets; + } + + if (m_wildshiny_found){ + return StarterRngOutcome::wild_shiny; + } + + return std::nullopt; +} + +void StarterRngAttempts::reset_game(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + send_program_status_notification( + env, m_notifications.status_update, + "Calibrating." + ); + env.update_stats(); + + + m_advances_radius = get_advances_radius(env.console, m_calibration_history, INITIAL_ADVANCES_RADIUS); + + const SeedMatch& seed_match = m_target.seed_match; + if (m_calibration_history.results.size() > 0){ + m_calibrations = get_calibrations(env.console, m_calibration_history, seed_match.seed_values, seed_match.seed_position, m_target.advances); + } + + // if previous resets had uncertain advances, slightly modify the seed delay to try to hit a different target + apply_seed_bump(m_calibrations, m_uncertain_history); + + uint64_t ingame_advances = m_target.advances - CONTINUE_SCREEN_FRAMES; + + RngTimings timings = prepare_timings( + env.console, PokemonFRLG_RngTarget::starters, + seed_match.seed_delay, CONTINUE_SCREEN_FRAMES, ingame_advances, + false, m_calibrations, + FIXED_SEED_OFFSET + ); + + env.log("Resetting Game..."); + reset_and_perform_blind_sequence( + env.console, context, + m_settings.language, PokemonFRLG_RngTarget::starters, + seed_match.seed_button, seed_match.extra_button, timings, + m_launch_delay, false, m_settings.profile + ); + m_stats.resets++; + m_search_hits.clear(); + if (m_settings.on_reset){ + m_settings.on_reset(StarterRngProgress{.resets = m_stats.resets.load(), .calibrations = m_calibrations}); + } + + m_displays.filters.reset(); + m_displays.calibration.set_calibrations(m_calibrations); + m_displays.calibration.reset_hits(); +} + +bool StarterRngAttempts::check_shiny_starter(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + bool shiny_found = check_for_shiny(env.console, context, PokemonFRLG_RngTarget::starters); + + if (shiny_found){ + env.log("Shiny found!"); + m_stats.shinies++; + m_displays.calibration.hits.set("Shiny!"); + send_shiny_notification(env, context, "Shiny found!"); + } + return shiny_found; +} + +// On the shiny's summary, as check_for_shiny() leaves it: reads it as +// stage 1 of an attempt does and searches near the target. +StarterRngOutcome StarterRngAttempts::identify_shiny_starter(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + AdvObservedPokemon pokemon = read_summary(env.console, context, m_settings.language, starter_species()); + pokemon.level[0] = 5; + AdvRngFilters filters = observation_to_filters(pokemon, m_base_stats); + m_displays.filters.set(filters); + const std::vector hits = search(env, filters); + m_displays.calibration.set_hits(hits); + const StarterRngOutcome outcome = identify_shiny(m_target.seed, m_target.advances, hits); + env.log(std::format( + "Shiny starter: {} states near the target match it; {}.", hits.size(), + outcome == StarterRngOutcome::shiny_on_target ? "the target is one of them" + : outcome == StarterRngOutcome::shiny_off_target ? "the target isn't" : "not known if it is the target" + )); + return outcome; +} + +void StarterRngAttempts::send_shiny_notification( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, const std::string& title +){ + VideoSnapshot screen = env.console.video().snapshot(); + send_program_notification( + env, + m_notifications.shiny, + COLOR_YELLOW, + title, + {}, "", + screen, + true + ); + if (m_settings.take_video){ + pbf_press_button(context, BUTTON_CAPTURE, 2000ms, 0ms); + } +} + +std::vector StarterRngAttempts::search(SingleSwitchProgramEnvironment& env, AdvRngFilters& filters){ + m_search_hits = get_search_results( + env.console, m_searcher, filters, m_target.seed_match.seed_values, + m_target.advances, m_advances_radius, GENDER_THRESHOLD + ); + return m_search_hits; +} + +void StarterRngAttempts::record_error(SingleSwitchProgramEnvironment& env, const std::vector& search_hits){ + update_history( + env.console, m_uncertain_history, m_calibration_history, + MAX_HISTORY_LENGTH, m_calibrations, search_hits, 1, 2, true + ); + m_stats.errors++; +} + + +void StarterRngAttempts::run_attempt(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + // Stage 1: initial search -- starter received + AdvObservedPokemon pokemon = read_summary(env.console, context, m_settings.language, starter_species()); + pokemon.level[0] = 5; + AdvRngFilters filters = observation_to_filters(pokemon, m_base_stats); + m_displays.filters.set(filters); + + std::vector search_hits = search(env, filters); + m_displays.calibration.set_hits(search_hits); + bool finished = update_history( + env.console, m_uncertain_history, m_calibration_history, + MAX_HISTORY_LENGTH, m_calibrations, search_hits, 1 + ); + if (finished){ + env.log("RNG search finished."); + update_failed_searches(m_failed_searches, search_hits); + return; + } + + if (!run_rival_stage(env, context, pokemon, filters, search_hits)){ + return; + } + run_route1_stage(env, context, pokemon, filters, search_hits); +} + +// Returns false when the attempt ends here. +bool StarterRngAttempts::run_rival_stage( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + AdvObservedPokemon& pokemon, AdvRngFilters& filters, std::vector& search_hits +){ + // Stage 2: first search update -- post-rival-battle + bool failed = walk_to_rival_battle(env, context); + if (failed){ + record_error(env, search_hits); + return false; // reset game + } + + failed = auto_battle_rival(env, context, pokemon, filters); + if (failed){ + record_error(env, search_hits); + return false; // reset game + } + if (pokemon.level.size() > 1){ + search_hits = search(env, filters); + m_displays.calibration.set_hits(search_hits); + env.log("Number of search hits: " + std::to_string(search_hits.size())); + bool finished = update_history( + env.console, m_uncertain_history, m_calibration_history, + MAX_HISTORY_LENGTH, m_calibrations, search_hits, 5 + ); + if (finished){ + env.log("RNG search finished."); + update_failed_searches(m_failed_searches, search_hits); + return false; + } + } + return true; +} + +void StarterRngAttempts::run_route1_stage( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + AdvObservedPokemon& pokemon, AdvRngFilters& filters, std::vector& search_hits +){ + // Stage 3: subsequent search updates -- leveling up from wild encounters + bool failed = walk_to_route1_from_lab(env, context); + if (failed){ + record_error(env, search_hits); + return; // reset game + } + + auto num_levels = pokemon.level.size(); + uint16_t MAX_LEVELS = 3; + while(true){ + if (num_levels > MAX_LEVELS){ + env.log("RNG search not complete after 3 level-ups."); + update_history( + env.console, m_uncertain_history, m_calibration_history, + MAX_HISTORY_LENGTH, m_calibrations, search_hits, 5, 2, true + ); + return; + } + + env.log("Level: " + std::to_string(4 + pokemon.level.size())); + env.log("Speed EVs: " + std::to_string(pokemon.evs.back().speed)); + + int ret2 = autolevel_on_route1(env, context, pokemon, filters); + if (ret2 < 0){ + record_error(env, search_hits); + return; + }else if(ret2 == 1){ + if (stop_on_wild_shiny(env, context)){ + return; + } + } + + // force the search to finish after enough level-ups + if (pokemon.level.size() > num_levels){ + finish_search(env, filters, search_hits); + return; + } + } +} + +// Returns whether the attempts stop for the wild shiny. +bool StarterRngAttempts::stop_on_wild_shiny(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("Wild shiny found!"); + m_stats.wildshinies++; + send_shiny_notification(env, context, "Wild Shiny found!"); + if (!m_settings.ignore_wild_shinies){ + m_wildshiny_found = true; + return true; + } + return false; +} + +void StarterRngAttempts::finish_search( + SingleSwitchProgramEnvironment& env, AdvRngFilters& filters, std::vector& search_hits +){ + search_hits = search(env, filters); + m_displays.calibration.set_hits(search_hits); + env.log("Number of search hits: " + std::to_string(search_hits.size())); + update_history( + env.console, m_uncertain_history, m_calibration_history, + MAX_HISTORY_LENGTH, m_calibrations, search_hits, 5, 2, true + ); + env.log("RNG search finished."); + update_failed_searches(m_failed_searches, search_hits); +} + + +bool StarterRngAttempts::walk_to_rival_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + // return to the overworld + pbf_mash_button(context, BUTTON_B, 5000ms); + int num_steps_to_the_left = steps_left_to_lab_door(m_target.starter); + + // dodge rival + pbf_move_left_joystick(context, {0, -1}, 40ms, 460ms); + pbf_move_left_joystick(context, {0, -1}, 100ms, 400ms); + + // line up with the doorway + pbf_move_left_joystick(context, {-1, 0}, 40ms, 460ms); // pivot left + for (int i=0; i( + env.console, context, + [](ProControllerContext& context){ + for (int i=0; i<5; i++){ + ssf_press_left_joystick(context, {0, -1}, 0ms, 20000ms, 0ms); + ssf_mash1_button(context, BUTTON_B, 20000ms); + } + }, + { black_screen } + ); + + bool failed = ret < 0; + if (failed){ + send_program_recoverable_error_notification( + env, m_notifications.error_recoverable, + "walk_to_rival_battle(): failed to initiate battle." + ); + } + return failed; +} + +// From the start of the rival battle: waits for the battle menu, uses the +// first move and waits out Oak's tutorial. Returns true on failure. +bool StarterRngAttempts::start_rival_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + // detect the battle menu + BattleMenuWatcher battle_ready(COLOR_RED); + context.wait_for_all_requests(); + int ret1 = run_until( + env.console, context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 30s); + }, + { battle_ready } + ); + if (ret1 < 0){ + send_program_recoverable_error_notification( + env, m_notifications.error_recoverable, + "auto_battle_rival(): failed to detect the battle menu." + ); + return true; + } + env.log("Battle started. Using first move..."); + + // perform the first move and get through Oak's dialogue, + // which messes up most detectors when it dims the screen + pbf_mash_button(context, BUTTON_A, 1000ms); // execute first move + context.wait_for_all_requests(); + int ret2 = run_until( + env.console, context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 30s); + }, + { battle_ready } + ); + if (ret2 < 0){ + send_program_recoverable_error_notification( + env, m_notifications.error_recoverable, + "auto_battle_rival(): failed to detect the battle menu." + ); + return true; + } + env.log("Oak tutorial dialogue finished. Mashing A..."); + return false; +} + +bool StarterRngAttempts::auto_battle_rival( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters +){ + Pokemon::EVs evyield = rival_ev_yield(m_target.starter); + + if (start_rival_battle(env, context)){ + return true; + } + + // mash A until somebody faints + BattleOpponentFaintWatcher player_won(COLOR_RED); + BattleFaintWatcher player_lost(COLOR_RED); + context.wait_for_all_requests(); + int ret3 = run_until( + env.console, context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_A, 300s); + }, + { player_won, player_lost } + ); + + switch(ret3){ + case 0: + env.log("Won battle against rival. Watching for level-up stats..."); + break; + case 1: + env.log("Lost battle against rival."); + pbf_mash_button(context, BUTTON_B, 20s); // exit battle and dialogue + return false; + default: + send_program_recoverable_error_notification( + env, m_notifications.error_recoverable, + "auto_battle_rival(): no fainting detected with 5 minutes." + ); + return true; + } + + return read_rival_level_up(env, context, pokemon, filters, evyield); +} + +// After winning the rival battle: reads the level-up stats and leaves the +// battle. Returns true on failure. +bool StarterRngAttempts::read_rival_level_up( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const EVs& evyield +){ + // slowly advance dialog until level-up stats are visible + BattleLevelUpWatcher level_up_stats(COLOR_RED, BattleLevelUpDialog::stats, m_settings.language); + BlackScreenWatcher black_screen(COLOR_RED); + context.wait_for_all_requests(); + int ret4 = run_until( + env.console, context, + [](ProControllerContext& context){ + for(int i=0; i<60; i++){ + pbf_press_button(context, BUTTON_A, 200ms, 1800ms); + } + }, + { level_up_stats, black_screen } + ); + + switch(ret4){ + case 0: + env.log("Level-up stats detected."); + break; + case 1: + env.log("Battle exited without detecting level-up stats"); + return true; // will cause issues with keeping track of level and EVs + default: + env.log("auto_battle_rival(): no recognized state within 2 minutes of winning battle."); + return true; + } + + read_rival_level_up_stats(env, pokemon, filters, evyield); + + // exit battle + pbf_mash_button(context, BUTTON_B, 20s); + context.wait_for_all_requests(); + + return false; +} + +void StarterRngAttempts::read_rival_level_up_stats( + SingleSwitchProgramEnvironment& env, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const EVs& evyield +){ + // read stats + BattleLevelUpReader 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, evyield, m_base_stats); + m_displays.filters.set(filters); +} + + + +bool StarterRngAttempts::walk_to_route1_from_lab(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + // exit the lab + env.log("Exiting the lab..."); + BlackScreenWatcher black_screen(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + env.console, context, + [](ProControllerContext& context){ + pbf_move_left_joystick(context, {0, -1}, 10s, 0ms); + }, + { black_screen } + ); + + if (ret < 0){ + send_program_recoverable_error_notification( + env, m_notifications.error_recoverable, + "walk_to_route1_from_lab(): failed to exit lab." + ); + return true; + } + + env.log("Lab exited. Walking to Route 1..."); + pbf_wait(context, 5000ms); + pbf_move_left_joystick(context, {-1, 0}, 1280ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 3150ms, 300ms); + pbf_move_left_joystick(context, {+1, 0}, 330ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 720ms, 300ms); + context.wait_for_all_requests(); + return false; +} + +bool StarterRngAttempts::walk_to_route1_from_home(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + // exit the house + env.log("Exiting the house..."); + BlackScreenWatcher black_screen(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + env.console, context, + [](ProControllerContext& context){ + pbf_move_left_joystick(context, {0, -1}, 1000ms, 300ms); + pbf_move_left_joystick(context, {-1, 0}, 900ms, 300ms); + pbf_move_left_joystick(context, {0, -1}, 1000ms, 300ms); + }, + { black_screen } + ); + + if (ret < 0){ + send_program_recoverable_error_notification( + env, m_notifications.error_recoverable, + "walk_to_route1_from_home(): failed to exit the house." + ); + return true; + } + + env.log("House exited. Walking to Route 1..."); + pbf_wait(context, 5s); + pbf_move_left_joystick(context, {+1, 0}, 1370ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 1450ms, 300ms); + pbf_move_left_joystick(context, {+1, 0}, 300ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 1250ms, 300ms); + + return false; +} + +int StarterRngAttempts::autolevel_on_route1( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters +){ + Pokemon::EVs evyield = {0, 0, 0, 0, 0, 0}; + + env.log("Arrived at Route 1."); + bool leftright = true; + context.wait_for_all_requests(); + + while (true){ + int ret = trigger_route1_encounter(env, context, leftright); + if (ret < 0){ + return -1; + } + + if (ret == 1){ // shiny + return 1; + } + + // auto battle + BattleResult ret2 = spam_first_move(env.console, context); + + switch (ret2){ + case BattleResult::opponentfainted: + env.log("Opponent fainted."); + evyield.speed++; // always rattata or pidgey + leftright = !leftright; + if (read_wild_level_up(env, context, pokemon, filters, evyield)){ + return 0; + } + continue; + case BattleResult::playerfainted: + if (return_to_route1_after_faint(env, context)){ + return -1; + } + leftright = true; + continue; + case BattleResult::outofpp: + case BattleResult::unknown: + default: + return -1; + } + } +} + +// Spins in the grass until a wild encounter. Returns grass_spin()'s result. +int StarterRngAttempts::trigger_route1_encounter( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, bool leftright +){ + // trigger encounter + env.log("Triggering wild encounters..."); + int ret = grass_spin(env.console, context, leftright); + if (ret < 0){ + send_program_recoverable_error_notification( + env, m_notifications.error_recoverable, + "autolevel_on_route1(): failed to trigger encounter." + ); + } + return ret; +} + +// After the player's Pokemon fainted: walks from home back to Route 1. +// Returns true on failure. +bool StarterRngAttempts::return_to_route1_after_faint(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("Pokemon fainted. Mashing B through dialogues..."); + pbf_mash_button(context, BUTTON_B, 30s); // skip through a few transitions and lots of dialogue + if (walk_to_route1_from_home(env, context)){ + return true; + } + context.wait_for_all_requests(); + return false; +} + +// After a wild Pokemon fainted: reads the level-up stats if they show and +// leaves the battle. Returns whether the stats were read. +bool StarterRngAttempts::read_wild_level_up( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const EVs& evyield +){ + BattleLevelUpWatcher level_up(COLOR_RED, BattleLevelUpDialog::stats, m_settings.language); + BlackScreenWatcher black_screen(COLOR_RED); + BattleLevelUpReader reader; + + context.wait_for_all_requests(); + int ret3 = run_until( + env.console, context, + [](ProControllerContext& context){ + for (int i=0; i<5; i++){ + pbf_press_button(context, BUTTON_B, 200ms, 1800ms); + } + }, + { level_up, black_screen } + ); + + switch (ret3){ + case 0:{ + env.log("Level-up stats detected. Reading stats..."); + VideoSnapshot screen = env.console.video().snapshot(); + Pokemon::StatReads stats = reader.read_stats(env.logger(), screen); + update_filters(filters, pokemon, stats, evyield, m_base_stats); + m_displays.filters.set(filters); + exit_wild_battle(env.console, context, false, true); + return true; + } + case -1: + exit_wild_battle(env.console, context, false, true); + [[fallthrough]]; + default: + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + return false; + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.h new file mode 100644 index 0000000000..f541f1f9f9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.h @@ -0,0 +1,263 @@ +/* Starter RNG Attempts + * + * From: https://github.com/PokemonAutomation/ + * + * Starter RNG's reset, calibrate and attempt loop, for one target. The + * Starter RNG program runs it with its options, stats and displays; another + * program (AutoStory) can run it with its own. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_StarterRngAttempts_H +#define PokemonAutomation_PokemonFRLG_StarterRngAttempts_H + +#include +#include +#include +#include +#include +#include "CommonFramework/Language.h" +#include "CommonFramework/Notifications/EventNotificationOption.h" +#include "CommonFramework/ProgramStats/StatsTracking.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Pokemon_AdvRng.h" +#include "Pokemon/Pokemon_StatsCalculation.h" +#include "PokemonFRLG_RngCalibration.h" +#include "PokemonFRLG_RngDisplays.h" +#include "PokemonFRLG_SeedsDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +enum class StarterSpecies{ + bulbasaur, + squirtle, + charmander +}; + +// The species' base stats. +BaseStats starter_base_stats(StarterSpecies starter); +// The EVs the rival's starter gives when the player's "starter" beats it. +EVs rival_ev_yield(StarterSpecies starter); +// After taking "starter" from its ball and dodging the rival, the steps +// left that line the player up with the lab's door. +int steps_left_to_lab_door(StarterSpecies starter); + + +struct StarterRngStats : public StatsTracker{ + StarterRngStats(); + + std::atomic& resets; + std::atomic& shinies; + std::atomic& nonshiny; + std::atomic& wildshinies; + std::atomic& errors; +}; + + +struct StarterRngTarget{ + StarterSpecies starter = StarterSpecies::bulbasaur; + uint16_t seed = 0; + uint64_t advances = 0; + // The seed's entry in the seeds database (found). + SeedMatch seed_match; +}; + +// Where an attempt is after a reset: the resets so far, and the +// calibration that reset was timed with. +struct StarterRngProgress{ + uint64_t resets = 0; + RngCalibrations calibrations{}; +}; + +// What a reset's reads came to, once its search is done. Gift RNG's +// attempts report theirs this way too. +struct RngResetRead{ + uint64_t resets = 0; + // The states near the target that the Pokemon read matches. + std::vector hits; + // The one the calibration took as hit; none when it couldn't tell. + std::optional hit; + // Shiny right after the reset; `hits` are then its summary's. + bool shiny = false; +}; + +// The state update_history() took as hit for a reset whose last search +// found `hits`: the newest in `calibration`. It takes none for no hits, and +// none for a tie between advances, which it leaves in `uncertain`. +std::optional reset_hit( + const RngUncertainHistory& uncertain, const RngCalibrationHistory& calibration, const std::vector& hits +); + +struct StarterRngSettings{ + Language language = Language::None; + // The Switch profile's position on the user selection screen; 0 for + // the default. + uint8_t profile = 0; + bool ignore_wild_shinies = false; + // Record a video when a shiny is found. + bool take_video = false; + // After a shiny, read its summary and search for the state hit, to + // tell whether it was the target (shiny_on_target or + // shiny_off_target). Off, the attempts end on the shiny at once. + bool identify_shiny = false; + // Read before each reset, since Starter RNG lets the user change it + // while running. + std::function max_resets; + // Called after each reset, if set, for a caller that logs the attempts. + std::function on_reset; + // Called after each reset's reads, if set, as on_reset is. + std::function on_read; +}; + +// Where the attempts show the target, the filters read so far and the +// calibration. The calibration display also holds the starting +// calibration. +struct StarterRngDisplays{ + RngTargetDisplay& target; + RngFilterDisplay& filters; + RngCalibrationDisplay& calibration; +}; + +struct StarterRngNotifications{ + EventNotificationOption& shiny; + EventNotificationOption& status_update; + EventNotificationOption& error_recoverable; +}; + + +enum class StarterRngOutcome{ + shiny, // The starter was shiny after a reset; not known if on the target. + shiny_on_target, // Shiny, and the target is among the states its stats match. + shiny_off_target, // Shiny, and the states its stats match aren't the target. + target_hit, // The calibration shows the target was hit; not shiny. + wild_shiny, // A wild shiny on Route 1, with ignore_wild_shinies off. + out_of_resets, // More resets than max_resets. + no_matches, // 5 searches in a row matched no state near the target. +}; + +// A shiny's outcome from the states its stats match near the target +// ("hits"): on the target if it is among them, off it if not, and just +// shiny if none match. +StarterRngOutcome identify_shiny(uint16_t seed, uint64_t advances, const std::vector& hits); + +struct StarterRngResult{ + StarterRngOutcome outcome = StarterRngOutcome::out_of_resets; + // The target's PID, nature and IVs, on target_hit and shiny_on_target. + std::optional pokemon; +}; + + +class StarterRngAttempts{ +public: + StarterRngAttempts( + StarterRngTarget target, + StarterRngSettings settings, + const StarterRngDisplays& displays, + const StarterRngNotifications& notifications, + StarterRngStats& stats + ); + StarterRngAttempts(const StarterRngAttempts&) = delete; + StarterRngAttempts& operator=(const StarterRngAttempts&) = delete; + + // From the HOME menu with the game closed: resets the game and takes + // the starter at the target's timing until the outcome is known. Each + // non-shiny starter is read, battled against the rival and leveled on + // Route 1 to find the state hit and recalibrate. Ends in the game on + // the last screen the attempts reached. Call once per object. + StarterRngResult run(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +private: + std::optional finished(SingleSwitchProgramEnvironment& env); + void reset_game(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + bool check_shiny_starter(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + StarterRngOutcome identify_shiny_starter(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + void send_shiny_notification(SingleSwitchProgramEnvironment& env, ProControllerContext& context, const std::string& title); + std::vector search(SingleSwitchProgramEnvironment& env, AdvRngFilters& filters); + void record_error(SingleSwitchProgramEnvironment& env, const std::vector& search_hits); + void report_read(bool shiny); + + void run_attempt(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + bool run_rival_stage( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + AdvObservedPokemon& pokemon, AdvRngFilters& filters, std::vector& search_hits + ); + void run_route1_stage( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + AdvObservedPokemon& pokemon, AdvRngFilters& filters, std::vector& search_hits + ); + bool stop_on_wild_shiny(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + void finish_search( + SingleSwitchProgramEnvironment& env, AdvRngFilters& filters, std::vector& search_hits + ); + + bool walk_to_rival_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + bool start_rival_battle(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + bool auto_battle_rival( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters + ); + bool read_rival_level_up( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const EVs& evyield + ); + void read_rival_level_up_stats( + SingleSwitchProgramEnvironment& env, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const EVs& evyield + ); + + bool walk_to_route1_from_lab(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + bool walk_to_route1_from_home(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + int trigger_route1_encounter( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, bool leftright + ); + bool return_to_route1_after_faint(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + int autolevel_on_route1( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters + ); + bool read_wild_level_up( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AdvObservedPokemon& pokemon, + AdvRngFilters& filters, + const EVs& evyield + ); + + const StarterRngTarget m_target; + const StarterRngSettings m_settings; + StarterRngDisplays m_displays; + StarterRngNotifications m_notifications; + StarterRngStats& m_stats; + const BaseStats m_base_stats; + + AdvRngSearcher m_searcher; + RngCalibrations m_calibrations{}; + Milliseconds m_launch_delay; + RngUncertainHistory m_uncertain_history; + RngCalibrationHistory m_calibration_history; + uint64_t m_advances_radius = 0; + bool m_wildshiny_found = false; + uint16_t m_failed_searches = 0; + // The last search's hits, for on_read. + std::vector m_search_hits; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index d7a99b9e74..2632021427 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1935,6 +1935,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngStatsDatabase.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngStatsDatabase.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngSteps_Tests.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch_Tests.cpp @@ -1945,8 +1947,12 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRng.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StaticRng.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_WildRng.cpp