diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 4ccaa6a0d7..b6428c1f00 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -43,6 +43,7 @@ #include "Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryTable.h" +#include "Programs/RngManipulation/PokemonFRLG_GiftRngAttempts_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h" @@ -670,6 +671,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); +} + } @@ -722,13 +735,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_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 d86fdf9ec4..b033fca258 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -19,6 +19,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" @@ -55,7 +56,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; @@ -70,6 +71,15 @@ 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.
" + "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() : RUN(checkpoint_database()) , NEW_GAME_FROM_TITLE( @@ -103,6 +113,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}) @@ -113,6 +124,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); } @@ -270,16 +282,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; } @@ -320,12 +333,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){ @@ -337,8 +350,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 ); } @@ -347,17 +360,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."); } } @@ -451,9 +468,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, @@ -462,28 +479,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 + planned.attempts, options.plan_candidates(env.console, planned.tid), options.SHINY_REQUIRED, RNG_LOOP_LIMITS.max_retries ); - // The settings file gets the calibration saved after each reset when - // the program's panel is closed or the app quits. - RngStopCalibrationOptions& calibration = options.CALIBRATION; - ConsoleStarterRngSteps console_steps(env, context, StarterRngConsoleSettings{ - .language = language, .sound = options.SOUND, .calibration = calibration.calibration(calibration.STARTER_INGAME), - }, notifications); - CalibrationSavingSteps steps(console_steps, calibration, calibration.STARTER_INGAME); 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){ @@ -497,7 +504,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; @@ -507,34 +522,84 @@ 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 +){ + // The settings file gets the calibration saved after each reset when + // the program's panel is closed or the app quits. + RngStopCalibrationOptions& calibration = options.CALIBRATION; + ConsoleStarterRngSteps console_steps(env, context, StarterRngConsoleSettings{ + .language = language, .sound = options.SOUND, .calibration = calibration.calibration(calibration.STARTER_INGAME), + }, notifications); + CalibrationSavingSteps steps(console_steps, calibration, calibration.STARTER_INGAME); + 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); + // 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)); +} + +// 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); @@ -546,6 +611,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, }; } @@ -577,32 +643,73 @@ 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()); - StarterRngPlanned planned; - std::function before; - if (plan.stop == StoryHandoff::STARTER_RNG){ - before = [&]{ planned = plan_starter_rng_attempts(env, context, RNG_PLAN, RUN.language()); }; - } - 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; + + 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); + } +} + +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); + } +} + +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; } - send_story_finished(env, NOTIFICATION_PROGRAM_FINISH, done_through, sid_before, RNG_PLAN.SID); + return run_story_plan(env, context, story, executor, std::move(*next)); } -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)); @@ -614,7 +721,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..a8dd3b56dd 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h @@ -49,13 +49,33 @@ 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 + ); + // 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. + 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 +89,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 c3c5d3461c..6512c02174 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp @@ -891,17 +891,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 3b86177eb9..fedb2c7713 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -452,6 +452,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); } @@ -477,6 +481,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){ @@ -492,6 +503,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 ed59ef4e97..627e5a5914 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,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. + // 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( @@ -225,6 +236,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 f6c1f5d3c9..53289eff21 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.cpp @@ -64,6 +64,33 @@ 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: 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."; + 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."); +} + + CalibrationSavingSteps::CalibrationSavingSteps( StarterRngLoopSteps& steps, RngStopCalibrationOptions& calibration, FloatingPointOption& ingame ) @@ -88,8 +115,25 @@ StarterRngResult CalibrationSavingSteps::run_attempt(const RngAttempt& attempt, } -std::vector rows_after_starter_rng( - const std::vector& checkpoints, size_t ball_row, size_t last +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."); @@ -104,12 +148,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 a6c0f248af..5e66e78e39 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. * */ @@ -26,7 +27,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; @@ -34,6 +35,8 @@ 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); // `steps`, with the calibration of each reset also kept in the RNG plan's @@ -56,10 +59,21 @@ class CalibrationSavingSteps : public StarterRngLoopSteps{ // 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 f042a5349c..480cdd0726 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. * */ @@ -10,10 +11,12 @@ #include #include #include +#include #include #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,41 +82,153 @@ 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(std::string_view 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 " + std::string(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) + ); + } + } + 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: 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)}, + .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 + "\"."; } - 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; +} + +// 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 (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( + "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( - "Expected rows {} starting at above_rival_trigger; got {} starting at {}", item.ids, ids, first_step + "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; } @@ -190,14 +305,40 @@ 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; +} + } 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); 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 57943c2dcd..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{ @@ -162,6 +167,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); @@ -183,7 +206,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 dc0a8e1343..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; }; @@ -68,6 +70,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(). @@ -90,6 +95,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 001a7b7998..1e4e1a960a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -359,12 +359,26 @@ StoryCheckpoint route3(){ // (40, 9) (run_route(); console B, 2026-11-02, she stopped the player // there and spoke after the leg's short watch). StoryCheckpoint magikarp_salesman(){ - return row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), after_brock(), StartMenu::FULL, { + StoryCheckpoint checkpoint = row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), after_brock(), 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. 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), 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; + return checkpoint; } } @@ -373,7 +387,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 de56612556..f524fb6c16 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -215,8 +215,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++){ @@ -749,6 +749,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 "?"; } @@ -833,6 +835,45 @@ 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 = "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", + .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){ @@ -872,16 +913,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; @@ -932,7 +974,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_GiftRng.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRng.cpp index 3341d8cef2..9dbc7d6b26 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,42 @@ 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, + .on_read = 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..dcf9f1f9d3 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.cpp @@ -0,0 +1,332 @@ +/* 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; + 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, + }); + } +} + +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++; + m_search_hits.clear(); + 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){ + 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; +} + + +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); + } + ); + + 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 new file mode 100644 index 0000000000..8da780ae6e --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngAttempts.h @@ -0,0 +1,191 @@ +/* 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" +#include "PokemonFRLG_StarterRngAttempts.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; + // Called after each reset's reads, if set, as on_reset is. + std::function on_read; +}; + +// Where the attempts show the target, the filters read so far and the +// calibration. The calibration display also holds the starting +// calibration. +struct 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); + void report_read(bool shiny); + + 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; + // The last search's hits, for on_read. + std::vector m_search_hits; +}; + + + +} +} +} +#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/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp new file mode 100644 index 0000000000..cd79be3b96 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.cpp @@ -0,0 +1,127 @@ +/* Gift RNG Console Steps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "PokemonFRLG_StarterRngConsoleSteps.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, +// 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, + .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}); + } + }, + .on_read = std::move(on_read), + }; +} + +} + + +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)) +{ + m_calibration_display.set_calibrations(settings.calibration); +} + +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, reset_line_logger(m_env.console, "Gift RNG", attempt, seed_match, call.max_resets) + ), + 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..9ceb3cae48 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_GiftRngConsoleSteps.h @@ -0,0 +1,73 @@ +/* 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 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). 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: + 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 e1dee1738e..19d9f6b5f0 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1904,6 +1904,12 @@ 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_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