diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp
index df4bdb0714..928a5c4bd0 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp
@@ -68,6 +68,15 @@ AutoStory::AutoStory()
LockMode::LOCK_WHILE_RUNNING,
"Kosmos", ""
)
+ , STOP_FOR_SID_HELPER(
+ "Stop for SID Helper:
Off by default. At 00.01, stop on the player's keyboard with the name typed and OK not pressed, "
+ "where the SID Helper program starts. After SID Helper, start AutoStory at 00.01b, with the game as SID Helper left it "
+ "(the Trainer Card, or HOME): it closes the card, sets the options and saves in the bedroom.
"
+ "SID Helper needs the text speed and button settings already saved. On a cartridge with that save, choose NEW GAME "
+ "by hand and start AutoStory on the first message.",
+ LockMode::LOCK_WHILE_RUNNING,
+ false
+ )
, NOTIFICATIONS({
&NOTIFICATION_PROGRAM_FINISH,
&NOTIFICATION_ERROR_FATAL,
@@ -76,6 +85,7 @@ AutoStory::AutoStory()
PA_ADD_OPTION(RUN);
PA_ADD_OPTION(NEW_GAME_FROM_TITLE);
PA_ADD_OPTION(PLAYER_NAME);
+ PA_ADD_OPTION(STOP_FOR_SID_HELPER);
PA_ADD_OPTION(NOTIFICATIONS);
}
@@ -156,6 +166,11 @@ void load_verify_save(
"Verify mode reloads the checkpoint's save between runs: " + checkpoint.id + " starts a new game and has none."
);
}
+ if (checkpoint.live_start){
+ throw UserSetupError(env.console,
+ "Verify mode reloads the checkpoint's save between runs: " + checkpoint.id + " starts from the game as it is, with no save."
+ );
+ }
env.console.log("Verify mode: loading the save for " + checkpoint.id + ".");
soft_reset(env.console, context);
story.has_save = true;
@@ -212,6 +227,50 @@ static void check_setup(
}
}
+static void throw_if_failed(const Pokemon::SegmentReport& report){
+ if (report.completed){
+ return;
+ }
+ throw OperationFailedException(
+ ErrorReportMode::NO_ERROR_REPORT,
+ std::format(
+ "AutoStory stopped at {} {}: {}",
+ report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure)
+ )
+ );
+}
+
+// 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.
+static void prepare_start(
+ SingleSwitchProgramEnvironment& env, ProControllerContext& context,
+ const std::vector& checkpoints, size_t first, StoryEnv& story
+){
+ const StoryCheckpoint& checkpoint = checkpoints[first];
+ if (!checkpoint.start){
+ return;
+ }
+ if (checkpoint.live_start){
+ env.console.log(checkpoint.id + " starts from the game as it is: no save to load.");
+ return;
+ }
+ env.console.log("Loading the save for " + checkpoint.id + ".");
+ soft_reset(env.console, context);
+ story.has_save = true;
+ check_start_save(env, story, checkpoints[first]);
+ read_lead_moves(story, checkpoints[first].menu);
+ read_bag_supplies(story, checkpoints[first].menu);
+}
+
+// The run stopped for an RNG program, as its option asked: the user's
+// turn, with what to do next.
+static void stop_for_handoff(SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, const std::string& message){
+ env.console.log(message, COLOR_BLUE);
+ env.console.overlay().add_log(message);
+ send_program_finished_notification(env, notification, message);
+}
+
void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){
if (const std::optional verify = RUN.VERIFY.policy()){
verify_story_checkpoint(env, context, RUN, *verify, NOTIFICATION_PROGRAM_FINISH);
@@ -222,40 +281,34 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex
const size_t last = RUN.end_index();
const std::string player_name = PLAYER_NAME;
check_setup(env.console, checkpoints, first, last, NEW_GAME_FROM_TITLE, player_name);
+ const StoryRunPlan plan = plan_story_run(checkpoints, first, last, StoryHandoffs{.sid_helper = STOP_FOR_SID_HELPER});
+ if (plan.stop == StoryHandoff::SID_HELPER){
+ play_new_game_to_player_name(env.console, context, player_name);
+ stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH,
+ "Stopped for SID Helper: the player's name is typed and OK is not pressed. "
+ "Run SID Helper now, then start AutoStory at 00.01b."
+ );
+ return;
+ }
StoryEnv story{env.console, context, checkpoints[first].supplies, checkpoints[first].supplies, false, RUN.checks()};
story.player_name = player_name;
- if (!(first == 0 && NEW_GAME_FROM_TITLE)){
- env.console.log("Loading the save for " + checkpoints[first].id + ".");
- soft_reset(env.console, context);
- story.has_save = true;
- check_start_save(env, story, checkpoints[first]);
- read_lead_moves(story, checkpoints[first].menu);
- read_bag_supplies(story, checkpoints[first].menu);
- }
+ prepare_start(env, context, checkpoints, first, story);
StoryExecutor executor(story);
executor.set_checks(RUN.checks());
executor.set_language(RUN.language());
- Pokemon::Segment segment = make_story_segment(checkpoints, executor);
+ Pokemon::Segment segment = make_story_segment(plan.rows, executor);
Pokemon::SegmentHooks hooks = story_hooks(env, story);
Pokemon::RunState state;
const Pokemon::SegmentReport report = run_segment(
- state, segment, Pokemon::SegmentRange{checkpoints[first].id, checkpoints[last].id}, RUN.policy(), hooks
+ state, segment, Pokemon::SegmentRange{plan.rows.front().id, plan.rows.back().id}, RUN.policy(), hooks
);
Pokemon::record_story_stats(env.current_stats(), report);
env.update_stats();
- if (!report.completed){
- throw OperationFailedException(
- ErrorReportMode::NO_ERROR_REPORT,
- std::format(
- "AutoStory stopped at {} {}: {}",
- report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure)
- )
- );
- }
- env.console.log("AutoStory: done through " + checkpoints[last].id + ".");
+ throw_if_failed(report);
+ env.console.log("AutoStory: done through " + plan.rows.back().id + ".");
send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH);
}
diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h
index e349fc9d9d..3915332642 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h
@@ -47,6 +47,8 @@ class AutoStory : public SingleSwitchProgramInstance{
Pokemon::StoryRunOptions RUN;
BooleanCheckBoxOption NEW_GAME_FROM_TITLE;
StringOption PLAYER_NAME;
+ // The RNG programs the run stops for (StoryHandoff).
+ BooleanCheckBoxOption STOP_FOR_SID_HELPER;
EventNotificationsOption NOTIFICATIONS;
};
diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp
index debf1311a6..c3c5d3461c 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp
@@ -16,6 +16,8 @@
#include "CommonTools/Async/InferenceRoutines.h"
#include "CommonTools/Images/SolidColorTest.h"
#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h"
+#include "NintendoSwitch/Inference/NintendoSwitch_HomeMenuDetector.h"
+#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h"
#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h"
#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h"
#include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h"
@@ -179,10 +181,18 @@ void open_start_menu_row(ConsoleHandle& console, ProControllerContext& context,
// GREEN.
class NewGameIntro{
public:
- NewGameIntro(ConsoleHandle& console, ProControllerContext& context, std::string player_name)
+ // Where the intro stops: the bedroom, or the player's keyboard with the
+ // name typed and OK not pressed (for SID Helper).
+ enum class End{
+ BEDROOM,
+ PLAYER_NAME_TYPED,
+ };
+
+ NewGameIntro(ConsoleHandle& console, ProControllerContext& context, std::string player_name, End end)
: m_console(console)
, m_context(context)
, m_player_name(std::move(player_name))
+ , m_end(end)
{}
void run(){
@@ -192,7 +202,7 @@ class NewGameIntro{
m_last_press = m_last_dialog;
while (current_time() < deadline){
if (step()){
- m_console.log("New game: the bedroom.");
+ m_console.log(m_end == End::BEDROOM ? "New game: the bedroom." : "New game: the player's name is typed, OK not pressed.");
return;
}
}
@@ -231,6 +241,12 @@ class NewGameIntro{
pbf_wait(m_context, POLL);
return false;
}
+ if (m_end == End::PLAYER_NAME_TYPED && m_keyboards_seen == 0 && NamingScreenDetector(COLOR_RED).detect(screen)){
+ m_keyboards_seen++;
+ clear_stray_keys();
+ type_player_name(false);
+ return true;
+ }
if (names(screen)){
m_last_dialog = current_time();
m_last_press = m_last_dialog;
@@ -320,10 +336,15 @@ class NewGameIntro{
return false;
}
- void enter_name(bool player){
+ // B deletes whatever a stray A typed; the cursor stays on its key.
+ void clear_stray_keys(){
for (int c = 0; c < 8; c++){
press(m_context, BUTTON_B, 340ms);
}
+ }
+
+ void enter_name(bool player){
+ clear_stray_keys();
if (!player || m_player_name.empty()){
m_console.log("New game: the keyboard, confirming an empty name.");
press(m_context, BUTTON_PLUS, 500ms);
@@ -331,19 +352,30 @@ class NewGameIntro{
m_context.wait_for_all_requests();
return;
}
- if (!type_name(m_console, m_context, m_player_name, NamingScreenKind::PLAYER)){
+ type_player_name(true);
+ // Past the keyboard's fade, so it isn't taken for the rival's.
+ pbf_wait(m_context, 2000ms);
+ m_context.wait_for_all_requests();
+ }
+
+ void type_player_name(bool confirm){
+ if (m_player_name.empty()){
+ return;
+ }
+ const bool typed = confirm
+ ? type_name(m_console, m_context, m_player_name, NamingScreenKind::PLAYER)
+ : type_name_without_confirming(m_console, m_context, m_player_name, NamingScreenKind::PLAYER);
+ if (!typed){
OperationFailedExceptionWithScreenshot::fire(
ErrorReportMode::SEND_ERROR_REPORT, "play_new_game_intro(): the keyboard closed before the name was typed.", m_console
);
}
- // Past the keyboard's fade, so it isn't taken for the rival's.
- pbf_wait(m_context, 2000ms);
- m_context.wait_for_all_requests();
}
ConsoleHandle& m_console;
ProControllerContext& m_context;
const std::string m_player_name;
+ const End m_end;
WallClock m_last_dialog;
WallClock m_last_press;
bool m_dialog_seen = false;
@@ -851,7 +883,23 @@ void buy_items(
void play_new_game_intro(ConsoleHandle& console, ProControllerContext& context, const std::string& player_name){
console.log("New game: from the title screen.");
- NewGameIntro(console, context, player_name).run();
+ NewGameIntro(console, context, player_name, NewGameIntro::End::BEDROOM).run();
+}
+
+void play_new_game_to_player_name(ConsoleHandle& console, ProControllerContext& context, const std::string& player_name){
+ console.log("New game: from the title screen to the player's keyboard, for SID Helper.");
+ NewGameIntro(console, context, player_name, NewGameIntro::End::PLAYER_NAME_TYPED).run();
+}
+
+void close_trainer_card(ConsoleHandle& console, ProControllerContext& context){
+ 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.");
+ resume_game_from_home(console, context);
+ }
+ // B closes the card, then the START menu.
+ mash_until_overworld(console, context, 20000ms);
}
diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h
index 2850e1576c..7065c8e1f7 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h
@@ -219,6 +219,15 @@ void buy_items(
// (naming_text_error()).
void play_new_game_intro(ConsoleHandle& console, ProControllerContext& context, const std::string& player_name);
+// play_new_game_intro() up to the player's keyboard: `player_name` typed,
+// OK not pressed, which is where SID Helper starts. Empty types nothing.
+void play_new_game_to_player_name(ConsoleHandle& console, ProControllerContext& context, const std::string& player_name);
+
+// From the Trainer Card SID Helper leaves open, or from HOME (its "go
+// home when done"): back to the game, the card and the START menu closed.
+// Returns on the overworld.
+void close_trainer_card(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 f575125826..60796919a0 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp
@@ -443,6 +443,34 @@ Outcome StoryExecutor::ready(RunState& state, const StoryStep& step){
return get_ready(m_env, state, step);
}
+Outcome StoryExecutor::game(RunState&, const StoryStep& step){
+ if (step.variant == CLOSE_TRAINER_CARD){
+ close_trainer_card(m_env.console, m_env.context);
+ return Pokemon::outcome_ok();
+ }
+ return Pokemon::unsupported_step(step);
+}
+
+
+StoryRunPlan plan_story_run(
+ const std::vector& checkpoints, size_t first, size_t last, const StoryHandoffs& handoffs
+){
+ StoryRunPlan plan;
+ for (size_t c = first; c <= last && c < checkpoints.size(); c++){
+ const StoryCheckpoint& row = checkpoints[c];
+ if (row.live_start && c != first){
+ continue;
+ }
+ if (row.handoff == StoryHandoff::SID_HELPER && handoffs.sid_helper){
+ plan.rows.clear();
+ plan.stop = StoryHandoff::SID_HELPER;
+ return plan;
+ }
+ plan.rows.emplace_back(row);
+ }
+ return plan;
+}
+
std::string check_save_place(StoryEnv& env, const std::string& start_map){
if (save_card_place(start_map).empty()){
diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h
index e8e5817f76..053cadbdeb 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h
@@ -56,18 +56,57 @@ struct StoryPose{
// has the Running Shoes (00.10 on).
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";
+
// How `step`, a ROUTE or LEGS, moves the player: RUN_VARIANT runs.
MoveSpeed move_speed(const StoryStep& step);
+// An upstream RNG program the user may run part-way through the story.
+// Each is off unless its option is on.
+enum class StoryHandoff{
+ NONE,
+ // The checkpoint's new game stops on the player's keyboard, the name
+ // typed and OK not pressed, which is where SID Helper starts.
+ SID_HELPER,
+};
+
// A shared checkpoint with what FRLG adds to it.
struct StoryCheckpoint : public Pokemon::StoryCheckpoint{
// The healing items at its start, for a run that loads its save.
Supplies supplies;
// The START menu at its end, when it saves.
StartMenu menu = StartMenu::FULL;
+ // Starts from the game as another program left it, loading no save:
+ // the way back into the story after an RNG program. A run that starts
+ // before it passes over it.
+ bool live_start = false;
+ // The RNG program the user may run at this checkpoint.
+ StoryHandoff handoff = StoryHandoff::NONE;
+};
+
+// The handoffs a run stops for (AutoStory's options).
+struct StoryHandoffs{
+ bool sid_helper = false;
};
+// What a run from table row `first` to `last` does.
+struct StoryRunPlan{
+ // The checkpoints to run, in order.
+ std::vector rows;
+ // 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.
+ 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, and a run that reaches a handoff that is on stops for it.
+StoryRunPlan plan_story_run(
+ const std::vector& checkpoints, size_t first, size_t last, const StoryHandoffs& handoffs
+);
+
// The maps the story plans on: every map to Brock, with the objects the
// game hides after the Hall of Fame removed, as the lab's frlg_route does.
@@ -175,6 +214,8 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{
// The Viridian Mart, before the Boulder Badge: Potions, Antidotes and Parlyz Heals.
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()).
+ virtual Pokemon::Outcome game(Pokemon::RunState& state, const StoryStep& step) override;
private:
StoryEnv& m_env;
diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp
index 6410a0710c..53b2da73ad 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp
@@ -166,6 +166,7 @@ class FakeGame : public Pokemon::StoryStepExecutor{
virtual Outcome mash(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); }
virtual Outcome battle(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); }
virtual Outcome heal(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); }
+ virtual Outcome game(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); }
// The Viridian Mart's list as the game shows it. After a purchase the
// cursor stays on the row bought (console B, 2026-11-01), so each
// order moves it from there, as buy_items() does since ad5d40885.
@@ -220,9 +221,11 @@ class FakeGame : public Pokemon::StoryStepExecutor{
};
-// One simulated run, segment built as the program builds it: each
-// checkpoint saves at its end (hooks.save stands for save_game()).
+// One simulated run, segment built as the program builds it: the rows a
+// run takes with every handoff off, each saving at its end (hooks.save
+// stands for save_game()).
struct Simulation{
+ std::vector rows = plan_story_run(story_checkpoints(), 0, story_checkpoints().size() - 1, StoryHandoffs{}).rows;
FakeGame game;
std::vector lines;
// Where each save stood: the end of its checkpoint.
@@ -234,7 +237,7 @@ struct Simulation{
game.set_language(Language::Japanese);
Pokemon::Segment segment;
segment.id = "00";
- for (const StoryCheckpoint& data : story_checkpoints()){
+ for (const StoryCheckpoint& data : rows){
segment.checkpoints.push_back(Pokemon::CheckpointSpec{
.id = data.id, .title = data.title, .saves = true,
.build = [this, &data]{ return Pokemon::build_story_checkpoint(data, game); },
@@ -303,15 +306,15 @@ UnitTestResult test_whole_story(){
return "Expected the whole story through:" + joined(sim.lines);
}
std::vector ids;
- for (const StoryCheckpoint& data : story_checkpoints()){
+ for (const StoryCheckpoint& data : sim.rows){
ids.push_back(data.id);
}
if (sim.begun() != ids || sim.saves.size() != ids.size()){
return "Expected each checkpoint once, in order, each saving at its end:" + joined(sim.game.trace);
}
// Each checkpoint starts where the save before it stands.
- for (size_t c = 1; c < story_checkpoints().size(); c++){
- const StoryCheckpoint& data = story_checkpoints()[c];
+ for (size_t c = 1; c < sim.rows.size(); c++){
+ const StoryCheckpoint& data = sim.rows[c];
const std::optional start = story_pose(*data.start);
const std::optional saved = story_pose(sim.saves[c - 1].second);
const bool same = start && saved && start->map == saved->map && start->tile == saved->tile
diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp
index 1bc03b8b6c..9905a6cf5d 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp
@@ -146,12 +146,37 @@ StoryStep shop(std::string name, std::vector items, std::str
}
+StoryStep game_step(std::string variant, Pokemon::Pose pose_after, std::string why){
+ StoryStep step = make(StoryStepKind::GAME, variant, std::move(why));
+ step.variant = std::move(variant);
+ step.pose_after = std::move(pose_after);
+ return step;
+}
+
+StoryStep options_step(){
+ return make(StoryStepKind::OPTIONS, "options", "fast text; no switch prompt between a trainer's Pokemon");
+}
+
+
StoryCheckpoint new_game(){
- return row("00.01", "new game to the bedroom", std::nullopt, {}, StartMenu::BEFORE_POKEMON, {
+ StoryCheckpoint checkpoint = row("00.01", "new game to the bedroom", std::nullopt, {}, StartMenu::BEFORE_POKEMON, {
// new_game.c: the bedroom at (6, 6), facing the TV.
new_game_intro(at(BEDROOM, 6, 6, NORTH), "Oak's speech, BOY, the player's name (default if none is set), rival GREEN"),
- make(StoryStepKind::OPTIONS, "options", "fast text; no switch prompt between a trainer's Pokemon"),
+ options_step(),
+ });
+ checkpoint.handoff = StoryHandoff::SID_HELPER;
+ return checkpoint;
+}
+
+// SID Helper confirms the name, finishes the intro and reads the Trainer
+// Card from the START menu, without moving the player or saving.
+StoryCheckpoint after_sid_helper(){
+ StoryCheckpoint checkpoint = row("00.01b", "after SID Helper: close the Trainer Card, save in the bedroom", at(BEDROOM, 6, 6, NORTH), {}, StartMenu::BEFORE_POKEMON, {
+ game_step(CLOSE_TRAINER_CARD, at(BEDROOM, 6, 6, NORTH), "the Trainer Card SID Helper left open, or HOME"),
+ options_step(),
});
+ checkpoint.live_start = true;
+ return checkpoint;
}
StoryCheckpoint leave_house(){
@@ -284,7 +309,7 @@ StoryCheckpoint running_shoes(){
const std::vector& story_checkpoints(){
static const std::vector checkpoints{
- new_game(), leave_house(), oaks_trigger(), starter(), parcel(), pokedex(), shopping(), forest(), brock(),
+ new_game(), after_sid_helper(), leave_house(), oaks_trigger(), starter(), parcel(), pokedex(), shopping(), forest(), brock(),
running_shoes(),
};
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 d8a3f8b15d..37531a70c7 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h
@@ -21,7 +21,8 @@ namespace NintendoSwitch{
namespace PokemonFRLG{
-// Checkpoints 00.01 to 00.10, in order.
+// Checkpoints 00.01 to 00.10, in order, with 00.01b, the way back in after
+// SID Helper, after 00.01.
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 fa4b926b71..8b0ab92acb 100644
--- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp
+++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
#include
#include
#include "Common/Cpp/Exceptions.h"
@@ -211,8 +212,8 @@ UnitTestResult check_checkpoint(const StoryCheckpoint& checkpoint, StoryPose& po
UnitTestResult test_routes_and_chain(){
const std::vector& checkpoints = story_checkpoints();
- if (checkpoints.size() != 10 || checkpoints[0].start){
- return "Expected 10 checkpoints, the first from a new game.";
+ if (checkpoints.size() != 11 || checkpoints[0].start){
+ return "Expected 11 checkpoints, the first from a new game.";
}
StoryPose pose;
for (size_t c = 0; c < checkpoints.size(); c++){
@@ -527,6 +528,113 @@ UnitTestResult test_intro_names(){
return true;
}
+
+size_t row_index(std::string_view id){
+ const std::vector& checkpoints = story_checkpoints();
+ for (size_t c = 0; c < checkpoints.size(); c++){
+ if (checkpoints[c].id == id){
+ return c;
+ }
+ }
+ throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No checkpoint " + std::string(id));
+}
+
+std::string joined_ids(const std::vector& rows){
+ std::string text;
+ for (const StoryCheckpoint& row : rows){
+ text += (text.empty() ? "" : " ") + row.id;
+ }
+ return text;
+}
+
+const char* handoff_name(StoryHandoff handoff){
+ switch (handoff){
+ case StoryHandoff::NONE:
+ return "none";
+ case StoryHandoff::SID_HELPER:
+ return "SID Helper";
+ }
+ return "?";
+}
+
+struct RunPlanScenario{
+ std::string name = "";
+ std::string first = "";
+ // The table's last row if empty.
+ std::string last = "";
+ StoryHandoffs handoffs = {};
+ std::string expected_ids = "";
+ StoryHandoff expected_stop = StoryHandoff::NONE;
+};
+
+class Test_RunPlan : public UnitTest{
+public:
+ Test_RunPlan(RunPlanScenario scenario)
+ : UnitTest("PokemonFRLG::AutoStory - run plan: " + scenario.name)
+ , m_scenario(std::move(scenario))
+ {}
+
+ virtual UnitTestResult run(Logger&, CancellableScope&) const override{
+ try{
+ const size_t last = m_scenario.last.empty() ? story_checkpoints().size() - 1 : row_index(m_scenario.last);
+ const StoryRunPlan plan = plan_story_run(story_checkpoints(), row_index(m_scenario.first), last, m_scenario.handoffs);
+ const std::string ids = joined_ids(plan.rows);
+ if (ids != m_scenario.expected_ids || plan.stop != m_scenario.expected_stop){
+ return "Expected rows \"" + m_scenario.expected_ids + "\", stop " + handoff_name(m_scenario.expected_stop)
+ + "; got \"" + ids + "\", stop " + handoff_name(plan.stop) + ".";
+ }
+ return true;
+ }catch (Exception& e){
+ return std::string(e.name()) + ": " + e.message();
+ }
+ }
+
+private:
+ const RunPlanScenario m_scenario;
+};
+
+void add_tests_RunPlan(UnitTestDatabase& database){
+ const std::string TODAY = "00.01 00.02 00.03 00.04 00.05 00.06 00.07 00.08 00.09 00.10";
+ const RunPlanScenario scenarios[] = {
+ {.name = "handoffs off: the story as before", .first = "00.01", .expected_ids = TODAY},
+ {.name = "handoffs off: a range", .first = "00.02", .last = "00.04", .expected_ids = "00.02 00.03 00.04"},
+ {
+ .name = "SID Helper: 00.01 stops on the keyboard",
+ .first = "00.01",
+ .handoffs = {.sid_helper = true},
+ .expected_ids = "",
+ .expected_stop = StoryHandoff::SID_HELPER,
+ },
+ {
+ .name = "SID Helper: back in at 00.01b",
+ .first = "00.01b",
+ .last = "00.03",
+ .handoffs = {.sid_helper = true},
+ .expected_ids = "00.01b 00.02 00.03",
+ },
+ {.name = "00.01b runs when started at, option off", .first = "00.01b", .last = "00.02", .expected_ids = "00.01b 00.02"},
+ {.name = "SID Helper: a later start passes over it", .first = "00.02", .last = "00.03", .handoffs = {.sid_helper = true}, .expected_ids = "00.02 00.03"},
+ };
+ for (const RunPlanScenario& scenario : scenarios){
+ database.add(scenario);
+ }
+}
+
+// The way back in after SID Helper loads no save and starts where 00.01
+// leaves the player, so 00.02 follows it 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.";
+ }
+ }
+ return true;
+}
+
// set_options()'s RIGHT presses from each value an old save can leave:
// on console B a save with FAST and SET became SLOW and SHIFT when the
// presses assumed MID and SHIFT.
@@ -571,6 +679,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);
database.add("every route plans, and checkpoints chain", test_routes_and_chain);
database.add("step names are unique per checkpoint", test_unique_names);
database.add("every save has a place on the save card", test_save_places);