diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 80ef896adb..93171350f8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -5,9 +5,11 @@ */ #include +#include #include #include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/Notifications/ProgramNotifications.h" #include "CommonFramework/VideoPipeline/VideoOverlay.h" #include "Pokemon/AutoStory/Pokemon_AutoStoryStats.h" @@ -15,6 +17,8 @@ #include "Pokemon/Pokemon_Strings.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h" +#include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryTable.h" #include "PokemonFRLG_AutoStory.h" @@ -80,7 +84,7 @@ AutoStory::AutoStory() , STOP_FOR_STARTER_RNG( "Stop for Starter RNG:
Off by default. At the end of 00.04, save south of the table facing Squirtle's ball " "and stop, where the Starter RNG program starts. After Starter RNG, save right after the rival's battle without moving " - "and start AutoStory at 00.05.
" + "and start AutoStory at 00.05, listing the targets to try from the RNG plan.
" "With this off, 00.04 and 00.04b run as one checkpoint, as before.", LockMode::LOCK_WHILE_RUNNING, false @@ -95,6 +99,7 @@ AutoStory::AutoStory() PA_ADD_OPTION(PLAYER_NAME); PA_ADD_OPTION(STOP_FOR_SID_HELPER); PA_ADD_OPTION(STOP_FOR_STARTER_RNG); + PA_ADD_OPTION(RNG_PLAN); PA_ADD_OPTION(NOTIFICATIONS); } @@ -273,11 +278,105 @@ static void prepare_start( } // The run stopped for an RNG program, as its option asked: the user's -// turn, with what to do next. -static void stop_for_handoff(SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, const std::string& message){ +// turn, with what to do next, and the lines that go with it (the RNG +// plan's attempts). +static void stop_for_handoff( + SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, + const std::string& message, const std::vector& lines = {} +){ env.console.log(message, COLOR_BLUE); env.console.overlay().add_log(message); - send_program_finished_notification(env, notification, message); + std::string full = message; + for (const std::string& line : lines){ + env.console.log(line, COLOR_BLUE); + env.console.overlay().add_log(line); + full += "\n" + line; + } + send_program_finished_notification(env, notification, full); +} + +// SID Helper's set_sid_from_name_screen(), still file-static in +// PokemonFRLG_SidHelper.cpp, could run here instead of stopping. +static void stop_for_sid_helper( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + EventNotificationOption& notification, const std::string& player_name +){ + play_new_game_to_player_name(env.console, context, player_name); + stop_for_handoff(env, notification, + "Stopped for SID Helper: the player's name is typed and OK is not pressed. Run SID Helper now, " + "enter its Target Advances and # Candidate SIDs in the RNG plan, then start AutoStory at 00.01b." + ); +} + +// 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( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const StarterRngPlanOptions& options, Language language +){ + std::optional tid = options.tid_override(env.console); + if (tid){ + env.console.log(rng_log_tid(*tid, false)); + return *tid; + } + navigate_to_trainer_card(env.console, context); + tid = try_read_trainer_card_tid(env.console, context, language); + 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.", + env.console + ); + } + close_trainer_card(env.console, context); + env.console.log(rng_log_tid(*tid, true)); + return *tid; +} + +// The Starter RNG attempts, as lines for the stop. Run under the ball before +// 00.04 saves there, so a failure ends the run with 00.03's save kept. +static std::vector starter_rng_attempts( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const StarterRngPlanOptions& options, Language language +){ + const uint16_t tid = starter_rng_tid(env, context, options, language); + try{ + return rng_attempt_lines(options.plan(env.console, language, tid)); + }catch (UserSetupError& e){ + throw UserSetupError(env.console, e.message() + " Nothing was saved: fix the RNG plan and start AutoStory at 00.04 again."); + } +} + +// Has the last checkpoint of `segment` call `before` just before it saves. +static void before_last_save(Pokemon::Segment& segment, std::function before){ + Pokemon::CheckpointSpec& spec = segment.checkpoints.back(); + spec.build = [build = std::move(spec.build), before]() mutable{ + Pokemon::Checkpoint checkpoint = build(); + checkpoint.save = [save = std::move(checkpoint.save), before](Pokemon::RunState& state) mutable{ + before(); + return save(state); + }; + return checkpoint; + }; +} + +// Starter RNG runs its own reset and calibration loop in its program(), so +// the attempts are the user's to run until that loop can be called from here. +// TODO: LeafGreen and Bulbasaur/Charmander, with AutoStory's starter choice. +static void stop_for_starter_rng( + SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, + const std::string& sound, const std::vector& attempts +){ + stop_for_handoff(env, notification, + "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. " + "Run Starter RNG (Squirtle, FireRed, Sound " + sound + ") with attempt 1's Target Seed and Advances. " + "If it hits the target and it isn't shiny, load this save and run the next attempt. If it is shiny, " + "its SID is the one listed (or one of them): enter it as the RNG plan's SID. " + "Then save right after the rival's battle, without moving, and start AutoStory at 00.05.", + attempts + ); } void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ @@ -294,24 +393,28 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex checkpoints, first, last, StoryHandoffs{.sid_helper = STOP_FOR_SID_HELPER, .starter_rng = STOP_FOR_STARTER_RNG} ); if (plan.stop == StoryHandoff::SID_HELPER){ - play_new_game_to_player_name(env.console, context, player_name); - stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH, - "Stopped for SID Helper: the player's name is typed and OK is not pressed. " - "Run SID Helper now, then start AutoStory at 00.01b." - ); + stop_for_sid_helper(env, context, NOTIFICATION_PROGRAM_FINISH, player_name); return; } + if (plan.stop == StoryHandoff::STARTER_RNG){ + // The options' text, before anything is pressed. + RNG_PLAN.check(env.console); + } 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); - StoryExecutor executor(story); executor.set_checks(RUN.checks()); executor.set_language(RUN.language()); Pokemon::Segment segment = make_story_segment(plan.rows, executor); + std::vector rng_attempts; + if (plan.stop == StoryHandoff::STARTER_RNG){ + before_last_save(segment, [&]{ + rng_attempts = starter_rng_attempts(env, context, RNG_PLAN, RUN.language()); + }); + } Pokemon::SegmentHooks hooks = story_hooks(env, story); - Pokemon::RunState state; const Pokemon::SegmentReport report = run_segment( state, segment, Pokemon::SegmentRange{plan.rows.front().id, plan.rows.back().id}, RUN.policy(), hooks @@ -320,10 +423,7 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex env.update_stats(); throw_if_failed(report); if (plan.stop == StoryHandoff::STARTER_RNG){ - stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH, - "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. Run Starter RNG now. " - "Then save right after the rival's battle, without moving, and start AutoStory at 00.05." - ); + stop_for_starter_rng(env, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN.sound_name(), rng_attempts); return; } env.console.log("AutoStory: done through " + plan.rows.back().id + "."); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h index 8fd043efb9..274ad9e61e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h @@ -19,6 +19,7 @@ #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "Pokemon/Options/Pokemon_StoryRunOptions.h" #include "PokemonFRLG_AutoStoryActions.h" +#include "PokemonFRLG_AutoStoryRngOptions.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -50,6 +51,7 @@ class AutoStory : public SingleSwitchProgramInstance{ // The RNG programs the run stops for (StoryHandoff). BooleanCheckBoxOption STOP_FOR_SID_HELPER; BooleanCheckBoxOption STOP_FOR_STARTER_RNG; + StarterRngPlanOptions RNG_PLAN; EventNotificationsOption NOTIFICATIONS; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp new file mode 100644 index 0000000000..676ec0d9f9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp @@ -0,0 +1,183 @@ +/* AutoStory RNG Options + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h" +#include "PokemonFRLG_AutoStoryRngOptions.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +RngStopCalibrationOptions::RngStopCalibrationOptions() + : GroupOption("RNG Calibration (this console)", LockMode::LOCK_WHILE_RUNNING) + , SEED( + "Seed Calibration (ms):
As in Starter RNG's RNG Calibration. Each RNG stop starts from it, " + "and saves what it learns after every reset.", + LockMode::LOCK_WHILE_RUNNING, 0 + ) + , CSF( + "Continue Screen Frames Calibration:
As in Starter RNG's RNG Calibration, kept as the seed's is.", + LockMode::LOCK_WHILE_RUNNING, 0.0 + ) +{ + PA_ADD_OPTION(SEED); + PA_ADD_OPTION(CSF); +} + +RngCalibrations RngStopCalibrationOptions::calibration(const FloatingPointOption& ingame) const{ + return RngCalibrations{ + .seed_offset = double(int64_t(SEED)) / FRLG_FRAME_DURATION, + .csf_offset = CSF, + .ingame_offset = ingame, + }; +} + +void RngStopCalibrationOptions::save(const RngCalibrations& calibration, FloatingPointOption& ingame){ + // In ms, as RngCalibrationDisplay::set_calibrations() shows it. + SEED.set(int64_t(std::round(calibration.seed_offset * FRLG_FRAME_DURATION))); + CSF.set(calibration.csf_offset); + ingame.set(calibration.ingame_offset); +} + + +StarterRngPlanOptions::StarterRngPlanOptions() + : GroupOption("RNG Plan (Stop for Starter RNG)", LockMode::LOCK_WHILE_RUNNING) + , SOUND( + "Sound:
The in-game sound setting, as in Starter RNG. It picks the seeds.", + {{SoundSetting::Mono, "mono", "Mono"}, {SoundSetting::Stereo, "stereo", "Stereo"}}, + LockMode::LOCK_WHILE_RUNNING, SoundSetting::Mono + ) + , TID( + false, "TID:
Empty: read from the Trainer Card at the stop. Set it only if that read fails.", + LockMode::LOCK_WHILE_RUNNING, "", "" + ) + , SID( + false, + "SID:
The SID, once known. Empty: plan for the SIDs SID Helper lists, from the two options below.", + LockMode::LOCK_WHILE_RUNNING, "", "" + ) + , SID_HELPER_ADVANCES( + "SID Helper's Target Advances:
As set in SID Helper.", + LockMode::LOCK_WHILE_RUNNING, 3001, 2275 + ) + , SID_HELPER_CANDIDATES( + "SID Helper's # Candidate SIDs:
As set in SID Helper. The middle one counts as the likeliest.", + LockMode::LOCK_WHILE_RUNNING, 5, 1, 11 + ) + , SHINY_REQUIRED( + "Shiny required:
Only targets shiny for a SID candidate, tried until one comes out shiny. " + "Off: the quickest target that passes the natures and IVs.", + LockMode::LOCK_WHILE_RUNNING, true + ) + , NATURES(false, "Natures:
Separated by commas. Empty: any nature.", LockMode::LOCK_WHILE_RUNNING, "", "Modest, Timid") + , MIN_IVS( + false, "Minimum IVs:
HP/Atk/Def/SpA/SpD/Spe.", + LockMode::LOCK_WHILE_RUNNING, "0/0/0/0/0/0", "0/0/0/0/0/0" + ) + , MAX_ATTEMPTS("Max attempts:
The most targets to list.", LockMode::LOCK_WHILE_RUNNING, 5, 1, 20) +{ + PA_ADD_OPTION(SOUND); + PA_ADD_OPTION(TID); + PA_ADD_OPTION(SID); + PA_ADD_OPTION(SID_HELPER_ADVANCES); + PA_ADD_OPTION(SID_HELPER_CANDIDATES); + PA_ADD_OPTION(SHINY_REQUIRED); + PA_ADD_OPTION(NATURES); + PA_ADD_OPTION(MIN_IVS); + PA_ADD_OPTION(MAX_ATTEMPTS); + PA_ADD_OPTION(CALIBRATION); +} + +std::string StarterRngPlanOptions::sound_name() const{ + return SOUND == SoundSetting::Stereo ? "Stereo" : "Mono"; +} + +std::optional StarterRngPlanOptions::tid_override(Logger& logger) const{ + const std::string text = TID; + if (text.empty()){ + return std::nullopt; + } + const std::optional tid = parse_trainer_id(text); + if (!tid){ + throw UserSetupError(logger, "RNG plan: the TID must be a number from 0 to 65535, or empty."); + } + return tid; +} + +std::optional StarterRngPlanOptions::sid(Logger& logger) const{ + const std::string text = SID; + if (text.empty()){ + return std::nullopt; + } + const std::optional sid = parse_trainer_id(text); + if (!sid){ + throw UserSetupError(logger, "RNG plan: the SID must be a number from 0 to 65535, or empty."); + } + return sid; +} + +RngTargetFilters StarterRngPlanOptions::filters(Logger& logger) const{ + const std::optional> natures = parse_natures(NATURES); + if (!natures){ + throw UserSetupError(logger, "RNG plan: natures must be nature names separated by commas, e.g. \"Modest, Timid\"."); + } + const std::optional min_ivs = parse_ivs(MIN_IVS); + if (!min_ivs){ + throw UserSetupError(logger, "RNG plan: minimum IVs must be six numbers from 0 to 31, e.g. \"0/0/0/31/0/31\"."); + } + RngTargetFilters filters; + filters.natures = *natures; + filters.min_ivs = *min_ivs; + filters.shiny_required = SHINY_REQUIRED; + return filters; +} + +void StarterRngPlanOptions::check(Logger& logger) const{ + tid_override(logger); + sid(logger); + filters(logger); +} + +std::vector StarterRngPlanOptions::plan(Logger& logger, Language language, uint16_t tid) const{ + // SID Helper's candidates are logged even with the SID known, to place + // it among them (rng_log_confirmed()). + const std::vector helper = sid_helper_candidates(tid, SID_HELPER_ADVANCES, SID_HELPER_CANDIDATES); + logger.log(rng_log_sid_helper(tid, SID_HELPER_ADVANCES, helper)); + const std::optional known = sid(logger); + if (known){ + logger.log(rng_log_confirmed(tid, *known, helper)); + } + + const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ + // TODO: LeafGreen, once AutoStory plays it. + .firered = true, + .language = language, + .sound = SOUND, + .tid = tid, + .candidates = known ? std::vector{{*known, 1}} : helper, + .filters = filters(logger), + .max_attempts = MAX_ATTEMPTS, + }); + 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) + "." + ); + } + for (size_t c = 0; c < attempts.size(); c++){ + logger.log(rng_log_attempt(c, attempts[c])); + } + return attempts; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h new file mode 100644 index 0000000000..3e08f2b94f --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h @@ -0,0 +1,94 @@ +/* AutoStory RNG Options + * + * From: https://github.com/PokemonAutomation/ + * + * The RNG plan options: what AutoStory's Starter RNG stop searches targets + * with (RngManipulation/PokemonFRLG_RngPlan.h). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryRngOptions_H +#define PokemonAutomation_PokemonFRLG_AutoStoryRngOptions_H + +#include +#include +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/FloatingPointOption.h" +#include "Common/Cpp/Options/GroupOption.h" +#include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "Common/Cpp/Options/StringOption.h" +#include "CommonFramework/Language.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// This console's RNG calibration, as Starter RNG's RNG Calibration shows +// it, for AutoStory's RNG stops to start from: the seed and Continue +// Screen Frames offsets, the same for every stop, and an in-game advances +// offset for each stop. +class RngStopCalibrationOptions : public GroupOption{ +public: + RngStopCalibrationOptions(); + + // The calibration in frames for the stop whose in-game advances offset + // `ingame` keeps. + RngCalibrations calibration(const FloatingPointOption& ingame) const; + // Keeps `calibration` as calibration() returns it. Called after each + // reset, so a stopped run keeps what it learned. + void save(const RngCalibrations& calibration, FloatingPointOption& ingame); + +public: + SimpleIntegerOption SEED; + FloatingPointOption CSF; +}; + + +class StarterRngPlanOptions : public GroupOption{ +public: + StarterRngPlanOptions(); + + // Throws UserSetupError for a TID, SID, natures or IVs it can't read, + // before the run presses anything. + void check(Logger& logger) const; + // The TID typed in, or nothing to read it from the Trainer Card. + std::optional tid_override(Logger& logger) const; + + // The attempts these options ask for, for TID "tid" in a FireRed game + // in "language", logged as "RngPlan:" lines. Throws UserSetupError + // when no target passes. + std::vector plan(Logger& logger, Language language, uint16_t tid) const; + // The sound setting as Starter RNG's dropdown shows it. + std::string sound_name() const; + +public: + EnumDropdownOption SOUND; + StringOption TID; + StringOption SID; + SimpleIntegerOption SID_HELPER_ADVANCES; + SimpleIntegerOption SID_HELPER_CANDIDATES; + BooleanCheckBoxOption SHINY_REQUIRED; + StringOption NATURES; + StringOption MIN_IVS; + SimpleIntegerOption MAX_ATTEMPTS; + RngStopCalibrationOptions CALIBRATION; + +private: + RngTargetFilters filters(Logger& logger) const; + std::optional sid(Logger& logger) const; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 27aa5c8be4..d7a99b9e74 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1859,6 +1859,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryParty.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryParty.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryParty_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySummary.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySummary.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp