From 4d019e4b3cf348b252165a12a33a498a69c80943 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 19:28:43 -0800 Subject: [PATCH 1/8] FRLG Gift RNG: make the attempts callable Move GiftRng::program's reset, calibrate and attempt loop into a GiftRngAttempts class whose run() returns the outcome, so another program (AutoStory) can run Gift RNG with its own options and displays. GiftRng::program builds it from its options and keeps its behaviour. The per-target base stats move into gift_rng_species(), tested along with the shiny outcome. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + .../RngManipulation/PokemonFRLG_GiftRng.cpp | 286 ++-------------- .../RngManipulation/PokemonFRLG_GiftRng.h | 3 +- .../PokemonFRLG_GiftRngAttempts.cpp | 316 ++++++++++++++++++ .../PokemonFRLG_GiftRngAttempts.h | 185 ++++++++++ .../PokemonFRLG_GiftRngAttempts_Tests.cpp | 159 +++++++++ .../PokemonFRLG_GiftRngAttempts_Tests.h | 24 ++ SerialPrograms/cmake/SourceFiles.cmake | 4 + 8 files changed, 726 insertions(+), 253 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index d734e275f7..fdc0881e23 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -28,6 +28,7 @@ #include "Inference/PokemonFRLG_TrainerIdReader.h" #include "Inference/PokemonFRLG_WildEncounterReader.h" #include "PokemonFRLG_Navigation.h" +#include "Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h" #include "Tests/TestUtils.h" #include "PokemonFRLG_Tests.h" @@ -288,6 +289,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_BattleMoveSelectionArrow(database); add_tests_BagDetector(database); add_tests_ExitWildBattle(database); + add_tests_GiftRngAttempts(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp index 3341d8cef2..2abd7ef2e9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp @@ -38,23 +38,6 @@ GiftRng_Descriptor::GiftRng_Descriptor() ) {} -struct GiftRng_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 GiftRng_Descriptor::make_stats() const{ return std::unique_ptr(new Stats()); } @@ -232,7 +215,7 @@ void GiftRng::program(SingleSwitchProgramEnvironment& env, ProControllerContext& * Settings: Text Speed fast */ - GiftRng_Descriptor::Stats& stats = env.current_stats(); + GiftRngStats& stats = env.current_stats(); home_black_border_check(env.console, context); @@ -245,242 +228,41 @@ void GiftRng::program(SingleSwitchProgramEnvironment& env, ProControllerContext& 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; - int16_t GENDER_THRESHOLD = -1; - uint8_t LEVEL = 0; - switch (TARGET){ - case PokemonFRLG_RngTarget::magikarp: - BASE_STATS = { 20, 10, 55, 15, 20, 80 }; - GENDER_THRESHOLD = 126; - LEVEL = 5; - break; - case PokemonFRLG_RngTarget::hitmonchan: - BASE_STATS = { 50, 105, 79, 35, 110, 76 }; - GENDER_THRESHOLD = -1; - LEVEL = 25; - break; - case PokemonFRLG_RngTarget::hitmonlee: - BASE_STATS = { 50, 120, 53, 35, 110, 87 }; - GENDER_THRESHOLD = -1; - LEVEL = 25; - break; - case PokemonFRLG_RngTarget::eevee: - BASE_STATS = { 55, 55, 50, 45, 65, 55 }; - GENDER_THRESHOLD = 30; - LEVEL = 25; - break; - case PokemonFRLG_RngTarget::lapras: - BASE_STATS = { 130, 85, 80, 85, 95, 60 }; - GENDER_THRESHOLD = 126; - LEVEL = 25; - break; - case PokemonFRLG_RngTarget::omanyte: - BASE_STATS = { 35, 40, 100, 90, 55, 35 }; - GENDER_THRESHOLD = 30; - LEVEL = 5; - break; - case PokemonFRLG_RngTarget::kabuto: - BASE_STATS = { 30, 80, 90, 55, 45, 55 }; - GENDER_THRESHOLD = 30; - LEVEL = 5; - break; - case PokemonFRLG_RngTarget::aerodactyl: - BASE_STATS = { 80, 105, 65, 60, 75, 130 }; - GENDER_THRESHOLD = 30; - LEVEL = 5; - break; - case PokemonFRLG_RngTarget::gamecornerabra: - BASE_STATS = { 25, 20, 15, 105, 55, 90 }; - GENDER_THRESHOLD = 63; - break; - case PokemonFRLG_RngTarget::gamecornerclefairy: - BASE_STATS = { 70, 45, 48, 60, 65, 35 }; - GENDER_THRESHOLD = 190; - break; - case PokemonFRLG_RngTarget::gamecornerdratinifr: - case PokemonFRLG_RngTarget::gamecornerdratinilg: - BASE_STATS = { 41, 64, 45, 50, 50, 50 }; - GENDER_THRESHOLD = 126; - break; - case PokemonFRLG_RngTarget::gamecornerscyther: - BASE_STATS = { 70, 110, 80, 55, 80, 105 }; - GENDER_THRESHOLD = 126; - LEVEL = 25; - break; - case PokemonFRLG_RngTarget::gamecornerpinsir: - BASE_STATS = { 65, 125, 100, 55, 70, 85 }; - GENDER_THRESHOLD = 126; - LEVEL = 18; - break; - case PokemonFRLG_RngTarget::gamecornerporygon: - BASE_STATS = { 65, 60, 70, 85, 75, 40 }; - GENDER_THRESHOLD = -1; - break; - case PokemonFRLG_RngTarget::togepi: - case PokemonFRLG_RngTarget::togepifast: - BASE_STATS = { 35, 20, 65, 40, 65, 20 }; - GENDER_THRESHOLD = 30; - LEVEL = 5; - break; - default: - break; - } - - static const int64_t FIXED_SEED_OFFSET = -845; // milliseconds, approximate - - static const uint64_t CONTINUE_SCREEN_FRAMES = 200; - - const uint64_t INITIAL_ADVANCES_RADIUS = USE_TEACHY_TV ? 4096 : 1024; - - const uint8_t MAX_HISTORY_LENGTH = USE_TEACHY_TV ? 2 : 10; - - static const std::set SPECIES_LIST = { - "magikarp", "hitmonchan", "hitmonlee", "eevee", "lapras", - "omanyte", "kabuto", "aerodactyl", - "abra", "clefairy", "dratini", "scyther", "pinsir", "porygon", - "togepi" - }; - - env.log("RNG Target: " + std::to_string(TARGET.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; - - 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; - } - - 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, TARGET, - SEED_DELAY, CONTINUE_SCREEN_FRAMES, ingame_advances, - USE_TEACHY_TV, calibrations, - FIXED_SEED_OFFSET, LANGUAGE - ); - - env.log("Resetting Game..."); - reset_and_perform_blind_sequence( - env.console, context, LANGUAGE, TARGET, - 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, TARGET); - - 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; - } - - AdvObservedPokemon pokemon = read_summary(env.console, context, LANGUAGE, SPECIES_LIST); - if (LEVEL > 0){ - pokemon.level[0] = LEVEL; - } - AdvRngFilters filters = observation_to_filters(pokemon, BASE_STATS); - RNG_FILTERS.set(filters); - - std::vector search_hits = refine_calibration_with_rare_candy( - env, context, LANGUAGE, pokemon, filters, BASE_STATS, - uncertain_history, calibration_history, calibrations, - MAX_HISTORY_LENGTH, MAX_RARE_CANDIES, AdvRngMethod::Method1, false, - stats.errors, NOTIFICATION_ERROR_RECOVERABLE, - [&](AdvRngFilters& f){ - return get_search_results(env.console, searcher, f, SEED_VALUES, ADVANCES, advances_radius, GENDER_THRESHOLD); - }, - [&](const std::vector& h){ RNG_CALIBRATION.set_hits(h); }, - [&](const AdvRngFilters& f){ RNG_FILTERS.set(f); }, - [&](const std::vector& h){ - return all_equal(h) || all_indistinguishable(h, searcher, GENDER_THRESHOLD); - } - ); - - env.log("RNG search finished."); - update_failed_searches(failed_searches, search_hits); - + GiftRngAttempts attempts( + GiftRngTarget{ + .target = TARGET, + .seed = TARGET_SEED, + .advances = ADVANCES, + .seed_match = seed_match, + }, + GiftRngSettings{ + .language = LANGUAGE, + .profile = PROFILE, + .use_teachy_tv = USE_TEACHY_TV, + .take_video = TAKE_VIDEO, + .identify_shiny = false, + .max_resets = [this]{ return uint64_t(MAX_RESETS); }, + .max_rare_candies = [this]{ return uint64_t(MAX_RARE_CANDIES); }, + .on_reset = nullptr, + }, + GiftRngDisplays{.target = RNG_TARGET, .filters = RNG_FILTERS, .calibration = RNG_CALIBRATION}, + GiftRngNotifications{ + .shiny = NOTIFICATION_SHINY, + .status_update = NOTIFICATION_STATUS_UPDATE, + .error_recoverable = NOTIFICATION_ERROR_RECOVERABLE, + }, + stats + ); + GiftRngResult result = attempts.run(env, context); + if (result.outcome == GiftRngOutcome::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_GiftRng.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.h index 3df8e600ac..1f7d8463e5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.h @@ -15,6 +15,7 @@ #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "NintendoSwitch/Options/NintendoSwitch_GoHomeWhenDoneOption.h" #include "PokemonFRLG_BlindNavigation.h" +#include "PokemonFRLG_GiftRngAttempts.h" #include "PokemonFRLG_RngDisplays.h" #include "PokemonFRLG_SeedsDatabase.h" @@ -25,7 +26,7 @@ namespace PokemonFRLG{ class GiftRng_Descriptor : public SingleSwitchProgramDescriptor{ public: GiftRng_Descriptor(); - struct Stats; + using Stats = GiftRngStats; virtual std::unique_ptr make_stats() const override; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp new file mode 100644 index 0000000000..fd6c3c050a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp @@ -0,0 +1,316 @@ +/* Gift RNG Attempts + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonFRLG_RngNavigation.h" +#include "PokemonFRLG_HardReset.h" +#include "PokemonFRLG_RngLoopRoutines.h" +#include "PokemonFRLG_GiftRngAttempts.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 std::set& gift_species_names(){ + static const std::set SPECIES_LIST = { + "magikarp", "hitmonchan", "hitmonlee", "eevee", "lapras", + "omanyte", "kabuto", "aerodactyl", + "abra", "clefairy", "dratini", "scyther", "pinsir", "porygon", + "togepi" + }; + return SPECIES_LIST; +} + +} + + +GiftRngSpecies gift_rng_species(PokemonFRLG_RngTarget target){ + switch (target){ + case PokemonFRLG_RngTarget::magikarp: + return {.base_stats = { 20, 10, 55, 15, 20, 80 }, .gender_threshold = 126, .level = 5}; + case PokemonFRLG_RngTarget::hitmonchan: + return {.base_stats = { 50, 105, 79, 35, 110, 76 }, .gender_threshold = -1, .level = 25}; + case PokemonFRLG_RngTarget::hitmonlee: + return {.base_stats = { 50, 120, 53, 35, 110, 87 }, .gender_threshold = -1, .level = 25}; + case PokemonFRLG_RngTarget::eevee: + return {.base_stats = { 55, 55, 50, 45, 65, 55 }, .gender_threshold = 30, .level = 25}; + case PokemonFRLG_RngTarget::lapras: + return {.base_stats = { 130, 85, 80, 85, 95, 60 }, .gender_threshold = 126, .level = 25}; + case PokemonFRLG_RngTarget::omanyte: + return {.base_stats = { 35, 40, 100, 90, 55, 35 }, .gender_threshold = 30, .level = 5}; + case PokemonFRLG_RngTarget::kabuto: + return {.base_stats = { 30, 80, 90, 55, 45, 55 }, .gender_threshold = 30, .level = 5}; + case PokemonFRLG_RngTarget::aerodactyl: + return {.base_stats = { 80, 105, 65, 60, 75, 130 }, .gender_threshold = 30, .level = 5}; + case PokemonFRLG_RngTarget::gamecornerabra: + return {.base_stats = { 25, 20, 15, 105, 55, 90 }, .gender_threshold = 63, .level = 0}; + case PokemonFRLG_RngTarget::gamecornerclefairy: + return {.base_stats = { 70, 45, 48, 60, 65, 35 }, .gender_threshold = 190, .level = 0}; + case PokemonFRLG_RngTarget::gamecornerdratinifr: + case PokemonFRLG_RngTarget::gamecornerdratinilg: + return {.base_stats = { 41, 64, 45, 50, 50, 50 }, .gender_threshold = 126, .level = 0}; + case PokemonFRLG_RngTarget::gamecornerscyther: + return {.base_stats = { 70, 110, 80, 55, 80, 105 }, .gender_threshold = 126, .level = 25}; + case PokemonFRLG_RngTarget::gamecornerpinsir: + return {.base_stats = { 65, 125, 100, 55, 70, 85 }, .gender_threshold = 126, .level = 18}; + case PokemonFRLG_RngTarget::gamecornerporygon: + return {.base_stats = { 65, 60, 70, 85, 75, 40 }, .gender_threshold = -1, .level = 0}; + case PokemonFRLG_RngTarget::togepi: + case PokemonFRLG_RngTarget::togepifast: + return {.base_stats = { 35, 20, 65, 40, 65, 20 }, .gender_threshold = 30, .level = 5}; + default: + return {}; + } +} + + +GiftRngOutcome gift_shiny_outcome(uint16_t seed, uint64_t advances, const std::vector& hits){ + if (hits.empty()){ + return GiftRngOutcome::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 ? GiftRngOutcome::shiny_on_target : GiftRngOutcome::shiny_off_target; +} + + +GiftRngStats::GiftRngStats() + : 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); +} + + +GiftRngAttempts::GiftRngAttempts( + GiftRngTarget target, + GiftRngSettings settings, + const GiftRngDisplays& displays, + const GiftRngNotifications& notifications, + GiftRngStats& stats +) + : m_target(std::move(target)) + , m_settings(std::move(settings)) + , m_displays(displays) + , m_notifications(notifications) + , m_stats(stats) + , m_species(gift_rng_species(m_target.target)) + , m_initial_advances_radius(m_settings.use_teachy_tv ? 4096 : 1024) + , m_max_history_length(m_settings.use_teachy_tv ? 2 : 10) + , m_searcher(m_target.seed, m_target.advances, AdvRngMethod::Method1) + , m_launch_delay(INITIAL_LAUNCH_DELAY) +{} + + +GiftRngResult GiftRngAttempts::run(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("RNG Target: " + std::to_string(static_cast(m_target.target))); + 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, m_species.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){ + GiftRngResult result{.outcome = *outcome, .pokemon = std::nullopt}; + if (*outcome == GiftRngOutcome::target_hit){ + result.pokemon = target_result; + } + return result; + } + + reset_game(env, context); + + if (check_shiny_gift(env, context)){ + const GiftRngOutcome shiny = m_settings.identify_shiny ? identify_shiny_gift(env, context) : GiftRngOutcome::shiny; + const bool on_target = shiny == GiftRngOutcome::shiny_on_target; + return GiftRngResult{.outcome = shiny, .pokemon = on_target ? std::optional(target_result) : std::nullopt}; + } + + run_attempt(env, context); + } +} + +std::optional GiftRngAttempts::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 GiftRngOutcome::target_hit; + } + env.log("Missed target."); + } + + if (m_failed_searches >= 5){ + env.log("Failed to find any matches 5 times in a row"); + return GiftRngOutcome::no_matches; + } + + if (m_stats.resets > m_settings.max_resets()){ + env.log("Max resets reached."); + return GiftRngOutcome::out_of_resets; + } + + return std::nullopt; +} + +void GiftRngAttempts::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, m_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, m_target.target, + seed_match.seed_delay, CONTINUE_SCREEN_FRAMES, ingame_advances, + m_settings.use_teachy_tv, m_calibrations, + FIXED_SEED_OFFSET, m_settings.language + ); + + env.log("Resetting Game..."); + reset_and_perform_blind_sequence( + env.console, context, m_settings.language, m_target.target, + seed_match.seed_button, seed_match.extra_button, timings, + m_launch_delay, false, m_settings.profile + ); + m_stats.resets++; + if (m_settings.on_reset){ + m_settings.on_reset(GiftRngProgress{.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 GiftRngAttempts::check_shiny_gift(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + bool shiny_found = check_for_shiny(env.console, context, m_target.target); + + if (shiny_found){ + env.log("Shiny found!"); + m_stats.shinies++; + m_displays.calibration.hits.set("Shiny!"); + send_program_notification( + env, + m_notifications.shiny, + COLOR_YELLOW, + "Shiny found!", + {}, "", + env.console.video().snapshot(), + true + ); + if (m_settings.take_video){ + pbf_press_button(context, BUTTON_CAPTURE, 2000ms, 0ms); + } + } + return shiny_found; +} + +// On the shiny's summary, as check_for_shiny() leaves it: reads it as an +// attempt does and searches near the target, without Rare Candies. +GiftRngOutcome GiftRngAttempts::identify_shiny_gift(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + AdvObservedPokemon pokemon = read_gift(env, context); + AdvRngFilters filters = observation_to_filters(pokemon, m_species.base_stats); + m_displays.filters.set(filters); + const std::vector hits = search(env, filters); + m_displays.calibration.set_hits(hits); + const GiftRngOutcome outcome = gift_shiny_outcome(m_target.seed, m_target.advances, hits); + env.log(std::format( + "Shiny gift: {} states near the target match it; {}.", hits.size(), + outcome == GiftRngOutcome::shiny_on_target ? "the target is one of them" + : outcome == GiftRngOutcome::shiny_off_target ? "the target isn't" : "not known if it is the target" + )); + return outcome; +} + +AdvObservedPokemon GiftRngAttempts::read_gift(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + AdvObservedPokemon pokemon = read_summary(env.console, context, m_settings.language, gift_species_names()); + if (m_species.level > 0){ + pokemon.level[0] = m_species.level; + } + return pokemon; +} + +std::vector GiftRngAttempts::search(SingleSwitchProgramEnvironment& env, AdvRngFilters& filters){ + return get_search_results( + env.console, m_searcher, filters, m_target.seed_match.seed_values, + m_target.advances, m_advances_radius, m_species.gender_threshold + ); +} + + +void GiftRngAttempts::run_attempt(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + AdvObservedPokemon pokemon = read_gift(env, context); + AdvRngFilters filters = observation_to_filters(pokemon, m_species.base_stats); + m_displays.filters.set(filters); + + std::vector search_hits = refine_calibration_with_rare_candy( + env, context, m_settings.language, pokemon, filters, m_species.base_stats, + m_uncertain_history, m_calibration_history, m_calibrations, + m_max_history_length, m_settings.max_rare_candies(), AdvRngMethod::Method1, false, + m_stats.errors, m_notifications.error_recoverable, + [&](AdvRngFilters& f){ + return search(env, f); + }, + [&](const std::vector& h){ m_displays.calibration.set_hits(h); }, + [&](const AdvRngFilters& f){ m_displays.filters.set(f); }, + [&](const std::vector& h){ + return all_equal(h) || all_indistinguishable(h, m_searcher, m_species.gender_threshold); + } + ); + + env.log("RNG search finished."); + update_failed_searches(m_failed_searches, search_hits); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h new file mode 100644 index 0000000000..335ebef350 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h @@ -0,0 +1,185 @@ +/* Gift RNG Attempts + * + * From: https://github.com/PokemonAutomation/ + * + * Gift RNG's reset, calibrate and attempt loop, for one target. The Gift + * RNG program runs it with its options, stats and displays; another + * program (AutoStory) can run it with its own. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_GiftRngAttempts_H +#define PokemonAutomation_PokemonFRLG_GiftRngAttempts_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_BlindNavigation.h" +#include "PokemonFRLG_RngCalibration.h" +#include "PokemonFRLG_RngDisplays.h" +#include "PokemonFRLG_SeedsDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// What the search needs to know about a gift: its base stats, its gender +// threshold (-1 if genderless) and the level it is received at (0 to keep +// the level read from its summary). +struct GiftRngSpecies{ + BaseStats base_stats{}; + int16_t gender_threshold = -1; + uint8_t level = 0; +}; + +// The gift that "target" gives. Targets that aren't gifts get the defaults. +GiftRngSpecies gift_rng_species(PokemonFRLG_RngTarget target); + + +struct GiftRngStats : public StatsTracker{ + GiftRngStats(); + + std::atomic& resets; + std::atomic& shinies; + std::atomic& nonshiny; + std::atomic& errors; +}; + + +struct GiftRngTarget{ + PokemonFRLG_RngTarget target = PokemonFRLG_RngTarget::magikarp; + 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 GiftRngProgress{ + uint64_t resets = 0; + RngCalibrations calibrations{}; +}; + +struct GiftRngSettings{ + Language language = Language::None; + // The Switch profile's position on the user selection screen; 0 for + // the default. + uint8_t profile = 0; + // Advance the RNG in the Teachy TV. + bool use_teachy_tv = 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 Gift RNG lets the user change it + // while running. + std::function max_resets; + // The Rare Candies at the top of the bag. Read before each search, as + // max_resets is. + std::function max_rare_candies; + // Called after each reset, if set, for a caller that logs the attempts. + std::function on_reset; +}; + +// Where the attempts show the target, the filters read so far and the +// calibration. The calibration display also holds the starting +// calibration. +struct GiftRngDisplays{ + RngTargetDisplay& target; + RngFilterDisplay& filters; + RngCalibrationDisplay& calibration; +}; + +struct GiftRngNotifications{ + EventNotificationOption& shiny; + EventNotificationOption& status_update; + EventNotificationOption& error_recoverable; +}; + + +enum class GiftRngOutcome{ + shiny, // The gift 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. + out_of_resets, // More resets than max_resets. + no_matches, // 5 searches in a row matched no state near the target. +}; + +// A shiny gift'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. +GiftRngOutcome gift_shiny_outcome(uint16_t seed, uint64_t advances, const std::vector& hits); + +struct GiftRngResult{ + GiftRngOutcome outcome = GiftRngOutcome::out_of_resets; + // The target's PID, nature and IVs, on target_hit and shiny_on_target. + std::optional pokemon; +}; + + +class GiftRngAttempts{ +public: + GiftRngAttempts( + GiftRngTarget target, + GiftRngSettings settings, + const GiftRngDisplays& displays, + const GiftRngNotifications& notifications, + GiftRngStats& stats + ); + GiftRngAttempts(const GiftRngAttempts&) = delete; + GiftRngAttempts& operator=(const GiftRngAttempts&) = delete; + + // From the HOME menu with the game closed: resets the game and + // receives the gift at the target's timing until the outcome is known. + // Each non-shiny gift is read, leveled with Rare Candies if needed, to + // find the state hit and recalibrate. Ends in the game on the last + // screen the attempts reached. Call once per object. + GiftRngResult run(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +private: + std::optional finished(SingleSwitchProgramEnvironment& env); + void reset_game(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + bool check_shiny_gift(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + GiftRngOutcome identify_shiny_gift(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + AdvObservedPokemon read_gift(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + std::vector search(SingleSwitchProgramEnvironment& env, AdvRngFilters& filters); + void run_attempt(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + + const GiftRngTarget m_target; + const GiftRngSettings m_settings; + GiftRngDisplays m_displays; + GiftRngNotifications m_notifications; + GiftRngStats& m_stats; + const GiftRngSpecies m_species; + const uint64_t m_initial_advances_radius; + const uint8_t m_max_history_length; + + AdvRngSearcher m_searcher; + RngCalibrations m_calibrations{}; + Milliseconds m_launch_delay; + RngUncertainHistory m_uncertain_history; + RngCalibrationHistory m_calibration_history; + uint64_t m_advances_radius = 0; + uint16_t m_failed_searches = 0; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.cpp new file mode 100644 index 0000000000..c76a7e90f9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.cpp @@ -0,0 +1,159 @@ +/* Gift RNG Attempts Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Gift RNG's per-target base stats, gender thresholds and levels, as Gift + * RNG set them before its attempts moved out of the program; and whether + * a shiny gift was the target. + * + */ + +#include +#include +#include +#include +#include "PokemonFRLG_GiftRngAttempts.h" +#include "PokemonFRLG_GiftRngAttempts_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +struct GiftRow{ + PokemonFRLG_RngTarget target = PokemonFRLG_RngTarget::magikarp; + std::string name = ""; + BaseStats base_stats{}; + int16_t gender_threshold = -1; + uint8_t level = 0; +}; + +const std::vector GIFT_ROWS = { + {.target = PokemonFRLG_RngTarget::magikarp, .name = "Magikarp", .base_stats = {20, 10, 55, 15, 20, 80}, .gender_threshold = 126, .level = 5}, + {.target = PokemonFRLG_RngTarget::hitmonchan, .name = "Hitmonchan", .base_stats = {50, 105, 79, 35, 110, 76}, .gender_threshold = -1, .level = 25}, + {.target = PokemonFRLG_RngTarget::hitmonlee, .name = "Hitmonlee", .base_stats = {50, 120, 53, 35, 110, 87}, .gender_threshold = -1, .level = 25}, + {.target = PokemonFRLG_RngTarget::eevee, .name = "Eevee", .base_stats = {55, 55, 50, 45, 65, 55}, .gender_threshold = 30, .level = 25}, + {.target = PokemonFRLG_RngTarget::lapras, .name = "Lapras", .base_stats = {130, 85, 80, 85, 95, 60}, .gender_threshold = 126, .level = 25}, + {.target = PokemonFRLG_RngTarget::omanyte, .name = "Omanyte", .base_stats = {35, 40, 100, 90, 55, 35}, .gender_threshold = 30, .level = 5}, + {.target = PokemonFRLG_RngTarget::kabuto, .name = "Kabuto", .base_stats = {30, 80, 90, 55, 45, 55}, .gender_threshold = 30, .level = 5}, + {.target = PokemonFRLG_RngTarget::aerodactyl, .name = "Aerodactyl", .base_stats = {80, 105, 65, 60, 75, 130}, .gender_threshold = 30, .level = 5}, + {.target = PokemonFRLG_RngTarget::gamecornerabra, .name = "Game Corner Abra", .base_stats = {25, 20, 15, 105, 55, 90}, .gender_threshold = 63, .level = 0}, + {.target = PokemonFRLG_RngTarget::gamecornerclefairy, .name = "Game Corner Clefairy", .base_stats = {70, 45, 48, 60, 65, 35}, .gender_threshold = 190, .level = 0}, + {.target = PokemonFRLG_RngTarget::gamecornerdratinifr, .name = "Game Corner Dratini FR", .base_stats = {41, 64, 45, 50, 50, 50}, .gender_threshold = 126, .level = 0}, + {.target = PokemonFRLG_RngTarget::gamecornerdratinilg, .name = "Game Corner Dratini LG", .base_stats = {41, 64, 45, 50, 50, 50}, .gender_threshold = 126, .level = 0}, + {.target = PokemonFRLG_RngTarget::gamecornerscyther, .name = "Game Corner Scyther", .base_stats = {70, 110, 80, 55, 80, 105}, .gender_threshold = 126, .level = 25}, + {.target = PokemonFRLG_RngTarget::gamecornerpinsir, .name = "Game Corner Pinsir", .base_stats = {65, 125, 100, 55, 70, 85}, .gender_threshold = 126, .level = 18}, + {.target = PokemonFRLG_RngTarget::gamecornerporygon, .name = "Game Corner Porygon", .base_stats = {65, 60, 70, 85, 75, 40}, .gender_threshold = -1, .level = 0}, + {.target = PokemonFRLG_RngTarget::togepi, .name = "Togepi", .base_stats = {35, 20, 65, 40, 65, 20}, .gender_threshold = 30, .level = 5}, + {.target = PokemonFRLG_RngTarget::togepifast, .name = "Togepi pre-approved", .base_stats = {35, 20, 65, 40, 65, 20}, .gender_threshold = 30, .level = 5}, +}; + +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 GiftRngSpecies& species){ + return std::format( + "base stats {}, gender threshold {}, level {}", + to_string(species.base_stats), species.gender_threshold, species.level + ); +} + +class Test_GiftSpecies : public UnitTest{ +public: + explicit Test_GiftSpecies(const GiftRow& row) + : UnitTest("PokemonFRLG::GiftRngAttempts - Gift species " + row.name) + , m_row(row) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string expected = to_string(GiftRngSpecies{ + .base_stats = m_row.base_stats, + .gender_threshold = m_row.gender_threshold, + .level = m_row.level, + }); + const std::string got = to_string(gift_rng_species(m_row.target)); + if (expected != got){ + return "Expected " + expected + "; got " + got; + } + return true; + } + +private: + GiftRow 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 = {}; + GiftRngOutcome expected = GiftRngOutcome::shiny; +}; + +const std::vector SHINY_ROWS = { + {.name = "the target alone", .hits = {{0x70fe, 1000}}, .expected = GiftRngOutcome::shiny_on_target}, + {.name = "the target among others", .hits = {{0x70fe, 998}, {0x70fe, 1000}, {0x70ff, 1000}}, .expected = GiftRngOutcome::shiny_on_target}, + {.name = "another advance", .hits = {{0x70fe, 1002}}, .expected = GiftRngOutcome::shiny_off_target}, + {.name = "another seed", .hits = {{0x70fd, 1000}}, .expected = GiftRngOutcome::shiny_off_target}, + {.name = "no match", .hits = {}, .expected = GiftRngOutcome::shiny}, +}; + +const char* outcome_name(GiftRngOutcome outcome){ + switch (outcome){ + case GiftRngOutcome::shiny: return "shiny"; + case GiftRngOutcome::shiny_on_target: return "shiny_on_target"; + case GiftRngOutcome::shiny_off_target: return "shiny_off_target"; + case GiftRngOutcome::target_hit: return "target_hit"; + case GiftRngOutcome::out_of_resets: return "out_of_resets"; + case GiftRngOutcome::no_matches: return "no_matches"; + } + return "?"; +} + +class Test_GiftShinyOutcome : public UnitTest{ +public: + explicit Test_GiftShinyOutcome(const ShinyRow& row) + : UnitTest("PokemonFRLG::GiftRngAttempts - 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 GiftRngOutcome got = gift_shiny_outcome(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; +}; + +} + + +void add_tests_GiftRngAttempts(UnitTestDatabase& database){ + for (const GiftRow& row : GIFT_ROWS){ + database.add(row); + } + for (const ShinyRow& row : SHINY_ROWS){ + database.add(row); + } +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h new file mode 100644 index 0000000000..e9795466f5 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h @@ -0,0 +1,24 @@ +/* Gift RNG Attempts Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_GiftRngAttempts_Tests_H +#define PokemonAutomation_PokemonFRLG_GiftRngAttempts_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The pure parts of the Gift RNG attempts. +void add_tests_GiftRngAttempts(UnitTestDatabase& database); + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index dad3af3e6b..6119467ae1 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1789,6 +1789,10 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_EncountersDatabase.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_LocationsDatabase.cpp From 6701329956a5d06bd777a5a8e9549e2390f3513a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 19:57:53 -0800 Subject: [PATCH 2/8] FRLG AutoStory: run Gift RNG for the Magikarp after 00.12 Adds a GIFT_RNG handoff at 00.12, which saves facing Route 4's Magikarp salesman, and a Run Gift RNG in AutoStory option (off by default). With it on, AutoStory plans the attempts from the RNG plan's TID and SID before 00.12 saves, runs Gift RNG's attempts through the RNG plan loop, and on a shiny runs the new live-start row 00.12b, which leaves the summary and saves. The program now runs its plan as a loop of handoffs, so a run with both Starter RNG and Gift RNG on goes from one to the other. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 21 +- .../AutoStory/PokemonFRLG_AutoStory.cpp | 217 +++++++++++++----- .../AutoStory/PokemonFRLG_AutoStory.h | 25 +- .../PokemonFRLG_AutoStoryActions.cpp | 21 +- .../AutoStory/PokemonFRLG_AutoStoryActions.h | 5 + .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 9 + .../AutoStory/PokemonFRLG_AutoStoryEvents.h | 11 +- .../PokemonFRLG_AutoStoryRngLoop.cpp | 69 +++++- .../AutoStory/PokemonFRLG_AutoStoryRngLoop.h | 30 ++- .../PokemonFRLG_AutoStoryRngLoop_Tests.cpp | 158 +++++++++++-- .../PokemonFRLG_AutoStoryRngOptions.cpp | 20 +- .../PokemonFRLG_AutoStoryRngOptions.h | 6 + .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 19 +- .../AutoStory/PokemonFRLG_AutoStoryTable.h | 7 +- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 42 +++- .../PokemonFRLG_GiftRngConsoleSteps.cpp | 119 ++++++++++ .../PokemonFRLG_GiftRngConsoleSteps.h | 70 ++++++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 18 files changed, 723 insertions(+), 128 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index cdacda0cce..99d9e7930d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -670,6 +670,18 @@ void add_tests_NamingText(UnitTestDatabase& database){ } } +// The RNG programs' searches, plans and loops. +void add_tests_RngManipulation(UnitTestDatabase& database){ + add_tests_RngTargetSearch(database); + add_tests_RngPlanInputs(database); + add_tests_RngPlan(database); + add_tests_RngPlanTracker(database); + add_tests_RngPlanLog(database); + add_tests_RngSteps(database); + add_tests_StarterRngLoop(database); + add_tests_GiftRngAttempts(database); +} + } @@ -715,14 +727,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_NamingPlan(database); add_tests_NamingKeys(database); add_tests_NamingText(database); - add_tests_RngTargetSearch(database); - add_tests_RngPlanInputs(database); - add_tests_RngPlan(database); - add_tests_RngPlanTracker(database); - add_tests_RngPlanLog(database); - add_tests_RngSteps(database); - add_tests_StarterRngLoop(database); - add_tests_GiftRngAttempts(database); + add_tests_RngManipulation(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index dba48205fa..ecbb6a15ea 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -18,6 +18,7 @@ #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidSteps.h" @@ -54,7 +55,7 @@ std::unique_ptr AutoStory_Descriptor::make_stats() const{ namespace{ // The start and end dropdowns' entries: the table's checkpoints, 00.01 to -// 00.12. The options hold a reference to it. +// 00.12b. The options hold a reference to it. const StringSelectDatabase& checkpoint_database(){ static const StringSelectDatabase database = Pokemon::story_checkpoint_database(story_checkpoints()); return database; @@ -69,6 +70,13 @@ const char* const RUN_STARTER_RNG_TEXT = "goes on; otherwise the run stops with the save south of the table.
" "SID Helper's timing needs the text speed and button settings already saved, as with Stop for SID Helper."; +const char* const RUN_GIFT_RNG_TEXT = + "Run Gift RNG in AutoStory:
Off by default. After 00.12 saves beside the Magikarp salesman in Route 4's " + "Pokemon Center, run Gift RNG's attempts for the Magikarp from that save, as the RNG plan lists them for its TID and " + "SID (set by Run Starter RNG in AutoStory, or typed in). A shiny Magikarp is saved there (00.12b) and the run goes " + "on; otherwise the run stops with the save beside the salesman.
" + "English only. The Magikarp costs 500 and needs a free party slot; AutoStory's options leave the text fast."; + AutoStory::AutoStory() : RUN(checkpoint_database()) , NEW_GAME_FROM_TITLE( @@ -102,6 +110,7 @@ AutoStory::AutoStory() false ) , RUN_STARTER_RNG(RUN_STARTER_RNG_TEXT, LockMode::LOCK_WHILE_RUNNING, false) + , RUN_GIFT_RNG(RUN_GIFT_RNG_TEXT, LockMode::LOCK_WHILE_RUNNING, false) , 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, &NOTIFICATION_ERROR_FATAL}) @@ -112,6 +121,7 @@ AutoStory::AutoStory() PA_ADD_OPTION(STOP_FOR_SID_HELPER); PA_ADD_OPTION(STOP_FOR_STARTER_RNG); PA_ADD_OPTION(RUN_STARTER_RNG); + PA_ADD_OPTION(RUN_GIFT_RNG); PA_ADD_OPTION(RNG_PLAN); PA_ADD_OPTION(NOTIFICATIONS); } @@ -261,12 +271,12 @@ static void stop_for_sid_helper( ); } -// The TID for the Starter RNG plan: the RNG plan's, or read off the -// Trainer Card (START, the player's entry), which is then closed again. -// Before the first Pokemon, as under the ball, the player's entry is second. -static uint16_t starter_rng_tid( +// The TID for the RNG plan: the RNG plan's, or read off the Trainer Card +// (START, the player's entry), which is then closed again. Run before +// `checkpoint` saves, which a failure tells the user to start at again. +static uint16_t rng_plan_tid( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - const StarterRngPlanOptions& options, Language language + const StarterRngPlanOptions& options, Language language, const std::string& checkpoint ){ std::optional tid = options.tid_override(env.console); if (tid){ @@ -278,8 +288,8 @@ static uint16_t starter_rng_tid( if (!tid){ OperationFailedExceptionWithScreenshot::fire( ErrorReportMode::NO_ERROR_REPORT, - "Couldn't read the TID on the Trainer Card, so there is no Starter RNG plan. Nothing was saved: " - "type the TID into the RNG plan's TID option and start AutoStory at 00.04 again.", + "Couldn't read the TID on the Trainer Card, so there is no RNG plan. Nothing was saved: " + "type the TID into the RNG plan's TID option and start AutoStory at " + checkpoint + " again.", env.console ); } @@ -288,17 +298,21 @@ static uint16_t starter_rng_tid( return *tid; } -// The Starter RNG attempts. Run under the ball before 00.04 saves there, so -// a failure ends the run with 00.03's save kept. -static StarterRngPlanned plan_starter_rng_attempts( +// The RNG plan's attempts for the handoff `stop` (STARTER_RNG or +// GIFT_RNG) that ends `checkpoint`. Run where it ends, before it saves +// there, so a failure ends the run with the save it started from kept. +static StarterRngPlanned plan_handoff_attempts( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - const StarterRngPlanOptions& options, Language language + const StarterRngPlanOptions& options, Language language, StoryHandoff stop, const std::string& checkpoint ){ - const uint16_t tid = starter_rng_tid(env, context, options, language); + const uint16_t tid = rng_plan_tid(env, context, options, language, checkpoint); try{ - return StarterRngPlanned{.tid = tid, .attempts = options.plan(env.console, language, tid)}; + std::vector attempts = stop == StoryHandoff::GIFT_RNG + ? options.gift_plan(env.console, language, tid) + : options.plan(env.console, language, tid); + return StarterRngPlanned{.tid = tid, .attempts = std::move(attempts)}; }catch (UserSetupError& e){ - throw UserSetupError(env.console, e.message() + " Nothing was saved: fix the RNG plan and start AutoStory at 00.04 again."); + throw UserSetupError(env.console, e.message() + " Nothing was saved: fix the RNG plan and start AutoStory at " + checkpoint + " again."); } } @@ -392,9 +406,9 @@ static void set_sid_in_program( namespace{ -// Run Starter RNG in AutoStory: the time caps, Starter RNG's default Max -// Resets per try, and an attempt's misses before the run stops. -const StarterRngLoopLimits STARTER_RNG_LIMITS{ +// Run Starter RNG and Gift RNG in AutoStory: the time caps, both programs' +// default Max Resets per try, and an attempt's misses before the run stops. +const StarterRngLoopLimits RNG_LOOP_LIMITS{ .attempt_time = std::chrono::hours(3), .total_time = std::chrono::hours(12), .max_resets = 50, @@ -403,24 +417,18 @@ const StarterRngLoopLimits STARTER_RNG_LIMITS{ } -// Starter RNG's attempts in the program, from the save south of the table. -// Returns true for a shiny Squirtle, on the target or not, the game on its -// summary; otherwise the run has stopped with what to do next. A shiny that -// confirms the SID writes it into the RNG plan's SID option. -static bool run_starter_rng_in_program( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, - const StarterRngPlanned& planned, StarterRngPlanOptions& options, Language language, - const StarterRngNotifications& notifications, EventNotificationOption& finished +// The RNG plan's attempts for `planned`, each tried through `steps`. A +// shiny that confirms the SID writes it into the RNG plan's SID option. +static StarterRngLoopResult run_rng_plan_in_program( + SingleSwitchProgramEnvironment& env, StarterRngLoopSteps& steps, + const StarterRngPlanned& planned, StarterRngPlanOptions& options ){ RngPlanTracker tracker( - planned.attempts, options.plan_candidates(env.console, planned.tid), options.SHINY_REQUIRED, STARTER_RNG_LIMITS.max_retries - ); - ConsoleStarterRngSteps steps( - env, context, StarterRngConsoleSettings{.language = language, .sound = options.SOUND}, notifications + planned.attempts, options.plan_candidates(env.console, planned.tid), options.SHINY_REQUIRED, RNG_LOOP_LIMITS.max_retries ); RngPlanLogger logger(env.console); const StarterRngLoopResult result = run_starter_rng_plan( - logger, tracker, steps, STARTER_RNG_LIMITS, planned.tid, options.helper_candidates(planned.tid) + logger, tracker, steps, RNG_LOOP_LIMITS, planned.tid, options.helper_candidates(planned.tid) ); const std::string confirmed_sid = result.sid ? std::to_string(*result.sid) : ""; if (result.end == StarterRngLoopEnd::SHINY && result.sid && static_cast(options.SID) != confirmed_sid){ @@ -434,7 +442,15 @@ static bool run_starter_rng_in_program( "when the program's panel is closed or the app quits.", COLOR_BLUE ); } - const std::string message = starter_rng_loop_message(result); + return result; +} + +// True for a shiny, on the target or not, with `message` logged; otherwise +// the run has stopped with it. +static bool shiny_or_stop( + SingleSwitchProgramEnvironment& env, EventNotificationOption& finished, + const StarterRngLoopResult& result, const std::string& message +){ if (result.end != StarterRngLoopEnd::SHINY && result.end != StarterRngLoopEnd::SHINY_OFF_TARGET){ stop_for_handoff(env, finished, message); return false; @@ -444,34 +460,76 @@ static bool run_starter_rng_in_program( return true; } -// After a shiny Squirtle: off its summary, then the rival's battle, which -// saves (00.04b), and the rows after it to `last`. Until that save, a -// reload would load the save under the ball and lose the shiny, so a -// failure stops the run instead. Returns the last row's id. -static std::string run_after_shiny_starter( +// Starter RNG's attempts in the program, from the save south of the table. +// Returns true for a shiny Squirtle, the game on its summary; otherwise +// the run has stopped with what to do next. +static bool run_starter_rng_in_program( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const StarterRngPlanned& planned, StarterRngPlanOptions& options, Language language, + const StarterRngNotifications& notifications, EventNotificationOption& finished +){ + ConsoleStarterRngSteps steps( + env, context, StarterRngConsoleSettings{.language = language, .sound = options.SOUND}, notifications + ); + const StarterRngLoopResult result = run_rng_plan_in_program(env, steps, planned, options); + return shiny_or_stop(env, finished, result, starter_rng_loop_message(result)); +} + +// Gift RNG's attempts in the program, from the save beside the Magikarp +// salesman. Returns true for a shiny Magikarp, the game on its summary; +// otherwise the run has stopped with what to do next. +static bool run_gift_rng_in_program( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - StoryEnv& story, StoryExecutor& executor, const Pokemon::SegmentPolicy& policy, size_t ball_row, size_t last + const StarterRngPlanned& planned, StarterRngPlanOptions& options, Language language, + const GiftRngNotifications& notifications, EventNotificationOption& finished ){ - const std::vector rows = rows_after_starter_rng(story_checkpoints(), ball_row, last); + ConsoleGiftRngSteps steps( + env, context, GiftRngConsoleSettings{.language = language, .sound = options.SOUND}, notifications + ); + const StarterRngLoopResult result = run_rng_plan_in_program(env, steps, planned, options); + return shiny_or_stop(env, finished, result, gift_rng_loop_message(result)); +} + +// After a shiny Squirtle: off its summary, and the rows to run next: the +// rival's battle, which saves (00.04b), and the rows after it to `last`. +// Until that save, a reload would load the save under the ball and lose +// the shiny, so a failure stops the run instead. +static StoryRunPlan after_shiny_starter( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, StoryEnv& story, + size_t ball_row, size_t last, const StoryHandoffs& handoffs +){ + StoryRunPlan plan = rows_after_starter_rng(story_checkpoints(), ball_row, last, handoffs); // As Starter RNG leaves the summary for the overworld. pbf_mash_button(context, BUTTON_B, 5000ms); context.wait_for_all_requests(); - env.console.log("The shiny Squirtle isn't saved until " + rows.front().id + " saves after the rival's battle. " + env.console.log("The shiny Squirtle isn't saved until " + plan.rows.front().id + " saves after the rival's battle. " + "If it fails before then, the run stops without reloading: save by hand.", COLOR_BLUE); + story.has_save = false; + return plan; +} + +// After a shiny Magikarp: the rows to run next, from 00.12b, which leaves +// its summary and saves, to `last`. Until that save, a failure stops the +// run, as after a shiny Squirtle. +static StoryRunPlan after_shiny_magikarp( + SingleSwitchProgramEnvironment& env, StoryEnv& story, size_t salesman_row, size_t last, const StoryHandoffs& handoffs +){ + StoryRunPlan plan = rows_after_gift_rng(story_checkpoints(), salesman_row, last, handoffs); + env.console.log("The shiny Magikarp isn't saved until " + plan.rows.front().id + " saves. " "If it fails before then, the run stops without reloading: save by hand.", COLOR_BLUE); story.has_save = false; - run_story_rows(env, story, executor, rows, policy, nullptr); - return rows.back().id; + return plan; } // The run is done through `done_through`. The finish message also gives -// the SID when Starter RNG set it, from `sid_before` to `sid_after`. +// the SID when an RNG loop set it, from `sid_before` to `sid_after`. static void send_story_finished( SingleSwitchProgramEnvironment& env, EventNotificationOption& finished, const std::string& done_through, const std::string& sid_before, const std::string& sid_after ){ std::string message = "AutoStory: done through " + done_through + "."; if (sid_after != sid_before){ - message += " Starter RNG confirmed the SID, " + sid_after + ", and saved it as the RNG plan's SID."; + message += " The RNG plan's attempts confirmed the SID, " + sid_after + ", and saved it as the RNG plan's SID."; } env.console.log(message); send_program_finished_notification(env, finished, message); @@ -483,6 +541,7 @@ StoryHandoffs AutoStory::story_handoffs() const{ return StoryHandoffs{ .sid_helper = RUN_STARTER_RNG ? static_cast(RNG_PLAN.SID).empty() : bool(STOP_FOR_SID_HELPER), .starter_rng = STOP_FOR_STARTER_RNG || RUN_STARTER_RNG, + .gift_rng = RUN_GIFT_RNG, }; } @@ -516,28 +575,51 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex executor.set_checks(RUN.checks()); executor.set_language(RUN.language()); - StarterRngPlanned planned; - std::function before; - if (plan.stop == StoryHandoff::STARTER_RNG){ - before = [&]{ planned = plan_starter_rng_attempts(env, context, RNG_PLAN, RUN.language()); }; + const std::optional done_through = run_story_plan(env, context, story, executor, std::move(plan)); + if (done_through){ + send_story_finished(env, NOTIFICATION_PROGRAM_FINISH, *done_through, sid_before, RNG_PLAN.SID); } - run_story_rows(env, story, executor, plan.rows, RUN.policy(), std::move(before)); - std::string done_through = plan.rows.back().id; - if (plan.stop == StoryHandoff::STARTER_RNG){ - std::optional done = after_starter_ball( - env, context, story, executor, planned, checkpoint_index(checkpoints, done_through) - ); - if (!done){ - return; +} + +std::optional AutoStory::run_story_plan( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + StoryEnv& story, StoryExecutor& executor, StoryRunPlan plan +){ + while (true){ + const std::string last_id = plan.rows.back().id; + const StoryHandoff stop = plan.stop; + StarterRngPlanned planned; + std::function before; + if (stop == StoryHandoff::STARTER_RNG || stop == StoryHandoff::GIFT_RNG){ + before = [&, stop, last_id]{ + planned = plan_handoff_attempts(env, context, RNG_PLAN, RUN.language(), stop, last_id); + }; + } + run_story_rows(env, story, executor, plan.rows, RUN.policy(), std::move(before)); + const size_t row = checkpoint_index(story_checkpoints(), last_id); + std::optional next; + switch (stop){ + case StoryHandoff::NONE: + return last_id; + case StoryHandoff::SID_HELPER: + throw InternalProgramError(&env.logger(), PA_CURRENT_FUNCTION, "SID Helper's stop has no rows to run."); + case StoryHandoff::STARTER_RNG: + next = after_starter_ball(env, context, story, planned, row); + break; + case StoryHandoff::GIFT_RNG: + next = after_magikarp_salesman(env, context, story, planned, row); + break; } - done_through = *done; + if (!next){ + return std::nullopt; + } + plan = std::move(*next); } - send_story_finished(env, NOTIFICATION_PROGRAM_FINISH, done_through, sid_before, RNG_PLAN.SID); } -std::optional AutoStory::after_starter_ball( +std::optional AutoStory::after_starter_ball( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - StoryEnv& story, StoryExecutor& executor, const StarterRngPlanned& planned, size_t ball_row + StoryEnv& story, const StarterRngPlanned& planned, size_t ball_row ){ if (!RUN_STARTER_RNG){ stop_for_starter_rng(env, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN.sound_name(), rng_attempt_lines(planned.attempts)); @@ -549,7 +631,20 @@ std::optional AutoStory::after_starter_ball( if (!run_starter_rng_in_program(env, context, planned, RNG_PLAN, RUN.language(), notifications, NOTIFICATION_PROGRAM_FINISH)){ return std::nullopt; } - return run_after_shiny_starter(env, context, story, executor, RUN.policy(), ball_row, RUN.end_index()); + return after_shiny_starter(env, context, story, ball_row, RUN.end_index(), story_handoffs()); +} + +std::optional AutoStory::after_magikarp_salesman( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + StoryEnv& story, const StarterRngPlanned& planned, size_t salesman_row +){ + const GiftRngNotifications notifications{ + .shiny = NOTIFICATION_SHINY, .status_update = NOTIFICATION_STATUS_UPDATE, .error_recoverable = NOTIFICATION_ERROR_RECOVERABLE, + }; + if (!run_gift_rng_in_program(env, context, planned, RNG_PLAN, RUN.language(), notifications, NOTIFICATION_PROGRAM_FINISH)){ + return std::nullopt; + } + return after_shiny_magikarp(env, story, salesman_row, RUN.end_index(), story_handoffs()); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h index c25edad269..664cf0536a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h @@ -49,13 +49,26 @@ class AutoStory : public SingleSwitchProgramInstance{ private: // The RNG programs' handoffs the run takes, from the options. StoryHandoffs story_handoffs() const; + // Runs `plan`'s rows, then the RNG handoff they stop at and, for a + // shiny, the rows after it, to the end checkpoint. Returns the last + // checkpoint run, or nothing once the run has stopped. + std::optional run_story_plan( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + StoryEnv& story, StoryExecutor& executor, StoryRunPlan plan + ); // After 00.04 saved under Squirtle's ball (row `ball_row`): the stop - // for Starter RNG, or its attempts run here and, for a shiny, the run - // on to the end checkpoint. Returns the last checkpoint run, or nothing - // once the run has stopped. - std::optional after_starter_ball( + // for Starter RNG, or its attempts run here. Returns the rows to run + // after a shiny, or nothing once the run has stopped. + std::optional after_starter_ball( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + StoryEnv& story, const StarterRngPlanned& planned, size_t ball_row + ); + // After 00.12 saved beside the Magikarp salesman (row `salesman_row`): + // Gift RNG's attempts run here. Returns the rows to run after a shiny, + // or nothing once the run has stopped. + std::optional after_magikarp_salesman( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - StoryEnv& story, StoryExecutor& executor, const StarterRngPlanned& planned, size_t ball_row + StoryEnv& story, const StarterRngPlanned& planned, size_t salesman_row ); // The start and end checkpoints, attempts, checks and language, as in @@ -69,6 +82,8 @@ class AutoStory : public SingleSwitchProgramInstance{ // Runs SID Helper's steps and Starter RNG's attempts in the program // instead of stopping for them. BooleanCheckBoxOption RUN_STARTER_RNG; + // Runs Gift RNG's attempts for the Magikarp in the program after 00.12. + BooleanCheckBoxOption RUN_GIFT_RNG; StarterRngPlanOptions RNG_PLAN; EventNotificationOption NOTIFICATION_SHINY; EventNotificationOption NOTIFICATION_STATUS_UPDATE; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp index cbfc48c364..dc95c894d5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp @@ -872,17 +872,32 @@ void play_new_game_to_player_name(ConsoleHandle& console, ProControllerContext& NewGameIntro(console, context, player_name, NewGameIntro::End::PLAYER_NAME_TYPED).run(); } -void close_trainer_card(ConsoleHandle& console, ProControllerContext& context){ +namespace{ + +// After the RNG program `program`: from HOME, if it went there, back to +// the game, then B until the overworld. +void back_to_overworld_after(ConsoleHandle& console, ProControllerContext& context, const std::string& program){ context.wait_for_all_requests(); VideoSnapshot screen = console.video().snapshot(); if (screen && HomeMenuDetector(console, COLOR_RED).detect(screen)){ - console.log("After SID Helper: on HOME, back to the game."); + console.log("After " + program + ": on HOME, back to the game."); resume_game_from_home(console, context); } - // B closes the card, then the START menu. mash_until_overworld(console, context, 20000ms); } +} + +void close_trainer_card(ConsoleHandle& console, ProControllerContext& context){ + // B closes the card, then the START menu. + back_to_overworld_after(console, context, "SID Helper"); +} + +void leave_gift_summary(ConsoleHandle& console, ProControllerContext& context){ + // B closes the summary, the party, then the START menu. + back_to_overworld_after(console, context, "Gift RNG"); +} + } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h index 7065c8e1f7..5e07d1cd4c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h @@ -228,6 +228,11 @@ void play_new_game_to_player_name(ConsoleHandle& console, ProControllerContext& // Returns on the overworld. void close_trainer_card(ConsoleHandle& console, ProControllerContext& context); +// From the gift's summary Gift RNG leaves open, or from HOME (its "go home +// when done"): back to the game, the summary, the party and the START +// menu closed. Returns on the overworld. +void leave_gift_summary(ConsoleHandle& console, ProControllerContext& context); + } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index 8225ceb38c..5f330ee775 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -417,6 +417,10 @@ Outcome StoryExecutor::game(RunState&, const StoryStep& step){ close_trainer_card(m_env.console, m_env.context); return Pokemon::outcome_ok(); } + if (step.variant == LEAVE_GIFT_SUMMARY){ + leave_gift_summary(m_env.console, m_env.context); + return Pokemon::outcome_ok(); + } return Pokemon::unsupported_step(step); } @@ -457,6 +461,11 @@ StoryRunPlan plan_story_run( plan.stop = StoryHandoff::STARTER_RNG; return plan; } + if (row.handoff == StoryHandoff::GIFT_RNG && handoffs.gift_rng){ + plan.rows.emplace_back(row); + plan.stop = StoryHandoff::GIFT_RNG; + return plan; + } if (row.handoff == StoryHandoff::STARTER_RNG && c + 1 < checkpoints.size()){ plan.rows.emplace_back(joined_rows(row, checkpoints[c + 1])); c++; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h index 297120ba5a..0776320475 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -59,6 +59,10 @@ inline const std::string RUN_VARIANT = "run"; // The GAME step that closes the Trainer Card SID Helper leaves open. inline const std::string CLOSE_TRAINER_CARD = "close_trainer_card"; +// The GAME step that leaves the gift's summary Gift RNG leaves open for +// the overworld. +inline const std::string LEAVE_GIFT_SUMMARY = "leave_gift_summary"; + // How `step`, a ROUTE or LEGS, moves the player: RUN_VARIANT runs. MoveSpeed move_speed(const StoryStep& step); @@ -74,6 +78,9 @@ enum class StoryHandoff{ // which is where Starter RNG starts. With the handoff off, it runs on // into the next checkpoint as one, as the story did before the split. STARTER_RNG, + // The checkpoint saves beside the Magikarp salesman, facing him, which + // is where Gift RNG starts. + GIFT_RNG, }; // A shared checkpoint with what FRLG adds to it. @@ -94,6 +101,7 @@ struct StoryCheckpoint : public Pokemon::StoryCheckpoint{ struct StoryHandoffs{ bool sid_helper = false; bool starter_rng = false; + bool gift_rng = false; }; // What a run from table row `first` to `last` does. @@ -103,7 +111,7 @@ struct StoryRunPlan{ // NONE: the run ends after `rows`. SID_HELPER: `rows` is empty; the run // plays the new game to the player's keyboard and stops there. // STARTER_RNG: the run stops after `rows`, the last of which saves at - // the starter's ball. + // the starter's ball. GIFT_RNG: likewise, beside the Magikarp salesman. StoryHandoff stop = StoryHandoff::NONE; }; @@ -211,6 +219,7 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ virtual Pokemon::Outcome shop(Pokemon::RunState& state, const StoryStep& step) override; virtual Pokemon::Outcome ready(Pokemon::RunState& state, const StoryStep& step) override; // CLOSE_TRAINER_CARD: back from SID Helper (close_trainer_card()). + // LEAVE_GIFT_SUMMARY: back from Gift RNG (leave_gift_summary()). virtual Pokemon::Outcome game(Pokemon::RunState& state, const StoryStep& step) override; private: diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp index b30418f240..ac111cb298 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp @@ -64,8 +64,51 @@ std::string starter_rng_loop_message(const StarterRngLoopResult& result){ } -std::vector rows_after_starter_rng( - const std::vector& checkpoints, size_t ball_row, size_t last +std::string gift_rng_loop_message(const StarterRngLoopResult& result){ + const std::string save = "The save is still beside the salesman, facing him: run Gift RNG (Magikarp) from it with " + "an attempt from the log, then start AutoStory at 00.12b."; + switch (result.end){ + case StarterRngLoopEnd::SHINY: + return "Gift RNG: shiny Magikarp, on the planned target. 00.12b saves it."; + case StarterRngLoopEnd::SHINY_OFF_TARGET: + return "Gift RNG: shiny Magikarp, but not the planned target. 00.12b saves it."; + case StarterRngLoopEnd::TARGET: + return "Gift RNG hit the target (shiny not required). Nothing was saved after it: start AutoStory at 00.12b " + "to save the Magikarp where it stands."; + case StarterRngLoopEnd::OUT_OF_ATTEMPTS: + return std::format( + "Gift RNG: every planned attempt hit its target and none was shiny, so the RNG plan's SID doesn't fit. " + "SIDs left: {}. {}", join_sids(result.remaining_sids), save + ); + case StarterRngLoopEnd::MISSED_EVERY_ATTEMPT_LEFT: + return "Gift RNG: every attempt left missed its target more often than the retries allow, or Gift RNG refused " + "its timings (see the log). Check the RNG plan's sound setting. " + save; + case StarterRngLoopEnd::OUT_OF_TIME: + return std::format("Gift RNG: out of time after {} attempts. {}", result.attempts_run, save); + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown Gift RNG loop end."); +} + + +namespace{ + +// `plan` with the rows from `first` to `last` as plan_story_run() takes +// them after it, and the handoff they stop at. +StoryRunPlan then_rows( + StoryRunPlan plan, const std::vector& checkpoints, size_t first, size_t last, const StoryHandoffs& handoffs +){ + if (last >= first){ + StoryRunPlan rest = plan_story_run(checkpoints, first, last, handoffs); + plan.rows.insert(plan.rows.end(), rest.rows.begin(), rest.rows.end()); + plan.stop = rest.stop; + } + return plan; +} + +} + +StoryRunPlan rows_after_starter_rng( + const std::vector& checkpoints, size_t ball_row, size_t last, const StoryHandoffs& handoffs ){ if (ball_row + 1 >= checkpoints.size()){ throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No checkpoint after the starter's ball."); @@ -80,12 +123,24 @@ std::vector rows_after_starter_rng( row.steps.erase(row.steps.begin(), taken + 1); row.title = "after Starter RNG in AutoStory: the rival's battle"; - std::vector rows{std::move(row)}; - if (last > ball_row + 1){ - StoryRunPlan rest = plan_story_run(checkpoints, ball_row + 2, last, StoryHandoffs{}); - rows.insert(rows.end(), rest.rows.begin(), rest.rows.end()); + StoryRunPlan plan; + plan.rows.emplace_back(std::move(row)); + return then_rows(std::move(plan), checkpoints, ball_row + 2, last, handoffs); +} + +StoryRunPlan rows_after_gift_rng( + const std::vector& checkpoints, size_t salesman_row, size_t last, const StoryHandoffs& handoffs +){ + if (salesman_row + 1 >= checkpoints.size()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No checkpoint after the Magikarp salesman."); + } + const StoryCheckpoint& row = checkpoints[salesman_row + 1]; + if (row.steps.empty() || row.steps.front().variant != LEAVE_GIFT_SUMMARY){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, row.id + " doesn't start by leaving the gift's summary."); } - return rows; + StoryRunPlan plan; + plan.rows.emplace_back(row); + return then_rows(std::move(plan), checkpoints, salesman_row + 2, last, handoffs); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h index 266204c6c4..4f028d77c2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h @@ -2,9 +2,10 @@ * * From: https://github.com/PokemonAutomation/ * - * AutoStory's Starter RNG stop, run in the program with the Starter RNG - * loop (RngManipulation/PokemonFRLG_StarterRngLoop.h) from the save under - * Squirtle's ball: the plan it runs, what the run says when the loop + * AutoStory's RNG stops, run in the program with the RNG plan loop + * (RngManipulation/PokemonFRLG_StarterRngLoop.h): Starter RNG from the + * save under Squirtle's ball, and Gift RNG from the save beside Route 4's + * Magikarp salesman. The plan each runs, what the run says when the loop * ends, and the rows it takes after a shiny. * */ @@ -25,7 +26,7 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ -// The Starter RNG plan, for the TID it was made for. +// An RNG plan (Starter RNG's or Gift RNG's), for the TID it was made for. struct StarterRngPlanned{ uint16_t tid = 0; std::vector attempts; @@ -33,15 +34,28 @@ struct StarterRngPlanned{ // What the run says when the loop ends with `result`. std::string starter_rng_loop_message(const StarterRngLoopResult& result); +// As starter_rng_loop_message(), for Gift RNG's Magikarp. +std::string gift_rng_loop_message(const StarterRngLoopResult& result); // The rows a run takes after a shiny from the loop, which leaves the player // under Squirtle's ball with the rival's ball taken: the row after // `ball_row` (00.04b) from its step after "rival_takes_bulbasaur", then -// the rows after it to `last` as plan_story_run() takes them. Throws -// InternalProgramError for a table without that row or step. -std::vector rows_after_starter_rng( - const std::vector& checkpoints, size_t ball_row, size_t last +// the rows after it to `last` as plan_story_run() takes them with +// `handoffs`, with the handoff they stop at. Throws InternalProgramError +// for a table without that row or step. +StoryRunPlan rows_after_starter_rng( + const std::vector& checkpoints, size_t ball_row, size_t last, const StoryHandoffs& handoffs +); + +// The rows a run takes after a shiny Magikarp from the loop, which leaves +// its summary open beside the salesman: the row after `salesman_row` +// (00.12b), which leaves the summary and saves, then the rows after it to +// `last` as plan_story_run() takes them with `handoffs`. Throws +// InternalProgramError for a table whose next row doesn't leave the +// summary. +StoryRunPlan rows_after_gift_rng( + const std::vector& checkpoints, size_t salesman_row, size_t last, const StoryHandoffs& handoffs ); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp index 3dc8e25675..4ecb91ce13 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp @@ -2,7 +2,8 @@ * * From: https://github.com/PokemonAutomation/ * - * What the run says after a shiny, and the rows it takes then. + * What the run says when an RNG loop (Starter RNG's or Gift RNG's) ends, + * and the rows it takes after a shiny. * */ @@ -14,6 +15,7 @@ #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Logging/AbstractLogger.h" #include "Common/Cpp/TestRunners/UnitTest.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h" #include "PokemonFRLG_AutoStoryTable.h" #include "PokemonFRLG_AutoStoryRngLoop.h" @@ -79,50 +81,164 @@ UnitTestResult test_shiny_messages(){ } -// After a shiny, 00.04b picks up at the rival's trigger and runs on. -UnitTestResult test_rows_after_shiny(){ +size_t row_index(const std::string& id){ const std::vector& checkpoints = story_checkpoints(); - size_t ball_row = 0; - size_t parcel_row = 0; for (size_t c = 0; c < checkpoints.size(); c++){ - if (checkpoints[c].id == "00.04"){ - ball_row = c; - } - if (checkpoints[c].id == "00.05"){ - parcel_row = c; + if (checkpoints[c].id == id){ + return c; } } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No checkpoint " + id); +} + +std::string joined_ids(const StoryRunPlan& plan){ + std::string ids; + for (const StoryCheckpoint& row : plan.rows){ + ids += (ids.empty() ? "" : ",") + row.id; + } + return ids; +} + +std::string first_step(const StoryRunPlan& plan){ + return plan.rows.empty() || plan.rows.front().steps.empty() ? "" : plan.rows.front().steps.front().name; +} + +// After a shiny, 00.04b picks up at the rival's trigger and runs on, to +// Gift RNG's stop when it is on. +UnitTestResult test_rows_after_shiny(){ + const std::vector& checkpoints = story_checkpoints(); + const size_t ball_row = row_index("00.04"); struct Case{ - size_t last; + std::string last; + StoryHandoffs handoffs = {}; std::string ids; + StoryHandoff stop = StoryHandoff::NONE; }; const std::vector cases = { - {.last = ball_row, .ids = "00.04b"}, - {.last = ball_row + 1, .ids = "00.04b"}, - {.last = parcel_row, .ids = "00.04b,00.05"}, + {.last = "00.04", .ids = "00.04b"}, + {.last = "00.04b", .ids = "00.04b"}, + {.last = "00.05", .ids = "00.04b,00.05"}, + {.last = "00.12b", .ids = "00.04b,00.05,00.06,00.07,00.08,00.09,00.10,00.11,00.12"}, + { + .last = "00.12b", + .handoffs = {.starter_rng = true, .gift_rng = true}, + .ids = "00.04b,00.05,00.06,00.07,00.08,00.09,00.10,00.11,00.12", + .stop = StoryHandoff::GIFT_RNG, + }, }; for (const Case& item : cases){ - const std::vector rows = rows_after_starter_rng(checkpoints, ball_row, item.last); - std::string ids; - for (const StoryCheckpoint& row : rows){ - ids += (ids.empty() ? "" : ",") + row.id; + const StoryRunPlan plan = rows_after_starter_rng(checkpoints, ball_row, row_index(item.last), item.handoffs); + if (joined_ids(plan) != item.ids || first_step(plan) != "above_rival_trigger" || plan.stop != item.stop){ + return std::format( + "To {}: expected rows {} starting at above_rival_trigger, stop {}; got {} starting at {}, stop {}", + item.last, item.ids, int(item.stop), joined_ids(plan), first_step(plan), int(plan.stop) + ); } - const std::string first_step = rows.front().steps.empty() ? "" : rows.front().steps.front().name; - if (ids != item.ids || first_step != "above_rival_trigger"){ + } + return true; +} + + +// What the run says when Gift RNG's loop ends: a shiny goes on to 00.12b; +// otherwise the save beside the salesman is where to go on from. +UnitTestResult test_gift_messages(){ + struct Case{ + StarterRngLoopResult result; + std::string expected; + }; + const std::string save = "The save is still beside the salesman, facing him: run Gift RNG (Magikarp) from it with " + "an attempt from the log, then start AutoStory at 00.12b."; + const std::vector cases = { + { + .result = {.end = StarterRngLoopEnd::SHINY, .attempts_run = 1, .remaining_sids = {300}, .sid = uint16_t(300)}, + .expected = "Gift RNG: shiny Magikarp, on the planned target. 00.12b saves it.", + }, + { + .result = {.end = StarterRngLoopEnd::SHINY_OFF_TARGET, .attempts_run = 1, .remaining_sids = {300}, .sid = std::nullopt}, + .expected = "Gift RNG: shiny Magikarp, but not the planned target. 00.12b saves it.", + }, + { + .result = {.end = StarterRngLoopEnd::OUT_OF_ATTEMPTS, .attempts_run = 3, .remaining_sids = {}, .sid = std::nullopt}, + .expected = "Gift RNG: every planned attempt hit its target and none was shiny, so the RNG plan's SID doesn't fit. " + "SIDs left: -. " + save, + }, + { + .result = {.end = StarterRngLoopEnd::OUT_OF_TIME, .attempts_run = 4, .remaining_sids = {300}, .sid = std::nullopt}, + .expected = "Gift RNG: out of time after 4 attempts. " + save, + }, + }; + for (const Case& item : cases){ + const std::string got = gift_rng_loop_message(item.result); + if (got != item.expected){ + return "Expected \"" + item.expected + "\", got \"" + got + "\"."; + } + } + return true; +} + +// 00.12 saves facing the salesman for Gift RNG; after a shiny Magikarp, +// 00.12b leaves its summary and saves there, then the run goes on. +UnitTestResult test_rows_after_magikarp(){ + const std::vector& checkpoints = story_checkpoints(); + const size_t salesman_row = row_index("00.12"); + const StoryCheckpoint& salesman = checkpoints[salesman_row]; + const StoryCheckpoint& after = checkpoints[salesman_row + 1]; + const std::optional start = after.start ? story_pose(*after.start) : std::nullopt; + if (salesman.handoff != StoryHandoff::GIFT_RNG || !after.live_start || !start + || start->map != "Route4_PokemonCenter_1F" || !(start->tile == TilePos{2, 3}) || start->facing != Direction::WEST + ){ + return "Expected 00.12 to be Gift RNG's handoff and 00.12b to start from the live game at (2, 3) facing west."; + } + for (const std::string& last : {"00.12", "00.12b"}){ + const StoryRunPlan plan = rows_after_gift_rng(checkpoints, salesman_row, row_index(last), StoryHandoffs{.gift_rng = true}); + if (joined_ids(plan) != "00.12b" || first_step(plan) != LEAVE_GIFT_SUMMARY || plan.stop != StoryHandoff::NONE){ return std::format( - "Expected rows {} starting at above_rival_trigger; got {} starting at {}", item.ids, ids, first_step + "To {}: expected 00.12b from {}; got {} from {}", last, LEAVE_GIFT_SUMMARY, joined_ids(plan), first_step(plan) ); } } return true; } +// Gift RNG's outcomes count in the RNG plan as Starter RNG's do: only a +// shiny on the target confirms anything. +UnitTestResult test_gift_outcomes(){ + struct Case{ + GiftRngOutcome outcome; + RngAttemptOutcome expected; + }; + const std::vector cases = { + {.outcome = GiftRngOutcome::shiny_on_target, .expected = RngAttemptOutcome::SHINY}, + {.outcome = GiftRngOutcome::shiny, .expected = RngAttemptOutcome::SHINY_OFF_TARGET}, + {.outcome = GiftRngOutcome::shiny_off_target, .expected = RngAttemptOutcome::SHINY_OFF_TARGET}, + {.outcome = GiftRngOutcome::target_hit, .expected = RngAttemptOutcome::HIT_NOT_SHINY}, + {.outcome = GiftRngOutcome::out_of_resets, .expected = RngAttemptOutcome::MISSED}, + {.outcome = GiftRngOutcome::no_matches, .expected = RngAttemptOutcome::MISSED}, + }; + for (const Case& item : cases){ + const RngAttemptOutcome got = plan_outcome(loop_outcome(item.outcome)); + if (got != item.expected){ + return std::format( + "Gift RNG outcome {}: expected plan outcome {}, got {}.", int(item.outcome), int(item.expected), int(got) + ); + } + } + // The plan's targets are Starter RNG's (StarterRngPlanOptions::gift_plan()). + if (fixed_advances_offset(PokemonFRLG_RngTarget::magikarp) != fixed_advances_offset(PokemonFRLG_RngTarget::starters)){ + return "Expected the Magikarp's advances offset to be the starters'."; + } + return true; +} + } void add_tests_AutoStoryRngLoop(UnitTestDatabase& database){ database.add("shiny messages", test_shiny_messages); database.add("rows after a shiny", test_rows_after_shiny); + database.add("Gift RNG messages", test_gift_messages); + database.add("00.12 and 00.12b: rows after a shiny Magikarp", test_rows_after_magikarp); + database.add("Gift RNG outcomes in the RNG plan", test_gift_outcomes); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp index 17a5018541..613eab3c57 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp @@ -125,6 +125,24 @@ std::vector StarterRngPlanOptions::plan_candidates(Logger& logger, } std::vector StarterRngPlanOptions::plan(Logger& logger, Language language, uint16_t tid) const{ + return plan_for(logger, language, tid, "Starter RNG"); +} + +std::vector StarterRngPlanOptions::gift_plan(Logger& logger, Language language, uint16_t tid) const{ + // In English, Gift RNG's Magikarp is timed as Starter RNG's Squirtle is: + // the same title screen offset (-845 ms), 200 Continue Screen frames, an + // in-game wait of at least 7,500 ms (7,200 ms of presses and a 300 ms + // gap) and advances offset 160, so Starter RNG's plan suits it. The + // salesman's text takes 1.5 s less in French and 1.5 s more in Japanese. + if (language != Language::English){ + throw UserSetupError(logger, "RNG plan: Gift RNG in AutoStory plans for English games only."); + } + return plan_for(logger, language, tid, "Gift RNG"); +} + +std::vector StarterRngPlanOptions::plan_for( + Logger& logger, Language language, uint16_t tid, const std::string& program +) const{ // SID Helper's candidates are logged even with the SID known, to place // it among them (rng_log_confirmed()). const std::vector helper = helper_candidates(tid); @@ -146,7 +164,7 @@ std::vector StarterRngPlanOptions::plan(Logger& logger, Language lan }); if (attempts.empty()){ throw UserSetupError(logger, - "RNG plan: no Starter RNG target passes the natures, minimum IVs and shiny options for TID " + std::to_string(tid) + "." + "RNG plan: no " + program + " target passes the natures, minimum IVs and shiny options for TID " + std::to_string(tid) + "." ); } for (size_t c = 0; c < attempts.size(); c++){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h index 5420f4193a..19e0626e31 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h @@ -43,6 +43,9 @@ class StarterRngPlanOptions : public GroupOption{ // in "language", logged as "RngPlan:" lines. Throws UserSetupError // when no target passes. std::vector plan(Logger& logger, Language language, uint16_t tid) const; + // As plan(), for Gift RNG's Magikarp from Route 4's salesman. Throws + // UserSetupError in a language other than English. + std::vector gift_plan(Logger& logger, Language language, uint16_t tid) const; // SID Helper's candidates for "tid", from the two SID Helper options. std::vector helper_candidates(uint16_t tid) const; // The SIDs plan() plans for: the SID if set, else helper_candidates(). @@ -64,6 +67,9 @@ class StarterRngPlanOptions : public GroupOption{ private: RngTargetFilters filters(Logger& logger) const; std::optional sid(Logger& logger) const; + // The attempts plan_starter_rng() finds, logged, for `program` + // ("Starter RNG") in the error when there are none. + std::vector plan_for(Logger& logger, Language language, uint16_t tid, const std::string& program) const; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 552bc29f1b..c113ae18d9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -342,12 +342,25 @@ StoryCheckpoint route3(){ // (40, 11) wanders up and down at x 40 across the way east, looking north // and south: if she sees the player, the walk fights her (story_interrupts()). StoryCheckpoint magikarp_salesman(){ - return row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), {}, StartMenu::FULL, { + StoryCheckpoint checkpoint = row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), {}, StartMenu::FULL, { running(route("route4_nurse", at(ROUTE4_CENTER, 7, 4, NORTH), "east over Route 3, off the grass, north into Route 4 and its Pokemon Center")), heal("heal_route4"), - // The seam for a Gift RNG program: the save faces the salesman (1, 3). + // Gift RNG starts from a save facing the salesman (1, 3). running(route("before_salesman", at(ROUTE4_CENTER, 2, 3, WEST), "beside the Magikarp salesman, facing him")), }); + checkpoint.handoff = StoryHandoff::GIFT_RNG; + return checkpoint; +} + +// Gift RNG buys the Magikarp (500 of the money 00.12 brings) without +// moving the player and leaves its summary open, or HOME. This saves the +// shiny where 00.12 ended. +StoryCheckpoint after_gift_rng(){ + StoryCheckpoint checkpoint = row("00.12b", "after Gift RNG: save with the Magikarp", at(ROUTE4_CENTER, 2, 3, WEST), {}, StartMenu::FULL, { + game_step(LEAVE_GIFT_SUMMARY, at(ROUTE4_CENTER, 2, 3, WEST), "the Magikarp's summary Gift RNG left open, or HOME"), + }); + checkpoint.live_start = true; + return checkpoint; } } @@ -356,7 +369,7 @@ StoryCheckpoint magikarp_salesman(){ const std::vector& story_checkpoints(){ static const std::vector checkpoints{ new_game(), after_sid_helper(), leave_house(), oaks_trigger(), to_starter_ball(), starter(), parcel(), pokedex(), shopping(), forest(), brock(), - running_shoes(), route3(), magikarp_salesman(), + running_shoes(), route3(), magikarp_salesman(), after_gift_rng(), }; return checkpoints; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h index e1123642c5..466a64e390 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h @@ -4,7 +4,7 @@ * * FireRed from a new game to the Boulder Badge, the Running Shoes and Route * 3 to the Magikarp salesman in Route 4's Pokemon Center, as story - * checkpoints 00.01 to 00.12, each saved at its end. After 00.10 every + * checkpoints 00.01 to 00.12b, each saved at its end. After 00.10 every * route and leg runs. Taken from a by-hand run on console B (2026-11-01) * and the pokefirered map scripts. * @@ -23,8 +23,9 @@ namespace PokemonFRLG{ // Checkpoints 00.01 to 00.12, in order, with 00.01b, the way back in after -// SID Helper, after 00.01, and 00.04 split in two at Squirtle's ball for -// Starter RNG: 00.04 and 00.04b. +// SID Helper, after 00.01, 00.04 split in two at Squirtle's ball for +// Starter RNG: 00.04 and 00.04b, and 00.12b, the way back in after Gift +// RNG, after 00.12. const std::vector& story_checkpoints(); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp index d89620b953..48088b6a25 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -212,8 +212,8 @@ UnitTestResult check_checkpoint(const StoryCheckpoint& checkpoint, StoryPose& po UnitTestResult test_routes_and_chain(){ const std::vector& checkpoints = story_checkpoints(); - if (checkpoints.size() != 14 || checkpoints[0].start){ - return "Expected 14 checkpoints, the first from a new game."; + if (checkpoints.size() != 15 || checkpoints[0].start){ + return "Expected 15 checkpoints, the first from a new game."; } StoryPose pose; for (size_t c = 0; c < checkpoints.size(); c++){ @@ -698,6 +698,8 @@ const char* handoff_name(StoryHandoff handoff){ return "SID Helper"; case StoryHandoff::STARTER_RNG: return "Starter RNG"; + case StoryHandoff::GIFT_RNG: + return "Gift RNG"; } return "?"; } @@ -782,6 +784,30 @@ void add_tests_RunPlan(UnitTestDatabase& database){ } } +void add_tests_GiftRunPlan(UnitTestDatabase& database){ + const RunPlanScenario scenarios[] = { + { + .name = "Gift RNG: stops after 00.12's save", + .first = "00.11", + .handoffs = {.gift_rng = true}, + .expected_ids = "00.11 00.12", + .expected_stop = StoryHandoff::GIFT_RNG, + }, + {.name = "Gift RNG off: 00.12b is passed over", .first = "00.11", .expected_ids = "00.11 00.12"}, + {.name = "00.12b runs when started at", .first = "00.12b", .handoffs = {.gift_rng = true}, .expected_ids = "00.12b"}, + { + .name = "Starter RNG first, with both on", + .first = "00.03", + .handoffs = {.starter_rng = true, .gift_rng = true}, + .expected_ids = "00.03 00.04", + .expected_stop = StoryHandoff::STARTER_RNG, + }, + }; + for (const RunPlanScenario& scenario : scenarios){ + database.add(scenario); + } +} + std::string step_names(const StoryCheckpoint& checkpoint){ std::string text; for (const StoryStep& step : checkpoint.steps){ @@ -821,16 +847,17 @@ UnitTestResult test_starter_rows_joined(){ return true; } -// The way back in after SID Helper loads no save and starts where 00.01 -// leaves the player, so 00.02 follows it unchanged. +// The ways back in after SID Helper and Gift RNG load no save and start +// where 00.01 and 00.12 leave the player, so the rows after them follow +// unchanged. UnitTestResult test_after_sid_helper_row(){ const StoryCheckpoint& sid = story_checkpoints()[row_index("00.01b")]; if (!sid.live_start || story_checkpoints()[0].handoff != StoryHandoff::SID_HELPER){ return "Expected 00.01 to be SID Helper's handoff and 00.01b to start from the live game."; } for (const StoryCheckpoint& checkpoint : story_checkpoints()){ - if (checkpoint.live_start != (checkpoint.id == "00.01b")){ - return checkpoint.id + ": only 00.01b starts from the live game."; + if (checkpoint.live_start != (checkpoint.id == "00.01b" || checkpoint.id == "00.12b")){ + return checkpoint.id + ": only 00.01b and 00.12b start from the live game."; } } return true; @@ -881,7 +908,8 @@ UnitTestResult test_option_presses(){ void add_tests_AutoStory(UnitTestDatabase& database){ add_tests_RunPlan(database); - database.add("00.01b: back in after SID Helper", test_after_sid_helper_row); + add_tests_GiftRunPlan(database); + database.add("00.01b and 00.12b: back in after SID Helper and Gift RNG", test_after_sid_helper_row); database.add("00.04 and 00.04b: one checkpoint with Starter RNG off", test_starter_rows_joined); database.add("every route plans, and checkpoints chain", test_routes_and_chain); database.add("step names are unique per checkpoint", test_unique_names); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp new file mode 100644 index 0000000000..1dff61fbeb --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp @@ -0,0 +1,119 @@ +/* Gift RNG Console Steps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "PokemonFRLG_GiftRngConsoleSteps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +StarterRngOutcome loop_outcome(GiftRngOutcome outcome){ + switch (outcome){ + case GiftRngOutcome::shiny: + return StarterRngOutcome::shiny; + case GiftRngOutcome::shiny_on_target: + return StarterRngOutcome::shiny_on_target; + case GiftRngOutcome::shiny_off_target: + return StarterRngOutcome::shiny_off_target; + case GiftRngOutcome::target_hit: + return StarterRngOutcome::target_hit; + case GiftRngOutcome::out_of_resets: + return StarterRngOutcome::out_of_resets; + case GiftRngOutcome::no_matches: + return StarterRngOutcome::no_matches; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown Gift RNG outcome."); +} + + +namespace{ + +// Gift RNG's settings for one try: no resets after the try's deadline, +// and its resets reported to the loop. +GiftRngSettings try_settings(Language language, const StarterRngAttemptCall& call){ + return GiftRngSettings{ + .language = language, + .profile = 0, + .use_teachy_tv = false, + .take_video = false, + .identify_shiny = true, + .max_resets = [deadline = call.deadline, max_resets = call.max_resets]{ + return current_time() >= deadline ? uint64_t(0) : max_resets; + }, + .max_rare_candies = []{ return uint64_t(0); }, + .on_reset = [on_reset = call.on_reset](const GiftRngProgress& progress){ + if (on_reset){ + on_reset(StarterRngProgress{.resets = progress.resets, .calibrations = progress.calibrations}); + } + }, + }; +} + +} + + +ConsoleGiftRngSteps::ConsoleGiftRngSteps( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const GiftRngConsoleSettings& settings, const GiftRngNotifications& notifications +) + : m_env(env) + , m_context(context) + , m_settings(settings) + , m_notifications(notifications) + // TODO: LeafGreen, once AutoStory plays it. + , m_seeds(seeds_json_path(true, settings.language)) +{} + +WallClock ConsoleGiftRngSteps::now(){ + return current_time(); +} + +StarterRngResult ConsoleGiftRngSteps::run_attempt(const RngAttempt& attempt, const StarterRngAttemptCall& call){ + // Gift RNG's default Nearby Seed Radius. + const int SEED_RADIUS = 5; + const SeedMatch seed_match = m_seeds.find_seed(attempt.target.seed.seed, m_settings.sound, SEED_RADIUS); + if (!seed_match.found){ + throw InternalProgramError(&m_env.logger(), PA_CURRENT_FUNCTION, "The plan's seed isn't in the seed list it came from."); + } + + GiftRngStats stats; + GiftRngAttempts attempts( + GiftRngTarget{ + .target = PokemonFRLG_RngTarget::magikarp, + .seed = attempt.target.seed.seed, + .advances = attempt.target.advances, + .seed_match = seed_match, + }, + try_settings(m_settings.language, call), + GiftRngDisplays{ + .target = m_target_display, + .filters = m_filter_display, + .calibration = m_calibration_display, + }, + m_notifications, + stats + ); + try{ + const GiftRngResult result = attempts.run(m_env, m_context); + return StarterRngResult{.outcome = loop_outcome(result.outcome), .pokemon = result.pokemon}; + }catch (OperationFailedException& e){ + // Gift RNG refuses timings it can't press (check_timings()) this + // way; a failure in the game sends an error report. + if (e.error_report_mode() != ErrorReportMode::NO_ERROR_REPORT){ + throw; + } + throw StarterRngTimingsRefused(e.message()); + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h new file mode 100644 index 0000000000..ef51bf1e1a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h @@ -0,0 +1,70 @@ +/* Gift RNG Console Steps + * + * From: https://github.com/PokemonAutomation/ + * + * The RNG plan loop's steps (PokemonFRLG_StarterRngLoop.h) on the console + * for Gift RNG's Magikarp: each try runs Gift RNG's attempts + * (PokemonFRLG_GiftRngAttempts.h) from the save beside Route 4's salesman. + * The loop counts tries in Starter RNG's outcomes; Gift RNG's map onto + * them one to one. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_GiftRngConsoleSteps_H +#define PokemonAutomation_PokemonFRLG_GiftRngConsoleSteps_H + +#include "Common/Cpp/Time.h" +#include "CommonFramework/Language.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngDisplays.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngAttempts.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_StarterRngLoop.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Gift RNG's outcome as the loop counts it. +StarterRngOutcome loop_outcome(GiftRngOutcome outcome); + + +struct GiftRngConsoleSettings{ + Language language = Language::English; + SoundSetting sound = SoundSetting::Mono; +}; + +// The loop's steps on the console, with Gift RNG's displays, stats and +// calibration kept here (AutoStory doesn't show them). Magikarp, FireRed, +// the Switch's default profile, no Teachy TV and no Rare Candies. +class ConsoleGiftRngSteps : public StarterRngLoopSteps{ +public: + ConsoleGiftRngSteps( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const GiftRngConsoleSettings& settings, const GiftRngNotifications& notifications + ); + + virtual WallClock now() override; + virtual StarterRngResult run_attempt(const RngAttempt& attempt, const StarterRngAttemptCall& call) override; + +private: + SingleSwitchProgramEnvironment& m_env; + ProControllerContext& m_context; + GiftRngConsoleSettings m_settings; + GiftRngNotifications m_notifications; + SeedsDatabase m_seeds; + RngTargetDisplay m_target_display; + RngFilterDisplay m_filter_display; + RngCalibrationDisplay m_calibration_display; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index b5cf91658a..9c1496db29 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1885,6 +1885,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_HardReset.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_LocationsDatabase.cpp From 72430b53e79b17d8f6d17cff9ce12f9ca921cc65 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 20:12:52 -0800 Subject: [PATCH 3/8] FRLG AutoStory: start at 00.12b with Run Gift RNG on to run Gift RNG from the salesman save A save beside the Magikarp salesman (00.12's end, or a failed Gift RNG run) had no way back into the in-program handoff short of replaying 00.12. Now a run started at 00.12b with Run Gift RNG in AutoStory on loads that save, checks it is in Route 4, runs Gift RNG's attempts and, for a shiny, goes on with 00.12b. With the option off, 00.12b stays the way back in after a by-hand Gift RNG run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStory.cpp | 30 +++++++++++++++---- .../AutoStory/PokemonFRLG_AutoStory.h | 7 +++++ .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 7 +++++ .../AutoStory/PokemonFRLG_AutoStoryEvents.h | 7 +++-- .../PokemonFRLG_AutoStoryRngLoop.cpp | 5 ++-- .../PokemonFRLG_AutoStoryRngLoop_Tests.cpp | 5 ++-- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 3 +- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 17 ++++++++++- 8 files changed, 68 insertions(+), 13 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index ecbb6a15ea..115acc6bad 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -75,6 +75,8 @@ const char* const RUN_GIFT_RNG_TEXT = "Pokemon Center, run Gift RNG's attempts for the Magikarp from that save, as the RNG plan lists them for its TID and " "SID (set by Run Starter RNG in AutoStory, or typed in). A shiny Magikarp is saved there (00.12b) and the run goes " "on; otherwise the run stops with the save beside the salesman.
" + "To try again from that save, start at 00.12b with this on: it loads the save and runs the attempts first. " + "After Gift RNG run by hand, start at 00.12b with this off.
" "English only. The Magikarp costs 500 and needs a free party slot; AutoStory's options leave the text fast."; AutoStory::AutoStory() @@ -223,16 +225,17 @@ static void throw_if_failed(const Pokemon::SegmentReport& report){ // Gets the game to where checkpoint `first` starts: a new game from the // title screen, the game as another program left it, or the checkpoint's -// save loaded. +// save loaded. `at_handoff`: the run starts with the RNG handoff before a +// live-start row, whose save is where that row starts, so it is loaded. static void prepare_start( SingleSwitchProgramEnvironment& env, ProControllerContext& context, - const std::vector& checkpoints, size_t first, StoryEnv& story + const std::vector& checkpoints, size_t first, StoryEnv& story, bool at_handoff ){ const StoryCheckpoint& checkpoint = checkpoints[first]; if (!checkpoint.start){ return; } - if (checkpoint.live_start){ + if (checkpoint.live_start && !at_handoff){ env.console.log(checkpoint.id + " starts from the game as it is: no save to load."); return; } @@ -570,12 +573,15 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex StoryEnv story{env.console, context, checkpoints[first].supplies, checkpoints[first].supplies, false, RUN.checks()}; story.player_name = player_name; - prepare_start(env, context, checkpoints, first, story); + const bool at_gift_rng = plan.stop == StoryHandoff::GIFT_RNG && plan.rows.empty(); + prepare_start(env, context, checkpoints, first, story, at_gift_rng); StoryExecutor executor(story); executor.set_checks(RUN.checks()); executor.set_language(RUN.language()); - const std::optional done_through = run_story_plan(env, context, story, executor, std::move(plan)); + const std::optional done_through = at_gift_rng + ? run_from_gift_rng_save(env, context, story, executor, first) + : run_story_plan(env, context, story, executor, std::move(plan)); if (done_through){ send_story_finished(env, NOTIFICATION_PROGRAM_FINISH, *done_through, sid_before, RNG_PLAN.SID); } @@ -617,6 +623,20 @@ std::optional AutoStory::run_story_plan( } } +std::optional AutoStory::run_from_gift_rng_save( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + StoryEnv& story, StoryExecutor& executor, size_t first +){ + const StarterRngPlanned planned = plan_handoff_attempts( + env, context, RNG_PLAN, RUN.language(), StoryHandoff::GIFT_RNG, story_checkpoints()[first].id + ); + std::optional next = after_magikarp_salesman(env, context, story, planned, first - 1); + if (!next){ + return std::nullopt; + } + return run_story_plan(env, context, story, executor, std::move(*next)); +} + std::optional AutoStory::after_starter_ball( SingleSwitchProgramEnvironment& env, ProControllerContext& context, StoryEnv& story, const StarterRngPlanned& planned, size_t ball_row diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h index 664cf0536a..a8dd3b56dd 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h @@ -63,6 +63,13 @@ class AutoStory : public SingleSwitchProgramInstance{ SingleSwitchProgramEnvironment& env, ProControllerContext& context, StoryEnv& story, const StarterRngPlanned& planned, size_t ball_row ); + // Started at 00.12b (row `first`) with Run Gift RNG in AutoStory on, + // the save beside the salesman loaded: Gift RNG's attempts, then for a + // shiny the rows after them. As run_story_plan() returns. + std::optional run_from_gift_rng_save( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + StoryEnv& story, StoryExecutor& executor, size_t first + ); // After 00.12 saved beside the Magikarp salesman (row `salesman_row`): // Gift RNG's attempts run here. Returns the rows to run after a shiny, // or nothing once the run has stopped. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index 5f330ee775..c5b496c231 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -446,6 +446,13 @@ StoryRunPlan plan_story_run( const std::vector& checkpoints, size_t first, size_t last, const StoryHandoffs& handoffs ){ StoryRunPlan plan; + // Started at the way back in from Gift RNG with its handoff on: the run + // takes the handoff again, from the save the row before ends at. + if (first > 0 && first < checkpoints.size() && checkpoints[first].live_start + && checkpoints[first - 1].handoff == StoryHandoff::GIFT_RNG && handoffs.gift_rng){ + plan.stop = StoryHandoff::GIFT_RNG; + return plan; + } for (size_t c = first; c <= last && c < checkpoints.size(); c++){ const StoryCheckpoint& row = checkpoints[c]; if (row.live_start && c != first){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h index 0776320475..50771d8186 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -111,13 +111,16 @@ struct StoryRunPlan{ // NONE: the run ends after `rows`. SID_HELPER: `rows` is empty; the run // plays the new game to the player's keyboard and stops there. // STARTER_RNG: the run stops after `rows`, the last of which saves at - // the starter's ball. GIFT_RNG: likewise, beside the Magikarp salesman. + // the starter's ball. GIFT_RNG: likewise, beside the Magikarp salesman; + // with `rows` empty, the run started at 00.12b and takes Gift RNG from + // the save beside the salesman, where the row before `first` ends. StoryHandoff stop = StoryHandoff::NONE; }; // The rows of `checkpoints` from `first` to `last`, both inclusive, as a // run with `handoffs` takes them: live-start rows only when the run -// starts there, a run that reaches a handoff that is on stops for it, and +// starts there (00.12b with Gift RNG's handoff on starts at the handoff +// instead), a run that reaches a handoff that is on stops for it, and // a STARTER_RNG row whose handoff is off is joined with the row after it, // under its own id. StoryRunPlan plan_story_run( diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp index ac111cb298..73a001a417 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp @@ -65,8 +65,9 @@ std::string starter_rng_loop_message(const StarterRngLoopResult& result){ std::string gift_rng_loop_message(const StarterRngLoopResult& result){ - const std::string save = "The save is still beside the salesman, facing him: run Gift RNG (Magikarp) from it with " - "an attempt from the log, then start AutoStory at 00.12b."; + const std::string save = "The save is still beside the salesman, facing him: start AutoStory at 00.12b with Run Gift " + "RNG in AutoStory on to try again, or run Gift RNG (Magikarp) from it by hand with an attempt from the log, then " + "start AutoStory at 00.12b with that option off."; switch (result.end){ case StarterRngLoopEnd::SHINY: return "Gift RNG: shiny Magikarp, on the planned target. 00.12b saves it."; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp index 4ecb91ce13..0316fca831 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp @@ -146,8 +146,9 @@ UnitTestResult test_gift_messages(){ StarterRngLoopResult result; std::string expected; }; - const std::string save = "The save is still beside the salesman, facing him: run Gift RNG (Magikarp) from it with " - "an attempt from the log, then start AutoStory at 00.12b."; + const std::string save = "The save is still beside the salesman, facing him: start AutoStory at 00.12b with Run Gift " + "RNG in AutoStory on to try again, or run Gift RNG (Magikarp) from it by hand with an attempt from the log, then " + "start AutoStory at 00.12b with that option off."; const std::vector cases = { { .result = {.end = StarterRngLoopEnd::SHINY, .attempts_run = 1, .remaining_sids = {300}, .sid = uint16_t(300)}, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index c113ae18d9..064ab67681 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -354,7 +354,8 @@ StoryCheckpoint magikarp_salesman(){ // Gift RNG buys the Magikarp (500 of the money 00.12 brings) without // moving the player and leaves its summary open, or HOME. This saves the -// shiny where 00.12 ended. +// shiny where 00.12 ended. A run started here with Gift RNG's handoff on +// loads the save beside the salesman and runs Gift RNG first. StoryCheckpoint after_gift_rng(){ StoryCheckpoint checkpoint = row("00.12b", "after Gift RNG: save with the Magikarp", at(ROUTE4_CENTER, 2, 3, WEST), {}, StartMenu::FULL, { game_step(LEAVE_GIFT_SUMMARY, at(ROUTE4_CENTER, 2, 3, WEST), "the Magikarp's summary Gift RNG left open, or HOME"), diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp index 48088b6a25..485269a0d2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -794,7 +794,22 @@ void add_tests_GiftRunPlan(UnitTestDatabase& database){ .expected_stop = StoryHandoff::GIFT_RNG, }, {.name = "Gift RNG off: 00.12b is passed over", .first = "00.11", .expected_ids = "00.11 00.12"}, - {.name = "00.12b runs when started at", .first = "00.12b", .handoffs = {.gift_rng = true}, .expected_ids = "00.12b"}, + {.name = "Gift RNG off: 00.12b runs when started at", .first = "00.12b", .expected_ids = "00.12b"}, + { + .name = "Gift RNG: started at 00.12b, from the save beside the salesman", + .first = "00.12b", + .handoffs = {.gift_rng = true}, + .expected_ids = "", + .expected_stop = StoryHandoff::GIFT_RNG, + }, + { + .name = "Gift RNG: started at 00.12b with Starter RNG on too", + .first = "00.12b", + .handoffs = {.sid_helper = true, .starter_rng = true, .gift_rng = true}, + .expected_ids = "", + .expected_stop = StoryHandoff::GIFT_RNG, + }, + {.name = "Starter RNG alone: 00.12b runs when started at", .first = "00.12b", .handoffs = {.starter_rng = true}, .expected_ids = "00.12b"}, { .name = "Starter RNG first, with both on", .first = "00.03", From 5c37581738beab20bede945494c3543d4cda1c67 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 20:33:56 -0800 Subject: [PATCH 4/8] FRLG AutoStory tests: loop over const char* to satisfy GCC's range-loop-construct Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp index 0316fca831..98de7d450b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp @@ -190,7 +190,7 @@ UnitTestResult test_rows_after_magikarp(){ ){ return "Expected 00.12 to be Gift RNG's handoff and 00.12b to start from the live game at (2, 3) facing west."; } - for (const std::string& last : {"00.12", "00.12b"}){ + for (const char* last : {"00.12", "00.12b"}){ const StoryRunPlan plan = rows_after_gift_rng(checkpoints, salesman_row, row_index(last), StoryHandoffs{.gift_rng = true}); if (joined_ids(plan) != "00.12b" || first_step(plan) != LEAVE_GIFT_SUMMARY || plan.stop != StoryHandoff::NONE){ return std::format( From ed05b858965505569184b422c321ad796bdd1b34 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 20:37:37 -0800 Subject: [PATCH 5/8] FRLG AutoStory tests: take checkpoint ids as std::string_view row_index() only compares and reports the id, and the Gift RNG case now loops over its two ids as string_views rather than raw char pointers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp index 98de7d450b..004c499ad9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Logging/AbstractLogger.h" @@ -81,14 +82,14 @@ UnitTestResult test_shiny_messages(){ } -size_t row_index(const std::string& id){ +size_t row_index(std::string_view id){ const std::vector& checkpoints = story_checkpoints(); for (size_t c = 0; c < checkpoints.size(); c++){ if (checkpoints[c].id == id){ return c; } } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No checkpoint " + id); + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No checkpoint " + std::string(id)); } std::string joined_ids(const StoryRunPlan& plan){ @@ -190,7 +191,7 @@ UnitTestResult test_rows_after_magikarp(){ ){ return "Expected 00.12 to be Gift RNG's handoff and 00.12b to start from the live game at (2, 3) facing west."; } - for (const char* last : {"00.12", "00.12b"}){ + for (std::string_view last : {"00.12", "00.12b"}){ const StoryRunPlan plan = rows_after_gift_rng(checkpoints, salesman_row, row_index(last), StoryHandoffs{.gift_rng = true}); if (joined_ids(plan) != "00.12b" || first_step(plan) != LEAVE_GIFT_SUMMARY || plan.stop != StoryHandoff::NONE){ return std::format( From f7978bde54676541352ba450b769d68cc1654399 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 21:00:12 -0800 Subject: [PATCH 6/8] FRLG AutoStory: start Gift RNG from the stored calibration, save it back Gift RNG in AutoStory starts from the console's seed and CSF offsets in the RNG plan's calibration, which Starter RNG may already have learned, and its own in-game advances offset (GIFT_INGAME, 0 until saved): the salesman's in-game offset isn't Starter RNG's. Each reset's calibration is saved back as Starter RNG's is. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStory.cpp | 10 +++++--- .../PokemonFRLG_AutoStoryRngLoop_Tests.cpp | 23 +++++++++++++++++++ .../PokemonFRLG_AutoStoryRngOptions.cpp | 5 ++++ .../PokemonFRLG_AutoStoryRngOptions.h | 2 ++ .../PokemonFRLG_GiftRngConsoleSteps.cpp | 4 +++- .../PokemonFRLG_GiftRngConsoleSteps.h | 5 +++- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 8174bf8f23..41cbaf14e9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -490,9 +490,13 @@ static bool run_gift_rng_in_program( const StarterRngPlanned& planned, StarterRngPlanOptions& options, Language language, const GiftRngNotifications& notifications, EventNotificationOption& finished ){ - ConsoleGiftRngSteps steps( - env, context, GiftRngConsoleSettings{.language = language, .sound = options.SOUND}, notifications - ); + // The console's seed and CSF offsets, from Starter RNG too, and Gift + // RNG's own in-game offset, saved as Starter RNG's are. + RngStopCalibrationOptions& calibration = options.CALIBRATION; + ConsoleGiftRngSteps console_steps(env, context, GiftRngConsoleSettings{ + .language = language, .sound = options.SOUND, .calibration = calibration.calibration(calibration.GIFT_INGAME), + }, notifications); + CalibrationSavingSteps steps(console_steps, calibration, calibration.GIFT_INGAME); const StarterRngLoopResult result = run_rng_plan_in_program(env, steps, planned, options); return shiny_or_stop(env, finished, result, gift_rng_loop_message(result)); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp index 3fe7de0dea..480cdd0726 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp @@ -305,6 +305,28 @@ UnitTestResult test_starter_calibration_saved(){ return true; } +// Gift RNG starts from the console's seed and CSF offsets, which Starter +// RNG may have saved, and its own in-game offset: 0 until it saves one. +// Saving Gift RNG's leaves Starter RNG's in-game offset as it was. +UnitTestResult test_gift_calibration(){ + StarterRngPlanOptions options; + RngStopCalibrationOptions& calibration = options.CALIBRATION; + calibration.save({.seed_offset = 1.0, .csf_offset = 0.5, .ingame_offset = -1.2}, calibration.STARTER_INGAME); + const RngCalibrations gift = calibration.calibration(calibration.GIFT_INGAME); + const RngCalibrations expected{.seed_offset = 17 / FRLG_FRAME_DURATION, .csf_offset = 0.5, .ingame_offset = 0}; + if (gift != expected){ + return std::format("Expected Gift RNG to start from {}; got {}", calibration_text(expected), calibration_text(gift)); + } + calibration.save({.seed_offset = 1.0, .csf_offset = 0.5, .ingame_offset = 9.0}, calibration.GIFT_INGAME); + if (double(calibration.GIFT_INGAME) != 9.0 || double(calibration.STARTER_INGAME) != -1.2){ + return std::format( + "Expected in-game offsets Gift 9 and Starter -1.2; got {} and {}", + double(calibration.GIFT_INGAME), double(calibration.STARTER_INGAME) + ); + } + return true; +} + } @@ -316,6 +338,7 @@ void add_tests_AutoStoryRngLoop(UnitTestDatabase& database){ database.add("Gift RNG outcomes in the RNG plan", test_gift_outcomes); database.add("Starter RNG starts from the RNG plan's calibration", test_starter_calibration_from_options); database.add("Starter RNG saves its calibration after each reset", test_starter_calibration_saved); + database.add("Gift RNG keeps its own in-game calibration", test_gift_calibration); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp index 5a8f10853e..36eb29dd0c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp @@ -31,10 +31,15 @@ RngStopCalibrationOptions::RngStopCalibrationOptions() "for Squirtle only: each RNG stop keeps its own.", LockMode::LOCK_WHILE_RUNNING, 0.0 ) + , GIFT_INGAME( + "Gift RNG's In-Game Advances Calibration:
As in Gift RNG's RNG Calibration, for the Magikarp.", + LockMode::LOCK_WHILE_RUNNING, 0.0 + ) { PA_ADD_OPTION(SEED); PA_ADD_OPTION(CSF); PA_ADD_OPTION(STARTER_INGAME); + PA_ADD_OPTION(GIFT_INGAME); } RngCalibrations RngStopCalibrationOptions::calibration(const FloatingPointOption& ingame) const{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h index b2ecea1e11..47e8e7a8bf 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h @@ -51,6 +51,8 @@ class RngStopCalibrationOptions : public GroupOption{ FloatingPointOption CSF; // Starter RNG's in-game advances offset. FloatingPointOption STARTER_INGAME; + // Gift RNG's, for the Magikarp. + FloatingPointOption GIFT_INGAME; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp index 1dff61fbeb..30ec8cc45f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp @@ -68,7 +68,9 @@ ConsoleGiftRngSteps::ConsoleGiftRngSteps( , m_notifications(notifications) // TODO: LeafGreen, once AutoStory plays it. , m_seeds(seeds_json_path(true, settings.language)) -{} +{ + m_calibration_display.set_calibrations(settings.calibration); +} WallClock ConsoleGiftRngSteps::now(){ return current_time(); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h index ef51bf1e1a..9ceb3cae48 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h @@ -36,10 +36,13 @@ StarterRngOutcome loop_outcome(GiftRngOutcome outcome); struct GiftRngConsoleSettings{ Language language = Language::English; SoundSetting sound = SoundSetting::Mono; + // The calibration the first try starts from. + RngCalibrations calibration = {}; }; // The loop's steps on the console, with Gift RNG's displays, stats and -// calibration kept here (AutoStory doesn't show them). Magikarp, FireRed, +// calibration kept here (AutoStory doesn't show them). Each try after the +// first starts from the calibration the last one ended with. Magikarp, FireRed, // the Switch's default profile, no Teachy TV and no Rare Candies. class ConsoleGiftRngSteps : public StarterRngLoopSteps{ public: From 2d660ffe78a9c205cfc2b5758f010b32fe4a206c Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 21:11:18 -0800 Subject: [PATCH 7/8] FRLG AutoStory: one short line per Gift RNG reset on the overlay log Gift RNG's attempts report each reset's reads (on_read) as Starter RNG's do, and AutoStory puts them on the overlay log as one line, as "Gift RNG try 28/50: seed 9cf3 = target, advance 1669 (target 1670, -1). Missed." Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../RngManipulation/PokemonFRLG_GiftRng.cpp | 1 + .../PokemonFRLG_GiftRngAttempts.cpp | 18 +++++++++++++++++- .../PokemonFRLG_GiftRngAttempts.h | 6 ++++++ .../PokemonFRLG_GiftRngConsoleSteps.cpp | 12 +++++++++--- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp index 2abd7ef2e9..9dbc7d6b26 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp @@ -247,6 +247,7 @@ void GiftRng::program(SingleSwitchProgramEnvironment& env, ProControllerContext& .max_resets = [this]{ return uint64_t(MAX_RESETS); }, .max_rare_candies = [this]{ return uint64_t(MAX_RARE_CANDIES); }, .on_reset = nullptr, + .on_read = nullptr, }, GiftRngDisplays{.target = RNG_TARGET, .filters = RNG_FILTERS, .calibration = RNG_CALIBRATION}, GiftRngNotifications{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp index fd6c3c050a..dcf9f1f9d3 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp @@ -155,10 +155,23 @@ GiftRngResult GiftRngAttempts::run(SingleSwitchProgramEnvironment& env, ProContr if (check_shiny_gift(env, context)){ const GiftRngOutcome shiny = m_settings.identify_shiny ? identify_shiny_gift(env, context) : GiftRngOutcome::shiny; const bool on_target = shiny == GiftRngOutcome::shiny_on_target; + report_read(true); return GiftRngResult{.outcome = shiny, .pokemon = on_target ? std::optional(target_result) : std::nullopt}; } run_attempt(env, context); + report_read(false); + } +} + +void GiftRngAttempts::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, + }); } } @@ -220,6 +233,7 @@ void GiftRngAttempts::reset_game(SingleSwitchProgramEnvironment& env, ProControl m_launch_delay, false, m_settings.profile ); m_stats.resets++; + m_search_hits.clear(); if (m_settings.on_reset){ m_settings.on_reset(GiftRngProgress{.resets = m_stats.resets.load(), .calibrations = m_calibrations}); } @@ -278,10 +292,11 @@ AdvObservedPokemon GiftRngAttempts::read_gift(SingleSwitchProgramEnvironment& en } std::vector GiftRngAttempts::search(SingleSwitchProgramEnvironment& env, AdvRngFilters& filters){ - return get_search_results( + m_search_hits = get_search_results( env.console, m_searcher, filters, m_target.seed_match.seed_values, m_target.advances, m_advances_radius, m_species.gender_threshold ); + return m_search_hits; } @@ -305,6 +320,7 @@ void GiftRngAttempts::run_attempt(SingleSwitchProgramEnvironment& env, ProContro } ); + m_search_hits = search_hits; env.log("RNG search finished."); update_failed_searches(m_failed_searches, search_hits); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h index 335ebef350..8da780ae6e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h @@ -27,6 +27,7 @@ #include "PokemonFRLG_RngCalibration.h" #include "PokemonFRLG_RngDisplays.h" #include "PokemonFRLG_SeedsDatabase.h" +#include "PokemonFRLG_StarterRngAttempts.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -92,6 +93,8 @@ struct GiftRngSettings{ std::function max_rare_candies; // 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 @@ -158,6 +161,7 @@ class GiftRngAttempts{ AdvObservedPokemon read_gift(SingleSwitchProgramEnvironment& env, ProControllerContext& context); std::vector search(SingleSwitchProgramEnvironment& env, AdvRngFilters& filters); void run_attempt(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + void report_read(bool shiny); const GiftRngTarget m_target; const GiftRngSettings m_settings; @@ -175,6 +179,8 @@ class GiftRngAttempts{ RngCalibrationHistory m_calibration_history; uint64_t m_advances_radius = 0; uint16_t m_failed_searches = 0; + // The last search's hits, for on_read. + std::vector m_search_hits; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp index 30ec8cc45f..cd79be3b96 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp @@ -6,6 +6,7 @@ #include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedException.h" +#include "PokemonFRLG_StarterRngConsoleSteps.h" #include "PokemonFRLG_GiftRngConsoleSteps.h" namespace PokemonAutomation{ @@ -35,8 +36,10 @@ StarterRngOutcome loop_outcome(GiftRngOutcome outcome){ namespace{ // Gift RNG's settings for one try: no resets after the try's deadline, -// and its resets reported to the loop. -GiftRngSettings try_settings(Language language, const StarterRngAttemptCall& call){ +// its resets reported to the loop, and its reads to `on_read`. +GiftRngSettings try_settings( + Language language, const StarterRngAttemptCall& call, std::function on_read +){ return GiftRngSettings{ .language = language, .profile = 0, @@ -52,6 +55,7 @@ GiftRngSettings try_settings(Language language, const StarterRngAttemptCall& cal on_reset(StarterRngProgress{.resets = progress.resets, .calibrations = progress.calibrations}); } }, + .on_read = std::move(on_read), }; } @@ -92,7 +96,9 @@ StarterRngResult ConsoleGiftRngSteps::run_attempt(const RngAttempt& attempt, con .advances = attempt.target.advances, .seed_match = seed_match, }, - try_settings(m_settings.language, call), + try_settings( + m_settings.language, call, reset_line_logger(m_env.console, "Gift RNG", attempt, seed_match, call.max_resets) + ), GiftRngDisplays{ .target = m_target_display, .filters = m_filter_display, From 8f813a51ac4f8fcf0c12064b9e2c74b722740c9c Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 21:33:19 -0800 Subject: [PATCH 8/8] FRLG AutoStory: 00.12b starts with the moves the lead has after Brock Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 85ada6aaf6..49b63128ec 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -372,7 +372,7 @@ StoryCheckpoint magikarp_salesman(){ // shiny where 00.12 ended. A run started here with Gift RNG's handoff on // loads the save beside the salesman and runs Gift RNG first. StoryCheckpoint after_gift_rng(){ - StoryCheckpoint checkpoint = row("00.12b", "after Gift RNG: save with the Magikarp", at(ROUTE4_CENTER, 2, 3, WEST), {}, StartMenu::FULL, { + StoryCheckpoint checkpoint = row("00.12b", "after Gift RNG: save with the Magikarp", at(ROUTE4_CENTER, 2, 3, WEST), after_brock(), StartMenu::FULL, { game_step(LEAVE_GIFT_SUMMARY, at(ROUTE4_CENTER, 2, 3, WEST), "the Magikarp's summary Gift RNG left open, or HOME"), }); checkpoint.live_start = true;