From ac5f55d63546708fe382688275fb71899b89d51e Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:24:57 -0800 Subject: [PATCH 1/2] BDSP AutoStory walk: put the route walker's console steps behind RouteIO No behaviour change. RouteWalker (walk_route(), talk_to_npc()) now does everything on the console through a RouteIO: walks to a wall, D-pad tap walks, presses and waits, the dialogue watch, the at-the-wall holding check, screen looks and its log and failures. ConsoleRouteIO is today's code: holding_against_wall() moved unchanged into RouteIO.cpp, and each call site makes the same controller commands, waits and reads as before. walk_route() and talk_to_npc() keep their signatures and build a ConsoleRouteIO; overloads take a RouteIO so tests can script the console. route_turn() and the walker's taps, A presses and waits are now a RouteInput (kind, direction, hold, release, count, whether to wait for them), pressed by press_route_input(); route_turn_tap() is the D-pad tap route_turn() turns with. Review with: git diff --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change HEAD~1 HEAD Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 287 ++++++++---------- .../PokemonBDSP_AutoStory_PlannedRoute.h | 11 + .../Utils/PokemonBDSP_AutoStory_RouteIO.cpp | 101 ++++++ .../Utils/PokemonBDSP_AutoStory_RouteIO.h | 103 +++++++ .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 38 ++- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 28 ++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 7 files changed, 411 insertions(+), 159 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 0a061b3184..bbeafd4c0e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -7,17 +7,13 @@ #include #include #include +#include "Common/Cpp/Exceptions.h" #include "Common/Cpp/PrettyPrint.h" #include "Common/Cpp/Filesystem/Filesystem.h" #include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "Pokemon/Navigation/Pokemon_RouteLog.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_RouteIO.h" #include "PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_PlannedRoute.h" @@ -55,21 +51,21 @@ PlannedRouteLeg planned_route_leg(const StoryWorld& maps, const PlannedLeg& leg) return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; } -void log_plan(VideoStream& stream, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ +void log_plan(RouteIO& io, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ std::vector logged; for (const PlannedLeg& leg : legs){ logged.push_back(planned_route_leg(maps, leg)); } - stream.log(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged)); + io.note(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged), RouteNote::LOG); } // Logs a leg that ended on its planned tiles, and how that is known: an // open leg counts its taps through a wild battle, so its end is assumed; a // leg to a wall is walked again to its wall. -void log_leg_done(VideoStream& stream, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ +void log_leg_done(RouteIO& io, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; - stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), - assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); + io.note(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), + assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : ""), RouteNote::LOG); } // The tiles the player passes when walking `tiles` tiles from `from`, the @@ -89,30 +85,6 @@ std::vector leg_path(const StoryWorld& grid, TilePos from, Direction di return path; } -// Holding toward a wall with people or Pokemon moving beside us never reads -// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking -// scrolls the view far more (35 to 90). True if holding toward (x, y) for -// 1.6 s changes the screen less than walking would: we stand at a wall. -bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ - constexpr double WALKING_CHANGE = 25.0; - context.wait_for_all_requests(); - VideoSnapshot before = stream.video().snapshot(); - double most = 0; - for (size_t chunk = 0; chunk < 4 && before; chunk++){ - pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); - context.wait_for_all_requests(); - VideoSnapshot after = stream.video().snapshot(); - if (!after){ - return false; - } - most = std::max(most, walk_screen_change(*before.frame, *after.frame)); - before = std::move(after); - } - stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + - (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); - return before && most < WALKING_CHANGE; -} - // The map all of `tiles` are on, or empty if they are on several or none. std::string map_of(const StoryWorld& maps, const std::vector& tiles){ std::string name; @@ -157,6 +129,28 @@ Milliseconds wall_limit(const TileLeg& leg){ return ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; } +// Fails the program with `message` (RouteNote::FAILURE). +[[noreturn]] void fail(RouteIO& io, const std::string& message){ + io.note(message, RouteNote::FAILURE); + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "RouteIO::note() returned from a failure: " + message); +} + +// route_turn() through `io`. +void turn(RouteIO& io, Direction direction){ + io.note(std::string("[Route] Turning ") + direction_name(direction) + ".", RouteNote::LOG); + io.press(route_turn_tap(direction)); +} + +// A press of A, to talk to someone in front of us. +RouteInput press_a(RouteInputEnd end){ + return {.kind = RouteInputKind::BUTTON_A, .direction = Direction::NORTH, .hold = 80ms, .release = 200ms, + .count = 1, .end = end}; +} + +// A wait for someone walking about to get out of the way. +constexpr RouteInput WAIT_FOR_THE_WAY{.kind = RouteInputKind::WAIT, .direction = Direction::NORTH, .hold = 2500ms, + .release = 0ms, .count = 1, .end = RouteInputEnd::DONE}; + // Tiles a walk to the wall moved, from the time it walked. size_t tiles_moved(const WalkResult& walk, Milliseconds per_tile){ const Milliseconds moving = walk.walked - ROUTE_START_DELAY - @@ -175,7 +169,6 @@ enum class LegNext{ struct LegWalk{ const PlannedLeg& planned; std::string label; - JoystickPosition stick; std::string map_before; std::string map_after; std::vector banners = {}; @@ -212,13 +205,8 @@ DpadWalkOptions run_watching(LegWalk& walk){ // turned out to be when something stopped it. class RouteWalker{ public: - RouteWalker( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, RouteTrainers& trainers, const RouteWalkPlan& plan - ) - : m_stream(stream) - , m_context(context) - , m_escape(escape) + RouteWalker(RouteIO& io, const StoryWorld& maps, RouteTrainers& trainers, const RouteWalkPlan& plan) + : m_io(io) , m_maps(maps) , m_grid(maps) , m_trainers(trainers) @@ -255,9 +243,7 @@ class RouteWalker{ LegNext replan_from(const LegWalk& walk, std::vector stopped, const std::string& why); void mark_blocker(const LegWalk& walk, const std::vector& stopped); - VideoStream& m_stream; - ProControllerContext& m_context; - const WildBattleEscape& m_escape; + RouteIO& m_io; const StoryWorld& m_maps; // Our own copy, so things found in the way can be added to it. StoryWorld m_grid; @@ -286,8 +272,8 @@ std::vector RouteWalker::run(std::vector from){ m_map_start = map_of(m_maps, m_from); for (size_t plans = 0; plans < 8; plans++){ const std::vector legs = plan_legs(); - route_status(m_stream, m_plan.label + ": " + std::to_string(legs.size()) + " legs planned"); - log_plan(m_stream, m_maps, m_plan.label, m_from, legs); + m_io.note(m_plan.label + ": " + std::to_string(legs.size()) + " legs planned", RouteNote::STATUS); + log_plan(m_io, m_maps, m_plan.label, m_from, legs); bool replanned = false; for (size_t number = 1; number <= legs.size() && !replanned; number++){ replanned = walk_leg(legs[number - 1], number, legs.size()) == LegNext::REPLAN; @@ -296,8 +282,7 @@ std::vector RouteWalker::run(std::vector from){ return arrive(); } } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": too many plans.", m_stream); + fail(m_io, m_plan.label + ": too many plans."); } std::vector RouteWalker::plan_legs() const{ @@ -306,23 +291,21 @@ std::vector RouteWalker::plan_legs() const{ // From a few possible tiles the exact-open-leg costs found no // plan where the default ones did (r240a, three tiles on Route // 204's row 27); the default plan is longer but sound. - m_stream.log("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + - "; trying the default costs.", COLOR_ORANGE); + m_io.note("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + + "; trying the default costs.", RouteNote::WARNING); legs = plan_robust_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); } if (!legs){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": no plan from " + describe(m_maps, m_from) + ".", m_stream); + fail(m_io, m_plan.label + ": no plan from " + describe(m_maps, m_from) + "."); } return std::move(*legs); } LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t count){ - route_status(m_stream, route_leg_line(number, count, planned_route_leg(m_maps, planned))); + m_io.note(route_leg_line(number, count, planned_route_leg(m_maps, planned)), RouteNote::STATUS); LegWalk walk{ .planned = planned, .label = m_plan.label + ": " + to_string(planned.leg), - .stick = route_stick(planned.leg.direction), .map_before = map_of(m_maps, m_from), .map_after = map_of(m_maps, planned.after), }; @@ -333,8 +316,7 @@ LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t c m_facing = planned.leg.direction; check_banners(walk); if (walk.result.stop == WalkStop::TRANSITION){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": unexpected transition.", m_stream); + fail(m_io, walk.label + ": unexpected transition."); } return after_leg(walk); } @@ -350,25 +332,24 @@ std::vector RouteWalker::arrive() const{ // The banner can come up after the last leg ends: Sandgem's showed about // a second later (r35). Watch for it a while before judging. for (const WallClock end = current_time() + 3000ms; changed_map && !seen && current_time() < end;){ - m_context.wait_for_all_requests(); - if (const VideoSnapshot screen = m_stream.video().snapshot()){ - seen = location_banner_matches(read_location_banner(*screen.frame), map_end); - } + m_io.look([&](const ImageViewRGB32& screen){ + seen = location_banner_matches(read_location_banner(screen), map_end); + }); if (!seen){ - m_context.wait_for(250ms); + m_io.press({.kind = RouteInputKind::PAUSE, .direction = Direction::NORTH, .hold = 250ms, .release = 0ms, + .count = 1, .end = RouteInputEnd::QUEUED}); } } // No banner read at all is only a warning (r20-r22: none we could read on // the Lakefront-to-Route 201 crossing); the next step checks where we are. if (changed_map && !seen && m_all_banners.empty()){ - m_stream.log("[Route] " + m_plan.label + ": no banner read for " + map_end + "; going on.", COLOR_ORANGE); + m_io.note("[Route] " + m_plan.label + ": no banner read for " + map_end + "; going on.", RouteNote::WARNING); }else if (changed_map && !seen){ const std::string reads = quoted_list(m_all_banners); - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + - "'s location banner never showed (read " + (reads.empty() ? "nothing" : reads) + ").", m_stream); + fail(m_io, m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + + "'s location banner never showed (read " + (reads.empty() ? "nothing" : reads) + ")."); } - m_stream.log(ROUTE_LOG_TAG + route_arrived_line(m_plan.label, route_tiles(m_maps, m_from))); + m_io.note(ROUTE_LOG_TAG + route_arrived_line(m_plan.label, route_tiles(m_maps, m_from)), RouteNote::LOG); return m_from; } @@ -378,15 +359,15 @@ std::vector RouteWalker::arrive() const{ LegNext RouteWalker::wall_leg(LegWalk& walk){ const TileLeg& leg = walk.planned.leg; const Milliseconds limit = wall_limit(leg); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); // A wild battle may end the walk early. Walking to a wall again ends in // the same place, so finish the leg. const bool interrupted = walk.result.wild_battles > 0; for (size_t again = 0; again < 3 && walk.result.wild_battles > 0 && (walk.result.stop == WalkStop::LIMIT || walk.result.stop == WalkStop::BLOCKED); again++ ){ - m_stream.log("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again."); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + m_io.note("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again.", RouteNote::LOG); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); } if (interrupted){ return LegNext::NEXT_LEG; @@ -401,9 +382,9 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ size_t walked_tiles = tiles_moved(walk.result, route_ms_per_tile(leg.direction, m_plan.speed)); // Every leg that reaches its wall measures the pace: the tiles are known. if (walk.result.stop == WalkStop::BLOCKED && nearest != SIZE_MAX && walked_tiles >= nearest){ - m_stream.log("[Speed] " + std::string(m_plan.speed == MoveSpeed::RUN ? "run" : "walk") + " " + + m_io.note("[Speed] " + std::string(m_plan.speed == MoveSpeed::RUN ? "run" : "walk") + " " + direction_name(leg.direction) + ": " + std::to_string(nearest) + " tiles in " + - std::to_string(walk.result.walked.count()) + " ms to the wall."); + std::to_string(walk.result.walked.count()) + " ms to the wall.", RouteNote::LOG); } walked_tiles = unstick_wall_leg(walk, walked_tiles, nearest); // Any stop short of the wall's distance: someone who stays put is @@ -435,28 +416,20 @@ size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t // Route 203's stairs the walk west stopped nine times in a row and two // taps crossed (r225a, 2026-10-31). Tap twice, then walk on to the wall. if (walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ - const Direction direction = walk.planned.leg.direction; - const DpadPosition dpad = - direction == Direction::NORTH ? DPAD_UP : - direction == Direction::SOUTH ? DPAD_DOWN : - direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); - for (size_t tap = 0; tap < 2; tap++){ - pbf_press_dpad(m_context, dpad, 160ms, 640ms); - } - m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + m_io.note("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; two D-pad taps, then on to the wall.", RouteNote::WARNING); + m_io.press({.kind = RouteInputKind::DPAD, .direction = walk.planned.leg.direction, .hold = 160ms, .release = 640ms, + .count = 2, .end = RouteInputEnd::DONE}); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); walked_tiles += tiles_moved(walk.result, per_tile); } // Someone walking about may stand in the way for a moment: wait and // walk to the wall again before calling it an obstacle. for (size_t again = 0; again < 2 && walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++){ - m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(m_context, 2500ms); - m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + m_io.note("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; waiting for the way to clear.", RouteNote::WARNING); + m_io.press(WAIT_FOR_THE_WAY); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); walked_tiles += tiles_moved(walk.result, per_tile); } return walked_tiles; @@ -475,16 +448,15 @@ LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ } stopped = sorted_unique(std::move(stopped)); if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": stopped early, and no tile fits.", m_stream); + fail(m_io, walk.label + ": stopped early, and no tile fits."); } mark_blocker(walk, stopped); return replan_from(walk, std::move(stopped), "stopped early after about " + std::to_string(walked_tiles) + " tiles"); } void RouteWalker::check_at_wall(LegWalk& walk){ - WalkResult check = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, 2000ms, - walk.label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); + WalkResult check = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = 2000ms, + .label = walk.label + ": at the wall?", .options = {.arrived = nullptr, .pace = DpadPace::RUN}}); if (check.stop == WalkStop::DIALOG){ // A trainer who saw us froze the walk (the "!") until its time ran // out, then walked over (r241a: Aroma Lady Taylor's line crosses @@ -492,33 +464,32 @@ void RouteWalker::check_at_wall(LegWalk& walk){ // they saw us from is not reliable (BDSP's trainers stand a few // tiles from Platinum's, r242a), and a leg to a wall ends at its // wall however far it got. - route_status(m_stream, walk.label + ": a trainer stopped us on the way; battling."); + m_io.note(walk.label + ": a trainer stopped us on the way; battling.", RouteNote::STATUS); m_trainers.fight("a trainer on the way"); - m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), - walk.label + ": on to the wall", run_watching(walk)); + m_io.note("[Route] " + walk.label + ": on to the wall after the battle.", RouteNote::WARNING); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = wall_limit(walk.planned.leg), + .label = walk.label + ": on to the wall", .options = run_watching(walk)}); // Where the walk on stopped is what counts now, not the line that // stopped the check (r54: at the wall after Route 202's second // trainer, and failed on the "dialog"). check = walk.result; } if (check.stop != WalkStop::BLOCKED && - !(check.stop == WalkStop::LIMIT && holding_against_wall(m_stream, m_context, walk.stick.x, walk.stick.y, walk.label + ": at the wall?")) + !(check.stop == WalkStop::LIMIT && m_io.holding_against_wall(walk.planned.leg.direction, walk.label + ": at the wall?")) ){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": ran out of time short of the wall, and walking on gets " + - walk_stop_name(check.stop) + ": not where the plan has us.", m_stream); + fail(m_io, walk.label + ": ran out of time short of the wall, and walking on gets " + + walk_stop_name(check.stop) + ": not where the plan has us."); } } // After a battle on a leg to a wall: a leg to a wall ends at its wall // however far it got, so walk on to it and carry on. void RouteWalker::on_to_wall(LegWalk& walk){ - m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), - walk.label + ": on to the wall", run_watching(walk)); + m_io.note("[Route] " + walk.label + ": on to the wall after the battle.", RouteNote::WARNING); + m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = wall_limit(walk.planned.leg), + .label = walk.label + ": on to the wall", .options = run_watching(walk)}); m_from = walk.planned.after; - log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, 0); + log_leg_done(m_io, m_maps, m_plan.label, walk.planned, 0); } @@ -531,13 +502,13 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ turn_tap(walk); } if (leg.tiles > walk.turn_moved){ - walk.result = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - walk.turn_moved, walk.label); + walk.result = m_io.walk_taps(leg.direction, leg.tiles - walk.turn_moved, walk.label); } // A wild battle starts once the step onto the grass is done, so the // taps still count; the walk went on after it. if (walk.result.wild_battles > 0){ - m_stream.log("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + - " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); + m_io.note("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + + " wild battle(s) on the way; the tap count holds.", RouteNote::WARNING); } // Taps that moved nothing: someone stands in the way (the Youngster // below the Ravaged Path wanders across the row under the cliff, r239a, @@ -548,11 +519,10 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ // which the taps give exactly. size_t moved = std::min(leg.tiles, walk.result.moved_chunks + walk.turn_moved); if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ - m_stream.log("[Route] " + walk.label + ": blocked after " + std::to_string(moved) + " of " + - std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(m_context, 2500ms); - m_context.wait_for_all_requests(); - const WalkResult rest = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - moved, walk.label + ": the rest"); + m_io.note("[Route] " + walk.label + ": blocked after " + std::to_string(moved) + " of " + + std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", RouteNote::WARNING); + m_io.press(WAIT_FOR_THE_WAY); + const WalkResult rest = m_io.walk_taps(leg.direction, leg.tiles - moved, walk.label + ": the rest"); moved = std::min(leg.tiles, moved + rest.moved_chunks); walk.result.stop = rest.stop; walk.result.walked += rest.walked; @@ -571,7 +541,7 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ // stepped; r60 only turned but counted a tile, a column off below // Jubilife's Center). void RouteWalker::turn_tap(LegWalk& walk){ - route_turn(m_stream, m_context, walk.planned.leg.direction); + turn(m_io, walk.planned.leg.direction); walk.turn_moved = 0; } @@ -593,10 +563,9 @@ LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ // walk did not end where the plan says. From tiles on several maps (a leg // not confirmed) the banner may already have shown. void RouteWalker::check_banners(LegWalk& walk){ - m_context.wait_for_all_requests(); - if (VideoSnapshot screen = m_stream.video().snapshot()){ - watch_banner(walk, *screen.frame); - } + m_io.look([&walk](const ImageViewRGB32& screen){ + watch_banner(walk, screen); + }); const std::string banner_text = quoted_list(walk.banners); m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ @@ -605,22 +574,21 @@ void RouteWalker::check_banners(LegWalk& walk){ // Go on and let the next step check where we are; a banner for another // map still fails. if (banner_text.empty()){ - m_stream.log("[Route] " + walk.label + ": no banner read for " + walk.map_after + "; going on.", COLOR_ORANGE); + m_io.note("[Route] " + walk.label + ": no banner read for " + walk.map_after + "; going on.", RouteNote::WARNING); return; } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + - (banner_text.empty() ? "nothing" : banner_text) + ".", m_stream); + fail(m_io, walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + + (banner_text.empty() ? "nothing" : banner_text) + "."); } if (!banner_text.empty()){ - m_stream.log("[Route] " + walk.label + ": location banner " + banner_text + "."); + m_io.note("[Route] " + walk.label + ": location banner " + banner_text + ".", RouteNote::LOG); } // Where each leg ended, to compare with the plan after a run. - if (VideoSnapshot screen = m_stream.video().snapshot()){ + m_io.look([&walk](const ImageViewRGB32& screen){ const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(walk.label) + ".png"); - } + screen.save(folder + now_to_filestring() + "-" + normalize_location_name(walk.label) + ".png"); + }); } // A trainer who sees us freezes the player and walks over; the dialogue @@ -634,13 +602,12 @@ LegNext RouteWalker::after_leg(LegWalk& walk){ const std::vector watching = watching_trainers(paths); bool talking = walk.result.stop == WalkStop::DIALOG; if (!talking){ - ShortDialogWatcher dialog(COLOR_YELLOW); const Milliseconds wait = watching.empty() ? 500ms : 4000ms; - talking = wait_until(m_stream, m_context, wait, {{dialog}}) == 0; + talking = m_io.dialog_within(wait, COLOR_YELLOW); } if (!talking){ m_from = walk.planned.after; - log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, walk.result.wild_battles); + log_leg_done(m_io, m_maps, m_plan.label, walk.planned, walk.result.wild_battles); return LegNext::NEXT_LEG; } if (watching.empty()){ @@ -677,10 +644,9 @@ LegNext RouteWalker::unknown_trainer(LegWalk& walk, const std::vector 2){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": dialogue the map does not explain stopped us 3 times.", m_stream); + fail(m_io, walk.label + ": dialogue the map does not explain stopped us 3 times."); } - route_status(m_stream, m_plan.label + ": a trainer the map lacks stopped us; fighting."); + m_io.note(m_plan.label + ": a trainer the map lacks stopped us; fighting.", RouteNote::STATUS); m_trainers.fight("unknown trainer"); if (walk.planned.leg.ends_at_wall){ on_to_wall(walk); @@ -707,11 +673,10 @@ LegNext RouteWalker::known_trainer(LegWalk& walk, const std::vector& } const TilePos blocker = stopped[0].next(walk.planned.leg.direction); m_grid.block(blocker); - m_stream.log("[Route] " + walk.label + ": something the map lacks is at " + - describe(m_maps, blocker) + "; marked it.", COLOR_ORANGE); + m_io.note("[Route] " + walk.label + ": something the map lacks is at " + + describe(m_maps, blocker) + "; marked it.", RouteNote::WARNING); } LegNext RouteWalker::replan_from(const LegWalk& walk, std::vector stopped, const std::string& why){ - m_stream.log(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.planned.leg), why, route_tiles(m_maps, stopped)), COLOR_ORANGE); + m_io.note(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.planned.leg), why, route_tiles(m_maps, stopped)), RouteNote::WARNING); m_facing = walk.planned.leg.direction; m_from = std::move(stopped); return LegNext::REPLAN; @@ -778,7 +742,15 @@ std::vector walk_route( const StoryWorld& maps, std::vector from, RouteTrainers& trainers, const RouteWalkPlan& plan ){ - return RouteWalker(stream, context, escape, maps, trainers, plan).run(std::move(from)); + ConsoleRouteIO io(stream, context, escape); + return walk_route(io, maps, std::move(from), trainers, plan); +} + +std::vector walk_route( + RouteIO& io, const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +){ + return RouteWalker(io, maps, trainers, plan).run(std::move(from)); } std::vector walk_waypoints( @@ -803,13 +775,21 @@ void talk_to_npc( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const StoryWorld& maps, std::vector& at, RouteTrainers& trainers, const NpcTalk& talk +){ + ConsoleRouteIO io(stream, context, escape); + talk_to_npc(io, maps, at, trainers, talk); +} + +void talk_to_npc( + RouteIO& io, const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk ){ for (Direction side : talk.sides){ const TilePos stand = talk.npc.next(side); if (!standable(maps, stand)){ continue; } - at = walk_route(stream, context, escape, maps, at, trainers, { + at = walk_route(io, maps, at, trainers, { .goal = [stand](const TilePos& pos){ return pos == stand; }, .label = talk.label, .avoid = {}, @@ -820,18 +800,15 @@ void talk_to_npc( side == Direction::NORTH ? Direction::SOUTH : side == Direction::SOUTH ? Direction::NORTH : side == Direction::EAST ? Direction::WEST : Direction::EAST; - route_turn(stream, context, face); - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, talk.label + ": talking from " + describe(maps, stand) + "."); + turn(io, face); + io.press(press_a(RouteInputEnd::DONE)); + if (io.dialog_within(2000ms, COLOR_CYAN)){ + io.note(talk.label + ": talking from " + describe(maps, stand) + ".", RouteNote::STATUS); return; } - route_status(stream, talk.label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); + io.note(talk.label + ": nobody answered from " + describe(maps, stand) + "; trying another side.", RouteNote::STATUS); } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue.", stream); + fail(io, talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue."); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h index 8577d82743..e422d6891c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -19,6 +19,7 @@ #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_RouteIO.h" #include "PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Walk.h" @@ -64,6 +65,11 @@ std::vector walk_route( const StoryWorld& maps, std::vector from, RouteTrainers& trainers, const RouteWalkPlan& plan ); +// walk_route() through `io`. +std::vector walk_route( + RouteIO& io, const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +); // The corners of a line a human walked, and the costs to plan each // stretch with. @@ -101,6 +107,11 @@ void talk_to_npc( const StoryWorld& maps, std::vector& at, RouteTrainers& trainers, const NpcTalk& talk ); +// talk_to_npc() through `io`. +void talk_to_npc( + RouteIO& io, const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk +); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp new file mode 100644 index 0000000000..37e6e3b06a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp @@ -0,0 +1,101 @@ +/* BDSP AutoStory - Route IO + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#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_AutoStory_RouteIO.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +// Holding toward a wall with people or Pokemon moving beside us never reads +// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking +// scrolls the view far more (35 to 90). True if holding toward (x, y) for +// 1.6 s changes the screen less than walking would: we stand at a wall. +bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ + constexpr double WALKING_CHANGE = 25.0; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + double most = 0; + for (size_t chunk = 0; chunk < 4 && before; chunk++){ + pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (!after){ + return false; + } + most = std::max(most, walk_screen_change(*before.frame, *after.frame)); + before = std::move(after); + } + stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + + (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); + return before && most < WALKING_CHANGE; +} + +} + + +WalkResult ConsoleRouteIO::walk_to_wall(const RouteWallWalk& walk){ + return walk_until_dpad(m_stream, m_context, m_escape, route_stick(walk.direction), walk.limit, walk.label, walk.options); +} + +WalkResult ConsoleRouteIO::walk_taps(Direction direction, size_t tiles, const std::string& label){ + return walk_tiles_dpad(m_stream, m_context, m_escape, route_stick(direction), tiles, label); +} + +void ConsoleRouteIO::press(const RouteInput& input){ + press_route_input(m_context, input); +} + +bool ConsoleRouteIO::dialog_within(Milliseconds wait, Color color){ + ShortDialogWatcher dialog(color); + return wait_until(m_stream, m_context, wait, {{dialog}}) == 0; +} + +bool ConsoleRouteIO::holding_against_wall(Direction direction, const std::string& label){ + const JoystickPosition stick = route_stick(direction); + return PokemonBDSP::holding_against_wall(m_stream, m_context, stick.x, stick.y, label); +} + +void ConsoleRouteIO::look(const std::function& at){ + m_context.wait_for_all_requests(); + if (VideoSnapshot screen = m_stream.video().snapshot()){ + at(*screen.frame); + } +} + +void ConsoleRouteIO::note(const std::string& text, RouteNote kind){ + switch (kind){ + case RouteNote::LOG: + m_stream.log(text); + return; + case RouteNote::WARNING: + m_stream.log(text, COLOR_ORANGE); + return; + case RouteNote::STATUS: + route_status(m_stream, text); + return; + case RouteNote::FAILURE: + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, text, m_stream); + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h new file mode 100644 index 0000000000..1db4631068 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h @@ -0,0 +1,103 @@ +/* BDSP AutoStory - Route IO + * + * From: https://github.com/PokemonAutomation/ + * + * What the planned route walker (PlannedRoute) does on the console: walks, + * presses, screen reads and its log. The walker's decisions take a RouteIO, + * so tests can run them on a script of console outcomes; ConsoleRouteIO is + * the console. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_RouteIO_H +#define PokemonAutomation_PokemonBDSP_AutoStory_RouteIO_H + +#include +#include +#include "Common/Cpp/Color.h" +#include "Common/Cpp/Time.h" +#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_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// A walk on the D-pad toward a wall: walk_until_dpad() toward `direction`. +struct RouteWallWalk{ + Pokemon::Direction direction; + Milliseconds limit; + std::string label; + DpadWalkOptions options; +}; + +// How a line goes to the log. +enum class RouteNote{ + LOG, + WARNING, // in orange + STATUS, // route_status(): also on the video overlay + FAILURE, // fails the program with a screenshot; does not return +}; + +class RouteIO{ +public: + virtual ~RouteIO() = default; + + // Holds the D-pad toward a wall (walk_until_dpad()). + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) = 0; + + // Taps `tiles` whole tiles toward `direction` (walk_tiles_dpad()). + virtual WalkResult walk_taps(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; + + // Presses or waits (press_route_input()). + virtual void press(const RouteInput& input) = 0; + + // Whether a dialogue box shows within `wait`. + virtual bool dialog_within(Milliseconds wait, Color color) = 0; + + // Holds the stick toward `direction` for 1.6 s, and returns whether the + // screen changed less than walking would: we stand at a wall. + virtual bool holding_against_wall(Pokemon::Direction direction, const std::string& label) = 0; + + // Once the presses so far are done, shows `at` the screen, if there is one. + virtual void look(const std::function& at) = 0; + + // Writes `text` to the log; a FAILURE fails the program. + virtual void note(const std::string& text, RouteNote kind) = 0; +}; + + +// The console: today's walker steps, unchanged. +class ConsoleRouteIO : public RouteIO{ +public: + ConsoleRouteIO(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape) + : m_stream(stream) + , m_context(context) + , m_escape(escape) + {} + + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) override; + virtual WalkResult walk_taps(Pokemon::Direction direction, size_t tiles, const std::string& label) override; + virtual void press(const RouteInput& input) override; + virtual bool dialog_within(Milliseconds wait, Color color) override; + virtual bool holding_against_wall(Pokemon::Direction direction, const std::string& label) override; + virtual void look(const std::function& at) override; + virtual void note(const std::string& text, RouteNote kind) override; + +private: + VideoStream& m_stream; + ProControllerContext& m_context; + const WildBattleEscape& m_escape; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index bfc9160262..cbd3f3c2d4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -79,14 +79,44 @@ Milliseconds route_open_hold(Direction direction, size_t tiles){ return step * tiles - step / 2; } -void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ - stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); +void press_route_input(ProControllerContext& context, const RouteInput& input){ + const Direction direction = input.direction; const DpadPosition dpad = direction == Direction::NORTH ? DPAD_UP : direction == Direction::SOUTH ? DPAD_DOWN : direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - pbf_press_dpad(context, dpad, ROUTE_TURN_TAP, 300ms); - context.wait_for_all_requests(); + for (size_t press = 0; press < input.count; press++){ + switch (input.kind){ + case RouteInputKind::DPAD: + pbf_press_dpad(context, dpad, input.hold, input.release); + break; + case RouteInputKind::STICK: + pbf_move_left_joystick(context, route_stick(direction), input.hold, input.release); + break; + case RouteInputKind::BUTTON_A: + pbf_press_button(context, BUTTON_A, input.hold, input.release); + break; + case RouteInputKind::WAIT: + pbf_wait(context, input.hold); + break; + case RouteInputKind::PAUSE: + context.wait_for(input.hold); + break; + } + } + if (input.end == RouteInputEnd::DONE){ + context.wait_for_all_requests(); + } +} + +RouteInput route_turn_tap(Direction direction){ + return {.kind = RouteInputKind::DPAD, .direction = direction, .hold = ROUTE_TURN_TAP, .release = 300ms, + .count = 1, .end = RouteInputEnd::DONE}; +} + +void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ + stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); + press_route_input(context, route_turn_tap(direction)); } std::optional talk_to_adjacent_npc( diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index 1568e5ae26..ea46031644 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -68,6 +68,34 @@ Milliseconds route_open_hold(Pokemon::Direction direction, size_t tiles); // Sandgem's Center door; r248a). constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(50); +// What a RouteInput presses. +enum class RouteInputKind{ + DPAD, // the D-pad toward `direction`: whole tiles + STICK, // the left stick toward `direction`: any distance + BUTTON_A, + WAIT, // nothing, for `hold`, after what is already queued + PAUSE, // nothing, for `hold` from now (ControllerContext::wait_for()) +}; +// Whether press_route_input() returns once the presses are done, or at once. +enum class RouteInputEnd{ + DONE, + QUEUED, +}; +// A short press, `count` times: held for `hold`, then let go for `release`. +// `direction` is for DPAD and STICK only. +struct RouteInput{ + RouteInputKind kind; + Pokemon::Direction direction; + Milliseconds hold; + Milliseconds release; + size_t count; + RouteInputEnd end; +}; +void press_route_input(ProControllerContext& context, const RouteInput& input); + +// The tap route_turn() turns with: on the D-pad, ROUTE_TURN_TAP long. +RouteInput route_turn_tap(Pokemon::Direction direction); + // Turns the player to face `direction` without moving. Only for a player who // faces another way: one already facing it may take a step. void route_turn(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 4c4e69c4f8..801fa55772 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1659,6 +1659,8 @@ file(GLOB LIBRARY_SOURCES 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 From e8a5efc8b32e6ccc49c628700d7b68d8b229f03b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:25:04 -0800 Subject: [PATCH 2/2] BDSP AutoStory walk: test the route walker on scripted console outcomes ScriptedRouteIO plays a strict script of console calls and outcomes; a call the script does not expect fails the test. New tests: - A trainer's line freezes the at-the-wall check (DIALOG), the battle runs, and the walk on reaches the wall (BLOCKED): the walker goes on (r54: it judged the stale DIALOG and failed). - Logan (Route 202, (26, 4), looking south) stops an open leg west along row 5 after 3 taps: the replan starts from (27, 5) and (26, 5), and its legs never walk into him from either (r55). - A wall leg out of time whose check also runs out of time counts as at the wall only when the holding check says so. - route_turn() turns with one D-pad tap of at most 80 ms (40 to 80 ms only turned on the console; a 100 ms stick tap stepped 0.6 tile). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + ...kemonBDSP_AutoStory_PlannedRoute_Tests.cpp | 385 ++++++++++++++++++ ...PokemonBDSP_AutoStory_PlannedRoute_Tests.h | 28 ++ .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 32 ++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 5 files changed, 449 insertions(+) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 60409b4a26..145096b969 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -6,6 +6,7 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.h" @@ -26,6 +27,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_StoryMaps(database); add_tests_AutoStory_Segment00(database); add_tests_AutoStory_Walk(database); + add_tests_AutoStory_PlannedRoute(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp new file mode 100644 index 0000000000..ac87afdcc7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp @@ -0,0 +1,385 @@ +/* BDSP AutoStory Planned Route Tests + * + * From: https://github.com/PokemonAutomation/ + * + * walk_route() (Utils/PokemonBDSP_AutoStory_PlannedRoute.h) on a script + * of console outcomes: each case lists the walks, presses and screen reads + * the walker must make, in order, and what the console answers. A call the + * script does not expect fails the case. + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "Utils/PokemonBDSP_AutoStory_PlannedRoute.h" +#include "Utils/PokemonBDSP_AutoStory_RouteIO.h" +#include "PokemonBDSP_AutoStory_PlannedRoute_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::TilePos; + + +// One call the walker is expected to make, and what the console answers: +// `walk` for a walk, `answer` for a dialogue watch or the holding check. +struct ScriptStep{ + std::string call; + WalkResult walk; + bool answer; +}; + +ScriptStep walk_step(std::string call, WalkStop stop, Milliseconds walked, size_t moved){ + return {.call = std::move(call), .walk = {.stop = stop, .walked = walked, .wild_battles = 0, .moved_chunks = moved}, .answer = false}; +} +ScriptStep answer_step(std::string call, bool answer){ + return {.call = std::move(call), .walk = {.stop = WalkStop::LIMIT, .walked = 0ms, .wild_battles = 0, .moved_chunks = 0}, .answer = answer}; +} +ScriptStep press_step(std::string call){ + return answer_step(std::move(call), false); +} + +// What the console did on a scripted walk: the steps called so far, every +// line logged, and the legs walked (to a wall, or a number of taps). +struct Script{ + std::vector steps; + size_t next = 0; + std::vector notes = {}; + std::vector legs = {}; +}; + +// Thrown by ScriptedRouteIO: a call the script did not expect, or the +// walker failing the program. +struct ScriptMismatch{ + std::string message; +}; +struct RouteFailed{ + std::string message; +}; + +std::string describe(const RouteInput& input){ + const char* kind = + input.kind == RouteInputKind::DPAD ? "D-pad" : + input.kind == RouteInputKind::STICK ? "stick" : + input.kind == RouteInputKind::BUTTON_A ? "A" : + input.kind == RouteInputKind::WAIT ? "wait" : "pause"; + const bool directed = input.kind == RouteInputKind::DPAD || input.kind == RouteInputKind::STICK; + return std::format("{}{}{} {} ms x{}", kind, directed ? " " : "", directed ? Pokemon::direction_name(input.direction) : "", + input.hold.count(), input.count); +} + +// The console as a script: each call must be the script's next step. +// look() sees no screen. +class ScriptedRouteIO : public RouteIO{ +public: + explicit ScriptedRouteIO(Script& script) + : m_script(script) + {} + + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) override{ + m_script.legs.push_back({.direction = walk.direction, .tiles = 0, .ends_at_wall = true}); + return expect("wall: " + walk.label).walk; + } + virtual WalkResult walk_taps(Direction direction, size_t tiles, const std::string& label) override{ + m_script.legs.push_back({.direction = direction, .tiles = tiles, .ends_at_wall = false}); + return expect(std::format("{} taps: {}", tiles, label)).walk; + } + virtual void press(const RouteInput& input) override{ + expect(describe(input)); + } + virtual bool dialog_within(Milliseconds wait, Color) override{ + return expect(std::format("dialog within {} ms", wait.count())).answer; + } + virtual bool holding_against_wall(Direction direction, const std::string&) override{ + return expect(std::string("holding ") + Pokemon::direction_name(direction)).answer; + } + virtual void look(const std::function&) override{} + virtual void note(const std::string& text, RouteNote kind) override{ + m_script.notes.push_back(text); + if (kind == RouteNote::FAILURE){ + throw RouteFailed{text}; + } + } + +private: + const ScriptStep& expect(const std::string& call){ + if (m_script.next >= m_script.steps.size()){ + throw ScriptMismatch{"a call after the script's end: \"" + call + "\""}; + } + const ScriptStep& step = m_script.steps[m_script.next++]; + if (step.call != call){ + throw ScriptMismatch{std::format("step {}: expected \"{}\", got \"{}\"", m_script.next, step.call, call)}; + } + return step; + } + + Script& m_script; +}; + + +// How a scripted walk ended: where it arrived, or why it failed. +struct ScriptedWalk{ + std::vector at; + std::string failure; + std::vector fights; +}; + +ScriptedWalk walk_script(const StoryWorld& maps, const TilePos& from, const TilePos& goal, std::vector sights, Script& script){ + ScriptedWalk out; + RouteTrainers trainers{ + .sights = std::move(sights), + .fight = [&out](const std::string& trainer){ out.fights.push_back(trainer); }, + }; + ScriptedRouteIO io(script); + try{ + out.at = walk_route(io, maps, {from}, trainers, { + .goal = [goal](const TilePos& pos){ return pos == goal; }, + .label = "T", + .avoid = {}, + .costs = route_plan_costs(), + .speed = MoveSpeed::WALK, + }); + }catch (const RouteFailed& e){ + out.failure = e.message; + } + return out; +} + +// The script's calls all made, or which are left. +std::string script_left(const Script& script){ + if (script.next == script.steps.size()){ + return ""; + } + return std::format("{} of {} steps called; next \"{}\".", script.next, script.steps.size(), script.steps[script.next].call); +} + +StoryWorld make_world(const std::vector& rows){ + Pokemon::MapData map; + map.name = "Test"; + map.tiles = Pokemon::Grid{(int)rows[0].size(), (int)rows.size(), {}}; + for (const std::string& row : rows){ + for (char glyph : row){ + map.tiles.cells.push_back(*Pokemon::tile_from_glyph(glyph)); + } + } + StoryWorld world; + world.add("Test", map, {0, 0}); + return world; +} + + +// r54: a trainer's line froze the check walk at a leg's wall; after the +// battle the walk on reached the wall, but the check still judged the +// frozen walk's DIALOG and failed. The walk on is what counts. +class Test_WallLegTrainerOnTheWay : public UnitTest{ +public: + Test_WallLegTrainerOnTheWay() + : UnitTest("PokemonBDSP::AutoStory - Planned route: a trainer on a wall leg, then on to the wall") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + Script script{.steps = { + walk_step("wall: T: east 4 (to wall)", WalkStop::LIMIT, 2050ms, 0), + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::DIALOG, 400ms, 0), + walk_step("wall: T: east 4 (to wall): on to the wall", WalkStop::BLOCKED, 1200ms, 0), + answer_step("dialog within 500 ms", false), + }}; + try{ + const ScriptedWalk walk = walk_script(make_world({"#.....#"}), {1, 0}, {5, 0}, {}, script); + if (!walk.failure.empty()){ + return "failed after the walk on reached the wall: " + walk.failure; + } + if (walk.fights != std::vector{"a trainer on the way"}){ + return std::format("fought {} times, expected once.", walk.fights.size()); + } + if (walk.at != std::vector{{5, 0}}){ + return "did not arrive at (5, 0)."; + } + }catch (const ScriptMismatch& e){ + return e.message; + } + const std::string left = script_left(script); + return left.empty() ? UnitTestResult(true) : UnitTestResult(left); + } +}; + + +// A leg to a wall that ran out of time: a check walk that also runs out +// of time counts as at the wall only if holding on there changes the +// screen less than walking (r127a ran along the wrong row). +struct WallCheckCase{ + std::string name; + std::vector check; + bool fails; +}; + +class Test_WallLegLimit : public UnitTest{ +public: + Test_WallLegLimit() + : UnitTest("PokemonBDSP::AutoStory - Planned route: a wall leg out of time needs the holding check") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "the check stops at the wall", .check = { + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::BLOCKED, 1050ms, 0), + answer_step("dialog within 500 ms", false), + }, .fails = false}, + {.name = "the check runs out of time, holding on reads a wall", .check = { + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::LIMIT, 2000ms, 0), + answer_step("holding east", true), + answer_step("dialog within 500 ms", false), + }, .fails = false}, + {.name = "the check runs out of time, holding on still moves", .check = { + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::LIMIT, 2000ms, 0), + answer_step("holding east", false), + }, .fails = true}, + }; + for (const WallCheckCase& item : cases){ + Script script{.steps = {walk_step("wall: T: east 4 (to wall)", WalkStop::LIMIT, 2050ms, 0)}}; + script.steps.insert(script.steps.end(), item.check.begin(), item.check.end()); + try{ + const ScriptedWalk walk = walk_script(make_world({"#.....#"}), {1, 0}, {5, 0}, {}, script); + if (walk.failure.empty() == item.fails){ + return item.name + (item.fails ? ": went on as at the wall." : ": failed: " + walk.failure); + } + }catch (const ScriptMismatch& e){ + return item.name + ": " + e.message; + } + const std::string left = script_left(script); + if (!left.empty()){ + return item.name + ": " + left; + } + } + return true; + } +}; + + +// Whether walking `legs` from `from` ever steps into `person`'s tile: a +// leg to a wall that stops against them, or a tap toward them. +bool walks_into(const StoryWorld& world, TilePos from, const std::vector& legs, const TilePos& person){ + for (const TileLeg& leg : legs){ + for (size_t tap = 0; leg.ends_at_wall || tap < leg.tiles; tap++){ + if (from.next(leg.direction) == person){ + return true; + } + const std::optional next = step(world, from, leg.direction); + if (!next){ + break; + } + from = *next; + } + } + return false; +} + +// Route 202 alone, at its own tiles, and Logan's sight: he stands at +// (26, 4) and looks south. +StoryWorld route202_world(){ + StoryWorld world; + world.add("Route 202", story_map("Route202"), {0, 0}); + return world; +} +std::vector logan_sight(){ + std::vector sights; + for (const TrainerSight& sight : route202_trainer_sights()){ + if (sight.name == "logan"){ + sights.push_back(sight); + } + } + return sights; +} + +// Whether the replan after the battle starts from each of `tiles`. +std::string check_replan_from(const Script& script, const std::vector& tiles){ + const auto replan = std::find_if(script.notes.begin(), script.notes.end(), [](const std::string& note){ + return note.find("planning again") != std::string::npos; + }); + if (replan == script.notes.end()){ + return "never planned again after the battle."; + } + for (const std::string& tile : tiles){ + if (replan->find(tile) == std::string::npos){ + return "the replan does not start from " + tile + ": " + *replan; + } + } + return ""; +} + +// r55: Logan (Route 202, (26, 4), looking south) stopped an open leg west +// along row 5. "5 taps" were 4, under his nose at (26, 5); the replan from +// the tap count alone walked into him. The replan must start from both the +// tile the taps give and the tile where the leg enters his sight, and walk +// on from either without bumping into him. +class Test_OpenLegKnownTrainer : public UnitTest{ +public: + Test_OpenLegKnownTrainer() + : UnitTest("PokemonBDSP::AutoStory - Planned route: a known trainer stops an open leg") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const StoryWorld world = route202_world(); + // West along row 5 from (30, 5) to (24, 5): his line is the 4th tap, + // and his dialogue comes after 3 taps that moved. + Script script{.steps = { + press_step("D-pad west 50 ms x1"), + walk_step("6 taps: T: west 6 (open)", WalkStop::DIALOG, 1500ms, 3), + walk_step("wall: T: east 4 (to wall)", WalkStop::BLOCKED, 2000ms, 0), + answer_step("dialog within 500 ms", false), + press_step("D-pad west 50 ms x1"), + walk_step("6 taps: T: west 6 (open)", WalkStop::LIMIT, 1500ms, 6), + answer_step("dialog within 500 ms", false), + }}; + ScriptedWalk walk; + try{ + walk = walk_script(world, {30, 5}, {24, 5}, logan_sight(), script); + }catch (const ScriptMismatch& e){ + return e.message; + } + if (!walk.failure.empty() || walk.fights != std::vector{"logan"}){ + return std::format("expected to fight Logan once and go on; fought {}, failure \"{}\".", walk.fights.size(), walk.failure); + } + // The taps say (27, 5); his sight line crosses the row at (26, 5). + const std::string replan = check_replan_from(script, {"Route 202 (27, 5)", "Route 202 (26, 5)"}); + if (!replan.empty()){ + return replan; + } + const std::vector after_battle(script.legs.begin() + 1, script.legs.end()); + const TilePos logan = map_point("Route202", "trainer_logan"); + for (const TilePos& from : {TilePos{27, 5}, TilePos{26, 5}}){ + if (walks_into(world, from, after_battle, logan)){ + return "the replan walks into Logan from " + Pokemon::to_string(from) + "."; + } + } + if (walk.at != std::vector{{24, 5}}){ + return "did not arrive at (24, 5)."; + } + const std::string left = script_left(script); + return left.empty() ? UnitTestResult(true) : UnitTestResult(left); + } +}; + +} + + +void add_tests_AutoStory_PlannedRoute(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h new file mode 100644 index 0000000000..13331b8db4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h @@ -0,0 +1,28 @@ +/* BDSP AutoStory Planned Route Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// walk_route() on a script of console outcomes (a ScriptedRouteIO): what +// the walker does after a trainer stops a leg, and when a leg to a wall +// counts as at its wall. +void add_tests_AutoStory_PlannedRoute(UnitTestDatabase& database); + + + +} +} +} +#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 9357b6cee2..c38ff46daf 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -23,6 +23,37 @@ namespace PokemonBDSP{ namespace{ +// A turn must not step: on the console D-pad taps of 40 to 80 ms only +// turned, a 160 ms D-pad tap stepped a whole tile, and a 100 ms stick tap +// stepped about 0.6 tile, off the tile grid (r49, r248a). So route_turn() +// taps the D-pad, once, for at most 80 ms. +class Test_RouteTurnTap : public UnitTest{ +public: + Test_RouteTurnTap() + : UnitTest("PokemonBDSP::AutoStory - a turn is a D-pad tap too short to step") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + constexpr Milliseconds LONGEST_TURN(80); + for (Pokemon::Direction direction : {Pokemon::Direction::NORTH, Pokemon::Direction::SOUTH, Pokemon::Direction::EAST, Pokemon::Direction::WEST}){ + const RouteInput tap = route_turn_tap(direction); + const std::string name = std::string("turning ") + Pokemon::direction_name(direction); + if (tap.kind != RouteInputKind::DPAD){ + return name + ": not a D-pad tap; a stick tap moves part of a tile."; + } + if (tap.direction != direction || tap.count != 1){ + return name + ": expected one tap that way."; + } + if (tap.hold > LONGEST_TURN){ + return name + ": a " + std::to_string(tap.hold.count()) + " ms tap; over 80 ms may step."; + } + } + if (ROUTE_TURN_TAP > LONGEST_TURN){ + return "ROUTE_TURN_TAP is " + std::to_string(ROUTE_TURN_TAP.count()) + " ms; over 80 ms may step."; + } + return true; + } +}; + // indoors() decides when leave_door_south() is out: r257 walked on south // past Sandgem's road into a house after leaving the Center, because the // evening tint made the outside as warm as a room. Synthetic screens: the @@ -211,6 +242,7 @@ class Test_LocationBannerShowing : public UnitTest{ void add_tests_AutoStory_Walk(UnitTestDatabase& database){ + database.add(); database.add(); database.add(); database.add(); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 801fa55772..4ff2eeeaf0 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1641,6 +1641,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp + 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