diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp index e2aa07801f..21c0c74c6c 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp @@ -7,6 +7,8 @@ #include "CommonFramework/StaticGlobals.h" #include "CommonFramework/GlobalAutoPaths.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h" + #include "PokemonBDSP_Panels.h" #include "PokemonBDSP_Settings.h" @@ -57,6 +59,25 @@ PanelListFactory::PanelListFactory() ) {} +namespace{ + +// The programs shown only in beta builds or developer mode. +void add_beta_and_developer_panels(std::vector& ret){ + if (IS_BETA_VERSION || STATIC_GLOBALS.DEVELOPER_MODE){ + ret.emplace_back("---- Untested/Beta/WIP ----"); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back(make_SingleSwitchProgram()); + } + if (STATIC_GLOBALS.DEVELOPER_MODE){ + ret.emplace_back("---- Developer Tools ----"); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back(make_SingleSwitchProgram()); + } +} + +} + std::vector PanelListFactory::make_panels() const{ std::vector ret; @@ -91,6 +112,9 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back("---- Auto Story ----"); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back("---- Glitches (v1.1.3) ----"); ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back(make_SingleSwitchProgram()); @@ -98,17 +122,7 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back("---- Glitches (v1.1.2) ----"); ret.emplace_back(make_SingleSwitchProgram()); - if (IS_BETA_VERSION || STATIC_GLOBALS.DEVELOPER_MODE){ - ret.emplace_back("---- Untested/Beta/WIP ----"); - ret.emplace_back(make_SingleSwitchProgram()); - ret.emplace_back(make_SingleSwitchProgram()); - } - if (STATIC_GLOBALS.DEVELOPER_MODE){ - ret.emplace_back("---- Developer Tools ----"); - ret.emplace_back(make_SingleSwitchProgram()); - ret.emplace_back(make_SingleSwitchProgram()); - ret.emplace_back(make_SingleSwitchProgram()); - } + add_beta_and_developer_panels(ret); return ret; } diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index a53516ad5f..c4f3752ab1 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -12,6 +12,8 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h" #include "PokemonBDSP_Tests.h" @@ -28,6 +30,8 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_RoutePlanner(database); add_tests_AutoStory_StoryMaps(database); add_tests_AutoStory_Segment00(database); + add_tests_AutoStory_StorySimulation(database); + add_tests_AutoStory_StoryTable(database); add_tests_AutoStory_Walk(database); add_tests_AutoStory_PlannedRoute(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp index ce7b7bc124..386089d180 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp @@ -29,13 +29,14 @@ RobustPlanCosts waypoint_plan_costs(){ } RobustPlanCosts town_plan_costs(){ + // Open legs go on the D-pad now, a whole tile a tap, so they land exactly + // at any length ("south 5" went 6 in r134a on stick holds). A wall at a + // leg's end is still worth a little: r64 went 38 tiles round the town + // to end each leg on one, for 12 straight. RobustPlanCosts costs; - // A tile's hold moves exactly a tile; a longer open leg may end a tile - // either way ("south 5" went 6 in r134a), so the plan ends on a wall. - costs.exact_open_tiles = 1; - costs.open_error_per = 8; - costs.leg_not_at_wall = 6; - costs.per_open_tile = 2; + costs.exact_open_tiles = costs.max_open_tiles; + costs.per_open_tile = 1; + costs.leg_not_at_wall = 4; return costs; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp index 9736436658..e0ee6d0579 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp @@ -32,8 +32,10 @@ std::vector checkpoint_0001_records(){ }, "Segment00Lines line 2, not walked by the program yet"}, {"00.01", "Route 201: Barry's tall-grass warning, then west to the Lakefront", "Route 201", "Route201", true, map_point("Route201", "route_201_arrival"), { {Direction::NORTH, 6, LegEnd::COUNT, "up the exit path to (15, 18): Barry's warning fires on that row"}, - {Direction::WEST, 3, LegEnd::COUNT, "after the scene, along row 18 to column 12"}, - {Direction::NORTH, 2, LegEnd::COUNT, "to row 16, the lower level's row to the Lakefront"}, + {Direction::WEST, 1, LegEnd::COUNT, "after the scene, to (14, 18); west of it is the 201 sign and trees (r16)"}, + {Direction::NORTH, 1, LegEnd::COUNT, "onto row 17, the top of the exit path's T"}, + {Direction::WEST, 2, LegEnd::COUNT, "along row 17 to column 12"}, + {Direction::NORTH, 1, LegEnd::COUNT, "to row 16, the lower level's row to the Lakefront"}, {Direction::WEST, 12, LegEnd::COUNT, "to (0, 16), the route's last tile"}, {Direction::WEST, 0, LegEnd::BANNER, "on west onto the Verity Lakefront"}, }, "Segment00Lines lines 2 and 3, not walked by the program yet; traced on the Route201 map, the decomp's since #111"}, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp index 70b9d9bac1..251c399195 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp @@ -124,7 +124,7 @@ const std::vector& lines(){ twinleaf_point("barry_doorstep"), twinleaf({15, 12}), twinleaf_point("twinleaf_north_exit"), route201_point("barry_warning"), }, "Barry's tall-grass warning", {"Barry's tall-grass warning"}), walked("00.01", "Route 201's west half to the lake's entrance", { - route201_point("barry_warning"), route201({12, 18}), route201({12, 16}), route201({0, 16}), + route201_point("barry_warning"), route201({14, 18}), route201({14, 17}), route201({12, 17}), route201({12, 16}), route201({0, 16}), lakefront({20, 23}), lakefront({20, 21}), lakefront({17, 21}), lakefront_point("lake_entrance"), }, "Barry at the lake's entrance", {"Barry at the lake's entrance", "Route 201's row 19 after the warning"}), walked("00.03", "the lake's entrance back to Rowan", { diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json index 0043b0bea2..5df7f3b6d4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json @@ -61,7 +61,7 @@ "#####################################..####.#S....###.....######", "###########################################.......####.......###", "############################################.....#####.......###", - "############################################.....#####.....#####", + "############################################DDDDD#####.....#####", "######################################################..#..#####", "################################################################", "################################################################", diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghMineB1F.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghMineB1F.json new file mode 100644 index 0000000000..1d564db27c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghMineB1F.json @@ -0,0 +1,43 @@ +{ + "name": "OreburghMineB1F", + "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0313.bin; zone_event narc_0196", "revision": "default branch", "date": "2026-10-31"}, + "tiles": [ + "################################", + "###########DDD##################", + "######...##...##.###############", + "######...##...##.###############", + "####.................###########", + "####.................###########", + "####.................###########", + "####.................###########", + "####.................###########", + "####.................###########", + "######.............#############", + "######.............#############", + "######.............#############", + "######.............#############", + "########.........###############", + "########.........###############", + "########.........###############", + "########.........###############", + "##########.....#################", + "##########.....#################", + "##########.....#################", + "##########.....#################", + "############D###################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################" + ], + "objects": [ + {"name": "worker_0", "kind": "npc", "x": 7, "y": 4}, + {"name": "item_1", "kind": "item", "x": 19, "y": 4}, + {"name": "twin_2", "kind": "npc", "x": 6, "y": 3} + ] +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp new file mode 100644 index 0000000000..6bdcdcb259 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -0,0 +1,271 @@ +/* BDSP AutoStory + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#include +#include +#include +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" +#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" +#include "Pokemon/AutoStory/Pokemon_AutoStoryStats.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +BDSPAutoStory_Descriptor::BDSPAutoStory_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonBDSP:AutoStory", + "Pokemon BDSP", "Auto Story", + "Programs/PokemonBDSP/AutoStory.html", + "Automatically completes BDSP story segments.", + PanelQualityStanding::OFFICIAL_BETA, + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS, + {"Tesseract"} + ) +{} + +std::unique_ptr BDSPAutoStory_Descriptor::make_stats() const{ + return std::make_unique(); +} + + +namespace{ + +// The start and end dropdowns' entries: segments 00 and 01 joined, 00.00 +// to 01.11. The options hold a reference to it. +const StringSelectDatabase& checkpoint_database(){ + static const StringSelectDatabase database = Pokemon::story_checkpoint_database(story_checkpoints()); + return database; +} + +std::set rows_that_stop(const std::vector& rows){ + std::set ids; + for (const StoryCheckpoint& row : rows){ + if (row.on_failure == StoryFailure::STOP){ + ids.insert(row.id); + } + } + return ids; +} + +// A failed attempt resets the game from HOME, which loads the save the +// row made at its start; the engine's lines go to the console's log. +Pokemon::SegmentHooks story_hooks(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + Pokemon::SegmentHooks hooks; + hooks.save = [](Pokemon::RunState&){ + return Pokemon::outcome_failed("no_save_hook", "story rows save at their start"); + }; + hooks.reload = [&env, &context](Pokemon::RunState&){ + env.console.log("Resetting game from HOME before retry...", COLOR_ORANGE); + if (!reset_game_from_home(env, env.console, context, true, 1000ms)){ + return Pokemon::outcome_failed("reload_failed", "Failed to reset the game from HOME."); + } + return Pokemon::outcome_ok(); + }; + hooks.log = [&env](const std::string& line){ + env.log(line, COLOR_BLUE); + }; + hooks.status = [&env](const std::string& line){ + env.console.overlay().add_log(line); + }; + return hooks; +} + +// Leave the Switch in a known state after a failure: on HOME with the +// game closed, so the next run (or the user) starts from there. +void close_game_after_failure(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("AutoStory failed: going to HOME and closing the game.", COLOR_RED); + try{ + go_home(env.console, context); + close_game_from_home(env.console, context); + }catch (OperationFailedException& e){ + env.log("Could not close the game: " + e.message(), COLOR_RED); + } +} + +// A run that stopped short throws, closing the game first unless it +// stopped at the first failure as asked. +void throw_if_stopped(SingleSwitchProgramEnvironment& env, ProControllerContext& context, const Pokemon::SegmentReport& report){ + if (report.completed){ + return; + } + if (!report.stopped_at_first_failure){ + close_game_after_failure(env, context); + } + throw OperationFailedException( + ErrorReportMode::NO_ERROR_REPORT, + std::format( + "AutoStory stopped at {} {}: {}", + report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure) + ) + ); +} + +// Verify mode's checkpoint and runs, and whether the game starts as a new +// game (refused: no save to reload) or stays open after a failure. +struct VerifyRequest{ + std::string id; + Pokemon::VerifyPolicy policy; + bool new_game = false; + bool keep_game = false; +}; + +// Verify mode: the checkpoint run as the request's policy says. Rows save +// at their start, never at their end, so the reset from HOME between runs +// loads the save the first run started from. Throws unless enough runs +// pass, closing the game first as after any failure unless `keep_game`. +void verify_story_checkpoint( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, Pokemon::RunState& state, + Pokemon::Segment& segment, Pokemon::SegmentHooks& hooks, const VerifyRequest& request +){ + if (request.new_game){ + throw UserSetupError(env.console, "Verify mode reloads the start's save between runs: turn off \"Start a new game first\"."); + } + const std::string& id = request.id; + const Pokemon::VerifyPolicy& policy = request.policy; + const bool keep_game = request.keep_game; + Pokemon::VerifyReport report; + try{ + report = Pokemon::verify_checkpoint(state, segment, id, policy, hooks); + }catch (OperationFailedException&){ + if (!keep_game){ + close_game_after_failure(env, context); + } + throw; + } + const std::string summary = std::format( + "AutoStory verify: {} {}/{} passed, {} needed.", id, report.passes, policy.runs, policy.passes_needed + ); + if (!report.verified){ + if (!keep_game){ + close_game_after_failure(env, context); + } + throw OperationFailedException(ErrorReportMode::NO_ERROR_REPORT, summary); + } + env.console.log(summary); +} + +} + + +BDSPAutoStory::BDSPAutoStory() + : RUN(checkpoint_database()) + , NEW_GAME_INTRO( + "Start a new game first:
" + "For a game with no save: restart it and play the intro (language, appearance, names) " + "until the player is in control in the bedroom. Only when starting at 00.00.", + LockMode::LOCK_WHILE_RUNNING, false + ) + , JUST_LOADED( + "Just loaded the start's save:
" + "The game was just loaded from the start checkpoint's save, so its save at the start is skipped (about 10 s). " + "Leave off when the game has moved since its last save.", + LockMode::LOCK_WHILE_RUNNING, false + ) + , PLAYER_NAME(false, "Player name:
For a new game. Letters and digits.", LockMode::LOCK_WHILE_RUNNING, "Kosmos", "Kosmos") + , STARTER( + "Starter Pokemon:", + { + {StarterChoice::TURTWIG, "turtwig", "Turtwig"}, + {StarterChoice::CHIMCHAR, "chimchar", "Chimchar"}, + {StarterChoice::PIPLUP, "piplup", "Piplup"}, + }, + LockMode::LOCK_WHILE_RUNNING, + StarterChoice::CHIMCHAR + ) + , NOTIFICATIONS({ + &NOTIFICATION_PROGRAM_FINISH, + &NOTIFICATION_ERROR_RECOVERABLE, + &NOTIFICATION_ERROR_FATAL, + }) +{ + PA_ADD_OPTION(RUN); + PA_ADD_OPTION(NEW_GAME_INTRO); + PA_ADD_OPTION(JUST_LOADED); + PA_ADD_OPTION(PLAYER_NAME); + PA_ADD_OPTION(STARTER); + PA_ADD_OPTION(NOTIFICATIONS); +} + +namespace{ + +// The checkpoints whose start is already saved: the first one when the game +// was just loaded from its save (saving again cost ~10 s). After play, the +// save is older and the first checkpoint must save its start. +std::set saved_before_the_run(const std::vector& checkpoints, size_t first, bool just_loaded){ + if (!just_loaded){ + return {}; + } + return {checkpoints[first].id}; +} + +} + +void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + 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."); + } + // The reads that use text (Location.h) take the game's language from here. + set_story_language(RUN.language()); + + StoryEnv story{env, context, STARTER, PLAYER_NAME}; + StoryExecutor executor(story); + executor.set_checks(RUN.checks()); + executor.set_language(RUN.language()); + executor.set_stop_rows(rows_that_stop(checkpoints)); + std::set saved = saved_before_the_run(checkpoints, first, JUST_LOADED && !(first == 0 && NEW_GAME_INTRO)); + Pokemon::Segment segment = make_story_segment(checkpoints, executor, saved); + Pokemon::SegmentHooks hooks = story_hooks(env, context); + + Pokemon::RunState state; + if (!(first == 0 && NEW_GAME_INTRO)){ + state.flags.insert("save_loaded"); + } + if (const std::optional verify = RUN.VERIFY.policy()){ + const VerifyRequest request{checkpoints[first].id, *verify, first == 0 && NEW_GAME_INTRO, RUN.STOP_AT_FIRST_FAILURE}; + verify_story_checkpoint(env, context, state, segment, hooks, request); + send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); + return; + } + Pokemon::SegmentReport report; + try{ + report = run_segment(state, segment, Pokemon::SegmentRange{checkpoints[first].id, checkpoints[last].id}, RUN.policy(), hooks); + }catch (OperationFailedException&){ + if (!RUN.STOP_AT_FIRST_FAILURE){ + close_game_after_failure(env, context); + } + throw; + } + Pokemon::AutoStoryStats& stats = env.current_stats(); + Pokemon::record_story_stats(stats, report); + env.update_stats(); + throw_if_stopped(env, context, report); + env.console.log("AutoStory: done through " + checkpoints[last].id + "."); + send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h new file mode 100644 index 0000000000..36fc8ea581 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h @@ -0,0 +1,60 @@ +/* BDSP AutoStory + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * Plays Brilliant Diamond / Shining Pearl from a new game to Floaroma Town: + * the story table (PokemonBDSP_AutoStory_StoryTable.h) run by the shared + * story engine, from the start checkpoint to the end one. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_H +#define PokemonAutomation_PokemonBDSP_AutoStory_H + +#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" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class BDSPAutoStory_Descriptor : public SingleSwitchProgramDescriptor{ +public: + BDSPAutoStory_Descriptor(); + + virtual std::unique_ptr make_stats() const override; +}; + + +class BDSPAutoStory : public SingleSwitchProgramInstance{ +public: + using Descriptor = BDSPAutoStory_Descriptor; + BDSPAutoStory(); + + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + // The start and end checkpoints, attempts, checks and language. + Pokemon::StoryRunOptions RUN; + BooleanCheckBoxOption NEW_GAME_INTRO; + BooleanCheckBoxOption JUST_LOADED; + StringOption PLAYER_NAME; + EnumDropdownOption STARTER; + + EventNotificationsOption NOTIFICATIONS; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp new file mode 100644 index 0000000000..0037449ea4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp @@ -0,0 +1,39 @@ +/* BDSP AutoStory - Events + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +const StoryEvents& story_events(){ + static const StoryEvents events = []{ + StoryEvents ret; + add_lake_events(ret); + add_sandgem_events(ret); + add_jubilife_events(ret); + add_oreburgh_events(ret); + add_route204_events(ret); + return ret; + }(); + return events; +} + +StoryEvent console_event(std::function action){ + return [action = std::move(action)](StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep&){ + action(executor.stream(), executor.context()); + return Pokemon::outcome_ok(); + }; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h new file mode 100644 index 0000000000..eb34744814 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -0,0 +1,58 @@ +/* BDSP AutoStory - Events + * + * From: https://github.com/PokemonAutomation/ + * + * The story's steps that only BDSP has, by name: a step of the table whose + * variant names one of these runs it (StoryExecutor::run()). Some are the + * scripted scenes (the Azure Flute, the lake's briefcase, the starter, + * Dawn's catching tutorial and the catch); the rest do what the shared + * steps cannot, as they ran on the console: the Poke Mart lady's Potions + * and the stairs into Jubilife (00.04c, 00.04d), a clown who wanders + * (00.05b), the Gym's tries from the Pokemon Center (01.04) and Rock Smash + * (01.09). Each fails by throwing, with a screenshot; the executor turns + * that into a failed step. + * + * They are in files by area: _Events_Lake (00.00 to 00.04), _Sandgem + * (00.04c, 00.04d), _Jubilife (00.05b), _Oreburgh (01.04) and _Route204 + * (01.09). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Events_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Events_H + +#include +#include +#include +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Runs a step on the console. Returns where it left the player when it +// knows; throws OperationFailedException when it fails. +using StoryEvent = std::function; +using StoryEvents = std::map; + +// Every event of the story, by name. +const StoryEvents& story_events(); + +// An event that only acts and says nothing of where it leaves us. +StoryEvent console_event(std::function action); + +void add_lake_events(StoryEvents& events); +void add_sandgem_events(StoryEvents& events); +void add_jubilife_events(StoryEvents& events); +void add_oreburgh_events(StoryEvents& events); +void add_route204_events(StoryEvents& events); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp new file mode 100644 index 0000000000..af124bdb90 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -0,0 +1,96 @@ +/* BDSP AutoStory - Events: Jubilife City (00.05b) + * + * From: https://github.com/PokemonAutomation/ + * + * The Jubilife beat of 00.05b the story table cannot plan: the talk to a + * wandering clown. + * + */ + +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +namespace{ + +// 00.05b: a talk to someone who wanders a tile either side of home along a +// row (the clowns by the Mart and the Poketch Company, WANDER_AROUND with +// x range 1). Waiting under home alone missed him for a minute (r66: he +// stood a tile east), so walk the row below his range, a whole D-pad tile +// at a time, asking north from each tile. His question's answer is Yes, so +// A answers and takes the coupon; then back under his home tile. +bool ask_north(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::NORTH); + for (size_t ask = 0; ask < 2; ask++){ + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 1500ms, {{dialog}}) == 0){ + return true; + } + } + return false; +} +// One tile along the row, east (+1) or west (-1). +void step_along(VideoStream& stream, ProControllerContext& context, int east, const std::string& label){ + route_turn(stream, context, east > 0 ? Direction::EAST : Direction::WEST); + walk_tiles_dpad(stream, context, run_from_wild_battle, {east > 0 ? 1.0 : -1.0, 0}, 1, label); +} +Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep& step){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + const std::string label = "Along the row below " + step.name; + // Home, east, west, west, east: each move one tile on the D-pad. + const int moves[] = {0, 1, -1, -1, 1}; + int at = 0; + for (size_t round = 0; round < 6; round++){ + for (int move : moves){ + if (move != 0){ + step_along(stream, context, move, label); + at += move; + } + if (!ask_north(stream, context)){ + continue; + } + stream.log(std::format("[Talk] {}: answered in round {}, {} tiles east of home.", step.name, round + 1, at)); + finish_dialogue(stream, context, BUTTON_A, step.name); + for (; at != 0; at += at > 0 ? -1 : 1){ + step_along(stream, context, at > 0 ? -1 : 1, "Back below " + step.name + "'s home"); + } + route_turn(stream, context, Direction::NORTH); + return Pokemon::outcome_ok(); + } + } + fail_step(stream, step.name + ": no answer from any tile below his range."); +} + +} + + +void add_jubilife_events(StoryEvents& events){ + events.emplace("talk_to_wanderer", talk_to_wanderer); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp new file mode 100644 index 0000000000..0ecd9e3f77 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp @@ -0,0 +1,147 @@ +/* BDSP AutoStory - Events: the lake and the Pokedex (00.02 to 00.04) + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * The scenes of the story's start that need scripted input: the briefcase + * at Lake Verity, the starter, Mom's and Barry's mother's lines, and Dawn's + * catching tutorial. The button sequences are + * Butters' (#1717); they ran on + * the console in his recordings, but not yet from the planned walks around + * them. + * + */ + +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/ImageMatchDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP_AutoStory_Actions.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +// 00.02: the lake's scene, from Barry's first line by the entrance to the +// starters' briefcase: B until the briefcase shows, then until it is open. +void starter_briefcase(VideoStream& stream, ProControllerContext& context){ + wait_for_dialogue(stream, context, "Barry before the starter selection"); + pbf_mash_button(context, BUTTON_A, 1000ms); + pbf_wait(context, 500ms); + std::shared_ptr briefcase = std::make_shared( + RESOURCE_PATH() + "PokemonBDSP/StarterBriefcase.png" + ); + ImageMatchWatcher detector(briefcase, {0.5, 0.1, 0.5, 0.7}, 100, true); + const int ret = run_until( + stream, context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 120000ms); + }, + {{detector}} + ); + if (ret != 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Failed to detect the starters' briefcase.", stream); + } + stream.log("Detected briefcase! Mashing B for it to open."); + pbf_mash_button(context, BUTTON_B, 2000ms); +} + +// 00.02: the starter from the program's option, in the briefcase: Turtwig +// is first, Chimchar a press right, Piplup two; ZL picks, then Up and ZL +// answer "this one?" with Yes. +Pokemon::Outcome choose_starter(StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep&){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + size_t rights = 0; + switch (executor.env().starter){ + case StarterChoice::TURTWIG: + rights = 0; + break; + case StarterChoice::CHIMCHAR: + rights = 1; + break; + case StarterChoice::PIPLUP: + rights = 2; + break; + } + for (size_t i = 0; i < rights; i++){ + pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); + } + pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); + context.wait_for_all_requests(); + ShortDialogWatcher watcher(COLOR_CYAN); + int ret = wait_until(stream, context, 5000ms, {{watcher}}); + if (ret != 0){ + ShortDialogWatcher watcher_red(COLOR_RED); + ret = wait_until(stream, context, 5000ms, {{watcher_red}}); + } + stream.log(ret == 0 ? "[Starter] Confirmation dialogue detected." : "[Starter] Dialogue detection failed, proceeding anyway.", ret == 0 ? COLOR_BLACK : COLOR_ORANGE); + pbf_wait(context, 400ms); + pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); + pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); + context.wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + +// 00.01: BDSP puts an Azure Flute on the bedroom floor at (7, 4) when the console +// has Legends: Arceus save data, and nothing there otherwise. Facing that tile: +// A, and if text follows ("You found an Azure Flute!", then the pocket line), B +// through it. Without the ball the A does nothing and we go on. +void azure_flute_if_there(VideoStream& stream, ProControllerContext& context){ + // The route ends facing the way its last leg went (east, r15): a short tap + // north only turns us to face the tile. + pbf_press_dpad(context, DPAD_UP, 40ms, 400ms); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + for (int presses = 0; presses < 6; presses++){ + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) != 0){ + return; + } + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + } +} + + + +// 00.04: Dawn's catching tutorial on Route 202: her lines and her demo +// battle, B for 70 s. +void catch_tutorial(VideoStream& stream, ProControllerContext& context){ + wait_for_dialogue(stream, context, "Dawn's catching tutorial"); + pbf_mash_button(context, BUTTON_B, 70000ms); + context.wait_for_all_requests(); + pbf_wait(context, 2000ms); +} + +} + + +void add_lake_events(StoryEvents& events){ + events.emplace("azure_flute_if_there", console_event(azure_flute_if_there)); + events.emplace("starter_briefcase", console_event(starter_briefcase)); + events.emplace("choose_starter", choose_starter); + + + events.emplace("catch_tutorial", console_event(catch_tutorial)); + events.emplace("catch_pokemon", console_event(catch_1_pokemon)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp new file mode 100644 index 0000000000..c7c85493a2 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -0,0 +1,142 @@ +/* BDSP AutoStory - Events: Oreburgh Gym (01.04) + * + * From: https://github.com/PokemonAutomation/ + * + * 01.04's walk into the Oreburgh Gym and up it to Roark, again from the + * Pokemon Center after each loss, as it ran on the console (2026-10-29 to + * 10-31). A loss keeps the experience, which a reload of the row's save + * would not. + * + */ + +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +// Oreburgh, from the street in front of the Pokemon Center (its door or its +// front wall) into the gym, as walked by hand on 2026-10-29: west to the +// cliff, south to the posts, a row up, then 17 tiles east is the gym door's +// column. The hold is checked by the door itself: walk north, and if the +// gym's wall stops us, try the columns either side. +// D-pad moves from the Center's doorstep (as walk_out_of_center() leaves us +// there) to below the gym's door: 20. The program's own runs landed 2 tiles +// west with 22 and 1 tile west with 21 (r217d, r218d, the same tiles each +// time). By hand, after a stick exit, it took 22 taps; the hand exit and +// count did not carry over to the program's. Stick holds drift: 160 ms moved +// 36 to 48 px, so 18 or 19 of them missed the door by a part of a tile. +constexpr size_t GYM_DPAD_TAPS_WEST_OF_CENTER = 20; + +const uint64_t GYM_DARKER_THAN = 90; +void oreburgh_street_to_gym(VideoStream& stream, ProControllerContext& context){ + // From the Center's doorstep, straight west along its row to below the + // gym's door, then north through it, all on the D-pad (whole tiles; see + // GYM_DPAD_TAPS_WEST_OF_CENTER). A save inside the gym: already there. + // The gym's rock floor is darker than the street (mean brightness 75 + // against 104, r74), which indoors() does not tell apart. + context.wait_for_all_requests(); + if (in_dim_cave(stream, GYM_DARKER_THAN)){ + stream.log("[Route] 01.04: already in the gym."); + return; + } + dpad_tiles(stream, context, DPAD_LEFT, GYM_DPAD_TAPS_WEST_OF_CENTER, "west to the gym's door"); + if (dpad_through_door_north(stream, context, "Oreburgh Gym door")){ + return; + } + fail_step(stream, "Oreburgh: no column near the gym's door went in."); +} + +// Inside the Oreburgh Gym: straight up the middle, fighting the trainers +// who stop us, to Roark on the top tier. Returns whether Roark was beaten; +// false after a whiteout, which leaves us in the Pokemon Center. +// Walking straight up meets both gym trainers (one at the entrance, one on +// the second tier); their experience helps an under-levelled team, and +// after a whiteout they stay beaten. Butters' route goes round them. +bool oreburgh_gym_battles(VideoStream& stream, ProControllerContext& context){ + // The entry camera pans for about 7.5 s (Butters' reference video). + context.wait_for(8000ms); + if (dialogue_opens(stream, context, 3000ms)){ + if (talk_or_fight(stream, context, "Gym trainer at the entrance") && in_pokemon_center(stream)){ + return false; + } + } + size_t stuck = 0; + for (size_t leg = 0; leg < 10; leg++){ + const WalkResult up = walk_until_no_wild(stream, context, {0, 1}, 8000ms, "north up the gym"); + if (up.stop == WalkStop::DIALOG){ + if (talk_or_fight(stream, context, "Gym trainer") && in_pokemon_center(stream)){ + return false; + } + continue; + } + if (up.stop != WalkStop::BLOCKED){ + continue; + } + // Roark stands at the top facing us; a wall or a stair edge says + // nothing to A. Only Roark starts a battle from our A here. The walk + // left us facing north: a turn tap that way would be a step. + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + if (dialogue_opens(stream, context, 3000ms)){ + // His lines pause for more than 4 s before the battle (r210d: "only + // talked"), so the battle is expected. + if (talk_or_fight(stream, context, "Roark", TalkExpect::BATTLE)){ + if (in_pokemon_center(stream)){ + return false; + } + finish_dialogue(stream, context, BUTTON_B, "Roark after the battle"); + return true; + } + continue; + } + if (up.walked <= 800ms && ++stuck >= 3){ + fail_step(stream, "Oreburgh Gym: stuck on the way up, and nobody answers A."); + } + } + fail_step(stream, "Oreburgh Gym: did not reach Roark."); +} + +// 01.04: west along the row into the gym and up it to Roark, again from the Center after a loss. +void gym_until_roark_beaten(VideoStream& stream, ProControllerContext& context){ + // Losing costs some money and keeps the experience, so each try comes + // back stronger: Roark won twice by hand against Piplup Lv 10 and 14 and + // lost the third time. + bool won = false; + for (size_t attempt = 0; attempt < 5 && !won; attempt++){ + oreburgh_street_to_gym(stream, context); + won = oreburgh_gym_battles(stream, context); + if (!won){ + stream.log("[01.04] Lost in the gym (try " + std::to_string(attempt + 1) + "); back from the Center.", COLOR_ORANGE); + walk_out_of_center(stream, context, "Oreburgh"); + } + } + if (!won){ + fail_step(stream, "Oreburgh Gym: lost to Roark five times."); + } +} + +} + + +void add_oreburgh_events(StoryEvents& events){ + events.emplace("gym_until_roark_beaten", console_event(gym_until_roark_beaten)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp new file mode 100644 index 0000000000..f7975a6685 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -0,0 +1,85 @@ +/* BDSP AutoStory - Events: the Ravaged Path (01.09) + * + * From: https://github.com/PokemonAutomation/ + * + * The Ravaged Path's Rock Smash, as it ran on the console (2026-10-31). + * + */ + +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +namespace{ + +// "This rock seems breakable with a hidden move. Would you like to use Rock +// Smash?" is open, from an A at a smashable rock. Its Yes/No box comes only +// once the text is advanced again (by hand, 2026-10-31: the text complete +// 1.2 s after the first A, the box 1.2 s after a second), and r237a's timed +// A, A, B put its B on that box: No. So press A until the box's cursor +// shows, then once more for Yes, and wait out the move. False if no box +// shows: whatever is open is not this prompt. +bool answer_rock_smash_prompt(VideoStream& stream, ProControllerContext& context, const std::string& label){ + static const ImageFloatBox YES_NO_ARROW_BOX{0.700, 0.620, 0.070, 0.120}; + SelectionArrowFinder yes_no(stream.overlay(), YES_NO_ARROW_BOX, COLOR_GREEN); + for (size_t press = 0; press < 5; press++){ + context.wait_for(1200ms); + context.wait_for_all_requests(); + const VideoSnapshot screen = stream.video().snapshot(); + if (screen && yes_no.detect(*screen.frame)){ + stream.log("[Route] " + label + ": Rock Smash offered; Yes."); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for(5000ms); + if (dialogue_opens(stream, context, 500ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": after Rock Smash"); + } + return true; + } + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + } + stream.log("[Route] " + label + ": no Yes/No box for Rock Smash.", COLOR_RED); + return false; +} + +// 01.09: Rock Smash on the rock east of us, from the tile the route ends on +// facing it. Only the prompt's Yes/No box tells the rock from someone in +// the way (r237a: the walker took one for a person and said No). +void smash_the_rock(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::EAST); + if (!talk_with_nudges(stream, context, run_from_wild_battle, Direction::EAST, "the rock east of us", 2000ms)){ + fail_step(stream, "Ravaged Path: nothing answered facing the rock east of us."); + } + if (!answer_rock_smash_prompt(stream, context, "the rock east of us")){ + fail_step(stream, "Ravaged Path: no Rock Smash offer facing the rock east of us."); + } +} + +} + + +void add_route204_events(StoryEvents& events){ + events.emplace("smash_the_rock", console_event(smash_the_rock)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp new file mode 100644 index 0000000000..1473ffd8ec --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp @@ -0,0 +1,255 @@ +/* BDSP AutoStory - Events: Sandgem and Route 202 (00.04) + * + * From: https://github.com/PokemonAutomation/ + * + * 00.04's Potions from the Poke Mart lady on Route 201's east end, the + * way back to Route 202's save spot, and the stairs into Jubilife. + * + */ + +#include +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Bag.h" +#include "PokemonBDSP_AutoStory_Center.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::TilePos; + + +namespace{ + +// A walk_until() callback that keeps every location banner it sees. +std::function banner_keeper(std::vector& banners){ + return [&banners](const ImageViewRGB32& screen){ + const std::string read = read_location_banner(screen); + if (!read.empty() && (banners.empty() || banners.back() != read)){ + banners.push_back(read); + } + return false; + }; +} + +bool any_banner_names(const std::vector& banners, const char* map){ + return std::any_of(banners.begin(), banners.end(), + [&](const std::string& read){ return location_banner_matches(read, map); }); +} + +// Whether all of `tiles` are on `map`. +bool all_on_map(const StoryWorld& maps, const std::vector& tiles, const char* map){ + return !tiles.empty() && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos){ + const std::optional tile = maps.to_local(pos); + return tile && tile->map == map; + }); +} + +// North from below the gap between the signs until a wall stops us, +// nudging a tile west, twice at most, when blocked at once (below a +// sign or the Mart). Returns whether Route 202's banner showed: only +// one of the gap's columns leads out of town before a wall. +bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context){ + using Pokemon::Direction; + std::vector banners; + for (size_t nudges = 0;; nudges++){ + const WalkResult north = walk_until(stream, context, run_from_wild_battle, {0, 1}, 8000ms, "North through the gap", banner_keeper(banners)); + if (north.stop == WalkStop::TRANSITION || north.stop == WalkStop::DIALOG || north.wild_battles > 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "North through the gap: " + std::string(walk_stop_name(north.stop)) + " instead of a wall.", stream); + } + // Blocked at once: the wall reads after about 800ms still. + if (north.walked > ROUTE_START_DELAY + 800ms + 2 * ROUTE_MS_PER_TILE_UP_DOWN){ + break; + } + if (nudges == 2){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "North through the gap: still blocked after two tiles west.", stream); + } + route_status(stream, "North through the gap: blocked at once, below a sign or the Mart; a tile west."); + route_turn(stream, context, Direction::WEST); + walk_for(stream, context, run_from_wild_battle, {-1, 0}, route_open_hold(Direction::WEST, 1), "A tile west"); + route_turn(stream, context, Direction::NORTH); + } + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + banner_keeper(banners)(*screen.frame); + } + return any_banner_names(banners, MAP_ROUTE_202); +} + +// From the trees at the east end of the doors' row, west into the gap +// between the Center's sign and the Mart's (below_sign_gap()) and +// north through it until a wall stops us. Six tiles west from column +// 29 ends on 23, and on 20 to 26 if two tiles off: north from all of +// those passes no door, and only the gap lets us through. A walk +// north that goes nowhere stood below a sign or the Mart: a tile +// west, twice at most, never reaches the Center's door at 17. The +// planner would rather go round the south of town on walls, where +// townspeople wander. Returns where the walk north may have ended: +// the gap's columns end on Sandgem's top row, bar one that runs on to +// Route 202, and its banner tells the two apart. +std::vector north_through_sign_gap(VideoStream& stream, ProControllerContext& context, const StoryWorld& maps){ + using Pokemon::Direction; + route_status(stream, "To the gap between the Center's and the Mart's signs: west 6."); + route_turn(stream, context, Direction::WEST); + walk_for(stream, context, run_from_wild_battle, {-1, 0}, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); + const bool on_route = walk_north_through_gap(stream, context); + std::vector ends; + for (const TilePos& pos : map_region("SandgemTown", "below_sign_gap")){ + const TilePos end = slide_to_wall(maps, maps.to_world(MAP_SANDGEM_TOWN, pos), Direction::NORTH).stop; + if (all_on_map(maps, {end}, MAP_ROUTE_202) == on_route){ + ends.push_back(end); + } + } + route_status(stream, std::string("North through the gap: Route 202's banner ") + + (on_route ? "showed; on Route 202." : "did not show; on Sandgem Town's top row.")); + return ends; +} + +// Back up to Route 202's save spot under the ledge: it is where a +// retry may start (story_save_spots()), its walls pin it, and the +// trainers are north of it. First east to the trees that end the +// doors' row, so the way north starts from one known tile, then north +// through the gap between the signs: r123a, planned round the south +// of town on a map that had it wrong, walked into a house. +void walk_to_route_202_save_spot( + VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, + std::vector at, RouteTrainers& trainers +){ + const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end")); + const TilePos save_spot = maps.to_world(MAP_ROUTE_202, map_point("Route202", "save_spot")); + if (!all_on_map(maps, at, MAP_ROUTE_202)){ + at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos){ return pos == row_end; }, "To the trees east of the Poke Mart", {}, route_plan_costs()}); + at = north_through_sign_gap(stream, context, maps); + } + walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos){ return pos == save_spot; }, "Back to Route 202", {}, route_plan_costs()}); +} + +// North into Jubilife until Dawn's scene starts (00.04d's +// jubilife_stairs). Jubilife's south edge is a wall with stairs in it, +// west of where the route may leave us (r78 stood at its east end). +// Entering starts a scene: Dawn walks over while we stand still. So +// walk north; if the wall stops us and no dialogue follows, the stairs +// are further west. The scene itself is the next step. +void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context){ + for (size_t attempt = 0; attempt < 6; attempt++){ + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, {0, 1}, 4000ms, "to Dawn in Jubilife"); + if (walk.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: left the area instead of meeting Dawn.", stream); + } + ShortDialogWatcher dialog(COLOR_CYAN); + if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0){ + return; + } + stream.log("[00.04] Jubilife: the wall stopped us; looking for the stairs one tile west."); + route_turn(stream, context, Pokemon::Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: no stairs found and no Dawn after six tiles west.", stream); +} + +// Reads a sign facing `facing`: its box must open and close, and its text, +// when there is a reader for it, have `key`. A walk that stopped short of +// it is put right by walking on until the sign stops us, once. +void read_sign_on_the_way(VideoStream& stream, ProControllerContext& context, Direction facing, const std::string& sign, const std::string& key){ + for (size_t attempt = 0; attempt < 2; attempt++){ + const DialogueRead read = read_sign(stream, context, facing, sign); + if (read.opened && read.closed && (read.text.empty() || sign_text_matches(read.text, key))){ + return; + } + walk_until_stopped(stream, context, facing, 2, "On to the sign"); + } + fail_step(stream, "Sandgem: " + sign + " did not read facing " + Pokemon::direction_name(facing) + "."); +} + +// From the doors' row in front of Sandgem's Pokemon Center to the Poke Mart +// lady and back to Route 202's save spot. Every leg but three one-tile +// steps walks until something stops it, as measured on the console +// (2026-10-30, CFR "walk"): west to the Lab's sign; south a tile, west to +// the grass's edge; north to the trees, on her row; east until she stops +// us (she gives new games 10 Potions); south a tile, east to Route 201's +// sign; south a tile, east to the trees east of the Mart, north a row; then +// the planned walk to the save spot. She stands on the top row of her +// grass, a tile or two in from its west edge: missed, back west to the +// edge and again, twice. +void mart_lady(VideoStream& stream, ProControllerContext& context){ + walk_until_stopped(stream, context, Direction::WEST, 20, "West to the Lab's sign"); + read_sign_on_the_way(stream, context, Direction::WEST, "the Lab's sign", "lab"); + hold_for(stream, context, Direction::SOUTH, 200ms, "South onto the path"); + walk_until_stopped(stream, context, Direction::WEST, 28, "West along the path"); + walk_until_stopped(stream, context, Direction::NORTH, 3, "North to the trees"); + bool talked = false; + for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ + if (attempt > 0){ + walk_until_stopped(stream, context, Direction::WEST, 6, "West to the grass's edge"); + } + const DialogueRead read = talk_walking(stream, context, Direction::EAST, 4, "Poke Mart lady"); + talked = read.opened && read.closed; + } + if (!talked){ + fail_step(stream, "Route 201: the Poke Mart lady did not talk."); + } + hold_for(stream, context, Direction::SOUTH, 200ms, "South onto the signs' row"); + walk_until_stopped(stream, context, Direction::EAST, 16, "East to the Route 201 sign"); + read_sign_on_the_way(stream, context, Direction::EAST, "Route 201's sign", "201"); + hold_for(stream, context, Direction::SOUTH, 200ms, "South onto the path"); + walk_until_stopped(stream, context, Direction::EAST, 40, "East along the path"); + walk_until_stopped(stream, context, Direction::NORTH, 2, "North to the front row"); + const StoryWorld& maps = story_maps(); + RouteTrainers trainers{story_trainer_sights(), [&](const std::string& name){ fight_route_trainer(stream, context, name); }}; + walk_to_route_202_save_spot(stream, context, maps, {maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end"))}, trainers); +} + +// 00.04c's potions step, from the save out of Sandgem's Center: the Poke +// Mart lady's 10, on to Route 202's save spot, where 00.04d's save goes. +// The Bag must have Potions after. +void sandgem_potions(VideoStream& stream, ProControllerContext& context){ + // Onto the doors' row first: out of the door we stand a tile to + // three below it (r261 walked west into the trees from three). + route_turn(stream, context, Direction::EAST); + walk_tiles_dpad(stream, context, run_from_wild_battle, {1, 0}, 1, "east under the Center's wall"); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, {0, 1}, 3000ms, "north to the Center's front"); + if (up.stop != WalkStop::BLOCKED){ + fail_step(stream, std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + "."); + } + mart_lady(stream, context); + if (!bag_has_item(stream, context, "potion", 1)){ + fail_step(stream, "00.04: no Potions in the Bag after the shop."); + } +} + +} + + +void add_sandgem_events(StoryEvents& events){ + events.emplace("sandgem_potions", console_event(sandgem_potions)); + events.emplace("jubilife_stairs", console_event(up_jubilife_stairs)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp new file mode 100644 index 0000000000..e87167f910 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -0,0 +1,655 @@ +/* BDSP AutoStory - Story executor + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/FatalProgramException.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Actions.h" +#include "PokemonBDSP_AutoStory_Center.h" +#include "PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP_AutoStory_NewGame.h" +#include "PokemonBDSP_AutoStory_Settings.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::RunState; +using Pokemon::StoryStep; +using Pokemon::TilePos; + + +std::optional story_route_costs(const std::string& variant){ + if (variant.empty()){ + return route_plan_costs(); + } + if (variant == "town"){ + return town_plan_costs(); + } + if (variant == "robust"){ + return RobustPlanCosts(); + } + if (variant == "line"){ + return waypoint_plan_costs(); + } + return std::nullopt; +} + + +namespace{ + +// "to_catch_grass" as "to catch grass", for the logs. +std::string label_of(const StoryStep& step){ + std::string label = step.name; + std::replace(label.begin(), label.end(), '_', ' '); + return label; +} + +// The screen at a failed step, kept: the retry resets the game. +void save_failure_screenshot(VideoStream& stream, size_t attempt){ + VideoSnapshot screen = stream.video().snapshot(); + if (!screen){ + return; + } + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/failures/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = std::format("{}{}-attempt{}.png", folder, now_to_filestring(), attempt); + screen.frame->save(path); + stream.log("Failure screenshot: " + path); +} + +// A trainer who saw us on a route: Route 204's Twins battle as a pair +// (01.10), everyone else alone. +void fight_story_trainer(VideoStream& stream, ProControllerContext& context, const std::string& trainer){ + if (trainer.find("Twins") != std::string::npos){ + talk_or_fight(stream, context, trainer, TalkExpect::BATTLE, BattleFormat::DOUBLES); + return; + } + fight_route_trainer(stream, context, trainer); +} + +} + + +StoryExecutor::StoryExecutor(StoryEnv& env) + : m_env(env) +{} + +void StoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ + m_checkpoint = data.id; + // A row starts from its save: after the shoes' row, the save has them. + m_can_run = data.id > std::string(RUNNING_SHOES_ROW); + m_trainers.clear(); +} + +Outcome StoryExecutor::run(RunState& state, const StoryStep& step){ + stream().log(std::format("[{}] {} {}: {}", m_checkpoint, Pokemon::story_step_kind_name(step.kind), step.name, step.why), COLOR_BLUE); + try{ + Outcome outcome = run_guarded(state, step); + if (outcome.succeeded() && m_checkpoint == RUNNING_SHOES_ROW && step.name == RUNNING_SHOES_STEP){ + m_can_run = true; + } + return outcome; + }catch (OperationFailedException& e){ + if (m_stop_rows.contains(m_checkpoint)){ + throw; + } + // Attempts are the segment's policy; this only counts them for the + // log line the lab's run watcher reads. + const size_t attempt = m_failures[m_checkpoint]++; + stream().log(std::format("checkpoint_reattempt_loop: attempt {} failed: {}: {}", attempt, m_checkpoint, e.message()), COLOR_RED); + save_failure_screenshot(stream(), attempt); + return Pokemon::outcome_failed("operation_failed", e.message()); + } +} + +Outcome StoryExecutor::run_guarded(RunState& state, const StoryStep& step){ + const StoryEvents& events = story_events(); + if (auto event = events.find(step.variant); event != events.end()){ + return event->second(*this, state, step); + } + return Pokemon::StoryStepExecutor::run(state, step); +} + +Pose StoryExecutor::walk_to(const Pose& from, const Pose& goal, const StoryStep& step){ + const StoryWorld* maps = story_plan_world(from.map, goal.map); + if (maps == nullptr){ + fail_step(stream(), std::format("{}: no maps join {} and {}.", step.name, from.map, goal.map)); + } + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown route costs: " + step.variant); + } + std::vector start; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + start.push_back(story_to_world(*maps, from.map, pos)); + } + std::vector targets; + for (const TilePos& pos : Pokemon::tiles_of(goal)){ + targets.push_back(story_to_world(*maps, goal.map, pos)); + } + // The ones fought stay off m_trainers until the next attempt. + std::vector& sights = m_trainers.try_emplace(maps, story_trainers(*maps)).first->second; + RouteTrainers trainers{ + sights, + [this](const std::string& trainer){ fight_story_trainer(stream(), context(), trainer); } + }; + const RouteWalkPlan plan{ + [&](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, + label_of(step), {}, *costs, m_can_run ? MoveSpeed::RUN : MoveSpeed::WALK + }; + const std::vector at = walk_route(stream(), context(), run_from_wild_battle, *maps, start, trainers, plan); + sights = std::move(trainers.sights); + std::vector local; + for (const TilePos& pos : at){ + const std::optional tile = story_to_local(*maps, goal.map, pos); + if (!tile){ + fail_step(stream(), std::format("{}: the walk ended off {}.", step.name, goal.map)); + } + local.push_back(*tile); + } + return Pokemon::tile_pose(goal.map, local, Pokemon::facing_of(goal)); +} + +Outcome StoryExecutor::new_game(RunState&, const StoryStep&){ + play_new_game_intro(m_env.env, context(), m_env.player_name); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::options(RunState&, const StoryStep&){ + verify_tutorial_settings(stream(), context()); + context().wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::route(RunState& state, const StoryStep& step){ + if (Pokemon::tiles_of(state.pose).empty()){ + return Pokemon::outcome_failed("no_pose", Pokemon::to_string(state.pose)); + } + return Pokemon::outcome_ok(walk_to(state.pose, *step.target, step)); +} + +namespace{ + +// A LEGS step "to_dialogue": taps its one leg until a dialogue starts on the +// way, wherever it starts (Dawn's at Sandgem's edge, r35/r36: an exact tile +// and banner raced her scene). The scene's lines are the next step's. +Outcome leg_to_dialogue(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ + const Pokemon::StoryLeg& leg = step.legs.front(); + route_turn(stream, context, leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const WalkResult walk = walk_tiles_dpad(stream, context, run_from_wild_battle, {x, y}, (size_t)leg.tiles, step.name); + stream.log(std::format("[{}] {}: taps {} to a dialogue: {} tiles, {}.", checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, walk_stop_name(walk.stop))); + if (walk.stop == WalkStop::DIALOG){ + return Pokemon::outcome_ok(); + } + // A scene can lock us before its first line: Dawn walks over first (r37). + ShortDialogWatcher dialog(COLOR_CYAN); + if (walk.stop == WalkStop::BLOCKED && wait_until(stream, context, 10000ms, {{dialog}}) == 0){ + return Pokemon::outcome_ok(); + } + return Pokemon::outcome_failed("no_dialogue", step.name); +} + +// A LEGS step "to_map": runs its one leg until the location banner of +// pose_after's map shows, wherever it shows. A straight run along an open +// row needs no tile count (r41, r42: Sandgem's row 11 took us into the lab). +Outcome leg_to_map(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ + const Pokemon::StoryLeg& leg = step.legs.front(); + const std::string map = step.pose_after ? step.pose_after->map : ""; + route_turn(stream, context, leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const WalkResult walk = walk_until_dpad(stream, context, run_from_wild_battle, {x, y}, + Milliseconds(250) * leg.tiles + 2000ms, step.name, { + .arrived = [&](const ImageViewRGB32& screen){ return location_banner_matches(read_location_banner(screen), map); }, + .pace = DpadPace::RUN, + }); + stream.log(std::format("[{}] {}: ran {} to {}'s banner: {}.", checkpoint, step.name, direction_name(leg.direction), map, walk_stop_name(walk.stop))); + return walk.stop == WalkStop::ARRIVED ? Pokemon::outcome_ok() : Pokemon::outcome_failed("no_banner", map); +} + +} + +Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ + if (step.variant == "to_wall"){ + for (const Pokemon::StoryLeg& leg : step.legs){ + walk_until_stopped(stream(), context(), leg.direction, (size_t)leg.tiles, label_of(step)); + } + return Pokemon::outcome_ok(); + } + if (step.variant == "to_dialogue"){ + return leg_to_dialogue(stream(), context(), step, m_checkpoint); + } + if (step.variant == "to_map"){ + return leg_to_map(stream(), context(), step, m_checkpoint); + } + const bool one_short_ok = step.variant == "one_short_ok"; + if (!one_short_ok && !step.variant.empty()){ + return Pokemon::outcome_failed("unknown_variant", step.variant); + } + std::vector at = Pokemon::tiles_of(state.pose); + for (const Pokemon::StoryLeg& leg : step.legs){ + // Facing another way, a tap only turns (r256): turn first. + route_turn(stream(), context(), leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const size_t tiles = (size_t)leg.tiles; + const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, tiles, step.name); + stream().log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); + if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ + return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); + } + // Where we may be: the end, or a tile short of it. + std::vector next; + for (const TilePos& start : at){ + TilePos end = start; + for (size_t i = 0; i < tiles; i++){ + if (one_short_ok && i + 1 == tiles){ + next.push_back(end); + } + end = end.next(leg.direction); + } + next.push_back(end); + } + at = std::move(next); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.legs.back().direction)); +} + +namespace{ + +// One rule for scenes and map changes: v2's long B mashes, with an end +// check. Press through the text, and keep pressing B while the screen is +// quiet: a scene can pause while someone walks off (Dawn and Barry after +// Rowan leaves, r23), fade dimly, not to black, to another map (r25), or +// wait on a box the detector misses (r26). B never starts a talk on the +// overworld. Done once the overworld has shown, with no text and a still +// screen, for 5 s of presses: after r31's fade home a scripted walk-in ran +// 5 s with no text before Mom spoke. +// +// A step or scene that ends on another map (`fade`) must fade on the way, +// and only the quiet after the fade counts: r30's scene went quiet for 5 s +// before the fade home, and r28 "went out" a tile beside the door and +// walked a whole route in the house. No fade is a map mismatch. +constexpr double FADE_DARKNESS = 40; // brightness 0 to 255; r24's dim fade home goes darker +void settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label, bool fade){ + const WallClock none = WallClock::max(); + const WallClock deadline = current_time() + 120s; + ShortDialogDetector dialog; + OverworldDetector overworld; + WallClock quiet_since = none; + WallClock calm_since = none; // overworld, no text, still or not + double darkest = 255; + VideoSnapshot previous; + while (current_time() < deadline){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const WallClock now = current_time(); + if (frame){ + darkest = std::min(darkest, image_average(*frame.frame).sum() / 3); + } + const bool talking = frame && dialog.detect(frame); + const bool still = frame && previous && walk_screen_change(*previous.frame, *frame.frame) < WALK_TAP_BLOCKED_CHANGE; + previous = frame; + // Cloud shadows drift over Floaroma, so its screen is never still: r83 + // got Mew and then waited out the 120 s on a calm overworld. 15 s of + // overworld with no text is three times r31's silent walk-in. + const bool calm = !talking && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS); + calm_since = calm ? (calm_since == none ? now : calm_since) : none; + if (calm && now - calm_since >= 15000ms){ + stream.log(std::format("[Story] {}: settled on 15 s of overworld without text (the screen kept moving); darkest frame {:.0f}.", label, darkest)); + return; + } + if (!talking && still && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS)){ + quiet_since = quiet_since == none ? now : quiet_since; + if (now - quiet_since >= 5000ms){ + stream.log(std::format("[Story] {}: settled; darkest frame {:.0f}.", label, darkest)); + return; + } + }else{ + quiet_since = none; + } + pbf_press_button(context, talking ? button : BUTTON_B, 80ms, 1200ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + fade && darkest >= FADE_DARKNESS + ? label + ": no fade within 120 s, so we are not where the step goes (map mismatch)." + : label + ": not back on a quiet overworld within 120 s.", + stream); +} + +} + +namespace{ + +// Holds the D-pad toward a door (with B after the shoes) until the screen goes +// dark or a line shows, up to 12 s. Returns whether it went dark. A line +// stops it: holding on under Mom's "Oh, Kosmos!" wasted 12 s (r45). The walker's black-screen watch +// missed the lab's fade into Sandgem (r44, "dialog"), and settle's samples +// 1.3 s apart can fall either side of it, so watch at settle's own darkness. +DpadPosition dpad_toward(Direction direction){ + return + direction == Direction::NORTH ? DPAD_UP : + direction == Direction::SOUTH ? DPAD_DOWN : + direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; +} + +bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction, bool run){ + const DpadPosition dpad = dpad_toward(direction); + BlackScreenWatcher dark(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 3 * FADE_DARKNESS, 60); + ShortDialogWatcher dialog(COLOR_YELLOW); + const int ret = run_until(stream, context, [&](ProControllerContext& context){ + if (run){ + ssf_press_button(context, BUTTON_B, 0ms, 12000ms, 0ms); + } + pbf_press_dpad(context, dpad, 12000ms, 0ms); + }, {{dark}, {dialog}}); + return ret == 0; +} + +// What the screen showed after a tap toward a door or a cave mouth. +enum class TapLook{ + NOTHING, + DARK, + BANNER, +}; + +// Looks for `map`'s location banner (and, if `dark_too`, the fade) for up +// to `wait`. +TapLook look_after_tap(VideoStream& stream, ProControllerContext& context, const std::string& map, Milliseconds wait, bool dark_too){ + const WallClock deadline = current_time() + wait; + while (true){ + context.wait_for_all_requests(); + const VideoSnapshot screen = stream.video().snapshot(); + if (screen && location_banner_matches(read_location_banner(*screen.frame), map)){ + return TapLook::BANNER; + } + if (dark_too && screen && image_average(*screen.frame).sum() / 3 < FADE_DARKNESS){ + return TapLook::DARK; + } + if (current_time() >= deadline){ + return TapLook::NOTHING; + } + context.wait_for(100ms); + } +} + +// A STEP "to_banner": D-pad taps one at a time toward a door or cave mouth, +// each followed by a look, until the fade (then no more taps) and +// pose_after's map's location banner. A held walk can carry on past a +// fade that goes unseen: out of the Gym it walked on south (r219g), and +// through the Oreburgh Gate's west mouth on west into a pocket by Route +// 203's pond (r75). Taps during the fade are eaten. No banner fails the +// step right there. +Outcome tap_to_banner(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ + const std::string map = step.pose_after ? story_banner_name(step.pose_after->map) : ""; + for (size_t tap = 1; tap <= 30; tap++){ + pbf_press_dpad(context, dpad_toward(step.direction), 160ms, 0ms); + TapLook look = look_after_tap(stream, context, map, 1500ms, true); + const bool faded = look == TapLook::DARK; + if (faded){ + look = look_after_tap(stream, context, map, 8000ms, false); + } + if (look == TapLook::BANNER){ + stream.log(std::format("[{}] {}: {}'s banner after {} taps {}.", checkpoint, step.name, map, tap, direction_name(step.direction))); + context.wait_for(1000ms); + return Pokemon::outcome_ok(); + } + if (faded){ + break; + } + } + return Pokemon::outcome_failed("no_banner", map); +} + +} + +Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ + route_turn(stream(), context(), step.direction); + if (step.variant == "to_banner"){ + return tap_to_banner(stream(), context(), step, m_checkpoint); + } + // A step through a door: walk (or run) on until the screen goes dark, + // from next to the door or from anywhere in its line (r43: tapping 8 tiles + // down the lab took 7 s), then settle for any scene on arrival (Mom's, + // 00.01; Dawn's tour, 00.04). Without the fade, settle must see it itself + // (r33 was outside, "no fade"). + if (step.pose_after && step.pose_after->map != state.pose.map){ + const bool faded = run_into_door(stream(), context(), step.direction, m_can_run); + stream().log(std::format("[{}] {}: {} through the door: {}.", m_checkpoint, step.name, direction_name(step.direction), faded ? "faded" : "no fade yet")); + settle(stream(), context(), BUTTON_B, label_of(step), !faded); + return Pokemon::outcome_ok(); + } + const auto [x, y] = dpad_of(step.direction); + const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); + stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); + std::vector at; + for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ + at.push_back(pos.next(step.direction)); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.direction)); +} + +Outcome StoryExecutor::talk(RunState& state, const StoryStep& step){ + // Face whom we talk to: a route ends facing its last leg, not its goal + // (r40 reached Mom facing north). They block the tile, so the tap only turns. + if (const std::optional facing = Pokemon::facing_of(state.pose)){ + route_turn(stream(), context(), *facing); + } + pbf_press_button(context(), BUTTON_A, 80ms, 200ms); + context().wait_for_all_requests(); + // Roark's first line in the mine comes after the camera zooms in on + // us, which can take some seconds (r98; the old event waited 8 s). + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream(), context(), 8000ms, {{dialog}}) != 0){ + fail_step(stream(), label_of(step) + ": nobody answered."); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ + wait_for_dialogue(stream(), context(), label_of(step)); + pbf_press_button(context(), step.yes ? BUTTON_A : BUTTON_B, 80ms, 1000ms); + context().wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ + // The step before may have played the scene already (a door step settles + // its arrival scene), so its first line is not required. + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream(), context(), 15000ms, {{dialog}}) != 0){ + stream().log(label_of(step) + ": no line within 15 s; the step before played it.", COLOR_ORANGE); + } + settle(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step), + step.pose_after && step.pose_after->map != state.pose.map); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::battle(RunState&, const StoryStep& step){ + // "starter": the Starly battle at the lake (00.02). It opens on its own, + // with no talk, and the starter has no Poke Balls to throw; the scene + // goes on whoever wins. + if (step.variant == "starter"){ + fight_starly(stream(), context()); + return Pokemon::outcome_ok(); + } + // A loss sends us to a Pokemon Center, so ending on its floor means a + // loss - unless the floor check already passed where the battle began: + // Jubilife's yellow plaza reads as Center floor (63 %, r79 won Rowan's + // tag battle there and was called lost). + const bool center_before = !step.may_lose && in_pokemon_center(stream()); + if (!talk_or_fight(stream(), context(), label_of(step), TalkExpect::BATTLE, step.variant == "doubles" ? BattleFormat::DOUBLES : BattleFormat::SINGLES)){ + fail_step(stream(), label_of(step) + ": talked but did not battle."); + } + if (center_before){ + stream().log(label_of(step) + ": the floor read as a Center's before the battle too; no loss check here.", COLOR_ORANGE); + }else if (!step.may_lose && in_pokemon_center(stream())){ + fail_step(stream(), label_of(step) + ": lost the battle."); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::train(RunState&, const StoryStep& step){ + for (size_t battle = 0; battle < step.battles; battle++){ + const std::string label = std::format("Training battle {}/{}", battle + 1, step.battles); + route_status(stream(), label); + battle_1_pokemon(stream(), context()); + fail_if_blacked_out(stream(), context(), label); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ + // From the planned tile below the door, run in like any door. The lit- + // doorway lineup is for when the run in finds no door: people on the + // way can leave the route unsure of the column (r59, Jubilife). Below + // Sandgem's door the lineup read 0.7 tile off and stepped us off it (r48). + route_turn(stream(), context(), Direction::NORTH); + const bool in = run_into_door(stream(), context(), Direction::NORTH, m_can_run); + stream().log(std::format("[{}] {}: north into the Center: {}.", m_checkpoint, step.name, in ? "faded" : "no door; lining up by sight")); + if (in){ + pbf_wait(context(), 1500ms); + }else{ + enter_lit_door(stream(), context(), label_of(step)); + } + talk_to_nurse(stream(), context(), label_of(step)); + // Her last line ("We hope to see you again!") can still be up, and a + // line stops the run out at once (r62): B until none shows. + ShortDialogDetector line; + for (size_t presses = 0; presses < 10; presses++){ + context().wait_for_all_requests(); + const VideoSnapshot screen = stream().video().snapshot(); + if (!screen || !line.detect(*screen.frame)){ + break; + } + pbf_press_button(context(), BUTTON_B, 80ms, 700ms); + } + // Out the same way: south until the fade. The walk out's indoor check + // read "not a room" a step from the counter and stopped inside (r50, r59). + route_turn(stream(), context(), Direction::SOUTH); + const bool out = run_into_door(stream(), context(), Direction::SOUTH, m_can_run); + stream().log(std::format("[{}] {}: south out of the Center: {}.", m_checkpoint, step.name, out ? "faded" : "no fade yet")); + settle(stream(), context(), BUTTON_B, label_of(step), !out); + // r143a's heal said it had walked out while still inside. + context().wait_for_all_requests(); + VideoSnapshot screen = stream().video().snapshot(); + if (screen && indoors(*screen.frame)){ + fail_step(stream(), label_of(step) + ": still inside the Pokemon Center."); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::save(RunState&, const StoryStep& step){ + if (step.variant.empty()){ + save_game(m_env.env, context()); + return Pokemon::outcome_ok(); + } + // A row's start save, its variant the map the card must name. With a + // map, save_game() throws only when the card names another one, having + // backed out unsaved. That stops the program: a reset would load an + // older save and run this row from wherever that is. + try{ + save_game(m_env.env, context(), step.variant); + }catch (OperationFailedException& e){ + throw FatalProgramException( + ErrorReportMode::NO_ERROR_REPORT, + std::format("[{}] Not at the checkpoint's start, so not saving: {}", m_checkpoint, e.message()), + stream() + ); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::game(RunState&, const StoryStep& step){ + return Pokemon::outcome_failed("unknown_event", step.variant); +} + + +Pokemon::Segment make_story_segment( + const std::vector& rows, Pokemon::StoryStepExecutor& executor, + std::set& saved +){ + static const StoryStep SAVE_AT_START = []{ + StoryStep step; + step.kind = Pokemon::StoryStepKind::SAVE; + step.name = "save"; + step.why = "the save a failed step resets to"; + return step; + }(); + Pokemon::Segment segment; + segment.id = "BDSP"; + for (const StoryCheckpoint& row : rows){ + segment.checkpoints.push_back(Pokemon::CheckpointSpec{ + .id = row.id, .title = row.title, .saves = false, + .build = [&row, &executor, &saved]{ + Pokemon::Checkpoint steps = Pokemon::build_story_checkpoint(row, executor); + if (row.on_failure != StoryFailure::RELOAD_AND_RETRY){ + return steps; + } + StoryStep save_at_start = SAVE_AT_START; + save_at_start.variant = row.saves_in; + Pokemon::Event save = Pokemon::story_event(save_at_start, executor); + save.skip_if.push_back(Pokemon::condition("saved at the start", [&saved, id = row.id](RunState&){ + return saved.contains(id); + })); + // A save that fails is a failed first attempt: the retry + // resets and does not save again. + save.act = [&executor, &saved, id = row.id, save_at_start](RunState& state){ + saved.insert(id); + return executor.run(state, save_at_start); + }; + Pokemon::Checkpoint checkpoint; + checkpoint.id = row.id; + checkpoint.events.push_back(std::move(save)); + for (Pokemon::Event& event : steps.events){ + checkpoint.events.push_back(std::move(event)); + } + return checkpoint; + }, + }); + } + return segment; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h new file mode 100644 index 0000000000..d8b57457bc --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -0,0 +1,184 @@ +/* BDSP AutoStory - Story executor + * + * From: https://github.com/PokemonAutomation/ + * + * BDSP's way of doing the shared story steps + * (Pokemon/AutoStory/Pokemon_StorySteps.h) on the console. A route is + * planned on the joined maps that hold its start and its target + * (story_plan_world()), from the tiles the run's pose holds, and walked by + * the walker, which fights the trainers who see us and runs from wild + * battles. A step whose variant names one of the story's events + * (story_events(), PokemonBDSP_AutoStory_Events.h) runs that event, + * whatever its kind: the scripted scenes and the console-measured walks + * of segment 01. + * + * Every step runs inside a guard that turns a failed console step into a + * failed Outcome, with a screenshot and the "checkpoint_reattempt_loop: + * attempt N failed" line the lab's run watcher reads; the segment then + * reloads the row's save and runs it again. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H + +#include +#include +#include +#include +#include +#include +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class StarterChoice{ + TURTWIG, + CHIMCHAR, + PIPLUP, +}; + +// What a row does when one of its steps fails. +enum class StoryFailure{ + // Save before the first step; on a failure reset to that save and run + // the row again from its first step. + RELOAD_AND_RETRY, + // No save and no retry: the failure ends the program (00.00, before + // the game has anything worth saving). + STOP, +}; + +// A row of the story table: the shared checkpoint, and what a failure does. +struct StoryCheckpoint : public Pokemon::StoryCheckpoint{ + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY; + // The map the save screen's card names where the row starts ("Route + // 203"). The save at its start is refused anywhere else and the program + // stops: r76/r77 started 01.06 a walk away from its start and saved over + // the good save there. Empty for a row that does not save first. + std::string saves_in; +}; + +// What the story's steps act on. +struct StoryEnv{ + SingleSwitchProgramEnvironment& env; + ProControllerContext& context; + StarterChoice starter = StarterChoice::CHIMCHAR; + std::string player_name; +}; + + +// The plan costs a ROUTE step's variant names: "" the route costs, +// "town" town_plan_costs(), "robust" the planner's defaults, "line" +// waypoint_plan_costs() (a stretch of a line a human walked). Nothing for +// another name. +std::optional story_route_costs(const std::string& variant); + + +// Does BDSP's kinds of story step on the console. +class StoryExecutor : public Pokemon::StoryStepExecutor{ +public: + explicit StoryExecutor(StoryEnv& env); + + StoryEnv& env(){ + return m_env; + } + VideoStream& stream(){ + return m_env.env.console; + } + ProControllerContext& context(){ + return m_env.context; + } + // The rows whose failure ends the program (StoryFailure::STOP): a + // failed step there throws on instead of failing the step. + void set_stop_rows(std::set rows){ + m_stop_rows = std::move(rows); + } + // A new attempt of a row: every trainer can see us again. + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override; + virtual Pokemon::Outcome run(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + +protected: + // The row running now. + const std::string& checkpoint() const{ + return m_checkpoint; + } + // Walks from the tiles of `from` on its map to any of `goal`'s tiles, + // fighting the trainers who see us this attempt. Returns where it left + // us, as a pose on `goal`'s map. + Pokemon::Pose walk_to(const Pokemon::Pose& from, const Pokemon::Pose& goal, const Pokemon::StoryStep& step); + + // The intro of a new game, from the title screen to the bedroom. + virtual Pokemon::Outcome new_game(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // verify_tutorial_settings(): fast text, Autosave off. + virtual Pokemon::Outcome options(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // Variant: the plan's costs ("" route, "town", "robust", "line"). + virtual Pokemon::Outcome route(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // Each leg in D-pad taps, turning first. Variant "one_short_ok": a leg + // may end a tile short (a door lined up by sight after). Variant + // "to_wall": each leg walks until something stops it, for at most its + // tiles' time, as measured on the console; the pose is then unknown + // unless the step gives one. + virtual Pokemon::Outcome legs(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // One D-pad step `direction`, turning first: onto a scene's trigger or + // into a door. Variant "to_banner": one tap into a cave mouth, then + // wait for pose_after's map's location banner. + virtual Pokemon::Outcome step(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A at what we face; a dialogue must open. + virtual Pokemon::Outcome talk(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A for YES, B for NO, once the question's box shows. + virtual Pokemon::Outcome answer(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A dialogue opens (or has), then B through it to the overworld; + // variant "a": A through it, for lines that give an item. + virtual Pokemon::Outcome mash(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A trainer's text opens (or has) and ends in a battle, fought with the + // best move; one lost fails the step unless `may_lose`. Variant + // "doubles" for a tag battle. + virtual Pokemon::Outcome battle(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // `battles` wild battles in the grass we stand in, with the best move. + virtual Pokemon::Outcome train(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // In front of a Pokemon Center, its lit doorway on screen: in, the + // nurse, and out (heal_at_pokemon_center()). + virtual Pokemon::Outcome heal(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + virtual Pokemon::Outcome save(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // The event its variant names (story_events()). + virtual Pokemon::Outcome game(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + +private: + Pokemon::Outcome run_guarded(Pokemon::RunState& state, const Pokemon::StoryStep& step); + + StoryEnv& m_env; + std::string m_checkpoint; + // Whether the player has the Running Shoes: the pace routes are timed at. + bool m_can_run = false; + // The trainers who can still see us on each story_plan_world() world, + // in its tiles, from its first walk this attempt; all of them again at + // each attempt. + std::map> m_trainers; + std::map m_failures; + std::map m_run_counts; + std::set m_stop_rows; +}; + + +// `rows` (kept alive by the caller) as one segment. A row that reloads on +// failure saves first, unless `saved` says it has in this run: a retry +// starts from that save. `executor` and `saved` must outlive the segment. +Pokemon::Segment make_story_segment( + const std::vector& rows, Pokemon::StoryStepExecutor& executor, + std::set& saved +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp new file mode 100644 index 0000000000..c5c2e2021b --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -0,0 +1,259 @@ +/* BDSP AutoStory - Story places + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; +using Pokemon::TilePos; + + +namespace{ + +// The JSON maps' names and the location banners'. Both halves of Route +// 204 show "Route 204"; no world holds both. +const std::map& banner_names(){ + static const std::map names{ + {"TwinleafTown", "Twinleaf Town"}, + {"Route201", MAP_ROUTE_201}, + {"VerityLakefront", "Verity Lakefront"}, + {"SandgemTown", MAP_SANDGEM_TOWN}, + {"Route202", MAP_ROUTE_202}, + {"JubilifeCity", MAP_JUBILIFE_CITY}, + {"Route203", MAP_ROUTE_203}, + {"OreburghCity", MAP_OREBURGH_CITY}, + {"Route204South", "Route 204"}, + {"Route204North", "Route 204"}, + {"FloaromaTown", MAP_FLOAROMA_TOWN}, + }; + return names; +} + +const std::string& banner_name(const std::string& map){ + const auto& names = banner_names(); + if (auto iter = names.find(map); iter != names.end()){ + return iter->second; + } + return map; +} + +// `maps` (JSON names) in one world, each with its tile (0,0) at the corner +// of its decomp rectangle (devtools/maps/bdsp_maps.py), so maps that meet +// in the game meet here. +StoryWorld joined(const std::vector>& maps){ + StoryWorld world; + for (const auto& [map, corner] : maps){ + world.add(banner_name(map), story_map(map), corner); + } + return world; +} + +// TODO(#114 multi-map view): MapData has no joined view of maps yet. +// Twinleaf to Route 202 in one world, at the decomp's world tiles (its +// cells are 32 tiles square; Route 201's map starts 7 rows into its cell +// and Route 202's a column west of its own). +const StoryWorld& south_world(){ + static const StoryWorld world = joined({ + {"TwinleafTown", {96, 864}}, + {"Route201", {96, 839}}, + {"VerityLakefront", {64, 832}}, + {"SandgemTown", {160, 832}}, + {"Route202", {159, 800}}, + }); + return world; +} + +// Jubilife without its east column: Route 203's map starts on that column +// (world x 191, its column 0), and two maps may not both hold open ground. +// Jubilife's tiles keep their coordinates. +Pokemon::MapData jubilife_west_of_route_203(){ + const Pokemon::MapData& city = story_map("JubilifeCity"); + Pokemon::MapData cut; + cut.name = city.name; + cut.source = city.source; + cut.tiles = Pokemon::Grid{city.tiles.width - 1, city.tiles.height, {}}; + for (int y = 0; y < cut.tiles.height; y++){ + for (int x = 0; x < cut.tiles.width; x++){ + cut.tiles.cells.push_back(city.tiles.at({x, y})); + } + } + for (const Pokemon::MapObject& object : city.objects){ + if (cut.tiles.contains({object.x, object.y})){ + cut.objects.push_back(object); + } + } + return cut; +} + +// 01.06: Route 203 west into Jubilife (decomp rectangles (191, 740) and +// (128, 736)): Route 203's rows 17..20 are Jubilife's street, rows 21..24. +const StoryWorld& route_203_world(){ + static const StoryWorld world = []{ + StoryWorld world = joined({{"Route203", {191, 740}}}); + world.add(MAP_JUBILIFE_CITY, jubilife_west_of_route_203(), {128, 736}); + return world; + }(); + return world; +} + +// 01.08: Jubilife north onto Route 204's south half (160, 704), whose street +// (12..15, 31) carries on Jubilife's (44..47, 0). +const StoryWorld& route_204_south_world(){ + static const StoryWorld world = joined({{"JubilifeCity", {128, 736}}, {"Route204South", {160, 704}}}); + return world; +} + +// 01.10: Route 204's north half (160, 672) up into Floaroma (160, 640). +const StoryWorld& route_204_north_world(){ + static const StoryWorld world = joined({{"Route204North", {160, 672}}, {"FloaromaTown", {160, 640}}}); + return world; +} + +// The Ravaged Path with the rock 01.09 smashes, (22, 43), open: no route +// before the smash can use it, since it only leads east, where the rocks +// shut everything else off. +Pokemon::MapData ravaged_path_smashed(){ + Pokemon::MapData map = story_map("RavagedPath"); + Pokemon::cell_at(map.tiles, map_point("RavagedPath", "rocks.13")) = Pokemon::TileKind::OPEN; + return map; +} + +// The interiors (houses, the lab, the Gym, the Gate, the mine's two floors, +// the Ravaged Path), each its own world, named by its JSON map. +const std::map& room_worlds(){ + static const std::map rooms = []{ + std::map ret; + for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab", "OreburghGym", "OreburghGate", "OreburghMineB1F", "OreburghMine"}){ + ret[room].add(room, story_map(room), {0, 0}); + } + ret["RavagedPath"].add("RavagedPath", ravaged_path_smashed(), {0, 0}); + return ret; + }(); + return rooms; +} + +// A world and the JSON maps it holds. +struct PlanWorld{ + const StoryWorld* world; + std::set maps; +}; + +// In order: the first that holds both ends of a walk plans it. Jubilife +// alone comes before the worlds that join it to a route, so 00.05's walks +// plan on the city alone. +const std::vector& plan_worlds(){ + static const std::vector worlds = []{ + std::vector ret{ + {&south_world(), {"TwinleafTown", "Route201", "VerityLakefront", "SandgemTown", "Route202"}}, + {&jubilife_maps(), {"JubilifeCity"}}, + {&oreburgh_city_maps(), {"OreburghCity"}}, + {&route_203_world(), {"Route203", "JubilifeCity"}}, + {&route_204_south_world(), {"JubilifeCity", "Route204South"}}, + {&route_204_north_world(), {"Route204North", "FloaromaTown"}}, + }; + for (const auto& [name, world] : room_worlds()){ + ret.push_back({&world, {name}}); + } + return ret; + }(); + return worlds; +} + +// Route 203's trainers, from the Diamond decomp's events (zone event +// narc_0329): where each stands on the JSON map, the way they look and +// how far. The one by the Gate wanders in BDSP and is off the map. +std::vector route_203_trainer_sights(){ + const auto at = [](const char* name){ + for (const Pokemon::MapObject& object : story_map("Route203").objects){ + if (object.name == name){ + return TilePos{object.x, object.y}; + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::string("Route 203 has no ") + name + "."); + }; + return { + {.name = "Youngster (west)", .pos = at("youngster_1"), .facing = Direction::WEST, .range = 2}, + {.name = "Lass (above row 18)", .pos = at("lass_2"), .facing = Direction::WEST, .range = 5}, + {.name = "Youngster (above row 18)", .pos = at("youngster_3"), .facing = Direction::SOUTH, .range = 4}, + {.name = "Lass (by the Gate)", .pos = at("lass_4"), .facing = Direction::SOUTH, .range = 5}, + }; +} + +// The trainers of the JSON map `map`, in its own tiles. +std::vector map_trainers(const std::string& map){ + if (map == "Route202"){ + return route202_trainer_sights(); + } + if (map == "Route203"){ + return route_203_trainer_sights(); + } + if (map == "Route204South"){ + return route204_south_trainer_sights(); + } + if (map == "Route204North"){ + return route204_north_trainer_sights(); + } + return {}; +} + +} + + +const std::string& story_banner_name(const std::string& map){ + return banner_name(map); +} + +const StoryWorld* story_plan_world(const std::string& from, const std::string& to){ + for (const PlanWorld& world : plan_worlds()){ + if (world.maps.contains(from) && world.maps.contains(to)){ + return world.world; + } + } + return nullptr; +} + +TilePos story_to_world(const StoryWorld& world, const std::string& map, const TilePos& local){ + return world.to_world(banner_name(map), local); +} + +std::optional story_to_local(const StoryWorld& world, const std::string& map, const TilePos& pos){ + const std::optional tile = world.to_local(pos); + if (!tile || tile->map != banner_name(map)){ + return std::nullopt; + } + return tile->pos; +} + +std::vector story_trainers(const StoryWorld& world){ + std::vector sights; + for (const PlanWorld& plan : plan_worlds()){ + if (plan.world != &world){ + continue; + } + for (const std::string& map : plan.maps){ + for (TrainerSight sight : map_trainers(map)){ + sight.pos = story_to_world(world, map, sight.pos); + sights.push_back(std::move(sight)); + } + } + } + return sights; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h new file mode 100644 index 0000000000..c2ff2d9ee6 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h @@ -0,0 +1,51 @@ +/* BDSP AutoStory - Story places + * + * From: https://github.com/PokemonAutomation/ + * + * Where the story's steps walk: poses are tiles of the JSON maps + * ("Route202"), named by map_point() and map_region(). A walk from one map + * to another is planned on a story world that holds both: the outdoors + * from Twinleaf to Route 202, Jubilife, Oreburgh, Route 203 into Jubilife, + * Jubilife up Route 204's south half, Route 204's north half into + * Floaroma, and each interior on its own. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryPlaces_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryPlaces_H + +#include +#include +#include +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The story world a walk from `from` to `to` is planned on, or nothing if +// no world holds both. Its maps are named as the game's location banners +// name them ("Route 202"), which the walker reads. +const StoryWorld* story_plan_world(const std::string& from, const std::string& to); + +// The location banner of the JSON map `map` ("Route 204" for both of its +// halves), or `map` itself for one no banner list names. +const std::string& story_banner_name(const std::string& map); + +// The tile `local` of the map `map` in `world`. +Pokemon::TilePos story_to_world(const StoryWorld& world, const std::string& map, const Pokemon::TilePos& local); + +// The world tile `pos` as a tile of `map`, or nothing if it is not on `map`. +std::optional story_to_local(const StoryWorld& world, const std::string& map, const Pokemon::TilePos& pos); + +// The trainers a walk on `world` may meet (Routes 202 to 204's), in its tiles. +std::vector story_trainers(const StoryWorld& world); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp new file mode 100644 index 0000000000..febf93d8cb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -0,0 +1,513 @@ +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + * A run of the whole story table, new game to Floaroma, without the + * console: story_checkpoints() built into one segment as the program builds + * it (make_story_segment()), run by the shared runner with the program's + * retry policy, against a fake game. The fake plans each ROUTE on the story + * world that holds its start and target, walks each LEGS tile by tile, and + * passes every other step unless told to fail it. Events pass as a whole. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StorySimulation_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::RunState; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + +std::string describe(const std::string& map, const std::vector& tiles){ + std::string text = " " + map; + for (const TilePos& pos : tiles){ + text += " " + Pokemon::to_string(pos); + } + return text; +} + +std::string joined(const std::vector& lines){ + std::string text; + for (const std::string& line : lines){ + text += "\n " + line; + } + return text; +} + +bool contains(const std::vector& tiles, const TilePos& pos){ + return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); +} + +// Where a step ends, or why it does not plan. +struct Walked{ + std::optional at; + std::string error; + std::string plan; +}; + +// The tiles a plan walks. +int tiles_walked(const std::vector& plan){ + int tiles = 0; + for (const PlannedLeg& leg : plan){ + tiles += leg.leg.tiles; + } + return tiles; +} + +// A route may walk a little further than the shortest way, for a wall at a +// leg's end, but not round the town: r64 went 38 tiles to the School's door +// for 12 straight (the "town" costs still feared stick-era open legs). +std::string detour_error(const StoryWorld& world, const std::vector& start, + const std::function& at_goal, const std::vector& plan +){ + const auto shortest = plan_robust_route(world, start, at_goal, waypoint_plan_costs()); + const int walked = tiles_walked(plan); + const int least = shortest ? tiles_walked(*shortest) : walked; + if (walked <= least + least / 2 + 4){ + return ""; + } + return std::format("the plan walks {} tiles where {} would do.", walked, least); +} + + +// A ROUTE step from `from`, planned as StoryExecutor::walk_to() walks it. +Walked plan_route(const Pose& from, const StoryStep& step){ + const Pose& goal = *step.target; + const StoryWorld* world = story_plan_world(from.map, goal.map); + const std::optional costs = story_route_costs(step.variant); + if (world == nullptr || !costs){ + return {std::nullopt, std::format("{}: no world joins {} and {}, or no costs \"{}\".", step.name, from.map, goal.map, step.variant), ""}; + } + std::vector start; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + start.push_back(story_to_world(*world, from.map, pos)); + } + std::vector targets; + for (const TilePos& pos : Pokemon::tiles_of(goal)){ + targets.push_back(story_to_world(*world, goal.map, pos)); + } + const std::function at_goal = [&](const TilePos& pos){ return contains(targets, pos); }; + const auto plan = plan_robust_route(*world, start, at_goal, *costs); + if (!plan){ + return {std::nullopt, step.name + ": no plan from" + describe(from.map, Pokemon::tiles_of(from)) + ".", ""}; + } + Walked walked{std::nullopt, "", std::format("{}: {} legs", step.name, plan->size())}; + std::vector local; + for (const PlannedLeg& leg : *plan){ + walked.plan += " " + to_string(leg.leg); + } + const std::string detour = detour_error(*world, start, at_goal, *plan); + if (!detour.empty()){ + return {std::nullopt, step.name + ": " + detour, walked.plan}; + } + for (const TilePos& pos : plan->empty() ? start : plan->back().after){ + const std::optional tile = story_to_local(*world, goal.map, pos); + if (!tile || !contains(Pokemon::tiles_of(goal), *tile)){ + return {std::nullopt, step.name + ": the plan may end off the target, at " + describe(*world, pos) + ".", walked.plan}; + } + local.push_back(*tile); + } + walked.at = Pokemon::tile_pose(goal.map, local, Pokemon::facing_of(goal)); + return walked; +} + +// `leg` of `step` from `pos`: adds where it ends to `next` (and a tile +// short where the step allows it). Returns why it is blocked, or "". +std::string walk_leg_from( + const StoryWorld& world, TilePos pos, const Pokemon::StoryLeg& leg, const StoryStep& step, + std::vector& next +){ + for (int i = 0; i < leg.tiles; i++){ + const std::optional stepped = PokemonBDSP::step(world, pos, leg.direction); + if (!stepped){ + return step.name + ": " + direction_name(leg.direction) + " is blocked at " + describe(world, pos) + "."; + } + if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ + next.push_back(pos); + } + pos = *stepped; + } + next.push_back(pos); + return ""; +} + +// A LEGS step from `from`, walked tile by tile on its map, a tile short too +// where the step allows it. Walks to walls end where nothing is known. +Walked walk_legs(const Pose& from, const StoryStep& step){ + if (step.variant == "to_wall"){ + return {std::nullopt, "", ""}; + } + const StoryWorld* world = story_plan_world(from.map, from.map); + if (world == nullptr){ + return {std::nullopt, step.name + ": no world holds " + from.map + ".", ""}; + } + std::vector at; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + at.push_back(story_to_world(*world, from.map, pos)); + } + for (const Pokemon::StoryLeg& leg : step.legs){ + std::vector next; + for (const TilePos& pos : at){ + const std::string blocked = walk_leg_from(*world, pos, leg, step, next); + if (!blocked.empty()){ + return {std::nullopt, blocked, ""}; + } + } + at = std::move(next); + } + std::vector local; + for (const TilePos& pos : at){ + local.push_back(*story_to_local(*world, from.map, pos)); + } + return {Pokemon::tile_pose(from.map, local, step.legs.back().direction), "", ""}; +} + +// A STEP onto the next tile of the same map. +Walked step_on(const Pose& from, const StoryStep& step){ + std::vector at; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + at.push_back(pos.next(step.direction)); + } + return {Pokemon::tile_pose(from.map, at, step.direction), "", ""}; +} + + +// The game: steps pass, routes, legs and steps move the pose on the maps, +// and the steps in `failures` ("/") fail, so many times. +class FakeGame : public Pokemon::StoryStepExecutor{ +public: + struct Failure{ + std::string failure; + size_t times = 0; + }; + + std::vector& trace; + std::map failures; + std::vector plans; + + explicit FakeGame(std::vector& trace) + : trace(trace) + {} + + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ + m_row = data.id; + trace.push_back("begin " + data.id); + } + virtual Outcome run(RunState& state, const StoryStep& step) override{ + const std::string key = m_row + "/" + step.name; + trace.push_back(key); + auto failure = failures.find(key); + if (failure != failures.end() && failure->second.times > 0){ + failure->second.times--; + return Pokemon::outcome_failed(failure->second.failure, key); + } + return Pokemon::StoryStepExecutor::run(state, step); + } +protected: + virtual Outcome route(RunState& state, const StoryStep& step) override{ + return moved(plan_route(state.pose, step), "no_plan"); + } + virtual Outcome legs(RunState& state, const StoryStep& step) override{ + return moved(walk_legs(state.pose, step), "blocked"); + } + virtual Outcome step(RunState& state, const StoryStep& step) override{ + return moved(step_on(state.pose, step), "blocked"); + } + virtual Outcome new_game(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome options(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome talk(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome mash(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome battle(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome train(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome heal(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome shop(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome save(RunState&, const StoryStep& step) override{ + trace.push_back("save in " + m_row); + if (!step.variant.empty()){ + trace.push_back("card for " + m_row + ": " + step.variant); + } + return Pokemon::outcome_ok(); + } + virtual Outcome game(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + +private: + Outcome moved(const Walked& walked, const char* failure){ + if (!walked.error.empty()){ + return Pokemon::outcome_failed(failure, walked.error); + } + if (!walked.plan.empty()){ + plans.push_back(walked.plan); + } + return walked.at ? Pokemon::outcome_ok(*walked.at) : Pokemon::outcome_ok(Pose{}); + } + + std::string m_row; +}; + + +// One simulated run, as the program runs it, in `language`. Japanese has +// no text reader, so a step that needed text would show. (The console's +// global language is left alone: tests run in parallel.) +struct Simulation{ + std::vector trace; + std::vector lines; + FakeGame game{trace}; + std::set saved; + Pokemon::SegmentPolicy policy; + + Pokemon::SegmentReport run(const Pokemon::SegmentRange& range, Language language = Language::Japanese){ + game.set_language(language); + Pokemon::Segment segment = make_story_segment(story_checkpoints(), game, saved); + Pokemon::SegmentHooks hooks; + hooks.reload = [this](RunState&){ + trace.push_back("reload"); + return Pokemon::outcome_ok(); + }; + hooks.log = [this](const std::string& line){ lines.push_back(line); }; + RunState state; + return Pokemon::run_segment(state, segment, range, policy, hooks); + } + std::vector begun() const{ + std::vector ids; + for (const std::string& item : trace){ + if (item.starts_with("begin ")){ + ids.push_back(item.substr(6)); + } + } + return ids; + } + size_t count(const std::string& item) const{ + return (size_t)std::count(trace.begin(), trace.end(), item); + } + bool logged(const std::string& line) const{ + return std::find(lines.begin(), lines.end(), line) != lines.end(); + } +}; + +std::vector all_ids(){ + std::vector ids; + for (const StoryCheckpoint& row : story_checkpoints()){ + ids.push_back(row.id); + } + return ids; +} + + +UnitTestResult test_whole_table(Logger& logger){ + for (Language language : {Language::English, Language::Japanese}){ + Simulation sim; + const Pokemon::SegmentReport report = sim.run({}, language); + for (const std::string& plan : sim.game.plans){ + logger.log(plan); + } + if (!report.completed){ + return std::string(language_data(language).name) + ": expected the whole story through:" + joined(sim.lines); + } + if (sim.begun() != all_ids()){ + return "Unexpected checkpoint order:" + joined(sim.begun()); + } + // Every row saves first but 00.00, which a failure ends; 01.10 and + // 01.11 save again at their ends. + for (const std::string& id : all_ids()){ + const size_t saves = sim.count("save in " + id); + const size_t expected = id == "00.00" ? 0 : id == "01.10" || id == "01.11" ? 2 : 1; + if (saves != expected){ + return std::format("{} saved {} times, expected {}:{}", id, saves, expected, joined(sim.trace)); + } + } + // Each start save checks the card for its row's map. + for (const StoryCheckpoint& row : story_checkpoints()){ + if (!row.saves_in.empty() && sim.count("card for " + row.id + ": " + row.saves_in) != 1){ + return std::format("{}'s start save did not check the card for {}:{}", row.id, row.saves_in, joined(sim.trace)); + } + } + if (report.one_shot_passes() != report.checkpoints_run){ + return "Expected every checkpoint one-shot:" + joined(sim.lines); + } + } + return true; +} + +UnitTestResult test_start_and_end(Logger&){ + Simulation sim; + Pokemon::SegmentReport report = sim.run({.from = "00.04b", .to = "00.04d"}); + if (!report.completed || sim.begun() != std::vector{"00.04b", "00.04c", "00.04d"}){ + return "Expected 00.04b to 00.04d only:" + joined(sim.begun()); + } + Simulation tail; + report = tail.run({.from = "01.10", .to = std::nullopt}); + if (!report.completed || tail.begun() != std::vector{"01.10", "01.11"}){ + return "Expected 01.10 to the end:" + joined(tail.begun()); + } + Simulation one; + report = one.run({.from = "00.02", .to = "00.02"}); + if (!report.completed || one.begun() != std::vector{"00.02"}){ + return "Expected 00.02 alone:" + joined(one.begun()); + } + return true; +} + +// r72: a blackout in 00.04b's training. The checkpoint resets to the save +// it made at its start, does not save again, and walks from that save's +// pose to the grass again. +UnitTestResult test_training_blackout(Logger&){ + Simulation sim; + sim.game.failures["00.04b/train"] = {.failure = "blackout", .times = 1}; + const Pokemon::SegmentReport report = sim.run({.from = "00.04b", .to = "00.04d"}); + const std::vector expected{"00.04b", "00.04b", "00.04c", "00.04d"}; + if (!report.completed || report.reloads != 1 || sim.begun() != expected){ + return "Expected 00.04b reloaded once, then on:" + joined(sim.trace); + } + if (sim.count("save in 00.04b") != 1 || sim.count("00.04b/to_catch_grass") != 2){ + return "Expected the retry to start from 00.04b's one save and walk to the grass again:" + joined(sim.trace); + } + if (!sim.logged("[Checkpoint] 00.04b: passed on attempt 2 (1 reset)")){ + return "Expected the retried pass logged:" + joined(sim.lines); + } + return true; +} + +// A step that fails every time: three attempts from the checkpoint's +// save, then the segment stops on it and runs nothing after. +UnitTestResult test_gives_up_after_attempts(Logger&){ + const std::string step = "01.04/gym_until_roark_beaten"; + Simulation sim; + sim.game.failures[step] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport report = sim.run({.from = "01.01", .to = std::nullopt}); + if (report.completed || report.reloads != 2 || report.failed_checkpoint != "01.04" || report.failed_event != "gym_until_roark_beaten" + || report.runs.empty() || report.runs.back().attempts != 3 || sim.count(step) != 3 + || sim.count("save in 01.04") != 1 || sim.count("begin 01.05") != 0 + ){ + return "Expected 01.04 tried 3 times from its save, then a stop:" + joined(sim.trace) + joined(sim.lines); + } + Simulation lab; + lab.policy.stop_at_first_failure = true; + lab.game.failures[step] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport stopped = lab.run({.from = "01.01", .to = std::nullopt}); + if (stopped.completed || !stopped.stopped_at_first_failure || lab.count("reload") != 0){ + return "Expected a stop at the first failure without a reload:" + joined(lab.trace); + } + return true; +} + +// Each row that has a start pose starts where the row before it leaves +// the player, by the same planning and walking the fake run does. +UnitTestResult test_poses_chain(Logger&){ + std::optional at; + std::string last; + for (const StoryCheckpoint& row : story_checkpoints()){ + if (row.start){ + const std::vector start = Pokemon::tiles_of(*row.start); + if (at && (at->map != row.start->map || + !std::ranges::all_of(Pokemon::tiles_of(*at), [&](const TilePos& pos){ return contains(start, pos); })) + ){ + return std::format("{} starts at{}, but {} may end at{}.", row.id, describe(row.start->map, start), + last, describe(at->map, Pokemon::tiles_of(*at))); + } + at = row.start; + }else{ + at.reset(); + } + for (const StoryStep& step : row.steps){ + if (step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || step.kind == StoryStepKind::STEP){ + if (!at && step.variant != "to_wall" && step.variant != "jubilife_stairs"){ + return row.id + " " + step.name + ": walks from an unknown pose."; + } + const Walked walked = !at ? Walked{} : step.kind == StoryStepKind::ROUTE ? plan_route(*at, step) + : step.kind == StoryStepKind::LEGS ? walk_legs(*at, step) : step_on(*at, step); + if (!walked.error.empty()){ + return row.id + " " + walked.error; + } + at = walked.at; + }else if (step.kind == StoryStepKind::GAME || step.kind == StoryStepKind::SHOP || step.kind == StoryStepKind::MASH){ + // A scene or an event: where it ends is the table's to say. + at.reset(); + } + if (step.pose_after){ + at = step.pose_after; + } + } + last = row.id; + } + return true; +} + +// With no reader for the game's language, the reads that used text give +// way: a banner or save card that showed matches any map. +UnitTestResult test_no_text_reader(Logger&){ + for (Language language : {Language::Japanese, Language::Korean, Language::French}){ + if (story_reads_text(language)){ + return std::string("Expected no text reader for ") + language_data(language).name + "."; + } + } + if (!location_banner_matches(UNREAD_LOCATION, "Route 202") || location_banner_matches("", "Route 202")){ + return "Expected a shown, unread banner to match and no banner not to."; + } + if (!story_reads_text(Language::English) || location_banner_matches("sandgemtown", "Route 202")){ + return "Expected English banners read and compared."; + } + return true; +} + + +class Test_StorySimulation : public UnitTest{ +public: + Test_StorySimulation(std::string name, std::function body) + : UnitTest("PokemonBDSP::AutoStory - Simulation: " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + +} + + +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database){ + database.add("00.00 to Floaroma, in order, each row saved first, English and Japanese", test_whole_table); + database.add("start and end checkpoints", test_start_and_end); + database.add("a blackout in 00.04b's training resets to its save", test_training_blackout); + database.add("a step that keeps failing stops after 3 attempts", test_gives_up_after_attempts); + database.add("each start pose is where the row before ends; routes plan on the maps", test_poses_chain); + database.add("no text reader for the game's language", test_no_text_reader); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h new file mode 100644 index 0000000000..9af37ca9b4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Segments 00 and 01 run end to end without a console: the story table, +// the shared segment runner and its retry policy, against a fake game +// that plans every route and walk on the built-in maps. +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp new file mode 100644 index 0000000000..03069d92a0 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -0,0 +1,913 @@ +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + * 00.00 to 00.05 are planned walks on the JSON maps, with the scenes' + * triggers from the Platinum and Diamond decomps (Segment00Lines) and + * Butters' button sequences for the scenes (#1717). They replace his + * recorded stick holds, and have NOT run on a console yet: where a scene + * leaves the player (pose_after) is the decomp's or a guess, marked + * "assumed", until a new-game run checks it. 00.04b to 00.04d were measured + * on the console (r72 to r261, 2026-10-30); 00.05 to 00.05c, measured there + * first, are now planned on Jubilife's map with the people and triggers of + * the Diamond decomp's zone events and scripts. Segment 01 ran on the + * console end to end (2026-10-31), its walks as events; all of it but + * 01.04's Gym is now planned on the maps the same way (2026-11-02), and + * has not run on a console as plans yet. + * + */ + +#include +#include +#include +#include +#include +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; +using Pokemon::Pose; +using Pokemon::StoryLeg; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +namespace{ + +constexpr Direction NORTH = Direction::NORTH; +constexpr Direction SOUTH = Direction::SOUTH; +constexpr Direction EAST = Direction::EAST; +constexpr Direction WEST = Direction::WEST; + +const char* const BEDROOM = "PlayerHouse2F"; +const char* const HOME = "PlayerHouse1F"; +const char* const BARRYS_HOUSE = "RivalHouse1F"; +const char* const BARRYS_ROOM = "RivalHouse2F"; +const char* const LAB = "RowansLab"; +const char* const TWINLEAF = "TwinleafTown"; +const char* const ROUTE_201 = "Route201"; +const char* const LAKEFRONT = "VerityLakefront"; +const char* const SANDGEM = "SandgemTown"; +const char* const ROUTE_202 = "Route202"; +const char* const JUBILIFE = "JubilifeCity"; +// No map of it: a pose there names the room, not its tiles. +const char* const TRAINERS_SCHOOL = "TrainersSchool"; +const char* const OREBURGH = "OreburghCity"; +const char* const GYM = "OreburghGym"; +// The mine's entrance hall (Diamond's MAP_OREBURGH_MINE_B1F) and the coal +// room below it, Roark's. +const char* const MINE_HALL = "OreburghMineB1F"; +const char* const MINE = "OreburghMine"; +const char* const GATE = "OreburghGate"; +const char* const ROUTE_203 = "Route203"; +const char* const ROUTE_204_SOUTH = "Route204South"; +const char* const RAVAGED_PATH = "RavagedPath"; +const char* const ROUTE_204_NORTH = "Route204North"; +const char* const FLOAROMA = "FloaromaTown"; + +Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, {TilePos{x, y}}, facing); +} +Pose at(const char* map, const char* point, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, {map_point(map, point)}, facing); +} +// The tile next to `point` of `map` on its `side`. +Pose next_to(const char* map, const char* point, Direction side, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, {map_point(map, point).next(side)}, facing); +} +// `point` of `map` or the tile on its `side`: where a scene may leave us. +Pose at_or_next_to(const char* map, const char* point, Direction side){ + const TilePos tile = map_point(map, point); + return Pokemon::tile_pose(map, {tile, tile.next(side)}); +} +Pose on(const char* map, const char* region){ + return Pokemon::tile_pose(map, map_region(map, region)); +} +// Somewhere in `map`, which has no tiles here: enough for a door step to +// know it goes out to another map. +Pose inside(const char* map){ + return Pokemon::tile_pose(map, {}); +} +// Columns from below Sandgem's Pokemon Center sign west to below its door. +int sandgem_sign_to_center_door(){ + return map_point(SANDGEM, "pokemon_center_sign").x - map_point(SANDGEM, "pokemon_center_door").x; +} +// The tile below the door `door` of `map` and its neighbours either side: +// a door is not lined up with one tile (Jubilife's Center, r85). +Pose door_front(const char* map, const char* door){ + const TilePos below = map_point(map, door).next(SOUTH); + return Pokemon::tile_pose(map, {below.next(WEST), below, below.next(EAST)}); +} + +StoryCheckpoint row( + std::string id, std::string title, std::optional start, std::vector steps, + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY +){ + StoryCheckpoint checkpoint; + checkpoint.id = std::move(id); + checkpoint.title = std::move(title); + checkpoint.start = std::move(start); + checkpoint.steps = std::move(steps); + checkpoint.on_failure = on_failure; + return checkpoint; +} + +StoryStep make(StoryStepKind kind, std::string name, std::string why){ + StoryStep step; + step.kind = kind; + step.name = std::move(name); + step.why = std::move(why); + return step; +} +StoryStep route(std::string name, Pose target, std::string why, std::string costs = ""){ + StoryStep step = make(StoryStepKind::ROUTE, std::move(name), std::move(why)); + step.target = std::move(target); + step.variant = std::move(costs); + return step; +} +StoryStep legs(std::string name, std::vector legs, std::string why, std::string variant = ""){ + StoryStep step = make(StoryStepKind::LEGS, std::move(name), std::move(why)); + step.legs = std::move(legs); + step.variant = std::move(variant); + return step; +} +// Legs that each walk until something stops them, as measured on the console. +StoryStep to_walls(std::string name, std::vector legs, std::string why){ + return PokemonBDSP::legs(std::move(name), std::move(legs), std::move(why), "to_wall"); +} +// `step`, after which the player stands on `pose`: a walk to a wall from +// a few tiles ends on one. +StoryStep anchored(StoryStep step, Pose pose){ + step.pose_after = std::move(pose); + return step; +} +// TODO(#114 coord events): the map schema has no scene triggers yet, so +// each row steps onto the decomp's trigger tile, named in its reason. +StoryStep step_into(std::string name, Direction direction, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::STEP, std::move(name), std::move(why)); + step.direction = direction; + step.pose_after = std::move(pose_after); + return step; +} +// Through a door or a cave mouth `direction` a D-pad tap at a time, each +// looked at, ending on the far map's location banner (the executor's +// "to_banner"): a hold can carry on past a fade it missed (r75, r219g). +// A tap after an unseen fade may step once more on the far side, so +// `pose_after` allows for it. +StoryStep tap_into(std::string name, Direction direction, std::string why, Pose pose_after){ + StoryStep step = step_into(std::move(name), direction, std::move(why), std::move(pose_after)); + step.variant = "to_banner"; + return step; +} +StoryStep talk(std::string name, std::string why){ + return make(StoryStepKind::TALK, std::move(name), std::move(why)); +} +// A talk to someone who wanders a tile either side of home along a row: +// asks north from each tile below the range, answers with A and steps back +// below home (the "talk_to_wanderer" event). +StoryStep talk_to_wanderer(std::string name, Pose pose_after, std::string why){ + StoryStep step = talk(std::move(name), std::move(why)); + step.variant = "talk_to_wanderer"; + step.pose_after = std::move(pose_after); + return step; +} +// Text to the overworld, with B; with A ("a") for lines that give an item. +StoryStep scene(std::string name, std::optional pose_after, std::string why, std::string button = ""){ + StoryStep step = make(StoryStepKind::MASH, std::move(name), std::move(why)); + step.pose_after = std::move(pose_after); + step.variant = std::move(button); + return step; +} +StoryStep battle(std::string name, bool may_lose, std::string why, std::string variant = ""){ + StoryStep step = make(StoryStepKind::BATTLE, std::move(name), std::move(why)); + step.may_lose = may_lose; + step.variant = std::move(variant); + return step; +} +// One of the story's events (PokemonBDSP_AutoStory_Events.h), by name. +StoryStep event(std::string name, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::GAME, name, std::move(why)); + step.variant = std::move(name); + step.pose_after = std::move(pose_after); + return step; +} +StoryStep heal(std::string name, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::HEAL, std::move(name), std::move(why)); + step.pose_after = std::move(pose_after); + return step; +} +// `battles` wild battles in the grass we stand in. +StoryStep train(size_t battles, Pose pose_after, std::string why){ + StoryStep step = make(StoryStepKind::TRAIN, "train", std::move(why)); + step.battles = battles; + step.pose_after = std::move(pose_after); + return step; +} +StoryStep save(){ + return make(StoryStepKind::SAVE, "save_here", "the next checkpoint starts here"); +} +StoryStep skip_if(StoryStep step, std::string flag){ + step.skip_if.push_back(std::move(flag)); + return step; +} + + +// 00.01's last stretch: Barry's Route 201 warning, then west to the Lakefront. +std::vector route_201_to_the_lakefront(){ + return { + // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. + // Barry's warning covers the whole exit path (14..19, 18), so it ends our walk + // north: exact legs to the row below it, never a run to a wall through it (r14 + // ran north to (15, 13) and back, into it and the row-19 pull-back, in a loop). + legs("below_barrys_warning", {{.direction = EAST, .tiles = 6}, {.direction = NORTH, .tiles = 18}}, + "along row 12, then up Twinleaf's exit to Route 201 (15, 19)"), + step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), + scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; he then stands north of us at (15, 17)"), + // West at once, never back south onto row 19, which pulls us back. + legs("away_from_barry", {{.direction = WEST, .tiles = 1}, {.direction = NORTH, .tiles = 1}, {.direction = WEST, .tiles = 2}}, + "to (12, 17): row 18 west of the path is trees (r16), the dirt goes on along row 17"), + // Route 201's lower level, row 16, clear of the grass, then the Lakefront. + route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), + }; +} + +// 00.01: down from the bedroom, Barry's house, and out to the Lakefront. +StoryCheckpoint bedroom_to_lakefront(){ + std::vector steps{ + // The top row: the Azure Flute's tile (7, 4), then east over the stairs' mat. + route("below_the_flute", at(BEDROOM, 7, 5, NORTH), "below the Azure Flute's tile"), + event("azure_flute_if_there", "take the Azure Flute if this save has one", at(BEDROOM, 7, 5, NORTH)), + legs("onto_the_flute_tile", {{.direction = NORTH, .tiles = 1}, {.direction = EAST, .tiles = 1}}, + "up onto (7, 4), then east onto the top of the stairs' mat (8, 4)"), + // The stairs are entered moving east from the top of the two-tile mat, + // the decomp's stairs point (8, 4); east from (8, 5) or north are walls (r8, r9). + // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). + step_into("down_the_stairs", EAST, "onto the stairs", at_or_next_to(HOME, "stairs", SOUTH)), + // Automatic on arrival (1F frame script): Mom walks over, talks, walks back to + // (7, 8). No input; Butters' 1.1 s south hold fell inside the fade (timeline 00.01 #2). + scene("mom_calls", at_or_next_to(HOME, "stairs", SOUTH), + "Mom calls the player over as they come down; we stay put"), + route("above_the_door", at(HOME, 6, 9, SOUTH), "past Mom at (7, 8) to above the door"), + step_into("mom_warning", SOUTH, "the door's tile (6, 10): Mom's warning (coord event, VAR_PLAYER_HOUSE_STATE 1)"), + scene("mom_warns_us", at(HOME, "door", SOUTH), "\"don't go into the tall grass\""), + step_into("out_of_the_house", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), + route("short_of_barrys_door", at(TWINLEAF, 10, 12, WEST), "a tile east of Barry's doorstep"), + step_into("barrys_door", WEST, "the trigger on his doorstep (9, 12)"), + // Barry bumps into us and knocks us back a tile, still facing north (decomp; + // r10: the first north tap after the scene gave no warp, the second did). + scene("barry_bursts_out", at(TWINLEAF, 9, 13, NORTH), "Barry runs out, bumps into us and runs back in", "a"), + scene("barry_goes_home", at(TWINLEAF, 9, 13, NORTH), "his second lines (Butters waited for two)", "a"), + legs("back_onto_barrys_doorstep", {{.direction = NORTH, .tiles = 1}}, "back to (9, 12); its trigger is spent"), + step_into("into_barrys_house", NORTH, "his door", at(BARRYS_HOUSE, "door", NORTH)), + legs("off_barrys_mat", {{.direction = NORTH, .tiles = 1}}, "off the door's mat, which no route starts on"), + // Like ours: stairs are entered sideways from the top of their mat, the + // stairs point (r11: north from (2, 4) only bumps the wall). + route("below_barrys_stairs", at(BARRYS_HOUSE, "stairs", EAST), "past his mother at (4, 6) to the top of the stairs' mat"), + step_into("up_barrys_stairs", EAST, "onto the stairs", at(BARRYS_ROOM, "stairs")), + // Automatic on arrival (2F frame script), not a talk: Barry runs past us down + // the stairs and the game walks us a tile south of them (decomp; Butters + // never walked to him). + scene("barry_runs_to_the_lake", at(BARRYS_ROOM, 3, 5, NORTH), "Barry's lines; he runs off to the lake", "a"), + // Butters went west from there straight into the stairs; ours are entered from + // the mat's top tile, so go there first (unchecked: which works). + route("above_barrys_stairs", at(BARRYS_ROOM, "stairs", WEST), "back to the top of the stairs' mat"), + step_into("down_barrys_stairs", WEST, "onto the stairs", at(BARRYS_HOUSE, "stairs")), + route("above_barrys_door", at(BARRYS_HOUSE, 6, 9, SOUTH), "back to his door"), + step_into("out_of_barrys_house", SOUTH, "out of his door", at(TWINLEAF, "barry_doorstep", SOUTH)), + }; + for (StoryStep& step : route_201_to_the_lakefront()){ + steps.push_back(std::move(step)); + } + return row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), std::move(steps)); +} + +std::vector intro(){ + StoryStep new_game = make(StoryStepKind::NEW_GAME, "intro", "language, appearance, the player's name, Barry's default name"); + new_game.pose_after = at(BEDROOM, "start", NORTH); + return { + row("00.00", "New game: the intro and the settings", std::nullopt, { + skip_if(new_game, "save_loaded"), + make(StoryStepKind::OPTIONS, "settings", "fast text, Send to Boxes automatic, no nickname prompt, Autosave off"), + }, StoryFailure::STOP), + bedroom_to_lakefront(), + }; +} + +std::vector lake_and_pokedex(){ + return { + row("00.02", "Lake Verity: the starter and the Starly battle", at(LAKEFRONT, "below_lake_entrance", NORTH), { + legs("to_the_lake_entrance", {{.direction = NORTH, .tiles = 1}}, "a tile short of the trigger"), + step_into("lake_entrance", NORTH, "the trigger (16..17, 12): Barry hears Rowan and the two walk in"), + event("starter_briefcase", "the lake's scene, to the open briefcase"), + event("choose_starter", "the starter from the program's option"), + battle("starly", true, "the Starly battle, with the starter alone", "starter"), + // The game walks us out of the lake, then the Lakefront's scene walks us 3 + // south: (17, 14) facing south, or a tile below (decomp; timeline 00.02 #5). + // Butters' sequence was B for a fixed 60 s; the scene rule ends it on + // the quiet overworld instead (unchecked on the console). + scene("after_starly", at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH), + "Rowan's lines and the walk out of the lake"), + }), + row("00.03", "Back to Sandgem: Rowan, Dawn and the Pokedex", at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH), { + route("short_of_rowan", at(ROUTE_201, 4, 14, EAST), "back east onto Route 201's lower level"), + step_into("rowans_stop", EAST, "the trigger (5, 14..16)"), + // BDSP, unlike Diamond: after Rowan leaves, Dawn's and Barry's lines, then + // the game fades to the house, where Mom gives the Running Shoes. The + // scene ends beside her on the rug, facing her: (6, 8) in r24 and r27, + // (5, 8) in r28. + scene("rowan_and_dawn", Pokemon::tile_pose(HOME, {TilePos{5, 8}, TilePos{6, 8}}, EAST), + "Rowan and Dawn come back; Barry, then home for the Running Shoes"), + // Mom's warning was spent in 00.01, so the door's tile (6, 10) is a plain + // door now: stepping onto it goes out (r27). + // Line up on the table's corner west first: a plan from either tile + // ran east into Mom at (7, 8), who talks (r29). + route("by_the_counter", at(HOME, 4, 8, WEST), "west against the table's corner (3, 8)"), + route("above_the_door_again", at(HOME, 6, 9, SOUTH), "to above the door"), + step_into("out_of_the_house_again", SOUTH, "onto the door's tile (6, 10), and out", at(TWINLEAF, "house_doorstep", SOUTH)), + // Through the grass now, with a starter: wild battles are run from. + route("short_of_dawn", at(ROUTE_201, 63, 5, EAST), "Twinleaf, then east along Route 201 to its east edge"), + // Dawn's scene starts as we come into Sandgem (trigger (4, 10..15)); + // walk until it does rather than to a tile (r35, r36). + legs("into_sandgem", {{.direction = EAST, .tiles = 8}}, "east into Sandgem until Dawn's scene starts", "to_dialogue"), + // Butters' sequence pressed B through Dawn's and Rowan's lines and + // the nickname question (No), then A for the Pokedex. The scene's + // B answers No too, and closes an item's lines as it did Barry's + // parcel (r45). Into the lab is a fade. + scene("rowan_gives_pokedex", at(LAB, "rowan_talk", NORTH), + "Dawn walks us to the lab: Rowan, no nickname, the Pokedex"), + }), + }; +} + +// West along Sandgem's path (row 12) until Route 201's banner shows: it +// shows a tile or a few past the border, Sandgem (-1, 12) = Route 201 (63, 5). +StoryStep route_201_by_the_path(){ + StoryStep step = legs("to_route_201", {{.direction = WEST, .tiles = 40}}, "run west along the path until Route 201's banner", "to_map"); + step.pose_after = Pokemon::tile_pose(ROUTE_201, { + TilePos{57, 5}, TilePos{58, 5}, TilePos{59, 5}, TilePos{60, 5}, TilePos{61, 5}, TilePos{62, 5}, TilePos{63, 5} + }, WEST); + return step; +} + +StoryStep out_of_the_lab_into_dawns_tour(){ + // Rowan's spot is in the door's column: run south out of the door. Dawn's + // tour plays on arrival, inside the step's settle (r43: a scene row after + // it waited 20 s for lines already played). She leaves us below the Mart's + // door (27, 10), a row or two down: r39's walk west from "row 11" ran on + // along row 12, which is open to Route 201. + return step_into("out_of_the_lab", SOUTH, "out of the lab; Dawn's tour of Sandgem", Pokemon::tile_pose(SANDGEM, { + TilePos{26, 11}, TilePos{27, 11}, TilePos{28, 11}, TilePos{26, 12}, TilePos{27, 12}, TilePos{28, 12} + }, NORTH)); +} + +// Anywhere in the door's column: the step runs north into the door, so the +// route can end where the south run along x 14 stops (row 26) and go east. +StoryStep home_to_the_doors_column(){ + return route("home", Pokemon::tile_pose(TWINLEAF, { + TilePos{20, 22}, TilePos{20, 23}, TilePos{20, 24}, TilePos{20, 25}, TilePos{20, 26} + }, NORTH), "west along Route 201 to Twinleaf, running from wild battles"); +} + +std::vector to_the_catch(){ + return { + row("00.04", "Home to Mom, then Dawn's catching tutorial on Route 202", at(LAB, "rowan_talk", NORTH), { + out_of_the_lab_into_dawns_tour(), + // Not west along the doors' row: running it took us into the lab at its + // mat (8, 11) (r41, r42). Row 12 is open into Route 201: run it until + // Route 201's banner shows, a few tiles in. + route("onto_the_path", at(SANDGEM, 29, 12, WEST), "to the east end of the path below the doors' row", "town"), + route_201_by_the_path(), + home_to_the_doors_column(), + // The game sets us a tile inside, off the mat: (6, 9) (r40: the old extra + // step north put us beside the TV, above Mom). + step_into("into_the_house", NORTH, "the house's door", at(HOME, 6, 9, NORTH)), + // Mom stands at (7, 7) now, a row above the decomp's (7, 8): from (6, 8) + // she was up and to the right; a tap north, east and A answered (r43). + route("beside_mom", at(HOME, 6, 7, EAST), "west of Mom at (7, 7)"), + talk("mom", "Mom"), + // Mom heals us, then Barry's mom comes in with his parcel; the scene + // leaves Mom at (6, 8), below us (r45). + scene("mom_heals_us", at(HOME, 6, 7, EAST), "Mom heals us; Barry's mom gives us his parcel"), + legs("around_mom", {{.direction = WEST, .tiles = 1}, {.direction = SOUTH, .tiles = 2}}, + "around Mom at (6, 8), to beside the door's column"), + // Stepping onto (6, 9), above the door, Mom calls us back ("Oh, Kosmos!", + // r46), gives us the hat and walks out with us; we end a tile west of the + // doorstep, beside her (r45). + legs("to_moms_call", {{.direction = EAST, .tiles = 1}}, "east above the door until Mom calls", "to_dialogue"), + scene("moms_hat", at(TWINLEAF, 19, 22, EAST), "Mom's hat; she walks out with us"), + route("short_of_the_catch_tutorial", at(ROUTE_202, 22, 27, WEST), "through Sandgem onto Route 202, east of its trigger column"), + step_into("catch_tutorial_trigger", WEST, "the trigger (21, 25..29), VAR_ROUTE_202_STATE 0"), + event("catch_tutorial", "Dawn's catching tutorial"), + event("catch_pokemon", "a Pokemon of our own from the grass", on(ROUTE_202, "catch_save_area")), + }), + // A Lv5 starter fainted to Route 202's trainers (r72): training, a + // heal, and Potions first, each row from the save the one before ends on. + row("00.04b", "After the catch: train and heal in Sandgem", on(ROUTE_202, "catch_save_area"), { + route("to_catch_grass", on(ROUTE_202, "catch_grass"), "into the training column's grass"), + train(3, on(ROUTE_202, "after_training_area"), "three wild battles with the best move"), + // One walk round the houses ended two tiles short (r252): down + // the road first, then the town plan. + route("into_sandgem", at(SANDGEM, "route_202_entry_stop"), "south down the road to above the Mart", "robust"), + route("to_center_sign", next_to(SANDGEM, "pokemon_center_sign", SOUTH), "below the Center's sign, its doorway in sight", "town"), + // From below the sign the finder read the signs' panels as + // doorways (r252, r255): taps instead. One tile short is fine. + legs("below_center_door", {{.direction = WEST, .tiles = sandgem_sign_to_center_door()}}, "taps west below the Center's door", "one_short_ok"), + heal("heal_sandgem", "Sandgem Town Pokemon Center", door_front(SANDGEM, "pokemon_center_door")), + }), + }; +} + +// Where Dawn's scene leaves 00.04d, and 00.05's save: below Jubilife's +// steps (45..47, 61), against the wall east of them, (48, 62). BDSP starts +// her scene as we come in below the steps, not on Diamond's trigger above +// them (45..48, 60): 00.05's old event went "a tile west, then up the +// steps" from its save, two taps north (2026-10-30). Not measured as a +// tile: the event's numbers and the map's (#134). +Pose below_jubilifes_steps(){ + return at(JUBILIFE, 48, 62); +} + +std::vector to_jubilife(){ + StoryStep potions = make(StoryStepKind::SHOP, "potions", + "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp)"); + potions.items = {{.item = "potion", .quantity = 10}}; + potions.variant = "sandgem_potions"; + potions.pose_after = at(ROUTE_202, "save_spot"); + // An event, not a step: it walks north and west until Dawn's scene + // starts. It leaves us on Jubilife, so the scene after it plays where + // we stand, with no fade to wait for. + const StoryStep stairs = event("jubilife_stairs", + "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)", + below_jubilifes_steps()); + return { + row("00.04c", "Potions, back to Route 202's save spot", door_front(SANDGEM, "pokemon_center_door"), { + potions, + }), + // From the one save spot: from both at once the planner gives up. + row("00.04d", "Route 202's trainers, Dawn in Jubilife", at(ROUTE_202, "save_spot"), { + route("north_to_jubilife", on(ROUTE_202, "jubilife_exit"), "north past Route 202's trainers, fighting who sees us"), + stairs, + scene("dawn_scene", below_jubilifes_steps(), "Dawn walks over while we stand still"), + }), + }; +} + +// Below the Trainers' School's door, where it lets us out (40, 41). +Pose below_the_school(){ + return next_to(JUBILIFE, "trainers_school_door", SOUTH, SOUTH); +} + +// 00.05: the steps as measured on the console (2026-10-30); the rest is +// planned on the map. +StoryCheckpoint the_town_map(){ + return row("00.05", "Jubilife: the Center, the Trainers' School and the Town Map", below_jubilifes_steps(), { + // D-pad taps, a tile each: the stick holds this was (200 ms west, + // 480 ms north) left us between tiles, and every leg after it a + // column off (r57 to r61). + legs("up_the_steps", {{.direction = WEST, .tiles = 1}, {.direction = NORTH, .tiles = 2}}, + "a tile west of the save, then up the steps to (47, 60)"), + // Planned on the map: the hand-measured walls (west to the sign, east + // across the road, north) leaned on the old turn's nudge and ended + // beside the Trainers' School once turns stopped stepping (r57, r58). + route("to_the_center", at(JUBILIFE, 52, 41, NORTH), + "below the Center's door (52, 40)", "town"), + // The kids south of the Center can push us aside: the heal lines up by sight. + heal("heal_jubilife", "in, the nurse, out onto the doorstep (r143a: check we are out)"), + // Same rule as the Center: plan to the tile below the door, then run in + // (r63: "eleven tiles west" counted on the old turn's step; the map says twelve). + route("to_the_school", next_to(JUBILIFE, "trainers_school_door", SOUTH, NORTH), + "below the School's door (40, 40)", "town"), + step_into("into_the_school", NORTH, "the School's door", inside(TRAINERS_SCHOOL)), + // Barry stands at the blackboard a tile west of the door's column, + // which is clear to the back wall: beside him there, face west and + // talk. A tile west by a hold went two in r149a, and the boy at the + // table stopped us. (The event walked to the wall again once when he + // did not answer.) + anchored(to_walls("up_to_barry", {{.direction = NORTH, .tiles = 10}}, "the door's column to the back wall, beside Barry"), + Pokemon::tile_pose(TRAINERS_SCHOOL, {}, WEST)), + talk("barry", "Barry, west of us at the blackboard"), + scene("barry_town_map", inside(TRAINERS_SCHOOL), "his lines to \"You obtained a Town Map!\", closed with A", "a"), + // Barry stands beside the door's column, which is clear down to the door. + step_into("out_of_the_school", SOUTH, "straight down the door's column and out", below_the_school()), + }); +} + +// 00.05b, from the Diamond decomp (zone event narc_0002, scripts +// scr_seq narc_0002; local = the decomp's world tile - (128, 736)): the +// president's coord event, row 40 from x 44 to 48 while +// VAR_POKETCH_CAMPAIGN_STATE is 1, walks him from (46, 35) to above us; +// then the three clowns, each with a question whose answer is Yes. +StoryCheckpoint the_coupons(){ + return row("00.05b", "Jubilife: the president and the clowns' coupons", below_the_school(), { + // From x 46 he walks straight down 4 to (46, 39); from the others he turns. + route("below_the_presidents_trigger", at(JUBILIFE, 46, 41, NORTH), "east along the School's row", "town"), + step_into("presidents_trigger", NORTH, "the trigger (44..48, 40)"), + scene("president_walks_up", at(JUBILIFE, 46, 40, NORTH), "he walks down to (46, 39) and sends us for the coupons"), + // He stays at (46, 39) until the TV station clown's coupon sends him + // home; a plan north from his column would walk into him. + legs("past_the_president", {{.direction = EAST, .tiles = 2}}, "east of his column"), + // He wanders (54..56, 33); he comes through (55, 33) every other move. + route("below_the_mart_clown", next_to(JUBILIFE, "clown_mart", SOUTH, NORTH), "up the road east of the Center", "town"), + talk_to_wanderer("mart_clown", next_to(JUBILIFE, "clown_mart", SOUTH, NORTH), + "the clown by the Mart: 'Do Pokemon grow by defeating others?' Yes; Coupon 1"), + // He stands still below the TV station's door, facing south. + route("below_the_tv_clown", next_to(JUBILIFE, "clown_tv_station", SOUTH, NORTH), "west and north to the TV station", "town"), + talk("tv_clown", "the clown below the TV station's door"), + // His script also puts the president back at (46, 35), facing south. + scene("tv_clowns_coupon", next_to(JUBILIFE, "clown_tv_station", SOUTH, NORTH), "\"Can a Pokemon hold an item?\" Yes; Coupon 3", "a"), + // He wanders (14..16, 18), behind the Poketch Company's sign (13, 19). + route("below_the_company_clown", next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), "west along the long road, north beside the sign", "town"), + talk_to_wanderer("company_clown", next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), + "the clown by the Poketch Company: 'Do moves have types?' Yes; Coupon 2"), + }); +} + +// 00.05c: the president trades the coupons for the Poketch, walks 9 north +// and is gone (decomp); then up to the street east to Route 203, where +// 01.01 walks east until Barry stops us. +StoryCheckpoint the_poketch(){ + const Pose below_the_president = next_to(JUBILIFE, "poketch_president", SOUTH, NORTH); + return row("00.05c", "Jubilife: the Poketch, up to the street east to Route 203", + next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), { + route("below_the_president", below_the_president, "east along the long road, down the main road", "town"), + talk("president", "the Poketch Company's president at (46, 35)"), + // BDSP adds a help panel on the Poketch; B closes it. + scene("the_poketch", below_the_president, "the coupons for the Poketch; he walks off north"), + // The street's rows 21 to 24 lead onto Route 203 at x 63. + route("to_the_route_203_street", at(JUBILIFE, 58, 22, EAST), "up the main road and east along the street", "town"), + }); +} + +std::vector jubilife(){ + return {the_town_map(), the_coupons(), the_poketch()}; +} + +// `tile` of `map`, or the tile past it `direction`: where a tap_into() may +// leave us. +Pose arrival(const char* map, TilePos tile, Direction direction){ + return Pokemon::tile_pose(map, {tile, tile.next(direction)}, direction); +} +// Where the Oreburgh Gate lets us out onto Route 203: its warp tile (55, 9) +// (Diamond's warp (246, 749), zone event narc_0329), walking west. The one +// tile only: from it and (54, 9) together the joined Route 203 and Jubilife +// find no plan. The tap stops at the fade, which puts us on the warp tile; +// r78 passed 01.06 from a save there (by the sign, the cave mouth east). +Pose out_of_oreburgh_gate(Direction facing = WEST){ + return at(ROUTE_203, 55, 9, facing); +} +// Barry's coord event on Route 203 (Diamond's zone event narc_0329), column +// 5 on rows 17 to 20 (the street's rows 21 to 24) while +// VAR_ROUTE_203_RIVAL_STATE is 0. He walks up, battles and runs off, and +// we stay on it (assumed; the event's walk east stopped there, on any row). +Pose on_barrys_trigger(){ + return Pokemon::tile_pose(ROUTE_203, {TilePos{5, 17}, TilePos{5, 18}, TilePos{5, 19}, TilePos{5, 20}}, EAST); +} +// Where Route 204's door (11, 1) lets us into the Ravaged Path: (19, 50), +// as in r237a. The tap stops at the fade, on that tile. +Pose into_the_ravaged_path(){ + return at(RAVAGED_PATH, "entrance", NORTH); +} + +// Where Oreburgh's welcome scene leaves us, Barry in the Center's door: "a row +// or two below the Center's front, a little west of its door" (01.03's +// save, 2026-10-29); "two tiles west of the door and a row down" (the old +// event). Not Diamond's: its man walks us to the Gym, (23, 23). +Pose after_the_welcome(){ + return Pokemon::tile_pose(OREBURGH, { + TilePos{43, 21}, TilePos{44, 21}, TilePos{45, 21}, TilePos{43, 22}, TilePos{44, 22}, TilePos{45, 22} + }); +} +// Below Oreburgh's Pokemon Center's door (47, 20), facing it. +Pose below_oreburghs_center(){ + return at(OREBURGH, 47, 21, NORTH); +} +// Out of Oreburgh's Pokemon Center, facing south: below its door, or on +// the map's doorstep (46, 21), from which 01.04's 20 taps west reach the +// Gym's door column (r217d, r218d). +Pose out_of_oreburgh_center(){ + return Pokemon::tile_pose(OREBURGH, {TilePos{47, 21}, map_point(OREBURGH, "pokemon_center_doorstep")}, SOUTH); +} + +// 01.05, planned on Oreburgh's and the Gate's maps with the Diamond decomp's +// zone events (narc_0044, narc_0250) and Platinum's script for the scene +// (scripts_oreburgh_city.s, the same trigger and variable). Barry's coord +// event, column 6 on rows 12 to 15 while VAR_OREBURGH_CITY_STATE is 2 (after +// Roark: the old walk met his "*thud*" there, 2026-10-29), runs him into us +// from the east: the game pushes us a tile west, facing east, and he +// leaves. The Gate's way west is row 22, across the Hiker's trigger (7, 22), +// spent in 01.02. +StoryCheckpoint oreburgh_to_route_203(){ + return row("01.05", "Oreburgh to Route 203", next_to(GYM, "roark", SOUTH, NORTH), { + // 01.04 talks to Roark from below him, up the clear middle column. + tap_into("out_of_the_gym", SOUTH, "down the middle column (5, 4..23) and out of the door (5, 24)", + arrival(OREBURGH, map_point(OREBURGH, "gym_doorstep"), SOUTH)), + route("east_of_barrys_trigger", at(OREBURGH, 7, 13, WEST), + "west along the street, north up the stairs west of the houses, west along the Gate's row", "town"), + step_into("barrys_trigger", WEST, "the trigger (6, 12..15)"), + scene("barry_bumps_into_us", at(OREBURGH, 5, 13, EAST), "Barry runs into us, talks of Eterna, and leaves (assumed: the push)"), + route("by_the_gate", next_to(OREBURGH, "gate_mouth", EAST, WEST), "west to the Gate's warp tile (2, 13)"), + // The warp's other end is the Gate's (27, 22), beside its east mouth. + tap_into("into_oreburgh_gate", WEST, "the Gate's mouth (1, 13)", arrival(GATE, {27, 22}, WEST)), + route("through_the_gate", at(GATE, 4, 22, WEST), "west along row 22 to the warp tile by the west mouth (3, 22)"), + // Route 203's banner or a failure here: the old walk west "until the + // banner" went on into the pond's pocket (r75). 01.06 heads south. + tap_into("out_of_oreburgh_gate", WEST, "the west mouth (3, 22)", out_of_oreburgh_gate()), + }); +} + +// 01.02, planned on Route 203's, the Gate's and Oreburgh's maps with the +// Diamond decomp's zone events (narc_0329, narc_0250, narc_0044): east +// along Route 203 to the Gate, fighting the trainers who see us +// (route_203_trainer_sights()); the Hiker's coord event (7, 22) while +// VAR_OREBURGH_GATE_1F_HIKER_STATE is 1; east along row 22 past Diana and +// Curtis (oreburgh_gate_trainer_sights()); then the coord event of the man +// at Oreburgh's entrance, column 10 on rows 12 to 15 while +// VAR_OREBURGH_CITY_STATE is 0. Each mouth is a tap to its banner, as in +// 01.05; its warp's other end is the tile beside the far mouth. +// +// Diana and Curtis are not on the trainer list: the walker fights them as +// trainers the map lacks, one per route (in the grid sim, the second of +// two on one leg stopped its walk on to the wall unseen). On the list, +// the grid sim, which does not know them beaten, had them stop 01.05's +// walk back. +StoryCheckpoint route_203_to_oreburgh(){ + return row("01.02", "Route 203 to Oreburgh", on_barrys_trigger(), { + // One known tile to plan from: from all four of the trigger's rows + // at once the planner finds no plan. Its column is open from row + // 17 down to the cliff below row 20. + anchored(to_walls("south_to_the_cliff", {{.direction = SOUTH, .tiles = 4}}, "down the trigger's column to the cliff"), + at(ROUTE_203, 5, 20, SOUTH)), + route("by_the_gate", out_of_oreburgh_gate(EAST), "east along Route 203 to the Gate's warp tile (55, 9)"), + tap_into("into_oreburgh_gate", EAST, "the Gate's west mouth (56, 9)", arrival(GATE, {4, 22}, EAST)), + route("west_of_the_hikers_trigger", at(GATE, 6, 22, EAST), "east along row 22"), + step_into("hikers_trigger", EAST, "the trigger (7, 22): the Hiker walks down to us"), + // The "Hidden Moves" card after his lines is no dialogue box, and + // can come up a moment after them; the scene's B closes it. + scene("the_hiker", at(GATE, 7, 22, EAST), "HM Rock Smash, then the Hidden Moves card"), + route("past_curtis", at(GATE, 20, 22, EAST), "east along row 22 past Camper Curtis's column (17), fighting him"), + route("by_the_east_mouth", at(GATE, 27, 22, EAST), "east past Picnicker Diana's column (24), fighting her"), + tap_into("into_oreburgh", EAST, "the Gate's east mouth (28, 22)", + arrival(OREBURGH, map_point(OREBURGH, "gate_mouth").next(EAST), EAST)), + route("west_of_the_welcome", at(OREBURGH, 9, 13, EAST), "east along row 13"), + step_into("welcome_trigger", EAST, "the trigger (10, 12..15): the man walks over"), + // He walks us to the Pokemon Center between his lines; Barry joins + // there with more lines after each walk. + scene("oreburgh_welcome", after_the_welcome(), "the man's lines, the walk to the Center, and Barry's"), + }); +} + +// 01.03, planned on Oreburgh's map and the coal room's (Diamond's zone +// events narc_0044 and narc_0197): Barry in the Center's door says Roark +// went to the mine (Roark ignored A until then, r101 and r104); down the +// door's column to the mine's warp tiles (44..48, 59); straight down the +// entrance hall to its warps (11..13, 21); in the coal room down to +// Roark, whose scene ends with him gone (Platinum's +// scripts_oreburgh_mine_b2f.s: he smashes the rock east of him and walks +// off east); back up and out; and the Center's nurse, so 01.04 starts on +// the doorstep. The Center's door is the decomp's (47, 20): out of +// Sandgem's and Jubilife's Centers the console stood in their doors' +// columns. The map's doorstep (46, 21) is the Gym's door column plus the +// 20 taps of 01.04 (r217d, r218d), which do not say where the taps began; +// the heal lines up on the lit doorway if north from (47, 21) misses. +// +// The hall is walked on counted taps, not held: it is a cave, and a wild +// battle on a hold toward a fade would read as the fade. Its warp tiles +// are only entered on the step after, which meets no wild Pokemon. Down, +// from the city's (46, 59) we land on its (12, 1); up, from the coal +// room's warp we land on its (11, 21) and come out on the city's (45, 59), +// below the mine's sign (the old walk north stopped against it, r103). +StoryCheckpoint oreburgh_mine_to_roark(){ + const Pose below_roark = next_to(MINE, "roark", SOUTH, NORTH); + return row("01.03", "Oreburgh Mine to Roark", after_the_welcome(), { + // The fence south of the Center's east corner (column 49) stops a + // walk east on these rows a tile east of the door, as by hand. + anchored(to_walls("to_the_centers_fence", {{.direction = EAST, .tiles = 6}, {.direction = NORTH, .tiles = 2}}, + "east to the fence below the Center's east corner, north to the Center's front"), at(OREBURGH, 48, 21, NORTH)), + anchored(legs("below_the_centers_door", {{.direction = WEST, .tiles = 1}}, "a tap west below the door"), + below_oreburghs_center()), + // He stands in the door the first time; walking into him reads as a wall. + talk("barry", "Barry in the Center's door"), + scene("barrys_lines", below_oreburghs_center(), "Roark has gone to the mine"), + // The Hiker wanders (46..48, 25..27), in the door's column: the + // route goes round him. + route("above_the_mine", at(OREBURGH, 46, 58, SOUTH), "down the door's column to above the mine's warp tiles", "town"), + step_into("into_the_mine", SOUTH, "the warp tile (46, 59)", at(MINE_HALL, 12, 1, SOUTH)), + // We land on the warp tile we came by, (12, 1): 19 taps end on row + // 20, a step short of the warp row. + legs("down_the_hall", {{.direction = SOUTH, .tiles = 19}}, "down column 12 of the entrance hall"), + step_into("into_the_coal_room", SOUTH, "the hall's warp tiles (11..13, 21)", + Pokemon::tile_pose(MINE, {TilePos{15, 2}, TilePos{15, 1}}, SOUTH)), + // One known tile to plan from: the corridor's column 15 ends at the + // boulder (15, 10), where the old walk south stopped too. + anchored(to_walls("down_to_the_boulder", {{.direction = SOUTH, .tiles = 9}}, "down the corridor to the boulder (15, 10)"), + at(MINE, 15, 9, SOUTH)), + route("below_roark", below_roark, "down the corridor and the room's east side, west along the bottom row"), + // His first line comes after the camera zooms in on us (r98); his + // rocks either side answer A too, but he is the one north of us. + talk("roark", "Roark, north of us between his rocks"), + scene("roark_smashes_the_rock", below_roark, "his lines, Rock Smash on the rock east of him, and he walks off"), + route("below_the_coal_rooms_warp", at(MINE, 15, 2, NORTH), "back up the corridor"), + step_into("up_to_the_hall", NORTH, "the warp tile (15, 1)", at(MINE_HALL, 11, 21, NORTH)), + legs("up_the_hall", {{.direction = NORTH, .tiles = 19}}, "up column 11 of the entrance hall"), + step_into("out_of_the_mine", NORTH, "the hall's warp tiles (11..13, 1)", + Pokemon::tile_pose(OREBURGH, {TilePos{45, 59}, TilePos{45, 58}}, NORTH)), + anchored(to_walls("up_to_the_mines_sign", {{.direction = NORTH, .tiles = 3}}, "north against the mine's sign (45, 56)"), + at(OREBURGH, 45, 57, NORTH)), + route("below_the_centers_door_again", below_oreburghs_center(), "up to below the Center's door", "town"), + heal("heal_oreburgh", "the nurse; out onto the doorstep, where 01.04 starts", out_of_oreburgh_center()), + }); +} + +// Below Rowan's trigger in Jubilife: (47, 8), the east tile under the +// trigger (45..47, 7), which (48, 7) beside it is not. +Pose below_rowans_trigger(){ + return at(JUBILIFE, 47, 8, NORTH); +} +// Where Rowan's scene leaves us: walked up to him at (47, 5), then a tile +// west in front of the grunt (Platinum's movements; assumed for BDSP). +Pose after_the_grunts(){ + return Pokemon::tile_pose(JUBILIFE, {TilePos{46, 5}, TilePos{47, 5}}); +} + +// 01.07, from the Diamond decomp (zone event narc_0002) and Platinum's +// script (scripts_jubilife_city.s, the same trigger and variable): the +// coord event (45..47, 7) while VAR_JUBILIFE_CITY_STATE is 3 walks us up to +// Rowan, his and the grunts' lines, then the tag battle with Dawn; after +// it they leave. +StoryCheckpoint rowan_and_the_grunts(){ + return row("01.07", "Jubilife: Rowan, Dawn and the Galactic grunts", below_rowans_trigger(), { + step_into("rowans_trigger", NORTH, "the trigger (45..47, 7): the game walks us up to Rowan"), + battle("galactic_grunts", false, "the lines, then the tag battle with Dawn", "doubles"), + scene("rowan_and_dawn_leave", after_the_grunts(), + "Rowan's and Dawn's lines, then Bebe walks over with hers (r80 saved over her lines)"), + }); +} + +// 01.09 on the Ravaged Path's map (Platinum's, checked on the console in +// r235b): up the stairs to west of the rock (22, 43), smash it, east to +// above the way out (28, 45). Its other end is Route 204's (20, 26). +StoryCheckpoint through_the_ravaged_path(){ + const Pose west_of_the_rock = at(RAVAGED_PATH, "before_rocks.2", EAST); + return row("01.09", "Ravaged Path", into_the_ravaged_path(), { + route("west_of_the_rock", west_of_the_rock, "up the stairs and along the corridor"), + event("smash_the_rock", "Rock Smash on the rock east of us", west_of_the_rock), + route("above_the_way_out", at(RAVAGED_PATH, "above_exit", SOUTH), "east through the rock's tile"), + // One tap and Route 204's banner: a timed walk south went on past + // the cave's mouth to the cliff (r81). + tap_into("out_of_the_ravaged_path", SOUTH, "the way out (28, 45)", at(ROUTE_204_NORTH, "cave_exit", SOUTH)), + }); +} + +// 01.10: the line a human walked up Route 204's north half (2026-10-31, +// r246a to r251a), corner to corner (Test_Route204NorthLine checks each +// stretch is one straight leg), then north between the flower beds into +// Floaroma. Its trainers who see us are fought: Taylor on the line, the +// Twins as a pair. +StoryCheckpoint route_204_to_floaroma(){ + std::vector steps; + const std::vector line = map_region(ROUTE_204_NORTH, "floaroma_waypoints"); + for (size_t i = 0; i < line.size(); i++){ + steps.push_back(route(std::format("line_{}", i + 1), Pokemon::tile_pose(ROUTE_204_NORTH, {line[i]}), + "a corner of the human's line", "line")); + } + // 01.11 walks on from here: west 8 along row 29, north to the old woman. + steps.push_back(route("into_floaroma", at(FLOAROMA, "route_204_arrival", NORTH), "north up column 11 into Floaroma", "line")); + steps.push_back(save()); + return row("01.10", "Route 204 to Floaroma", at(ROUTE_204_NORTH, "cave_exit", SOUTH), std::move(steps)); +} + +std::vector to_oreburgh(){ + return { + // From 00.05c's end on the street east to Route 203. + row("01.01", "Jubilife to Barry", at(JUBILIFE, 58, 22, EAST), { + route("west_of_barrys_trigger", at(ROUTE_203, 4, 18, EAST), "east along the street onto Route 203", "town"), + step_into("barrys_trigger", EAST, "the trigger (5, 17..20): Barry walks over"), + anchored(battle("barry", true, "Barry's battle; a loss goes on, as it did on the console"), on_barrys_trigger()), + }), + route_203_to_oreburgh(), + oreburgh_mine_to_roark(), + // Stays an event: after a loss it walks back from the Center with + // the experience the battles gave, which a reload of this row's + // save would take away (Roark won twice by hand before he lost). + row("01.04", "Oreburgh Gym", out_of_oreburgh_center(), { + event("gym_until_roark_beaten", "west into the gym, up it to Roark; again from the Center after a loss", + next_to(GYM, "roark", SOUTH, NORTH)), + }), + oreburgh_to_route_203(), + }; +} + +// 01.11 on Floaroma's map: the old woman (3, 25) and the old man (2, 25) +// west of her stand still above the flower field's bottom row (r251a). +// The walk to her is the one that ran on the console: 8 taps west along +// row 29, then north until she stops us (r250a: 3 steps, then bumps). +StoryCheckpoint floaromas_old_couple(){ + const Pose below_the_old_woman = next_to(FLOAROMA, "old_woman", SOUTH, NORTH); + const Pose below_the_old_man = next_to(FLOAROMA, "old_man", SOUTH, NORTH); + return row("01.11", "Floaroma's old couple", at(FLOAROMA, "route_204_arrival", NORTH), { + legs("west_along_the_field", {{.direction = WEST, .tiles = 8}}, "8 taps west along row 29"), + anchored(to_walls("up_to_the_old_woman", {{.direction = NORTH, .tiles = 5}}, "north until she stops us"), + below_the_old_woman), + talk("old_woman", "she gives Mew when a Let's Go save is on the console"), + scene("old_woman_talks", below_the_old_woman, "her lines"), + // He stands above that tile, so talk() turning north only turns. + anchored(legs("below_the_old_man", {{.direction = WEST, .tiles = 1}}, "a tile west"), below_the_old_man), + talk("old_man", "he gives Jirachi with a Sword/Shield save"), + scene("old_man_talks", below_the_old_man, "his lines"), + save(), + }); +} + +std::vector to_floaroma(){ + return { + row("01.06", "Route 203 to Jubilife", out_of_oreburgh_gate(), { + // Trainers who saw us going east in 01.02 are beaten; any other + // sees us here and is fought (route_203_trainer_sights()). + route("below_rowans_trigger", below_rowans_trigger(), + "down Route 203 (its ledges only south), west along it into Jubilife's street, north up the road", "town"), + }), + rowan_and_the_grunts(), + row("01.08", "Jubilife to the Ravaged Path", after_the_grunts(), { + // One known tile to plan from: from both of the scene's ends + // the joined Jubilife and Route 204 find no plan. Row 5 is open + // from x 44 to 49, a wall at 43. + anchored(to_walls("west_to_the_wall", {{.direction = WEST, .tiles = 4}}, "west along row 5 to the wall"), + at(JUBILIFE, 44, 5, WEST)), + // Lass Sarah's east sight crosses the street (r233a, r234a): she + // may stop us for a battle. + route("below_the_ravaged_path", at(ROUTE_204_SOUTH, "below_door", NORTH), + "north out of Jubilife, up Route 204's street and grass to below the door (11, 1)", "town"), + // Its warp (Diamond's zone event narc_0330) lands us on the + // cave's (19, 50), as in r237a. + tap_into("into_the_ravaged_path", NORTH, "the door (11, 1)", into_the_ravaged_path()), + }), + through_the_ravaged_path(), + route_204_to_floaroma(), + floaromas_old_couple(), + }; +} + +// The map the save screen's card names at each row's start, as the console +// read it there (lab-B's logs, 2026-10-31 to 11-02). A building's card +// names its town: 00.04 starts in Rowan's lab. +const std::map& start_save_cards(){ + static const std::map cards{ + {"00.01", "Twinleaf Town"}, + {"00.02", "Verity Lakefront"}, + {"00.03", "Verity Lakefront"}, + {"00.04", "Sandgem Town"}, + {"00.04b", "Route 202"}, + {"00.04c", "Sandgem Town"}, + {"00.04d", "Route 202"}, + {"00.05", "Jubilife City"}, + {"00.05b", "Jubilife City"}, + {"00.05c", "Jubilife City"}, + {"01.01", "Jubilife City"}, + {"01.02", "Route 203"}, + {"01.03", "Oreburgh City"}, + {"01.04", "Oreburgh City"}, + {"01.05", "Oreburgh City"}, + {"01.06", "Route 203"}, + {"01.07", "Jubilife City"}, + {"01.08", "Jubilife City"}, + {"01.09", "Ravaged Path"}, + {"01.10", "Route 204"}, + {"01.11", "Floaroma Town"}, + }; + return cards; +} + +std::vector make_table(){ + std::vector table; + for (auto part : {intro, lake_and_pokedex, to_the_catch, to_jubilife, jubilife, to_oreburgh, to_floaroma}){ + for (StoryCheckpoint& checkpoint : part()){ + table.push_back(std::move(checkpoint)); + } + } + for (StoryCheckpoint& checkpoint : table){ + const auto card = start_save_cards().find(checkpoint.id); + if (checkpoint.on_failure == StoryFailure::RELOAD_AND_RETRY && card != start_save_cards().end()){ + checkpoint.saves_in = card->second; + } + } + return table; +} + +} + + +const std::vector& story_checkpoints(){ + static const std::vector checkpoints = make_table(); + return checkpoints; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h new file mode 100644 index 0000000000..cd29187eb4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h @@ -0,0 +1,39 @@ +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + * The story from a new game to Floaroma, 00.00 to 01.11, as rows of the + * shared story steps (Pokemon/AutoStory/Pokemon_StorySteps.h): plain + * data, as FRLG's table is, so the tests plan its routes and run it + * against a fake game. Each row but 00.00 saves at its start, which is + * where the row before it ends, and only if the save screen's card names + * the row's map (saves_in). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H + +#include +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Checkpoints 00.00 to 01.11, in order: segment 00 (the intro to the +// Poketch) and segment 01 (Jubilife to Floaroma). +const std::vector& story_checkpoints(); + +// Mom gives the Running Shoes in this row's step: from then on, B held +// with the D-pad runs (r24, r27). +constexpr const char* RUNNING_SHOES_ROW = "00.03"; +constexpr const char* RUNNING_SHOES_STEP = "rowan_and_dawn"; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp new file mode 100644 index 0000000000..43d2818e0b --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -0,0 +1,260 @@ +/* BDSP AutoStory - Story table Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/AutoStory/Pokemon_StoryEvents.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StoryTable_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Direction; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + +// The variants each kind does itself, besides the story's events. +const std::map>& built_in_variants(){ + static const std::map> variants{ + {StoryStepKind::ROUTE, {"", "town", "robust", "line"}}, + {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "to_dialogue", "to_map"}}, + {StoryStepKind::STEP, {"", "to_banner"}}, + {StoryStepKind::MASH, {"", "a"}}, + {StoryStepKind::BATTLE, {"", "doubles", "starter"}}, + }; + return variants; +} + +// The table: unique ids in order, each row lints, and every step's variant +// is one its kind does or one of the story's events. +class Test_StoryTableLints : public UnitTest{ +public: + Test_StoryTableLints() + : UnitTest("PokemonBDSP::AutoStory - Story table: rows lint, variants known") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const StoryEvents& events = story_events(); + std::set ids; + std::string previous; + for (const StoryCheckpoint& row : story_checkpoints()){ + if (!ids.insert(row.id).second || row.id <= previous){ + return row.id + ": repeated or out of order."; + } + previous = row.id; + if ((row.on_failure == StoryFailure::RELOAD_AND_RETRY) == row.saves_in.empty()){ + return row.id + ": a row names the map its start save is made in (saves_in) exactly when it saves first."; + } + const std::vector problems = Pokemon::lint_story_checkpoint(row); + if (!problems.empty()){ + return row.id + ": " + problems[0]; + } + for (const Pokemon::StoryStep& step : row.steps){ + const auto built_in = built_in_variants().find(step.kind); + const bool known = events.contains(step.variant) || + (step.kind != StoryStepKind::GAME && step.variant.empty()) || + (built_in != built_in_variants().end() && built_in->second.contains(step.variant)); + if (!known){ + return std::format("{} {}: no event or {} way named \"{}\".", + row.id, step.name, Pokemon::story_step_kind_name(step.kind), step.variant); + } + } + } + if (!ids.contains("00.00") || !ids.contains("01.11") || story_checkpoints().front().on_failure != StoryFailure::STOP){ + return "Expected 00.00 (which stops on a failure) to 01.11."; + } + return true; + } +}; + +// Where a row's last step says it leaves the player: the end of its scene, +// or where its route (or heal) goes. None for a step that does not say. +std::optional end_pose(const StoryCheckpoint& row){ + if (row.steps.empty()){ + return std::nullopt; + } + const Pokemon::StoryStep& last = row.steps.back(); + return last.pose_after ? last.pose_after : last.target; +} + +// Each row starts where the row before it ends: every tile its start allows +// is one the last row's end allows, on the same map. Checked where the last +// step says where it ends. +class Test_StoryTableRowsJoin : public UnitTest{ +public: + Test_StoryTableRowsJoin() + : UnitTest("PokemonBDSP::AutoStory - Story table: each row starts where the last one ends") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector& rows = story_checkpoints(); + for (size_t i = 1; i < rows.size(); i++){ + const std::optional end = end_pose(rows[i - 1]); + const std::optional& start = rows[i].start; + if (!end || !start){ + continue; + } + const std::vector ends = Pokemon::tiles_of(*end); + bool joins = start->map == end->map; + for (const TilePos& pos : Pokemon::tiles_of(*start)){ + joins = joins && std::find(ends.begin(), ends.end(), pos) != ends.end(); + } + if (!joins){ + return std::format("{} starts at {}, but {} ends at {}.", + rows[i].id, Pokemon::to_string(*start), rows[i - 1].id, Pokemon::to_string(*end)); + } + } + return true; + } +}; + +struct LadyLeg{ + Direction direction; + size_t tiles; // 0: walk until the wall + TilePos end; // world tile + std::string name; +}; + +// 00.04c's Potions: the Mart lady's walk (mart_lady()) is walks until +// stopped, so on the map each leg ends at its wall: from a tile to three +// below the Center's door, a tap east and north to the Center's front, +// west to the Lab's sign, a row south and west to her grass's edge, north +// to the trees on her row (she stands east of there), back a row south and +// east to Route 201's sign, a row south and east to the trees east of the +// Mart, and north to the front row's end. +class Test_MartLadyWalk : public UnitTest{ +public: + Test_MartLadyWalk() + : UnitTest("PokemonBDSP::AutoStory - 00.04: the walk to the Poke Mart lady and back") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const StoryWorld& maps = story_maps(); + const auto sandgem = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; + const auto route201 = [&](const TilePos& pos){ return maps.to_world(MAP_ROUTE_201, pos); }; + const std::vector legs{ + {Direction::EAST, 1, sandgem({18, 0}), "a tap east under the Center's wall"}, + {Direction::NORTH, 0, sandgem({18, 11}), "north to the Center's front"}, + {Direction::WEST, 0, sandgem({6, 11}), "west to the Lab's sign"}, + {Direction::SOUTH, 1, sandgem({6, 12}), "a row south onto the path"}, + {Direction::WEST, 0, route201({42, 5}), "west to her grass's edge"}, + {Direction::NORTH, 0, route201({42, 3}), "north to the trees, on her row"}, + {Direction::SOUTH, 1, route201({42, 4}), "a row south onto the signs' row"}, + {Direction::EAST, 0, route201(map_point("Route201", "border_sign").next(Direction::WEST)), "east to Route 201's sign"}, + {Direction::SOUTH, 1, route201({54, 5}), "a row south onto the path"}, + {Direction::EAST, 0, sandgem({29, 12}), "east to the trees east of the Mart"}, + {Direction::NORTH, 0, sandgem(map_point("SandgemTown", "front_row_east_end")), "north to the front row's end"}, + }; + // Out of the Center's door we stand a tile to three below it (r261). + for (int below = 1; below <= 3; below++){ + const std::string error = follow(maps, legs, sandgem({17, 10 + below}), logger); + if (!error.empty()){ + return std::format("From {} below the door: {}", below, error); + } + } + return true; + } + +private: + static std::string follow(const StoryWorld& maps, const std::vector& legs, TilePos at, Logger& logger){ + for (const LadyLeg& leg : legs){ + if (leg.tiles == 0){ + at = slide_to_wall(maps, at, leg.direction).stop; + }else{ + const std::optional next = PokemonBDSP::step(maps, at, leg.direction); + if (!next){ + return leg.name + ": blocked at " + describe(maps, at) + "."; + } + at = *next; + } + // The tap east lands on whichever row we stood on. + if (&leg != &legs.front() && !(at == leg.end)){ + return leg.name + ": ends at " + describe(maps, at) + ", not " + describe(maps, leg.end) + "."; + } + logger.log(leg.name + ": " + describe(maps, at)); + } + return ""; + } +}; + +// 01.10 on Route 204's north half: the line a human walked (2026-10-31), +// corner to corner, from the Ravaged Path's exit into Floaroma. Each +// stretch must plan as the one straight tap leg the human took, on the map +// as it is: a detour here means the map or the line changed. +class Test_Route204NorthLine : public UnitTest{ +public: + Test_Route204NorthLine() + : UnitTest("PokemonBDSP::AutoStory - Route 204 North: the line into Floaroma") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const StoryWorld& route = route204_north_maps(); + const std::vector waypoints = map_region("Route204North", "floaroma_waypoints"); + struct Expected{ Direction direction; size_t tiles; }; + const std::vector expected{ + {Direction::SOUTH, 1}, {Direction::WEST, 9}, {Direction::NORTH, 5}, {Direction::EAST, 8}, + {Direction::NORTH, 10}, {Direction::WEST, 8}, {Direction::NORTH, 7}, + }; + if (waypoints.size() != expected.size()){ + return std::format("Route 204 North: {} waypoints, expected {}.", waypoints.size(), expected.size()); + } + std::vector at{map_point("Route204North", "cave_exit")}; + for (size_t i = 0; i < waypoints.size(); i++){ + const TilePos goal = waypoints[i]; + const std::string name = std::format("Route 204 North stretch {} to {}", i + 1, describe(route, goal)); + const auto plan = plan_robust_route(route, at, [&](const TilePos& pos){ return pos == goal; }, waypoint_plan_costs()); + if (!plan){ + return name + ": no plan."; + } + if (plan->size() != 1){ + return std::format("{}: {} legs, expected the one straight leg.", name, plan->size()); + } + const TileLeg& leg = (*plan)[0].leg; + logger.log(name + ": " + to_string(leg)); + if (leg.ends_at_wall || leg.direction != expected[i].direction || leg.tiles != expected[i].tiles){ + return std::format("{}: planned {}, expected an open leg of {} tiles.", name, to_string(leg), expected[i].tiles); + } + if ((*plan)[0].after.size() != 1 || !((*plan)[0].after[0] == goal)){ + return name + ": the leg does not end exactly on the waypoint."; + } + at = {goal}; + } + if (!in_floaroma_path(at[0])){ + return "Route 204 North: the last waypoint is not in the path into Floaroma."; + } + return true; + } +}; + +} + + +void add_tests_AutoStory_StoryTable(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h new file mode 100644 index 0000000000..ffde11f4bb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h @@ -0,0 +1,25 @@ +/* BDSP AutoStory - StoryTable Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Tests that need no console: the story table: rows lint, every step's variant is known, and the walks its events plan. +void add_tests_AutoStory_StoryTable(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp new file mode 100644 index 0000000000..487994981c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp @@ -0,0 +1,325 @@ +/* BDSP AutoStory - Talks + * + * From: https://github.com/PokemonAutomation/ + * + * Talking to people and fighting the trainers among them, and the looks + * at the screen the story's events decide by. + * + */ + +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "CommonTools/OCR/OCR_Routines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Bag.h" +#include "PokemonBDSP_AutoStory_Battle.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_Talks.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +[[noreturn]] void fail_step(VideoStream& stream, const std::string& message){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, message, stream); +} + +// True if a dialogue opens within `timeout`: someone who stopped us as the +// walk ended reads as BLOCKED, and their box comes up a moment later. +bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Milliseconds timeout){ + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + return wait_until(stream, context, timeout, {{dialog}}) == 0; +} + +bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std::string& label, TalkExpect expect, BattleFormat format){ + ShortDialogDetector dialog; + OverworldDetector overworld; + BattleMenuDetector battle_menu(BattleType::TRAINER); + // Roark's battle menu read as STANDARD, not TRAINER, and nothing fought + // it (r210d): either counts here, as only a trainer's battle can start. + BattleMenuDetector battle_menu_standard(BattleType::STANDARD); + BlackScreenDetector black; + auto quiet_since = std::chrono::steady_clock::now(); + for (size_t check = 0; check < 120; check++){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + if (!frame){ + fail_step(stream, label + ": No video frame."); + } + if (battle_menu.detect(*frame.frame) || battle_menu_standard.detect(*frame.frame) || black.detect(*frame.frame)){ + route_status(stream, "Fighting " + label + "."); + fight_story_battle(stream, context, label, format); + // Lines after the battle (Roark's badge, r211d) keep the menu + // from opening: finish them before the Potion. + if (dialogue_opens(stream, context, 3000ms)){ + finish_dialogue(stream, context, BUTTON_B, label + " after the battle"); + } + if (!in_pokemon_center(stream)){ + use_potion_first_pokemon(stream, context, 0.5); + } + return true; + } + const bool talking = dialog.detect(*frame.frame); + if (!talking && overworld.detect(*frame.frame)){ + if (expect != TalkExpect::BATTLE && std::chrono::steady_clock::now() - quiet_since >= 4s){ + route_status(stream, label + " only talked."); + return false; + } + pbf_wait(context, 300ms); + continue; + } + quiet_since = std::chrono::steady_clock::now(); + if (talking){ + pbf_press_button(context, BUTTON_B, 80ms, 1100ms); + }else{ + pbf_wait(context, 300ms); + } + } + fail_step(stream, label + ": neither a battle nor the overworld after the dialogue."); +} + +// A rough check that we stand in a Pokemon Center: its floor fills the lower +// half of the screen with warm yellow, where the gym is grey and routes are +// green. After a battle it tells a loss (a whiteout ends at the counter) +// from a win. +bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent){ + const ImageRGB32 small = screen.scale_to(192, 108); + size_t warm = 0, total = 0; + for (size_t y = 54; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + const int r = (pixel >> 16) & 0xff, g = (pixel >> 8) & 0xff, b = pixel & 0xff; + total++; + if (r > 200 && g > 150 && b < 150 && r - b > 70){ + warm++; + } + } + } + if (percent){ + *percent = warm * 100 / total; + } + return warm * 10 > total * 3; +} + +bool pokemon_center_on_screen(const ImageViewRGB32& screen, size_t* percent){ + return center_floor_on_screen(screen, percent) && indoors(screen); +} + +bool in_pokemon_center(VideoStream& stream){ + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + size_t percent = 0; + const bool inside = pokemon_center_on_screen(*frame.frame, &percent); + stream.log("[Center check] warm floor " + std::to_string(percent) + " %, " + + (indoors(*frame.frame) ? "a room" : "outside") + ": " + + (inside ? "in a Pokemon Center." : "not in a Pokemon Center.")); + return inside; +} + +// The Oreburgh Mine's hall and corridors are dim: mean brightness 45 to 64 +// on the console, against 99 to 116 for the city and its yard (2026-10-31). +bool in_dim_cave(VideoStream& stream, uint64_t below){ + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + const ImageRGB32 small = frame.frame->scale_to(192, 108); + uint64_t sum = 0; + for (size_t y = 0; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); + } + } + const uint64_t mean = sum / (3 * 192 * 108); + stream.log("[Cave check] mean brightness " + std::to_string(mean) + "."); + return mean < below; +} + +// Mean of the three channels over a small copy of the frame, 0 to 255. +uint64_t mean_brightness(const ImageViewRGB32& screen){ + const ImageRGB32 small = screen.scale_to(192, 108); + uint64_t sum = 0; + for (size_t y = 0; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); + } + } + return sum / (3 * 192 * 108); +} + +// Fights a trainer whose dialogue has opened, with whatever starter we +// have. Someone who only talks leaves no battle to fight. +void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name){ + pbf_mash_button(context, BUTTON_B, 5000ms); + context.wait_for_all_requests(); + BattleMenuWatcher battle_menu(BattleType::TRAINER, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + if (wait_until(stream, context, 8s, {{battle_menu}, {battle_dialog}}) < 0){ + stream.log("[Route] " + name + ": no battle followed the talk."); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + return; + } + fight_trainer(stream, context, name); + context.wait_for_all_requests(); + // The next trainer may be a few tiles on: check HP now. With no + // Potion in the Bag this only looks. + use_potion_first_pokemon(stream, context, 0.5); +} + +bool sign_text_matches(const std::string& text, const std::string& key){ + std::string lower; + for (char ch : text){ + lower += (char)std::tolower((unsigned char)ch); + } + return lower.find(key) != std::string::npos; +} + +namespace{ + +// Where a sign's box shows: the bottom of the screen, as a dialogue's. The +// Lab's is grey, Route 201's green; opening either changes the area far +// more than the overworld behind it moves standing still. +const ImageFloatBox SIGN_BOX(0.19, 0.80, 0.64, 0.18); +// Its text, inside the margins. +const ImageFloatBox SIGN_TEXT(0.22, 0.82, 0.58, 0.15); +constexpr double SIGN_BOX_OPENED = 20.0; + +bool dialogue_showing(VideoStream& stream, ProControllerContext& context){ + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + return screen && ShortDialogDetector().detect(*screen.frame); +} + +// After a talk opened: reads the first box, then presses `next` once +// per line, 1.5 s apart, and only while a box shows: a press after it +// has closed would start the talk over. +void finish_talk(VideoStream& stream, ProControllerContext& context, DialogueRead& read, Button next, const std::string& label){ + // The first box's text, inside its white margins. + // For the log only; a talk is checked by its box opening and closing. + VideoSnapshot screen = stream.video().snapshot(); + if (screen && story_reads_text()){ + read.text = OCR::ocr_read(story_language(), extract_box_reference(*screen.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + } + stream.log("[Talk] " + label + ": the dialogue reads \"" + read.text + "\"."); + size_t quiet = 0; + // Barry's scene, to the Town Map, took more than 20 (r150a). + for (size_t presses = 0; presses < 80 && quiet < 2; presses++){ + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 1500ms, {{dialog}}) == 0){ + quiet = 0; + pbf_press_button(context, next, 80ms, 0ms); + }else{ + quiet++; + } + } + read.closed = !dialogue_showing(stream, context); + stream.log("[Talk] " + label + (read.closed ? ": closed." : ": still open.")); +} + +} + +DialogueRead talk_facing(VideoStream& stream, ProControllerContext& context, Direction direction, Button next, const std::string& label){ + route_turn(stream, context, direction); + return talk_ahead(stream, context, next, label); +} + +DialogueRead talk_ahead(VideoStream& stream, ProControllerContext& context, Button next, const std::string& label){ + DialogueRead read; + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher opened(COLOR_CYAN); + if (wait_until(stream, context, 2000ms, {{opened}}) != 0){ + stream.log("[Talk] " + label + ": no dialogue opened."); + return read; + } + read.opened = true; + finish_talk(stream, context, read, next, label); + return read; +} + +DialogueRead talk_walking(VideoStream& stream, ProControllerContext& context, Direction direction, size_t tiles, const std::string& label){ + DialogueRead read; + const auto [x, y] = dpad_of(direction); + const bool across = direction == Direction::EAST || direction == Direction::WEST; + const Milliseconds limit = (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN) + 800ms; + const WalkResult walked = walk_to_talk(stream, context, run_from_wild_battle, {x, y}, limit, label); + if (walked.stop != WalkStop::DIALOG){ + stream.log("[Talk] " + label + ": nobody answered."); + return read; + } + read.opened = true; + finish_talk(stream, context, read, BUTTON_A, label); + return read; +} + +DialogueRead read_sign(VideoStream& stream, ProControllerContext& context, Direction facing, const std::string& label){ + DialogueRead read; + route_turn(stream, context, facing); + // Let the camera settle after the turn: its drift read as a box + // opening (r132a). + pbf_wait(context, 800ms); + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + // The text types out over about a second; B before it has finished + // only finishes it (r131a). + pbf_press_button(context, BUTTON_A, 80ms, 1500ms); + context.wait_for_all_requests(); + VideoSnapshot shown = stream.video().snapshot(); + if (!before || !shown){ + return read; + } + const double opened = walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*shown.frame, SIGN_BOX)); + read.opened = opened > SIGN_BOX_OPENED; + if (!read.opened){ + stream.log("[Sign] " + label + ": no box opened (change " + std::to_string(opened) + ")."); + return read; + } + // Tesseract read the Lab's white-on-grey text as "e R O e IO" (r131a). + // Only to say which sign, when there is a reader for the game's + // language; the box opening and closing is the check. + if (story_reads_text()){ + read.text = OCR::ocr_read(story_language(), extract_box_reference(*shown.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + } + stream.log("[Sign] " + label + ": reads \"" + read.text + "\" (change " + std::to_string(opened) + ")."); + for (size_t presses = 0; presses < 3 && !read.closed; presses++){ + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + read.closed = after && walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*after.frame, SIGN_BOX)) < SIGN_BOX_OPENED; + } + stream.log("[Sign] " + label + (read.closed ? ": closed." : ": still open.")); + return read; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h new file mode 100644 index 0000000000..8f64bf9a1b --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h @@ -0,0 +1,102 @@ +/* BDSP AutoStory - Talks + * + * From: https://github.com/PokemonAutomation/ + * + * Talking to people and fighting the trainers among them, and the looks + * at the screen the story's events decide by. Measured on the console + * (segment 01, 2026-10-29 to 10-31; the Jubilife talks, 2026-10-30). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Talks_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Talks_H + +#include +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP_AutoStory_Battle.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Fails the step with a screenshot of the screen now. +[[noreturn]] void fail_step(VideoStream& stream, const std::string& message); + +// True if a dialogue opens within `timeout`: someone who stopped us as the +// walk ended reads as BLOCKED, and their box comes up a moment later. +bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Milliseconds timeout); + +// For a dialogue that has opened: B through it. A trainer's ends in a +// battle, which is fought with the best move (Potions when HP runs low), +// followed by a Potion if HP is under half. Anyone else's ends back in the +// overworld. Returns whether there was a battle. Between a trainer's last +// line and the battle the overworld shows with no dialogue for a moment, +// so "only talked" needs 4 s of it, and never comes when `expect` is +// BATTLE. `format` DOUBLES for tag and twin battles. A lost battle ends in +// the last Pokemon Center; see in_pokemon_center(). +enum class TalkExpect{ + TALK_OR_BATTLE, + BATTLE, +}; +bool talk_or_fight( + VideoStream& stream, ProControllerContext& context, const std::string& label, + TalkExpect expect = TalkExpect::TALK_OR_BATTLE, BattleFormat format = BattleFormat::SINGLES +); + +// Fights a trainer whose dialogue has opened, with whatever starter we +// have, then a Potion if HP is under half. Someone who only talks leaves +// no battle to fight. How the story's planned walks fight (RouteTrainerFight). +void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name); + +// A rough check that we stand in a Pokemon Center: its floor fills the lower +// half of the screen with warm yellow, where the gym is grey and routes are +// green. After a battle it tells a loss (a whiteout ends at the counter) +// from a win. `percent`, if given, gets the warm share of the lower half. +bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent = nullptr); +// In a Pokemon Center: its floor on screen, and a room (indoors()). Floor +// colour alone read Jubilife's yellow plaza as a Center (63 %, r79) and +// Sandgem's and Oreburgh's streets at 30 to 35 %; of 635 lab frames, the +// two together pick out exactly the Center interiors. +bool pokemon_center_on_screen(const ImageViewRGB32& screen, size_t* percent = nullptr); +bool in_pokemon_center(VideoStream& stream); +// The Oreburgh Mine's hall and corridors are dim: mean brightness 45 to 64 +// on the console, against 99 to 116 for the city and its yard (2026-10-31). +bool in_dim_cave(VideoStream& stream, uint64_t below = 85); +// Mean of the three channels over a small copy of the frame, 0 to 255. +uint64_t mean_brightness(const ImageViewRGB32& screen); + +// What a talk showed: whether a dialogue opened, whether it had closed +// after, and what OCR read in its first box. +struct DialogueRead{ + bool opened = false; + bool closed = false; + std::string text; +}; +// Whether a sign's OCR text has `key` (lower case), ignoring case. +bool sign_text_matches(const std::string& text, const std::string& key); +// Faces `direction` and presses A. If a dialogue opens, reads its first +// box, then presses `next` once per line, 1.5 s apart, while it shows. +DialogueRead talk_facing(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, Button next, const std::string& label); +// Presses A the way we face, without the turn talk_facing() taps first: a +// turn tap the way we already face is a step. +DialogueRead talk_ahead(VideoStream& stream, ProControllerContext& context, Button next, const std::string& label); +// Walks `direction`, pressing A after each short stretch, for at most +// `tiles` tiles' time until someone answers; then one A per line, 1.5 s +// apart, while the box shows. +DialogueRead talk_walking(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, size_t tiles, const std::string& label); +// Faces `facing` and presses A: whether a box opened at the bottom of +// the screen (a sign's may be green, not a dialogue's), what OCR reads +// in it, and whether B closed it. +DialogueRead read_sign(VideoStream& stream, ProControllerContext& context, Pokemon::Direction facing, const std::string& label); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp index c38ff46daf..0c62971e4e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -14,6 +14,7 @@ #include "Utils/PokemonBDSP_AutoStory_Indoors.h" #include "Utils/PokemonBDSP_AutoStory_Location.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Talks.h" #include "PokemonBDSP_AutoStory_Walk_Tests.h" namespace PokemonAutomation{ @@ -237,6 +238,46 @@ class Test_LocationBannerShowing : public UnitTest{ } }; +// pokemon_center_on_screen(): the Center's warm floor in a room. r79 read +// Jubilife's yellow plaza as a Center and called a won battle lost. +class Test_PokemonCenterOnScreen : public UnitTest{ +public: + Test_PokemonCenterOnScreen() + : UnitTest("PokemonBDSP::AutoStory - a Pokemon Center is its floor in a room, not a yellow plaza") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + constexpr uint32_t FLOOR = 0xfff0c860; // the Center's warm yellow + constexpr uint32_t WALL = 0xffe08030; // its orange back wall + constexpr uint32_t GRASS = 0xff60b040; + struct Case{ + std::string name; + uint32_t top; + uint32_t bottom; + double dark_bottom; // how much of the screen's height, from the bottom, is black + bool center; + }; + const std::vector cases{ + {"a Center: wall over floor, black below it", WALL, FLOOR, 0.15, true}, + {"Jubilife's plaza: yellow paving, no black edge", FLOOR, FLOOR, 0, false}, + {"a street: grass over yellow paving", GRASS, FLOOR, 0, false}, + {"a grey gym in a room", 0xff808080, 0xff909090, 0.15, false}, + }; + for (const Case& item : cases){ + ImageRGB32 screen(1920, 1080); + const size_t dark_from = (size_t)(1080 * (1 - item.dark_bottom)); + for (size_t y = 0; y < 1080; y++){ + for (size_t x = 0; x < 1920; x++){ + screen.pixel(x, y) = y >= dark_from ? 0xff000000 : y < 540 ? item.top : item.bottom; + } + } + if (pokemon_center_on_screen(screen) != item.center){ + return item.name + (item.center ? ": not read as a Center." : ": read as a Center."); + } + } + return true; + } +}; + } @@ -248,6 +289,7 @@ void add_tests_AutoStory_Walk(UnitTestDatabase& database){ database.add(); database.add(); database.add(); + database.add(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp new file mode 100644 index 0000000000..4495803e9d --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp @@ -0,0 +1,250 @@ +/* BDSP AutoStory - Walks + * + * From: https://github.com/PokemonAutomation/ + * + * Walks measured on the console that the planner does not make: straight + * holds and D-pad taps, walks until something stops us, and the ways in + * and out of doors. + * + */ + +#include +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Walks.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +std::pair dpad_of(Direction direction){ + switch (direction){ + case Direction::NORTH: + return {0, 1}; + case Direction::EAST: + return {1, 0}; + case Direction::SOUTH: + return {0, -1}; + case Direction::WEST: + return {-1, 0}; + } + return {0, 0}; +} + +void walk_until_stopped(VideoStream& stream, ProControllerContext& context, Direction direction, size_t tiles, const std::string& label){ + const bool across = direction == Direction::EAST || direction == Direction::WEST; + const auto [x, y] = dpad_of(direction); + // Twice every tile's time, and time for the wall to read as still: it + // stops at the wall anyway. One tile's time each fell short once the + // turn stopped stepping us ahead (r57: west to Jubilife's sign ended + // on the time, a column short). + const Milliseconds limit = ROUTE_START_DELAY + 800ms + + (int64_t)tiles * 2 * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN); + // Turn first: the first press of a new way only turns us, the screen + // holds still meanwhile, and that read as a wall two tiles into the + // walk west to the Lab's sign (r132a). + route_turn(stream, context, direction); + const WalkResult walked = walk_until(stream, context, run_from_wild_battle, {x, y}, limit, label); + stream.log("[Walk] " + label + ": stopped on " + walk_stop_name(walked.stop) + "."); + if (walked.stop == WalkStop::TRANSITION || walked.stop == WalkStop::DIALOG){ + fail_step(stream, label + ": " + walk_stop_name(walked.stop) + " instead of a stop."); + } + // The leg was measured to end against a wall, so holding toward it + // for every tile's time leaves us there whatever the screen shows: + // people walking or a TV screen nearby kept it changing and the stop + // went unseen (r162a, r167a, r171a). Seeing the stop only ends the + // hold early; a wrong place is caught by the next gate (a sign, a + // door, someone who answers). + if (walked.stop == WalkStop::LIMIT){ + stream.log("[Walk] " + label + ": held for " + std::to_string(tiles) + + " tiles' time without seeing the stop; taking the wall as reached."); + } +} + +void hold_for(VideoStream& stream, ProControllerContext& context, Direction direction, Milliseconds hold, const std::string& label){ + const auto [x, y] = dpad_of(direction); + const WalkResult walk = walk_for(stream, context, run_from_wild_battle, {x, y}, hold, label); + if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ + fail_step(stream, label + ": " + walk_stop_name(walk.stop) + "."); + } +} + +void tap_tiles(VideoStream& stream, ProControllerContext& context, Direction direction, size_t tiles, const std::string& label){ + const auto [x, y] = dpad_of(direction); + stream.log("[Walk] " + label + ": " + std::to_string(tiles) + " taps."); + for (size_t i = 0; i < tiles; i++){ + pbf_move_left_joystick(context, {x, y}, 160ms, 400ms); + } + pbf_wait(context, 800ms); + context.wait_for_all_requests(); +} + +void nudge(VideoStream& stream, ProControllerContext& context, Direction direction, const std::string& label){ + route_turn(stream, context, direction); + walk_for(stream, context, run_from_wild_battle, {direction == Direction::EAST ? 1.0 : -1.0, 0.0}, 80ms, label); +} + +bool door_north_by_scene(VideoStream& stream, ProControllerContext& context, const std::string& label){ + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, {0, 1}, 1200ms, label); + context.wait_for(1500ms); + if (up.stop == WalkStop::TRANSITION){ + return true; + } + // The School's fade is not always black enough to read as one: r146b + // went in (changes 156, 261) and read "limit". A whole new scene is + // through, as leave_door_south() counts it. + VideoSnapshot after = stream.video().snapshot(); + const bool through = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE; + if (through){ + stream.log("[Route] " + label + ": a new scene; through the door."); + } + return through; +} + +// One tile south, for a player facing another way. +void step_south(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 1), 200ms); + context.wait_for_all_requests(); +} + +// Holds the stick toward `dir` for `tiles` tiles, turning first. +void hold_tiles(VideoStream& stream, ProControllerContext& context, Direction dir, size_t tiles){ + route_turn(stream, context, dir); + const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; + const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; + pbf_move_left_joystick(context, {x, y}, route_open_hold(dir, tiles), 200ms); + context.wait_for_all_requests(); +} + +WalkResult walk_dir( + VideoStream& stream, ProControllerContext& context, Direction dir, Milliseconds limit, const std::string& label, + const std::function& arrived +){ + const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; + const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; + return walk_until(stream, context, run_from_wild_battle, {x, y}, limit, label, arrived); +} + +// `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the +// player moves a whole tile per tap (measured above), so counted legs do +// not drift the way stick holds do. +void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label){ + stream.log("[Walk] " + label + ": " + std::to_string(taps) + " D-pad taps."); + // No turn tap: a 160 ms tap is a whole step from any facing, so N taps + // are N tiles (r80: the old 160 ms "turn" tap walked one tile too many, + // two tiles west in the Ravaged Path, not one). Turning only matters to + // talk to someone, and they block like a wall, so a tap toward them turns. + size_t moved = 0; + // Count only taps that moved the view: one after the turn was lost and + // the leg ended a tile short (r215d). A tile's scroll changes the view by + // about 100; standing still, under 15. A tap that does not move is someone + // in the way: people walk about these streets (r220g: blocked on tap 9 + // for 12 s while a passer-by stood in our row). Wait for them to move on, + // up to 30 s, before giving up. + const auto deadline = current_time() + std::chrono::milliseconds(taps * 1000 + 30000); + while (moved < taps && current_time() < deadline){ + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + pbf_press_dpad(context, dpad, 160ms, 700ms); + context.wait_for_all_requests(); + const VideoSnapshot after = stream.video().snapshot(); + const double change = before && after ? walk_screen_change(*before.frame, *after.frame) : 100; + if (change > 30){ + moved++; + continue; + } + stream.log("[Walk] " + label + ": tap " + std::to_string(moved + 1) + " did not move (view change " + std::to_string(change) + "); someone in the way? Waiting."); + if (dialogue_opens(stream, context, 300ms)){ + talk_or_fight(stream, context, label + ": someone in the way"); + } + context.wait_for(1500ms); + } + if (moved < taps){ + fail_step(stream, label + ": only " + std::to_string(moved) + " of " + std::to_string(taps) + " D-pad taps moved us."); + } +} + +// Through a door straight north of us: tap up to face it, hold up into it, +// and wait for its dark fade (by hand: brightness 7, about 1 s after). +bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label){ + pbf_press_dpad(context, DPAD_UP, 160ms, 300ms); + pbf_press_dpad(context, DPAD_UP, 300ms, 0ms); + context.wait_for_all_requests(); + const auto start = current_time(); + while (current_time() - start < 5000ms){ + context.wait_for(100ms); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && mean_brightness(*frame.frame) < 25){ + stream.log("[Route] " + label + ": the screen went dark; through the door."); + context.wait_for(2000ms); + return true; + } + } + stream.log("[Route] " + label + ": no dark screen after the D-pad step north."); + return false; +} + +// For a player inside a Pokemon Center after a whiteout, at the counter: +// out through the door. The door is straight south of the nurse in most +// Centers; in Oreburgh's it was two tiles east of where we stood (by hand). +void walk_out_of_center(VideoStream& stream, ProControllerContext& context, const std::string& label){ + finish_dialogue(stream, context, BUTTON_B, label + ": the nurse"); + const int shifts[] = {0, 2, -4}; + for (int shift : shifts){ + if (shift != 0){ + hold_tiles(stream, context, shift > 0 ? Direction::EAST : Direction::WEST, (size_t)std::abs(shift)); + } + // A tile at a time, looking after each, so we stop on the doorstep: + // walk_until() looks only between 400 ms holds, and r205d held on + // through the fade and stepped a tile further south. + bool out = false; + for (size_t tile = 0; tile < 10 && !out; tile++){ + // D-pad taps: whole tiles (the first only turns us from the nurse). + pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); + context.wait_for(1500ms); + if (dialogue_opens(stream, context, 100ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": someone in the Center"); + } + const VideoSnapshot frame = stream.video().snapshot(); + size_t percent = 100; + if (frame){ + center_floor_on_screen(*frame.frame, &percent); + } + out = percent < 30; + } + if (out){ + // "No floor" also reads true on the door's black fade, and the game + // takes no input until it ends: the first D-pad taps after it were + // lost and the gym's door ended a tile away (r215d, r216d; a shot + // per press by hand showed the black frame). Wait for daylight. + const auto start = current_time(); + while (current_time() - start < 6000ms){ + context.wait_for(100ms); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && mean_brightness(*frame.frame) > 40){ + break; + } + } + context.wait_for(1000ms); + return; + } + } + fail_step(stream, label + ": could not find the Pokemon Center's door."); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h new file mode 100644 index 0000000000..5672d71d00 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h @@ -0,0 +1,80 @@ +/* BDSP AutoStory - Walks + * + * From: https://github.com/PokemonAutomation/ + * + * Walks measured on the console that the planner does not make: straight + * holds and D-pad taps, walks until something stops us, and the ways in + * and out of doors. Each fails the step with a screenshot when the walk + * ends where it should not (a dialogue or a door on a walk to a wall). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walks_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Walks_H + +#include +#include +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The stick's (x, y) for `direction`: y is up the screen. +std::pair dpad_of(Pokemon::Direction direction); + +// Walks `direction` until something stops us, for at most `tiles` tiles' +// time, turning first. Holding on for the whole time counts as stopped: +// the leg was measured to end against a wall. Fails on a transition or a +// dialogue. +void walk_until_stopped(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, size_t tiles, const std::string& label); +// Holds the stick `direction` for `hold` from standing still (200 ms is a +// row, turning included; 160 ms a tile across, facing that way already). +// Fails on a transition or a dialogue. +void hold_for(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, Milliseconds hold, const std::string& label); +// `tiles` one-tile taps `direction` in a row, facing that way already, +// without waiting for the screen to settle in between: six of these moved +// exactly six tiles on the console, where six single holds moved seven +// (2026-10-30). +void tap_tiles(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, size_t tiles, const std::string& label); +// Half a tile `direction` (EAST or WEST), facing it first. +void nudge(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, const std::string& label); +// Walks north; true if a door took us through, by its fade or by a whole +// new scene (the Trainers' School's fade is not always black, r146b). +bool door_north_by_scene(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// One tile south, for a player facing another way. +void step_south(VideoStream& stream, ProControllerContext& context); +// Holds the stick toward `dir` for `tiles` tiles, turning first. +void hold_tiles(VideoStream& stream, ProControllerContext& context, Pokemon::Direction dir, size_t tiles); +// walk_until() toward `dir`. +WalkResult walk_dir( + VideoStream& stream, ProControllerContext& context, Pokemon::Direction dir, Milliseconds limit, const std::string& label, + const std::function& arrived = nullptr +); + +// `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the +// player moves a whole tile per tap, so counted legs do not drift the way +// stick holds do. The first tap toward a new way only turns the player, so +// turn first. +void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label); +// Through a door straight north of us: tap up to face it, hold up into it, +// and wait for its dark fade (by hand: brightness 7, about 1 s after). +bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label); +// For a player inside a Pokemon Center after a whiteout, at the counter: +// out through the door. The door is straight south of the nurse in most +// Centers; in Oreburgh's it was two tiles east of where we stood (by hand). +void walk_out_of_center(VideoStream& stream, ProControllerContext& context, const std::string& label); + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 6ed34f2e3f..5db22e18b2 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -338,6 +338,8 @@ file(GLOB CORE_LIBRARY_SOURCES Source/NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController_from_Keyboard.h Source/NintendoSwitch/Controllers/Procon/NintendoSwitch_ProControllerState.cpp Source/NintendoSwitch/Controllers/Procon/NintendoSwitch_ProControllerState.h + Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.cpp + Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h Source/NintendoSwitch/NintendoSwitch_Settings.cpp Source/NintendoSwitch/NintendoSwitch_Settings.h Source/NintendoSwitch/Options/NintendoSwitch_CodeEntrySettingsOption.cpp @@ -733,8 +735,6 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.5.h Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.cpp Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.h - Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickView.cpp - Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickView.h Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickWidget.cpp Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickWidget.h Source/CommonFramework/VideoPipeline/Backends/MediaServicesQt6.cpp @@ -952,7 +952,6 @@ file(GLOB LIBRARY_SOURCES Source/Controllers/NullControllerWidget.h Source/Controllers/PABotBase2/SerialPABotBase2_SelectorWidget.cpp Source/Controllers/PABotBase2/SerialPABotBase2_SelectorWidget.h - Source/Controllers/PABotBase2/SerialPABotBase_StatusThread.h Source/Controllers/SerialPort/SerialPortPollerQt.cpp Source/Controllers/SerialPort/SerialPortPollerQt.h Source/Integrations/DiscordIntegrationSettings.cpp @@ -1131,7 +1130,6 @@ file(GLOB LIBRARY_SOURCES Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x16_x64_AVX2.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x32_x64_AVX512.h - Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x4_Default.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x64_x64_AVX512.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x8_arm64_NEON.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x8_x64_SSE42.h @@ -1153,7 +1151,6 @@ file(GLOB LIBRARY_SOURCES Source/Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_64x8_x64_SSE42.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_64xH_Default.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_Debugging.h - Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrix.h Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.h Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.tpp Source/Kernels/BinaryMatrix/Kernels_SparseBinaryMatrixCore.h @@ -1357,8 +1354,6 @@ file(GLOB LIBRARY_SOURCES Source/NintendoSwitch/Inference/NintendoSwitch_SelectedSettingDetector.h Source/NintendoSwitch/Inference/NintendoSwitch_StartGameUserSelectDetector.cpp Source/NintendoSwitch/Inference/NintendoSwitch_StartGameUserSelectDetector.h - Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.cpp - Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h Source/NintendoSwitch/Inference/NintendoSwitch_UpdatePopupDetector.cpp Source/NintendoSwitch/Inference/NintendoSwitch_UpdatePopupDetector.h Source/NintendoSwitch/NintendoSwitch_ConsoleHandle.cpp @@ -1556,6 +1551,36 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Resources/Pokemon_PokemonNames.h Source/Pokemon/Resources/Pokemon_PokemonSlugs.cpp Source/Pokemon/Resources/Pokemon_PokemonSlugs.h + Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.cpp + Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.cpp Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.cpp @@ -1586,8 +1611,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h - Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.cpp - Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_PokeballSpriteMatcher.cpp Source/PokemonBDSP/Inference/PokemonBDSP_PokeballSpriteMatcher.h Source/PokemonBDSP/Inference/PokemonBDSP_ReceivePokemonDetector.cpp @@ -1630,12 +1653,16 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h Source/PokemonBDSP/Options/PokemonBDSP_ShortcutDirection.cpp Source/PokemonBDSP/Options/PokemonBDSP_ShortcutDirection.h - Source/PokemonBDSP/Panels_PokemonBDSP.cpp - Source/PokemonBDSP/Panels_PokemonBDSP.h Source/PokemonBDSP/PokemonBDSP_Panels.cpp Source/PokemonBDSP/PokemonBDSP_Panels.h Source/PokemonBDSP/PokemonBDSP_Settings.cpp Source/PokemonBDSP/PokemonBDSP_Settings.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -1652,28 +1679,33 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_BattleMoves.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.h - Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp - Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h - Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp - Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.cpp @@ -1682,28 +1714,12 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.cpp Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomousState.cpp @@ -2104,8 +2120,6 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonLA/Options/PokemonLA_TravelLocation.h Source/VideoGames/PokemonLA/Options/QtWidgets/PokemonLA_CustomPathTableWidget.cpp Source/VideoGames/PokemonLA/Options/QtWidgets/PokemonLA_CustomPathTableWidget.h - Source/VideoGames/PokemonLA/Panels_PokemonLA.cpp - Source/VideoGames/PokemonLA/Panels_PokemonLA.h Source/VideoGames/PokemonLA/PokemonLA_Locations.cpp Source/VideoGames/PokemonLA/PokemonLA_Locations.h Source/VideoGames/PokemonLA/PokemonLA_Panels.cpp @@ -2334,8 +2348,6 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonLZA/Programs/PokemonLZA_GameEntry.h Source/VideoGames/PokemonLZA/Programs/PokemonLZA_HyperspaceNavigation.cpp Source/VideoGames/PokemonLZA/Programs/PokemonLZA_HyperspaceNavigation.h - Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MegaShardFarmer.cpp - Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MegaShardFarmer.h Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MenuNavigation.cpp Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MenuNavigation.h Source/VideoGames/PokemonLZA/Programs/PokemonLZA_PostKillCatcher.cpp diff --git a/devtools/maps/bdsp_maps.py b/devtools/maps/bdsp_maps.py index 6454bb3b47..1d54c8ac99 100644 --- a/devtools/maps/bdsp_maps.py +++ b/devtools/maps/bdsp_maps.py @@ -125,6 +125,9 @@ # walk north into the hall went up column 14 (the warp is column 15: a # stick hold drifts, or BDSP's stairs are wider). 'OreburghMine': {'header': 'OREBURGH_MINE_B2F', 'rect': (0, 0, 32, 32)}, + # B1F, the entrance hall: 01.03 crosses it on column 12 going down and + # column 11 coming up (the console's walks went straight through). + 'OreburghMineB1F': {'header': 'OREBURGH_MINE_B1F', 'rect': (0, 0, 32, 32)}, # Platinum's matrix 051 (Diamond's matrix is not in the cache), rows # 0..53. Console (2026-10-31, r235b and D-pad probes): in from the south # door onto (19, 50), up the stairs (18..20, 45..47) to the wall at row @@ -411,6 +414,12 @@ def box(x0, y0, x1, y1): 'JubilifeCity': [(0, 22), (0, 23), (40, 40), (21, 42)], # The way up to the entrance hall: stepping on it leaves the room. 'OreburghMine': [(15, 1)], + # The mine's mouth: 01.03 walks down to the row above it and steps on. + # We come out on it too, and walk north off it to a wall first. + 'OreburghCity': [(x, 59) for x in range(44, 49)], + # The hall's way up to the city: its other warp row (11..13, 21) has + # cave mouths below it, so a step south from row 20 goes through. + 'OreburghMineB1F': [(11, 1), (12, 1), (13, 1)], }