diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.cpp new file mode 100644 index 0000000000..fa1605e5f7 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.cpp @@ -0,0 +1,47 @@ +/* AutoStory Retry + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Pokemon_AutoStoryRetry.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +void run_checkpoint_with_retries( + CheckpointRetryHooks& hooks, + const CheckpointRetryPolicy& policy, + const std::string& checkpoint_text, + const std::function& action +){ + for (size_t attempt = 0;; attempt++){ + try{ + if (attempt == 0){ + hooks.save_checkpoint(); + } + hooks.before_attempt(attempt); + hooks.log(checkpoint_text + " Num attempts: " + std::to_string(attempt)); + + hooks.wait_for_all_requests(); + action(attempt); + hooks.after_success(); + return; + }catch (OperationFailedException& e){ + if (attempt == policy.report_at_attempt){ + hooks.report_recoverable(std::to_string(attempt) + " failed attempts. " + checkpoint_text); + } + if (attempt > policy.max_attempts){ + hooks.fail(e); + } + hooks.wait_for_all_requests(); + hooks.log("Resetting from checkpoint."); + hooks.reset_for_retry(); + } + } +} + + +} +} diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.h new file mode 100644 index 0000000000..eb5e300035 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.h @@ -0,0 +1,68 @@ +/* AutoStory Retry + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Pokemon_AutoStoryRetry_H +#define PokemonAutomation_Pokemon_AutoStoryRetry_H + +#include +#include +#include "CommonFramework/Exceptions/OperationFailedException.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// The game steps around an AutoStory checkpoint attempt. Each game supplies its +// own; tests supply a scripted one. +class CheckpointRetryHooks{ +public: + virtual ~CheckpointRetryHooks() = default; + + virtual void log(const std::string& message) = 0; + + // Wait for the controller commands sent so far to finish. + virtual void wait_for_all_requests() = 0; + + // Save the game before the first attempt. A save that throws counts as a + // failed attempt, and later attempts do not save again. + virtual void save_checkpoint() = 0; + + // Before each attempt, after the save. + virtual void before_attempt(size_t attempt) = 0; + + // After the checkpoint's action returns. Throwing makes the attempt fail. + virtual void after_success() = 0; + + // Report a checkpoint that keeps failing, without stopping. + virtual void report_recoverable(const std::string& message) = 0; + + // Give up on the checkpoint. Must throw. + [[noreturn]] virtual void fail(const OperationFailedException& e) = 0; + + // Reload the last save for the next attempt. + virtual void reset_for_retry() = 0; +}; + +struct CheckpointRetryPolicy{ + // Give up once the attempt number is greater than this. + size_t max_attempts = 100; + // Report once, when this attempt fails. + size_t report_at_attempt = 10; +}; + +// Save, then run `action` until it returns, reloading the save after each attempt +// that throws OperationFailedException. Other exceptions are not caught. +void run_checkpoint_with_retries( + CheckpointRetryHooks& hooks, + const CheckpointRetryPolicy& policy, + const std::string& checkpoint_text, + const std::function& action +); + + +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp index de82dd3b2d..3f6cb1cd6d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp @@ -5,6 +5,7 @@ */ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Pokemon/AutoStory/Pokemon_AutoStoryRetry.h" #include "Pokemon/AutoStory/Pokemon_AutoStorySelect.h" #include "Inference/PokemonSV_WhiteButtonDetector.h" #include "Inference/Boxes/PokemonSV_BoxDetection.h" @@ -97,6 +98,141 @@ class Test_AutoStorySelectDatabase : public UnitTest{ } }; +// Checkpoint steps that record what runs, and fail the attempts they are told to. +class ScriptedCheckpoint : public Pokemon::CheckpointRetryHooks{ +public: + std::set failing_actions; + std::set failing_checks; + bool save_fails = false; + std::vector steps; + + virtual void log(const std::string&) override{} + virtual void wait_for_all_requests() override{} + virtual void save_checkpoint() override{ + steps.push_back("save"); + if (save_fails){ + throw OperationFailedException(ErrorReportMode::NO_ERROR_REPORT, "save failed"); + } + } + virtual void before_attempt(size_t attempt) override{ + steps.push_back("before" + std::to_string(attempt)); + } + void action(size_t attempt){ + steps.push_back("action" + std::to_string(attempt)); + if (failing_actions.contains(attempt)){ + throw OperationFailedException(ErrorReportMode::NO_ERROR_REPORT, "action failed"); + } + m_attempt = attempt; + } + virtual void after_success() override{ + steps.push_back("check"); + if (failing_checks.contains(m_attempt)){ + throw OperationFailedException(ErrorReportMode::NO_ERROR_REPORT, "not in the overworld"); + } + } + virtual void report_recoverable(const std::string& message) override{ + steps.push_back("report(" + message + ")"); + } + [[noreturn]] virtual void fail(const OperationFailedException&) override{ + steps.push_back("fail"); + throw std::runtime_error("gave up"); + } + virtual void reset_for_retry() override{ + steps.push_back("reset"); + } + +private: + size_t m_attempt = 0; +}; + +struct CheckpointRetryScenario{ + std::string name; + std::set failing_actions = {}; + std::set failing_checks = {}; + bool save_fails = false; + std::string expected_steps; + bool expected_give_up = false; +}; + +class Test_CheckpointRetry : public UnitTest{ +public: + Test_CheckpointRetry(CheckpointRetryScenario scenario) + : UnitTest("Pokemon::CheckpointRetry - " + scenario.name) + , m_scenario(std::move(scenario)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + ScriptedCheckpoint hooks; + hooks.failing_actions = m_scenario.failing_actions; + hooks.failing_checks = m_scenario.failing_checks; + hooks.save_fails = m_scenario.save_fails; + Pokemon::CheckpointRetryPolicy policy; + policy.max_attempts = 2; + policy.report_at_attempt = 1; + + bool gave_up = false; + try{ + Pokemon::run_checkpoint_with_retries(hooks, policy, "Checkpoint 1.", [&](size_t attempt){ + hooks.action(attempt); + }); + }catch (const std::runtime_error&){ + gave_up = true; + } + std::string steps; + for (const std::string& step : hooks.steps){ + steps += (steps.empty() ? "" : " ") + step; + } + if (gave_up != m_scenario.expected_give_up){ + return std::string("Expected ") + (m_scenario.expected_give_up ? "to give up" : "to finish") + ", steps: " + steps; + } + if (steps != m_scenario.expected_steps){ + return "Expected steps: " + m_scenario.expected_steps + "\nActual steps: " + steps; + } + return true; + } + +private: + const CheckpointRetryScenario m_scenario; +}; + +void add_tests_CheckpointRetry(UnitTestDatabase& database){ + const CheckpointRetryScenario scenarios[] = { + { + .name = "first attempt succeeds", + .expected_steps = "save before0 action0 check", + }, + { + // Only the first attempt saves; the retry starts from that save. + .name = "a failed attempt resets and retries without saving", + .failing_actions = {0}, + .expected_steps = "save before0 action0 reset before1 action1 check", + }, + { + // A save that fails is a failed attempt, and is not tried again. + .name = "a failed save is a failed attempt", + .save_fails = true, + .expected_steps = "save reset before1 action1 check", + }, + { + // SV checks for the overworld after the action; not finding it retries. + .name = "a failed check after the action retries", + .failing_checks = {0}, + .expected_steps = "save before0 action0 check reset before1 action1 check", + }, + { + // With max 2 and the report at 1: attempts 0..2 retry, attempt 3 gives up. + .name = "reports once and gives up past the maximum", + .failing_actions = {0, 1, 2, 3}, + .expected_steps = "save before0 action0 reset before1 action1 report(1 failed attempts. Checkpoint 1.) reset " + "before2 action2 reset before3 action3 fail", + .expected_give_up = true, + }, + }; + for (const CheckpointRetryScenario& scenario : scenarios){ + database.add(scenario); + } +} + void add_tests_AutoStory(UnitTestDatabase& database){ const AutoStoryIndexScenario scenarios[] = { { @@ -127,6 +263,7 @@ void add_tests_AutoStory(UnitTestDatabase& database){ database.add(scenario); } database.add(); + add_tests_CheckpointRetry(database); } } diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.cpp new file mode 100644 index 0000000000..6162a7f707 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.cpp @@ -0,0 +1,94 @@ +/* AutoStory Checkpoint Hooks + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "VideoGames/PokemonSV/Programs/PokemonSV_GameEntry.h" +#include "VideoGames/PokemonSV/Programs/PokemonSV_MenuNavigation.h" +#include "VideoGames/PokemonSV/Programs/PokemonSV_SaveGame.h" +#include "PokemonSV_AutoStoryTools.h" +#include "PokemonSV_AutoStoryCheckpointHooks.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonSV{ + + +SVCheckpointHooks::SVCheckpointHooks( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + EventNotificationOption& notif_error_recoverable, + Pokemon::AutoStoryStats& stats, + std::string fail_message, + SVCheckpointMode mode +) + : m_env(env), m_context(context) + , m_notif_status_update(notif_status_update) + , m_notif_error_recoverable(notif_error_recoverable) + , m_stats(stats) + , m_fail_message(std::move(fail_message)) + , m_mode(mode) +{} + +void SVCheckpointHooks::log(const std::string& message){ + m_env.console.log(message); +} +void SVCheckpointHooks::wait_for_all_requests(){ + m_context.wait_for_all_requests(); +} +void SVCheckpointHooks::save_checkpoint(){ + if (m_mode != SVCheckpointMode::TUTORIAL){ + checkpoint_save(m_env, m_context, m_notif_status_update, m_stats); + return; + } + save_game_tutorial(m_env.program_info(), m_env.console, m_context); + m_stats.m_checkpoint++; + m_env.update_stats(); + send_program_status_notification(m_env, m_notif_status_update, "Saved at checkpoint."); +} +void SVCheckpointHooks::before_attempt(size_t attempt){ + if (m_mode == SVCheckpointMode::MAIN_STORY_DAY_SKIP && (attempt > 0 || ENABLE_TEST)){ + day_skip_from_overworld(m_env.console, m_context); + save_game_from_overworld(m_env.program_info(), m_env.console, m_context); + } +} +void SVCheckpointHooks::after_success(){ + if (m_mode != SVCheckpointMode::TUTORIAL){ + enter_menu_from_overworld(m_env.program_info(), m_env.console, m_context, -1); + } +} +void SVCheckpointHooks::report_recoverable(const std::string& message){ + // Send an error report for debugging. + auto snapshot = m_env.console.video().snapshot().frame; + send_program_recoverable_error_notification_and_telemetry_report( + m_env, &m_env.logger(), m_env.program_info(), + m_notif_error_recoverable, + ErrorReportMode::SEND_ERROR_REPORT, + message, + "OperationFailedExceptionWithScreenshot", + *snapshot, + &m_env.console.history() + ); +} +void SVCheckpointHooks::fail(const OperationFailedException& e){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + m_fail_message + e.message(), + m_env.console + ); +} +void SVCheckpointHooks::reset_for_retry(){ + reset_game(m_env.program_info(), m_env.console, m_context); + m_stats.m_reset++; + m_env.update_stats(); +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.h b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.h new file mode 100644 index 0000000000..501b097fe6 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.h @@ -0,0 +1,66 @@ +/* AutoStory Checkpoint Hooks + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonSV_AutoStoryCheckpointHooks_H +#define PokemonAutomation_PokemonSV_AutoStoryCheckpointHooks_H + +#include +#include "CommonFramework/Notifications/EventNotificationOption.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/AutoStory/Pokemon_AutoStoryRetry.h" +#include "Pokemon/AutoStory/Pokemon_AutoStoryStats.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonSV{ + + +enum class SVCheckpointMode{ + // Saves without the overworld menu, never day skips, and does not check for + // the overworld after the action. + TUTORIAL, + MAIN_STORY, + // Also day skips and saves before each retry. + MAIN_STORY_DAY_SKIP, +}; + +// The SV game steps around an AutoStory checkpoint attempt. +class SVCheckpointHooks : public Pokemon::CheckpointRetryHooks{ +public: + SVCheckpointHooks( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + EventNotificationOption& notif_error_recoverable, + Pokemon::AutoStoryStats& stats, + std::string fail_message, + SVCheckpointMode mode + ); + + virtual void log(const std::string& message) override; + virtual void wait_for_all_requests() override; + virtual void save_checkpoint() override; + virtual void before_attempt(size_t attempt) override; + virtual void after_success() override; + virtual void report_recoverable(const std::string& message) override; + [[noreturn]] virtual void fail(const OperationFailedException& e) override; + virtual void reset_for_retry() override; + +private: + SingleSwitchProgramEnvironment& m_env; + ProControllerContext& m_context; + EventNotificationOption& m_notif_status_update; + EventNotificationOption& m_notif_error_recoverable; + Pokemon::AutoStoryStats& m_stats; + const std::string m_fail_message; + const SVCheckpointMode m_mode; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.cpp index c2d61152df..0065cccd82 100644 --- a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryTools.cpp @@ -29,6 +29,7 @@ #include "VideoGames/PokemonSV/Inference/PokemonSV_PokemonMovesReader.h" #include "VideoGames/PokemonSV/Inference/Map/PokemonSV_DestinationMarkerDetector.h" #include "VideoGames/PokemonSV/Inference/Map/PokemonSV_MapPokeCenterIconDetector.h" +#include "PokemonSV_AutoStoryCheckpointHooks.h" #include "PokemonSV_AutoStoryTools.h" //#include @@ -1433,59 +1434,16 @@ void checkpoint_reattempt_loop( std::function&& action, bool day_skip ){ - size_t max_attempts = 100; - for (size_t i = 0;;i++){ - try{ - if (i==0){ - checkpoint_save(env, context, notif_status_update, stats); - } - - if (day_skip && (i > 0 || ENABLE_TEST)){ - day_skip_from_overworld(env.console, context); - save_game_from_overworld(env.program_info(), env.console, context); - } - - env.console.log(checkpoint_text + " Num attempts: " + std::to_string(i)); - - context.wait_for_all_requests(); - action(i); - - enter_menu_from_overworld(env.program_info(), env.console, context, -1); - - break; - }catch (OperationFailedException& e){ - if (i == 10){ // send an error report for debugging if 10 failed attempts for a given checkpoint. - auto snapshot = env.console.video().snapshot().frame; - std::string message = "10 failed attempts. " + checkpoint_text; - send_program_recoverable_error_notification_and_telemetry_report( - env, &env.logger(), env.program_info(), - notif_error_recoverable, - ErrorReportMode::SEND_ERROR_REPORT, - message, - "OperationFailedExceptionWithScreenshot", - *snapshot, - &env.console.history() - ); - } - - if (i > max_attempts){ - OperationFailedExceptionWithScreenshot::fire( - ErrorReportMode::SEND_ERROR_REPORT, - "Autostory checkpoint failed " + std::to_string(max_attempts) + " times.\n" + - checkpoint_text + "\n" - "Make sure you selected the correct Start Point, and your character is in the exactly correct starting position." - "Also, make sure you have set the correct Language.\n" + e.message(), - env.console - ); - } - context.wait_for_all_requests(); - env.console.log("Resetting from checkpoint."); - reset_game(env.program_info(), env.console, context); - stats.m_reset++; - env.update_stats(); - } - } - + const Pokemon::CheckpointRetryPolicy policy; + SVCheckpointHooks hooks( + env, context, notif_status_update, notif_error_recoverable, stats, + "Autostory checkpoint failed " + std::to_string(policy.max_attempts) + " times.\n" + + checkpoint_text + "\n" + "Make sure you selected the correct Start Point, and your character is in the exactly correct starting position." + "Also, make sure you have set the correct Language.\n", + day_skip ? SVCheckpointMode::MAIN_STORY_DAY_SKIP : SVCheckpointMode::MAIN_STORY + ); + Pokemon::run_checkpoint_with_retries(hooks, policy, checkpoint_text, action); } void checkpoint_reattempt_loop_tutorial( @@ -1497,53 +1455,15 @@ void checkpoint_reattempt_loop_tutorial( const std::string& checkpoint_text, std::function&& action ){ - size_t max_attempts = 100; - for (size_t i = 0;;i++){ - try{ - if(i==0){ - save_game_tutorial(env.program_info(), env.console, context); - stats.m_checkpoint++; - env.update_stats(); - send_program_status_notification(env, notif_status_update, "Saved at checkpoint."); - } - - env.console.log(checkpoint_text + " Num attempts: " + std::to_string(i)); - - context.wait_for_all_requests(); - action(i); - - break; - }catch (OperationFailedException& e){ - if (i == 10){ // send an error report for debugging if 10 failed attempts for a given checkpoint. - auto snapshot = env.console.video().snapshot().frame; - std::string message = "10 failed attempts. " + checkpoint_text; - send_program_recoverable_error_notification_and_telemetry_report( - env, &env.logger(), env.program_info(), - notif_error_recoverable, - ErrorReportMode::SEND_ERROR_REPORT, - message, - "OperationFailedExceptionWithScreenshot", - *snapshot, - &env.console.history() - ); - } - - if (i > max_attempts){ - OperationFailedExceptionWithScreenshot::fire( - ErrorReportMode::SEND_ERROR_REPORT, - "Autostory checkpoint failed " + std::to_string(max_attempts) + " times.\n" - "Make sure you selected the correct Start Point, and your character is in the exactly correct starting position." - "Also, make sure you have set the correct Language.\n" + e.message(), - env.console - ); - } - context.wait_for_all_requests(); - env.console.log("Resetting from checkpoint."); - reset_game(env.program_info(), env.console, context); - stats.m_reset++; - env.update_stats(); - } - } + const Pokemon::CheckpointRetryPolicy policy; + SVCheckpointHooks hooks( + env, context, notif_status_update, notif_error_recoverable, stats, + "Autostory checkpoint failed " + std::to_string(policy.max_attempts) + " times.\n" + "Make sure you selected the correct Start Point, and your character is in the exactly correct starting position." + "Also, make sure you have set the correct Language.\n", + SVCheckpointMode::TUTORIAL + ); + Pokemon::run_checkpoint_with_retries(hooks, policy, checkpoint_text, action); } void move_player_forward( diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index accb20e1c9..ae1b163f10 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1435,6 +1435,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Options/Pokemon_NameSelectWidget.h Source/Pokemon/Options/Pokemon_StatsHuntFilter.cpp Source/Pokemon/Options/Pokemon_StatsHuntFilter.h + Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.cpp + Source/Pokemon/AutoStory/Pokemon_AutoStoryRetry.h Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h Source/Pokemon/AutoStory/Pokemon_AutoStoryStats.h Source/Pokemon/Pokemon_AdvRng.cpp @@ -2474,6 +2476,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonSV/PokemonSV_Settings.h Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp Source/VideoGames/PokemonSV/PokemonSV_Tests.h + Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.cpp + Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStoryCheckpointHooks.h Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStory.cpp Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStory.h Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStory_Segment_00.cpp