From f92f9e41c122ac4b991d76730d739c2683e4fc66 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 29 Oct 2026 23:44:06 -0700 Subject: [PATCH 1/5] FRLG: type names on the in-game naming screen Adds the naming screen keyboard from pret/pokefirered's naming_screen.c (three pages, the button column, wrap-around and page-change cursor rules), a planner that turns a name into the fewest presses, a type_name() routine that presses them, and a detector for the screen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_NamingScreenDetector.cpp | 65 ++++ .../Menus/PokemonFRLG_NamingScreenDetector.h | 57 +++ .../PokemonFRLG/PokemonFRLG_Tests.cpp | 345 ++++++++++++++++++ .../Programs/PokemonFRLG_NamingScreen.cpp | 89 +++++ .../Programs/PokemonFRLG_NamingScreen.h | 36 ++ .../PokemonFRLG_NamingScreenKeyboard.cpp | 341 +++++++++++++++++ .../PokemonFRLG_NamingScreenKeyboard.h | 134 +++++++ SerialPrograms/cmake/SourceFiles.cmake | 6 + 8 files changed, 1073 insertions(+) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.cpp new file mode 100644 index 0000000000..c92e0d883e --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.cpp @@ -0,0 +1,65 @@ +/* Naming Screen Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/ImageTools/FloatPixel.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" +#include "PokemonFRLG_NamingScreenDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +// Keyboard fill colors by page (keyboard.pal 13, 14, 15) as channel ratios: +// (123, 172, 197), (213, 156, 115), (148, 189, 106). +const FloatPixel KEYBOARD_COLORS[] = { + {0.250, 0.350, 0.400}, // upper case + {0.440, 0.322, 0.238}, // lower case + {0.334, 0.427, 0.239}, // symbols +}; + +bool is_keyboard_color(const ImageViewRGB32& image, const FloatPixel& color){ + return is_solid(image, color, 0.08, 30); +} + +} + + +// Boxes are fractions of the 240x160 GBA screen inside the game box. +NamingScreenDetector::NamingScreenDetector(Color color) + : m_color(color) + , m_keyboard_left_box(25.0 / 240, 82.0 / 160, 4.0 / 240, 60.0 / 160) // x 25-29, y 82-142 + , m_keyboard_right_box(170.0 / 240, 82.0 / 160, 5.0 / 240, 60.0 / 160) // x 170-175, y 82-142 + , m_entry_box(168.0 / 240, 50.0 / 160, 6.0 / 240, 12.0 / 160) // x 168-174, y 50-62 +{} +void NamingScreenDetector::make_overlays(VideoOverlaySet& items) const{ + const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; + items.add(m_color, GAME_BOX.inner_to_outer(m_keyboard_left_box)); + items.add(m_color, GAME_BOX.inner_to_outer(m_keyboard_right_box)); + items.add(m_color, GAME_BOX.inner_to_outer(m_entry_box)); +} +bool NamingScreenDetector::detect(const ImageViewRGB32& screen){ + ImageViewRGB32 game_screen = extract_box_reference(screen, GameSettings::instance().GAME_BOX); + ImageViewRGB32 left = extract_box_reference(game_screen, m_keyboard_left_box); + ImageViewRGB32 right = extract_box_reference(game_screen, m_keyboard_right_box); + if (!is_white(extract_box_reference(game_screen, m_entry_box))){ + return false; + } + for (const FloatPixel& color : KEYBOARD_COLORS){ + if (is_keyboard_color(left, color) && is_keyboard_color(right, color)){ + return true; + } + } + return false; +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.h new file mode 100644 index 0000000000..c486fbf9bf --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.h @@ -0,0 +1,57 @@ +/* Naming Screen Detector + * + * From: https://github.com/PokemonAutomation/ + * + * Detects the in-game keyboard used to name the player, the rival, boxes and Pokemon. + * + * The boxes and colors come from pret/pokefirered src/naming_screen.c (commit + * ce6406d8c2eaf46d543c8a2606de0c3cb1951982), not from screenshots: the keyboard window + * covers tiles (3, 10)-(22, 18) and is filled with keyboard.pal color 13, 14 or 15 by page, + * and the text entry window at tiles (8, 6)-(22, 8) is white. + * Not yet checked against a capture. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_NamingScreenDetector_H +#define PokemonAutomation_PokemonFRLG_NamingScreenDetector_H + +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/VisualDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +class NamingScreenDetector : public StaticScreenDetector{ +public: + NamingScreenDetector(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + // The keyboard's margins left and right of the keys, clear of the cursor. + ImageFloatBox m_keyboard_left_box; + ImageFloatBox m_keyboard_right_box; + // The text entry window, right of the last character. + ImageFloatBox m_entry_box; +}; +class NamingScreenWatcher : public DetectorToFinder{ +public: + NamingScreenWatcher(Color color = COLOR_RED) + : DetectorToFinder("NamingScreenWatcher", std::chrono::milliseconds(250), color) + {} +}; + + +} +} +} + +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index d51e585590..ec49581831 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -5,6 +5,7 @@ */ #include +#include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Filesystem/Filesystem.h" #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/StaticGlobals.h" @@ -23,6 +24,7 @@ #include "Inference/PokemonFRLG_StatsReader.h" #include "Inference/PokemonFRLG_TrainerIdReader.h" #include "Inference/PokemonFRLG_WildEncounterReader.h" +#include "Programs/PokemonFRLG_NamingScreenKeyboard.h" #include "PokemonFRLG_Navigation.h" #include "PokemonFRLG_Tests.h" @@ -255,6 +257,346 @@ void add_tests_ExitWildBattle(UnitTestDatabase& database){ } } + +// Naming screen keyboard: cursor moves, plans and text checks, against pokefirered's +// src/naming_screen.c. + +struct NamingCursorScenario{ + std::string name = ""; + NamingCursor start = {}; + std::vector actions = {}; + NamingCursor expected = {}; +}; + +class Test_NamingCursor : public UnitTest{ +public: + Test_NamingCursor(NamingCursorScenario scenario) + : UnitTest("PokemonFRLG::NamingScreen cursor - " + scenario.name) + , m_scenario(std::move(scenario)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + NamingCursor cursor = m_scenario.start; + for (NamingAction action : m_scenario.actions){ + cursor = apply_naming_action(cursor, action); + } + if (cursor != m_scenario.expected){ + return "After " + to_string(m_scenario.actions) + ": expected " + to_string(m_scenario.expected) + + ", got " + to_string(cursor); + } + return true; + } + +private: + const NamingCursorScenario m_scenario; +}; + +// Crossing and moving on the button column. +std::vector naming_cursor_button_column_scenarios(){ + using enum NamingAction; + using enum NamingScreenPage; + return { + { + .name = "left from A goes to the page button", + .actions = {LEFT}, + .expected = {.page = UPPER, .row = 0, .column = 8, .remembered_row = 1}, + }, + { + .name = "left and right return to A", + .actions = {LEFT, RIGHT}, + .expected = {.page = UPPER, .row = 0, .column = 0, .remembered_row = 1}, + }, + { + // Rows 1 and 2 share the back button; the game remembers which one it came from. + .name = "row 1 crosses the button column", + .start = {.page = UPPER, .row = 1, .column = 0}, + .actions = {LEFT, RIGHT}, + .expected = {.page = UPPER, .row = 1, .column = 0, .remembered_row = 1}, + }, + { + .name = "row 2 crosses the button column", + .start = {.page = UPPER, .row = 2, .column = 0}, + .actions = {LEFT, RIGHT}, + .expected = {.page = UPPER, .row = 2, .column = 0, .remembered_row = 2}, + }, + { + .name = "row 3 crosses the OK button", + .start = {.page = UPPER, .row = 3, .column = 0}, + .actions = {LEFT, RIGHT}, + .expected = {.page = UPPER, .row = 3, .column = 0, .remembered_row = 2}, + }, + { + .name = "right from the last column wraps through the buttons", + .start = {.page = UPPER, .row = 0, .column = 7}, + .actions = {RIGHT, RIGHT}, + .expected = {.page = UPPER, .row = 0, .column = 0, .remembered_row = 1}, + }, + { + // Moving on the button column overwrites the remembered row with BACK. + .name = "moving on the button column forgets row 2", + .start = {.page = UPPER, .row = 2, .column = 0}, + .actions = {LEFT, UP, DOWN, RIGHT}, + .expected = {.page = UPPER, .row = 1, .column = 0, .remembered_row = 1}, + }, + }; +} +// Wrap-around on the keys and the button column. +std::vector naming_cursor_wrap_scenarios(){ + using enum NamingAction; + using enum NamingScreenPage; + return { + { + .name = "button column wraps over three buttons", + .start = {.page = UPPER, .row = 0, .column = 8, .remembered_row = 1}, + .actions = {UP}, + .expected = {.page = UPPER, .row = 2, .column = 8, .remembered_row = 2}, + }, + { + .name = "rows wrap up", + .actions = {UP}, + .expected = {.page = UPPER, .row = 3, .column = 0}, + }, + { + .name = "rows wrap down", + .start = {.page = UPPER, .row = 3, .column = 0}, + .actions = {DOWN}, + .expected = {.page = UPPER, .row = 0, .column = 0}, + }, + { + .name = "symbols page has six key columns", + .start = {.page = SYMBOLS, .row = 0, .column = 0}, + .actions = {LEFT, LEFT}, + .expected = {.page = SYMBOLS, .row = 0, .column = 5, .remembered_row = 1}, + }, + }; +} +// SELECT and START. +std::vector naming_cursor_page_scenarios(){ + using enum NamingAction; + using enum NamingScreenPage; + return { + { + .name = "SELECT goes to lower case", + .actions = {SELECT}, + .expected = {.page = LOWER, .row = 0, .column = 0}, + }, + { + .name = "SELECT twice goes to symbols", + .actions = {SELECT, SELECT}, + .expected = {.page = SYMBOLS, .row = 0, .column = 0}, + }, + { + .name = "SELECT three times returns to upper case", + .actions = {SELECT, SELECT, SELECT}, + .expected = {.page = UPPER, .row = 0, .column = 0}, + }, + { + .name = "SELECT moves the cursor into the symbols columns", + .start = {.page = UPPER, .row = 0, .column = 7}, + .actions = {SELECT, SELECT, SELECT}, + .expected = {.page = UPPER, .row = 0, .column = 5}, + }, + { + .name = "SELECT keeps the cursor on the button column", + .start = {.page = UPPER, .row = 1, .column = 8, .remembered_row = 1}, + .actions = {SELECT, SELECT}, + .expected = {.page = SYMBOLS, .row = 1, .column = 6, .remembered_row = 1}, + }, + { + .name = "START goes to OK", + .start = {.page = LOWER, .row = 1, .column = 3}, + .actions = {START}, + .expected = {.page = LOWER, .row = 2, .column = 8}, + }, + { + .name = "START goes to OK on symbols", + .start = {.page = SYMBOLS, .row = 0, .column = 0}, + .actions = {START}, + .expected = {.page = SYMBOLS, .row = 2, .column = 6}, + }, + }; +} +void add_tests_NamingCursor(UnitTestDatabase& database){ + for (auto scenarios : { + naming_cursor_button_column_scenarios(), + naming_cursor_wrap_scenarios(), + naming_cursor_page_scenarios(), + }){ + for (const NamingCursorScenario& scenario : scenarios){ + database.add(scenario); + } + } +} + + +struct NamingPlanScenario{ + std::string name = ""; + std::string text = ""; + NamingScreenKind kind = NamingScreenKind::PLAYER; + // What the game shows when it differs from `text` (another spelling of a key). + std::string typed = ""; + // The exact plan, when the shortest one is unique; "" to skip. + std::string expected_actions = ""; + // The number of presses, including A and the final START A; 0 to skip. + size_t expected_presses = 0; +}; + +class Test_NamingPlan : public UnitTest{ +public: + Test_NamingPlan(NamingPlanScenario scenario) + : UnitTest("PokemonFRLG::NamingScreen plan - " + scenario.name) + , m_scenario(std::move(scenario)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector actions = plan_naming_input(m_scenario.text, m_scenario.kind); + const std::string plan = to_string(actions); + if (!m_scenario.expected_actions.empty() && plan != m_scenario.expected_actions){ + return "Expected plan: " + m_scenario.expected_actions + "\nActual plan: " + plan; + } + if (m_scenario.expected_presses != 0 && actions.size() != m_scenario.expected_presses){ + return "Expected " + std::to_string(m_scenario.expected_presses) + " presses, got " + + std::to_string(actions.size()) + ": " + plan; + } + + // Replay the plan: every A but the last types the key under the cursor, and the last + // presses OK. + std::string typed; + NamingCursor cursor; + for (size_t i = 0; i < actions.size(); i++){ + cursor = apply_naming_action(cursor, actions[i]); + if (actions[i] != NamingAction::A){ + continue; + } + const uint8_t columns = NAMING_SCREEN_KEY_COLUMNS[(size_t)cursor.page]; + if (i + 1 == actions.size()){ + if (cursor.column != columns || cursor.row != NAMING_SCREEN_BUTTON_OK){ + return "The last A is not on OK but on " + to_string(cursor) + ": " + plan; + } + break; + } + if (cursor.column >= columns){ + return "A pressed on the button column at " + to_string(cursor) + ": " + plan; + } + typed += NAMING_SCREEN_KEYS[(size_t)cursor.page][cursor.row][cursor.column]; + } + const std::string& expected = m_scenario.typed.empty() ? m_scenario.text : m_scenario.typed; + if (typed != expected){ + return "Expected the plan to type \"" + expected + "\", it types \"" + typed + "\": " + plan; + } + return true; + } + +private: + const NamingPlanScenario m_scenario; +}; + +void add_tests_NamingPlan(UnitTestDatabase& database){ + using enum NamingScreenKind; + const NamingPlanScenario scenarios[] = { + {.name = "starts on A", .text = "A", .expected_actions = "A START A"}, + {.name = "next key", .text = "AB", .expected_actions = "A RIGHT A START A"}, + {.name = "next row", .text = "AG", .expected_actions = "A DOWN A START A"}, + {.name = "rows wrap", .text = "T", .expected_actions = "UP A START A"}, + { + .name = "columns wrap through the button column", + .text = ".", + .expected_actions = "LEFT LEFT A START A", + }, + {.name = "lower case page", .text = "a", .expected_actions = "SELECT A START A"}, + {.name = "symbols page", .text = "0", .expected_actions = "SELECT SELECT A START A"}, + { + // The cursor moves from column 7 to the symbols page's last column, 5. + .name = "page change moves the cursor into the symbols columns", + .text = ".4", + .expected_actions = "LEFT LEFT A SELECT SELECT LEFT A START A", + }, + {.name = "both ways round", .text = "Z", .expected_presses = 7}, + {.name = "male sign", .text = "\xE2\x99\x82", .expected_presses = 9}, + {.name = "full player name", .text = "ABCDEFG", .expected_presses = 19}, + {.name = "mixed case", .text = "Gary", .expected_presses = 16}, + {.name = "space", .text = "A B", .expected_presses = 12}, + {.name = "full nickname", .text = "BULBASAURS", .kind = POKEMON}, + {.name = "female sign", .text = "NIDORAN\xE2\x99\x80", .kind = POKEMON}, + {.name = "box name", .text = "BOX 1-2!", .kind = BOX}, + {.name = "rival", .text = "Blue?", .kind = RIVAL}, + { + .name = "symbols row 4", + .text = "\xE2\x80\xA6\xE2\x80\x9C\xE2\x80\x9D\xE2\x80\x98'", + .kind = POKEMON, + }, + {.name = "curly apostrophe", .text = "IT\xE2\x80\x99S", .typed = "IT'S"}, + }; + for (const NamingPlanScenario& scenario : scenarios){ + database.add(scenario); + } +} + + +struct NamingTextScenario{ + std::string name = ""; + std::string text = ""; + NamingScreenKind kind = NamingScreenKind::PLAYER; + // A part of the expected error; "" if the text is fine. + std::string expected_error = ""; +}; + +class Test_NamingText : public UnitTest{ +public: + Test_NamingText(NamingTextScenario scenario) + : UnitTest("PokemonFRLG::NamingScreen text - " + scenario.name) + , m_scenario(std::move(scenario)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string error = naming_text_error(m_scenario.text, m_scenario.kind); + if (m_scenario.expected_error.empty()){ + if (!error.empty()){ + return "Expected \"" + m_scenario.text + "\" to be accepted, got: " + error; + } + return true; + } + if (error.find(m_scenario.expected_error) == std::string::npos){ + return "Expected an error containing \"" + m_scenario.expected_error + "\", got: \"" + error + "\""; + } + try{ + plan_naming_input(m_scenario.text, m_scenario.kind); + }catch (InternalProgramError&){ + return true; + } + return "Expected plan_naming_input() to throw for \"" + m_scenario.text + "\""; + } + +private: + const NamingTextScenario m_scenario; +}; + +void add_tests_NamingText(UnitTestDatabase& database){ + using enum NamingScreenKind; + const NamingTextScenario scenarios[] = { + {.name = "player name of 7", .text = "ABCDEFG", .kind = PLAYER}, + {.name = "player name of 8", .text = "ABCDEFGH", .kind = PLAYER, .expected_error = "at most 7"}, + {.name = "rival name of 7", .text = "ABCDEFG", .kind = RIVAL}, + {.name = "rival name of 8", .text = "ABCDEFGH", .kind = RIVAL, .expected_error = "at most 7"}, + {.name = "box name of 8", .text = "ABCDEFGH", .kind = BOX}, + {.name = "box name of 9", .text = "ABCDEFGHI", .kind = BOX, .expected_error = "at most 8"}, + {.name = "nickname of 10", .text = "ABCDEFGHIJ", .kind = POKEMON}, + {.name = "nickname of 11", .text = "ABCDEFGHIJK", .kind = POKEMON, .expected_error = "at most 10"}, + { + // Seven characters, 21 bytes. + .name = "symbols count as one character", + .text = "\xE2\x99\x82\xE2\x99\x82\xE2\x99\x82\xE2\x99\x82\xE2\x99\x82\xE2\x99\x82\xE2\x99\x82", + }, + {.name = "unknown character", .text = "Ash@", .expected_error = "type: \"@\""}, + {.name = "unknown accented character", .text = "Jos\xC3\xA9", .expected_error = "type: \"\xC3\xA9\""}, + {.name = "empty", .text = "", .expected_error = "empty"}, + {.name = "only spaces", .text = " ", .expected_error = "only spaces"}, + }; + for (const NamingTextScenario& scenario : scenarios){ + database.add(scenario); + } +} + } @@ -278,6 +620,9 @@ void add_tests(UnitTestDatabase& database){ add_tests_HallOfFameSavingDetector(database); add_tests_BattleMoveSelectionArrow(database); add_tests_ExitWildBattle(database); + add_tests_NamingCursor(database); + add_tests_NamingPlan(database); + add_tests_NamingText(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp new file mode 100644 index 0000000000..c4eded99f2 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp @@ -0,0 +1,89 @@ +/* Naming Screen + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Color.h" +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Time.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.h" +#include "PokemonFRLG_NamingScreen.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +// The game reads new presses once per frame and starts repeating a held direction after +// about 40 frames, so a short hold with a release of several frames is one press each. +constexpr Milliseconds PRESS_HOLD = 80ms; +constexpr Milliseconds MOVE_RELEASE = 120ms; +constexpr Milliseconds KEY_RELEASE = 200ms; +// The page swap animation runs 32 frames (about 530 ms) with input disabled. +constexpr Milliseconds PAGE_SWAP_RELEASE = 900ms; +// After the last character fits, the cursor animates to OK with input disabled. +constexpr Milliseconds START_RELEASE = 500ms; + +void press(ProControllerContext& context, NamingAction action){ + switch (action){ + case NamingAction::UP: + pbf_press_dpad(context, DPAD_UP, PRESS_HOLD, MOVE_RELEASE); + return; + case NamingAction::DOWN: + pbf_press_dpad(context, DPAD_DOWN, PRESS_HOLD, MOVE_RELEASE); + return; + case NamingAction::LEFT: + pbf_press_dpad(context, DPAD_LEFT, PRESS_HOLD, MOVE_RELEASE); + return; + case NamingAction::RIGHT: + pbf_press_dpad(context, DPAD_RIGHT, PRESS_HOLD, MOVE_RELEASE); + return; + case NamingAction::A: + pbf_press_button(context, BUTTON_A, PRESS_HOLD, KEY_RELEASE); + return; + case NamingAction::SELECT: + pbf_press_button(context, BUTTON_MINUS, PRESS_HOLD, PAGE_SWAP_RELEASE); + return; + case NamingAction::START: + pbf_press_button(context, BUTTON_PLUS, PRESS_HOLD, START_RELEASE); + return; + } +} + +} + + +bool type_name( + ConsoleHandle& console, ProControllerContext& context, + const std::string& text, NamingScreenKind kind +){ + const std::string error = naming_text_error(text, kind); + if (!error.empty()){ + throw UserSetupError(console, error); + } + const std::vector actions = plan_naming_input(text, kind); + + NamingScreenWatcher naming_screen(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, std::chrono::seconds(5), {naming_screen}) < 0){ + console.log("Naming screen not found. Not typing \"" + text + "\".", COLOR_RED); + return false; + } + + console.log("Typing " + std::string(naming_screen_kind_name(kind)) + " name \"" + text + "\": " + to_string(actions)); + for (NamingAction action : actions){ + press(context, action); + } + context.wait_for_all_requests(); + return true; +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h new file mode 100644 index 0000000000..2da50003ce --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h @@ -0,0 +1,36 @@ +/* Naming Screen + * + * From: https://github.com/PokemonAutomation/ + * + * Types a name on the in-game naming screen (player, rival, box, Pokemon nickname). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_NamingScreen_H +#define PokemonAutomation_PokemonFRLG_NamingScreen_H + +#include +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG_NamingScreenKeyboard.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Types `text` on a naming screen that was just opened (cursor on "A", nothing typed) and +// presses OK. Returns once OK is pressed; what follows (e.g. "So your name is ...?") is up to +// the caller. +// Returns false, without pressing anything, if the naming screen is not on screen. +// Throws UserSetupError if the game can't take `text` (see naming_text_error()). +bool type_name( + ConsoleHandle& console, ProControllerContext& context, + const std::string& text, NamingScreenKind kind +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp new file mode 100644 index 0000000000..35b0b737ee --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp @@ -0,0 +1,341 @@ +/* Naming Screen Keyboard + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Compiler.h" +#include "Common/Cpp/Exceptions.h" +#include "PokemonFRLG_NamingScreenKeyboard.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +size_t naming_screen_max_chars(NamingScreenKind kind){ + switch (kind){ + case NamingScreenKind::PLAYER: return 7; + case NamingScreenKind::RIVAL: return 7; + case NamingScreenKind::BOX: return 8; + case NamingScreenKind::POKEMON: return 10; + } + return 0; +} +const char* naming_screen_kind_name(NamingScreenKind kind){ + switch (kind){ + case NamingScreenKind::PLAYER: return "player"; + case NamingScreenKind::RIVAL: return "rival"; + case NamingScreenKind::BOX: return "box"; + case NamingScreenKind::POKEMON: return "Pokemon"; + } + return "?"; +} + +std::string to_string(const NamingCursor& cursor){ + static const char* const PAGE_NAMES[] = {"UPPER", "LOWER", "SYMBOLS"}; + return std::string(PAGE_NAMES[(size_t)cursor.page]) + + " row " + std::to_string(cursor.row) + + " column " + std::to_string(cursor.column) + + " (remembered row " + std::to_string(cursor.remembered_row) + ")"; +} + +const char* naming_action_name(NamingAction action){ + switch (action){ + case NamingAction::UP: return "UP"; + case NamingAction::DOWN: return "DOWN"; + case NamingAction::LEFT: return "LEFT"; + case NamingAction::RIGHT: return "RIGHT"; + case NamingAction::A: return "A"; + case NamingAction::SELECT: return "SELECT"; + case NamingAction::START: return "START"; + } + return "?"; +} +std::string to_string(const std::vector& actions){ + std::string ret; + for (NamingAction action : actions){ + if (!ret.empty()){ + ret += ' '; + } + ret += naming_action_name(action); + } + return ret; +} + + +namespace{ + +constexpr uint8_t BUTTON_COUNT = 3; +// sKeyRowToButtonRow and sButtonRowToKeyRow in HandleDpadMovement(). +constexpr uint8_t KEY_ROW_TO_BUTTON[NAMING_SCREEN_KEY_ROWS] = {0, 1, 1, 2}; +constexpr uint8_t BUTTON_TO_KEY_ROW[BUTTON_COUNT] = {0, 0, 3}; + +uint8_t key_columns(NamingScreenPage page){ + return NAMING_SCREEN_KEY_COLUMNS[(size_t)page]; +} + +// Wraps the row after a move: three buttons on the button column, four key rows elsewhere. +void wrap_row(NamingCursor& cursor, bool on_button_column, int y){ + if (on_button_column){ + if (y < 0){ + y = BUTTON_COUNT - 1; + } + if (y >= BUTTON_COUNT){ + y = 0; + } + // The game reuses tButtonId here, so the middle button can return to row 1 or 2. + if (y == 0){ + cursor.remembered_row = NAMING_SCREEN_BUTTON_BACK; + }else if (y == BUTTON_COUNT - 1){ + cursor.remembered_row = NAMING_SCREEN_BUTTON_OK; + } + }else{ + if (y < 0){ + y = NAMING_SCREEN_KEY_ROWS - 1; + } + if (y >= (int)NAMING_SCREEN_KEY_ROWS){ + y = 0; + } + } + cursor.row = (uint8_t)y; +} + +NamingCursor move(NamingCursor cursor, int dx, int dy){ + const int columns = key_columns(cursor.page); + const int previous_x = cursor.column; + int x = cursor.column + dx; + int y = cursor.row + dy; + + // Wrap X. The button column is part of the ring. + if (x < 0){ + x = columns; + } + if (x > columns){ + x = 0; + } + + // Moving onto or off the button column. + if (dx != 0){ + if (x == columns){ + cursor.remembered_row = (uint8_t)y; + y = KEY_ROW_TO_BUTTON[y]; + }else if (previous_x == columns){ + y = y == BUTTON_COUNT / 2 ? cursor.remembered_row : BUTTON_TO_KEY_ROW[y]; + } + } + + cursor.column = (uint8_t)x; + wrap_row(cursor, x == columns, y); + return cursor; +} + +NamingCursor next_page(NamingCursor cursor){ + const bool on_button_column = cursor.column == key_columns(cursor.page); + cursor.page = (NamingScreenPage)(((size_t)cursor.page + 1) % NAMING_SCREEN_PAGES); + const uint8_t columns = key_columns(cursor.page); + if (on_button_column || cursor.column >= columns){ + cursor.column = on_button_column ? columns : columns - 1; + } + return cursor; +} + +const char* key_at(const NamingCursor& cursor){ + if (cursor.column >= key_columns(cursor.page)){ + return nullptr; + } + return NAMING_SCREEN_KEYS[(size_t)cursor.page][cursor.row][cursor.column]; +} + + +struct KeyAlias{ + const char* typed; + const char* key; +}; +// Other spellings of a key's character. +constexpr KeyAlias KEY_ALIASES[] = { + {"\xE2\x80\x99", "'"}, // right single quote: the apostrophe key types it +}; + +bool text_has_at(const std::string& text, size_t position, const char* str){ + return text.compare(position, strlen(str), str) == 0; +} + +// The key text at `position` in `text`, or nullptr if no key types it. +const char* key_text_at(const std::string& text, size_t position, size_t& length){ + const char* best = nullptr; + length = 0; + for (const auto& page : NAMING_SCREEN_KEYS){ + for (const auto& row : page){ + for (const char* key : row){ + if (key != nullptr && strlen(key) > length && text_has_at(text, position, key)){ + best = key; + length = strlen(key); + } + } + } + } + for (const KeyAlias& alias : KEY_ALIASES){ + if (strlen(alias.typed) > length && text_has_at(text, position, alias.typed)){ + best = alias.key; + length = strlen(alias.typed); + } + } + return best; +} + +// The UTF-8 character at `position`, for error messages. +std::string utf8_char_at(const std::string& text, size_t position){ + const unsigned char lead = (unsigned char)text[position]; + size_t length = 1; + if (lead >= 0xF0){ + length = 4; + }else if (lead >= 0xE0){ + length = 3; + }else if (lead >= 0xC0){ + length = 2; + } + return text.substr(position, length); +} + +// Splits `text` into the keys that type it. Returns the error, or "" on success. +std::string split_into_keys(const std::string& text, std::vector& keys){ + keys.clear(); + for (size_t position = 0; position < text.size();){ + size_t length; + const char* key = key_text_at(text, position, length); + if (key == nullptr){ + return "\"" + text + "\" has a character the naming screen can't type: \"" + + utf8_char_at(text, position) + "\" (byte " + std::to_string(position) + "). " + "It types A-Z, a-z, 0-9, space and . , ! ? / - ' and the game's " + "male/female signs, ellipsis and curly quotes."; + } + keys.emplace_back(key); + position += length; + } + return ""; +} + + +size_t state_index(const NamingCursor& cursor){ + return (((size_t)cursor.page * (NAMING_SCREEN_MAX_KEY_COLUMNS + 1) + cursor.column) + * NAMING_SCREEN_KEY_ROWS + cursor.row) * NAMING_SCREEN_KEY_ROWS + cursor.remembered_row; +} +constexpr size_t STATE_COUNT = + NAMING_SCREEN_PAGES * (NAMING_SCREEN_MAX_KEY_COLUMNS + 1) * NAMING_SCREEN_KEY_ROWS * NAMING_SCREEN_KEY_ROWS; + +// Breadth-first search over cursor states for the fewest presses that reach a key typing `key`. +// Appends the moves and returns the cursor on that key. +NamingCursor append_moves_to_key(const NamingCursor& start, const char* key, std::vector& actions){ + // START is left out: it only jumps to OK, which the plan uses once at the end. + static const NamingAction MOVES[] = { + NamingAction::UP, NamingAction::DOWN, NamingAction::LEFT, NamingAction::RIGHT, NamingAction::SELECT, + }; + struct Visit{ + bool seen = false; + size_t parent = 0; + NamingAction action = NamingAction::A; + }; + std::vector visits(STATE_COUNT); + std::vector queue{start}; + visits[state_index(start)].seen = true; + + for (size_t head = 0; head < queue.size(); head++){ + const NamingCursor cursor = queue[head]; + const char* here = key_at(cursor); + if (here != nullptr && strcmp(here, key) == 0){ + std::vector path; + for (size_t index = state_index(cursor); index != state_index(start); index = visits[index].parent){ + path.emplace_back(visits[index].action); + } + actions.insert(actions.end(), path.rbegin(), path.rend()); + return cursor; + } + for (NamingAction action : MOVES){ + const NamingCursor next = apply_naming_action(cursor, action); + Visit& visit = visits[state_index(next)]; + if (!visit.seen){ + visit.seen = true; + visit.parent = state_index(cursor); + visit.action = action; + queue.emplace_back(next); + } + } + } + // Every key is reachable, so this is a bug. + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::string("No path to naming screen key: ") + key); +} + +} + + +NamingCursor apply_naming_action(const NamingCursor& cursor, NamingAction action){ + switch (action){ + case NamingAction::UP: return move(cursor, 0, -1); + case NamingAction::DOWN: return move(cursor, 0, 1); + case NamingAction::LEFT: return move(cursor, -1, 0); + case NamingAction::RIGHT: return move(cursor, 1, 0); + case NamingAction::SELECT: return next_page(cursor); + case NamingAction::START:{ + NamingCursor ret = cursor; + ret.column = key_columns(cursor.page); + ret.row = NAMING_SCREEN_BUTTON_OK; + return ret; + } + case NamingAction::A: + return cursor; + } + return cursor; +} + + +std::string naming_text_error(const std::string& text, NamingScreenKind kind){ + std::vector keys; + std::string error = split_into_keys(text, keys); + if (!error.empty()){ + return error; + } + bool only_spaces = true; + for (const char* key : keys){ + only_spaces &= strcmp(key, " ") == 0; + } + if (only_spaces){ + return "The name \"" + text + "\" is empty or only spaces, so the game would not keep it."; + } + const size_t max_chars = naming_screen_max_chars(kind); + if (keys.size() > max_chars){ + return "\"" + text + "\" is " + std::to_string(keys.size()) + " characters, but a " + + naming_screen_kind_name(kind) + " name holds at most " + std::to_string(max_chars) + "."; + } + return ""; +} + +std::vector plan_naming_input( + const std::string& text, NamingScreenKind kind, const NamingCursor& start +){ + const std::string error = naming_text_error(text, kind); + if (!error.empty()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, error); + } + std::vector keys; + split_into_keys(text, keys); + + std::vector actions; + NamingCursor cursor = start; + for (const char* key : keys){ + cursor = append_moves_to_key(cursor, key, actions); + actions.emplace_back(NamingAction::A); + } + // When the last key fills the name, the game moves the cursor to OK by itself; START is + // then harmless. + actions.emplace_back(NamingAction::START); + actions.emplace_back(NamingAction::A); + return actions; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h new file mode 100644 index 0000000000..f362f276bc --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h @@ -0,0 +1,134 @@ +/* Naming Screen Keyboard + * + * From: https://github.com/PokemonAutomation/ + * + * The in-game keyboard used to name the player, the rival, PC boxes and Pokemon, and a + * planner that turns a name into button presses. Pure data and logic: no console needed. + * + * Everything here follows pret/pokefirered src/naming_screen.c at commit + * ce6406d8c2eaf46d543c8a2606de0c3cb1951982 (English game). The decomp includes the Switch + * revision (REVISION >= 0xA), so the layout is the one on the Switch release. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_NamingScreenKeyboard_H +#define PokemonAutomation_PokemonFRLG_NamingScreenKeyboard_H + +#include +#include +#include +#include + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The keyboard pages, in the order SELECT cycles through them. The screen opens on UPPER. +enum class NamingScreenPage : uint8_t{ + UPPER, + LOWER, + SYMBOLS, +}; +constexpr size_t NAMING_SCREEN_PAGES = 3; +constexpr size_t NAMING_SCREEN_KEY_ROWS = 4; +constexpr size_t NAMING_SCREEN_MAX_KEY_COLUMNS = 8; + +// Key columns on each page (sPageColumnCounts). The button column (page / back / OK) comes +// right after the last key column, so on a page with N key columns it is column N. +constexpr uint8_t NAMING_SCREEN_KEY_COLUMNS[NAMING_SCREEN_PAGES] = {8, 8, 6}; + +// The text each key types, in UTF-8 (sKeyboardChars), indexed [page][row][column]. +// nullptr: no key (past the page's last column). +// The decomp pads each row with byte 0x00, which is a space in the game's charmap, and the +// keyboard labels draw a blank there: those cells type a space just like the spaces in rows 0-1. +// The apostrophe key types the game's right single quote (charmap 0xB4). +constexpr const char* NAMING_SCREEN_KEYS + [NAMING_SCREEN_PAGES][NAMING_SCREEN_KEY_ROWS][NAMING_SCREEN_MAX_KEY_COLUMNS] = { + { // UPPER + {"A", "B", "C", "D", "E", "F", " ", "."}, + {"G", "H", "I", "J", "K", "L", " ", ","}, + {"M", "N", "O", "P", "Q", "R", "S", " "}, + {"T", "U", "V", "W", "X", "Y", "Z", " "}, + }, + { // LOWER + {"a", "b", "c", "d", "e", "f", " ", "."}, + {"g", "h", "i", "j", "k", "l", " ", ","}, + {"m", "n", "o", "p", "q", "r", "s", " "}, + {"t", "u", "v", "w", "x", "y", "z", " "}, + }, + { // SYMBOLS + {"0", "1", "2", "3", "4", " ", nullptr, nullptr}, + {"5", "6", "7", "8", "9", " ", nullptr, nullptr}, + {"!", "?", "\xE2\x99\x82", "\xE2\x99\x80", "/", "-", nullptr, nullptr}, // ! ? male female / - + {"\xE2\x80\xA6", "\xE2\x80\x9C", "\xE2\x80\x9D", "\xE2\x80\x98", "'", " ", nullptr, nullptr}, // ellipsis, double quotes, left single quote, ' + }, +}; + +// The three buttons in the button column, top to bottom. +constexpr uint8_t NAMING_SCREEN_BUTTON_PAGE = 0; +constexpr uint8_t NAMING_SCREEN_BUTTON_BACK = 1; +constexpr uint8_t NAMING_SCREEN_BUTTON_OK = 2; + + +// Which naming screen is open; it sets how many characters fit (NamingScreenTemplate.maxChars). +enum class NamingScreenKind : uint8_t{ + PLAYER, // PLAYER_NAME_LENGTH + RIVAL, // PLAYER_NAME_LENGTH + BOX, // BOX_NAME_LENGTH + POKEMON, // POKEMON_NAME_LENGTH, for caught Pokemon and the nickname rater +}; +size_t naming_screen_max_chars(NamingScreenKind kind); +const char* naming_screen_kind_name(NamingScreenKind kind); + + +// The cursor, as the game tracks it. +struct NamingCursor{ + NamingScreenPage page = NamingScreenPage::UPPER; + // Key row (0-3), or button (0-2) when on the button column. + uint8_t row = 0; + // Key column, or NAMING_SCREEN_KEY_COLUMNS[page] for the button column. + uint8_t column = 0; + // The game's tButtonId: the key row to return to when leaving the middle button. + uint8_t remembered_row = 0; + + bool operator==(const NamingCursor& other) const = default; +}; +std::string to_string(const NamingCursor& cursor); + + +enum class NamingAction : uint8_t{ + UP, + DOWN, + LEFT, + RIGHT, + A, + SELECT, // next page + START, // move the cursor to OK +}; +const char* naming_action_name(NamingAction action); +// Space-separated action names, e.g. "LEFT LEFT A START A". +std::string to_string(const std::vector& actions); + + +// Where the cursor ends up after one press (HandleDpadMovement, MainState_WaitPageSwap, +// MoveCursorToOKButton). A leaves the cursor in place. +NamingCursor apply_naming_action(const NamingCursor& cursor, NamingAction action); + +// Why the game can't take `text` as a name of this kind, or "" if it can: a character with +// no key, more characters than fit, or nothing but spaces (the game then keeps no name). +std::string naming_text_error(const std::string& text, NamingScreenKind kind); + +// The presses that type `text` from `start` and confirm it: the shortest moves to each +// character's key, A, and at the end START and A to press OK. +// Throws InternalProgramError if naming_text_error() rejects `text`; check that first. +std::vector plan_naming_input( + const std::string& text, NamingScreenKind kind, const NamingCursor& start = NamingCursor() +); + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 28f811ded0..967c83adce 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1675,6 +1675,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_BagDetector.h Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_LoadMenuDetector.cpp Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_LoadMenuDetector.h + Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.cpp + Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_NamingScreenDetector.h Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_PartyEmptySlotDetector.cpp Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_PartyEmptySlotDetector.h Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_PartyHeldItemDetector.cpp @@ -1742,6 +1744,10 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/Farming/PokemonFRLG_PickupFarmer.h Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h + Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp + Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h + Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp + Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.cpp Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.cpp From 47c3ffcfe9ced3b015c8e7c49471b12f0835268a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 13:16:47 -0800 Subject: [PATCH 2/5] FRLG naming screen: type a name without pressing OK plan_naming_keys() plans the presses that type a name and stops before START A; plan_naming_input() is now that plan plus START A. type_name_without_confirming() types the name and leaves the naming screen open, which is where SID Helper starts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 49 +++++++++++++++++++ .../Programs/PokemonFRLG_NamingScreen.cpp | 33 ++++++++++--- .../Programs/PokemonFRLG_NamingScreen.h | 7 +++ .../PokemonFRLG_NamingScreenKeyboard.cpp | 9 +++- .../PokemonFRLG_NamingScreenKeyboard.h | 10 +++- 5 files changed, 97 insertions(+), 11 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index aaac70026f..0bdcd6ba03 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -538,6 +538,54 @@ void add_tests_NamingPlan(UnitTestDatabase& database){ } +struct NamingKeysScenario{ + std::string name = ""; + std::string text = ""; + NamingScreenKind kind = NamingScreenKind::PLAYER; + // The exact plan, when the shortest one is unique; "" to skip. + std::string expected_actions = ""; +}; + +// plan_naming_keys() stops before OK: it is plan_naming_input() without the final START A. +class Test_NamingKeys : public UnitTest{ +public: + Test_NamingKeys(NamingKeysScenario scenario) + : UnitTest("PokemonFRLG::NamingScreen keys only - " + scenario.name) + , m_scenario(std::move(scenario)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string keys = to_string(plan_naming_keys(m_scenario.text, m_scenario.kind)); + if (!m_scenario.expected_actions.empty() && keys != m_scenario.expected_actions){ + return "Expected keys: " + m_scenario.expected_actions + "\nActual keys: " + keys; + } + const std::string input = to_string(plan_naming_input(m_scenario.text, m_scenario.kind)); + if (input != keys + " START A"){ + return "Expected the confirmed plan to be \"" + keys + " START A\", got \"" + input + "\""; + } + return true; + } + +private: + const NamingKeysScenario m_scenario; +}; + +void add_tests_NamingKeys(UnitTestDatabase& database){ + using enum NamingScreenKind; + const NamingKeysScenario scenarios[] = { + {.name = "one key", .text = "A", .expected_actions = "A"}, + {.name = "next key", .text = "AB", .expected_actions = "A RIGHT A"}, + {.name = "lower case page", .text = "a", .expected_actions = "SELECT A"}, + // A full name: the game moves the cursor to OK by itself, but nothing presses it. + {.name = "full player name", .text = "ABCDEFG"}, + {.name = "mixed case", .text = "Kosmos"}, + }; + for (const NamingKeysScenario& scenario : scenarios){ + database.add(scenario); + } +} + + struct NamingTextScenario{ std::string name = ""; std::string text = ""; @@ -632,6 +680,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_ExitWildBattle(database); add_tests_NamingCursor(database); add_tests_NamingPlan(database); + add_tests_NamingKeys(database); add_tests_NamingText(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp index c4eded99f2..974acd42c9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp @@ -55,19 +55,17 @@ void press(ProControllerContext& context, NamingAction action){ } } -} - - -bool type_name( - ConsoleHandle& console, ProControllerContext& context, - const std::string& text, NamingScreenKind kind -){ +void check_name(ConsoleHandle& console, const std::string& text, NamingScreenKind kind){ const std::string error = naming_text_error(text, kind); if (!error.empty()){ throw UserSetupError(console, error); } - const std::vector actions = plan_naming_input(text, kind); +} +bool press_on_naming_screen( + ConsoleHandle& console, ProControllerContext& context, + const std::string& text, NamingScreenKind kind, const std::vector& actions +){ NamingScreenWatcher naming_screen(COLOR_RED); context.wait_for_all_requests(); if (wait_until(console, context, std::chrono::seconds(5), {naming_screen}) < 0){ @@ -83,6 +81,25 @@ bool type_name( return true; } +} + + +bool type_name( + ConsoleHandle& console, ProControllerContext& context, + const std::string& text, NamingScreenKind kind +){ + check_name(console, text, kind); + return press_on_naming_screen(console, context, text, kind, plan_naming_input(text, kind)); +} + +bool type_name_without_confirming( + ConsoleHandle& console, ProControllerContext& context, + const std::string& text, NamingScreenKind kind +){ + check_name(console, text, kind); + return press_on_naming_screen(console, context, text, kind, plan_naming_keys(text, kind)); +} + } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h index 2da50003ce..65acefc8f4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h @@ -29,6 +29,13 @@ bool type_name( const std::string& text, NamingScreenKind kind ); +// Like type_name(), but leaves the name on screen with OK not pressed, for a program that +// confirms the name itself (SID Helper presses PLUS then A on its own timing). +bool type_name_without_confirming( + ConsoleHandle& console, ProControllerContext& context, + const std::string& text, NamingScreenKind kind +); + } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp index 35b0b737ee..c148871808 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp @@ -311,7 +311,7 @@ std::string naming_text_error(const std::string& text, NamingScreenKind kind){ return ""; } -std::vector plan_naming_input( +std::vector plan_naming_keys( const std::string& text, NamingScreenKind kind, const NamingCursor& start ){ const std::string error = naming_text_error(text, kind); @@ -327,6 +327,13 @@ std::vector plan_naming_input( cursor = append_moves_to_key(cursor, key, actions); actions.emplace_back(NamingAction::A); } + return actions; +} + +std::vector plan_naming_input( + const std::string& text, NamingScreenKind kind, const NamingCursor& start +){ + std::vector actions = plan_naming_keys(text, kind, start); // When the last key fills the name, the game moves the cursor to OK by itself; START is // then harmless. actions.emplace_back(NamingAction::START); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h index f362f276bc..496c46df63 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h @@ -119,9 +119,15 @@ NamingCursor apply_naming_action(const NamingCursor& cursor, NamingAction action // no key, more characters than fit, or nothing but spaces (the game then keeps no name). std::string naming_text_error(const std::string& text, NamingScreenKind kind); -// The presses that type `text` from `start` and confirm it: the shortest moves to each -// character's key, A, and at the end START and A to press OK. +// The presses that type `text` from `start` without confirming it: the shortest moves to +// each character's key, then A. The name stays on screen with OK not pressed, which is where +// SID Helper starts. // Throws InternalProgramError if naming_text_error() rejects `text`; check that first. +std::vector plan_naming_keys( + const std::string& text, NamingScreenKind kind, const NamingCursor& start = NamingCursor() +); + +// plan_naming_keys(), then START and A to press OK. std::vector plan_naming_input( const std::string& text, NamingScreenKind kind, const NamingCursor& start = NamingCursor() ); From 651cad219326efb86b1902de5b57e59e0ba57737 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 13:22:09 -0800 Subject: [PATCH 3/5] FRLG AutoStory: type the player's name in the new game's intro A PLAYER_NAME option (default "Kosmos") is typed on the player's keyboard in 00.01 with the naming screen's type_name(). Empty keeps the default name, as before. The rival's keyboard still confirms an empty name. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStory.cpp | 25 ++++++++++++ .../AutoStory/PokemonFRLG_AutoStory.h | 2 + .../PokemonFRLG_AutoStoryActions.cpp | 38 ++++++++++++++----- .../AutoStory/PokemonFRLG_AutoStoryActions.h | 9 +++-- .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 2 +- .../AutoStory/PokemonFRLG_AutoStoryEvents.h | 3 ++ .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 2 +- 7 files changed, 66 insertions(+), 15 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 192ade4046..4f7e55d34c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -13,6 +13,7 @@ #include "Pokemon/AutoStory/Pokemon_StorySegments.h" #include "Pokemon/Pokemon_Strings.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" +#include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryTable.h" #include "PokemonFRLG_AutoStory.h" @@ -59,6 +60,13 @@ AutoStory::AutoStory() LockMode::LOCK_WHILE_RUNNING, true ) + , PLAYER_NAME( + false, + "Player name:
Typed on the new game's keyboard (00.01). Empty keeps the game's default name. " + "The rival always keeps the default name.", + LockMode::LOCK_WHILE_RUNNING, + "Kosmos", "" + ) , NOTIFICATIONS({ &NOTIFICATION_PROGRAM_FINISH, &NOTIFICATION_ERROR_FATAL, @@ -66,6 +74,7 @@ AutoStory::AutoStory() { PA_ADD_OPTION(RUN); PA_ADD_OPTION(NEW_GAME_FROM_TITLE); + PA_ADD_OPTION(PLAYER_NAME); PA_ADD_OPTION(NOTIFICATIONS); } @@ -101,6 +110,17 @@ static void check_save_place( namespace{ +// Before the new game starts: a name the keyboard can't type is the user's to fix. +void check_player_name(ConsoleHandle& console, const std::string& name){ + if (name.empty()){ + return; + } + const std::string error = naming_text_error(name, NamingScreenKind::PLAYER); + if (!error.empty()){ + throw UserSetupError(console, "Player name: " + error); + } +} + // A failed attempt reloads the last save, with the supplies it had; the // engine's lines go to the console's log. Pokemon::SegmentHooks story_hooks(SingleSwitchProgramEnvironment& env, StoryEnv& story){ @@ -140,8 +160,13 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex if (first == 0 && !NEW_GAME_FROM_TITLE){ throw UserSetupError(env.console, "Checkpoint 00.01 starts a new game: start on the title screen with no save."); } + const std::string player_name = PLAYER_NAME; + if (first == 0){ + check_player_name(env.console, player_name); + } 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); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h index e92eb74549..e349fc9d9d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h @@ -14,6 +14,7 @@ #include "Common/Cpp/Options/BooleanCheckBoxOption.h" #include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/StringOption.h" #include "CommonFramework/Notifications/EventNotificationsTable.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "Pokemon/Options/Pokemon_StoryRunOptions.h" @@ -45,6 +46,7 @@ class AutoStory : public SingleSwitchProgramInstance{ // every game's AutoStory. Pokemon::StoryRunOptions RUN; BooleanCheckBoxOption NEW_GAME_FROM_TITLE; + StringOption PLAYER_NAME; 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 a45ea62dfc..6de8818900 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp @@ -25,6 +25,7 @@ #include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.h" #include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_SelectionArrowDetector.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" +#include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h" #include "PokemonFRLG_AutoStoryActions.h" @@ -132,13 +133,16 @@ void open_start_menu_row(ConsoleHandle& console, ProControllerContext& context, // goes back to the controls pages (console B, 2026-11-01). The controls // pages read as a message box without an arrow, and the arrow at the end // of Oak's lines may be missed, so a message box gets A every 1.5 s -// whatever it shows. On the keyboard, B deletes whatever a stray A typed -// and START jumps to OK, which confirms the empty name as the default. +// whatever it shows. On the keyboard, B deletes whatever a stray A typed; +// then the player's name is typed, or START jumps to OK, which confirms +// the empty name as the default. The first keyboard is the player's, the +// second the rival's, who always keeps the default name. class NewGameIntro{ public: - NewGameIntro(ConsoleHandle& console, ProControllerContext& context) + NewGameIntro(ConsoleHandle& console, ProControllerContext& context, std::string player_name) : m_console(console) , m_context(context) + , m_player_name(std::move(player_name)) {} void run(){ @@ -188,7 +192,7 @@ class NewGameIntro{ return false; } if (NamingScreenDetector(COLOR_RED).detect(screen)){ - enter_empty_name(); + enter_name(); m_last_dialog = current_time(); return false; } @@ -227,18 +231,32 @@ class NewGameIntro{ return false; } - void enter_empty_name(){ - m_console.log("New game: the keyboard, confirming an empty name."); + void enter_name(){ + const bool player = m_keyboards_seen++ == 0; for (int c = 0; c < 8; c++){ press(m_context, BUTTON_B, 340ms); } - press(m_context, BUTTON_PLUS, 500ms); - press(m_context, BUTTON_A, 2000ms); + if (!player || m_player_name.empty()){ + m_console.log("New game: the keyboard, confirming an empty name."); + press(m_context, BUTTON_PLUS, 500ms); + press(m_context, BUTTON_A, 2000ms); + m_context.wait_for_all_requests(); + return; + } + if (!type_name(m_console, m_context, m_player_name, NamingScreenKind::PLAYER)){ + 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; + size_t m_keyboards_seen = 0; WallClock m_last_dialog; WallClock m_last_press; bool m_dialog_seen = false; @@ -509,9 +527,9 @@ void buy_items( mash_until_overworld(console, context, 20000ms); } -void play_new_game_intro(ConsoleHandle& console, ProControllerContext& context){ +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).run(); + NewGameIntro(console, context, player_name).run(); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h index c72d13942c..77c699a90a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h @@ -128,9 +128,12 @@ void buy_items( ); // From the title screen of a game with no save, to the bedroom: the -// controls pages, Oak's speech, BOY, and the default names for the player -// and the rival (an empty name confirmed). Returns on the overworld. -void play_new_game_intro(ConsoleHandle& console, ProControllerContext& context); +// controls pages, Oak's speech, BOY, `player_name` typed on the player's +// keyboard, and the default name for the rival (an empty name confirmed). +// An empty `player_name` keeps the player's default name too. Returns on +// the overworld. Throws UserSetupError if the game can't take the name +// (naming_text_error()). +void play_new_game_intro(ConsoleHandle& console, ProControllerContext& context, const std::string& player_name); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index 9c416d9850..5860ca649b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -344,7 +344,7 @@ Outcome StoryExecutor::run(RunState& state, const StoryStep& step){ } Outcome StoryExecutor::new_game(RunState&, const StoryStep&){ - play_new_game_intro(m_env.console, m_env.context); + play_new_game_intro(m_env.console, m_env.context, m_env.player_name); return Pokemon::outcome_ok(); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h index da3bc0eff7..f4cb9095a2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -112,6 +112,9 @@ struct StoryEnv{ // The game's language, for reading item names. In one the readers have // no names for, the checks fall back to positions and counts. Language language = Language::English; + // The name the new game's intro types for the player; empty keeps the + // game's default name. + std::string player_name; }; // Does FRLG's kinds of story step on the console. A console step that diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index d94a59d66b..856ad57476 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -137,7 +137,7 @@ StoryStep shop(std::string name, std::vector items, std::str StoryCheckpoint new_game(){ return 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, default names, rival GREEN"), + 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"), }); } From 75ea90fd62eb111302ebbae47f31bb1c9534bd47 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:34:46 -0800 Subject: [PATCH 4/5] FRLG AutoStory: give StoryEnv's player_name a default Windows Clang fails on -Wmissing-field-initializers where AutoStory::program() builds StoryEnv without player_name, which it sets right after. Behavior is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h index 36e425d705..c6f72c2c70 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -119,7 +119,7 @@ struct StoryEnv{ Language language = Language::English; // The name the new game's intro types for the player; empty keeps the // game's default name. - std::string player_name; + std::string player_name = {}; // The map the checkpoint under way starts on, where its save is: a // reload checks the save card against it. std::string start_map = {}; From ca6321a23ca6ee2b35c4d8e51ba9166be8415e7b Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 09:21:07 -0800 Subject: [PATCH 5/5] FRLG AutoStory: check the options in check_setup(), keeping program() within code health's 50 lines Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStory.cpp | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 591fa3ba26..df4bdb0714 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -196,6 +196,22 @@ void verify_story_checkpoint( } +// The options' problems, before anything is pressed. +static void check_setup( + ConsoleHandle& console, const std::vector& checkpoints, size_t first, size_t last, + bool new_game_from_title, const std::string& player_name +){ + if (first > last || last >= checkpoints.size()){ + throw UserSetupError(console, "The start checkpoint must not come after the end checkpoint."); + } + if (first == 0 && !new_game_from_title){ + throw UserSetupError(console, "Checkpoint 00.01 starts a new game: start on the title screen with no save."); + } + if (first == 0){ + check_player_name(console, player_name); + } +} + 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); @@ -204,16 +220,8 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex const std::vector& checkpoints = story_checkpoints(); const size_t first = RUN.start_index(); const size_t last = RUN.end_index(); - if (first > last || last >= checkpoints.size()){ - throw UserSetupError(env.console, "The start checkpoint must not come after the end checkpoint."); - } - if (first == 0 && !NEW_GAME_FROM_TITLE){ - throw UserSetupError(env.console, "Checkpoint 00.01 starts a new game: start on the title screen with no save."); - } const std::string player_name = PLAYER_NAME; - if (first == 0){ - check_player_name(env.console, player_name); - } + check_setup(env.console, checkpoints, first, last, NEW_GAME_FROM_TITLE, player_name); StoryEnv story{env.console, context, checkpoints[first].supplies, checkpoints[first].supplies, false, RUN.checks()}; story.player_name = player_name;