diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp new file mode 100644 index 0000000000..f7174de9b8 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp @@ -0,0 +1,76 @@ +/* Battle Flash Watcher + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" +#include "PokemonFRLG_BattleFlashWatcher.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace std::chrono_literals; + + +namespace{ + +// On the console a flash took the screen from 145 to 98 (two thirds) and +// back within 80ms, twice in about 300ms. +constexpr std::chrono::milliseconds FLASH_WINDOW = 400ms; +constexpr double FLASH_DARKER = 0.8; // the dip is below this much of the brightness before it +constexpr double FLASH_FLOOR = 0.4; // and above this much: lower is a fade to black +constexpr double FLASH_RECOVERY = 1.1; // then the screen comes back to this much of the dip + +} + + +BattleFlashWatcher::BattleFlashWatcher(Color color) + : VisualInferenceCallback("BattleFlashWatcher") + , m_color(color) +{} + +void BattleFlashWatcher::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, GameSettings::instance().GAME_BOX); +} + +bool BattleFlashWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp){ + const ImageViewRGB32 game_screen = extract_box_reference(frame, GameSettings::instance().GAME_BOX); + const double brightness = image_stats(game_screen).average.sum(); + while (!m_history.empty() && timestamp - m_history.front().first > FLASH_WINDOW){ + m_history.pop_front(); + } + + // The brightest frame before the screen first dips, and the darkest + // frame since. + double before = 0; + double dip = -1; + for (const auto& item : m_history){ + if (dip < 0){ + if (item.second < before * FLASH_DARKER){ + dip = item.second; + }else{ + before = std::max(before, item.second); + } + }else{ + dip = std::min(dip, item.second); + } + } + m_history.emplace_back(timestamp, brightness); + + return dip >= 0 + && dip > before * FLASH_FLOOR + && brightness > dip * FLASH_RECOVERY; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h new file mode 100644 index 0000000000..a705d3473c --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h @@ -0,0 +1,46 @@ +/* Battle Flash Watcher + * + * From: https://github.com/PokemonAutomation/ + * + * Every wild battle starts with the overworld flashing dark twice, within + * half a second of the step that set it off, before the battle's own wipe. + * The wipe and the battle screen differ by map and come seconds later, so + * the flash is the earliest and the same sign of a battle everywhere. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_BattleFlashWatcher_H +#define PokemonAutomation_PokemonFRLG_BattleFlashWatcher_H + +#include +#include +#include "Common/Cpp/Color.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Fires when the game screen darkens by a fifth or more and starts to come +// back within a moment. A warp's fade, or the Switch dimming its screen, +// only darkens and so doesn't count. +class BattleFlashWatcher : public VisualInferenceCallback{ +public: + BattleFlashWatcher(Color color); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; + +private: + Color m_color; + // Recent brightness of the game screen. + std::deque> m_history; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.cpp new file mode 100644 index 0000000000..a3ac1c9106 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.cpp @@ -0,0 +1,36 @@ +/* Battle Screen Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "PokemonFRLG_BattleScreenDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +BattleScreenDetector::BattleScreenDetector(Color color) + : m_dialog(color) + , m_menu(color) + , m_opponent(color) + , m_player(color) +{} +void BattleScreenDetector::make_overlays(VideoOverlaySet& items) const{ + m_dialog.make_overlays(items); + m_opponent.make_overlays(items); + m_player.make_overlays(items); +} +bool BattleScreenDetector::detect(const ImageViewRGB32& screen){ + if (m_menu.detect(screen)){ + return true; + } + return m_dialog.detect(screen) && (m_opponent.detect(screen) || m_player.detect(screen)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.h new file mode 100644 index 0000000000..ac4257c707 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.h @@ -0,0 +1,54 @@ +/* Battle Screen Detector + * + * From: https://github.com/PokemonAutomation/ + * + * A battle on screen: the battle text box, and with it the opponent's HP + * box, the player's or the battle menu. The text box alone also reads on + * the black below a small indoor map (console B, 2026-11-01: Oak's Lab's + * mat, Viridian Forest's north gate). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_BattleScreenDetector_H +#define PokemonAutomation_PokemonFRLG_BattleScreenDetector_H + +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/VisualDetector.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +class BattleScreenDetector : public StaticScreenDetector{ +public: + BattleScreenDetector(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + BattleDialogDetector m_dialog; + BattleMenuDetector m_menu; + BattleOpponentDetector m_opponent; + BattlePokemonDetector m_player; +}; + +class BattleScreenWatcher : public DetectorToFinder{ +public: + BattleScreenWatcher(Color color = COLOR_RED) + : DetectorToFinder("BattleScreenWatcher", std::chrono::milliseconds(250), color) + {} +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp new file mode 100644 index 0000000000..348b57bcf6 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -0,0 +1,445 @@ +/* Overworld Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" +#include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_StartMenuDetector.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" +#include "PokemonFRLG_PlayerFacingReader.h" +#include "PokemonFRLG_OverworldWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace std::chrono_literals; +using namespace OverworldWalkSteps; + + +namespace{ + +constexpr Milliseconds FACE_SETTLE = 300ms; // the turn animation, before A reaches what is ahead +// From the end of the hold, through the fade out and in. +constexpr Milliseconds WARP_TIMEOUT = 8000ms; +constexpr Milliseconds SURF_PROMPT_TIMEOUT = 5000ms; +constexpr Milliseconds SURF_ANIMATION = 3000ms; +// After a message closes: a trainer who saw the player speaks first, then +// the battle starts, with " would like to battle!" seconds later. +constexpr Milliseconds TRAINER_BATTLE_WATCH = 6000ms; + + +// A wild battle, seen by the flashes it starts with. +class WildBattleInterrupt : public OverworldInterrupt{ +public: + virtual std::string name() const override{ + return "a wild battle"; + } + virtual std::unique_ptr make_watcher() const override{ + return std::make_unique(COLOR_RED); + } + // The first flash shows about 130ms after the step on the console, and + // the watcher needs it to start fading back. Not calibrated yet. + virtual Milliseconds detection_delay() const override{ + return 300ms; + } + virtual void clear(ConsoleHandle& console, ProControllerContext& context) const override{ + flee_battle(console, context); + } +}; + +// A wild battle whose flashes were missed, seen once "Wild +// appeared!" shows. Where an open leg stopped is then only a rough guess. +class LateWildBattleInterrupt : public WildBattleInterrupt{ +public: + virtual std::string name() const override{ + return "a wild battle (seen late)"; + } + virtual std::unique_ptr make_watcher() const override{ + return std::make_unique(COLOR_RED); + } + // The flashes, the wipe and the opening animation: 3.7s after the step's + // press, twice on Route 1 (console B, 2026-11-01). Other maps open + // battles with other wipes, so elsewhere this is a guess. + virtual Milliseconds detection_delay() const override{ + return 3700ms; + } +}; + +class MessageInterrupt : public OverworldInterrupt{ +public: + virtual std::string name() const override{ + return "a message"; + } + virtual std::unique_ptr make_watcher() const override{ + return std::make_unique(COLOR_RED); + } + // The box shows as the step ends; then video lag and the watcher's + // 250ms. Not calibrated yet. + virtual Milliseconds detection_delay() const override{ + return 400ms; + } + virtual void clear(ConsoleHandle& console, ProControllerContext& context) const override{ + WhiteDialogOverWatcher closed(COLOR_RED); + context.wait_for_all_requests(); + const int ret = run_until( + console, context, + [](ProControllerContext& context){ + for (int c = 0; c < 5; c++){ + pbf_press_button(context, BUTTON_B, 160ms, 840ms); + } + }, + {closed} + ); + if (ret < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): a message on the overworld didn't close.", + console + ); + } + check_no_trainer_battle(console, context); + } + +private: + // A trainer's battle can't be fled, and the walker must not try (console + // B, 2026-11-01, Viridian Forest): it stops here for the caller. + static void check_no_trainer_battle(ConsoleHandle& console, ProControllerContext& context){ + BattleScreenWatcher battle(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, TRAINER_BATTLE_WATCH, {battle}) >= 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::NO_ERROR_REPORT, + "walk_overworld_route(): trainer battle: a battle started after a message on the overworld.", + console + ); + } + } +}; + +// A message, taken to be "REPEL's effect wore off...", after which another +// Max Repel is used from the Items pocket. +class RepelWoreOffInterrupt : public MessageInterrupt{ +public: + explicit RepelWoreOffInterrupt(int max_repel_slot) + : m_max_repel_slot(max_repel_slot) + {} + virtual std::string name() const override{ + return "a message (Repel wore off?)"; + } + virtual void clear(ConsoleHandle& console, ProControllerContext& context) const override{ + MessageInterrupt::clear(console, context); + use_max_repel_from_bag(console, context, m_max_repel_slot); + } + +private: + int m_max_repel_slot; +}; + + +Pokemon::LegSpec leg_spec(const RouteStep& step, MoveSpeed speed){ + return Pokemon::LegSpec{step.direction, step.tiles, step.to_wall, tile_time(step, speed)}; +} + +} + + +namespace OverworldWalkSteps{ + +DpadPosition dpad(Direction direction){ + switch (direction){ + case Direction::NORTH: + return DPAD_UP; + case Direction::EAST: + return DPAD_RIGHT; + case Direction::SOUTH: + return DPAD_DOWN; + case Direction::WEST: + return DPAD_LEFT; + } + return DPAD_NONE; +} + +void hold_dpad(ProControllerContext& context, Direction direction, Milliseconds duration){ + pbf_press_dpad(context, dpad(direction), duration, STEP_RELEASE); +} + +void hold_dpad_running(ProControllerContext& context, Direction direction, Milliseconds duration){ + pbf_controller_state(context, BUTTON_B, dpad(direction), {0, 0}, {0, 0}, duration); + pbf_wait(context, STEP_RELEASE); +} + +Milliseconds tile_time(const RouteStep& step, MoveSpeed speed){ + if (step.surfing){ + return SURF_TILE_TIME; + } + return speed == MoveSpeed::RUN ? RUN_TILE_TIME : TILE_TIME; +} + +Pokemon::LegMover leg_mover(const RouteStep& step, MoveSpeed speed){ + if (speed == MoveSpeed::RUN && !step.surfing){ + return hold_dpad_running; + } + return hold_dpad; +} + +Milliseconds leg_hold_time(const RouteStep& step, MoveSpeed speed){ + return Pokemon::leg_hold(leg_spec(step, speed), step.tiles); +} + +std::string speed_note(const RouteStep& step, MoveSpeed speed){ + if (step.action != RouteAction::WALK || !step.to_wall || step.surfing){ + return ""; + } + return std::format( + "[Speed] {} {}: {} tiles in {} ms, held {} ms", + move_speed_name(speed), Pokemon::direction_name(step.direction), step.tiles, + (tile_time(step, speed) * step.tiles).count(), leg_hold_time(step, speed).count() + ); +} + +// One watcher for the whole fade: a ladder's black is short enough to be +// over before a second watcher, started once the fade is seen, looks. +void wait_out_warp(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step){ + BlackScreenOverWatcher arrival(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, WARP_TIMEOUT, {arrival}) < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): no fade in and out after walking onto the warp to " + step.end.map + ".", + console + ); + } +} + +// Fails unless the overworld is up. Under the START menu or a message the +// D-pad only moves a cursor, and legs to a wall seem to go fine: three +// "walked" that way in Viridian Forest without the player moving (console B, +// 2026-11-01). +void check_overworld(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step){ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + if (!screen){ + return; + } + const char* open = nullptr; + if (StartMenuDetector(COLOR_RED).detect(screen)){ + open = "the START menu"; + }else if (SelectionDialogDetector(COLOR_RED).detect(screen)){ + open = "a menu"; + }else if (WhiteDialogDetector(COLOR_RED).detect(screen)){ + open = "a message"; + } + if (open != nullptr){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + std::string("walk_overworld_route(): ") + open + " is open, before: " + to_string(step), + console + ); + } +} + +void clear_open_message( + ConsoleHandle& console, ProControllerContext& context, + const OverworldInterrupts& interrupts, const RouteStep& step +){ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + if (!screen || !WhiteDialogDetector(COLOR_RED).detect(screen)){ + return; + } + console.log("Route: a message is open before " + to_string(step) + "; handing it to the leg's interrupts.", COLOR_ORANGE); + Pokemon::clear_late_interrupts(console, context, interrupts, OPEN_MESSAGE_WATCH, 1); +} + +// After a warp's fade: fails, once the battle is fled, if it was a wild +// battle's. In run 7 (console B, 2026-11-01) the player had drifted onto +// Route 1's grass, and the step meant for Oak's Lab's door set off a +// battle; its flashes and wipe passed for the warp's fade, the route went +// on as if inside, and the next screen was the battle's FIGHT menu. +void check_warp_was_no_battle(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step){ + BattleScreenWatcher battle(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, AFTER_WARP_BATTLE_WATCH, {battle}) < 0){ + return; + } + flee_battle(console, context); + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): a wild battle, not the warp to " + step.end.map + ": the player is off the route.", + console + ); +} + +// Each leg starts with a turn tap unless it goes the way the player faces. +// A tap while already facing that way takes a step, so a facing not given +// is read from the screen. If that fails too it taps anyway. +std::optional read_facing(ConsoleHandle& console, ProControllerContext& context){ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + std::optional facing; + if (screen){ + facing = read_player_facing(screen); + } + console.log(std::string("Route: the player faces ") + (facing ? Pokemon::direction_name(*facing) : "an unknown way") + "."); + return facing; +} + +void start_surf(ConsoleHandle& console, ProControllerContext& context, Direction direction, bool facing_water){ + // "The water is a deep blue... Would you like to SURF?", with YES selected. + // An A pressed while the player is still turning is lost. + if (!facing_water){ + pbf_press_dpad(context, dpad(direction), FACE_HOLD, FACE_SETTLE); + } + pbf_press_button(context, BUTTON_A, 160ms, 0ms); + SelectionDialogWatcher prompt(COLOR_RED); + context.wait_for_all_requests(); + if (wait_until(console, context, SURF_PROMPT_TIMEOUT, {prompt}) < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "walk_overworld_route(): no Surf prompt facing the water.", + console + ); + } + pbf_press_button(context, BUTTON_A, 160ms, 1000ms); + + // " used SURF!" waits for A, then the player jumps onto the water. + for (int c = 0; c < 5; c++){ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + if (!screen || !WhiteDialogDetector(COLOR_RED).detect(screen)){ + break; + } + pbf_press_button(context, BUTTON_A, 160ms, 1000ms); + } + pbf_wait(context, SURF_ANIMATION); +} + +} + + + +const char* move_speed_name(MoveSpeed speed){ + switch (speed){ + case MoveSpeed::WALK: + return "walk"; + case MoveSpeed::RUN: + return "run"; + } + return "?"; +} + +OverworldInterruptSet overworld_interrupts_with_repel(int max_repel_slot){ + OverworldInterruptSet ret; + ret.wild.emplace_back(std::make_unique()); + ret.wild.emplace_back(std::make_unique()); + ret.wild.emplace_back(std::make_unique(max_repel_slot)); + ret.other.emplace_back(std::make_unique(max_repel_slot)); + return ret; +} + +void use_max_repel_from_bag(ConsoleHandle& console, ProControllerContext& context, int slot){ + console.log(std::format("Using a Max Repel (Items pocket, slot {}).", slot + 1)); + open_bag_from_overworld(console, context); + // The bag is seen while it still slides in, and ignores presses until + // it is done. + pbf_wait(context, 1000ms); + // To the Items pocket, then to the top of it. + for (int c = 0; c < 3; c++){ + pbf_press_dpad(context, DPAD_LEFT, 100ms, 600ms); + } + for (int c = 0; c < 20; c++){ + pbf_press_dpad(context, DPAD_UP, 100ms, 100ms); + } + for (int c = 0; c < slot; c++){ + pbf_press_dpad(context, DPAD_DOWN, 100ms, 300ms); + } + // USE, " used the MAX REPEL.", back to the bag, then out of the + // bag and the start menu. A third A here would open the item's menu again. + pbf_press_button(context, BUTTON_A, 160ms, 800ms); + pbf_press_button(context, BUTTON_A, 160ms, 1500ms); + pbf_press_button(context, BUTTON_A, 160ms, 1500ms); + pbf_press_button(context, BUTTON_B, 160ms, 2000ms); + pbf_press_button(context, BUTTON_B, 160ms, 1000ms); + context.wait_for_all_requests(); +} + +const OverworldInterruptSet& default_overworld_interrupts(){ + // Battles first: their screens are checked before the message box's. + static const OverworldInterruptSet interrupts = []{ + OverworldInterruptSet ret; + ret.wild.emplace_back(std::make_unique()); + ret.wild.emplace_back(std::make_unique()); + ret.wild.emplace_back(std::make_unique()); + ret.other.emplace_back(std::make_unique()); + return ret; + }(); + return interrupts; +} + + +void walk_overworld_route( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& route, + std::optional facing, + const OverworldInterruptSet& interrupts, + MoveSpeed speed +){ + if (!facing){ + facing = read_facing(console, context); + } + for (const RouteStep& step : route){ + console.log("Route: " + to_string(step)); + clear_open_message(console, context, step.wild ? interrupts.wild : interrupts.other, step); + check_overworld(console, context, step); + switch (step.action){ + case RouteAction::WALK: + // Turning first, with a tap, makes the hold start from a standstill + // facing the right way: holding through a turn lost a tile on the + // console, while holds from a standstill were exact over 15 tiles. + if (facing != step.direction){ + pbf_press_dpad(context, dpad(step.direction), TURN_TAP, TURN_SETTLE); + } + if (const std::string note = speed_note(step, speed); !note.empty()){ + console.log(note); + } + Pokemon::walk_leg(console, context, leg_mover(step, speed), step.wild ? interrupts.wild : interrupts.other, leg_spec(step, speed), MAX_INTERRUPTIONS_PER_LEG); + if (step.warps){ + wait_out_warp(console, context, step); + static_assert(AFTER_WARP_BATTLE_WATCH >= AFTER_WARP); + check_warp_was_no_battle(console, context, step); + } + break; + case RouteAction::SURF: + start_surf(console, context, step.direction, facing == step.direction); + break; + case RouteAction::FACE: + pbf_press_dpad(context, dpad(step.direction), FACE_HOLD, STEP_RELEASE); + break; + } + if (step.action != RouteAction::FACE){ + Pokemon::clear_late_interrupts( + console, context, step.wild ? interrupts.wild : interrupts.other, + step.wild ? AFTER_WILD_LEG_WATCH : AFTER_LEG_WATCH, MAX_INTERRUPTIONS_PER_LEG + ); + } + facing = step.facing_after; + } + context.wait_for_all_requests(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h new file mode 100644 index 0000000000..2c3b665033 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -0,0 +1,183 @@ +/* Overworld Walk + * + * From: https://github.com/PokemonAutomation/ + * + * Walks a planned overworld route with the D-pad. FRLG moves one tile per + * 16 frames and finishes a step once it starts, so a leg is one hold timed + * to land mid-way between the last tile and one more. Warps are waited out + * by their fade; Surf goes through its dialog. + * + * A message or a wild battle can stop the player part-way through a leg. + * Each kind is an OverworldInterrupt: the walker watches for all of them + * while it holds the D-pad, and when one shows it stops, has it cleared and + * walks the rest of the leg. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldWalk_H +#define PokemonAutomation_PokemonFRLG_OverworldWalk_H + +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Pokemon/Navigation/Pokemon_LegWalker.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Something that stops the player part-way through a leg: a wild battle, a +// message. See Pokemon::LegInterrupt. +using OverworldInterrupt = Pokemon::LegInterrupt; +using OverworldInterrupts = Pokemon::LegInterrupts; + +// What to watch for on a route's steps: `wild` on steps where a wild +// Pokemon can appear (RouteStep::wild), `other` on the rest. Watching for a +// battle where none can start only risks a false one: walking to the edge +// of a small room, the screen darkens like a battle's flash (console B, +// 2026-11-01, the bedroom's east wall). +struct OverworldInterruptSet{ + OverworldInterrupts wild; + OverworldInterrupts other; +}; + +// A wild battle, run from, and a message (e.g. "REPEL's effect wore off..."), +// closed with B. See OverworldInterruptSet. +const OverworldInterruptSet& default_overworld_interrupts(); + +// The same, except that after a message another Max Repel is used, from +// slot `max_repel_slot` (from 0) of the Items pocket. Any message on the +// overworld is taken to be the one saying the last Repel wore off. +OverworldInterruptSet overworld_interrupts_with_repel(int max_repel_slot); + +// From the overworld, uses the Max Repel in `slot` (from 0) of the Items +// pocket and returns to the overworld. +void use_max_repel_from_bag(ConsoleHandle& console, ProControllerContext& context, int slot); + + +// How the player moves on foot: walking, or running with B held, which +// needs the Running Shoes (FLAG_SYS_B_DASH, from Oak's aide in Pewter City +// after Brock). On the water the player surfs at its own pace either way. +enum class MoveSpeed{ + WALK, + RUN, +}; +const char* move_speed_name(MoveSpeed speed); + + +// Walks `route` from where it was planned, at `speed`. The player must be +// standing still on the overworld and not on the bike. `facing` is the way +// the player faces at the start; if not given, it is read from the screen. +// Throws OperationFailedException if a warp's fade or the Surf prompt +// doesn't show, or a leg is interrupted too many times. +void walk_overworld_route( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& route, + std::optional facing = std::nullopt, + const OverworldInterruptSet& interrupts = default_overworld_interrupts(), + MoveSpeed speed = MoveSpeed::WALK +); + + +// The pieces walk_overworld_route() is made of, for a walker that runs a +// route its own way. Each throws OperationFailedException as +// walk_overworld_route() describes. +namespace OverworldWalkSteps{ + +inline constexpr Milliseconds TILE_TIME(267); // 16 frames at 60 fps, measured exact over 15 tiles +inline constexpr Milliseconds SURF_TILE_TIME(133); // the game moves a surfing player at running speed +// 8 frames: running is 2 pixels a frame, twice walking. Not measured on the +// console yet; the walker logs each leg to a wall (speed_note()) for that. +inline constexpr Milliseconds RUN_TILE_TIME(133); +inline constexpr Milliseconds TURN_TAP(40); // turns a player facing another way, without a step +inline constexpr Milliseconds TURN_SETTLE(300); +inline constexpr Milliseconds FACE_HOLD(40); // short enough to turn without stepping +inline constexpr Milliseconds STEP_RELEASE(100); +// After a warp's fade: leaving by a door the game walks the player a tile out +// on its own, and a leg started before that ends loses tiles (on the console a +// 600ms wait cost 2 tiles). +inline constexpr Milliseconds AFTER_WARP(1500); +// After a warp's fade, which a wild battle's flashes and wipe can pass for: +// a battle's HP box shows within this long (console B, 2026-11-01). +inline constexpr Milliseconds AFTER_WARP_BATTLE_WATCH(5000); +// A message set off by a leg's last step shows after the hold ends. Caught +// here, before the next leg's turn tap, which the message would swallow. +inline constexpr Milliseconds AFTER_LEG_WATCH(500); +// The same after a leg through grass. A battle whose flashes are missed is +// seen only by its "Wild appeared!", 3.7s after its step's press +// (twice on Route 1, console B, 2026-11-01, run 7). Unless the walker waits +// that long, the next leg is walked into the battle and lost: there, "east +// 4" and "west 4". +inline constexpr Milliseconds AFTER_WILD_LEG_WATCH(3500); +// How long clear_open_message() watches for the message it saw. +inline constexpr Milliseconds OPEN_MESSAGE_WATCH(1000); +inline constexpr size_t MAX_INTERRUPTIONS_PER_LEG = 5; + +DpadPosition dpad(Direction direction); + +// The leg mover: holds the D-pad. FRLG moves exactly a tile per 16 frames. +void hold_dpad(ProControllerContext& context, Direction direction, Milliseconds duration); + +// The leg mover for running: holds the D-pad with B for the whole hold. +// Turn taps go without B: a tap only turns the player. +void hold_dpad_running(ProControllerContext& context, Direction direction, Milliseconds duration); + +// How long one tile of `step` takes at `speed`: on the water, the Surf +// pace whatever the speed. +Milliseconds tile_time(const RouteStep& step, MoveSpeed speed); + +// The mover for `step` at `speed`: hold_dpad_running() for a run on foot, +// hold_dpad() otherwise (B does nothing on the water). +Pokemon::LegMover leg_mover(const RouteStep& step, MoveSpeed speed); + +// How long the walker holds the D-pad for a WALK `step` at `speed` +// (Pokemon::leg_hold()). +Milliseconds leg_hold_time(const RouteStep& step, MoveSpeed speed); + +// For a leg to a wall on foot, the line the walker logs so a console run +// can check the pace: "[Speed] run west: 40 tiles in 5320 ms, held 5586 ms", +// the tiles at the assumed pace and the hold. Empty for other steps. +std::string speed_note(const RouteStep& step, MoveSpeed speed); + +// After a hold onto `step`'s warp: waits out its fade. +void wait_out_warp(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step); + +// Before `step`: fails unless the overworld is up. +void check_overworld(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step); + +// Before `step`: if a message is open (a trainer who saw the player on the +// last leg and spoke after its watch), hands what shows to `interrupts`, +// once, instead of failing at once. check_overworld() then fails if the +// overworld still isn't up. +void clear_open_message( + ConsoleHandle& console, ProControllerContext& context, + const OverworldInterrupts& interrupts, const RouteStep& step +); + +// After `step`'s warp: fails, once the battle is fled, if its fade was a +// wild battle's. +void check_warp_was_no_battle(ConsoleHandle& console, ProControllerContext& context, const RouteStep& step); + +// The way the player faces, read from the screen; nothing if it can't be read. +std::optional read_facing(ConsoleHandle& console, ProControllerContext& context); + +// Facing `direction` (turning first unless `facing_water`), starts Surf. +void start_surf(ConsoleHandle& console, ProControllerContext& context, Direction direction, bool facing_water); + +} + + +void add_tests_OverworldWalk(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp new file mode 100644 index 0000000000..8265cb6bd2 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp @@ -0,0 +1,101 @@ +/* Overworld Walk Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_OverworldWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace OverworldWalkSteps; + + +namespace{ + +class Test_OverworldWalk : public UnitTest{ +public: + Test_OverworldWalk(std::string name, std::function body) + : UnitTest("PokemonFRLG::OverworldWalk - " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + return m_body(); + } + +private: + std::function m_body; +}; + + +struct HoldCase{ + std::string name; + MoveSpeed speed; + int tiles; + bool to_wall; + bool surfing; + // How long the D-pad is held. + Milliseconds hold; + // speed_note()'s line, or empty. + std::string note; +}; + +// An open leg lets go half a tile before its last step would end; a leg +// to a wall is held two tiles longer. Walking is 267ms a tile, running and +// surfing 133ms. +const std::vector& hold_cases(){ + static const std::vector cases{ + {.name = "walk, open", .speed = MoveSpeed::WALK, .tiles = 5, .to_wall = false, .surfing = false, + .hold = Milliseconds(4 * 267 + 133), .note = ""}, + {.name = "walk, to a wall", .speed = MoveSpeed::WALK, .tiles = 5, .to_wall = true, .surfing = false, + .hold = Milliseconds(7 * 267), .note = "[Speed] walk west: 5 tiles in 1335 ms, held 1869 ms"}, + {.name = "run, open", .speed = MoveSpeed::RUN, .tiles = 5, .to_wall = false, .surfing = false, + .hold = Milliseconds(4 * 133 + 66), .note = ""}, + {.name = "run, to a wall", .speed = MoveSpeed::RUN, .tiles = 40, .to_wall = true, .surfing = false, + .hold = Milliseconds(42 * 133), .note = "[Speed] run west: 40 tiles in 5320 ms, held 5586 ms"}, + {.name = "run, surfing to a wall", .speed = MoveSpeed::RUN, .tiles = 3, .to_wall = true, .surfing = true, + .hold = Milliseconds(5 * 133), .note = ""}, + {.name = "walk, surfing", .speed = MoveSpeed::WALK, .tiles = 3, .to_wall = false, .surfing = true, + .hold = Milliseconds(2 * 133 + 66), .note = ""}, + }; + return cases; +} + +UnitTestResult check_hold(const HoldCase& test){ + RouteStep step; + step.direction = Direction::WEST; + step.tiles = test.tiles; + step.to_wall = test.to_wall; + step.surfing = test.surfing; + const Milliseconds hold = leg_hold_time(step, test.speed); + if (hold != test.hold){ + return std::format("Expected a hold of {}ms, got {}ms.", test.hold.count(), hold.count()); + } + const std::string note = speed_note(step, test.speed); + if (note != test.note){ + return "Expected the note \"" + test.note + "\", got \"" + note + "\"."; + } + return true; +} + +} + + +void add_tests_OverworldWalk(UnitTestDatabase& database){ + for (const HoldCase& test : hold_cases()){ + database.add("hold: " + test.name, [&test]{ return check_hold(test); }); + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 7378cf7599..f144a90940 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -29,6 +29,7 @@ #include "Inference/PokemonFRLG_WildEncounterReader.h" #include "Overworld/PokemonFRLG_OverworldMaps.h" #include "Overworld/PokemonFRLG_OverworldRoute.h" +#include "Overworld/PokemonFRLG_OverworldWalk.h" #include "Overworld/PokemonFRLG_PlayerFacingReader.h" #include "PokemonFRLG_Navigation.h" #include "Tests/TestUtils.h" @@ -293,6 +294,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_ExitWildBattle(database); add_tests_OverworldMaps(database); add_tests_OverworldRoute(database); + add_tests_OverworldWalk(database); add_tests_PlayerFacingReader(database); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index c096e70382..b072a343c3 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1761,8 +1761,12 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_TrainerCardDetector.h Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.cpp Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleLevelUpReader.h + Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.cpp + Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleFlashWatcher.h Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.cpp Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h + Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.cpp + Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleScreenDetector.h Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_DaycareManDetector.cpp @@ -1798,6 +1802,9 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.cpp