diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 1973dbe3e4..121bcd6077 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" @@ -37,6 +38,7 @@ #include "Overworld/PokemonFRLG_OverworldRoute.h" #include "Overworld/PokemonFRLG_OverworldWalk.h" #include "Overworld/PokemonFRLG_PlayerFacingReader.h" +#include "Programs/PokemonFRLG_NamingScreenKeyboard.h" #include "PokemonFRLG_Navigation.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryTable.h" @@ -272,6 +274,394 @@ 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 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 = ""; + 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); + } +} + } @@ -319,6 +709,10 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory(database); add_tests_AutoStorySimulation(database); add_tests_AutoStoryItems(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/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index fb400f1e09..df4bdb0714 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -14,6 +14,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" @@ -60,6 +61,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, @@ -67,6 +75,7 @@ AutoStory::AutoStory() { PA_ADD_OPTION(RUN); PA_ADD_OPTION(NEW_GAME_FROM_TITLE); + PA_ADD_OPTION(PLAYER_NAME); PA_ADD_OPTION(NOTIFICATIONS); } @@ -89,6 +98,17 @@ static void check_start_save( 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, read // from its bag again; the engine's lines go to the console's log. Pokemon::SegmentHooks story_hooks(SingleSwitchProgramEnvironment& env, StoryEnv& story){ @@ -176,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); @@ -184,14 +220,11 @@ 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; + 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; 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 a67e107e31..debf1311a6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp @@ -26,6 +26,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" @@ -168,18 +169,20 @@ void open_start_menu_row(ConsoleHandle& console, ProControllerContext& context, // 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. The player's name // is asked on the keyboard straight away: B deletes whatever a stray A -// typed and START jumps to OK, which confirms the empty name as the -// default. The rival's is asked on a list with NEW NAME first, where A -// would open the keyboard: the cursor goes to the first preset name (the -// game's default rival, GREEN in FireRed, RED in LeafGreen) and A picks -// it, then YES confirms it (pokefirered oak_speech.c). If the keyboard -// opens anyway, the list was missed, and the empty name is confirmed -// there instead, which also names the rival GREEN. +// typed, then the name is typed, or START jumps to OK, which confirms the +// empty name as the default. The rival's is asked on a list with NEW NAME +// first, where A would open the keyboard: the cursor goes to the first +// preset name (the game's default rival, GREEN in FireRed, RED in +// LeafGreen) and A picks it, then YES confirms it (pokefirered +// oak_speech.c). If the keyboard opens anyway, the list was missed, and +// the empty name is confirmed there instead, which also names the rival +// GREEN. 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(){ @@ -262,12 +265,12 @@ class NewGameIntro{ switch (intro_name_step(shown, m_keyboards_seen, m_presets_picked)){ case IntroNameStep::PLAYER_KEYBOARD: m_keyboards_seen++; - enter_empty_name(); + enter_name(true); return true; case IntroNameStep::RIVAL_KEYBOARD: m_keyboards_seen++; m_console.log("New game: the rival's keyboard opened, so the name list was missed.", COLOR_ORANGE); - enter_empty_name(); + enter_name(false); return true; case IntroNameStep::RIVAL_PRESET: pick_rival_preset(*list_row); @@ -317,18 +320,30 @@ class NewGameIntro{ return false; } - void enter_empty_name(){ - m_console.log("New game: the keyboard, confirming an empty name."); + void enter_name(bool player){ 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; WallClock m_last_dialog; WallClock m_last_press; bool m_dialog_seen = false; @@ -834,9 +849,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 78d88a0d14..2850e1576c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h @@ -212,10 +212,12 @@ void buy_items( ); // From the title screen of a game with no save, to the bedroom: the -// controls pages, Oak's speech, BOY, the default name for the player (an -// empty name confirmed) and the rival's first preset name. 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 rival's first preset name. An empty `player_name` +// keeps the player's default name (an empty name confirmed). 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 4c205cbb49..f575125826 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -389,7 +389,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 887ca363b6..e8e5817f76 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -117,6 +117,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 = {}; // The map the checkpoint under way starts on, where its save is: a // reload checks the save card against it. std::string start_map = {}; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 7db4cec429..1bc03b8b6c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -149,7 +149,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"), }); } 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..974acd42c9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.cpp @@ -0,0 +1,106 @@ +/* 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; + } +} + +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); + } +} + +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){ + 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; +} + +} + + +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 new file mode 100644 index 0000000000..65acefc8f4 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreen.h @@ -0,0 +1,43 @@ +/* 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 +); + +// 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 +); + + +} +} +} +#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..c148871808 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.cpp @@ -0,0 +1,348 @@ +/* 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_keys( + 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); + } + 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); + 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..496c46df63 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h @@ -0,0 +1,140 @@ +/* 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` 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() +); + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index c99a56150f..905c7a5d90 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1881,6 +1881,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