From ade83c4268a365c18913d1f74183ae0e4427ba66 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 12:01:46 -0800 Subject: [PATCH 01/98] BDSP AutoStory: inference and detectors for new game to Floaroma The screen readers the story's segments 00 and 01 use, in their own files, with the upstream detectors (DialogDetector, MenuDetector, BattleMenuDetector, SelectionArrow, GameNavigation) left as they are on main: - Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters, kept nearly as written: TransitionWatchers, ShopDetectors and the shop item name and price readers, BattleOpponentNameReader, the BDSP move and type data, the AutoStory OverworldDetector, MenuCursorDetector and OverworldMatchers. - BattleMenuReader: the battle command and move under the cursor, from #1717's additions to BattleMenuDetector, beside it rather than in it. - NameListDetector and SystemKeyboardDetector for the new-game intro; NameListDetector finds the menu arrows itself, as SelectionArrow.cpp keeps its finder out of its header. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../NintendoSwitch_SystemKeyboardDetector.cpp | 156 + .../NintendoSwitch_SystemKeyboardDetector.h | 67 + .../PokemonBDSP_BattleOpponentNameReader.cpp | 75 + .../PokemonBDSP_BattleOpponentNameReader.h | 44 + .../PokemonBDSP_NameListDetector.cpp | 96 + .../Inference/PokemonBDSP_NameListDetector.h | 41 + .../Inference/PokemonBDSP_ShopDetectors.cpp | 273 ++ .../Inference/PokemonBDSP_ShopDetectors.h | 123 + .../PokemonBDSP_ShopItemNameReader.cpp | 41 + .../PokemonBDSP_ShopItemNameReader.h | 40 + .../PokemonBDSP_ShopItemPriceReader.cpp | 41 + .../PokemonBDSP_ShopItemPriceReader.h | 40 + .../PokemonBDSP_TransitionWatchers.cpp | 36 + .../PokemonBDSP_TransitionWatchers.h | 56 + .../Data/PokemonBDSP_AutoStory_BDSPData.cpp | 250 ++ .../Data/PokemonBDSP_AutoStory_BDSPData.h | 104 + ...PokemonBDSP_AutoStory_BattleMenuReader.cpp | 150 + .../PokemonBDSP_AutoStory_BattleMenuReader.h | 54 + ...kemonBDSP_AutoStory_MenuCursorDetector.cpp | 292 ++ ...PokemonBDSP_AutoStory_MenuCursorDetector.h | 146 + ...okemonBDSP_AutoStory_OverworldDetector.cpp | 135 + .../PokemonBDSP_AutoStory_OverworldDetector.h | 98 + .../Utils/PokemonBDSP_OverworldMatchers.cpp | 3697 +++++++++++++++++ .../Utils/PokemonBDSP_OverworldMatchers.h | 101 + SerialPrograms/cmake/SourceFiles.cmake | 24 + 25 files changed, 6180 insertions(+) create mode 100644 SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.cpp create mode 100644 SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h diff --git a/SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.cpp b/SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.cpp new file mode 100644 index 0000000000..765baba05f --- /dev/null +++ b/SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.cpp @@ -0,0 +1,156 @@ +/* System Keyboard Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch_SystemKeyboardDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ + +using namespace std::chrono_literals; + + +// Key grid of the QWERTY layout and where each key sits: rows "1".."z", columns "1".."-". +static const char* const KEY_ROWS[] = {"1234567890-", "qwertyuiop/", "asdfghjkl:'", "zxcvbnm,.?!"}; +static constexpr double KEY_ROW_Y[] = {0.500, 0.593, 0.681, 0.770}; +static constexpr double KEY_COL_X0 = 0.077; +static constexpr double KEY_COL_STEP = 0.075; + + +SystemKeyboardDetector::SystemKeyboardDetector(Color color) + : m_color(color) + , m_key_panel(0.15, 0.555, 0.65, 0.020) + , m_bottom_bar(0.10, 0.935, 0.35, 0.030) + , m_ok_button(0.875, 0.760, 0.045, 0.020) +{} +void SystemKeyboardDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_key_panel); + items.add(m_color, m_bottom_bar); + items.add(m_color, m_ok_button); +} +bool SystemKeyboardDetector::detect(const ImageViewRGB32& screen){ + if (!is_white(extract_box_reference(screen, m_key_panel)) || + !is_white(extract_box_reference(screen, m_bottom_bar)) + ){ + return false; + } + // OK is blue, or flat grey while the field is empty (disabled). + const ImageStats ok = image_stats(extract_box_reference(screen, m_ok_button)); + return is_solid(ok, {0.172, 0.276, 0.552}) || is_grey(ok, 600, 690); +} + + +bool find_system_keyboard_cursor(const ImageViewRGB32& screen, int& row, int& col){ + // The cursor is a cyan border around one key. Neighbouring keys overlap that border, + // so take the centroid of all cyan pixels and map it to the nearest key. + const ImageFloatBox area(0.02, 0.44, 0.84, 0.38); + ImageViewRGB32 keys = extract_box_reference(screen, area); + double count = 0, sum_x = 0, sum_y = 0; + for (size_t y = 0; y < keys.height(); y++){ + for (size_t x = 0; x < keys.width(); x++){ + const uint32_t pixel = keys.pixel(x, y); + const int red = (pixel >> 16) & 0xff; + const int green = (pixel >> 8) & 0xff; + const int blue = pixel & 0xff; + if (red < 150 && green > 200 && blue > 190){ + count++; + sum_x += (double)x; + sum_y += (double)y; + } + } + } + // The border is about 0.15% of the screen; a few stray pixels are not a cursor. + if (count < 0.0005 * screen.width() * screen.height()){ + return false; + } + const double cx = area.x + sum_x / count / keys.width() * area.width; + const double cy = area.y + sum_y / count / keys.height() * area.height; + col = (int)std::lround((cx - KEY_COL_X0) / KEY_COL_STEP); + row = 0; + for (int r = 1; r < 4; r++){ + if (std::abs(KEY_ROW_Y[r] - cy) < std::abs(KEY_ROW_Y[row] - cy)){ + row = r; + } + } + return 0 <= col && col < 11; +} + +bool system_keyboard_field_has_text(const ImageViewRGB32& screen){ + // Left part of the field, after the caret's position on an empty field. + const ImageStats stats = image_stats(extract_box_reference(screen, ImageFloatBox(0.11, 0.235, 0.19, 0.030))); + return stats.stddev.sum() > 30; +} + + +void type_on_system_keyboard(VideoStream& stream, ProControllerContext& context, const std::string& text){ + for (char ch : text){ + const char key = (char)std::tolower((unsigned char)ch); + int target_row = -1; + int target_col = -1; + for (int r = 0; r < 4 && target_row < 0; r++){ + const char* p = std::strchr(KEY_ROWS[r], key); + if (p != nullptr && key != '\0'){ + target_row = r; + target_col = (int)(p - KEY_ROWS[r]); + } + } + if (target_row < 0){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + std::string("System keyboard: no key for '") + ch + "'.", + stream + ); + } + + // A button pressed while the keyboard slides in is ignored, so read the cursor + // again after each move until it is on the target. + bool arrived = false; + for (size_t attempt = 0; attempt < 10 && !arrived; attempt++){ + context.wait_for_all_requests(); + int row = -1; + int col = -1; + VideoSnapshot screen = stream.video().snapshot(); + if (!screen || !find_system_keyboard_cursor(screen, row, col)){ + pbf_wait(context, 300ms); + continue; + } + if (row == target_row && col == target_col){ + arrived = true; + break; + } + for (; row < target_row; row++) pbf_press_dpad(context, DPAD_DOWN, 100ms, 200ms); + for (; row > target_row; row--) pbf_press_dpad(context, DPAD_UP, 100ms, 200ms); + for (; col < target_col; col++) pbf_press_dpad(context, DPAD_RIGHT, 100ms, 200ms); + for (; col > target_col; col--) pbf_press_dpad(context, DPAD_LEFT, 100ms, 200ms); + pbf_wait(context, 300ms); + } + if (!arrived){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + std::string("System keyboard: the cursor did not reach '") + key + "'.", + stream + ); + } + if (std::isupper((unsigned char)ch)){ + pbf_press_button(context, BUTTON_LCLICK, 100ms, 250ms); + } + pbf_press_button(context, BUTTON_A, 80ms, 300ms); + } + context.wait_for_all_requests(); +} + + + +} +} diff --git a/SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h b/SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h new file mode 100644 index 0000000000..aa23e7ec81 --- /dev/null +++ b/SerialPrograms/Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h @@ -0,0 +1,67 @@ +/* System Keyboard Detector + * + * From: https://github.com/PokemonAutomation/ + * + * The Switch's own software keyboard, which games open for names (e.g. the + * player's name in BDSP): detect it, find the key under its cursor, and type + * a string by moving the cursor and checking where it landed. + * + * Measured on 1920x1080 captures of the QWERTY layout. + * + */ + +#ifndef PokemonAutomation_NintendoSwitch_SystemKeyboardDetector_H +#define PokemonAutomation_NintendoSwitch_SystemKeyboardDetector_H + +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "CommonTools/VisualDetector.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ + + +// White key panel and bottom bar, and the blue OK button. +class SystemKeyboardDetector : public StaticScreenDetector{ +public: + SystemKeyboardDetector(Color color = COLOR_CYAN); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ImageFloatBox m_key_panel; + ImageFloatBox m_bottom_bar; + ImageFloatBox m_ok_button; +}; +class SystemKeyboardWatcher : public DetectorToFinder{ +public: + SystemKeyboardWatcher(Color color = COLOR_CYAN) + : DetectorToFinder("SystemKeyboardWatcher", std::chrono::milliseconds(250), color) + {} +}; + + +// The key under the cursor, as row (0: digits .. 3: "zxcv") and column (0..10). +// The keyboard remembers the cursor between uses, so read it, don't assume it. +// Returns false if no cursor border is visible. +bool find_system_keyboard_cursor(const ImageViewRGB32& screen, int& row, int& col); + +// Whether the text field holds any text. Pressing B on an empty field closes the +// keyboard, so delete only while this is true. +bool system_keyboard_field_has_text(const ImageViewRGB32& screen); + +// Types `text` (letters, digits and the symbols on the QWERTY grid). For each character, +// reads the cursor, moves it with the D-pad and checks it arrived before pressing A; an +// uppercase letter is typed with Shift (left stick click). Throws, with a screenshot, if +// the cursor can't be read or doesn't arrive. +void type_on_system_keyboard(VideoStream& stream, ProControllerContext& context, const std::string& text); + + + +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp new file mode 100644 index 0000000000..5934464203 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp @@ -0,0 +1,75 @@ +/* BDSP Battle Opponent Name Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "CommonTools/OCR/OCR_Routines.h" +#include "Pokemon/Inference/Pokemon_NameReader.h" +#include "PokemonBDSP_BattleOpponentNameReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace Pokemon; + + +// ⚠ These regions are estimated from BDSP's typical battle layout. +// Calibrate against a real capture if OCR reads are consistently wrong. +// +// In BDSP singles, the one opponent's info box is in the top-right quadrant. +// In BDSP doubles, two boxes sit side-by-side in the top half of the screen. +static const ImageFloatBox BOX_SINGLES_OPP (0.505, 0.010, 0.420, 0.085); +static const ImageFloatBox BOX_DOUBLES_LEFT (0.505, 0.010, 0.200, 0.085); +static const ImageFloatBox BOX_DOUBLES_RIGHT (0.710, 0.010, 0.260, 0.085); + + +static std::string ocr_name_box( + Logger& logger, + Language language, + const ImageViewRGB32& frame, + const ImageFloatBox& box +){ + ImageViewRGB32 cropped = extract_box_reference(frame, box); + OCR::StringMatchResult result = PokemonNameReader::instance().read_substring( + logger, + language, + cropped, + OCR::BLACK_OR_WHITE_TEXT_FILTERS() + ); + if (result.results.empty()){ + return ""; + } + // Use the best match (lowest log10p) if it meets the confidence threshold + double log10p = result.results.begin()->first; + if (log10p > PokemonNameReader::MAX_LOG10P){ + return ""; + } + return result.results.begin()->second.token; +} + + +std::vector read_battle_opponent_names( + Logger& logger, + Language language, + const ImageViewRGB32& frame, + BDSPBattleType battle_type +){ + std::vector names; + if (battle_type == BDSPBattleType::SINGLES){ + names.push_back(ocr_name_box(logger, language, frame, BOX_SINGLES_OPP)); + } else { + names.push_back(ocr_name_box(logger, language, frame, BOX_DOUBLES_LEFT)); + names.push_back(ocr_name_box(logger, language, frame, BOX_DOUBLES_RIGHT)); + } + return names; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h b/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h new file mode 100644 index 0000000000..b1592c8705 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h @@ -0,0 +1,44 @@ +/* BDSP Battle Opponent Name Reader + * + * From: https://github.com/PokemonAutomation/ + * + * OCR-based detection of opponent pokemon names from the BDSP battle screen. + * Used by the outside-help battle handler to track which pokemon are currently + * on the field and update move selection accordingly. + * + * ⚠ Screen region coordinates are estimates and may require calibration + * against a real BDSP capture before they work reliably in production. + */ + +#ifndef PokemonAutomation_PokemonBDSP_BattleOpponentNameReader_H +#define PokemonAutomation_PokemonBDSP_BattleOpponentNameReader_H + +#include +#include +#include "CommonFramework/Language.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h" + +namespace PokemonAutomation{ + class Logger; + class ImageViewRGB32; +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Read the opponent pokemon name boxes at the top of the BDSP battle screen. +// +// Returns one slug per opponent slot (up to 1 for singles, 2 for doubles). +// An empty string in a slot means OCR confidence was too low to identify the pokemon. +// The caller should keep the previous state for any slot that returns empty string. +std::vector read_battle_opponent_names( + Logger& logger, + Language language, + const ImageViewRGB32& frame, + BDSPBattleType battle_type +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.cpp new file mode 100644 index 0000000000..456e914374 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.cpp @@ -0,0 +1,96 @@ +/* Name List Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Kernels/Waterfill/Kernels_Waterfill.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonTools/Images/BinaryImage_FilterRgb32.h" +#include "CommonTools/ImageMatch/ExactImageMatcher.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "PokemonBDSP_NameListDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ +// Row centers, as a fraction of the screen height (1080p: 307, 370, 433, ...). +const double FIRST_ROW_Y = 0.284; +const double ROW_STEP_Y = 0.0587; +const int ROWS = 5; + +// The menu's selection arrows in `image`, as PokemonBDSP_SelectionArrow.cpp +// finds them: that file keeps its finder out of its header, so the same +// waterfill and template match are repeated here rather than changing it. +const ImageMatch::ExactImageMatcher& name_list_arrow(){ + static ImageMatch::ExactImageMatcher matcher(RESOURCE_PATH() + "PokemonBDSP/SelectionArrow.png"); + return matcher; +} +std::vector find_name_list_arrows(const ImageViewRGB32& image){ + using namespace Kernels; + using namespace Kernels::Waterfill; + PackedBinaryMatrix matrix = compress_rgb32_to_binary_max(image, 200, 200, 200); + std::vector objects = find_objects_inplace(matrix, 200); + std::vector ret; + for (const WaterfillObject& object : objects){ + if (object.width() > object.height() || object.height() > 3 * object.width()){ + continue; + } + if (name_list_arrow().rmsd(extract_box_reference(image, object)) <= 90){ + ret.emplace_back(object.min_x, object.min_y, object.max_x, object.max_y); + } + } + return ret; +} +} + + +NameListDetector::NameListDetector(Color color) + : m_color(color) + , m_arrows(0.137, 0.245, 0.030, 0.310) + , m_panel(0.140, 0.245, 0.140, 0.300) +{} +void NameListDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_arrows); + items.add(m_color, m_panel); +} +bool NameListDetector::detect(const ImageViewRGB32& screen){ + return selected_row(screen) >= 0; +} +int NameListDetector::selected_row(const ImageViewRGB32& screen) const{ + // The list is a white panel with a little dark text. A thin margin strip is not + // enough: the border's antialiasing reaches into it on some frames. + const ImageViewRGB32 panel = extract_box_reference(screen, m_panel); + size_t white = 0; + for (size_t y = 0; y < panel.height(); y += 2){ + for (size_t x = 0; x < panel.width(); x += 2){ + const uint32_t p = panel.pixel(x, y); + white += ((p >> 16) & 0xff) > 220 && ((p >> 8) & 0xff) > 220 && (p & 0xff) > 220; + } + } + const size_t samples = ((panel.width() + 1) / 2) * ((panel.height() + 1) / 2); + if (samples == 0 || white < samples * 6 / 10){ + return -1; + } + ImageViewRGB32 area = extract_box_reference(screen, m_arrows); + std::vector arrows = find_name_list_arrows(area); + if (arrows.size() != 1){ + return -1; + } + const double center_y = m_arrows.y + + m_arrows.height * (arrows[0].min_y + arrows[0].max_y) / 2.0 / (double)area.height(); + const int row = (int)std::lround((center_y - FIRST_ROW_Y) / ROW_STEP_Y); + return row >= 0 && row < ROWS ? row : -1; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h new file mode 100644 index 0000000000..59fa1efbeb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h @@ -0,0 +1,41 @@ +/* Name List Detector + * + * From: https://github.com/PokemonAutomation/ + * + * The list of preset names shown for the rival: "I'll tell you...", then four names. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_NameListDetector_H +#define PokemonAutomation_PokemonBDSP_NameListDetector_H + +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonTools/VisualDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class NameListDetector : public StaticScreenDetector{ +public: + NameListDetector(Color color = COLOR_MAGENTA); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + + // The row the arrow is on: 0 is "I'll tell you...", 1 the first preset name. + // -1 if the list is not showing. + int selected_row(const ImageViewRGB32& screen) const; + +private: + Color m_color; + ImageFloatBox m_arrows; + ImageFloatBox m_panel; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp new file mode 100644 index 0000000000..fc48e45fa5 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp @@ -0,0 +1,273 @@ +/* Shop Detectors + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" +#include "CommonTools/Images/BinaryImage_FilterRgb32.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "Kernels/Waterfill/Kernels_Waterfill_Session.h" +#include "Kernels/Waterfill/Kernels_Waterfill_Types.h" +#include "PokemonBDSP_ShopItemNameReader.h" +#include "PokemonBDSP_ShopItemPriceReader.h" +#include "PokemonBDSP_ShopDetectors.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// --------------------------------------------------------------------------- +// ShopBuyingWindowDetector +// --------------------------------------------------------------------------- + +// TODO: Fill in the actual coordinates of the blue bottom border strip. +// At 1280x720, the buying-list bottom bar appears near y ≈ 0.93. +// The colour ratio below matches BoxDetector's bottom bar ({0.190, 0.327, 0.482}). +static const ImageFloatBox SHOP_BUYING_WINDOW_BOX{0.020000, 0.960000, 0.750000, 0.035000}; +static const FloatPixel SHOP_BLUE_RATIO{0.190, 0.327, 0.483}; +static constexpr double SHOP_BLUE_MAX_DISTANCE = 0.10; +static constexpr double SHOP_BLUE_MAX_STDDEV = 15.0; + +ShopBuyingWindowDetector::ShopBuyingWindowDetector(Color color) + : m_color(color) + , m_box(SHOP_BUYING_WINDOW_BOX) +{} +void ShopBuyingWindowDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_box); +} +bool ShopBuyingWindowDetector::detect(const ImageViewRGB32& screen){ + ImageStats stats = image_stats(extract_box_reference(screen, m_box)); + return is_solid(stats, SHOP_BLUE_RATIO, SHOP_BLUE_MAX_DISTANCE, SHOP_BLUE_MAX_STDDEV); +} + + +// --------------------------------------------------------------------------- +// ShopQuantityWindowDetector +// --------------------------------------------------------------------------- + +// TODO: Fill in the coordinates of the yellow quantity-input box. +// It typically appears in the centre-right of the screen when selecting quantity. +static const ImageFloatBox SHOP_QUANTITY_BOX{0.800000, 0.570000, 0.100000, 0.140000}; +// Expected colour ratio for the gold/yellow quantity box. +// High R, high G, low B — adjust if the box colour looks different in your capture. +static const FloatPixel SHOP_YELLOW_RATIO{0.58, 0.38, 0.04}; +static constexpr double SHOP_YELLOW_MAX_DISTANCE = 0.20; +static constexpr double SHOP_YELLOW_MAX_STDDEV = 20.0; + +ShopQuantityWindowDetector::ShopQuantityWindowDetector(Color color) + : m_color(color) + , m_dialog(color) + , m_qty_box(SHOP_QUANTITY_BOX) +{} +void ShopQuantityWindowDetector::make_overlays(VideoOverlaySet& items) const{ + m_dialog.make_overlays(items); + items.add(m_color, m_qty_box); +} +bool ShopQuantityWindowDetector::detect(const ImageViewRGB32& screen){ + if (!m_dialog.detect(screen)){ + return false; + } + ImageStats stats = image_stats(extract_box_reference(screen, m_qty_box)); + return is_solid(stats, SHOP_YELLOW_RATIO, SHOP_YELLOW_MAX_DISTANCE, SHOP_YELLOW_MAX_STDDEV); +} + + +// --------------------------------------------------------------------------- +// ShopItemHoverDetector +// --------------------------------------------------------------------------- + +// Color filter for the red hover rectangle. +static constexpr uint32_t RED_MIN = 0xff800000; // R≥128, G=0, B=0 +static constexpr uint32_t RED_MAX = 0xffff6060; // R≤255, G≤96, B≤96 + +// Minimum pixel area for a red blob to be considered (filters noise). +static constexpr size_t RED_MIN_AREA = 200; + +// The hover rectangle must span at least this fraction of the scan area width. +static constexpr double RED_MIN_WIDTH_RATIO = 0.80; + +// Minimum aspect ratio (width/height) — the row is much wider than tall. +static constexpr double RED_MIN_ASPECT = 4.0; + +// Minimum consecutive frames that must confirm before the detector fires. +static constexpr size_t HOVER_MIN_CONSECUTIVE = 3; + +// OCR region X offsets (as fractions of outline row width). +// Tune NAME_X_OFFSET if the first character of the item name is being cut off. +// Name: [ 5%, 70%] → x_offset=0.05, width_fraction=0.65 +// Price: [70%, 100%] → x_offset=0.70, width_fraction=0.30 +static constexpr double NAME_X_OFFSET = 0.08; +static constexpr double NAME_X_WIDTH = 0.65; +static constexpr double PRICE_X_OFFSET = 0.70; +static constexpr double PRICE_X_WIDTH = 0.30; +// Fixed OCR box height in normalized screen coordinates. The waterfill detects +// only the thin red border lines, so outline_box.height is just the border +// thickness. This constant covers the full item row height instead. +static constexpr double OCR_ROW_HEIGHT = 0.06; + +// Maps item slug → expected price slug. +static const std::map ITEM_PRICE_TABLE{ + {"poke-ball", "200"}, + {"great-ball", "600"}, + {"ultra-ball", "1200"}, + {"heal-ball", "300"}, + {"net-ball", "1000"}, + {"nest-ball", "1000"}, + {"dusk-ball", "1000"}, + {"quick-ball", "1000"}, + {"timer-ball", "1000"}, + {"repeat-ball", "1000"}, + {"luxury-ball", "1000"}, + {"potion", "300"}, + {"super-potion", "700"}, + {"hyper-potion", "1200"}, + {"max-potion", "2500"}, + {"full-restore", "3000"}, + {"revive", "1500"}, + {"antidote", "100"}, + {"paralyze-heal", "200"}, + {"awakening", "250"}, + {"burn-heal", "250"}, + {"ice-heal", "250"}, + {"full-heal", "600"}, + {"escape-rope", "550"}, + {"repel", "350"}, + {"super-repel", "500"}, + {"max-repel", "700"}, +}; + +ShopItemHoverDetector::ShopItemHoverDetector( + Logger& logger, + const ImageFloatBox& scan_area, + const std::string& item_slug, + Language language, + Color color +) + : VisualInferenceCallback("ShopItemHoverDetector") + , m_logger(logger) + , m_color(color) + , m_language(language) + , m_scan_area(scan_area) + , m_item_slug(item_slug) + , m_price_slug(ITEM_PRICE_TABLE.at(item_slug)) +{} + +bool ShopItemHoverDetector::find_outline(const ImageViewRGB32& screen, ImageFloatBox& out_box) const{ + using namespace Kernels::Waterfill; + + const ImageViewRGB32 region = extract_box_reference(screen, m_scan_area); + PackedBinaryMatrix matrix = compress_rgb32_to_binary_range(region, RED_MIN, RED_MAX); + + auto session = make_WaterfillSession(matrix); + auto iter = session->make_iterator(RED_MIN_AREA); + + WaterfillObject object; + while (iter->find_next(object, false)){ + if (object.aspect_ratio() < RED_MIN_ASPECT) continue; + if ((double)object.width() / region.width() < RED_MIN_WIDTH_RATIO) continue; + + out_box = ImageFloatBox{ + m_scan_area.x + (double)object.min_x / region.width() * m_scan_area.width, + m_scan_area.y + (double)object.min_y / region.height() * m_scan_area.height, + (double)object.width() / region.width() * m_scan_area.width, + (double)object.height() / region.height() * m_scan_area.height + }; + return true; + } + return false; +} + +bool ShopItemHoverDetector::match_ocr(const ImageViewRGB32& screen, const ImageFloatBox& outline_box) const{ + m_last_name_box = ImageFloatBox{ + outline_box.x + NAME_X_OFFSET * outline_box.width, + outline_box.y, + NAME_X_WIDTH * outline_box.width, + OCR_ROW_HEIGHT + }; + m_last_price_box = ImageFloatBox{ + outline_box.x + PRICE_X_OFFSET * outline_box.width, + outline_box.y, + PRICE_X_WIDTH * outline_box.width, + OCR_ROW_HEIGHT + }; + const ImageFloatBox& name_box = m_last_name_box; + const ImageFloatBox& price_box = m_last_price_box; + + const ShopItemNameReader& name_reader = ShopItemNameReader::instance(); + OCR::StringMatchResult name_result = name_reader.read_substring( + m_logger, m_language, extract_box_reference(screen, name_box) + ); + name_result.clear_beyond_log10p(ShopItemNameReader::MAX_LOG10P); + if (name_result.results.empty()) return false; + if (name_result.results.begin()->second.token != m_item_slug) return false; + + const ShopItemPriceReader& price_reader = ShopItemPriceReader::instance(); + OCR::StringMatchResult price_result = price_reader.read_substring( + m_logger, m_language, extract_box_reference(screen, price_box) + ); + price_result.clear_beyond_log10p(ShopItemPriceReader::MAX_LOG10P); + if (price_result.results.empty()) return false; + if (price_result.results.begin()->second.token != m_price_slug) return false; + + if (!m_ocr_logged){ + m_ocr_logged = true; + name_result.log(m_logger, ShopItemNameReader::MAX_LOG10P, "want: " + m_item_slug); + price_result.log(m_logger, ShopItemPriceReader::MAX_LOG10P, "want: " + m_price_slug); + } + return true; +} + +bool ShopItemHoverDetector::detect(const ImageViewRGB32& screen){ + ImageFloatBox outline_box; + if (!find_outline(screen, outline_box)){ + m_detected_box = ImageFloatBox{0, 0, 0, 0}; + m_last_name_box = ImageFloatBox{0, 0, 0, 0}; + m_last_price_box = ImageFloatBox{0, 0, 0, 0}; + m_ocr_logged = false; + return false; + } + if (!match_ocr(screen, outline_box)){ + m_last_name_box = ImageFloatBox{0, 0, 0, 0}; + m_last_price_box = ImageFloatBox{0, 0, 0, 0}; + return false; + } + m_detected_box = outline_box; + return true; +} + +void ShopItemHoverDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_scan_area); + if (m_detected_box.width > 0){ + items.add(m_color, m_detected_box); + } + if (m_last_name_box.width > 0){ + items.add(COLOR_BLUE, m_last_name_box); + } + if (m_last_price_box.width > 0){ + items.add(COLOR_GREEN, m_last_price_box); + } +} + +bool ShopItemHoverDetector::process_frame(const ImageViewRGB32& frame, WallClock /*timestamp*/){ + // Only run OCR every 5th frame to reduce CPU load. + if (m_frame_count++ % 5 != 0){ + return false; + } + if (!detect(frame)){ + m_trigger_count = 0; + return false; + } + m_trigger_count++; + return m_trigger_count >= HOVER_MIN_CONSECUTIVE; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h new file mode 100644 index 0000000000..bfe3298e66 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h @@ -0,0 +1,123 @@ +/* Shop Detectors + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_ShopDetectors_H +#define PokemonAutomation_PokemonBDSP_ShopDetectors_H + +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/Language.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "CommonTools/VisualDetector.h" +#include "PokemonBDSP_DialogDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Detects the blue decorative border at the bottom of the screen that appears +// when the mart buying list is open. +class ShopBuyingWindowDetector : public StaticScreenDetector{ +public: + // box: a thin strip covering the blue bottom border (user fills in coordinates). + ShopBuyingWindowDetector(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ImageFloatBox m_box; +}; +class ShopBuyingWindowWatcher : public DetectorToFinder{ +public: + ShopBuyingWindowWatcher(Color color = COLOR_RED) + : DetectorToFinder("ShopBuyingWindowWatcher", std::chrono::milliseconds(250), color) + {} +}; + + +// Detects the quantity-input overlay (yellow/gold box + ShortDialog) that appears +// after selecting an item to buy. +class ShopQuantityWindowDetector : public StaticScreenDetector{ +public: + // box: the area containing the yellow quantity-input box (user fills in coordinates). + ShopQuantityWindowDetector(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ShortDialogDetector m_dialog; + ImageFloatBox m_qty_box; +}; +class ShopQuantityWindowWatcher : public DetectorToFinder{ +public: + ShopQuantityWindowWatcher(Color color = COLOR_RED) + : DetectorToFinder("ShopQuantityWindowWatcher", std::chrono::milliseconds(250), color) + {} +}; + + +// Scans `scan_area` to dynamically locate the red selection outline, then +// verifies the highlighted item by running OCR on the item name and price +// regions within the row and comparing against the expected `item_slug`. +// Fires only when the outline is found AND both OCR checks pass. +class ShopItemHoverDetector : public VisualInferenceCallback{ +public: + // scan_area: normalized screen region that covers the entire shop item list. + // item_slug: identifies the expected item (e.g. "full-restore"). Must match + // a key in ShopItemNameOCR.json and have a known price in the + // internal lookup table. + // language: language used for OCR dictionary lookup. + ShopItemHoverDetector( + Logger& logger, + const ImageFloatBox& scan_area, + const std::string& item_slug, + Language language = Language::English, + Color color = COLOR_CYAN + ); + + bool detect(const ImageViewRGB32& screen); + + // Returns the last outline box confirmed by detect() / process_frame(). + // Valid only after a successful detection; width == 0 if nothing detected yet. + const ImageFloatBox& detected_box() const { return m_detected_box; } + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; + +private: + // Scans scan_area for a horizontal band of red edges (left + right) that + // indicate the selection outline. Fills out_box with the outline's screen- + // normalized bounds and returns true when found. + bool find_outline(const ImageViewRGB32& screen, ImageFloatBox& out_box) const; + + // Runs OCR on the name and price sub-regions of outline_box. + // Returns true when both match the expected item slug and its price. + bool match_ocr(const ImageViewRGB32& screen, const ImageFloatBox& outline_box) const; + + Logger& m_logger; + Color m_color; + Language m_language; + ImageFloatBox m_scan_area; + std::string m_item_slug; + std::string m_price_slug; + mutable ImageFloatBox m_detected_box{0, 0, 0, 0}; + mutable ImageFloatBox m_last_name_box{0, 0, 0, 0}; + mutable ImageFloatBox m_last_price_box{0, 0, 0, 0}; + mutable bool m_ocr_logged = false; + size_t m_trigger_count = 0; + size_t m_frame_count = 0; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp new file mode 100644 index 0000000000..96fa48eae0 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp @@ -0,0 +1,41 @@ +/* Shop Item Name Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonTools/OCR/OCR_Routines.h" +#include "PokemonBDSP_ShopItemNameReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +ShopItemNameReader::ShopItemNameReader() + : SmallDictionaryMatcher("PokemonBDSP/ShopItemNameOCR.json") +{} + +const ShopItemNameReader& ShopItemNameReader::instance(){ + static ShopItemNameReader reader; + return reader; +} + +OCR::StringMatchResult ShopItemNameReader::read_substring( + Logger& logger, + Language language, + const ImageViewRGB32& image, + double min_text_ratio, double max_text_ratio +) const{ + return match_substring_from_image_multifiltered( + &logger, language, image, + OCR::BLACK_TEXT_FILTERS(), + MAX_LOG10P, MAX_LOG10P_SPREAD, + min_text_ratio, max_text_ratio + ); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h new file mode 100644 index 0000000000..ae32b1dda5 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h @@ -0,0 +1,40 @@ +/* Shop Item Name Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_ShopItemNameReader_H +#define PokemonAutomation_PokemonBDSP_ShopItemNameReader_H + +#include "CommonFramework/Language.h" +#include "CommonTools/OCR/OCR_SmallDictionaryMatcher.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class ShopItemNameReader : public OCR::SmallDictionaryMatcher{ +public: + static constexpr double MAX_LOG10P = -1.40; + static constexpr double MAX_LOG10P_SPREAD = 0.50; + +public: + ShopItemNameReader(); + + static const ShopItemNameReader& instance(); + + OCR::StringMatchResult read_substring( + Logger& logger, + Language language, + const ImageViewRGB32& image, + double min_text_ratio = 0.01, double max_text_ratio = 0.50 + ) const; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp new file mode 100644 index 0000000000..82d6cfba3d --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp @@ -0,0 +1,41 @@ +/* Shop Item Price Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonTools/OCR/OCR_Routines.h" +#include "PokemonBDSP_ShopItemPriceReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +ShopItemPriceReader::ShopItemPriceReader() + : SmallDictionaryMatcher("PokemonBDSP/ShopItemPriceOCR.json") +{} + +const ShopItemPriceReader& ShopItemPriceReader::instance(){ + static ShopItemPriceReader reader; + return reader; +} + +OCR::StringMatchResult ShopItemPriceReader::read_substring( + Logger& logger, + Language language, + const ImageViewRGB32& image, + double min_text_ratio, double max_text_ratio +) const{ + return match_substring_from_image_multifiltered( + &logger, language, image, + OCR::BLACK_TEXT_FILTERS(), + MAX_LOG10P, MAX_LOG10P_SPREAD, + min_text_ratio, max_text_ratio + ); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h new file mode 100644 index 0000000000..0d5cc49303 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h @@ -0,0 +1,40 @@ +/* Shop Item Price Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_ShopItemPriceReader_H +#define PokemonAutomation_PokemonBDSP_ShopItemPriceReader_H + +#include "CommonFramework/Language.h" +#include "CommonTools/OCR/OCR_SmallDictionaryMatcher.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class ShopItemPriceReader : public OCR::SmallDictionaryMatcher{ +public: + static constexpr double MAX_LOG10P = -1.40; + static constexpr double MAX_LOG10P_SPREAD = 0.50; + +public: + ShopItemPriceReader(); + + static const ShopItemPriceReader& instance(); + + OCR::StringMatchResult read_substring( + Logger& logger, + Language language, + const ImageViewRGB32& image, + double min_text_ratio = 0.01, double max_text_ratio = 0.50 + ) const; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp new file mode 100644 index 0000000000..3655b5244d --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp @@ -0,0 +1,36 @@ +#include "PokemonBDSP_TransitionWatchers.h" +namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ +WhiteScreenOverWatcher::WhiteScreenOverWatcher( + Color color, const ImageFloatBox& box, + double min_rgb_sum, + double max_stddev_sum, + std::chrono::milliseconds hold_duration, + std::chrono::milliseconds release_duration +) + : VisualInferenceCallback("WhiteScreenOverWatcher") + , m_on(color, box, min_rgb_sum, max_stddev_sum, WhiteScreenWatcher::FinderType::PRESENT, hold_duration) + , m_off(color, box, min_rgb_sum, max_stddev_sum, WhiteScreenWatcher::FinderType::GONE, release_duration) +{} +void WhiteScreenOverWatcher::make_overlays(VideoOverlaySet& items) const{ + m_on.make_overlays(items); +} +bool WhiteScreenOverWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp){ + if (m_white_is_over.load(std::memory_order_acquire)){ + return true; + } + if (!m_has_been_white){ + m_has_been_white = m_on.process_frame(frame, timestamp); + return false; + } + bool is_over = m_off.process_frame(frame, timestamp); + if (!is_over){ + return false; + } + m_white_is_over.store(true, std::memory_order_release); + return true; +} +bool WhiteScreenOverWatcher::white_is_over(const ImageViewRGB32& frame){ + return m_white_is_over.load(std::memory_order_acquire); +} + +}}} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h new file mode 100644 index 0000000000..ce5a34d2cb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h @@ -0,0 +1,56 @@ +#pragma once +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ +// AutoStory-specific transition handling; shared watchers retain upstream behavior. +class WhiteScreenWatcher : public DetectorToFinder{ +public: + WhiteScreenWatcher( + Color color = COLOR_RED, + const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, + double min_rgb_sum = 500, + double max_stddev_sum = 10, + FinderType finder_type = FinderType::PRESENT, + std::chrono::milliseconds duration = std::chrono::milliseconds(100) + ) + : DetectorToFinder("WhiteScreenWatcher", finder_type, duration, color, box, min_rgb_sum, max_stddev_sum) + {} +}; + +class BlackScreenFlashWatcher : public BlackScreenOverWatcher{ +public: + BlackScreenFlashWatcher( + Color color = COLOR_RED, + const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, + double max_rgb_sum = 100, + double max_stddev_sum = 10, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(20), + std::chrono::milliseconds release_duration = std::chrono::milliseconds(100) + ) + : BlackScreenOverWatcher(color, box, max_rgb_sum, max_stddev_sum, hold_duration, release_duration) + {} +}; + +class WhiteScreenOverWatcher : public VisualInferenceCallback{ +public: + WhiteScreenOverWatcher( + Color color = COLOR_RED, + const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, + double min_rgb_sum = 500, + double max_stddev_sum = 10, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(100), + std::chrono::milliseconds release_duration = std::chrono::milliseconds(100) + ); + + bool white_is_over(const ImageViewRGB32& frame); + + virtual void make_overlays(VideoOverlaySet& items) const override; + + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; + +private: + WhiteScreenWatcher m_on; + WhiteScreenWatcher m_off; + bool m_has_been_white = false; + std::atomic m_white_is_over = false; +}; +}}} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp new file mode 100644 index 0000000000..f72ee6f780 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp @@ -0,0 +1,250 @@ +/* BDSP AutoStory - Game Data + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include + +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Json/JsonValue.h" + +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Globals.h" + +#include "PokemonBDSP_AutoStory_BDSPData.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +static const std::string RESOURCE_DIR = "PokemonBDSP/AutoStory/"; + + +// --------------------------------------------------------------------------- +// Shared parsing helpers +// --------------------------------------------------------------------------- + +static PokemonType parse_type(const std::string& slug, const std::string& path){ + static const std::map TABLE{ + {"normal", PokemonType::NORMAL}, + {"fire", PokemonType::FIRE}, + {"fighting", PokemonType::FIGHTING}, + {"water", PokemonType::WATER}, + {"flying", PokemonType::FLYING}, + {"grass", PokemonType::GRASS}, + {"poison", PokemonType::POISON}, + {"electric", PokemonType::ELECTRIC}, + {"ground", PokemonType::GROUND}, + {"psychic", PokemonType::PSYCHIC}, + {"rock", PokemonType::ROCK}, + {"ice", PokemonType::ICE}, + {"bug", PokemonType::BUG}, + {"dragon", PokemonType::DRAGON}, + {"ghost", PokemonType::GHOST}, + {"dark", PokemonType::DARK}, + {"steel", PokemonType::STEEL}, + {"fairy", PokemonType::FAIRY}, + }; + auto iter = TABLE.find(slug); + if (iter == TABLE.end()){ + throw FileException(nullptr, PA_CURRENT_FUNCTION, "Unknown type: " + slug, path); + } + return iter->second; +} + +static MoveCategory parse_category(const std::string& slug, const std::string& path){ + if (slug == "physical"){ + return MoveCategory::PHYSICAL; + } + if (slug == "special"){ + return MoveCategory::SPECIAL; + } + if (slug == "status"){ + return MoveCategory::STATUS; + } + throw FileException(nullptr, PA_CURRENT_FUNCTION, "Unknown move category: " + slug, path); +} + + +// --------------------------------------------------------------------------- +// Move database +// --------------------------------------------------------------------------- + +struct BDSPMoveDatabase { + std::map data; + + static const BDSPMoveDatabase& instance(){ + static BDSPMoveDatabase db; + return db; + } + + BDSPMoveDatabase(){ + std::string path = RESOURCE_PATH() + RESOURCE_DIR + "bdsp_move_data.json"; + JsonValue json = load_json_file(path); + JsonObject& root = json.to_object_throw(path); + + for (auto& item : root){ + const std::string& slug = item.first; + JsonObject& obj = item.second.to_object_throw(path); + + BDSPMoveData& move = data[slug]; + move.category = parse_category(obj.get_string_throw("category", path), path); + move.type = parse_type(obj.get_string_throw("type", path), path); + move.base_power = (uint8_t)obj.get_integer_throw("base_power", path); + move.pp = (uint8_t)obj.get_integer_throw("pp", path); + move.spread = obj.get_boolean_throw("spread", path); + move.accuracy = (uint8_t)obj.get_integer_default("accuracy", 100); + } + } +}; + +const BDSPMoveData& bdsp_move(const std::string& slug){ + const auto& db = BDSPMoveDatabase::instance().data; + auto iter = db.find(slug); + if (iter == db.end()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown BDSP move slug: " + slug); + } + return iter->second; +} + + +// --------------------------------------------------------------------------- +// Pokemon database +// --------------------------------------------------------------------------- + +struct BDSPPokemonDatabase { + std::map data; + + static const BDSPPokemonDatabase& instance(){ + static BDSPPokemonDatabase db; + return db; + } + + BDSPPokemonDatabase(){ + std::string path = RESOURCE_PATH() + RESOURCE_DIR + "bdsp_pokemon_data.json"; + JsonValue json = load_json_file(path); + JsonObject& root = json.to_object_throw(path); + + for (auto& item : root){ + const std::string& slug = item.first; + JsonObject& obj = item.second.to_object_throw(path); + + BDSPPokemonData& mon = data[slug]; + mon.type[0] = PokemonType::NONE; + mon.type[1] = PokemonType::NONE; + { + JsonArray& type_arr = obj.get_array_throw("type", path); + if (type_arr.size() >= 1){ + mon.type[0] = parse_type(type_arr[0].to_string_throw(path), path); + } + if (type_arr.size() >= 2){ + mon.type[1] = parse_type(type_arr[1].to_string_throw(path), path); + } + } + { + JsonObject& stats = obj.get_object_throw("base_stats", path); + mon.hp = (uint8_t)stats.get_integer_throw("hp", path); + mon.attack = (uint8_t)stats.get_integer_throw("attack", path); + mon.defense = (uint8_t)stats.get_integer_throw("defense", path); + mon.spatk = (uint8_t)stats.get_integer_throw("spatk", path); + mon.spdef = (uint8_t)stats.get_integer_throw("spdef", path); + mon.speed = (uint8_t)stats.get_integer_throw("speed", path); + } + } + } +}; + +const BDSPPokemonData& bdsp_pokemon(const std::string& slug){ + const auto& db = BDSPPokemonDatabase::instance().data; + auto iter = db.find(slug); + if (iter == db.end()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown BDSP pokemon slug: " + slug); + } + return iter->second; +} + + +// --------------------------------------------------------------------------- +// Trainer database +// --------------------------------------------------------------------------- + +struct BDSPTrainerDatabase { + std::map data; + + static const BDSPTrainerDatabase& instance(){ + static BDSPTrainerDatabase db; + return db; + } + + BDSPTrainerDatabase(){ + std::string path = RESOURCE_PATH() + RESOURCE_DIR + "trainers.json"; + JsonValue json = load_json_file(path); + JsonObject& root = json.to_object_throw(path); + + for (auto& item : root){ + const std::string& battle_id = item.first; + JsonObject& obj = item.second.to_object_throw(path); + + BDSPTrainerData& battle = data[battle_id]; + { + const std::string& type_str = obj.get_string_throw("battle_type", path); + if (type_str == "2v2"){ + battle.battle_type = BDSPBattleType::DOUBLES; + }else{ + battle.battle_type = BDSPBattleType::SINGLES; + } + } + { + JsonArray& trainers_arr = obj.get_array_throw("trainers", path); + for (auto& t_val : trainers_arr){ + JsonObject& t_obj = t_val.to_object_throw(path); + BDSPTrainer trainer; + JsonArray& pkmn_arr = t_obj.get_array_throw("pokemon", path); + for (auto& p_val : pkmn_arr){ + JsonObject& p_obj = p_val.to_object_throw(path); + BDSPTrainerPokemon tp; + tp.species = p_obj.get_string_throw("species", path); + tp.level = (uint8_t)p_obj.get_integer_throw("level", path); + + auto parse_stat_set = [&](BDSPStatSet& set, const char* key){ + JsonObject* s = p_obj.get_object(key); + if (!s){ + return; + } + set.hp = (uint8_t)s->get_integer_default("hp", 0); + set.attack = (uint8_t)s->get_integer_default("attack", 0); + set.defense = (uint8_t)s->get_integer_default("defense", 0); + set.spatk = (uint8_t)s->get_integer_default("spatk", 0); + set.spdef = (uint8_t)s->get_integer_default("spdef", 0); + set.speed = (uint8_t)s->get_integer_default("speed", 0); + }; + parse_stat_set(tp.ivs, "ivs"); + parse_stat_set(tp.evs, "evs"); + tp.has_intimidate = p_obj.get_boolean_default("intimidate", false); + + trainer.pokemon.push_back(std::move(tp)); + } + battle.trainers.push_back(std::move(trainer)); + } + } + } + } +}; + +const BDSPTrainerData& bdsp_trainer(const std::string& battle_id){ + const auto& db = BDSPTrainerDatabase::instance().data; + auto iter = db.find(battle_id); + if (iter == db.end()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown BDSP battle ID: " + battle_id); + } + return iter->second; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h new file mode 100644 index 0000000000..a8a6bce913 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h @@ -0,0 +1,104 @@ +/* BDSP AutoStory - Game Data + * + * From: https://github.com/PokemonAutomation/ + * + * Provides access to move metadata, pokemon base stats, and the trainer + * database used by the outside-help battle handler. + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_BDSPData_H +#define PokemonAutomation_PokemonBDSP_AutoStory_BDSPData_H + +#include +#include + +#include "Pokemon/Pokemon_Types.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + +using namespace Pokemon; + + +// --------------------------------------------------------------------------- +// Move data +// --------------------------------------------------------------------------- + +struct BDSPMoveData { + MoveCategory category; + PokemonType type; + uint8_t base_power; + uint8_t pp; + bool spread; + uint8_t accuracy; // 0 = never-miss (e.g. Swift); otherwise 1-100 +}; + +// Returns move data for the given slug. +// Throws InternalProgramError if the slug is not found. +const BDSPMoveData& bdsp_move(const std::string& slug); + + +// --------------------------------------------------------------------------- +// Pokemon base stats +// --------------------------------------------------------------------------- + +struct BDSPPokemonData { + PokemonType type[2]; // type[1] == NONE for mono-type pokemon + uint8_t hp; + uint8_t attack; + uint8_t defense; + uint8_t spatk; + uint8_t spdef; + uint8_t speed; +}; + +// Returns base stats for the given species slug (e.g. "beautifly"). +// Throws InternalProgramError if the slug is not found. +const BDSPPokemonData& bdsp_pokemon(const std::string& slug); + + +// --------------------------------------------------------------------------- +// Trainer database +// --------------------------------------------------------------------------- + +enum class BDSPBattleType { + SINGLES, + DOUBLES, +}; + +struct BDSPStatSet { + uint8_t hp = 0; + uint8_t attack = 0; + uint8_t defense = 0; + uint8_t spatk = 0; + uint8_t spdef = 0; + uint8_t speed = 0; +}; + +struct BDSPTrainerPokemon { + std::string species; + uint8_t level; + BDSPStatSet ivs; + BDSPStatSet evs; + bool has_intimidate = false; +}; + +struct BDSPTrainer { + std::vector pokemon; +}; + +struct BDSPTrainerData { + BDSPBattleType battle_type; + std::vector trainers; +}; + +// Returns trainer data for the given battle ID (e.g. "Veilstone_GalacticGrunts_2v2"). +// Throws InternalProgramError if the ID is not found. +const BDSPTrainerData& bdsp_trainer(const std::string& battle_id); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp new file mode 100644 index 0000000000..a54eadcc5e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp @@ -0,0 +1,150 @@ +/* BDSP AutoStory - Battle Menu Reader + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "PokemonBDSP_AutoStory_BattleMenuReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +BattleMenuOption battle_menu_selected_option(BattleMenuDetector& menu, const ImageViewRGB32& screen){ + if (!screen || !menu.detect(screen)){ + return BattleMenuOption::UNKNOWN; + } + int selected = -1; + for (int row = 0; row < 4; row++){ + const double offset = row * 0.1075; + bool brackets = false; + // The corner brackets wiggle; all four must agree on one small offset. + const double offsets[] = {0.0, -0.003, 0.003, -0.006, 0.006}; + for (double dy : offsets){ + for (double dx : offsets){ + const ImageFloatBox corners[] = { + {0.793 + dx, 0.562 + offset + dy, 0.018, 0.023}, + {0.976 + dx, 0.562 + offset + dy, 0.018, 0.023}, + {0.793 + dx, 0.652 + offset + dy, 0.018, 0.023}, + {0.976 + dx, 0.652 + offset + dy, 0.018, 0.023}, + }; + bool candidate = true; + for (const ImageFloatBox& box : corners){ + const ImageViewRGB32 patch = extract_box_reference(screen, box); + size_t white = 0; + const size_t total = patch.width() * patch.height(); + for (size_t y = 0; y < patch.height(); y++){ + for (size_t x = 0; x < patch.width(); x++){ + const uint32_t pixel = patch.pixel(x, y); + const unsigned r = (pixel >> 16) & 255; + const unsigned g = (pixel >> 8) & 255; + const unsigned b = pixel & 255; + const unsigned low = std::min(r, std::min(g, b)); + const unsigned high = std::max(r, std::max(g, b)); + white += low > 210 && high - low < 35; + } + } + if (total == 0 || static_cast(white) / total < 0.15){ + candidate = false; + break; + } + } + if (candidate){ + brackets = true; + break; + } + } + if (brackets){ + break; + } + } + if (brackets){ + if (selected >= 0){ + return BattleMenuOption::UNKNOWN; + } + selected = row; + } + } + return static_cast(selected); +} + +BattleMoveMenuState detect_battle_move_menu(const ImageViewRGB32& screen){ + BattleMoveMenuState result; + if (!screen) return result; + // Every populated move has a white PP panel, irrespective of move type or + // selected row. Find connected panels rather than assume a 4-row layout. + const ImageFloatBox search{0.90, 0.48, 0.075, 0.505}; + const ImageViewRGB32 patch = extract_box_reference(screen, search); + cv::Mat white((int)patch.height(), (int)patch.width(), CV_8UC1); + for (size_t y = 0; y < patch.height(); y++) for (size_t x = 0; x < patch.width(); x++) { + uint32_t p = patch.pixel(x, y); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + white.at((int)y, (int)x) = std::min(r, std::min(g, b)) > 225 && + std::max(r, std::max(g, b)) - std::min(r, std::min(g, b)) < 30 ? 255 : 0; + } + cv::Mat labels, stats, centers; + int components = cv::connectedComponentsWithStats(white, labels, stats, centers); + std::vector panels; + for (int component = 1; component < components; component++) { + cv::Rect box(stats.at(component, cv::CC_STAT_LEFT), stats.at(component, cv::CC_STAT_TOP), + stats.at(component, cv::CC_STAT_WIDTH), stats.at(component, cv::CC_STAT_HEIGHT)); + if (box.width < patch.width() * 0.60 || box.height < screen.height() * 0.055 || + box.height > screen.height() * 0.13) continue; + double area = (double)stats.at(component, cv::CC_STAT_AREA); + if (area / box.area() < 0.55) continue; + panels.push_back(box); + } + if (panels.empty() || panels.size() > 4) return result; + std::sort(panels.begin(), panels.end(), [](const cv::Rect& a, const cv::Rect& b){ return a.y < b.y; }); + // Reject inconsistent/overlapping geometry rather than count noise as moves. + for (size_t i = 1; i < panels.size(); i++) { + if (panels[i].y <= panels[i - 1].y + panels[i - 1].height || + std::abs(panels[i].height - panels[0].height) > screen.height() * 0.025) return result; + } + result.move_count = panels.size(); + auto is_corner = [&](double x, double y){ + if (x < 0 || y < 0 || x + 0.018 > 1 || y + 0.023 > 1) return false; + ImageViewRGB32 corner = extract_box_reference(screen, ImageFloatBox{x, y, 0.018, 0.023}); + size_t white_count = 0, total = corner.width() * corner.height(); + for (size_t py = 0; py < corner.height(); py++) for (size_t px = 0; px < corner.width(); px++) { + uint32_t p = corner.pixel(px, py); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + white_count += std::min(r, std::min(g, b)) > 210 && + std::max(r, std::max(g, b)) - std::min(r, std::min(g, b)) < 35; + } + return total && (double)white_count / total >= 0.15; + }; + for (size_t slot = 0; slot < panels.size(); slot++) { + double top = search.y + (double)panels[slot].y / screen.height(); + double bottom = top + (double)panels[slot].height / screen.height(); + result.buttons[slot] = ImageFloatBox{0.665, std::max(0.0, top - 0.01), 0.333, + std::min(1.0, bottom + 0.01) - std::max(0.0, top - 0.01)}; + bool selected = false; + for (double dy : {0.0, -0.003, 0.003, -0.006, 0.006, -0.010, 0.010}) { + for (double dx : {0.0, -0.003, 0.003}) { + if (is_corner(0.663 + dx, top - 0.010 + dy) && + is_corner(0.976 + dx, top - 0.010 + dy) && + is_corner(0.663 + dx, bottom - 0.014 + dy) && + is_corner(0.976 + dx, bottom - 0.014 + dy)) { selected = true; break; } + } + if (selected) break; + } + if (selected) { + if (result.selected_slot) { result.selected_slot = 0; return result; } + result.selected_slot = slot + 1; + } + } + return result; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h new file mode 100644 index 0000000000..07d5c68eab --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h @@ -0,0 +1,54 @@ +/* BDSP AutoStory - Battle Menu Reader + * + * From: https://github.com/PokemonAutomation/ + * + * Which battle command and which move the cursor is on. Adapted from + * PokemonAutomation/Arduino-Source#1717 by Developer-Butters, which added + * them to PokemonBDSP_BattleMenuDetector; here they sit beside it, so the + * upstream detector stays as it is. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_BattleMenuReader_H +#define PokemonAutomation_PokemonBDSP_AutoStory_BattleMenuReader_H + +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Vertical command order in the BDSP battle menu. +enum class BattleMenuOption{ + UNKNOWN = -1, + BATTLE = 0, + POKEMON = 1, + BAG = 2, + RUN = 3, +}; + +// The command under the cursor: `menu` must see its battle menu, and white +// selection brackets must sit at all four corners of exactly one command. +// Missing or ambiguous brackets read UNKNOWN; the cursor is never guessed. +BattleMenuOption battle_menu_selected_option(BattleMenuDetector& menu, const ImageViewRGB32& screen); + +// Logical move slots are ordered top to bottom among the populated move +// buttons. `selected_slot` is 1 to 4, or 0 when missing or ambiguous. +struct BattleMoveMenuState{ + size_t move_count = 0; + size_t selected_slot = 0; + std::array buttons; +}; +BattleMoveMenuState detect_battle_move_menu(const ImageViewRGB32& screen); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp new file mode 100644 index 0000000000..9cf2086703 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp @@ -0,0 +1,292 @@ +/* BDSP AutoStory - Menu Cursor Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include + +#include "CommonFramework/VideoPipeline/VideoFeed.h" + +#include "CommonTools/Images/ImageFilter.h" + +#include "PokemonBDSP_AutoStory_MenuCursorDetector.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +std::string menu_cursor_to_string(MenuCursorPosition pos){ + switch (pos){ + case MenuCursorPosition::POKEDEX: + return "POKEDEX"; + case MenuCursorPosition::POKEMON: + return "POKEMON"; + case MenuCursorPosition::BAG: + return "BAG"; + case MenuCursorPosition::CARD: + return "CARD"; + case MenuCursorPosition::OPTIONS: + return "OPTIONS"; + case MenuCursorPosition::MAP: + return "MAP"; + case MenuCursorPosition::CAPSULES: + return "CAPSULES"; + case MenuCursorPosition::MYSTERYGIFT: + return "MYSTERYGIFT"; + default: + return "UNKNOWN"; + } +} + + +MenuCursorDetector::MenuCursorDetector(Color color) : + m_color(color) + // ------------------------------------------------------------------------- + // Top row — left to right + // ------------------------------------------------------------------------- + // TODO: replace each ImageFloatBox(0,0,0,0) with the real normalized + // (x, y, width, height) for a small region that gains dark pixels when + // that icon is hovered. Use the test program overlays to tune coordinates. + // ------------------------------------------------------------------------- + , m_pokedex_box{ + ImageFloatBox(0.180000, 0.115000, 0.135000, 0.195000), + }, + m_pokemon_box{ + ImageFloatBox(0.347500, 0.115000, 0.135000, 0.195000), + }, + m_bag_box{ + ImageFloatBox(0.515000, 0.115000, 0.135000, 0.195000), + }, + m_card_box{ + ImageFloatBox(0.682500, 0.115000, 0.135000, 0.195000), + } // ------------------------------------------------------------------------- + // Bottom row — left to right + // 5-icon layout: OPTIONS is the only bottom icon (far-left position). + // 8-icon layout: MAP, CAPSULES, OPTIONS, MYSTERYGIFT from left to right. + // OPTIONS sits at a different x-position in each layout — use a separate + // MenuCursorDetector instance if you need to detect OPTIONS in both. + // ------------------------------------------------------------------------- + , m_options_box{ + ImageFloatBox(0.515000, 0.364000, 0.135000, 0.195000), + }, + m_map_box{ + ImageFloatBox(0.180000, 0.364000, 0.135000, 0.195000), + }, + m_capsules_box{ + ImageFloatBox(0.347500, 0.364000, 0.135000, 0.195000), + }, + m_mysterygift_box{ + ImageFloatBox(0.682500, 0.364000, 0.135000, 0.195000), + } // ------------------------------------------------------------------------- + // Approach 2 search boxes — one per icon, same area as the dark-pixel box. + // The template PNG is slid across this area at its native size. + // ------------------------------------------------------------------------- + , m_icon_match_box{ + ImageFloatBox(0.180000, 0.115000, 0.135000, 0.195000), // 0 POKEDEX + ImageFloatBox(0.347500, 0.115000, 0.135000, 0.195000), // 1 POKEMON + ImageFloatBox(0.515000, 0.115000, 0.135000, 0.195000), // 2 BAG + ImageFloatBox(0.682500, 0.115000, 0.135000, 0.195000), // 3 CARD + ImageFloatBox(0.515000, 0.364000, 0.135000, 0.195000), // 4 OPTIONS (8-icon layout) + ImageFloatBox(0.180000, 0.364000, 0.135000, 0.195000), // 5 MAP + ImageFloatBox(0.347500, 0.364000, 0.135000, 0.195000), // 6 CAPSULES + ImageFloatBox(0.682500, 0.364000, 0.135000, 0.195000), // 7 MYSTERYGIFT + } { +} + + +void MenuCursorDetector::make_overlays(VideoOverlaySet& items) const { + for (const ImageFloatBox* icon_boxes : { + m_pokedex_box, + m_pokemon_box, + m_bag_box, + m_card_box, + m_options_box, + m_map_box, + m_capsules_box, + m_mysterygift_box, + }){ + for (int i = 0; i < BOXES_PER_ICON; i++){ + if (icon_boxes[i].width > 0.0 && icon_boxes[i].height > 0.0){ + items.add(m_color, icon_boxes[i]); + } + } + } + for (int i = 0; i < MENU_CURSOR_NUM_ICONS; i++){ + if (m_icon_match_box[i].width > 0.0 && m_icon_match_box[i].height > 0.0){ + items.add(m_color, m_icon_match_box[i]); + } + } +} + + +namespace { + +// Count how many of `n` boxes contain >= MIN_DARK_PIXEL_COUNT pixels with +// R, G, B all <= BLACK_CHANNEL_MAX. Boxes with zero width are skipped. +int count_dark_boxes(const ImageViewRGB32& screen, const ImageFloatBox* boxes, int n){ + const uint32_t ch = uint32_t(MenuCursorDetector::BLACK_CHANNEL_MAX); + const uint32_t maxs = 0xff000000u | (ch << 16) | (ch << 8) | ch; + + int count = 0; + for (int i = 0; i < n; i++){ + if (boxes[i].width == 0.0){ + continue; + } + size_t dark_pixels = 0; + filter_rgb32_range(dark_pixels, + extract_box_reference(screen, boxes[i]), + 0xff000000u, + maxs, + Color(0xffffffff), + false); + if (dark_pixels >= MenuCursorDetector::MIN_DARK_PIXEL_COUNT){ + count++; + } + } + return count; +} + +// Pixel-wise RMSD between two same-size images over RGB channels only. +double compute_rmsd(const ImageViewRGB32& a, const ImageViewRGB32& b){ + size_t n = a.width() * a.height(); + if (n == 0){ + return 0.0; + } + double sum_sq = 0.0; + for (size_t y = 0; y < a.height(); y++){ + for (size_t x = 0; x < a.width(); x++){ + uint32_t pa = a.pixel(x, y); + uint32_t pb = b.pixel(x, y); + int dr = int((pa >> 16) & 0xff) - int((pb >> 16) & 0xff); + int dg = int((pa >> 8) & 0xff) - int((pb >> 8) & 0xff); + int db = int(pa & 0xff) - int(pb & 0xff); + sum_sq += double(dr * dr + dg * dg + db * db); + } + } + return std::sqrt(sum_sq / (3.0 * n)); +} + +// Slide `tmpl` (at its native pixel size) across the region of `screen` +// defined by `box`. Returns the minimum RMSD found over all window positions. +double template_match_rmsd(const ImageViewRGB32& screen, const ImageFloatBox& box, const ImageViewRGB32& tmpl){ + ImageViewRGB32 region = extract_box_reference(screen, box); + size_t rw = tmpl.width(), rh = tmpl.height(); + size_t lw = region.width(), lh = region.height(); + if (rw == 0 || rh == 0 || rw > lw || rh > lh){ + return 0.0; + } + + double min_rmsd = std::numeric_limits::max(); + for (size_t dy = 0; dy <= lh - rh; dy++){ + for (size_t dx = 0; dx <= lw - rw; dx++){ + double r = compute_rmsd(region.sub_image(dx, dy, rw, rh), tmpl); + if (r < min_rmsd){ + min_rmsd = r; + } + } + } + return min_rmsd; +} + +} // anonymous namespace + + +MenuCursorPosition MenuCursorDetector::detect_dark_pixels(const ImageViewRGB32& screen) const { + struct IconEntry { + MenuCursorPosition position; + const ImageFloatBox* boxes; + }; + const IconEntry icons[] = { + {MenuCursorPosition::POKEDEX, m_pokedex_box}, + {MenuCursorPosition::POKEMON, m_pokemon_box}, + {MenuCursorPosition::BAG, m_bag_box}, + {MenuCursorPosition::CARD, m_card_box}, + {MenuCursorPosition::OPTIONS, m_options_box}, + {MenuCursorPosition::MAP, m_map_box}, + {MenuCursorPosition::CAPSULES, m_capsules_box}, + {MenuCursorPosition::MYSTERYGIFT, m_mysterygift_box}, + }; + + MenuCursorPosition best_pos = MenuCursorPosition::UNKNOWN; + int best_count = 0; + + for (const auto& entry : icons){ + int count = count_dark_boxes(screen, entry.boxes, BOXES_PER_ICON); + if (count > best_count){ + best_count = count; + best_pos = entry.position; + } + } + + return (best_count >= REQUIRED_VOTES) ? best_pos : MenuCursorPosition::UNKNOWN; +} + + +MenuCursorPosition MenuCursorDetector::detect_image_match(const ImageViewRGB32& screen) const { + MenuCursorPosition best_pos = MenuCursorPosition::UNKNOWN; + double best_rmsd = MATCH_RMSD_THRESHOLD; // must exceed this + + for (int i = 0; i < MENU_CURSOR_NUM_ICONS; i++){ + if (!m_icon_references[i].has_value()){ + continue; + } + if (m_icon_match_box[i].width == 0.0){ + continue; + } + double r = template_match_rmsd(screen, m_icon_match_box[i], *m_icon_references[i]); + if (r > best_rmsd){ + best_rmsd = r; + best_pos = static_cast(i); + } + } + return best_pos; +} + + +MenuCursorPosition MenuCursorDetector::detect(const ImageViewRGB32& screen) const { + MenuCursorPosition p1 = detect_dark_pixels(screen); + MenuCursorPosition p2 = detect_image_match(screen); + + if (p1 == p2){ + return p1; // both agree (or both UNKNOWN) + } + if (p1 == MenuCursorPosition::UNKNOWN){ + return p2; // only approach 2 fired + } + if (p2 == MenuCursorPosition::UNKNOWN){ + return p1; // only approach 1 fired + } + return p1; // conflict: prefer approach 1 +} + + +void MenuCursorDetector::set_icon_reference(MenuCursorPosition icon, ImageRGB32 reference){ + m_icon_references[static_cast(icon)].emplace(std::move(reference)); +} + + +double MenuCursorDetector::icon_rmsd(const ImageViewRGB32& screen, MenuCursorPosition icon) const { + int idx = static_cast(icon); + if (!m_icon_references[idx].has_value() || m_icon_match_box[idx].width == 0.0){ + return -1.0; + } + return template_match_rmsd(screen, m_icon_match_box[idx], *m_icon_references[idx]); +} + + +MenuCursorPosition detect_menu_cursor(VideoStream& stream, ProControllerContext& /*context*/){ + VideoSnapshot screen = stream.video().snapshot(); + if (!screen){ + return MenuCursorPosition::UNKNOWN; + } + MenuCursorDetector detector; + return detector.detect(screen); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h new file mode 100644 index 0000000000..e7090deafb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h @@ -0,0 +1,146 @@ +/* BDSP AutoStory - Menu Cursor Detector + * + * From: https://github.com/PokemonAutomation/ + * + * Detects which icon the cursor is hovering over in the X-menu. + * + * Two independent detection approaches are provided for testing: + * + * Approach 1 — dark pixel count (detect_dark_pixels): + * Each icon has BOXES_PER_ICON small boxes placed over a region that gains + * dark pixels when hovered. A box fires if >= MIN_DARK_PIXEL_COUNT pixels + * in it have R, G, B all <= BLACK_CHANNEL_MAX. An icon is selected when its + * fire count reaches REQUIRED_VOTES. + * Tune BLACK_CHANNEL_MAX upward if real dark pixels are being missed. + * Tune it downward (or raise MIN_DARK_PIXEL_COUNT) to reduce false positives. + * + * Approach 2 — template matching (detect_image_match): + * Each icon has one larger search box and a hovered-state reference PNG loaded + * at runtime via set_icon_reference(). The reference PNG is slid (at its native + * pixel size, without resizing) across the search box and the minimum RMSD over + * all window positions is computed. The icon with the lowest min-RMSD that is + * also <= MATCH_RMSD_THRESHOLD is considered selected. + * Tune MATCH_RMSD_THRESHOLD downward to require a closer match. + * + * detect() is the production method and currently delegates to approach 1. + * It will become a combined vote once both approaches are validated. + * + * Menu layouts: + * 5-icon (early game): POKEDEX POKEMON BAG CARD / OPTIONS + * 8-icon (late game): POKEDEX POKEMON BAG CARD / MAP CAPSULES OPTIONS MYSTERYGIFT + * + * Note: OPTIONS sits at a different screen position in each layout. + * Use separate MenuCursorDetector instances with different box coordinates + * if you need to detect OPTIONS reliably in both layouts. + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_MenuCursorDetector_H +#define PokemonAutomation_PokemonBDSP_AutoStory_MenuCursorDetector_H + +#include +#include +#include + +#include "Common/Cpp/Color.h" + +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +enum class MenuCursorPosition { + UNKNOWN = -1, + POKEDEX = 0, + POKEMON = 1, + BAG = 2, + CARD = 3, + OPTIONS = 4, + MAP = 5, + CAPSULES = 6, + MYSTERYGIFT = 7, +}; + +static constexpr int MENU_CURSOR_NUM_ICONS = 8; + +std::string menu_cursor_to_string(MenuCursorPosition pos); + + +class MenuCursorDetector { +public: + // ── Approach 1: dark pixel detection ───────────────────────────────────── + static constexpr int BOXES_PER_ICON = 3; + static constexpr int REQUIRED_VOTES = 1; // out of BOXES_PER_ICON + static constexpr uint8_t BLACK_CHANNEL_MAX = 50; // R,G,B <= this → dark pixel + static constexpr size_t MIN_DARK_PIXEL_COUNT = 1; // dark pixels per box to fire + + // ── Approach 2: template match ─────────────────────────────────────────── + // Reference PNGs are the UN-hovered state of each icon. When hovered, the + // icon gains dark pixels and diverges from the reference, so RMSD increases. + // detect_image_match() selects the icon with the HIGHEST RMSD >= threshold. + // Tune upward to reduce false positives; downward if the correct icon is missed. + static constexpr double MATCH_RMSD_THRESHOLD = 53.0; + + MenuCursorDetector(Color color = COLOR_RED); + + void make_overlays(VideoOverlaySet& items) const; + + // Approach 1: votes across dark-pixel boxes, returns icon with highest + // vote count >= REQUIRED_VOTES, or UNKNOWN. + MenuCursorPosition detect_dark_pixels(const ImageViewRGB32& screen) const; + + // Approach 2: returns the icon with the highest RMSD against its reference, + // provided that RMSD >= MATCH_RMSD_THRESHOLD (high RMSD = hovered). + // Returns UNKNOWN if no references are loaded or nothing exceeds threshold. + MenuCursorPosition detect_image_match(const ImageViewRGB32& screen) const; + + // Production method — currently delegates to detect_dark_pixels(). + MenuCursorPosition detect(const ImageViewRGB32& screen) const; + + // Load a hovered-state reference image for one icon (enables approach 2 for + // that icon). The match box for that icon must already be set in the + // constructor (m_icon_match_box); this method just attaches the reference. + void set_icon_reference(MenuCursorPosition icon, ImageRGB32 reference); + + // Returns the raw RMSD of `icon` against its reference on the given frame. + // Returns -1.0 if no reference is loaded for that icon. + double icon_rmsd(const ImageViewRGB32& screen, MenuCursorPosition icon) const; + +private: + Color m_color; + + // ── Approach 1 boxes ───────────────────────────────────────────────────── + // 3 boxes per icon targeting regions that gain dark pixels when hovered. + // All initialized to (0,0,0,0) — replace with real normalized coordinates. + ImageFloatBox m_pokedex_box[BOXES_PER_ICON]; + ImageFloatBox m_pokemon_box[BOXES_PER_ICON]; + ImageFloatBox m_bag_box[BOXES_PER_ICON]; + ImageFloatBox m_card_box[BOXES_PER_ICON]; + ImageFloatBox m_options_box[BOXES_PER_ICON]; + ImageFloatBox m_map_box[BOXES_PER_ICON]; + ImageFloatBox m_capsules_box[BOXES_PER_ICON]; + ImageFloatBox m_mysterygift_box[BOXES_PER_ICON]; + + // ── Approach 2 search boxes + references ──────────────────────────────── + // One search box per icon (the area to slide the template across). + // References are populated via set_icon_reference() at runtime. + ImageFloatBox m_icon_match_box[MENU_CURSOR_NUM_ICONS]; + std::array, MENU_CURSOR_NUM_ICONS> m_icon_references; +}; + + +// Captures the current frame from `stream` and runs detect() on it. +MenuCursorPosition detect_menu_cursor(VideoStream& stream, ProControllerContext& context); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp new file mode 100644 index 0000000000..3e4e0f41ca --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp @@ -0,0 +1,135 @@ +/* BDSP AutoStory - Overworld Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/Images/SolidColorTest.h" + +#include "PokemonBDSP_AutoStory_OverworldDetector.h" + +using namespace std::chrono_literals; + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// --------------------------------------------------------------------------- +// OverworldDetector +// --------------------------------------------------------------------------- + +OverworldDetector::OverworldDetector(Color color) + : m_color(color) + , m_battle_menu(BattleType::STANDARD, color){ +} + +void OverworldDetector::make_overlays(VideoOverlaySet& items) const { + m_dialog.make_overlays(items); + m_battle_menu.make_overlays(items); + m_menu.make_overlays(items); +} + +bool OverworldDetector::detect(const ImageViewRGB32& screen){ + // We are in the overworld when none of the overlay screens are present. + if (m_dialog.detect(screen)){ + return false; + } + if (m_battle_menu.detect(screen)){ + return false; + } + if (m_menu.detect(screen)){ + return false; + } + return true; +} + + +// --------------------------------------------------------------------------- +// OverworldWatcher +// --------------------------------------------------------------------------- + +OverworldWatcher::OverworldWatcher(Color color) + : OverworldDetector(color) + , VisualInferenceCallback("OverworldWatcher"){ +} + +void OverworldWatcher::make_overlays(VideoOverlaySet& items) const { + OverworldDetector::make_overlays(items); +} + +bool OverworldWatcher::process_frame(const ImageViewRGB32& frame, WallClock /*timestamp*/){ + return detect(frame); +} + + +// --------------------------------------------------------------------------- +// PoketchButtonDetector +// --------------------------------------------------------------------------- + +PoketchButtonDetector::PoketchButtonDetector(Color color) + : m_color(color) + , m_button(0.968, 0.170, 0.022, 0.120){ +} + +void PoketchButtonDetector::make_overlays(VideoOverlaySet& items) const { + items.add(m_color, m_button); +} + +bool PoketchButtonDetector::detect(const ImageViewRGB32& screen){ + ImageStats stats = image_stats(extract_box_reference(screen, m_button)); + // The button is a glossy red with a highlight, so allow a wide stddev. + return is_solid(stats, {0.74, 0.10, 0.16}, 0.12, 80); +} + +PoketchButtonWatcher::PoketchButtonWatcher(Color color) + : PoketchButtonDetector(color) + , VisualInferenceCallback("PoketchButtonWatcher"){ +} + +void PoketchButtonWatcher::make_overlays(VideoOverlaySet& items) const { + PoketchButtonDetector::make_overlays(items); +} + +bool PoketchButtonWatcher::process_frame(const ImageViewRGB32& frame, WallClock){ + return detect(frame); +} + + +static bool look_for_poketch_button(VideoStream& stream, ProControllerContext& context){ + context.wait_for_all_requests(); + PoketchButtonWatcher watcher; + return wait_until(stream, context, 2s, {{watcher}}) == 0; +} + +bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context){ + for (size_t attempt = 0; attempt < 3; attempt++){ + if (look_for_poketch_button(stream, context)){ + return true; + } + stream.log("[AutoStory] Poketch button not found. Attempt " + std::to_string(attempt + 1) + " of 3.", + COLOR_ORANGE); + + if (attempt == 0){ + // Most likely hidden: one tap shows it. + pbf_press_button(context, BUTTON_R, 205ms, 0ms); + }else{ + // Long press hides it, then a tap shows it at the normal size. + pbf_press_button(context, BUTTON_R, 1548ms, 0ms); + pbf_wait(context, 1418ms); + pbf_press_button(context, BUTTON_R, 143ms, 0ms); + } + } + + // Check the result of the last press sequence. + return look_for_poketch_button(stream, context); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h new file mode 100644 index 0000000000..4741325711 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h @@ -0,0 +1,98 @@ +/* BDSP AutoStory - Overworld Detector + * + * From: https://github.com/PokemonAutomation/ + * + * Detects that the player is in the free-roaming overworld state: + * no dialog box, no battle menu, and no in-game menu open. + * + * BDSP does not ship a standalone OverworldDetector in PokemonBDSP/Inference/, + * so this composite detector is defined here for AutoStory use. + * + * Reuses: + * - ShortDialogDetector (PokemonBDSP_DialogDetector.h) — absence required + * - BattleMenuDetector (PokemonBDSP_BattleMenuDetector.h) — absence required + * - MenuDetector (PokemonBDSP_MenuDetector.h) — absence required + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H +#define PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H + +#include "Common/Cpp/Color.h" + +#include "CommonFramework/Tools/VideoStream.h" + +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "CommonTools/VisualDetector.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// Static (single-frame) version — use when you already have a frame. +class OverworldDetector : public StaticScreenDetector { +public: + OverworldDetector(Color color = COLOR_CYAN); + + virtual void make_overlays(VideoOverlaySet& items) const override; + + // Returns true when none of: short dialog, battle menu, or in-game menu + // are present on the given frame. + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ShortDialogDetector m_dialog; + BattleMenuDetector m_battle_menu; + MenuDetector m_menu; +}; + + +// Watcher (callback) version — use with run_until<> or wait_until(). +class OverworldWatcher : public OverworldDetector, public VisualInferenceCallback { +public: + OverworldWatcher(Color color = COLOR_CYAN); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; +}; + + +// Detects the big red button on the right edge of the Poketch overlay. +// Present only while the Poketch is shown at its normal size. +class PoketchButtonDetector : public StaticScreenDetector { +public: + PoketchButtonDetector(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ImageFloatBox m_button; +}; + +class PoketchButtonWatcher : public PoketchButtonDetector, public VisualInferenceCallback { +public: + PoketchButtonWatcher(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; +}; + +// Tries up to 3 times to get the Poketch button on screen using R presses. +// Returns true if the button was seen. +bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp new file mode 100644 index 0000000000..6b7c2b8182 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp @@ -0,0 +1,3697 @@ + + +#include +#include +#include + +#include + +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Globals.h" + +#include "PokemonBDSP_OverworldMatchers.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { +enum class LandmarkFeatures { + GRAYSCALE, + LOCAL_CONTRAST, + BLUE_FRACTION +}; +struct LandmarkConfig { + const char* name; + const char* path; + ImageFloatBox target, search; + double threshold, margin; + LandmarkFeatures features = LandmarkFeatures::GRAYSCALE; +}; +// Screen-relative half-widths of the accepted position box, and how closely +// two landmarks (or reference variants) must agree to be trusted together. +static constexpr double POSITION_TOLERANCE_X = 1.0 / 50; +static constexpr double POSITION_TOLERANCE_Y = 1.0 / 54; +static constexpr double AGREEMENT_X = 1.0 / 64; +static constexpr double AGREEMENT_Y = 1.0 / 54; +static cv::Mat gray_image(const ImageRGB32& image){ + cv::Mat pixels((int)image.height(), + (int)image.width(), + CV_8UC4, + const_cast(image.data()), + image.bytes_per_row()); + cv::Mat gray; + cv::cvtColor(pixels, gray, cv::COLOR_BGRA2GRAY); + return gray; +} +// Opt-in for selected outdoor structural anchors. Divide out slow local lighting +// changes so a moving cloud does not alter the printed sign/emblem structure. +static cv::Mat normalize_landmark_illumination(const cv::Mat& gray){ + cv::Mat pixels, background, normalized; + gray.convertTo(pixels, CV_32F); + cv::GaussianBlur(pixels, background, cv::Size(0, 0), 3.0); + cv::divide(pixels, background + 1.0, normalized); + cv::Mat structure; + normalized.convertTo(structure, CV_8U, 128.0); + return structure; +} +// White Pokeball versus yellow fascia is a color-ratio boundary. A cast +// shadow changes their brightness, so compare blue fraction rather than luma. +static cv::Mat landmark_blue_fraction(const ImageRGB32& image){ + cv::Mat pixels((int)image.height(), + (int)image.width(), + CV_8UC4, + const_cast(image.data()), + image.bytes_per_row()); + cv::Mat small, floating; + cv::resize(pixels, small, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + small.convertTo(floating, CV_32F); + std::vector channels; + cv::split(floating, channels); + cv::Mat ratio, structure; + cv::divide(channels[0], channels[0] + channels[1] + channels[2] + 3.0, ratio); + ratio.convertTo(structure, CV_8U, 768.0); + return structure; +} +static OverworldMatchResult match_landmarks(const ImageRGB32& frame, + const std::vector& configs, + size_t required, + double tolerance_x, + double tolerance_y, + bool smooth_resize = false){ + OverworldMatchResult result; + if (!frame){ + return result; + } + cv::Mat gray; + if (smooth_resize){ + // Center matching uses explicit area averaging for stable emblem edges. + // Other matchers retain their existing ImageRGB32 resize behavior. + cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + }else{ + ImageRGB32 scaled = frame.scale_to(960, 540); + gray = gray_image(scaled); + } + cv::Mat normalized_gray, chromaticity; + for (const auto& config : configs){ + OverworldLandmarkResult landmark; + landmark.name = config.name; + landmark.search = config.search; + ImageRGB32 reference(RESOURCE_PATH() + config.path); + // Convert normalized geometry to pixels only at the image-processing boundary. + const int template_width = std::max(1, (int)std::lround(config.target.width * gray.cols)); + const int template_height = std::max(1, (int)std::lround(config.target.height * gray.rows)); + cv::Mat templ; + if (smooth_resize){ + cv::resize(gray_image(reference), templ, cv::Size(template_width, template_height), 0, 0, cv::INTER_AREA); + }else{ + ImageRGB32 small = reference.scale_to(template_width, template_height); + templ = gray_image(small); + } + cv::Rect search((int)(config.search.x * 960), + (int)(config.search.y * 540), + (int)(config.search.width * 960), + (int)(config.search.height * 540)); + search &= cv::Rect(0, 0, 960, 540); + if (search.width < templ.cols || search.height < templ.rows){ + result.landmarks.push_back(landmark); + continue; + } + cv::Mat scores; + if (config.features == LandmarkFeatures::BLUE_FRACTION){ + if (chromaticity.empty()){ + chromaticity = landmark_blue_fraction(frame); + } + cv::matchTemplate(chromaticity(search), templ, scores, cv::TM_CCOEFF_NORMED); + }else if (config.features == LandmarkFeatures::LOCAL_CONTRAST){ + if (normalized_gray.empty()){ + normalized_gray = normalize_landmark_illumination(gray); + } + // These opt-in references are normalized from their full source + // frame before cropping, avoiding false edges at template borders. + cv::matchTemplate(normalized_gray(search), templ, scores, cv::TM_CCOEFF_NORMED); + }else{ + cv::matchTemplate(gray(search), templ, scores, cv::TM_CCOEFF_NORMED); + } + cv::Point location; + cv::minMaxLoc(scores, nullptr, &landmark.score, nullptr, &location); + cv::Mat competitors = scores.clone(); + const int peak_radius_x = (int)std::lround(0.015625 * gray.cols); + const int peak_radius_y = (int)std::lround((1.0 / 36) * gray.rows); + cv::Rect exclusion = cv::Rect(location.x - peak_radius_x, + location.y - peak_radius_y, + 2 * peak_radius_x + 1, + 2 * peak_radius_y + 1) & + cv::Rect(0, 0, scores.cols, scores.rows); + competitors(exclusion).setTo(-1); + double runner_up; + cv::minMaxLoc(competitors, nullptr, &runner_up); + landmark.margin = landmark.score - runner_up; + location += search.tl(); + landmark.dx = (double)location.x / gray.cols - config.target.x; + landmark.dy = (double)location.y / gray.rows - config.target.y; + landmark.accepted = landmark.score >= config.threshold && landmark.margin >= config.margin; + landmark.found = {(double)location.x / 960, + (double)location.y / 540, + (double)templ.cols / 960, + (double)templ.rows / 540}; + result.landmarks.push_back(landmark); + } + if (required == 1 && result.landmarks.size() == 1){ + result.located = result.landmarks[0].accepted; + result.dx = result.landmarks[0].dx; + result.dy = result.landmarks[0].dy; + }else{ + size_t best_support = 0; + bool ambiguous = false; + for (const auto& candidate : result.landmarks){ + if (!candidate.accepted){ + continue; + } + size_t support = 0; + double dx = 0, dy = 0; + for (const auto& other : result.landmarks){ + if (other.accepted && std::abs(candidate.dx - other.dx) <= AGREEMENT_X && + std::abs(candidate.dy - other.dy) <= AGREEMENT_Y){ + support++; + dx += other.dx; + dy += other.dy; + } + } + if (support < required){ + continue; + } + dx /= support; + dy /= support; + if (support > best_support){ + best_support = support; + result.dx = dx; + result.dy = dy; + ambiguous = false; + }else if (support == best_support && + (std::abs(dx - result.dx) > AGREEMENT_X || std::abs(dy - result.dy) > AGREEMENT_Y)){ + ambiguous = true; + } + } + result.located = best_support >= required && !ambiguous; + } + result.at_target = result.located && std::abs(result.dx) <= tolerance_x && std::abs(result.dy) <= tolerance_y; + return result; +} +OverworldMatchResult match_poketch_landmarks(const ImageRGB32& frame){ + // Same cropped reference emblems and local search windows as production. + OverworldMatchResult fine = match_landmarks(frame, + {{"Mart", + "PokemonBDSP/Autostory/PoketchInteraction/Mart.png", + {0.768750000000, 0.044444444444, 0.079166666667, 0.088888888889}, + {598.0 / 960, 0, 356.0 / 960, 212.0 / 540}, + .90, + 0}, + {"Center", + "PokemonBDSP/Autostory/PoketchInteraction/Center.png", + {0.900000000000, 0.748148148148, 0.075000000000, 0.137037037037}, + {724.0 / 960, 264.0 / 540, 236.0 / 960, 276.0 / 540}, + .90, + 0}, + {"School", + "PokemonBDSP/Autostory/PoketchInteraction/School.png", + {0.006250000000, 0.755555555556, 0.097916666667, 0.133333333333}, + {0, 268.0 / 540, 240.0 / 960, 272.0 / 540}, + .90, + 0}}, + 2, + 7.0 / 960, + 7.0 / 540); + if (fine.located){ + return fine; + } + // Coarse acquisition searches the whole frame. Agreement and a peak margin + // guard against confusing the Mart/Center emblems with another building. + OverworldMatchResult coarse = match_landmarks(frame, + {{"Mart", + "PokemonBDSP/Autostory/PoketchInteraction/Mart.png", + {0.768750000000, 0.044444444444, 0.079166666667, 0.088888888889}, + {0, 0, 1, 1}, + .93, + .12}, + {"Center", + "PokemonBDSP/Autostory/PoketchInteraction/Center.png", + {0.900000000000, 0.748148148148, 0.075000000000, 0.137037037037}, + {0, 0, 1, 1}, + .93, + .12}, + {"School", + "PokemonBDSP/Autostory/PoketchInteraction/School.png", + {0.006250000000, 0.755555555556, 0.097916666667, 0.133333333333}, + {0, 0, 1, 1}, + .92, + .40}}, + 2, + 7.0 / 960, + 7.0 / 540); + if (!coarse.located){ + // Acquisition may expose just one building. School keeps its strong + // spatial margin; Mart/Center require a higher score than pair matching. + // A single anchor guides coarse movement only, never interaction. + const OverworldLandmarkResult* anchor = nullptr; + for (const auto& landmark : coarse.landmarks){ + const bool strong = + landmark.accepted && (landmark.name == "School" || (landmark.score >= .95 && landmark.margin >= .12)); + if (strong && (!anchor || landmark.score > anchor->score)){ + anchor = &landmark; + } + } + if (anchor){ + bool contradictory = false; + for (const auto& other : coarse.landmarks){ + if (other.accepted && + (std::abs(other.dx - anchor->dx) > 1.0 / 192 || std::abs(other.dy - anchor->dy) > 1.0 / 108)){ + contradictory = true; + } + } + if (!contradictory){ + coarse.located = true; + coarse.dx = anchor->dx; + coarse.dy = anchor->dy; + } + } + } + coarse.coarse = true; + // A coarse match may guide movement, but never authorize interaction. + // Final position must pass the normal local two-landmark confirmation. + coarse.at_target = false; + return coarse; +} +OverworldMatchResult match_oreburgh_mart(const ImageRGB32& frame){ + const ImageFloatBox target{0.502083333333, 0.235185185185, 0.056250000000, 0.079629629630}; + auto original = match_landmarks(frame, + {{"Mart emblem", + "PokemonBDSP/Autostory/OreburghMart/MartEmblem.png", + target, + {0, 0, 1, 360.0 / 540}, + .93, + .10}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y); + // The recorded daytime roof appearance narrowly missed the old template. + // Preserve both confidence gates and reject disagreement between variants. + auto recorded = match_landmarks(frame, + {{"Mart emblem", + "PokemonBDSP/Autostory/OreburghMart/MartEmblemRecorded.png", + target, + {0, 0, 1, 360.0 / 540}, + .93, + .10}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y); + if (original.located && recorded.located && + (std::abs(original.dx - recorded.dx) > AGREEMENT_X || std::abs(original.dy - recorded.dy) > AGREEMENT_Y)){ + original.located = false; + original.at_target = false; + return original; + } + if ((recorded.located && !original.located) || + (recorded.located == original.located && recorded.landmarks[0].score > original.landmarks[0].score)){ + return recorded; + } + return original; +} +OverworldMatchResult match_jubilife_center_entrance(const ImageRGB32& frame){ + // Native lighting-test captures avoid resizing artifacts in the reference. + // The emblem is about 5.73% of screen width left of the player at approach: + // shift its target right to center the door, preserving the approach height. + OverworldMatchResult clear = match_landmarks(frame, + {{"Center entrance", + "PokemonBDSP/Autostory/JubilifeCenter/CenterEmblem.png", + {0.462500000000, 0.109259259259, 0.072916666667, 0.111111111111}, + {0, 0, 1, 1}, + .90, + .20}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + OverworldMatchResult shadow = match_landmarks(frame, + {{"Center entrance", + "PokemonBDSP/Autostory/JubilifeCenter/CenterEmblemShadow.png", + {0.462500000000, 0.109259259259, 0.072916666667, 0.111111111111}, + {0, 0, 1, 1}, + .90, + .20}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + // Lighting alternatives must identify the same position when both pass. + if (clear.located && shadow.located && + (std::abs(clear.dx - shadow.dx) > AGREEMENT_X || std::abs(clear.dy - shadow.dy) > AGREEMENT_Y)){ + clear.located = false; + clear.at_target = false; + return clear; + } + if (shadow.located && (!clear.located || shadow.landmarks[0].score > clear.landmarks[0].score)){ + return shadow; + } + return clear; +} +OverworldMatchResult match_route201_potion_exit(const ImageRGB32& frame){ + // The sign face is stable across lighting; its post and surrounding grass are not. + // Path shadows change shape, so validate the corner against four lighting states. + // Each alternative still requires the sign and path to agree on position. + const char* corners[] = {"PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour0.png", + "PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour9.png", + "PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour12.png", + "PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour17.png"}; + OverworldMatchResult best; + double best_score = -1; + for (const char* corner : corners){ + auto candidate = match_landmarks(frame, + {{"Route sign face", + "PokemonBDSP/Autostory/Route201PotionExit/RouteSignFace.png", + {0.292708333333, 0.470370370370, 0.109375, 0.072222222222}, + {0, 0, 1, 1}, + .94, + .20}, + {"Path corner", + corner, + {0.202083333333, 0.648148148148, 0.133333333333, 0.174074074074}, + {0, 0, 1, 1}, + .90, + .10}}, + 2, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if (best.located && candidate.located && + (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ + best.located = false; + best.at_target = false; + return best; + } + if (candidate.landmarks.size() != 2){ + continue; + } + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + return best; +} +OverworldMatchResult match_sandgem_junction(const ImageRGB32& frame){ + // Building emblems contain no localized words. Ignore NPCs and door lights. + auto result = match_landmarks(frame, + {{"Center emblem", + "PokemonBDSP/Autostory/SandgemJunction/Center.png", + {0.171875, 0.2, 0.058333333333, 0.092592592593}, + {0, 0, 1, 1}, + .90, + .08}, + {"Mart emblem", + "PokemonBDSP/Autostory/SandgemJunction/Mart.png", + {0.726041666667, 0.222222222222, 0.0625, 0.088888888889}, + {0, 0, 1, 1}, + .90, + .08}}, + 2, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if (result.located || result.landmarks.size() != 2){ + return result; + } + const auto& center = result.landmarks[0]; + const auto& mart = result.landmarks[1]; + const double dy = (center.dy + mart.dy) / 2; + const double horizontal_disagreement = center.dx - mart.dx; + // At a different approach height, perspective changes the buildings' spacing. + // Trust matching height offsets for vertical acquisition only. Bound the + // horizontal discrepancy and require its direction to agree with perspective: + // scenery above target contracts, scenery below target expands. + if (center.accepted && mart.accepted && std::abs(center.dy - mart.dy) <= AGREEMENT_Y && std::abs(dy) > 1.0 / 72 && + std::abs(dy) <= 0.20 && std::abs(horizontal_disagreement) <= 0.04 && + std::abs(horizontal_disagreement) <= 0.35 * std::abs(dy) + 1.0 / 192 && horizontal_disagreement * dy <= 0){ + result.located = true; + result.coarse = true; + result.dx = (center.dx + mart.dx) / 2; + result.dy = dy; + // Acquisition may move vertically; it never confirms the route endpoint. + result.at_target = false; + } + return result; +} +OverworldMatchResult match_route202_grass_exit(const ImageRGB32& frame){ + // Route marker and upper path bend avoid the player, NPCs, and localized text. + // Morning tree shadows need a second corner reference. Each alternative + // must still agree with the sign; neither can confirm the destination alone. + const char* corners[] = {"PokemonBDSP/Autostory/Route202GrassExit/PathCorner.png", + "PokemonBDSP/Autostory/Route202GrassExit/PathCornerMorning.png"}; + OverworldMatchResult best; + double best_score = -1; + for (const char* corner : corners){ + auto candidate = match_landmarks(frame, + {{"Route 202 sign", + "PokemonBDSP/Autostory/Route202GrassExit/RouteSign.png", + {0.59375, 0.194444444444, 0.091666666667, 0.059259259259}, + {0, 0, 1, 1}, + .94, + .20}, + {"Path corner", + corner, + {0.405208333333, 0.331481481481, 0.1375, 0.062962962963}, + {0, 0, 1, 1}, + .90, + .08}}, + 2, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if (best.located && candidate.located && + (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ + best.located = false; + best.at_target = false; + return best; + } + if (candidate.landmarks.size() != 2){ + continue; + } + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + return best; +} +// Lighting alternatives describe one physical landmark, not extra votes. +// Select one reference per landmark before requiring the two positions to agree. +static OverworldMatchResult match_lighting_pair(const ImageRGB32& frame, + const std::vector& variants, + const char* first, + const char* second, + double tolerance_x = POSITION_TOLERANCE_X, + bool allow_vertical_acquisition = false, + double agreement_x = AGREEMENT_X){ + auto candidates = + match_landmarks(frame, variants, variants.size() + 1, POSITION_TOLERANCE_X, POSITION_TOLERANCE_Y, true); + OverworldMatchResult result; + for (const char* name : {first, second}){ + const OverworldLandmarkResult* best = nullptr; + for (const auto& candidate : candidates.landmarks){ + if (candidate.name != name){ + continue; + } + if (!best || (candidate.accepted && !best->accepted) || + (candidate.accepted == best->accepted && candidate.score > best->score)){ + best = &candidate; + } + } + if (!best){ + return result; + } + result.landmarks.push_back(*best); + auto& chosen = result.landmarks.back(); + // Similar strong references must not disagree about location. + for (const auto& candidate : candidates.landmarks){ + if (candidate.name == name && candidate.accepted && candidate.score >= chosen.score - 0.02 && + (std::abs(candidate.dx - chosen.dx) > AGREEMENT_X || + std::abs(candidate.dy - chosen.dy) > AGREEMENT_Y)){ + chosen.accepted = false; + } + } + } + const auto& a = result.landmarks[0]; + const auto& b = result.landmarks[1]; + result.located = + a.accepted && b.accepted && std::abs(a.dx - b.dx) <= agreement_x && std::abs(a.dy - b.dy) <= AGREEMENT_Y; + result.dx = (a.dx + b.dx) / 2; + result.dy = (a.dy + b.dy) / 2; + result.at_target = result.located && std::abs(a.dx - b.dx) <= AGREEMENT_X && std::abs(result.dx) <= tolerance_x && + std::abs(result.dy) <= POSITION_TOLERANCE_Y; + const double spacing_difference = a.dx - b.dx; + // At the street approach height, perspective can change column spacing. + // Agreeing vertical offsets may guide an upward correction, but cannot + // confirm the destination or authorize a horizontal correction yet. + if (!result.located && allow_vertical_acquisition && a.accepted && b.accepted && + std::abs(a.dy - b.dy) <= AGREEMENT_Y && std::abs(result.dy) > POSITION_TOLERANCE_Y && + std::abs(result.dy) <= 0.20 && std::abs(spacing_difference) <= 0.04 && + std::abs(spacing_difference) <= 0.35 * std::abs(result.dy) + 1.0 / 192 && spacing_difference * result.dy <= 0){ + result.located = true; + result.coarse = true; + result.at_target = false; + } + return result; +} +OverworldMatchResult match_eterna_center_entrance(const ImageRGB32& frame){ + // Native screenshot-20261016-214300767696 is the intended doorway pose. + // The Pokeball is primary. Sign and bush assist when visible, but cloud + // shadows must not prevent arrival from a strong, unique emblem match. + OverworldMatchResult best; + double best_score = -1; + bool best_primary_only = false; + // Recorded evening/daytime appearances retain the same destination and gates. + for (int appearance = 0; appearance < 5; appearance++){ + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + std::vector configs = {{"Center Pokeball", + "PokemonBDSP/Autostory/EternaCenter/BallTarget.png", + {904.0 / 1920, 144.0 / 1080, 112.0 / 1920, 104.0 / 1080}, + {0, 0, 1, .65}, + .93, + .20}, + {"Right sign", + "PokemonBDSP/Autostory/EternaCenter/RightSignTarget.png", + {1762.0 / 1920, 304.0 / 1080, 128.0 / 1920, 138.0 / 1080}, + {0, 0, 1, .85}, + .93, + .20}, + {"Lower left bush", + "PokemonBDSP/Autostory/EternaCenter/BushTarget.png", + {248.0 / 1920, 854.0 / 1080, 180.0 / 1920, 146.0 / 1080}, + {0, .35, 1, .65}, + .93, + .20}}; + if (appearance == 1 || appearance == 2){ + configs = {{"Center Pokeball", + appearance == 1 ? "PokemonBDSP/Autostory/EternaCenter/BallEvening1.png" + : "PokemonBDSP/Autostory/EternaCenter/BallEvening2.png", + {920.0 / 1920, 154.0 / 1080, 82.0 / 1920, 84.0 / 1080}, + {0, 0, 1, .65}, + .93, + .20}, + {"Right sign", + appearance == 1 ? "PokemonBDSP/Autostory/EternaCenter/RightSignEvening1.png" + : "PokemonBDSP/Autostory/EternaCenter/RightSignEvening2.png", + {1770.0 / 1920, 310.0 / 1080, 116.0 / 1920, 63.0 / 1080}, + {0, 0, 1, .85}, + .93, + .20}, + {"Lower left bush", + appearance == 1 ? "PokemonBDSP/Autostory/EternaCenter/BushEvening1.png" + : "PokemonBDSP/Autostory/EternaCenter/BushEvening2.png", + {248.0 / 1920, 932.0 / 1080, 180.0 / 1920, 68.0 / 1080}, + {0, .35, 1, .65}, + .93, + .20}}; + } + if (appearance >= 3){ + // Recorded daylight changes the bush/planter contrast. These + // crops retain the original doorway geometry and all three votes. + configs[0].path = appearance == 3 ? "PokemonBDSP/Autostory/EternaCenter/BallDaytime1.png" + : "PokemonBDSP/Autostory/EternaCenter/BallDaytime2.png"; + configs[1].path = appearance == 3 ? "PokemonBDSP/Autostory/EternaCenter/RightSignDaytime1.png" + : "PokemonBDSP/Autostory/EternaCenter/RightSignDaytime2.png"; + configs[2].path = appearance == 3 ? "PokemonBDSP/Autostory/EternaCenter/BushDaytime1.png" + : "PokemonBDSP/Autostory/EternaCenter/BushDaytime2.png"; + } + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (candidate.located){ + double score = 1; + for (const auto& landmark : candidate.landmarks){ + score = std::min(score, landmark.score); + } + if (!best.located || best_primary_only || score > best_score){ + best = candidate; + best_score = score; + best_primary_only = false; + } + }else if (!best.located || best_primary_only){ + // Optional sign/bush can disappear under clouds or at frame edges. + // Retain the existing stricter primary-emblem confidence gate. + const auto& ball = candidate.landmarks[0]; + if (ball.accepted && ball.score >= .95 && (!best.located || ball.score > best_score)){ + best = candidate; + best.located = true; + best.coarse = false; + best.dx = ball.dx; + best.dy = ball.dy; + best.at_target = std::abs(best.dx) <= 1.0 / 128 && std::abs(best.dy) <= POSITION_TOLERANCE_Y; + best_score = ball.score; + best_primary_only = true; + }else if (best.landmarks.empty()){ + best = candidate; + } + } + } + } + return best; +} +OverworldMatchResult match_route208_stair_top(const ImageRGB32& frame){ + // Native screenshot 20261022-213928168268: centered above the east stairs. + // The isolated upper rock and lower rock pair must both agree before descent. + OverworldMatchResult best; + double best_score = -1; + // Stationary hourly captures supply lighting appearances, not new goals. + // Both physical rock groups still need the same confidence and agreement. + const char* references[][2] = { + {"PokemonBDSP/Autostory/Route208Stairs/UpperRock.png", "PokemonBDSP/Autostory/Route208Stairs/LowerRocks.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Hour0Frame1Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Hour0Frame1Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Hour4Frame1Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Hour4Frame1Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Hour17Frame1Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Hour17Frame1Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Hour11Frame3Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Hour11Frame3Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Hour15Frame1Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Hour15Frame1Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Hour11Frame5Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Hour11Frame5Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Hour15Frame3Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Hour15Frame3Lower.png"}, + }; + for (const auto& reference : references){ + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + std::vector configs = {{"Stair-top upper rock", + reference[0], + {170.0 / 1920, 350.0 / 1080, 120.0 / 1920, 120.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20}, + {"Stair-top lower rock pair", + reference[1], + {240.0 / 1920, 425.0 / 1080, 130.0 / 1920, 180.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20}}; + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + double score = 1; + for (const auto& landmark : candidate.landmarks){ + score = std::min(score, landmark.score); + } + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + } + if (best.located){ + return best; + } + const char* approaches[][2] = { + {"PokemonBDSP/Autostory/Route208Stairs/Approach0507Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Approach0507Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Approach0538Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Approach0538Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/Approach0549Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/Approach0549Lower.png"}, + // The eastern cliff pose changes rock perspective and the local shadow + // pattern. These real-run appearances guide acquisition only: retain + // .92 acquisition NCC/.20 uniqueness and two-rock offset agreement. + // Arrival still requires the original .93 strict reference bank. + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive01Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive01Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive02Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive02Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive03Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive03Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive04Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive04Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive05Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive05Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive06Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive06Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive07Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive07Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive08Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive08Lower.png"}, + {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive09Upper.png", + "PokemonBDSP/Autostory/Route208Stairs/ApproachLive09Lower.png"}, + }; + for (const auto& reference : approaches){ + for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ + std::vector configs = {{"Stair-top approach upper rock", + reference[0], + {170.0 / 1920, 350.0 / 1080, 120.0 / 1920, 120.0 / 1080}, + {0, 0, .50, .85}, + .92, + .20}, + {"Stair-top approach lower rock pair", + reference[1], + {240.0 / 1920, 425.0 / 1080, 130.0 / 1920, 180.0 / 1080}, + {0, 0, .50, .85}, + .92, + .20}}; + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + candidate.coarse = true; + candidate.at_target = false; + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if (candidate.located && (!best.located || score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + } + return best; +} +OverworldMatchResult match_route208_lower_stair_lane(const ImageRGB32& frame){ + // Successful 1825ms descent endpoint at video 230s (2026-10-24 15-38-06). + // Independent cliff/tree anchors must agree before movement is authorized. + OverworldMatchResult best; + double best_score = -1; + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + std::vector configs = {{"Lower stair cliff corner", + "PokemonBDSP/Autostory/Route208LowerLane/CliffCorner.png", + {690.0 / 1920, 540.0 / 1080, 110.0 / 1920, 150.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20}, + {"Honey tree", + "PokemonBDSP/Autostory/Route208LowerLane/HoneyTree.png", + {1510.0 / 1920, 600.0 / 1080, 100.0 / 1920, 170.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20}}; + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (candidate.landmarks.size() != 2){ + continue; + } + const auto& cliff = candidate.landmarks[0]; + const auto& tree = candidate.landmarks[1]; + // Perspective changes anchor spacing farther down the field. Two strong + // anchors can authorize north-only recovery while bracketing the player. + // This cue never confirms arrival or authorizes east travel. + if (!candidate.located && cliff.accepted && tree.accepted && cliff.dy < -1.0 / 24 && tree.dy < -1.0 / 24 && + std::abs(cliff.dy - tree.dy) <= AGREEMENT_Y && cliff.found.x + cliff.found.width < .5 && + tree.found.x > .65){ + candidate.located = true; + candidate.coarse = true; + candidate.at_target = false; + candidate.dx = 0; + candidate.dy = (cliff.dy + tree.dy) / 2; + } + double score = 1; + for (const auto& landmark : candidate.landmarks){ + score = std::min(score, landmark.score); + } + if ((candidate.located && !best.located) || + (candidate.located && best.located && best.coarse && !candidate.coarse) || + (candidate.located == best.located && candidate.coarse == best.coarse && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + return best; +} +OverworldMatchResult match_route209_repel_point(const ImageRGB32& frame){ + // butters' marked sign frame and rock pair; the cloud-sensitive pillar is excluded. + // Only the sign's left frame/post is used; lettering is excluded. + OverworldMatchResult best; + double best_score = -1; + // Lighting samples are translated back to the original marked target. + // Appearance 1 remains acquisition-only; all others describe that target. + static const std::array appearances[] = { + {"PokemonBDSP/Autostory/Route209Repel/SignFrame.png", "PokemonBDSP/Autostory/Route209Repel/RockPair.png"}, + {"PokemonBDSP/Autostory/Route209Repel/EastSignFrame.png", + "PokemonBDSP/Autostory/Route209Repel/EastRockPair.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H04F1.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H04F1.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H18F4.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H18F4.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H12F1.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H12F1.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H02F3.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H02F3.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F4.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F4.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H12F4.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H12F4.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H12F5.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H12F5.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H02F5.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H02F5.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F2.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F2.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F3.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F3.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F4.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F4.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F5.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F5.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F1.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F1.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F2.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F2.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F3.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F3.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H02F4.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H02F4.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H03F5.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H03F5.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F1.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F1.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F5.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F5.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H13F5.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H13F5.png"}, + {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H14F5.png", + "PokemonBDSP/Autostory/Route209Repel/RockPair-H14F5.png"}, + }; + for (double scale : {1.0, .95, 1.05, .90, 1.10}){ + std::vector lighting_landmarks(2); + for (size_t appearance = 0; appearance < std::size(appearances); appearance++){ + std::vector configs = {{"Sign frame", + "PokemonBDSP/Autostory/Route209Repel/SignFrame.png", + {1242.0 / 1920, 580.0 / 1080, 28.0 / 1920, 80.0 / 1080}, + {0, 0, 1, 1}, + .93, + .15}, + {"Rock pair", + "PokemonBDSP/Autostory/Route209Repel/RockPair.png", + {580.0 / 1920, 690.0 / 1080, 155.0 / 1920, 165.0 / 1080}, + {0, 0, 1, 1}, + .93, + .15}}; + for (size_t i = 0; i < configs.size(); i++){ + configs[i].path = appearances[appearance][i]; + } + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (appearance != 1){ + for (size_t i = 0; i < candidate.landmarks.size(); i++){ + const auto& landmark = candidate.landmarks[i]; + if (landmark.accepted && + (!lighting_landmarks[i].accepted || landmark.score > lighting_landmarks[i].score)){ + lighting_landmarks[i] = landmark; + } + } + } + double score = 1; + // Both remaining anchors must pass confidence and agree spatially. + for (const auto& landmark : candidate.landmarks){ + score = std::min(score, landmark.score); + } + candidate.at_target = appearance != 1 && candidate.located && std::abs(candidate.dx) <= 1.0 / 128 && + std::abs(candidate.dy) <= POSITION_TOLERANCE_Y; + // East-side appearance is acquisition-only; the marked pose owns arrival. + if (appearance == 1 && std::abs(candidate.dx) <= 1.0 / 128 && + std::abs(candidate.dy) <= POSITION_TOLERANCE_Y){ + candidate.located = false; + } + if (candidate.located){ + return candidate; + } + if ((candidate.at_target && !best.at_target) || (candidate.located && !best.located) || + (candidate.at_target == best.at_target && candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + // Combine lighting references only when the sign and rock agree. + OverworldMatchResult combined; + combined.landmarks = std::move(lighting_landmarks); + const auto& sign = combined.landmarks[0]; + const auto& rock = combined.landmarks[1]; + combined.located = sign.accepted && rock.accepted && std::abs(sign.dx - rock.dx) <= AGREEMENT_X && + std::abs(sign.dy - rock.dy) <= AGREEMENT_Y; + if (combined.located){ + combined.dx = (sign.dx + rock.dx) / 2; + combined.dy = (sign.dy + rock.dy) / 2; + combined.at_target = std::abs(combined.dx) <= 1.0 / 128 && std::abs(combined.dy) <= POSITION_TOLERANCE_Y; + return combined; + } + } + return best; +} +OverworldMatchResult match_route208_hannah_departure(const ImageRGB32& frame){ + OverworldMatchResult best; + double best_score = -1; + for (int appearance : {0, 865, 930, 965, 978}){ + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + std::vector configs = {{"Field stair foot", + "PokemonBDSP/Autostory/Route208Hannah/StairFoot.png", + {225.0 / 1920, 275.0 / 1080, 250.0 / 1920, 110.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Field sign frame", + "PokemonBDSP/Autostory/Route208Hannah/TreeSign.png", + {1000.0 / 1920, 125.0 / 1080, 100.0 / 1920, 80.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20}, + {"Field house sign frame", + "PokemonBDSP/Autostory/Route208Hannah/HouseSign.png", + {1440.0 / 1920, 110.0 / 1080, 110.0 / 1920, 110.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20}}; + if (appearance){ + // Recorded dodges stand farther east, with the stairs clipped. + // These agreeing signs guide acquisition only; the successful + // departure reference above alone may confirm arrival. + configs.erase(configs.begin()); + configs[0].path = appearance == 865 ? "PokemonBDSP/Autostory/Route208Hannah/FieldSign865.png" + : appearance == 930 ? "PokemonBDSP/Autostory/Route208Hannah/FieldSign930.png" + : appearance == 965 ? "PokemonBDSP/Autostory/Route208Hannah/FieldSign965.png" + : "PokemonBDSP/Autostory/Route208Hannah/FieldSign978.png"; + configs[1].path = appearance == 865 ? "PokemonBDSP/Autostory/Route208Hannah/HouseSign865.png" + : appearance == 930 ? "PokemonBDSP/Autostory/Route208Hannah/HouseSign930.png" + : appearance == 965 ? "PokemonBDSP/Autostory/Route208Hannah/HouseSign965.png" + : "PokemonBDSP/Autostory/Route208Hannah/HouseSign978.png"; + } + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (appearance){ + candidate.coarse = true; + candidate.at_target = false; + } + double score = 1; + size_t accepted = 0; + for (const auto& landmark : candidate.landmarks){ + if (landmark.accepted){ + score = std::min(score, landmark.score); + accepted++; + } + } + if (!accepted){ + score = -1; + } + if ((candidate.located && !best.located) || + (candidate.located && best.located && !candidate.coarse && best.coarse) || + (candidate.located == best.located && candidate.coarse == best.coarse && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + } + return best; +} +OverworldMatchResult match_hearthome_center_entrance(const ImageRGB32& frame){ + // Real stationary hourly views cover daylight, night, dusk and cloud edges. + // The target is butters' street pose below the door; entry remains watched. + struct Reference { + const char* ball; + const char* awning; + double ball_width, ball_height; + }; + const Reference references[] = {{"PokemonBDSP/Autostory/HearthomeCenter/Lighting00Ball.png", + "PokemonBDSP/Autostory/HearthomeCenter/Lighting00Awning.png", + 144.0 / 1920, + 128.0 / 1080}, + {"PokemonBDSP/Autostory/HearthomeCenter/Lighting01Ball.png", + "PokemonBDSP/Autostory/HearthomeCenter/Lighting01Awning.png", + 144.0 / 1920, + 128.0 / 1080}, + {"PokemonBDSP/Autostory/HearthomeCenter/Lighting02Ball.png", + "PokemonBDSP/Autostory/HearthomeCenter/Lighting02Awning.png", + 144.0 / 1920, + 128.0 / 1080}, + {"PokemonBDSP/Autostory/HearthomeCenter/Lighting03Ball.png", + "PokemonBDSP/Autostory/HearthomeCenter/Lighting03Awning.png", + 144.0 / 1920, + 128.0 / 1080}, + {"PokemonBDSP/Autostory/HearthomeCenter/Lighting04Ball.png", + "PokemonBDSP/Autostory/HearthomeCenter/Lighting04Awning.png", + 112.0 / 1920, + 108.0 / 1080}}; + std::vector variants; + for (const auto& reference : references){ + for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ + const double w = reference.ball_width * scale, h = reference.ball_height * scale; + const double aw = 272.0 / 1920 * scale, ah = 80.0 / 1080 * scale; + variants.push_back({"Hearthome Center roof ball", + reference.ball, + {972.0 / 1920 - w / 2, 208.0 / 1080 - h / 2, w, h}, + {0, 0, 1, .65}, + .95, + .20}); + variants.push_back({"Hearthome Center orange awning", + reference.awning, + {968.0 / 1920 - aw / 2, 308.0 / 1080 - ah / 2, aw, ah}, + {0, 0, 1, .65}, + .95, + .20}); + } + } + return match_lighting_pair(frame, + variants, + "Hearthome Center roof ball", + "Hearthome Center orange awning", + 1.0 / 128); +} +OverworldMatchResult match_hearthome_center_interior(const ImageRGB32& frame){ + // Three observed entry-camera angles. Perspective changes the machine + // and register independently, so require a pair from the same real view. + struct View { + const char* machine; + const char* desk; + ImageFloatBox a, b; + }; + const View views[] = {{"InteriorMachine.png", + "InteriorRegister.png", + {660.0 / 1920, 160.0 / 1080, 220.0 / 1920, 185.0 / 1080}, + {1150.0 / 1920, 155.0 / 1080, 140.0 / 1920, 175.0 / 1080}}, + {"EntryRightMachine.png", + "EntryRightRegister.png", + {390.0 / 1920, 160.0 / 1080, 240.0 / 1920, 185.0 / 1080}, + {905.0 / 1920, 155.0 / 1080, 130.0 / 1920, 175.0 / 1080}}, + {"EntryLeftMachine.png", + "EntryLeftRegister.png", + {1045.0 / 1920, 160.0 / 1080, 240.0 / 1920, 185.0 / 1080}, + {1550.0 / 1920, 220.0 / 1080, 155.0 / 1920, 110.0 / 1080}}}; + OverworldMatchResult best; + double best_score = -1; + for (const auto& view : views){ + const std::string machine = "PokemonBDSP/Autostory/HearthomeCenter/" + std::string(view.machine); + const std::string desk = "PokemonBDSP/Autostory/HearthomeCenter/" + std::string(view.desk); + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + auto a = view.a, b = view.b; + a.x = .5 + (a.x - .5) * scale; + a.y = .5 + (a.y - .5) * scale; + a.width *= scale; + a.height *= scale; + b.x = .5 + (b.x - .5) * scale; + b.y = .5 + (b.y - .5) * scale; + b.width *= scale; + b.height *= scale; + auto candidate = match_landmarks(frame, + {{"Center healing machine", machine.c_str(), a, {0, 0, 1, .8}, .93, .20}, + {"Center register", desk.c_str(), b, {0, 0, 1, .8}, .93, .20}}, + 2, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + double score = 1; + for (const auto& anchor : candidate.landmarks){ + score = std::min(score, anchor.score); + } + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + } + return best; +} +OverworldMatchResult match_hearthome_contest_fantina(const ImageRGB32& frame){ + // Static sign rim above localized text and roof emblem; Fantina can move. + // Hourly captures vary the sign illumination. The roof remains reliable. + const char* signs[] = {"PokemonBDSP/Autostory/HearthomeContest/SignRim.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting00Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting01Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting02Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting03Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting04Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting05Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting06Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting07Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting08Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting09Sign.png", + "PokemonBDSP/Autostory/HearthomeContest/RimLighting10Sign.png"}; + std::vector variants; + for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ + const double sw = 162.0 / 1920 * scale, sh = 62.0 / 1080 * scale; + const double rw = 136.0 / 1920 * scale, rh = 98.0 / 1080 * scale; + for (const char* sign : signs){ + variants.push_back({"Contest Hall sign frame", + sign, + {595.0 / 1920 - sw / 2, 467.0 / 1080 - sh / 2, sw, sh}, + {0, .15, .75, .65}, + .93, + .15}); + } + variants.push_back({"Contest Hall roof emblem", + "PokemonBDSP/Autostory/HearthomeContest/RoofEmblem.png", + {940.0 / 1920 - rw / 2, 175.0 / 1080 - rh / 2, rw, rh}, + {.1, 0, .8, .65}, + .93, + .15}); + } + return match_lighting_pair(frame, variants, "Contest Hall sign frame", "Contest Hall roof emblem", 1.0 / 128); +} +// Fixed city landmarks preserve the recorded passage and western stair lane. +OverworldMatchResult match_veilstone_city_passage(const ImageRGB32& frame){ + auto fine = match_landmarks(frame, + {{"Passage house", + "PokemonBDSP/Autostory/VeilstoneDawn/PassageHouse.png", + {42.0 / 960, 222.0 / 540, 210.0 / 960, 110.0 / 540}, + {0, 0, .55, 1}, + .93, + .12}, + {"Game Corner facade", + "PokemonBDSP/Autostory/VeilstoneDawn/PassageGameCorner.png", + {592.0 / 960, 227.0 / 540, 230.0 / 960, 110.0 / 540}, + {.45, 0, .55, 1}, + .93, + .12}}, + 2, + 1.0 / 128, + 1.0 / 108, + true); + if (fine.located){ + return fine; + } + // The former reference column lies just inside the left cliff in the + // live corridor. Recenter eight pixels horizontally and two vertically in the + // matcher image; keep both arrival tolerances unchanged. + // A farther-east expiry may expose only the house's right window. + std::vector variants; + for (double scale : {.85, .90, .95, 1.0, 1.05, 1.10}){ + const double hw = 95.0 / 960 * scale, hh = 80.0 / 540 * scale; + const double gw = 230.0 / 960 * scale, gh = 110.0 / 540 * scale; + variants.push_back({"House window", + "PokemonBDSP/Autostory/VeilstoneDawn/PassageHouseWindow.png", + {(165.0 + 95.0 / 2 - 8) / 960 - hw / 2, (250.0 + 80.0 / 2 + 2) / 540 - hh / 2, hw, hh}, + {0, 0, .55, 1}, + .94, + .15}); + variants.push_back({"Game Corner facade", + "PokemonBDSP/Autostory/VeilstoneDawn/PassageGameCorner.png", + {(600.0 + 230.0 / 2 - 8) / 960 - gw / 2, (225.0 + 110.0 / 2 + 2) / 540 - gh / 2, gw, gh}, + {.25, 0, .75, 1}, + .93, + .12}); + } + // At the east-side roof obstruction, perspective changes the facade. + // Calibrate this acquisition-only view to the simultaneously visible house + // window. It guides west off that roof corner; fine arrival stays mandatory. + variants.push_back({"Game Corner facade", + "PokemonBDSP/Autostory/VeilstoneDawn/GameCornerEastApproach.png", + {644.0 / 960, 228.0 / 540, 240.0 / 960, 100.0 / 540}, + {.25, 0, .75, 1}, + .93, + .12}); + auto approach = match_lighting_pair(frame, variants, "House window", "Game Corner facade", 1.0 / 128); + approach.coarse = true; + approach.at_target = false; + return approach; +} +OverworldMatchResult match_veilstone_dawn_stairs(const ImageRGB32& frame){ + // The residential terrace to the east has similar stairs. Require its + // distinct west corner together with the western staircase, not NPCs. + return match_landmarks(frame, + {{"Western stairs", + "PokemonBDSP/Autostory/VeilstoneDawn/GymStairs.png", + {445.0 / 960, 40.0 / 540, 165.0 / 960, 55.0 / 540}, + {.10, 0, .65, .70}, + .93, + .12}, + {"East terrace corner", + "PokemonBDSP/Autostory/VeilstoneDawn/EastTerraceCorner.png", + {620.0 / 960, 155.0 / 540, 200.0 / 960, 110.0 / 540}, + {.35, 0, .65, .85}, + .93, + .12}}, + 2, + 1.0 / 128, + 1.0 / 108, + true); +} + +OverworldMatchResult match_hearthome_gym_entrance(const ImageRGB32& frame){ + // Wider fixed facade distinguishes the window bays at night. Real hourly + // references cover the illuminated windows; cursor, NPCs and text stay out. + const char* facades[] = {"PokemonBDSP/Autostory/HearthomeGym/LeftFacade.png", + "PokemonBDSP/Autostory/HearthomeGym/Lighting00Facade.png", + "PokemonBDSP/Autostory/HearthomeGym/Lighting01Facade.png", + "PokemonBDSP/Autostory/HearthomeGym/Lighting02Facade.png", + "PokemonBDSP/Autostory/HearthomeGym/Lighting03Facade.png", + "PokemonBDSP/Autostory/HearthomeGym/Lighting04Facade.png"}; + std::vector variants; + for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ + const double aw = 160.0 / 1920 * scale, ah = 84.0 / 1080 * scale; + const double fw = 218.0 / 1920 * scale, fh = 222.0 / 1080 * scale; + variants.push_back({"Hearthome Gym doorway arch", + "PokemonBDSP/Autostory/HearthomeGym/DoorArch.png", + {966.0 / 1920 - aw / 2, 236.0 / 1080 - ah / 2, aw, ah}, + {.15, 0, .75, .65}, + .93, + .20}); + for (const char* facade : facades){ + variants.push_back({"Hearthome Gym left facade", + facade, + {719.0 / 1920 - fw / 2, 315.0 / 1080 - fh / 2, fw, fh}, + {0, 0, .55, .65}, + .93, + .20}); + } + } + return match_lighting_pair(frame, variants, "Hearthome Gym doorway arch", "Hearthome Gym left facade", 1.0 / 128); +} + +OverworldMatchResult match_eterna_gym_entrance(const ImageRGB32& frame){ + // Recorded centered doorway pose. All three stationary landmarks must agree + // before entering; two can guide acquisition without confirming arrival. + OverworldMatchResult best; + double best_score = -1; + for (int appearance : {19, 16}){ + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + std::vector configs = {{"Left gym sign", + appearance == 19 + ? "PokemonBDSP/Autostory/EternaGym/LeftSign19.png" + : "PokemonBDSP/Autostory/EternaGym/LeftSign16.png", + {458.0 / 1920, 358.0 / 1080, 140.0 / 1920, 150.0 / 1080}, + {0, 0, .65, .85}, + .93, + .20}, + {"Gym Pokeball", + appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/Ball19.png" + : "PokemonBDSP/Autostory/EternaGym/Ball16.png", + {880.0 / 1920, 264.0 / 1080, 150.0 / 1920, 80.0 / 1080}, + {.15, 0, .7, .70}, + .93, + .20}, + {"Right bike post", + appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/BikePost19.png" + : "PokemonBDSP/Autostory/EternaGym/BikePost16.png", + {1468.0 / 1920, 460.0 / 1080, 130.0 / 1920, 178.0 / 1080}, + {.35, .15, .65, .80}, + .93, + .20}}; + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (!candidate.located){ + candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + candidate.coarse = true; + candidate.at_target = false; + } + double score = 1; + for (const auto& landmark : candidate.landmarks){ + if (landmark.accepted){ + score = std::min(score, landmark.score); + } + } + if (candidate.located && + (!best.located || (best.coarse && !candidate.coarse) || + (best.coarse == candidate.coarse && score > best_score))){ + best = candidate; + best_score = score; + }else if (best.landmarks.empty()){ + best = candidate; + } + } + } + if (best.located){ + return best; + } + // The gym first appears at the far right of the approach street. Search + // the whole frame for its left sign and emblem; the bike post may be offscreen. + // These broad matches guide travel only. Arrival still needs all three above. + double coarse_score = -1; + for (int appearance : {680, 19, 333, 303, 306, 339}){ + for (double scale : {.85, .90, 1.0, 1.10, 1.15}){ + std::vector configs = { + {"Left gym sign", + appearance == 680 ? "PokemonBDSP/Autostory/EternaGym/LeftSignApproach.png" + : appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/LeftSign19.png" + : appearance == 333 ? "PokemonBDSP/Autostory/EternaGym/LeftSignWest.png" + : appearance == 306 ? "PokemonBDSP/Autostory/EternaGym/LeftSignSouthShadow.png" + : appearance == 339 ? "PokemonBDSP/Autostory/EternaGym/LeftSignWestShadow.png" + : "PokemonBDSP/Autostory/EternaGym/LeftSignSouth.png", + {458.0 / 1920, 358.0 / 1080, 140.0 / 1920, 150.0 / 1080}, + {0, 0, 1, .95}, + .93, + .20}, + {"Gym Pokeball", + appearance == 680 ? "PokemonBDSP/Autostory/EternaGym/BallApproach.png" + : appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/Ball19.png" + : "PokemonBDSP/Autostory/EternaGym/BallWest.png", + {880.0 / 1920, 264.0 / 1080, 150.0 / 1920, 80.0 / 1080}, + {0, 0, 1, .90}, + .93, + .20}}; + if (appearance == 680){ + configs[0].target.width = 148.0 / 1920; + configs[0].target.height = 158.0 / 1080; + configs[1].target.width = 190.0 / 1920; + } + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + const auto& sign = candidate.landmarks[0]; + const auto& ball = candidate.landmarks[1]; + double score = std::min(sign.score, ball.score); + // Perspective changes their spacing while the gym is far right. + // Both accepted anchors must still agree on the rightward direction + // and vertical row before using that directional-only acquisition. + if (!candidate.located && sign.accepted && ball.accepted && sign.dx > .08 && ball.dx > .08 && + std::abs(sign.dy - ball.dy) <= .04 && std::min(sign.dx, ball.dx) > 2 * std::abs(sign.dx - ball.dx)){ + candidate.located = true; + candidate.dx = (sign.dx + ball.dx) / 2; + candidate.dy = (sign.dy + ball.dy) / 2; + } + if (!candidate.located){ + // One fully visible unique landmark is enough to guide the + // approach when the other is clipped at the right edge. + for (const auto& anchor : candidate.landmarks){ + if (anchor.accepted && anchor.score >= .97 && anchor.margin >= .30 && anchor.dx > .08 && + std::abs(anchor.dy) <= .22 && anchor.score > score){ + candidate.located = true; + candidate.dx = anchor.dx; + candidate.dy = anchor.dy; + score = anchor.score; + } + } + } + candidate.coarse = true; + candidate.at_target = false; + if (candidate.located && score > coarse_score){ + best = candidate; + coarse_score = score; + } + } + } + if (best.located){ + return best; + } + // The yellow-roofed house opposite the gym remains visible when the + // gym itself is clipped offscreen. Reuse paired landmark agreement to + // recover the doorway row and guide east until gym anchors take over. + // Its virtual target is off the left edge: these approach anchors can + // never satisfy near-door moderate confidence or confirm gym arrival. + double house_score = -1; + for (int perspective : {720, 734, 708}){ + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + std::vector configs = { + {"Gym approach house door", + perspective == 708 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseDoorFarWest.png" + : perspective == 734 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseDoorWest.png" + : "PokemonBDSP/Autostory/EternaGym/ApproachHouseDoor.png", + {548.0 / 1920 - .55, 448.0 / 1080, 128.0 / 1920, 156.0 / 1080}, + {0, .15, .75, .70}, + .93, + .20}, + {"Gym approach house window", + perspective == 708 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseWindowFarWest.png" + : perspective == 734 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseWindowWest.png" + : "PokemonBDSP/Autostory/EternaGym/ApproachHouseWindow.png", + {718.0 / 1920 - .55, 462.0 / 1080, 132.0 / 1920, 112.0 / 1080}, + {0, .15, .85, .70}, + .93, + .20}}; + if (perspective == 734){ + configs[0].target = {338.0 / 1920 - .55, 450.0 / 1080, 132.0 / 1920, 150.0 / 1080}; + configs[1].target = {496.0 / 1920 - .55, 464.0 / 1080, 122.0 / 1920, 102.0 / 1080}; + } + if (perspective == 708){ + configs[0].target = {240.0 / 1920 - .55, 464.0 / 1080, 150.0 / 1920, 150.0 / 1080}; + configs[1].target = {416.0 / 1920 - .55, 478.0 / 1080, 118.0 / 1920, 90.0 / 1080}; + } + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + candidate.coarse = true; + candidate.at_target = false; + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if (candidate.located && candidate.dx > .08 && score > house_score){ + best = candidate; + house_score = score; + } + } + } + return best; +} +double overworld_entry_confidence(const OverworldMatchResult& result, + double tolerance_x, + double tolerance_y, + double minimum_margin){ + double confidence = 0; + for (size_t i = 0; i < result.landmarks.size(); i++){ + const auto& a = result.landmarks[i]; + if (a.margin < minimum_margin || std::abs(a.dx) > tolerance_x || std::abs(a.dy) > tolerance_y){ + continue; + } + for (size_t j = i + 1; j < result.landmarks.size(); j++){ + const auto& b = result.landmarks[j]; + if (b.margin < minimum_margin || std::abs(b.dx) > tolerance_x || std::abs(b.dy) > tolerance_y){ + continue; + } + if (std::abs(a.dx - b.dx) > AGREEMENT_X || std::abs(a.dy - b.dy) > AGREEMENT_Y){ + continue; + } + confidence = std::max(confidence, std::min(a.score, b.score)); + } + } + return confidence; +} +OverworldMatchResult match_galactic_hq_jupiter(const ImageRGB32& frame){ + // The supplied reference is the interaction pose directly north of Jupiter. + // Confirm the table-facing chair sides, adjacent table edges, and stairs. + // Exclude the NPC-facing chair edges where Buneary can overlap the reference. + const std::vector configs{{"LeftChairs", + "PokemonBDSP/Autostory/GalacticHQ/AboveLeftChairTableEdge.png", + {300.0 / 1920, 780.0 / 1080, 156.0 / 1920, 206.0 / 1080}, + {0, .40, .45, .60}, + .93, + .20}, + {"RightChairs", + "PokemonBDSP/Autostory/GalacticHQ/AboveRightChairTableEdge.png", + {1286.0 / 1920, 780.0 / 1080, 154.0 / 1920, 206.0 / 1080}, + {.50, .40, .50, .60}, + .93, + .20}, + {"StairBoundary", + "PokemonBDSP/Autostory/GalacticHQ/AboveStairs.png", + {424.0 / 1920, 310.0 / 1080, 214.0 / 1920, 198.0 / 1080}, + {0, 0, 1, .85}, + .93, + .20}}; + auto result = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (!result.located){ + // Two agreeing points guide the approach; all three still own arrival. + auto coarse = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (coarse.located){ + coarse.coarse = true; + coarse.at_target = false; + result = coarse; + } + } + if (!result.located){ + // Existing recorded chair views can guide recovery from the old row. + // Their coordinates still point to the new north-side destination; + // only the three canonical above-Jupiter references confirm arrival. + auto approach = std::vector{configs[0], configs[1]}; + approach[0].path = "PokemonBDSP/Autostory/GalacticHQ/LeftChairs.png"; + approach[1].path = "PokemonBDSP/Autostory/GalacticHQ/RightChairs.png"; + // Retain the original geometry for the old-row guide-only references. + approach[0].target = {334.0 / 1920, 756.0 / 1080, 156.0 / 1920, 228.0 / 1080}; + approach[1].target = {1238.0 / 1920, 756.0 / 1080, 164.0 / 1920, 230.0 / 1080}; + for (auto& chair : approach){ + chair.threshold = .95; + chair.margin = .25; + } + auto coarse = match_landmarks(frame, approach, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (coarse.located){ + result.located = true; + result.coarse = true; + result.at_target = false; + result.dx = coarse.dx; + result.dy = coarse.dy; + result.landmarks[0] = coarse.landmarks[0]; + result.landmarks[1] = coarse.landmarks[1]; + } + } + return result; +} + +OverworldMatchResult match_galactic_hq_cut(const ImageRGB32& frame){ + // The sign face and Center emblem identify the cuttable tree's column. + // Exclude the HQ door, whose open/night appearance changed in the failed run. + // Tight face/emblem crops avoid grass and moving cloud shadows on the roof. + // The stronger emblem needs a corroborating sign, with a lower identity + // gate for the smaller sign face. Both still must agree on the same pose. + auto result = match_lighting_pair(frame, + {{"HQ sign", + "PokemonBDSP/Autostory/GalacticHQ/CutSignStructure.png", + {810.0 / 1920, 500.0 / 1080, 80.0 / 1920, 58.0 / 1080}, + {0, .25, 1, .45}, + .85, + .10, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Center Pokeball", + "PokemonBDSP/Autostory/GalacticHQ/CutCenterBallStructure.png", + {1048.0 / 1920, 1004.0 / 1080, 136.0 / 1920, 66.0 / 1080}, + {0, .65, 1, .35}, + .93, + .20, + LandmarkFeatures::BLUE_FRACTION}}, + "HQ sign", + "Center Pokeball", + 1.0 / 128); + if (!result.located && result.landmarks.size() == 2){ + const auto& sign = result.landmarks[0]; + const auto& ball = result.landmarks[1]; + // In the recorded far-west misses, the low fascia emblem shifts 1.31 + // screen pixels per sign pixel. Calibrate that acquisition observation + // before applying the same position-agreement bound; never use it for arrival. + const double calibrated_ball_dx = ball.dx / 1.31; + if (sign.accepted && ball.accepted && sign.dx * ball.dx > 0 && std::abs(sign.dx) > .03 && + std::abs(sign.dx) <= .20 && std::abs(sign.dx - calibrated_ball_dx) <= AGREEMENT_X && + std::abs(sign.dy - ball.dy) <= AGREEMENT_Y){ + result.located = true; + result.coarse = true; + result.at_target = false; + result.dx = (sign.dx + calibrated_ball_dx) / 2; + } + } + // The recorded approach already clamps against the hedge. Correct only + // its column; the watched Up/A loop owns the final north interaction. + result.dy = 0; + result.at_target = result.located && !result.coarse && std::abs(result.dx) <= 1.0 / 128; + return result; +} + +OverworldMatchResult match_eterna_cycle_shop_entrance(const ImageRGB32& frame){ + const std::vector configs{ + {"Shop sign", + "PokemonBDSP/Autostory/EternaBikeShop/eterna_cycle_shop_entrance-0.png", + {555.0 / 1920, 350.0 / 1080, 100.0 / 1920, 120.0 / 1080}, + {0, 0.1, 0.65, 0.75}, + .93, + .20}, + {"Shop roof braces", + "PokemonBDSP/Autostory/EternaBikeShop/ShopRoofBraces.png", + {680.0 / 1920, 145.0 / 1080, 470.0 / 1920, 36.0 / 1080}, + {0, 0, .95, .75}, + .93, + .20}}; + return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); +} + +OverworldMatchResult match_eterna_cycle_shop_interior(const ImageRGB32& frame){ + // Entry mat view: fixed wall tools and register, clear of NPCs/Pokemon. + // This identifies the room; it is not the counter interaction position. + const std::vector configs{{"Shop wall tools", + "PokemonBDSP/Autostory/EternaBikeShop/InteriorWallTools.png", + {920.0 / 1920, 20.0 / 1080, 350.0 / 1920, 150.0 / 1080}, + {.15, 0, .60, .55}, + .93, + .20}, + {"Shop register", + "PokemonBDSP/Autostory/EternaBikeShop/InteriorRegister.png", + {760.0 / 1920, 160.0 / 1080, 120.0 / 1920, 145.0 / 1080}, + {.20, 0, .55, .65}, + .93, + .20}}; + auto result = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (result.located){ + return result; + } + const std::vector north_configs{ + {"Shop wall tools", + "PokemonBDSP/Autostory/EternaBikeShop/InteriorNorthWallTools.png", + {928.0 / 1920, 84.0 / 1080, 350.0 / 1920, 150.0 / 1080}, + {.15, 0, .60, .55}, + .93, + .20}, + {"Shop register", + "PokemonBDSP/Autostory/EternaBikeShop/InteriorNorthRegister.png", + {754.0 / 1920, 230.0 / 1080, 120.0 / 1920, 145.0 / 1080}, + {.20, 0, .55, .65}, + .93, + .20}}; + return match_landmarks(frame, north_configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); +} + +OverworldMatchResult match_eterna_cycle_shop_reward(const ImageRGB32& frame){ + // Reward returns the player outside on the bicycle, at a separate pose. + // Reuse the stable roof braces; the nearby post changes appearance/shading. + const std::vector configs{{"Shop sign", + "PokemonBDSP/Autostory/EternaBikeShop/reward-sign.png", + {580.0 / 1920, 330.0 / 1080, 100.0 / 1920, 120.0 / 1080}, + {0, .1, .65, .75}, + .93, + .20}, + {"Shop roof braces", + "PokemonBDSP/Autostory/EternaBikeShop/ShopRoofBraces.png", + {706.0 / 1920, 128.0 / 1080, 470.0 / 1920, 36.0 / 1080}, + {0, 0, .95, .75}, + .93, + .20}}; + return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); +} + +OverworldMatchResult match_eterna_bike_riding_card(const ImageRGB32& frame){ + // Fixed UI card: require both the blue illustration and B Close prompt. + // Small search boxes tolerate capture offsets without matching other menus. + const std::vector configs{{"Blue Bike illustration", + "PokemonBDSP/Autostory/EternaBikeShop/BikeRidingBlueFeature.png", + {104.0 / 1920, 314.0 / 1080, 520.0 / 1920, 450.0 / 1080}, + {.035, .26, .32, .48}, + .93, + .20, + LandmarkFeatures::BLUE_FRACTION}, + {"B Close prompt", + "PokemonBDSP/Autostory/EternaBikeShop/BikeRidingClosePrompt.png", + {1570.0 / 1920, 730.0 / 1080, 160.0 / 1920, 60.0 / 1080}, + {.79, .65, .14, .10}, + .93, + .20}}; + return match_landmarks(frame, configs, 2, 1.0 / 64, 1.0 / 54, true); +} + +OverworldMatchResult match_eterna_cycle_shop_counter(const ImageRGB32& frame){ + const std::vector configs{ + {"Red frame", + "PokemonBDSP/Autostory/EternaBikeShop/eterna_cycle_shop_counter-0.png", + {620.0 / 1920, 65.0 / 1080, 390.0 / 1920, 240.0 / 1080}, + {0.1, 0, 0.65, 0.55}, + .93, + .20}, + {"Counter detail", + "PokemonBDSP/Autostory/EternaBikeShop/eterna_cycle_shop_counter-1.png", + {1110.0 / 1920, 486.0 / 1080, 86.0 / 1920, 38.0 / 1080}, + {0.3, 0.15, 0.65, 0.65}, + .93, + .20}}; + return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); +} + +// Two stationary architectural features confirm the intended gate interior. +// The doorway and west window avoid the moving player, NPCs and localized text. +OverworldMatchResult match_cycling_road_south_gate_interior(const ImageRGB32& frame){ + return match_landmarks(frame, + {{"Gate north doorway", + "PokemonBDSP/Autostory/CyclingRoadSouthGate/NorthDoorway.png", + {630.0 / 1920, 118.0 / 1080, 220.0 / 1920, 270.0 / 1080}, + {.15, 0, .70, .55}, + .93, + .20}, + {"Gate west window", + "PokemonBDSP/Autostory/CyclingRoadSouthGate/WestWindow.png", + {302.0 / 1920, 140.0 / 1080, 157.0 / 1920, 145.0 / 1080}, + {.05, 0, .35, .55}, + .93, + .20}}, + 2, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y); +} +OverworldMatchResult match_cycling_road_bridge_approach(const ImageRGB32& frame){ + // Use the actual bridge entrance: cyan right rail with a broad metal deck + // immediately to its left. Neither the Route 206 sign nor gate is involved. + OverworldMatchResult best; + if (!frame){ + return best; + } + cv::Mat pixels((int)frame.height(), + (int)frame.width(), + CV_8UC4, + const_cast(frame.data()), + frame.bytes_per_row()); + cv::Mat small, bgr, hsv, cyan; + cv::resize(pixels, small, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + cv::cvtColor(small, bgr, cv::COLOR_BGRA2BGR); + cv::cvtColor(bgr, hsv, cv::COLOR_BGR2HSV); + // Hue/saturation retain the cyan rail under the recorded moving shadows. + cv::inRange(hsv, cv::Scalar(78, 95, 55), cv::Scalar(105, 255, 255), cyan); + cv::morphologyEx(cyan, cyan, cv::MORPH_CLOSE, cv::Mat::ones(3, 3, CV_8U)); + const int w = hsv.cols, h = hsv.rows; + std::vector> contours; + cv::findContours(cyan, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); + double best_support = 0; + bool ambiguous = false; + for (const auto& contour : contours){ + const auto rail = cv::boundingRect(contour); + if (rail.x < w * .25 || rail.x > w * .85 || rail.height < h * .10){ + continue; + } + // A left-hand rail has grass/cliff on this side and fails the deck test. + const int left = std::max(0, rail.x - (int)(w * .22)); + const int right = rail.x - (int)(w * .02); + if (right - left < w * .12){ + continue; + } + const int begin = std::max(0, rail.y + (int)(h * .035)); + const int end = std::min(h - (int)(h * .05), rail.y + (int)(h * .24)); + const int sustained = std::max(3, (int)(h * .018)); + int deck_y = -1; + double deck_support = 0; + for (int y = begin; y < end; y++){ + bool supported = true; + double total = 0; + // Both halves must see the same broad deck edge, not a flower, + // narrow painted line or one white patch beside a cyan object. + for (int half = 0; half < 2; half++){ + int neutral = 0; + const int x0 = left + (right - left) * half / 2; + const int x1 = left + (right - left) * (half + 1) / 2; + for (int row = y; row < y + sustained; row++){ + for (int x = x0; x < x1; x++){ + const auto p = hsv.at(row, x); + // Shadowed metal remains low-saturation; the grass edge + // and paired rail reject unrelated dark neutral panels. + neutral += p[1] <= 80 && p[2] >= 60; + } + } + const double fraction = (double)neutral / ((x1 - x0) * sustained); + if (fraction < .72){ + supported = false; + } + total += fraction / 2; + } + if (supported){ + deck_y = y; + deck_support = total; + break; + } + } + if (deck_y < 0 || deck_y < h * .15){ + continue; + } + // The entrance is a grass-to-metal boundary. A road panel or sky + // behind an unrelated cyan object cannot stand in for this edge. + int grass = 0; + const int above_begin = std::max(0, deck_y - (int)(h * .05)); + const int above_end = deck_y - (int)(h * .02); + if (above_end <= above_begin){ + continue; + } + for (int row = above_begin; row < above_end; row++){ + for (int x = left; x < right; x++){ + const auto p = hsv.at(row, x); + grass += p[0] >= 25 && p[0] <= 85 && p[1] >= 60 && p[2] >= 30; + } + } + if ((double)grass / ((right - left) * (above_end - above_begin)) < .45){ + continue; + } + // Measure the inner rail boundary at deck height, rather than using + // the cliff wall or the rail cap's perspective-dependent bounding box. + double inner = 0, ink = 0; + size_t rows = 0; + const int measure_begin = std::max(deck_y + (int)(h * .015), (int)(h * .52)); + const int measure_end = std::min(h, measure_begin + (int)(h * .02)); + for (int y = measure_begin; y < measure_end; y++){ + for (int x = rail.x; x < std::min(w, rail.x + rail.width); x++){ + if (cyan.at(y, x)){ + int last = x, count = 0; + for (int edge = x; edge < std::min(w, rail.x + rail.width); edge++){ + if (cyan.at(y, edge)){ + last = edge; + ++count; + } + } + const int width = last - x + 1; + if (width >= w * .008 && width <= w * .055 && (double)count / width >= .80){ + inner += x; + ink += (double)count / width; + ++rows; + } + break; + } + } + } + if (rows < 3 || rows < (measure_end - measure_begin) * .75){ + continue; + } + inner /= rows; + OverworldMatchResult candidate; + candidate.located = true; + candidate.dx = inner / w - .55; + candidate.dy = (double)deck_y / h - .36; + // Park on the upper white platform beside the right rail. The user + // demonstrated this stationary pose in the 12:33:35 recording; the + // downhill section starts well beyond it. Measure the rail at wheel + // height, not at its distant cap or the offset cliff wall. + candidate.at_target = std::abs(candidate.dx) <= .012 && std::abs(candidate.dy) <= 1.0 / 54; + OverworldLandmarkResult rail_anchor; + rail_anchor.name = "Bridge right rail inner edge"; + rail_anchor.accepted = true; + rail_anchor.score = ink / rows; + rail_anchor.dx = candidate.dx; + rail_anchor.dy = candidate.dy; + rail_anchor.search = {.25, 0, .68, 1}; + rail_anchor.found = {(double)inner / w, (double)rail.y / h, (double)rail.width / w, (double)rail.height / h}; + OverworldLandmarkResult deck_anchor; + deck_anchor.name = "Bridge deck leading edge"; + deck_anchor.accepted = true; + deck_anchor.score = deck_support; + deck_anchor.dx = candidate.dx; + deck_anchor.dy = candidate.dy; + deck_anchor.search = {0, (double)begin / h, (double)right / w, (double)(end - begin) / h}; + deck_anchor.found = {(double)left / w, (double)deck_y / h, (double)(right - left) / w, (double)sustained / h}; + candidate.landmarks = {rail_anchor, deck_anchor}; + const double support = ink / rows + deck_support; + if (best.located && + (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ + ambiguous = true; + } + if (support > best_support){ + best_support = support; + best = std::move(candidate); + } + } + if (ambiguous){ + best.located = false; + best.at_target = false; + } + return best; +} + + +OverworldMatchResult match_galactic_hq_exterior(const ImageRGB32& frame){ + const std::vector configs{{"HQ sign", + "PokemonBDSP/Autostory/GalacticHQ/galactic_hq_exterior-0.png", + {660.0 / 1920, 615.0 / 1080, 118.0 / 1920, 124.0 / 1080}, + {0, 0.2, 0.65, 0.8}, + .93, + .20}, + {"HQ doorway lintel", + "PokemonBDSP/Autostory/GalacticHQ/galactic_hq_exterior-1.png", + {874.0 / 1920, 350.0 / 1080, 172.0 / 1920, 78.0 / 1080}, + {0.2, 0, 0.6, 0.7}, + .93, + .20}}; + return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); +} + +OverworldMatchResult match_eterna_center_departure(const ImageRGB32& frame){ + // Center-exit pose in the recorded Center->Mart and bicycle-shop route. + // Reuse the existing shadow/appearance-aware Center identification. + auto result = match_eterna_center_entrance(frame); + result.dy -= .070463; + result.at_target = result.located && !result.coarse && std::abs(result.dx) <= 1.0 / 128 && + std::abs(result.dy) <= POSITION_TOLERANCE_Y; + return result; +} + +OverworldMatchResult match_eterna_gym_statues(const ImageRGB32& frame){ + // Actual crops from screenshot-20261018-171535781091. Separate left/right + // search regions prevent one identical statue from satisfying both votes. + OverworldMatchResult best; + double best_score = -1; + if (!frame){ + return best; + } + // Normalized correlation also matches almost-black fade frames. Require + // visible statue regions before allowing those shapes to verify entry. + const cv::Mat gray = gray_image(frame); + const auto visible = [&](double x, double width){ + const cv::Rect box((int)(x * gray.cols), + (int)(458.0 / 1080 * gray.rows), + (int)(width * gray.cols), + (int)(350.0 / 1080 * gray.rows)); + return cv::mean(gray(box))[0] >= 60; + }; + const bool statues_visible = visible(448.0 / 1920, 146.0 / 1920) && visible(1322.0 / 1920, 154.0 / 1920); + for (double scale : {.95, 1.0, 1.05}){ + std::vector configs = {{"Left gym statue", + "PokemonBDSP/Autostory/EternaGym/LeftStatue.png", + {448.0 / 1920, 458.0 / 1080, 146.0 / 1920, 350.0 / 1080}, + {.12, .30, .27, .53}, + .93, + .20}, + {"Right gym statue", + "PokemonBDSP/Autostory/EternaGym/RightStatue.png", + {1322.0 / 1920, 458.0 / 1080, 154.0 / 1920, 350.0 / 1080}, + {.60, .30, .28, .53}, + .93, + .20}}; + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, .04, .06, true); + // Interior verification requires both statues near the expected entry + // positions, not merely anywhere in the room or during a fade. + candidate.at_target = candidate.at_target && statues_visible; + candidate.located = candidate.at_target; + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = candidate; + best_score = score; + } + } + return best; +} +OverworldMatchResult match_eterna_underground_house(const ImageRGB32& frame){ + // User's doorway pose: the Center ball and the same house sign, viewed + // from the east. The sign is optional when a strong unique ball confirms + // the doorway pose; cloud shadow on the sign must not block entry. + OverworldMatchResult best; + double best_score = -1; + bool best_primary_only = false; + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + std::vector configs{{"Center Pokeball", + "PokemonBDSP/Autostory/EternaUndergroundMan/HouseBall.png", + {332.0 / 1920, 224.0 / 1080, 112.0 / 1920, 104.0 / 1080}, + {0, 0, .65, .7}, + .93, + .20}, + {"House sign", + "PokemonBDSP/Autostory/EternaUndergroundMan/HouseSign.png", + {1240.0 / 1920, 396.0 / 1080, 110.0 / 1920, 106.0 / 1080}, + {.35, 0, .65, .85}, + .93, + .20}}; + for (auto& config : configs){ + const double w = config.target.width, h = config.target.height; + config.target.width *= scale; + config.target.height *= scale; + config.target.x += (w - config.target.width) / 2; + config.target.y += (h - config.target.height) / 2; + } + auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); + double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if (candidate.located){ + if (!best.located || best_primary_only || score > best_score){ + best = candidate; + best_score = score; + best_primary_only = false; + } + }else if (!best.located || best_primary_only){ + // The optional door-side sign can change perspective or disappear in + // cloud shadow. Preserve the high uniqueness margin on the primary ball. + const auto& ball = candidate.landmarks[0]; + if (ball.accepted && ball.score >= .95 && ball.margin >= .30 && std::abs(ball.dx) < .20 && + std::abs(ball.dy) < .20 && (!best.located || ball.score > best_score)){ + best = candidate; + best.located = true; + best.coarse = false; + best.dx = ball.dx; + best.dy = ball.dy; + best.at_target = std::abs(best.dx) <= 1.0 / 128 && std::abs(best.dy) <= POSITION_TOLERANCE_Y; + best_score = ball.score; + best_primary_only = true; + }else if (best.landmarks.empty()){ + best = candidate; + } + } + } + return best; +} +OverworldMatchResult match_eterna_underground_man(const ImageRGB32& frame){ + // Reuse fixed furniture, excluding people and the animated TV picture. + // The right-window search excludes the identical window above the stool. + const std::vector configs{{"Stool", + "PokemonBDSP/Autostory/EternaUndergroundMan/Stool.png", + {380.0 / 1920, 610.0 / 1080, 86.0 / 1920, 70.0 / 1080}, + {0, .3, .65, .5}, + .93, + .20}, + {"Right tree top", + "PokemonBDSP/Autostory/EternaUndergroundMan/TreeTop.png", + {1372.0 / 1920, 320.0 / 1080, 102.0 / 1920, 96.0 / 1080}, + {.5, 0, .4, .7}, + .93, + .20}, + {"Wall sign", + "PokemonBDSP/Autostory/EternaUndergroundMan/WallSign.png", + {642.0 / 1920, 354.0 / 1080, 114.0 / 1920, 70.0 / 1080}, + {.2, 0, .5, .6}, + .93, + .20}, + {"Window beside TV", + "PokemonBDSP/Autostory/EternaUndergroundMan/RightWindow.png", + {906.0 / 1920, 260.0 / 1080, 162.0 / 1920, 160.0 / 1080}, + {.35, 0, .55, .55}, + .93, + .20}}; + auto result = match_landmarks(frame, configs, 4, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (!result.located){ + // Three agreeing furniture anchors identify the room during entry and + // guide acquisition when a fourth is clipped. Never authorize talking. + auto coarse = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); + if (coarse.located){ + coarse.coarse = true; + coarse.at_target = false; + return coarse; + } + } + return result; +} +OverworldMatchResult match_jubilife_street_approach(const ImageRGB32& frame){ + // The lighting-run pose was 1/108 screen height south of the original + // screenshot. Targets preserve the original destination height. + // Capture-derived references cover lamp, daylight, and moving tree shadows. + return match_lighting_pair(frame, + {{"BuildingCorner", + "PokemonBDSP/Autostory/JubilifeStreet/BuildingCornerHour3Frame4.png", + {0.588541666667, 0.351851851852, 0.088541666667, 0.259259259259}, + {0, 0, 1, 1}, + .93, + .08}, + {"BuildingCorner", + "PokemonBDSP/Autostory/JubilifeStreet/BuildingCornerHour14Frame2.png", + {0.588541666667, 0.351851851852, 0.088541666667, 0.259259259259}, + {0, 0, 1, 1}, + .93, + .08}, + {"BuildingCorner", + "PokemonBDSP/Autostory/JubilifeStreet/BuildingCornerHour17Frame4.png", + {0.588541666667, 0.351851851852, 0.088541666667, 0.259259259259}, + {0, 0, 1, 1}, + .93, + .08}, + {"BuildingBase", + "PokemonBDSP/Autostory/JubilifeStreet/BuildingBaseHour15Frame2.png", + {0.812500000000, 0.388888888889, 0.078125000000, 0.203703703704}, + {0, 0, 1, 1}, + .93, + .08}, + {"BuildingBase", + "PokemonBDSP/Autostory/JubilifeStreet/BuildingBaseHour17Frame5.png", + {0.812500000000, 0.388888888889, 0.078125000000, 0.203703703704}, + {0, 0, 1, 1}, + .93, + .08}, + {"BuildingBase", + "PokemonBDSP/Autostory/JubilifeStreet/BuildingBaseHour20Frame5.png", + {0.812500000000, 0.388888888889, 0.078125000000, 0.203703703704}, + {0, 0, 1, 1}, + .93, + .08}}, + "BuildingCorner", + "BuildingBase", + POSITION_TOLERANCE_X, + true); +} +OverworldMatchResult match_jubilife_plaza_approach(const ImageRGB32& frame){ + // Capture-derived references cover lamp, daylight, and moving tree shadows. + auto result = match_lighting_pair(frame, + { + {"RoofEdge", + "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour2Frame2.png", + {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, + {0, 0, 1, 1}, + .93, + .08}, + {"RoofEdge", + "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour11Frame1.png", + {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, + {0, 0, 1, 1}, + .93, + .08}, + {"RoofEdge", + "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour11Frame2.png", + {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, + {0, 0, 1, 1}, + .93, + .08}, + {"RoofEdge", + "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour11Frame5.png", + {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, + {0, 0, 1, 1}, + .93, + .08}, + {"RoofEdge", + "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour16Frame5.png", + {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, + {0, 0, 1, 1}, + .93, + .08}, + {"RoofEdge", + "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour23Frame2.png", + {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, + {0, 0, 1, 1}, + .93, + .08}, + {"Fence", + "PokemonBDSP/Autostory/JubilifePlaza/FenceHour3Frame1.png", + {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, + {0, 0, 1, 1}, + .93, + .08}, + {"Fence", + "PokemonBDSP/Autostory/JubilifePlaza/FenceHour5Frame4.png", + {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, + {0, 0, 1, 1}, + .93, + .08}, + {"Fence", + "PokemonBDSP/Autostory/JubilifePlaza/FenceHour11Frame3.png", + {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, + {0, 0, 1, 1}, + .93, + .08}, + {"Fence", + "PokemonBDSP/Autostory/JubilifePlaza/FenceHour14Frame1.png", + {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, + {0, 0, 1, 1}, + .93, + .08}, + {"Fence", + "PokemonBDSP/Autostory/JubilifePlaza/FenceHour19Frame1.png", + {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, + {0, 0, 1, 1}, + .93, + .08}, + {"Fence", + "PokemonBDSP/Autostory/JubilifePlaza/FenceHour21Frame4.png", + {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, + {0, 0, 1, 1}, + .93, + .08} // Small screen-relative quantization allowance avoids bouncing across the + // plaza target when one movement step spans both sides of the tolerance. + }, + "RoofEdge", + "Fence", + POSITION_TOLERANCE_X, + false, + 1.0 / 32); + if (!result.located && result.landmarks.size() == 2){ + const auto& fence = result.landmarks[1]; + // A moving roof shadow must not trigger a blind lateral sweep when the + // distinctive fence still gives a strong, nearby position estimate. + // Fence-only guidance can reacquire the roof; it cannot confirm arrival. + if (fence.accepted && fence.score >= .95 && fence.margin >= .30 && std::abs(fence.dx) <= .20 && + std::abs(fence.dy) <= .20){ + result.located = true; + result.coarse = true; + result.at_target = false; + result.dx = fence.dx; + result.dy = fence.dy; + } + } + return result; +} +OverworldMatchResult match_route204_sign(const ImageRGB32& frame){ + // One vote per physical landmark. Lighting variants avoid treating the + // moving tree shadow over the fence as a missing route destination. + const ImageFloatBox sign_target{0.468750000000, 0.376851851852, 0.072916666667, 0.052777777778}; + const ImageFloatBox fence_target{0.598958333333, 0.421296296296, 0.041666666667, 0.152777777778}; + std::vector variants{ + {"Route sign", "PokemonBDSP/Autostory/Route204Sign/Sign.png", sign_target, {0, 0, 1, 1}, .93, .10}, + {"Fence corner", "PokemonBDSP/Autostory/Route204Sign/FenceCorner.png", fence_target, {0, 0, 1, 1}, .93, .10}, + {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour0Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, + {"Fence corner", + "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour0Frame3.png", + fence_target, + {0, 0, 1, 1}, + .93, + .10}, + {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour8Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, + {"Fence corner", + "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour8Frame3.png", + fence_target, + {0, 0, 1, 1}, + .93, + .10}, + {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour13Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, + {"Fence corner", + "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour13Frame3.png", + fence_target, + {0, 0, 1, 1}, + .93, + .10}, + {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour17Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, + {"Fence corner", + "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour17Frame3.png", + fence_target, + {0, 0, 1, 1}, + .93, + .10}, + {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour2Frame1.png", sign_target, {0, 0, 1, 1}, .93, .10}, + {"Fence corner", + "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour2Frame1.png", + fence_target, + {0, 0, 1, 1}, + .93, + .10}, + {"Fence corner", + "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour10Frame3.png", + fence_target, + {0, 0, 1, 1}, + .93, + .10}, + {"Fence corner", + "PokemonBDSP/Autostory/Route204Sign/FenceCornerRecordedCloud.png", + fence_target, + {0, 0, 1, 1}, + .93, + .10}, + }; + auto result = match_lighting_pair(frame, variants, "Route sign", "Fence corner"); + if (!result.located && result.landmarks.size() == 2){ + const auto& sign = result.landmarks[0]; + // A distinctive high-confidence sign can guide bounded acquisition. + // It cannot confirm the endpoint without the fence agreeing as well. + if (sign.accepted && sign.score >= .95 && sign.margin >= .30 && std::abs(sign.dx) <= .20 && + std::abs(sign.dy) <= .25){ + result.located = true; + result.coarse = true; + result.at_target = false; + result.dx = sign.dx; + result.dy = sign.dy; + } + } + return result; +} +static OverworldMatchResult match_route205_marker(const ImageRGB32& frame, double cx, double cy){ + OverworldMatchResult best; + // Native sign faces at the two route positions. The post, moving NPCs, + // grass and water are excluded from the acquisition references. + for (int variant : {0, 1, 2, 3, 4, 5, 6}){ + for (double scale : {.9, 1.0, 1.1, 1.2}){ + const double w = (variant == 6 ? 188.0 + : variant == 5 ? 184.0 + : variant == 4 ? 220.0 + : variant == 3 ? 211.0 + : variant == 2 ? 208.0 + : variant == 1 ? 206.0 + : 205.0) / + 1920 * scale; + const double h = (variant == 6 ? 74.0 : variant >= 3 ? 72.0 : variant ? 80.0 : 82.0) / 1080 * scale; + auto candidate = match_landmarks(frame, + {{"Route 205 marker", + variant == 6 ? "PokemonBDSP/Autostory/Route205/BridgeSignMidCloud.png" + : variant == 5 ? "PokemonBDSP/Autostory/Route205/BridgeSignCloud.png" + : variant == 4 ? "PokemonBDSP/Autostory/Route205/BridgeSignLow.png" + : variant == 3 ? "PokemonBDSP/Autostory/Route205/BridgeSignLive.png" + : variant == 2 ? "PokemonBDSP/Autostory/Route205/NorthSignRecorded.png" + : variant == 1 ? "PokemonBDSP/Autostory/Route205/NorthSign.png" + : "PokemonBDSP/Autostory/Route205/BridgeSign.png", + {cx - w / 2, cy - h / 2, w, h}, + {0, 0, 1, 1}, + .93, + .20}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if (best.landmarks.empty() || (candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + } + return best; +} +OverworldMatchResult match_route205_zackary(const ImageRGB32& frame){ + OverworldMatchResult best; + double best_score = -1; + for (int view : {0, 1, 2, 3}){ + for (double scale : {.9, 1.0, 1.1}){ + const double sw = 91.0 / 1920 * scale, sh = 65.0 / 1080 * scale; + // Include the fixed cliff edge beside both pipes: the repeated blue caps + // alone produce a second strong peak in cloud shadow. + const double cw = 300.0 / 1920 * scale, ch = 300.0 / 1080 * scale; + auto candidate = match_landmarks(frame, + {{"Zackary bridge sign", + view == 2 ? "PokemonBDSP/Autostory/Route205/PatrolSignCloud.png" + : view >= 1 ? "PokemonBDSP/Autostory/Route205/PatrolSignLive.png" + : "PokemonBDSP/Autostory/Route205/PatrolSign.png", + {959.5 / 1920 - sw / 2, 67.5 / 1080 - sh / 2, sw, sh}, + {0, 0, 1, 1}, + .93, + .20}, + {"Zackary twin chimneys", + view == 3 ? "PokemonBDSP/Autostory/Route205/PatrolChimneysPartialCloud.png" + : view == 2 ? "PokemonBDSP/Autostory/Route205/PatrolChimneysCloud.png" + : view == 1 ? "PokemonBDSP/Autostory/Route205/PatrolChimneysLive.png" + : "PokemonBDSP/Autostory/Route205/PatrolChimneys.png", + {1210.0 / 1920 - cw / 2, 290.0 / 1080 - ch / 2, cw, ch}, + {0, 0, 1, 1}, + .93, + .20}}, + 2, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if (best.located && candidate.located && + (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ + best.located = false; + best.at_target = false; + return best; + } + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + } + return best; +} +OverworldMatchResult match_route205_kelsey(const ImageRGB32& frame){ + OverworldMatchResult best; + // This item is retained on the route. Restrict acquisition to the left of + // the player, excluding the red cap and the northern bridge's item ball. + for (double scale : {.9, 1.0, 1.1}){ + const double w = 56.0 / 1920 * scale, h = 53.0 / 1080 * scale; + auto candidate = match_landmarks(frame, + {{"Kelsey west item ball", + "PokemonBDSP/Autostory/Route205/KelseyItem.png", + {280.0 / 1920 - w / 2, 574.5 / 1080 - h / 2, w, h}, + {0, .2, .48, .65}, + .93, + .20}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if (best.landmarks.empty() || (candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = std::move(candidate); + } + } + return best; +} +OverworldMatchResult match_route205_bank_tree(const ImageRGB32& frame){ + // Use the tree crown above the player's reach, plus the stationary child. + // Recorded cloud-edge views retain both anchor gates and agreement. + // Trunk crops can include the player's cap when he stands under the tree. + OverworldMatchResult best; + double best_score = -1; + for (int view : {0, 1, 2, 3, 4, 5, 6, 7, 8}){ + for (double scale : {.9, 1.0, 1.1}){ + const double tw = 140.0 / 1920 * scale, th = 130.0 / 1080 * scale; + const double nw = 120.0 / 1920 * scale, nh = 170.0 / 1080 * scale; + auto candidate = match_landmarks(frame, + {{"Bank tree crown", + view == 8 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownWest.png" + : view == 7 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud333.png" + : view == 6 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud76.png" + : view == 5 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud336.png" + : view == 4 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud328.png" + : view == 3 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud80.png" + : view == 2 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud72.png" + : view == 1 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownLive.png" + : "PokemonBDSP/Autostory/Route205/BankTreeCrown.png", + {770.0 / 1920 - tw / 2, 235.0 / 1080 - th / 2, tw, th}, + {0, 0, 1, 1}, + .93, + .20}, + {"Bank blue-hat child", + view == 8 ? "PokemonBDSP/Autostory/Route205/BankBlueHatWest.png" + : view == 7 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud333.png" + : view == 6 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud76.png" + : view == 5 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud336.png" + : view == 4 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud328.png" + : view == 3 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud80.png" + : view == 2 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud72.png" + : view == 1 ? "PokemonBDSP/Autostory/Route205/BankBlueHatLive.png" + : "PokemonBDSP/Autostory/Route205/BankBlueHat.png", + {700.0 / 1920 - nw / 2, 525.0 / 1080 - nh / 2, nw, nh}, + {0, 0, 1, 1}, + .93, + .20}}, + 2, + 1.0 / 96, + POSITION_TOLERANCE_Y, + true); + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if (best.located && candidate.located && + (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ + best.located = false; + best.at_target = false; + return best; + } + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + } + return best; +} +OverworldMatchResult match_route205_bridge(const ImageRGB32& frame){ + return match_route205_marker(frame, 722.5 / 1920, 465.0 / 1080); +} +OverworldMatchResult match_route205_forest_entrance(const ImageRGB32& frame){ + return match_route205_marker(frame, 1245.0 / 1920, 508.0 / 1080); +} +OverworldMatchResult match_windworks_interior_grunt(const ImageRGB32& frame){ + OverworldMatchResult result; + cv::Mat pixels((int)frame.height(), (int)frame.width(), CV_8UC4, (void*)frame.data(), frame.bytes_per_row()); + cv::Mat small, bgr, hsv; + cv::resize(pixels, small, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + cv::cvtColor(small, bgr, cv::COLOR_BGRA2BGR); + cv::cvtColor(bgr, hsv, cv::COLOR_BGR2HSV); + // Grey indoor tiles distinguish this scene from the outdoor grunt, battle + // screens and fades. Pale turquoise partitions have too little saturation + // to qualify as the grunt's hair. + cv::Mat floor; + cv::inRange(hsv, cv::Scalar(0, 0, 100), cv::Scalar(179, 65, 255), floor); + const cv::Rect room(288, 81, 624, 405); + if ((double)cv::countNonZero(floor(room)) / room.area() < .30){ + return result; + } + auto head = [&](cv::Scalar low, cv::Scalar high, bool player, cv::Point2d& center){ + cv::Mat mask, labels, stats, centroids; + cv::inRange(hsv, low, high, mask); + const int count = cv::connectedComponentsWithStats(mask, labels, stats, centroids); + int best = -1, best_area = 0, runner_area = 0; + for (int i = 1; i < count; i++){ + int area = stats.at(i, cv::CC_STAT_AREA); + int w = stats.at(i, cv::CC_STAT_WIDTH), h = stats.at(i, cv::CC_STAT_HEIGHT); + double x = centroids.at(i, 0), y = centroids.at(i, 1); + if (area < 80 || area > 4000 || w < 12 || w > 110 || h < 8 || h > 90 || (double)area / (w * h) < .35 || + x < 288 || x > (player ? 672 : 864) || y < (player ? 135 : 15) || y > 459){ + continue; + } + if (area > best_area){ + runner_area = best_area; + best_area = area; + best = i; + }else{ + runner_area = std::max(runner_area, area); + } + } + if (best < 0 || runner_area > best_area * .70){ + return false; + } + center = {centroids.at(best, 0), centroids.at(best, 1)}; + return true; + }; + cv::Point2d player, grunt; + if (!head({0, 120, 120}, {22, 255, 255}, true, player) || !head({75, 100, 120}, {105, 255, 255}, false, grunt)){ + return result; + } + if (std::abs(player.x - grunt.x) > 288 || std::abs(player.y - grunt.y) > 350){ + return result; + } + result.located = true; + result.dx = (grunt.x - player.x) / 960; + result.dy = (grunt.y - player.y) / 540; + return result; +} +OverworldMatchResult match_windworks_sign_approach(const ImageRGB32& frame){ + // Sign beside the open lane west of the doorway turbine. Keep the player + // east of the sign/grass before the blind building-wall approach. + OverworldMatchResult best; + // The sign grows as it approaches the bottom of the camera view. Scale + // acquisition templates around the same target center, keeping all gates. + for (int view : {0, 1, 2}){ + for (double scale : {1.0, 1.1, 1.2, 1.3}){ + const double w = (view == 2 ? 124.0 : view == 1 ? 144.0 : 121.0) / 1920 * scale; + const double h = (view == 1 ? 140.0 : 122.0) / 1080 * scale; + auto candidate = match_landmarks(frame, + {{"Windworks approach sign", + view == 2 ? "PokemonBDSP/Autostory/WindworksApproach/SignNight.png" + : view == 1 ? "PokemonBDSP/Autostory/WindworksApproach/SignLow.png" + : "PokemonBDSP/Autostory/WindworksApproach/Sign.png", + {836.5 / 1920 - w / 2, 515.0 / 1080 - h / 2, w, h}, + {0, 0, 1, 1}, + .93, + .20}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if (best.landmarks.empty() || (candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + } + // Face-only references cover the high/mid camera perspectives without + // relying on the moving grass that can cover the sign's lower border. + for (bool high_view : {false, true}){ + for (double scale : {.9, 1.0, 1.1}){ + const double w = (high_view ? 86.0 : 106.0) / 1920 * scale; + const double h = (high_view ? 72.0 : 94.0) / 1080 * scale; + auto candidate = match_landmarks(frame, + {{"Windworks approach sign face", + high_view ? "PokemonBDSP/Autostory/WindworksApproach/SignHighFace.png" + : "PokemonBDSP/Autostory/WindworksApproach/SignMidFace.png", + {838.0 / 1920 - w / 2, 516.0 / 1080 - h / 2, w, h}, + {0, 0, 1, 1}, + .93, + .20}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if ((candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + } + return best; +} +OverworldMatchResult match_route204_floaroma_turn(const ImageRGB32& frame){ + OverworldMatchResult best; + // The northern 204 marker is wider in this camera view. Match its face, + // placing it above the player so the westward turn clears its post and + // the flower-bed fence. Align horizontally with the sign before the + // subsequent westward route, rather than staying in the eastern lane. + for (double scale_x : {1.0, 1.05, 1.10, 1.15, 1.20}){ + for (double scale_y : {.90, 1.0, 1.10}){ + const double w = 140.0 / 1920 * scale_x, h = 57.0 / 1080 * scale_y; + auto candidate = match_landmarks(frame, + {{"Northern Route 204 sign", + "PokemonBDSP/Autostory/Route204Sign/SignHour13Frame3.png", + {.5 - w / 2, 420.0 / 1080 - h / 2, w, h}, + {0, 0, 1, 1}, + .93, + .20}}, + 1, + POSITION_TOLERANCE_X, + POSITION_TOLERANCE_Y, + true); + if (best.landmarks.empty() || (candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + } + return best; +} +OverworldMatchResult match_route204_north_approach(const ImageRGB32& frame){ + auto result = match_route204_sign(frame); + // The pre-battle reference stands below the sign. After Tyler, place + // the player one body-width west of it before starting north. + constexpr double west_offset = .04; + for (auto& anchor : result.landmarks){ + anchor.dx -= west_offset; + } + result.dx -= west_offset; + if (!result.located && !result.landmarks.empty()){ + const auto& sign = result.landmarks[0]; + if (sign.accepted && sign.score >= .93 && sign.margin >= .30 && std::abs(sign.dx) <= .45 && + std::abs(sign.dy) <= .25){ + result.located = true; + result.coarse = true; + result.dx = sign.dx; + result.dy = sign.dy; + } + } + result.at_target = result.located && !result.coarse; + for (const auto& anchor : result.landmarks){ + if (!anchor.accepted || std::abs(anchor.dx) > 1.0 / 128 || std::abs(anchor.dy) > POSITION_TOLERANCE_Y){ + result.at_target = false; + } + } + return result; +} +OverworldMatchResult match_floaroma_house_entrance(const ImageRGB32& frame){ + // Fixed facade details above/aside the player avoid door/player occlusion. + // Both must agree on the pose recorded just outside the central house. + return match_landmarks(frame, + {{"House window", + "PokemonBDSP/Autostory/FloaromaHouse/Window.png", + {1060.0 / 1920, 370.0 / 1080, 128.0 / 1920, 106.0 / 1080}, + {0, 0, 1, 1}, + .93, + .10}, + {"House gable", + "PokemonBDSP/Autostory/FloaromaHouse/Gable.png", + {796.0 / 1920, 196.0 / 1080, 460.0 / 1920, 154.0 / 1080}, + {0, 0, 1, 1}, + .93, + .10}}, + 2, + 1.0 / 64, + 1.0 / 54, + true); +} +OverworldMatchResult match_floaroma_center_entrance(const ImageRGB32& frame){ + OverworldMatchResult best; + // Perspective changes the emblem size as we approach the doorway gap. + for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ + const double w = 105.0 / 1920 * scale, h = 86.0 / 1080 * scale; + auto candidate = match_landmarks(frame, + {{"Center emblem", + "PokemonBDSP/Autostory/FloaromaCenter/Emblem.png", + {.5 - w / 2, 135.0 / 1080 - h / 2, w, h}, + {0, 0, 1, 1}, + .93, + .20}}, + 1, + 1.0 / 64, + 1.0 / 36, + true); + if (best.landmarks.empty() || (candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + // At the west wall the camera makes the emblem nearly circular, unlike + // the vertically compressed doorway reference. Search that recorded + // perspective without lowering the score or uniqueness requirements. + if (!best.located){ + for (double scale_x : {1.0, 1.05, 1.10, 1.15, 1.20}){ + for (double scale_y : {1.15, 1.25, 1.35, 1.45}){ + const double w = 105.0 / 1920 * scale_x, h = 86.0 / 1080 * scale_y; + auto candidate = match_landmarks(frame, + {{"Center emblem west perspective", + "PokemonBDSP/Autostory/FloaromaCenter/Emblem.png", + {.5 - w / 2, 135.0 / 1080 - h / 2, w, h}, + {0, 0, 1, 1}, + .93, + .20}}, + 1, + 1.0 / 64, + 1.0 / 36, + true); + candidate.coarse = true; + candidate.at_target = false; + if ((candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + } + } + // A lower-face reference excludes the tester's burned-in overlay line + // and covers the doorway-facing perspective, retaining score/margin gates. + auto lower = match_landmarks(frame, + {{"Center emblem lower face", + "PokemonBDSP/Autostory/FloaromaCenter/EmblemLower.png", + {.5 - 52.0 / 1920, 114.0 / 1080, 104.0 / 1920, 65.0 / 1080}, + {0, 0, 1, 1}, + .93, + .20}}, + 1, + 1.0 / 64, + 1.0 / 36, + true); + if ((lower.located && (!best.located || best.coarse)) || + (lower.located == best.located && lower.landmarks[0].score > best.landmarks[0].score)){ + best = lower; + } + return best; +} +OverworldMatchResult match_route204_north_stairs(const ImageRGB32& frame){ + OverworldMatchResult best; + // Fixed rails/steps, with sunny and cloud-covered perspectives. + for (const char* path : {"PokemonBDSP/Autostory/Route204NorthStairs/Stairs.png", + "PokemonBDSP/Autostory/Route204NorthStairs/StairsCloud.png"}){ + // Camera pitch changes height and width by different amounts. + for (double scale_x : {.90, 1.0, 1.10}){ + for (double scale_y : {.80, .90, 1.0, 1.10}){ + const double w = 298.0 / 1920 * scale_x, h = 282.0 / 1080 * scale_y; + auto candidate = match_landmarks(frame, + {{"North route stairs", path, {.5 - w / 2, 650.0 / 1080, w, h}, {0, 0, 1, 1}, .93, .10}}, + 1, + 1.0 / 64, + 1.0 / 54, + true); + if (best.landmarks.empty() || (candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + } + } + if (!best.located){ + // The lower steps distort as the camera moves north. The rail tops, + // grass edge and first steps retain a stable physical upper edge. + for (const auto& config : + std::vector{{"North route stairs top", + "PokemonBDSP/Autostory/Route204NorthStairs/Top.png", + {.5 - 149.0 / 1920, 640.0 / 1080, 298.0 / 1920, 70.0 / 1080}, + {0, 0, 1, 1}, + .93, + .10}, + {"North route stairs top", + "PokemonBDSP/Autostory/Route204NorthStairs/TopRaised.png", + {.5 - 157.0 / 1920, 640.0 / 1080, 314.0 / 1920, 70.0 / 1080}, + {0, 0, 1, 1}, + .93, + .10}}){ + for (double scale_x : {.95, 1.0, 1.05}){ + for (double scale_y : {.90, 1.0, 1.10}){ + auto variant = config; + variant.target.width *= scale_x; + variant.target.height *= scale_y; + variant.target.x = .5 - variant.target.width / 2; + auto candidate = match_landmarks(frame, {variant}, 1, 1.0 / 64, 1.0 / 54, true); + if ((candidate.located && !best.located) || + (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ + best = candidate; + } + } + } + } + } + return best; +} +OverworldMatchResult match_teighler_alcove_corner(const ImageRGB32& frame){ + auto result = match_route204_north_stairs(frame); + // Reuse the fixed stairs only as an anchor. The destination is the + // bottom-left of Teighler's alcove, east of the stairs, not their center. + // Geometry comes from the recorded 1920x1080 corner reference. + constexpr double shift_x = 393.0 / 1920, shift_y = 182.0 / 1080; + result.dx += shift_x; + result.dy += shift_y; + for (auto& anchor : result.landmarks){ + anchor.dx += shift_x; + anchor.dy += shift_y; + } + result.at_target = result.located && std::abs(result.dx) <= 1.0 / 64 && std::abs(result.dy) <= 1.0 / 54; + return result; +} +OverworldMatchResult match_ravaged_path_south_entrance(const ImageRGB32& frame){ + const ImageFloatBox mouth{860.0 / 1920, 220.0 / 1080, 200.0 / 1920, 155.0 / 1080}; + const ImageFloatBox sign{810.0 / 1920, 380.0 / 1080, 100.0 / 1920, 110.0 / 1080}; + std::vector variants; + // A different approach height changes perspective. Compare a small range + // of sizes, keeping each physical anchor's reference center fixed. + for (double scale : {.90, 1.0, 1.10}){ + for (const auto& config : + std::vector{{"Cave mouth", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/Mouth.png", + mouth, + {0, 0, 1, 1}, + .93, + .10}, + {"Entrance sign", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/Sign.png", + sign, + {0, 0, 1, 1}, + .93, + .10}, + {"Cave mouth", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/MouthShadow.png", + mouth, + {0, 0, 1, 1}, + .93, + .10}, + {"Entrance sign", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignShadow.png", + sign, + {0, 0, 1, 1}, + .93, + .10}, + {"Entrance sign", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignPartialShadow.png", + sign, + {0, 0, 1, 1}, + .93, + .10}, + {"Entrance sign", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignFace.png", + {828.0 / 1920, 400.0 / 1080, 64.0 / 1920, 62.0 / 1080}, + {0, 0, 1, 1}, + .93, + .10}, + {"Entrance sign", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignFaceLowApproach.png", + {828.0 / 1920, 400.0 / 1080, 64.0 / 1920, 62.0 / 1080}, + {0, 0, 1, 1}, + .93, + .10}, + {"Entrance sign", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignFaceCloud.png", + {828.0 / 1920, 400.0 / 1080, 64.0 / 1920, 62.0 / 1080}, + {0, 0, 1, 1}, + .93, + .10}}){ + auto variant = config; + variant.target.width *= scale; + variant.target.height *= scale; + variant.target.x += (config.target.width - variant.target.width) / 2; + variant.target.y += (config.target.height - variant.target.height) / 2; + variants.push_back(variant); + } + } + auto result = match_lighting_pair(frame, variants, "Cave mouth", "Entrance sign", 1.0 / 64, true); + if (!result.located && result.landmarks.size() == 2){ + for (const auto& landmark : result.landmarks){ + if (landmark.accepted && landmark.score >= .95 && landmark.margin >= .20 && std::abs(landmark.dx) <= .35 && + std::abs(landmark.dy) <= .40){ + result.located = true; + result.coarse = true; + result.at_target = false; + result.dx = landmark.dx; + result.dy = landmark.dy; + break; + } + } + } + // The moving blue-hatted boy is deliberately absent from the references. + // The lass is optional evidence for the southern movement boundary, never + // a replacement for the fixed mouth/sign arrival anchors. + auto boundary = match_landmarks(frame, + {{"Lass south boundary", + "PokemonBDSP/Autostory/RavagedPathSouthEntrance/Lass.png", + {910.0 / 1920, 788.0 / 1080, 100.0 / 1920, 142.0 / 1080}, + {0, 0, 1, 1}, + .88, + .08}}, + 1, + 1.0 / 64, + 1.0 / 36, + true); + if (!boundary.landmarks.empty()){ + result.landmarks.push_back(boundary.landmarks[0]); + } + return result; +} +OverworldMatchResult match_route215_bridge_renewal(const ImageRGB32& frame){ + // Restore the successful west bridge approach after variable expiry movement. + if (!frame){ + return {}; + } + cv::Mat gray, smoothed; + cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); + ImageRGB32 filtered(960, 540); + cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); + cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); + auto result = match_lighting_pair(filtered, + {{"Bridge west edge", + "PokemonBDSP/Autostory/Route215Renewal/BridgeEdge.png", + {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, + {.15, .25, .85, .75}, + .90, + .10}, + {"Landing cliff corner", + "PokemonBDSP/Autostory/Route215Renewal/BridgeCliff.png", + {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, + {.10, .25, .85, .75}, + .90, + .10}, + {"Bridge west edge", + "PokemonBDSP/Autostory/Route215Renewal/BridgeFarEdge.png", + {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, + {.15, .25, .85, .75}, + .90, + .10}, + {"Landing cliff corner", + "PokemonBDSP/Autostory/Route215Renewal/BridgeFarCliff.png", + {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, + {.10, .25, .85, .75}, + .90, + .10}, + {"Bridge west edge", + "PokemonBDSP/Autostory/Route215Renewal/BridgeNightEdge.png", + {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, + {.15, .25, .85, .75}, + .90, + .10}, + {"Landing cliff corner", + "PokemonBDSP/Autostory/Route215Renewal/BridgeNightCliff.png", + {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, + {.10, .25, .85, .75}, + .90, + .10}, + {"Bridge west edge", + "PokemonBDSP/Autostory/Route215Renewal/BridgeNightFarEdge.png", + {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, + {.15, .25, .85, .75}, + .90, + .10}, + {"Landing cliff corner", + "PokemonBDSP/Autostory/Route215Renewal/BridgeNightFarCliff.png", + {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, + {.10, .25, .85, .75}, + .90, + .10}}, + "Bridge west edge", + "Landing cliff corner", + 1.0 / 128); + result.at_target = result.located && std::abs(result.dx) <= 1.0 / 128 && std::abs(result.dy) <= 1.0 / 108; + return result; +} +OverworldMatchResult match_route215_ace_renewal(const ImageRGB32& frame){ + // The road bend and fence corner exclude the patrolling trainers. + if (!frame){ + return {}; + } + cv::Mat gray, smoothed; + cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); + ImageRGB32 filtered(960, 540); + cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); + cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); + auto result = match_landmarks(filtered, + {{"Road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceRoad.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {.10, .15, .85, .80}, + .92, + .08}, + {"Fence corner", + "PokemonBDSP/Autostory/Route215Renewal/AceFence.png", + {165.0 / 960, 389.0 / 540, 110.0 / 960, 78.0 / 540}, + {.05, .25, .90, .75}, + .90, + .10, + LandmarkFeatures::LOCAL_CONTRAST}}, + 2, + 1.0 / 128, + 1.0 / 108, + true); + if (result.located){ + return result; + } + // Expiry may finish east of the fence's visible range. Two separate road + // bends establish a horizontal recovery direction along this clear row. + // Perspective changes their spacing, so this pair cannot confirm arrival. + // Suppress thin rain streaks and puddle rings in the broader road shapes; + // retain the original input and confidence gates for the fine fence pair. + cv::medianBlur(gray, smoothed, 5); + cv::GaussianBlur(smoothed, smoothed, cv::Size(0, 0), .8); + cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); + auto approach = match_lighting_pair(filtered, + {{"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadGoal.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadNear.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadFar.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWestFar.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWestNear.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWest.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWestNearRain.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Upper road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadFarRain.png", + {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, + {0, .25, 1, .30}, + .92, + .08}, + {"Lower road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireLowerRoadEast.png", + {500.0 / 960, 420.0 / 540, 80.0 / 960, 70.0 / 540}, + {0, .70, 1, .30}, + .93, + .10}, + {"Lower road bend", + "PokemonBDSP/Autostory/Route215Renewal/AceAcquireLowerRoad.png", + {500.0 / 960, 420.0 / 540, 80.0 / 960, 70.0 / 540}, + {0, .70, 1, .30}, + .93, + .10}}, + "Upper road bend", + "Lower road bend", + 1.0 / 128); + // At the eastern extreme, the original bends leave the screen. The two + // corners of the same grass bed remain visible above the clear road. + auto east = match_lighting_pair(filtered, + {{"Grass west corner", + "PokemonBDSP/Autostory/Route215Renewal/AceGrassWest.png", + {665.0 / 960, 145.0 / 540, 110.0 / 960, 75.0 / 540}, + {0, .20, 1, .40}, + .92, + .10}, + {"Grass east corner", + "PokemonBDSP/Autostory/Route215Renewal/AceGrassEast.png", + {890.0 / 960, 175.0 / 540, 65.0 / 960, 62.0 / 540}, + {0, .20, 1, .40}, + .92, + .10}, + {"Grass west corner", + "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarWest.png", + {665.0 / 960, 145.0 / 540, 110.0 / 960, 75.0 / 540}, + {0, .20, 1, .40}, + .92, + .10}, + {"Grass east corner", + "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarEast.png", + {890.0 / 960, 175.0 / 540, 65.0 / 960, 62.0 / 540}, + {0, .20, 1, .40}, + .92, + .10}, + {"Grass east corner", + "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarEastShoulder.png", + {850.0 / 960, 145.0 / 540, 105.0 / 960, 80.0 / 540}, + {0, .20, 1, .40}, + .92, + .10}}, + "Grass west corner", + "Grass east corner", + 1.0 / 128); + const auto& west = east.landmarks[0]; + const auto& right = east.landmarks[1]; + const double east_nearest = std::min(std::abs(west.dx), std::abs(right.dx)); + east.located = west.accepted && right.accepted && std::abs(west.dy - right.dy) <= AGREEMENT_Y && + west.dx < -1.0 / 12 && right.dx < -1.0 / 12 && east_nearest > 2 * std::abs(west.dx - right.dx) && + east.dy >= -1.0 / 27 && east.dy <= 1.0 / 9; + if (east.located){ + east.coarse = true; + east.at_target = false; + return east; + } + // From the eastern end, the fixed route sign remains unique while the + // road bends are off-screen. Its virtual goal lies beyond the right edge; + // this pair guides acquisition only and can never confirm arrival. + auto far_east = match_lighting_pair(filtered, + {{"Grass west corner", + "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarWest.png", + {665.0 / 960, 145.0 / 540, 110.0 / 960, 75.0 / 540}, + {0, .20, 1, .40}, + .92, + .10}, + {"Route 215 sign", + "PokemonBDSP/Autostory/Route215Renewal/AceRoute215Sign.png", + {1128.0 / 960, 359.0 / 540, 90.0 / 960, 45.0 / 540}, + {.50, .40, .50, .50}, + .94, + .20}, + {"Route 215 sign", + "PokemonBDSP/Autostory/Route215Renewal/AceRoute215SignRain.png", + {1128.0 / 960, 359.0 / 540, 90.0 / 960, 45.0 / 540}, + {.50, .40, .50, .50}, + .94, + .20}}, + "Grass west corner", + "Route 215 sign", + 1.0 / 128); + const auto& corner = far_east.landmarks[0]; + const auto& sign = far_east.landmarks[1]; + const double far_nearest = std::min(std::abs(corner.dx), std::abs(sign.dx)); + far_east.located = corner.accepted && sign.accepted && std::abs(corner.dy - sign.dy) <= AGREEMENT_Y && + corner.dx < -1.0 / 4 && sign.dx < -1.0 / 4 && far_nearest > 2 * std::abs(corner.dx - sign.dx) && + far_east.dy >= -1.0 / 27 && far_east.dy <= 1.0 / 9; + if (far_east.located){ + far_east.coarse = true; + far_east.at_target = false; + return far_east; + } + const auto& upper = approach.landmarks[0]; + const auto& lower = approach.landmarks[1]; + // Keep the shared row when both bends already agree. In the recorded + // perspective-shifted rows the nearer bend moves 1.35 times as far; + // compare those acquisition errors in the farther bend's plane. + // Neither model changes the strict fine-arrival checks above. + const double lower_plane_dy = std::abs(upper.dy - lower.dy) <= AGREEMENT_Y ? lower.dy : lower.dy / 1.35; + approach.dy = (upper.dy + lower_plane_dy) / 2; + const double nearest = std::min(std::abs(upper.dx), std::abs(lower.dx)); + const bool horizontal_support = + upper.dx * lower.dx > 0 && nearest > 1.0 / 64 && nearest > 2 * std::abs(upper.dx - lower.dx); + // Expiry can also finish one row south, already near the goal column. + // Both accepted bends must agree before correcting that row vertically. + const bool vertical_support = std::abs(upper.dx - lower.dx) <= AGREEMENT_X && + std::max(std::abs(upper.dx), std::abs(lower.dx)) <= 1.0 / 12 && + std::abs(upper.dy - lower_plane_dy) <= 1.0 / 108 && upper.dy * lower_plane_dy > 0 && + std::abs(approach.dy) > 1.0 / 54; + approach.located = upper.accepted && lower.accepted && std::abs(upper.dy - lower_plane_dy) <= AGREEMENT_Y && + std::abs(approach.dy) <= 1.0 / 9 && (horizontal_support || vertical_support); + approach.coarse = true; + approach.at_target = false; + return approach; +} +OverworldMatchResult match_route215_derek_stairs_renewal(const ImageRGB32& frame){ + if (!frame){ + return {}; + } + cv::Mat gray, smoothed; + cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); + ImageRGB32 filtered(960, 540); + cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); + cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); + // Clear landing below the short stairs; neither tree nor player is an anchor. + auto result = match_lighting_pair(filtered, + {{"White stair railing", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairsRailing.png", + {500.0 / 960, 78.0 / 540, 90.0 / 960, 110.0 / 540}, + {.10, .05, .85, .85}, + .93, + .12}, + {"Lower stair corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairsCorner.png", + {589.0 / 960, 315.0 / 540, 95.0 / 960, 105.0 / 540}, + {.10, .20, .85, .80}, + .93, + .12}, + {"White stair railing", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairsNightRailing.png", + {500.0 / 960, 78.0 / 540, 90.0 / 960, 110.0 / 540}, + {.10, .05, .85, .85}, + .93, + .12}, + {"Lower stair corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairsNightCorner.png", + {589.0 / 960, 315.0 / 540, 95.0 / 960, 105.0 / 540}, + {.10, .20, .85, .80}, + .93, + .12}}, + "White stair railing", + "Lower stair corner", + 1.0 / 128); + result.at_target = result.located && std::abs(result.dx) <= 1.0 / 128 && std::abs(result.dy) <= 1.0 / 108; + if (result.located){ + return result; + } + // Far west of this landing, perspective shifts the two structures by + // different amounts. Use that view only to acquire the horizontal column; + // the original close pair above remains the sole arrival check. + auto approach = match_landmarks(filtered, + {{"White stair railing", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairsWestRailing.png", + {500.0 / 960, 78.0 / 540, 90.0 / 960, 110.0 / 540}, + {.10, .05, .85, .85}, + .93, + .12}, + {"Lower stair corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairsWestCorner.png", + {589.0 / 960, 315.0 / 540, 95.0 / 960, 105.0 / 540}, + {.10, .20, .85, .80}, + .93, + .12}}, + 2, + 1.0 / 128, + 1.0 / 108, + true); + const auto& rail = approach.landmarks[0]; + const auto& stair = approach.landmarks[1]; + const double nearest = std::min(std::abs(rail.dx), std::abs(stair.dx)); + approach.located = rail.accepted && stair.accepted && std::abs(rail.dy - stair.dy) <= AGREEMENT_Y && + std::abs((rail.dy + stair.dy) / 2) <= 1.0 / 108 && rail.dx * stair.dx > 0 && nearest > 1.0 / 64 && + nearest > 2 * std::abs(rail.dx - stair.dx); + approach.dx = (rail.dx + stair.dx) / 2; + approach.dy = (rail.dy + stair.dy) / 2; + approach.coarse = true; + approach.at_target = false; + return approach; +} +OverworldMatchResult match_route215_derek_stair_top_renewal(const ImageRGB32& frame){ + if (!frame){ + return {}; + } + cv::Mat gray, smoothed; + cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); + ImageRGB32 filtered(960, 540); + cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); + cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); + // The road row above the stairs is clear west/east, below the tall grass. + auto result = match_lighting_pair(filtered, + {{"Stair-top barrier", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopBarrier.png", + {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, + {.05, .15, .90, .85}, + .93, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Stair-top barrier", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopBarrierEast.png", + {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, + {.05, .15, .90, .85}, + .93, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFenceEast.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Stair-top barrier", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightBarrier.png", + {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, + {.05, .15, .90, .85}, + .93, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Stair-top barrier", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightEastBarrier.png", + {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, + {.05, .15, .90, .85}, + .93, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightEastFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Stair-top barrier", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightWestBarrier.png", + {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, + {.05, .15, .90, .85}, + .93, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightWestFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNorthFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Stair-top barrier", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastNorthBarrier.png", + {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, + {.05, .15, .90, .85}, + .93, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastNorthFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Stair-top barrier", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastBarrier.png", + {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, + {.05, .15, .90, .85}, + .93, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST} // Raw-luminance alternatives preserve fence structure when local + // contrast normalization amplifies a passing rain streak. + , + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastNorthGrayFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12}, + {"West fence corner", + "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastGrayFence.png", + {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, + {.05, .15, .90, .85}, + .90, + .12}}, + "Stair-top barrier", + "West fence corner", + 1.0 / 128); + result.at_target = result.located && std::abs(result.dx) <= 1.0 / 128 && std::abs(result.dy) <= 1.0 / 108; + if (!result.located && result.landmarks.size() == 2){ + const auto& barrier = result.landmarks[0]; + const auto& fence = result.landmarks[1]; + const double nearest = std::min(std::abs(barrier.dx), std::abs(fence.dx)); + // Far-east perspective changes spacing: acquire the column first, + // then use the original strict two-anchor arrival check. + if (barrier.accepted && fence.accepted && std::abs(barrier.dy - fence.dy) <= AGREEMENT_Y && + std::abs(result.dy) <= 1.0 / 20 && barrier.dx * fence.dx > 0 && nearest > 1.0 / 64 && + nearest > 2 * std::abs(barrier.dx - fence.dx)){ + result.located = true; + result.coarse = true; + result.at_target = false; + } + } + return result; +} +OverworldMatchResult match_route215_jogger_corner(const ImageRGB32& frame){ + if (!frame){ + return {}; + } + // These references describe the game capture, without the recording canvas. + // Smooth only this locator before contrast normalization to suppress rain + // streaks and capture resampling noise; other detectors retain their inputs. + cv::Mat gray, smoothed; + cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); + cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); + ImageRGB32 filtered(960, 540); + cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); + cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); + // Fixed fence cap and road bend; neither the moving jogger nor the item + // supplies position evidence. Each view maps back to the same lane-4 corner. + // Search only the cap/bend height of lanes 4 and 3. This excludes similar + // lower fence sections; a missing cap never authorizes a vertical search. + // Low-frequency cap/cliff alternatives require higher correlation (.95/.97) + // as well as a unique peak and an independently agreeing structural anchor. + const std::vector appearances = { + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceGoal.png", + {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceRainWest.png", + {0.535416666667, 0.311111111111, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceCloudWest.png", + {0.535416666667, 0.311111111111, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceLane3.png", + {0.535416666667, 0.312962962963, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceMid.png", + {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceNear.png", + {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceApproachFar.png", + {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceApproachMiddle.png", + {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceApproachNear.png", + {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, + {0, .24, 1, .26}, + .93, + .08, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Fence top", + "PokemonBDSP/Autostory/Route215JoggerV2/FenceGrayLane3.png", + {0.535416666667, 0.312962962963, 32.0 / 960, 24.0 / 540}, + {0, .24, 1, .26}, + .95, + .05}, + {"Road bend", + "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperDayGoal.png", + {422.0 / 960, 164.0 / 540, 80.0 / 960, 25.0 / 540}, + {0, .24, 1, .26}, + .90, + .15}, + // In lane 3 the cap overlaps the lower part of the full road patch. + // Upper-bend alternatives keep position evidence clear of the player. + {"Road bend", + "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperGoal.png", + {0.439583333333, 0.303703703704, 80.0 / 960, 25.0 / 540}, + {0, .24, 1, .26}, + .90, + .15}, + {"Road bend", + "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperRainWest.png", + {0.439583333333, 0.303703703704, 80.0 / 960, 25.0 / 540}, + {0, .24, 1, .26}, + .90, + .15}, + {"Road bend", + "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperCloudWest.png", + {0.447916666667, 0.307407407407, 80.0 / 960, 25.0 / 540}, + {0, .24, 1, .26}, + .90, + .15}, + {"Road bend", + "PokemonBDSP/Autostory/Route215JoggerV2/RoadGoal.png", + {0.439583333333, 0.303703703704, 80.0 / 960, 41.0 / 540}, + {0, .24, 1, .26}, + .90, + .15}, + {"Road bend", + "PokemonBDSP/Autostory/Route215JoggerV2/RoadRainWest.png", + {0.439583333333, 0.303703703704, 80.0 / 960, 41.0 / 540}, + {0, .24, 1, .26}, + .90, + .15}, + {"Road bend", + "PokemonBDSP/Autostory/Route215JoggerV2/RoadCloudWest.png", + {0.447916666667, 0.307407407407, 80.0 / 960, 41.0 / 540}, + {0, .24, 1, .26}, + .90, + .15}, + }; + std::vector variants; + for (const auto& appearance : appearances){ + if (std::string(appearance.name) == "Road bend"){ + variants.push_back(appearance); + continue; + } + // Horizontal perspective changes as the fence crosses the screen. + for (double scale : {.96, 1.0, 1.04}){ + auto variant = appearance; + variant.target.width *= scale; + variant.target.x += (appearance.target.width - variant.target.width) / 2; + variants.push_back(variant); + } + } + // A road bend can disambiguate repeated fence sections. Choose complete + // agreeing pairs, rather than rejecting one anchor before considering the other. + const auto candidates = + match_landmarks(filtered, variants, variants.size() + 1, 1.0 / 128, POSITION_TOLERANCE_Y, true); + OverworldMatchResult best; + double best_score = -1; + bool supported = false, ambiguous = false; + double first_x = 0, first_y = 0; + for (const auto& fence : candidates.landmarks){ + if (fence.name != "Fence top"){ + continue; + } + for (const auto& road : candidates.landmarks){ + if (road.name != "Road bend"){ + continue; + } + const double score = std::min(fence.score, road.score); + const bool agrees = + std::abs(fence.dx - road.dx) <= AGREEMENT_X && std::abs(fence.dy - road.dy) <= AGREEMENT_Y; + const bool valid = fence.accepted && road.accepted && agrees; + const double dx = (fence.dx + road.dx) / 2, dy = (fence.dy + road.dy) / 2; + if (valid){ + if (supported && (std::abs(dx - first_x) > AGREEMENT_X || std::abs(dy - first_y) > AGREEMENT_Y)){ + ambiguous = true; + } + if (!supported){ + first_x = dx; + first_y = dy; + } + } + if ((valid && !supported) || (valid == supported && score > best_score)){ + best.landmarks = {fence, road}; + best.dx = dx; + best.dy = dy; + best_score = score; + } + supported |= valid; + } + } + best.located = supported && !ambiguous; + best.at_target = best.located && std::abs(best.dx) <= 1.0 / 128 && std::abs(best.dy) <= POSITION_TOLERANCE_Y; + if (best.located){ + return best; + } + // West of the visible fence, bridge/cliff pairs establish the walking row. + // Their x coordinates are acquisition-only and can never confirm arrival. + const std::vector> approaches = { + {// The observed lane-4 to lane-3 step changes the bridge-foot view. + // Both corners must agree; this pair never confirms arrival. + {"Bridge west corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestNightLane3.png", + {106.0 / 960 - .435625, 297.0 / 540 - 70.0 / 1080, 60.0 / 960, 75.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Bridge east corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeEastNightLane3.png", + {260.0 / 960 - .435625, 295.0 / 540 - 70.0 / 1080, 65.0 / 960, 75.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}}, + {// When the east fence is off-screen, these two full bridge-foot + // corners establish lane 4. They never authorize arrival. + {"Bridge west corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestCurrent.png", + {106.0 / 960 - .435, 257.0 / 540, 60.0 / 960, 75.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Bridge east corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeEastCurrent.png", + {260.0 / 960 - .435, 255.0 / 540, 65.0 / 960, 75.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}}, + { + + {"Bridge west corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestBridgeFar.png", + {240.0 / 960 - .61, 246.0 / 540, 64.0 / 960, 60.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"Bridge east corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeEastBridgeFar.png", + {388.0 / 960 - .61, 246.0 / 540, 64.0 / 960, 60.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}}, + {{"Bridge west corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestBridgeMid.png", + {108.0 / 960 - .445, 245.0 / 540, 64.0 / 960, 60.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"South cliff corner", + "PokemonBDSP/Autostory/Route215JoggerV2/SouthCliffBridgeMid.png", + {77.0 / 960 - .445, 295.0 / 540, 90.0 / 960, 80.0 / 540}, + {0, .2, 1, .6}, + .97, + .05}}, + {{"Bridge west corner", + "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestBridgeNear.png", + {68.0 / 960 - .405, 245.0 / 540, 64.0 / 960, 60.0 / 540}, + {0, .2, 1, .6}, + .90, + .12, + LandmarkFeatures::LOCAL_CONTRAST}, + {"South cliff corner", + "PokemonBDSP/Autostory/Route215JoggerV2/SouthCliffBridgeNear.png", + {37.0 / 960 - .405, 295.0 / 540, 90.0 / 960, 80.0 / 540}, + {0, .2, 1, .6}, + .97, + .05}}}; + for (const auto& approach : approaches){ + auto candidate = match_landmarks(filtered, approach, 2, 1.0 / 128, 1.0 / 54, true); + const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); + if (best.located && candidate.located && std::abs(best.dy - candidate.dy) > AGREEMENT_Y){ + best.located = false; + best.at_target = false; + best.coarse = true; + return best; + } + candidate.coarse = true; + candidate.at_target = false; + if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ + best = std::move(candidate); + best_score = score; + } + } + return best; +} +const std::vector& overworld_matchers(){ + static const std::vector matchers = { + {"route204-north-approach", "Route 204 left of sign after Tyler", match_route204_north_approach}, + {"teighler-alcove-corner", "Teighler alcove bottom-left corner", match_teighler_alcove_corner}, + {"route204-north-stairs", "Route 204 north stairs junction", match_route204_north_stairs}, + {"floaroma-center-entrance", "Floaroma Pokemon Center entrance", match_floaroma_center_entrance}, + {"ravaged-path-south-entrance", "Ravaged Path south entrance", match_ravaged_path_south_entrance}, + {"route204-sign", "Route 204 sign before the next trainer", match_route204_sign}, + {"windworks-sign", "Valley Windworks west of the door grunt", match_windworks_sign_approach}, + {"route205-bridge", "Route 205 south of the bridge", match_route205_bridge}, + {"route205-bank-tree", "Route 205 north bank beside the golden tree", match_route205_bank_tree}, + {"route205-zackary", "Route 205 Zackary patrol intercept", match_route205_zackary}, + {"route205-kelsey", "Route 205 Kelsey patrol intercept", match_route205_kelsey}, + {"route205-forest", "Route 205 outside Eterna Forest", match_route205_forest_entrance}, + {"jubilife-street-approach", "Jubilife street before Galactic plaza", match_jubilife_street_approach}, + {"jubilife-plaza-approach", "Jubilife Galactic plaza approach", match_jubilife_plaza_approach}, + {"poketch-landmarks", "Jubilife Poketch landmarks", match_poketch_landmarks}, + {"oreburgh-mart", "Oreburgh Mart emblem", match_oreburgh_mart}, + {"jubilife-center", "Jubilife Pokemon Center entrance", match_jubilife_center_entrance}, + {"eterna-center", "Eterna Pokemon Center doorway", match_eterna_center_entrance}, + {"hearthome-center", "Hearthome Pokemon Center doorway", match_hearthome_center_entrance}, + {"hearthome-center-interior", "Hearthome Pokemon Center interior", match_hearthome_center_interior}, + {"hearthome-fantina", "Hearthome Contest Hall sign + roof emblem (Fantina)", match_hearthome_contest_fantina}, + {"veilstone-city-passage", "Veilstone city passage before the west turn", match_veilstone_city_passage}, + {"veilstone-dawn-stairs", "Veilstone western stairs toward Dawn", match_veilstone_dawn_stairs}, + {"hearthome-gym", "Hearthome Gym doorway (arch + facade)", match_hearthome_gym_entrance}, + {"route208-stair-top", "Route 208 stair-top rocks", match_route208_stair_top}, + {"route209-repel-point", "Route 209 Repel renewal point", match_route209_repel_point}, + {"route215-jogger-corner", "Route 215 east corner beyond the jogger", match_route215_jogger_corner}, + {"route215-bridge-renewal", "Route 215 west bridge renewal", match_route215_bridge_renewal}, + {"route215-ace-renewal", "Route 215 renewal before Ace Trainers", match_route215_ace_renewal}, + {"route215-derek-stairs-renewal", "Route 215 renewal below Derek stairs", match_route215_derek_stairs_renewal}, + {"route215-derek-stair-top-renewal", + "Route 215 renewal above Derek stairs", + match_route215_derek_stair_top_renewal}, + {"route208-hannah-departure", "Route 208 field after Hannah", match_route208_hannah_departure}, + {"eterna-gym", "Eterna Gym doorway", match_eterna_gym_entrance}, + {"galactic-hq-jupiter", "Galactic HQ Jupiter interaction", match_galactic_hq_jupiter}, + {"eterna-gym-statues", "Eterna Gym interior statues", match_eterna_gym_statues}, + {"eterna-underground-house", "Eterna Underground Man house doorway", match_eterna_underground_house}, + {"eterna-underground-man", "Eterna Underground Man interaction", match_eterna_underground_man}, + {"route201-potion-exit", "Route 201 after Potion pickup", match_route201_potion_exit}, + {"sandgem-junction", "Sandgem Center/Mart junction", match_sandgem_junction}, + {"route202-grass-exit", "Route 202 after catching: grass exit", match_route202_grass_exit}, + }; + return matchers; +} +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h new file mode 100644 index 0000000000..6377da938c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include + +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { +struct OverworldLandmarkResult { + std::string name; + double score = 0, margin = 0; + // Signed fractions of screen width/height, independent of capture resolution. + double dx = 0, dy = 0; + bool accepted = false; + ImageFloatBox search, found; +}; +struct OverworldMatchResult { + std::vector landmarks; + bool located = false, at_target = false; + bool coarse = false; + // Signed fractions of screen width/height, independent of capture resolution. + double dx = 0, dy = 0; +}; +struct OverworldMatcher { + std::string slug, name; + std::function match; +}; +// Register additional functions here; each owns its landmarks and coordinate system. +const std::vector& overworld_matchers(); +OverworldMatchResult match_route204_north_approach(const ImageRGB32& frame); +OverworldMatchResult match_teighler_alcove_corner(const ImageRGB32& frame); +OverworldMatchResult match_route204_north_stairs(const ImageRGB32& frame); +OverworldMatchResult match_floaroma_center_entrance(const ImageRGB32& frame); +OverworldMatchResult match_floaroma_house_entrance(const ImageRGB32& frame); +OverworldMatchResult match_ravaged_path_south_entrance(const ImageRGB32& frame); +OverworldMatchResult match_route204_sign(const ImageRGB32& frame); +OverworldMatchResult match_windworks_sign_approach(const ImageRGB32& frame); +// Interior only: offsets from the player's red cap to the grunt's cyan hair. +// Positive dy > .025 means the player is above him; otherwise below/level. +OverworldMatchResult match_windworks_interior_grunt(const ImageRGB32& frame); +OverworldMatchResult match_route205_bridge(const ImageRGB32& frame); +OverworldMatchResult match_route205_bank_tree(const ImageRGB32& frame); +OverworldMatchResult match_route205_zackary(const ImageRGB32& frame); +OverworldMatchResult match_route205_kelsey(const ImageRGB32& frame); +OverworldMatchResult match_route205_forest_entrance(const ImageRGB32& frame); +// Northern sign: align primarily westward, then correct height below the flower bed. +OverworldMatchResult match_route204_floaroma_turn(const ImageRGB32& frame); +OverworldMatchResult match_jubilife_street_approach(const ImageRGB32& frame); +OverworldMatchResult match_jubilife_plaza_approach(const ImageRGB32& frame); +OverworldMatchResult match_poketch_landmarks(const ImageRGB32& frame); +OverworldMatchResult match_oreburgh_mart(const ImageRGB32& frame); +OverworldMatchResult match_jubilife_center_entrance(const ImageRGB32& frame); +OverworldMatchResult match_eterna_center_entrance(const ImageRGB32& frame); +OverworldMatchResult match_hearthome_center_entrance(const ImageRGB32& frame); +OverworldMatchResult match_hearthome_center_interior(const ImageRGB32& frame); +OverworldMatchResult match_hearthome_contest_fantina(const ImageRGB32& frame); +OverworldMatchResult match_veilstone_city_passage(const ImageRGB32& frame); +OverworldMatchResult match_veilstone_dawn_stairs(const ImageRGB32& frame); +OverworldMatchResult match_hearthome_gym_entrance(const ImageRGB32& frame); +OverworldMatchResult match_route208_stair_top(const ImageRGB32& frame); +OverworldMatchResult match_route208_lower_stair_lane(const ImageRGB32& frame); +// User-marked sign frame and rock pair at the Route 209 renewal point. +OverworldMatchResult match_route209_repel_point(const ImageRGB32& frame); +// Lane 4 east corner; coarse bridge references only guide approach acquisition. +OverworldMatchResult match_route215_jogger_corner(const ImageRGB32& frame); +OverworldMatchResult match_route215_bridge_renewal(const ImageRGB32& frame); +OverworldMatchResult match_route215_ace_renewal(const ImageRGB32& frame); +OverworldMatchResult match_route215_derek_stairs_renewal(const ImageRGB32& frame); +OverworldMatchResult match_route215_derek_stair_top_renewal(const ImageRGB32& frame); +OverworldMatchResult match_route208_hannah_departure(const ImageRGB32& frame); +OverworldMatchResult match_eterna_gym_entrance(const ImageRGB32& frame); +OverworldMatchResult match_galactic_hq_cut(const ImageRGB32& frame); +OverworldMatchResult match_galactic_hq_jupiter(const ImageRGB32& frame); +OverworldMatchResult match_eterna_cycle_shop_entrance(const ImageRGB32& frame); +OverworldMatchResult match_eterna_cycle_shop_interior(const ImageRGB32& frame); +OverworldMatchResult match_eterna_cycle_shop_reward(const ImageRGB32& frame); +OverworldMatchResult match_eterna_bike_riding_card(const ImageRGB32& frame); +OverworldMatchResult match_eterna_cycle_shop_counter(const ImageRGB32& frame); +OverworldMatchResult match_cycling_road_bridge_approach(const ImageRGB32& frame); +OverworldMatchResult match_cycling_road_south_gate_interior(const ImageRGB32& frame); +OverworldMatchResult match_galactic_hq_exterior(const ImageRGB32& frame); +OverworldMatchResult match_eterna_center_departure(const ImageRGB32& frame); +OverworldMatchResult match_eterna_gym_statues(const ImageRGB32& frame); +// Two unique, agreeing landmarks close to the doorway; this confidence only +// authorizes a watched entry attempt, never proves the interior location. +double overworld_entry_confidence(const OverworldMatchResult& result, + double tolerance_x = 1.0 / 50, + double tolerance_y = 1.0 / 27, + double minimum_margin = .15); +OverworldMatchResult match_eterna_underground_house(const ImageRGB32& frame); +OverworldMatchResult match_eterna_underground_man(const ImageRGB32& frame); +OverworldMatchResult match_route201_potion_exit(const ImageRGB32& frame); +OverworldMatchResult match_sandgem_junction(const ImageRGB32& frame); +OverworldMatchResult match_route202_grass_exit(const ImageRGB32& frame); +} +} +} diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index d2997174ec..834a1194f5 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -338,6 +338,8 @@ file(GLOB CORE_LIBRARY_SOURCES Source/NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController_from_Keyboard.h Source/NintendoSwitch/Controllers/Procon/NintendoSwitch_ProControllerState.cpp Source/NintendoSwitch/Controllers/Procon/NintendoSwitch_ProControllerState.h + Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.cpp + Source/NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h Source/NintendoSwitch/NintendoSwitch_Settings.cpp Source/NintendoSwitch/NintendoSwitch_Settings.h Source/NintendoSwitch/Options/NintendoSwitch_CodeEntrySettingsOption.cpp @@ -1535,6 +1537,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Resources/Pokemon_PokemonNames.h Source/Pokemon/Resources/Pokemon_PokemonSlugs.cpp Source/Pokemon/Resources/Pokemon_PokemonSlugs.h + Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.cpp + Source/PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_JubilifeCity.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_JubilifeCity.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_OreburghMine.cpp @@ -1581,6 +1585,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.cpp Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.cpp @@ -1591,6 +1597,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/Battles/PokemonBDSP_ExperienceGainDetector.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.cpp Source/PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.h + Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp + Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxDetector.cpp Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxDetector.h Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxGenderDetector.cpp @@ -1607,6 +1615,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/PokemonBDSP_MapDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_MarkFinder.cpp Source/PokemonBDSP/Inference/PokemonBDSP_MarkFinder.h + Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp + Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_PokeballSpriteMatcher.cpp @@ -1615,6 +1625,12 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/PokemonBDSP_ReceivePokemonDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.cpp Source/PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h + Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp + Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h + Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp + Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h + Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp + Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h Source/PokemonBDSP/Inference/PokemonBDSP_VSSeekerReaction.cpp Source/PokemonBDSP/Inference/PokemonBDSP_VSSeekerReaction.h Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.cpp @@ -1657,6 +1673,14 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/PokemonBDSP_Panels.h Source/PokemonBDSP/PokemonBDSP_Settings.cpp Source/PokemonBDSP/PokemonBDSP_Settings.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h Source/PokemonBDSP/PokemonBDSP_Tests.cpp Source/PokemonBDSP/PokemonBDSP_Tests.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.cpp From e8f42844aa923d6f3471ec2f88bb8146b47f9c55 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 12:01:54 -0800 Subject: [PATCH 02/98] BDSP AutoStory: the program, segments 00 and 01 on the shared story steps The AutoStory program from a new game to Floaroma: its options and segment and checkpoint lists, and segments 00 and 01 as one table of the shared story steps (StoryTable), run by the console story executor. 00.04b to 00.04d walk planned routes on the story maps; the walker (Walk, RouteWalk), the Pokemon Center and the location reads are ours. Segments 02 and later are not carried. Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters, kept nearly as written: AutoStoryTools (stats, checkpoint save and save_game, the Bag search, Potion use, buy_items, open_menu, dialogue waits), Segment_00_tools and Segment_01_tools (the starter, the Starly battle, trainer and wild battles, settings check, grass walks, running from wild battles), Utils Battle, Dialog and Navigation, and the recorded stick holds of 00.01 to 00.05 (tutorial parts 1 to 4, the landmark alignment, the Poketch scene) in Segment_00. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Panels.cpp | 5 + .../AutoStory/PokemonBDSP_AutoStory.cpp | 713 +++++ .../AutoStory/PokemonBDSP_AutoStory.h | 96 + .../AutoStory/PokemonBDSP_AutoStoryTools.cpp | 2306 +++++++++++++++++ .../AutoStory/PokemonBDSP_AutoStoryTools.h | 541 ++++ .../PokemonBDSP_AutoStory_StoryTable.cpp | 252 ++ .../PokemonBDSP_AutoStory_StoryTable.h | 42 + ...emonBDSP_AutoStory_SandgemCenterEvents.cpp | 1484 +++++++++++ ...okemonBDSP_AutoStory_SandgemCenterEvents.h | 344 +++ .../PokemonBDSP_AutoStory_Segment_00.cpp | 1386 ++++++++++ .../PokemonBDSP_AutoStory_Segment_00.h | 170 ++ .../PokemonBDSP_AutoStory_Segment_01.cpp | 1655 ++++++++++++ .../PokemonBDSP_AutoStory_Segment_01.h | 189 ++ .../Utils/PokemonBDSP_AutoStory_Battle.cpp | 1024 ++++++++ .../Utils/PokemonBDSP_AutoStory_Battle.h | 111 + .../Utils/PokemonBDSP_AutoStory_Dialog.cpp | 44 + .../Utils/PokemonBDSP_AutoStory_Dialog.h | 61 + .../Utils/PokemonBDSP_AutoStory_Location.cpp | 135 + .../Utils/PokemonBDSP_AutoStory_Location.h | 65 + .../PokemonBDSP_AutoStory_Navigation.cpp | 57 + .../Utils/PokemonBDSP_AutoStory_Navigation.h | 78 + .../Utils/PokemonBDSP_AutoStory_NewGame.cpp | 286 ++ .../Utils/PokemonBDSP_AutoStory_NewGame.h | 71 + .../PokemonBDSP_AutoStory_PokemonCenter.cpp | 485 ++++ .../PokemonBDSP_AutoStory_PokemonCenter.h | 127 + .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 926 +++++++ .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 202 ++ ...PokemonBDSP_AutoStory_Segment_00_tools.cpp | 2082 +++++++++++++++ .../PokemonBDSP_AutoStory_Segment_00_tools.h | 161 ++ .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 202 ++ .../PokemonBDSP_AutoStory_StoryExecutor.h | 123 + .../PokemonBDSP_AutoStory_StoryTables.cpp | 191 ++ .../Utils/PokemonBDSP_AutoStory_StoryTables.h | 119 + .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 457 ++++ .../Utils/PokemonBDSP_AutoStory_Walk.h | 167 ++ ...PokemonBDSP_Autostory_Segment_01_tools.cpp | 267 ++ .../PokemonBDSP_Autostory_Segment_01_tools.h | 28 + SerialPrograms/cmake/SourceFiles.cmake | 36 + 38 files changed, 16688 insertions(+) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp index e2aa07801f..c35f8a047a 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp @@ -7,6 +7,8 @@ #include "CommonFramework/StaticGlobals.h" #include "CommonFramework/GlobalAutoPaths.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h" + #include "PokemonBDSP_Panels.h" #include "PokemonBDSP_Settings.h" @@ -91,6 +93,9 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back("---- Auto Story ----"); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back("---- Glitches (v1.1.3) ----"); ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back(make_SingleSwitchProgram()); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp new file mode 100644 index 0000000000..ba6801157d --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -0,0 +1,713 @@ +#include +#include + +#include "PokemonBDSP_AutoStory.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "Utils/PokemonBDSP_AutoStory_NewGame.h" + +#include "CommonFramework/Globals.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/ImageMatchDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" + +#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" +#include "StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h" + +#include "CommonTools/Options/StringSelectOption.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +// --------------------------------------------------------------------------- +// Segment list +// --------------------------------------------------------------------------- + +static std::vector> make_segment_list(){ + std::vector> segment_list; + segment_list.emplace_back(std::make_unique()); + segment_list.emplace_back(std::make_unique()); + return segment_list; +} + +static const std::vector>& ALL_SEGMENTS(){ + static std::vector> list = make_segment_list(); + return list; +} +static std::vector> make_checkpoint_list(){ + std::vector> checkpoint_list; + + //Segment 00 + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + + //Segment 01 + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + checkpoint_list.emplace_back(std::make_unique()); + + return checkpoint_list; +} + +static const std::vector>& ALL_CHECKPOINTS(){ + static std::vector> list = make_checkpoint_list(); + return list; +} + +// --------------------------------------------------------------------------- +// Segment database +// --------------------------------------------------------------------------- + +static const StringSelectDatabase& section_database(bool checkpoints){ + static const StringSelectDatabase segments = []{ + StringSelectDatabase db; + for (size_t i = 0; i < ALL_SEGMENTS().size(); i++) + db.add_entry(StringSelectEntry(std::to_string(i), ALL_SEGMENTS()[i]->name())); + return db; + }(); + static const StringSelectDatabase checkpoint_db = []{ + StringSelectDatabase db; + for (size_t i = 0; i < ALL_CHECKPOINTS().size(); i++) + db.add_entry(StringSelectEntry(std::to_string(i), ALL_CHECKPOINTS()[i]->name())); + return db; + }(); + return checkpoints ? checkpoint_db : segments; +} + +// --------------------------------------------------------------------------- +// Descriptor +// --------------------------------------------------------------------------- + +BDSPAutoStory_Descriptor::BDSPAutoStory_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonBDSP:AutoStory", + "Pokemon BDSP", + "Auto Story", + "Programs/PokemonBDSP/AutoStory.html", + "Automatically completes BDSP story segments.", + PanelQualityStanding::OFFICIAL_BETA, + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS, + {"Tesseract"} + ) +{} + + + +std::unique_ptr +BDSPAutoStory_Descriptor::make_stats() const{ + return std::make_unique(); +} + + +// --------------------------------------------------------------------------- +// Program Instance +// --------------------------------------------------------------------------- + +BDSPAutoStory::~BDSPAutoStory(){ + OUTSIDE_HELP_POKEMON.remove_listener(*this); + HELP_MODE.remove_listener(*this); + START_CHECKPOINT_TUTORIAL.remove_listener(*this); + END_CHECKPOINT_TUTORIAL.remove_listener(*this); + START_SEGMENT.remove_listener(*this); + END_SEGMENT.remove_listener(*this); + ENABLE_ADVANCED_MODE.remove_listener(*this); + + +} + + +// --------------------------------------------------------------------------- +// Constructor +// --------------------------------------------------------------------------- + +BDSPAutoStory::BDSPAutoStory() + : SingleSwitchProgramInstance() + , DIAGNOSTIC_POST_CATCH_ONLY( + "Diagnostic only: Run 00.04 after catch through 00.05
" + "Start at the saved position immediately after catching the second Pokemon. " + "Applies only in advanced Tutorial mode when Start Checkpoint is 00.04. Runs its post-catch part, then 00.05. Other starting checkpoints run normally. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, + false + ) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_FAKE_CHECKPOINT_SAVE( + "Diagnostic only: Fake checkpoint saves
" + "Use the fake save helper instead of normal checkpoint saving. " + "Use an existing in-game save at the selected starting checkpoint or segment. " + "Any failure reloads that save and reruns the entire selected range from its start. " + "Checkpoint saves are not verified in this mode. Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER( + "Diagnostic only: Route 202 to Sandgem's Pokemon Center (events)
" + "Start at Route 202's save spot under the south ledge. Walks into Sandgem Town to the front of the " + "Pokemon Center's door, facing it, and stops there: no heal, no save. Runs only this, whatever the other options say " + "(with the Mart lady option too, runs both in turn). " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_SANDGEM_MART_LADY( + "Diagnostic only: Sandgem's Pokemon Center to the Poke Mart lady and back to Route 202 (events)
" + "Start in front of Sandgem's Pokemon Center door. Reads Route 201's border sign, talks to the Poke Mart lady, reads the sign " + "again, goes back to the Center's door, then to Route 202's save spot under the south ledge and saves there. " + "With the Route 202 option too, runs that first, from its Route 202 save. Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_JUBILIFE_CENTER( + "Diagnostic only: 00.05a Jubilife, steps to the Pokemon Center (events)
" + "Start at 00.05's save below Jubilife's steps. Walks to the Pokemon Center and heals; no save. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_JUBILIFE_SCHOOL( + "Diagnostic only: 00.05b Jubilife, Pokemon Center to the Trainers' School (events)
" + "Start on the Pokemon Center's doorstep (or after 00.05a). Walks into the Trainers' School; no save. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_JUBILIFE_BARRY( + "Diagnostic only: 00.05c Jubilife, Barry in the Trainers' School (events)
" + "Start inside the School at its door (or after 00.05b). Talks to Barry for the Town Map and walks out; no save. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_JUBILIFE_PRESIDENT( + "Diagnostic only: 00.05d Jubilife, the Poketch Company president (events)
" + "Start on the Trainers' School's doorstep (or after 00.05c). Walks into the road until the president talks; no save. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_JUBILIFE_CLOWN_MART( + "Diagnostic only: 00.05e Jubilife, the clown by the Poke Mart (events)
" + "Start where the president leaves us (or after 00.05d). Walks to the clown and gets Coupon 1; no save. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY( + "Diagnostic only: 00.05f Jubilife, the clown by the Poketch Company (events)
" + "Start beside the TV station clown (after his coupon). Walks to the third clown and gets Coupon 3; no save. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + // DIAGNOSTIC ONLY: Remove before pushing to production. + , DIAGNOSTIC_JUBILIFE_POKETCH( + "Diagnostic only: 00.05g Jubilife, the Poketch from the president (events)
" + "Start in the gap beside the Poketch Company's sign, with the three coupons (after 00.05f). " + "Gets the Poketch and goes up to the street east to Route 203; no save. " + "Remove before pushing to production.", + LockMode::LOCK_WHILE_RUNNING, false) + , ENABLE_ADVANCED_MODE( + "Advanced mode:
" + "Use checkpoints instead of segments.", + LockMode::UNLOCK_WHILE_RUNNING, + false + ) + , START_CHECKPOINT_TUTORIAL( + "Start Checkpoint:", + section_database(true), + LockMode::LOCK_WHILE_RUNNING, + "0" + ) + , END_CHECKPOINT_TUTORIAL( + "End Checkpoint:", + section_database(true), + LockMode::UNLOCK_WHILE_RUNNING, + "5" + ) + + , START_DESCRIPTION_SEGMENT("Start:
", "") + , END_DESCRIPTION_SEGMENT("End:
", "") + + , START_DESCRIPTION_CHECKPOINT("Start:
", "") + , END_DESCRIPTION_CHECKPOINT("End:
", "") + + , START_SEGMENT( + "Start Segment:", + section_database(false), + LockMode::LOCK_WHILE_RUNNING, + "0" + ) + + , END_SEGMENT( + "End Segment:", + section_database(false), + LockMode::UNLOCK_WHILE_RUNNING, + "0" + ) + + , NEW_GAME_INTRO( + "Start a new game first:
" + "For a game with no save: restart it and play the intro (language, appearance, names) " + "until the player is in control in the bedroom. Runs before the selected range.", + LockMode::LOCK_WHILE_RUNNING, false) + , PLAYER_NAME(false, "Player name:
For a new game. Letters and digits.", LockMode::LOCK_WHILE_RUNNING, "Kosmos", "Kosmos") + , STARTERCHOICE( + "Starter Pokemon:", + { + {StarterChoice::TURTWIG, "turtwig", "Turtwig"}, + {StarterChoice::CHIMCHAR, "chimchar", "Chimchar"}, + {StarterChoice::PIPLUP, "piplup", "Piplup"}, + }, + LockMode::LOCK_WHILE_RUNNING, + StarterChoice::CHIMCHAR + ) + + , STARTER_SHINY( + "Shiny Starter:", + LockMode::LOCK_WHILE_RUNNING, + false + ) + + , CATCH_LEGENDARY( + "Catch Box Legendary:", + LockMode::LOCK_WHILE_RUNNING, + false + ) + + , HELP_MODE( + "Help Mode:", + { + {HelpMode::NO_OUTSIDE_HELP, "no_outside_help", "No Outside Help"}, + {HelpMode::OUTSIDE_HELP, "outside_help", "Outside Help"}, + }, + LockMode::LOCK_WHILE_RUNNING, + HelpMode::NO_OUTSIDE_HELP + ) + + , OUTSIDE_HELP_POKEMON( + "Pokemon:", + { + {OutsideHelpPokemon::TORTERRA, "torterra", "Torterra"}, + }, + LockMode::LOCK_WHILE_RUNNING, + OutsideHelpPokemon::TORTERRA + ) + + , OUTSIDE_HELP_ATTACK_1( + "Attack 1:", + { + {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, + {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, + {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, + {TorterraMove::CRUNCH, "crunch", "Crunch"}, + {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, + {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, + {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, + {TorterraMove::FACADE, "facade", "Facade"}, + {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, + {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, + {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, + {TorterraMove::STRENGTH, "strength", "Strength"}, + {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, + {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, + }, + LockMode::LOCK_WHILE_RUNNING, + TorterraMove::EARTHQUAKE + ) + + , OUTSIDE_HELP_ATTACK_2( + "Attack 2:", + { + {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, + {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, + {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, + {TorterraMove::CRUNCH, "crunch", "Crunch"}, + {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, + {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, + {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, + {TorterraMove::FACADE, "facade", "Facade"}, + {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, + {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, + {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, + {TorterraMove::STRENGTH, "strength", "Strength"}, + {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, + {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, + }, + LockMode::LOCK_WHILE_RUNNING, + TorterraMove::ROCK_SLIDE + ) + + , OUTSIDE_HELP_ATTACK_3( + "Attack 3:", + { + {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, + {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, + {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, + {TorterraMove::CRUNCH, "crunch", "Crunch"}, + {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, + {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, + {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, + {TorterraMove::FACADE, "facade", "Facade"}, + {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, + {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, + {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, + {TorterraMove::STRENGTH, "strength", "Strength"}, + {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, + {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, + }, + LockMode::LOCK_WHILE_RUNNING, + TorterraMove::WOOD_HAMMER + ) + + , OUTSIDE_HELP_ATTACK_4( + "Attack 4:", + { + {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, + {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, + {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, + {TorterraMove::CRUNCH, "crunch", "Crunch"}, + {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, + {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, + {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, + {TorterraMove::FACADE, "facade", "Facade"}, + {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, + {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, + {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, + {TorterraMove::STRENGTH, "strength", "Strength"}, + {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, + {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, + }, + LockMode::LOCK_WHILE_RUNNING, + TorterraMove::CRUNCH + ) + + , NOTIFICATION_STATUS_UPDATE("Status Update", true, false, std::chrono::seconds(30)) + , NOTIFICATIONS({ + &NOTIFICATION_STATUS_UPDATE, + &NOTIFICATION_PROGRAM_FINISH, + &NOTIFICATION_ERROR_RECOVERABLE, + &NOTIFICATION_ERROR_FATAL, + }) + +{ + // ----------------------------------------------------------------------- + // Options + // ----------------------------------------------------------------------- + + PA_ADD_OPTION(RETRIES); + PA_ADD_OPTION(DIAGNOSTIC_POST_CATCH_ONLY); + PA_ADD_OPTION(DIAGNOSTIC_FAKE_CHECKPOINT_SAVE); + PA_ADD_OPTION(DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER); + PA_ADD_OPTION(DIAGNOSTIC_SANDGEM_MART_LADY); + PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_CENTER); + PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_SCHOOL); + PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_BARRY); + PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_PRESIDENT); + PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_CLOWN_MART); + PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY); + PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_POKETCH); + PA_ADD_OPTION(ENABLE_ADVANCED_MODE); + + // Segment UI + PA_ADD_OPTION(START_SEGMENT); + PA_ADD_OPTION(START_DESCRIPTION_SEGMENT); + + PA_ADD_OPTION(END_SEGMENT); + PA_ADD_OPTION(END_DESCRIPTION_SEGMENT); + + // Checkpoint UI + PA_ADD_OPTION(START_CHECKPOINT_TUTORIAL); + PA_ADD_OPTION(START_DESCRIPTION_CHECKPOINT); + + PA_ADD_OPTION(END_CHECKPOINT_TUTORIAL); + PA_ADD_OPTION(END_DESCRIPTION_CHECKPOINT); + + + PA_ADD_OPTION(NEW_GAME_INTRO); + PA_ADD_OPTION(PLAYER_NAME); + PA_ADD_OPTION(STARTERCHOICE); + PA_ADD_OPTION(STARTER_SHINY); + PA_ADD_OPTION(CATCH_LEGENDARY); + + PA_ADD_OPTION(HELP_MODE); + PA_ADD_OPTION(OUTSIDE_HELP_POKEMON); + + PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_1); + PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_2); + PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_3); + PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_4); + + PA_ADD_OPTION(NOTIFICATIONS); + + HELP_MODE.add_listener(*this); + OUTSIDE_HELP_POKEMON.add_listener(*this); + START_CHECKPOINT_TUTORIAL.add_listener(*this); + END_CHECKPOINT_TUTORIAL.add_listener(*this); + ENABLE_ADVANCED_MODE.add_listener(*this); + START_SEGMENT.add_listener(*this); + END_SEGMENT.add_listener(*this); + on_config_value_changed(nullptr); +} +void BDSPAutoStory::update_ui_state(){ + const bool advanced = ENABLE_ADVANCED_MODE; + auto visible = [](ConfigOption& option, bool show){ + option.set_visibility(show ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); + }; + visible(START_SEGMENT, !advanced); + visible(END_SEGMENT, !advanced); + visible(START_CHECKPOINT_TUTORIAL, advanced); + visible(END_CHECKPOINT_TUTORIAL, advanced); + visible(START_DESCRIPTION_SEGMENT, !advanced); + visible(END_DESCRIPTION_SEGMENT, !advanced); + visible(START_DESCRIPTION_CHECKPOINT, advanced); + visible(END_DESCRIPTION_CHECKPOINT, advanced); + + bool show_help = ((HelpMode)HELP_MODE == HelpMode::OUTSIDE_HELP); + + OUTSIDE_HELP_POKEMON.set_visibility( + show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN + ); + + OUTSIDE_HELP_ATTACK_1.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); + OUTSIDE_HELP_ATTACK_2.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); + OUTSIDE_HELP_ATTACK_3.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); + OUTSIDE_HELP_ATTACK_4.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); +} +void BDSPAutoStory::on_config_value_changed(void*){ + if (ENABLE_ADVANCED_MODE){ + const auto& checkpoints = ALL_CHECKPOINTS(); + + size_t start = std::stoul(START_CHECKPOINT_TUTORIAL.slug()); + size_t end = std::stoul(END_CHECKPOINT_TUTORIAL.slug()); + + if (start < checkpoints.size()){ + START_DESCRIPTION_CHECKPOINT.set_text( + "Start: " + checkpoints[start]->start_text() + ); + } + if (end < checkpoints.size()){ + END_DESCRIPTION_CHECKPOINT.set_text( + "End: " + checkpoints[end]->end_text() + ); + } + + }else{ + const auto& segments = ALL_SEGMENTS(); + + size_t start = std::stoul(START_SEGMENT.slug()); + size_t end = std::stoul(END_SEGMENT.slug()); + + if (start < segments.size()){ + START_DESCRIPTION_SEGMENT.set_text( + "Start: " + segments[start]->start_text() + ); + } + if (end < segments.size()){ + END_DESCRIPTION_SEGMENT.set_text( + "End: " + segments[end]->end_text() + ); + } + } + + update_ui_state(); +} +// --------------------------------------------------------------------------- +// Program logic +// --------------------------------------------------------------------------- + +void BDSPAutoStory::program( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context +){ + AutoStoryStats& stats = env.current_stats(); + // The checkpoints not yet in tables still run checkpoint_reattempt_loop(). + g_stop_at_first_failure = RETRIES.STOP_AT_FIRST_FAILURE; + g_story_language = RETRIES.language(); + + AutoStoryOptions options{ + RETRIES.language(), + STARTERCHOICE, + NOTIFICATION_STATUS_UPDATE, + (HelpMode)HELP_MODE, + OutsideHelpConfig{ + (OutsideHelpPokemon)OUTSIDE_HELP_POKEMON, + { + (TorterraMove)OUTSIDE_HELP_ATTACK_1, + (TorterraMove)OUTSIDE_HELP_ATTACK_2, + (TorterraMove)OUTSIDE_HELP_ATTACK_3, + (TorterraMove)OUTSIDE_HELP_ATTACK_4, + } + }, + DIAGNOSTIC_FAKE_CHECKPOINT_SAVE ? CheckpointSaveMode::FAKE : CheckpointSaveMode::REAL + }; + options.policy = RETRIES.policy(); + options.checks = RETRIES.checks(); + + const bool fake_saves = options.checkpoint_save_mode == CheckpointSaveMode::FAKE; + if (fake_saves) options.checkpoint_retry_scope = CheckpointRetryScope::DEFER_TO_SELECTED_RANGE; + // Logs what a checkpoint is for before it runs, and that it finished after. + auto run_checkpoint_logged = [&](size_t index, const AutoStory_Checkpoint& checkpoint){ + stats.m_current = index; + env.update_stats(); + env.log("[AutoStory] Objective: " + checkpoint.name() + ". Ends: " + checkpoint.end_text(), COLOR_BLUE); + add_wrapped_log(env.console.overlay(), "Now: " + checkpoint.name(), COLOR_WHITE); + add_wrapped_log(env.console.overlay(), "Goal: " + checkpoint.end_text(), COLOR_GRAY); + checkpoint.run_checkpoint(env, context, options, stats); + stats.m_completed++; + stats.m_last_completed = index; + env.update_stats(); + env.log("[AutoStory] Completed: " + checkpoint.name() + ".", COLOR_DARKGREEN); + add_wrapped_log(env.console.overlay(), "Done: " + checkpoint.name(), COLOR_GREEN); + }; + auto run_selected_range = [&](size_t attempt){ + if (attempt > 0) env.log("[Fake saves] Reloaded initial save; restarting the entire selected range from its start."); + // DIAGNOSTIC ONLY: Remove this branch before pushing to production. + if (DIAGNOSTIC_JUBILIFE_CENTER || DIAGNOSTIC_JUBILIFE_SCHOOL || DIAGNOSTIC_JUBILIFE_BARRY || DIAGNOSTIC_JUBILIFE_PRESIDENT || DIAGNOSTIC_JUBILIFE_CLOWN_MART || DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY || DIAGNOSTIC_JUBILIFE_POKETCH){ + const std::pair parts[] = { + {DIAGNOSTIC_JUBILIFE_CENTER, JUBILIFE_CENTER_NAME}, + {DIAGNOSTIC_JUBILIFE_SCHOOL, JUBILIFE_SCHOOL_NAME}, + {DIAGNOSTIC_JUBILIFE_BARRY, JUBILIFE_BARRY_NAME}, + {DIAGNOSTIC_JUBILIFE_PRESIDENT, JUBILIFE_PRESIDENT_NAME}, + {DIAGNOSTIC_JUBILIFE_CLOWN_MART, JUBILIFE_CLOWN_MART_NAME}, + {DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY, JUBILIFE_CLOWN_COMPANY_NAME}, + {DIAGNOSTIC_JUBILIFE_POKETCH, JUBILIFE_POKETCH_NAME}, + }; + for (const auto& [on, name] : parts){ + if (!on){ + continue; + } + env.log(std::string("[AutoStory] Objective: ") + name + ".", COLOR_BLUE); + add_wrapped_log(env.console.overlay(), std::string("Now: ") + name, COLOR_WHITE); + if (name == JUBILIFE_CENTER_NAME){ + diagnostic_jubilife_center(env, context); + }else if (name == JUBILIFE_SCHOOL_NAME){ + diagnostic_jubilife_school(env, context); + }else if (name == JUBILIFE_BARRY_NAME){ + diagnostic_jubilife_barry(env, context); + }else if (name == JUBILIFE_PRESIDENT_NAME){ + diagnostic_jubilife_president(env, context); + }else if (name == JUBILIFE_CLOWN_MART_NAME){ + diagnostic_jubilife_clown_mart(env, context); + }else if (name == JUBILIFE_CLOWN_COMPANY_NAME){ + diagnostic_jubilife_clown_company(env, context); + }else{ + diagnostic_jubilife_poketch(env, context); + } + env.log(std::string("[AutoStory] Completed: ") + name + ".", COLOR_DARKGREEN); + add_wrapped_log(env.console.overlay(), std::string("Done: ") + name, COLOR_GREEN); + } + return; + } + // DIAGNOSTIC ONLY: Remove this branch before pushing to production. + if (DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER || DIAGNOSTIC_SANDGEM_MART_LADY){ + if (DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER){ + const std::string name = ROUTE202_TO_SANDGEM_CENTER_NAME; + env.log("[AutoStory] Objective: " + name + ". Ends: in front of Sandgem Town's Pokemon Center door, not healed, not saved.", COLOR_BLUE); + add_wrapped_log(env.console.overlay(), "Now: " + name, COLOR_WHITE); + diagnostic_route202_to_sandgem_center(env, context); + env.log("[AutoStory] Completed: " + name + ".", COLOR_DARKGREEN); + add_wrapped_log(env.console.overlay(), "Done: " + name, COLOR_GREEN); + } + if (DIAGNOSTIC_SANDGEM_MART_LADY){ + const std::string name = SANDGEM_MART_LADY_NAME; + env.log("[AutoStory] Objective: " + name + ". Ends: at Route 202's save spot, saved.", COLOR_BLUE); + add_wrapped_log(env.console.overlay(), "Now: " + name, COLOR_WHITE); + diagnostic_sandgem_mart_lady(env, context); + env.log("[AutoStory] Completed: " + name + ".", COLOR_DARKGREEN); + add_wrapped_log(env.console.overlay(), "Done: " + name, COLOR_GREEN); + } + return; + } + // DIAGNOSTIC ONLY: Remove this branch before pushing to production. + if (DIAGNOSTIC_POST_CATCH_ONLY && ENABLE_ADVANCED_MODE && + START_CHECKPOINT_TUTORIAL.slug() == "4"){ + env.log("Diagnostic only: Running checkpoint 00.04 after catch."); + stats.m_current = 4; + env.update_stats(); + env.log("[AutoStory] Objective: " + AutoStory_Checkpoint_0004().name() + " (after the catch). Ends: " + + AutoStory_Checkpoint_0004().end_text(), COLOR_BLUE); + add_wrapped_log(env.console.overlay(), "Now: " + AutoStory_Checkpoint_0004().name() + " (after the catch)", COLOR_WHITE); + add_wrapped_log(env.console.overlay(), "Goal: " + AutoStory_Checkpoint_0004().end_text(), COLOR_GRAY); + diagnostic_checkpoint_0004_after_catch(env, context, options, stats); + stats.m_completed++; + stats.m_last_completed = 4; + env.update_stats(); + env.log("[AutoStory] Completed: " + AutoStory_Checkpoint_0004().name() + ".", COLOR_DARKGREEN); + env.log("Diagnostic continuation: Running checkpoint 00.05."); + run_checkpoint_logged(5, AutoStory_Checkpoint_0005()); + return; + } + + const auto& segments = ALL_SEGMENTS(); + + if (ENABLE_ADVANCED_MODE){ + size_t start = std::stoul(START_CHECKPOINT_TUTORIAL.slug()); + size_t end = std::stoul(END_CHECKPOINT_TUTORIAL.slug()); + + env.log("Starting AutoStory from checkpoint " + std::to_string(start) + + " to " + std::to_string(end)); + + const auto& checkpoints = ALL_CHECKPOINTS(); + + for (size_t i = start; i <= end && i < checkpoints.size(); i++){ + env.log("Running Checkpoint: " + std::to_string(i)); + run_checkpoint_logged(i, *checkpoints[i]); + } + + }else{ + size_t start = std::stoul(START_SEGMENT.slug()); + size_t end = std::stoul(END_SEGMENT.slug()); + + for (size_t i = start; i <= end && i < segments.size(); i++){ + env.log("Running Segment: " + std::to_string(i)); + segments[i]->run_segment(env, context, options, stats); + } + } + }; + try{ + if (NEW_GAME_INTRO){ + play_new_game_intro(env, context, PLAYER_NAME); + } + if (fake_saves){ + checkpoint_reattempt_loop(env, context, options.notif_status_update, stats, + std::move(run_selected_range), true, CheckpointSaveMode::FAKE, + CheckpointRetryScope::SELECTED_RANGE); + }else{ + run_selected_range(0); + } + }catch (OperationFailedException&){ + if (RETRIES.STOP_AT_FIRST_FAILURE){ + env.log("AutoStory failed: leaving the game where it failed (lab option).", COLOR_RED); + throw; + } + // Leave the Switch in a known state: on HOME with the game closed, so the next + // run (or the user) starts from there. The error already holds its screenshot. + env.log("AutoStory failed: going to HOME and closing the game.", COLOR_RED); + try{ + go_home(env.console, context); + close_game_from_home(env.console, context); + }catch (OperationFailedException& e){ + env.log("Could not close the game: " + e.message(), COLOR_RED); + } + throw; + } + + send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); +} + + + +} +} +} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h new file mode 100644 index 0000000000..6677d84e2e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/StaticTextOption.h" +#include "Common/Cpp/Options/StringOption.h" +#include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "CommonTools/Options/StringSelectOption.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "PokemonBDSP_AutoStoryTools.h" +#include "Pokemon/Options/Pokemon_StoryRunOptions.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +class BDSPAutoStory_Descriptor : public SingleSwitchProgramDescriptor{ +public: + BDSPAutoStory_Descriptor(); + + virtual std::unique_ptr make_stats() const override; +}; + + +class BDSPAutoStory : public SingleSwitchProgramInstance, public ConfigOption::Listener{ +public: + using Descriptor = BDSPAutoStory_Descriptor; + ~BDSPAutoStory(); + BDSPAutoStory(); + + virtual void program( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context + ) override; + + +private: + virtual void on_config_value_changed(void* object) override; + void update_ui_state(); +private: + // Attempts per checkpoint and stop at the first failure, as in FRLG. + Pokemon::StoryRunOptions RETRIES; + BooleanCheckBoxOption DIAGNOSTIC_POST_CATCH_ONLY; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_FAKE_CHECKPOINT_SAVE; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_SANDGEM_MART_LADY; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_CENTER; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_SCHOOL; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_BARRY; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_PRESIDENT; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_CLOWN_MART; + // DIAGNOSTIC ONLY: Remove before pushing to production. + BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY; + BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_POKETCH; + BooleanCheckBoxOption ENABLE_ADVANCED_MODE; + + StringSelectOption START_CHECKPOINT_TUTORIAL; + StringSelectOption END_CHECKPOINT_TUTORIAL; + StaticTextOption START_DESCRIPTION_SEGMENT; + StaticTextOption END_DESCRIPTION_SEGMENT; + + StaticTextOption START_DESCRIPTION_CHECKPOINT; + StaticTextOption END_DESCRIPTION_CHECKPOINT; + + StringSelectOption START_SEGMENT; + StringSelectOption END_SEGMENT; + + BooleanCheckBoxOption NEW_GAME_INTRO; + StringOption PLAYER_NAME; + EnumDropdownOption STARTERCHOICE; + BooleanCheckBoxOption STARTER_SHINY; + BooleanCheckBoxOption CATCH_LEGENDARY; + EnumDropdownOption HELP_MODE; + EnumDropdownOption OUTSIDE_HELP_POKEMON; + EnumDropdownOption OUTSIDE_HELP_ATTACK_1; + EnumDropdownOption OUTSIDE_HELP_ATTACK_2; + EnumDropdownOption OUTSIDE_HELP_ATTACK_3; + EnumDropdownOption OUTSIDE_HELP_ATTACK_4; + + EventNotificationOption NOTIFICATION_STATUS_UPDATE; + EventNotificationsOption NOTIFICATIONS; +}; + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp new file mode 100644 index 0000000000..98587454ea --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp @@ -0,0 +1,2306 @@ +#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +/* BDSP AutoStory Tools + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include + +#include "Common/Cpp/Filesystem/Filesystem.h" + +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageDiff.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "CommonTools/OCR/OCR_Routines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "CommonTools/VisualDetectors/ImageMatchDetector.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" + +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" +#include "PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h" +#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" + +#include "Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" + +#include "PokemonBDSP_AutoStoryTools.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +const char* torterra_move_slug(TorterraMove move){ + switch (move){ + case TorterraMove::EARTHQUAKE: + return "earthquake"; + case TorterraMove::WOOD_HAMMER: + return "wood-hammer"; + case TorterraMove::RAZOR_LEAF: + return "razor-leaf"; + case TorterraMove::CRUNCH: + return "crunch"; + case TorterraMove::GIGA_DRAIN: + return "giga-drain"; + case TorterraMove::IRON_TAIL: + return "iron-tail"; + case TorterraMove::ROCK_TOMB: + return "rock-tomb"; + case TorterraMove::FACADE: + return "facade"; + case TorterraMove::STONE_EDGE: + return "stone-edge"; + case TorterraMove::ROCK_SLIDE: + return "rock-slide"; + case TorterraMove::BULLDOZE: + return "bulldoze"; + case TorterraMove::STRENGTH: + return "strength"; + case TorterraMove::ROCK_SMASH: + return "rock-smash"; + case TorterraMove::ROCK_CLIMB: + return "rock-climb"; + } + return "unknown"; +} + +const char* trainer_slug(Trainer t){ + switch (t){ + case Trainer::Veilstone_GalacticGrunts_2v2: + return "Veilstone_GalacticGrunts_2v2"; + } + return "unknown"; +} + +bool fake_save_game(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + pbf_mash_button(context, BUTTON_L, 1000ms); + + context.wait_for_all_requests(); + pbf_press_button(context, BUTTON_X, 80ms, 1000ms); + pbf_press_button(context, BUTTON_R, 80ms, 1000ms); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + + return true; +} + +void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color){ + // The log box is 35% of the video wide at a font 4% of its height: about + // 34 characters. + constexpr size_t WIDTH = 34; + std::string line; + size_t start = 0; + while (start < text.size()){ + size_t end = text.find(' ', start); + if (end == std::string::npos){ + end = text.size(); + } + const std::string word = text.substr(start, end - start); + if (!line.empty() && line.size() + 1 + word.size() > WIDTH){ + overlay.add_log(line, color); + line = " "; + } + line += (line.empty() || line == " " ? "" : " ") + word; + start = end + 1; + } + if (!line.empty()){ + overlay.add_log(line, color); + } +} + +// Opens the save prompt from the overworld and keeps a screenshot of it: it +// shows the play time and badges, a record of where each checkpoint save +// stands. Returns the map its card names. +static std::string open_save_prompt(VideoStream& stream, ProControllerContext& context){ + overworld_to_menu(stream, context); + pbf_press_button(context, BUTTON_R, 80ms, 2000ms); + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + if (!screen){ + return ""; + } + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/saves/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = folder + now_to_filestring() + ".png"; + screen.frame->save(path); + stream.log("Save prompt screenshot: " + path); + const std::string location = read_save_screen_location(*screen.frame); + stream.log("Save screen location: \"" + location + "\"."); + return location; +} + +// Backs out of the save prompt and its menu to the overworld. +static void leave_save_prompt(VideoStream& stream, ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(15), {overworld}) < 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Failed to detect the overworld after leaving the save prompt.", stream); + } +} + +std::string read_location_from_save_screen(VideoStream& stream, ProControllerContext& context){ + const std::string location = open_save_prompt(stream, context); + leave_save_prompt(stream, context); + return location; +} + +bool save_game( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::string& expected_location +) { + static constexpr size_t MAX_RETRIES = 3; + + std::string wrong_place; + for (size_t attempt = 0; attempt < MAX_RETRIES && wrong_place.empty(); attempt++){ + try { + env.console.log("Checkpoint save attempt: " + std::to_string(attempt + 1)); + + // Only mash B on retries - no need to on first attempt + // if already cleanly in the overworld. + if (attempt > 0){ + pbf_mash_button(context, BUTTON_B, 1000ms); + } + + // The feedback-based save of save_game(VideoStream&, ...), split at the + // save prompt to check where it stands. + const std::string location = open_save_prompt(env.console, context); + if (!expected_location.empty() && !location_banner_matches(location, expected_location)){ + wrong_place = "About to save in \"" + location + "\", expected " + expected_location + "; not saving."; + env.console.log(wrong_place, COLOR_RED); + leave_save_prompt(env.console, context); + break; + } + pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); + + // Return to overworld cleanly. + pbf_mash_button(context, BUTTON_B, 1000ms); + + // Drain R/confirm/B before detection; an earlier overworld frame + // must not report success while saving commands are still queued. + context.wait_for_all_requests(); + + // Verify overworld. Generous timeout: save animation + fade can be slow. + OverworldWatcher overworld; + int ret = wait_until(env.console, context, std::chrono::seconds(15), {overworld}); + if (ret < 0){ + throw OperationFailedExceptionWithScreenshot(ErrorReportMode::SEND_ERROR_REPORT, + "Failed to detect overworld after save.", + env.console); + } + + env.console.log("Checkpoint save verified."); + return true; + + } catch (OperationFailedException& e){ + env.console.log("Checkpoint save failed: " + e.message(), COLOR_RED); + context.wait_for(1000ms); + } + } + // No blind fallback for the wrong place: it would save there. + if (!wrong_place.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, wrong_place, env.console); + } + + // Blind fallback: reuse the non-feedback save_game(context) from GameNavigation + // rather than duplicating the button sequence manually. + env.console.log("All save retries failed. Using blind fallback.", COLOR_ORANGE); + pbf_mash_button(context, BUTTON_B, 1000ms); + save_game(context); // PokemonBDSP_GameNavigation.h - non-feedback overload + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + + return false; +} + + +void checkpoint_save(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats, + CheckpointSaveMode save_mode){ + const bool fake = save_mode == CheckpointSaveMode::FAKE; + env.console.log( + fake ? "[Checkpoint save] DIAGNOSTIC FAKE mode: save is not verified." : "[Checkpoint save] REAL mode."); + bool success = fake ? fake_save_game(env, context) : save_game(env, context); + + stats.m_checkpoint++; + env.update_stats(); + + send_program_status_notification(env, + notif_status_update, + fake ? "Diagnostic fake checkpoint save (not verified)." + : success ? "Saved at checkpoint (verified)." + : "Saved at checkpoint (blind fallback)."); +} + + +void reload_backup_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + // TODO: implement backup save reload. + // This is called when a checkpoint with save=false fails. + // Load the game's automatic backup save rather than resetting from HOME. + (void)env; + (void)context; +} + + +bool g_stop_at_first_failure = false; + +void checkpoint_reattempt_loop(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats, + std::function&& action, + bool save, + CheckpointSaveMode save_mode, + CheckpointRetryScope retry_scope){ + static constexpr size_t MAX_ATTEMPTS = 20; + + for (size_t i = 0;; i++){ + try { + if (i == 0 && retry_scope != CheckpointRetryScope::SELECTED_RANGE){ + if (save || stats.m_first_run){ + checkpoint_save(env, context, notif_status_update, stats, save_mode); + } + stats.m_first_run = false; + } + + action(i); + return; // success + + }catch (OperationFailedException& e){ + if (retry_scope == CheckpointRetryScope::DEFER_TO_SELECTED_RANGE) throw; + env.console.log("checkpoint_reattempt_loop: attempt " + std::to_string(i) + " failed: " + e.message(), COLOR_RED); + // Keep what the screen showed at the failure; the retry below resets the game. + if (VideoSnapshot screen = env.console.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/failures/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = folder + now_to_filestring() + "-attempt" + std::to_string(i) + ".png"; + screen.frame->save(path); + env.console.log("Failure screenshot: " + path); + } + stats.m_reset++; + env.update_stats(); + + if (i + 1 >= MAX_ATTEMPTS){ + throw; + } + if (g_stop_at_first_failure){ + env.console.log("Stopping at the first failure; the game stays where it failed.", COLOR_ORANGE); + throw; + } + + if (save || retry_scope == CheckpointRetryScope::SELECTED_RANGE){ + env.console.log("Resetting game from HOME before retry...", COLOR_ORANGE); + bool ok = reset_game_from_home(env, env.console, context, true, 1000ms); + if (!ok){ + throw OperationFailedExceptionWithScreenshot(ErrorReportMode::SEND_ERROR_REPORT, + "Failed to reset game from HOME during checkpoint retry.", + env.console); + } + }else{ + env.console.log("Reloading from backup save before retry...", COLOR_ORANGE); + reload_backup_save(env, context); + } + } + } +} + + +void repeat(std::initializer_list> actions, size_t times){ + for (size_t i = 0; i < times; i++){ + for (const auto& action : actions){ + action(); + } + } +} + + +void wait_for_dialogue(VideoStream& stream, + ProControllerContext& context, + const std::string& label, + Milliseconds timeout){ + auto log_detected = [&] { + if (!label.empty()){ + stream.log(label + " dialogue detected."); + } + }; + + ShortDialogWatcher watcher_cyan(COLOR_CYAN); + int ret = run_until(stream, + context, + [timeout](ProControllerContext& context){ + context.wait_for(timeout); + }, + {{watcher_cyan}}); + if (ret == 0){ + log_detected(); + return; + } + + ShortDialogWatcher watcher_red(COLOR_RED); + ret = run_until(stream, + context, + [timeout](ProControllerContext& context){ + context.wait_for(timeout); + }, + {{watcher_red}}); + if (ret == 0){ + log_detected(); + return; + } + + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + (label.empty() ? "wait_for_dialogue" : label) + ": dialogue not detected within timeout.", + stream); +} + + +void mash_until_dialogue_ends(VideoStream& stream, ProControllerContext& context, Button button, Milliseconds timeout){ + OverworldWatcher overworld; + int ret = run_until(stream, + context, + [button, timeout](ProControllerContext& context){ + pbf_mash_button(context, button, timeout); + }, + {{overworld}}); + if (ret < 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "mash_until_dialogue_ends: overworld not reached within timeout.", + stream); + } +} + + +void repeat_dpad(ProControllerContext& context, + DpadState& state, + DpadPosition dir, + Milliseconds press, + Milliseconds hold, + size_t times, + bool include_last_dir){ + if (include_last_dir && state.last_dir != DPAD_NONE && state.last_dir != dir){ + times += 1; + } + state.last_dir = dir; + for (size_t i = 0; i < times; i++){ + pbf_press_dpad(context, dir, press, hold); + } +} + +static const char* shop_item_name(ShopItem item){ + switch (item){ + case ShopItem::FullRestore: + return "Full restore"; + case ShopItem::MaxPotion: + return "Max potion"; + case ShopItem::HyperPotion: + return "Hyper potion"; + case ShopItem::SuperPotion: + return "Super potion"; + case ShopItem::Potion: + return "Potion"; + case ShopItem::Antidote: + return "Antidote"; + case ShopItem::ParalyzHeal: + return "Paralyz heal"; + case ShopItem::Awakening: + return "Awakening"; + case ShopItem::Repel: + return "Repel"; + case ShopItem::SuperRepel: + return "Super repel"; + case ShopItem::MaxRepel: + return "Max repel"; + case ShopItem::EscapeRope: + return "Escape rope"; + default: + return "Unknown"; + } +} + +// Returns the OCR slug used as the dictionary key in ShopItemNameOCR.json. +static const char* shop_item_slug(ShopItem item){ + switch (item){ + case ShopItem::FullRestore: + return "full-restore"; + case ShopItem::MaxPotion: + return "max-potion"; + case ShopItem::HyperPotion: + return "hyper-potion"; + case ShopItem::SuperPotion: + return "super-potion"; + case ShopItem::Potion: + return "potion"; + case ShopItem::Antidote: + return "antidote"; + case ShopItem::ParalyzHeal: + return "paralyze-heal"; + case ShopItem::Awakening: + return "awakening"; + case ShopItem::Repel: + return "repel"; + case ShopItem::SuperRepel: + return "super-repel"; + case ShopItem::MaxRepel: + return "max-repel"; + case ShopItem::EscapeRope: + return "escape-rope"; + default: + return "unknown"; + } +} + +// {MaxRepel, 2, 4}, {FullRestore, 0, 3} → "4 Max repel, 3 Full restore" +static std::string make_buy_label(const std::vector& items){ + std::string label; + for (size_t i = 0; i < items.size(); i++){ + if (i > 0){ + label += ", "; + } + if (items[i].quantity == BUY_MAX){ + label += "max "; + }else{ + label += std::to_string(items[i].quantity) + " "; + } + label += shop_item_name(items[i].item); + } + return label; +} + +bool buy_items(VideoStream& stream, + ProControllerContext& context, + const std::vector& items, + Language language){ + if (items.empty()){ + return true; + } + + const std::string label = make_buy_label(items); + context.wait_for_all_requests(); + pbf_wait(context, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + context.wait_for_all_requests(); + + + // ── Coordinate constants ───────────────────────────────────────────────── + + // Area where the selection arrow appears at the vendor greeting menu (Buy/Sell/Leave). + static const ImageFloatBox VENDOR_ARROW_BOX{0.630000, 0.550000, 0.070000, 0.120000}; + + // Area where the selection arrow appears at the YES/NO confirmation dialog. + static const ImageFloatBox CONFIRM_ARROW_BOX{0.700000, 0.620000, 0.070000, 0.120000}; + + // Full area containing all item rows in the shop list. + // The hover detector scans this region to dynamically locate the red outline. + static const ImageFloatBox ITEM_SCAN_AREA{0.417000, 0.100000, 0.540000, 0.625000}; + + static constexpr size_t MAX_OUTER = 50; + static constexpr size_t MAX_ITEM_SEARCH = 50; // max iterations per item before giving up + + // ── Outer state machine ────────────────────────────────────────────────── + for (size_t outer = 0; outer < MAX_OUTER; outer++){ + SelectionArrowFinder arrow_vendor(stream.overlay(), VENDOR_ARROW_BOX, COLOR_GREEN); + ShopBuyingWindowWatcher buying_window; + ShortDialogWatcher dialog(COLOR_RED); + + // case 0: no detectors fired — press A in a for-loop (5 presses) while watching. + stream.log("Starting to loop over items"); + int state = run_until(stream, + context, + [](ProControllerContext& ctx){ + for (int i = 0; i < 5; i++){ + pbf_press_button(ctx, BUTTON_A, 80ms, 1000ms); + } + }, + {{arrow_vendor}, {buying_window}, {dialog}}); + + context.wait_for_all_requests(); + pbf_wait(context, 500ms); + + switch (state){ + + case 0: // case 1: SelectionArrow at vendor menu → press A to choose "Buy" + stream.log("Found the selection arrow, pressing A to enter the buying window...", COLOR_GREEN); + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + break; + + case 1: // case 2: buying window open → buy each requested item + stream.log("Found the buying window, searching for the right item...", COLOR_GREEN); + { + bool buying_failed = false; + + for (const ItemToBuy& item : items){ + stream.log("Buying " + std::to_string(item.quantity) + "x " + shop_item_slug(item.item)); + if (buying_failed){ + break; + } + + // Detector fires only when the red outline is found AND both + // name and price OCR match the expected item slug. + ShopItemHoverDetector hover(stream.logger(), + ITEM_SCAN_AREA, + shop_item_slug(item.item), + language, + COLOR_CYAN); + ShopQuantityWindowWatcher qty_window; + SelectionArrowFinder confirm_arrow(stream.overlay(), CONFIRM_ARROW_BOX, COLOR_GREEN); + + bool item_bought = false; + bool hover_confirmed = false; + for (size_t inner = 0; inner < MAX_ITEM_SEARCH && !item_bought; inner++){ + context.wait_for_all_requests(); + + int sub = run_until(stream, + context, + [](ProControllerContext& ctx){ + ctx.wait_for(1000ms); + }, + {{confirm_arrow}, {qty_window}, {hover}}); + + context.wait_for_all_requests(); + + stream.log("buy_items: sub=" + std::to_string(sub), COLOR_WHITE); + + switch (sub){ + + case 0: // YES/NO confirmation arrow → confirm purchase + stream.log("Found the confirmation to buy arrow, pressing A to buy", COLOR_GREEN); + { + context.wait_for_all_requests(); + ShopBuyingWindowWatcher back_in_shop; + run_until(stream, + context, + [](ProControllerContext& ctx){ + for (int i = 0; i < 10; i++){ + pbf_press_button(ctx, BUTTON_A, 80ms, 800ms); + } + }, + {{back_in_shop}}); + item_bought = true; + break; + } + + case 1: // quantity window + if (!hover_confirmed){ + // Quantity window appeared before we confirmed the right item — + // an earlier A press accidentally opened the wrong item's selector. + // Press B to go back to the list and continue searching. + stream.log("Unexpected quantity window (wrong item), pressing B...", COLOR_YELLOW); + pbf_press_button(context, BUTTON_B, 80ms, 800ms); + break; + } + stream.log("Found the quantity window", COLOR_GREEN); + if (item.quantity == BUY_MAX){ + stream.log("Buying max quantity"); + pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); + }else{ + stream.log("Buying " + std::to_string(item.quantity) + " of the item"); + for (int q = 1; q < item.quantity; q++){ + pbf_press_dpad(context, DPAD_UP, 80ms, 300ms); + } + } + pbf_press_button(context, BUTTON_A, 80ms, 300ms); + pbf_wait(context, 1000ms); + break; + + case 2: // correct item highlighted → press A to open quantity selector + stream.log("Found the right item, pressing A to buy a certain quantity...", COLOR_GREEN); + hover_confirmed = true; + pbf_press_button(context, BUTTON_A, 80ms, 300ms); + pbf_wait(context, 1000ms); + break; + + default: // outline not found or wrong item → advance to next row + stream.log("Not on the right item, moving...", COLOR_GREEN); + pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); + pbf_wait(context, 300ms); + break; + } + } + + if (!item_bought){ + stream.log(label + ": buy_items: could not locate " + shop_item_name(item.item) + + " in shop list.", + COLOR_RED); + buying_failed = true; + } + } + + if (!buying_failed){ + stream.log("All items bought, exiting shop...", COLOR_GREEN); + pbf_mash_button(context, BUTTON_B, 5000ms); + return true; + } + break; + } + + case 2: // case 3: unexpected ShortDialog → press B until back in buying window + { + ShopBuyingWindowWatcher back_in_shop; + run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_B, 5000ms); + }, + {{back_in_shop}}); + break; + } + + default: // outer default: unrecognised state → press B to recover + pbf_press_button(context, BUTTON_B, 80ms, 3000ms); + break; + } + } + + stream.log(label + ": buy_items: failed to complete after max iterations.", COLOR_RED); + return false; +} + + +bool heal_pokemon(VideoStream& stream, ProControllerContext& context, const std::string& label){ + context.wait_for_all_requests(); + pbf_wait(context, 2000ms); + + pbf_move_left_joystick(context, {0, +1}, 1800ms, 100ms); // 7+ + context.wait_for_all_requests(); + + pbf_mash_button(context, BUTTON_A, 5000ms); + context.wait_for_all_requests(); + pbf_wait(context, 3000ms); + wait_for_dialogue(stream, context, label + " - Pokemon healing"); + mash_until_dialogue_ends(stream, context, BUTTON_B); + context.wait_for_all_requests(); + pbf_wait(context, 1000ms); + + // Exiting the Pokemon Center + BlackScreenOverWatcher black_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); + int ret = run_until(stream, + context, + [](ProControllerContext& context){ + pbf_move_left_joystick(context, {0, -1}, 15000ms, 100ms); + }, + {{black_screen}}); + if (ret < 0){ + stream.log("heal_pokemon(" + label + "): black screen not detected", COLOR_RED); + return false; + } + + return true; +} + +std::string fly_point_name(FlyPoint place){ + switch (place){ + case FlyPoint::CanalavCity: + return "canalave_city"; + case FlyPoint::CelesticTown: + return "celestic_town"; + case FlyPoint::EternaCity: + return "eterna_city"; + case FlyPoint::FloaromaTown: + return "floaroma_town"; + case FlyPoint::HearthomeCity: + return "hearthome_city"; + case FlyPoint::JubilifeCity: + return "jubilife_city"; + case FlyPoint::OreburghCity: + return "oreburgh_city"; + case FlyPoint::PastoriaCity: + return "pastoria_city"; + case FlyPoint::PokemonLeagueLower: + return "pokemon_league_lower"; + case FlyPoint::PokemonLeagueUpper: + return "pokemon_league_upper"; + case FlyPoint::Route221: + return "route_221"; + case FlyPoint::SandgemTown: + return "sandgem_town"; + case FlyPoint::SolaceonTown: + return "solaceon_town"; + case FlyPoint::SnowpointCity: + return "snowpoint_city"; + case FlyPoint::SunyshoreCity: + return "sunyshore_city"; + case FlyPoint::TwinleafTown: + return "twinleaf_town"; + case FlyPoint::VeilstoneCity: + return "veilstone_city"; + case FlyPoint::CurrentCity: + return "current_city"; + } + return "unknown"; +} + +bool fly_to(VideoStream& stream, ProControllerContext& context, FlyPoint place){ + // Each city branch below should update this to the city icon's position on the fly map. + // Detection: when the cursor brackets overlap the icon, its normally uniform + // color becomes non-uniform (stddev rises). Box stays as a rough fallback + // until each city is calibrated. + ImageFloatBox city_icon_box{0.700000, 0.550000, 0.100000, 0.120000}; + + DpadState dpad; + context.wait_for_all_requests(); + pbf_wait(context, 1000ms); + open_menu(stream, context, MenuCursorPosition::MAP, 8); + + if (place == FlyPoint::CurrentCity){ + ImageFloatBox button_a_box{0.540000, 0.960000, 0.070000, 0.035000}; + struct FlyAButtonDetector : VisualInferenceCallback { + ImageFloatBox m_box; + FlyAButtonDetector(const ImageFloatBox& box) + : VisualInferenceCallback("FlyAButtonDetector") + , m_box(box){ + } + void make_overlays(VideoOverlaySet&) const override { + } + bool process_frame(const ImageViewRGB32& frame, WallClock) override { + ImageStats s = image_stats(extract_box_reference(frame, m_box)); + return s.stddev.sum() > 30.0; + } + }; + FlyAButtonDetector watcher(button_a_box); + int ret = wait_until(stream, context, 3000ms, {{watcher}}); + if (ret < 0){ + stream.log("CurrentCity: A button not found.", COLOR_RED); + return false; + } + BlackScreenOverWatcher flying_black_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); + int fly_ret = run_until(stream, + context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_A, 200000ms); + }, + {{flying_black_screen}}); + if (fly_ret < 0){ + stream.log("CurrentCity: black screen not detected!", COLOR_RED); + return false; + } + stream.log("CurrentCity: black screen detected, arrived.", COLOR_GREEN); + return true; + } + + switch (place){ + case FlyPoint::CelesticTown: + pbf_move_left_joystick(context, {-1, +1}, 4000ms, 500ms); + break; + case FlyPoint::HearthomeCity: + case FlyPoint::OreburghCity: + pbf_move_left_joystick(context, {-1, -1}, 4000ms, 500ms); + break; + case FlyPoint::PastoriaCity: + case FlyPoint::PokemonLeagueLower: + case FlyPoint::PokemonLeagueUpper: + case FlyPoint::SolaceonTown: + case FlyPoint::SunyshoreCity: + case FlyPoint::VeilstoneCity: + pbf_move_left_joystick(context, {+1, -1}, 4000ms, 500ms); + break; + case FlyPoint::SnowpointCity: + pbf_move_left_joystick(context, {-1, +1}, 4000ms, 500ms); + break; + case FlyPoint::TwinleafTown: + pbf_move_left_joystick(context, {-1, -1}, 4000ms, 500ms); + break; + default: + pbf_move_left_joystick(context, {-1, -1}, 4000ms, 500ms); + break; + } + context.wait_for_all_requests(); + + ImageFloatBox button_a_box{0.540000, 0.960000, 0.070000, 0.035000}; + + struct FlyAButtonDetector : VisualInferenceCallback { + ImageFloatBox m_box; + FlyAButtonDetector(const ImageFloatBox& box) + : VisualInferenceCallback("FlyAButtonDetector") + , m_box(box){ + } + void make_overlays(VideoOverlaySet&) const override { + } + bool process_frame(const ImageViewRGB32& frame, WallClock) override { + ImageStats s = image_stats(extract_box_reference(frame, m_box)); + // Blue stripe is uniform when no button shown; A button disrupts it + return s.stddev.sum() > 30.0; + } + }; + FlyAButtonDetector watcher_1(button_a_box); + + switch (place){ + case FlyPoint::CanalavCity: + break; + case FlyPoint::CelesticTown: + city_icon_box = {0.660000, 0.470000, 0.012000, 0.020000}; + repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 15, false); + repeat_dpad(context, dpad, DPAD_DOWN, 80ms, 300ms, 13, false); + break; + case FlyPoint::EternaCity: + break; + case FlyPoint::FloaromaTown: + break; + case FlyPoint::HearthomeCity: + city_icon_box = {0.660000, 0.637000, 0.029000, 0.052000}; + repeat_dpad(context, dpad, DPAD_LEFT, 80ms, 300ms, 16, false); + repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 7, false); + break; + case FlyPoint::JubilifeCity: + city_icon_box = {0.453000, 0.702000, 0.016000, 0.054000}; + repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 4, false); + repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 5, false); + break; + case FlyPoint::OreburghCity: + city_icon_box = {0.566000, 0.702000, 0.012000, 0.054000}; + repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 10, false); + repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 5, false); + break; + case FlyPoint::PastoriaCity: + break; + case FlyPoint::PokemonLeagueLower: + break; + case FlyPoint::PokemonLeagueUpper: + break; + case FlyPoint::Route221: + break; + case FlyPoint::SandgemTown: + break; + case FlyPoint::SolaceonTown: + city_icon_box = {0.735000, 0.603000, 0.030000, 0.022000}; + repeat_dpad(context, dpad, DPAD_LEFT, 80ms, 300ms, 12, false); + repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 9, false); + break; + case FlyPoint::SnowpointCity: + city_icon_box = {0.602000, 0.070000, 0.013000, 0.054000}; + repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 12, false); + repeat_dpad(context, dpad, DPAD_DOWN, 80ms, 300ms, 1, false); + break; + case FlyPoint::SunyshoreCity: + break; + case FlyPoint::TwinleafTown: + city_icon_box = {0.415000, 0.870000, 0.013000, 0.022000}; + repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 2, false); + repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 1, false); + break; + case FlyPoint::VeilstoneCity: + city_icon_box = {0.829000, 0.537000, 0.017000, 0.054000}; + repeat_dpad(context, dpad, DPAD_LEFT, 80ms, 300ms, 8, false); + repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 10, false); + break; + case FlyPoint::CurrentCity: + break; + } + + context.wait_for_all_requests(); + + struct CityIconDetector : VisualInferenceCallback { + ImageFloatBox m_box; + CityIconDetector(const ImageFloatBox& box) + : VisualInferenceCallback("CityIconDetector") + , m_box(box){ + } + void make_overlays(VideoOverlaySet&) const override { + } + bool process_frame(const ImageViewRGB32& frame, WallClock) override { + ImageStats s = image_stats(extract_box_reference(frame, m_box)); + // City icon is normally a uniform color; cursor brackets disrupt it + return s.stddev.sum() > 30.0; + } + }; + CityIconDetector watcher_2(city_icon_box); + + const std::string name = fly_point_name(place); + int ret_1 = wait_until(stream, context, 3000ms, {{watcher_1}}); + int ret_2 = wait_until(stream, context, 4000ms, {{watcher_2}}); + if (ret_1 < 0){ + stream.log("A button not found, retrying...", COLOR_RED); + }; + if (ret_2 < 0){ + stream.log(name + " not found, retrying...", COLOR_RED); + }; + if (ret_1 == 0 and ret_2 == 0){ + stream.log("Found " + name + ", flying towards it.", COLOR_GREEN); + WhiteScreenOverWatcher flying_white_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); + BlackScreenOverWatcher flying_black_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); + int ret = run_until(stream, + context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_A, 200000ms); + }, + {{flying_white_screen}, {flying_black_screen}}); + if (ret < 0){ + stream.log("Flying to " + name + ": white / black screen not detected!", COLOR_RED); + return false; + }else if (ret >= 0){ + stream.log("Flying to " + name + ": white / black screen detected!", COLOR_GREEN); + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- +// Arrow - Based Potion Usage Helper + +// This version assumes : +// +// you are already inside the Bag +// Potion has already been selected +// the game is currently waiting for you to choose a Pokemon +// +// Flow : +// 1. Move LEFT once to Pokemon list +// 2. Press A on first Pokemon +// 3. Use SelectionArrow to detect arrow position +// 4. Move DOWN until arrow reaches slot 3 +// 5. Mash A for 3 seconds +// 6. Mash B for 10 seconds +// +// --------------------------------------------------------------------------- + +// Slot 1's colored HP bar in the Bag. Read fill position rather than OCR. +static double potion_hp_fraction(const ImageViewRGB32& frame){ + if (bag_pocket_position(frame) == 0){ + return -1; + } + const ImageViewRGB32 bar = extract_box_reference(frame, ImageFloatBox{0.137, 0.160, 0.202, 0.016}); + size_t filled = 0; + for (size_t x = 0; x < bar.width(); x++){ + size_t color = 0; + for (size_t y = 0; y < bar.height(); y++){ + uint32_t p = bar.pixel(x, y); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + const bool green = g > r + 30 && g > b + 20; + const bool yellow = r > 150 && g > 120 && b < 100; + const bool red = r > 150 && r > g + 50 && r > b + 50; + color += green || yellow || red; + } + filled += color > bar.height() / 4; + } + return bar.width() ? (double)filled / bar.width() : -1; +} + +static double confirmed_potion_hp(VideoStream& stream, ProControllerContext& context){ + double previous = -1; + size_t stable = 0; + for (size_t check = 0; check < 20; check++){ + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + double hp = frame ? potion_hp_fraction(frame) : -1; + if (hp >= 0 && previous >= 0 && std::abs(hp - previous) <= 0.015){ + if (++stable >= 3){ + return hp; + } + }else{ + stable = 0; + } + previous = hp; + pbf_wait(context, 150ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Slot 1 HP bar could not be confirmed.", + stream); +} + +bool bag_has_item(VideoStream& stream, ProControllerContext& context, const std::string& name, int target_pocket) { + const bool found = find_bag_item_by_name(stream, context, name, target_pocket, 5); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Bag: Did not return to the overworld after looking for " + name + ".", stream); + } + return found; +} + +void use_potion_first_pokemon(VideoStream& stream, ProControllerContext& context, double heal_below) { + OverlayBoxScope hp_overlay(stream.overlay(), COLOR_GREEN, {0.137, 0.160, 0.202, 0.016}, ""); + bool healed = false; + for (size_t dose = 0; dose < 3; dose++){ + stream.log("[Potion] Finding Bag/Medicine/Potion; dose " + std::to_string(dose + 1) + "/3."); + if (!find_bag_item_by_name(stream, context, "potion", 1, 5)) { + if (dose == 0 && heal_below < 1.0) { + // Only a check: without Potions there is nothing to do here. + stream.log("[Potion] No Potion in the Bag; skipping the HP check.", COLOR_ORANGE); + healed = true; + break; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Could not verify Potion in the Medicine pocket.", + stream); + } + const double before = confirmed_potion_hp(stream, context); + stream.log("[Potion] Slot 1 HP bar before use=" + std::to_string(before * 100) + "%."); + if (before >= 0.98) { healed = true; break; } + if (dose == 0 && before >= heal_below) { + stream.log("[Potion] Slot 1 HP is at least " + std::to_string(heal_below * 100) + "%; no Potion needed."); + healed = true; + break; + } + if (before <= 0.005) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Slot 1 appears fainted; a Potion cannot revive it.", + stream); + } + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Did not return to the overworld after use.", + stream); + } + // Reopen Bag to verify the result, including when the last Potion was used. + open_menu(stream, context, MenuCursorPosition::BAG, 5); + pbf_wait(context, 750ms); + context.wait_for_all_requests(); + const double after = confirmed_potion_hp(stream, context); + stream.log("[Potion] Slot 1 HP bar after use=" + std::to_string(after * 100) + "%."); + if (after <= before + 0.005){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Slot 1 HP did not increase; refusing to continue movement.", + stream); + } + if (after >= 0.98){ + healed = true; + break; + } + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + } + if (!healed){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Full HP not confirmed after three uses.", + stream); + } + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Final overworld return not confirmed.", + stream); + } + stream.log("[Potion] Slot 1 HP bar full and overworld confirmed."); +} + + +namespace { + +struct IconGrid { + int row, col; +}; + +IconGrid icon_to_grid(MenuCursorPosition pos, int num_icons){ + switch (pos){ + case MenuCursorPosition::POKEDEX: + return {0, 0}; + case MenuCursorPosition::POKEMON: + return {0, 1}; + case MenuCursorPosition::BAG: + return {0, 2}; + case MenuCursorPosition::CARD: + return {0, 3}; + case MenuCursorPosition::OPTIONS: + return {1, num_icons <= 5 ? 0 : 2}; + case MenuCursorPosition::MAP: + return {1, 0}; + case MenuCursorPosition::CAPSULES: + return {1, 1}; + case MenuCursorPosition::MYSTERYGIFT: + return {1, 3}; + default: + return {-1, -1}; + } +} + +} // anonymous namespace + + +void open_menu(VideoStream& stream, ProControllerContext& context, MenuCursorPosition target, int num_icons){ + using namespace std::chrono_literals; + + // Previous single opening attempt: + // context.wait_for_all_requests(); + // overworld_to_menu(stream, context); + // pbf_wait(context, 1000ms); + // context.wait_for_all_requests(); + context.wait_for_all_requests(); + + MenuCursorDetector detector; + { + static const char* ICON_NAMES[MENU_CURSOR_NUM_ICONS] = { + "POKEDEX", + "POKEMON", + "BAG", + "CARD", + "OPTIONS", + "MAP", + "CAPSULES", + "MYSTERYGIFT", + }; + // Previous optional template directory: PokemonBDSP/AutoStory. + std::string ref_dir = RESOURCE_PATH() + "PokemonBDSP/Autostory"; + for (int i = 0; i < MENU_CURSOR_NUM_ICONS; i++){ + std::string path = ref_dir + "/" + ICON_NAMES[i] + ".png"; + // These templates are optional. The dark-pixel cursor detector + // remains available when a template is not packaged. + if (!Filesystem::is_regular_file(path)){ + continue; + } + // Previously constructed the image before checking its availability. + ImageRGB32 img(path); + if (img){ + detector.set_icon_reference(static_cast(i), std::move(img)); + } + } + } + // Previous single detection attempt retained for comparison. + // MenuCursorPosition cur = MenuCursorPosition::UNKNOWN; + // for (int i = 0; i < 10 && cur == MenuCursorPosition::UNKNOWN; i++){ + // context.wait_for(50ms); + // VideoSnapshot snap = stream.video().snapshot(); + // if (snap) cur = detector.detect(snap); + // } + // if (cur == MenuCursorPosition::UNKNOWN){ + // OperationFailedExceptionWithScreenshot::fire( + // ErrorReportMode::SEND_ERROR_REPORT, + // "Menu cursor not detected after opening menu.", + // stream + // ); + // } + + MenuCursorPosition cur = MenuCursorPosition::UNKNOWN; + bool menu_confirmed_open = false; + for (size_t attempt = 1; attempt <= 3; attempt++){ + stream.log("[Open menu] Open and detect attempt " + std::to_string(attempt) + "/3."); + try { + // A late-opening menu may already be visible after a timeout. + // Check before pressing X again, since X would close that menu. + if (!menu_confirmed_open){ + MenuWatcher already_open; + if (wait_until(stream, context, 300ms, {{already_open}}) < 0){ + overworld_to_menu(stream, context); + }else{ + stream.log("[Open menu] Menu already visible; skipping X."); + } + menu_confirmed_open = true; + }else{ + // Cursor detection retries send no input that could close the + // confirmed menu. Keep observing it instead of toggling X. + stream.log("[Open menu] Retrying cursor detection in the open menu."); + } + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + for (int frame = 0; frame < 10 && cur == MenuCursorPosition::UNKNOWN; frame++){ + context.wait_for(50ms); + const VideoSnapshot snapshot = stream.video().snapshot(); + if (snapshot){ + cur = detector.detect(snapshot); + } + } + if (cur != MenuCursorPosition::UNKNOWN){ + stream.log("[Open menu] Menu and cursor detected on attempt " + std::to_string(attempt) + ".", + COLOR_GREEN); + break; + } + stream.log("[Open menu] Menu cursor not detected on attempt " + std::to_string(attempt) + ".", + COLOR_ORANGE); + } catch (OperationFailedException& error){ + stream.log("[Open menu] Attempt " + std::to_string(attempt) + " failed: " + error.message(), COLOR_ORANGE); + } + } + if (cur == MenuCursorPosition::UNKNOWN){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Menu or cursor detection failed after 3 opening attempts.", + stream); + } + + IconGrid cur_g = icon_to_grid(cur, num_icons); + IconGrid tgt_g = icon_to_grid(target, num_icons); + if (tgt_g.row == -1){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Unknown target menu icon.", + stream); + } + + if (cur_g.row != tgt_g.row){ + DpadPosition vert = (cur_g.row < tgt_g.row) ? DPAD_DOWN : DPAD_UP; + pbf_press_dpad(context, vert, 80ms, 300ms); + context.wait_for_all_requests(); + } + + int col_diff = tgt_g.col - cur_g.col; + if (col_diff != 0){ + DpadPosition horiz = (col_diff > 0) ? DPAD_RIGHT : DPAD_LEFT; + for (int i = 0; i < std::abs(col_diff); i++){ + pbf_press_dpad(context, horiz, 80ms, 300ms); + context.wait_for_all_requests(); + } + } + + pbf_press_button(context, BUTTON_A, 80ms, 200ms); +} + + +// Search the selected item's fixed description header rather than guessing +// which list row is selected. This remains in place while the list scrolls. +// A cursor parked at row 2 or 6 does not indicate a list end: only repeated +// unchanged item headers after movement are used to detect an end. +// Previous selected-item-only search, retained for reference. +// bool find_bag_item( +// VideoStream& stream, +// ProControllerContext& context, +// const BagItemTemplate& item, +// int num_icons, +// size_t max_scrolls_per_pocket, +// std::chrono::milliseconds search_timeout +// ){ +// auto reference = std::make_shared( +// RESOURCE_PATH() + item.reference_path +// ); +// ImageMatchDetector icon(reference, item.icon_box, item.max_icon_rmsd, true); +// ImageMatchDetector name(reference, item.name_box, item.max_name_rmsd, true); +// ImageMatchDetector bag(reference, ImageFloatBox(0.055, 0.012, 0.044, 0.045), 35.0, true); +// const ImageFloatBox selected_header(0.428, 0.754, 0.382, 0.046); +// const auto deadline = std::chrono::steady_clock::now() + search_timeout; +// +// stream.log("[Bag search] Looking for " + item.label + "."); +// open_menu(stream, context, MenuCursorPosition::BAG, num_icons); +// pbf_wait(context, 1000ms); +// context.wait_for_all_requests(); +// +// for (size_t pocket = 0; pocket < 8; pocket++){ +// ImageRGB32 initial_header; +// ImageRGB32 previous_header; +// size_t unchanged = 0; +// size_t matching_frames = 0; +// size_t scrolls = 0; +// bool scanning_up = false; +// +// while (std::chrono::steady_clock::now() < deadline){ +// context.wait_for_all_requests(); +// const VideoSnapshot frame = stream.video().snapshot(); +// if (!frame || !bag.detect(frame)){ +// stream.log("[Bag search] Bag screen not detected.", COLOR_ORANGE); +// return false; +// } +// const double icon_score = icon.rmsd(frame); +// const double name_score = item.check_name ? name.rmsd(frame) : 0.0; +// stream.log( +// "[Bag search] Pocket " + std::to_string(pocket + 1) + +// ", scroll " + std::to_string(scrolls) + +// ": icon=" + std::to_string(icon_score) + +// ", name=" + std::to_string(name_score) +// ); +// if (icon_score <= item.max_icon_rmsd && +// name_score <= item.max_name_rmsd){ +// if (++matching_frames >= 3){ +// stream.log("[Bag search] " + item.label + " confirmed.", COLOR_GREEN); +// return true; +// } +// pbf_wait(context, 150ms); +// continue; +// } +// matching_frames = 0; +// +// const ImageRGB32 header = extract_box_reference(frame, selected_header).scale_to(382, 46); +// if (!initial_header){ +// initial_header = header.copy(); +// } +// else if (scrolls > 0){ +// const bool same_as_previous = +// ImageMatch::pixel_RMSD(previous_header, header) <= 3.0; +// const bool returned_to_start = +// ImageMatch::pixel_RMSD(initial_header, header) <= 3.0; +// if (returned_to_start && !same_as_previous){ +// break; +// } +// // Repeated unchanged rows mean an empty pocket or list end. +// // Returning to the initial row means the list wrapped. +// unchanged = same_as_previous || returned_to_start ? unchanged + 1 : 0; +// if (unchanged >= 3){ +// if (scanning_up){ +// break; +// } +// // A non-wrapping list may have opened in the middle. +// // Search upward as well before moving to another pocket. +// scanning_up = true; +// unchanged = 0; +// initial_header = header.copy(); +// } +// } +// previous_header = header.copy(); +// if (scrolls >= max_scrolls_per_pocket){ +// stream.log("[Bag search] Scroll limit reached; search incomplete.", COLOR_ORANGE); +// return false; +// } +// scrolls++; +// pbf_press_dpad(context, scanning_up ? DPAD_UP : DPAD_DOWN, 80ms, 350ms); +// } +// if (std::chrono::steady_clock::now() >= deadline){ +// stream.log("[Bag search] Search timed out; search incomplete.", COLOR_ORANGE); +// return false; +// } +// if (pocket < 7){ +// pbf_press_dpad(context, DPAD_RIGHT, 80ms, 500ms); +// context.wait_for_all_requests(); +// } +// } +// stream.log("[Bag search] Item not found in any pocket.", COLOR_ORANGE); +// return false; +// } +// + +// One narrow strip on the left side of the red selection border. Divide its +// height into the seven row bands; the item's art and NEW badge are outside it. +int bag_cursor_position(const ImageViewRGB32& frame){ + if (!frame){ + return 0; + } + const ImageViewRGB32 strip = extract_box_reference(frame, ImageFloatBox(0.431, 0.229, 0.005, 0.470)); + double best = 0.0; + double second = 0.0; + int position = 0; + for (int row = 0; row < 7; row++){ + const size_t top = static_cast(strip.height() * (row * 0.075 / 0.470)); + const size_t bottom = static_cast(strip.height() * ((row * 0.075 + 0.020) / 0.470)); + size_t red = 0; + size_t total = 0; + for (size_t y = top; y < bottom && y < strip.height(); y++){ + for (size_t x = 0; x < strip.width(); x++){ + const uint32_t pixel = strip.pixel(x, y); + const unsigned r = (pixel >> 16) & 255; + const unsigned g = (pixel >> 8) & 255; + const unsigned b = pixel & 255; + red += r > 180 && g < 120 && b < 120; + total++; + } + } + const double score = total == 0 ? 0.0 : static_cast(red) / total; + if (score > best){ + second = best; + best = score; + position = row + 1; + }else if (score > second){ + second = score; + } + } + return best >= 0.15 && best - second >= 0.10 ? position : 0; +} + +static ImageFloatBox bag_list_icon_box(size_t row){ + return ImageFloatBox(0.449, 0.212 + (row - 1) * 0.075, 0.033, 0.050); +} + +static ImageFloatBox bag_list_name_box(size_t row){ + // Previous height included the faint lower selection border in rows 2/6. + // return ImageFloatBox(0.492, 0.213 + (row - 1) * 0.075, 0.335, 0.052); + return ImageFloatBox(0.492, 0.213 + (row - 1) * 0.075, 0.335, 0.050); +} + +static double bag_row_patch_score(const ImageRGB32& reference, const ImageViewRGB32& frame, const ImageFloatBox& box){ + const ImageRGB32 live = extract_box_reference(frame, box).scale_to(reference.width(), reference.height()); + return ImageMatch::pixel_RMSD(reference, live); +} + +// Previous name-area empty-list check retained for reference. +// static bool bag_list_is_empty(const ImageViewRGB32& frame){ +// const ImageViewRGB32 names = extract_box_reference( +// frame, ImageFloatBox(0.492, 0.213, 0.335, 0.500) +// ); +// size_t dark = 0; +// for (size_t y = 0; y < names.height(); y++){ +// for (size_t x = 0; x < names.width(); x++){ +// const uint32_t pixel = names.pixel(x, y); +// dark += ((pixel >> 16) & 255) < 100 && +// ((pixel >> 8) & 255) < 100 && (pixel & 255) < 100; +// } +// } +// return dark < names.width() * names.height() * 0.001; +// } + +size_t bag_clear_tab_count(const ImageViewRGB32& frame){ + if (!frame || bag_pocket_position(frame) == 0){ + return 0; + } + static const std::vector> references = [] { + std::vector> refs; + for (const char* name : {"Normal", + "NormalMedium", + "NormalSmall", + "Alternate", + "AlternateMedium", + "AlternateSmall", + "Selected", + "SelectedMedium", + "SelectedSmall", + "SelectedBlue", + "SelectedBlueMedium", + "SelectedBlueSmall"}){ + refs.emplace_back( + std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/BagTabs/" + name + ".png")); + } + return refs; + }(); + size_t clear = 0; + for (size_t tab = 0; tab < 8; tab++){ + const double x = .570 + tab * .0421; + // A selected tab has a thin red outline. The NEW marker fills the + // interior of this upper-left patch, which the outline leaves white. + auto badge = extract_box_reference(frame, ImageFloatBox(x + .007, .074, .006, .014)); + size_t red = 0; + for (size_t y = 0; y < badge.height(); y++){ + for (size_t bx = 0; bx < badge.width(); bx++){ + const uint32_t pixel = badge.pixel(bx, y); + int rr = (pixel >> 16) & 255, gg = (pixel >> 8) & 255, bb = pixel & 255; + if (rr > 150 && rr > gg + 60 && rr > bb + 60){ + red++; + } + } + } + const size_t badge_area = badge.width() * badge.height(); + if (badge_area > 0 && (double)red / badge_area > .45){ + continue; + } + const ImageFloatBox box{x, .06, .038, .072}; + for (const auto& reference : references){ + ImageMatchDetector detector(reference, box, 50.0); + if (detector.detect(frame)){ + clear++; + break; + } + } + } + return clear; +} + +bool clear_new_item_flags(VideoStream& stream, ProControllerContext& context, int num_icons){ + stream.log("[Bag NEW flags] Opening bag to verify all eight tabs."); + open_menu(stream, context, MenuCursorPosition::BAG, num_icons); + pbf_wait(context, 1000ms); + auto all_clear = [&](){ + context.wait_for_all_requests(); + size_t confirmed = 0; + for (size_t check = 0; check < 3; check++){ + const auto frame = stream.video().snapshot(); + const size_t count = frame ? bag_clear_tab_count(*frame.frame) : 0; + stream.log("[Bag NEW flags] Clear tabs: " + std::to_string(count) + "/8."); + if (count != 8){ + return false; + } + confirmed++; + pbf_wait(context, 150ms); + context.wait_for_all_requests(); + } + return confirmed == 3; + }; + bool cleared = all_clear(); + for (size_t pocket = 0; pocket < 32 && !cleared; pocket++){ + if (pocket){ + pbf_press_dpad(context, DPAD_RIGHT, 80ms, 300ms); + } + // Let the initial selection and each subsequent item clear its NEW + // flag. Short taps avoid skipping rows through held-button repeat. + pbf_wait(context, 1000ms); + for (size_t tap = 0; tap < 12; tap++){ + pbf_press_dpad(context, DPAD_DOWN, 80ms, 1000ms); + } + cleared = all_clear(); + } + pbf_press_dpad(context, DPAD_UP, 3000ms, 250ms); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + stream.log(cleared ? "[Bag NEW flags] All eight tabs clear; bag closed." + : "[Bag NEW flags] Full pocket pass did not clear all eight tabs; bag closed.", + cleared ? COLOR_GREEN : COLOR_ORANGE); + return cleared; +} + +int bag_pocket_position(const ImageViewRGB32& frame){ + if (!frame){ + return 0; + } + int selected = 0; + for (int pocket = 1; pocket <= 8; pocket++){ + // The lower left edge of each tab is below the red NEW badge. + // This strip includes both the normal outline and enlarged selection. + const auto edge = + extract_box_reference(frame, ImageFloatBox(0.570 + (pocket - 1) * 0.0421, 0.098, 0.0045, 0.021)); + size_t colored = 0; + for (size_t y = 0; y < edge.height(); y++){ + for (size_t x = 0; x < edge.width(); x++){ + const uint32_t pixel = edge.pixel(x, y); + const int r = (pixel >> 16) & 255; + const int g = (pixel >> 8) & 255; + const int b = pixel & 255; + // Red when focused, blue when the item list has focus. + colored += (r > 160 && r > g + 60 && r > b + 60) || (b > 160 && b > r + 40 && b > g + 20); + } + } + const double pixels = (double)edge.width() * edge.height(); + if (pixels > 0 && colored / pixels >= 0.18){ + if (selected != 0){ + return 0; + } + selected = pocket; + } + } + return selected; +} + +bool bag_list_is_empty(const ImageViewRGB32& frame){ + if (!frame || bag_pocket_position(frame) == 0 || bag_cursor_position(frame) != 0){ + return false; + } + // Empty pockets have no item icons. The central Empty label is outside + // this column; counting dark pixels in the name area included that label. + const auto icons = extract_box_reference(frame, ImageFloatBox(0.449, 0.213, 0.032, 0.500)); + const auto stats = image_stats(icons); + return stats.average.r > 220 && stats.average.g > 220 && stats.average.b > 220 && stats.stddev.sum() < 20; +} + + +std::array +match_bag_item_rows(const ImageViewRGB32& frame, const ImageViewRGB32& reference, const BagItemTemplate& item){ + std::array result; + if (!frame || !reference || item.reference_row < 1 || item.reference_row > 7){ + return result; + } + const ImageRGB32 icon = extract_box_reference(reference, bag_list_icon_box(item.reference_row)).copy(); + const ImageRGB32 name = extract_box_reference(reference, bag_list_name_box(item.reference_row)).copy(); + for (size_t row = 1; row <= 7; row++){ + auto& match = result[row - 1]; + match.icon_rmsd = bag_row_patch_score(icon, frame, bag_list_icon_box(row)); + match.name_rmsd = item.check_name ? bag_row_patch_score(name, frame, bag_list_name_box(row)) : 0.0; + match.matched = match.icon_rmsd <= item.max_icon_rmsd && match.name_rmsd <= item.max_name_rmsd; + } + return result; +} + +BagMovePlan plan_bag_item_move(int cursor_row, int item_row){ + if (cursor_row < 1 || cursor_row > 7 || item_row < 0 || item_row > 7){ + return {}; + } + if (item_row != 0){ + if (item_row == cursor_row){ + return {BagMoveAction::CONFIRM, 0}; + } + return {item_row > cursor_row ? BagMoveAction::DOWN : BagMoveAction::UP, + static_cast(std::abs(item_row - cursor_row))}; + } + if (cursor_row == 7){ + return {BagMoveAction::REWIND, 1}; + } + return {BagMoveAction::PAGE_DOWN, cursor_row == 1 ? 13 : static_cast(13 - cursor_row)}; +} + + +static int bag_confirm_pocket(VideoStream& stream, ProControllerContext& context){ + int previous = 0; + size_t matches = 0; + for (size_t check = 0; check < 10; check++){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const int pocket = frame ? bag_pocket_position(frame) : 0; + matches = pocket != 0 && pocket == previous ? matches + 1 : pocket != 0 ? 1 : 0; + previous = pocket; + if (matches >= 3){ + return pocket; + } + pbf_wait(context, 100ms); + } + return 0; +} + +// static bool bag_advance_pocket(VideoStream& stream, ProControllerContext& context, int current){ +// const int expected = current % 8 + 1; +// for (size_t attempt = 0; attempt < 3; attempt++){ +// // Recheck before retrying: a delayed change must not cause a second step. +// const int observed = bag_confirm_pocket(stream, context); +// if (observed == expected) return true; +// if (observed != current){ +// stream.log("[Bag search] Pocket selection uncertain; stopping.", COLOR_ORANGE); +// return false; +// } +// pbf_press_dpad(context, DPAD_RIGHT, 80ms, 500ms); +// const int result = bag_confirm_pocket(stream, context); +// stream.log("[Bag search] Pocket change: " + std::to_string(current) + " -> " + +// std::to_string(result) + ", expected " + std::to_string(expected) + "."); +// if (result == expected) return true; +// } +// stream.log("[Bag search] Could not confirm pocket change after 3 attempts.", COLOR_ORANGE); +// return false; +// } + +BagPocketMovePlan plan_bag_pocket_move(int current, int target){ + if (current < 1 || current > 8 || target < 1 || target > 8){ + return {}; + } + const int right = (target - current + 8) % 8; + const int left = (current - target + 8) % 8; + if (right == 0){ + return {BagPocketAction::CONFIRM, 0}; + } + return right <= left ? BagPocketMovePlan{BagPocketAction::RIGHT, static_cast(right)} + : BagPocketMovePlan{BagPocketAction::LEFT, static_cast(left)}; +} + +static bool bag_navigate_to_pocket(VideoStream& stream, ProControllerContext& context, int target){ + int current = bag_confirm_pocket(stream, context); + for (size_t attempt = 0; attempt < 3; attempt++){ + const auto plan = plan_bag_pocket_move(current, target); + if (plan.action == BagPocketAction::CONFIRM){ + return true; + } + if (plan.action == BagPocketAction::UNKNOWN){ + stream.log("[Bag search] Selected pocket not detected.", COLOR_ORANGE); + return false; + } + stream.log("[Bag search] Pocket " + std::to_string(current) + " to " + std::to_string(target) + ": " + + (plan.action == BagPocketAction::RIGHT ? "right " : "left ") + std::to_string(plan.presses) + + " presses, attempt " + std::to_string(attempt + 1) + "/3."); + for (size_t press = 0; press < plan.presses; press++){ + pbf_press_dpad(context, plan.action == BagPocketAction::RIGHT ? DPAD_RIGHT : DPAD_LEFT, 80ms, 150ms); + } + pbf_wait(context, 350ms); + current = bag_confirm_pocket(stream, context); + stream.log( + "[Bag search] Final pocket=" + std::to_string(current) + ", expected=" + std::to_string(target) + "."); + if (current == target){ + return true; + } + } + stream.log("[Bag search] Target pocket not reached after 3 movement batches.", COLOR_ORANGE); + return false; +} + + +bool find_bag_item(VideoStream& stream, + ProControllerContext& context, + const BagItemTemplate& item, + int target_pocket, + int num_icons, + size_t max_scrolls_per_pocket, + std::chrono::milliseconds search_timeout){ + if (target_pocket < 1 || target_pocket > 8 || item.reference_row < 1 || item.reference_row > 7){ + stream.log("[Bag search] Invalid template reference row.", COLOR_ORANGE); + return false; + } + auto reference = std::make_shared(RESOURCE_PATH() + item.reference_path); + // const ImageRGB32 reference_icon = extract_box_reference( + // *reference, bag_list_icon_box(item.reference_row) + // ).copy(); + // const ImageRGB32 reference_name = extract_box_reference( + // *reference, bag_list_name_box(item.reference_row) + // ).copy(); + ImageMatchDetector bag(reference, ImageFloatBox(0.055, 0.012, 0.044, 0.045), 35.0, true); + const auto deadline = std::chrono::steady_clock::now() + search_timeout; + + open_menu(stream, context, MenuCursorPosition::BAG, num_icons); + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + + if (!bag_navigate_to_pocket(stream, context, target_pocket)){ + return false; + } + // Previously searched all eight pockets. Only inspect the requested pocket now. + // for (size_t pocket = 0; pocket < 8; pocket++){ + { + const int selected_pocket = bag_confirm_pocket(stream, context); + if (selected_pocket == 0){ + stream.log("[Bag search] Selected pocket not detected.", COLOR_ORANGE); + return false; + } + size_t commands = 0; + bool empty = false; + auto move = [&](DpadPosition direction, size_t count, bool check_rows = false){ + for (size_t press = 0; press < count; press++){ + if (commands >= max_scrolls_per_pocket || std::chrono::steady_clock::now() >= deadline){ + stream.log("[Bag search] Limit reached; search incomplete.", COLOR_ORANGE); + return false; + } + commands++; + pbf_press_dpad(context, direction, 80ms, 150ms); + if (check_rows){ + context.wait_for_all_requests(); + const VideoSnapshot page_frame = stream.video().snapshot(); + if (!page_frame || !bag.detect(page_frame)){ + return false; + } + const auto matches = match_bag_item_rows(page_frame, *reference, item); + for (const auto& match : matches){ + if (match.matched){ + // Stop paging; the main loop verifies and selects the row. + return true; + } + } + } + } + pbf_wait(context, 250ms); + context.wait_for_all_requests(); + return true; + }; + for (size_t pass = 0; pass < 3 && !empty; pass++){ + size_t matching_frames = 0; + size_t missing_cursor = 0; + size_t unchanged_pages = 0; + ImageRGB32 previous_page; + stream.log("[Bag search] Looking for " + item.label + ": pocket " + std::to_string(selected_pocket) + + ", pass " + std::to_string(pass + 1) + "/3."); + while (std::chrono::steady_clock::now() < deadline){ + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame || !bag.detect(frame)){ + stream.log("[Bag search] Bag screen not detected.", COLOR_ORANGE); + return false; + } + const int cursor = bag_cursor_position(frame); + int target = 0; + double best = 1000.0; + bool ambiguous = false; + // The seven list ROIs move neither with the cursor nor with + // scrolling. Each contains an icon patch and a name patch. + // Previous per-row loop retained for reference. + // for (size_t row = 1; row <= 7; row++){ + // const double icon = bag_row_patch_score(reference_icon, frame, bag_list_icon_box(row)); + // const double name = item.check_name + // ? bag_row_patch_score(reference_name, frame, bag_list_name_box(row)) : 0.0; + // if (icon <= item.max_icon_rmsd && name <= item.max_name_rmsd){ + // if (target != 0){ + // ambiguous = true; + // } + // if (icon + name < best){ + // target = static_cast(row); + // best = icon + name; + // } + // } + // } + const auto matches = match_bag_item_rows(frame, *reference, item); + for (size_t row = 1; row <= 7; row++){ + const auto& match = matches[row - 1]; + if (match.matched){ + if (target != 0){ + ambiguous = true; + } + if (match.icon_rmsd + match.name_rmsd < best){ + target = static_cast(row); + best = match.icon_rmsd + match.name_rmsd; + } + } + } + stream.log("[Bag search] " + item.label + (target == 0 ? " is not detected" : " is detected") + + " on screen; item row=" + std::to_string(target) + ", cursor row=" + std::to_string(cursor) + "."); + if (ambiguous){ + stream.log("[Bag search] Multiple rows match; selection is uncertain.", COLOR_ORANGE); + return false; + } + if (cursor == 0){ + matching_frames = 0; + if (bag_list_is_empty(frame)){ + empty = true; + break; + } + if (++missing_cursor >= 3){ + stream.log("[Bag search] Cursor location uncertain.", COLOR_ORANGE); + return false; + } + pbf_wait(context, 200ms); + context.wait_for_all_requests(); + continue; + } + missing_cursor = 0; + if (target != 0){ + previous_page = ImageRGB32(); + unchanged_pages = 0; + // A unique list sprite/name match with the cursor on that + // row is sufficient; do not depend on the detail panel. + if (target == cursor){ + if (++matching_frames >= 3){ + stream.log("[Bag search] Item and cursor confirmed in row " + std::to_string(cursor) + ".", + COLOR_GREEN); + return true; + } + pbf_wait(context, 150ms); + context.wait_for_all_requests(); + continue; + } + matching_frames = 0; + const size_t count = plan_bag_item_move(cursor, target).presses; + stream.log("[Bag search] Moving " + std::string(target > cursor ? "down " : "up ") + + std::to_string(count) + " time(s), then checking again."); + if (!move(target > cursor ? DPAD_DOWN : DPAD_UP, count)){ + return false; + } + continue; + } + matching_frames = 0; + const ImageRGB32 page = + extract_box_reference(frame, ImageFloatBox(0.449, 0.212, 0.378, 0.500)).scale_to(378, 500); + if (previous_page && ImageMatch::pixel_RMSD(previous_page, page) <= 3.0){ + unchanged_pages++; + }else{ + unchanged_pages = 0; + } + previous_page = page.copy(); + // Row 7 signals the bottom of a long list. Repeated pages + // also cover shorter lists whose final row is less than 7. + if (cursor == 7 || unchanged_pages >= 2){ + stream.log("[Bag search] List end; returning to row 1."); + while (true){ + const VideoSnapshot rewind_frame = stream.video().snapshot(); + if (!rewind_frame || !bag.detect(rewind_frame)){ + return false; + } + const int rewind_cursor = bag_cursor_position(rewind_frame); + if (rewind_cursor == 1){ + break; + } + if (rewind_cursor == 0 || !move(DPAD_UP, 1)){ + return false; + } + } + break; + } + const size_t count = plan_bag_item_move(cursor, 0).presses; + stream.log("[Bag search] Advancing " + std::to_string(count) + " presses to another page."); + if (!move(DPAD_DOWN, count, true)){ + return false; + } + } + if (std::chrono::steady_clock::now() >= deadline){ + stream.log("[Bag search] Timeout; search incomplete.", COLOR_ORANGE); + return false; + } + } + // if (pocket < 7){ + // // pbf_press_dpad(context, DPAD_RIGHT, 80ms, 500ms); + // // context.wait_for_all_requests(); + // if (!bag_advance_pocket(stream, context, selected_pocket)) return false; + // } + } + stream.log("[Bag search] Item not found after three passes per nonempty pocket.", COLOR_ORANGE); + return false; +} + +bool find_bag_item_by_name( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket, + int num_icons +){ + open_menu(stream, context, MenuCursorPosition::BAG, num_icons); + pbf_wait(context, 1000ms); + return find_bag_item_in_open_bag(stream, context, name, target_pocket); +} + +bool find_bag_item_in_open_bag( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket +){ + context.wait_for_all_requests(); + if (!bag_navigate_to_pocket(stream, context, target_pocket)) return false; + + // No reader for the game's language: the cursor's row, the pocket's + // first, if the pocket has one. Early in the story the pocket holds only + // what was bought or given (Potions in Medicine). + if (!story_reads_text()){ + const VideoSnapshot frame = stream.video().snapshot(); + const bool any = frame && !bag_list_is_empty(*frame.frame) && bag_cursor_position(*frame.frame) != 0; + stream.log("[Bag search] " + name + ": no text reader for the game's language; " + + (any ? "taking the pocket's cursor row." : "the pocket is empty."), COLOR_ORANGE); + return any; + } + + // Walk down the list reading the cursor row. The list does not wrap, so + // turn around once the cursor stops moving. + DpadPosition direction = DPAD_DOWN; + std::string previous; + size_t same = 0; + size_t confirmed = 0; + size_t unknown_cursor = 0; + for (size_t scan = 0; scan < 128; scan++){ + context.wait_for_all_requests(); + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame || bag_list_is_empty(*frame.frame)){ + stream.log("[Bag search] " + name + ": pocket is empty.", COLOR_ORANGE); + return false; + } + const int cursor = bag_cursor_position(*frame.frame); + if (cursor == 0){ + if (++unknown_cursor >= 3){ + stream.log("[Bag search] " + name + ": cursor not found.", COLOR_ORANGE); + return false; + } + pbf_wait(context, 200ms); + continue; + } + unknown_cursor = 0; + const std::string text = OCR::tesseract_ocr_read(Language::English, + extract_box_reference(*frame.frame, bag_list_name_box(cursor))); + std::string normalized; + for (unsigned char ch : text){ + if (std::isalpha(ch)) normalized.push_back((char)std::tolower(ch)); + } + stream.log("[Bag search] Row " + std::to_string(cursor) + " reads \"" + normalized + "\"."); + if (normalized == name){ + if (++confirmed >= 2){ + stream.log("[Bag search] " + name + " confirmed in row " + std::to_string(cursor) + ".", COLOR_GREEN); + return true; + } + pbf_wait(context, 150ms); + continue; + } + confirmed = 0; + same = normalized == previous ? same + 1 : 0; + previous = normalized; + if (same >= 2){ + if (direction == DPAD_UP) break; + direction = DPAD_UP; + same = 0; + } + pbf_press_dpad(context, direction, 80ms, 300ms); + } + stream.log("[Bag search] " + name + " not found.", COLOR_ORANGE); + return false; +} + +bool use_escape_rope( + VideoStream& stream, + ProControllerContext& context, + int num_icons +){ + const BagItemTemplate rope{ + // "PokemonBDSP/AutoStory/Bag_EscapeRope.png", "escape-rope" + "PokemonBDSP/Autostory/Bag_EscapeRope.png", "escape-rope" + }; + if (!find_bag_item(stream, context, rope, 5, num_icons)){ + return false; + } + + pbf_press_button(context, BUTTON_A, 80ms, 350ms); + context.wait_for_all_requests(); + SelectionArrowFinder use_menu(stream.overlay(), ImageFloatBox(0.660, 0.100, 0.310, 0.430), COLOR_GREEN); + if (wait_until(stream, context, 3s, {{use_menu}}) != 0){ + stream.log("[Escape Rope] Item action menu not detected.", COLOR_ORANGE); + return false; + } + + // Watch while selecting Use so the transition cannot happen before + // the watcher starts. Extra A presses handle a confirmation prompt. + BlackScreenWatcher transition; + // Escape Rope fades white in the supplied clips; support black transitions too. + WhiteScreenWatcher white_transition; + const int result = run_until(stream, + context, + [](ProControllerContext& ctx){ + for (size_t press = 0; press < 10; press++){ + pbf_press_button(ctx, BUTTON_A, 80ms, 500ms); + } + ctx.wait_for_all_requests(); + pbf_wait(ctx, 10s); + ctx.wait_for_all_requests(); + }, + {transition, white_transition}); + if (result < 0){ + stream.log("[Escape Rope] No exit transition detected.", COLOR_ORANGE); + return false; + } + BlackScreenWatcher finished(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 100, 10, BlackScreenWatcher::FinderType::GONE, 500ms); + WhiteScreenWatcher white_finished(COLOR_RED, + {0.1, 0.1, 0.8, 0.8}, + 500, + 10, + WhiteScreenWatcher::FinderType::GONE, + 500ms); + stream.log(std::string("[Escape Rope] ") + (result == 1 ? "White" : "Black") + " exit transition detected."); + // if (wait_until(stream, context, 15s, {{finished}}) != 0){ return false; } + const int finish_result = result == 1 ? wait_until(stream, context, 15s, {{white_finished}}) + : wait_until(stream, context, 15s, {{finished}}); + if (finish_result != 0){ + stream.log("[Escape Rope] Exit transition did not finish.", COLOR_ORANGE); + return false; + } + OverworldWatcher overworld; + if (wait_until(stream, context, 10s, {{overworld}}) != 0){ + stream.log("[Escape Rope] Overworld not detected after transition.", COLOR_ORANGE); + return false; + } + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + stream.log("[Escape Rope] Exit transition and overworld confirmed.", COLOR_GREEN); + return true; +} + + +bool activate_repel(VideoStream& stream, ProControllerContext& context, int num_icons, int repel_type){ + static const char* const slugs[] = {"repel", "super-repel", "max-repel"}; + if (repel_type < 0 || repel_type > 2){ + stream.log("activate_repel: type must be 0 (Repel), 1 (Super Repel), or 2 (Max Repel).", COLOR_RED); + return false; + } + const std::string slug = slugs[repel_type]; + stream.log("Activating " + slug); + open_menu(stream, context, MenuCursorPosition::BAG, num_icons); + context.wait_for_all_requests(); + pbf_wait(context, 1000ms); + + auto repel_tab_ref = std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTab.png"); + // A smaller reference covers capture-resize sampling differences without + // loosening the match threshold or changing shared image processing. + auto repel_tab_small_ref = + std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTabSmall.png"); + // Obtaining Honey adds a NEW badge to the same selected Other Items tab. + // Keep the original references and threshold for the earlier route stages. + auto repel_tab_honey_ref = + std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTabHoney.png"); + auto repel_tab_honey_small_ref = + std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTabHoneySmall.png"); + const ImageFloatBox repel_tab_box{0.735000, 0.055000, 0.045000, 0.080000}; + const double repel_tab_rmsd = 50.0; + + // BDSP bag has 8 compartment tabs; check before each press so zero-press + // detection works when the bag already starts on the repel tab. + bool found = false; + for (int press = 0; press <= 8; press++){ + context.wait_for_all_requests(); + + ImageMatchWatcher watcher(repel_tab_ref, repel_tab_box, repel_tab_rmsd); + ImageMatchWatcher small_watcher(repel_tab_small_ref, repel_tab_box, repel_tab_rmsd); + ImageMatchWatcher honey_watcher(repel_tab_honey_ref, repel_tab_box, repel_tab_rmsd); + ImageMatchWatcher honey_small_watcher(repel_tab_honey_small_ref, repel_tab_box, repel_tab_rmsd); + int ret = + wait_until(stream, context, 400ms, {{watcher}, {small_watcher}, {honey_watcher}, {honey_small_watcher}}); + if (ret >= 0){ + stream.log("[AutoStory] activate_repel: repel tab found after " + std::to_string(press) + " press(es).", + COLOR_GREEN); + found = true; + break; + } + + if (press < 8){ + stream.log( + "[AutoStory] activate_repel: tab not found, pressing right (" + std::to_string(press + 1) + "/8)."); + pbf_press_dpad(context, DPAD_RIGHT, 80ms, 300ms); + } + } + + if (!found){ + stream.log("[AutoStory] activate_repel: failed to find repel compartment tab.", COLOR_RED); + return false; + } + + // Reuse the Bag cursor/name geometry and existing Tesseract backend. A later + // save may contain all three Repels and unrelated Other Items in any order. + bool item_found = false; + size_t confirmed = 0; + size_t unknown_cursor = 0; + for (size_t scan = 0; scan < 256; scan++){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + if (!frame || bag_list_is_empty(*frame.frame)){ + break; + } + const int cursor = bag_cursor_position(*frame.frame); + if (!cursor){ + if (++unknown_cursor >= 3){ + break; + } + pbf_wait(context, 200ms); + continue; + } + unknown_cursor = 0; + // Use the existing Tesseract entry point directly so item selection + // does not depend on the optional Paddle/ONNX runtime configuration. + const std::string text = + OCR::tesseract_ocr_read(Language::English, extract_box_reference(*frame.frame, bag_list_name_box(cursor))); + std::string normalized; + for (unsigned char ch : text){ + if (std::isalpha(ch)){ + normalized.push_back((char)std::tolower(ch)); + } + } + static const char* const names[] = {"repel", "superrepel", "maxrepel"}; + if (normalized == names[repel_type]){ + if (++confirmed >= 2){ + item_found = true; + break; + } + pbf_wait(context, 150ms); + }else{ + confirmed = 0; + // Search both directions so a non-wrapping list opened halfway + // down still reaches items above the initial cursor position. + pbf_press_dpad(context, scan < 128 ? DPAD_DOWN : DPAD_UP, 80ms, 300ms); + } + } + if (!item_found){ + stream.log("activate_repel: requested item not confirmed: " + slug, COLOR_RED); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + return false; + } + stream.log("[AutoStory] activate_repel: selected item confirmed: " + slug, COLOR_GREEN); + + context.wait_for_all_requests(); + pbf_wait(context, 400ms); + pbf_press_button(context, BUTTON_A, 80ms, 300ms); + context.wait_for_all_requests(); + + // The Use-item popup follows the selected row, which may be anywhere + // in a later save's list. Exclude the Bag list cursor on the left. + const ImageFloatBox repel_item_box{0.700000, 0.100000, 0.280000, 0.650000}; + SelectionArrowFinder arrow_finder(stream.overlay(), repel_item_box, COLOR_GREEN); + int arrow_ret = wait_until(stream, context, 2000ms, {{arrow_finder}}); + if (arrow_ret != 0){ + stream.log("[AutoStory] activate_repel: selection arrow not found after pressing A.", COLOR_RED); + return false; + } + stream.log("[AutoStory] activate_repel: selection arrow confirmed.", COLOR_GREEN); + pbf_press_button(context, BUTTON_A, 80ms, 105ms); + pbf_mash_button(context, BUTTON_B, 2000ms); + return true; +} + + +void use_HM(VideoStream& stream, ProControllerContext& context, const std::string& label + +){ + stream.log("Using " + label); + const ImageFloatBox box{0.670000, 0.600000, 0.100000, 0.150000}; + SelectionArrowFinder arrow(stream.overlay(), box, COLOR_GREEN); + + int ret = run_until(stream, + context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_A, 30000ms); + }, + {{arrow}}); + if (ret != 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Using " + label + ": HM confirmation arrow not found; stopping before further movement.", + stream); + } + + stream.log("Using " + label + ": selection arrow found, confirming.", COLOR_GREEN); + pbf_mash_button(context, BUTTON_A, 1000ms); + pbf_mash_button(context, BUTTON_B, 6000ms); +} + + +bool get_on_bicycle(VideoStream& stream, ProControllerContext& context, GearLevel /*gear*/){ + static const ImageFloatBox FRONT_BOX{0.440000, 0.470000, 0.035000, 0.054000}; + static const ImageFloatBox BACK_BOX{0.520000, 0.470000, 0.022000, 0.054000}; + // Spark color range: white (R=232,G=255,B=255) down to light green (R=163,G=217,B=181) + static const uint32_t SPARK_MIN = 0xFF000000u | (163u << 16) | (217u << 8) | 181u; + static const uint32_t SPARK_MAX = 0xFFFFFFFFu; + static const size_t SPARK_THRESHOLD = 13; + + stream.log("[AutoStory] get_on_bicycle: mounting bicycle.", COLOR_WHITE); + pbf_press_button(context, BUTTON_PLUS, 80ms, 800ms); + + // Press B, then poll up to 1 s for Fast-gear sparks. + // Logs the peak pixel counts in both boxes for threshold tuning. + // Returns true if sparks are detected (switched TO Fast gear). + auto toggle_and_check = [&]() -> bool { + context.wait_for_all_requests(); + pbf_press_button(context, BUTTON_B, 80ms, 800ms); + + size_t max_front = 0, max_back = 0; + auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000); + while (std::chrono::steady_clock::now() < deadline){ + context.wait_for(50ms); + VideoSnapshot screen = stream.video().snapshot(); + if (!screen){ + continue; + } + + size_t front_pixels = 0, back_pixels = 0; + filter_rgb32_range(front_pixels, + extract_box_reference(screen, FRONT_BOX), + SPARK_MIN, + SPARK_MAX, + Color(0), + false); + filter_rgb32_range(back_pixels, + extract_box_reference(screen, BACK_BOX), + SPARK_MIN, + SPARK_MAX, + Color(0), + false); + + if (front_pixels > max_front){ + max_front = front_pixels; + } + if (back_pixels > max_back){ + max_back = back_pixels; + } + + if (front_pixels >= SPARK_THRESHOLD || back_pixels >= SPARK_THRESHOLD){ + stream.log("[AutoStory] get_on_bicycle: sparks detected " + "(front=" + + std::to_string(front_pixels) + " back=" + std::to_string(back_pixels) + + " threshold=" + std::to_string(SPARK_THRESHOLD) + ").", + COLOR_GREEN); + return true; + } + } + stream.log("[AutoStory] get_on_bicycle: no sparks detected " + "(peak front=" + + std::to_string(max_front) + " back=" + std::to_string(max_back) + + " threshold=" + std::to_string(SPARK_THRESHOLD) + ").", + COLOR_ORANGE); + return false; + }; + + if (toggle_and_check()){ + stream.log("[AutoStory] get_on_bicycle: Fast gear confirmed.", COLOR_GREEN); + return true; + } + + // First toggle produced no sparks -> we switched to Slow gear; toggle back to Fast. + stream.log("[AutoStory] get_on_bicycle: Slow gear detected, toggling back to Fast.", COLOR_ORANGE); + if (toggle_and_check()){ + stream.log("[AutoStory] get_on_bicycle: Fast gear confirmed after correction.", COLOR_GREEN); + return true; + } + + stream.log("[AutoStory] get_on_bicycle: Failed to confirm Fast gear after two toggles.", COLOR_RED); + return false; +} + + +} +} +} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h new file mode 100644 index 0000000000..9435548d14 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h @@ -0,0 +1,541 @@ +/* BDSP AutoStory Tools + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStoryTools_H +#define PokemonAutomation_PokemonBDSP_AutoStoryTools_H + +#include +#include +#include +#include +#include + +#include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" + +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/Language.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" +#include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +#include "CommonTools/VisualDetector.h" + +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" + +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" + +#include "Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// --------------------------------------------------------------------------- +// Stats +// --------------------------------------------------------------------------- + +struct AutoStoryStats : public StatsTracker { + AutoStoryStats() + : m_checkpoint(m_stats["Checkpoint"]) + , m_segment(m_stats["Segment"]) + , m_reset(m_stats["Reset"]) + , m_current(m_stats["Current Checkpoint (00.0X = X)"]) + , m_completed(m_stats["Checkpoints Completed"]) + , m_last_completed(m_stats["Last Completed (00.0X = X)"]) + { + m_display_order.emplace_back("Checkpoint"); + m_display_order.emplace_back("Segment"); + m_display_order.emplace_back("Reset"); + m_display_order.emplace_back("Current Checkpoint (00.0X = X)"); + m_display_order.emplace_back("Checkpoints Completed"); + m_display_order.emplace_back("Last Completed (00.0X = X)"); + } + std::atomic& m_checkpoint; + std::atomic& m_segment; + std::atomic& m_reset; + std::atomic& m_current; + std::atomic& m_completed; + std::atomic& m_last_completed; + bool m_first_run = true; +}; + + +// --------------------------------------------------------------------------- +// Options passed to every segment/checkpoint +// --------------------------------------------------------------------------- + +enum class StarterChoice { + TURTWIG, + CHIMCHAR, + PIPLUP, +}; + +enum class HelpMode { + NO_OUTSIDE_HELP, + OUTSIDE_HELP, +}; + +enum class OutsideHelpPokemon { + TORTERRA, +}; + +enum class TorterraMove { + EARTHQUAKE, + WOOD_HAMMER, + RAZOR_LEAF, + CRUNCH, + GIGA_DRAIN, + IRON_TAIL, + ROCK_TOMB, + FACADE, + STONE_EDGE, + ROCK_SLIDE, + BULLDOZE, + STRENGTH, + ROCK_SMASH, + ROCK_CLIMB, +}; + +// Returns the JSON slug for a configured helper move. +const char* torterra_move_slug(TorterraMove move); + +enum class Trainer { + Veilstone_GalacticGrunts_2v2, + // Extend with new trainer battle IDs as the AutoStory adds them. +}; + +// Returns the trainers.json battle-ID string for a Trainer value. +const char* trainer_slug(Trainer t); + +struct OutsideHelpConfig { + OutsideHelpPokemon pokemon; + TorterraMove attacks[4]; +}; + +enum class CheckpointSaveMode { + REAL, + FAKE +}; +// Real saves retry one checkpoint. Fake-save runs own one retry loop around +// the selected range; inner checkpoints propagate failures to that owner. +enum class CheckpointRetryScope { + CHECKPOINT, + SELECTED_RANGE, + DEFER_TO_SELECTED_RANGE +}; + +struct AutoStoryOptions { + Language language; + StarterChoice starter_choice; + EventNotificationOption& notif_status_update; + HelpMode help_mode; + OutsideHelpConfig outside_help; + CheckpointSaveMode checkpoint_save_mode = CheckpointSaveMode::REAL; + CheckpointRetryScope checkpoint_retry_scope = CheckpointRetryScope::CHECKPOINT; + // How the story tables retry a checkpoint (StoryRunOptions). + Pokemon::SegmentPolicy policy = {}; + // What the story steps confirm on screen (StoryRunOptions). + Pokemon::StoryChecks checks = Pokemon::StoryChecks::THOROUGH; +}; + + +// --------------------------------------------------------------------------- +// Base classes +// --------------------------------------------------------------------------- + +class AutoStory_Segment { +public: + virtual ~AutoStory_Segment() = default; + virtual std::string name() const = 0; + virtual std::string start_text() const = 0; + virtual std::string end_text() const = 0; + virtual void run_segment(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const = 0; +}; + +class AutoStory_Checkpoint { +public: + virtual ~AutoStory_Checkpoint() = default; + virtual std::string name() const = 0; + virtual std::string start_text() const = 0; + virtual std::string end_text() const = 0; + virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const = 0; +}; + + +// --------------------------------------------------------------------------- +// Shared utility functions (implementations in .cpp) +// --------------------------------------------------------------------------- + +// Save the game from the overworld with retries. Returns true if the save was +// verified via overworld detection, false if the blind fallback was used. +// Adds `text` to the video overlay's log, split at spaces into lines short +// enough for the log box, which draws each line on one row and cuts it off. +void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color); + +// With `expected_location`, the map named on the save screen's card must be +// it (compared as a location banner is), or the save is backed out of and +// the function fails with a screenshot: r124a saved in Sandgem Town after a +// walk that was sure it stood on Route 202. +bool save_game( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::string& expected_location = "" +); + +// Opens the save screen from the overworld, reads where the game says we +// are (read_save_screen_location() in the Location module), and backs out +// without saving. +std::string read_location_from_save_screen(VideoStream& stream, ProControllerContext& context); + +// Fake save game to be replaced with save_game when program runs smoothly +void checkpoint_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +// Save the game from the overworld, increment the checkpoint counter, +// update stats display, and send a status notification. +void checkpoint_save(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats, + CheckpointSaveMode save_mode = CheckpointSaveMode::REAL); + +// Reload the game from the most recent backup save (auto-save). +// Called on retry when a checkpoint has no hard save (save=false). +// TODO: implement this function. +void reload_backup_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +// Lab: checkpoint_reattempt_loop() rethrows the first failure after its +// screenshot instead of resetting the game, so the game stays where it +// failed. Set from the program's option when it starts. +extern bool g_stop_at_first_failure; + +// If save=true (default): save the game before the first attempt, then run +// `action(attempt_number)`. On failure, reset from HOME and retry. +// If save=false: skip the hard save. On failure, reload from backup save and retry. +// SELECTED_RANGE skips the entry save and owns HOME resets for the range. +// DEFER_TO_SELECTED_RANGE lets an inner failure reach that outer retry loop. +// Throws a fatal error after too many consecutive failures. +void checkpoint_reattempt_loop(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats, + std::function&& action, + bool save = true, + CheckpointSaveMode save_mode = CheckpointSaveMode::REAL, + CheckpointRetryScope retry_scope = CheckpointRetryScope::CHECKPOINT); + +// Forward the explicit checkpoint save mode selected by AutoStory's options. +inline void checkpoint_reattempt_loop(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats, + std::function&& action, + bool save = true){ + checkpoint_reattempt_loop(env, + context, + options.notif_status_update, + stats, + std::move(action), + save, + options.checkpoint_save_mode, + options.checkpoint_retry_scope); +} + +// Execute every action in `actions` in order, repeated `times` times. +void repeat(std::initializer_list> actions, size_t times); + +// Tracks the last D-pad direction pressed. Pass one per navigation function +// so each function gets a clean slate on each attempt. +struct DpadState { + DpadPosition last_dir = DPAD_NONE; +}; + +// Press `dir` on the D-pad `times` times (plus one extra if the direction +// changed from the previous call, since the first press turns the character). +void repeat_dpad(ProControllerContext& context, + DpadState& state, + DpadPosition dir, + Milliseconds press, + Milliseconds hold, + size_t times, + bool include_last_dir = true); + +// Wait (without pressing any buttons) until a short dialogue box appears. +// Logs `label + " dialogue detected."` when found (omit label to skip logging). +// Throws OperationFailedException if no dialogue appears within `timeout`. +void wait_for_dialogue(VideoStream& stream, + ProControllerContext& context, + const std::string& label = "", + Milliseconds timeout = std::chrono::seconds(60)); + +// Mash `button` (default B) until the game returns to the free-roaming overworld +// (no dialogue, menu, or battle UI visible). +// Throws OperationFailedException if the overworld is not reached within `timeout`. +void mash_until_dialogue_ends(VideoStream& stream, + ProControllerContext& context, + Button button = BUTTON_B, + Milliseconds timeout = std::chrono::seconds(60)); + +// Scripted trainer battle: choose a 1-based move position each turn, then +// confirm the default target with A and clear narration with B. No target steering. +// A single position is reused for the whole battle. A list is consumed in +// order, then its final position is reused. Empty lists and slots outside 1-4 fail. +// post_battle_dialogue_cleanup must be chosen explicitly: false returns after +// the completed battle-exit transition; true also clears post-fight NPC dialogue. +// Examples: +// fight_trainer_using_moves(stream, context, 2, "Grunt", false); +// const std::vector moves{2, 1, 3}; +// fight_trainer_using_moves(stream, context, moves, "Gym leader", true); +void fight_trainer_using_moves(VideoStream& stream, + ProControllerContext& context, + size_t move, + const std::string& label, + bool post_battle_dialogue_cleanup); +void fight_trainer_using_moves(VideoStream& stream, + ProControllerContext& context, + const std::vector& moves, + const std::string& label, + bool post_battle_dialogue_cleanup); + +// A full Bag screenshot with the desired item highlighted. Paths are relative +// to RESOURCE_PATH(); quantities and selection borders are excluded. +struct BagItemTemplate { + std::string reference_path; + std::string label; + ImageFloatBox icon_box{0.428, 0.754, 0.029, 0.044}; + ImageFloatBox name_box{0.460, 0.755, 0.350, 0.045}; + double max_icon_rmsd = 25.0; + double max_name_rmsd = 20.0; + bool check_name = true; + // Selected item row in the reference screenshot (1-7). + size_t reference_row = 1; +}; + +// Screenshot-only helpers shared by live search and developer tests. +// Cursor 0 means unknown. Array index 0 corresponds to visible row 1. +struct BagItemRowMatch { + double icon_rmsd = 1000.0; + double name_rmsd = 1000.0; + bool matched = false; +}; + +// Pocket positions 1-8: Medicine, Balls, Battle Items, Berries, Other Items, +// TMs, Treasures, Key Items. Zero means the selected tab is uncertain. +int bag_pocket_position(const ImageViewRGB32& frame); +// Count normal, unobscured pocket icons; NEW badges prevent a tab match. +size_t bag_clear_tab_count(const ImageViewRGB32& frame); +// Open bag, pause 1s on the initial selection in each pocket, then scan +// with 80ms Down taps / 1s rest until all 8 tabs match, +// then restore the top of the item list and close the bag. +// Returns false after four bounded pocket passes. +bool clear_new_item_flags(VideoStream& stream, ProControllerContext& context, int num_icons = 8); +bool bag_list_is_empty(const ImageViewRGB32& frame); +int bag_cursor_position(const ImageViewRGB32& frame); +std::array +match_bag_item_rows(const ImageViewRGB32& frame, const ImageViewRGB32& reference, const BagItemTemplate& item); + +enum class BagMoveAction { + UNKNOWN, + CONFIRM, + UP, + DOWN, + PAGE_DOWN, + REWIND +}; +struct BagMovePlan { + BagMoveAction action = BagMoveAction::UNKNOWN; + size_t presses = 0; +}; + +// Describes the next input without sending it. REWIND means repeat Up and +// recheck until row 1; a screenshot cannot tell the total distance to the top. +BagMovePlan plan_bag_item_move(int cursor_row, int item_row); + +enum class BagPocketAction { + UNKNOWN, + CONFIRM, + LEFT, + RIGHT +}; +struct BagPocketMovePlan { + BagPocketAction action = BagPocketAction::UNKNOWN; + size_t presses = 0; +}; +// Circular eight-pocket route; ties prefer Right. Invalid positions send no input. +BagPocketMovePlan plan_bag_pocket_move(int current, int target); + + +// Starts in the overworld, opens the Bag, and leaves the requested item +// highlighted. Navigates to the requested pocket, then searches its item rows. +// Checks all seven visible rows and the cursor, then confirms the selected +// item in the green header. Makes up to three passes per pocket. +// Limits report an incomplete search, not absence. +// Returns false on missing frames, timeout, or no match; does not use an item. +bool find_bag_item(VideoStream& stream, + ProControllerContext& context, + const BagItemTemplate& item, + int target_pocket, + int num_icons = 8, + size_t max_scrolls_per_pocket = 512, + std::chrono::milliseconds search_timeout = std::chrono::minutes(10)); + +// Starts in the overworld, opens the Bag on `target_pocket`, and moves the cursor +// onto the item whose name reads `name` (letters only, lower case, e.g. "potion"). +// Reads the cursor row's name with OCR, so it does not depend on the capture +// card's colors or sharpness the way a template match does. +// Returns false if the pocket is empty or the name is not found. +bool find_bag_item_by_name( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket, + int num_icons = 8 +); + +// The same search in a Bag that is already open, such as the one a battle's +// Bag command opens. +bool find_bag_item_in_open_bag( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket +); + +// Find Escape Rope, choose Use, and verify a transition back to the overworld. +bool use_escape_rope(VideoStream& stream, ProControllerContext& context, int num_icons = 8); + +// Reuse the tutorial's verified Jubilife Center alignment and entry. +void align_and_enter_jubilife_center(VideoStream& stream, ProControllerContext& context); + +// Open the bag, navigate to the repel compartment tab, and use a repel. +// Returns false if the repel compartment tab could not be found. +// num_icons: early/late menu layout (preserved for existing callers). +// repel_type: 0=Repel, 1=Super Repel, 2=Max Repel. English item names. +bool activate_repel(VideoStream& stream, ProControllerContext& context, int num_icons = 8, int repel_type = 0); + +// Press A until the any HM selection arrow appears, confirm with A, then mash B for 3 s. +void use_HM(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// Walk to the nurse, heal the party, and exit the Pokemon Center. +// `label` names the center for log messages (e.g. "Veilstone"). +// Returns false if the exit black screen was not detected. +bool heal_pokemon(VideoStream& stream, ProControllerContext& context, const std::string& label); + +enum class FlyPoint { + CanalavCity, + CelesticTown, + EternaCity, + FloaromaTown, + HearthomeCity, + JubilifeCity, + OreburghCity, + PastoriaCity, + PokemonLeagueLower, + PokemonLeagueUpper, + Route221, + SandgemTown, + SolaceonTown, + SnowpointCity, + SunyshoreCity, + TwinleafTown, + VeilstoneCity, + CurrentCity, // cursor already starts on the city; skip all map navigation +}; + +std::string fly_point_name(FlyPoint place); + +// Open the map, move the cursor to either bottom left, bottom right or +// top left, then move the cursor a number of steps, then checks for +// correctness, and mashes A until a black screen is detected +bool fly_to(VideoStream& stream, ProControllerContext& context, FlyPoint place); + +// Opens the X-menu from the overworld, navigates the cursor to `target`, and presses A. +// num_icons: 5 (early-game layout) or 8 (late-game layout). +void open_menu(VideoStream& stream, ProControllerContext& context, MenuCursorPosition target, int num_icons); + +// Open the Bag from the overworld, look for `name` in the pocket, and close +// it again. True if the item is there. +bool bag_has_item( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket +); + +// Read the lead's HP bar in the Bag and, if it is below `heal_below` (a +// fraction of full), use Potions until it is full. heal_below = 1 always +// heals; 0.5 only checks and heals when the lead is under half, and is +// skipped when the Bag has no Potion. +void use_potion_first_pokemon( + VideoStream& stream, + ProControllerContext& context, + double heal_below = 1.0 +); + +enum class GearLevel { + Fast, +}; + + +// --------------------------------------------------------------------------- +// Mart buying +// --------------------------------------------------------------------------- + +enum class ShopItem { + FullRestore, + MaxPotion, + HyperPotion, + SuperPotion, + Potion, + Antidote, + ParalyzHeal, + Awakening, + Repel, + SuperRepel, + MaxRepel, + EscapeRope, +}; + +// Pass as the `quantity` field to buy the maximum available amount. +static constexpr int BUY_MAX = -1; + +struct ItemToBuy { + ShopItem item; + int quantity; // positive count to buy, or BUY_MAX +}; + +// Walk up to a vendor (already talking to them), navigate through the Buy menu, +// and purchase each item in `items` in order. The function presses DPAD_DOWN +// until the matching item name is found via OCR, so items may appear in any +// list order. Returns true on success. +bool buy_items(VideoStream& stream, + ProControllerContext& context, + const std::vector& items, + Language language = Language::English); + + +// Mount the bicycle (press +) and confirm it is in the requested gear. +// Presses B to toggle gear and uses the Fast-gear spark visual effect to +// determine which gear was entered. If the wrong gear is entered, presses B +// again and re-checks. Player must be on foot when called. +// Returns false if gear state cannot be confirmed after two toggles. +bool get_on_bicycle(VideoStream& stream, ProControllerContext& context, GearLevel gear); + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp new file mode 100644 index 0000000000..cc6e2f4c9e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -0,0 +1,252 @@ +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Data/PokemonBDSP_AutoStory_Route202.h" +#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; +using Pokemon::Pose; +using Pokemon::StoryLeg; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +namespace{ + +constexpr Direction NORTH = Direction::NORTH; +constexpr Direction WEST = Direction::WEST; + +TilePos world(const char* map, const TilePos& local){ + return story_maps().to_world(map, local); +} + +// A pose on any of `tiles`, world tiles of story_maps(). +Pose on(const char* map, std::vector tiles, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, std::move(tiles), facing); +} + +Pose save_spot(){ + return on(MAP_ROUTE_202, {world(MAP_ROUTE_202, Route202::SAVE_SPOT)}); +} + +// The catch grass training walks into: the tall grass of the training area. +std::vector catch_grass(){ + std::vector grass = story_after_training_area(); + std::erase_if(grass, [](const TilePos& pos){ + return story_maps().world().at(pos) != Pokemon::TileType::TALL_GRASS; + }); + return grass; +} + +// Route 202's open rows at the top: north from any of them is Jubilife. +std::vector jubilife_exit(){ + std::vector exit; + const Pokemon::TileGrid& grid = Route202::grid(); + for (int y = 0; y <= 3; y++){ + for (int x = 0; x < grid.width(); x++){ + const TilePos pos = world(MAP_ROUTE_202, {x, y}); + if (story_maps().world().standable(pos) && story_at_jubilife_exit(pos)){ + exit.push_back(pos); + } + } + } + return exit; +} + + +StoryStep make(StoryStepKind kind, std::string name, std::string why){ + StoryStep step; + step.kind = kind; + step.name = std::move(name); + step.why = std::move(why); + return step; +} + +// A step that calls the old function named `name` (the segment's variant). +StoryStep call(std::string name, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::GAME, name, std::move(why)); + step.variant = std::move(name); + step.pose_after = std::move(pose_after); + return step; +} + +StoryStep route(std::string name, Pose target, std::string why, std::string costs = ""){ + StoryStep step = make(StoryStepKind::ROUTE, std::move(name), std::move(why)); + step.target = std::move(target); + step.variant = std::move(costs); + return step; +} + +StoryStep legs(std::string name, std::vector legs, std::string why, std::string variant = ""){ + StoryStep step = make(StoryStepKind::LEGS, std::move(name), std::move(why)); + step.legs = std::move(legs); + step.variant = std::move(variant); + return step; +} + +StoryStep train(size_t battles, Pose pose_after, std::string why){ + StoryStep step = make(StoryStepKind::TRAIN, "train", std::move(why)); + step.battles = battles; + step.pose_after = std::move(pose_after); + return step; +} + +StoryStep heal(std::string name, Pose pose_after, std::string why){ + StoryStep step = make(StoryStepKind::HEAL, std::move(name), std::move(why)); + step.pose_after = std::move(pose_after); + return step; +} + +StoryStep with_variant(StoryStep step, std::string variant){ + step.variant = std::move(variant); + return step; +} + +StoryRow row(std::string id, std::string title, std::optional start, std::vector steps, + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY +){ + StoryRow ret; + ret.id = std::move(id); + ret.title = std::move(title); + ret.start = std::move(start); + ret.steps = std::move(steps); + ret.on_failure = on_failure; + return ret; +} + + +std::vector make_segment_00(){ + const int to_center_door = SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x; + const Pose center_door_front = on(MAP_SANDGEM_TOWN, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR)); + StoryStep potions = make(StoryStepKind::SHOP, "potions", + "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp), the clerk's 5 after two misses"); + potions.items = {{.item = "potion", .quantity = 10}}; + potions.variant = "sandgem_potions"; + potions.pose_after = save_spot(); + StoryStep stairs = make(StoryStepKind::STEP, "into_jubilife", + "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); + stairs.direction = NORTH; + stairs.variant = "jubilife_stairs"; + + return { + row("00.00", "Verify Settings", std::nullopt, { + call("verify_settings", "before any route movement or checkpoint save: fail visibly if options cannot be read"), + }, StoryFailure::STOP), + row("00.01", "After TV", std::nullopt, { + call("tutorial_part_1_navigation", "intro and starter selection: Butters' recorded stick holds to the lake"), + }), + row("00.02", "Outside Verity before starter", std::nullopt, { + call("starter_nav", "into the lake's scene"), + call("select_starter", "the starter from the program's option"), + with_variant(make(StoryStepKind::BATTLE, "fight_starly", "the Starly battle"), "fight_starly"), + with_variant(make(StoryStepKind::MASH, "post_starly_cleanup", "the scene after the battle"), "post_starly_cleanup"), + }), + row("00.03", "Outside Verity after starter", std::nullopt, { + call("tutorial_part_2_navigation", "Pokedex acquisition: back through Route 201 to Rowan's lab"), + }), + row("00.04", "After Pokedex, to the catch", std::nullopt, { + call("tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", + on(MAP_ROUTE_202, story_catch_save_area())), + }), + // A Lv5 starter fainted to Route 202's trainers (r72): training, a + // heal, and Potions first, each row from the save the one before ends on. + row("00.04b", "After the catch: train and heal in Sandgem", on(MAP_ROUTE_202, story_catch_save_area()), { + route("to_catch_grass", on(MAP_ROUTE_202, catch_grass()), "into the training column's grass"), + train(3, on(MAP_ROUTE_202, story_after_training_area()), "three wild battles with the best move"), + // One walk round the houses ended two tiles short (r252): down + // the road first, then the town plan. + route("into_sandgem", on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP)}), + "south down the road to above the Mart", "robust"), + route("to_center_sign", on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH))}), + "below the Center's sign, its doorway in sight", "town"), + // From below the sign the finder read the signs' panels as + // doorways (r252, r255): taps instead. One tile short is fine. + legs("below_center_door", {{.direction = WEST, .tiles = to_center_door}}, "taps west below the Center's door", "one_short_ok"), + heal("heal_sandgem", center_door_front, "Sandgem Town Pokemon Center"), + }), + row("00.04c", "Potions, back to Route 202's save spot", center_door_front, { + potions, + }), + // From the one save spot: from both at once the planner gives up. + row("00.04d", "Route 202's trainers, Dawn in Jubilife", save_spot(), { + route("north_to_jubilife", on(MAP_ROUTE_202, jubilife_exit()), "north past Route 202's trainers, fighting who sees us"), + stairs, + make(StoryStepKind::MASH, "dawn_scene", "Dawn walks over while we stand still"), + }), + row("00.05", "Jubilife After Dawn", std::nullopt, { + call("tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC"), + }), + }; +} + +std::vector make_segment_01(){ + StoryStep save = make(StoryStepKind::SAVE, "save_here", "the next checkpoint starts here"); + return { + row("01.01", "Jubilife to Barry", std::nullopt, { + call("jubilife_to_barry", "from the Center's PC out to Barry on Route 203"), + }), + row("01.02", "Route 203 to Oreburgh", std::nullopt, { + call("route203_to_oreburgh", "east through the Oreburgh Gate into Oreburgh City"), + }), + row("01.03", "Oreburgh Mine to Roark", std::nullopt, { + call("oreburgh_to_roark", "into the Mine to Roark and back to the Center"), + }), + row("01.04", "Oreburgh Gym", std::nullopt, { + call("oreburgh_gym", "the Gym's trainers and Roark for the Coal Badge"), + }), + row("01.05", "Oreburgh to Route 203", std::nullopt, { + call("oreburgh_to_route203", "heal, then west out of the Oreburgh Gate"), + }), + row("01.06", "Route 203 to Jubilife", std::nullopt, { + call("route203_to_jubilife", "west along Route 203 into Jubilife"), + }), + row("01.07", "Jubilife through Rowan's scene", std::nullopt, { + call("jubilife_to_rowan", "to the north street, Rowan's scene and the tag battle"), + }), + row("01.08", "Jubilife to the Ravaged Path", std::nullopt, { + call("jubilife_to_ravaged_path", "up Route 204's south half into the Ravaged Path"), + }), + row("01.09", "Ravaged Path", std::nullopt, { + call("ravaged_path", "through the cave with Rock Smash, out onto Route 204 North"), + }), + row("01.10", "Route 204 to Floaroma", std::nullopt, { + call("route204_to_floaroma", "the human's line north into Floaroma"), + save, + }), + row("01.11", "Floaroma's old couple", std::nullopt, { + call("floaroma_old_couple", "the old woman and the old man in the flower field"), + save, + }), + }; +} + +} + + +const std::vector& segment_00_table(){ + static const std::vector table = make_segment_00(); + return table; +} + +const std::vector& segment_01_table(){ + static const std::vector table = make_segment_01(); + return table; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h new file mode 100644 index 0000000000..53a92dd035 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h @@ -0,0 +1,42 @@ +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + * Segments 00 and 01, new game to Floaroma, as rows of the shared story + * steps (Pokemon/Navigation/Pokemon_StorySteps.h): plain data, as FRLG's + * table is, so the tests can plan its routes and run it against a fake + * game. Each row saves at its start, which is where the row before it + * ends. + * + * Steps that have no declarative form yet call one of the old functions: + * a GAME step (or another kind with a variant) names it, and the segment + * registers it with its executor (segment_00_variants(), + * segment_01_variants()). Those are the next to split. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H + +#include +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// 00.00 to 00.05: the settings, the lake, the Pokedex, the catch, Sandgem +// and Route 202 (00.04b to 00.04d as story steps), Jubilife to the Poketch. +const std::vector& segment_00_table(); + +// 01.01 to 01.11: Jubilife to Oreburgh and the Coal Badge, back, and north +// to Floaroma's old couple. +const std::vector& segment_01_table(); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp new file mode 100644 index 0000000000..81f16f511d --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp @@ -0,0 +1,1484 @@ +/* BDSP AutoStory - Route 202 to Sandgem's Pokemon Center, as events + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/Language.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "CommonTools/OCR/OCR_Routines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route201.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "PokemonBDSP_AutoStory_SandgemCenterEvents.h" +#include "Pokemon/Navigation/Pokemon_TilePose.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Checkpoint; +using Pokemon::Condition; +using Pokemon::Direction; +using Pokemon::Event; +using Pokemon::Interrupt; +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::TilePose; +using Pokemon::tiles_of; +using Pokemon::PoseCertainty; +using Pokemon::Recovery; +using Pokemon::RecoveryKind; +using Pokemon::RunState; +using Pokemon::TilePos; +using Pokemon::condition; +using Pokemon::outcome_failed; +using Pokemon::outcome_ok; +using Pokemon::pose_known; + + +const char* const ROUTE202_TO_SANDGEM_CENTER_NAME = "Route 202 to Sandgem's Pokemon Center (events)"; + +TilePos sandgem_entry_stop(){ + return story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); +} + +TilePos below_sandgem_center_sign(){ + return story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH)); +} + +TilePos east_of_lab_sign(){ + return story_maps().to_world(MAP_SANDGEM_TOWN, {6, 11}); +} + +bool sign_text_matches(const std::string& text, const std::string& key){ + std::string lower; + for (char ch : text){ + lower += (char)std::tolower((unsigned char)ch); + } + return lower.find(key) != std::string::npos; +} + +bool doorway_lined_up(const LitDoorway& doorway){ + return doorway.rows_up > 0 && doorway.rows_up <= 1.5 && std::abs(doorway.tiles_across) <= 0.5; +} + +bool center_doorway_at_front_row(const LitDoorway& doorway){ + // From below the sign the door is three tiles west; a tile either way + // for where the walk stopped, and the rows as tiles_short_of_door() + // counts the front row (1.5). + // Inside, the lit floor reads as a doorway well under a row up (0.28 + // in r129c). + return doorway.rows_up >= 0.7 && doorway.rows_up <= 1.5 && + doorway.tiles_across >= -4.5 && doorway.tiles_across <= 1.5; +} + + + +namespace{ + +// Where a sign's box shows: the bottom of the screen, as a dialogue's. The +// Lab's is grey, Route 201's green; opening either changes the area far +// more than the overworld behind it moves standing still. +const ImageFloatBox SIGN_BOX(0.19, 0.80, 0.64, 0.18); +// Its text, inside the margins. +const ImageFloatBox SIGN_TEXT(0.22, 0.82, 0.58, 0.15); +constexpr double SIGN_BOX_OPENED = 20.0; + +// The pose of a player who may be on any of `tiles`: on their map if they +// all share one. +Pose pose_on(const std::vector& tiles, std::optional facing = std::nullopt){ + const Pokemon::MapSet& maps = story_maps(); + Pose pose; + pose.where = std::make_shared(tiles, facing); + if (tiles.empty()){ + return pose; + } + const std::optional first = maps.to_local(tiles.front()); + const bool one_map = first && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos){ + const std::optional tile = maps.to_local(pos); + return tile && tile->map == first->map; + }); + pose.map = one_map ? first->map : ""; + pose.certainty = tiles.size() == 1 ? PoseCertainty::TILE : PoseCertainty::SET; + return pose; +} + +Recovery recovery(std::string on, RecoveryKind kind, size_t attempts, Pokemon::Act prepare, std::string why){ + Recovery recovery; + recovery.on = std::move(on); + recovery.kind = kind; + recovery.attempts = attempts; + recovery.prepare = std::move(prepare); + recovery.why = std::move(why); + return recovery; +} + +Condition outside(SandgemCenterActions& game){ + return condition("outside, not in a Pokemon Center", [&game](RunState&){ + return !game.inside_pokemon_center(); + }); +} + +// The doorway alone cannot tell the Center's from the Mart's, so the pose +// must also be in front of the Center: on its doors' row or a few rows +// south, within a few columns of its door. +Condition center_doorway_ahead(SandgemCenterActions& game){ + return condition("the Center's doorway at the front row", [&game](RunState& state){ + const TilePos door = story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); + const bool near = state.pose.map == MAP_SANDGEM_TOWN && !tiles_of(state.pose).empty() && + std::all_of(tiles_of(state.pose).begin(), tiles_of(state.pose).end(), [&](const TilePos& pos){ + return std::abs(pos.x - door.x) <= 4 && pos.y > door.y && pos.y <= door.y + 4; + }); + if (!near){ + return false; + } + const std::optional doorway = game.lit_doorway(); + return doorway && center_doorway_at_front_row(*doorway); + }); +} + +// 1. The save the checkpoint starts from is Route 202's save spot: the +// save screen's card says which map, and its walls pin the tile. +Event confirm_on_route_202(SandgemCenterActions& game){ + std::shared_ptr read = std::make_shared(); + Event event; + event.name = "confirm_on_route_202"; + event.why = "the save must stand at Route 202's save spot"; + event.act = [&game, read](RunState&){ + *read = game.save_screen_location(); + return outcome_ok(); + }; + event.expect.push_back(condition("the save screen reads Route 202", [read](RunState&){ + return location_banner_matches(*read, MAP_ROUTE_202); + })); + event.pose_learned = pose_on({story_maps().to_world(MAP_ROUTE_202, Route202::SAVE_SPOT)}, Direction::EAST); + event.recoveries.push_back(recovery("expect_failed", RecoveryKind::WAIT, 1, nullptr, + "OCR may have misread the card; read it again")); + return event; +} + +// 2. South along the trees east of Route 202, into Sandgem Town. Column +// 30 has no tall grass, and no trainer's sight reaches it below the ledge. +// walk_route() fails unless Sandgem's banner shows on the way. +Event walk_to_sandgem(SandgemCenterActions& game){ + Event event; + event.name = "walk_to_sandgem"; + event.why = "south out of Route 202, past Sandgem's banner"; + event.skip_if.push_back(pose_known(MAP_SANDGEM_TOWN, PoseCertainty::SET)); + event.require.push_back(pose_known(MAP_ROUTE_202, PoseCertainty::SET)); + event.act = [&game](RunState& state){ + std::vector at = tiles_of(state.pose); + const TilePos stop = sandgem_entry_stop(); + const Outcome walked = game.walk(at, [stop](const TilePos& pos){ return pos == stop; }, "Route 202 to Sandgem Town"); + if (walked.succeeded()){ + return outcome_ok(pose_on(at, Direction::SOUTH)); + } + return walked; + }; + event.expect.push_back(pose_known(MAP_SANDGEM_TOWN, PoseCertainty::SET)); + // From anywhere on Route 202 under the ledge, or Sandgem's top rows, + // east to the trees and south to the wall ends at the same tile. + event.recoveries.push_back(recovery("", RecoveryKind::REANCHOR, 1, [&game](RunState& state){ + if (!game.east_then_south_into_sandgem()){ + return outcome_failed("no_sandgem_banner", "east to the trees and south showed no Sandgem Town banner"); + } + return outcome_ok(pose_on({sandgem_entry_stop()}, Direction::SOUTH)); + }, "east to the trees and south to the wall, watching for Sandgem's banner")); + return event; +} + +// 3. From where the walk south stops to below the Center's sign, planned +// with town costs: west to the wall, a tile east into the gap's column, +// straight south through the gap between the signs, a tile west (the +// user's route, 2026-10-30; the plan test checks the planner finds it). +// With walls only, the plan went round the town in 13 legs, into the Lab +// (r127a) and the Mart (r128b); judging the row by the doorway's height put +// us a row up, level with the signs (r132a, r133a). +Event walk_to_center_sign(SandgemCenterActions& game){ + Event event; + event.name = "walk_to_center_sign"; + event.why = "through the signs' gap to below the Center's sign"; + event.require.push_back(pose_known(MAP_SANDGEM_TOWN, PoseCertainty::SET)); + event.act = [&game](RunState& state){ + std::vector at = tiles_of(state.pose); + const TilePos goal = below_sandgem_center_sign(); + const Outcome walked = game.walk_in_town(at, [goal](const TilePos& pos){ return pos == goal; }, "To below the Center's sign"); + if (walked.succeeded()){ + return outcome_ok(pose_on(at)); + } + return walked; + }; + event.expect.push_back(outside(game)); + return event; +} + +// 4. The gate: the Center's sign, at the building's bottom right, reads +// from the tile below it facing north, and only from there. +Event read_center_sign(SandgemCenterActions& game){ + std::shared_ptr read = std::make_shared(); + Event event; + event.name = "read_center_sign"; + event.why = "read the Center's sign from below it"; + event.act = [&game, read](RunState&){ + *read = game.read_sign(Direction::NORTH, "the Center's sign"); + return outcome_ok(); + }; + event.expect.push_back(condition("the Center's sign's box opened and closed", [read](RunState&){ + return read->opened && read->closed; + })); + event.expect.push_back(condition("the Center's sign's text, if read, has \"center\"", [read](RunState&){ + return read->text.empty() || sign_text_matches(read->text, "center"); + })); + event.pose_learned = pose_on({below_sandgem_center_sign()}, Direction::NORTH); + return event; +} + +// A townsperson who talks stops a walk where it stands, and walk_route() +// fails on a talk no trainer explains: close it and run the event again, +// from wherever its recoveries can place us. +Interrupt dialogue_interrupt(SandgemCenterActions& game){ + Interrupt interrupt; + interrupt.name = "dialogue"; + interrupt.detect = [&game](RunState&){ return game.dialogue_showing(); }; + interrupt.handle = [&game](RunState&){ return game.finish_dialogue(); }; + interrupt.pose = Pokemon::InterruptPose::LOST; + interrupt.resume = Pokemon::ResumePolicy::RETRY_EVENT; + return interrupt; +} + + +// The console's side of the checkpoint. +class ConsoleSandgemCenterActions : public SandgemCenterActions{ +public: + ConsoleSandgemCenterActions(VideoStream& stream, ProControllerContext& context) + : m_stream(stream) + , m_context(context) + , m_trainers(story_trainer_sights()) + {} + + virtual std::string save_screen_location() override{ + return read_location_from_save_screen(m_stream, m_context); + } + + virtual Outcome walk( + std::vector& at, + const std::function& goal, + const std::string& label + ) override{ + return walk_planned(at, goal, label, Pokemon::RobustPlanCosts()); + } + + Outcome walk_planned( + std::vector& at, + const std::function& goal, + const std::string& label, + const Pokemon::RobustPlanCosts& costs + ){ + // No trainer sees this way; one who does is a failure to look at. + const RouteTrainerFight fight = [this](const std::string& trainer){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Trainer " + trainer + " saw us on a way no trainer should see.", m_stream); + }; + try{ + at = walk_route(m_stream, m_context, story_maps(), at, goal, m_trainers, fight, label, {}, costs); + return outcome_ok(); + }catch (OperationFailedException& e){ + // Through a door: nothing to re-anchor on in there. + const bool through_door = e.message().find("transition") != std::string::npos; + return outcome_failed(through_door ? "walk_transition" : "walk_failed", e.message()); + } + } + + virtual bool east_then_south_into_sandgem() override{ + bool saw_sandgem = false; + const auto banner = [&saw_sandgem](const ImageViewRGB32& screen){ + saw_sandgem = saw_sandgem || location_banner_matches(read_location_banner(screen), MAP_SANDGEM_TOWN); + return false; + }; + const WalkResult east = walk_until(m_stream, m_context, 1, 0, 6000ms, "East to the trees"); + const WalkResult south = walk_until(m_stream, m_context, 0, -1, 9000ms, "South to the wall", banner); + for (const WalkResult& walk : {east, south}){ + if (walk.stop == WalkStop::TRANSITION || walk.stop == WalkStop::DIALOG){ + route_status(m_stream, std::string("Re-anchor: ") + walk_stop_name(walk.stop) + " instead of a wall."); + return false; + } + } + return saw_sandgem; + } + + virtual Outcome walk_in_town( + std::vector& at, + const std::function& goal, + const std::string& label + ) override{ + return walk_planned(at, goal, label, town_plan_costs()); + } + + virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ + VideoStream& stream = m_stream; + DialogueRead read; + route_turn(stream, m_context, facing); + // Let the camera settle after the turn: its drift read as a box + // opening (r132a). + pbf_wait(m_context, 800ms); + m_context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + // The text types out over about a second; B before it has finished + // only finishes it (r131a). + pbf_press_button(m_context, BUTTON_A, 80ms, 1500ms); + m_context.wait_for_all_requests(); + VideoSnapshot shown = stream.video().snapshot(); + if (!before || !shown){ + return read; + } + const double opened = walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*shown.frame, SIGN_BOX)); + read.opened = opened > SIGN_BOX_OPENED; + if (!read.opened){ + stream.log("[Sign] " + label + ": no box opened (change " + std::to_string(opened) + ")."); + return read; + } + // Tesseract read the Lab's white-on-grey text as "e R O e IO" (r131a). + // Only to say which sign, when there is a reader for the game's + // language; the box opening and closing is the check. + if (story_reads_text()){ + read.text = OCR::ocr_read(g_story_language, extract_box_reference(*shown.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + } + stream.log("[Sign] " + label + ": reads \"" + read.text + "\" (change " + std::to_string(opened) + ")."); + for (size_t presses = 0; presses < 3 && !read.closed; presses++){ + pbf_press_button(m_context, BUTTON_B, 80ms, 1000ms); + m_context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + read.closed = after && walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*after.frame, SIGN_BOX)) < SIGN_BOX_OPENED; + } + stream.log("[Sign] " + label + (read.closed ? ": closed." : ": still open.")); + return read; + } + + virtual std::optional lit_doorway() override{ + m_context.wait_for_all_requests(); + VideoSnapshot screen = m_stream.video().snapshot(); + return screen ? find_lit_doorway(*screen.frame) : std::nullopt; + } + + virtual Outcome line_up_with_door() override{ + if (!line_up_with_lit_door(m_stream, m_context, "Sandgem Pokemon Center: in front of the door")){ + return outcome_failed("not_lined_up", "no lit doorway, inside, or not lined up after 6 steps"); + } + return outcome_ok(); + } + + virtual bool inside_pokemon_center() override{ + m_context.wait_for_all_requests(); + VideoSnapshot screen = m_stream.video().snapshot(); + return screen && pokemon_center_inside(*screen.frame); + } + + virtual Outcome east_then_north_in_sandgem() override{ + const WalkResult east = walk_until(m_stream, m_context, 1, 0, 6000ms, "Sandgem: east to a wall"); + const WalkResult north = walk_until(m_stream, m_context, 0, 1, 6000ms, "Sandgem: north to a wall"); + for (const WalkResult& walk : {east, north}){ + if (walk.stop != WalkStop::BLOCKED){ + return outcome_failed("not_anchored", std::string("Sandgem: ") + walk_stop_name(walk.stop) + " instead of a wall"); + } + } + return outcome_ok(); + } + + virtual bool dialogue_showing() override{ + m_context.wait_for_all_requests(); + VideoSnapshot screen = m_stream.video().snapshot(); + return screen && ShortDialogDetector().detect(*screen.frame); + } + + virtual Outcome finish_dialogue() override{ + try{ + PokemonBDSP::finish_dialogue(m_stream, m_context, BUTTON_B, "Sandgem: a talk on the way"); + return outcome_ok(); + }catch (OperationFailedException& e){ + return outcome_failed("dialogue_failed", e.message()); + } + } + +protected: + VideoStream& m_stream; + ProControllerContext& m_context; + std::vector m_trainers; +}; + + +// --------------------------------------------------------------------------- +// The Poke Mart lady +// --------------------------------------------------------------------------- + +// A step of 00.04 that Segment 00 walks as one function, wrapped as an +// event: done once `flag` is set. +Event wrapped_step(std::string name, std::string why, std::string flag, Pokemon::Act step){ + Event event; + event.name = std::move(name); + event.why = std::move(why); + event.skip_if.push_back(condition(flag, [flag](RunState& state){ return state.flags.count(flag) > 0; })); + event.act = [flag, step = std::move(step)](RunState& state) mutable{ + const Outcome done = step(state); + if (done.succeeded()){ + state.flags.insert(flag); + } + return done; + }; + event.expect.push_back(condition(flag, [flag](RunState& state){ return state.flags.count(flag) > 0; })); + return event; +} + +// A gate: the sign we walked into, read with A facing it. Only the tiles +// beside it read it, so the read says which row we are on; the text, when +// OCR gets it, says which sign. A walk that stopped short of it is put +// right by walking on until the sign stops us. +Event read_sign(MartLadyActions& game, std::string name, std::string sign, Direction facing, std::string key){ + std::shared_ptr read = std::make_shared(); + Event event; + event.name = std::move(name); + event.why = "read " + sign + " facing " + std::string(Pokemon::direction_name(facing)); + event.act = [&game, read, sign, facing](RunState&){ + *read = game.read_sign(facing, sign); + return outcome_ok(); + }; + event.expect.push_back(condition(sign + "'s box opened and closed", [read](RunState&){ + return read->opened && read->closed; + })); + event.expect.push_back(condition(sign + "'s text, if read, has \"" + key + "\"", [read, key](RunState&){ + return read->text.empty() || sign_text_matches(read->text, key); + })); + event.recoveries.push_back(recovery("expect_failed", RecoveryKind::REANCHOR, 1, [&game, facing](RunState&){ + return game.walk_until_stopped(facing, 2, "On to the sign"); + }, "not at the sign: on until it stops us")); + return event; +} + +// She stands on the top row of her grass, under the trees, a tile or two +// in from its west edge. From the edge's top tile: east until she stops us, +// pressing A; then one A per line. Missed: back west to the edge, again. +Event talk_to_mart_lady(MartLadyActions& game){ + std::shared_ptr read = std::make_shared(); + Event event; + event.name = "talk_to_mart_lady"; + event.why = "the Poke Mart lady on Route 201 talks (she gives new games Potions)"; + event.act = [&game, read](RunState&){ + *read = game.talk_walking(Direction::EAST, 4, "Poke Mart lady"); + return outcome_ok(); + }; + event.expect.push_back(condition("a dialogue opened and was closed", [read](RunState&){ + return read->opened && read->closed; + })); + event.recoveries.push_back(recovery("expect_failed", RecoveryKind::REANCHOR, 2, [&game](RunState&){ + return game.walk_until_stopped(Direction::WEST, 6, "West to the grass's edge"); + }, "missed her: back west to the grass's edge and again")); + return event; +} + + +// The console's side of the Mart lady checkpoint; Jubilife's actions add +// to it. +template +class ConsoleMartLadyActionsT : public Base{ +public: + ConsoleMartLadyActionsT(SingleSwitchProgramEnvironment& env, ProControllerContext& context) + : m_env(env) + , m_context(context) + , m_center(env.console, context) + {} + + virtual std::string save_screen_location() override{ return m_center.save_screen_location(); } + virtual Outcome walk( + std::vector& at, + const std::function& goal, + const std::string& label + ) override{ + return m_center.walk(at, goal, label); + } + virtual bool east_then_south_into_sandgem() override{ return m_center.east_then_south_into_sandgem(); } + virtual Outcome walk_in_town( + std::vector& at, const std::function& goal, const std::string& label + ) override{ + return m_center.walk_in_town(at, goal, label); + } + virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ return m_center.read_sign(facing, label); } + virtual std::optional lit_doorway() override{ return m_center.lit_doorway(); } + virtual Outcome line_up_with_door() override{ return m_center.line_up_with_door(); } + virtual bool inside_pokemon_center() override{ return m_center.inside_pokemon_center(); } + virtual Outcome east_then_north_in_sandgem() override{ return m_center.east_then_north_in_sandgem(); } + virtual bool dialogue_showing() override{ return m_center.dialogue_showing(); } + virtual Outcome finish_dialogue() override{ return m_center.finish_dialogue(); } + + virtual Outcome walk_until_stopped(Direction direction, size_t tiles, const std::string& label) override{ + const bool across = direction == Direction::EAST || direction == Direction::WEST; + const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; + const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; + // Time for every tile, and for the wall to read as still. + const Milliseconds limit = ROUTE_START_DELAY + 800ms + + (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN); + // Turn first: the first press of a new way only turns us, the screen + // holds still meanwhile, and that read as a wall two tiles into the + // walk west to the Lab's sign (r132a). + route_turn(m_env.console, m_context, direction); + const WalkResult walked = walk_until(m_env.console, m_context, x, y, limit, label); + m_env.console.log("[Walk] " + label + ": stopped on " + walk_stop_name(walked.stop) + "."); + if (walked.stop == WalkStop::TRANSITION || walked.stop == WalkStop::DIALOG){ + return outcome_failed("walk_failed", label + ": " + walk_stop_name(walked.stop) + " instead of a stop"); + } + // The leg was measured to end against a wall, so holding toward it + // for every tile's time leaves us there whatever the screen shows: + // people walking or a TV screen nearby kept it changing and the stop + // went unseen (r162a, r167a, r171a). Seeing the stop only ends the + // hold early; a wrong place is caught by the next gate (a sign, a + // door, someone who answers). + if (walked.stop == WalkStop::LIMIT){ + m_env.console.log("[Walk] " + label + ": held for " + std::to_string(tiles) + + " tiles' time without seeing the stop; taking the wall as reached."); + } + return outcome_ok(); + } + + virtual DialogueRead talk(Direction direction, Button next, const std::string& label) override{ + VideoStream& stream = m_env.console; + DialogueRead read; + route_turn(stream, m_context, direction); + pbf_press_button(m_context, BUTTON_A, 80ms, 0ms); + m_context.wait_for_all_requests(); + ShortDialogWatcher opened(COLOR_CYAN); + if (wait_until(stream, m_context, 2000ms, {{opened}}) != 0){ + stream.log("[Talk] " + label + ": no dialogue opened."); + return read; + } + read.opened = true; + finish_talk(read, next, label); + return read; + } + + virtual DialogueRead talk_walking(Direction direction, size_t tiles, const std::string& label) override{ + DialogueRead read; + const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; + const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; + const bool across = direction == Direction::EAST || direction == Direction::WEST; + const Milliseconds limit = (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN) + 800ms; + const WalkResult walked = walk_to_talk(m_env.console, m_context, x, y, limit, label); + if (walked.stop != WalkStop::DIALOG){ + m_env.console.log("[Talk] " + label + ": nobody answered."); + return read; + } + read.opened = true; + finish_talk(read, BUTTON_A, label); + return read; + } + + virtual Outcome step(Direction direction, const std::string& label) override{ + // 200 ms took exactly one row on the console, turning included; 300 ms + // took one or two (2026-10-30). + const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; + const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; + const WalkResult walk = walk_for(m_env.console, m_context, x, y, 200ms, label); + if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ + return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); + } + return outcome_ok(); + } + + + virtual Outcome walk_to_route_202_save_spot() override{ + return wrap("walk_failed", [&]{ walk_from_sandgem_center_to_route_202_save_spot( + m_env.console, m_context, {story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END)} + ); }); + } + virtual Outcome save_on_route_202() override{ + bool saved = false; + const Outcome outcome = wrap("save_failed", [&]{ saved = save_game(m_env, m_context, MAP_ROUTE_202); }); + if (outcome.succeeded() && !saved){ + return outcome_failed("save_failed", "the save screen did not read Route 202, or the save did not finish"); + } + return outcome; + } + +protected: + // After a talk opened: reads the first box, then presses `next` once + // per line, 1.5 s apart, and only while a box shows: a press after it + // has closed would start the talk over. + void finish_talk(DialogueRead& read, Button next, const std::string& label){ + VideoStream& stream = m_env.console; + // The first box's text, inside its white margins. + // For the log only; a talk is checked by its box opening and closing. + VideoSnapshot screen = stream.video().snapshot(); + if (screen && story_reads_text()){ + read.text = OCR::ocr_read(g_story_language, extract_box_reference(*screen.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + } + stream.log("[Talk] " + label + ": the dialogue reads \"" + read.text + "\"."); + size_t quiet = 0; + // Barry's scene, to the Town Map, took more than 20 (r150a). + for (size_t presses = 0; presses < 80 && quiet < 2; presses++){ + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, m_context, 1500ms, {{dialog}}) == 0){ + quiet = 0; + pbf_press_button(m_context, next, 80ms, 0ms); + }else{ + quiet++; + } + } + read.closed = !dialogue_showing(); + stream.log("[Talk] " + label + (read.closed ? ": closed." : ": still open.")); + } + +private: + static Outcome wrap(const char* failure, const std::function& step){ + try{ + step(); + return outcome_ok(); + }catch (OperationFailedException& e){ + return outcome_failed(failure, e.message()); + } + } + + SingleSwitchProgramEnvironment& m_env; + ProControllerContext& m_context; + ConsoleSandgemCenterActions m_center; +}; +using ConsoleMartLadyActions = ConsoleMartLadyActionsT; + + +void save_end_screenshot(SingleSwitchProgramEnvironment& env, const std::string& name){ + if (VideoSnapshot screen = env.console.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = folder + now_to_filestring() + "-" + name + ".png"; + screen.frame->save(path); + env.log("[Checkpoint] Screenshot: " + path); + } +} + +// Runs `checkpoint`, logging its events, and fails with a screenshot +// naming the event and why if it does not complete. +void run_on_console(SingleSwitchProgramEnvironment& env, Checkpoint& checkpoint, const char* name){ + const std::vector problems = Pokemon::lint_checkpoint(checkpoint); + for (const std::string& problem : problems){ + env.log("[Checkpoint] lint: " + problem, COLOR_RED); + } + RunState state; + // A screenshot as each event ends, so a run that passes can be + // checked step by step, not only by its end. + Pokemon::EventLogger log = [&env, &checkpoint](const std::string& line){ + env.log(line, COLOR_BLUE); + add_wrapped_log(env.console.overlay(), line, COLOR_WHITE); + const size_t done = line.rfind(": done"); + const size_t failed = line.find(": failed"); + if ((done != std::string::npos && done + 6 == line.size()) || failed != std::string::npos){ + const size_t slash = line.find(checkpoint.id + "/"); + const size_t colon = line.find(':', slash == std::string::npos ? 0 : slash); + const std::string event = slash == std::string::npos || colon == std::string::npos + ? "event" : line.substr(slash + checkpoint.id.size() + 1, colon - slash - checkpoint.id.size() - 1); + std::string name = "step-" + checkpoint.id + "-" + event; + for (char& c : name){ + if (c == ' ' || c == '/'){ + c = '_'; + } + } + save_end_screenshot(env, name); + } + }; + const Pokemon::CheckpointReport report = Pokemon::run_checkpoint(state, checkpoint, log); + // Where it ended, to check by eye. + save_end_screenshot(env, "end-" + checkpoint.id); + if (!report.completed){ + const std::string where = report.failed_event == 0 ? "" : " at event " + + std::to_string(report.failed_event) + " " + report.failed_name; + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + std::string(name) + ": failed" + where + ": " + + report.failure.failure + (report.failure.detail.empty() ? "" : ": " + report.failure.detail), env.console); + } +} + +} + + +Checkpoint route202_to_sandgem_center_checkpoint(SandgemCenterActions& game){ + Checkpoint checkpoint; + checkpoint.id = "sandgem_center"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + checkpoint.events.push_back(confirm_on_route_202(game)); + checkpoint.events.push_back(walk_to_sandgem(game)); + // It ends below the Center's sign, facing it: lining up with the door + // itself overshot or walked in (r128, r129), and nothing after needs it. + checkpoint.events.push_back(walk_to_center_sign(game)); + checkpoint.events.push_back(read_center_sign(game)); + // No save: the save stays on Route 202, so the run can be repeated. + return checkpoint; +} + +void diagnostic_route202_to_sandgem_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleSandgemCenterActions game(env.console, context); + Checkpoint checkpoint = route202_to_sandgem_center_checkpoint(game); + run_on_console(env, checkpoint, ROUTE202_TO_SANDGEM_CENTER_NAME); +} + + +const char* const SANDGEM_MART_LADY_NAME = "Sandgem's Pokemon Center to the Poke Mart lady and back to Route 202 (events)"; + +TilePos route201_sign_west(){ + return story_maps().to_world(MAP_ROUTE_201, Route201::BORDER_SIGN.next(Direction::WEST)); +} + +TilePos route201_row_3_east_end(){ + // Row 3 is open east of her grass to Sandgem's fence: (64,3) is + // Sandgem's (0,6), a wall. + return story_maps().to_world(MAP_ROUTE_201, {63, 3}); +} + +// Where the Route 202 checkpoint ends: below the Center's sign. +Event on_doors_row(SandgemCenterActions& game){ + Event event; + event.name = "on_doors_row"; + event.why = "below the Center's sign, where the Route 202 checkpoint ends"; + event.act = [](RunState&){ + return outcome_ok(pose_on({below_sandgem_center_sign()}, Direction::NORTH)); + }; + event.expect.push_back(outside(game)); + return event; +} + +Checkpoint sandgem_mart_lady_checkpoint(MartLadyActions& game){ + // Walks that stop on their own, measured on the console (2026-10-30) and + // traced on the grid by Test_MartLadyWalk. + Checkpoint checkpoint; + checkpoint.id = "mart_lady"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + checkpoint.events.push_back(on_doors_row(game)); + checkpoint.events.push_back(wrapped_step("west_to_lab_sign", + "west along the doors' row until the Lab's sign stops us", "at_lab_sign", + [&game](RunState&){ return game.walk_until_stopped(Direction::WEST, 20, "West to the Lab's sign"); })); + checkpoint.events.push_back(read_sign(game, "read_lab_sign", "the Lab's sign", Direction::WEST, "lab")); + checkpoint.events.push_back(wrapped_step("west_along_the_path", + "a tile south onto the path, then west to the grass's edge", "at_grass_edge", + [&game](RunState&){ + const Outcome down = game.step(Direction::SOUTH, "South onto the path"); + return down.succeeded() ? game.walk_until_stopped(Direction::WEST, 28, "West along the path") : down; + })); + checkpoint.events.push_back(wrapped_step("north_to_the_trees", + "north until the trees stop us, on her row", "on_her_row", + [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 3, "North to the trees"); })); + checkpoint.events.push_back(talk_to_mart_lady(game)); + checkpoint.events.push_back(wrapped_step("back_to_route_201_sign", + "a tile south onto the signs' row, then east until Route 201's sign stops us", "at_route_201_sign", + [&game](RunState&){ + const Outcome down = game.step(Direction::SOUTH, "South onto the signs' row"); + return down.succeeded() ? game.walk_until_stopped(Direction::EAST, 16, "East to the Route 201 sign") : down; + })); + checkpoint.events.push_back(read_sign(game, "read_route_201_sign", "Route 201's sign", Direction::EAST, "201")); + checkpoint.events.push_back(wrapped_step("east_along_the_path", + "a tile south onto the path, east to the trees east of the Mart, north a row", "at_front_row_east_end", + [&game](RunState&){ + const Outcome down = game.step(Direction::SOUTH, "South onto the path"); + if (!down.succeeded()){ + return down; + } + const Outcome east = game.walk_until_stopped(Direction::EAST, 40, "East along the path"); + return east.succeeded() ? game.walk_until_stopped(Direction::NORTH, 2, "North to the front row") : east; + })); + // Back to where 00.04 saves with its Potions, as Segment 00 walks it + // from the trees east of the Mart. + checkpoint.events.push_back(wrapped_step("walk_to_route_202_save_spot", + "from the trees east of the Mart to Route 202's save spot", "at_route_202_save_spot", + [&game](RunState& state){ + const Outcome walked = game.walk_to_route_202_save_spot(); + if (walked.succeeded()){ + return outcome_ok(pose_on({story_maps().to_world(MAP_ROUTE_202, Route202::SAVE_SPOT)})); + } + return walked; + })); + // The save the checkpoint starts from stands there, so it can be repeated. + checkpoint.save = [&game](RunState&){ return game.save_on_route_202(); }; + return checkpoint; +} + +void mart_lady_without_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleMartLadyActions game(env, context); + Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); + checkpoint.save = {}; + run_on_console(env, checkpoint, SANDGEM_MART_LADY_NAME); +} +void diagnostic_sandgem_mart_lady(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleMartLadyActions game(env, context); + Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); + run_on_console(env, checkpoint, SANDGEM_MART_LADY_NAME); +} + + + + +// --------------------------------------------------------------------------- +// Jubilife: 00.05a and 00.05b +// --------------------------------------------------------------------------- + +const char* const JUBILIFE_CENTER_NAME = "00.05a Jubilife: steps to the Pokemon Center (events)"; +const size_t SCHOOL_TILES_WEST_OF_CENTER = 11; +// The School's lamp, east of its door, from its doorsteps' row at night +// (2026-10-30): at 0.65 to 0.69 of the screen across the door takes us in; +// at 0.61 (right of it) and 0.75 (past it) its front stops us. A tile is +// 0.065 of the screen. The box leaves out the Center's lamp further east. +const double SCHOOL_LAMP_X_AT_DOOR = 0.67; +const ImageFloatBox SCHOOL_LAMP_BOX(0.55, 0.20, 0.30, 0.10); +const char* const JUBILIFE_SCHOOL_NAME = "00.05b Jubilife: Pokemon Center to the Trainers' School (events)"; + +Checkpoint jubilife_center_checkpoint(JubilifeActions& game){ + Checkpoint checkpoint; + checkpoint.id = "jubilife_center"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + checkpoint.events.push_back(wrapped_step("up_the_steps", + "a tile west of the save, then up the steps onto the main road", "on_main_road", + [&game](RunState&){ return game.climb_the_steps(); })); + // Every leg walks until a wall stops it, measured on the console + // (2026-10-30): the fence's end west of the steps; the sign by the lamp + // west of the road; across the road, the house east of it; and north up + // the east sidewalk, the Center's doorway. The kids south of the Center + // can push us part of a tile aside, so the heal lines up by sight. + checkpoint.events.push_back(wrapped_step("west_to_the_fence", + "west until the fence's end stops us", "at_fence_end", + [&game](RunState&){ return game.walk_until_stopped(Direction::WEST, 8, "West to the fence's end"); })); + checkpoint.events.push_back(wrapped_step("north_to_the_sign", + "north until the sign by the lamp west of the road stops us", "at_sign", + [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 8, "North to the sign"); })); + checkpoint.events.push_back(wrapped_step("east_to_the_house", + "east across the road until the house east of it stops us", "at_east_house", + [&game](RunState&){ return game.walk_until_stopped(Direction::EAST, 14, "East to the house"); })); + checkpoint.events.push_back(wrapped_step("north_to_the_center", + "north up the east sidewalk until the Pokemon Center stops us", "at_center", + [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 14, "North to the Pokemon Center"); })); + { + Event heal = wrapped_step("heal", + "in, the nurse heals, out onto the doorstep", "healed", + [&game](RunState&){ return game.heal_at_center(); }); + // r143a's heal said it had walked out while still inside. + heal.expect.push_back(condition("out of the Pokemon Center, in the overworld", [&game](RunState&){ + return !game.indoors(); + })); + checkpoint.events.push_back(std::move(heal)); + } + return checkpoint; +} + +Checkpoint jubilife_school_checkpoint(JubilifeActions& game){ + Checkpoint checkpoint; + checkpoint.id = "jubilife_school"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + Event into = wrapped_step("into_the_school", + "west from the Center's doorstep, counting tiles, north through the School's door", "in_school", + [&game](RunState&){ + // Measured on the console (2026-10-30): from the Center's doorstep, + // a tap to face west and eleven one-tile holds put us under the + // School's doorway; twelve is past it. A hold ends a part of a + // tile off (back east, eleven stopped beside the Center's door), + // so if the School's front stops us, nudge half a tile each way. + game.turn(Direction::WEST); + for (size_t tiles = 0; tiles < SCHOOL_TILES_WEST_OF_CENTER; tiles++){ + const Outcome moved = game.tile(Direction::WEST, "West to the School's door"); + if (!moved.succeeded()){ + return moved; + } + } + // At night the lamp east of the door says how far off we are. + for (size_t looks = 0; looks < 3; looks++){ + const std::optional across = game.school_door_across(); + // The door took us from -0.19 and 0.19 but not -0.34 (r148b). + if (!across || std::abs(*across) <= 0.3){ + break; + } + game.step_across(*across, "Line up with the School's door by its lamp"); + } + if (game.through_door_north("Trainers' School door")){ + return outcome_ok(); + } + for (Direction side : {Direction::EAST, Direction::WEST, Direction::WEST}){ + game.nudge(side, "Line up with the School's door"); + if (game.through_door_north("Trainers' School door")){ + return outcome_ok(); + } + } + return outcome_failed("door_failed", "the School's door did not take us from 11 tiles west of the Center's doorstep, or half a tile either side"); + }); + // It starts on the Center's doorstep: outside, in the overworld. + into.require.push_back(condition("out of the Pokemon Center, in the overworld", [&game](RunState&){ + return !game.indoors(); + })); + checkpoint.events.push_back(std::move(into)); + return checkpoint; +} + +const char* const JUBILIFE_BARRY_NAME = "00.05c Jubilife: Barry in the Trainers' School (events)"; + +Checkpoint jubilife_barry_checkpoint(JubilifeActions& game){ + Checkpoint checkpoint; + checkpoint.id = "jubilife_barry"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + // Barry stands at the blackboard a tile west of the door's column, and + // that column is clear to the back wall (shots from inside the door). + // A tile west by a hold went two in r149a and the boy at the table + // stopped us: stay on the door's column, north to the wall, face west. + checkpoint.events.push_back(wrapped_step("up_to_barry", + "north up the door's column until the back wall stops us, beside Barry", "at_barry", + [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 10, "North to the back wall"); })); + { + std::shared_ptr read = std::make_shared(); + Event talk; + talk.name = "talk_to_barry"; + talk.why = "Barry gives the Town Map"; + talk.skip_if.push_back(condition("talked_to_barry", [](RunState& state){ return state.flags.count("talked_to_barry") > 0; })); + talk.act = [&game, read](RunState& state){ + *read = game.talk(Direction::WEST, BUTTON_A, "Barry"); + if (read->opened && read->closed){ + state.flags.insert("talked_to_barry"); + } + return outcome_ok(); + }; + talk.expect.push_back(condition("his dialogue opened and was closed", [read](RunState&){ + return read->opened && read->closed; + })); + talk.recoveries.push_back(recovery("expect_failed", RecoveryKind::REANCHOR, 1, [&game](RunState&){ + return game.walk_until_stopped(Direction::NORTH, 4, "North to the back wall"); + }, "not beside him: north until the back wall stops us")); + checkpoint.events.push_back(std::move(talk)); + } + checkpoint.events.push_back(wrapped_step("out_of_the_school", + "straight south down the door's column and out", "out_of_school", + [&game](RunState&){ return game.leave_south("Out of the Trainers' School"); })); + return checkpoint; +} + +const char* const JUBILIFE_PRESIDENT_NAME = "00.05d Jubilife: the Poketch Company president (events)"; + +Checkpoint jubilife_president_checkpoint(JubilifeActions& game){ + Checkpoint checkpoint; + checkpoint.id = "jubilife_president"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + checkpoint.events.push_back(wrapped_step("into_the_road", + "a tap to face east, four one-tile steps into the main road", "in_road", + [&game](RunState&){ + game.turn(Direction::EAST); + for (size_t tiles = 0; tiles < 4; tiles++){ + const Outcome moved = game.tile(Direction::EAST, "East into the road"); + if (!moved.succeeded()){ + return moved; + } + } + return outcome_ok(); + })); + { + std::shared_ptr read = std::make_shared(); + Event meet; + meet.name = "meet_the_president"; + meet.why = "the president sends us for the three clowns' coupons"; + meet.skip_if.push_back(condition("met_president", [](RunState& state){ return state.flags.count("met_president") > 0; })); + meet.act = [&game, read](RunState& state){ + *read = game.walk_until_talked_to(6, "The Poketch Company president"); + if (read->opened && read->closed){ + state.flags.insert("met_president"); + } + return outcome_ok(); + }; + meet.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ + return read->opened && read->closed; + })); + checkpoint.events.push_back(std::move(meet)); + } + return checkpoint; +} + +const char* const JUBILIFE_CLOWN_MART_NAME = "00.05e Jubilife: the clown by the Poke Mart (events)"; + +Checkpoint jubilife_clown_mart_checkpoint(JubilifeActions& game){ + Checkpoint checkpoint; + checkpoint.id = "jubilife_clown_mart"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + checkpoint.events.push_back(wrapped_step("east_to_the_lamp", + "east until the lamp by the Pokemon Center stops us", "at_center_lamp", + [&game](RunState&){ return game.walk_until_stopped(Direction::EAST, 8, "East to the Center's lamp"); })); + checkpoint.events.push_back(wrapped_step("up_to_the_mart_road", + "a tile west, then a second north onto the road between the Center and the Mart", "on_mart_road", + [&game](RunState&){ + game.turn(Direction::WEST); + const Outcome west = game.tile(Direction::WEST, "West of the lamp"); + return west.succeeded() ? game.hold(Direction::NORTH, 1000, "North to the Mart's road") : west; + })); + checkpoint.events.push_back(wrapped_step("east_to_the_building", + "east along the road until the tall building stops us", "at_east_building", + [&game](RunState&){ return game.walk_until_stopped(Direction::EAST, 14, "East to the building"); })); + { + std::shared_ptr read = std::make_shared(); + Event talk; + talk.name = "talk_to_clown"; + talk.why = "the clown asks a question (Yes is right) and gives Coupon 1"; + talk.skip_if.push_back(condition("coupon_1", [](RunState& state){ return state.flags.count("coupon_1") > 0; })); + talk.act = [&game, read](RunState& state){ + // He may be what stopped the walk east (r155c, 2.8 s against + // 2.2): ask ahead first. Else he stands a row south and a tile + // west: try that row, then one more, facing west each time. + *read = game.talk(Direction::EAST, BUTTON_A, "Clown by the Poke Mart"); + for (size_t rows = 0; rows < 2 && !read->opened; rows++){ + const Outcome down = game.step(Direction::SOUTH, "A row south"); + if (!down.succeeded()){ + return down; + } + game.turn(Direction::WEST); + *read = game.talk(Direction::WEST, BUTTON_A, "Clown by the Poke Mart"); + } + if (read->opened && read->closed){ + state.flags.insert("coupon_1"); + } + return outcome_ok(); + }; + talk.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ + return read->opened && read->closed; + })); + checkpoint.events.push_back(std::move(talk)); + } + return checkpoint; +} + +const char* const JUBILIFE_CLOWN_COMPANY_NAME = "00.05f Jubilife: the clown by the Poketch Company (events)"; + +Checkpoint jubilife_clown_company_checkpoint(JubilifeActions& game){ + Checkpoint checkpoint; + checkpoint.id = "jubilife_clown_company"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + checkpoint.events.push_back(wrapped_step("to_the_lamp", + "west, south and west to the lamp at the long road's west end, north a row", "at_west_lamp", + [&game](RunState&){ + for (const auto& [direction, tiles, label] : { + // The TV station's screen keeps the picture moving after we + // stop: r161 saw the stop only at 14 tiles' time, r162a not + // within it. + std::tuple{Direction::WEST, 20, "West to the hedges"}, + {Direction::SOUTH, 10, "South to the long road"}, + {Direction::WEST, 26, "West to the lamp"}, + {Direction::NORTH, 6, "North by the lamp"}, + }){ + const Outcome walked = game.walk_until_stopped(direction, tiles, label); + if (!walked.succeeded()){ + return walked; + } + } + return outcome_ok(); + })); + // The clown wanders the plaza behind the hedges, and a walk can pass him + // by. Measured on the console (2026-10-30): from the lamp, the + // Poketch Company's sign is six tiles east along the hedges (taps put us + // there or a tile past it); it reads facing north, and the tile east of + // it is a gap in the hedges into the plaza. A step into the gap, then A + // every second or so, talked to him within seconds: he comes by. So we + // tap four tiles, then go a tile at a time asking north (him, or the + // sign), and wait in the gap. `progress` keeps a rerun after a talk + // interrupt from walking the tiles again. + { + struct Progress{ bool tapped = false; size_t tiles = 0; bool in_gap = false; }; + std::shared_ptr progress = std::make_shared(); + std::shared_ptr read = std::make_shared(); + Event talk; + talk.name = "talk_to_clown"; + talk.why = "the clown asks a question (Yes is right) and gives Coupon 3"; + talk.skip_if.push_back(condition("coupon_3", [](RunState& state){ return state.flags.count("coupon_3") > 0; })); + talk.act = [&game, read, progress](RunState& state){ + const auto is_sign = [](const DialogueRead& r){ + return r.text.find("World") != std::string::npos || r.text.find("Company") != std::string::npos; + }; + // Him north of us, or the sign. + const auto ask = [&](Direction direction) -> bool{ + *read = game.talk(direction, BUTTON_A, "Clown by the Poketch Company"); + if (read->opened && is_sign(*read)){ + *read = DialogueRead(); + } + return read->opened; + }; + if (!progress->tapped){ + game.turn(Direction::EAST); + const Outcome moved = game.taps(Direction::EAST, 4, "East along the hedges"); + if (!moved.succeeded()){ + return moved; + } + progress->tapped = true; + } + // A turn tap the way we already face is a step (r161c walked into + // the plaza so), so we face east between two asks north. + while (!progress->in_gap && progress->tiles < 5){ + if (ask(Direction::NORTH)){ + break; + } + game.turn(Direction::EAST); + const DialogueRead sign = game.read_sign(Direction::NORTH, "the Poketch Company's sign"); + if (sign.opened && !is_sign(sign)){ + // Him after all (or text OCR missed); his talk was + // closed with B: ask again. + game.turn(Direction::EAST); + if (ask(Direction::NORTH)){ + break; + } + } + game.turn(Direction::EAST); + const Outcome moved = game.taps(Direction::EAST, 1, "On along the hedges"); + if (!moved.succeeded()){ + return moved; + } + progress->tiles++; + if (sign.opened){ + // East of the sign: north is the gap. + const Outcome up = game.step(Direction::NORTH, "Into the gap beside the sign"); + if (!up.succeeded()){ + return up; + } + progress->in_gap = true; + } + } + // Up to about a minute and a half; west of the gap is the sign. + // East first: the step into the gap left us facing north. + for (size_t round = 0; round < 18 && progress->in_gap && !read->opened; round++){ + for (Direction direction : {Direction::EAST, Direction::NORTH, Direction::WEST}){ + if (ask(direction)){ + break; + } + } + } + if (read->opened && read->closed){ + state.flags.insert("coupon_3"); + } + return outcome_ok(); + }; + talk.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ + return read->opened && read->closed; + })); + checkpoint.events.push_back(std::move(talk)); + } + return checkpoint; +} + +const char* const JUBILIFE_POKETCH_NAME = "00.05g Jubilife: the Poketch from the president, then up to the street east to Route 203 (events)"; + +// Measured on the console (2026-10-30), from the gap beside the Poketch +// Company's sign: a row south, then east along the long road stops at the +// main road (on the building north of the Mart, or on the green-haired man +// who wanders the crossing; either leaves us in the main road). Six rows +// down that road, east stops against the Poke Mart's west wall: the column +// between the Mart's lamp and the Center's, so south stops above the +// Center's lamp. The president stands still a row or two north of that +// row, a few tiles west, so from a row up we go west a tile at a time, +// asking ahead and north. Back the same way to the lamp; the Center's lit +// door is then on screen a tile west and a few rows south, and the heal +// lines up with it. +Checkpoint jubilife_poketch_checkpoint(JubilifeActions& game){ + Checkpoint checkpoint; + checkpoint.id = "jubilife_poketch"; + checkpoint.interrupts.push_back(dialogue_interrupt(game)); + const auto steps = [&game](Direction direction, size_t rows, const char* label){ + for (size_t i = 0; i < rows; i++){ + const Outcome moved = game.step(direction, label); + if (!moved.succeeded()){ + return moved; + } + } + return outcome_ok(); + }; + checkpoint.events.push_back(wrapped_step("to_the_main_road", + "a row south of the gap, then east along the long road until the main road stops us", "on_main_road", + [&game](RunState&){ + const Outcome down = game.step(Direction::SOUTH, "Out of the gap"); + // A hold, not a walk until still: when the wanderer is what stops + // us, his steps keep the screen moving and the stop is never seen + // (r167a). The stop took 5.9 s; holding on against it is harmless, + // and the Mart's wall puts the column right afterwards. + return down.succeeded() ? game.hold(Direction::EAST, 7500, "East along the long road") : down; + })); + checkpoint.events.push_back(wrapped_step("to_the_center_lamp", + "six rows down the main road, east to the Mart's wall, south to the Center's lamp", "at_center_lamp_north", + [&game, steps](RunState&){ + const Outcome down = steps(Direction::SOUTH, 6, "Down the main road"); + if (!down.succeeded()){ + return down; + } + const Outcome east = game.walk_until_stopped(Direction::EAST, 8, "East to the Mart's wall"); + return east.succeeded() ? game.walk_until_stopped(Direction::SOUTH, 8, "South to the Center's lamp") : east; + })); + { + std::shared_ptr read = std::make_shared(); + Event talk; + talk.name = "talk_to_president"; + talk.why = "the president trades the three coupons for the Poketch"; + talk.skip_if.push_back(condition("poketch", [](RunState& state){ return state.flags.count("poketch") > 0; })); + talk.act = [&game, read](RunState& state){ + const Outcome up = game.step(Direction::NORTH, "A row up from the lamp"); + if (!up.succeeded()){ + return up; + } + game.turn(Direction::WEST); + // Facing west: ask ahead, turn north and ask, turn back, one tile + // on. Turns only ever change the way we face, so none is a step. + for (size_t tiles = 0; tiles < 8 && !read->opened; tiles++){ + *read = game.talk_ahead(BUTTON_B, "The Poketch Company president"); + if (read->opened){ + break; + } + game.turn(Direction::NORTH); + *read = game.talk_ahead(BUTTON_B, "The Poketch Company president"); + if (read->opened){ + break; + } + game.turn(Direction::WEST); + const Outcome moved = game.taps(Direction::WEST, 1, "West along the road"); + if (!moved.succeeded()){ + return moved; + } + } + if (read->opened){ + // His last line asks us to try the R Button; a help panel + // about the Poketch follows, closed with B. + game.close_panel("The Poketch's help"); + } + if (read->opened && read->closed){ + state.flags.insert("poketch"); + } + return outcome_ok(); + }; + talk.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ + return read->opened && read->closed; + })); + checkpoint.events.push_back(std::move(talk)); + } + // 00.05 ends here: we healed at the Center on arriving (00.05a) and have + // not fought since. 01.01 starts on the street east to Route 203, about + // ten rows north (the long road's crossing is six rows above the Mart's + // wall, which is four above the talk; r164a, by hand). A row off is + // fine: 01.01 walks east and tries the rows either side at a wall. The + // president walks off north after his talk, up our way (r164a): let him. + checkpoint.events.push_back(wrapped_step("up_to_the_route_203_street", + "let the president walk off, then ten rows north to the street east to Route 203", "on_route_203_street", + [&game, steps](RunState&){ + for (size_t presses = 0; presses < 8; presses++){ + game.close_panel("Waiting for the president to walk off"); + } + return steps(Direction::NORTH, 10, "Up the main road"); + })); + return checkpoint; +} + +class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ +public: + ConsoleJubilifeActions(SingleSwitchProgramEnvironment& env, ProControllerContext& context) + : ConsoleMartLadyActionsT(env, context) + , m_stream(env.console) + , m_context(context) + {} + virtual Outcome climb_the_steps() override{ + route_turn(m_stream, m_context, Direction::WEST); + walk_for(m_stream, m_context, -1, 0, 200ms, "To the steps"); + const WalkResult up = walk_for(m_stream, m_context, 0, 1, 480ms, "Up the steps"); + if (up.stop == WalkStop::DIALOG || up.stop == WalkStop::TRANSITION){ + return outcome_failed("walk_failed", std::string("up the steps: ") + walk_stop_name(up.stop)); + } + return outcome_ok(); + } + virtual Outcome heal_at_center() override{ + try{ + heal_at_pokemon_center(m_stream, m_context, "Jubilife Pokemon Center"); + }catch (OperationFailedException& e){ + return outcome_failed("heal_failed", e.message()); + } + return outcome_ok(); + } + virtual void turn(Direction direction) override{ + route_turn(m_stream, m_context, direction); + } + virtual std::optional school_door_across() override{ + m_context.wait_for_all_requests(); + VideoSnapshot screen = m_stream.video().snapshot(); + if (!screen){ + return std::nullopt; + } + const std::optional lamp = find_lit_lamp_x(*screen.frame, SCHOOL_LAMP_BOX); + if (!lamp){ + m_stream.log("[Route] School's door: no lit lamp on screen."); + return std::nullopt; + } + const double across = tiles_across_at(*lamp) - tiles_across_at(SCHOOL_LAMP_X_AT_DOOR); + m_stream.log("[Route] School's door: the lamp at " + std::to_string(*lamp) + " of the screen, the door " + + std::to_string(across) + " tiles across."); + return across; + } + virtual void step_across(double tiles, const std::string& label) override{ + m_stream.log("[Route] " + label + ": " + std::to_string(tiles) + " tiles across."); + move_across(m_stream, m_context, tiles); + m_context.wait_for_all_requests(); + } + virtual DialogueRead walk_until_talked_to(size_t tiles, const std::string& label) override{ + DialogueRead read; + const Milliseconds limit = (int64_t)tiles * ROUTE_MS_PER_TILE_UP_DOWN + 800ms; + walk_until(m_stream, m_context, 0, 1, limit, label); + // He walks up after the "!" and only then talks. + ShortDialogWatcher opened(COLOR_CYAN); + if (wait_until(m_stream, m_context, 5000ms, {{opened}}) != 0){ + m_stream.log("[Talk] " + label + ": no one talked."); + return read; + } + read.opened = true; + PokemonBDSP::finish_dialogue(m_stream, m_context, BUTTON_B, label); + read.closed = true; + return read; + } + virtual Outcome hold(Direction direction, int64_t ms, const std::string& label) override{ + const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; + const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; + const WalkResult walk = walk_for(m_stream, m_context, x, y, Milliseconds(ms), label); + if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ + return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); + } + return outcome_ok(); + } + virtual bool indoors() override{ + m_context.wait_for_all_requests(); + VideoSnapshot screen = m_stream.video().snapshot(); + return screen && PokemonBDSP::indoors(*screen.frame); + } + virtual Outcome leave_south(const std::string& label) override{ + try{ + leave_door_south(m_stream, m_context, label); + }catch (OperationFailedException& e){ + return outcome_failed("walk_failed", e.message()); + } + return outcome_ok(); + } + virtual void nudge(Direction direction, const std::string& label) override{ + route_turn(m_stream, m_context, direction); + walk_for(m_stream, m_context, direction == Direction::EAST ? 1 : -1, 0, 80ms, label); + } + virtual Outcome tile(Direction direction, const std::string& label) override{ + const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; + const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; + const WalkResult walk = walk_for(m_stream, m_context, x, y, 160ms, label); + if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ + return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); + } + return outcome_ok(); + } + virtual Outcome taps(Direction direction, size_t tiles, const std::string& label) override{ + const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; + const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; + m_stream.log("[Walk] " + label + ": " + std::to_string(tiles) + " taps."); + for (size_t i = 0; i < tiles; i++){ + pbf_move_left_joystick(m_context, {x, y}, 160ms, 400ms); + } + pbf_wait(m_context, 800ms); + m_context.wait_for_all_requests(); + return outcome_ok(); + } + virtual DialogueRead talk_ahead(Button next, const std::string& label) override{ + DialogueRead read; + pbf_press_button(m_context, BUTTON_A, 80ms, 0ms); + m_context.wait_for_all_requests(); + ShortDialogWatcher opened(COLOR_CYAN); + if (wait_until(m_stream, m_context, 2000ms, {{opened}}) != 0){ + m_stream.log("[Talk] " + label + ": no dialogue opened."); + return read; + } + read.opened = true; + this->finish_talk(read, next, label); + return read; + } + virtual void close_panel(const std::string& label) override{ + m_stream.log("[Talk] " + label + ": B to close the panel."); + pbf_press_button(m_context, BUTTON_B, 80ms, 1500ms); + m_context.wait_for_all_requests(); + } + virtual bool through_door_north(const std::string& label) override{ + m_context.wait_for_all_requests(); + VideoSnapshot before = m_stream.video().snapshot(); + const WalkResult up = walk_until(m_stream, m_context, 0, 1, 1200ms, label); + m_context.wait_for(1500ms); + if (up.stop == WalkStop::TRANSITION){ + return true; + } + // The School's fade is not always black enough to read as one: r146b + // went in (changes 156, 261) and read "limit". A whole new scene is + // through, as leave_door_south() counts it. + VideoSnapshot after = m_stream.video().snapshot(); + const bool through = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE; + if (through){ + m_stream.log("[Route] " + label + ": a new scene; through the door."); + } + return through; + } +private: + VideoStream& m_stream; + ProControllerContext& m_context; +}; + +void diagnostic_jubilife_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleJubilifeActions game(env, context); + Checkpoint checkpoint = jubilife_center_checkpoint(game); + run_on_console(env, checkpoint, JUBILIFE_CENTER_NAME); +} + +void diagnostic_jubilife_clown_company(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleJubilifeActions game(env, context); + Checkpoint checkpoint = jubilife_clown_company_checkpoint(game); + run_on_console(env, checkpoint, JUBILIFE_CLOWN_COMPANY_NAME); +} + +void diagnostic_jubilife_poketch(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleJubilifeActions game(env, context); + Checkpoint checkpoint = jubilife_poketch_checkpoint(game); + run_on_console(env, checkpoint, JUBILIFE_POKETCH_NAME); +} + +void diagnostic_jubilife_clown_mart(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleJubilifeActions game(env, context); + Checkpoint checkpoint = jubilife_clown_mart_checkpoint(game); + run_on_console(env, checkpoint, JUBILIFE_CLOWN_MART_NAME); +} + +void diagnostic_jubilife_president(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleJubilifeActions game(env, context); + Checkpoint checkpoint = jubilife_president_checkpoint(game); + run_on_console(env, checkpoint, JUBILIFE_PRESIDENT_NAME); +} + +void diagnostic_jubilife_barry(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleJubilifeActions game(env, context); + Checkpoint checkpoint = jubilife_barry_checkpoint(game); + run_on_console(env, checkpoint, JUBILIFE_BARRY_NAME); +} + +void diagnostic_jubilife_school(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleJubilifeActions game(env, context); + Checkpoint checkpoint = jubilife_school_checkpoint(game); + run_on_console(env, checkpoint, JUBILIFE_SCHOOL_NAME); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h new file mode 100644 index 0000000000..4415b1b61b --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h @@ -0,0 +1,344 @@ +/* BDSP AutoStory - Route 202 to Sandgem's Pokemon Center, as events + * + * From: https://github.com/PokemonAutomation/ + * + * A test checkpoint, and the first written with the story event model + * (Pokemon/Navigation/Pokemon_StoryEvents.h): from Route 202's save spot + * under the south ledge, south into Sandgem Town and in through the + * Pokemon Center's door. It neither heals nor saves, so the save stays on + * Route 202 and the run can be repeated. + * + * The game steps are behind SandgemCenterActions, so the events and their + * recoveries can be run against a scripted fake. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_SandgemCenterEvents_H +#define PokemonAutomation_PokemonBDSP_AutoStory_SandgemCenterEvents_H + +#include +#include +#include +#include +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Navigation/Pokemon_StoryEvents.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// What a talk showed: whether a dialogue opened, whether it had closed +// after, and what OCR read in its first box. +struct DialogueRead{ + bool opened = false; + bool closed = false; + std::string text; +}; + +// The game steps the checkpoint is made of. +class SandgemCenterActions{ +public: + virtual ~SandgemCenterActions() = default; + + // The map named on the save screen's card, as read (see + // read_location_from_save_screen()); back on the overworld after. + virtual std::string save_screen_location() = 0; + + // Walks the story maps from any of `at` until every tile we may be on + // is `goal`, and sets `at` to those tiles. A walk onto another map must + // see its location banner on the way (walk_route()). + virtual Pokemon::Outcome walk( + std::vector& at, + const std::function& goal, + const std::string& label + ) = 0; + + // East until the trees stop us, then south until a wall does. Returns + // whether Sandgem Town's location banner showed on the way. + virtual bool east_then_south_into_sandgem() = 0; + + // walk() with town_plan_costs(): short open legs allowed, so the plan + // can step a tile across and go straight through a gap. + virtual Pokemon::Outcome walk_in_town( + std::vector& at, + const std::function& goal, + const std::string& label + ) = 0; + + // Faces `facing` and presses A: whether a box opened at the bottom of + // the screen (a sign's may be green, not a dialogue's), what OCR reads + // in it, and whether B closed it. + virtual DialogueRead read_sign(Pokemon::Direction facing, const std::string& label) = 0; + + // The lit doorway nearest the middle of the screen, if any. + virtual std::optional lit_doorway() = 0; + + // From the doors' row: lines up with the lit doorway on screen, across, + // without going in (line_up_with_lit_door()). + virtual Pokemon::Outcome line_up_with_door() = 0; + + // Whether a Pokemon Center's inside shows (pokemon_center_inside()). + virtual bool inside_pokemon_center() = 0; + + // Anywhere in Sandgem Town: east until a wall stops us, then north + // until one does, which ends on the doors' row or above it, and never + // walks into a door. + virtual Pokemon::Outcome east_then_north_in_sandgem() = 0; + + virtual bool dialogue_showing() = 0; + virtual Pokemon::Outcome finish_dialogue() = 0; +}; + + +// Where the walk south stops in Sandgem Town, in world tiles of +// story_maps(): Route 202's column 30 runs into Sandgem's column 29, and +// walking south from Route 202 stops at (29,5), above the Poke Mart, where +// the road narrows (the console counted it as two rows above the signs' row +// on the image grid; the decomp has three, with (29,6) a wall). +Pokemon::TilePos sandgem_entry_stop(); + +// Below the Center's sign, three tiles east of its door: where 00.04's +// heal walks to (r75), with the doorway in sight. +Pokemon::TilePos below_sandgem_center_sign(); + +// On the doors' row, east of the Lab's sign, where a walk west along the +// row stops (checked on the console). +Pokemon::TilePos east_of_lab_sign(); + +// Whether `doorway` is straight ahead on the row below it: about a row up, +// within half a tile across. +bool doorway_lined_up(const LitDoorway& doorway); + +// Whether a sign's OCR text has `key` (lower case), ignoring case. +bool sign_text_matches(const std::string& text, const std::string& key); + +// Whether `doorway` is the Center's, seen from the row below its door: up +// the screen by about a row, and no further across than the Center's sign. +// The Mart's doorway, seven tiles east of the sign, is too far off the +// middle to be found from there (find_lit_doorway()). +bool center_doorway_at_front_row(const LitDoorway& doorway); + + + +// The checkpoint's events, run against `game`, which must outlive it. It +// ends in front of the Center's door, not inside. +// Events: +// 1. confirm_on_route_202 the save screen reads Route 202 +// 2. walk_to_sandgem the pose is on Sandgem Town +// 3. walk_to_center_sign below the Center's sign (planned, town costs) +// 4. read_center_sign it reads, facing north +// 4. at_center_door the doorway straight ahead, a row up, outside +Pokemon::Checkpoint route202_to_sandgem_center_checkpoint(SandgemCenterActions& game); + +extern const char* const ROUTE202_TO_SANDGEM_CENTER_NAME; + +// DIAGNOSTIC ONLY: Remove before pushing to production. +// Runs the checkpoint on the console, from Route 202's save spot. Fails +// with a screenshot, naming the event and why, if it does not complete. +void diagnostic_route202_to_sandgem_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + + + +// The game steps of the Poke Mart lady checkpoint. +class MartLadyActions : public SandgemCenterActions{ +public: + // Walks `direction` until something stops us, for at most `tiles` + // tiles' time. Fails on a transition or a dialogue. + virtual Pokemon::Outcome walk_until_stopped(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; + + // Faces `direction` and presses A. If a dialogue opens, reads its first + // box, then presses `next` once per line, 1.5 s apart, while it shows. + virtual DialogueRead talk(Pokemon::Direction direction, Button next, const std::string& label) = 0; + + // One tile `direction`: the one hold that is not a walk until stopped. + // The walk after it stops on something that tells whether it took. + virtual Pokemon::Outcome step(Pokemon::Direction direction, const std::string& label) = 0; + + // Walks `direction`, pressing A after each short stretch, for at most + // `tiles` tiles' time until someone answers; then one A per line, 1.5 s + // apart, while the box shows. + virtual DialogueRead talk_walking(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; + + // As 00.04 walks it after the Potions, from the trees east of the Mart + // (the front row's east end) to Route 202's save spot, and the save + // there, which must read Route 202. + virtual Pokemon::Outcome walk_to_route_202_save_spot() = 0; + virtual Pokemon::Outcome save_on_route_202() = 0; +}; + +// West of Route 201's border sign, in world tiles of story_maps(): the +// only tile it is read from facing east. +Pokemon::TilePos route201_sign_west(); + +// The east end of the lady's row, against Sandgem's fence. +Pokemon::TilePos route201_row_3_east_end(); + +// From below Sandgem's Pokemon Center sign (where the Center checkpoint +// ends): to the Poke Mart lady and back to Route 202's save spot, where the +// save it starts from stands, and save there, so it can be repeated. Every +// leg but three one-tile steps walks until something stops it, as measured +// on the console (2026-10-30, CFR "walk"): +// 1. on_doors_row below the Center's sign +// 2. west_to_lab_sign west until the Lab's sign stops us +// 3. read_lab_sign facing west, "Pokemon Research Lab" +// 4. west_along_the_path south a tile, west to the grass's edge +// 5. north_to_the_trees north until the trees stop us (2 rows) +// 6. talk_to_mart_lady east until she stops us; she talks +// 7. back_to_route_201_sign south a tile, east until the sign stops us +// 8. read_route_201_sign facing east, "Route 201 -> Sandgem Town" +// 9. east_along_the_path south a tile, east to the trees, north a row +// 10. walk_to_route_202_save_spot the walk ended there (Segment 00's) +// then the save, which must read Route 202. +Pokemon::Checkpoint sandgem_mart_lady_checkpoint(MartLadyActions& game); + +extern const char* const SANDGEM_MART_LADY_NAME; + +// 00.04c's Potions: the Mart lady checkpoint, from the doors' row in front +// of Sandgem's Pokemon Center, without its save: it ends on Route 202's +// save spot, where 00.04d saves. +void mart_lady_without_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +// DIAGNOSTIC ONLY: Remove before pushing to production. +// Runs the Mart lady checkpoint on the console, from in front of Sandgem's +// Pokemon Center door. +void diagnostic_sandgem_mart_lady(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + + +// The game steps of 00.05's first two checkpoints, in Jubilife. +class JubilifeActions : public MartLadyActions{ +public: + // From 00.05's save below the steps, against the wall east of them: a + // tile west and up the steps onto the main road. + virtual Pokemon::Outcome climb_the_steps() = 0; + // From in front of a Pokemon Center, its lit doorway on screen: heals + // and walks out. Ends on the Center's doorstep. + virtual Pokemon::Outcome heal_at_center() = 0; + // Faces `direction` without moving: a tap the game takes as a turn. + virtual void turn(Pokemon::Direction direction) = 0; + // Half a tile `direction` (EAST or WEST), facing it first. + virtual void nudge(Pokemon::Direction direction, const std::string& label) = 0; + // On the School's doorsteps' row, near its door: tiles across from the + // player to the door's column, read from the lit lamp east of it; + // nothing if no lamp is lit (by day). + virtual std::optional school_door_across() = 0; + // Moves across by `tiles` (positive: east). + virtual void step_across(double tiles, const std::string& label) = 0; + // Whether the screen shows a building's inside. + virtual bool indoors() = 0; + // Holds the stick `direction` for `ms`, from standing still. + virtual Pokemon::Outcome hold(Pokemon::Direction direction, int64_t ms, const std::string& label) = 0; + // Walks north for at most `tiles` tiles' time until someone's talk + // opens (a person who sees us walks up and talks), then reads it on + // with B, which never starts a talk over. + virtual DialogueRead walk_until_talked_to(size_t tiles, const std::string& label) = 0; + // Inside a building whose door is straight south: out through it. Fails + // if three walks south do not change the scene. + virtual Pokemon::Outcome leave_south(const std::string& label) = 0; + // One tile `direction` from standing still, facing that way already. + virtual Pokemon::Outcome tile(Pokemon::Direction direction, const std::string& label) = 0; + // `tiles` one-tile taps `direction` in a row, facing that way already, + // without waiting for the screen to settle in between: six of these + // moved exactly six tiles on the console, where six tile() calls moved + // seven (2026-10-30). + virtual Pokemon::Outcome taps(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; + // Presses A the way we face, without the turn talk() taps first: a turn + // tap the way we already face is a step. Reads a talk as talk() does. + virtual DialogueRead talk_ahead(Button next, const std::string& label) = 0; + // Presses B once: closes a help panel (the Poketch's) that is not a + // dialogue box. Does nothing in the overworld. + virtual void close_panel(const std::string& label) = 0; + // Walks north; true if a door took us through. + virtual bool through_door_north(const std::string& label) = 0; +}; + +// 00.05a: from 00.05's save below Jubilife's steps to the Pokemon Center, +// healed, on its doorstep. Measured on the console (2026-10-30): +// 1. up_the_steps a tile west, then north up the steps +// 2. west_to_the_fence west until the fence's end stops us +// 3. north_to_the_sign north until the sign by the west lamp stops us +// 4. east_to_the_house east across the road until the house stops us +// 5. north_to_the_center north until the Center's doorway stops us +// 6. heal in, the nurse, out onto the doorstep +// No save: the save stays below the steps, so it can be repeated. +Pokemon::Checkpoint jubilife_center_checkpoint(JubilifeActions& game); +extern const char* const JUBILIFE_CENTER_NAME; + +// 00.05b: from the Center's doorstep into the Trainers' School. Nothing on +// the doorsteps' row stops a walk, so the School's door is counted: eleven +// one-tile steps west (measured both ways), then north; half a tile either +// side if the School's front stops us. +// 1. into_the_school through the School's door +Pokemon::Checkpoint jubilife_school_checkpoint(JubilifeActions& game); +extern const char* const JUBILIFE_SCHOOL_NAME; + +// 00.05c: in the Trainers' School, from its door: Barry, who gives the Town +// Map, and back out onto the School's doorstep. +// 1. up_to_barry north up the door's column to the back wall +// 2. talk_to_barry face west: his scene, to "You obtained a Town Map!" +// 3. out_of_the_school south down the door's column and out +Pokemon::Checkpoint jubilife_barry_checkpoint(JubilifeActions& game); +extern const char* const JUBILIFE_BARRY_NAME; + +// 00.05d: from the School's doorstep, the Poketch Company president on the +// main road, who sends us for the clowns' coupons. Stepped through on the +// console (2026-10-30): four tiles east is the road; a couple of tiles +// north he sees us, walks up and talks. +// 1. into_the_road a tap east, four one-tile steps +// 2. meet_the_president north until his talk opens; read it with B +Pokemon::Checkpoint jubilife_president_checkpoint(JubilifeActions& game); +extern const char* const JUBILIFE_PRESIDENT_NAME; + +// 00.05e: from where the president leaves us, the clown by the Poke Mart, +// for Coupon 1. Stepped through on the console (2026-10-30): east stops +// at the lamp by the Center; a tile west and a second north put us on the +// road between the Center and the Mart; east along it stops at the tall +// building, a tile east and a row north of the clown. +// 1. east_to_the_lamp east until the lamp by the Center stops us +// 2. up_to_the_mart_road a tile west, north about four rows +// 3. east_to_the_building east until the building stops us +// 4. talk_to_clown a row south, face west, A; his question's +// default answer is right; Coupon 1 +Pokemon::Checkpoint jubilife_clown_mart_checkpoint(JubilifeActions& game); +extern const char* const JUBILIFE_CLOWN_MART_NAME; + +// 00.05f: from beside the TV station clown (after 00.05e and his coupon), +// the clown in front of the Poketch Company, for Coupon 3. Stepped through +// on the console (2026-10-30): west, south and west stop at the lamp at the +// long road's west end; north stops a row up; seven tiles east and north +// stop at the Poketch Company's front wall, between its doors. The clown +// wanders a tile or two by the sign south-west of there: sweep the rows +// below, pressing A. +// 1. to_the_lamp west, south, west, north: the lamp's corner +// 2. to_the_company seven tiles east, north to the front wall +// 3. talk_to_clown a row south: east 3, west 6 pressing A; a +// second row the same; Coupon 3 +Pokemon::Checkpoint jubilife_clown_company_checkpoint(JubilifeActions& game); +extern const char* const JUBILIFE_CLOWN_COMPANY_NAME; +// 00.05g: from where 00.05f leaves us (in the gap beside the Poketch +// Company's sign) to the president, who trades the coupons for the Poketch, +// then up the main road to the street east to Route 203. +Pokemon::Checkpoint jubilife_poketch_checkpoint(JubilifeActions& game); +extern const char* const JUBILIFE_POKETCH_NAME; +// DIAGNOSTIC ONLY: Remove before pushing to production. +void diagnostic_jubilife_clown_company(SingleSwitchProgramEnvironment& env, ProControllerContext& context); +void diagnostic_jubilife_poketch(SingleSwitchProgramEnvironment& env, ProControllerContext& context); +// DIAGNOSTIC ONLY: Remove before pushing to production. +void diagnostic_jubilife_clown_mart(SingleSwitchProgramEnvironment& env, ProControllerContext& context); +// DIAGNOSTIC ONLY: Remove before pushing to production. +void diagnostic_jubilife_president(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + +// DIAGNOSTIC ONLY: Remove before pushing to production. +void diagnostic_jubilife_barry(SingleSwitchProgramEnvironment& env, ProControllerContext& context); +// DIAGNOSTIC ONLY: Remove before pushing to production. +void diagnostic_jubilife_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context); +void diagnostic_jubilife_school(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp new file mode 100644 index 0000000000..a6221ee38d --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -0,0 +1,1386 @@ +/* BDSP AutoStory - Segment 00 + * From player's bedroom to pokedex acquisition. + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/GlobalAutoPaths.h" +#include "PokemonBDSP_AutoStory_Segment_00.h" +#include "PokemonBDSP_AutoStory_SandgemCenterEvents.h" +#include "../Data/PokemonBDSP_AutoStory_JubilifeCity.h" +#include "../Data/PokemonBDSP_AutoStory_Route202.h" +#include "../Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "../Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "../Utils/PokemonBDSP_AutoStory_Location.h" +#include "../Utils/PokemonBDSP_AutoStory_PokemonCenter.h" +#include "../Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "../Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "../Utils/PokemonBDSP_AutoStory_StoryExecutor.h" +#include "../PokemonBDSP_AutoStory_StoryTable.h" +#include "../Utils/PokemonBDSP_AutoStory_Walk.h" +#include "../Utils/PokemonBDSP_OverworldMatchers.h" + +#include +#include +#include +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "Pokemon/Pokemon_Strings.h" +#include "Pokemon/Pokemon_Notification.h" + +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP/Programs/ShinyHunting/PokemonBDSP_StarterReset.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "CommonTools/VisualDetectors/ImageMatchDetector.h" + +#include "NintendoSwitch/Programs/DateManip/NintendoSwitch_DateManip.h" + +#include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" + +using namespace std::chrono_literals; +using PokemonAutomation::Pokemon::MapSet; +using PokemonAutomation::Pokemon::TilePos; +using PokemonAutomation::Pokemon::TrainerSight; + +namespace PokemonAutomation { + namespace NintendoSwitch { + namespace PokemonBDSP { + // --------------------------------------------------------------------------- + // Starter selection + // --------------------------------------------------------------------------- + + // Watch before movement starts, release on the fade, and hold the + // following recorded wait until the destination is visible again. + static void recorded_move_until_transition( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds duration, const std::string& label + ) { + context.wait_for_all_requests(); + stream.log("[Tutorial transition] " + label + ": approaching fade."); + BlackScreenWatcher black; + int ret = run_until(stream, context, + [x, y, duration](ProControllerContext& ctx) { + pbf_move_left_joystick(ctx, {x, y}, duration, 0ms); + // Keep observing a delayed fade after the recorded hold. + pbf_wait(ctx, 5000ms); + }, {{black}}); + if (ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Black transition not detected.", stream); + } + BlackScreenWatcher visible(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, + 100, 10, BlackScreenWatcher::FinderType::GONE, 500ms); + if (wait_until(stream, context, 15s, {{visible}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Transition did not finish.", stream); + } + stream.log("[Tutorial transition] " + label + ": destination visible."); + } + + static void select_turtwig(VideoStream& stream, ProControllerContext& context) { + stream.log("[DEBUG] Turtwig Selection: Starting starter selection"); + + pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); + context.wait_for_all_requests(); + + ShortDialogWatcher watcher(COLOR_CYAN); + int ret = run_until( + stream, context, + [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, + { watcher } + ); + if (ret != 0) { + ShortDialogWatcher watcher_red(COLOR_RED); + ret = run_until( + stream, context, + [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, + { watcher_red } + ); + } + stream.log(ret == 0 ? "[DEBUG] Turtwig Selection: Confirmation dialogue detected" + : "[WARNING] Turtwig Selection: Dialogue detection failed, proceeding anyway"); + + pbf_wait(context, 400ms); + pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); + pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); + context.wait_for_all_requests(); + stream.log("[DEBUG] Turtwig Selection: Selection complete"); + } + + static void select_chimchar(VideoStream& stream, ProControllerContext& context) { + stream.log("[DEBUG] Chimchar Selection: Starting starter selection"); + + pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); + pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); + context.wait_for_all_requests(); + + ShortDialogWatcher watcher(COLOR_CYAN); + int ret = run_until( + stream, context, + [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, + { watcher } + ); + if (ret != 0) { + ShortDialogWatcher watcher_red(COLOR_RED); + ret = run_until( + stream, context, + [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, + { watcher_red } + ); + } + stream.log(ret == 0 ? "[DEBUG] Chimchar Selection: Confirmation dialogue detected" + : "[WARNING] Chimchar Selection: Dialogue detection failed, proceeding anyway"); + + pbf_wait(context, 400ms); + pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); + pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); + context.wait_for_all_requests(); + stream.log("[DEBUG] Chimchar Selection: Selection complete"); + } + + static void select_piplup(VideoStream& stream, ProControllerContext& context) { + stream.log("[DEBUG] Piplup Selection: Starting starter selection"); + + pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); + pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); + pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); + context.wait_for_all_requests(); + + ShortDialogWatcher watcher(COLOR_CYAN); + int ret = run_until( + stream, context, + [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, + { watcher } + ); + if (ret != 0) { + ShortDialogWatcher watcher_red(COLOR_RED); + ret = run_until( + stream, context, + [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, + { watcher_red } + ); + } + stream.log(ret == 0 ? "[DEBUG] Piplup Selection: Confirmation dialogue detected" + : "[WARNING] Piplup Selection: Dialogue detection failed, proceeding anyway"); + + pbf_wait(context, 400ms); + pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); + pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); + context.wait_for_all_requests(); + stream.log("[DEBUG] Piplup Selection: Selection complete"); + } + + // Navigation functions for each part of the tutorial, used in order; each relies on the game state the one before left. + // TODO (task #22): they replay Butters' recorded stick holds; the same walks are lines on the decomp grids in Data/PokemonBDSP_AutoStory_Segment00Lines, + // tested offline with the route records. Swap in walk_waypoints() over Segment00Lines::lines() only after a new-game run has confirmed where each scene leaves the player, with walk_through_grass()'s battle handling on the Route 201 grass. + + // --------------------------------------------------------------------------- + // Segment 00-01 begins + // Navigates from player's room to Route 201 through Mom/Barry dialogues. + // --------------------------------------------------------------------------- + + static void tutorial_part_1_navigation(SingleSwitchProgramEnvironment& env, + VideoStream& stream, ProControllerContext& context) { + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 527ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, 1.000000 }, 456ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 915ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, 1.000000 }, 642ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 557ms, 0ms); + pbf_wait(context, 978ms); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1112ms, 0ms); + pbf_wait(context, 4967ms); + + wait_for_dialogue(stream, context, "Phase 1 (Mom)"); + + pbf_mash_button(context, BUTTON_A, 5000ms); + pbf_wait(context, 300ms); + + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1662ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, -1.000000 }, 49ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1215ms, 0ms); + pbf_wait(context, 1419ms); + + wait_for_dialogue(stream, context, "Phase 2 (Mom Warning)"); + + pbf_mash_button(context, BUTTON_A, 2500ms); + pbf_wait(context, 300ms); + context.wait_for_all_requests(); + + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 680ms, 0ms); + + pbf_wait(context, 4000ms); + + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1748ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 18ms, 0ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 2763ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 58ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1364ms, 0ms); + pbf_wait(context, 4000ms); + context.wait_for_all_requests(); + + wait_for_dialogue(stream, context, "Phase 3 (Barry 1st)"); + pbf_mash_button(context, BUTTON_A, 6000ms); + pbf_wait(context, 800ms); + wait_for_dialogue(stream, context, "Phase 3 (Barry 1st)"); + pbf_mash_button(context, BUTTON_A, 6000ms); + pbf_wait(context, 800ms); + + pbf_wait(context, 1306ms); + context.wait_for_all_requests(); + stream.log("[Barry house] Entering; waiting for entry fade."); + BlackScreenWatcher entry_black; + int entry_ret = run_until(stream, context, + [](ProControllerContext& ctx) { + pbf_move_left_joystick(ctx, {0, 1}, 5000ms, 0ms); + }, {{entry_black}}); + if (entry_ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Barry house: Entry fade not detected.", stream); + } + // Release the joystick during loading; resume indoor movement only + // after the screen has stayed non-black for half a second. + BlackScreenWatcher entry_visible(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, + 100, 10, BlackScreenWatcher::FinderType::GONE, 500ms); + if (wait_until(stream, context, 15s, {{entry_visible}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Barry house: Entry fade did not finish.", stream); + } + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + stream.log("[Barry house] Entry settled; starting stair approach."); + // Align left after entry before the next upward stair approach. + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 400ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 1600ms, 0ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 2722ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1600ms, 0ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 501ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 652ms, 0ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 1817ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 975ms, 0ms); + pbf_wait(context, 1422ms); + + wait_for_dialogue(stream, context, "Phase 8 (Barry 2nd dialog)"); + pbf_mash_button(context, BUTTON_A, 10000ms); + + pbf_wait(context, 856ms); + + + // Recorded route from the top of Barry's stairs. Moves rounded to + // 100ms, waits to 50ms; transition waits gate both floor changes. + recorded_move_until_transition(stream, context, -1.0, 0.0, 900ms, + "Barry house downstairs"); + pbf_wait(context, 750ms); + pbf_move_left_joystick(context, {-1.0, 0.0}, 2000ms, 0ms); + pbf_wait(context, 250ms); + pbf_move_left_joystick(context, {0.0, -1.0}, 2000ms, 0ms); + pbf_wait(context, 150ms); + pbf_move_left_joystick(context, {1.0, 0.0}, 1400ms, 0ms); + pbf_wait(context, 500ms); + recorded_move_until_transition(stream, context, 0.0, -1.0, 1200ms, + "Barry house exit"); + // First-run video placed the player east of the northbound path. + // Shorten the rightward approach; tune further from the next recording. + stream.log("[Route 201] Aligning east for 2300ms before northbound approach."); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 2300ms, 0ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 5861ms, 0ms); + pbf_wait(context, 134ms); + + wait_for_dialogue(stream, context, "Phase 9 (Barry Route 201)"); + + pbf_mash_button(context, BUTTON_B, 6000ms); + pbf_wait(context, 800ms); + + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 700ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 279ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 10ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 145ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 7703ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 140ms, 0ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 1700ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 39ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 544ms, 1000ms); + + pbf_wait(context, 1000ms); + + // save_game(stream, context); // Disabled: checkpoint save mode controls saving. + context.wait_for_all_requests(); + } + // --------------------------------------------------------------------------- + // End of Segment 00 - 01 + // Standing in front lake varity ready to enter lake Verity for starter selection. + // --------------------------------------------------------------------------- + // --------------------------------------------------------------------------- + // Segment 00-02 begins + // Navigates into lake Verity to recieve starter + // --------------------------------------------------------------------------- + static void starter_nav(VideoStream& stream, ProControllerContext& context) { + // re-sync control + pbf_press_button(context, BUTTON_B, 20ms, 200ms); + pbf_wait(context, 2000ms); + context.wait_for_all_requests(); + + // continue movement + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 300ms, 0ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 300ms, 0ms); + + wait_for_dialogue(stream, context, "Phase 10 (Barry before the starter selection)"); + + pbf_mash_button(context, BUTTON_A, 1000ms); + + pbf_wait(context, 500ms); + std::shared_ptr briefcase = + std::make_shared( + RESOURCE_PATH() + "PokemonBDSP/StarterBriefcase.png" + ); + + // Mash B until we see the briefcase. + ImageMatchWatcher detector(briefcase, { 0.5, 0.1, 0.5, 0.7 }, 100, true); + + int ret = run_until( + stream, context, + [](ProControllerContext& context) { + pbf_mash_button(context, BUTTON_B, 120000ms); + }, + { {detector} } + ); + + if (ret == 0) { + stream.log("Detected briefcase!"); + } + else { + stream.log("Timed out waiting for briefcase."); + throw OperationFailedExceptionWithScreenshot( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to detect briefcase in Segment 00.", + stream + ); + } + + // Wait for briefcase to fully open. + stream.log("Mashing B for briefcase to fully open."); + pbf_mash_button(context, BUTTON_B, 2000ms); + } + static void post_starly_clenaup(VideoStream& stream, ProControllerContext& context) { + pbf_mash_button(context, BUTTON_B, 60000ms); + } + // --------------------------------------------------------------------------- + // End of Segment 00 - 02 + // Standing in front lake varity after starter selection. + // --------------------------------------------------------------------------- + // --------------------------------------------------------------------------- + // Segment 00-03 begins + // Navigates from Exit of lake Verity to receipt of the Pokedex + // --------------------------------------------------------------------------- + + static void tutorial_part_2_navigation(SingleSwitchProgramEnvironment& env, + VideoStream& stream, ProControllerContext& context) { + pbf_wait(context, 2000ms); + context.wait_for_all_requests(); + + pbf_wait(context, 563ms); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1700ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, -1.000000 }, 510ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 50ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, -1.000000 }, 310ms, 0ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 5500ms, 0ms); + pbf_wait(context, 3963ms); + context.wait_for_all_requests(); + + wait_for_dialogue(stream, context, "Phase 10 (Professor dialog to mom)"); + pbf_mash_button(context, BUTTON_B, 40000ms); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 820ms, 0ms); + pbf_wait(context, 4000ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 800ms, 0ms); + pbf_wait(context, 555ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 50ms, 0ms); + pbf_wait(context, 23ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 5020ms, 0ms); + pbf_wait(context, 169ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 745ms, 0ms); + walk_through_grass(stream, context, 1, 0, 20, "right through grass to path", BattleType::STARTER); + walk_through_grass(stream, context, 0, 1, 18, "up through grass 2", BattleType::STARTER); + walk_through_grass(stream, context, 1, 0, 10, "right through grass 2", BattleType::STARTER); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 730ms, 0ms); + pbf_wait(context, 941ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 1950ms, 0ms); + wait_for_dialogue(stream, context, "Phase 11 (dawn conversation)"); + pbf_mash_button(context, BUTTON_B, 50000ms); + wait_for_dialogue(stream, context, "Phase 12 (Nickname reject)"); + pbf_mash_button(context, BUTTON_B, 40000ms); + wait_for_dialogue(stream, context, "Phase 13 (Pokedex accept)"); + pbf_mash_button(context, BUTTON_A, 40000ms); + + + // pokedex received + } + + + // --------------------------------------------------------------------------- + // End of Segment 00 - 03 + // Standing in front of the professor after receiving the pokedex. + // --------------------------------------------------------------------------- + + // --------------------------------------------------------------------------- + // Begin Segment 00 - 04 + // Will heal at Sandgem pokeCenter to reliably catch a second pokemon for Home + // transfer. + // Collects poketech and stands in front of the PC to end the tutorial section + // and send the starter to pokemon HOME. + // --------------------------------------------------------------------------- + + // Inside the player's house, just through the door: Mom watches TV a + // column east of the door and two rows north of the mat. The door fixes + // our column, but counted taps north lost a row (r108b stood a row below + // her). So find her row: face east and press A, and step one tile north + // at a time until she answers. A press from a row off talks to nobody, + // and the TV stand stops us a row above her. + static void talk_to_mom_at_home(VideoStream& stream, ProControllerContext& context) { + for (size_t row = 0; row < 4; row++) { + if (talk_to_adjacent_npc(stream, context, {Pokemon::Direction::EAST}, "Home: Mom")) { + stream.log("[00.04] Mom answered after " + std::to_string(row) + " step(s) north."); + return; + } + // Facing east, a short tap north only turns; facing north, it is one tile. + route_turn(stream, context, Pokemon::Direction::NORTH); + pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Home: Mom did not answer from any of four rows north of the door.", stream); + } + + static void tutorial_part_3_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { + // Arm exit detection before movement and finish the fade before + // starting the dialogue watcher, which cancels queued commands. + recorded_move_until_transition(stream, context, 0.0, -1.0, 2010ms, + "Rowan lab exit"); + context.wait_for_all_requests(); + + wait_for_dialogue(stream, context, "Phase 14 (dawn tour)"); + + pbf_mash_button(context, BUTTON_B, 25000ms); + + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 5125ms, 0ms); + pbf_wait(context, 336ms); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1700ms, 0ms); + pbf_wait(context, 213ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 2325ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 950ms, 0ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 2360ms, 0ms); + pbf_wait(context, 253ms); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 4610ms, 0ms); + pbf_wait(context, 954ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 220ms, 0ms); + pbf_wait(context, 587ms); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1375ms, 0ms); + pbf_wait(context, 464ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 775ms, 0ms); + pbf_wait(context, 1655ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 640ms, 0ms); + pbf_wait(context, 3128ms); + talk_to_mom_at_home(stream, context); + + wait_for_dialogue(stream, context, "Phase 15 (Mom conversation to accept parcel)"); + + pbf_mash_button(context, BUTTON_B, 40000ms); + + context.wait_for_all_requests(); + pbf_wait(context, 1500ms); + pbf_press_dpad(context, DPAD_LEFT, 150ms, 0ms); + pbf_wait(context, 1143ms); + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 875ms, 0ms); + pbf_wait(context, 1169ms); + pbf_press_button(context, BUTTON_A, 180ms, 0ms); + pbf_wait(context, 946ms); + pbf_press_button(context, BUTTON_A, 180ms, 0ms); + pbf_wait(context, 9092ms); + pbf_press_button(context, BUTTON_A, 180ms, 0ms); + pbf_wait(context, 740ms); + pbf_press_button(context, BUTTON_A, 180ms, 0ms); + pbf_wait(context, 2998ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 240ms, 0ms); + pbf_wait(context, 893ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 5300ms, 0ms); + pbf_wait(context, 282ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 260ms, 0ms); + context.wait_for_all_requests(); + pbf_wait(context, 1500ms); + + walk_through_grass(stream, context, 1, 0, 20, "right through grass to path", BattleType::STARTER); + walk_through_grass(stream, context, 0, 1, 18, "up through grass 2", BattleType::STARTER); + walk_through_grass(stream, context, 1, 0, 10, "right through grass 2", BattleType::STARTER); + context.wait_for_all_requests(); + pbf_wait(context, 1500ms); + + pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 560ms, 0ms); + pbf_wait(context, 1500ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 5000ms, 0ms); + pbf_wait(context, 1500ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1000ms, 0ms); + pbf_wait(context, 1500ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 1855ms, 0ms); + pbf_wait(context, 1500ms); + pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 430ms, 0ms); + pbf_wait(context, 1500ms); + pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 850ms, 0ms); + pbf_wait(context, 1500ms); + pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1120ms, 0ms); + + wait_for_dialogue(stream, context, "Phase 16 (catch tutorial)"); + + pbf_mash_button(context, BUTTON_B, 70000ms); + context.wait_for_all_requests(); + pbf_wait(context, 2000ms); + catch_1_pokemon(stream, context); + pbf_wait(context, 1500ms); + context.wait_for_all_requests(); + } + + // Align to a route reference before continuing recorded navigation. + // Matcher offsets and thresholds are fractions of screen width/height. + static void align_to_tutorial_landmarks( + VideoStream& stream, ProControllerContext& context, + OverworldMatchResult (*matcher)(const ImageRGB32&), const std::string& label + ) { + context.wait_for_all_requests(); + pbf_wait(context, 700ms); + context.wait_for_all_requests(); + size_t confirmations = 0, coarse_confirmations = 0; + double previous_dx = 0, previous_dy = 0; + for (size_t attempt = 0; attempt < 60; attempt++) { + const auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No video frame.", stream); + const auto result = matcher(*frame.frame); + VideoOverlaySet overlays(stream.overlay()); + for (const auto& landmark : result.landmarks) { + overlays.add(COLOR_CYAN, landmark.search); + if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); + stream.log("[" + label + "] " + landmark.name + + " score=" + std::to_string(landmark.score) + + " margin=" + std::to_string(landmark.margin) + + " accepted=" + std::to_string(landmark.accepted) + + " screen-relative offset=(" + std::to_string(landmark.dx) + + "," + std::to_string(landmark.dy) + ")"); + } + stream.log("[" + label + "] located=" + std::to_string(result.located) + + " mode=" + (result.coarse ? "vertical acquisition" : "fine alignment") + + " reference screen-relative offset=(" + std::to_string(result.dx) + + "," + std::to_string(result.dy) + ")"); + if (result.coarse && result.located) { + if (coarse_confirmations && std::abs(result.dx-previous_dx)<=0.00625 && + std::abs(result.dy-previous_dy)<=1.0/90) coarse_confirmations++; + else coarse_confirmations=1; + previous_dx=result.dx; + previous_dy=result.dy; + } else coarse_confirmations=0; + if (!result.located) { + confirmations = 0; + pbf_wait(context, 250ms); + } else if (result.coarse && coarse_confirmations<3) { + confirmations = 0; + stream.log("[" + label + "] Waiting for consistent vertical acquisition."); + pbf_wait(context, 250ms); + } else if (result.at_target) { + if (++confirmations >= 3) { + stream.log("[" + label + "] Reference position confirmed."); + return; + } + pbf_wait(context, 300ms); + } else { + confirmations = 0; + const bool horizontal = !result.coarse && + std::abs(result.dx) >= std::abs(result.dy)*9.0/16; + const double error = horizontal ? result.dx : result.dy*9.0/16; + const auto duration = std::abs(error) > 1.0/24 ? 80ms : + std::abs(error) > 0.015625 ? 40ms : 16ms; + stream.log("[" + label + "] Correcting " + + std::string(horizontal ? "horizontal" : "vertical") + " axis " + + (horizontal ? (result.dx>0 ? "right" : "left") : (result.dy<0 ? "up" : "down")) + + " for " + std::to_string(duration.count()) + "ms."); + // Reuse confirmed-Run encounter handling if correction enters grass. + walk_through_grass(stream, context, + horizontal ? (result.dx > 0 ? 1 : -1) : 0, + horizontal ? 0 : (result.dy < 0 ? 1 : -1), + 1, label, BattleType::STANDARD, duration); + } + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Reference position not confirmed; route movement stopped.", stream); + } + + static void align_to_sandgem_junction(VideoStream& stream, ProControllerContext& context) { + align_to_tutorial_landmarks(stream, context, match_sandgem_junction, "Sandgem junction"); + } + + // Fights a trainer whose dialogue has opened, with whatever starter we + // have. Someone who only talks leaves no battle to fight. + static void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name) { + pbf_mash_button(context, BUTTON_B, 5000ms); + context.wait_for_all_requests(); + BattleMenuWatcher battle_menu(BattleType::TRAINER, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + if (wait_until(stream, context, 8s, {{battle_menu}, {battle_dialog}}) < 0) { + stream.log("[00.04] " + name + ": no battle followed the talk."); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + return; + } + fight_trainer(stream, context, name); + context.wait_for_all_requests(); + // The next trainer may be a few tiles on: check HP now. With no + // Potion in the Bag this only looks. + use_potion_first_pokemon(stream, context, 0.5); + } + + // How many Potions to buy. A Potion is 300 and a new game starts with + // 3000; the money on screen cannot be read yet. Five heal the starter + // through Route 202's four trainers and leave half the money. + constexpr int POTIONS_TO_BUY = 5; + + // From below the Center's door to below the Poke Mart's, ten tiles + // east along the same row, and in. The last leg is open and movement + // is not locked to tiles, so we may stand part of a tile off the door: + // whole-tile steps either side missed it in r121a. The Mart's doorway + // is lit like the Center's, so it is lined up with by sight the same + // way. Two townspeople wander the dirt south of the Mart and stop a + // leg short without the walk knowing (r122a counted to the door's + // row from three rows south of it): where the doorway shows on screen + // says so, and the walk is made once more from there. + static void enter_sandgem_poke_mart( + VideoStream& stream, ProControllerContext& context, const MapSet& maps, + std::vector& trainers, const RouteTrainerFight& fight + ) { + const TilePos door = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); + const TilePos front = door.next(Pokemon::Direction::SOUTH); + const auto at_front = [&](const TilePos& pos) { return pos.y == front.y && std::abs(pos.x - front.x) <= 1; }; + walk_route(stream, context, maps, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), + at_front, trainers, fight, "To the Poke Mart"); + const std::vector short_of_door = tiles_short_of_door(stream, context, maps.world(), door, "To the Poke Mart"); + if (!short_of_door.empty()) { + route_status(stream, "To the Poke Mart: stopped short of the door; walking again."); + walk_route(stream, context, maps, short_of_door, at_front, trainers, fight, "To the Poke Mart again"); + } + enter_lit_door(stream, context, "Sandgem Poke Mart: through the door"); + } + + // Inside the Mart, the clerk's counter is straight ahead of the door + // (every Mart is laid out so; Oreburgh's and Eterna's are walked the + // same way). Walk up to it, buy the Potions, and walk back out. + // buy_items() opens the talk and finds the Potion row by OCR of its + // name and price; the quantity is counted out blind. + static void buy_potions_at_the_counter(VideoStream& stream, ProControllerContext& context) { + route_status(stream, "Poke Mart: to the clerk."); + const WalkResult walk = walk_until(stream, context, 0, 1, 6000ms, "Poke Mart: to the clerk"); + if (walk.stop != WalkStop::BLOCKED && walk.stop != WalkStop::DIALOG) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poke Mart: the counter was not reached (" + std::string(walk_stop_name(walk.stop)) + ").", stream); + } + route_status(stream, "Poke Mart: buying " + std::to_string(POTIONS_TO_BUY) + " Potions."); + if (!buy_items(stream, context, {{ShopItem::Potion, POTIONS_TO_BUY}})) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poke Mart: could not buy " + std::to_string(POTIONS_TO_BUY) + " Potions.", stream); + } + leave_door_south(stream, context, "Poke Mart: out the door"); + } + + // Potions let us heal where we stand. Sandgem's Poke Mart is next + // door east of its Center. The purchase is checked in the Bag after, + // since buy_items() counts the quantity out without reading it. + // Returns where we stand after: below the Mart's door. + static std::vector buy_potions_in_sandgem( + VideoStream& stream, ProControllerContext& context, const MapSet& maps, + std::vector& trainers, const RouteTrainerFight& fight + ) { + enter_sandgem_poke_mart(stream, context, maps, trainers, fight); + buy_potions_at_the_counter(stream, context); + if (!bag_has_item(stream, context, "potion", 1)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poke Mart: the purchase went through, but no Potion is in the Bag.", stream); + } + return story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); + } + + // A walk_until() callback that keeps every location banner it sees. + static std::function banner_keeper(std::vector& banners) { + return [&banners](const ImageViewRGB32& screen) { + const std::string read = read_location_banner(screen); + if (!read.empty() && (banners.empty() || banners.back() != read)) { + banners.push_back(read); + } + return false; + }; + } + + static bool any_banner_names(const std::vector& banners, const char* map) { + return std::any_of(banners.begin(), banners.end(), + [&](const std::string& read) { return location_banner_matches(read, map); }); + } + + // Whether all of `tiles` are on `map`. + static bool all_on_map(const MapSet& maps, const std::vector& tiles, const char* map) { + return !tiles.empty() && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos) { + const std::optional tile = maps.to_local(pos); + return tile && tile->map == map; + }); + } + + // North from below the gap between the signs until a wall stops us, + // nudging a tile west, twice at most, when blocked at once (below a + // sign or the Mart). Returns whether Route 202's banner showed: only + // one of the gap's columns leads out of town before a wall. + static bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context) { + using Pokemon::Direction; + std::vector banners; + for (size_t nudges = 0;; nudges++){ + const WalkResult north = walk_until(stream, context, 0, 1, 8000ms, "North through the gap", banner_keeper(banners)); + if (north.stop == WalkStop::TRANSITION || north.stop == WalkStop::DIALOG || north.wild_battles > 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "North through the gap: " + std::string(walk_stop_name(north.stop)) + " instead of a wall.", stream); + } + // Blocked at once: the wall reads after about 800ms still. + if (north.walked > ROUTE_START_DELAY + 800ms + 2 * ROUTE_MS_PER_TILE_UP_DOWN){ + break; + } + if (nudges == 2){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "North through the gap: still blocked after two tiles west.", stream); + } + route_status(stream, "North through the gap: blocked at once, below a sign or the Mart; a tile west."); + route_turn(stream, context, Direction::WEST); + walk_for(stream, context, -1, 0, route_open_hold(Direction::WEST, 1), "A tile west"); + route_turn(stream, context, Direction::NORTH); + } + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + banner_keeper(banners)(*screen.frame); + } + return any_banner_names(banners, MAP_ROUTE_202); + } + + // From the trees at the east end of the doors' row, west into the gap + // between the Center's sign and the Mart's (below_sign_gap()) and + // north through it until a wall stops us. Six tiles west from column + // 29 ends on 23, and on 20 to 26 if two tiles off: north from all of + // those passes no door, and only the gap lets us through. A walk + // north that goes nowhere stood below a sign or the Mart: a tile + // west, twice at most, never reaches the Center's door at 17. The + // planner would rather go round the south of town on walls, where + // townspeople wander. Returns where the walk north may have ended: + // the gap's columns end on Sandgem's top row, bar one that runs on to + // Route 202, and its banner tells the two apart. + static std::vector north_through_sign_gap(VideoStream& stream, ProControllerContext& context, const MapSet& maps){ + using Pokemon::Direction; + route_status(stream, "To the gap between the Center's and the Mart's signs: west 6."); + route_turn(stream, context, Direction::WEST); + walk_for(stream, context, -1, 0, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); + const bool on_route = walk_north_through_gap(stream, context); + std::vector ends; + for (const TilePos& pos : SandgemTown::below_sign_gap()){ + const TilePos end = Pokemon::walk_to_wall(maps.world(), maps.to_world(MAP_SANDGEM_TOWN, pos), Direction::NORTH).stop; + if (all_on_map(maps, {end}, MAP_ROUTE_202) == on_route){ + ends.push_back(end); + } + } + route_status(stream, std::string("North through the gap: Route 202's banner ") + + (on_route ? "showed; on Route 202." : "did not show; on Sandgem Town's top row.")); + return ends; + } + + // Back up to Route 202's save spot under the ledge: it is where a + // retry may start (story_save_spots()), its walls pin it, and the + // trainers are north of it. First east to the trees that end the + // doors' row, so the way north starts from one known tile, then north + // through the gap between the signs: r123a, planned round the south + // of town on a map that had it wrong, walked into a house. + static void walk_to_route_202_save_spot( + VideoStream& stream, ProControllerContext& context, const MapSet& maps, + std::vector at, std::vector& trainers, const RouteTrainerFight& fight + ) { + const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END); + const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); + if (!all_on_map(maps, at, MAP_ROUTE_202)) { + at = walk_route(stream, context, maps, std::move(at), + [&](const TilePos& pos) { return pos == row_end; }, trainers, fight, "To the trees east of the Poke Mart"); + at = north_through_sign_gap(stream, context, maps); + } + walk_route(stream, context, maps, std::move(at), + [&](const TilePos& pos) { return pos == save_spot; }, trainers, fight, "Back to Route 202"); + } + + // DIAGNOSTIC ONLY (the Mart lady checkpoint): 00.04's walk back to + // Route 202's save spot, from below Sandgem Center's door, without saving. + void walk_from_sandgem_center_to_route_202_save_spot( + VideoStream& stream, ProControllerContext& context, + const std::vector& at + ) { + const MapSet& maps = story_maps(); + std::vector trainers = story_trainer_sights(); + const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; + walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); + } + + // North into Jubilife until Dawn's scene starts (00.04d's + // into_jubilife). Jubilife's south edge is a wall with stairs in it, + // west of where the route may leave us (r78 stood at its east end). + // Entering starts a scene: Dawn walks over while we stand still. So + // walk north; if the wall stops us and no dialogue follows, the stairs + // are further west. The scene itself is the next step. + static void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { + for (size_t attempt = 0; attempt < 6; attempt++) { + const WalkResult walk = walk_until(stream, context, 0, 1, 4000ms, "to Dawn in Jubilife"); + if (walk.stop == WalkStop::TRANSITION) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: left the area instead of meeting Dawn.", stream); + } + ShortDialogWatcher dialog(COLOR_CYAN); + if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0) { + return; + } + stream.log("[00.04] Jubilife: the wall stopped us; looking for the stairs one tile west."); + route_turn(stream, context, Pokemon::Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: no stairs found and no Dawn after six tiles west.", stream); + } + + // Mart lady runs that failed this program run: the clerk after two. + static size_t g_mart_lady_misses = 0; + // 00.04c's potions step, from the save out of Sandgem's Center: the + // Poke Mart lady's (her checkpoint without its own save), or the + // clerk's after two misses, on to Route 202's save spot, where + // 00.04d's save goes. The Bag must have Potions after. + static Pokemon::Outcome sandgem_potions( + ConsoleStoryExecutor& executor, StoryConsole& c, Pokemon::RunState&, const Pokemon::StoryStep& + ) { + VideoStream& stream = c.env.console; + ProControllerContext& context = c.context; + // The clerk's list is found by reading item names, which needs a + // reader for the game's language; without one, the lady every time. + if (g_mart_lady_misses < 2 || !story_reads_text()) { + // Onto the doors' row first: out of the door we stand a tile to + // three below it (r261 walked west into the trees from three). + route_turn(stream, context, Pokemon::Direction::EAST); + walk_tiles_dpad(stream, context, 1, 0, 1, "east under the Center's wall"); + const WalkResult up = walk_until(stream, context, 0, 1, 3000ms, "north to the Center's front"); + if (up.stop != WalkStop::BLOCKED) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + ".", stream); + } + try { mart_lady_without_save(c.env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } + } else { + const std::vector at = buy_potions_in_sandgem(stream, context, executor.maps(), executor.trainers(), executor.fight(c)); + walk_to_route_202_save_spot(stream, context, executor.maps(), at, executor.trainers(), executor.fight(c)); + } + if (!bag_has_item(stream, context, "potion", 1)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after the shop.", stream); + } + return Pokemon::outcome_ok(); + } + // --------------------------------------------------------------------------- + // End of Segment 00 - 04 + // Standing in Jubilife after speaking to dawn. + // --------------------------------------------------------------------------- + // --------------------------------------------------------------------------- + // Begin Segment 00 - 05 + // Standing at the entrance to jubilife after speaking to dawn, will navigate to the poketech challenge. + // Collects poketech and stands in front of the PC to end the tutorial section + // and send the starter to pokemon HOME. + // --------------------------------------------------------------------------- + + // Stop the approach as soon as buildings can guide it, instead of + // overshooting south and then blindly walking back north. + static void approach_poketch_landmarks(VideoStream& stream, ProControllerContext& context) { + context.wait_for_all_requests(); + Milliseconds walked = 0ms; + while (walked < 1900ms) { + auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poketch approach: No video frame.", stream); + const auto result = match_poketch_landmarks(*frame.frame); + VideoOverlaySet overlays(stream.overlay()); + for (const auto& landmark : result.landmarks) { + overlays.add(COLOR_CYAN, landmark.search); + if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); + stream.log("[Poketch approach] " + landmark.name + " score=" + std::to_string(landmark.score) + + " margin=" + std::to_string(landmark.margin)); + } + if (result.located) { + stream.log("[Poketch approach] Landmarks acquired after " + std::to_string(walked.count()) + "ms; ending downward approach."); + return; + } + const Milliseconds step = std::min(200ms, 1900ms - walked); + pbf_move_left_joystick(context, {0, -1}, step, 0ms); + walked += step; + pbf_wait(context, 700ms); + context.wait_for_all_requests(); + } + stream.log("[Poketch approach] Reached 1900ms downward limit; handing off to locator."); + } + + static void align_and_talk_to_poketch_man(VideoStream& stream, ProControllerContext& context) { + context.wait_for_all_requests(); + pbf_wait(context, 700ms); + context.wait_for_all_requests(); + size_t confirmed = 0, coarse_confirmed = 0; + double previous_dx = 0, previous_dy = 0; + size_t acquisition_misses = 0, vertical_probe = 0; + // Two small excursions around the approach position. This is + // visibility recovery, not permission to interact without a match. + const int vertical_probes[] = {-80, 160, -240, 320, -160, -120, 240, -360, 480, -240}; + for (size_t attempt = 0; attempt < 80; attempt++) { + auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poketch interaction: No video frame.", stream); + const OverworldMatchResult result = match_poketch_landmarks(*frame.frame); + stream.log(result.coarse ? "[Poketch landmarks] Coarse acquisition." : "[Poketch landmarks] Fine alignment."); + VideoOverlaySet overlays(stream.overlay()); + for (const auto& landmark : result.landmarks) { + overlays.add(COLOR_CYAN, landmark.search); + if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); + stream.log("[Poketch landmarks] " + landmark.name + " score=" + std::to_string(landmark.score) + + " margin=" + std::to_string(landmark.margin) + " accepted=" + std::to_string(landmark.accepted) + + " screen-relative offset=(" + std::to_string(landmark.dx) + "," + std::to_string(landmark.dy) + ")"); + } + if (result.located) acquisition_misses = 0; + if (result.coarse && result.located) { + if (coarse_confirmed && std::abs(result.dx - previous_dx) <= 0.00625 && std::abs(result.dy - previous_dy) <= 1.0/90) + coarse_confirmed++; + else coarse_confirmed = 1; + previous_dx = result.dx; previous_dy = result.dy; + } else coarse_confirmed = 0; + if (!result.located) { + confirmed = 0; + if (++acquisition_misses >= 4 && vertical_probe < sizeof(vertical_probes) / sizeof(vertical_probes[0])) { + int probe = vertical_probes[vertical_probe++]; + acquisition_misses = 0; + stream.log("[Poketch landmarks] Bounded vertical visibility probe " + std::to_string(vertical_probe) + + "/10: " + (probe < 0 ? "down " : "up ") + std::to_string(std::abs(probe)) + "ms."); + pbf_move_left_joystick(context, {0, probe < 0 ? -1.0 : 1.0}, Milliseconds(std::abs(probe)), 0ms); + pbf_wait(context, 700ms); + } else { + stream.log("[Poketch landmarks] No trustworthy acquisition; waiting for another frame."); + pbf_wait(context, 250ms); + } + } else if (result.coarse && coarse_confirmed < 3) { + confirmed = 0; + stream.log("[Poketch landmarks] Waiting for consistent coarse offset before movement."); + pbf_wait(context, 250ms); + } else if (result.at_target) { + if (++confirmed >= 3) { + stream.log("[Poketch landmarks] Above-NPC reference confirmed; turning down."); + pbf_press_dpad(context, DPAD_DOWN, 40ms, 400ms); + context.wait_for_all_requests(); + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + context.wait_for_all_requests(); + wait_for_dialogue(stream, context, "Phase 27 (poketech challenge completed dialog)", 10s); + return; + } + pbf_wait(context, 300ms); + } else { + confirmed = 0; + const double dx = result.dx, dy = result.dy; + stream.log("[Poketch landmarks] Reference screen-relative offset=(" + std::to_string(result.dx) + "," + std::to_string(result.dy) + ")."); + const bool horizontal = std::abs(dx) > 7.0/960 && + (std::abs(dy) <= 7.0/540 || std::abs(dx) >= std::abs(dy)*9.0/16); + double error = horizontal ? dx : dy*9.0/16; + auto duration = std::abs(error) > 0.03125 ? 80ms : std::abs(error) > 0.015625 ? 40ms : 16ms; + if (std::abs(dx) <= 7.0/960 && std::abs(dy) <= 7.0/540) { + stream.log("[Poketch landmarks] Coarse position near target; waiting for fine confirmation."); + pbf_wait(context, 300ms); + context.wait_for_all_requests(); + continue; + } + stream.log("[Poketch landmarks] Correcting " + std::string(horizontal ? "horizontal" : "vertical") + + " axis for " + std::to_string(duration.count()) + "ms."); + pbf_move_left_joystick(context, + {horizontal ? (dx > 0 ? 1.0 : -1.0) : 0.0, + horizontal ? 0.0 : (dy < 0 ? 1.0 : -1.0)}, duration, 0ms); + pbf_wait(context, 700ms); + } + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poketch interaction: Building landmarks did not confirm the reference position; A not pressed.", stream); + } + + void align_and_enter_jubilife_center(VideoStream& stream, ProControllerContext& context) { + context.wait_for_all_requests(); + pbf_wait(context, 700ms); + context.wait_for_all_requests(); + size_t confirmed = 0; + for (size_t attempt = 0; attempt < 60; attempt++) { + const auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife Center alignment: No video frame.", stream); + const auto result = match_jubilife_center_entrance(*frame.frame); + VideoOverlaySet overlays(stream.overlay()); + for (const auto& landmark : result.landmarks) { + overlays.add(COLOR_CYAN, landmark.search); + if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); + stream.log("[Jubilife Center alignment] score=" + std::to_string(landmark.score) + + " margin=" + std::to_string(landmark.margin) + " screen-relative offset=(" + + std::to_string(landmark.dx) + "," + std::to_string(landmark.dy) + ")"); + } + if (!result.located) { + confirmed = 0; + pbf_wait(context, 250ms); + } else if (result.at_target) { + if (++confirmed >= 3) { + stream.log("[Jubilife Center alignment] Door position confirmed; entering."); + recorded_move_until_transition(stream, context, 0, 1, 2000ms, "Jubilife Pokemon Center entry"); + pbf_wait(context, 700ms); + context.wait_for_all_requests(); + return; + } + pbf_wait(context, 300ms); + } else { + confirmed = 0; + bool horizontal = std::abs(result.dx) >= std::abs(result.dy)*9.0/16; + double error = horizontal ? result.dx : result.dy*9.0/16; + const auto duration = std::abs(error) > 1.0/24 ? 80ms : std::abs(error) > 0.015625 ? 40ms : 16ms; + pbf_move_left_joystick(context, + {horizontal ? (result.dx > 0 ? 1.0 : -1.0) : 0.0, + horizontal ? 0.0 : (result.dy < 0 ? 1.0 : -1.0)}, duration, 0ms); + pbf_wait(context, 700ms); + } + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife Center alignment: Door reference not confirmed; entry not attempted.", stream); + } + + static void tutorial_part_4_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { + // What 00.05 does, stepped through on the console before writing it. + // Every walk is planned on Jubilife's tile grid; every talk stands + // beside the person, faces them and presses A once. + static const char* const STEPS[] = { + "Trainers' School: talk to Barry, who gives the Town Map", + "Talk to the Poketch Company president on the main road", + "Get the three coupons from the clowns by the Poke Mart, the TV station and the Poketch Company", + "Show the coupons to the president for the Poketch", + "Walk into the Pokemon Center and stand at the PC", + }; + constexpr size_t STEP_COUNT = sizeof(STEPS) / sizeof(STEPS[0]); + { + std::string text = "[00.05] In Jubilife, the steps are:"; + for (size_t i = 0; i < STEP_COUNT; i++) { + text += "\n " + std::to_string(i + 1) + ". " + STEPS[i]; + } + stream.log(text, COLOR_BLUE); + } + auto step = [&](size_t number) { + std::string text = "[00.05 step " + std::to_string(number) + "/" + std::to_string(STEP_COUNT) + "] " + STEPS[number - 1]; + text += number < STEP_COUNT ? std::string(" -- next: ") + STEPS[number] : std::string(" -- last step"); + stream.log(text, COLOR_BLUE); + add_wrapped_log(stream.overlay(), "00.05 step " + std::to_string(number) + "/" + std::to_string(STEP_COUNT) + ": " + STEPS[number - 1], COLOR_WHITE); + if (number < STEP_COUNT) { + add_wrapped_log(stream.overlay(), "Next: " + std::string(STEPS[number]), COLOR_GRAY); + } + }; + + const MapSet& maps = jubilife_maps(); + auto world = [&](const TilePos& pos) { return maps.to_world(MAP_JUBILIFE_CITY, pos); }; + // Jubilife has no trainers; someone in the way only talks. + std::vector trainers; + auto in_the_way = [&](const std::string&) { finish_dialogue(stream, context, BUTTON_B, "Someone in the way"); }; + // From the save below the steps, against the wall east of them: + // a tile west (the first press only turns), then up the steps. + route_turn(stream, context, Pokemon::Direction::WEST); + walk_for(stream, context, -1, 0, 200ms, "To the steps"); + walk_for(stream, context, 0, 1, 480ms, "Up the steps"); + std::vector at; + for (const TilePos& pos : JubilifeCity::up_the_steps()) { + at.push_back(world(pos)); + } + + step(1); + const TilePos school = world(JubilifeCity::TRAINERS_SCHOOL_DOOR.next(Pokemon::Direction::SOUTH)); + auto by_door = [](const TilePos& front) { + return [front](const TilePos& pos) { return pos.y == front.y && pos.x >= front.x - 1 && pos.x <= front.x + 1; }; + }; + // To the Pokemon Center first, and heal: going in proves where we + // are, and coming out leaves us on its doorstep, the door row's + // front, which no wall pins. Measured on the console (2026-10-30): + // from the top of the steps, east stops at the house east of the + // road, and north from there stops in front of the Center. + walk_until(stream, context, 1, 0, 4000ms, "East to the house"); + walk_until(stream, context, 0, 1, 4000ms, "North to the Pokemon Center"); + heal_at_pokemon_center(stream, context, "Jubilife Pokemon Center"); + // From the doorstep the School's door is ten one-tile steps west + // (measured). Nothing on that row stops a walk, so count the + // steps, and from the eighth on try north: a wall there is the + // School's front, the door takes us in. + route_turn(stream, context, Pokemon::Direction::WEST); + bool in_school = false; + for (size_t tiles = 1; tiles <= 12 && !in_school; tiles++) { + walk_for(stream, context, -1, 0, 160ms, "West to the School's door"); + if (tiles < 8) { + continue; + } + const WalkResult up = walk_until(stream, context, 0, 1, 1200ms, "Trainers' School door"); + in_school = up.stop == WalkStop::TRANSITION; + if (!in_school) { + walk_for(stream, context, 0, -1, 200ms, "Back to the doorsteps' row"); + route_turn(stream, context, Pokemon::Direction::WEST); + } + } + if (!in_school) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Trainers' School door: not found 8 to 12 tiles west of the Center's doorstep.", stream); + } + context.wait_for(1500ms); + // Barry stands at the blackboard, a tile west of the door's column: + // step west, walk north until he stops us, talk. + route_turn(stream, context, Pokemon::Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); + walk_until(stream, context, 0, 1, 3000ms, "to Barry"); + 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) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Trainers' School: Barry did not answer.", stream); + } + } + // His scene ends with "You obtained a Town Map!", which A closes. + finish_dialogue(stream, context, BUTTON_A, "Barry"); + // Out: south to the front wall, a tile east (the door's column), out. + walk_until(stream, context, 0, -1, 4000ms, "to the school's front wall"); + route_turn(stream, context, Pokemon::Direction::EAST); + pbf_move_left_joystick(context, {1, 0}, route_open_hold(Pokemon::Direction::EAST, 1), 200ms); + if (walk_until(stream, context, 0, -1, 2500ms, "out of the Trainers' School").stop != WalkStop::TRANSITION) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Trainers' School: did not leave by the door.", stream); + } + context.wait_for(1500ms); + at = {school.next(Pokemon::Direction::WEST), school, school.next(Pokemon::Direction::EAST)}; + + step(2); + // The president talks on with A after each last line; B moves his + // lines on and never starts a new talk. + const TilePos president = world(JubilifeCity::POKETCH_PRESIDENT); + talk_to_npc(stream, context, maps, at, president, + {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH}, trainers, in_the_way, "Poketch president"); + finish_dialogue(stream, context, BUTTON_B, "Poketch president"); + + step(3); + // Each clown asks a question whose default answer, Yes, is right, + // and hands over a coupon with an "obtained" message A closes. + talk_to_npc(stream, context, maps, at, world(JubilifeCity::CLOWN_MART), + {Pokemon::Direction::SOUTH, Pokemon::Direction::WEST, Pokemon::Direction::EAST}, + trainers, in_the_way, "Clown by the Poke Mart"); + finish_dialogue(stream, context, BUTTON_A, "Clown by the Poke Mart"); + talk_to_npc(stream, context, maps, at, world(JubilifeCity::CLOWN_TV_STATION), + {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH, Pokemon::Direction::WEST}, + trainers, in_the_way, "Clown by the TV station"); + finish_dialogue(stream, context, BUTTON_A, "Clown by the TV station"); + talk_to_npc(stream, context, maps, at, world(JubilifeCity::CLOWN_POKETCH_COMPANY), + {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH, Pokemon::Direction::WEST}, + trainers, in_the_way, "Clown by the Poketch Company"); + finish_dialogue(stream, context, BUTTON_A, "Clown by the Poketch Company"); + + step(4); + talk_to_npc(stream, context, maps, at, president, + {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH}, trainers, in_the_way, "Poketch president"); + finish_dialogue(stream, context, BUTTON_B, "Poketch president"); + + step(5); + const TilePos center = world(JubilifeCity::POKEMON_CENTER_DOOR.next(Pokemon::Direction::SOUTH)); + at = walk_route(stream, context, maps, at, by_door(center), + trainers, in_the_way, "To the Pokemon Center"); + enter_door_north(stream, context, "Pokemon Center door"); + // Inside: north to the counter, three tiles east, north to the PC + // at the back wall right of the counter. + walk_until(stream, context, 0, 1, 2500ms, "to the counter"); + route_turn(stream, context, Pokemon::Direction::EAST); + pbf_move_left_joystick(context, {1, 0}, route_open_hold(Pokemon::Direction::EAST, 3), 200ms); + walk_until(stream, context, 0, 1, 1500ms, "to the PC"); + context.wait_for_all_requests(); + } + // --------------------------------------------------------------------------- + // Segment 00-05 ends + // Standing in front of the Jubilife City Pokemon Center PC. + // Player will need to deposit pokemon in the pc and transfer to Pokemon Home + // and level to 100 before starting the next segment. + // --------------------------------------------------------------------------- + + // --------------------------------------------------------------------------- + // Segment / Checkpoint classes + // --------------------------------------------------------------------------- + + std::string AutoStory_Segment_00::name() const { return "00: Intro to Poketech"; } + std::string AutoStory_Segment_00::start_text() const { return "Start: Game launched, Standing in the player's room after tv stops playing"; } + std::string AutoStory_Segment_00::end_text() const { return "End: Starter Pokemon chosen, received, and Pokedex acquired, standing in front of the PC."; } + + void AutoStory_Segment_00::run_segment( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const { + stats.m_segment++; + env.update_stats(); + context.wait_for_all_requests(); + env.console.log("Start Segment " + name(), COLOR_ORANGE); + + run_story_table(env, context, options, stats, make_segment_00, {}); + + context.wait_for_all_requests(); + env.console.log("End Segment " + name(), COLOR_GREEN); + } + + std::string AutoStory_Checkpoint_0000::name() const { return "00.00 - Verify Settings"; } + std::string AutoStory_Checkpoint_0000::start_text() const { return "Standing in the initial bedroom before beginning the tutorial."; } + std::string AutoStory_Checkpoint_0000::end_text() const { return "Required game settings verified and saved; ready for 00.01."; } + void AutoStory_Checkpoint_0000::run_checkpoint( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + AutoStoryOptions options, AutoStoryStats& stats + ) const { + run_story_table(env, context, options, stats, make_segment_00, {"00.00", "00.00"}); + } + + static void verify_settings_step(StoryConsole& c) { + verify_tutorial_settings(c.env.console, c.context); + c.context.wait_for_all_requests(); + } + static void select_starter_step(StoryConsole& c) { + switch (c.options.starter_choice) { + case StarterChoice::TURTWIG: select_turtwig(c.env.console, c.context); break; + case StarterChoice::CHIMCHAR: select_chimchar(c.env.console, c.context); break; + case StarterChoice::PIPLUP: select_piplup(c.env.console, c.context); break; + } + } + + std::map segment_00_variants() { + return { + {"verify_settings", story_call(verify_settings_step)}, + {"tutorial_part_1_navigation", story_call([](StoryConsole& c) { tutorial_part_1_navigation(c.env, c.env.console, c.context); })}, + {"starter_nav", story_call([](StoryConsole& c) { starter_nav(c.env.console, c.context); })}, + {"select_starter", story_call(select_starter_step)}, + {"fight_starly", story_call([](StoryConsole& c) { fight_starly(c.env.console, c.context); })}, + {"post_starly_cleanup", story_call([](StoryConsole& c) { post_starly_clenaup(c.env.console, c.context); })}, + {"tutorial_part_2_navigation", story_call([](StoryConsole& c) { tutorial_part_2_navigation(c.env, c.env.console, c.context); })}, + {"tutorial_part_3_navigation", story_call([](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); })}, + {"sandgem_potions", sandgem_potions}, + {"jubilife_stairs", story_call([](StoryConsole& c) { up_jubilife_stairs(c.env.console, c.context); })}, + {"tutorial_part_4_navigation", story_call([](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); })}, + }; + } + + // The table is segment_00_table() (PokemonBDSP_AutoStory_StoryTable.h). + Pokemon::Segment make_segment_00(StoryStepRunner& runner) { + const std::vector& table = segment_00_table(); + return story_table_segment(runner, "00", table, std::make_shared( + runner, rows_that_stop(table), story_maps(), story_trainer_sights(), + [](StoryConsole& c, const std::string& name) { fight_route_trainer(c.env.console, c.context, name); }, + segment_00_variants() + )); + } + + // DIAGNOSTIC ONLY: Remove before pushing to production. + void diagnostic_checkpoint_0004_after_catch( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + AutoStoryOptions options, AutoStoryStats& stats + ) { + run_story_table(env, context, options, stats, make_segment_00, {"00.04b", "00.04d"}); + } + + // ======================== + // Checkpoint 0001 Wrapper + // ======================== + std::string AutoStory_Checkpoint_0001::name() const { return "00.01 - After TV"; } + std::string AutoStory_Checkpoint_0001::start_text() const { return "Game launched, Standing in the player's room after tv stops playing"; } + std::string AutoStory_Checkpoint_0001::end_text() const { return "Standing outside Lake Verity before picking starter."; } + + void AutoStory_Checkpoint_0001::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const { + run_story_table(env, context, options, stats, make_segment_00, {"00.01", "00.01"}); + } + + // ======================== + // Checkpoint 0002 Wrapper + // ======================== + + std::string AutoStory_Checkpoint_0002::name() const { return "00.02 - Outside Verity before starter"; } + std::string AutoStory_Checkpoint_0002::start_text() const { return AutoStory_Checkpoint_0001().end_text(); } + std::string AutoStory_Checkpoint_0002::end_text() const { return "Standing outside Lake Verity after picking starter."; } + + void AutoStory_Checkpoint_0002::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const { + run_story_table(env, context, options, stats, make_segment_00, {"00.02", "00.02"}); + } + + + // ======================== + // Checkpoint 0003 Wrapper + // ======================== + + std::string AutoStory_Checkpoint_0003::name() const { return "00.03 - Outside Verity after starter"; } + std::string AutoStory_Checkpoint_0003::start_text() const { return AutoStory_Checkpoint_0002().end_text(); } + std::string AutoStory_Checkpoint_0003::end_text() const { return "Standing in front of The professor after recieving the Pokedex"; } + + void AutoStory_Checkpoint_0003::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const { + run_story_table(env, context, options, stats, make_segment_00, {"00.03", "00.03"}); + } + // ======================== + // Checkpoint 0004 Wrapper + // ======================== + + std::string AutoStory_Checkpoint_0004::name() const { return "00.04 - After Pokedex"; } + std::string AutoStory_Checkpoint_0004::start_text() const { return AutoStory_Checkpoint_0003().end_text(); } + std::string AutoStory_Checkpoint_0004::end_text() const { return "Standing in in jubilife after speaking to dawn. Next: give town map to barry"; } + + void AutoStory_Checkpoint_0004::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const { + run_story_table(env, context, options, stats, make_segment_00, {"00.04", "00.04d"}); + } + // ======================== + // Checkpoint 0005 Wrapper + // ======================== + + std::string AutoStory_Checkpoint_0005::name() const { return "00.05 - Jubilife After Dawn"; } + std::string AutoStory_Checkpoint_0005::start_text() const { return AutoStory_Checkpoint_0004().end_text(); } + std::string AutoStory_Checkpoint_0005::end_text() const { return "Standing in front of PC. Recieved the Poketech. Ready to deposit starter"; } + + void AutoStory_Checkpoint_0005::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const { + run_story_table(env, context, options, stats, make_segment_00, {"00.05", "00.05"}); + } + + } + } +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h new file mode 100644 index 0000000000..a789a26969 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h @@ -0,0 +1,170 @@ +/* BDSP AutoStory - Segment 00 + * From beginning cinematic until after choosing the starter Pokemon. + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment00_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Segment00_H + +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// Segment 00 as a table of checkpoints, segment_00_table(): 00.00 to 00.05, +// with 00.04 in four rows (to the catch, then 00.04b to 00.04d as story +// steps), each saving at its start. +Pokemon::Segment make_segment_00(StoryStepRunner& runner); + +// How segment_00_table()'s steps that call an old function run on the +// console, by their variants. +std::map segment_00_variants(); + +// DIAGNOSTIC ONLY: Remove before pushing to production. +// Starts at the post-catch save and runs only the rest of checkpoint 00.04. +void diagnostic_checkpoint_0004_after_catch(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats); + +// DIAGNOSTIC ONLY: Remove before pushing to production. +// 00.04's walk after the Potions, for the event-model diagnostic +// (PokemonBDSP_AutoStory_SandgemCenterEvents.h): from below Sandgem's +// Pokemon Center to Route 202's save spot, without saving. Fails with +// a screenshot as 00.04 does. `at`: the tiles in Sandgem we may stand on. +void walk_from_sandgem_center_to_route_202_save_spot( + VideoStream& stream, ProControllerContext& context, + const std::vector& at +); + +class AutoStory_Segment_00 : public AutoStory_Segment { +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_segment(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const override; +}; + +class AutoStory_Checkpoint_0000 : public AutoStory_Checkpoint { +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const override; +}; + +class AutoStory_Checkpoint_0001 : public AutoStory_Checkpoint { +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const override; +}; + +class AutoStory_Checkpoint_0002 : public AutoStory_Checkpoint { +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const override; +}; + +class AutoStory_Checkpoint_0003 : public AutoStory_Checkpoint { +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const override; +}; + +class AutoStory_Checkpoint_0004 : public AutoStory_Checkpoint { +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const override; +}; + +class AutoStory_Checkpoint_0005 : public AutoStory_Checkpoint { +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + + virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats) const override; +}; + +// start: game launched, in front of the TV. +// end: Standing outside lake varety before starter +//Segment 00-00 begins +void checkpoint_000(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats); + +// start: Standing outside lake varety before starter +// end: Standing outside lake varety after starter battle +//Segment 00-001 begins +void checkpoint_001(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats); + +// start: Standing outside lake varety after starter battle +// end: recieved Pokedex, standing in front of the professor +//Segment 00-002 begins +void checkpoint_002(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats); + +// start: recieved Pokedex, standing in front of the professor +// end: Standing in the entrance to Jubilife City after speaking to dawn +//Segment 00-003 begins +void checkpoint_003(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats); + +// start: Standing in the entrance to Jubilife City after speaking to dawn +// end: Standing in the Pokemon Center ready to deposit pokemon after recieving poketech. +//Segment 00-004 begins +void checkpoint_004(SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + EventNotificationOption& notif_status_update, + AutoStoryStats& stats); +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp new file mode 100644 index 0000000000..00cc3cf623 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp @@ -0,0 +1,1655 @@ +/* BDSP AutoStory - Segment 01 + * From the Jubilife Pokemon Center to Oreburgh City. + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "PokemonBDSP_AutoStory_Segment_01.h" +#include "../Data/PokemonBDSP_AutoStory_OreburghMine.h" +#include "../Data/PokemonBDSP_AutoStory_RavagedPath.h" +#include "../Data/PokemonBDSP_AutoStory_Route203.h" +#include "../Data/PokemonBDSP_AutoStory_Route204.h" +#include "../Data/PokemonBDSP_AutoStory_Route204South.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" +#include "../Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "../Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "../Utils/PokemonBDSP_AutoStory_PokemonCenter.h" +#include "../Utils/PokemonBDSP_AutoStory_Location.h" +#include "../Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "../Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "../Utils/PokemonBDSP_AutoStory_Walk.h" +#include "../PokemonBDSP_AutoStory_StoryTable.h" +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" + +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" + +#include +#include + +using namespace std::chrono_literals; + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; + + +namespace{ + +[[noreturn]] void fail(VideoStream& stream, const std::string& message){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, message, stream); +} + +// One tile south, for a player facing another way. +void step_south(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 1), 200ms); + context.wait_for_all_requests(); +} + +// True if a dialogue opens within `timeout`: someone who stopped us as the +// walk ended reads as BLOCKED, and their box comes up a moment later. +bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Milliseconds timeout){ + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + return wait_until(stream, context, timeout, {{dialog}}) == 0; +} + +// For a dialogue that has opened: B through it. A trainer's ends in a +// battle, which is fought with the best move (Potions when HP runs low), +// followed by a Potion if HP is under half. Anyone else's ends back in the +// overworld. Returns whether there was a battle. Between a trainer's last +// line and the battle the overworld shows with no dialogue for a moment, +// so "only talked" needs 4 s of it, and never comes when `battle_expected`. +// `doubles` for tag and twin battles. A lost battle ends in the last +// Pokemon Center; see in_pokemon_center(). +bool in_pokemon_center(VideoStream& stream); +uint64_t mean_brightness(const ImageViewRGB32& screen); +bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent = nullptr); +bool in_dim_cave(VideoStream& stream, uint64_t below = 85); +bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std::string& label, bool battle_expected = false, bool doubles = false){ + ShortDialogDetector dialog; + OverworldDetector overworld; + BattleMenuDetector battle_menu(BattleType::TRAINER); + // Roark's battle menu read as STANDARD, not TRAINER, and nothing fought + // it (r210d): either counts here, as only a trainer's battle can start. + BattleMenuDetector battle_menu_standard(BattleType::STANDARD); + BlackScreenDetector black; + auto quiet_since = std::chrono::steady_clock::now(); + for (size_t check = 0; check < 120; check++){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + if (!frame){ + fail(stream, label + ": No video frame."); + } + if (battle_menu.detect(*frame.frame) || battle_menu_standard.detect(*frame.frame) || black.detect(*frame.frame)){ + route_status(stream, "Fighting " + label + "."); + fight_story_battle(stream, context, label, doubles); + // Lines after the battle (Roark's badge, r211d) keep the menu + // from opening: finish them before the Potion. + if (dialogue_opens(stream, context, 3000ms)){ + finish_dialogue(stream, context, BUTTON_B, label + " after the battle"); + } + if (!in_pokemon_center(stream)){ + use_potion_first_pokemon(stream, context, 0.5); + } + return true; + } + const bool talking = dialog.detect(*frame.frame); + if (!talking && overworld.detect(*frame.frame)){ + if (!battle_expected && std::chrono::steady_clock::now() - quiet_since >= 4s){ + route_status(stream, label + " only talked."); + return false; + } + pbf_wait(context, 300ms); + continue; + } + quiet_since = std::chrono::steady_clock::now(); + if (talking){ + pbf_press_button(context, BUTTON_B, 80ms, 1100ms); + }else{ + pbf_wait(context, 300ms); + } + } + fail(stream, label + ": neither a battle nor the overworld after the dialogue."); +} + +// Logs a checkpoint's steps up front, then each step as it starts, with the +// one after it, in the log and the overlay. +class StepLog{ +public: + StepLog(VideoStream& stream, std::string checkpoint, std::string intro, std::vector steps) + : m_stream(stream), m_checkpoint(std::move(checkpoint)), m_steps(std::move(steps)) + { + std::string text = "[" + m_checkpoint + "] " + intro + ", the steps are:"; + for (size_t i = 0; i < m_steps.size(); i++){ + text += "\n " + std::to_string(i + 1) + ". " + m_steps[i]; + } + m_stream.log(text, COLOR_BLUE); + } + void operator()(size_t number){ + const std::string count = std::to_string(number) + "/" + std::to_string(m_steps.size()); + const bool last = number >= m_steps.size(); + m_stream.log("[" + m_checkpoint + " step " + count + "] " + m_steps[number - 1] + + (last ? std::string(" -- last step") : " -- next: " + m_steps[number]), COLOR_BLUE); + add_wrapped_log(m_stream.overlay(), m_checkpoint + " step " + count + ": " + m_steps[number - 1], COLOR_WHITE); + if (!last){ + add_wrapped_log(m_stream.overlay(), "Next: " + m_steps[number], COLOR_GRAY); + } + } + +private: + VideoStream& m_stream; + std::string m_checkpoint; + std::vector m_steps; +}; + +void jubilife_to_barry(VideoStream& stream, ProControllerContext& context){ + // What 01.01 does, stepped through on the console before writing it. + StepLog step(stream, "01.01", "From the Jubilife Pokemon Center to Barry", { + "Heal at the nurse, then walk out of the Pokemon Center", + "Walk to Jubilife's east exit and battle Barry at the Route 203 entrance", + }); + + // From 00.05g we are already outdoors on the street east to Route 203, + // healed on arriving in Jubilife: straight to the walk east. + bool outdoors = false; + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + outdoors = !indoors(*screen.frame); + } + if (outdoors){ + stream.log("[Route] 01.01: outdoors; skipping the Pokemon Center."); + }else{ + step(1); + // The save stands us at the PC, right of the counter, facing it; west + // is the counter's end. Two tiles south clears it, then three west is + // the floor emblem's column, with the nurse north and the door south. + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 2), 200ms); + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 3), 200ms); + if (walk_to_talk(stream, context, 0, 1, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ + fail(stream, "Jubilife Pokemon Center: the nurse did not answer."); + } + // A throughout: Yes to "rest your Pokemon?" (B would answer No). + finish_dialogue(stream, context, BUTTON_A, "Nurse"); + // Healing shows no text box for a few seconds, which reads as the end; + // her "Thank you for waiting" lines follow it. + if (dialogue_opens(stream, context, 6000ms)){ + finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); + } + // South through the door until a girl standing below it stops us. The + // door's fade may or may not read as a transition. + WalkResult out{WalkStop::LIMIT, 0ms, 0}; + for (size_t walk = 0; walk < 3 && out.stop != WalkStop::BLOCKED; walk++){ + out = walk_until(stream, context, 0, -1, 8000ms, "out of the Pokemon Center"); + if (out.stop == WalkStop::DIALOG || out.stop == WalkStop::ARRIVED){ + break; + } + } + if (out.stop != WalkStop::BLOCKED){ + fail(stream, std::string("Jubilife Pokemon Center: walking out ended with ") + walk_stop_name(out.stop) + ", not at the girls below the door."); + } + + step(2); + // Jubilife's plaza has too few walls the grid gets right to plan on (a + // leg to a wall the map shows walked on 19 tiles). These are the holds + // that worked on the console: west onto the main road, north to the + // street east to Route 203, and back a couple of tiles. + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, 720ms, 300ms); + route_turn(stream, context, Direction::NORTH); + pbf_move_left_joystick(context, {0, 1}, 3960ms, 300ms); + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, 600ms, 300ms); + context.wait_for_all_requests(); + } + // East along the street. The opening to the route is a few rows high; + // Barry stops us a little way in. A wall means we are a row off it: try + // rows to the south, then to the north. + static const int ROW_CHANGES[] = {0, 1, 1, -3, -1}; + bool barry = false; + for (int change : ROW_CHANGES){ + if (barry){ + break; + } + for (int i = 0; i < std::abs(change); i++){ + const Direction dir = change > 0 ? Direction::SOUTH : Direction::NORTH; + route_turn(stream, context, dir); + pbf_move_left_joystick(context, {0, change > 0 ? -1.0 : 1.0}, route_open_hold(dir, 1), 200ms); + } + context.wait_for_all_requests(); + WalkResult walk{WalkStop::LIMIT, 0ms, 0}; + for (size_t stretch = 0; stretch < 3 && walk.stop == WalkStop::LIMIT; stretch++){ + walk = walk_until(stream, context, 1, 0, 9000ms, "east to Route 203"); + } + barry = walk.stop == WalkStop::DIALOG; + } + if (!barry){ + fail(stream, "Route 203: Barry did not stop us at the entrance."); + } + if (!talk_or_fight(stream, context, "Barry", true)){ + fail(stream, "Route 203: Barry talked but did not battle."); + } +} + +void route203_to_oreburgh(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.02", "From Barry on Route 203 to Oreburgh City", { + "Walk east along Route 203 into the Oreburgh Gate, fighting the trainers who see us", + "Listen to the Hiker, who gives HM Rock Smash", + "Walk east through the Gate past Curtis and Diana, out to Oreburgh City", + "Listen to the man who meets us at the city's entrance", + }); + + step(1); + // Route 203 only goes east, over three stretches joined by stairs, and + // the Gate's opening is in the cliff at its north-east end. Who stops us + // differs from run to run (a trainer's sight depends on our row), so + // there is no fixed list: walk east, talk or fight whoever stops us, and + // at a wall find a row from which east goes on. + size_t fights = 0; + bool inside = false; + // On Route 203 only trainers start a talk; one with no battle is the + // Hiker, who meets us inside the Gate (whose fade may not read as a + // transition). + bool met_hiker = false; + auto stopped_by = [&](){ + if (talk_or_fight(stream, context, "Route 203 trainer " + std::to_string(fights + 1))){ + fights++; + }else{ + route_status(stream, "No battle: that was the Hiker, inside the Oreburgh Gate."); + met_hiker = true; + inside = true; + } + }; + for (size_t leg = 0; leg < 40 && !inside; leg++){ + const WalkResult walk = walk_until(stream, context, 1, 0, 10000ms, "east along Route 203"); + if (walk.stop == WalkStop::TRANSITION){ + inside = true; + break; + } + if (walk.stop == WalkStop::LIMIT){ + continue; + } + if (walk.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 1500ms)){ + stopped_by(); + continue; + } + // A wall. Find the row from which east goes on: north to the top wall + // first (a step north there does not move us, so rows are counted + // from it), then a row at a time south. At the route's end this is the + // corner by the second sign, with the Gate's opening two rows down. + walk_until(stream, context, 0, 1, 6000ms, "north to the top wall"); + bool moved_on = false; + for (size_t row = 1; row <= 10 && !moved_on; row++){ + step_south(stream, context); + const WalkResult next = walk_until(stream, context, 1, 0, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); + if (next.stop == WalkStop::TRANSITION){ + inside = true; + } + if (next.stop == WalkStop::DIALOG || (next.stop == WalkStop::BLOCKED && dialogue_opens(stream, context, 1500ms))){ + stopped_by(); + } + // Two still chunks are the least a wall takes; more is progress. + moved_on = inside || next.stop != WalkStop::BLOCKED || next.walked > 1200ms; + } + if (!moved_on){ + fail(stream, "Route 203: walls east on every row down to ten below the top wall."); + } + } + if (!inside){ + fail(stream, "Route 203: did not reach the Oreburgh Gate."); + } + route_status(stream, "Route 203: " + std::to_string(fights) + " trainer battle(s) on the way."); + context.wait_for(1500ms); + + step(2); + // The Hiker steps up to us as we walk east. + if (!met_hiker){ + const WalkResult to_hiker = walk_until(stream, context, 1, 0, 4000ms, "to the Hiker"); + if (to_hiker.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ + fail(stream, "Oreburgh Gate: the Hiker did not stop us."); + } + finish_dialogue(stream, context, BUTTON_B, "Hiker"); + } + // Then a "Hidden Moves" tutorial card, closed with B. It is no dialogue + // box, and it can come up a moment after his last line. + pbf_wait(context, 1500ms); + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + + step(3); + // East: Camper Curtis and Picnicker Diana see us; past them the exit's + // white fade, and the man in Oreburgh stops us right outside. + fights = 0; + bool city = false; + size_t stuck = 0; + for (size_t leg = 0; leg < 12 && !city; leg++){ + const WalkResult walk = walk_until(stream, context, 1, 0, 10000ms, "east through the Oreburgh Gate"); + if (walk.stop == WalkStop::LIMIT || walk.stop == WalkStop::TRANSITION){ + continue; + } + if (walk.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ + // A card or menu left up holds the screen still: B once, then retry. + if (++stuck > 1){ + fail(stream, "Oreburgh Gate: a wall stopped us walking east."); + } + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + continue; + } + stuck = 0; + if (fights >= 2){ + city = true; + break; + } + fights += talk_or_fight(stream, context, "Oreburgh Gate trainer " + std::to_string(fights + 1)) ? 1 : 0; + } + if (!city){ + fail(stream, "Oreburgh Gate: did not reach Oreburgh City."); + } + + step(4); + // He walks us to the Pokemon Center between his lines, and Barry joins + // there: more lines after each walk. + finish_dialogue(stream, context, BUTTON_B, "Man at Oreburgh's entrance"); + for (size_t scene = 0; scene < 6 && dialogue_opens(stream, context, 6000ms); scene++){ + finish_dialogue(stream, context, BUTTON_B, "Oreburgh scene"); + } + context.wait_for_all_requests(); +} + +// Holds the stick toward `dir` for `tiles` tiles, turning first. +void hold_tiles(VideoStream& stream, ProControllerContext& context, Direction dir, size_t tiles){ + route_turn(stream, context, dir); + const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; + const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; + pbf_move_left_joystick(context, {x, y}, route_open_hold(dir, tiles), 200ms); + context.wait_for_all_requests(); +} + +WalkResult walk_dir( + VideoStream& stream, ProControllerContext& context, Direction dir, Milliseconds limit, const std::string& label, + const std::function& arrived = nullptr +){ + const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; + const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; + return walk_until(stream, context, x, y, limit, label, arrived); +} + +// Out of a building whose door is straight south of us, a D-pad tile at a +// time, until the location banner shows: it names the map we step into +// (e.g. "Oreburgh City") for a few seconds, and never shows inside. Ends in +// the doorway, facing south. Out of the Oreburgh Gym from in front of Roark +// (by hand, 2026-10-31): 18 taps inside, 2 taps of black fade, the banner on +// the 21st. The door's fade alone was missed between steps (r219g walked on +// south to the rocks below the gym). +bool dpad_out_of_building(VideoStream& stream, ProControllerContext& context, size_t max_taps, const std::string& label){ + pbf_press_dpad(context, DPAD_DOWN, 160ms, 700ms); // turn + for (size_t tap = 0; tap < max_taps; tap++){ + pbf_press_dpad(context, DPAD_DOWN, 160ms, 900ms); + context.wait_for_all_requests(); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && location_banner_showing(*frame.frame)){ + stream.log("[Walk] " + label + ": the location banner shows after " + std::to_string(tap + 1) + " taps; outside."); + context.wait_for(1000ms); + return true; + } + if (dialogue_opens(stream, context, 100ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": someone in the way"); + } + } + return false; +} + +// For a player inside a Pokemon Center after a whiteout, at the counter: +// out through the door. The door is straight south of the nurse in most +// Centers; in Oreburgh's it was two tiles east of where we stood (by hand). +void walk_out_of_center(VideoStream& stream, ProControllerContext& context, const std::string& label){ + finish_dialogue(stream, context, BUTTON_B, label + ": the nurse"); + const int shifts[] = {0, 2, -4}; + for (int shift : shifts){ + if (shift != 0){ + hold_tiles(stream, context, shift > 0 ? Direction::EAST : Direction::WEST, (size_t)std::abs(shift)); + } + // A tile at a time, looking after each, so we stop on the doorstep: + // walk_until() looks only between 400 ms holds, and r205d held on + // through the fade and stepped a tile further south. + bool out = false; + for (size_t tile = 0; tile < 10 && !out; tile++){ + // D-pad taps: whole tiles (the first only turns us from the nurse). + pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); + context.wait_for(1500ms); + if (dialogue_opens(stream, context, 100ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": someone in the Center"); + } + const VideoSnapshot frame = stream.video().snapshot(); + size_t percent = 100; + if (frame){ + center_floor_on_screen(*frame.frame, &percent); + } + out = percent < 30; + } + if (out){ + // "No floor" also reads true on the door's black fade, and the game + // takes no input until it ends: the first D-pad taps after it were + // lost and the gym's door ended a tile away (r215d, r216d; a shot + // per press by hand showed the black frame). Wait for daylight. + const auto start = current_time(); + while (current_time() - start < 6000ms){ + context.wait_for(100ms); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && mean_brightness(*frame.frame) > 40){ + break; + } + } + context.wait_for(1000ms); + return; + } + } + fail(stream, label + ": could not find the Pokemon Center's door."); +} + +// Oreburgh: from a row or two below the Pokemon Center's front, west of +// the fence below its east corner, into the Center by its door, stopping +// just inside. Talks to Barry if he stands in the door (01.03, the first +// time). +void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ + // The save stands us a row or two below the Center's front, a little + // west of its door. The posts and the road beside the Center make north + // from there unreliable (r191a, r192a walked up the road to the city's + // north exit), and counted taps east drift. The fence running south from + // the Center's east corner stops a walk east on these rows a tile east of + // the door (by hand): walk to it, north to the Center's front, tap back + // one, and shift one tap either side. + walk_to_wall(stream, context, 1, 0, 5000ms, "east to the fence below the Center"); + walk_to_wall(stream, context, 0, 1, 3000ms, "north to the Center's front"); + // The door wants us near its middle: a turn and a tap west (half a tile + // too far, by hand) or a turn alone leave north blocked by the facade + // beside it. The facade blocks north for two tiles either side of the + // door, so look for it in nudges of about a third of a tile. + route_turn(stream, context, Direction::WEST); + context.wait_for_all_requests(); + auto inside_now = [&](){ + const VideoSnapshot frame = stream.video().snapshot(); + size_t percent = 0; + return frame && (center_floor_on_screen(*frame.frame, &percent), percent >= 45); + }; + bool inside = false; + const int offsets[] = {0, -1, -2, 1, -3, 2, -4, 3}; // nudges from here + int nudged = 0; + for (size_t i = 0; i < 8 && !inside; i++){ + const int offset = offsets[i]; + if (offset != nudged){ + // No turn first: a turn is a tap too and moves us (see + // talk_with_nudges()); a nudge both turns and moves a little. + for (; nudged != offset; nudged += offset > nudged ? 1 : -1){ + pbf_move_left_joystick(context, {offset > nudged ? 1.0 : -1.0, 0}, ROUTE_NUDGE_TAP, 300ms); + } + context.wait_for_all_requests(); + } + // The door's fade often reads as the time limit, not a black screen + // (r183a went in and on to the counter): look at the floor. + // Stop as soon as the floor shows: walking on to the counter left the + // door's column, and the walk back out drifted into the yard (r188b). + // Oreburgh's sand reads up to 34 % warm outside (r189a), the floor + // 56 to 64 % inside. + const WalkResult in = walk_until(stream, context, 0, 1, 2500ms, "north into the Center's door", + [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); + if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && inside_now())){ + inside = true; + break; + } + if (in.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, "Someone at the Center's door"); + }else{ + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + if (!dialogue_opens(stream, context, 2000ms)){ + continue; + } + finish_dialogue(stream, context, BUTTON_B, "Barry at the Center's door"); + context.wait_for(2000ms); // he runs off + } + // Talked in the door's column: the door is free now. + walk_until(stream, context, 0, 1, 2500ms, "north into the Center's door", + [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); + inside = inside_now(); + } + if (!inside || !inside_now()){ + fail(stream, "Oreburgh: no column east of the save went into the Pokemon Center."); + } +} + +// Oreburgh: from just inside the Pokemon Center's door, up to the nurse +// and heal; ends at the counter facing her. The door is straight south of +// her (by hand, 2026-10-31: north from the door stopped at the counter). +void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context){ + if (walk_to_talk(stream, context, 0, 1, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ + fail(stream, "Oreburgh Pokemon Center: the nurse did not answer."); + } + // A throughout: Yes to "rest your Pokemon?" (B would answer No). + finish_dialogue(stream, context, BUTTON_A, "Nurse"); + // Healing shows no text box for a few seconds, which reads as the end; + // her "Thank you for waiting" lines follow it. + if (dialogue_opens(stream, context, 6000ms)){ + finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); + } +} + +// Oreburgh: from just inside the Pokemon Center's door, out onto the tile +// below it, facing south, on the door's column. Stops as soon as the floor +// is gone: walking on runs down the column to the mine. +void leave_center_onto_door_row(VideoStream& stream, ProControllerContext& context){ + walk_until(stream, context, 0, -1, 4000ms, "out of the Center's door", + [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent < 30; }); + context.wait_for(1500ms); +} + +void oreburgh_to_roark(VideoStream& stream, ProControllerContext& context){ + // What 01.03 does, stepped through on the console before writing it. + StepLog step(stream, "01.03", "From the Oreburgh Pokemon Center to Roark in the mine", { + "Into the Pokemon Center by its door (talking to Barry if he stands in it) and back out onto the door's column", + "Straight south from the door into the mine", + "Through the mine's entrance hall to the rocky corridor", + "Walk the planned route down the corridor and round the coal block, and talk to Roark", + "Walk back to the corridor's top and out of the mine", + "Into the Pokemon Center and heal at the counter", + }); + + step(1); + // Planned on Bulbapedia's Oreburgh map (devtools/maps/examples/ + // oreburgh_center_to_mine.png): the column of the Center's door runs + // straight south down the yard's stairs, between the conveyors, into + // the mine (walked north up it by hand, 2026-10-31). The door is the one + // place that pins that column: go in and back out, and we stand on it + // facing south. The save stands us two tiles west of the door and a row + // down; north stops against the Center's front or its post, then a tile + // at a time east until north goes in. Barry stands in the door the first + // time: walking into him reads as a wall, so A there; he tells us Roark + // went to the mine, which Roark needs before he answers (2026-10-29: he + // ignored A in r101 and r104 until then). A save after his talk has him + // gone already. + // + enter_oreburgh_center(stream, context); + context.wait_for(1000ms); + + step(2); + // Straight back south: out of the door and on into the mine. Fades go + // unseen at times (r184b missed the mine's, r185a the Center's and walked + // on into the mine hall), so count: the Center's door and the mine are + // two transitions, or about 1 s inside and 8.6 s outside of walking (by + // hand). A worker walks about on the sand path and stops the walk as a + // wall would (r95, r96): wait for him to move and walk on. Step 3 copes + // with ending in the hall or in the corridor. + bool in_mine = false; + size_t transitions = 0; + Milliseconds walked = 0ms; + for (size_t attempt = 0; attempt < 6 && !in_mine; attempt++){ + const WalkResult down = walk_dir(stream, context, Direction::SOUTH, 15000ms, "south out of the Center and into the mine"); + walked += down.walked; + if (down.stop == WalkStop::TRANSITION){ + transitions++; + context.wait_for(1000ms); + }else if (down.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, "Someone on the way to the mine"); + }else if (down.stop == WalkStop::BLOCKED){ + context.wait_for(2000ms); + } + // Counting alone took a walk that drifted into the yard (r188b) for the + // mine: it also has to look like the mine. + in_mine = (transitions >= 2 || walked >= 9500ms) && in_dim_cave(stream); + } + if (!in_mine){ + fail(stream, "Oreburgh: the walk south from the Center's door did not reach the mine."); + } + context.wait_for(1500ms); + + step(3); + // If the fade into the mine was missed, we are already in the corridor + // and this walk stops at its boulder; otherwise it crosses the hall. + const WalkResult hall = walk_dir(stream, context, Direction::SOUTH, 10000ms, "through the mine's entrance hall"); + if (hall.stop == WalkStop::TRANSITION){ + context.wait_for(1500ms); + } + + step(4); + // Planned on the coal room's tile grid (Data/OreburghMine): the + // corridor's rocks zigzag a row at a time, the room's second row passes + // under the worker on the first, and the east side leads down to the + // bottom row below Roark. + const Pokemon::MapSet& maps = oreburgh_mine_maps(); + auto world = [&](const Pokemon::TilePos& pos){ return maps.to_world(MAP_OREBURGH_MINE, pos); }; + std::vector no_trainers; + auto in_the_way = [&](const std::string& who){ talk_or_fight(stream, context, who); }; + std::vector at; + for (const Pokemon::TilePos& pos : OreburghMine::corridor_top()){ + at.push_back(world(pos)); + } + // Roark stands with a rock on each side, so the rock east of him is the + // anchor: plan to his row's open stretch east of the rock, walk north to + // the wall above it and west until the rock stops us, two tiles east of + // him (on the console, r97). Then south to the bottom wall and two tiles + // west puts us below him. + const Pokemon::TilePos roark = world(OreburghMine::ROARK); + at = walk_route(stream, context, maps, at, + [&](const Pokemon::TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }, + no_trainers, in_the_way, "To the stretch east of Roark"); + walk_to_wall(stream, context, 0, 1, 2000ms, "north to the wall on Roark's row"); + // His rock and the bottom wall end these legs (see walk_to_wall()). The + // two taps west from there land within a third of a tile or so of his + // column (s03e2 missed him by that much), so look for him with nudges + // either side. Any tap here can start a wild battle, which leaves us + // off his column (r180a, even on his row west of his rock): then line up + // again from the east end of the bottom row, three taps west and north to the wall on his row (Data/ + // OreburghMine: columns 23 to 25 have that wall above). + bool talked = false; + for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ + if (attempt > 0){ + // The battle may leave us on his row or the bottom one. + walk_to_wall(stream, context, 0, -1, 2000ms, "south to the bottom row"); + walk_to_wall(stream, context, 1, 0, 6000ms, "east along the bottom row"); + route_turn(stream, context, Direction::WEST); + for (size_t tap = 0; tap < 3; tap++){ + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + } + walk_to_wall(stream, context, 0, 1, 2000ms, "north to the wall on Roark's row"); + } + // A wild battle on the way eats into the walk's time (r100): walk again. + for (size_t walk = 0; walk < 3; walk++){ + if (walk_to_wall(stream, context, -1, 0, 6000ms, "west to the rock beside Roark").wild_battles == 0){ + break; + } + } + walk_to_wall(stream, context, 0, -1, 2000ms, "south to the bottom wall"); + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + route_turn(stream, context, Direction::NORTH); + context.wait_for_all_requests(); + if (run_from_late_encounter(stream, context, "below Roark")){ + continue; + } + if (VideoSnapshot screen = stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-below-roark.png"); + } + // The camera zooms in and his first line, "OK, stand back and watch + // this.", can take some seconds (r98). + // His rocks answer A too ("It's a rugged rock..."): his talk is the + // one that zooms the camera in on us (by hand), which the rock's + // message does not. Compare the view above the dialogue box. + talked = talk_with_nudges(stream, context, Direction::NORTH, "Roark", 8000ms, + [&](const ImageViewRGB32& before, const ImageViewRGB32& after){ + const ImageFloatBox above_box{0.0, 0.0, 1.0, 0.6}; + const double change = walk_screen_change( + extract_box_reference(before, above_box), extract_box_reference(after, above_box) + ); + stream.log("[01.03] View change when the talk opened: " + std::to_string(change) + " (Roark zooms in)."); + return change > 35; // by hand: 50 for his zoom + }); + } + if (!talked){ + fail(stream, "Oreburgh Mine: no talk opened below Roark (has Barry told us about the mine?)."); + } + // His scene (by hand, 2026-10-31): the camera zooms in, about eight + // lines that each wait for a press ("OK, stand back and watch this.", + // Rock Smash, "...That'd be me!"), then he smashes his rock and leaves. + // finish_dialogue() ended early on it, and stepping onto his tile could + // not tell him from the time the stop takes to read (r184, r186 "passed" + // with him still there, and Barry kept the gym shut). Press B through it + // on a clock, then ask his tile: once he is gone, A there opens nothing. + // B and A in the overworld facing a wall do nothing. + bool gone = false; + for (size_t round = 0; round < 3 && !gone; round++){ + for (size_t press = 0; press < 14; press++){ + pbf_press_button(context, BUTTON_B, 80ms, 2420ms); + } + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + gone = !dialogue_opens(stream, context, 2500ms); + } + if (!gone){ + fail(stream, "Oreburgh Mine: Roark is still there after the talk."); + } + walk_dir(stream, context, Direction::SOUTH, 2000ms, "back to the bottom wall"); + at = {roark.next(Direction::SOUTH)}; + + step(5); + // Back up the corridor, then north out through the hall. A route leg + // north to the top's wall walks on through it into the hall (r99), and + // no wall stops us a row short of it, so the route ends on the corner + // at (14,10), where that leg started, and the walk does the rest. + const Pokemon::TilePos exit = world(OreburghMine::CORRIDOR_EXIT_START); + at = walk_route(stream, context, maps, at, + [&](const Pokemon::TilePos& pos){ return pos == exit; }, + no_trainers, in_the_way, "Back to the corridor's top"); + // Up into the entrance hall (a transition, as on the way in), then on + // north out of the mine: in r103 that walk left the mine without a + // transition we saw and ended against the sign above the stairs, which + // the moving belts make read as the time limit. + if (walk_dir(stream, context, Direction::NORTH, 10000ms, "into the entrance hall").stop != WalkStop::TRANSITION){ + fail(stream, "Oreburgh Mine: walking north from the corridor did not reach the entrance hall."); + } + context.wait_for(1500ms); + if (walk_dir(stream, context, Direction::NORTH, 10000ms, "out of the mine").stop == WalkStop::DIALOG){ + fail(stream, "Oreburgh Mine: someone stopped us on the way out of the mine."); + } + context.wait_for(1500ms); + + step(6); + // End at the nurse, healed: the counter is a fixed start for 01.04, + // where the mine's mouth is not (2026-10-31, at the user's request). Out + // of the mine we stand below its sign; a tile east is the Center door's + // column, and north on it goes in, or stops on the front rows. + hold_tiles(stream, context, Direction::EAST, 1); + walk_until(stream, context, 0, 1, 12000ms, "north into the Center's door", + [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); + size_t floor = 0; + if (VideoSnapshot frame = stream.video().snapshot()){ + center_floor_on_screen(*frame.frame, &floor); + } + if (floor < 45){ + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, 480ms, 300ms); + enter_oreburgh_center(stream, context); + } + heal_at_oreburgh_counter(stream, context); +} + + +// A rough check that we stand in a Pokemon Center: its floor fills the lower +// half of the screen with warm yellow, where the gym is grey and routes are +// green. After a battle it tells a loss (a whiteout ends at the counter) +// from a win. +bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent){ + const ImageRGB32 small = screen.scale_to(192, 108); + size_t warm = 0, total = 0; + for (size_t y = 54; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + const int r = (pixel >> 16) & 0xff, g = (pixel >> 8) & 0xff, b = pixel & 0xff; + total++; + if (r > 200 && g > 150 && b < 150 && r - b > 70){ + warm++; + } + } + } + if (percent){ + *percent = warm * 100 / total; + } + return warm * 10 > total * 3; +} +bool in_pokemon_center(VideoStream& stream){ + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + size_t percent = 0; + const bool inside = center_floor_on_screen(*frame.frame, &percent); + stream.log("[Center check] warm floor " + std::to_string(percent) + " %: " + + (inside ? "in a Pokemon Center." : "not in a Pokemon Center.")); + return inside; +} + +// The Oreburgh Mine's hall and corridors are dim: mean brightness 45 to 64 +// on the console, against 99 to 116 for the city and its yard (2026-10-31). +bool in_dim_cave(VideoStream& stream, uint64_t below){ + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + const ImageRGB32 small = frame.frame->scale_to(192, 108); + uint64_t sum = 0; + for (size_t y = 0; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); + } + } + const uint64_t mean = sum / (3 * 192 * 108); + stream.log("[Cave check] mean brightness " + std::to_string(mean) + "."); + return mean < below; +} + + +// Oreburgh, from the street in front of the Pokemon Center (its door or its +// front wall) into the gym, as walked by hand on 2026-10-29: west to the +// cliff, south to the posts, a row up, then 17 tiles east is the gym door's +// column. The hold is checked by the door itself: walk north, and if the +// gym's wall stops us, try the columns either side. +// D-pad moves from the Center's doorstep (as walk_out_of_center() leaves us +// there) to below the gym's door: 20. The program's own runs landed 2 tiles +// west with 22 and 1 tile west with 21 (r217d, r218d, the same tiles each +// time). By hand, after a stick exit, it took 22 taps; the hand exit and +// count did not carry over to the program's. Stick holds drift: 160 ms moved +// 36 to 48 px, so 18 or 19 of them missed the door by a part of a tile. +constexpr size_t GYM_DPAD_TAPS_WEST_OF_CENTER = 20; +uint64_t mean_brightness(const ImageViewRGB32& screen); + +// `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the +// player moves a whole tile per tap (measured above), so counted legs do +// not drift the way stick holds do. The first tap toward a new way only +// turns the player, so turn first. +void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label){ + stream.log("[Walk] " + label + ": " + std::to_string(taps) + " D-pad taps."); + pbf_press_dpad(context, dpad, 160ms, 700ms); // turn + // Count only taps that moved the view: one after the turn was lost and + // the leg ended a tile short (r215d). A tile's scroll changes the view by + // about 100; standing still, under 15. A tap that does not move is someone + // in the way: people walk about these streets (r220g: blocked on tap 9 + // for 12 s while a passer-by stood in our row). Wait for them to move on, + // up to 30 s, before giving up. + size_t moved = 0; + const auto deadline = current_time() + std::chrono::milliseconds(taps * 1000 + 30000); + while (moved < taps && current_time() < deadline){ + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + pbf_press_dpad(context, dpad, 160ms, 700ms); + context.wait_for_all_requests(); + const VideoSnapshot after = stream.video().snapshot(); + const double change = before && after ? walk_screen_change(*before.frame, *after.frame) : 100; + if (change > 30){ + moved++; + continue; + } + stream.log("[Walk] " + label + ": tap " + std::to_string(moved + 1) + " did not move (view change " + std::to_string(change) + "); someone in the way? Waiting."); + if (dialogue_opens(stream, context, 300ms)){ + talk_or_fight(stream, context, label + ": someone in the way"); + } + context.wait_for(1500ms); + } + if (moved < taps){ + fail(stream, label + ": only " + std::to_string(moved) + " of " + std::to_string(taps) + " D-pad taps moved us."); + } +} + +// Through a door straight north of us: tap up to face it, hold up into it, +// and wait for its dark fade (by hand: brightness 7, about 1 s after). +bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label){ + pbf_press_dpad(context, DPAD_UP, 160ms, 300ms); + pbf_press_dpad(context, DPAD_UP, 300ms, 0ms); + context.wait_for_all_requests(); + const auto start = current_time(); + while (current_time() - start < 5000ms){ + context.wait_for(100ms); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && mean_brightness(*frame.frame) < 25){ + stream.log("[Route] " + label + ": the screen went dark; through the door."); + context.wait_for(2000ms); + return true; + } + } + stream.log("[Route] " + label + ": no dark screen after the D-pad step north."); + return false; +} + +// Mean of the three channels over a small copy of the frame, 0 to 255. +uint64_t mean_brightness(const ImageViewRGB32& screen){ + const ImageRGB32 small = screen.scale_to(192, 108); + uint64_t sum = 0; + for (size_t y = 0; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); + } + } + return sum / (3 * 192 * 108); +} + +// North through a door from just below it: one tile's step, then watch +// for the door's black fade. Entering the Oreburgh Gym (by hand, +// 2026-10-31): the door opens and the camera zooms in, and about 2 s after +// the step the screen is black (brightness 7) for 1 to 1.5 s. The step's +// own walk stops watching when the stick is let go, so it missed that +// black; a scene comparison could not tell a tile's scroll (106 to 119) +// from going in (121, r205d, r207d). A miss steps back onto the row. +bool through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label){ + auto went_dark = [&](Milliseconds timeout){ + const auto start = current_time(); + while (current_time() - start < timeout){ + context.wait_for(100ms); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && mean_brightness(*frame.frame) < 25){ + return true; + } + } + return false; + }; + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + const WalkResult up = walk_for(stream, context, 0, 1, 240ms, label); + // Look for the fade ourselves: the library's black screen check missed it + // under the gym's door (r209d), while its frames read 7 against about + // 100 outside. + if (up.stop == WalkStop::TRANSITION || went_dark(5000ms)){ + stream.log("[Route] " + label + ": the screen went dark; through the door."); + context.wait_for(2000ms); + return true; + } + const VideoSnapshot after = stream.video().snapshot(); + const double moved = before && after ? walk_screen_change(*before.frame, *after.frame) : 0; + // The door opened and the camera zoomed in, but one step did not take us + // through (r213d: view change 160, no fade): step on north. + if (moved > WALK_SCENE_CHANGE){ + stream.log("[Route] " + label + ": the door opened (view change " + std::to_string(moved) + "); on north."); + walk_for(stream, context, 0, 1, 240ms, label + ": on through"); + if (went_dark(5000ms)){ + stream.log("[Route] " + label + ": the screen went dark; through the door."); + context.wait_for(2000ms); + return true; + } + } + // Back down only if the step moved us: against the building's front it + // does not, and stepping south anyway took us a row down each try + // (r208d drifted south along the gym's front). + stream.log("[Route] " + label + ": no dark screen; not the door (view change " + std::to_string(moved) + ")."); + if (moved > WALK_BLOCKED_CHANGE * 2){ + walk_for(stream, context, 0, -1, 240ms, label + ": back down"); + } + return false; +} + +void oreburgh_street_to_gym(VideoStream& stream, ProControllerContext& context){ + // From the Center's doorstep, straight west along its row to below the + // gym's door, then north through it, all on the D-pad (whole tiles; see + // GYM_DPAD_TAPS_WEST_OF_CENTER). + dpad_tiles(stream, context, DPAD_LEFT, GYM_DPAD_TAPS_WEST_OF_CENTER, "west to the gym's door"); + if (dpad_through_door_north(stream, context, "Oreburgh Gym door")){ + return; + } + fail(stream, "Oreburgh: no column near the gym's door went in."); +} + +// Inside the Oreburgh Gym: straight up the middle, fighting the trainers +// who stop us, to Roark on the top tier. Returns whether Roark was beaten; +// false after a whiteout, which leaves us in the Pokemon Center. +// Walking straight up meets both gym trainers (one at the entrance, one on +// the second tier); their experience helps an under-levelled team, and +// after a whiteout they stay beaten. Butters' route goes round them. +bool oreburgh_gym_battles(VideoStream& stream, ProControllerContext& context){ + // The entry camera pans for about 7.5 s (Butters' reference video). + context.wait_for(8000ms); + if (dialogue_opens(stream, context, 3000ms)){ + if (talk_or_fight(stream, context, "Gym trainer at the entrance") && in_pokemon_center(stream)){ + return false; + } + } + size_t stuck = 0; + for (size_t leg = 0; leg < 10; leg++){ + const WalkResult up = walk_until_no_wild(stream, context, 0, 1, 8000ms, "north up the gym"); + if (up.stop == WalkStop::DIALOG){ + if (talk_or_fight(stream, context, "Gym trainer") && in_pokemon_center(stream)){ + return false; + } + continue; + } + if (up.stop != WalkStop::BLOCKED){ + continue; + } + // Roark stands at the top facing us; a wall or a stair edge says + // nothing to A. Only Roark starts a battle from our A here. The walk + // left us facing north: a turn tap that way would be a step. + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + if (dialogue_opens(stream, context, 3000ms)){ + // His lines pause for more than 4 s before the battle (r210d: "only + // talked"), so the battle is expected. + if (talk_or_fight(stream, context, "Roark", true)){ + if (in_pokemon_center(stream)){ + return false; + } + finish_dialogue(stream, context, BUTTON_B, "Roark after the battle"); + return true; + } + continue; + } + if (up.walked <= 800ms && ++stuck >= 3){ + fail(stream, "Oreburgh Gym: stuck on the way up, and nobody answers A."); + } + } + fail(stream, "Oreburgh Gym: did not reach Roark."); +} + +void oreburgh_gym(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.04", "From the Oreburgh Mine to the Coal Badge", { + "Out of the Pokemon Center from the counter", + "West along the row into the gym", + "Up the gym, fighting its trainers, and beat Roark (after a loss: from the Center, again)", + }); + step(1); + // 01.03 leaves us at the Center's counter, facing the nurse; the door + // is straight south. + walk_out_of_center(stream, context, "Oreburgh"); + + // Losing costs some money and keeps the experience, so each try comes + // back stronger: Roark won twice by hand against Piplup Lv 10 and 14 and + // lost the third time. + bool won = false; + for (size_t attempt = 0; attempt < 5 && !won; attempt++){ + step(2); + oreburgh_street_to_gym(stream, context); + step(3); + won = oreburgh_gym_battles(stream, context); + if (!won){ + stream.log("[01.04] Lost in the gym (try " + std::to_string(attempt + 1) + "); back from the Center.", COLOR_ORANGE); + walk_out_of_center(stream, context, "Oreburgh"); + } + } + if (!won){ + fail(stream, "Oreburgh Gym: lost to Roark five times."); + } + +} + +// West until a transition, by the user's rule for a wall in the way: walk +// north to the top wall, which pins the row, then try the rows south of it +// one at a time. Trainers who stop us are fought. Returns at the first +// transition (a building, a cave, a gate), or when `arrived` matches. +void west_until_transition( + VideoStream& stream, ProControllerContext& context, const std::string& label, size_t max_legs, + const std::function& arrived = nullptr +){ + for (size_t leg = 0; leg < max_legs; leg++){ + const WalkResult west = walk_dir(stream, context, Direction::WEST, 20000ms, label, arrived); + if (west.stop == WalkStop::ARRIVED){ + return; + } + if (west.stop == WalkStop::TRANSITION){ + context.wait_for(2000ms); + return; + } + if (west.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, label + ": someone in the way"); + continue; + } + if (west.stop != WalkStop::BLOCKED){ + continue; + } + walk_dir(stream, context, Direction::NORTH, 4000ms, label + ": north to the top wall"); + bool moved = false; + for (size_t row = 0; row < 10 && !moved; row++){ + hold_tiles(stream, context, Direction::SOUTH, 1); + const WalkResult probe = walk_dir(stream, context, Direction::WEST, 1500ms, label + ": west on the next row", arrived); + if (probe.stop == WalkStop::ARRIVED){ + return; + } + moved = probe.stop != WalkStop::BLOCKED || probe.walked > 1200ms; + if (probe.stop == WalkStop::TRANSITION){ + context.wait_for(2000ms); + return; + } + if (probe.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, label + ": someone in the way"); + } + } + if (!moved){ + fail(stream, label + ": no row below the top wall leads west."); + } + } + fail(stream, label + ": no transition after " + std::to_string(max_legs) + " legs."); +} + +// Oreburgh: from in front of Roark at the top of the gym, back down and out +// of its door a tile at a time, east along the row to the Pokemon Center +// and heal; ends at the counter. +void oreburgh_gym_to_center(VideoStream& stream, ProControllerContext& context){ + if (!dpad_out_of_building(stream, context, 40, "out of the gym")){ + fail(stream, "Oreburgh Gym: did not leave by its door."); + } + + // Heal at the Center, so 01.05 starts at its counter: the gym's doorstep + // row runs east to the Center's door, the way 01.04 came. + // The way 01.04 came, reversed, on the D-pad. Untested on the console: + // the gym's doorstep is assumed to be the tile below its door. + dpad_tiles(stream, context, DPAD_RIGHT, GYM_DPAD_TAPS_WEST_OF_CENTER, "east to the Center's door"); + if (!dpad_through_door_north(stream, context, "Oreburgh Pokemon Center door")){ + fail(stream, "Oreburgh: the Pokemon Center's door did not take us from the gym's row."); + } + heal_at_oreburgh_counter(stream, context); +} + +void oreburgh_to_route203(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.05", "From the Oreburgh Gym out of the Oreburgh Gate onto Route 203", { + "Out of the gym onto its doorstep", + "Up to the platform by Oreburgh Gate and into it (Barry bumps into us there)", + "West through the Gate until Route 203's banner shows", + }); + step(1); + // 01.04 ends in front of Roark, just after his battle: its save at + // 01.05's start is right after the win (2026-10-31, at the user's + // request), so a failure later never has to beat him again. + // No trip back to the Oreburgh Center (2026-10-31, the user): talk_or_fight() + // gives a Potion after Roark when HP is low, and Jubilife's Center is on + // the way. Out of the gym onto its doorstep, which is on the Center door's + // row: west along it reaches the stairs' cliff. + if (!dpad_out_of_building(stream, context, 40, "out of the gym")){ + fail(stream, "Oreburgh Gym: did not leave by its door."); + } + + step(2); + // From the gym's doorstep, as walked by hand on 2026-10-29. + walk_dir(stream, context, Direction::WEST, 20000ms, "west to the stairs' cliff"); + walk_dir(stream, context, Direction::NORTH, 10000ms, "north along the cliff"); + hold_tiles(stream, context, Direction::EAST, 2); + walk_dir(stream, context, Direction::NORTH, 5000ms, "north up the stairs"); + walk_dir(stream, context, Direction::WEST, 6000ms, "west along the platform"); + hold_tiles(stream, context, Direction::SOUTH, 1); + // Barry runs into us on the platform ("*thud*") the first time; his lines + // end with no battle. + west_until_transition(stream, context, "Into Oreburgh Gate", 8); + + step(3); + // The cave's fade out of the Gate does not always read as a transition + // (r89), so the walk west ends on Route 203's location banner, which + // shows for a few seconds as we step out. 01.06 starts there, from a save. + west_until_transition(stream, context, "West out of Oreburgh Gate", 8, location_banner_showing); +} + +void route203_to_jubilife(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.06", "From the Oreburgh Gate's west exit into Jubilife City", { + "Route 203 on its grid to row 18, below the girl", + "West along row 18 into Jubilife, counting tiles", + }); + + step(1); + // On the grid (Data/PokemonBDSP_AutoStory_Route203, from the decomp, + // its east stretch checked by hand): south from the Gate down the sand, + // west along the sand's bottom row and on under the girl's row, which + // runs straight to Jubilife as row 18. Trainers the map does not list + // are fought where they stop us. + std::vector trainers; + auto fight = [&](const std::string& who){ + talk_or_fight(stream, context, "Route 203: " + who); + }; + const std::vector at = walk_route(stream, context, route203_maps(), Route203::gate_exit(), + on_route203_west_line, trainers, fight, "Route 203 to row 18"); + if (at.size() != 1){ + fail(stream, "Route 203: the walk to row 18 did not end on a known tile."); + } + + step(2); + // Row 18 has no wall before Jubilife, so count the tiles: from column + // `x` it is x + 1 taps to Jubilife's first tile (by hand from column 31: + // 32 taps, Jubilife's banner on the last one). 01.07 starts there. + dpad_tiles(stream, context, DPAD_LEFT, (size_t)at[0].x + 1, "west along row 18 into Jubilife"); + context.wait_for_all_requests(); + if (!location_banner_showing(*stream.video().snapshot().frame)){ + fail(stream, "Route 203: no Jubilife banner after counting the tiles west."); + } +} + +void jubilife_to_rowan(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.07", "From Jubilife City's east edge through Rowan's scene", { + "West along Jubilife's long road to the crossroads", + "North up the street to Rowan (Dawn and two Galactic grunts: a tag battle)", + }); + + step(1); + // Route 203's row 18 carries on as Jubilife's long road. From the first + // tile the crossroads with the street north to Route 204 is 14 tiles + // west; the street is about 4 tiles wide, so a stick hold does (by hand, + // 2026-10-31: 2 x 1120 ms landed mid-street; 7 D-pad taps stopped below + // a building, and 18 went on to the road down to Route 202, r230b). + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 14), 300ms); + context.wait_for_all_requests(); + + step(2); + // North up the street: Rowan's scene starts a few tiles up (by hand, a + // 6 s walk). It opens with a cutscene, not a dialog box, so the walk + // reads as stopped; wait for his first line. The green-haired man walks + // about the crossroads and may stand in our way: wait and walk again. + bool scene = false; + for (size_t tries = 0; tries < 3 && !scene; tries++){ + const WalkResult up = walk_dir(stream, context, Direction::NORTH, 6000ms, "north up the street to Route 204"); + scene = up.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 8000ms); + if (!scene){ + pbf_wait(context, 2500ms); + } + } + if (!scene){ + fail(stream, "Jubilife: Rowan's scene did not start up the street north."); + } + if (!talk_or_fight(stream, context, "Galactic grunts with Dawn", true, true)){ + fail(stream, "Jubilife: Rowan's scene ended without the tag battle."); + } + if (in_pokemon_center(stream)){ + fail(stream, "Jubilife: lost the tag battle with Dawn."); + } + finish_dialogue(stream, context, BUTTON_B, "Rowan and Dawn after the battle"); + // 01.08 starts here, from a save at the top of the street. +} + +void jubilife_to_ravaged_path(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.08", "From the top of Jubilife's north street into the Ravaged Path", { + "North out of Jubilife onto Route 204", + "Route 204 South on its grid to below the Ravaged Path's door", + "North through the door", + }); + + step(1); + // The street's column 14 carries on as the route's; the location banner + // names Route 204 as we cross. A D-pad walk keeps the column; it may + // stop a tile or two past the border, which the entry set allows for. + const WalkResult out = walk_until_dpad(stream, context, 0, 1, 8000ms, "north out of Jubilife", location_banner_showing); + if (out.stop != WalkStop::ARRIVED){ + fail(stream, "Jubilife: no Route 204 banner walking north up the street."); + } + context.wait_for(1500ms); + + step(2); + // On the grid decoded from the game's data (Data/..._Route204South): + // a tile west onto the tall grass column that runs up to the cliff, + // north up it, then west along the cliff to the signboard beside the + // door (the offline plan: 3 legs). Other trainers' sight lines are kept + // out of; town_plan_costs() lets the one-tile step land exactly. Lass Sarah's east line crosses + // the street, so she may stop us for a battle on the way; a Youngster + // who is not a trainer wanders in front of the door, and the walker + // waits for him. + { + std::vector trainers = Route204South::trainer_sights(); + auto fight = [&](const std::string& who){ talk_or_fight(stream, context, who); }; + const std::vector at = walk_route(stream, context, route204_south_maps(), + Route204South::jubilife_entry(), + [](const Pokemon::TilePos& pos){ return pos == Route204South::BELOW_DOOR; }, + trainers, fight, "Route 204 South to the Ravaged Path", Route204South::tiles_to_avoid(), town_plan_costs()); + if (at.size() != 1 || !(at[0] == Route204South::BELOW_DOOR)){ + fail(stream, "Route 204: the walk did not end below the Ravaged Path's door."); + } + } + + step(3); + if (!dpad_through_door_north(stream, context, "Ravaged Path door")){ + fail(stream, "Route 204: no dark screen walking north into the Ravaged Path."); + } + context.wait_for(2000ms); +} + +// "This rock seems breakable with a hidden move. Would you like to use Rock +// Smash?" is open, from an A at a smashable rock. Its Yes/No box comes only +// once the text is advanced again (by hand, 2026-10-31: the text complete +// 1.2 s after the first A, the box 1.2 s after a second), and r237a's timed +// A, A, B put its B on that box: No. So press A until the box's cursor +// shows, then once more for Yes, and wait out the move. False if no box +// shows: whatever is open is not this prompt. +bool answer_rock_smash_prompt(VideoStream& stream, ProControllerContext& context, const std::string& label){ + static const ImageFloatBox YES_NO_ARROW_BOX{0.700, 0.620, 0.070, 0.120}; + SelectionArrowFinder yes_no(stream.overlay(), YES_NO_ARROW_BOX, COLOR_GREEN); + for (size_t press = 0; press < 5; press++){ + context.wait_for(1200ms); + context.wait_for_all_requests(); + const VideoSnapshot screen = stream.video().snapshot(); + if (screen && yes_no.detect(*screen.frame)){ + stream.log("[Route] " + label + ": Rock Smash offered; Yes."); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for(5000ms); + if (dialogue_opens(stream, context, 500ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": after Rock Smash"); + } + return true; + } + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + } + stream.log("[Route] " + label + ": no Yes/No box for Rock Smash.", COLOR_RED); + return false; +} + +void ravaged_path(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.09", "Through the Ravaged Path", { + "Through the Ravaged Path, smashing its rocks", + }); + + step(1); + // On the traced grid (Data/PokemonBDSP_AutoStory_RavagedPath): from the + // south door up the stairs to the tile west of one of the two rocks + // across the lower corridor, smash it, then east and south to the + // corridor's east end; the way out is a tile west of it, south. + { + std::vector no_trainers; + // Blocked short of a wall in here, it is a rock the plan counted on + // being gone (r237a: the walker took one for a person and said No). + auto in_the_way = [&](const std::string& who){ + if (!answer_rock_smash_prompt(stream, context, who)){ + talk_or_fight(stream, context, who); + } + }; + std::vector at{RavagedPath::ENTRANCE}; + const Pokemon::MapSet rocks_in = ravaged_path_maps(); + at = walk_route(stream, context, rocks_in, at, + [&](const Pokemon::TilePos& pos){ + const auto& before = RavagedPath::before_rocks(); + return std::find(before.begin(), before.end(), pos) != before.end(); + }, + no_trainers, in_the_way, "To a rock across the corridor"); + route_turn(stream, context, Direction::EAST); + if (!talk_with_nudges(stream, context, Direction::EAST, "the rock east of us", 2000ms)){ + fail(stream, "Ravaged Path: nothing answered facing the rock east of us."); + } + if (!answer_rock_smash_prompt(stream, context, "the rock east of us")){ + fail(stream, "Ravaged Path: no Rock Smash offer facing the rock east of us."); + } + // We may be on either row; the rock east of the other stays, but no + // leg from our row crosses it. + std::vector smashed; + for (const Pokemon::TilePos& pos : RavagedPath::before_rocks()){ + smashed.push_back(pos.next(Direction::EAST)); + } + const Pokemon::MapSet rocks_out = ravaged_path_maps(smashed); + at = walk_route(stream, context, rocks_out, at, + [&](const Pokemon::TilePos& pos){ return pos == RavagedPath::EAST_END; }, + no_trainers, in_the_way, "To the corridor's east end"); + dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); + const WalkResult out = walk_dir(stream, context, Direction::SOUTH, 4000ms, "south out of the Ravaged Path"); + if (out.stop != WalkStop::TRANSITION && out.stop != WalkStop::LIMIT){ + fail(stream, "Ravaged Path: the walk south from a tile west of the corridor's end did not leave the cave."); + } + context.wait_for(2000ms); + } +} + +void route204_to_floaroma(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.10", "From the Ravaged Path's north exit to Floaroma Town", { + "Route 204 north on the human's line, into Floaroma", + }); + + step(1); + // The line a human walked (Data/PokemonBDSP_AutoStory_Route204, + // floaroma_waypoints()): a tile south out of the cave's mouth, west along + // the row under the cliff past the Youngster who wanders there, up the + // stairs, east along the corridor into Aroma Lady Taylor's battle, north + // through the tall grass, west below the flower beds, and north between + // the beds into Floaroma. Whole tiles on the D-pad, corner to corner. + { + std::vector trainers = Route204::north_trainer_sights(); + auto fight = [&](const std::string& who){ + const bool doubles = who.find("Twins") != std::string::npos || who.find("someone") != std::string::npos; + talk_or_fight(stream, context, who, false, doubles); + }; + std::vector at{Route204::CAVE_EXIT}; + at = walk_waypoints(stream, context, route204_north_maps(), at, Route204::floaroma_waypoints(), + trainers, fight, "To the path into Floaroma"); + if (!std::all_of(at.begin(), at.end(), Route204::in_floaroma_path)){ + fail(stream, "Route 204: the walk did not end in the path into Floaroma."); + } + // North until the town's banner shows: the checkpoint ends right + // there, where the save goes (r247a walked 20 s on and overshot). + const WalkResult in = walk_until_dpad(stream, context, 0, 1, 12000ms, "north into Floaroma", location_banner_showing); + if (in.stop != WalkStop::ARRIVED){ + fail(stream, "Route 204: no location banner walking north into Floaroma."); + } + context.wait_for(500ms); + const VideoSnapshot banner = stream.video().snapshot(); + const std::string read = banner ? read_location_banner(*banner.frame) : ""; + if (!location_banner_matches(read, "Floaroma Town")){ + fail(stream, "Route 204: the banner read \"" + read + "\", not Floaroma Town."); + } + stream.log("[01.10] In Floaroma Town; the banner read \"" + read + "\".", COLOR_GREEN); + } + +} + +// 01.11: the old couple in Floaroma's flower field, left of the path in +// from Route 204. Walked by hand on 2026-10-31 from 01.10's save: west 8 +// along the row (the field's bottom, purple flowers), north until the old +// woman stops us (r249a counted two and stood a row short: no count, a +// clip), talk (she gives Mew when a Let's Go save is on the console; +// otherwise she only talks), one tile west, north until the old man stops +// us (a tap that cannot step still turns), talk (Jirachi with a +// Sword/Shield save). B advances their lines and never re-talks; A at the +// end re-opens the talk (m5, m6). +void floaroma_old_couple(VideoStream& stream, ProControllerContext& context){ + StepLog step(stream, "01.11", "Floaroma's old couple in the flower field", { + "West 8, north until the old woman stops us", + "Talk to the old woman", + "Talk to the old man", + }); + + step(1); + { + const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, 8, "west 8 to the flower field"); + if (west.stop != WalkStop::LIMIT || west.moved_chunks < 7){ + fail(stream, "Floaroma: west 8 to the flower field moved " + std::to_string(west.moved_chunks) + " tiles."); + } + // Blocked, or out of taps with the last ones bumping: either way + // she is in front of us (r250a: 3 steps, 2 bumps, "limit"). + const WalkResult north = walk_tiles_dpad(stream, context, 0, 1, 5, "north until the old woman"); + if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 5){ + fail(stream, "Floaroma: nothing stopped the walk north to the old woman."); + } + } + + auto talk = [&](const std::string& who){ + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 3000ms, {{dialog}}) != 0){ + fail(stream, "Floaroma: " + who + " did not answer."); + } + finish_dialogue(stream, context, BUTTON_B, who); + }; + + step(2); + talk("the old woman"); + + step(3); + // One tile west, then a tap north: the old man stands there, so the + // tap only turns. + const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, 1, "west 1 below the old man"); + if (west.moved_chunks < 1){ + fail(stream, "Floaroma: the step west below the old man did not move."); + } + const WalkResult north = walk_tiles_dpad(stream, context, 0, 1, 3, "north until the old man"); + if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 3){ + fail(stream, "Floaroma: nothing stopped the walk north to the old man."); + } + talk("the old man"); +} + +} + + +std::map segment_01_variants(){ + return { + {"jubilife_to_barry", story_call([](StoryConsole& c){ jubilife_to_barry(c.env.console, c.context); })}, + {"route203_to_oreburgh", story_call([](StoryConsole& c){ route203_to_oreburgh(c.env.console, c.context); })}, + {"oreburgh_to_roark", story_call([](StoryConsole& c){ oreburgh_to_roark(c.env.console, c.context); })}, + {"oreburgh_gym", story_call([](StoryConsole& c){ oreburgh_gym(c.env.console, c.context); })}, + {"oreburgh_to_route203", story_call([](StoryConsole& c){ oreburgh_to_route203(c.env.console, c.context); })}, + {"route203_to_jubilife", story_call([](StoryConsole& c){ route203_to_jubilife(c.env.console, c.context); })}, + {"jubilife_to_rowan", story_call([](StoryConsole& c){ jubilife_to_rowan(c.env.console, c.context); })}, + {"jubilife_to_ravaged_path", story_call([](StoryConsole& c){ jubilife_to_ravaged_path(c.env.console, c.context); })}, + {"ravaged_path", story_call([](StoryConsole& c){ ravaged_path(c.env.console, c.context); })}, + {"route204_to_floaroma", story_call([](StoryConsole& c){ route204_to_floaroma(c.env.console, c.context); })}, + {"floaroma_old_couple", story_call([](StoryConsole& c){ floaroma_old_couple(c.env.console, c.context); })}, + }; +} + +// The table is segment_01_table() (PokemonBDSP_AutoStory_StoryTable.h). Its +// steps only call functions, so the executor plans nothing itself. +Pokemon::Segment make_segment_01(StoryStepRunner& runner){ + const std::vector& table = segment_01_table(); + return story_table_segment(runner, "01", table, std::make_shared( + runner, rows_that_stop(table), story_maps(), std::vector{}, + [](StoryConsole&, const std::string&){}, segment_01_variants() + )); +} + + +std::string AutoStory_Segment_01::name() const { return "01: Jubilife to Floaroma"; } +std::string AutoStory_Segment_01::start_text() const { return "Start: Standing in front of the Jubilife City Pokemon Center PC."; } +std::string AutoStory_Segment_01::end_text() const { return "End: In Floaroma's flower field below the old couple, with the Coal Badge, saved."; } + +void AutoStory_Segment_01::run_segment( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + stats.m_segment++; + env.update_stats(); + env.console.log("Start Segment " + name(), COLOR_ORANGE); + run_story_table(env, context, options, stats, make_segment_01, {}); + env.console.log("End Segment " + name(), COLOR_GREEN); +} + +std::string AutoStory_Checkpoint_0101::name() const { return "01.01 - Jubilife to Barry"; } +std::string AutoStory_Checkpoint_0101::start_text() const { return "Standing in front of the Jubilife City Pokemon Center PC."; } +std::string AutoStory_Checkpoint_0101::end_text() const { return "On Route 203 by its sign, after beating Barry."; } + +void AutoStory_Checkpoint_0101::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.01", "01.01"}); +} + +std::string AutoStory_Checkpoint_0102::name() const { return "01.02 - Route 203 to Oreburgh"; } +std::string AutoStory_Checkpoint_0102::start_text() const { return AutoStory_Checkpoint_0101().end_text(); } +std::string AutoStory_Checkpoint_0102::end_text() const { return "In front of the Oreburgh Pokemon Center, after the man's tour and Barry."; } + +void AutoStory_Checkpoint_0102::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.02", "01.02"}); +} + + + +std::string AutoStory_Checkpoint_0103::name() const { return "01.03 - Oreburgh Mine to Roark"; } +std::string AutoStory_Checkpoint_0103::start_text() const { return AutoStory_Checkpoint_0102().end_text(); } +std::string AutoStory_Checkpoint_0103::end_text() const { return "At the Oreburgh Pokemon Center counter, healed, after talking to Roark."; } + +void AutoStory_Checkpoint_0103::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.03", "01.03"}); +} + + + +std::string AutoStory_Checkpoint_0104::name() const { return "01.04 - Oreburgh Gym"; } +std::string AutoStory_Checkpoint_0104::start_text() const { return AutoStory_Checkpoint_0103().end_text(); } +std::string AutoStory_Checkpoint_0104::end_text() const { return "In the Oreburgh Gym, in front of Roark, just after beating him (Coal Badge)."; } + +void AutoStory_Checkpoint_0104::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.04", "01.04"}); +} + +std::string AutoStory_Checkpoint_0105::name() const { return "01.05 - Oreburgh to Route 203"; } +std::string AutoStory_Checkpoint_0105::start_text() const { return AutoStory_Checkpoint_0104().end_text(); } +std::string AutoStory_Checkpoint_0105::end_text() const { return "On Route 203, just out of the Oreburgh Gate's west exit."; } + +void AutoStory_Checkpoint_0105::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.05", "01.05"}); +} + +std::string AutoStory_Checkpoint_0106::name() const { return "01.06 - Route 203 to Jubilife"; } +std::string AutoStory_Checkpoint_0106::start_text() const { return AutoStory_Checkpoint_0105().end_text(); } +std::string AutoStory_Checkpoint_0106::end_text() const { return "In Jubilife City, on the first tile of its long road in from Route 203."; } + +void AutoStory_Checkpoint_0106::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.06", "01.06"}); +} + +std::string AutoStory_Checkpoint_0107::name() const { return "01.07 - Jubilife through Rowan's scene"; } +std::string AutoStory_Checkpoint_0107::start_text() const { return AutoStory_Checkpoint_0106().end_text(); } +std::string AutoStory_Checkpoint_0107::end_text() const { return "In Jubilife City, at the top of the street north to Route 204, after the tag battle with Dawn."; } + +void AutoStory_Checkpoint_0107::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.07", "01.07"}); +} + +std::string AutoStory_Checkpoint_0108::name() const { return "01.08 - Jubilife to the Ravaged Path"; } +std::string AutoStory_Checkpoint_0108::start_text() const { return AutoStory_Checkpoint_0107().end_text(); } +std::string AutoStory_Checkpoint_0108::end_text() const { return "In the Ravaged Path, just inside its south entrance."; } + +void AutoStory_Checkpoint_0108::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.08", "01.08"}); +} + +std::string AutoStory_Checkpoint_0109::name() const { return "01.09 - Ravaged Path"; } +std::string AutoStory_Checkpoint_0109::start_text() const { return AutoStory_Checkpoint_0108().end_text(); } +std::string AutoStory_Checkpoint_0109::end_text() const { return "On Route 204, just out of the Ravaged Path's north exit."; } + +void AutoStory_Checkpoint_0109::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.09", "01.09"}); +} + +std::string AutoStory_Checkpoint_0110::name() const { return "01.10 - Route 204 to Floaroma"; } +std::string AutoStory_Checkpoint_0110::start_text() const { return AutoStory_Checkpoint_0109().end_text(); } +std::string AutoStory_Checkpoint_0110::end_text() const { return "Just inside Floaroma Town from Route 204, saved."; } + +void AutoStory_Checkpoint_0110::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.10", "01.10"}); +} + +std::string AutoStory_Checkpoint_0111::name() const { return "01.11 - Floaroma's old couple"; } +std::string AutoStory_Checkpoint_0111::start_text() const { return AutoStory_Checkpoint_0110().end_text(); } +std::string AutoStory_Checkpoint_0111::end_text() const { return "In Floaroma's flower field below the old couple, both talked to (Mew and Jirachi when the console has the saves), saved."; } + +void AutoStory_Checkpoint_0111::run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats +) const{ + run_story_table(env, context, options, stats, make_segment_01, {"01.11", "01.11"}); +} + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h new file mode 100644 index 0000000000..ccb65f4ca8 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h @@ -0,0 +1,189 @@ +/* BDSP AutoStory - Segment 01 + * From the Jubilife Pokemon Center to Oreburgh City. + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment01_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Segment01_H + +#include "../PokemonBDSP_AutoStoryTools.h" +#include +#include +#include "../Utils/PokemonBDSP_AutoStory_StoryExecutor.h" +#include "../Utils/PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Segment 01 as a table of checkpoints, segment_01_table(): 01.01 to 01.11. +Pokemon::Segment make_segment_01(StoryStepRunner& runner); + +// How segment_01_table()'s steps that call an old function run on the +// console, by their variants. +std::map segment_01_variants(); + +class AutoStory_Segment_01 : public AutoStory_Segment{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_segment( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0101 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0102 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0103 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0104 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0105 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0106 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0107 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0108 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0109 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0110 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + +class AutoStory_Checkpoint_0111 : public AutoStory_Checkpoint{ +public: + virtual std::string name() const override; + virtual std::string start_text() const override; + virtual std::string end_text() const override; + virtual void run_checkpoint( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + AutoStoryOptions options, + AutoStoryStats& stats + ) const override; +}; + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp new file mode 100644 index 0000000000..883fa21a6c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp @@ -0,0 +1,1024 @@ +/* BDSP AutoStory - Battle Utilities + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include + +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" + +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "CommonTools/OCR/OCR_Routines.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "Pokemon/Inference/Pokemon_NameReader.h" +#include "Pokemon/Pokemon_StatsCalculation.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_ExperienceGainDetector.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" + +#include "VideoGames/PokemonSwSh/Resources/PokemonSwSh_TypeMatchup.h" + +#include "PokemonBDSP_AutoStory_Battle.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + +using namespace Pokemon; +using namespace PokemonSwSh; +using namespace std::chrono_literals; + + +// --------------------------------------------------------------------------- +// run_battle_press_A +// --------------------------------------------------------------------------- + +void run_battle_press_A(const ProgramInfo& /*info*/, VideoStream& stream, ProControllerContext& context){ + EndBattleWatcher end_battle; + run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_A, 120000ms); + }, + {{end_battle}}); + + OverworldWatcher overworld; + run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_A, 120000ms); + }, + {{overworld}}); +} + + +// --------------------------------------------------------------------------- +// handle_unexpected_battle +// --------------------------------------------------------------------------- + +void handle_unexpected_battle(const ProgramInfo& info, + VideoStream& stream, + ProControllerContext& context, + std::function&& action, + std::function&& recovery_action, + size_t max_attempts){ + for (size_t attempt = 0; attempt < max_attempts; attempt++){ + StartBattleDetector start_battle(stream.overlay()); + int result = run_until(stream, + context, + [&](ProControllerContext& ctx){ + action(info, stream, ctx); + }, + {{start_battle}}); + if (result < 0){ + return; + } + run_battle_press_A(info, stream, context); + recovery_action(info, stream, context); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "handle_unexpected_battle: exceeded max attempts", + stream); +} + + +// --------------------------------------------------------------------------- +// Internal helpers for handle_battle_with_outside_help +// --------------------------------------------------------------------------- + +static constexpr int TORTERRA_ATK = 238; +static constexpr int TORTERRA_SPATK = 170; + +// ⚠️ Estimated PP regions (fight menu, 2×2 move grid); require calibration. +static const ImageFloatBox PP_BOX[4] = { + {0.36, 0.590, 0.10, 0.060}, + {0.75, 0.590, 0.10, 0.060}, + {0.36, 0.730, 0.10, 0.060}, + {0.75, 0.730, 0.10, 0.060}, +}; + +static int ocr_pp_slot(Logger& logger, const ImageViewRGB32& frame, const ImageFloatBox& box){ + ImageViewRGB32 region = extract_box_reference(frame, box); + std::string text = OCR::ocr_read(Language::English, region, OCR::PageSegMode::SINGLE_LINE); + text.erase(0, text.find_first_not_of(" \t\r\n")); + if (!text.empty()){ + text.erase(text.find_last_not_of(" \t\r\n") + 1); + } + + size_t slash = text.find('/'); + std::string num_str = (slash != std::string::npos) ? text.substr(0, slash) : text; + size_t digit_pos = num_str.find_first_of("0123456789"); + if (digit_pos == std::string::npos){ + logger.log("PP OCR: no digit in \"" + text + "\"", COLOR_YELLOW); + return -1; + } + try { + return std::stoi(num_str.substr(digit_pos)); + } catch (...){ + logger.log("PP OCR: parse failed \"" + num_str + "\"", COLOR_YELLOW); + return -1; + } +} + +static std::array read_all_pp(Logger& logger, VideoStream& stream){ + VideoSnapshot frame = stream.video().snapshot(); + std::array result; + for (int i = 0; i < 4; i++){ + result[i] = ocr_pp_slot(logger, frame, PP_BOX[i]); + } + return result; +} + +static std::pair damage_range(int level, int base_power, int atk, int def, double type_mult){ + if (base_power == 0){ + return {0, 0}; + } + int base = (((2 * level) / 5 + 2) * base_power * atk / def) / 50 + 2; + return {(int)(base * 85.0 / 100.0 * type_mult), (int)(base * type_mult)}; +} + +struct MoveScore { + int lower_sum = 0; + int upper_sum = 0; + bool ohko_all = false; +}; + +static MoveScore +score_move(const BDSPMoveData& mdata, const std::vector& on_field, const BDSPTrainerData& trainer_data){ + if (mdata.category == MoveCategory::STATUS){ + return {}; + } + + int atk_stat = (mdata.category == MoveCategory::PHYSICAL) ? TORTERRA_ATK : TORTERRA_SPATK; + + MoveScore score; + score.ohko_all = !on_field.empty(); + + for (const std::string& slug : on_field){ + if (slug.empty()){ + score.ohko_all = false; + continue; + } + + const BDSPTrainerPokemon* tp = nullptr; + for (const BDSPTrainer& trainer : trainer_data.trainers){ + for (const BDSPTrainerPokemon& mon : trainer.pokemon){ + if (mon.species == slug){ + tp = &mon; + break; + } + } + if (tp){ + break; + } + } + if (!tp){ + score.ohko_all = false; + continue; + } + + const BDSPPokemonData& pdata = bdsp_pokemon(slug); + uint16_t hp = calc_stats_hp(pdata.hp, tp->level, tp->ivs.hp, tp->evs.hp); + uint16_t def = (mdata.category == MoveCategory::PHYSICAL) + ? calc_stats_nonhp(pdata.defense, tp->level, tp->ivs.defense, tp->evs.defense, NatureAdjustment::NEUTRAL) + : calc_stats_nonhp(pdata.spdef, tp->level, tp->ivs.spdef, tp->evs.spdef, NatureAdjustment::NEUTRAL); + + double mult = type_multiplier(mdata.type, pdata.type[0], pdata.type[1]); + if (mdata.spread && trainer_data.battle_type == BDSPBattleType::DOUBLES){ + mult *= 0.75; + } + + auto [lo, hi] = damage_range(100, (int)mdata.base_power, atk_stat, (int)def, mult); + score.lower_sum += lo; + score.upper_sum += hi; + if (lo < (int)hp){ + score.ohko_all = false; + } + } + return score; +} + +static void navigate_to_slot(ProControllerContext& context, int slot){ + if (slot == 1 || slot == 3){ + pbf_press_dpad(context, DPAD_RIGHT, 10ms, 40ms); + } + if (slot == 2 || slot == 3){ + pbf_press_dpad(context, DPAD_DOWN, 10ms, 40ms); + } +} + + +// --------------------------------------------------------------------------- +// handle_battle_with_outside_help +// --------------------------------------------------------------------------- + +void handle_battle_with_outside_help(const ProgramInfo& /*info*/, + VideoStream& stream, + ProControllerContext& context, + const std::string& trainer_id, + const AutoStoryOptions& options){ + Logger& logger = stream.logger(); + const BDSPTrainerData& trainer_data = bdsp_trainer(trainer_id); + const OutsideHelpConfig& help = options.outside_help; + + std::array move_slugs; + for (int i = 0; i < 4; i++){ + move_slugs[i] = torterra_move_slug(help.attacks[i]); + } + + std::array pp_remaining; + for (int i = 0; i < 4; i++){ + pp_remaining[i] = (int)bdsp_move(move_slugs[i]).pp; + } + + std::vector on_field; + for (const BDSPTrainer& trainer : trainer_data.trainers){ + if (!trainer.pokemon.empty()){ + on_field.push_back(trainer.pokemon[0].species); + } + } + + bool battle_active = true; + while (battle_active){ + + // Wait for the lead Pokemon's battle menu + { + BattleMenuWatcher menu(BattleType::TRAINER); + EndBattleWatcher end; + int r = wait_until(stream, context, 120s, {{menu}, {end}}); + if (r == 1 || r < 0){ + battle_active = false; + break; + } + } + + // Open Fight menu + pbf_press_button(context, BUTTON_A, 10ms, 60ms); + context.wait_for_all_requests(); + context.wait_for(300ms); + + // OCR current PP + { + auto ocr_pp = read_all_pp(logger, stream); + for (int i = 0; i < 4; i++){ + if (ocr_pp[i] >= 0){ + pp_remaining[i] = ocr_pp[i]; + }else{ + logger.log("PP OCR failed slot " + std::to_string(i) + "; using tracked", COLOR_YELLOW); + } + } + } + + // Check for all-zero PP (Struggle) + bool all_zero = true; + for (int pp : pp_remaining){ + if (pp > 0){ + all_zero = false; + break; + } + } + + if (all_zero){ + logger.log("All PP at 0 — using Struggle", COLOR_YELLOW); + pbf_press_button(context, BUTTON_B, 10ms, 60ms); + context.wait_for_all_requests(); + }else{ + // Score all usable moves + int best_slot = -1, best_hi = -1; + bool best_ohko = false; + + for (int i = 0; i < 4; i++){ + if (pp_remaining[i] <= 0){ + continue; + } + MoveScore s = score_move(bdsp_move(move_slugs[i]), on_field, trainer_data); + if (bdsp_move(move_slugs[i]).category == MoveCategory::STATUS){ + continue; + } + + bool prefer = (best_slot < 0) || (s.ohko_all && !best_ohko) || + (s.ohko_all && best_ohko && s.upper_sum < best_hi) || + (!s.ohko_all && !best_ohko && s.upper_sum > best_hi); + + if (prefer){ + best_slot = i; + best_hi = s.upper_sum; + best_ohko = s.ohko_all; + } + } + if (best_slot < 0){ + for (int i = 0; i < 4; i++){ + if (pp_remaining[i] > 0){ + best_slot = i; + break; + } + } + } + + logger.log("Move slot " + std::to_string(best_slot) + " (" + move_slugs[best_slot] + ")" + + (best_ohko ? " [OHKO]" : " [best effort]"), + COLOR_WHITE); + + navigate_to_slot(context, best_slot); + pbf_mash_button(context, BUTTON_A, 500ms); + context.wait_for_all_requests(); + pp_remaining[best_slot]--; + + // Single-target in doubles: confirm default target + if (!bdsp_move(move_slugs[best_slot]).spread && trainer_data.battle_type == BDSPBattleType::DOUBLES){ + pbf_press_button(context, BUTTON_A, 10ms, 60ms); + context.wait_for_all_requests(); + } + + // Mash through target confirmation and partner's move selection + pbf_mash_button(context, BUTTON_A, 3000ms); + context.wait_for_all_requests(); + } + + // Wait for next event + { + BattleMenuWatcher menu2(BattleType::TRAINER); + ExperienceGainWatcher exp; + EndBattleWatcher end; + + int event = wait_until(stream, context, 60s, {{menu2}, {exp}, {end}}); + + if (event == 2 || event < 0){ + battle_active = false; + }else if (event == 1){ + // XP gain — mash through, then update on_field via OCR + BattleMenuWatcher menu3(BattleType::TRAINER); + EndBattleWatcher end2; + int after = run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_A, 120000ms); + }, + {{menu3}, {end2}}); + if (after == 1 || after < 0){ + battle_active = false; + }else{ + // Update on_field via OCR + VideoSnapshot frame = stream.video().snapshot(); + std::vector names = + read_battle_opponent_names(logger, options.language, frame, trainer_data.battle_type); + for (size_t i = 0; i < names.size() && i < on_field.size(); i++){ + if (!names[i].empty() && names[i] != on_field[i]){ + logger.log("Slot " + std::to_string(i) + ": " + on_field[i] + " → " + names[i], + COLOR_WHITE); + on_field[i] = names[i]; + } + } + } + } + // event == 0: next turn, loop continues + } + } + + // Mash through XP/dialog until overworld + OverworldWatcher overworld; + run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_A, 120000ms); + }, + {{overworld}}); +} + + +// --------------------------------------------------------------------------- +// Internal helpers for handle_expert_battle +// --------------------------------------------------------------------------- + +// ⚠️ Estimated player-side HP bar region — calibrate against a real capture. +static const ImageFloatBox PLAYER_HP_BAR{0.057, 0.652, 0.168, 0.008}; + +// ⚠️ Estimated player-side status badge region — calibrate against a real capture. +static const ImageFloatBox PLAYER_STATUS_BOX{0.140, 0.710, 0.080, 0.018}; + +// ⚠️ Estimated battle-bag item name region (highlighted row) — calibrate. +static const ImageFloatBox BATTLE_BAG_ITEM_NAME_BOX{0.470, 0.150, 0.350, 0.050}; + +// Returns the fraction [0.0, 1.0] of the player's HP bar that is filled. +// Scans each pixel column; a column with high colour saturation counts as "HP present". +static double read_player_hp_ratio(const ImageViewRGB32& frame){ + ImageViewRGB32 strip = extract_box_reference(frame, PLAYER_HP_BAR); + int total = (int)strip.width(); + int mid_row = (int)(strip.height() / 2); + if (total <= 0){ + return 1.0; + } + + int colored_cols = 0; + for (int x = 0; x < total; x++){ + uint32_t px = strip.pixel(x, mid_row); + uint8_t r = (px >> 16) & 0xFF; + uint8_t g = (px >> 8) & 0xFF; + uint8_t b = px & 0xFF; + int mx = std::max({(int)r, (int)g, (int)b}); + int mn = std::min({(int)r, (int)g, (int)b}); + if (mx - mn > 30){ + colored_cols++; + } + } + return (double)colored_cols / total; +} + +// Returns true if a coloured status badge is visible on the player's active pokemon. +static bool detect_player_status(const ImageViewRGB32& frame){ + return !is_white(extract_box_reference(frame, PLAYER_STATUS_BOX)); +} + +// Cycle through battle-bag pockets with BUTTON_R until the highlighted item +// matches "full-restore", then use it on the first pokemon. +// Called while the battle-bag is already open (navigate to Bag from battle menu first). +static void use_full_restore_in_open_bag(VideoStream& stream, ProControllerContext& context, Language language){ + Logger& logger = stream.logger(); + static constexpr size_t MAX_POCKETS = 9; + static constexpr size_t MAX_SCROLL = 25; + + // Cycle pockets until we see Full Restore in the highlighted row + bool pocket_found = false; + for (size_t tab = 0; tab < MAX_POCKETS; tab++){ + context.wait_for(500ms); + VideoSnapshot frame = stream.video().snapshot(); + ImageViewRGB32 region = extract_box_reference(frame, BATTLE_BAG_ITEM_NAME_BOX); + OCR::StringMatchResult result = ShopItemNameReader::instance().read_substring(logger, language, region); + for (const auto& [score, match] : result.results){ + if (match.token == "full-restore" && score <= ShopItemNameReader::MAX_LOG10P){ + pocket_found = true; + break; + } + } + if (pocket_found){ + break; + } + pbf_press_button(context, BUTTON_R, 10ms, 400ms); + } + + if (!pocket_found){ + logger.log("Full Restore pocket not found — scanning list", COLOR_YELLOW); + } + + // Scroll down within the pocket until the highlighted row is Full Restore + for (size_t i = 0; i < MAX_SCROLL; i++){ + context.wait_for(400ms); + VideoSnapshot frame = stream.video().snapshot(); + ImageViewRGB32 region = extract_box_reference(frame, BATTLE_BAG_ITEM_NAME_BOX); + OCR::StringMatchResult result = ShopItemNameReader::instance().read_substring(logger, language, region); + bool found = false; + for (const auto& [score, match] : result.results){ + if (match.token == "full-restore" && score <= ShopItemNameReader::MAX_LOG10P){ + found = true; + break; + } + } + if (found){ + pbf_press_button(context, BUTTON_A, 10ms, 600ms); // select item + pbf_press_button(context, BUTTON_A, 10ms, 600ms); // "Use" + pbf_press_button(context, BUTTON_A, 10ms, 800ms); // confirm on first pokemon + context.wait_for(500ms); + logger.log("Full Restore used", COLOR_WHITE); + pbf_mash_button(context, BUTTON_B, 1500ms); + return; + } + pbf_press_dpad(context, DPAD_DOWN, 10ms, 200ms); + } + + logger.log("Full Restore not found in bag — skipping", COLOR_YELLOW); + pbf_mash_button(context, BUTTON_B, 1500ms); +} + +// From the battle menu (Fight/Pokemon/Bag/Run), navigate to Bag and open it. +// In BDSP the battle menu is a 2×2 grid: Fight(top-left) Bag(top-right) +// Pokemon(bottom-left) Run(bottom-right). Cursor starts at Fight. +static void open_battle_bag(ProControllerContext& context){ + pbf_press_dpad(context, DPAD_RIGHT, 10ms, 200ms); + pbf_press_button(context, BUTTON_A, 10ms, 800ms); + context.wait_for(500ms); +} + +// Use a Full Restore on the first party pokemon from the battle menu. +// Returns immediately after item use; the caller is responsible for resuming the turn. +static void use_full_restore_in_battle(VideoStream& stream, ProControllerContext& context, Language language){ + open_battle_bag(context); + use_full_restore_in_open_bag(stream, context, language); + // Mash B to ensure we're back at the battle menu + context.wait_for(300ms); +} + +// OCR the opponent name box and restrict matches to `candidates`. +// Returns the best matching slug in candidates, or the best unrestricted match +// if no candidate qualifies. +static std::string ocr_opponent_restricted(Logger& logger, + Language language, + const ImageViewRGB32& frame, + const ImageFloatBox& box, + const std::vector& candidates){ + using namespace Pokemon; + ImageViewRGB32 cropped = extract_box_reference(frame, box); + OCR::StringMatchResult result = + PokemonNameReader::instance().read_substring(logger, language, cropped, OCR::BLACK_OR_WHITE_TEXT_FILTERS()); + if (result.results.empty()){ + return ""; + } + + // Prefer the best candidate-restricted match + for (const auto& [score, match] : result.results){ + if (score > PokemonNameReader::MAX_LOG10P){ + break; + } + for (const std::string& c : candidates){ + if (match.token == c){ + return c; + } + } + } + // Fallback: best unrestricted match (if it meets threshold) + const auto& best = result.results.begin(); + if (best->first <= PokemonNameReader::MAX_LOG10P){ + return best->second.token; + } + return ""; +} + + +// --------------------------------------------------------------------------- +// Pre-plan computation +// --------------------------------------------------------------------------- + +struct BattlePlan { + // species slug → move slot to use against that pokemon (for current trainer) + std::map move_for_species; + // Estimated remaining PP after the full trainer sequence is played out + std::array estimated_pp_after{}; +}; + +// Returns the Intimidate attack-stage multiplier for `count` Intimidate activations. +// Stage formula: 2 / (2 + count), capped at 6 drops. +static double intimidate_atk_mult(int count){ + if (count <= 0){ + return 1.0; + } + int capped = std::min(count, 6); + return 2.0 / (2.0 + capped); +} + +// Score move `slot` (0-3) against a single opponent pokemon. +// Returns {lower_damage, upper_damage, is_ohko}. +static std::tuple score_move_expert(const BDSPMoveData& mdata, + const BDSPTrainerPokemon& tp, + int effective_atk, + int effective_spatk, + bool doubles){ + if (mdata.category == MoveCategory::STATUS){ + return {0, 0, false}; + } + + const BDSPPokemonData& pdata = bdsp_pokemon(tp.species); + uint16_t hp = calc_stats_hp(pdata.hp, tp.level, tp.ivs.hp, tp.evs.hp); + + int atk_stat = (mdata.category == MoveCategory::PHYSICAL) ? effective_atk : effective_spatk; + uint16_t def = (mdata.category == MoveCategory::PHYSICAL) + ? calc_stats_nonhp(pdata.defense, tp.level, tp.ivs.defense, tp.evs.defense, NatureAdjustment::NEUTRAL) + : calc_stats_nonhp(pdata.spdef, tp.level, tp.ivs.spdef, tp.evs.spdef, NatureAdjustment::NEUTRAL); + + double mult = type_multiplier(mdata.type, pdata.type[0], pdata.type[1]); + if (mdata.spread && doubles){ + mult *= 0.75; + } + + // Attacker level is always 100 (helper Pokemon); defender level is tp.level + auto [lo, hi] = damage_range(100, (int)mdata.base_power, atk_stat, (int)def, mult); + bool ohko = (lo >= (int)hp); + return {lo, hi, ohko}; +} + +static BattlePlan build_expert_plan(Logger& logger, + const std::array& move_slugs, + const std::array& current_pp, + const std::vector& all_trainers, + Trainer current_trainer, + int base_atk, + int base_spatk){ + // Running PP budget across the full sequence + std::array pp = current_pp; + + BattlePlan plan; + plan.estimated_pp_after = pp; + + int intimidate_count = 0; + + // Iterate through all listed trainers in sequence order. + // For current_trainer, populate move_for_species. + for (Trainer t : all_trainers){ + const std::string slug = trainer_slug(t); + const BDSPTrainerData& tdata = bdsp_trainer(slug); + bool is_current = (t == current_trainer); + bool doubles = (tdata.battle_type == BDSPBattleType::DOUBLES); + + for (const BDSPTrainer& side : tdata.trainers){ + // Check first-out pokemon for Intimidate (activates before we attack) + if (!side.pokemon.empty() && side.pokemon[0].has_intimidate){ + intimidate_count++; + } + } + + int eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); + int eff_spatk = base_spatk; // Intimidate doesn't affect SpAtk + + for (const BDSPTrainer& side : tdata.trainers){ + for (const BDSPTrainerPokemon& tp : side.pokemon){ + // Find best move for this pokemon + int best_slot = -1; + int best_hi = -1; + bool best_ohko = false; + + for (int i = 0; i < 4; i++){ + if (pp[i] <= 0){ + continue; + } + const BDSPMoveData& mdata = bdsp_move(move_slugs[i]); + if (mdata.category == MoveCategory::STATUS){ + continue; + } + + auto [lo, hi, ohko] = score_move_expert(mdata, tp, eff_atk, eff_spatk, doubles); + (void)lo; + + // Log adjusted accuracy (informational only, not used in OHKO decisions) + uint8_t acc = mdata.accuracy; + if (acc > 0 && acc < 100){ + int adj = 100 - 2 * (100 - acc); + logger.log(move_slugs[i] + " vs " + tp.species + ": accuracy=" + std::to_string(acc) + + "% (adjusted=" + std::to_string(adj) + "%)", + COLOR_YELLOW); + } + + bool prefer = (best_slot < 0) || (ohko && !best_ohko) // new OHKO > non-OHKO + || (ohko && best_ohko && hi < best_hi) // both OHKO: lower overkill + || (!ohko && !best_ohko && hi > best_hi); // no OHKO: higher damage + + if (prefer){ + best_slot = i; + best_hi = hi; + best_ohko = ohko; + } + } + + if (best_slot < 0){ + // All PP at 0 — Struggle will be used in practice + logger.log("PP exhausted for " + tp.species + " — Struggle expected", COLOR_YELLOW); + }else{ + pp[best_slot]--; + logger.log(std::string(is_current ? "[plan] " : "[pre-plan] ") + tp.species + " → slot " + + std::to_string(best_slot) + " (" + move_slugs[best_slot] + ")" + + (best_ohko ? " [OHKO]" : " [best effort, hi=" + std::to_string(best_hi) + "]"), + COLOR_WHITE); + if (is_current){ + // Only override if this species hasn't been planned yet + // (in case the same species appears twice — first occurrence wins) + if (plan.move_for_species.find(tp.species) == plan.move_for_species.end()){ + plan.move_for_species[tp.species] = best_slot; + } + } + } + + // Track Intimidate for subsequent pokemon (after the initial send-out) + if (tp.has_intimidate){ + intimidate_count++; + eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); + } + } + } + } + + plan.estimated_pp_after = pp; + return plan; +} + + +// --------------------------------------------------------------------------- +// handle_expert_battle +// --------------------------------------------------------------------------- + +// Snapshot the current frame, OCR on-field names, and update `on_field`. +static void update_on_field(Logger& logger, + Language language, + VideoStream& stream, + BDSPBattleType battle_type, + const std::vector& candidates, + std::vector& on_field){ + VideoSnapshot frame = stream.video().snapshot(); + if (battle_type == BDSPBattleType::SINGLES){ + static const ImageFloatBox BOX(0.505, 0.010, 0.420, 0.085); + std::string name = ocr_opponent_restricted(logger, language, frame, BOX, candidates); + if (!name.empty() && !on_field.empty()){ + on_field[0] = name; + } + }else{ + static const ImageFloatBox BOX_L(0.505, 0.010, 0.200, 0.085); + static const ImageFloatBox BOX_R(0.710, 0.010, 0.260, 0.085); + std::string l = ocr_opponent_restricted(logger, language, frame, BOX_L, candidates); + std::string r = ocr_opponent_restricted(logger, language, frame, BOX_R, candidates); + if (!l.empty() && on_field.size() > 0){ + on_field[0] = l; + } + if (!r.empty() && on_field.size() > 1){ + on_field[1] = r; + } + } +} + + +void handle_expert_battle(const ProgramInfo& /*info*/, + VideoStream& stream, + ProControllerContext& context, + const std::vector& all_trainers, + Trainer current_trainer, + const AutoStoryOptions& options){ + using namespace Pokemon; + + Logger& logger = stream.logger(); + const std::string current_slug = trainer_slug(current_trainer); + const BDSPTrainerData& trainer_data = bdsp_trainer(current_slug); + const OutsideHelpConfig& help = options.outside_help; + bool doubles = (trainer_data.battle_type == BDSPBattleType::DOUBLES); + + // Move slugs for the helper's 4 moves + std::array move_slugs; + for (int i = 0; i < 4; i++){ + move_slugs[i] = torterra_move_slug(help.attacks[i]); + } + + // Helper stats — 0IV/0EV at level 100, neutral nature + const std::string helper_base_slug = "torterra"; + const BDSPPokemonData& helper_base = bdsp_pokemon(helper_base_slug); + int base_atk = (int)calc_stats_nonhp(helper_base.attack, 100, 0, 0, NatureAdjustment::NEUTRAL); + int base_spatk = (int)calc_stats_nonhp(helper_base.spatk, 100, 0, 0, NatureAdjustment::NEUTRAL); + + // Candidate species slugs for restricted OCR (all pokemon in current trainer's party) + std::vector candidates; + for (const BDSPTrainer& side : trainer_data.trainers){ + for (const BDSPTrainerPokemon& tp : side.pokemon){ + candidates.push_back(tp.species); + } + } + + // PP tracking — initialised to max PP from JSON; OCR updates each turn + std::array pp_remaining; + for (int i = 0; i < 4; i++){ + pp_remaining[i] = (int)bdsp_move(move_slugs[i]).pp; + } + + // Intimidate tracking across turns + int intimidate_count = 0; + std::set intimidate_seen; + + // Current on-field opponents (initialised to first pokemon of each side) + std::vector on_field; + for (const BDSPTrainer& side : trainer_data.trainers){ + on_field.push_back(side.pokemon.empty() ? "" : side.pokemon[0].species); + } + + // plan is built on the first turn after PP are OCR'd + BattlePlan plan; + bool plan_built = false; + + // ── Wait for the initial battle menu ───────────────────────────────────── + bool battle_active = true; + { + BattleMenuWatcher menu(BattleType::TRAINER); + EndBattleWatcher end; + int r = wait_until(stream, context, 60s, {{menu}, {end}}); + if (r != 0 || r < 0){ + battle_active = false; + } + } + + // ── Main battle loop ────────────────────────────────────────────────────── + // Each iteration starts with the battle menu (Fight/Pokemon/Bag/Run) visible. + // We check HP/status, open the fight menu, OCR PP, identify the opponent, + // select a move, then wait for the next event. + while (battle_active){ + + // 1. Check HP / status → use Full Restore if needed + { + VideoSnapshot frame = stream.video().snapshot(); + double hp = read_player_hp_ratio(frame); + bool has_status = detect_player_status(frame); + if (hp < 0.33 || has_status){ + logger.log("Full Restore needed (HP=" + std::to_string((int)(hp * 100)) + + "%, status=" + (has_status ? "yes" : "no") + ")", + COLOR_WHITE); + use_full_restore_in_battle(stream, context, options.language); + } + } + + // 2. Open fight menu and re-read PP + pbf_press_button(context, BUTTON_A, 10ms, 60ms); + context.wait_for_all_requests(); + context.wait_for(300ms); + { + auto ocr_pp = read_all_pp(logger, stream); + for (int i = 0; i < 4; i++){ + if (ocr_pp[i] >= 0){ + pp_remaining[i] = ocr_pp[i]; + }else{ + logger.log("PP OCR failed slot " + std::to_string(i), COLOR_YELLOW); + } + } + } + + // 3. Build plan on first turn (after we have real PP values from OCR) + if (!plan_built){ + plan = build_expert_plan(logger, + move_slugs, + pp_remaining, + all_trainers, + current_trainer, + base_atk, + base_spatk); + plan_built = true; + } + + // 4. Identify current on-field opponent(s) via restricted OCR + // (Fight menu is open at bottom; opponent info boxes are at the top — still readable) + update_on_field(logger, options.language, stream, trainer_data.battle_type, candidates, on_field); + + // 5. Track Intimidate from newly-observed opponents + { + int eff_atk = base_atk; + for (const std::string& slug : on_field){ + if (slug.empty() || intimidate_seen.count(slug)){ + continue; + } + for (const BDSPTrainer& side : trainer_data.trainers){ + for (const BDSPTrainerPokemon& tp : side.pokemon){ + if (tp.species == slug && tp.has_intimidate){ + intimidate_count++; + intimidate_seen.insert(slug); + eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); + logger.log(slug + " has Intimidate — ATK stage -" + std::to_string(intimidate_count) + + " (effective ATK=" + std::to_string(eff_atk) + ")", + COLOR_YELLOW); + } + } + } + } + } + + // 6. Select move — prefer pre-plan; fall back to live scoring if plan misses + int best_slot = -1; + if (!on_field.empty() && !on_field[0].empty()){ + auto it = plan.move_for_species.find(on_field[0]); + if (it != plan.move_for_species.end() && pp_remaining[it->second] > 0){ + best_slot = it->second; + } + } + + if (best_slot < 0){ + logger.log("Plan miss for " + (on_field.empty() ? "?" : on_field[0]) + " — live scoring", COLOR_YELLOW); + int eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); + int eff_spatk = base_spatk; + int best_hi = -1; + bool best_ohko = false; + + for (int i = 0; i < 4; i++){ + if (pp_remaining[i] <= 0){ + continue; + } + const BDSPMoveData& mdata = bdsp_move(move_slugs[i]); + if (mdata.category == MoveCategory::STATUS){ + continue; + } + + int hi = 0; + bool ohko = false; + if (!on_field.empty() && !on_field[0].empty()){ + for (const BDSPTrainer& side : trainer_data.trainers){ + for (const BDSPTrainerPokemon& tp : side.pokemon){ + if (tp.species != on_field[0]){ + continue; + } + auto [lo, h, ok] = score_move_expert(mdata, tp, eff_atk, eff_spatk, doubles); + hi = h; + ohko = ok; + } + } + } + + bool prefer = (best_slot < 0) || (ohko && !best_ohko) || (ohko && best_ohko && hi < best_hi) || + (!ohko && !best_ohko && hi > best_hi); + + if (prefer){ + best_slot = i; + best_hi = hi; + best_ohko = ohko; + } + } + // Last resort: first move with any PP remaining + if (best_slot < 0){ + for (int i = 0; i < 4; i++){ + if (pp_remaining[i] > 0){ + best_slot = i; + break; + } + } + } + } + + // 7. Execute selected move (or Struggle if all PP are gone) + if (best_slot < 0){ + logger.log("All PP at 0 — Struggle", COLOR_YELLOW); + pbf_press_button(context, BUTTON_B, 10ms, 60ms); // close fight menu + context.wait_for_all_requests(); + }else{ + logger.log("Move slot " + std::to_string(best_slot) + " (" + move_slugs[best_slot] + ") vs " + + (on_field.empty() ? "?" : on_field[0]), + COLOR_WHITE); + navigate_to_slot(context, best_slot); + pbf_mash_button(context, BUTTON_A, 500ms); + context.wait_for_all_requests(); + pp_remaining[best_slot]--; + + // Single-target move in doubles: confirm default target + if (!bdsp_move(move_slugs[best_slot]).spread && doubles){ + pbf_press_button(context, BUTTON_A, 10ms, 60ms); + context.wait_for_all_requests(); + } + // Mash through target confirm and partner's move selection + pbf_mash_button(context, BUTTON_A, 3000ms); + context.wait_for_all_requests(); + } + + // 8. Wait for next event: next battle menu, XP gain, or battle end + { + BattleMenuWatcher menu2(BattleType::TRAINER); + ExperienceGainWatcher exp; + EndBattleWatcher end; + + int event = wait_until(stream, context, 60s, {{menu2}, {exp}, {end}}); + + if (event == 2 || event < 0){ + // Battle over + break; + }else if (event == 1){ + // XP gain — mash until the NEXT battle menu appears (or battle ends) + BattleMenuWatcher menu3(BattleType::TRAINER); + EndBattleWatcher end2; + int after = run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_A, 120000ms); + }, + {{menu3}, {end2}}); + if (after != 0){ + // Battle ended during XP screens + break; + } + // Update on-field now that the next pokemon is visible + update_on_field(logger, options.language, stream, trainer_data.battle_type, candidates, on_field); + for (size_t i = 0; i < on_field.size(); i++){ + logger.log("On-field[" + std::to_string(i) + "] = " + on_field[i], COLOR_WHITE); + } + // Continue loop — we're now at the next battle menu + } + // event == 0: next battle menu appeared (mid-turn switch or next turn) + // Loop continues from the top, which is the battle menu state + } + } + + // Mash through any remaining XP/dialog until we're back in the overworld + OverworldWatcher overworld; + run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_A, 120000ms); + }, + {{overworld}}); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h new file mode 100644 index 0000000000..1ab3a67b78 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h @@ -0,0 +1,111 @@ +/* BDSP AutoStory - Battle Utilities + * + * From: https://github.com/PokemonAutomation/ + * + * Helpers for handling trainer battles and unexpected wild encounters. + * These wrap existing BDSP battle detectors: + * - BattleMenuDetector / BattleMenuWatcher (PokemonBDSP_BattleMenuDetector.h) + * - EndBattleWatcher (PokemonBDSP_EndBattleDetector.h) + * - StartBattleDetector (PokemonBDSP_StartBattleDetector.h) + * - ExperienceGainWatcher (PokemonBDSP_ExperienceGainDetector.h) + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Battle_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Battle_H + +#include +#include +#include + +#include "CommonFramework/Tools/VideoStream.h" + +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" + +namespace PokemonAutomation { +struct ProgramInfo; +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// --------------------------------------------------------------------------- +// Scripted (expected) battles +// --------------------------------------------------------------------------- + +// Mash A through a battle (trainer or scripted wild) until EndBattleWatcher fires. +// This does not attempt to catch — it simply presses A to select the first move +// repeatedly until the battle ends. +// After the black-screen transition and experience gain, returns to the caller. +void run_battle_press_A(const ProgramInfo& info, VideoStream& stream, ProControllerContext& context); + + +// --------------------------------------------------------------------------- +// Unexpected battle handling +// --------------------------------------------------------------------------- + +// Run `action`. If a StartBattleDetector fires during `action`, the action +// is aborted, the battle is resolved with run_battle_press_A(), and then +// `recovery_action` is called to restore the player's position before +// retrying `action`. +// The whole sequence retries up to `max_attempts` times. +// Throws OperationFailedException if max_attempts is exceeded. +void handle_unexpected_battle(const ProgramInfo& info, + VideoStream& stream, + ProControllerContext& context, + std::function&& action, + std::function&& recovery_action, + size_t max_attempts = 5); + + +// --------------------------------------------------------------------------- +// Outside-help battle handler +// --------------------------------------------------------------------------- + +// Handle a scripted trainer battle where the configured human helper joins. +// Each turn it: +// 1. OCRs current PP for all 4 configured moves. +// 2. Calculates damage against all on-field opponents. +// 3. Navigates to and selects the optimal move (guaranteed OHKO if possible; +// highest total damage otherwise). +// 4. Mashes A to cover target selection and partner's move. +// 5. After XP gain, updates on-field opponents via OCR. +// Returns when the battle ends and the player is back in the overworld. +void handle_battle_with_outside_help(const ProgramInfo& info, + VideoStream& stream, + ProControllerContext& context, + const std::string& trainer_id, + const AutoStoryOptions& options); + + +// --------------------------------------------------------------------------- +// Expert battle handler (sequence-aware, per-turn status/HP check) +// --------------------------------------------------------------------------- + +// Handle one scripted trainer battle with intelligent per-turn move selection. +// +// `all_trainers` — the full upcoming sequence of trainer battles. Used to +// pre-plan PP budget so moves are allocated efficiently across the sequence. +// `current_trainer` — the trainer being fought in this call. +// +// Each turn the function: +// 1. Checks player HP (<33%) and status condition; uses a Full Restore if needed. +// 2. OCRs current PP for all 4 configured moves. +// 3. Identifies the current opponent via restricted-dictionary OCR. +// 4. Tracks Intimidate activations and reduces effective ATK accordingly. +// 5. Selects the pre-planned optimal move and executes it. +// 6. After XP gain, OCRs the new opponent and loops. +// Returns when the battle ends and the player is back in the overworld. +void handle_expert_battle(const ProgramInfo& info, + VideoStream& stream, + ProControllerContext& context, + const std::vector& all_trainers, + Trainer current_trainer, + const AutoStoryOptions& options); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp new file mode 100644 index 0000000000..5d1fde2c18 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp @@ -0,0 +1,44 @@ +/* BDSP AutoStory - Dialog Utilities + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Exceptions/OperationFailedException.h" + +#include "CommonTools/Async/InferenceRoutines.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" + +#include "PokemonBDSP_AutoStory_Dialog.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +void clear_dialog(VideoStream& stream, ProControllerContext& context, ClearDialogMode mode, uint16_t timeout_sec){ + // TODO: implement + // - use ShortDialogWatcher + (optionally) ShortDialogPromptDetector + // + OverworldDetector (from Detect/) with run_until<> + // - loop mashing A until stop condition fires + (void)stream; + (void)context; + (void)mode; + (void)timeout_sec; +} + +void wait_for_dialog(VideoStream& stream, ProControllerContext& context, uint16_t timeout_sec){ + // TODO: implement + // - use wait_until() with ShortDialogWatcher + (void)stream; + (void)context; + (void)timeout_sec; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h new file mode 100644 index 0000000000..50fc7d7204 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h @@ -0,0 +1,61 @@ +/* BDSP AutoStory - Dialog Utilities + * + * From: https://github.com/PokemonAutomation/ + * + * Helpers for clearing NPC dialog and waiting for dialog to appear. + * These wrap the existing BDSP dialog detectors: + * - ShortDialogDetector / ShortDialogWatcher (PokemonBDSP_DialogDetector.h) + * - ShortDialogPromptDetector (PokemonBDSP_DialogDetector.h) + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Dialog_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Dialog_H + +#include + +#include "CommonFramework/Tools/VideoStream.h" + +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation { +struct ProgramInfo; +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// --------------------------------------------------------------------------- +// Stop conditions for clear_dialog() +// --------------------------------------------------------------------------- + +enum class ClearDialogMode { + // Stop once the dialog has closed and the overworld is detected. + STOP_OVERWORLD, + + // Stop when a YES/NO prompt (ShortDialogPromptDetector) is detected. + STOP_PROMPT, + + // Run for exactly `timeout_sec` seconds regardless of screen state. + STOP_TIMEOUT, +}; + + +// --------------------------------------------------------------------------- +// Dialog clearing +// --------------------------------------------------------------------------- + +// Mash A through NPC dialog until `mode` stop condition is met. +// Uses ShortDialogWatcher as the primary "still in dialog" signal. +// Throws OperationFailedException if no dialog is ever detected, or if +// the stop condition is not met within `timeout_sec` seconds. +void clear_dialog(VideoStream& stream, ProControllerContext& context, ClearDialogMode mode, uint16_t timeout_sec = 60); + +// Block until a short dialog box appears, then return. +// Throws OperationFailedException if no dialog appears within `timeout_sec`. +void wait_for_dialog(VideoStream& stream, ProControllerContext& context, uint16_t timeout_sec = 30); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp new file mode 100644 index 0000000000..ecf90056c7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -0,0 +1,135 @@ +/* BDSP AutoStory - Location + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/Language.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "PokemonBDSP_AutoStory_Location.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// The location banner: a white box at the top left that names a map for a +// few seconds after the player enters it. Left of the centred name it is plain +// white (251 on console B, against 27-75 for the trees there otherwise). +const ImageFloatBox LOCATION_BANNER_WHITE{0.035, 0.035, 0.085, 0.050}; +const ImageFloatBox LOCATION_BANNER_TEXT{0.100, 0.025, 0.300, 0.080}; + +// The save card's header: the map's name in salmon (255, 152, 140) on near +// white (245), top left, right of the card's red border. Too faint for OCR +// as it is: the reddish pixels are made black on white first. +const ImageFloatBox SAVE_SCREEN_LOCATION{0.036, 0.045, 0.274, 0.070}; +const uint32_t SAVE_SCREEN_TEXT_MIN = 0xff960000; +const uint32_t SAVE_SCREEN_TEXT_MAX = 0xffffa9a9; + +std::string digits_of(const std::string& text){ + std::string out; + for (char ch : text){ + if (std::isdigit((unsigned char)ch)){ + out.push_back(ch); + } + } + return out; +} + +} + + +std::string normalize_location_name(const std::string& text){ + std::string out; + for (unsigned char ch : text){ + if (std::isalnum(ch)){ + out.push_back((char)std::tolower(ch)); + } + } + return out; +} + +Language g_story_language = Language::English; + +bool story_reads_text(){ + return story_reads_text(g_story_language); +} + +bool story_reads_text(Language language){ + return language == Language::English; +} + +bool location_banner_showing(const ImageViewRGB32& screen){ + const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); + return white.average.sum() >= 690 && white.stddev.sum() <= 30; +} + +std::string location_banner_stats(const ImageViewRGB32& screen){ + const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); + return "mean " + std::to_string((int)white.average.sum()) + ", spread " + std::to_string((int)white.stddev.sum()); +} + +std::string read_location_banner(const ImageViewRGB32& screen){ + if (!location_banner_showing(screen)){ + return ""; + } + if (!story_reads_text()){ + return UNREAD_LOCATION; + } + return normalize_location_name(OCR::tesseract_ocr_read(Language::English, + extract_box_reference(screen, LOCATION_BANNER_TEXT))); +} + +std::string read_save_screen_location(const ImageViewRGB32& screen){ + if (!story_reads_text()){ + return UNREAD_LOCATION; + } + const ImageRGB32 text = to_blackwhite_rgb32_range( + extract_box_reference(screen, SAVE_SCREEN_LOCATION), true, SAVE_SCREEN_TEXT_MIN, SAVE_SCREEN_TEXT_MAX); + return normalize_location_name(OCR::tesseract_ocr_read(Language::English, text)); +} + +bool location_banner_matches(const std::string& read, const std::string& expected){ + // No reader for the game's language: a banner (or the save card) that + // showed is all there is; where we stand is the walk's to say. + if (read == UNREAD_LOCATION){ + return true; + } + const std::string a = normalize_location_name(read); + const std::string b = normalize_location_name(expected); + if (a.empty() || b.empty()){ + return false; + } + if (digits_of(a) != digits_of(b)){ + return false; + } + // Levenshtein distance. + std::vector row(b.size() + 1); + for (size_t j = 0; j <= b.size(); j++){ + row[j] = j; + } + for (size_t i = 1; i <= a.size(); i++){ + size_t diagonal = row[0]; + row[0] = i; + for (size_t j = 1; j <= b.size(); j++){ + const size_t above = row[j]; + row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] == b[j - 1] ? 0 : 1)}); + diagonal = above; + } + } + return row[b.size()] <= std::max(1, b.size() / 4); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h new file mode 100644 index 0000000000..4294ede6eb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h @@ -0,0 +1,65 @@ +/* BDSP AutoStory - Location + * + * From: https://github.com/PokemonAutomation/ + * + * Where the game says the player is: the location banner shown on entering + * a map, and the map named on the save screen's card. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Location_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Location_H + +#include +#include "CommonFramework/Language.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Lower-case letters and digits only: "Route 202" and "ROUTE 2O2 " compare +// by "route202". +std::string normalize_location_name(const std::string& text); + +// The game's language, for the reads that still use text (item names, the +// location banner, a sign's or talk's first box), set from the program's +// options (StoryRunOptions). story_reads_text() says whether those reads +// have a model and dictionary for it (English only, so far); when not, +// callers use language-free checks: the banner's box showing, a sign's box +// opening, the first row of a Bag pocket. +extern Language g_story_language; +bool story_reads_text(); +bool story_reads_text(Language language); + +// What the reads below return for a banner or save card that showed, when +// there is no text reader for the game's language (story_reads_text()): +// location_banner_matches() takes it as a match. +inline const std::string UNREAD_LOCATION = "(unread)"; + +// The location banner (the white box at the top left that names a map for a +// few seconds after the player enters it), read by OCR and normalized; +// empty when no banner shows. +std::string read_location_banner(const ImageViewRGB32& screen); +// Whether the location banner's white box shows, without reading it. +bool location_banner_showing(const ImageViewRGB32& screen); +// The banner box's mean and spread, for the log. +std::string location_banner_stats(const ImageViewRGB32& screen); + +// The map named at the top left of the save screen's card ("Sandgem Town", +// "Route 202"), read by OCR and normalized; empty if nothing reads. +std::string read_save_screen_location(const ImageViewRGB32& screen); + +// Whether the location banner's OCR `read` names the map `expected`. Both +// are compared as lower-case letters and digits. OCR drops or swaps letters +// ("andgemltown" for Sandgem Town on r109), so up to a quarter of the name's +// letters may differ; the digits must match exactly, so Route 201 is never +// read as Route 202. +bool location_banner_matches(const std::string& read, const std::string& expected); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp new file mode 100644 index 0000000000..28a871f34a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp @@ -0,0 +1,57 @@ +/* BDSP AutoStory - Navigation Utilities + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Exceptions/OperationFailedException.h" + +#include "CommonTools/Async/InferenceRoutines.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" + +#include "PokemonBDSP_AutoStory_Navigation.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +void walk_steps(ProControllerContext& context, uint16_t direction, uint16_t steps){ + // TODO: implement — press D-pad `direction` for `steps` tiles + (void)context; + (void)direction; + (void)steps; +} + +void walk_until_dialog(VideoStream& stream, ProControllerContext& context, uint16_t direction, uint16_t timeout_sec){ + // TODO: implement — walk in direction; stop when ShortDialogWatcher fires + (void)stream; + (void)context; + (void)direction; + (void)timeout_sec; +} + +void walk_until_battle(VideoStream& stream, ProControllerContext& context, uint16_t direction, uint16_t timeout_sec){ + // TODO: implement — walk in direction; stop when StartBattleDetector fires + (void)stream; + (void)context; + (void)direction; + (void)timeout_sec; +} + +void recalibrate_after_battle(const ProgramInfo& /*info*/, + VideoStream& /*stream*/, + ProControllerContext& context, + std::function&& recovery_action){ + // TODO: implement — wait for overworld, then run recovery_action + recovery_action(context); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h new file mode 100644 index 0000000000..55bdab77f9 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h @@ -0,0 +1,78 @@ +/* BDSP AutoStory - Navigation Utilities + * + * From: https://github.com/PokemonAutomation/ + * + * Helpers for moving the player character in the BDSP overworld. + * BDSP uses a tile-based grid, so most movement is fixed D-pad sequences. + * Detection-based stop conditions and post-battle recalibration are also here. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Navigation_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Navigation_H + +#include + +#include "CommonFramework/Tools/VideoStream.h" + +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation { +struct ProgramInfo; +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// --------------------------------------------------------------------------- +// Fixed-sequence walking (no detection — pure tile counting) +// --------------------------------------------------------------------------- + +// Walk `steps` tiles in the given D-pad direction. +// direction must be one of DPAD_UP / DPAD_DOWN / DPAD_LEFT / DPAD_RIGHT. +void walk_steps(ProControllerContext& context, uint16_t direction, uint16_t steps); + + +// --------------------------------------------------------------------------- +// Detection-guided walking (walk until a screen state is detected) +// --------------------------------------------------------------------------- + +// Walk in `direction` until a short dialog box is detected, or `timeout_sec` elapses. +// Throws OperationFailedException on timeout. +void walk_until_dialog(VideoStream& stream, + ProControllerContext& context, + uint16_t direction, + uint16_t timeout_sec = 30); + +// Walk in `direction` until a battle starts (StartBattleDetector fires), +// or `timeout_sec` elapses. +// Throws OperationFailedException on timeout. +void walk_until_battle(VideoStream& stream, + ProControllerContext& context, + uint16_t direction, + uint16_t timeout_sec = 30); + + +// --------------------------------------------------------------------------- +// Post-battle recalibration +// +// BDSP movement is deterministic on a grid, but a random encounter shifts +// the player's position unpredictably (post-battle exit tile varies). +// These functions restore the player to a known reference tile by navigating +// to a fixed landmark (e.g. a building entrance). +// --------------------------------------------------------------------------- + +// After an unexpected battle, navigate to the nearest known landmark and +// re-establish a known tile position. The exact recovery sequence depends +// on the calling segment and is passed as a lambda. +// `recovery_action` should move the player from wherever they ended up back +// to a defined starting tile for the current checkpoint. +void recalibrate_after_battle(const ProgramInfo& info, + VideoStream& stream, + ProControllerContext& context, + std::function&& recovery_action); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp new file mode 100644 index 0000000000..ac311dcba9 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp @@ -0,0 +1,286 @@ +/* BDSP AutoStory - New Game + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/ImageTools/ImageDiff.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/ScreenMachine/ScreenMachine.h" +#include "CommonTools/ScreenMachine/StepTrace.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Inference/NintendoSwitch_HomeMenuDetector.h" +#include "NintendoSwitch/Inference/NintendoSwitch_StartGameUserSelectDetector.h" +#include "NintendoSwitch/Inference/NintendoSwitch_SystemKeyboardDetector.h" +#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_NameListDetector.h" +#include "PokemonBDSP_AutoStory_NewGame.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +std::string classify_intro_screen(ConsoleHandle* console, const ImageViewRGB32& screen){ + // The Switch's own screens come first: BDSP's dialogue detector also fires on them. + if (console != nullptr && StartGameUserSelectDetector(*console).detect(screen)){ + return "user-select"; + } + if (console != nullptr && HomeMenuDetector(*console, COLOR_RED).detect(screen)){ + return "home"; + } + if (SystemKeyboardDetector().detect(screen)){ + return "keyboard"; + } + switch (NameListDetector().selected_row(screen)){ + case -1: + break; + case 0: + return "rival-list-tell"; + default: + return "rival-list-preset"; + } + if (MenuDetector().detect(screen)){ + return "menu"; + } + // Includes the appearance grid, whose prompt sits in a dialogue box. + if (ShortDialogDetector().detect(screen)){ + return "dialog"; + } + // Language, cutscenes, fades, the bedroom with no menu open. + return "other"; +} + +IntroStep decide_intro_step(const std::string& screen, const IntroProgress& progress){ + if (screen == "user-select") return IntroStep::SELECT_USER; + if (screen == "home") return IntroStep::FAIL_NOT_IN_GAME; + if (screen == "keyboard"){ + return progress.player_named ? IntroStep::CANCEL_RIVAL_KEYBOARD : IntroStep::ENTER_PLAYER_NAME; + } + if (screen == "rival-list-tell") return IntroStep::MOVE_TO_PRESET; + if (screen == "rival-list-preset") return IntroStep::PICK_PRESET; + if (screen == "menu") return IntroStep::CLOSE_MENU_AND_FINISH; + if (screen == "dialog") return IntroStep::ADVANCE; + // The bedroom comes only after the rival is named. Before that, every unknown + // screen (language, the appearance preview, cutscenes) takes A. + return progress.rival_named ? IntroStep::PROBE_FOR_MENU : IntroStep::ADVANCE; +} +bool advance_after_probe(bool screen_changed){ + return !screen_changed; +} + +size_t run_intro_screen_tests(Logger& logger, const std::string& folder){ + size_t passed = 0; + size_t failed = 0; + for (const Filesystem::Path& dir : Filesystem::list_directory(Filesystem::Path(folder))){ + if (!Filesystem::is_directory(dir)){ + continue; + } + const std::string expected = dir.filename().string(); + for (const Filesystem::Path& file : Filesystem::list_directory(dir)){ + if (file.extension().string() != ".png"){ + continue; + } + const ImageRGB32 image(file.string()); + // 1080p and 720p. At 960x540 the keyboard's and the name list's checks + // do not hold (blurred key text, an arrow below the object size limit). + for (size_t width : {size_t(1920), size_t(1280)}){ + const std::string actual = classify_intro_screen(nullptr, image.scale_to(width, width * 9 / 16)); + if (actual == expected){ + passed++; + continue; + } + failed++; + logger.log("[Intro tests] FAIL " + expected + "/" + file.filename().string() + + " at " + std::to_string(width) + "px: read as " + actual, COLOR_RED); + } + } + } + logger.log("[Intro tests] " + std::to_string(passed) + " passed, " + std::to_string(failed) + " failed.", + failed == 0 ? COLOR_BLUE : COLOR_RED); + return failed; +} + + +namespace{ + +// Waits until the keyboard is gone (or, with `gone` false, has appeared). +bool wait_for_keyboard(VideoStream& stream, ProControllerContext& context, bool gone, Milliseconds timeout){ + context.wait_for_all_requests(); + const WallClock deadline = current_time() + timeout; + while (current_time() < deadline){ + VideoSnapshot screen = stream.video().snapshot(); + if (screen && SystemKeyboardDetector().detect(screen) != gone){ + return true; + } + context.wait_for(100ms); + } + return false; +} + +void enter_player_name(VideoStream& stream, ProControllerContext& context, StepTrace& trace, const std::string& name){ + // Let the keyboard finish sliding in: inputs during that are dropped. + pbf_wait(context, 800ms); + context.wait_for_all_requests(); + // B deletes a character, and closes the keyboard on an empty field, so delete only + // while there is text (e.g. after a retry). + for (size_t c = 0; c < 14; c++){ + VideoSnapshot screen = stream.video().snapshot(); + if (!screen || !system_keyboard_field_has_text(screen)){ + break; + } + trace.action("Name field holds text; deleting a character."); + pbf_press_button(context, BUTTON_B, 60ms, 250ms); + context.wait_for_all_requests(); + } + trace.action("Typing the player's name: " + name); + type_on_system_keyboard(stream, context, name); + pbf_press_button(context, BUTTON_PLUS, 80ms, 0ms); + if (!wait_for_keyboard(stream, context, true, 5s)){ + trace.fail("The keyboard stayed open after PLUS (OK)."); + } + trace.action("Name confirmed."); +} + +} + + + +void play_new_game_intro( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::string& player_name +){ + VideoStream& stream = env.console; + StepTrace trace(stream, "Intro"); + + // Only close and reopen the game: the states below take it from the user select + // on. reset_game_from_home() waits for a loaded save's black screen to end, and a + // new game's first screen is a dialog on black, so it would sit there until its + // timeout. + trace.action("Restarting the game."); + go_home(env.console, context); + from_home_close_and_reopen_game(env.console, context, true); + + IntroProgress progress; + size_t x_without_menu = 0; + auto press = [&](Button button){ + pbf_press_button(context, button, 80ms, 0ms); + }; + + // Carries out what decide_intro_step() chose for `screen`. + auto act = [&](const char* screen){ + switch (decide_intro_step(screen, progress)){ + case IntroStep::SELECT_USER: + // The cursor starts on the user that launched the game. + press(BUTTON_A); + pbf_wait(context, 1000ms); + return; + case IntroStep::FAIL_NOT_IN_GAME: + trace.fail("The Switch HOME menu is showing: the game is not running in front."); + case IntroStep::ENTER_PLAYER_NAME: + enter_player_name(stream, context, trace, player_name); + progress.player_named = true; + return; + case IntroStep::CANCEL_RIVAL_KEYBOARD: + // Opened from "I'll tell you..."; inputs during the slide-in are dropped. + pbf_wait(context, 800ms); + press(BUTTON_X); + return; + case IntroStep::MOVE_TO_PRESET: + pbf_press_dpad(context, DPAD_DOWN, 100ms, 300ms); + return; + case IntroStep::PICK_PRESET: + press(BUTTON_A); + pbf_wait(context, 500ms); + progress.rival_named = true; + return; + case IntroStep::CLOSE_MENU_AND_FINISH:{ + trace.action("Main menu is open: the player is in control in the bedroom. Closing it."); + press(BUTTON_B); + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + VideoSnapshot screen_now = stream.video().snapshot(); + if (!screen_now || MenuDetector().detect(screen_now)){ + trace.fail("The main menu did not close."); + } + return; + } + case IntroStep::ADVANCE: + pbf_press_button(context, BUTTON_A, 60ms, 400ms); + return; + case IntroStep::PROBE_FOR_MENU:{ + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + press(BUTTON_X); + context.wait_for_all_requests(); + MenuWatcher menu; + if (wait_until(stream, context, 3s, {{menu}}) == 0){ + return; // The next read sees the menu and finishes. + } + VideoSnapshot after = stream.video().snapshot(); + const bool changed = !before || !after || ImageMatch::pixel_RMSD(before, after) > 10; + if (!advance_after_probe(changed)){ + trace.action("X changed the screen but no known menu opened; not pressing A."); + if (++x_without_menu >= 10){ + trace.fail("X keeps changing the screen without a known menu opening."); + } + return; + } + x_without_menu = 0; + pbf_press_button(context, BUTTON_A, 60ms, 400ms); + return; + } + } + }; + + auto screen_is = [&](const ImageViewRGB32& screen, const char* name){ + return classify_intro_screen(&env.console, screen) == name; + }; + // The machine's states: the screens classify_intro_screen() tells apart, with the + // keyboard split by whose name it asks for. Each state's step is act(). + const std::vector states{ + {"user-select", + [&](const ImageViewRGB32& s){ return screen_is(s, "user-select"); }, + [&]{ act("user-select"); }, {}, 3}, + {"home", + [&](const ImageViewRGB32& s){ return screen_is(s, "home"); }, + [&]{ act("home"); }, {}, 1}, + {"player-keyboard", + [&](const ImageViewRGB32& s){ return !progress.player_named && screen_is(s, "keyboard"); }, + [&]{ act("keyboard"); }, {"dialog", "other"}, 1}, + {"rival-keyboard", + [&](const ImageViewRGB32& s){ return progress.player_named && screen_is(s, "keyboard"); }, + [&]{ act("keyboard"); }, {"rival-list-tell", "dialog", "other"}, 3}, + {"rival-list-tell", + [&](const ImageViewRGB32& s){ return screen_is(s, "rival-list-tell"); }, + [&]{ act("rival-list-tell"); }, {"rival-list-preset", "other"}, 6}, + {"rival-list-preset", + [&](const ImageViewRGB32& s){ return screen_is(s, "rival-list-preset"); }, + [&]{ act("rival-list-preset"); }, {"dialog", "other"}, 3}, + {"menu", + [&](const ImageViewRGB32& s){ return screen_is(s, "menu"); }, + [&]{ act("menu"); }, {}, 2, true}, + {"dialog", + [&](const ImageViewRGB32& s){ return screen_is(s, "dialog"); }, + [&]{ act("dialog"); }, {}, 400}, + {"other", + [](const ImageViewRGB32&){ return true; }, + [&]{ act("other"); }, {}, 300, false, 60}, + }; + run_screen_machine(stream, context, trace, states, std::chrono::minutes(15)); + trace.action("Intro done."); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h new file mode 100644 index 0000000000..7ab9e4fb5c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h @@ -0,0 +1,71 @@ +/* BDSP AutoStory - New Game + * + * From: https://github.com/PokemonAutomation/ + * + * Plays a new game's intro (language, Professor Rowan, appearance, names) up to + * the bedroom with the player in control, which is where the Tutorial starts. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_NewGame_H +#define PokemonAutomation_PokemonBDSP_AutoStory_NewGame_H + +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Which intro screen `screen` shows, as the intro's steps tell them apart: +// "user-select", "home", "keyboard", "rival-list-tell" (the rival's name list on +// "I'll tell you..."), "rival-list-preset" (on a preset name), "menu", "dialog" or +// "other". The Switch's own screens need `console`; without it they read as +// whatever else they look like. +std::string classify_intro_screen(ConsoleHandle* console, const ImageViewRGB32& screen); + +// What the intro does on a screen. Decided from the screen and how far the intro has +// got only, so it is tested without a console. +enum class IntroStep{ + SELECT_USER, // A on the Switch's user select + FAIL_NOT_IN_GAME, // HOME is showing + ENTER_PLAYER_NAME, // type the name on the first keyboard + CANCEL_RIVAL_KEYBOARD, // X: back to the rival's name list + MOVE_TO_PRESET, // Down: off "I'll tell you..." + PICK_PRESET, // A on a preset name + CLOSE_MENU_AND_FINISH, // B: the menu opened in the bedroom, the intro is over + ADVANCE, // A: dialogue, language, appearance, cutscenes + PROBE_FOR_MENU, // X, to see whether the player is in control +}; +struct IntroProgress{ + bool player_named = false; + bool rival_named = false; +}; +IntroStep decide_intro_step(const std::string& screen, const IntroProgress& progress); + +// After PROBE_FOR_MENU found no menu: whether A may follow. Only when X changed +// nothing, since A on something X opened would pick from it (the Bag). +bool advance_after_probe(bool screen_changed); + +// Checks classify_intro_screen() on //*.png at 1920 and +// 1280 px wide. Logs each mismatch; returns the number of them. +size_t run_intro_screen_tests(Logger& logger, const std::string& folder); + +// From the title screen or any intro screen of a game with no save. Restarts the +// game first, so it always starts from the title. Names the player `player_name` +// and picks the rival's default name (Barry) from the list. Each step reads the +// screen and waits for the expected next one; an unexpected screen fails with a +// screenshot and the recent screens. +void play_new_game_intro( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::string& player_name +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp new file mode 100644 index 0000000000..148241df7e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp @@ -0,0 +1,485 @@ +/* BDSP AutoStory - Pokemon Center + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageRGB32.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_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_PokemonCenter.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +// How many tiles the 1080p view spans across: a tile is about 125 px. +constexpr double SCREEN_WIDTH_TILES = 1920.0 / 125; +// The camera keeps the player in the middle of the screen across, and the +// centre of his tile a little above the middle down (r121a and r122a: his +// feet at y = 590 of 1080). +constexpr double PLAYER_X = 0.5; +constexpr double PLAYER_Y = 0.505; +// How much of the screen's height one row of tiles takes near the player: +// the doorway straight ahead of him (r121a) is centred this far above his +// tile's centre. Rows further north look shorter, so this undercounts them. +constexpr double SCREEN_ROW_HEIGHT = 0.12; +// Doorways are looked for down to here, 0 to 1: a door the player can walk +// into is north of him. +constexpr double DOORWAY_SEARCH_BOTTOM = 0.67; +// A doorway found within this many rows of straight ahead is at the front +// row; further, the walk to the door ended short. +constexpr double DOORWAY_FRONT_ROWS = 1.5; + +// The doorway is looked for on a view this size: 4 px of 1080p per pixel. +constexpr size_t SMALL_WIDTH = 480; +constexpr size_t SMALL_HEIGHT = 270; +// A doorway's lit columns run at least this many small rows (the Mart's 12, +// the Center's 15); the buildings' lit roof bands 3 to 5. +constexpr size_t DOORWAY_COLUMN_ROWS = 8; +// A doorway has this many lit small pixels (the Mart's 260 to 350); a roof +// band's run 60 to 75. +constexpr double DOORWAY_MIN_MASS = 80; + +// Whether a pixel is the pale yellow-white of a lit doorway: the Mart's is +// (235, 243, 205), the Center's (255, 255, 185). The buildings' white roof +// bands are bluer, their orange bands and the light they throw on the +// ground outside are darker. +bool lit_doorway_pixel(uint32_t pixel){ + const int r = (pixel >> 16) & 0xff; + const int g = (pixel >> 8) & 0xff; + const int b = pixel & 0xff; + return r >= 215 && g >= 215 && b >= 150 && b <= g - 15 && r >= g - 25; +} + +// Each column of the small view: how many lit doorway pixels it has in the +// search band, and the sum of their rows. +struct LitColumns{ + std::vector count; + std::vector row_sum; +}; +LitColumns lit_columns(const ImageViewRGB32& screen){ + const ImageRGB32 small = screen.scale_to(SMALL_WIDTH, SMALL_HEIGHT); + LitColumns columns{std::vector(small.width(), 0), std::vector(small.width(), 0)}; + const size_t bottom = (size_t)(DOORWAY_SEARCH_BOTTOM * (double)small.height()); + for (size_t y = 0; y < bottom; y++){ + for (size_t x = 0; x < small.width(); x++){ + if (lit_doorway_pixel(small.pixel(x, y))){ + columns.count[x]++; + columns.row_sum[x] += (double)y; + } + } + } + return columns; +} + +// A run of lit columns: its lit pixels, and the sums of their columns and rows. +struct LitRun{ + double mass = 0; + double column_sum = 0; + double row_sum = 0; +}; +// The run of columns with at least DOORWAY_COLUMN_ROWS lit that starts at +// `x`, allowing gaps of up to 3 columns (the door's frame). Moves `x` past it. +LitRun lit_run(const LitColumns& columns, size_t& x){ + LitRun run; + size_t gap = 0; + for (; x < columns.count.size() && gap <= 3; x++){ + if (columns.count[x] < DOORWAY_COLUMN_ROWS){ + gap++; + continue; + } + run.mass += (double)columns.count[x]; + run.column_sum += (double)columns.count[x] * (double)x; + run.row_sum += columns.row_sum[x]; + gap = 0; + } + return run; +} + +// Where a building's lit doorway is across and down the screen, 0 to 1: the +// biggest run of lit columns within 0.35 of the middle (a second building's +// door may show further off). Nothing if no doorway shows. +std::optional> lit_doorway_position(const ImageViewRGB32& screen){ + const LitColumns columns = lit_columns(screen); + const double width = (double)columns.count.size(); + std::optional> best; + double best_mass = 0; + for (size_t x = 0; x < columns.count.size(); ){ + if (columns.count[x] < DOORWAY_COLUMN_ROWS){ + x++; + continue; + } + const LitRun run = lit_run(columns, x); + const double center = run.column_sum / run.mass / width; + if (run.mass < DOORWAY_MIN_MASS || std::abs(center - PLAYER_X) >= 0.35 || run.mass <= best_mass){ + continue; + } + best = {center, run.row_sum / run.mass / (double)SMALL_HEIGHT}; + best_mass = run.mass; + } + return best; +} + +// Whether the whole view is a different scene from "before". +bool scene_changed_since(VideoStream& stream, ProControllerContext& context, const VideoSnapshot& before){ + context.wait_for_all_requests(); + VideoSnapshot now = stream.video().snapshot(); + return before && now && walk_screen_change(*before.frame, *now.frame) > WALK_SCENE_CHANGE; +} + +// Moves across by "tiles", east if positive, west if negative. +void step_across(VideoStream& stream, ProControllerContext& context, double tiles){ + const Direction side = tiles > 0 ? Direction::EAST : Direction::WEST; + route_turn(stream, context, side); + const Milliseconds hold = std::max(Milliseconds(40), + Milliseconds((int64_t)(std::abs(tiles) * ROUTE_MS_PER_TILE_ACROSS.count()))); + pbf_move_left_joystick(context, {tiles > 0 ? 1.0 : -1.0, 0}, hold, 300ms); +} + +// Whether the screen now shows a Pokemon Center's inside. +bool center_inside_now(VideoStream& stream, ProControllerContext& context){ + context.wait_for_all_requests(); + VideoSnapshot now = stream.video().snapshot(); + return now && pokemon_center_inside(*now.frame); +} + +// Walks north into the door straight ahead, `doorway` away. Returns whether +// we are inside. +bool walk_into_door( + VideoStream& stream, ProControllerContext& context, + const VideoSnapshot& outside, const LitDoorway& doorway, const std::string& label +){ + // Time for the rows to the door (none when it is straight ahead), the + // door opening, and a start delay. + const double rows = std::max(0.0, doorway.rows_up - 1); + const Milliseconds limit = 1500ms + Milliseconds((int64_t)(rows * ROUTE_MS_PER_TILE_UP_DOWN.count())); + // The Center's fade is too light to read as black (r126 runs 2 and 3: a + // screen change of 49 at 400 ms), so its inside, once it shows, stops + // the walk too. + const WalkStop stop = walk_until(stream, context, 0, 1, limit, label + ": into the door", + [](const ImageViewRGB32& screen){ return pokemon_center_inside(screen); }).stop; + if (stop == WalkStop::TRANSITION || stop == WalkStop::ARRIVED){ + pbf_wait(context, 2000ms); + return true; + } + // The door slides open before it takes us in, and its fade is not always + // black enough to read as a transition, and may finish after the walk; a + // whole new scene says we are in. Not after a walk of several rows, which + // scrolls the view: the next look decides. + pbf_wait(context, 2000ms); + if (center_inside_now(stream, context)){ + stream.log("[Route] " + label + ": a Pokemon Center's inside shows; inside."); + return true; + } + if (doorway.rows_up > DOORWAY_FRONT_ROWS || !scene_changed_since(stream, context, outside)){ + return false; + } + stream.log("[Route] " + label + ": the scene changed; inside."); + return true; +} + +// Where the doorway is on screen, in the log. +void log_doorway(VideoStream& stream, const std::string& label, const LitDoorway& doorway){ + stream.log("[Route] " + label + ": doorway " + std::to_string(doorway.tiles_across) + + " tiles across, " + std::to_string(doorway.rows_up) + " rows up (at " + + std::to_string(doorway.x) + ", " + std::to_string(doorway.y) + " of the screen)."); +} + +} + + +bool pokemon_center_inside(const ImageViewRGB32& screen){ + // The orange back wall along the top, in two of three places: a plant + // or a bookcase can stand in one (Jubilife's top left), and at night + // it is darker (r 160..200 in Jubilife, 254 in Sandgem by day). + size_t wall = 0; + for (double x : {0.02, 0.25, 0.65}){ + const ImageStats box = image_stats(extract_box_reference(screen, ImageFloatBox(x, 0.03, 0.08, 0.06))); + const double r = box.average.r, g = box.average.g, b = box.average.b; + wall += r >= 140 && r > g + 40 && g > b + 40; + } + if (wall < 2){ + return false; + } + // Black below the floor, either side of the step out of the door. + for (const ImageFloatBox& box : {ImageFloatBox(0.02, 0.95, 0.30, 0.04), ImageFloatBox(0.65, 0.95, 0.33, 0.04)}){ + if (image_stats(extract_box_reference(screen, box)).average.sum() > 90){ + return false; + } + } + // And the floor lit between them: not a fade (349 in Jubilife at night). + return image_stats(extract_box_reference(screen, ImageFloatBox(0.30, 0.55, 0.40, 0.15))).average.sum() > 250; +} + +namespace{ + +// Near-black pixels in one edge strip of the screen, out of all its pixels. +struct EdgeDark{ + size_t dark = 0; + size_t count = 0; +}; +void add_pixel(EdgeDark& edge, bool dark){ + edge.dark += dark; + edge.count++; +} +bool mostly_dark(const EdgeDark& edge){ + return edge.count > 0 && edge.dark * 100 >= edge.count * 15; +} + +} + +bool indoors(const ImageViewRGB32& screen){ + const size_t width = screen.width(), height = screen.height(); + const size_t strip_h = height * 12 / 100, strip_w = width * 12 / 100; + size_t warm = 0; + size_t count = 0; + EdgeDark top, bottom, left, right; + for (size_t y = 0; y < height; y += 8){ + for (size_t x = 0; x < width; x += 8){ + const Color pixel(screen.pixel(x, y)); + count++; + warm += pixel.red() > 150 && pixel.red() > pixel.blue() + 70 && pixel.green() > pixel.blue() + 20; + const bool dark = pixel.red() + pixel.green() + pixel.blue() < 60; + if (y < strip_h){ add_pixel(top, dark); } + if (y >= height - strip_h){ add_pixel(bottom, dark); } + if (x < strip_w){ add_pixel(left, dark); } + if (x >= width - strip_w){ add_pixel(right, dark); } + } + } + if (count == 0 || warm * 4 <= count){ + return false; + } + return mostly_dark(top) || mostly_dark(bottom) || mostly_dark(left) || mostly_dark(right); +} + +void fail_if_blacked_out(VideoStream& stream, ProControllerContext& context, const std::string& label){ + for (size_t look = 0; look < 10; look++){ + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + if (screen && indoors(*screen.frame)){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": the party fainted; the game took us home (a room shows after the battle).", stream); + } + // The blackout's text and fade are dark; the grass is not. + if (screen && image_stats(*screen.frame).average.sum() > 150){ + return; + } + pbf_wait(context, 1000ms); + } +} + +std::optional find_lit_lamp_x(const ImageViewRGB32& screen, const ImageFloatBox& box){ + const size_t x0 = (size_t)(box.x * screen.width()), x1 = (size_t)((box.x + box.width) * screen.width()); + const size_t y0 = (size_t)(box.y * screen.height()), y1 = (size_t)((box.y + box.height) * screen.height()); + double sum = 0; + size_t count = 0; + for (size_t y = y0; y < y1 && y < screen.height(); y++){ + for (size_t x = x0; x < x1 && x < screen.width(); x++){ + const Color pixel(screen.pixel(x, y)); + if (pixel.red() > 235 && pixel.green() > 220 && pixel.blue() > 150){ + sum += x; + count++; + } + } + } + // Jubilife's lamps at night: 50 to 100 such pixels at 480 px across. + if (count * 480 * 480 < 30 * screen.width() * screen.width()){ + return std::nullopt; + } + return sum / count / screen.width(); +} + +double tiles_across_at(double screen_x){ + return (screen_x - PLAYER_X) * SCREEN_WIDTH_TILES; +} + +std::optional find_lit_doorway(const ImageViewRGB32& screen){ + // A Center's lit floor has the doorway's colour. + if (pokemon_center_inside(screen)){ + return std::nullopt; + } + const std::optional> position = lit_doorway_position(screen); + if (!position){ + return std::nullopt; + } + return LitDoorway{ + position->first, + position->second, + (position->first - PLAYER_X) * SCREEN_WIDTH_TILES, + (PLAYER_Y - position->second) / SCREEN_ROW_HEIGHT, + }; +} + +std::vector tiles_short_of_door( + VideoStream& stream, ProControllerContext& context, + const TileGrid& grid, const TilePos& door, const std::string& label +){ + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + const std::optional doorway = screen ? find_lit_doorway(*screen.frame) : std::nullopt; + if (!doorway){ + stream.log("[Route] " + label + ": no lit doorway on screen; not walking again."); + return {}; + } + log_doorway(stream, label, *doorway); + if (doorway->rows_up <= DOORWAY_FRONT_ROWS){ + return {}; + } + return tiles_by_lit_doorway(*doorway, grid, door); +} + +std::vector tiles_by_lit_doorway(const LitDoorway& doorway, const TileGrid& grid, const TilePos& door){ + const TilePos guess{door.x - (int)std::lround(doorway.tiles_across), door.y + (int)std::lround(doorway.rows_up)}; + std::vector tiles; + for (int y = guess.y - 1; y <= guess.y + 1; y++){ + for (int x = guess.x - 1; x <= guess.x + 1; x++){ + if (grid.standable(TilePos{x, y})){ + tiles.push_back(TilePos{x, y}); + } + } + } + return tiles; +} + +bool line_up_with_lit_door( + VideoStream& stream, ProControllerContext& context, + const std::string& label +){ + route_status(stream, label + ": lining up with the door."); + for (size_t tries = 0; tries < 6; tries++){ + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + if (screen && pokemon_center_inside(*screen.frame)){ + stream.log("[Route] " + label + ": a Pokemon Center's inside shows; not in front of the door."); + return false; + } + const std::optional doorway = screen ? find_lit_doorway(*screen.frame) : std::nullopt; + if (!doorway){ + stream.log("[Route] " + label + ": no lit doorway on screen."); + return false; + } + log_doorway(stream, label, *doorway); + // Steps overshoot: within a quarter tile, r128 measured 0.29, -0.28, + // 0.48, -0.67 and 0.48 tiles in turn and never settled. The door + // still takes us in from 0.4 (enter_lit_door()). + if (std::abs(doorway->tiles_across) <= 0.4){ + return true; + } + step_across(stream, context, doorway->tiles_across); + // Let a door that took us in show its inside before looking again. + pbf_wait(context, 1000ms); + } + stream.log("[Route] " + label + ": not lined up after 6 steps."); + return false; +} + +void enter_lit_door( + VideoStream& stream, ProControllerContext& context, + const std::string& label +){ + // Movement is not locked to tiles, and the door takes us in only when we + // are lined up with it. Its doorway is lit: find it on screen, where the + // camera keeps the player in the middle, move across by its offset, and + // walk in once it is straight ahead, looking again after every move. + route_status(stream, label + ": lining up with the door."); + VideoSnapshot last_outside; + for (size_t tries = 0; tries < 12; tries++){ + context.wait_for_all_requests(); + VideoSnapshot outside = stream.video().snapshot(); + if (!outside){ + continue; + } + // Inside a Center, its lit floor would read as another doorway. + if (pokemon_center_inside(*outside.frame)){ + stream.log("[Route] " + label + ": a Pokemon Center's inside shows; inside."); + return; + } + const std::optional doorway = find_lit_doorway(*outside.frame); + if (!doorway){ + // The door may have taken us in after all, more slowly than we watched. + pbf_wait(context, 1500ms); + if (!scene_changed_since(stream, context, last_outside)){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no lit doorway on screen.", stream); + } + stream.log("[Route] " + label + ": the scene changed; inside."); + pbf_wait(context, 1500ms); + return; + } + last_outside = outside; + log_doorway(stream, label, *doorway); + // We stand on tiles, so within half a tile is the door's column; + // a step moves about 0.7 of a reading, and at 0.25 r144a stepped + // between 0.3 and -0.4 twelve times. Try the door there, and step + // toward it if it does not take us. + if (std::abs(doorway->tiles_across) > 0.5){ + step_across(stream, context, doorway->tiles_across); + continue; + } + if (walk_into_door(stream, context, outside, *doorway, label)){ + pbf_wait(context, 1500ms); + return; + } + step_across(stream, context, doorway->tiles_across); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": not through the door after 12 tries.", stream); +} + +void heal_at_pokemon_center( + VideoStream& stream, ProControllerContext& context, + const std::string& label +){ + enter_lit_door(stream, context, label); + + // The nurse's counter is straight ahead of the door. + route_status(stream, label + ": talking to the nurse."); + if (walk_to_talk(stream, context, 0, 1, 6000ms, label + ": to the nurse").stop != WalkStop::DIALOG){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": nurse not reached.", stream); + } + // A through her lines; the cursor starts on "Yes". Stop once no dialogue + // has shown for a while, so A does not start the talk over. + size_t quiet = 0; + for (size_t presses = 0; presses < 30 && quiet < 2; presses++){ + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 1500ms, {{dialog}}) == 0){ + quiet = 0; + pbf_press_button(context, BUTTON_A, 80ms, 700ms); + }else{ + quiet++; + } + } + context.wait_for_all_requests(); + + route_status(stream, label + ": healed; walking out."); + leave_door_south(stream, context, label + ": out the door"); +} + + + +void move_across(VideoStream& stream, ProControllerContext& context, double tiles){ + step_across(stream, context, tiles); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h new file mode 100644 index 0000000000..6826d4dd09 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h @@ -0,0 +1,127 @@ +/* BDSP AutoStory - Pokemon Center + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_PokemonCenter_H +#define PokemonAutomation_PokemonBDSP_AutoStory_PokemonCenter_H + +#include +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// A building's lit doorway seen on the overworld, relative to the player, +// whom the camera keeps in the middle of the screen. +struct LitDoorway{ + double x; // centre across the screen, 0 to 1 + double y; // centre down the screen, 0 to 1 + double tiles_across; // east of the player if positive, west if negative + double rows_up; // rows north of the player's tile, roughly: 1 straight ahead +}; + +// Whether `screen` is the inside of a Pokemon Center seen from its door: +// the orange back wall at the top left, and the black below the entrance +// mat along the bottom edge. Its lit floor reads as a doorway +// (find_lit_doorway()), so this is what says we are in (r126 runs 2, 3). +bool pokemon_center_inside(const ImageViewRGB32& screen); + +// Finds a lit doorway (a Pokemon Center's or a Poke Mart's) in the upper +// two thirds of `screen`, the pale yellow-white of its open door. Nothing if +// none shows there, or inside a Pokemon Center, whose floor is that colour. +std::optional find_lit_doorway(const ImageViewRGB32& screen); + +// Whether the screen shows a building's inside: its warm walls and floor +// fill much of the view (Pokemon Centers, the Trainers' School: 42..65% of +// the screen), and the room floats in black, which shows along at least one +// edge (15% or more of a strip 12% deep). Warmth alone is not enough: the +// evening tint makes outside 50..72% warm (r256, r257, Sandgem), and +// Oreburgh's ground 58..69% by day. Of 1412 lab screenshots, the rule calls +// every outside one outside, at all hours, and the rooms rooms. A fade is +// dark everywhere but not warm, so it is not inside. +bool indoors(const ImageViewRGB32& screen); + +// After a wild battle out of doors: fails with a screenshot if the screen +// turns into a room within 10 s, which means the party fainted and the +// game took us home (r258 lost its third training battle in Route 202's +// grass, woke in Twinleaf, and walked the Sandgem plan there). Returns +// once the outside shows, not black and not a room. +void fail_if_blacked_out(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// A lit street lamp's head inside `box` (0 to 1 of the screen): the mean +// x of its white core, or nothing if fewer than a lamp's worth of white +// pixels show there (by day the lamps are not lit). +std::optional find_lit_lamp_x(const ImageViewRGB32& screen, const ImageFloatBox& box); + +// How many tiles a thing seen at screen x is across from the player +// (positive: east). +double tiles_across_at(double screen_x); + +// Moves across by `tiles`, east if positive, west if negative, facing that +// way first. +void move_across(VideoStream& stream, ProControllerContext& context, double tiles); + +// The tiles a player who sees `doorway` (the door on world tile `door`) may +// stand on: the one its offset says, give or take one each way, since the +// rows are a rough count. Only standable tiles of `grid`. +std::vector tiles_by_lit_doorway( + const LitDoorway& doorway, const Pokemon::TileGrid& grid, const Pokemon::TilePos& door +); + +// For a player who walked to the row below the door on world tile `door`: +// if its lit doorway shows further up the screen than straight ahead, the +// walk ended short (someone in the way stops an open leg, and the walk +// cannot tell), and the tiles he may stand on instead, read off the +// doorway's offset, are returned to walk from again. Empty when he is at +// the front row, or no doorway shows. +std::vector tiles_short_of_door( + VideoStream& stream, ProControllerContext& context, + const Pokemon::TileGrid& grid, const Pokemon::TilePos& door, const std::string& label +); + +// For a player on the row below a building's door, its lit doorway on +// screen: steps across by the doorway's offset, looking again after each +// step, until it is straight ahead. Does not go in, and does not turn north +// to face it: a turn there walked us in (r127c). Returns false if no +// doorway shows, a Center's inside shows, or after 6 steps. +bool line_up_with_lit_door( + VideoStream& stream, ProControllerContext& context, + const std::string& label +); + +// Goes in through a building's door (a Pokemon Center's or a Poke Mart's). +// Starts with its lit doorway on screen, within a few tiles: lines up with +// the doorway by sight and walks in, looking again after every move. Whole- +// tile steps sideways miss it, since movement is not locked to tiles, and +// its fade may not read as black: a new scene also counts as inside. Fails +// with a screenshot when no doorway shows, or after 12 moves. +void enter_lit_door( + VideoStream& stream, ProControllerContext& context, + const std::string& label +); + +// Heals the party at a Pokemon Center. Starts in front of the Center with its +// lit doorway on screen, within a few tiles of it: lines up with the doorway +// by sight, walks in, talks to the nurse, accepts, and walks back out. Ends +// on the tile below the door. +void heal_at_pokemon_center( + VideoStream& stream, ProControllerContext& context, + const std::string& label +); + + + +} +} +} +#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 new file mode 100644 index 0000000000..41a17da994 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -0,0 +1,926 @@ +/* BDSP AutoStory - Route Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageStats.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/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_PokemonCenter.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +bool across(Direction direction){ + return direction == Direction::EAST || direction == Direction::WEST; +} + +Milliseconds ms_per_tile(Direction direction){ + return across(direction) ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN; +} + +// The left stick for a direction. Row 0 is the top of a map, and the stick's +// y is positive upward. +void stick_for(Direction direction, double& x, double& y){ + x = 0; + y = 0; + switch (direction){ + case Direction::NORTH: y = 1; break; + case Direction::EAST: x = 1; break; + case Direction::SOUTH: y = -1; break; + case Direction::WEST: x = -1; break; + } +} + +// World tiles as tiles of the named maps, for the shared route log +// (Pokemon_RouteLog.h); a tile no map holds is a "world" tile. +std::vector route_tiles(const MapSet& maps, const std::vector& tiles){ + std::vector out; + for (const TilePos& pos : tiles){ + const std::optional local = maps.to_local(pos); + out.push_back(local ? RouteTile{local->map, local->pos} : RouteTile{"world", pos}); + } + return out; +} + +std::string describe(const MapSet& maps, const std::vector& tiles){ + return Pokemon::describe(route_tiles(maps, tiles)); +} + +RouteLogLeg route_log_leg(const TileLeg& leg){ + return {.action = RouteLegAction::WALK, .direction = leg.direction, .tiles = leg.tiles, + .end = leg.ends_at_wall ? RouteLegEnd::WALL : RouteLegEnd::OPEN}; +} + +PlannedRouteLeg planned_route_leg(const MapSet& maps, const PlannedLeg& leg){ + return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; +} + +void log_plan(VideoStream& stream, const MapSet& 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)); +} + +// 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 MapSet& 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" : "")); +} + +// The tiles the player passes when walking `tiles` tiles from `from`, the +// first one after `from` first. Stops early where the player is stopped. +std::vector leg_path(const TileGrid& grid, TilePos from, Direction direction, size_t tiles){ + std::vector path; + size_t travelled = 0; + while (travelled < tiles){ + const std::optional next = grid.step(from, direction); + if (!next){ + break; + } + travelled += std::abs(next->x - from.x) + std::abs(next->y - from.y); + from = *next; + path.push_back(from); + } + 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 MapSet& maps, const std::vector& tiles){ + std::string name; + for (const TilePos& pos : tiles){ + const std::optional tile = maps.to_local(pos); + if (!tile || (!name.empty() && tile->map != name)){ + return ""; + } + name = tile->map; + } + return name; +} + +} + + +bool route_wall_readable(const TileGrid& grid, const std::vector& after){ + return std::none_of(after.begin(), after.end(), [&](const TilePos& pos){ + return grid.at(pos) == TileType::TALL_GRASS; + }); +} + +void route_status(VideoStream& stream, const std::string& text){ + stream.log("[Route] " + text, COLOR_BLUE); + add_wrapped_log(stream.overlay(), text, COLOR_WHITE); +} + +Milliseconds route_open_hold(Direction direction, size_t tiles){ + const Milliseconds step = ms_per_tile(direction); + return step * tiles - step / 2; +} + +void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ + double x, y; + stick_for(direction, x, y); + stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); + pbf_move_left_joystick(context, {x, y}, ROUTE_TURN_TAP, 300ms); + context.wait_for_all_requests(); +} + +std::optional talk_to_adjacent_npc( + VideoStream& stream, ProControllerContext& context, + const std::vector& directions, + const std::string& label, Milliseconds wait +){ + for (Direction direction : directions){ + route_turn(stream, context, direction); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, wait, {{dialog}}) == 0){ + stream.log("[Route] " + label + ": answered from the " + direction_name(direction) + "."); + return direction; + } + } + stream.log("[Route] " + label + ": nobody answered next to us."); + return std::nullopt; +} + +bool talk_with_nudges( + VideoStream& stream, ProControllerContext& context, + Direction facing, const std::string& label, Milliseconds wait, + const std::function& accept +){ + const bool vertical = facing == Direction::NORTH || facing == Direction::SOUTH; + const Direction minus = vertical ? Direction::WEST : Direction::NORTH; + const Direction plus = vertical ? Direction::EAST : Direction::SOUTH; + int at = 0; + for (int offset : {0, -1, 1, -2, 2}){ + const Direction side = offset < at ? minus : plus; + for (; at != offset; at += offset < at ? -1 : 1){ + double x, y; + stick_for(side, x, y); + pbf_move_left_joystick(context, {x, y}, ROUTE_NUDGE_TAP, 300ms); + } + if (offset != 0){ + route_turn(stream, context, facing); + // A tap in a cave or in grass can start a wild battle, after + // which we are not where the nudges think: let the caller line up again. + if (run_from_late_encounter(stream, context, label + ": nudge")){ + return false; + } + } + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, wait, {{dialog}}) == 0){ + const VideoSnapshot after = stream.video().snapshot(); + if (!accept || (before && after && accept(*before.frame, *after.frame))){ + stream.log("[Route] " + label + ": answered " + std::to_string(offset) + " nudge(s) from where we stood."); + return true; + } + stream.log("[Route] " + label + ": something else answered " + std::to_string(offset) + " nudge(s) from where we stood; closing it."); + for (size_t press = 0; press < 3; press++){ + pbf_press_button(context, BUTTON_B, 80ms, 700ms); + } + context.wait_for_all_requests(); + } + } + stream.log("[Route] " + label + ": nobody answered within two nudges either side."); + return false; +} + +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, + const MapSet& maps, + std::vector from, + const std::function& goal, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const std::vector& avoid, + const RobustPlanCosts& costs +){ + // Our own copy, so things found in the way can be added to it. Tiles to + // avoid are ground the game lets us walk on (a trainer's line of sight), + // so they are off the map, not walls: a leg must not end "at" one, or + // the walk would run on through it. + TileGrid grid = maps.world(); + for (const TilePos& pos : avoid){ + if (std::find(from.begin(), from.end(), pos) == from.end()){ + grid.set(pos, TileType::UNKNOWN); + } + } + // Which way the player faces, once a leg has set it. + std::optional facing; + // Every banner read on the way, to check the map we arrive on against. + const std::string map_start = map_of(maps, from); + std::vector all_banners; + for (size_t plans = 0; plans < 8; plans++){ + std::optional> legs = plan_robust_route(grid, from, goal, costs); + if (!legs){ + // 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. + stream.log("[Route] " + label + ": no plan with the route costs from " + describe(maps, from) + + "; trying the default costs.", COLOR_ORANGE); + legs = plan_robust_route(grid, from, goal, RobustPlanCosts()); + } + if (!legs){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no plan from " + describe(maps, from) + ".", stream); + } + route_status(stream, label + ": " + std::to_string(legs->size()) + " legs planned"); + log_plan(stream, maps, label, from, *legs); + + bool replanned = false; + size_t leg_number = 0; + for (const PlannedLeg& planned : *legs){ + const TileLeg& leg = planned.leg; + leg_number++; + route_status(stream, route_leg_line(leg_number, legs->size(), planned_route_leg(maps, planned))); + double x, y; + stick_for(leg.direction, x, y); + const std::string leg_label = label + ": " + to_string(leg); + + // Note the location banners the walk passes, to check map changes. + // A wild battle or a second walk to the wall may follow the + // banner: any banner seen during the leg counts. + const std::string map_before = map_of(maps, from); + const std::string map_after = map_of(maps, planned.after); + std::vector banners; + auto banner_ok = [&]{ + return std::any_of(banners.begin(), banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_after); + }); + }; + auto watch_banner = [&](const ImageViewRGB32& screen){ + if (banners.empty() || (!map_after.empty() && !banner_ok() && banners.size() < 8)){ + const std::string read = read_location_banner(screen); + if (!read.empty() && (banners.empty() || banners.back() != read)){ + banners.push_back(read); + } + } + return false; + }; + + WalkResult result; + // Tiles the first tap of an open leg moved (see below). + size_t turn_moved = 0; + if (leg.ends_at_wall){ + // Run (B held) on the D-pad: a leg to a wall ends at the wall + // however fast we get there, and the D-pad keeps to the row. + const Milliseconds per_tile = ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; + // Long enough to reach the wall from the farthest tile, with room + // for a slow start; past the wall the player stands still. + const Milliseconds limit = ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; + result = walk_until_dpad(stream, context, x, y, limit, leg_label, watch_banner, true); + // 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 = result.wild_battles > 0; + for (size_t again = 0; again < 3 && result.wild_battles > 0 && + (result.stop == WalkStop::LIMIT || result.stop == WalkStop::BLOCKED); again++ + ){ + stream.log("[Route] " + leg_label + ": a wild battle on the way; walking to the wall again."); + result = walk_until_dpad(stream, context, x, y, limit, leg_label, watch_banner, true); + } + // A wall well before the nearest one the map has: something the + // map does not know is in the way. Work out where we stopped from + // the time walked, put it on our map and plan again from there. + size_t nearest = SIZE_MAX; + for (const TilePos& pos : from){ + nearest = std::min(nearest, walk_to_wall(grid, pos, leg.direction).tiles); + } + auto tiles_moved = [&](const WalkResult& walk){ + const Milliseconds moving = walk.walked - ROUTE_START_DELAY - + (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); + return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); + }; + size_t walked_tiles = tiles_moved(result); + // A stick walk can catch a corner when we stand part of a tile + // off centre, where a D-pad tap, which moves whole tiles, gets + // through: on 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 (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ + const DpadPosition dpad = + leg.direction == Direction::NORTH ? DPAD_UP : + leg.direction == Direction::SOUTH ? DPAD_DOWN : + leg.direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + stream.log("[Route] " + leg_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(context, dpad, 160ms, 640ms); + } + context.wait_for_all_requests(); + result = walk_until_dpad(stream, context, x, y, limit, leg_label, watch_banner, true); + walked_tiles += tiles_moved(result); + } + // 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; !interrupted && again < 2 && + result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++ + ){ + stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(context, 2500ms); + context.wait_for_all_requests(); + result = walk_until_dpad(stream, context, x, y, limit, leg_label, watch_banner, true); + walked_tiles += tiles_moved(result); + } + // Any stop short of the wall's distance: someone who stays put + // is talked to, and the leg is planned again from where we are. + // Within the time count's slack this may be the wall itself, + // where A does nothing and the new plan walks no further (r239a: + // a wanderer 3 tiles short passed as "about 8 of 10", and every + // later leg was off by those tiles). + if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ + // Someone who stays put: talk to them. A trainer we walked up + // to from the side starts the battle this way. + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + { + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, leg_label + ": someone is in the way; talking to them."); + fight("someone in the way"); + } + } + std::vector stopped; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, SIZE_MAX); + for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ + stopped.push_back(k == 0 ? pos : path[k - 1]); + } + } + std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": stopped early, and no tile fits.", stream); + } + if (stopped.size() == 1){ + const TilePos blocker = stopped[0].next(leg.direction); + grid.set(blocker, TileType::BLOCKED); + stream.log("[Route] " + leg_label + ": something the map lacks is at " + + maps.describe(blocker) + "; marked it.", COLOR_ORANGE); + } + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + "stopped early after about " + std::to_string(walked_tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); + facing = leg.direction; + from = stopped; + replanned = true; + break; + } + // A leg to the wall that ran out of time is not at its wall: + // the open leg before it stopped short, and it ran along + // another row (r127a went west along Sandgem's row 13 for row + // 14, and the next leg north walked into the lab). Out of tall + // grass a stop reads, so walk on: a player still moving is + // not where the plan has him. + if (!interrupted && result.stop == WalkStop::LIMIT && route_wall_readable(grid, planned.after)){ + const WalkResult check = walk_until_dpad(stream, context, x, y, 2000ms, leg_label + ": at the wall?", nullptr, true); + 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 Route 204's corridor). Battle, + // then plan again from where a sight line crosses the leg. + route_status(stream, leg_label + ": a trainer stopped us on the way; battling."); + fight("a trainer on the way"); + // Where they saw us from is not reliable (BDSP's trainers + // stand a few tiles from Platinum's, r242a), so do not + // guess the tile: a leg to a wall ends at its wall however + // far it got. Walk on to it. + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + result = walk_until_dpad(stream, context, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + } + if (check.stop != WalkStop::BLOCKED && + !(check.stop == WalkStop::LIMIT && holding_against_wall(stream, context, x, y, leg_label + ": at the wall?")) + ){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": ran out of time short of the wall, and walking on gets " + + walk_stop_name(check.stop) + ": not where the plan has us.", stream); + } + } + }else{ + // Whole tiles on the D-pad, a tap each: a tap moves exactly + // one tile, while a stick hold timed for three moved one in + // the Ravaged Path (r235b) and the belief drifted four rows. + // The first tap of a leg was meant to turn, but a 160 ms + // D-pad tap turns and steps whichever way the player faced + // (r248a: all seven did; r246a: "south 1" out of the cave + // went two tiles, onto the Youngster's row), so the tap is + // measured like any other and a step counts for the leg. + if (facing != leg.direction){ + const DpadPosition dpad = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + pbf_press_dpad(context, dpad, 160ms, 700ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ + turn_moved = 1; + stream.log("[Route] " + leg_label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); + } + } + if (leg.tiles > turn_moved){ + result = walk_tiles_dpad(stream, context, x, y, leg.tiles - turn_moved, leg_label); + }else{ + result = WalkResult{WalkStop::LIMIT, 0ms, 0}; + } + // A wild battle starts once the step onto the grass is done, + // so the taps still count; the walk went on after it. + if (result.wild_battles > 0){ + stream.log("[Route] " + leg_label + ": " + std::to_string(result.wild_battles) + + " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); + } + // Taps that moved nothing: someone stands in the way (the + // Youngster below the Ravaged Path wanders across the row + // under the cliff, r239a, r240a). Two still taps end the walk, + // so the tiles moved are the taps less those two. Wait for + // them to move on and tap the rest; someone who stays is + // talked to, put on the map, and the leg planned again from + // the tile we stand on, which the taps give exactly. + auto taps_moved = [](const WalkResult& walk){ return walk.moved_chunks; }; + size_t moved = std::min(leg.tiles, taps_moved(result) + turn_moved); + // One wait: a wanderer in the way steps off in a few seconds; + // anything still there after that is talked to (r246a lost + // 20 s per block on three waits). + for (size_t again = 0; again < 1 && result.stop == WalkStop::BLOCKED && moved < leg.tiles; again++){ + stream.log("[Route] " + leg_label + ": blocked after " + std::to_string(moved) + " of " + + std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(context, 2500ms); + context.wait_for_all_requests(); + const WalkResult rest = walk_tiles_dpad(stream, context, x, y, leg.tiles - moved, leg_label + ": the rest"); + moved = std::min(leg.tiles, moved + taps_moved(rest)); + result.stop = rest.stop; + result.walked += rest.walked; + result.wild_battles += rest.wild_battles; + } + if (result.stop == WalkStop::BLOCKED && moved < leg.tiles){ + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + { + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, leg_label + ": someone is in the way; talking to them."); + fight("someone in the way"); + } + } + std::vector stopped; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, moved); + stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); + } + std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); + if (stopped.size() == 1){ + const TilePos blocker = stopped[0].next(leg.direction); + grid.set(blocker, TileType::BLOCKED); + stream.log("[Route] " + leg_label + ": something the map lacks is at " + + maps.describe(blocker) + "; marked it.", COLOR_ORANGE); + } + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + "blocked after " + std::to_string(moved) + " of " + std::to_string(leg.tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); + facing = leg.direction; + from = stopped; + replanned = true; + break; + } + } + facing = leg.direction; + { + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + watch_banner(*screen.frame); + } + } + std::string banner_text; + for (const std::string& read : banners){ + banner_text += (banner_text.empty() ? "\"" : ", \"") + read + "\""; + } + all_banners.insert(all_banners.end(), banners.begin(), banners.end()); + // Entering another map shows its name; a missing or wrong name means + // the walk did not end where the plan says. From tiles on several + // maps (a leg not confirmed) the banner may already have shown. + if (!map_before.empty() && !map_after.empty() && map_after != map_before && !banner_ok()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": expected to enter " + map_after + ", but the location banner read " + + (banner_text.empty() ? "nothing" : banner_text) + ".", stream); + } + if (!banner_text.empty()){ + stream.log("[Route] " + leg_label + ": location banner " + banner_text + "."); + } + // Where each leg ended, to compare with the plan after a run. + if (VideoSnapshot screen = stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(leg_label) + ".png"); + } + if (result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": unexpected transition.", stream); + } + // A trainer who sees us freezes the player and walks over; the + // dialogue may open after the walk has ended. + const std::vector> paths = [&]{ + std::vector> out; + for (const TilePos& pos : from){ + out.push_back(leg_path(grid, pos, leg.direction, + leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); + } + return out; + }(); + std::vector watching; + for (size_t t = 0; t < trainers.size(); t++){ + const std::vector sight = sight_tiles(grid, trainers[t]); + for (const std::vector& path : paths){ + if (std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ + return std::find(sight.begin(), sight.end(), pos) != sight.end(); + })){ + watching.push_back(t); + break; + } + } + } + bool talking = result.stop == WalkStop::DIALOG; + if (!talking){ + ShortDialogWatcher dialog(COLOR_YELLOW); + const Milliseconds wait = watching.empty() ? 500ms : 4000ms; + talking = wait_until(stream, context, wait, {{dialog}}) == 0; + } + if (!talking){ + from = planned.after; + log_leg_done(stream, maps, label, planned, result.wild_battles); + continue; + } + + if (watching.empty()){ + // A trainer the map does not have. Fight, then plan again from + // anywhere the leg could have stopped. + std::vector anywhere = from; + for (const std::vector& path : paths){ + anywhere.insert(anywhere.end(), path.begin(), path.end()); + } + std::sort(anywhere.begin(), anywhere.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + anywhere.erase(std::unique(anywhere.begin(), anywhere.end()), anywhere.end()); + route_status(stream, label + ": a trainer the map lacks stopped us; fighting."); + fight("unknown trainer"); + if (leg.ends_at_wall){ + const Milliseconds limit = ROUTE_START_DELAY + + ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(stream, context, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + from = planned.after; + log_leg_done(stream, maps, label, planned, 0); + continue; + } + from = anywhere; + replanned = true; + break; + } + // Each possible start tile was stopped on the first tile of its path + // inside a watching trainer's sight. + const TrainerSight trainer = trainers[watching[0]]; + std::vector stopped; + for (const std::vector& path : paths){ + for (const TilePos& pos : path){ + bool seen = false; + for (size_t t : watching){ + const std::vector sight = sight_tiles(grid, trainers[t]); + seen |= std::find(sight.begin(), sight.end(), pos) != sight.end(); + } + if (seen){ + stopped.push_back(pos); + break; + } + } + } + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": " + trainer.name + " talked, but no path enters their sight.", stream); + } + stream.log(ROUTE_LOG_TAG + route_trainer_line(label, trainer.name, route_tiles(maps, stopped))); + route_status(stream, "Fighting " + trainer.name + "."); + fight(trainer.name); + trainers.erase(trainers.begin() + watching[0]); + // Their sight line only says roughly where: BDSP's trainers stand a + // few tiles from Platinum's (r242a). A leg to a wall ends at its + // wall however far it got: walk on to it and carry on. An open leg + // counts its taps: the step that brought us into sight was complete. + if (leg.ends_at_wall){ + const Milliseconds limit = ROUTE_START_DELAY + + ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(stream, context, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + from = planned.after; + log_leg_done(stream, maps, label, planned, 0); + continue; + } + // Only the taps that moved count: the trainer freezes the player + // on the tile they saw, and the taps pressed while they walk over + // move nobody (r248a: "4 taps" were 2 steps; the belief of two + // tiles then cost a 12-tile run to a wall and 5 back). + const size_t taps = std::min(leg.tiles, result.moved_chunks + turn_moved); + std::vector by_taps; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, leg.tiles); + const size_t k = std::min(taps, path.size()); + by_taps.push_back(k == 0 ? pos : path[k - 1]); + } + std::sort(by_taps.begin(), by_taps.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + by_taps.erase(std::unique(by_taps.begin(), by_taps.end()), by_taps.end()); + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + std::to_string(taps) + " taps before the battle", route_tiles(maps, by_taps)), COLOR_ORANGE); + from = by_taps; + replanned = true; + break; + } + if (!replanned){ + // Legs whose ends span two maps skip the per-leg check; the walk + // as a whole must still have seen the banner of the map it ends on. + const std::string map_end = map_of(maps, from); + const bool changed_map = !map_start.empty() && !map_end.empty() && map_end != map_start; + if (changed_map && std::none_of(all_banners.begin(), all_banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_end); + })){ + std::string seen; + for (const std::string& read : all_banners){ + seen += (seen.empty() ? "\"" : ", \"") + read + "\""; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": arrived at " + describe(maps, from) + " on the map, but " + map_end + + "'s location banner never showed (read " + (seen.empty() ? "nothing" : seen) + ").", stream); + } + stream.log(ROUTE_LOG_TAG + route_arrived_line(label, route_tiles(maps, from))); + return from; + } + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": too many plans.", stream); +} + + + +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, + const MapSet& maps, + std::vector from, + const std::vector& waypoints, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const RobustPlanCosts& costs +){ + for (size_t i = 0; i < waypoints.size(); i++){ + const TilePos goal = waypoints[i]; + from = walk_route(stream, context, maps, from, + [&](const TilePos& pos){ return pos == goal; }, + trainers, fight, + label + " (" + std::to_string(i + 1) + "/" + std::to_string(waypoints.size()) + ", to " + maps.describe(goal) + ")", + {}, costs); + } + return from; +} + +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, + const MapSet& maps, + std::vector& at, + const TilePos& npc, + const std::vector& sides, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label +){ + for (Direction side : sides){ + const TilePos stand = npc.next(side); + if (!maps.world().standable(stand)){ + continue; + } + at = walk_route(stream, context, maps, at, [&](const TilePos& pos){ return pos == stand; }, + trainers, fight, label); + // Face the NPC: the opposite of the side we stand on. + const Direction face = + 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, label + ": talking from " + maps.describe(stand) + "."); + return; + } + route_status(stream, label + ": nobody answered from " + maps.describe(stand) + "; trying another side."); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no side of " + maps.describe(npc) + " started a dialogue.", stream); +} + +void finish_dialogue( + VideoStream& stream, ProControllerContext& context, + Button button, const std::string& label, Milliseconds timeout +){ + // No line counts: talks differ in length (r115a: 8 A left the Poke Mart + // lady's Potions talk open). The box blinks off between lines and during + // "obtained" jingles, so only a stable absence ends the talk. + const WallClock none = WallClock::max(); + ShortDialogDetector dialog; + OverworldDetector overworld; + const WallClock deadline = current_time() + timeout; + WallClock quiet_since = none; + WallClock unknown_since = none; + while (current_time() < deadline){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const WallClock now = current_time(); + const bool talking = frame && dialog.detect(frame); + if (frame && !talking && overworld.detect(frame)){ + unknown_since = none; + if (quiet_since == none){ + quiet_since = now; + }else if (now - quiet_since >= 1500ms){ + return; + } + context.wait_for(200ms); + continue; + } + quiet_since = none; + // Press only on a box (an A on the overworld talks again), or on a + // screen that is neither for 3 s. + if (talking || (unknown_since != none && now - unknown_since >= 3000ms)){ + unknown_since = none; + pbf_press_button(context, button, 80ms, 1200ms); + continue; + } + if (unknown_since == none){ + unknown_since = now; + } + context.wait_for(200ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": the dialogue did not end within " + + std::to_string(std::chrono::duration_cast(timeout).count()) + " s.", stream); +} + +void enter_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label){ + // Sideways steps to try after a blocked walk: east, then back west and + // one more. + const Direction steps[] = {Direction::EAST, Direction::WEST, Direction::WEST}; + for (size_t attempt = 0; attempt <= 3; attempt++){ + const WalkResult walk = walk_until(stream, context, 0, 1, 2500ms, label); + if (walk.stop == WalkStop::TRANSITION){ + context.wait_for(1500ms); + return; + } + if (attempt == 3){ + break; + } + route_status(stream, label + ": the door frame stopped us; one tile " + + (steps[attempt] == Direction::EAST ? "east." : "west.")); + route_turn(stream, context, steps[attempt]); + const double x = steps[attempt] == Direction::EAST ? 1 : -1; + pbf_move_left_joystick(context, {x, 0}, route_open_hold(steps[attempt], 1), 200ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": could not go through the door.", stream); +} + +bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room){ + if (brightness < DOOR_FADE_DARK){ + watch.saw_dark = true; + } + return watch.saw_dark && !room && brightness >= DOOR_OUTSIDE_BRIGHT; +} + +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room){ + // Both are asked: the fade watch keeps what it has seen. + const bool faded = door_fade_look(watch, brightness, room); + return faded || !room; +} + +namespace{ + +// Three looks half a second apart, none a room: outside, whatever the fade +// did. A look of a lit Center can miss its black edge (r259 at 800 ms), +// three in a row standing still do not. +bool outside_for_three_looks(VideoStream& stream, ProControllerContext& context, const std::string& label){ + for (size_t look = 0; look < 3; look++){ + context.wait_for_all_requests(); + VideoSnapshot now = stream.video().snapshot(); + const bool room = !now || indoors(*now.frame); + stream.log("[Route] " + label + ": standing look " + std::to_string(look + 1) + ": " + (room ? "a room" : "not a room") + "."); + if (room){ + return false; + } + pbf_wait(context, 500ms); + } + return true; +} + +} + +void leave_door_south(VideoStream& stream, ProControllerContext& context, const std::string& label){ + // A last line of talk may still be up, and it would stop the walk. + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + // Stop the walk at the fade or at the first look that is not a room: a + // stick held on walks on outside (r124a ten rows south of the Mart's + // door, r257 into a house, r261 three rows). Standing looks then confirm. + DoorFadeWatch watch; + auto out_of_door = [&](const ImageViewRGB32& screen){ + const double brightness = image_stats(screen).average.sum(); + const bool room = indoors(screen); + const bool out = door_walk_stops(watch, brightness, room); + stream.log(std::format("[Route] {}: look: brightness {:.0f}, {}, fade {}.", label, brightness, + room ? "a room" : "not a room", watch.saw_dark ? "seen" : "not seen")); + return out; + }; + bool out = false; + for (size_t tries = 0; tries < 5 && !out; tries++){ + const WalkResult walk = walk_until(stream, context, 0, -1, 6000ms, label, out_of_door); + if (walk.stop == WalkStop::DIALOG){ + pbf_mash_button(context, BUTTON_B, 2000ms); + } + // Let the fade-in finish, then look again standing still. + pbf_wait(context, 1500ms); + out = walk.stop == WalkStop::TRANSITION || outside_for_three_looks(stream, context, label); + if (!out){ + stream.log("[Route] " + label + ": still indoors; walking on."); + } + } + if (!out){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": did not get back out.", stream); + } + // Let the fade-in finish. + pbf_wait(context, 1500ms); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h new file mode 100644 index 0000000000..98b122536e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -0,0 +1,202 @@ +/* BDSP AutoStory - Route Walk + * + * From: https://github.com/PokemonAutomation/ + * + * Walks a plan made on the story maps: legs to a wall are held until the + * player stops, open legs for their length in time. Tracks every tile the + * player may be on, and plans again after a trainer stops the walk. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H +#define PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H + +#include +#include +#include +#include +#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_MapSet.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Walking time per tile, measured on the console from the post-catch save: +// a row is deeper than a column is wide under the tilted camera. +constexpr Milliseconds ROUTE_MS_PER_TILE_ACROSS = Milliseconds(160); // east or west +constexpr Milliseconds ROUTE_MS_PER_TILE_UP_DOWN = Milliseconds(240); // north or south +// From pressing the stick to the first step, when standing still. +constexpr Milliseconds ROUTE_START_DELAY = Milliseconds(250); +// Running (B held) covers a tile in this fraction of the walking time. B +// held with the D-pad did not run: r238a walked the Ravaged Path's corridor +// at 160 ms a tile, and a limit set for twice that speed cut a wall leg +// short by two tiles. 1 until running is measured to work. +constexpr int ROUTE_RUN_SPEEDUP = 1; + +// How long to hold the stick for an open leg of `tiles` tiles, for a player +// already facing that way: a step once started is always finished, so stop +// half a tile early. On the console a 1290ms hold west went 9 tiles and an +// 800ms hold south 4: the player starts at once and finishes the step. +Milliseconds route_open_hold(Pokemon::Direction direction, size_t tiles); + +// A tap this short turns a player who faces another way, without a step: on +// the console a 150ms tap south after walking north only turned the player. +constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(100); + +// 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); + +// Talks to someone on a tile next to ours, without trusting the tile we +// counted ourselves onto: for each of `directions` in order, turns that way, +// presses A and waits up to `wait` for a dialogue. Returns the direction that +// answered, its dialogue open, or none if nobody did. Turns are taps (see +// route_turn()), so do not list the way the player already faces first. +std::optional talk_to_adjacent_npc( + VideoStream& stream, ProControllerContext& context, + const std::vector& directions, + const std::string& label, Milliseconds wait = Milliseconds(1500) +); + +// Talks to someone straight ahead in `facing`, where we may stand a little +// off their tile: BDSP moves by any distance, and a sideways tap moves about +// a third of a tile (a 50ms tap 0.2 to 0.45 tiles, a 100ms turn tap 0.4, on +// the console 2026-10-31). Presses A, and while nobody answers shifts by one +// such tap at a time to -1, +1, -2, +2 taps from the start and tries again. +// Never turns away and back between tries: each turn is a tap that moves us +// (s03e2 drifted east of Roark a third of a tile per try). Returns whether a +// dialogue opened, left open; false also after a wild battle on a nudge, +// which leaves us off the place we were. +// `accept`, if given, looks at the screen before A and once the dialogue +// is open, and says whether it is the one wanted: a rock or a sign next to +// someone answers A too (r187a read "It's a rugged rock" beside Roark). A +// dialogue it turns down is closed with B and the nudges go on. +bool talk_with_nudges( + VideoStream& stream, ProControllerContext& context, + Pokemon::Direction facing, const std::string& label, + Milliseconds wait = Milliseconds(3000), + const std::function& accept = nullptr +); +constexpr Milliseconds ROUTE_NUDGE_TAP = Milliseconds(50); + +// Whether a walk to the wall that ends on any of `after` can tell it has +// stopped: not in tall grass, whose sway keeps the screen changing at a +// wall. Elsewhere, a walk to the wall that stops confirms where it ended. +bool route_wall_readable(const Pokemon::TileGrid& grid, const std::vector& after); + +// Shows what the route walk is doing in the log and the video overlay. +void route_status(VideoStream& stream, const std::string& text); + +// Called when a trainer's dialogue has opened, or when someone standing in +// the way was talked to. Fights the battle if one starts, and returns to the +// overworld either way. +using RouteTrainerFight = std::function; + +// Walks from any of the world tiles `from` of `maps` until every tile the +// player may be on is `goal`, and returns those tiles. Trainers in +// `trainers` who see the player are fought with `fight` and taken off the +// list. Tiles in `avoid` (where someone wanders) are planned as walls. +// Fails with a screenshot +// on a transition, a dialogue no trainer explains, a wild battle during an +// open leg, or when no plan exists. A walk from one map to another must see +// the second map's location banner on the way: a walk whose legs all ended +// on walls can still end in the wrong place (r124a saved in Sandgem Town +// after "arriving" on Route 202). +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, + const Pokemon::MapSet& maps, + std::vector from, + const std::function& goal, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const std::vector& avoid = {}, + const Pokemon::RobustPlanCosts& costs = route_plan_costs() +); + +// Walks `from` to each of `waypoints` in turn, with walk_route() for every +// stretch, and returns where the last one ended. The corners of a line a +// human walked, so the plan is that line, with the walker's handling of +// trainers and blocks on each stretch (Route 204 North, 2026-10-31). +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, + const Pokemon::MapSet& maps, + std::vector from, + const std::vector& waypoints, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const Pokemon::RobustPlanCosts& costs = waypoint_plan_costs() +); + +// Walks next to the NPC on world tile `npc`, faces them and presses A, trying +// the sides in `sides` (the side of the NPC to stand on) in order until a +// dialogue opens. `at` is where the player may be, and is updated. Fails with +// a screenshot if no side starts a dialogue. A press from a diagonal tile, +// or from a tile a row off, talks to nobody, so each side is tried in turn. +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, + const Pokemon::MapSet& maps, + std::vector& at, + const Pokemon::TilePos& npc, + const std::vector& sides, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label +); + +// Finishes a dialogue that has opened, however many lines it has: presses +// `button` while a dialogue box shows, and returns once the overworld has +// shown with no box for 1.5 s (the box blinks off between lines and during +// "obtained" jingles). Use A where the talk asks Yes/No or gives an item (A +// answers Yes and closes "obtained" lines; B would answer No), B elsewhere +// (B never starts a new talk). Fails with a screenshot after `timeout`. +void finish_dialogue( + VideoStream& stream, ProControllerContext& context, + Button button, const std::string& label, + Milliseconds timeout = std::chrono::seconds(40) +); + +// For a player on the tile below a door: walks north through it. If a wall +// stops the walk (the door frame, a tile off the opening), steps a tile east, +// then west, and tries again. Fails with a screenshot if no step goes in. +void enter_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// For a player inside a building whose door is straight south: closes any +// talk still up and walks south out of it, stopping at the door's fade or +// the first look that is not a room, so it ends a tile or so outside. Out is +// then three standing looks in a row that are not a room: one look of a lit +// Center can miss its black edge (r259). Fails with a screenshot if five +// walks south get nowhere. +void leave_door_south(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// Brightness (mean r + g + b) below which a look is the door's fade, and +// above which a look that is not a room is outside. +inline constexpr double DOOR_FADE_DARK = 120.0; +inline constexpr double DOOR_OUTSIDE_BRIGHT = 200.0; + +// What leave_door_south() has seen of the door's fade so far. +struct DoorFadeWatch{ + bool saw_dark = false; +}; +// Takes one look (its brightness, and whether indoors() read a room) and +// returns whether we are out: a dark look seen, and now a bright one that +// is not a room. +bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room); +// Whether leave_door_south()'s walk stops at this look: at the door's fade +// out (door_fade_look()), or at the first look that is not a room, so a +// held stick does not walk on outside (r261 three rows). +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp new file mode 100644 index 0000000000..51c42a038a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp @@ -0,0 +1,2082 @@ +/* BDSP AutoStory - Segment 00 Tools + * + * Helper implementations for Segment 00. + * + * From: https://github.com/PokemonAutomation/ + * + */ + + +#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include +#include +#include +#include +#include + +#include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "PokemonBDSP_Autostory_Segment_01_tools.h" + +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "CommonFramework/Tools/ErrorDumper.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "CommonTools/VisualDetectors/ImageMatchDetector.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/NintendoSwitch_Settings.h" + +#include "NintendoSwitch/Programs/DateManip/NintendoSwitch_DateManip.h" +#include "NintendoSwitch/Programs/DateSpam/NintendoSwitch_HomeToDateTime.h" +#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" + +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_ReceivePokemonDetector.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_ExperienceGainDetector.h" + +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" + +#include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" +#include "PokemonBDSP/Programs/PokemonBDSP_BasicCatcher.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" + +using namespace std::chrono_literals; + +namespace PokemonAutomation { + namespace NintendoSwitch { + namespace PokemonBDSP { + + // --------------------------------------------------------------------------- + // BattleDialogWatcher + // --------------------------------------------------------------------------- + + BattleDialogWatcher::BattleDialogWatcher(Color color) + : DetectorToFinder( + "BattleDialogWatcher", + std::chrono::milliseconds(250), + color + ) + { + } + + // --------------------------------------------------------------------------- + // Poketech Clown dialog helper + // --------------------------------------------------------------------------- + + int detect_clown_choice_arrow( + VideoOverlay& overlay, + VideoSnapshot snapshot + ) { + SelectionArrowFinder top_arrow( + overlay, + { 0.62, 0.37, 0.08, 0.10 }, + COLOR_RED + ); + + SelectionArrowFinder bottom_arrow( + overlay, + { 0.62, 0.48, 0.08, 0.10 }, + COLOR_RED + ); + + if (top_arrow.detect(snapshot)) { + return 0; + } + + if (bottom_arrow.detect(snapshot)) { + return 1; + } + + return -1; + } + + void clown_dialog_helper( + VideoStream& stream, + ProControllerContext& context, + DpadPosition direction + ) { + stream.log( + "[AutoStory] Starting clown dialog helper...", + COLOR_BLUE + ); + + const auto overall_deadline = + std::chrono::steady_clock::now() + 30s; + + // ------------------------------------------------- + // Phase 1: Move + interact until dialog is detected. + // ------------------------------------------------- + ShortDialogWatcher dialog(COLOR_YELLOW); + bool dialog_started = false; + + while (!dialog_started) { + + if (std::chrono::steady_clock::now() > overall_deadline) { + stream.log( + "[AutoStory] Clown helper timed out finding dialog.", + COLOR_RED + ); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "[AutoStory] Clown helper timed out finding dialog.", stream); + } + + context.wait_for_all_requests(); + pbf_press_dpad(context, direction, 80ms, 200ms); + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + + if (wait_until(stream, context, 1500ms, { {dialog} }) == 0) { + dialog_started = true; + stream.log("[AutoStory] Dialog detected.", COLOR_GREEN); + } + } + + // ------------------------------------------------- + // Phase 2: A mash for 2 seconds. + // ------------------------------------------------- + stream.log("[AutoStory] Mashing A to advance dialog...", COLOR_BLUE); + pbf_mash_button(context, BUTTON_A, 4000ms); + context.wait_for_all_requests(); + + // ------------------------------------------------- + // Phase 3: Clear remaining dialog. + // ------------------------------------------------- + pbf_mash_button(context, BUTTON_B, 4000ms); + + context.wait_for_all_requests(); + + stream.log( + "[AutoStory] Clown dialog helper complete.", + COLOR_GREEN + ); + } + // --------------------------------------------------------------------------- + // Time helper + // --------------------------------------------------------------------------- + + // Options chrome and position detectors are independent of setting values. + class TutorialOptionsDetector { + public: + bool detect(const ImageViewRGB32& screen) const { + ImageViewRGB32 panel = extract_box_reference(screen, ImageFloatBox{0.04, 0.065, 0.93, 0.70}); + size_t white = 0; + for (size_t y = 0; y < panel.height(); y += 4) + for (size_t x = 0; x < panel.width(); x += 4) { + uint32_t p = panel.pixel(x, y); + if (((p >> 16) & 255) > 225 && ((p >> 8) & 255) > 225 && (p & 255) > 225) white++; + } + double samples = (double)(((panel.width() + 3) / 4) * ((panel.height() + 3) / 4)); + // Distinguish Options from the icon menu and bag using its + // blue heading and full-width white settings panel. + ImageViewRGB32 heading = extract_box_reference(screen, ImageFloatBox{0.01, 0.005, 0.20, 0.025}); + size_t blue = 0, total = 0; + for (size_t y = 0; y < heading.height(); y++) + for (size_t x = 0; x < heading.width(); x++) { + uint32_t p = heading.pixel(x, y); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + blue += b > r + 20 && g > r + 10; + total++; + } + return samples > 0 && white / samples > 0.70 && total > 0 && blue > total / 4; + } + }; + + // Options fades in after A on its icon: poll instead of judging one frame. + static bool wait_for_options_screen(VideoStream& stream, ProControllerContext& context, + const TutorialOptionsDetector& options_screen, Milliseconds timeout) { + context.wait_for_all_requests(); + const WallClock deadline = current_time() + timeout; + while (current_time() < deadline) { + auto frame = stream.video().snapshot(); + if (frame && options_screen.detect(frame)) return true; + context.wait_for(100ms); + } + return false; + } + + class TutorialOptionPositionDetector { + public: + // Locate the two long red horizontal lines surrounding the + // selected row. Scrolling changes their position automatically. + double selected_row(const ImageViewRGB32& screen) const { + int first = -1, last = -1; + for (size_t y = (size_t)(screen.height() * 0.08); y < screen.height() * 0.75; y++) { + size_t red = 0, total = 0; + for (size_t x = (size_t)(screen.width() * 0.08); x < screen.width() * 0.90; x += 2) { + uint32_t p = screen.pixel(x, y); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + red += r > 150 && r > g + 40 && r > b + 40; + total++; + } + if (total && red > total * 0.70) { + if (first < 0) first = (int)y; + last = (int)y; + } + } + if (first < 0 || last - first < screen.height() * 0.04 || last - first > screen.height() * 0.12) return -1; + return (first + last) / (2.0 * screen.height()); + } + // Locate selected red value text separately from the row border. + bool selected_value(const ImageViewRGB32& screen, double y) const { + ImageViewRGB32 value = extract_box_reference(screen, ImageFloatBox{0.33, y - 0.025, 0.54, 0.05}); + size_t red = 0; + for (size_t py = 0; py < value.height(); py++) + for (size_t px = 0; px < value.width(); px++) { + uint32_t p = value.pixel(px, py); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + red += r > g + 20 && r > b + 20; + } + return red > value.width() * value.height() * 0.0005; + } + }; + + + // Compare binary visual shapes with small alignment tolerance. + // No text recognition is needed for fixed English option labels. + static double option_image_score(const ImageViewRGB32& screen, + const ImageFloatBox& box, const ImageRGB32& reference, bool red_only) { + ImageRGB32 actual = extract_box_reference(screen, box).scale_to(384, 32); + ImageRGB32 expected = reference.scale_to(384, 32); + auto ink = [red_only](uint32_t p){ + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + return red_only ? r > g + 20 && r > b + 20 : r + g + b < 500; + }; + double best = 0; + for (int dy = -2; dy <= 2; dy++) for (int dx = -2; dx <= 2; dx++) { + size_t overlap = 0, count = 0; + for (int y = 2; y < 30; y++) for (int x = 2; x < 382; x++) { + bool a = ink(actual.pixel(x + dx, y + dy)); + bool b = ink(expected.pixel(x, y)); + overlap += a && b; + count += a + b; + } + if (count) best = std::max(best, 2.0 * overlap / count); + } + return best; + } + + static bool autosave_off_highlight(const ImageViewRGB32& screen, double y) { + auto red_count = [&](const ImageFloatBox& box){ + ImageViewRGB32 image = extract_box_reference(screen, box); + size_t red = 0; + for (size_t py = 0; py < image.height(); py++) + for (size_t px = 0; px < image.width(); px++) { + uint32_t p = image.pixel(px, py); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + red += r > g + 30 && r > b + 30; + } + return red; + }; + size_t off = red_count({0.63, y - 0.022, 0.055, 0.044}); + size_t on = red_count({0.34, y - 0.022, 0.055, 0.044}); + // Require a clear Off highlight and reject any competing On highlight. + return off >= 10 && on * 10 < off; + } + + static void verify_tutorial_settings_once(VideoStream& stream, ProControllerContext& context) { + context.wait_for_all_requests(); + MenuDetector menu; + VideoOverlaySet detector_overlays(stream.overlay()); + menu.make_overlays(detector_overlays); + detector_overlays.add(COLOR_CYAN, {0.04, 0.065, 0.93, 0.70}, ""); + detector_overlays.add(COLOR_BLUE, {0.01, 0.005, 0.20, 0.025}, ""); + detector_overlays.add(COLOR_RED, {0.08, 0.08, 0.82, 0.67}, ""); + auto initial = stream.video().snapshot(); + if (!initial || !menu.detect(initial)) { + pbf_press_button(context, BUTTON_X, 80ms, 1000ms); + context.wait_for_all_requests(); + } + MenuWatcher menu_open; + if (wait_until(stream, context, 5s, {{menu_open}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Main menu not detected.", stream); + } + stream.log("[Tutorial settings] Main menu detected; settling for 500ms before Options navigation."); + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + TutorialOptionsDetector options_screen; + TutorialOptionPositionDetector position; + // Only two icons are available at this initial checkpoint. + // Verify the main menu before navigating to the neighboring icon. + bool options_open = false; + for (size_t attempt = 0; attempt < 2; attempt++) { + pbf_press_dpad(context, attempt == 0 ? DPAD_RIGHT : DPAD_LEFT, 80ms, 350ms); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + if (wait_for_options_screen(stream, context, options_screen, 5s)) { + options_open = true; + break; + } + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + MenuWatcher back_to_menu; + if (wait_until(stream, context, 5s, {{back_to_menu}}) != 0) break; + } + if (!options_open) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Options screen not detected.", stream); + } + stream.log("[Tutorial settings] Options screen detected."); + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + struct Setting { const char* label; const char* value; }; + const Setting settings[] = { + {"textspeed", "fast"}, {"changewindow", "type1"}, + {"battleeffects", "off"}, {"battlestyle", "set"}, + {"sendtoboxes", "automatic"}, {"givenicknames", "dontgive"}, + {"gyroscope", "use"}, {"autosave", "off"}, + {"shownicknames", "show"}, {"backgroundmusic", "8"}, + {"soundeffects", "8"}, + }; + for (size_t row = 0; row < sizeof(settings) / sizeof(settings[0]); row++) { + // Detect the selected row afresh after every navigation/correction. + double y = -1; + const std::string base = RESOURCE_PATH() + "PokemonBDSP/Autostory/Options/" + settings[row].label; + const ImageRGB32 label_reference(base + "_label.png"); + const ImageRGB32 value_reference(base + "_value.png"); + bool slider_reset = false; + bool correct = false; + for (size_t attempt = 0; attempt < 30; attempt++) { + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + if (frame && options_screen.detect(frame)) y = position.selected_row(frame); + else y = -1; + const bool value_visible = y >= 0 && position.selected_value(frame, y); + const double label_score = y >= 0 ? option_image_score(frame, + {0.08, y - 0.025, 0.24, 0.05}, label_reference, false) : 0; + stream.log("[Tutorial settings] Expected row=" + std::string(settings[row].label) + + " row_y=" + std::to_string(y) + " highlighted_value=" + std::to_string(value_visible) + + " label_match=" + std::to_string(label_score)); + if (y < 0 || !value_visible || label_score < 0.80) { + // A menu can be detected while its row/text is still + // fading in. Re-read without directional inputs first. + if (attempt < 4) { + pbf_wait(context, 200ms); + context.wait_for_all_requests(); + continue; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Unable to confirm expected row " + std::string(settings[row].label), stream); + } + // These are regular video-overlay boxes, so the existing + // overlay display control can show/hide every active ROI. + OverlayBoxScope row_overlay(stream.overlay(), COLOR_RED, + {0.07, y - 0.04, 0.84, 0.08}, ""); + OverlayBoxScope label_overlay(stream.overlay(), COLOR_YELLOW, + {0.08, y - 0.025, 0.24, 0.05}, ""); + OverlayBoxScope value_overlay(stream.overlay(), COLOR_GREEN, + {0.33, y - 0.025, 0.54, 0.05}, ""); + VideoOverlaySet autosave_rois(stream.overlay()); + if (row == 7) { + autosave_rois.add(COLOR_GREEN, {0.63, y - 0.022, 0.055, 0.044}); + autosave_rois.add(COLOR_RED, {0.34, y - 0.022, 0.055, 0.044}); + } + double score; + if (row >= 9) { + // Compare the numeric value rather than the long slider: + // neighboring slider levels otherwise look too similar. + ImageViewRGB32 number = value_reference.sub_image( + value_reference.width() * 82 / 100, 0, + value_reference.width() - value_reference.width() * 82 / 100, + value_reference.height()); + ImageRGB32 number_reference = number.scale_to(96, 32); + score = option_image_score(frame, + {0.7728, y - 0.025, 0.0972, 0.05}, number_reference, true); + } else { + score = option_image_score(frame, + {0.33, y - 0.025, 0.54, 0.05}, value_reference, true); + } + stream.log("[Tutorial settings] " + std::string(settings[row].label) + + " target match=" + std::to_string(score)); + // The reference recording uses inactive pale-red text; + // the focused live row has a thicker dark-red glyph mask. + // Verified Fast scores 0.734 while Slow/Normal score 0. + if (score >= 0.65 && (row != 7 || autosave_off_highlight(frame, y))) { + if (row == 7) { + for (size_t confirmation = 0; confirmation < 3; confirmation++) { + pbf_wait(context, 200ms); + context.wait_for_all_requests(); + auto check = stream.video().snapshot(); + double check_y = check ? position.selected_row(check) : -1; + if (!check || !options_screen.detect(check) || check_y < 0 || + option_image_score(check, {0.08, check_y - 0.025, 0.24, 0.05}, label_reference, false) < 0.80 || + !autosave_off_highlight(check, check_y)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave Off could not be confirmed. Stopping before movement.", stream); + } + } + } + stream.log("[Tutorial settings] Verified " + std::string(settings[row].label) + "=" + settings[row].value); + correct = true; + break; + } + stream.log("[Tutorial settings] Correcting " + std::string(settings[row].label)); + if (row >= 9 && !slider_reset) { + // Sliders clamp at zero. Establish a known endpoint, + // then increment; verify the target image afterward. + for (size_t step = 0; step < 12; step++) + pbf_press_dpad(context, DPAD_LEFT, 80ms, 100ms); + slider_reset = true; + } else { + pbf_press_dpad(context, DPAD_RIGHT, 80ms, 350ms); + } + context.wait_for_all_requests(); + } + if (!correct) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Could not apply " + std::string(settings[row].label), stream); + } + if (row + 1 < sizeof(settings) / sizeof(settings[0])) + pbf_press_dpad(context, DPAD_DOWN, 80ms, 350ms); + } + // Confirm settings before exiting: A A A A B B. + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1200ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, 10s, {{overworld}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Did not return to the overworld after saving options.", stream); + } + // Reopen the saved Options screen and independently verify Autosave. + // No route movement is permitted until this persisted-value check passes. + pbf_press_button(context, BUTTON_X, 80ms, 1000ms); + context.wait_for_all_requests(); + MenuWatcher verify_menu; + if (wait_until(stream, context, 5s, {{verify_menu}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave verification: Main menu not detected.", stream); + } + bool reopened = false; + for (size_t attempt = 0; attempt < 2; attempt++) { + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + if (wait_for_options_screen(stream, context, options_screen, 5s)) { reopened = true; break; } + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + MenuWatcher return_menu; + if (wait_until(stream, context, 5s, {{return_menu}}) != 0) break; + pbf_press_dpad(context, DPAD_RIGHT, 80ms, 350ms); + } + if (!reopened) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave verification: Could not reopen saved Options.", stream); + } + // Options starts on Text Speed; Autosave is seven rows below it. + for (size_t step = 0; step < 7; step++) + pbf_press_dpad(context, DPAD_DOWN, 80ms, 350ms); + context.wait_for_all_requests(); + const ImageRGB32 autosave_label(RESOURCE_PATH() + "PokemonBDSP/Autostory/Options/autosave_label.png"); + for (size_t confirmation = 0; confirmation < 3; confirmation++) { + auto check = stream.video().snapshot(); + double check_y = check ? position.selected_row(check) : -1; + if (!check || !options_screen.detect(check) || check_y < 0 || + option_image_score(check, {0.08, check_y - 0.025, 0.24, 0.05}, autosave_label, false) < 0.80 || + !autosave_off_highlight(check, check_y)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Saved Autosave setting is not verified Off. Stopping before movement.", stream); + } + pbf_wait(context, 200ms); + context.wait_for_all_requests(); + } + stream.log("[Tutorial settings] Autosave = Off: confirmed after reopening saved Options (3 frames)."); + // Confirm settings before exiting: A A A A B B. + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1200ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + OverworldWatcher verified_exit; + if (wait_until(stream, context, 10s, {{verified_exit}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave verification: Could not return to the overworld.", stream); + } + // Report inferred values explicitly: these are visual matches, + // allowing the user to compare our conclusions with the screen. + stream.log("[Tutorial settings] Completed. Values believed applied (visual matches before saving):"); + stream.log("[Tutorial settings] Text Speed = Fast; Change Window = Type 1; Battle Effects = Off; Battle Style = Set."); + stream.log("[Tutorial settings] Send to Boxes = Automatic; Give Nicknames = Don't give; Gyroscope = Use."); + stream.log("[Tutorial settings] Autosave = Off; Show Nicknames = Show; Background Music = 8; Sound Effects = 8."); + } + + + void verify_tutorial_settings(VideoStream& stream, ProControllerContext& context) { + // Settings-only recovery: initial attempt plus three retries. + // Never permit route movement unless the complete check succeeds. + constexpr size_t MAX_RETRIES = 3; + for (size_t attempt = 0; attempt <= MAX_RETRIES; attempt++) { + stream.log("[Tutorial settings] Attempt " + std::to_string(attempt + 1) + "/4."); + try { + verify_tutorial_settings_once(stream, context); + return; + } catch (OperationFailedException& error) { + stream.log("[Tutorial settings] Attempt failed: " + error.message(), COLOR_ORANGE); + if (attempt == MAX_RETRIES) { + stream.log("[Tutorial settings] All three retries exhausted; stopping before movement.", COLOR_RED); + throw; + } + // Back out of Options, save prompts, bag, or menu without + // moving the player or resetting/loading the game. + stream.log("[Tutorial settings] Returning to overworld before retry."); + context.wait_for_all_requests(); + TutorialOptionsDetector options_screen; + const auto failed_frame = stream.video().snapshot(); + if (failed_frame && options_screen.detect(failed_frame)) { + // Finish the known Options save/exit sequence before + // observing state; never cancel it on an Options frame + // misclassified as overworld by the generic detector. + for (size_t press = 0; press < 4; press++) + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + } + pbf_mash_button(context, BUTTON_B, 3000ms); + context.wait_for_all_requests(); + OverworldDetector overworld; + MenuDetector menu; + BlackScreenDetector black; + bool recovered = false; + size_t confirmations = 0; + for (size_t check = 0; check < 15; check++) { + const auto frame = stream.video().snapshot(); + const bool clear = frame && !options_screen.detect(frame) && + !menu.detect(frame) && !black.detect(frame) && bag_pocket_position(frame) == 0 && overworld.detect(frame); + if (clear) { + if (++confirmations >= 3) { recovered = true; break; } + pbf_wait(context, 200ms); + } else { + confirmations = 0; + pbf_mash_button(context, BUTTON_B, 500ms); + } + context.wait_for_all_requests(); + } + stream.log(recovered ? "[Tutorial settings] Recovery confirmed outside Options/menu; retrying." + : "[Tutorial settings] Recovery not confirmed; next attempt will recheck the screen."); + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + } + } + } + + void set_time_fast( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const DateTime& target, + bool preserve_date + ) { + env.log("[AutoStory] Fast date set starting...", COLOR_ORANGE); + + // 1) Home + NintendoSwitch::go_home(env.console, context); + + // 2) Navigate to Date/Time settings quickly. + NintendoSwitch::home_to_date_time(env.console, context, true); + + // 3) Enter date editor. + pbf_press_button(context, BUTTON_A, 80ms, 240ms); + + // 4) Wait for date screen and set exact time. + NintendoSwitch::DateChangeWatcher date_reader(env.console); + int ret = wait_until( + env.console, context, 10s, + { date_reader } + ); + if (ret < 0) { + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to enter Date Change screen.", + env.console + ); + } + + { + VideoOverlaySet overlays(env.console.overlay()); + date_reader.make_overlays(overlays); + DateTime effective_target = target; + if (preserve_date) { + context.wait_for_all_requests(); + const auto snapshot = env.console.video().snapshot(); + if (!snapshot) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Clock-only setup: No date-editor frame.", env.console); + const DateTime current = date_reader.read_date(env.console, snapshot.frame).second; + if (current.year < 2000 || current.year > 2060 || current.month < 1 || current.month > 12 || + current.day < 1 || current.day > 31) + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Clock-only setup: Could not reliably read the current console date.", env.console); + effective_target.year = current.year; + effective_target.month = current.month; + effective_target.day = current.day; + env.log("[AutoStory] Clock-only target=" + std::to_string((int)target.hour) + ":" + + std::to_string((int)target.minute) + "; preserving date=" + std::to_string(current.year) + "-" + + std::to_string((int)current.month) + "-" + std::to_string((int)current.day) + "."); + } + date_reader.set_date(env.program_info(), env.console, context, effective_target); + } + + // 5) Commit date. + pbf_press_button(context, BUTTON_A, 160ms, 240ms); + + // 6) Back to game. + pbf_press_button( + context, BUTTON_HOME, 160ms, + ConsoleSettings::instance().SETTINGS_TO_HOME_DELAY0 + ); + NintendoSwitch::resume_game_from_home(env.console, context, false); + + context.wait_for_all_requests(); + context.wait_for(500ms); + } + + + // --------------------------------------------------------------------------- + // Natalie battle + // --------------------------------------------------------------------------- + // Early moves the starters and their foes have, as type, power and + // most PP. A status move has power 0. + struct KnownMove { const char* name; const char* type; int power; int pp; }; + static const KnownMove KNOWN_MOVES[] = { + {"pound", "normal", 40, 35}, {"tackle", "normal", 40, 35}, {"scratch", "normal", 40, 35}, + {"quickattack", "normal", 40, 30}, {"headbutt", "normal", 70, 15}, + {"watergun", "water", 40, 25}, {"bubble", "water", 40, 30}, {"bubblebeam", "water", 65, 20}, + {"ember", "fire", 40, 25}, {"flamewheel", "fire", 60, 25}, {"machpunch", "fighting", 40, 30}, + {"absorb", "grass", 20, 25}, {"razorleaf", "grass", 55, 25}, {"bite", "dark", 60, 25}, + {"peck", "flying", 35, 35}, {"wingattack", "flying", 60, 35}, {"metalclaw", "steel", 50, 35}, + {"growl", "normal", 0, 40}, {"leer", "normal", 0, 30}, {"tailwhip", "normal", 0, 30}, + {"withdraw", "water", 0, 40}, {"taunt", "dark", 0, 20}, + }; + + // A move button's PP left, "8/25" at its right end; -1 if it cannot be + // read. The text is dark, or yellow to red as PP runs low, so text is + // whatever differs from the button's own color. `max_pp` helps when + // the OCR reads the slash as a digit. + static int read_move_pp(const ImageViewRGB32& screen, const ImageFloatBox& button, int max_pp) { + const ImageViewRGB32 box = extract_box_reference(screen, ImageFloatBox{ + button.x + button.width * 0.75, button.y + button.height * 0.10, + button.width * 0.21, button.height * 0.80}); + if (box.width() == 0 || box.height() == 0) return -1; + std::vector pixels; + for (size_t y = 0; y < box.height(); y++) for (size_t x = 0; x < box.width(); x++) pixels.push_back(box.pixel(x, y)); + auto brightness = [](uint32_t p) { return ((p >> 16) & 255) + ((p >> 8) & 255) + (p & 255); }; + std::vector sorted = pixels; + std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end(), + [&](uint32_t a, uint32_t b) { return brightness(a) < brightness(b); }); + const uint32_t background = sorted[sorted.size() / 2]; + const size_t pad = 8; + ImageRGB32 text(box.width() + 2 * pad, box.height() + 2 * pad); + text.fill(0xffffffff); + for (size_t y = 0; y < box.height(); y++) { + for (size_t x = 0; x < box.width(); x++) { + const uint32_t p = box.pixel(x, y); + int distance = 0; + for (int shift : {16, 8, 0}) distance += std::abs((int)((p >> shift) & 255) - (int)((background >> shift) & 255)); + if (distance > 120) text.pixel(x + pad, y + pad) = 0xff000000; + } + } + const std::string read = OCR::tesseract_ocr_read(Language::English, text); + const size_t slash = read.find('/'); + std::string digits; + for (size_t i = 0; i < read.size() && i < slash; i++) if (std::isdigit((unsigned char)read[i])) digits.push_back(read[i]); + if (slash == std::string::npos && max_pp > 0) { + // "29135" for 29/35: drop the most PP and the misread slash. + const std::string max = std::to_string(max_pp); + if (digits.size() > max.size() + 1 && digits.compare(digits.size() - max.size(), max.size(), max) == 0) { + digits.resize(digits.size() - max.size() - 1); + } else { + return -1; + } + } + if (digits.empty() || digits.size() > 2) return -1; + return std::stoi(digits); + } + + // The best move on the move menu: the most powerful one we know, and + // on a tie one that is not Normal (an early starter's own type). + // Reads the names with OCR; 1 if none is known. + static size_t pick_best_move(VideoStream& stream, const ImageViewRGB32& screen, + const BattleMoveMenuState& moves, const std::string& label) { + // Keep the menu as it looked, to check the reads against. + { + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/moves/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.save(folder + now_to_filestring() + "-" + label + ".png"); + } + size_t best = 1; + int best_score = -1; + bool any_attack = false; + size_t last_not_status = 1; + for (size_t i = 0; i < moves.move_count; i++) { + // The name is dark text on a light button (grey, or light blue under the + // cursor). Take the box right of the type icon and left of the PP, tall + // enough for a status move's name, which sits lower: it has no "Effective" + // line under it. + const ImageFloatBox& button = moves.buttons[i]; + const ImageFloatBox name_box{button.x + button.width * 0.17, button.y + button.height * 0.08, + button.width * 0.53, button.height * 0.80}; + const ImageViewRGB32 box = extract_box_reference(screen, name_box); + // Text is darker than halfway between the background and the darkest pixel; + // a fixed cut thins the strokes on the blue button. + std::vector gray(box.width() * box.height()); + for (size_t y = 0; y < box.height(); y++) { + for (size_t x = 0; x < box.width(); x++) { + const uint32_t pixel = box.pixel(x, y); + gray[y * box.width() + x] = (int)(((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff)) / 3; + } + } + std::vector sorted = gray; + std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end()); + const int background = sorted[sorted.size() / 2]; + const int darkest = *std::min_element(gray.begin(), gray.end()); + const int threshold = (background + darkest) / 2; + // The name is the tallest band of text rows; "Effective" is smaller. + size_t band_top = 0, band_bottom = 0; + for (size_t y = 0; y < box.height();) { + auto has_text = [&](size_t row) { + for (size_t x = 0; x < box.width(); x++) if (gray[row * box.width() + x] < threshold) return true; + return false; + }; + if (!has_text(y)) { y++; continue; } + size_t end = y; + while (end < box.height() && has_text(end)) end++; + if (end - y > band_bottom - band_top) { band_top = y; band_bottom = end; } + y = end; + } + // Black text on white, with a margin, for the OCR. + const size_t pad = 6; + const size_t top = band_top >= 4 ? band_top - 4 : 0; + const size_t bottom = std::min(box.height(), band_bottom + 4); + ImageRGB32 text(box.width() + 2 * pad, bottom - top + 2 * pad); + text.fill(0xffffffff); + for (size_t y = top; y < bottom; y++) { + for (size_t x = 0; x < box.width(); x++) { + if (gray[y * box.width() + x] < threshold) text.pixel(x + pad, y - top + pad) = 0xff000000; + } + } + std::string name; + // Move names only where there is a reader for the game's + // language; an unread move counts as a plain attack. + for (unsigned char ch : story_reads_text() ? OCR::tesseract_ocr_read(g_story_language, text) : std::string()) { + if (std::isalpha(ch)) name.push_back((char)std::tolower(ch)); + } + // A slot read well once keeps its name when a later read fails. + static std::array last_read; + const KnownMove* known = nullptr; + for (int pass = 0; pass < 2 && !known; pass++) { + if (pass == 1) name = last_read[i]; + for (const KnownMove& move : KNOWN_MOVES) { + // The longest name that appears wins: "bubblebeam" over "bubble". + if (name.find(move.name) != std::string::npos && + (!known || std::string(move.name).size() > std::string(known->name).size())) { + known = &move; + } + } + } + if (known) last_read[i] = known->name; + // A move with no PP left cannot be chosen; the game says so and + // keeps the menu up. + const int pp = read_move_pp(screen, button, known ? known->pp : 0); + stream.log("[" + label + "] Move " + std::to_string(i + 1) + " PP left: " + (pp < 0 ? std::string("unread") : std::to_string(pp)) + "."); + if (pp == 0) { + continue; + } + // An unreadable move counts as a plain attack, ahead of known + // status moves. + const int score = known ? known->power * 2 + (std::string(known->type) != "normal" ? 1 : 0) : 40 * 2 - 1; + stream.log("[" + label + "] Move " + std::to_string(i + 1) + " reads \"" + name + "\"" + + (known ? std::string(" = ") + known->name + " (" + known->type + ", " + std::to_string(known->power) + ")" : " (unknown)") + "."); + if (score > best_score) { best_score = score; best = i + 1; } + if (known && known->power > 0) any_attack = true; + if (!known || known->power > 0) last_not_status = i + 1; + } + // No attack read: the newest move is the starter's own-type attack + // (Water Gun, Ember, Absorb), unless it is a status move. + if (!any_attack) best = last_not_status; + stream.log("[" + label + "] Best move: " + std::to_string(best) + "."); + return best; + } + + static void select_battle_move_slot(VideoStream& stream, ProControllerContext& context, + BattleType battle_type, size_t target, const std::string& label, + bool confirm_default_target = false, size_t current_move = 1) { + BattleMenuDetector commands(battle_type); + VideoOverlaySet overlays(stream.overlay()); + commands.make_overlays(overlays); + overlays.add(COLOR_GREEN, {0.90, 0.48, 0.075, 0.505}); + // Reenter Fight from an already-open list without assuming + // that this resets the remembered move cursor. + if (confirm_default_target) { + context.wait_for_all_requests(); + const auto frame=stream.video().snapshot(); + if (frame && detect_battle_move_menu(frame).move_count) { + pbf_press_button(context,BUTTON_B,80ms,500ms); + } + } + bool fight = false; + size_t confirmed = 0; + for (size_t check = 0; check < 30; check++) { + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + BattleMenuOption selected = frame ? battle_menu_selected_option(commands, frame) : BattleMenuOption::UNKNOWN; + stream.log("[" + label + "] Command cursor=" + std::to_string((int)selected + 1) + "; target Battle=1."); + if (selected == BattleMenuOption::BATTLE) { + if (++confirmed >= 3) { fight = true; break; } + pbf_wait(context, 100ms); + } else { + confirmed = 0; + if (selected == BattleMenuOption::UNKNOWN) pbf_wait(context, 150ms); + else pbf_press_dpad(context, DPAD_UP, 80ms, 250ms); + } + } + if (!fight) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Could not confirm Battle command; A not pressed.", stream); + pbf_press_button(context,BUTTON_A,80ms,confirm_default_target ? 1000ms : 500ms); + if (confirm_default_target) { + // Reuse the farming move/target confirmation sequence, but + // navigate relative to this battle's retained move cursor. + // Reopening Fight does not reset it to move 1. + size_t available=0,stable=0; + for (size_t check=0;check<20 && stable<3;check++) { + context.wait_for_all_requests(); + const auto frame=stream.video().snapshot(); + const auto moves=frame ? detect_battle_move_menu(frame) : BattleMoveMenuState{}; + if (moves.move_count && moves.move_count==available) stable++; + else {available=moves.move_count;stable=moves.move_count ? 1:0;} + if (stable<3) pbf_wait(context,100ms); + } + if (stable<3 || available "+std::to_string(target)+"."); + for (size_t slot=current_move;slottarget;slot--) pbf_press_dpad(context,DPAD_UP,80ms,400ms); + pbf_press_button(context,BUTTON_A,80ms,1000ms); + pbf_press_button(context,BUTTON_A,80ms,3000ms); + return; + } + confirmed = 0; + size_t previous_move_count = 0, stable_move_count = 0; + for (size_t check = 0; check < 30; check++) { + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + const BattleMoveMenuState moves = frame ? detect_battle_move_menu(frame) : BattleMoveMenuState{}; + const int slot = (int)moves.selected_slot; + VideoOverlaySet move_overlays(stream.overlay()); + for (size_t i = 0; i < moves.move_count; i++) + move_overlays.add(moves.selected_slot == i + 1 ? COLOR_GREEN : COLOR_YELLOW, moves.buttons[i]); + stream.log("[" + label + "] Available moves=" + std::to_string(moves.move_count) + + "; selected slot=" + std::to_string(slot) + "/4; target=" + std::to_string(target) + "."); + if (moves.move_count == previous_move_count && moves.move_count > 0) stable_move_count++; + else { previous_move_count = moves.move_count; stable_move_count = 1; confirmed = 0; } + // Target 0: pick the best move once the menu has settled. + if (target == 0 && stable_move_count >= 3 && frame) { + target = pick_best_move(stream, *frame.frame, moves, label); + } + if (moves.move_count > 0 && moves.move_count < target && stable_move_count >= 3) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Move slot " + std::to_string(target) + " is absent; no move confirmed.", stream); + } + if (slot == (int)target && stable_move_count >= 3) { + if (++confirmed >= 3) { + stream.log("[" + label + "] Move " + std::to_string(target) + " confirmed; using it."); + pbf_press_button(context, BUTTON_A, 80ms, 3000ms); + return; + } + pbf_wait(context, 100ms); + } else { + confirmed = 0; + if (slot == 0 || slot == (int)target) pbf_wait(context, 150ms); + else pbf_press_dpad(context, slot < (int)target ? DPAD_DOWN : DPAD_UP, 80ms, 250ms); + } + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Could not confirm move " + std::to_string(target) + "; A not pressed.", stream); + } + + // --------------------------------------------------------------------------- + // Starly battle + // --------------------------------------------------------------------------- + + void fight_starly(VideoStream& stream, ProControllerContext& context) { + stream.log("[AutoStory] Starting Starly battle...", COLOR_BLUE); + + // Declare OUTSIDE the loop so accumulated detector state + // (trigger counts, black-screen transitions) is preserved across frames. + // STARTER type: player has no Pokeballs, so ball-count areas won't + // be white. STARTER skips that check and only verifies status bars + // and the Fight button color. + BattleMenuWatcher battle_menu(BattleType::STARTER, COLOR_YELLOW); + EndBattleWatcher end_battle; + + while (true) { + context.wait_for_all_requests(); + + // Phase 1: wait for either battle menu or battle end. + int ret = run_until( + stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, + { + {battle_menu}, + {end_battle}, + } + ); + + switch (ret) { + case 0: + { + stream.log("[AutoStory] Battle menu detected. Selecting move...", COLOR_BLUE); + + // Select Fight, then move slot 1. + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 3000ms); + // Loop back - mash B through animations until next menu or end. + break; + } + case 1: + { + stream.log("[AutoStory] Starly battle complete.", COLOR_GREEN); + + // Mash through XP / level-up / dialog screens back to overworld. + OverworldWatcher overworld; + int cleanup_ret = run_until( + stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, + { {overworld} } + ); + if (cleanup_ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Starly battle: Overworld cleanup timed out.", stream); + } + return; + } + default: + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Starly battle timed out.", stream); + } + } + } + + // --------------------------------------------------------------------------- + // Tristan battle + // --------------------------------------------------------------------------- + // Two grass rows at 240 ms each, with room to spare: the wall stops us. + static constexpr Milliseconds CATCH_GRASS_WALK = 900ms; + + static void encounter_one_wild_pokemon(VideoStream& stream, ProControllerContext& context) { + context.wait_for_all_requests(); + pbf_wait(context, 1000ms); + + bool encounter_found = false; + const auto deadline = std::chrono::steady_clock::now() + 180s; + + // Walk up and down the catch grass until a battle starts. Its three rows + // sit between tree walls, so each walk ends at a wall, on the same tile + // every time, after two new grass tiles. Short sideways steps drift and + // can rock across one tile edge without entering new grass. + while (!encounter_found && std::chrono::steady_clock::now() < deadline) { + BlackScreenWatcher battle_start; + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + + route_turn(stream, context, Pokemon::Direction::NORTH); + pbf_move_left_joystick(context, {0, 1}, CATCH_GRASS_WALK, 200ms); + context.wait_for_all_requests(); + + int ret = wait_until( + stream, + context, + 1500ms, + { + {battle_start}, + {battle_menu}, + {battle_dialog}, + } + ); + + if (ret >= 0) { + encounter_found = true; + break; + } + + route_turn(stream, context, Pokemon::Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, CATCH_GRASS_WALK, 200ms); + context.wait_for_all_requests(); + + ret = wait_until( + stream, + context, + 1500ms, + { + {battle_start}, + {battle_menu}, + {battle_dialog}, + } + ); + + if (ret >= 0) { + encounter_found = true; + break; + } + } + + if (!encounter_found) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Wild encounter: walking up and down the grass found no battle within three minutes.", stream); + stream.log("[AutoStory] Wild battle detected!", COLOR_GREEN); + + // Wait until the actual battle menu appears. + BattleMenuWatcher confirm_menu(BattleType::STANDARD); + + int menu_ret = wait_until( + stream, + context, + 15s, + { + {confirm_menu}, + } + ); + + if (menu_ret < 0) { + stream.log( + "[AutoStory] Failed to detect battle menu.", + COLOR_RED + ); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "[AutoStory] Failed to detect battle menu.", stream); + } + + stream.log("[AutoStory] Battle menu detected.", COLOR_GREEN); + + context.wait_for_all_requests(); + pbf_wait(context, 500ms); + + } + + static void confirm_battle_overworld(VideoStream& stream, ProControllerContext& context, + const std::string& label) { + OverworldDetector overworld; + ShortDialogDetector dialogue(COLOR_YELLOW); + BlackScreenDetector black; + VideoOverlaySet overlays(stream.overlay()); + overworld.make_overlays(overlays); + dialogue.make_overlays(overlays); + black.make_overlays(overlays); + const auto deadline = std::chrono::steady_clock::now() + 180s; + auto quiet_since = std::chrono::steady_clock::now(); + bool finished = false; + while (std::chrono::steady_clock::now() < deadline) { + const auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No video frame.", stream); + const auto now = std::chrono::steady_clock::now(); + if (dialogue.detect(frame) || black.detect(frame) || !overworld.detect(frame)) { + quiet_since = now; + } else if (now - quiet_since >= 4s) { + finished = true; + break; + } + pbf_mash_button(context, BUTTON_B, 100ms); + context.wait_for_all_requests(); + } + if (!finished) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Dialogue-free overworld not confirmed for four seconds.", stream); + context.wait_for_all_requests(); + } + + // Our HP bar in the battle's bottom-left panel, as a fraction; -1 if + // no bar is there. Measured on 1920x1080 frames: the bar runs from + // x 88 to 466 on rows 961 to 965. + static double battle_player_hp(const ImageViewRGB32& frame) { + const ImageViewRGB32 bar = extract_box_reference(frame, ImageFloatBox{0.046, 0.890, 0.197, 0.004}); + size_t filled = 0; + for (size_t x = 0; x < bar.width(); x++) { + size_t color = 0; + for (size_t y = 0; y < bar.height(); y++) { + const uint32_t p = bar.pixel(x, y); + const int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + const bool green = g > r + 30 && g > b + 20; + const bool yellow = r > 150 && g > 120 && b < 100; + const bool red = r > 150 && r > g + 50 && r > b + 50; + color += green || yellow || red; + } + filled += color > bar.height() / 2; + } + return bar.width() && filled ? (double)filled / bar.width() : -1; + } + + enum class BattleHeal { NOT_NEEDED, USED, NONE_LEFT }; + + // At the battle menu: if our HP is under `heal_below`, use a Potion + // from the Bag on the lead, which takes the turn. Otherwise the + // battle menu is left up. + static BattleHeal battle_heal_if_low(VideoStream& stream, ProControllerContext& context, + BattleType battle_type, double heal_below, const std::string& label) { + context.wait_for_all_requests(); + double hp = -1; + for (size_t check = 0; check < 5 && hp < 0; check++) { + const auto frame = stream.video().snapshot(); + if (frame) hp = battle_player_hp(*frame.frame); + if (hp < 0) pbf_wait(context, 150ms); + context.wait_for_all_requests(); + } + stream.log("[" + label + "] Our HP bar reads " + std::to_string((int)(hp * 100)) + "%."); + if (hp < 0 || hp >= heal_below) return BattleHeal::NOT_NEEDED; + BattleMenuDetector commands(battle_type); + bool on_bag = false; + for (size_t check = 0; check < 30 && !on_bag; check++) { + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const BattleMenuOption selected = frame ? battle_menu_selected_option(commands, frame) : BattleMenuOption::UNKNOWN; + if (selected == BattleMenuOption::BAG) on_bag = true; + else if (selected == BattleMenuOption::UNKNOWN) pbf_wait(context, 150ms); + else pbf_press_dpad(context, (int)selected < (int)BattleMenuOption::BAG ? DPAD_DOWN : DPAD_UP, 80ms, 300ms); + } + if (!on_bag) { + stream.log("[" + label + "] Could not reach the Bag command; fighting on.", COLOR_ORANGE); + return BattleHeal::NONE_LEFT; + } + pbf_press_button(context, BUTTON_A, 80ms, 2000ms); + if (!find_bag_item_in_open_bag(stream, context, "potion", 1)) { + stream.log("[" + label + "] No Potion left; fighting on.", COLOR_ORANGE); + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + context.wait_for_all_requests(); + return BattleHeal::NONE_LEFT; + } + // Potion, then the lead in the party list, then Use. + stream.log("[" + label + "] Using a Potion on the lead.", COLOR_BLUE); + pbf_press_button(context, BUTTON_A, 80ms, 1500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1500ms); + pbf_press_button(context, BUTTON_A, 80ms, 2500ms); + context.wait_for_all_requests(); + return BattleHeal::USED; + } + + static void fight_using_move_list( + VideoStream& stream, ProControllerContext& context, + BattleType battle_type, const std::vector& move_slots, + const std::string& label, bool confirm_default_target, bool post_battle_dialogue_cleanup, + bool doubles = false, bool switch_on_faint = false + ) { + if (move_slots.empty() || std::any_of(move_slots.begin(),move_slots.end(), + [](size_t slot) { return slot>4; })) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label+": Supply a nonempty move list containing only positions 0 (best) through 4.",stream); + } + size_t move_index=0; + // The first Fight menu starts at move 1. Later turns retain + // this Pokemon's previous selection; a new battle resets it. + size_t current_move=1; + stream.log( + "[AutoStory] Starting " + label + " battle...", + COLOR_BLUE + ); + + // Scripted battles wait on the menu detector below, so no + // extra fixed delay is needed after a caller confirms it. + // Preserve the existing early-game transition delay. + if (!confirm_default_target) pbf_wait(context,5000ms); + + bool battle_menu_seen = false; + bool out_of_potions = false; + size_t potions_used = 0; + size_t turns = 0; + size_t faint_switches = 0; + const bool watch_learn_move = confirm_default_target || doubles || switch_on_faint; + EndBattleWatcher end_battle; + + while (true) { + + context.wait_for_all_requests(); + + BattleMenuWatcher battle_menu(battle_type); + // Reuse the farming handlers' move-learning prompt recovery. + SelectionArrowFinder learn_move(stream.overlay(),{0.50,0.62,0.40,0.18},COLOR_YELLOW); + + int ret = run_until( + stream, + context, + [&](ProControllerContext& context) { + // Clear narration with B while watching for commands. + // A here can open Fight before selection begins. + // With faint switching, a quiet 30 s means the party + // screen after a faint, which B cannot leave. + pbf_mash_button(context,BUTTON_B,switch_on_faint ? 30000ms : 120000ms); + }, + { + {battle_menu}, + battle_menu_seen + ? PeriodicInferenceCallback{end_battle} + : PeriodicInferenceCallback{}, + watch_learn_move ? PeriodicInferenceCallback{learn_move} : PeriodicInferenceCallback{}, + } + ); + + switch (ret) { + + // ------------------------------------------------- + // Battle menu detected. + // ------------------------------------------------- + case 0: + battle_menu_seen = true; + // Early battles take a few turns; this many means the chosen + // move is not being used (no PP, a misread menu). + if (++turns > 25) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": still at the battle menu after 25 turns.", stream); + } + + stream.log( + "[AutoStory] Battle menu detected.", + COLOR_BLUE + ); + + // A trainer battle we may lose: a Potion when HP is low. + if (battle_type == BattleType::TRAINER && !out_of_potions && potions_used < 4) { + const BattleHeal heal = battle_heal_if_low(stream, context, battle_type, 0.35, label); + if (heal == BattleHeal::USED) { + potions_used++; + break; + } + out_of_potions = heal == BattleHeal::NONE_LEFT; + } + stream.log("["+label+"] Move list position "+std::to_string(move_index+1)+ + ": use move "+std::to_string(move_slots[move_index])+"."); + select_battle_move_slot(stream, context, battle_type, move_slots[move_index], label, + confirm_default_target,current_move); + current_move=move_slots[move_index]; + // A double battle asks for the target next; B would + // back out of it. The default target is an opponent. + if (doubles) { + pbf_wait(context,1200ms); + pbf_press_button(context,BUTTON_A,80ms,1500ms); + } + // Advance once per move selection; retain the final entry. + if (move_index+1(stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, {{overworld}}); + if (cleanup_ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Post-battle cleanup timed out.", stream); + } + context.wait_for_all_requests(); + confirm_battle_overworld(stream, context, label + " cleanup"); + return; + } + + case 2: + stream.log("["+label+"] Declining move learning; preserving scripted move positions."); + pbf_move_right_joystick(context,{0,-1},160ms,840ms); + pbf_press_button(context,BUTTON_ZL,160ms,840ms); + break; + + default: + // After a faint the game shows the party: Down to the + // next Pokemon, A, then A on "Send out" (by hand, + // 2026-10-29 against Roark). + if (switch_on_faint && faint_switches < 5) { + faint_switches++; + stream.log("[" + label + "] No battle menu for 30 s: a faint; sending out the next Pokemon.", COLOR_ORANGE); + pbf_press_dpad(context, DPAD_DOWN, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1200ms); + pbf_press_button(context, BUTTON_A, 80ms, 2000ms); + break; + } + stream.log( + "[AutoStory] " + label + " battle timeout.", + COLOR_RED + ); + + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "[AutoStory] " + label + " battle timeout.", stream); + } + } + } + + // Preserve the existing early-game callers and their B text cleanup. + static void fight_using_move_slot(VideoStream& stream, ProControllerContext& context, + BattleType battle_type, size_t move_slot, const std::string& label) { + fight_using_move_list(stream,context,battle_type,{move_slot},label,false,true); + } + + void clear_post_battle_dialogue(VideoStream& stream, ProControllerContext& context, + Button button, Milliseconds quiet_period, + const char* instruction_card) { + context.wait_for_all_requests(); + std::unique_ptr card; + if (instruction_card) card = std::make_unique( + std::make_shared(RESOURCE_PATH()+instruction_card), + ImageFloatBox{.04,.32,.92,.34},30.0); + ShortDialogDetector dialogue(COLOR_YELLOW); + BlackScreenDetector black; + VideoOverlaySet overlays(stream.overlay()); + dialogue.make_overlays(overlays); + black.make_overlays(overlays); + const auto deadline = std::chrono::steady_clock::now() + 120s; + auto absent_since = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() < deadline) { + const auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Post-battle dialogue: No video frame.", stream); + const auto now = std::chrono::steady_clock::now(); + const bool dialogue_visible = dialogue.detect(*frame.frame); + const bool card_visible = card && card->detect(*frame.frame); + if (dialogue_visible || card_visible || black.detect(*frame.frame)) absent_since = now; + else if (now - absent_since >= quiet_period) { + stream.log("[Post-battle dialogue] " + std::to_string(quiet_period.count()) + + "ms without dialogue confirmed."); + return; + } + // A can interact again once dialogue disappears. Only confirm + // visible prompts; B retains the existing post-battle behavior. + if (card_visible) pbf_press_button(context, BUTTON_B, 80ms, 100ms); + else if (button == BUTTON_A && !dialogue_visible) pbf_wait(context, 100ms); + else pbf_mash_button(context, button, 100ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Post-battle dialogue: Three clear seconds not confirmed.", stream); + } + + void align_to_overworld_landmarks(VideoStream& stream, ProControllerContext& context, + OverworldMatchResult (*matcher)(const ImageRGB32&), const std::string& label, bool allow_story_start, bool horizontal_first, + bool search_down_first, + OverworldMatchResult (*entry_matcher)(const ImageRGB32&), + bool* entry_detected, + double arrival_tolerance_x, double arrival_tolerance_y, + double moderate_entry_confidence, bool* rock_acquisition_timed_out) { + if (rock_acquisition_timed_out) *rock_acquisition_timed_out=false; + auto missing_rocks_since=std::chrono::steady_clock::time_point::max(); + if (entry_detected) *entry_detected=false; + context.wait_for_all_requests(); + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + ShortDialogDetector dialogue(COLOR_YELLOW); + BattleMenuDetector battle(BattleType::TRAINER, COLOR_GREEN); + size_t confirmations = 0, missing = 0; + for (size_t attempt = 0; attempt < 80; attempt++) { + const auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No video frame.", stream); + if (entry_matcher){ + BlackScreenDetector transition; + if (entry_matcher(*frame.frame).located || transition.detect(*frame.frame)){ + BlackScreenWatcher visible(COLOR_RED,{0.1,0.1,0.8,0.8}, + 100,10,BlackScreenWatcher::FinderType::GONE,500ms); + if (wait_until(stream,context,30s,{{visible}})!=0) + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label+": Building transition did not finish.",stream); + const auto inside=stream.video().snapshot(); + if (!inside || !entry_matcher(*inside.frame).located) + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label+": Expected interior not confirmed after transition.",stream); + if(entry_detected)*entry_detected=true; + stream.log("["+label+"] Expected interior confirmed; ending outdoor alignment.",COLOR_GREEN); + return; + } + } + // The story can start before the exact reference point. Hand off + // to battle preparation instead of steering during dialogue. + if (dialogue.detect(*frame.frame) || battle.detect(*frame.frame)) { + if (allow_story_start) { + stream.log("[" + label + "] Story dialogue/battle started; handing off without further movement."); + return; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Unexpected dialogue/battle during alignment.", stream); + } + const auto result = matcher(*frame.frame); + if (result.located) + missing_rocks_since=std::chrono::steady_clock::time_point::max(); + VideoOverlaySet overlays(stream.overlay()); + for (const auto& landmark : result.landmarks) { + overlays.add(COLOR_CYAN, landmark.search); + if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); + stream.log("[" + label + "] " + landmark.name + " score=" + + std::to_string(landmark.score) + ", margin=" + std::to_string(landmark.margin) + + ", offset=(" + std::to_string(landmark.dx) + "," + std::to_string(landmark.dy) + ")"); + } + stream.log("[" + label + "] located=" + std::to_string(result.located) + + ", coarse=" + std::to_string(result.coarse) + ", at_target=" + std::to_string(result.at_target) + + ", consensus offset=(" + std::to_string(result.dx) + "," + std::to_string(result.dy) + ")"); + const double entry_confidence=moderate_entry_confidence>0 ? overworld_entry_confidence(result) : 0; + const bool moderate_arrival=moderate_entry_confidence>0 && entry_confidence>=moderate_entry_confidence; + if(moderate_entry_confidence>0) stream.log("["+label+"] Exterior entry confidence="+ + std::to_string(entry_confidence)+", moderate threshold="+std::to_string(moderate_entry_confidence)+ + ", strict target="+std::to_string(result.at_target)); + // Explicit loose bounds are for non-interacting route handoffs. + // Default callers still require the matcher's precise arrival. + const bool rough_arrival=arrival_tolerance_x>0 && arrival_tolerance_y>0 && + result.located && std::abs(result.dx)<=arrival_tolerance_x && + std::abs(result.dy)<=arrival_tolerance_y; + if (result.at_target || rough_arrival || moderate_arrival) { + if (++confirmations >= 3) { stream.log("[" + label + "] Position confirmed."); return; } + pbf_wait(context, 250ms); + } else if (result.located) { + confirmations = 0; + missing = 0; + // Wait at an aligned pose while strict visual support + // recovers, rather than stepping away under a cloud. + if ((matcher==match_galactic_hq_jupiter || + matcher==match_route208_hannah_departure || matcher==match_route208_stair_top) && result.coarse && + std::abs(result.dx)<=1.0/128 && std::abs(result.dy)<=1.0/54){ + pbf_wait(context,500ms); + context.wait_for_all_requests(); + continue; + } + // A coarse match cannot confirm position, but two accepted + // anchors can establish a safe horizontal direction when both + // offsets are well outside the uncertainty in their spacing. + bool coarse_horizontal = false; + if (horizontal_first && result.coarse && result.landmarks.size() == 2) { + const auto& a = result.landmarks[0]; + const auto& b = result.landmarks[1]; + const double nearest = std::min(std::abs(a.dx), std::abs(b.dx)); + coarse_horizontal = a.accepted && b.accepted && a.dx*b.dx > 0 && + nearest > 1.0/64 && nearest > 2*std::abs(a.dx-b.dx); + } + const bool plaza_anchor_horizontal = matcher == match_jubilife_plaza_approach && + result.coarse && std::abs(result.dx)>1.0/50 && std::abs(result.dy)<=1.0/54; + // A uniquely matched Eterna ball may correct the door + // column while the sign/bush enter the frame. Arrival + // still requires the matcher's full three-anchor check. + const bool eterna_anchor_horizontal = (matcher == match_eterna_center_entrance || + matcher == match_eterna_underground_house || matcher == match_eterna_underground_man) && + result.coarse && std::abs(result.dx)>1.0/128; + // Recover the gym's doorway row before moving right if + // the south approach overshot toward the fence/cliff. + const bool gym_anchor_horizontal=matcher==match_eterna_gym_entrance && + result.coarse && std::abs(result.dx)>1.0/128 && std::abs(result.dy)<=.06; + const bool gym_row_recovery=matcher==match_eterna_gym_entrance && + result.coarse && std::abs(result.dy)>.06; + // Reach the row north of Jupiter before entering her + // column. From above that row, align sideways before + // moving south so the NPC cannot block the correction. + const bool jupiter_row_recovery=matcher==match_galactic_hq_jupiter && + result.dy < -1.0/54; + const bool jupiter_anchor_horizontal=matcher==match_galactic_hq_jupiter && + !jupiter_row_recovery && result.coarse && std::abs(result.dx)>1.0/128; + // Stay south of the Contest Hall wall/sign before crossing to Fantina. + const bool contest_row_recovery=matcher==match_hearthome_contest_fantina && result.dy>1.0/54; + // Keep the stair approach horizontal-first until close to + // its column. Recorded fine poses otherwise spent repeated + // 16ms sideways turns while a larger vertical error waited. + // Fine correction changes axis priority, never arrival gates. + const bool stair_fine_correction=matcher==match_route208_stair_top && + !result.coarse && std::abs(result.dx)<=1.0/64; + const bool horizontal = !contest_row_recovery && !gym_row_recovery && !jupiter_row_recovery && (jupiter_anchor_horizontal || coarse_horizontal || plaza_anchor_horizontal || eterna_anchor_horizontal || gym_anchor_horizontal || (!result.coarse && + ((horizontal_first && !stair_fine_correction && std::abs(result.dx)>1.0/128) || + std::abs(result.dx)>=std::abs(result.dy)*9.0/16))); + if (stair_fine_correction && horizontal_first && !horizontal && std::abs(result.dx)>1.0/128) + stream.log("["+label+"] Near stair column; correcting the larger vertical error first."); + if (coarse_horizontal) stream.log("[" + label + + "] Both landmarks agree on a large horizontal offset; correcting the street lane first."); + const double error = horizontal ? result.dx : result.dy*9.0/16; + auto duration = std::abs(error) > 1.0/24 ? 80ms : std::abs(error) > 1.0/64 ? 40ms : 16ms; + // A 16ms turn may not complete a step. Use a bounded + // full correction while reacquiring the second plaza anchor. + if (matcher == match_jubilife_plaza_approach && result.coarse) + duration = std::max(duration, 60ms); + if (matcher==match_galactic_hq_jupiter){ + for (const auto& landmark:result.landmarks){ + // The player stands around y=.74. Do not steer + // sideways/north when the stair opening is close + // to that row; a southward correction moves away. + if (landmark.name=="StairBoundary" && landmark.accepted && + landmark.found.y+landmark.found.height>.60 && + (horizontal || result.dy<0)) + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label+": Correction would approach the stair boundary.",stream); + } + } + pbf_move_left_joystick(context, + {horizontal ? (result.dx > 0 ? 1.0 : -1.0) : 0.0, + horizontal ? 0.0 : (result.dy < 0 ? 1.0 : -1.0)}, duration, 0ms); + pbf_wait(context, 500ms); + } else { + confirmations = 0; + if (matcher==match_hearthome_center_entrance || matcher==match_hearthome_contest_fantina){ + // Do not sweep into a different doorway or around the Hall's + // side when a cloud, NPC or fade hides the required anchors. + if (++missing>20) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label+": Scene anchors missing; stationary reacquisition exhausted.",stream); + pbf_wait(context,500ms); + context.wait_for_all_requests(); + continue; + } + if (matcher==match_galactic_hq_jupiter || matcher==match_route208_stair_top){ + // Keep generic missing-anchor behavior unchanged. Route208's + // explicit caller can instead request its recorded recovery. + const bool stair_rocks=matcher==match_route208_stair_top; + if (stair_rocks && rock_acquisition_timed_out) { + const auto now=std::chrono::steady_clock::now(); + if (missing_rocks_since==std::chrono::steady_clock::time_point::max()) + missing_rocks_since=now; + // Matcher processing can take seconds per observation; + // measure real elapsed time rather than counting waits. + if (now-missing_rocks_since>=10s) { + context.wait_for_all_requests(); + *rock_acquisition_timed_out=true; + stream.log("["+label+"] Rock acquisition timed out after 10s; handing off to the requested cliff recovery."); + return; + } + } else if (++missing>(stair_rocks ? 20 : 6)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label+(stair_rocks ? ": Rock anchors missing; no blind search near stairs." : + ": Chair/stair anchors missing; no blind search near stairs."),stream); + } + pbf_wait(context,500ms); + context.wait_for_all_requests(); + continue; + } + // Search right first, then across the starting point to the + // left and back. Each complete sweep returns to its origin. + // Try that sweep at the initial height, above it, and below it; + // no missing/weak landmark ever confirms the destination. + if (++missing > (search_down_first ? 78 : 74)) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Landmarks missing during bounded bidirectional search.", stream); + if (search_down_first && missing <= 4) { + stream.log("[" + label + "] Post-battle acquisition: moving down before lateral search."); + pbf_move_left_joystick(context, {0.0, -1.0}, 100ms, 0ms); + } else { + const size_t search_step = missing - 1 - (search_down_first ? 4 : 0); + const size_t level = search_step / 25; + const size_t slot = search_step % 25; + const bool lateral = slot < 24; + const double direction = slot < 6 || slot >= 18 ? 1.0 : -1.0; + const auto search_duration = lateral ? 100ms : level == 0 ? 600ms : 1200ms; + stream.log("[" + label + "] Acquisition search step=" + std::to_string(missing) + + ", direction=" + (lateral ? (direction>0 ? "right" : "left") : (level==0 ? "up" : "down"))); + // On the street, a northward search from the east + // edge can stop at the building/NPC collision row. + // Step west around it before changing height, then + // return to the same lane for the next bounded sweep. + const bool street_height_search=!lateral && matcher==match_jubilife_street_approach; + if (street_height_search) pbf_move_left_joystick(context,{-1.0,0.0},150ms,0ms); + pbf_move_left_joystick(context, + {lateral ? direction : 0.0, lateral ? 0.0 : (level == 0 ? 1.0 : -1.0)}, search_duration, 0ms); + if (street_height_search) pbf_move_left_joystick(context,{1.0,0.0},150ms,0ms); + } + pbf_wait(context, 500ms); + } + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Could not confirm the reference position.", stream); + } + + void fight_trainer_using_moves(VideoStream& stream, ProControllerContext& context, + const std::vector& moves, const std::string& label, bool post_battle_dialogue_cleanup) { + fight_using_move_list(stream,context,BattleType::TRAINER,moves,label,true,post_battle_dialogue_cleanup); + } + + void fight_trainer_using_moves(VideoStream& stream, ProControllerContext& context, + size_t move, const std::string& label, bool post_battle_dialogue_cleanup) { + fight_trainer_using_moves(stream,context,std::vector{move},label,post_battle_dialogue_cleanup); + } + + void fight_trainer_using_move_slot(VideoStream& stream, ProControllerContext& context, + size_t move_slot, const std::string& label) { + fight_using_move_slot(stream,context,BattleType::TRAINER,move_slot,label); + } + + void fight_story_battle(VideoStream& stream, ProControllerContext& context, const std::string& label, bool doubles) { + // The best move each turn; the next Pokemon after a faint; no new moves. + fight_using_move_list(stream, context, BattleType::TRAINER, {0}, label, false, true, doubles, true); + } + + void fight_trainer(VideoStream& stream, ProControllerContext& context, const std::string& label) { + // Move 0: the best move on the menu each turn, whatever the starter. + fight_using_move_slot(stream, context, BattleType::TRAINER, 0, label); + } + + void battle_1_pokemon(VideoStream& stream, ProControllerContext& context) { + stream.log("[Training] Finding one wild Pokemon using the catch encounter wiggle.", COLOR_BLUE); + encounter_one_wild_pokemon(stream, context); + // The best move on the menu, whatever the starter. + fight_using_move_slot(stream, context, BattleType::STANDARD, 0, "Training"); + context.wait_for_all_requests(); + stream.log("[Training] One wild battle completed.", COLOR_GREEN); + } + + // --------------------------------------------------------------------------- + // Catch 1 Pokemon + // --------------------------------------------------------------------------- + + void catch_1_pokemon( + VideoStream& stream, + ProControllerContext& context + ) { + stream.log("[AutoStory] Starting catch_1_pokemon helper...", COLOR_BLUE); + + encounter_one_wild_pokemon(stream, context); + + // Catch the pokemon. + stream.log("[AutoStory] Throwing Poke Ball (manual bag open)...", COLOR_BLUE); + ReceivePokemonDetector received_pokemon; + // Preserve exit-transition state from the ball outcome through + // receipt/experience cleanup; do not recreate it between phases. + EndBattleWatcher battle_ended; + bool caught = false; + bool exit_seen = false; + for (int throw_attempt = 0; throw_attempt < 10; throw_attempt++) { + // Open bag from battle menu (X), confirm first slot (ZL). + pbf_press_button(context, BUTTON_X, 160ms, 840ms); + context.wait_for_all_requests(); + pbf_press_button(context, BUTTON_ZL, 160ms, 1000ms); + context.wait_for_all_requests(); + + // Wait to see if the battle menu comes back (ball failed) + // or if the battle ends (caught or fainted). + BattleMenuWatcher menu_back(BattleType::STANDARD, COLOR_YELLOW); + int throw_ret = run_until( + stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, + {{menu_back}, {received_pokemon}, {battle_ended}} + ); + + if (throw_ret == 1 || (throw_ret == 2 && received_pokemon.received())) { + caught = true; + exit_seen = throw_ret == 2; + stream.log("[AutoStory] Pokemon receipt verified.", COLOR_GREEN); + break; + } + if (throw_ret < 0 || throw_ret == 2) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Catch failed: No verified Pokemon receipt before battle ended or timed out.", stream); + } + stream.log( + "[AutoStory] Ball failed, retrying (attempt " + std::to_string(throw_attempt + 2) + ")...", + COLOR_ORANGE + ); + } + + context.wait_for_all_requests(); + + if (!caught) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Catch failed: Exhausted ten ball attempts.", stream); + } + // Receipt alone is not an overworld return: clear experience, + // automatic move learning, dex registration and nickname screens + // until the same completed-black-transition watcher used in battles fires. + if (!exit_seen) { + stream.log("[Catch cleanup] Receipt confirmed; waiting for battle exit transition."); + const int cleanup_ret = run_until(stream, context, + [](ProControllerContext& ctx) { pbf_mash_button(ctx, BUTTON_B, 120000ms); }, + {{battle_ended}}); + if (cleanup_ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Catch cleanup: Battle exit transition did not finish.", stream); + } + } + context.wait_for_all_requests(); + stream.log("[Catch cleanup] Battle exit transition confirmed; checking stable overworld."); + confirm_battle_overworld(stream, context, "Catch cleanup"); + stream.log("[Catch cleanup] Verified receipt, completed battle exit, and four seconds of clear overworld.", COLOR_GREEN); + } + + // --------------------------------------------------------------------------- + // catch_1_pokemon_v2 - burst wiggle variant for testing. + // Uses WIGGLES_PER_BURST consecutive left/right moves before checking for a + // battle, reducing total wait time compared to checking after every move. + // Commented out - enable for testing when ready. + // --------------------------------------------------------------------------- + // void catch_1_pokemon_v2( + // SingleSwitchProgramEnvironment& env, + // VideoStream& stream, + // ProControllerContext& context + // ) { + // stream.log("[AutoStory] Starting catch_1_pokemon_v2 helper...", COLOR_BLUE); + // + // // Save once before attempting to catch. On failure the game + // // resets back to this save, so the save lives outside the loop. + // stream.log("[AutoStory] Saving game before catch attempt...", COLOR_BLUE); + // { + // static constexpr int SAVE_MAX_RETRIES = 3; + // bool save_succeeded = false; + // for (int save_attempt = 0; save_attempt < SAVE_MAX_RETRIES; save_attempt++) { + // try { + // if (save_attempt > 0) { + // stream.log( + // "[AutoStory] Retrying save (attempt " + std::to_string(save_attempt + 1) + ")...", + // COLOR_ORANGE + // ); + // pbf_mash_button(context, BUTTON_B, 1000ms); + // context.wait_for_all_requests(); + // } + // save_game(stream, context); + // save_succeeded = true; + // break; + // } + // catch (OperationFailedException& e) { + // stream.log( + // "[AutoStory] Save attempt " + std::to_string(save_attempt + 1) + " failed: " + e.message(), + // COLOR_RED + // ); + // context.wait_for(1000ms); + // } + // } + // if (!save_succeeded) { + // stream.log("[AutoStory] All save attempts failed. Using blind fallback save.", COLOR_ORANGE); + // pbf_mash_button(context, BUTTON_B, 1000ms); + // save_game(context); + // pbf_mash_button(context, BUTTON_B, 2000ms); + // } + // } + // context.wait_for_all_requests(); + // pbf_wait(context, 1000ms); + // + // // ------------------------------------------------------- + // // Mini-checkpoint loop: retry from the save above if we + // // run out of balls or our Pokemon faints. + // // ------------------------------------------------------- + // while (true) { + // + // BlackScreenWatcher battle_start; + // BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + // BattleDialogWatcher battle_dialog(COLOR_YELLOW); + // + // bool encounter_found = false; + // + // // Wiggle left/right in bursts, checking for a battle after each burst. + // // Doing multiple moves before checking avoids waiting after every step. + // static constexpr int WIGGLES_PER_BURST = 3; + // while (!encounter_found) { + // for (int i = 0; i < WIGGLES_PER_BURST; i++) { + // pbf_move_left_joystick(context, {-1, 0}, 120ms, 80ms); + // pbf_move_left_joystick(context, { 1, 0}, 120ms, 80ms); + // } + // context.wait_for_all_requests(); + // + // int ret = wait_until( + // stream, + // context, + // 1500ms, + // { + // {battle_start}, + // {battle_menu}, + // {battle_dialog}, + // } + // ); + // + // if (ret >= 0) { + // encounter_found = true; + // } + // } + // + // stream.log("[AutoStory] Wild battle detected!", COLOR_GREEN); + // + // // Wait until the actual battle menu appears. + // BattleMenuWatcher confirm_menu(BattleType::STANDARD); + // + // int menu_ret = wait_until( + // stream, + // context, + // 15s, + // { + // {confirm_menu}, + // } + // ); + // + // if (menu_ret < 0) { + // stream.log( + // "[AutoStory] Failed to detect battle menu. Resetting to checkpoint...", + // COLOR_RED + // ); + // reset_game_from_home(env, env.console, context, true); + // continue; + // } + // + // stream.log("[AutoStory] Battle menu detected.", COLOR_GREEN); + // + // context.wait_for_all_requests(); + // pbf_wait(context, 500ms); + // + // // --- Throw balls --- + // CatchResults result = basic_catcher( + // stream, + // context, + // Language::English, + // "poke-ball", + // 10 + // ); + // + // switch (result.result) { + // case CatchResult::POKEMON_CAUGHT: + // stream.log( + // "[AutoStory] Pokemon successfully caught after " + + // std::to_string(result.balls_used) + " ball(s).", + // COLOR_GREEN + // ); + // { + // OverworldWatcher overworld; + // run_until( + // stream, context, + // [](ProControllerContext& ctx) { + // pbf_mash_button(ctx, BUTTON_B, 120000ms); + // }, + // {{overworld}} + // ); + // } + // return; + // + // case CatchResult::POKEMON_FAINTED: + // stream.log("[AutoStory] Wild Pokemon fainted. Exiting battle.", COLOR_ORANGE); + // { + // OverworldWatcher overworld; + // run_until( + // stream, context, + // [](ProControllerContext& ctx) { + // pbf_mash_button(ctx, BUTTON_B, 120000ms); + // }, + // {{overworld}} + // ); + // } + // return; + // + // case CatchResult::OUT_OF_BALLS: + // stream.log( + // "[AutoStory] Ran out of Poke Balls after " + + // std::to_string(result.balls_used) + " throw(s). Resetting to checkpoint...", + // COLOR_RED + // ); + // reset_game_from_home(env, env.console, context, true); + // continue; + // + // case CatchResult::OWN_FAINTED: + // stream.log( + // "[AutoStory] Own Pokemon fainted. Resetting to checkpoint...", + // COLOR_RED + // ); + // reset_game_from_home(env, env.console, context, true); + // continue; + // + // case CatchResult::BALL_LIMIT_REACHED: + // stream.log( + // "[AutoStory] Ball limit reached (" + + // std::to_string(result.balls_used) + " throw(s)). Resetting to checkpoint...", + // COLOR_RED + // ); + // reset_game_from_home(env, env.console, context, true); + // continue; + // + // case CatchResult::CANNOT_THROW_BALL: + // stream.log( + // "[AutoStory] Cannot throw ball (menu timing issue). Resetting to checkpoint...", + // COLOR_RED + // ); + // reset_game_from_home(env, env.console, context, true); + // continue; + // + // default: + // stream.log( + // "[AutoStory] Catch timed out after " + + // std::to_string(result.balls_used) + " ball(s). Resetting to checkpoint...", + // COLOR_RED + // ); + // reset_game_from_home(env, env.console, context, true); + // continue; + // } + // + // } // end while(true) + // } + + // --------------------------------------------------------------------------- + // Movement Helpers + // --------------------------------------------------------------------------- + + bool detect_black_screen_after_movement( + VideoStream& stream, + ProControllerContext& context, + Milliseconds timeout + ) { + BlackScreenWatcher black_screen; + return wait_until(stream, context, timeout, {{black_screen}}) == 0; + } + + bool try_right_down_exit_search( + VideoStream& stream, + ProControllerContext& context, + size_t iterations + ) { + for (size_t i = 0; i < iterations; i++) { + pbf_move_left_joystick(context, { 1.0, 0.0 }, 80ms, 0ms); + pbf_move_left_joystick(context, { 0.0, -1.0 }, 220ms, 0ms); + + if (detect_black_screen_after_movement(stream, context, 1000ms)) { + return true; + } + } + return false; + } + + void leave_pokemon_center_before_barry_1( + VideoStream& stream, + ProControllerContext& context + ) { + // Original down movement to leave Pokemon Center. + pbf_move_left_joystick(context, { 0.0, -1.0 }, 814ms, 0ms); + + if (detect_black_screen_after_movement(stream, context, 1200ms)) { + pbf_wait(context, 2340ms); + return; + } + + stream.log("[AutoStory] Pokemon Center exit black screen not detected. Searching with right/down movement.", COLOR_ORANGE); + + if (try_right_down_exit_search(stream, context, 20)) { + pbf_wait(context, 2340ms); + return; + } + + stream.log("[AutoStory] First exit search failed. Repositioning left and retrying.", COLOR_ORANGE); + pbf_move_left_joystick(context, { -1.0, 0.0 }, 5000ms, 0ms); + + if (try_right_down_exit_search(stream, context, 30)) { + pbf_wait(context, 2340ms); + return; + } + + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to find Pokemon Center exit black screen before Barry 1. Triggering checkpoint retry.", + stream + ); + } + + // --------------------------------------------------------------------------- + // Generic Movement Helper - Walk through grass in specified direction for + // specified steps, attempting to run from any battles that are encountered. + // --------------------------------------------------------------------------- + void walk_through_grass( + VideoStream& stream, ProControllerContext& context, + int8_t x, int8_t y, size_t steps, const std::string& direction_name, + BattleType battle_type, Milliseconds movement_duration, + OverworldMatchResult (*destination)(const ImageRGB32&) + ) { + size_t movements = 0, confirmations = 0; + while (true) { + context.wait_for_all_requests(); + // Destination checks happen before input and after each step/escape. + // With no matcher, retain the original fixed-step behavior. + VideoOverlaySet overlays(stream.overlay()); + OverworldMatchResult location; + if (destination) { + const auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Grass destination: No video frame.", stream); + location = destination(*frame.frame); + for (const auto& landmark : location.landmarks) { + overlays.add(COLOR_CYAN, landmark.search); + if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); + stream.log("[Grass destination] " + direction_name + " " + landmark.name + + " score=" + std::to_string(landmark.score) + + " margin=" + std::to_string(landmark.margin) + + " accepted=" + std::to_string(landmark.accepted)); + } + stream.log("[Grass destination] located=" + std::to_string(location.located) + + " at_target=" + std::to_string(location.at_target) + + " screen-relative offset=(" + std::to_string(location.dx) + + "," + std::to_string(location.dy) + ")"); + if (location.at_target) { + if (++confirmations >= 3) { + stream.log("[Grass] Destination confirmed; stopped " + direction_name + + " after " + std::to_string(movements) + "/" + std::to_string(steps) + " movements.", COLOR_GREEN); + return; + } + pbf_wait(context, 250ms); + continue; + } + confirmations = 0; + } + if (movements >= steps) { + if (destination) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Grass destination not confirmed within movement limit: " + direction_name, stream); + stream.log("[Grass] Completed " + direction_name + " (" + std::to_string(steps) + " steps).", COLOR_GREEN); + return; + } + int8_t move_x=x, move_y=y; + Milliseconds duration=movement_duration; + if (destination && location.located && !location.coarse && + std::abs(location.dx)<=0.10 && std::abs(location.dy)<=0.12) { + // Small corrections prevent stepping past a narrow stop target. + const bool horizontal=std::abs(location.dx)>=std::abs(location.dy)*9.0/16; + const double error=horizontal ? location.dx : location.dy*9.0/16; + duration=std::min(duration, std::abs(error)>0.03125 ? 80ms : + std::abs(error)>0.015625 ? 40ms : 16ms); + move_x=horizontal ? (location.dx>0 ? 1 : -1) : 0; + move_y=horizontal ? 0 : (location.dy<0 ? 1 : -1); + stream.log("[Grass destination] Correcting " + std::string(horizontal ? "horizontal" : "vertical") + + " axis for " + std::to_string(duration.count()) + "ms."); + } + // Each movement gets fresh watcher state. A completed white + // transition from a prior encounter must not trigger again. + BlackScreenWatcher battle_start; + WhiteScreenOverWatcher white_screen(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BattleMenuWatcher battle_menu(battle_type, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + pbf_move_left_joystick(context, {(double)move_x, (double)move_y}, duration, 0ms); + movements++; + context.wait_for_all_requests(); + int ret = wait_until(stream, context, 2500ms, + {{white_screen}, {battle_start}, {battle_dialog}, {battle_menu}}); + if (ret >= 0) { + confirmations = 0; + stream.log("[Grass] " + direction_name + " step " + std::to_string(movements) + + "/" + std::to_string(steps) + ": encounter trigger=" + std::to_string(ret) + + "; using shared confirmed-Run escape."); + escape_wild_battle(stream, context, battle_type); + context.wait_for_all_requests(); + stream.log("[Grass] Escape confirmed; resuming " + direction_name + "."); + } + } + } + + } + } +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h new file mode 100644 index 0000000000..351ed5ab5b --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h @@ -0,0 +1,161 @@ +#pragma once + +#include "PokemonBDSP_OverworldMatchers.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "../StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "CommonTools/Async/InferenceRoutines.h" + +#include "NintendoSwitch/Programs/DateManip/NintendoSwitch_DateManip.h" + +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" + +namespace PokemonAutomation { + namespace NintendoSwitch { + namespace PokemonBDSP { + + // --------------------------------------------------------------------------- + // Utility Watchers + // --------------------------------------------------------------------------- + + class BattleDialogWatcher : public DetectorToFinder { + public: + BattleDialogWatcher(Color color = COLOR_RED); + }; + + // --------------------------------------------------------------------------- + // Time / setup helpers + // --------------------------------------------------------------------------- + + // Shared visual alignment and dialogue cleanup used by story routes. + void clear_post_battle_dialogue(VideoStream& stream, ProControllerContext& context, + Button button = BUTTON_B, Milliseconds quiet_period = Milliseconds(3000), + const char* instruction_card = nullptr); + void align_to_overworld_landmarks(VideoStream& stream, ProControllerContext& context, + OverworldMatchResult (*matcher)(const ImageRGB32&), const std::string& label, bool allow_story_start = false, bool horizontal_first = false, + bool search_down_first = false, + OverworldMatchResult (*entry_matcher)(const ImageRGB32&) = nullptr, + bool* entry_detected = nullptr, + double arrival_tolerance_x = 0, double arrival_tolerance_y = 0, + double moderate_entry_confidence = 0, + bool* rock_acquisition_timed_out = nullptr); + + void verify_tutorial_settings(VideoStream& stream, ProControllerContext& context); + + void set_time_fast( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const DateTime& target, + bool preserve_date = false // Keep current year/month/day; set only target time. + ); + + // --------------------------------------------------------------------------- + // Battles + // --------------------------------------------------------------------------- + + void fight_starly( + VideoStream& stream, + ProControllerContext& context + ); + + // Select the requested 1-based move slot using visual cursor confirmation. + void fight_trainer_using_move_slot(VideoStream& stream, ProControllerContext& context, + size_t move_slot, const std::string& label); + + // Any trainer battle, whatever the starter: uses move slot 1 until the + // battle ends. Slot 1 is a damaging move (Tackle, Scratch or Pound) for + // every starter. + void fight_trainer(VideoStream& stream, ProControllerContext& context, const std::string& label); + + // A story battle that may go badly: the best move each turn, Potions + // under 35 %, the next Pokemon when one faints, and "Keep old moves" + // on level-up prompts. `doubles` confirms the default target each + // turn (tag and twin battles). Returns once the battle has ended, + // won or lost; after a loss we stand in the last Pokemon Center. + void fight_story_battle(VideoStream& stream, ProControllerContext& context, const std::string& label, bool doubles = false); + + // --------------------------------------------------------------------------- + // Catching / healing + // --------------------------------------------------------------------------- + + void catch_1_pokemon( + VideoStream& stream, + ProControllerContext& context + ); + + // Finds one wild Pokemon in the grass by stepping left and right, and + // fights it with the best move on the menu. Does not heal. + void battle_1_pokemon(VideoStream& stream, ProControllerContext& context); + + // Burst wiggle variant of catch_1_pokemon for testing. + // Commented out in the .cpp - uncomment both to enable. + // void catch_1_pokemon_v2( + // SingleSwitchProgramEnvironment& env, + // VideoStream& stream, + // ProControllerContext& context + // ); + + // --------------------------------------------------------------------------- + // Grass movement helpers + // --------------------------------------------------------------------------- + + void walk_through_grass( + VideoStream& stream, + ProControllerContext& context, + int8_t x, + int8_t y, + size_t steps, + const std::string& direction_name, + BattleType battle_type = BattleType::STANDARD, + Milliseconds movement_duration = Milliseconds(200), + OverworldMatchResult (*destination)(const ImageRGB32&) = nullptr + ); + + bool detect_black_screen_after_movement( + VideoStream& stream, + ProControllerContext& context, + Milliseconds timeout + ); + + bool try_right_down_exit_search( + VideoStream& stream, + ProControllerContext& context, + size_t iterations + ); + + void leave_pokemon_center_before_barry_1( + VideoStream& stream, + ProControllerContext& context + ); + // --------------------------------------------------------------------------- + // Helper function to navigate the poketech challenge clown dialogue. + // Example usage + // Move up and talk to clown. + // clown_dialog_helper( + // stream, + // context, + // DPAD_UP + // ); + + // Move left and talk to clown. + // clown_dialog_helper( + // stream, + // context, + // DPAD_LEFT + // ); + + // Move right and talk to clown. + // clown_dialog_helper( + // stream, + // context, + // DPAD_RIGHT + // ); + // ---------------------------------------------------------------------------- + void clown_dialog_helper( + VideoStream& stream, + ProControllerContext& context, + DpadPosition direction + ); + } + } +} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp new file mode 100644 index 0000000000..efd43585ea --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -0,0 +1,202 @@ +/* BDSP AutoStory - Story step executor + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP_AutoStory_PokemonCenter.h" +#include "PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Outcome; +using Pokemon::RunState; +using Pokemon::TilePos; + + +std::optional story_route_costs(const std::string& variant){ + if (variant.empty()){ + return route_plan_costs(); + } + if (variant == "town"){ + return town_plan_costs(); + } + if (variant == "robust"){ + return Pokemon::RobustPlanCosts(); + } + return std::nullopt; +} + +std::pair dpad_of(Direction direction){ + switch (direction){ + case Direction::NORTH: + return {0, 1}; + case Direction::EAST: + return {1, 0}; + case Direction::SOUTH: + return {0, -1}; + case Direction::WEST: + return {-1, 0}; + } + return {0, 0}; +} + + +ConsoleStoryExecutor::ConsoleStoryExecutor( + StoryStepRunner& runner, std::set stop_rows, + const Pokemon::MapSet& maps, std::vector trainers, + TrainerFight fight, std::map variants +) + : m_runner(runner) + , m_stop_rows(std::move(stop_rows)) + , m_maps(maps) + , m_all_trainers(std::move(trainers)) + , m_trainers(m_all_trainers) + , m_fight(std::move(fight)) + , m_variants(std::move(variants)) +{} + +void ConsoleStoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ + m_checkpoint = data.id; + m_trainers = m_all_trainers; +} + +RouteTrainerFight ConsoleStoryExecutor::fight(StoryConsole& console){ + return [this, &console](const std::string& trainer){ + m_fight(console, trainer); + }; +} + +StoryConsole& ConsoleStoryExecutor::console(){ + if (m_console == nullptr){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "A story step ran outside ConsoleStoryExecutor::run()."); + } + return *m_console; +} + +Outcome ConsoleStoryExecutor::run(RunState& state, const Pokemon::StoryStep& step){ + Outcome result = Pokemon::outcome_ok(); + const StoryFailure on_failure = m_stop_rows.contains(m_checkpoint) ? StoryFailure::STOP : StoryFailure::RELOAD_AND_RETRY; + const Outcome ran = m_runner.run_step(m_checkpoint, on_failure, [&](StoryConsole& console){ + m_console = &console; + set_checks(console.options.checks); + set_language(console.options.language); + const auto variant = m_variants.find(step.variant); + result = variant == m_variants.end() + ? Pokemon::StoryStepExecutor::run(state, step) + : variant->second(*this, console, state, step); + m_console = nullptr; + }); + m_console = nullptr; + return ran.succeeded() ? result : ran; +} + +Outcome ConsoleStoryExecutor::route(RunState& state, const Pokemon::StoryStep& step){ + const std::vector from = Pokemon::tiles_of(state.pose); + if (from.empty()){ + return Pokemon::outcome_failed("no_pose", Pokemon::to_string(state.pose)); + } + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + return Pokemon::outcome_failed("unknown_variant", step.variant); + } + const std::vector goal = Pokemon::tiles_of(*step.target); + StoryConsole& c = console(); + const std::vector at = walk_route( + c.env.console, c.context, m_maps, from, + [&](const TilePos& pos){ return std::find(goal.begin(), goal.end(), pos) != goal.end(); }, + m_trainers, fight(c), step.name, {}, *costs + ); + return Pokemon::outcome_ok(Pokemon::tile_pose(step.target->map, at)); +} + +Outcome ConsoleStoryExecutor::legs(RunState& state, const Pokemon::StoryStep& step){ + const bool one_short_ok = step.variant == "one_short_ok"; + if (!one_short_ok && !step.variant.empty()){ + return Pokemon::outcome_failed("unknown_variant", step.variant); + } + StoryConsole& c = console(); + std::vector at = Pokemon::tiles_of(state.pose); + for (const Pokemon::StoryLeg& leg : step.legs){ + // Facing another way, a tap only turns (r256): turn first. + route_turn(c.env.console, c.context, leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const size_t tiles = (size_t)leg.tiles; + const WalkResult walk = walk_tiles_dpad(c.env.console, c.context, x, y, tiles, step.name); + c.env.console.log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); + if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ + return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); + } + // Where we may be: the end, or a tile short of it. + std::vector next; + for (const TilePos& start : at){ + TilePos end = start; + for (size_t i = 0; i < tiles; i++){ + if (one_short_ok && i + 1 == tiles){ + next.push_back(end); + } + end = end.next(leg.direction); + } + next.push_back(end); + } + at = std::move(next); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.legs.back().direction)); +} + +Outcome ConsoleStoryExecutor::train(RunState&, const Pokemon::StoryStep& step){ + StoryConsole& c = console(); + for (size_t battle = 0; battle < step.battles; battle++){ + const std::string label = std::format("Training battle {}/{}", battle + 1, step.battles); + route_status(c.env.console, label); + battle_1_pokemon(c.env.console, c.context); + fail_if_blacked_out(c.env.console, c.context, label); + } + return Pokemon::outcome_ok(); +} + +Outcome ConsoleStoryExecutor::heal(RunState&, const Pokemon::StoryStep& step){ + StoryConsole& c = console(); + heal_at_pokemon_center(c.env.console, c.context, step.why.empty() ? step.name : step.why); + return Pokemon::outcome_ok(); +} + +Outcome ConsoleStoryExecutor::save(RunState&, const Pokemon::StoryStep&){ + StoryConsole& c = console(); + checkpoint_save(c.env, c.context, c.options.notif_status_update, c.stats, c.options.checkpoint_save_mode); + return Pokemon::outcome_ok(); +} + +Outcome ConsoleStoryExecutor::mash(RunState&, const Pokemon::StoryStep& step){ + StoryConsole& c = console(); + wait_for_dialogue(c.env.console, c.context, step.name); + pbf_mash_button(c.context, BUTTON_B, 5000ms); + c.context.wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + + +ConsoleStoryExecutor::Variant story_call(StoryAction action){ + return [action = std::move(action)](ConsoleStoryExecutor&, StoryConsole& console, RunState&, const Pokemon::StoryStep&){ + action(console); + return Pokemon::outcome_ok(); + }; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h new file mode 100644 index 0000000000..48a07aa476 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h @@ -0,0 +1,123 @@ +/* BDSP AutoStory - Story step executor + * + * From: https://github.com/PokemonAutomation/ + * + * BDSP's way of doing the shared story steps + * (Pokemon/Navigation/Pokemon_StorySteps.h) on the console. Routes are + * planned on the story's map set from the tiles the run's pose holds, + * with the trainers who can still see us fought on the way; a route's + * variant names its plan costs. A segment adds its own steps by variant + * (the Mart lady, a walk through a gap in town): a variant registered + * here is used for its step whatever the kind. + * + * Each step runs through the table's StoryStepRunner, so a failed step + * is answered as the table's other rows are: a screenshot, the + * "checkpoint_reattempt_loop: attempt N failed" line the lab watches + * for, and a failed Outcome that the segment's policy answers. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H + +#include +#include +#include +#include +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The plan costs a ROUTE step's variant names: "" the route costs, +// "town" town_plan_costs(), "robust" the planner's defaults. Nothing for +// another name. +std::optional story_route_costs(const std::string& variant); + +// The D-pad direction (x, y) of `direction`: y is up the screen. +std::pair dpad_of(Pokemon::Direction direction); + + +// Does BDSP's kinds of story step for one checkpoint of a table. +class ConsoleStoryExecutor : public Pokemon::StoryStepExecutor{ +public: + // A segment's own way of doing a step, by the step's variant. It may + // set run flags for later steps' skip_if, and returns where it left + // the player if it knows. + using Variant = std::function; + // Fights a trainer whose dialogue has opened. + using TrainerFight = std::function; + + // `stop_rows`: the rows whose failure ends the program (rows_that_stop()). + ConsoleStoryExecutor( + StoryStepRunner& runner, std::set stop_rows, + const Pokemon::MapSet& maps, std::vector trainers, + TrainerFight fight, std::map variants + ); + + // A new attempt of a row: every trainer can see us again. + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override; + + const Pokemon::MapSet& maps() const{ + return m_maps; + } + // The trainers who can still see us, and how to fight one, for a + // variant that walks with walk_route(). + std::vector& trainers(){ + return m_trainers; + } + RouteTrainerFight fight(StoryConsole& console); + + virtual Pokemon::Outcome run(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + + virtual Pokemon::Outcome route(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // Each leg in D-pad taps, turning first. Variant "one_short_ok": a leg + // may end a tile short (a door lined up by sight after). + virtual Pokemon::Outcome legs(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + virtual Pokemon::Outcome train(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // In front of a Pokemon Center, its lit doorway on screen: in, the + // nurse, and out (heal_at_pokemon_center()). + virtual Pokemon::Outcome heal(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // The checkpoint save (checkpoint_save(), real or fake as the options say). + virtual Pokemon::Outcome save(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A dialogue opens (or has), then B through it. + virtual Pokemon::Outcome mash(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + +private: + StoryConsole& console(); + + StoryStepRunner& m_runner; + std::set m_stop_rows; + std::string m_checkpoint; + const Pokemon::MapSet& m_maps; + std::vector m_all_trainers; + std::vector m_trainers; + TrainerFight m_fight; + std::map m_variants; + // The console of the step running now. + StoryConsole* m_console = nullptr; +}; + + +// A variant that calls an old console function, for a step that has no +// declarative form yet. +ConsoleStoryExecutor::Variant story_call(StoryAction action); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp new file mode 100644 index 0000000000..4ff9530c50 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp @@ -0,0 +1,191 @@ +/* BDSP AutoStory - Story tables + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" +#include "PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Checkpoint; +using Pokemon::Event; +using Pokemon::Outcome; +using Pokemon::RunState; + + +namespace{ + +Event save_event(StoryStepRunner& runner, const std::string& id){ + Event event; + event.name = "save"; + event.why = "the save a failed step resets to"; + event.skip_if.push_back(Pokemon::condition("saved at the start", [&runner, id](RunState&){ + return runner.saved_at_start(id); + })); + event.act = [&runner, id](RunState&){ + return runner.save_at_start(id); + }; + return event; +} + +} + + +Pokemon::Segment story_table_segment( + StoryStepRunner& runner, std::string id, const std::vector& rows, + std::shared_ptr executor +){ + Pokemon::Segment segment; + segment.id = std::move(id); + for (const StoryRow& row : rows){ + segment.checkpoints.push_back(Pokemon::CheckpointSpec{ + .id = row.id, .title = row.title, .saves = false, + .build = [&runner, &row, executor]{ + Checkpoint steps = Pokemon::build_story_checkpoint(row, *executor); + if (row.on_failure != StoryFailure::RELOAD_AND_RETRY){ + return steps; + } + Checkpoint checkpoint; + checkpoint.id = row.id; + checkpoint.events.push_back(save_event(runner, row.id)); + for (Event& event : steps.events){ + checkpoint.events.push_back(std::move(event)); + } + return checkpoint; + }, + }); + } + return segment; +} + +std::set rows_that_stop(const std::vector& rows){ + std::set ids; + for (const StoryRow& row : rows){ + if (row.on_failure == StoryFailure::STOP){ + ids.insert(row.id); + } + } + return ids; +} + + +ConsoleStoryRunner::ConsoleStoryRunner(StoryConsole console) + : m_console(console) +{} + +bool ConsoleStoryRunner::saved_at_start(const std::string& id) const{ + return m_saved.contains(id); +} + +// A save that fails is a failed first attempt, as in the loop: the retry +// resets and does not save again. +Outcome ConsoleStoryRunner::save_at_start(const std::string& id){ + m_saved.insert(id); + return run_step(id, StoryFailure::RELOAD_AND_RETRY, [](StoryConsole& console){ + checkpoint_save(console.env, console.context, console.options.notif_status_update, console.stats, console.options.checkpoint_save_mode); + console.stats.m_first_run = false; + }); +} + +Outcome ConsoleStoryRunner::run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step){ + if (on_failure == StoryFailure::STOP){ + step(m_console); + return Pokemon::outcome_ok(); + } + SingleSwitchProgramEnvironment& env = m_console.env; + try{ + step(m_console); + return Pokemon::outcome_ok(); + }catch (OperationFailedException& e){ + // The fake-save mode's loop around the whole range retries it. + if (m_console.options.checkpoint_retry_scope == CheckpointRetryScope::DEFER_TO_SELECTED_RANGE){ + throw; + } + // Attempts are the segment's policy (AutoStoryOptions::policy); this + // only counts them for the log line the lab's run watcher reads. + const size_t attempt = m_failures[id]++; + env.console.log(std::format("checkpoint_reattempt_loop: attempt {} failed: {}: {}", attempt, id, e.message()), COLOR_RED); + // Keep what the screen showed at the failure; the retry resets the game. + if (VideoSnapshot screen = env.console.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/failures/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = std::format("{}{}-attempt{}.png", folder, now_to_filestring(), attempt); + screen.frame->save(path); + env.console.log("Failure screenshot: " + path); + } + m_console.stats.m_reset++; + env.update_stats(); + return Pokemon::outcome_failed("operation_failed", e.message()); + } +} + +Outcome ConsoleStoryRunner::reload(){ + SingleSwitchProgramEnvironment& env = m_console.env; + env.console.log("Resetting game from HOME before retry...", COLOR_ORANGE); + if (!reset_game_from_home(env, env.console, m_console.context, true, 1000ms)){ + throw OperationFailedExceptionWithScreenshot( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to reset game from HOME during checkpoint retry.", + env.console + ); + } + return Pokemon::outcome_ok(); +} + +void ConsoleStoryRunner::run(Pokemon::Segment& segment, const Pokemon::SegmentRange& range){ + SingleSwitchProgramEnvironment& env = m_console.env; + Pokemon::SegmentHooks hooks; + hooks.save = [](RunState&){ + return Pokemon::outcome_failed("no_save_hook", "story checkpoints save at their start"); + }; + hooks.reload = [this](RunState&){ + return reload(); + }; + hooks.log = [&env](const std::string& line){ + env.log(line, COLOR_BLUE); + }; + const Pokemon::SegmentPolicy& policy = m_console.options.policy; + RunState state; + const Pokemon::SegmentReport report = Pokemon::run_segment(state, segment, range, policy, hooks); + if (!report.completed){ + throw OperationFailedExceptionWithScreenshot( + ErrorReportMode::SEND_ERROR_REPORT, + std::format( + "AutoStory segment {}: failed at {} {}: {}", + segment.id, report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure) + ), + env.console + ); + } +} + + +void run_story_table( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const AutoStoryOptions& options, AutoStoryStats& stats, + const StorySegmentFactory& make_segment, const Pokemon::SegmentRange& range +){ + ConsoleStoryRunner runner(StoryConsole{env, context, options, stats}); + Pokemon::Segment segment = make_segment(runner); + runner.run(segment, range); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h new file mode 100644 index 0000000000..165cc3cbd9 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h @@ -0,0 +1,119 @@ +/* BDSP AutoStory - Story tables + * + * From: https://github.com/PokemonAutomation/ + * + * A segment of the story as a table of checkpoints on the shared engine + * (Pokemon_StorySegments.h). Each checkpoint is the list of step functions + * it called in order before it was a table, each step one event. The + * checkpoint saves first, as checkpoint_reattempt_loop() did before its + * first attempt; a step that fails is answered the way that loop answered + * it: a screenshot, a reset to the save, and the checkpoint again from its + * first step, up to the attempts of the shared policy the program's retry + * options set (AutoStoryOptions::policy; 3 by default, where the loop had 20). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_H + +#include +#include +#include +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// What a step runs on on the console. +struct StoryConsole{ + SingleSwitchProgramEnvironment& env; + ProControllerContext& context; + const AutoStoryOptions& options; + AutoStoryStats& stats; +}; +// A console action: what a step that calls an old function runs. +using StoryAction = std::function; + +// What a checkpoint does when a step fails. +enum class StoryFailure{ + // Save before the first step; on a failure reset to that save and run + // the checkpoint again (checkpoint_reattempt_loop with save=true). + RELOAD_AND_RETRY, + // No save and no retry: the failure ends the program (00.00). + STOP, +}; + +// A row of a story table: the shared checkpoint, and what a failure does. +struct StoryRow : public Pokemon::StoryCheckpoint{ + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY; +}; + +// How a table saves at a row's start and runs a console action: the +// console runner below, or a fake in the tests. +class StoryStepRunner{ +public: + virtual ~StoryStepRunner() = default; + // Whether checkpoint `id` has saved at its start in this run; a retry + // starts from that save and does not save again. + virtual bool saved_at_start(const std::string& id) const = 0; + virtual Pokemon::Outcome save_at_start(const std::string& id) = 0; + virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step) = 0; +}; + +// The table `rows` (kept alive by the caller) as segment `id`: each row +// that retries saves first, through `runner`, unless this run has saved it +// already (a retry starts from that save); then its steps run with +// `executor`, which the segment keeps. +Pokemon::Segment story_table_segment( + StoryStepRunner& runner, std::string id, const std::vector& rows, + std::shared_ptr executor +); + +// The ids of the rows that stop on a failure instead of retrying. +std::set rows_that_stop(const std::vector& rows); + +using StorySegmentFactory = std::function; + + +// Runs tables on the Switch. +class ConsoleStoryRunner : public StoryStepRunner{ +public: + explicit ConsoleStoryRunner(StoryConsole console); + + virtual bool saved_at_start(const std::string& id) const override; + virtual Pokemon::Outcome save_at_start(const std::string& id) override; + virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step) override; + + // Runs the rows of `segment` in `range`; throws as the checkpoint + // loop did once a checkpoint has used up its attempts. + void run(Pokemon::Segment& segment, const Pokemon::SegmentRange& range); + +private: + Pokemon::Outcome reload(); + + StoryConsole m_console; + std::set m_saved; + std::map m_failures; +}; + +// Builds the table with a console runner and runs the checkpoints from +// `range.from` to `range.to` (the whole segment when both are unset). +void run_story_table( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const AutoStoryOptions& options, AutoStoryStats& stats, + const StorySegmentFactory& make_segment, const Pokemon::SegmentRange& range +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp new file mode 100644 index 0000000000..d4667cb8a0 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -0,0 +1,457 @@ +/* BDSP AutoStory - Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageDiff.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "PokemonBDSP_Autostory_Segment_01_tools.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +const char* walk_stop_name(WalkStop stop){ + switch (stop){ + case WalkStop::ARRIVED: return "arrived"; + case WalkStop::DIALOG: return "dialog"; + case WalkStop::TRANSITION: return "transition"; + case WalkStop::BLOCKED: return "blocked"; + case WalkStop::LIMIT: return "limit"; + } + return "?"; +} + +double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after){ + // Small copies: only the whole-view scroll matters, not detail. + return ImageMatch::pixel_RMSD(before.scale_to(192, 108), after.scale_to(192, 108)); +} + + +namespace{ + +VideoSnapshot snapshot_or_fail(VideoStream& stream, const std::string& label){ + VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No video frame.", stream); + } + return frame; +} + +// After a black screen: a wild battle if a battle screen shows up, else a transition. +bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context){ + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 100, 10, + BlackScreenWatcher::FinderType::GONE, 500ms); + const int ret = wait_until(stream, context, 10s, {{battle_menu}, {battle_dialog}, {gone}}); + if (ret == 2){ + // The battle intro also starts from black; give its first screen a moment. + BattleMenuWatcher late_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher late_dialog(COLOR_YELLOW); + return wait_until(stream, context, 2s, {{late_menu}, {late_dialog}}) >= 0; + } + return ret >= 0; +} + +// A grass encounter opens with a leaf animation, neither white nor black, and +// shows the battle's text box a few seconds later. After a change of the +// whole screen that walking cannot make, wait for the battle to show. +bool battle_follows(VideoStream& stream, ProControllerContext& context){ + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + return wait_until(stream, context, 5s, {{battle_menu}, {battle_dialog}}) >= 0; +} + +} + +// A step in tall grass that was started before the stick was released still +// finishes, and its encounter starts after the walk has ended. Watch a little +// longer, and run from it. Returns whether there was one. +bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const std::string& label){ + VideoSnapshot before = stream.video().snapshot(); + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = wait_until(stream, context, 1200ms, {{encounter_flash}, {black}, {battle_menu}, {battle_dialog}}); + bool battle = ret == 0 || ret == 2 || ret == 3 || (ret == 1 && black_screen_was_battle(stream, context)); + if (ret < 0){ + VideoSnapshot after = stream.video().snapshot(); + battle = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE && + battle_follows(stream, context); + } + if (!battle){ + return false; + } + stream.log("[Walk] " + label + ": wild battle after the walk; running."); + escape_wild_battle(stream, context, BattleType::STANDARD); + context.wait_for_all_requests(); + return true; +} +namespace{ + +// Samples the screen every 100ms for probe_walk() and stops it once the view +// has been still for 800ms. +class WalkProbeSampler : public VisualInferenceCallback{ +public: + WalkProbeSampler(WalkProbe& result, double moving_change, WallClock start) + : VisualInferenceCallback("WalkProbeSampler") + , m_result(result) + , m_moving_change(moving_change) + , m_start(start) + {} + virtual void make_overlays(VideoOverlaySet&) const override{} + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override{ + if (m_has_last && timestamp - m_last_time < 100ms){ + return false; + } + const Milliseconds at = std::chrono::duration_cast(timestamp - m_start); + ImageRGB32 small = frame.scale_to(192, 108); + if (m_has_last){ + const double change = ImageMatch::pixel_RMSD(m_last, small); + m_result.samples.push_back({at, change}); + if (change >= m_moving_change){ + if (m_result.first_move < 0ms){ + m_result.first_move = at; + } + m_result.last_move = at; + } + } + m_last = std::move(small); + m_last_time = timestamp; + m_has_last = true; + const Milliseconds still_since = m_result.last_move < 0ms ? 0ms : m_result.last_move; + return at - still_since >= 800ms; + } + +private: + WalkProbe& m_result; + double m_moving_change; + WallClock m_start; + ImageRGB32 m_last; + WallClock m_last_time; + bool m_has_last = false; +}; + +} + + +namespace{ +WalkResult walk_until_impl( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived, + bool wild, bool dpad, bool run, bool taps = false +); +} + +WalkResult walk_until( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived +){ + return walk_until_impl(stream, context, x, y, limit, label, arrived, true, false, false); +} + +WalkResult walk_until_dpad( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived, + bool run +){ + return walk_until_impl(stream, context, x, y, limit, label, arrived, true, true, run); +} + +WalkResult walk_tiles_dpad( + VideoStream& stream, ProControllerContext& context, + double x, double y, size_t tiles, const std::string& label +){ + return walk_until_impl(stream, context, x, y, 800ms * tiles, label, nullptr, true, true, false, true); +} + +WalkResult walk_until_no_wild( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +){ + return walk_until_impl(stream, context, x, y, limit, label, nullptr, false, false, false); +} + +namespace{ +WalkResult walk_until_impl( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived, + bool wild, bool dpad, bool run, bool taps +){ + // A tile a tap: 160 ms held, the rest released, so each chunk is one tile. + const Milliseconds CHUNK = taps ? 800ms : 400ms; + const DpadPosition dpad_direction = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; + WalkResult result{WalkStop::LIMIT, 0ms, 0}; + size_t unchanged = 0; + // The largest change a chunk of this walk made: a step. A bump into an + // NPC measured 17.2 after steps of 63 to 65 (r250a, Floaroma's old + // woman), over the fixed line of 16, so "still" is also anything under + // 40% of the walk's own steps. + double largest = 0; + + context.wait_for_all_requests(); + VideoSnapshot before = snapshot_or_fail(stream, label); + auto finish = [&](WalkStop stop){ + result.stop = stop; + if (wild && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && + run_from_late_encounter(stream, context, label) + ){ + result.wild_battles++; + } + stream.log("[Walk] " + label + ": " + walk_stop_name(stop) + " after " + + std::to_string(result.walked.count()) + "ms, " + + std::to_string(result.wild_battles) + " wild battle(s)."); + return result; + }; + + while (true){ + if (arrived && arrived(*before.frame)){ + return finish(WalkStop::ARRIVED); + } + if (result.walked >= limit){ + return finish(WalkStop::LIMIT); + } + const Milliseconds chunk = std::min(CHUNK, limit - result.walked); + + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + ShortDialogWatcher dialog(COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [&](ProControllerContext& context){ + if (taps){ + pbf_press_dpad(context, dpad_direction, 160ms, chunk - 160ms); + }else if (dpad && run){ + ssf_press_button(context, BUTTON_B, 0ms, chunk, 0ms); + ssf_press_dpad(context, dpad_direction, chunk, chunk, 0ms); + }else if (dpad){ + pbf_press_dpad(context, dpad_direction, chunk, 0ms); + }else{ + pbf_move_left_joystick(context, {x, y}, chunk, 0ms); + } + }, + {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} + ); + result.walked += chunk; + + bool battle = ret == 0 || ret == 2 || ret == 4; + if (ret == 1){ + battle = black_screen_was_battle(stream, context); + if (!battle){ + return finish(WalkStop::TRANSITION); + } + } + if (battle && !wild){ + stream.log("[Walk] " + label + ": a battle started; a trainer's here."); + return finish(WalkStop::DIALOG); + } + if (battle){ + result.wild_battles++; + stream.log("[Walk] " + label + ": wild battle; running."); + escape_wild_battle(stream, context, BattleType::STANDARD); + context.wait_for_all_requests(); + before = snapshot_or_fail(stream, label); + unchanged = 0; + continue; + } + if (ret == 3){ + return finish(WalkStop::DIALOG); + } + + context.wait_for_all_requests(); + VideoSnapshot after = snapshot_or_fail(stream, label); + const double change = walk_screen_change(*before.frame, *after.frame); + stream.log("[Walk] " + label + ": " + std::to_string(result.walked.count()) + + "ms, screen change " + std::to_string(change) + "."); + if (change > WALK_SCENE_CHANGE && !wild && battle_follows(stream, context)){ + stream.log("[Walk] " + label + ": a battle started; a trainer's here."); + return finish(WalkStop::DIALOG); + } + if (change > WALK_SCENE_CHANGE && battle_follows(stream, context)){ + result.wild_battles++; + stream.log("[Walk] " + label + ": wild battle (leaf opening); running."); + escape_wild_battle(stream, context, BattleType::STANDARD); + context.wait_for_all_requests(); + before = snapshot_or_fail(stream, label); + unchanged = 0; + continue; + } + // Two still chunks in a row, so one dropped frame does not stop us. A + // frame exactly the same is a stalled video or a game hitch, not a wall: + // standing at a wall still animates. + if (change == 0){ + before = std::move(after); + continue; + } + largest = std::max(largest, change); + const double line = taps ? std::max(WALK_TAP_BLOCKED_CHANGE, 0.4 * largest) : WALK_BLOCKED_CHANGE; + const bool still = change < line; + if (!still){ + result.moved_chunks++; + } + unchanged = still ? unchanged + 1 : 0; + if (unchanged >= 2){ + return finish(WalkStop::BLOCKED); + } + before = std::move(after); + } +} +} + +WalkResult walk_for( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds duration, const std::string& label +){ + WalkResult result{WalkStop::LIMIT, duration, 0}; + context.wait_for_all_requests(); + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + ShortDialogWatcher dialog(COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [&](ProControllerContext& context){ + pbf_move_left_joystick(context, {x, y}, duration, 0ms); + }, + {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} + ); + bool battle = ret == 0 || ret == 2 || ret == 4; + if (ret == 1){ + battle = black_screen_was_battle(stream, context); + if (!battle){ + result.stop = WalkStop::TRANSITION; + } + } + if (battle){ + result.wild_battles = 1; + stream.log("[Walk] " + label + ": wild battle; running."); + escape_wild_battle(stream, context, BattleType::STANDARD); + context.wait_for_all_requests(); + } + if (ret == 3){ + result.stop = WalkStop::DIALOG; + } + // An encounter after the hold leaves us where the walk ended, so it does + // not count as cutting the walk short. + if (ret < 0){ + run_from_late_encounter(stream, context, label); + } + stream.log("[Walk] " + label + ": " + walk_stop_name(result.stop) + " after a " + + std::to_string(duration.count()) + "ms hold, " + std::to_string(result.wild_battles) + " wild battle(s)."); + return result; +} + +WalkResult walk_to_talk( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +){ + WalkResult total{WalkStop::LIMIT, 0ms, 0}; + while (total.walked < limit){ + const WalkResult step = walk_until(stream, context, x, y, + std::min(400ms, limit - total.walked), label); + total.walked += step.walked; + total.wild_battles += step.wild_battles; + if (step.stop == WalkStop::DIALOG || step.stop == WalkStop::TRANSITION){ + total.stop = step.stop; + return total; + } + ShortDialogWatcher dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [](ProControllerContext& context){ + pbf_press_button(context, BUTTON_A, 80ms, 720ms); + }, + {{dialog}} + ); + if (ret == 0){ + total.stop = WalkStop::DIALOG; + stream.log("[Walk] " + label + ": talking after " + std::to_string(total.walked.count()) + "ms."); + return total; + } + } + stream.log("[Walk] " + label + ": nobody to talk to within " + std::to_string(limit.count()) + "ms."); + return total; +} + +WalkProbe probe_walk( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, + double moving_change, const std::string& label +){ + WalkProbe result; + context.wait_for_all_requests(); + WalkProbeSampler sampler(result, moving_change, current_time()); + run_until( + stream, context, + [&](ProControllerContext& context){ + pbf_move_left_joystick(context, {x, y}, limit, 0ms); + }, + {{sampler}} + ); + context.wait_for_all_requests(); + stream.log("[Walk] " + label + ": probe moved from " + std::to_string(result.first_move.count()) + + "ms to " + std::to_string(result.last_move.count()) + "ms."); + return result; +} + +WalkResult walk_to_wall( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +){ + const WalkResult result = walk_until(stream, context, x, y, limit, label); + if (result.stop == WalkStop::DIALOG || result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": " + walk_stop_name(result.stop) + " instead of a wall.", stream); + } + if (result.stop == WalkStop::LIMIT){ + stream.log("[Walk] " + label + ": held for the whole limit without seeing the stop; taking the wall as reached."); + } + return result; +} + +WalkResult walk_expecting( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + WalkStop expected, + const std::function& arrived +){ + const WalkResult result = walk_until(stream, context, x, y, limit, label, arrived); + if (result.stop != expected){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": expected " + walk_stop_name(expected) + ", got " + + walk_stop_name(result.stop) + ".", stream); + } + return result; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h new file mode 100644 index 0000000000..ce1ab4cc76 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -0,0 +1,167 @@ +/* BDSP AutoStory - Walk + * + * From: https://github.com/PokemonAutomation/ + * + * Walking that reads the screen instead of trusting a duration: hold a + * direction and stop on whatever the screen says happened. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walk_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Walk_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class WalkStop{ + ARRIVED, // `arrived` matched. + DIALOG, // A dialogue box opened: an NPC or a trainer stopped us. + TRANSITION, // The screen went black and came back without a battle: a door or a new area. + BLOCKED, // The screen stopped changing: we are walking into a wall. + LIMIT, // Walked `limit` without any of the above. +}; +const char* walk_stop_name(WalkStop stop); + +struct WalkResult{ + WalkStop stop; + Milliseconds walked; + size_t wild_battles; + // Chunks whose screen change passed the blocked threshold: on a tap + // walk, the taps that moved a tile. A trainer's freeze and a bump do + // not count (r248a: "4 taps before the battle" were 2 steps and 2 + // taps into Taylor's freeze). + size_t moved_chunks = 0; +}; + +// Holds the left stick at (x, y) until one of the WalkStop events. Wild battles on +// the way are run from, and walking resumes. Does not throw for any WalkStop; the +// caller decides which ones are expected. +WalkResult walk_until( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived = nullptr +); + +// walk_until() on the D-pad, for a straight walk along a row or a column +// ((x, y) is one of the four directions). The D-pad moves whole tiles and +// keeps to the row: the stick drifts off it, catches tree corners and slides +// a row on stairs (Route 203, r226a and r227b; by hand the D-pad went +// through the same places). With `run`, B is held too: twice as fast, and +// as tile-true. +WalkResult walk_until_dpad( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived = nullptr, + bool run = false +); + +// A set number of whole tiles on the D-pad, a tap a tile (a tap moves +// exactly one tile; a stick hold timed for n tiles is a tile or two off), +// with the same watch for wild battles and dialogue as walk_until(). Ends +// LIMIT after `tiles` taps, or BLOCKED when two taps in a row move nothing. +WalkResult walk_tiles_dpad( + VideoStream& stream, ProControllerContext& context, + double x, double y, size_t tiles, const std::string& label +); + +// walk_until() where there are no wild Pokemon (a gym, a building): any +// battle that starts is a trainer's, so the walk stops with DIALOG, left in +// the battle for the caller to fight, instead of trying to run from it +// (r206d tried to run from the second Oreburgh Gym trainer). +WalkResult walk_until_no_wild( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +); + +// Holds the left stick at (x, y) for `duration` in one go, for a walk of a +// set length: walk_until() checks the screen between short holds, and each +// break can cost a step. Stops early for a dialog or a transition. A wild +// battle is run from and ends the walk with `wild_battles` 1, at a place the +// caller cannot know. +WalkResult walk_for( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds duration, const std::string& label +); + +// walk_until() that requires `expected`. Fails with the screenshot otherwise. +WalkResult walk_expecting( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label, + WalkStop expected, + const std::function& arrived = nullptr +); + +// After a move that may have walked onto an encounter tile (a tap in a cave +// or in tall grass): waits briefly for a wild battle and runs from it. +// Returns whether there was one; we may then be off the place we expected. +bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// walk_until() toward a wall the route was measured to end at. Seeing the +// stop only ends the hold early: holding on for the whole `limit` also +// counts as there, as people or animations nearby can keep the screen +// changing at a wall (s03a1: a hiker and a child by the Mart fence; s03c1: +// Roark's pickaxe; r162a: Jubilife's TV). A wrong place is caught by the +// next gate. Fails on a dialog or a transition: we are not where we think. +WalkResult walk_to_wall( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +); + +// Walks toward someone to talk to: after each short stretch, presses A and stops +// once a dialogue opens. Use it where walking into the person does not read as +// BLOCKED, e.g. in tall grass, whose sway keeps the screen changing. +WalkResult walk_to_talk( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +); + +// How much the screen changed between two frames, 0 for identical. Walking scrolls +// the whole view: 16 to 85 per 400ms on Route 202, against about 6 for running in +// place against trees. +double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after); +constexpr double WALK_BLOCKED_CHANGE = 10.0; +// For whole-tile D-pad taps: a tap that moves changes the screen by 21 to 50 +// (r244a: 40 per tap along Route 204's row 27); a tap into a wall or an NPC +// bumps for 10 to 13.4, and an NPC walking past does the same, so 10 counted +// bumps as steps (r244a, 2026-10-31). +constexpr double WALK_TAP_BLOCKED_CHANGE = 16.0; +// More than walking changes in 400ms: a new scene, such as a battle opening. +constexpr double WALK_SCENE_CHANGE = 120.0; + + +// One straight walk timed finely, to check a map against the game: holds the +// stick at (x, y) and samples the screen every 100ms until the view has been +// still for 800ms or `limit` runs out. Times count from the start of the hold. +// A sample moved if it changed by at least `moving_change`: still is 4 to 7, +// walking 16 to 85. Handles no battles or dialogs: use it on open ground, +// away from tall grass. +struct WalkProbe{ + Milliseconds first_move{-1}; // the first sample that moved; -1 if none did + Milliseconds last_move{-1}; // the last sample that moved; -1 if none did + struct Sample{ + Milliseconds at; + double change; // walk_screen_change() since the previous sample + }; + std::vector samples; +}; +WalkProbe probe_walk( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, + double moving_change, const std::string& label +); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp new file mode 100644 index 0000000000..7b425518b2 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp @@ -0,0 +1,267 @@ +/* BDSP AutoStory - Segment 01 + * From after choosing the starter Pokemon until after acquiring the first badge. + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include + +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageDiff.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" + +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "CommonTools/VisualDetectors/ImageMatchDetector.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" + +#include "PokemonBDSP_AutoStory_Segment_00_tools.h" + +#include "PokemonBDSP_Autostory_Segment_01_tools.h" + +using namespace std::chrono_literals; + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + +// Extracted from the Segment 01 Oreburgh Gate approach. Both callers use +// the same bounded down/right search and cancel queued input at the black fade. +// Oreburgh preserves its original north sweep; Coronet uses only down/right. +void enter_cave_with_down_right_search(VideoStream& stream, + ProControllerContext& context, + const std::string& label, + Milliseconds north_fallback){ + context.wait_for_all_requests(); + for (size_t attempt = 0; attempt < 3; attempt++){ + BlackScreenWatcher black_screen; + const int result = run_until(stream, + context, + [north_fallback](ProControllerContext& ctx){ + for (size_t i = 0; i < 10; i++){ + pbf_press_button(ctx, BUTTON_DOWN, 180ms, 0ms); + pbf_press_button(ctx, BUTTON_RIGHT, 180ms, 0ms); + pbf_press_button(ctx, BUTTON_RIGHT, 180ms, 0ms); + } + if (north_fallback > 0ms){ + pbf_move_left_joystick(ctx, {0, 1}, north_fallback, 0ms); + } + }, + {{black_screen}}); + context.wait_for_all_requests(); + if (result == 0){ + stream.log(label + ": Black screen detected."); + return; + } + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No black screen after 3 attempts.", + stream); +} + +static bool mine_confirm_escape_via_menu(VideoStream& stream, ProControllerContext& context, BattleType battle_type){ + MenuWatcher game_menu; + OverworldWatcher overworld; + BattleMenuWatcher battle_menu(battle_type, COLOR_YELLOW); + for (size_t attempt = 1; attempt <= 3; attempt++){ + // Do not accept the absence-based overworld detector before menu proof. + const int state = wait_until(stream, context, 300ms, {{game_menu}, {battle_menu}}); + if (state == 1){ + stream.log("[Mine escape] Battle menu still present; retrying Run."); + return false; + } + int detected = state; + if (state != 0){ + stream.log("[Mine escape] Main menu fallback attempt " + std::to_string(attempt) + "/3."); + detected = run_until(stream, + context, + [](ProControllerContext& controller){ + pbf_press_button(controller, BUTTON_X, 160ms, 840ms); + pbf_wait(controller, 3s); + controller.wait_for_all_requests(); + }, + {game_menu, battle_menu}); + if (detected == 1){ + return false; + } + } + if (detected != 0){ + continue; + } + stream.log("[Mine escape] Main game menu confirmed; battle has ended.", COLOR_GREEN); + // Close the confirmed menu before returning control to navigation. + MenuDetector menu_visible; + for (size_t close = 0; close < 3; close++){ + pbf_press_button(context, BUTTON_B, 80ms, 500ms); + context.wait_for_all_requests(); + if (wait_until(stream, context, 1s, {{overworld}}) == 0){ + return true; + } + BlackScreenWatcher visible(COLOR_YELLOW, + {0.1, 0.1, 0.8, 0.8}, + 100, + 10, + BlackScreenWatcher::FinderType::GONE, + 500ms); + if (wait_until(stream, context, 2s, {{visible}}) == 0){ + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && !menu_visible.detect(frame)){ + stream.log("[Mine escape] Confirmed game menu closed; returning to navigation.", COLOR_GREEN); + return true; + } + } + } + stream.log("[Mine escape] Main menu detected but could not confirm it closed.", COLOR_ORANGE); + return false; + } + return false; +} + +static bool mine_select_run(VideoStream& stream, ProControllerContext& context, BattleType battle_type){ + BattleMenuDetector cursor(battle_type, COLOR_YELLOW); + size_t matching_frames = 0; + for (size_t check = 0; check < 16; check++){ + context.wait_for_all_requests(); + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + const BattleMenuOption selected = battle_menu_selected_option(cursor, frame); + stream.log( + "[Mine escape] Battle cursor row=" + std::to_string(static_cast(selected) + 1) + "; target Run=4."); + if (selected == BattleMenuOption::RUN){ + if (++matching_frames >= 3){ + stream.log("[Mine escape] Run cursor confirmed.", COLOR_GREEN); + return true; + } + pbf_wait(context, 100ms); + }else{ + matching_frames = 0; + if (selected == BattleMenuOption::UNKNOWN){ + // Do not send a direction or A without a known cursor. + pbf_wait(context, 150ms); + }else{ + const DpadPosition direction = selected == BattleMenuOption::BATTLE ? DPAD_UP : DPAD_DOWN; + pbf_press_dpad(context, direction, 80ms, 250ms); + } + } + } + stream.log("[Mine escape] Could not confirm Run selection; A was not pressed.", COLOR_ORANGE); + return false; +} + +void escape_wild_battle(VideoStream& stream, + ProControllerContext& context, + BattleType battle_type, + std::chrono::seconds timeout){ + EndBattleWatcher battle_end; + bool exit_transition = false; + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline){ + pbf_press_button(context, BUTTON_B, 40ms, 200ms); + context.wait_for_all_requests(); + BattleMenuWatcher menu_ready(battle_type, COLOR_YELLOW); + const int ready = wait_until(stream, context, 3s, {{battle_end}, {menu_ready}}); + if (ready == 0){ + exit_transition = true; + break; + } + if (ready != 1){ + continue; + } + if (!mine_select_run(stream, context, battle_type)){ + continue; + } + stream.log("[Mine escape] Run input sent; watching for black-screen exit."); + const int result = run_until(stream, + context, + [](ProControllerContext& controller){ + pbf_press_button(controller, BUTTON_A, 60ms, 0ms); + pbf_wait(controller, 4s); + controller.wait_for_all_requests(); + }, + {battle_end}); + if (result == 0){ + exit_transition = true; + break; + } + } + if (!exit_transition && wait_until(stream, context, 10s, {{battle_end}}) == 0){ + exit_transition = true; + } + if (exit_transition){ + // EndBattleWatcher requires black followed by a visible screen. + stream.log("[Mine escape] Black-screen exit transition confirmed.", COLOR_GREEN); + pbf_wait(context, 800ms); + context.wait_for_all_requests(); + return; + } + // Only probe X after normal escape attempts and the final transition wait. + // Absence of battle UI by itself is not evidence of an overworld return. + stream.log("[Mine escape] Exit transition missed; trying late main-menu fallback."); + if (mine_confirm_escape_via_menu(stream, context, battle_type)){ + return; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "escape_wild_battle: Exit transition and late main-menu fallback could not confirm escape.", + stream); +} + +void Barry_1_fight(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context){ + stream.log("Starting Barry 1 battle..."); + + // State machine: loop until EndBattleWatcher fires + for (size_t c = 0; c < 20; c++){ + context.wait_for_all_requests(); + + BattleMenuWatcher battle_menu(BattleType::STANDARD); + EndBattleWatcher end_battle; + int ret = run_until(stream, + context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_ZL, 120000ms); + }, + { + battle_menu, + end_battle, + }); + switch (ret){ + case 0: + stream.log("Battle menu detected - using first move."); + pbf_press_button(context, BUTTON_ZL, 160ms, 1000ms); + break; + case 1: + stream.log("Barry 1 battle complete."); + pbf_mash_button(context, BUTTON_B, 5000ms); + context.wait_for_all_requests(); + return; + default: + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Barry_1_fight: Timed out waiting for battle menu or end.", + stream); + } + } + + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Barry_1_fight: Battle did not end after 20 turns.", + stream); +} + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h new file mode 100644 index 0000000000..0a5d2378f8 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h @@ -0,0 +1,28 @@ +// Shared battle helpers used by AutoStory routes. +#pragma once + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { +// Reuse the Oreburgh Gate down/right entry search. Returns at fade start; +// caller owns destination-specific fade completion and interior confirmation. +// north_fallback preserves Oreburgh's final north sweep when explicitly set. +void enter_cave_with_down_right_search(VideoStream& stream, + ProControllerContext& context, + const std::string& label, + Milliseconds north_fallback = Milliseconds(0)); + +// Shared confirmed-Run escape: verifies exit transition or main menu. +void escape_wild_battle(VideoStream& stream, + ProControllerContext& context, + BattleType battle_type = BattleType::STANDARD, + std::chrono::seconds timeout = std::chrono::seconds(30)); + +// Shared legacy first-move battle routine; consolidation remains preliminary. +void Barry_1_fight(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context); +} +} +} diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 834a1194f5..7e72c2b3d2 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1585,6 +1585,18 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.cpp @@ -1673,8 +1685,24 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/PokemonBDSP_Panels.h Source/PokemonBDSP/PokemonBDSP_Settings.cpp Source/PokemonBDSP/PokemonBDSP_Settings.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h + Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp + Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h + Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp + Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h + Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp + Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp @@ -1683,6 +1711,14 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h Source/PokemonBDSP/PokemonBDSP_Tests.cpp Source/PokemonBDSP/PokemonBDSP_Tests.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.cpp Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomousState.cpp From e7e4161cd73aa17ee35113092eab5b40000d04c0 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 12:02:00 -0800 Subject: [PATCH 03/98] BDSP AutoStory tests: the story simulated end to end, and the program's units StorySimulation runs segments 00 and 01's table from 00.00 to Floaroma against a fake game, in English and in Japanese; StoryTables checks the table's rows; Indoors and the AutoStory tests cover the walker, the location reads, the Pokemon Center and the Sandgem and Jubilife events. Registered in PokemonBDSP_Tests, without #1717's Bag and MenuDetector screenshot tests. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 8 + .../PokemonBDSP_AutoStory_Indoors_Tests.cpp | 195 ++ .../PokemonBDSP_AutoStory_Indoors_Tests.h | 27 + ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 508 ++++ ...emonBDSP_AutoStory_StorySimulation_Tests.h | 27 + ...okemonBDSP_AutoStory_StoryTables_Tests.cpp | 141 ++ .../PokemonBDSP_AutoStory_StoryTables_Tests.h | 27 + .../AutoStory/PokemonBDSP_AutoStory_Tests.cpp | 2104 +++++++++++++++++ .../AutoStory/PokemonBDSP_AutoStory_Tests.h | 26 + SerialPrograms/cmake/SourceFiles.cmake | 8 + 10 files changed, 3071 insertions(+) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 608126f0db..46784cafa8 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -6,7 +6,11 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h" #include "PokemonBDSP_Tests.h" namespace PokemonAutomation{ @@ -17,7 +21,11 @@ namespace PokemonBDSP{ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_MapGrids(database); + add_tests_AutoStory(database); add_tests_AutoStory_Segment00(database); + add_tests_AutoStory_StorySimulation(database); + add_tests_AutoStory_StoryTables(database); + add_tests_AutoStory_Indoors(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp new file mode 100644 index 0000000000..d3762d2c1e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp @@ -0,0 +1,195 @@ +/* BDSP AutoStory Indoors Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h" +#include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Indoors_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Direction; +using Pokemon::MapSet; +using Pokemon::TilePos; + +// 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 +// evening outside (warm all over), a room (warm, black along the bottom), +// a fade (black all over), and warm with a thin dark edge (a shadow). +struct IndoorsCase{ + std::string name; + uint32_t fill; + double dark_bottom; // how much of the screen's height, from the bottom, is black + bool indoors; +}; + +class Test_IndoorsAnyLight : public UnitTest{ +public: + Test_IndoorsAnyLight() + : UnitTest("PokemonBDSP::AutoStory - indoors() under the evening tint") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "evening outside, orange all over", .fill = 0xffe0a040, .dark_bottom = 0, .indoors = false}, + {.name = "a room: orange, black below its floor", .fill = 0xffe0a040, .dark_bottom = 0.10, .indoors = true}, + {.name = "a fade: black all over", .fill = 0xff000000, .dark_bottom = 0, .indoors = false}, + {.name = "orange with a thin shadow at the bottom", .fill = 0xffe0a040, .dark_bottom = 0.01, .indoors = false}, + {.name = "daytime grass", .fill = 0xff60b040, .dark_bottom = 0.10, .indoors = false}, + }; + for (const IndoorsCase& item : cases){ + ImageRGB32 screen(1920, 1080); + const size_t dark_from = (size_t)(1080 * (1 - item.dark_bottom)); + for (size_t y = 0; y < 1080; y++){ + for (size_t x = 0; x < 1920; x++){ + screen.pixel(x, y) = y >= dark_from ? 0xff000000 : item.fill; + } + } + if (indoors(screen) != item.indoors){ + return item.name + (item.indoors ? ": read as outside." : ": read as inside."); + } + } + return true; + } +}; + +// door_fade_look(): out of a door is a dark look, then a bright one that is +// not a room. r259 turned round in Sandgem's lit Center, read "not a room" +// once at full brightness, and walked east inside it as if outside. +struct DoorLook{ + double brightness; + bool room; +}; +struct DoorCase{ + std::string name; + std::vector looks; + // The look (1-based) that reads out, or 0 for none. + size_t out_at; +}; + +class Test_DoorFade : public UnitTest{ +public: + Test_DoorFade() + : UnitTest("PokemonBDSP::AutoStory - out of a door needs its fade") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "r259: a bright look in the Center that misses the room", .looks = {{458, true}, {440, false}, {457, true}}, .out_at = 0}, + {.name = "the fade, then the town", .looks = {{458, true}, {300, true}, {60, false}, {310, false}}, .out_at = 4}, + {.name = "the fade, then the room again", .looks = {{458, true}, {60, false}, {420, true}}, .out_at = 0}, + {.name = "the fade, still dim", .looks = {{60, false}, {150, false}, {320, false}}, .out_at = 3}, + }; + for (const DoorCase& item : cases){ + DoorFadeWatch watch; + size_t out_at = 0; + for (size_t i = 0; i < item.looks.size() && out_at == 0; i++){ + if (door_fade_look(watch, item.looks[i].brightness, item.looks[i].room)){ + out_at = i + 1; + } + } + if (out_at != item.out_at){ + return item.name + ": out at look " + std::to_string(out_at) + ", expected " + std::to_string(item.out_at) + "."; + } + } + return true; + } +}; + +// 00.04c's Potions: the Mart lady's walk (after_catch_potions() and the Mart lady checkpoint) +// is walks until stopped, so on the grid each leg ends at its wall: from +// a tile to three below the Center's door, a tap east and north to the +// Center's front, west to the Lab's sign, a row south and west to +// her grass's edge, north to the trees on her row (she stands east of +// there), back a row south and east to Route 201's sign, a row south and +// east to the trees east of the Mart, and north to the front row's end. +struct LadyLeg{ + Direction direction; + size_t tiles; // 0: walk until the wall + TilePos end; // world tile + std::string name; +}; + +class Test_MartLadyWalk : public UnitTest{ +public: + Test_MartLadyWalk() + : UnitTest("PokemonBDSP::AutoStory - 00.04: the walk to the Poke Mart lady and back") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const MapSet& maps = story_maps(); + const auto sandgem = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; + const auto route201 = [&](const TilePos& pos){ return maps.to_world(MAP_ROUTE_201, pos); }; + const std::vector legs{ + {Direction::EAST, 1, sandgem({18, 0}), "a tap east under the Center's wall"}, + {Direction::NORTH, 0, sandgem({18, 11}), "north to the Center's front"}, + {Direction::WEST, 0, east_of_lab_sign(), "west to the Lab's sign"}, + {Direction::SOUTH, 1, sandgem({6, 12}), "a row south onto the path"}, + {Direction::WEST, 0, route201({42, 5}), "west to her grass's edge"}, + {Direction::NORTH, 0, route201({42, 3}), "north to the trees, on her row"}, + {Direction::SOUTH, 1, route201({42, 4}), "a row south onto the signs' row"}, + {Direction::EAST, 0, route201_sign_west(), "east to Route 201's sign"}, + {Direction::SOUTH, 1, route201({54, 5}), "a row south onto the path"}, + {Direction::EAST, 0, sandgem({29, 12}), "east to the trees east of the Mart"}, + {Direction::NORTH, 0, sandgem(SandgemTown::FRONT_ROW_EAST_END), "north to the front row's end"}, + }; + // Out of the Center's door we stand a tile to three below it (r261). + for (int below = 1; below <= 3; below++){ + const std::string error = follow(maps, legs, sandgem({17, 10 + below}), logger); + if (!error.empty()){ + return "From " + std::to_string(below) + " below the door: " + error; + } + } + return true; + } + +private: + static std::string follow(const MapSet& maps, const std::vector& legs, TilePos at, Logger& logger){ + for (const LadyLeg& leg : legs){ + if (leg.tiles == 0){ + at = Pokemon::walk_to_wall(maps.world(), at, leg.direction).stop; + }else{ + const std::optional next = maps.world().step(at, leg.direction); + if (!next){ + return leg.name + ": blocked at " + maps.describe(at) + "."; + } + at = *next; + } + // The tap east lands on whichever row we stood on. + if (&leg != &legs.front() && !(at == leg.end)){ + return leg.name + ": ends at " + maps.describe(at) + ", not " + maps.describe(leg.end) + "."; + } + logger.log(leg.name + ": " + maps.describe(at)); + } + return ""; + } +}; + +} + + +void add_tests_AutoStory_Indoors(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h new file mode 100644 index 0000000000..9feb34f088 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Indoors Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Indoors_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Indoors_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// indoors() on synthetic screens under any light: the evening outside, +// a room, a fade, a thin shadow, grass. +void add_tests_AutoStory_Indoors(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp new file mode 100644 index 0000000000..a3b6f3f2b6 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -0,0 +1,508 @@ +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + * A run of the whole story table, new game to Floaroma, without the + * console: segment_00_table() and segment_01_table() built into segments + * as the program builds them (story_table_segment()), run by the shared + * runner with the program's retry policy, against a fake game. The fake + * plans each ROUTE on the built-in JSON maps from where the run's pose + * says we stand, walks each LEGS tile by tile on them, and passes every + * other step unless told to fail it. Steps that still call an old + * function pass as a whole. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "Utils/PokemonBDSP_AutoStory_StoryExecutor.h" +#include "Utils/PokemonBDSP_AutoStory_StoryTables.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StorySimulation_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Outcome; +using Pokemon::RunState; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + +std::string describe(const std::vector& tiles){ + std::string text; + for (const TilePos& pos : tiles){ + const std::optional local = story_maps().to_local(pos); + text += " " + (local ? local->map + " " + Pokemon::to_string(local->pos) : Pokemon::to_string(pos)); + } + return text; +} + +std::string joined(const std::vector& lines){ + std::string text; + for (const std::string& line : lines){ + text += "\n " + line; + } + return text; +} + +bool contains(const std::vector& tiles, const TilePos& pos){ + return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); +} + +// Where a ROUTE step ends from `from`, planned as ConsoleStoryExecutor +// plans it, or why it does not plan. +struct Walked{ + std::vector at; + std::string error; + std::string plan; +}; +Walked plan_route(const std::vector& from, const Pokemon::StoryStep& step){ + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + return {{}, step.name + ": no plan costs named \"" + step.variant + "\".", ""}; + } + const std::vector goal = Pokemon::tiles_of(*step.target); + const auto plan = Pokemon::plan_robust_route(story_maps().world(), from, + [&](const TilePos& pos){ return contains(goal, pos); }, *costs); + if (!plan){ + return {{}, step.name + ": no plan from" + describe(from) + ".", ""}; + } + Walked walked{from, "", std::format("{}: {} legs from{}", step.name, plan->size(), describe(from))}; + for (const Pokemon::PlannedLeg& leg : *plan){ + walked.plan += "\n " + to_string(leg.leg) + " to" + describe(leg.after); + } + if (!plan->empty()){ + walked.at = plan->back().after; + } + for (const TilePos& pos : walked.at){ + if (!contains(goal, pos)){ + return {{}, step.name + ": the plan may end off the target, at" + describe({pos}) + ".", walked.plan}; + } + } + return walked; +} + +// Where a LEGS step ends from `from`, walked tile by tile on the maps, a +// tile short too where the step allows it. +Walked walk_legs(const std::vector& from, const Pokemon::StoryStep& step){ + const Pokemon::TileGrid& world = story_maps().world(); + std::vector at = from; + for (const Pokemon::StoryLeg& leg : step.legs){ + std::vector next; + for (TilePos pos : at){ + for (int i = 0; i < leg.tiles; i++){ + const std::optional stepped = world.step(pos, leg.direction); + if (!stepped){ + return {{}, step.name + ": " + direction_name(leg.direction) + " is blocked at" + describe({pos}) + ".", ""}; + } + if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ + next.push_back(pos); + } + pos = *stepped; + } + next.push_back(pos); + } + at = std::move(next); + } + return {at, "", ""}; +} + + +// Saves at a row's start into the trace, as the console runner saves. +class FakeRunner : public StoryStepRunner{ +public: + std::vector& trace; + std::set saved; + + explicit FakeRunner(std::vector& trace) + : trace(trace) + {} + virtual bool saved_at_start(const std::string& id) const override{ + return saved.contains(id); + } + virtual Outcome save_at_start(const std::string& id) override{ + saved.insert(id); + trace.push_back("save " + id); + return Pokemon::outcome_ok(); + } + virtual Outcome run_step(const std::string&, StoryFailure, const StoryAction&) override{ + return Pokemon::outcome_failed("no_console"); + } +}; + +// The game: steps pass, routes and legs move the pose on the maps, and +// the steps in `failures` ("/") fail, so many times, as said. +class FakeGame : public Pokemon::StoryStepExecutor{ +public: + struct Failure{ + std::string failure; + size_t times = 0; + }; + + std::vector& trace; + std::map failures; + // Every route's plan, for the log. + std::vector plans; + + explicit FakeGame(std::vector& trace) + : trace(trace) + {} + + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ + m_row = data.id; + trace.push_back("begin " + data.id); + } + virtual Outcome run(RunState& state, const Pokemon::StoryStep& step) override{ + const std::string key = m_row + "/" + step.name; + trace.push_back(key); + auto failure = failures.find(key); + if (failure != failures.end() && failure->second.times > 0){ + failure->second.times--; + return Pokemon::outcome_failed(failure->second.failure, key); + } + return Pokemon::StoryStepExecutor::run(state, step); + } + virtual Outcome route(RunState& state, const Pokemon::StoryStep& step) override{ + const Walked walked = plan_route(Pokemon::tiles_of(state.pose), step); + if (!walked.error.empty()){ + return Pokemon::outcome_failed("no_plan", walked.error); + } + plans.push_back(walked.plan); + return Pokemon::outcome_ok(Pokemon::tile_pose(step.target->map, walked.at)); + } + virtual Outcome legs(RunState& state, const Pokemon::StoryStep& step) override{ + const Walked walked = walk_legs(Pokemon::tiles_of(state.pose), step); + if (!walked.error.empty()){ + return Pokemon::outcome_failed("blocked", walked.error); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, walked.at)); + } + virtual Outcome step(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome mash(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome battle(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome train(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome heal(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome shop(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome save(RunState&, const Pokemon::StoryStep&) override{ + trace.push_back("save in " + m_row); + return Pokemon::outcome_ok(); + } + virtual Outcome game(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + +private: + std::string m_row; +}; + + +// One simulated run of both segments, as the program runs them. Its +// executor has a language no reader supports (Japanese), so a step that +// needed text would show. (The console's global language is left alone: +// tests run in parallel.) +struct Simulation{ + std::vector trace; + std::vector lines; + FakeRunner runner{trace}; + std::shared_ptr game = std::make_shared(trace); + Pokemon::SegmentPolicy policy; + + Pokemon::SegmentReport run(const std::string& id, const std::vector& table, const Pokemon::SegmentRange& range){ + game->set_language(Language::Japanese); + Pokemon::Segment segment = story_table_segment(runner, id, table, game); + Pokemon::SegmentHooks hooks; + hooks.reload = [this](RunState&){ + trace.push_back("reload"); + return Pokemon::outcome_ok(); + }; + hooks.log = [this](const std::string& line){ lines.push_back(line); }; + RunState state; + return Pokemon::run_segment(state, segment, range, policy, hooks); + } + // The rows begun, in order, one per attempt. + std::vector begun() const{ + std::vector ids; + for (const std::string& item : trace){ + if (item.starts_with("begin ")){ + ids.push_back(item.substr(6)); + } + } + return ids; + } + size_t count(const std::string& item) const{ + return (size_t)std::count(trace.begin(), trace.end(), item); + } + bool logged(const std::string& line) const{ + return std::find(lines.begin(), lines.end(), line) != lines.end(); + } +}; + +std::vector ids_of(const std::vector& table){ + std::vector ids; + for (const StoryRow& row : table){ + ids.push_back(row.id); + } + return ids; +} + + +UnitTestResult test_whole_table(Logger& logger){ + Simulation sim; + const Pokemon::SegmentReport first = sim.run("00", segment_00_table(), {}); + const Pokemon::SegmentReport second = sim.run("01", segment_01_table(), {}); + for (const std::string& plan : sim.game->plans){ + logger.log(plan); + } + if (!first.completed || !second.completed){ + return "Expected both segments through:" + joined(sim.lines); + } + std::vector expected = ids_of(segment_00_table()); + for (const std::string& id : ids_of(segment_01_table())){ + expected.push_back(id); + } + if (sim.begun() != expected){ + return "Unexpected checkpoint order:" + joined(sim.begun()); + } + // Every row saves first but 00.00, which a failure ends; 01.10 and + // 01.11 save again at their ends. + for (const std::string& id : expected){ + const size_t saves = sim.count("save " + id); + if (saves != (id == "00.00" ? 0 : 1)){ + return std::format("{} saved {} times at its start:{}", id, saves, joined(sim.trace)); + } + } + if (sim.count("save in 01.10") != 1 || sim.count("save in 01.11") != 1){ + return "Expected 01.10 and 01.11 to save at their ends:" + joined(sim.trace); + } + if (first.one_shot_passes() != first.checkpoints_run || second.one_shot_passes() != second.checkpoints_run){ + return "Expected every checkpoint one-shot:" + joined(sim.lines); + } + return true; +} + +UnitTestResult test_start_and_end(Logger&){ + Simulation sim; + Pokemon::SegmentReport report = sim.run("00", segment_00_table(), {.from = "00.04b", .to = "00.04d"}); + if (!report.completed || sim.begun() != std::vector{"00.04b", "00.04c", "00.04d"}){ + return "Expected 00.04b to 00.04d only:" + joined(sim.begun()); + } + Simulation tail; + report = tail.run("01", segment_01_table(), {.from = "01.10", .to = std::nullopt}); + if (!report.completed || tail.begun() != std::vector{"01.10", "01.11"}){ + return "Expected 01.10 to the end:" + joined(tail.begun()); + } + Simulation one; + report = one.run("00", segment_00_table(), {.from = "00.02", .to = "00.02"}); + if (!report.completed || one.begun() != std::vector{"00.02"}){ + return "Expected 00.02 alone:" + joined(one.begun()); + } + return true; +} + +// r72: a blackout in 00.04b's training. The checkpoint resets to the save +// it made at its start, does not save again, and walks from that save's +// pose to the grass again. +UnitTestResult test_training_blackout(Logger&){ + Simulation sim; + sim.game->failures["00.04b/train"] = {.failure = "blackout", .times = 1}; + const Pokemon::SegmentReport report = sim.run("00", segment_00_table(), {.from = "00.04b", .to = "00.04d"}); + const std::vector expected{"00.04b", "00.04b", "00.04c", "00.04d"}; + if (!report.completed || report.reloads != 1 || sim.begun() != expected){ + return "Expected 00.04b reloaded once, then on:" + joined(sim.trace); + } + if (sim.count("save 00.04b") != 1 || sim.count("00.04b/to_catch_grass") != 2){ + return "Expected the retry to start from 00.04b's one save and walk to the grass again:" + joined(sim.trace); + } + if (!sim.logged("[Checkpoint] 00.04b: passed on attempt 2 (1 reset)")){ + return "Expected the retried pass logged:" + joined(sim.lines); + } + return true; +} + +// A step that fails every time: three attempts from the checkpoint's +// save, then the segment stops on it and runs nothing after. +UnitTestResult test_gives_up_after_attempts(Logger&){ + Simulation sim; + sim.game->failures["01.04/oreburgh_gym"] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport report = sim.run("01", segment_01_table(), {}); + if (report.completed || report.reloads != 2 || report.failed_checkpoint != "01.04" || report.failed_event != "oreburgh_gym" + || report.runs.empty() || report.runs.back().attempts != 3 || sim.count("01.04/oreburgh_gym") != 3 + || sim.count("save 01.04") != 1 || sim.count("begin 01.05") != 0 + ){ + return "Expected 01.04 tried 3 times from its save, then a stop:" + joined(sim.trace) + joined(sim.lines); + } + // The stop-at-first-failure option: no reload. + Simulation lab; + lab.policy.stop_at_first_failure = true; + lab.game->failures["01.04/oreburgh_gym"] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport stopped = lab.run("01", segment_01_table(), {}); + if (stopped.completed || !stopped.stopped_at_first_failure || lab.count("reload") != 0){ + return "Expected a stop at the first failure without a reload:" + joined(lab.trace); + } + return true; +} + +// Each row that has a start pose starts where the row before it leaves +// the player, by the same planning and walking the fake run does. +UnitTestResult test_poses_chain(Logger&){ + for (const std::vector* table : {&segment_00_table(), &segment_01_table()}){ + std::optional> at; + std::string last = ""; + for (const StoryRow& row : *table){ + if (row.start){ + const std::vector start = Pokemon::tiles_of(*row.start); + if (at && !std::ranges::all_of(*at, [&](const TilePos& pos){ return contains(start, pos); })){ + return std::format("{} starts at{}, but {} may end at{}.", row.id, describe(start), last, describe(*at)); + } + at = start; + }else{ + at.reset(); + } + for (const Pokemon::StoryStep& step : row.steps){ + if (step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS){ + if (!at){ + return row.id + " " + step.name + ": walks from an unknown pose."; + } + const Walked walked = step.kind == StoryStepKind::ROUTE ? plan_route(*at, step) : walk_legs(*at, step); + if (!walked.error.empty()){ + return row.id + " " + walked.error; + } + at = walked.at; + }else if (step.kind == StoryStepKind::GAME || step.kind == StoryStepKind::SHOP){ + // A call to an old function: where it ends is the table's to say. + at.reset(); + } + if (step.pose_after){ + at = Pokemon::tiles_of(*step.pose_after); + } + } + last = row.id; + } + } + return true; +} + +// Every step that calls an old function names one the segment registers, +// and each table lints clean. +UnitTestResult test_variants_registered(Logger&){ + const struct{ const std::vector* table; std::map variants; } segments[] = { + {.table = &segment_00_table(), .variants = segment_00_variants()}, + {.table = &segment_01_table(), .variants = segment_01_variants()}, + }; + const std::set built_in{"", "town", "robust", "one_short_ok"}; + for (const auto& segment : segments){ + for (const StoryRow& row : *segment.table){ + const std::vector problems = Pokemon::lint_story_checkpoint(row); + if (!problems.empty()){ + return problems[0]; + } + for (const Pokemon::StoryStep& step : row.steps){ + if (!built_in.contains(step.variant) && !segment.variants.contains(step.variant)){ + return std::format("{} {}: no variant \"{}\" is registered.", row.id, step.name, step.variant); + } + } + } + } + return true; +} + +// With no reader for the game's language, the reads that used text give +// way: a banner or save card that showed matches any map, so walks check +// where they stand by the grid. +UnitTestResult test_no_text_reader(Logger&){ + for (Language language : {Language::Japanese, Language::Korean, Language::French}){ + if (story_reads_text(language)){ + return std::string("Expected no text reader for ") + language_data(language).name + "."; + } + } + if (!location_banner_matches(UNREAD_LOCATION, MAP_ROUTE_202) || location_banner_matches("", MAP_ROUTE_202)){ + return "Expected a shown, unread banner to match and no banner not to."; + } + if (!story_reads_text(Language::English) || location_banner_matches("sandgemtown", MAP_ROUTE_202)){ + return "Expected English banners read and compared."; + } + return true; +} + +// r261: out of the Center the walk saw no dark fade and walked on three +// rows. The walk stops at the first look that is not a room, fade or not. +UnitTestResult test_door_exit_looks(Logger&){ + const struct{ const char* name; std::vector> looks; size_t stops_at; } cases[] = { + {.name = "no fade: the first look outside", .looks = {{180, true}, {175, true}, {230, false}}, .stops_at = 2}, + {.name = "the fade, then outside", .looks = {{180, true}, {40, true}, {210, false}}, .stops_at = 2}, + {.name = "a dim look outside", .looks = {{180, true}, {150, false}, {230, false}}, .stops_at = 1}, + {.name = "still a room", .looks = {{180, true}, {40, true}, {190, true}}, .stops_at = 3}, + }; + for (const auto& c : cases){ + DoorFadeWatch watch; + size_t stopped = c.looks.size(); + for (size_t i = 0; i < c.looks.size(); i++){ + if (door_walk_stops(watch, c.looks[i].first, c.looks[i].second)){ + stopped = i; + break; + } + } + if (stopped != c.stops_at){ + return std::format("{}: stopped at look {}, expected {}.", c.name, stopped, c.stops_at); + } + } + return true; +} + + +class Test_StorySimulation : public UnitTest{ +public: + Test_StorySimulation(std::string name, std::function body) + : UnitTest("PokemonBDSP::AutoStory - Simulation: " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + +} + + +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database){ + database.add("00.00 to Floaroma, in order, each row saved first", test_whole_table); + database.add("start and end checkpoints", test_start_and_end); + database.add("a blackout in 00.04b's training resets to its save", test_training_blackout); + database.add("a step that keeps failing stops after 3 attempts", test_gives_up_after_attempts); + database.add("each start pose is where the row before ends; routes plan on the maps", test_poses_chain); + database.add("every called function is registered; tables lint", test_variants_registered); + database.add("out of a door: the first look that is not a room", test_door_exit_looks); + database.add("no text reader for the game's language", test_no_text_reader); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h new file mode 100644 index 0000000000..b893232ca3 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Segments 00 and 01 run end to end without a console: the story table, +// the shared segment runner and its retry policy, against a fake game +// that plans every route and walk on the built-in maps. +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp new file mode 100644 index 0000000000..82dcc701ed --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp @@ -0,0 +1,141 @@ +/* BDSP AutoStory Story Tables Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" +#include "Utils/PokemonBDSP_AutoStory_StoryTables.h" +#include "PokemonBDSP_AutoStory_StoryTables_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// Builds tables without a console; nothing in them runs. +class DescribeOnlyRunner : public StoryStepRunner{ +public: + virtual bool saved_at_start(const std::string&) const override{ + return false; + } + virtual Pokemon::Outcome save_at_start(const std::string&) override{ + return Pokemon::outcome_failed("not_run"); + } + virtual Pokemon::Outcome run_step(const std::string&, StoryFailure, const StoryAction&) override{ + return Pokemon::outcome_failed("not_run"); + } +}; + +struct TableCase{ + std::string name; + std::function make; + std::string segment_id; + std::vector checkpoint_ids; + // Rows whose first event is not the save (they neither save nor retry). + std::vector unsaved; +}; + +const std::vector& table_cases(){ + static const std::vector cases{ + { + .name = "Segment 00", + .make = make_segment_00, + .segment_id = "00", + .checkpoint_ids = {"00.00", "00.01", "00.02", "00.03", "00.04", "00.04b", "00.04c", "00.04d", "00.05"}, + .unsaved = {"00.00"}, + }, + { + .name = "Segment 01", + .make = make_segment_01, + .segment_id = "01", + .checkpoint_ids = {"01.01", "01.02", "01.03", "01.04", "01.05", "01.06", "01.07", "01.08", "01.09", "01.10", "01.11"}, + .unsaved = {}, + }, + }; + return cases; +} + +// An empty string when `table` matches `expected`; else what differs. +std::string check_table(Pokemon::Segment& table, const TableCase& expected){ + if (table.id != expected.segment_id){ + return std::format("{}: id is \"{}\", expected \"{}\".", expected.name, table.id, expected.segment_id); + } + std::vector ids; + for (const Pokemon::CheckpointSpec& spec : table.checkpoints){ + ids.push_back(spec.id); + } + if (ids != expected.checkpoint_ids){ + std::string read; + for (const std::string& id : ids){ + read += " " + id; + } + return std::format("{}: {} rows,{}; expected {}.", expected.name, ids.size(), read, expected.checkpoint_ids.size()); + } + for (Pokemon::CheckpointSpec& spec : table.checkpoints){ + Pokemon::Checkpoint checkpoint = spec.build(); + if (checkpoint.id != spec.id){ + return std::format("{}: row {} builds checkpoint \"{}\".", expected.name, spec.id, checkpoint.id); + } + if (checkpoint.events.empty()){ + return std::format("{}: {} has no events.", expected.name, spec.id); + } + const bool unsaved = std::find(expected.unsaved.begin(), expected.unsaved.end(), spec.id) != expected.unsaved.end(); + const bool saves_first = checkpoint.events[0].name == "save"; + if (saves_first == unsaved){ + return std::format("{}: {}'s first event is \"{}\", expected {}.", + expected.name, spec.id, checkpoint.events[0].name, unsaved ? "a step" : "the save"); + } + const std::vector problems = Pokemon::lint_checkpoint(checkpoint); + if (!problems.empty()){ + return std::format("{}: {} lint: {}", expected.name, spec.id, problems[0]); + } + } + return ""; +} + +class Test_StoryTables : public UnitTest{ +public: + Test_StoryTables() + : UnitTest("PokemonBDSP::AutoStory - Story tables: rows, events and describe()") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + for (const TableCase& expected : table_cases()){ + DescribeOnlyRunner runner; + Pokemon::Segment table = expected.make(runner); + const std::string problem = check_table(table, expected); + if (!problem.empty()){ + return problem; + } + const std::string text = Pokemon::describe(table); + for (const std::string& id : expected.checkpoint_ids){ + if (text.find(id) == std::string::npos){ + return std::format("{}: describe() does not name {}:\n{}", expected.name, id, text); + } + } + logger.log(text); + } + return true; + } +}; + +} + + +void add_tests_AutoStory_StoryTables(UnitTestDatabase& database){ + database.add(); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h new file mode 100644 index 0000000000..c8066e5016 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Story Tables Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// Offline checks of the Segment 00 and 01 tables: their rows in order, +// each checkpoint built and described without a console, none linted. +void add_tests_AutoStory_StoryTables(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp new file mode 100644 index 0000000000..9362b8c365 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp @@ -0,0 +1,2104 @@ +/* BDSP AutoStory Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "Data/PokemonBDSP_AutoStory_JubilifeCity.h" +#include "Data/PokemonBDSP_AutoStory_OreburghMine.h" +#include "Data/PokemonBDSP_AutoStory_RavagedPath.h" +#include "Data/PokemonBDSP_AutoStory_Route203.h" +#include "Data/PokemonBDSP_AutoStory_Route204.h" +#include "Data/PokemonBDSP_AutoStory_Route204South.h" +#include "Data/PokemonBDSP_AutoStory_RouteRecords.h" +#include "Data/PokemonBDSP_AutoStory_Route201.h" +#include "Data/PokemonBDSP_AutoStory_Route202.h" +#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "Utils/PokemonBDSP_AutoStory_NewGame.h" +#include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h" +#include "PokemonBDSP_AutoStory_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Direction; +using Pokemon::TileGrid; +using Pokemon::TileLeg; +using Pokemon::TilePos; +using Pokemon::TileType; + +// A small map with every tile type. +// Row 2 is a ledge with a gap at x = 4. The bottom row has a sign at (1,4), +// an NPC at (3,4) and a door at (5,4); (3,0) is off the map. +const TileGrid& small_grid(){ + static const TileGrid grid({ + R"(#..?..#)", + R"(#.."".#)", + R"(#vvv..#)", + R"(#.....#)", + R"(#S.N.D#)", + }); + return grid; +} + +class Test_TileGridParse : public UnitTest{ +public: + Test_TileGridParse() + : UnitTest("PokemonBDSP::AutoStory - tile grid parse") + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const TileGrid& grid = small_grid(); + if (grid.width() != 7 || grid.height() != 5){ + return "Small grid is " + std::to_string(grid.width()) + "x" + std::to_string(grid.height()) + "."; + } + struct Case{ + TilePos pos; + TileType type; + }; + const Case cases[] = { + {{0, 0}, TileType::BLOCKED}, + {{1, 0}, TileType::WALKABLE}, + {{3, 1}, TileType::TALL_GRASS}, + {{2, 2}, TileType::LEDGE}, + {{3, 4}, TileType::NPC}, + {{1, 4}, TileType::SIGN}, + {{5, 4}, TileType::DOOR}, + {{3, 0}, TileType::UNKNOWN}, + {{-1, 0}, TileType::UNKNOWN}, + {{7, 4}, TileType::UNKNOWN}, + }; + for (const Case& c : cases){ + if (grid.at(c.pos) != c.type){ + return "Tile " + Pokemon::to_string(c.pos) + " parsed as " + std::to_string((int)grid.at(c.pos)) + "."; + } + } + for (const std::vector& bad : { + std::vector{"...", ".."}, + std::vector{"..x"}, + }){ + try{ + TileGrid parsed(bad); + return "A malformed grid was accepted."; + }catch (InternalProgramError&){} + } + + if (grid.rows()[4] != "#S.N.D#"){ + return "Small grid row 4 prints as " + grid.rows()[4] + "."; + } + + struct Map{ + const char* name; + const TileGrid& grid; + int width; + int height; + }; + const Map maps[] = { + {MAP_ROUTE_201, Route201::grid(), 64, 25}, + {MAP_SANDGEM_TOWN, SandgemTown::grid(), 34, 32}, + {MAP_ROUTE_202, Route202::grid(), 36, 32}, + }; + for (const Map& map : maps){ + if (map.grid.width() != map.width || map.grid.height() != map.height){ + return std::string(map.name) + " is " + std::to_string(map.grid.width()) + "x" + std::to_string(map.grid.height()) + "."; + } + } + + struct Named{ + const char* map; + TilePos pos; + TileType type; + }; + const Named named[] = { + {MAP_SANDGEM_TOWN, SandgemTown::LAB_DOOR, TileType::DOOR}, + {MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR, TileType::DOOR}, + {MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN, TileType::SIGN}, + {MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR, TileType::DOOR}, + {MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END, TileType::WALKABLE}, + {MAP_ROUTE_202, Route202::CATCH_SPOT, TileType::TALL_GRASS}, + {MAP_ROUTE_202, Route202::SAVE_SPOT, TileType::WALKABLE}, + {MAP_ROUTE_202, Route202::TRAINER_TRISTAN, TileType::NPC}, + {MAP_ROUTE_202, Route202::TRAINER_NATALIE, TileType::NPC}, + {MAP_ROUTE_202, Route202::TRAINER_LOGAN, TileType::NPC}, + {MAP_ROUTE_202, Route202::EXIT_JUBILIFE, TileType::WALKABLE}, + }; + const Pokemon::MapSet& world = story_maps(); + for (const Named& c : named){ + const TilePos pos = world.to_world(c.map, c.pos); + if (world.world().at(pos) != c.type){ + return std::string(c.map) + " tile " + Pokemon::to_string(c.pos) + " is type " + + std::to_string((int)world.world().at(pos)) + " in the joined maps."; + } + const std::optional local = world.to_local(pos); + if (!local || local->map != c.map || local->pos != c.pos){ + return std::string(c.map) + " tile " + Pokemon::to_string(c.pos) + " does not map back."; + } + } + // Sandgem's Poke Mart is next door east of its Center, on the same + // front row: 00.04 walks from the one door to the other. + { + const TilePos center = world.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); + const TilePos mart = world.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); + if (center.y != mart.y || mart.x <= center.x){ + return "The Poke Mart's door is not east of the Pokemon Center's on the same row."; + } + for (int x = center.x; x <= mart.x; x++){ + if (!world.world().standable({x, mart.y + 1})){ + return "The row below the Center's and the Mart's doors is not open at " + world.describe({x, mart.y + 1}) + "."; + } + } + } + + // The maps join where their exits meet: the grids are the game's cells. + struct Join{ + const char* from_map; + TilePos from; + Direction direction; + const char* to_map; + TilePos to; + }; + const Join joins[] = { + {MAP_ROUTE_201, {63, 4}, Direction::EAST, MAP_SANDGEM_TOWN, {0, 11}}, + {MAP_ROUTE_201, {63, 6}, Direction::EAST, MAP_SANDGEM_TOWN, {0, 13}}, + {MAP_SANDGEM_TOWN, {24, 0}, Direction::NORTH, MAP_ROUTE_202, {25, 31}}, + {MAP_SANDGEM_TOWN, {29, 0}, Direction::NORTH, MAP_ROUTE_202, {30, 31}}, + }; + for (const Join& join : joins){ + const TilePos from = world.to_world(join.from_map, join.from); + const std::optional next = world.world().step(from, join.direction); + if (!next || *next != world.to_world(join.to_map, join.to)){ + return "No step from " + world.describe(from) + " to " + join.to_map + " " + Pokemon::to_string(join.to) + "."; + } + } + Pokemon::MapSet overlapping; + overlapping.add("a", small_grid(), {0, 0}); + overlapping.add("b", small_grid(), {6, 4}); // shares one blocked tile + try{ + overlapping.add("c", small_grid(), {5, 4}); + return "Maps that overlap on a door were accepted."; + }catch (InternalProgramError&){} + return true; + } +}; + +class Test_TileGridWallHit : public UnitTest{ +public: + Test_TileGridWallHit() + : UnitTest("PokemonBDSP::AutoStory - tile grid wall hit") + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + struct Case{ + const TileGrid& grid; + TilePos from; + Direction direction; + TilePos stop; + size_t tiles; + bool anchored; + }; + const TileGrid& small = small_grid(); + const TileGrid& route = Route202::grid(); + const Case cases[] = { + {small, {1, 1}, Direction::EAST, {5, 1}, 4, true}, // through grass to the east wall + {small, {1, 0}, Direction::SOUTH, {1, 3}, 3, true}, // hops the ledge, stops at the sign + {small, {2, 3}, Direction::NORTH, {2, 3}, 0, true}, // a ledge stops a north walk + {small, {3, 3}, Direction::SOUTH, {3, 3}, 0, true}, // the NPC is a wall + {small, {4, 4}, Direction::NORTH, {4, 0}, 4, false}, // through the gap, off the top edge + {small, {4, 3}, Direction::EAST, {5, 3}, 1, true}, + {small, {4, 4}, Direction::EAST, {4, 4}, 0, false}, // into the door + {small, {2, 0}, Direction::EAST, {2, 0}, 0, false}, // off the map + {route, Route202::CATCH_SPOT, Direction::WEST, {3, 27}, 16, true}, + {route, Route202::CATCH_SPOT, Direction::NORTH, {19, 25}, 2, true}, + {route, Route202::CATCH_SPOT, Direction::EAST, {30, 27}, 11, true}, + // Measured on the console from the post-catch save. + {route, {30, 27}, Direction::NORTH, {30, 24}, 3, true}, + {route, {30, 24}, Direction::WEST, {22, 24}, 8, true}, + {route, Route202::TRAINER_TRISTAN.next(Direction::SOUTH), Direction::NORTH, {7, 14}, 0, true}, // Tristan holds the player + // After Natalie, as measured in r75: on the row right above the south + // ledge the east wall is at column 31; column 30 is open up to row 4. + {route, {22, 22}, Direction::EAST, {30, 22}, 8, true}, + {route, {30, 22}, Direction::NORTH, {30, 4}, 18, true}, + // Through the gap east of the north ledge, over the south ledge + // and off the bottom edge to Sandgem Town. + {route, {29, 4}, Direction::SOUTH, {29, 31}, 27, false}, + {route, {12, 8}, Direction::SOUTH, {12, 29}, 21, true}, // hops both ledges + {route, Route202::EXIT_JUBILIFE, Direction::NORTH, Route202::EXIT_JUBILIFE, 0, false}, + }; + auto check = [](const Case& c) -> std::string{ + const Pokemon::WallHit hit = Pokemon::walk_to_wall(c.grid, c.from, c.direction); + if (hit.stop != c.stop || hit.tiles != c.tiles || hit.anchored != c.anchored){ + return "Walking " + std::string(Pokemon::direction_name(c.direction)) + " from " + + Pokemon::to_string(c.from) + " stops at " + Pokemon::to_string(hit.stop) + " after " + + std::to_string(hit.tiles) + " tiles" + (hit.anchored ? "" : " (not anchored)") + + ", expected " + Pokemon::to_string(c.stop) + " after " + std::to_string(c.tiles) + "."; + } + return ""; + }; + for (const Case& c : cases){ + const std::string error = check(c); + if (!error.empty()){ + return error; + } + } + // Sandgem Town on the joined maps (00.04): the front row of the + // Center and the Mart runs east to the trees past the Mart, and the + // Mart's door is a warp, not a wall. + const Pokemon::MapSet& maps = story_maps(); + const TileGrid& world = maps.world(); + auto town = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; + const TilePos below_mart = town(SandgemTown::POKE_MART_DOOR.next(Direction::SOUTH)); + const TilePos row_end = town(SandgemTown::FRONT_ROW_EAST_END); + const Case sandgem[] = { + {world, maps.to_world(MAP_ROUTE_202, {22, 24}), Direction::SOUTH, maps.to_world(MAP_ROUTE_202, {22, 31}), 7, true}, // under the ledge, down to Sandgem's trees (a timed count said 5) + {world, town(SandgemTown::POKEMON_CENTER_DOOR.next(Direction::SOUTH)), Direction::EAST, row_end, 12, true}, + {world, below_mart, Direction::EAST, row_end, 2, true}, + {world, below_mart, Direction::NORTH, below_mart, 0, false}, + {world, row_end, Direction::NORTH, row_end, 0, true}, + }; + for (const Case& c : sandgem){ + const std::string error = check(c); + if (!error.empty()){ + return error; + } + } + return true; + } +}; + +// Plans from `from` to `goal` allowing open legs a tile short or long, and +// logs the legs with every map tile each may end on. +std::string check_robust_plan( + Logger& logger, const std::string& name, const Pokemon::MapSet& maps, + const std::vector& from, const std::function& goal, + const Pokemon::RobustPlanCosts& costs = Pokemon::RobustPlanCosts() +){ + const auto legs = Pokemon::plan_robust_route(maps.world(), from, goal, costs); + if (!legs){ + return name + ": no robust route found."; + } + std::string text = name + " (robust): from"; + for (const TilePos& pos : from){ + text += " " + maps.describe(pos); + } + for (const Pokemon::PlannedLeg& leg : *legs){ + text += "\n " + Pokemon::to_string(leg.leg) + " to"; + for (const TilePos& pos : leg.after){ + text += " " + maps.describe(pos); + } + } + logger.log(text); + const std::vector& end = legs->empty() ? from : legs->back().after; + for (const TilePos& pos : end){ + if (!goal(pos)){ + return name + ": the robust plan may end on " + maps.describe(pos) + "."; + } + } + return ""; +} + +// Checks that no leg of a robust plan from `from` to `goal` can stop in tall +// grass, where a walk cannot read that it has stopped (the sway keeps the +// screen changing): a leg that only runs out of time there would be taken +// as an anchor. +std::string check_plan_stops_readable( + const std::string& name, const Pokemon::MapSet& maps, + const std::vector& from, const std::function& goal, + const Pokemon::RobustPlanCosts& costs = Pokemon::RobustPlanCosts() +){ + const auto legs = Pokemon::plan_robust_route(maps.world(), from, goal, costs); + if (!legs){ + return name + ": no robust route found."; + } + for (const Pokemon::PlannedLeg& leg : *legs){ + if (leg.leg.ends_at_wall && !route_wall_readable(maps.world(), leg.after)){ + return name + ": " + Pokemon::to_string(leg.leg) + " ends on a wall in tall grass, where a stop cannot be read."; + } + } + return ""; +} + +// Checks that no leg of a robust plan from `from` to `goal` can walk into a +// door, from any tile the leg before may have left us on, even if an open +// leg runs on to twice its length plus two: r123a's legs in Sandgem ran +// past where the grid said and the last one entered a house. +std::string check_plan_avoids_doors( + const std::string& name, const Pokemon::MapSet& maps, + const std::vector& from, const std::function& goal, + const Pokemon::RobustPlanCosts& costs = Pokemon::RobustPlanCosts() +){ + const TileGrid& grid = maps.world(); + const auto legs = Pokemon::plan_robust_route(grid, from, goal, costs); + if (!legs){ + return name + ": no robust route found."; + } + std::vector starts = from; + for (const Pokemon::PlannedLeg& planned : *legs){ + const TileLeg& leg = planned.leg; + const size_t reach = leg.ends_at_wall ? (size_t)grid.width() + grid.height() : leg.tiles * 2 + 2; + for (TilePos pos : starts){ + for (size_t i = 0; i < reach; i++){ + const TilePos next = pos.next(leg.direction); + if (grid.at(next) == TileType::DOOR){ + return name + ": " + Pokemon::to_string(leg) + " may walk into the door at " + maps.describe(next) + "."; + } + const std::optional step = grid.step(pos, leg.direction); + if (!step){ + break; + } + pos = *step; + } + } + starts = planned.after; + } + return ""; +} + +// Checks a plan from `from` to `to`: every leg can be walked, ledges are +// crossed only southward, and each leg's wall flag matches the grid. +// With `maps`, legs are printed with the map tile they end on, and with +// the trainers whose sight they cross. +std::string check_plan( + Logger& logger, const std::string& name, + const TileGrid& grid, const TilePos& from, const TilePos& to, + const Pokemon::MapSet* maps = nullptr, + const std::vector& trainers = {} +){ + const std::optional> legs = Pokemon::plan_tile_route(grid, from, to); + if (!legs){ + return name + ": no route found."; + } + auto describe = [&](const TilePos& pos){ + return maps ? maps->describe(pos) : Pokemon::to_string(pos); + }; + const std::vector> seen = Pokemon::legs_crossing_sight(grid, from, *legs, trainers); + std::string text = name + ": from " + describe(from); + TilePos end = from; + for (size_t i = 0; i < legs->size(); i++){ + const TileLeg& leg = (*legs)[i]; + end = Pokemon::trace_legs(grid, end, {leg}).back(); + text += "\n " + Pokemon::to_string(leg) + " to " + describe(end); + for (const std::string& trainer : seen[i]){ + text += ", crosses " + trainer + "'s sight"; + } + } + logger.log(text); + + std::vector tiles; + try{ + tiles = Pokemon::trace_legs(grid, from, *legs); + }catch (InternalProgramError& e){ + return name + ": " + e.message(); + } + if (tiles.back() != to){ + return name + ": the legs end at " + Pokemon::to_string(tiles.back()) + "."; + } + for (size_t i = 1; i < tiles.size(); i++){ + const TilePos& a = tiles[i - 1]; + const TilePos& b = tiles[i]; + if (!grid.standable(b)){ + return name + ": the route stands on " + Pokemon::to_string(b) + "."; + } + const int dx = b.x - a.x; + const int dy = b.y - a.y; + if (std::abs(dx) + std::abs(dy) == 1){ + continue; + } + if (dx != 0 || dy != 2 || grid.at(a.next(Direction::SOUTH)) != TileType::LEDGE){ + return name + ": jump from " + Pokemon::to_string(a) + " to " + Pokemon::to_string(b) + " is not a south ledge hop."; + } + } + TilePos pos = from; + for (const TileLeg& leg : *legs){ + if (leg.tiles == 0){ + return name + ": empty leg."; + } + pos = Pokemon::trace_legs(grid, pos, {leg}).back(); + if (leg.ends_at_wall != grid.at_wall(pos, leg.direction)){ + return name + ": leg " + Pokemon::to_string(leg) + " has the wrong wall flag."; + } + } + for (size_t i = 1; i < legs->size(); i++){ + if ((*legs)[i].direction == (*legs)[i - 1].direction){ + return name + ": two legs in a row go " + Pokemon::direction_name((*legs)[i].direction) + "."; + } + } + return ""; +} + +class Test_TileGridPlans : public UnitTest{ +public: + Test_TileGridPlans() + : UnitTest("PokemonBDSP::AutoStory - tile grid plans") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const TileGrid& small = small_grid(); + + // South over the ledge is a straight hop; back north must use the gap. + std::string error = check_plan(logger, "Small south", small, {2, 0}, {2, 4}); + if (!error.empty()){ + return error; + } + error = check_plan(logger, "Small north", small, {2, 4}, {2, 0}); + if (!error.empty()){ + return error; + } + const auto south = Pokemon::plan_tile_route(small, {2, 0}, {2, 4}); + if (south->size() != 1 || south->front().direction != Direction::SOUTH){ + return "Small south should be one leg over the ledge."; + } + + // Walled in by ledges from below. + const TileGrid pit({ + R"(#.#)", + R"(#v#)", + R"(#.#)", + }); + if (Pokemon::plan_tile_route(pit, {1, 2}, {1, 0})){ + return "A route north over a ledge was found."; + } + + // 00.04 after the catch: every leg it walks, in order. + const Pokemon::MapSet& maps = story_maps(); + const std::vector trainers = story_trainer_sights(); + const TilePos catch_spot = maps.to_world(MAP_ROUTE_202, Route202::CATCH_SPOT); + const TilePos jubilife = maps.to_world(MAP_ROUTE_202, Route202::EXIT_JUBILIFE); + error = check_plan(logger, "Catch spot to Jubilife", maps.world(), catch_spot, jubilife, &maps, trainers); + if (!error.empty()){ + return error; + } + // 00.04's walks in order, from where the step before may leave us. + const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); + const std::vector grass = story_after_training_area(); + const TilePos below_sign = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH)); + const TilePos below_mart = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR.next(Direction::SOUTH)); + const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END); + // From the trees east of the Mart, 00.04 walks six tiles west, three + // off at most, and north (north_through_sign_gap()): from every + // column it may reach, north passes no door, and only the gap + // between the signs leads out of town. + std::vector above_sign_gap; + for (int x = row_end.x - 11; x <= row_end.x - 3; x++){ + const TilePos pos{x, row_end.y}; + const Pokemon::WallHit hit = Pokemon::walk_to_wall(maps.world(), pos, Direction::NORTH); + if (maps.world().at(hit.wall) == TileType::DOOR){ + return "North from " + maps.describe(pos) + " walks into a door."; + } + const std::vector gap = SandgemTown::below_sign_gap(); + const bool in_gap = std::find(gap.begin(), gap.end(), maps.to_local(pos)->pos) != gap.end(); + if (in_gap != (hit.tiles > 2)){ + return "North from " + maps.describe(pos) + " goes " + std::to_string(hit.tiles) + " tiles."; + } + if (in_gap){ + above_sign_gap.push_back(hit.stop); + } + } + // The walks that start or end in the catch grass must stop in it; + // the Sandgem walks (`readable`) never need to. Each is planned with + // the costs the program plans it with: walk_route()'s route costs + // for 00.04's walks, the Sandgem Center events' default costs for + // walk() and town costs for walk_in_town(). The planner gives up + // after costs.max_searched sets, and the costs decide whether a + // walk gets a plan: after training, 00.04 walks to Sandgem's entry + // with the default costs (the route costs find none within the cap), + // then to the Center with the town costs, checked leg by leg below. + const struct{ + const char* name; + std::vector from; + std::function goal; + bool readable; + Pokemon::RobustPlanCosts costs; + } walks[] = { + {"Save spots to the catch grass", story_save_spots(), + [&](const TilePos& pos){ return std::find(grass.begin(), grass.end(), pos) != grass.end(); }, false, route_plan_costs()}, + {"After training to Sandgem's entry", story_after_training_area(), + [&](const TilePos& pos){ return pos == sandgem_entry_stop(); }, false, Pokemon::RobustPlanCosts()}, + // The Mart's door takes a tile either side: enter_lit_door() lines up by sight. + {"Pokemon Center to the Poke Mart", story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), + [&](const TilePos& pos){ return pos.y == below_mart.y && std::abs(pos.x - below_mart.x) <= 1; }, true, route_plan_costs()}, + {"Poke Mart to the trees east of it", story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR), + [&](const TilePos& pos){ return pos == row_end; }, true, route_plan_costs()}, + {"North of the gap between the signs to the save spot", above_sign_gap, + [&](const TilePos& pos){ return pos == save_spot; }, true, route_plan_costs()}, + // Step 5 starts on the save spot (see tutorial_part_3_after_catch_navigation()). + // From both save spots at once the route costs find no plan within the + // cap: exact open legs move the pair in step without joining it. + {"Save spot to Jubilife", {save_spot}, story_at_jubilife_exit, false, route_plan_costs()}, + // The Sandgem Center test checkpoint: south into town, then to the Center's sign. + {"Save spot to Sandgem Town", {save_spot}, + [&](const TilePos& pos){ return pos == sandgem_entry_stop(); }, true, Pokemon::RobustPlanCosts()}, + {"Sandgem's entry to the Pokemon Center", {sandgem_entry_stop()}, + [&](const TilePos& pos){ return pos == below_sign; }, true, town_plan_costs()}, + // The Mart lady test checkpoint: out of the Center to the tile west + // of Route 201's border sign, from the lady back to it, and back. The + // sign's row has no wall west of it: a step onto it, then east to it. + // These legs are walked by hand (walk_until_stopped), so the plan is + // only a check of the grid; the one-tile steps need the town costs, + // the default costs find no plan. + {"Pokemon Center to west of the Route 201 sign", story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), + [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, + {"Poke Mart lady to west of the Route 201 sign", {maps.to_world(MAP_ROUTE_201, Route201::POTION_GIVER_TALK_FROM)}, + [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, + {"Route 201 sign to the Pokemon Center", {route201_sign_west()}, + [&](const TilePos& pos){ return pos == below_sign; }, true, Pokemon::RobustPlanCosts()}, + // The Center walk's re-anchor at the trees east of the Mart. + {"Trees east of the Mart to the Pokemon Center", {maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END)}, + [&](const TilePos& pos){ return pos == below_sign; }, true, Pokemon::RobustPlanCosts()}, + // From and back to where the Route 202 checkpoint ends: the doors' + // row by the signs' gap; westward it first walks to the Lab's sign. + {"East of the Lab's sign to west of the Route 201 sign", {east_of_lab_sign()}, + [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, + {"Below the Center's sign to the trees east of the Mart", {below_sandgem_center_sign()}, + [&](const TilePos& pos){ return pos == row_end; }, true, Pokemon::RobustPlanCosts()}, + {"East of the Lab's sign to the trees east of the Mart", {east_of_lab_sign()}, + [&](const TilePos& pos){ return pos == row_end; }, true, Pokemon::RobustPlanCosts()}, + // Its re-anchor after a wild battle in the lady's grass. + {"East end of the lady's row to the Route 201 sign", {route201_row_3_east_end()}, + [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, + }; + for (const auto& walk : walks){ + error = check_robust_plan(logger, walk.name, maps, walk.from, walk.goal, walk.costs); + if (!error.empty()){ + return error; + } + error = check_plan_avoids_doors(walk.name, maps, walk.from, walk.goal, walk.costs); + if (!error.empty()){ + return error; + } + if (walk.readable){ + error = check_plan_stops_readable(walk.name, maps, walk.from, walk.goal, walk.costs); + if (!error.empty()){ + return error; + } + } + } + + // In town, short open legs are cheap: from where the walk south stops, + // the plan is the user's (2026-10-30): west to the wall, a tile east + // into the gap's column, straight south through the gap between the + // signs, a tile west, then north until the Center's sign stops us + // (r134a: "south 5" went 6; seven rows on the game's grid). Not round the town. + { + const auto legs = Pokemon::plan_robust_route(maps.world(), {sandgem_entry_stop()}, + [&](const TilePos& pos){ return pos == below_sign; }, town_plan_costs()); + std::string read; + for (const Pokemon::PlannedLeg& planned : legs ? *legs : std::vector()){ + read += " [" + Pokemon::to_string(planned.leg) + "]"; + } + const std::vector> expected{ + {Direction::WEST, 9}, {Direction::EAST, 1}, {Direction::SOUTH, 7}, {Direction::WEST, 1}, {Direction::NORTH, 2}, + }; + bool same = legs && legs->size() == expected.size(); + for (size_t i = 0; same && i < expected.size(); i++){ + same = (*legs)[i].leg.direction == expected[i].first && (*legs)[i].leg.tiles == expected[i].second; + } + if (!same){ + return "Sandgem's entry to below the Center's sign, town costs: expected west 9, east 1, south 7, west 1, north 2; planned" + + (read.empty() ? std::string(" nothing") : read) + "."; + } + logger.log("Sandgem's entry to below the Center's sign, town costs:" + read); + } + + // The walk south from the save spot keeps out of the tall grass, so + // no wild battle can stop it. + { + const auto legs = Pokemon::plan_robust_route(maps.world(), {save_spot}, sandgem_entry_stop()); + if (!legs){ + return "Save spot to Sandgem Town: no robust route found."; + } + std::vector starts{save_spot}; + for (const Pokemon::PlannedLeg& planned : *legs){ + for (TilePos pos : starts){ + for (size_t i = 0; i < planned.leg.tiles; i++){ + const std::optional step = maps.world().step(pos, planned.leg.direction); + if (!step){ + break; + } + pos = *step; + if (maps.world().at(pos) == TileType::TALL_GRASS){ + return "Save spot to Sandgem Town: " + Pokemon::to_string(planned.leg) + + " crosses tall grass at " + maps.describe(pos) + "."; + } + } + } + starts = planned.after; + } + } + + // 00.05 in Jubilife: every leg the segment walks, from where the one + // before it may have left us, with route_plan_costs() as talk_to_npc() plans. + { + using namespace JubilifeCity; + const Pokemon::MapSet& town = jubilife_maps(); + auto at = [&](const TilePos& pos){ return town.to_world(MAP_JUBILIFE_CITY, pos); }; + std::vector dawn; + for (const TilePos& pos : up_the_steps()){ + dawn.push_back(at(pos)); + } + const TilePos school = at(TRAINERS_SCHOOL_DOOR.next(Direction::SOUTH)); + const TilePos center = at(POKEMON_CENTER_DOOR.next(Direction::SOUTH)); + const std::vector out_of_school{school.next(Direction::WEST), school, school.next(Direction::EAST)}; + const TilePos president = at(POKETCH_PRESIDENT).next(Direction::EAST); + const TilePos clown_mart = at(CLOWN_MART).next(Direction::SOUTH); + const TilePos clown_tv = at(CLOWN_TV_STATION).next(Direction::EAST); + const TilePos clown_company = at(CLOWN_POKETCH_COMPANY).next(Direction::EAST); + // A door's goal is its front row within a tile of its column; an + // NPC's is the one tile beside them. 00.05 walks them with + // walk_route()'s route costs (talk_to_npc()), which the test + // plans with too: on the whole city the town costs find no plan + // for four of them within the planner's cap. The walk from the + // steps is made by hand, so its plan is only a check of the grid. + const struct{ const char* name; std::vector from; TilePos to; bool door; Pokemon::RobustPlanCosts costs; } legs[] = { + {"Jubilife: after Dawn to the Trainers' School", dawn, school, true, town_plan_costs()}, + {"Jubilife: school to the president", out_of_school, president, false, route_plan_costs()}, + {"Jubilife: president to the clown by the Mart", {president}, clown_mart, false, route_plan_costs()}, + {"Jubilife: Mart clown to the TV station clown", {clown_mart}, clown_tv, false, route_plan_costs()}, + {"Jubilife: TV station clown to the Poketch Company clown", {clown_tv}, clown_company, false, route_plan_costs()}, + {"Jubilife: Poketch Company clown to the president", {clown_company}, president, false, route_plan_costs()}, + {"Jubilife: president to the Pokemon Center", {president}, center, true, route_plan_costs()}, + }; + std::string failed; + // 01.03 in the Oreburgh Mine: the corridor's top to below Roark, and + // back, both walk_route() with its route costs. The way in finds no + // plan with them within the planner's cap and walk_route() falls back + // to the default costs, so that is the plan that runs; the way back + // plans with the route costs, and only those stay within the cap. + { + const Pokemon::MapSet& mine = oreburgh_mine_maps(); + auto m = [&](const TilePos& pos){ return mine.to_world(MAP_OREBURGH_MINE, pos); }; + std::vector top; + for (const TilePos& pos : OreburghMine::corridor_top()){ + top.push_back(m(pos)); + } + const TilePos below_roark = m(OreburghMine::ROARK).next(Direction::SOUTH); + const TilePos roark = m(OreburghMine::ROARK); + std::string leg_error = check_robust_plan(logger, "Mine: corridor top to the stretch east of Roark", mine, top, + [&](const TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }); + failed += leg_error.empty() ? "" : leg_error + " "; + const TilePos exit = m(OreburghMine::CORRIDOR_EXIT_START); + leg_error = check_robust_plan(logger, "Mine: below Roark to the corridor's corner", mine, {below_roark}, + [&](const TilePos& pos){ return pos == exit; }, route_plan_costs()); + failed += leg_error.empty() ? "" : leg_error + " "; + } + // 01.05 in the Ravaged Path: from the south door to a rock across the + // corridor, and once it is smashed, to the tile above the way out. + { + const Pokemon::MapSet cave = ravaged_path_maps(); + for (const TilePos& rock : RavagedPath::rocks()){ + if (cave.world().at(rock) != Pokemon::TileType::BLOCKED){ + failed += "Ravaged Path: rock " + cave.describe(rock) + " is not blocked. "; + } + } + const std::vector& before = RavagedPath::before_rocks(); + std::string leg_error = check_robust_plan(logger, "Ravaged Path: south door to a rock", cave, {RavagedPath::ENTRANCE}, + [&](const TilePos& pos){ return std::find(before.begin(), before.end(), pos) != before.end(); }); + failed += leg_error.empty() ? "" : leg_error + " "; + for (const TilePos& from : before){ + const TilePos rock = from.next(Direction::EAST); + const Pokemon::MapSet open = ravaged_path_maps({rock}); + leg_error = check_robust_plan(logger, "Ravaged Path: after smashing " + open.describe(rock) + ", to the corridor's east end", open, {from}, + [&](const TilePos& pos){ return pos == RavagedPath::EAST_END; }); + failed += leg_error.empty() ? "" : leg_error + " "; + } + // What 01.05 plans: from either tile, with both rocks open. + std::vector both; + for (const TilePos& from : before){ + both.push_back(from.next(Direction::EAST)); + } + leg_error = check_robust_plan(logger, "Ravaged Path: from either rock's tile to the corridor's east end", ravaged_path_maps(both), before, + [&](const TilePos& pos){ return pos == RavagedPath::EAST_END; }); + failed += leg_error.empty() ? "" : leg_error + " "; + if (!(RavagedPath::EAST_END.next(Direction::WEST) == RavagedPath::ABOVE_EXIT) || + cave.world().at(RavagedPath::ABOVE_EXIT.next(Direction::SOUTH)) != Pokemon::TileType::DOOR + ){ + failed += "Ravaged Path: the way out is not a door a tile west of the corridor's east end. "; + } + } + // 01.06 on Route 203, then Jubilife's north-east corner: from the + // Oreburgh Gate's west exit to its road near Jubilife, and from + // Jubilife's east stairs to the foot of its north road. + { + std::string leg_error = check_robust_plan(logger, "Route 203: Oreburgh Gate to row 18", route203_maps(), + Route203::gate_exit(), on_route203_west_line); + failed += leg_error.empty() ? "" : leg_error + " "; + leg_error = check_robust_plan(logger, "Jubilife: east stairs to the north road", jubilife_east_maps(), + jubilife_east_entrance(), on_jubilife_north_road, town_plan_costs()); + failed += leg_error.empty() ? "" : leg_error + " "; + } + // 01.08 on Route 204's south half: from the top of Jubilife's + // street to the tile below the Ravaged Path's door. + { + // Planned as the walker plans it: the sight lines it keeps + // out of are off the map. + Pokemon::TileGrid grid = Route204South::grid(); + for (const TilePos& pos : Route204South::tiles_to_avoid()){ + grid.set(pos, Pokemon::TileType::UNKNOWN); + } + Pokemon::MapSet route; + route.add(MAP_ROUTE_204_SOUTH, grid, {0, 0}); + const std::string leg_error = check_robust_plan(logger, "Route 204 South: Jubilife to the Ravaged Path door", route, + Route204South::jubilife_entry(), [](const TilePos& pos){ return pos == Route204South::BELOW_DOOR; }, town_plan_costs()); + failed += leg_error.empty() ? "" : leg_error + " "; + for (const Pokemon::TrainerSight& trainer : Route204South::trainer_sights()){ + if (route.world().at(trainer.pos) != Pokemon::TileType::NPC){ + failed += "Route 204 South: " + trainer.name + " is not on an NPC tile. "; + } + } + } + for (const auto& leg : legs){ + const TilePos to = leg.to; + const bool door = leg.door; + const std::string leg_error = check_robust_plan(logger, leg.name, town, leg.from, + [&](const TilePos& pos){ + return door ? pos.y == to.y && pos.x >= to.x - 1 && pos.x <= to.x + 1 : pos == to; + }, leg.costs); + failed += leg_error.empty() ? "" : leg_error + " "; + } + if (!failed.empty()){ + return failed; + } + } + return true; + } +}; + +class Test_IntroDecisions : public UnitTest{ +public: + Test_IntroDecisions() + : UnitTest("PokemonBDSP::AutoStory - new-game intro decisions") + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + struct Case{ + const char* screen; + IntroProgress progress; + IntroStep expected; + }; + const IntroProgress start{false, false}; + const IntroProgress named{true, false}; + const IntroProgress done{true, true}; + const Case cases[] = { + {"user-select", start, IntroStep::SELECT_USER}, + {"home", done, IntroStep::FAIL_NOT_IN_GAME}, + {"keyboard", start, IntroStep::ENTER_PLAYER_NAME}, + {"keyboard", named, IntroStep::CANCEL_RIVAL_KEYBOARD}, + {"rival-list-tell", named, IntroStep::MOVE_TO_PRESET}, + {"rival-list-preset", named, IntroStep::PICK_PRESET}, + {"menu", done, IntroStep::CLOSE_MENU_AND_FINISH}, + // The appearance grid has its prompt in a dialogue box. + {"dialog", start, IntroStep::ADVANCE}, + {"dialog", done, IntroStep::ADVANCE}, + // Language and the appearance preview: no X before the bedroom is possible. + {"other", start, IntroStep::ADVANCE}, + {"other", named, IntroStep::ADVANCE}, + {"other", done, IntroStep::PROBE_FOR_MENU}, + }; + for (const Case& c : cases){ + const IntroStep actual = decide_intro_step(c.screen, c.progress); + if (actual != c.expected){ + return std::string("Screen \"") + c.screen + "\" (player named " + + (c.progress.player_named ? "yes" : "no") + ", rival named " + + (c.progress.rival_named ? "yes" : "no") + "): step " + + std::to_string((int)actual) + ", expected " + std::to_string((int)c.expected) + "."; + } + } + if (advance_after_probe(true)){ + return "A after X changed the screen would pick from whatever X opened."; + } + if (!advance_after_probe(false)){ + return "A must advance a screen X left unchanged."; + } + return true; + } +}; + +} + + +class Test_LocationBanner : public UnitTest{ +public: + Test_LocationBanner() + : UnitTest("PokemonBDSP::AutoStory - location banner match") + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + struct Case{ + const char* read; + const char* expected; + bool match; + }; + const Case cases[] = { + {"sandgemtown", "Sandgem Town", true}, + {"andgemltown", "Sandgem Town", true}, // r109's OCR + {"Route 202", "Route 202", true}, + {"route2o2", "Route 202", false}, // a digit read as a letter is not trusted + {"route201", "Route 202", false}, + {"jubilifecity", "Sandgem Town", false}, + {"twinleaftown", "Sandgem Town", false}, + {"", "Sandgem Town", false}, + }; + for (const Case& c : cases){ + if (location_banner_matches(c.read, c.expected) != c.match){ + return std::string("Banner \"") + c.read + "\" for " + c.expected + + (c.match ? " should match." : " should not match."); + } + } + return true; + } +}; + + +// The walk to the Poke Mart may be stopped short by someone in the way, and +// is then made again from the tiles the doorway on screen puts us on, give +// 01.10 on Route 204's north half: the line a human walked (2026-10-31), +// corner to corner, from the Ravaged Path's exit into Floaroma. Each +// stretch must plan as the one straight tap leg the human took, on the +// grid as it is: a detour here means the grid or the line changed. +class Test_Route204NorthLine : public UnitTest{ +public: + Test_Route204NorthLine() + : UnitTest("PokemonBDSP::AutoStory - Route 204 North: the line into Floaroma") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const Pokemon::MapSet& route = route204_north_maps(); + const std::vector waypoints = Route204::floaroma_waypoints(); + struct Expected{ Direction direction; size_t tiles; }; + const std::vector expected{ + {Direction::SOUTH, 1}, {Direction::WEST, 9}, {Direction::NORTH, 5}, {Direction::EAST, 8}, + {Direction::NORTH, 10}, {Direction::WEST, 8}, {Direction::NORTH, 7}, + }; + if (waypoints.size() != expected.size()){ + return "Route 204 North: " + std::to_string(waypoints.size()) + " waypoints, expected " + std::to_string(expected.size()) + "."; + } + std::vector at{Route204::CAVE_EXIT}; + for (size_t i = 0; i < waypoints.size(); i++){ + const TilePos goal = waypoints[i]; + const std::string name = "Route 204 North stretch " + std::to_string(i + 1) + " to " + route.describe(goal); + const auto plan = Pokemon::plan_robust_route(route.world(), at, + [&](const TilePos& pos){ return pos == goal; }, waypoint_plan_costs()); + if (!plan){ + return name + ": no plan."; + } + std::string text = name + ":"; + for (const Pokemon::PlannedLeg& leg : *plan){ + text += " " + to_string(leg.leg); + } + logger.log(text); + if (plan->size() != 1){ + return name + ": " + std::to_string(plan->size()) + " legs, expected the one straight leg."; + } + const Pokemon::TileLeg& leg = (*plan)[0].leg; + if (leg.ends_at_wall || leg.direction != expected[i].direction || leg.tiles != expected[i].tiles){ + return name + ": planned " + to_string(leg) + ", expected an open leg of " + + std::to_string(expected[i].tiles) + " tiles."; + } + if ((*plan)[0].after.size() != 1 || !((*plan)[0].after[0] == goal)){ + return name + ": the leg does not end exactly on the waypoint."; + } + at = {goal}; + } + if (!Route204::in_floaroma_path(at[0])){ + return "Route 204 North: the last waypoint is not in the path into Floaroma."; + } + for (const Pokemon::TrainerSight& trainer : Route204::north_trainer_sights()){ + if (route.world().at(trainer.pos) != Pokemon::TileType::NPC){ + return "Route 204 North: " + trainer.name + " is not on an NPC tile."; + } + } + return true; + } +}; + +// Every walk a checkpoint makes without a plan is written down as legs +// (Data/PokemonBDSP_AutoStory_RouteRecords). A record with a start tile on +// a grid whose rows are the game's is traced leg by leg: counted legs stay +// on standable tiles, a leg to a wall finds one, a leg to a person or a +// door ends in front of one. The others are checked for shape and logged +// as documented, not traced, with why. +class Test_RouteRecords : public UnitTest{ +public: + Test_RouteRecords() + : UnitTest("PokemonBDSP::AutoStory - route records") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + size_t traced = 0, documented = 0; + for (const RouteRecords::RouteRecord& record : RouteRecords::all()){ + const std::string name = record.checkpoint + " " + record.title; + if (record.legs.empty() || record.source.empty() || record.title.empty()){ + return name + ": a record needs legs, a title and a source."; + } + for (const RouteRecords::RouteLeg& leg : record.legs){ + if (leg.end == RouteRecords::LegEnd::COUNT && leg.tiles == 0){ + return name + ": a counted leg of 0 tiles."; + } + if (!leg.exact && leg.end != RouteRecords::LegEnd::COUNT){ + return name + ": only a counted leg can be an estimate."; + } + } + const TileGrid* grid = record.grid ? record.grid() : nullptr; + if (!record.traceable || grid == nullptr || !record.start){ + logger.log(name + ": documented, not traced (" + + (grid == nullptr ? "no grid" : !record.traceable ? "grid rows not the game's" : "no start tile") + ")."); + documented++; + continue; + } + TilePos pos = *record.start; + if (!grid->standable(pos)){ + return name + ": the start tile is not standable."; + } + for (size_t i = 0; i < record.legs.size(); i++){ + const RouteRecords::RouteLeg& leg = record.legs[i]; + const std::string leg_name = name + " leg " + std::to_string(i + 1); + switch (leg.end){ + case RouteRecords::LegEnd::COUNT: + for (size_t k = 0; k < leg.tiles; k++){ + pos = pos.next(leg.direction); + if (!grid->standable(pos)){ + return leg_name + ": tile " + std::to_string(k + 1) + " is not standable."; + } + } + break; + case RouteRecords::LegEnd::WALL:{ + const Pokemon::WallHit hit = Pokemon::walk_to_wall(*grid, pos, leg.direction); + if (hit.tiles == 0){ + return leg_name + ": already at the wall."; + } + if (leg.tiles != 0 && hit.tiles != leg.tiles){ + return leg_name + ": the wall is " + std::to_string(hit.tiles) + " tiles away, the record says " + std::to_string(leg.tiles) + "."; + } + pos = hit.stop; + break; + } + case RouteRecords::LegEnd::NPC: + case RouteRecords::LegEnd::DOOR:{ + const Pokemon::WallHit hit = Pokemon::walk_to_wall(*grid, pos, leg.direction); + pos = hit.stop; + const Pokemon::TileType next = grid->at(pos.next(leg.direction)); + const Pokemon::TileType wanted = leg.end == RouteRecords::LegEnd::NPC ? Pokemon::TileType::NPC : Pokemon::TileType::DOOR; + if (next != wanted){ + return leg_name + ": does not end in front of " + (wanted == Pokemon::TileType::NPC ? "a person." : "a door."); + } + break; + } + case RouteRecords::LegEnd::BANNER: + case RouteRecords::LegEnd::TRANSITION: + case RouteRecords::LegEnd::SCENE: + // Leaves the map or the game takes over: nothing to trace. + break; + } + } + size_t estimates = 0; + for (const RouteRecords::RouteLeg& leg : record.legs){ + estimates += leg.exact ? 0 : 1; + } + logger.log(name + ": traced " + std::to_string(record.legs.size()) + " legs" + + (estimates ? ", " + std::to_string(estimates) + " of them stick-timed estimates" : "") + "."); + traced++; + } + logger.log("Route records: " + std::to_string(traced) + " traced, " + std::to_string(documented) + " documented only."); + return true; + } +}; + +// or take one each way: from every such block of tiles south of the Mart, +// a robust plan to the row below its door must exist. +class Test_PokeMartShortWalks : public UnitTest{ +public: + Test_PokeMartShortWalks() + : UnitTest("PokemonBDSP::AutoStory - walks short of the Poke Mart") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const Pokemon::MapSet& maps = story_maps(); + const TileGrid& grid = maps.world(); + const TilePos front = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR.next(Direction::SOUTH)); + const auto at_front = [&](const TilePos& pos){ return pos.y == front.y && std::abs(pos.x - front.x) <= 1; }; + // The open ground south of the Mart, to the trees east of town and + // the fence rows below. + for (int y = front.y + 1; y <= front.y + 7; y++){ + for (int x = front.x - 6; x <= front.x + 2; x++){ + std::vector block; + for (int dy = -1; dy <= 1; dy++){ + for (int dx = -1; dx <= 1; dx++){ + if (grid.standable({x + dx, y + dy})){ + block.push_back({x + dx, y + dy}); + } + } + } + if (block.empty()){ + continue; + } + const std::string error = check_robust_plan(logger, "Short of the Poke Mart about " + maps.describe({x, y}), + maps, block, at_front); + if (!error.empty()){ + return error; + } + } + } + return true; + } +}; + + +// The Mart's doorway on the failure screenshots of r121a and r122a, kept in +// the unit test resources (the CommandLineTests checkout): where it is +// relative to the player, who is in the middle of the screen. +class Test_LitDoorwayScreenshots : public UnitTest{ +public: + Test_LitDoorwayScreenshots() + : UnitTest("PokemonBDSP::AutoStory - lit doorway screenshots") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ + const char* file; + double tiles_across_min; + double tiles_across_max; + double rows_up_min; + double rows_up_max; + }; + const Case cases[] = { + // r121a: below-left of the door, which is part of a tile east, straight ahead. + {"PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", 0.2, 0.7, 0.5, 1.5}, + // r122a: a tile west of the door and three or four rows south of its front. + {"PokemonBDSP/AutoStory/SandgemMartDoorFarLeft-20261030-024648.png", -1.3, -0.5, 2.5, 4.5}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const ImageRGB32 screen(path); + const std::optional doorway = find_lit_doorway(screen); + if (!doorway){ + return std::string(c.file) + ": no lit doorway found."; + } + logger.log(std::string(c.file) + ": doorway at " + std::to_string(doorway->x) + ", " + + std::to_string(doorway->y) + ": " + std::to_string(doorway->tiles_across) + " tiles across, " + + std::to_string(doorway->rows_up) + " rows up."); + if (doorway->tiles_across < c.tiles_across_min || doorway->tiles_across > c.tiles_across_max){ + return std::string(c.file) + ": doorway " + std::to_string(doorway->tiles_across) + + " tiles across, expected " + std::to_string(c.tiles_across_min) + " to " + + std::to_string(c.tiles_across_max) + "."; + } + if (doorway->rows_up < c.rows_up_min || doorway->rows_up > c.rows_up_max){ + return std::string(c.file) + ": doorway " + std::to_string(doorway->rows_up) + + " rows up, expected " + std::to_string(c.rows_up_min) + " to " + + std::to_string(c.rows_up_max) + "."; + } + } + return true; + } +}; + + +// The save prompt r124a took beside Sandgem's Poke Mart, sure it stood on +// Route 202 (in CommandLineTests under PokemonBDSP/AutoStory): its card +// names Sandgem Town, which the save must read before confirming. +class Test_SaveScreenLocation : public UnitTest{ +public: + Test_SaveScreenLocation() + : UnitTest("PokemonBDSP::AutoStory - save screen location") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const std::string path = UNIT_TEST_RESOURCE_PATH() + "PokemonBDSP/AutoStory/SaveScreenSandgemTown-20261030-034444.png"; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const std::string read = read_save_screen_location(ImageRGB32(path)); + logger.log("Save screen location read: \"" + read + "\"."); + if (!location_banner_matches(read, MAP_SANDGEM_TOWN)){ + return "Save screen location \"" + read + "\" should match Sandgem Town."; + } + if (location_banner_matches(read, MAP_ROUTE_202)){ + return "Save screen location \"" + read + "\" should not match Route 202."; + } + return true; + } +}; + + +// The Route 202 to Sandgem Center checkpoint's events against a scripted +// game: where the player is, and what goes wrong on the way. +class FakeSandgemCenter : public SandgemCenterActions{ +public: + enum class Where{ ROUTE_202, SANDGEM_ENTRY, SHORT_OF_DOOR, BELOW_SIGN, DOOR_FRONT, LOST, MART_ROW }; + + std::string save_location = "Route 202"; + // Walks to the Center that end three rows short (someone in the way). + size_t short_walks = 0; + // Walks that a townsperson's talk stops, with the dialogue left open. + size_t talking_walks = 0; + // Walks to the Center that end lost in town (a wall leg short, r127a). + size_t lost_walks = 0; + Where where = Where::ROUTE_202; + bool dialogue = false; + std::vector trace; + + virtual std::string save_screen_location() override{ + trace.push_back("read save screen"); + return save_location; + } + virtual Pokemon::Outcome walk( + std::vector& at, const std::function& goal, const std::string& label + ) override{ + trace.push_back("walk: " + label); + if (talking_walks > 0){ + talking_walks--; + dialogue = true; + return Pokemon::outcome_failed("walk_failed", "a dialogue no trainer explains"); + } + if (goal(sandgem_entry_stop())){ + at = {sandgem_entry_stop()}; + where = Where::SANDGEM_ENTRY; + return Pokemon::outcome_ok(); + } + if (goal(below_sandgem_center_sign())){ + if (lost_walks > 0){ + lost_walks--; + where = Where::LOST; + return Pokemon::outcome_failed("walk_failed", "ran out of time short of the wall"); + } + at = {below_sandgem_center_sign()}; + where = Where::BELOW_SIGN; + if (short_walks > 0){ + short_walks--; + where = Where::SHORT_OF_DOOR; + } + return Pokemon::outcome_ok(); + } + return Pokemon::outcome_failed("no_plan", label); + } + virtual bool east_then_south_into_sandgem() override{ + trace.push_back("east, then south"); + const bool entered = where == Where::ROUTE_202; + where = Where::SANDGEM_ENTRY; + return entered; + } + virtual Pokemon::Outcome walk_in_town( + std::vector& at, const std::function& goal, const std::string& label + ) override{ + return walk(at, goal, label); + } + virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ + trace.push_back("read: " + label); + if (where == Where::BELOW_SIGN && facing == Direction::NORTH){ + return DialogueRead{.opened = true, .closed = true, .text = "Pokemon Center"}; + } + return DialogueRead{}; + } + virtual std::optional lit_doorway() override{ + switch (where){ + case Where::BELOW_SIGN: + return LitDoorway{.x = 0.3, .y = 0.38, .tiles_across = -3.0, .rows_up = 1.0}; + case Where::SHORT_OF_DOOR: + return LitDoorway{.x = 0.3, .y = 0.14, .tiles_across = -3.0, .rows_up = 3.0}; + case Where::DOOR_FRONT: + return LitDoorway{.x = 0.5, .y = 0.38, .tiles_across = 0.1, .rows_up = 1.0}; + case Where::MART_ROW: + return LitDoorway{.x = 0.37, .y = 0.38, .tiles_across = -2.0, .rows_up = 1.0}; + default: + return std::nullopt; + } + } + virtual Pokemon::Outcome line_up_with_door() override{ + trace.push_back("line up"); + if (where != Where::BELOW_SIGN){ + return Pokemon::outcome_failed("not_lined_up", "no lit doorway on screen"); + } + where = Where::DOOR_FRONT; + return Pokemon::outcome_ok(); + } + virtual bool inside_pokemon_center() override{ + return false; + } + virtual Pokemon::Outcome east_then_north_in_sandgem() override{ + trace.push_back("east, then north"); + where = Where::MART_ROW; + return Pokemon::outcome_ok(); + } + virtual bool dialogue_showing() override{ + return dialogue; + } + virtual Pokemon::Outcome finish_dialogue() override{ + trace.push_back("finish dialogue"); + dialogue = false; + return Pokemon::outcome_ok(); + } +}; + +class Test_SandgemCenterEvents : public UnitTest{ +public: + Test_SandgemCenterEvents() + : UnitTest("PokemonBDSP::AutoStory - Sandgem Center events") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const std::string SOUTH = "walk: Route 202 to Sandgem Town"; + const std::string ROW = "walk: To below the Center's sign"; + const std::string READ = "read: the Center's sign"; + struct Case{ + const char* name; + std::string save_location; + size_t short_walks; + size_t talking_walks; + size_t lost_walks; + bool completed; + size_t failed_event; + std::vector trace; + }; + const Case cases[] = { + { + .name = "Straight through", + .save_location = "Route 202", + .short_walks = 0, + .talking_walks = 0, + .lost_walks = 0, + .completed = true, + .failed_event = 0, + .trace = {"read save screen", SOUTH, ROW, READ}, + }, + { + .name = "The save is in Sandgem Town", + .save_location = "Sandgem Town", + .short_walks = 0, + .talking_walks = 0, + .lost_walks = 0, + .completed = false, + .failed_event = 1, + .trace = {"read save screen", "read save screen"}, + }, + { + .name = "A row off: the sign does not read", + .save_location = "Route 202", + .short_walks = 1, + .talking_walks = 0, + .lost_walks = 0, + .completed = false, + .failed_event = 4, + .trace = {"read save screen", SOUTH, ROW, READ}, + }, + { + .name = "A talk stops the walk south", + .save_location = "Route 202", + .short_walks = 0, + .talking_walks = 1, + .lost_walks = 0, + .completed = true, + .failed_event = 0, + .trace = {"read save screen", SOUTH, "finish dialogue", "east, then south", ROW, READ}, + }, + { + .name = "The walk in town fails", + .save_location = "Route 202", + .short_walks = 0, + .talking_walks = 0, + .lost_walks = 1, + .completed = false, + .failed_event = 3, + .trace = {"read save screen", SOUTH, ROW}, + }, + }; + for (const Case& c : cases){ + FakeSandgemCenter game; + game.save_location = c.save_location; + game.short_walks = c.short_walks; + game.talking_walks = c.talking_walks; + game.lost_walks = c.lost_walks; + Pokemon::Checkpoint checkpoint = route202_to_sandgem_center_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(checkpoint); + if (!problems.empty()){ + return std::string(c.name) + ": lint: " + problems.front(); + } + std::string log_text; + Pokemon::EventLogger log = [&](const std::string& line){ log_text += "\n " + line; }; + Pokemon::RunState state; + const Pokemon::CheckpointReport report = Pokemon::run_checkpoint(state, checkpoint, log); + logger.log(std::string(c.name) + ":" + log_text); + if (report.completed != c.completed || report.failed_event != c.failed_event){ + return std::string(c.name) + ": expected " + (c.completed ? "completed" : "failed") + + " at event " + std::to_string(c.failed_event) + ", read " + (report.completed ? "completed" : "failed") + + " at event " + std::to_string(report.failed_event) + ":" + log_text; + } + if (game.trace != c.trace){ + std::string expected, read; + for (const std::string& step : c.trace){ + expected += " [" + step + "]"; + } + for (const std::string& step : game.trace){ + read += " [" + step + "]"; + } + return std::string(c.name) + ": expected steps" + expected + ", read" + read + "."; + } + if (c.completed && game.where != FakeSandgemCenter::Where::BELOW_SIGN){ + return std::string(c.name) + ": completed, but not on the doors' row."; + } + } + return true; + } +}; + + +// The two shots around r126's door walk: below the Center's sign, outside, +// and on the entrance mat inside, where the lit floor read as a doorway and +// the walk kept lining up with it (runs 2 and 3). +class Test_PokemonCenterInside : public UnitTest{ +public: + Test_PokemonCenterInside() + : UnitTest("PokemonBDSP::AutoStory - Pokemon Center inside") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ + const char* file; + bool inside; + bool doorway; + }; + const Case cases[] = { + {.file = "PokemonBDSP/AutoStory/SandgemCenterInside-20261030-053555.png", .inside = true, .doorway = false}, + // r127c: walked in by the turn north at the door, facing north. + {.file = "PokemonBDSP/AutoStory/SandgemCenterInsideFacingNorth-r127c.png", .inside = true, .doorway = false}, + {.file = "PokemonBDSP/AutoStory/SandgemCenterBelowSign-20261030-053527.png", .inside = false, .doorway = true}, + {.file = "PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", .inside = false, .doorway = true}, + {.file = "PokemonBDSP/AutoStory/SandgemMartDoorFarLeft-20261030-024648.png", .inside = false, .doorway = true}, + // Jubilife (2026-10-30): inside its Center twice, at its door, and + // on the doorstep after walking out (the banner up). + {.file = "PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", .inside = true, .doorway = false}, + {.file = "PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", .inside = true, .doorway = false}, + {.file = "PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", .inside = false, .doorway = true}, + {.file = "PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", .inside = false, .doorway = true}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const ImageRGB32 screen(path); + const bool inside = pokemon_center_inside(screen); + const std::optional doorway = find_lit_doorway(screen); + logger.log(std::string(c.file) + ": inside " + std::to_string(inside) + ", doorway " + + (doorway ? std::to_string(doorway->tiles_across) + " across, " + std::to_string(doorway->rows_up) + " rows up" : "none")); + if (inside != c.inside){ + return std::string(c.file) + ": expected " + (c.inside ? "inside" : "outside") + + " a Pokemon Center, read " + (inside ? "inside" : "outside") + "."; + } + if (doorway.has_value() != c.doorway){ + return std::string(c.file) + ": expected " + (c.doorway ? "a doorway" : "no doorway") + + ", read " + (doorway ? "one" : "none") + "."; + } + } + return true; + } +}; + + +// The Poke Mart lady checkpoint against a scripted game: where the player +// stands along the route measured on the console. +class FakeMartLady : public MartLadyActions{ +public: + enum class Where{ + DOORS_ROW, SHORT_OF_LAB, LAB_EAST, PATH_WEST, GRASS_EDGE, TOP_EDGE, LADY, + SIGNS_ROW, SHORT_OF_201, ROUTE_201_WEST, PATH_EAST, MART_TREES_LOW, MART_TREES, ROUTE_202, + }; + + // Walks back east that a wild battle ends short of the sign. + size_t short_walks = 0; + // Walks west along the doors' row that a townsperson ends short of the Lab's sign. + size_t short_lab_walks = 0; + // No text reader for the game's language: boxes open and close, unread. + bool unread = false; + // Talks to the lady before she is there to answer. + size_t lady_away = 0; + Where where = Where::DOORS_ROW; + std::vector trace; + + virtual std::string save_screen_location() override{ return ""; } + virtual Pokemon::Outcome walk( + std::vector&, const std::function&, const std::string& label + ) override{ + trace.push_back("walk: " + label); + return Pokemon::outcome_failed("no_plan", label); + } + virtual bool east_then_south_into_sandgem() override{ return false; } + virtual Pokemon::Outcome walk_in_town( + std::vector&, const std::function&, const std::string& label + ) override{ + trace.push_back("walk: " + label); + return Pokemon::outcome_failed("no_plan", label); + } + virtual std::optional lit_doorway() override{ return std::nullopt; } + virtual Pokemon::Outcome line_up_with_door() override{ return Pokemon::outcome_failed("no_plan", "not on this checkpoint"); } + virtual bool inside_pokemon_center() override{ return false; } + virtual Pokemon::Outcome east_then_north_in_sandgem() override{ return Pokemon::outcome_ok(); } + virtual bool dialogue_showing() override{ return false; } + virtual Pokemon::Outcome finish_dialogue() override{ return Pokemon::outcome_ok(); } + virtual Pokemon::Outcome walk_until_stopped(Direction direction, size_t, const std::string& label) override{ + trace.push_back("go: " + label); + const struct{ Where from; Direction direction; Where to; } walks[] = { + {Where::DOORS_ROW, Direction::WEST, Where::LAB_EAST}, + {Where::SHORT_OF_LAB, Direction::WEST, Where::LAB_EAST}, + {Where::PATH_WEST, Direction::WEST, Where::GRASS_EDGE}, + {Where::GRASS_EDGE, Direction::NORTH, Where::TOP_EDGE}, + {Where::LADY, Direction::WEST, Where::TOP_EDGE}, + {Where::TOP_EDGE, Direction::WEST, Where::TOP_EDGE}, + {Where::SIGNS_ROW, Direction::EAST, Where::ROUTE_201_WEST}, + {Where::SHORT_OF_201, Direction::EAST, Where::ROUTE_201_WEST}, + {Where::PATH_EAST, Direction::EAST, Where::MART_TREES_LOW}, + {Where::MART_TREES_LOW, Direction::NORTH, Where::MART_TREES}, + }; + for (const auto& walk : walks){ + if (walk.from == where && walk.direction == direction){ + where = walk.to; + if (where == Where::ROUTE_201_WEST && short_walks > 0 && walk.from == Where::SIGNS_ROW){ + short_walks--; + where = Where::SHORT_OF_201; + } + if (where == Where::LAB_EAST && short_lab_walks > 0 && walk.from == Where::DOORS_ROW){ + short_lab_walks--; + where = Where::SHORT_OF_LAB; + } + break; + } + } + return Pokemon::outcome_ok(); + } + virtual Pokemon::Outcome step(Direction direction, const std::string& label) override{ + trace.push_back("step: " + label); + if (direction != Direction::SOUTH){ + return Pokemon::outcome_ok(); + } + if (where == Where::LAB_EAST){ + where = Where::PATH_WEST; + }else if (where == Where::LADY || where == Where::TOP_EDGE){ + where = Where::SIGNS_ROW; + }else if (where == Where::ROUTE_201_WEST){ + where = Where::PATH_EAST; + } + return Pokemon::outcome_ok(); + } + virtual DialogueRead talk(Direction, Button, const std::string& label) override{ + trace.push_back("talk: " + label); + return DialogueRead{}; + } + virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ + trace.push_back("read: " + label); + if (where == Where::LAB_EAST && facing == Direction::WEST){ + return DialogueRead{.opened = true, .closed = true, .text = unread ? "" : "Pokemon Research Lab"}; + } + if (where == Where::ROUTE_201_WEST && facing == Direction::EAST){ + return DialogueRead{.opened = true, .closed = true, .text = unread ? "" : "Route 201 Sandgem Town"}; + } + return DialogueRead{}; + } + virtual DialogueRead talk_walking(Direction direction, size_t, const std::string& label) override{ + trace.push_back("talk: " + label); + if (direction != Direction::EAST || (where != Where::TOP_EDGE && where != Where::LADY)){ + return DialogueRead{}; + } + where = Where::LADY; + if (lady_away > 0){ + lady_away--; + return DialogueRead{}; + } + return DialogueRead{.opened = true, .closed = true, .text = unread ? "" : "Did you know"}; + } + virtual Pokemon::Outcome walk_to_route_202_save_spot() override{ + trace.push_back("to Route 202"); + if (where != Where::MART_TREES){ + return Pokemon::outcome_failed("wrong_place", "to Route 202"); + } + where = Where::ROUTE_202; + return Pokemon::outcome_ok(); + } + virtual Pokemon::Outcome save_on_route_202() override{ + trace.push_back("save"); + return where == Where::ROUTE_202 ? Pokemon::outcome_ok() : Pokemon::outcome_failed("wrong_place", "save"); + } +}; + +class Test_MartLadyEvents : public UnitTest{ +public: + Test_MartLadyEvents() + : UnitTest("PokemonBDSP::AutoStory - Mart lady events") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const std::vector TO_LADY = {"go: West to the Lab's sign", "read: the Lab's sign", + "step: South onto the path", "go: West along the path", "go: North to the trees"}; + const std::vector LADY = {"talk: Poke Mart lady"}; + const std::vector RETRY = {"go: West to the grass's edge"}; + const std::vector TO_SIGN = {"step: South onto the signs' row", "go: East to the Route 201 sign"}; + const std::vector SIGN = {"read: Route 201's sign"}; + const std::vector HOME = {"step: South onto the path", "go: East along the path", + "go: North to the front row", "to Route 202", "save"}; + const auto join = [](std::initializer_list> parts){ + std::vector all; + for (const std::vector& part : parts){ + all.insert(all.end(), part.begin(), part.end()); + } + return all; + }; + const std::vector TO_LAB = {"go: West to the Lab's sign", "read: the Lab's sign"}; + const std::vector LAB_TO_LADY = {"step: South onto the path", "go: West along the path", "go: North to the trees"}; + struct Case{ + const char* name; + size_t short_walks; + size_t short_lab_walks = 0; + size_t lady_away; + bool unread = false; + bool completed; + size_t failed_event; + std::vector trace; + }; + const Case cases[] = { + { + .name = "Straight through", + .short_walks = 0, + .short_lab_walks = 0, + .lady_away = 0, + .unread = false, + .completed = true, + .failed_event = 0, + .trace = join({TO_LADY, LADY, TO_SIGN, SIGN, HOME}), + }, + { + .name = "Back east, a tile short of the sign", + .short_walks = 1, + .short_lab_walks = 0, + .lady_away = 0, + .unread = false, + .completed = true, + .failed_event = 0, + .trace = join({TO_LADY, LADY, TO_SIGN, SIGN, {"go: On to the sign"}, SIGN, HOME}), + }, + { + .name = "West, a tile short of the Lab's sign: re-anchored on it", + .short_walks = 0, + .short_lab_walks = 1, + .lady_away = 0, + .unread = false, + .completed = true, + .failed_event = 0, + .trace = join({TO_LAB, {"go: On to the sign", "read: the Lab's sign"}, LAB_TO_LADY, LADY, TO_SIGN, SIGN, HOME}), + }, + { + .name = "No text reader for the game's language: boxes open and close, unread", + .short_walks = 1, + .short_lab_walks = 1, + .lady_away = 0, + .unread = true, + .completed = true, + .failed_event = 0, + .trace = join({TO_LAB, {"go: On to the sign", "read: the Lab's sign"}, LAB_TO_LADY, LADY, TO_SIGN, SIGN, {"go: On to the sign"}, SIGN, HOME}), + }, + { + .name = "The lady answers the second time", + .short_walks = 0, + .short_lab_walks = 0, + .lady_away = 1, + .unread = false, + .completed = true, + .failed_event = 0, + .trace = join({TO_LADY, LADY, RETRY, LADY, TO_SIGN, SIGN, HOME}), + }, + { + .name = "The lady never answers", + .short_walks = 0, + .short_lab_walks = 0, + .lady_away = 5, + .unread = false, + .completed = false, + .failed_event = 6, + .trace = join({TO_LADY, LADY, RETRY, LADY, RETRY, LADY}), + }, + }; + for (const Case& c : cases){ + FakeMartLady game; + game.short_walks = c.short_walks; + game.short_lab_walks = c.short_lab_walks; + game.unread = c.unread; + game.lady_away = c.lady_away; + Pokemon::Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(checkpoint); + if (!problems.empty()){ + return std::string(c.name) + ": lint: " + problems.front(); + } + std::string log_text; + Pokemon::EventLogger log = [&](const std::string& line){ log_text += "\n " + line; }; + Pokemon::RunState state; + const Pokemon::CheckpointReport report = Pokemon::run_checkpoint(state, checkpoint, log); + logger.log(std::string(c.name) + ":" + log_text); + if (report.completed != c.completed || report.failed_event != c.failed_event){ + return std::string(c.name) + ": expected " + (c.completed ? "completed" : "failed") + + " at event " + std::to_string(c.failed_event) + ", read " + (report.completed ? "completed" : "failed") + + " at event " + std::to_string(report.failed_event) + ":" + log_text; + } + if (game.trace != c.trace){ + std::string expected, read; + for (const std::string& step : c.trace){ + expected += " [" + step + "]"; + } + for (const std::string& step : game.trace){ + read += " [" + step + "]"; + } + return std::string(c.name) + ": expected steps" + expected + ", read" + read + "."; + } + } + return true; + } +}; + + +// 00.05a and 00.05b against a scripted game: the door is `door_at` tiles +// west of the Center's doorstep. +class FakeJubilife : public JubilifeActions{ +public: + size_t door_at = 10; + bool stuck_on_house = false; + bool heal_stays_inside = false; + bool inside = false; + size_t column = 0; // tiles west of the doorstep + bool on_doorstep = false; + bool in_school = false; + std::vector trace; + + virtual std::string save_screen_location() override{ return ""; } + virtual Pokemon::Outcome walk(std::vector&, const std::function&, const std::string& label) override{ + return Pokemon::outcome_failed("no_plan", label); + } + virtual bool east_then_south_into_sandgem() override{ return false; } + virtual Pokemon::Outcome walk_in_town(std::vector&, const std::function&, const std::string& label) override{ + return Pokemon::outcome_failed("no_plan", label); + } + // The Poketch Company's sign reads after `sign_reads_after` tries. + size_t sign_reads_after = 1; + size_t sign_tries = 0; + virtual DialogueRead read_sign(Direction, const std::string& label) override{ + trace.push_back("sign: " + label); + if (label == "the Poketch Company's sign" && ++sign_tries >= sign_reads_after){ + return DialogueRead{.opened = true, .closed = true, .text = "The Poketch Company"}; + } + return {}; + } + virtual std::optional lit_doorway() override{ return std::nullopt; } + virtual Pokemon::Outcome line_up_with_door() override{ return Pokemon::outcome_ok(); } + virtual bool inside_pokemon_center() override{ return inside; } + virtual bool indoors() override{ return inside; } + virtual Pokemon::Outcome hold(Direction, int64_t, const std::string& label) override{ + trace.push_back("hold: " + label); + return Pokemon::outcome_ok(); + } + bool president_talks = true; + virtual DialogueRead walk_until_talked_to(size_t, const std::string& label) override{ + trace.push_back("meet: " + label); + return president_talks ? DialogueRead{.opened = true, .closed = true, .text = "Oh?"} : DialogueRead{}; + } + virtual Pokemon::Outcome east_then_north_in_sandgem() override{ return Pokemon::outcome_ok(); } + virtual bool dialogue_showing() override{ return false; } + virtual Pokemon::Outcome finish_dialogue() override{ return Pokemon::outcome_ok(); } + virtual Pokemon::Outcome walk_until_stopped(Direction, size_t, const std::string& label) override{ + trace.push_back("go: " + label); + return stuck_on_house && label == "East to the house" + ? Pokemon::outcome_failed("walk_failed", label) + : Pokemon::outcome_ok(); + } + virtual DialogueRead talk(Direction, Button, const std::string& label) override{ + trace.push_back("talk: " + label); + if (label == "Clown by the Poke Mart"){ + return DialogueRead{.opened = true, .closed = true, .text = "Hi!"}; + } + if (label == "Barry" && barry_answers){ + return DialogueRead{.opened = true, .closed = true, .text = "Barry"}; + } + if (label == "Clown by the Poketch Company" && ++clown_asks >= clown_answers_at){ + return DialogueRead{.opened = true, .closed = true, .text = "Hi!"}; + } + return {}; + } + virtual Pokemon::Outcome step(Direction direction, const std::string& label) override{ + trace.push_back("step: " + label); + (void)direction; + return Pokemon::outcome_ok(); + } + // The clown answers the `clown_answers_at`th ask. + size_t clown_answers_at = 7; + size_t clown_asks = 0; + virtual DialogueRead talk_walking(Direction, size_t, const std::string& label) override{ + trace.push_back("talk walking: " + label); + return {}; + } + virtual Pokemon::Outcome walk_to_route_202_save_spot() override{ return Pokemon::outcome_ok(); } + virtual Pokemon::Outcome save_on_route_202() override{ return Pokemon::outcome_ok(); } + virtual Pokemon::Outcome climb_the_steps() override{ + trace.push_back("climb"); + return Pokemon::outcome_ok(); + } + virtual Pokemon::Outcome heal_at_center() override{ + trace.push_back("heal"); + inside = heal_stays_inside; + on_doorstep = true; + column = 0; + return Pokemon::outcome_ok(); + } + virtual void turn(Direction direction) override{ + trace.push_back(std::string("turn ") + Pokemon::direction_name(direction)); + } + // The lamp: off by `lamp_off` tiles after the counted walk, or unlit. + std::optional lamp_off; + virtual std::optional school_door_across() override{ + if (!lamp_off){ + return std::nullopt; + } + return (double)door_at - (double)column; + } + virtual void step_across(double tiles, const std::string&) override{ + column += (size_t)std::lround(tiles); + } + bool barry_answers = true; + bool left_school = false; + virtual Pokemon::Outcome leave_south(const std::string&) override{ + left_school = true; + return Pokemon::outcome_ok(); + } + // Half tiles: a nudge east then west lands back on the column. + int half = 0; + virtual void nudge(Direction direction, const std::string&) override{ + half += direction == Direction::EAST ? -1 : 1; + if (half == 2){ column++; half = 0; } + if (half == -2){ column--; half = 0; } + } + virtual Pokemon::Outcome tile(Direction direction, const std::string&) override{ + if (direction == Direction::WEST){ + column++; + } + return Pokemon::outcome_ok(); + } + virtual void close_panel(const std::string& label) override{ + trace.push_back("close: " + label); + } + // The president answers the `president_answers_at`th ask ahead. + size_t president_answers_at = 5; + size_t president_asks = 0; + virtual DialogueRead talk_ahead(Button, const std::string& label) override{ + trace.push_back("ask: " + label); + if (label == "The Poketch Company president" && ++president_asks >= president_answers_at){ + return DialogueRead{.opened = true, .closed = true, .text = "Bravo!"}; + } + return {}; + } + virtual Pokemon::Outcome taps(Direction direction, size_t tiles, const std::string& label) override{ + trace.push_back("taps: " + label); + if (direction == Direction::WEST){ + column += tiles; + } + return Pokemon::outcome_ok(); + } + virtual bool through_door_north(const std::string&) override{ + trace.push_back("north at " + std::to_string(column)); + in_school = column == door_at; + return in_school; + } +}; + +class Test_JubilifeEvents : public UnitTest{ +public: + Test_JubilifeEvents() + : UnitTest("PokemonBDSP::AutoStory - Jubilife 00.05a/b events") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* name; size_t door_at; bool stuck; bool stays_inside; bool center_done; bool school_done; size_t tries; bool lamp = false; }; + const Case cases[] = { + {"Door eleven tiles west, as measured", 11, false, false, true, true, 1}, + {"Door a whole tile off: a map error, not found", 12, false, false, true, false, 4}, + {"Door off by two: not found", 14, false, false, true, false, 4}, + {"The house never stops us", 11, true, false, false, false, 0}, + {"The heal leaves us inside (r143a)", 11, false, true, false, false, 0}, + {"A tile off, the lamp lit: it lines us up", 12, false, false, true, true, 1, true}, + {"Two tiles off, the lamp lit: it lines us up", 9, false, false, true, true, 1, true}, + }; + for (const Case& c : cases){ + FakeJubilife game; + game.door_at = c.door_at; + game.stuck_on_house = c.stuck; + game.heal_stays_inside = c.stays_inside; + if (c.lamp){ + game.lamp_off = 0.0; + } + for (size_t which = 0; which < 2; which++){ + Pokemon::Checkpoint checkpoint = which == 0 ? jubilife_center_checkpoint(game) : jubilife_school_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(checkpoint); + if (!problems.empty()){ + return std::string(c.name) + ": lint: " + problems.front(); + } + } + Pokemon::EventLogger log = [&](const std::string& line){ logger.log(line); }; + Pokemon::RunState center_state; + Pokemon::Checkpoint center = jubilife_center_checkpoint(game); + const bool center_done = Pokemon::run_checkpoint(center_state, center, log).completed; + bool school_done = false; + if (center_done){ + Pokemon::RunState school_state; + Pokemon::Checkpoint school = jubilife_school_checkpoint(game); + school_done = Pokemon::run_checkpoint(school_state, school, log).completed; + } + if (school_done){ + Pokemon::RunState barry_state; + Pokemon::Checkpoint barry = jubilife_barry_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(barry); + if (!problems.empty()){ + return std::string(c.name) + ": 00.05c lint: " + problems.front(); + } + if (!Pokemon::run_checkpoint(barry_state, barry, log).completed || !game.left_school){ + return std::string(c.name) + ": 00.05c did not complete."; + } + } + if (school_done){ + Pokemon::RunState president_state; + Pokemon::Checkpoint president = jubilife_president_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(president); + if (!problems.empty()){ + return std::string(c.name) + ": 00.05d lint: " + problems.front(); + } + if (!Pokemon::run_checkpoint(president_state, president, log).completed){ + return std::string(c.name) + ": 00.05d did not complete."; + } + } + if (school_done){ + Pokemon::RunState clown_state; + Pokemon::Checkpoint clown = jubilife_clown_mart_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(clown); + if (!problems.empty()){ + return std::string(c.name) + ": 00.05e lint: " + problems.front(); + } + if (!Pokemon::run_checkpoint(clown_state, clown, log).completed){ + return std::string(c.name) + ": 00.05e did not complete."; + } + } + if (school_done){ + Pokemon::RunState clown_state; + Pokemon::Checkpoint clown = jubilife_clown_company_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(clown); + if (!problems.empty()){ + return std::string(c.name) + ": 00.05f lint: " + problems.front(); + } + if (!Pokemon::run_checkpoint(clown_state, clown, log).completed){ + return std::string(c.name) + ": 00.05f did not complete."; + } + } + if (school_done){ + Pokemon::RunState poketch_state; + Pokemon::Checkpoint poketch = jubilife_poketch_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(poketch); + if (!problems.empty()){ + return std::string(c.name) + ": 00.05g lint: " + problems.front(); + } + if (!Pokemon::run_checkpoint(poketch_state, poketch, log).completed){ + return std::string(c.name) + ": 00.05g did not complete."; + } + } + size_t tries = 0; + for (const std::string& step : game.trace){ + tries += step.rfind("north at", 0) == 0; + } + if (center_done != c.center_done || school_done != c.school_done || (school_done && tries != c.tries)){ + return std::string(c.name) + ": center " + (center_done ? "done" : "failed") + + ", school " + (school_done ? "done" : "failed") + ", " + std::to_string(tries) + " tries north."; + } + } + return true; + } +}; + + +// 00.05f: a tile at a time along the hedges asking north, then waiting in +// the gap beside the sign until the wandering clown steps next to us. +class Test_JubilifeClownCompany : public UnitTest{ +public: + Test_JubilifeClownCompany() + : UnitTest("PokemonBDSP::AutoStory - Jubilife 00.05f clown wait") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* name; size_t sign_after; size_t clown_at; bool completed; }; + const Case cases[] = { + {"Clown north of the hedge walk", 1, 2, true}, + {"Sign at the first tile, clown comes by in the gap", 1, 5, true}, + {"Sign at the third tile, clown comes by late", 3, 40, true}, + {"Sign never reads: never in the gap", 100, 40, false}, + {"Clown never answers", 1, 1000, false}, + }; + for (const Case& c : cases){ + FakeJubilife game; + game.sign_reads_after = c.sign_after; + game.clown_answers_at = c.clown_at; + Pokemon::EventLogger log = [&](const std::string& line){ logger.log(line); }; + Pokemon::RunState state; + Pokemon::Checkpoint checkpoint = jubilife_clown_company_checkpoint(game); + const std::vector problems = Pokemon::lint_checkpoint(checkpoint); + if (!problems.empty()){ + return std::string(c.name) + ": lint: " + problems.front(); + } + const bool completed = Pokemon::run_checkpoint(state, checkpoint, log).completed; + if (completed != c.completed){ + return std::string(c.name) + (completed ? ": completed" : ": did not complete") + "."; + } + } + return true; + } +}; + + +// The School's lamp at night, from its doorsteps' row (2026-10-30): where +// the door took us in, the lamp reads within half a tile of the door; a +// tile either side, more. +class Test_SchoolLamp : public UnitTest{ +public: + Test_SchoolLamp() + : UnitTest("PokemonBDSP::AutoStory - Jubilife School lamp") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const ImageFloatBox box(0.55, 0.20, 0.30, 0.10); + struct Case{ const char* file; double min; double max; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeSchoolRightOfDoor-20261030.png", -1.5, -0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolOneTileShort-20261030.png", -1.5, -0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolAtDoor-20261030.png", -0.5, 0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolAtDoor2-20261030.png", -0.5, 0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", -0.5, 0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolOneTilePast-20261030.png", 0.5, 1.5}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const ImageRGB32 screen(path); + const std::optional lamp = find_lit_lamp_x(screen, box); + if (!lamp){ + return std::string(c.file) + ": no lit lamp found."; + } + const double across = tiles_across_at(*lamp) - tiles_across_at(0.67); + logger.log(std::string(c.file) + ": lamp at " + std::to_string(*lamp) + ", door " + std::to_string(across) + " tiles across."); + if (across < c.min || across > c.max){ + return std::string(c.file) + ": door " + std::to_string(across) + " tiles across, expected " + + std::to_string(c.min) + " to " + std::to_string(c.max) + "."; + } + } + return true; + } +}; + + +// Out of a building, the town's location banner shows; inside, none +// (Jubilife, 2026-10-30): what says a walk out of a door got out. +// Indoors or out, wherever the camera is (2026-10-30 screenshots). +class Test_Indoors : public UnitTest{ +public: + Test_Indoors() + : UnitTest("PokemonBDSP::AutoStory - indoors") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* file; bool inside; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", true}, + {"PokemonBDSP/AutoStory/SandgemCenterInside-20261030-053555.png", true}, + {"PokemonBDSP/AutoStory/JubilifeSchoolInside-20261030.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", false}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", false}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep2-20261030.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, + {"PokemonBDSP/AutoStory/SandgemCenterBelowSign-20261030-053527.png", false}, + {"PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", false}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const bool inside = indoors(ImageRGB32(path)); + logger.log(std::string(c.file) + ": " + (inside ? "indoors" : "outside")); + if (inside != c.inside){ + return std::string(c.file) + ": expected " + (c.inside ? "indoors" : "outside") + "."; + } + } + return true; + } +}; + + +class Test_LocationBannerShowing : public UnitTest{ +public: + Test_LocationBannerShowing() + : UnitTest("PokemonBDSP::AutoStory - location banner showing") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* file; bool banner; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", true}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const bool banner = location_banner_showing(ImageRGB32(path)); + logger.log(std::string(c.file) + ": banner " + (banner ? "shows" : "none")); + if (banner != c.banner){ + return std::string(c.file) + ": expected " + (c.banner ? "a banner" : "no banner") + "."; + } + } + return true; + } +}; + + +void add_tests_AutoStory(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h new file mode 100644 index 0000000000..18b8c8a666 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h @@ -0,0 +1,26 @@ +/* BDSP AutoStory Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// AutoStory tests that need no console: the new-game intro's decisions. +void add_tests_AutoStory(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 7e72c2b3d2..8322f86bf9 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1583,10 +1583,18 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp From 03c62a69a4b7aa80fcf4104885c10c5b0357908e Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 12:09:21 -0800 Subject: [PATCH 04/98] BDSP AutoStory: pass the walker its wild-battle escape The walker no longer runs from wild battles itself: every walk and route call takes run_from_wild_battle(), escape_wild_battle() for a standard wild battle, from Segment_01_tools. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- ...emonBDSP_AutoStory_SandgemCenterEvents.cpp | 33 ++++---- .../PokemonBDSP_AutoStory_Segment_00.cpp | 63 +++++++------- .../PokemonBDSP_AutoStory_Segment_01.cpp | 83 ++++++++++--------- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 5 +- ...PokemonBDSP_Autostory_Segment_01_tools.cpp | 4 + .../PokemonBDSP_Autostory_Segment_01_tools.h | 5 ++ 6 files changed, 103 insertions(+), 90 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp index 81f16f511d..2f6f37c43f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp @@ -34,6 +34,7 @@ #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" #include "PokemonBDSP_AutoStory_SandgemCenterEvents.h" #include "Pokemon/Navigation/Pokemon_TilePose.h" @@ -306,7 +307,7 @@ class ConsoleSandgemCenterActions : public SandgemCenterActions{ "Trainer " + trainer + " saw us on a way no trainer should see.", m_stream); }; try{ - at = walk_route(m_stream, m_context, story_maps(), at, goal, m_trainers, fight, label, {}, costs); + at = walk_route(m_stream, m_context, run_from_wild_battle, story_maps(), at, goal, m_trainers, fight, label, {}, costs); return outcome_ok(); }catch (OperationFailedException& e){ // Through a door: nothing to re-anchor on in there. @@ -321,8 +322,8 @@ class ConsoleSandgemCenterActions : public SandgemCenterActions{ saw_sandgem = saw_sandgem || location_banner_matches(read_location_banner(screen), MAP_SANDGEM_TOWN); return false; }; - const WalkResult east = walk_until(m_stream, m_context, 1, 0, 6000ms, "East to the trees"); - const WalkResult south = walk_until(m_stream, m_context, 0, -1, 9000ms, "South to the wall", banner); + const WalkResult east = walk_until(m_stream, m_context, run_from_wild_battle, 1, 0, 6000ms, "East to the trees"); + const WalkResult south = walk_until(m_stream, m_context, run_from_wild_battle, 0, -1, 9000ms, "South to the wall", banner); for (const WalkResult& walk : {east, south}){ if (walk.stop == WalkStop::TRANSITION || walk.stop == WalkStop::DIALOG){ route_status(m_stream, std::string("Re-anchor: ") + walk_stop_name(walk.stop) + " instead of a wall."); @@ -400,8 +401,8 @@ class ConsoleSandgemCenterActions : public SandgemCenterActions{ } virtual Outcome east_then_north_in_sandgem() override{ - const WalkResult east = walk_until(m_stream, m_context, 1, 0, 6000ms, "Sandgem: east to a wall"); - const WalkResult north = walk_until(m_stream, m_context, 0, 1, 6000ms, "Sandgem: north to a wall"); + const WalkResult east = walk_until(m_stream, m_context, run_from_wild_battle, 1, 0, 6000ms, "Sandgem: east to a wall"); + const WalkResult north = walk_until(m_stream, m_context, run_from_wild_battle, 0, 1, 6000ms, "Sandgem: north to a wall"); for (const WalkResult& walk : {east, north}){ if (walk.stop != WalkStop::BLOCKED){ return outcome_failed("not_anchored", std::string("Sandgem: ") + walk_stop_name(walk.stop) + " instead of a wall"); @@ -545,7 +546,7 @@ class ConsoleMartLadyActionsT : public Base{ // holds still meanwhile, and that read as a wall two tiles into the // walk west to the Lab's sign (r132a). route_turn(m_env.console, m_context, direction); - const WalkResult walked = walk_until(m_env.console, m_context, x, y, limit, label); + const WalkResult walked = walk_until(m_env.console, m_context, run_from_wild_battle, x, y, limit, label); m_env.console.log("[Walk] " + label + ": stopped on " + walk_stop_name(walked.stop) + "."); if (walked.stop == WalkStop::TRANSITION || walked.stop == WalkStop::DIALOG){ return outcome_failed("walk_failed", label + ": " + walk_stop_name(walked.stop) + " instead of a stop"); @@ -585,7 +586,7 @@ class ConsoleMartLadyActionsT : public Base{ const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; const bool across = direction == Direction::EAST || direction == Direction::WEST; const Milliseconds limit = (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN) + 800ms; - const WalkResult walked = walk_to_talk(m_env.console, m_context, x, y, limit, label); + const WalkResult walked = walk_to_talk(m_env.console, m_context, run_from_wild_battle, x, y, limit, label); if (walked.stop != WalkStop::DIALOG){ m_env.console.log("[Talk] " + label + ": nobody answered."); return read; @@ -600,7 +601,7 @@ class ConsoleMartLadyActionsT : public Base{ // took one or two (2026-10-30). const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - const WalkResult walk = walk_for(m_env.console, m_context, x, y, 200ms, label); + const WalkResult walk = walk_for(m_env.console, m_context, run_from_wild_battle, x, y, 200ms, label); if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); } @@ -1295,8 +1296,8 @@ class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ {} virtual Outcome climb_the_steps() override{ route_turn(m_stream, m_context, Direction::WEST); - walk_for(m_stream, m_context, -1, 0, 200ms, "To the steps"); - const WalkResult up = walk_for(m_stream, m_context, 0, 1, 480ms, "Up the steps"); + walk_for(m_stream, m_context, run_from_wild_battle, -1, 0, 200ms, "To the steps"); + const WalkResult up = walk_for(m_stream, m_context, run_from_wild_battle, 0, 1, 480ms, "Up the steps"); if (up.stop == WalkStop::DIALOG || up.stop == WalkStop::TRANSITION){ return outcome_failed("walk_failed", std::string("up the steps: ") + walk_stop_name(up.stop)); } @@ -1337,7 +1338,7 @@ class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ virtual DialogueRead walk_until_talked_to(size_t tiles, const std::string& label) override{ DialogueRead read; const Milliseconds limit = (int64_t)tiles * ROUTE_MS_PER_TILE_UP_DOWN + 800ms; - walk_until(m_stream, m_context, 0, 1, limit, label); + walk_until(m_stream, m_context, run_from_wild_battle, 0, 1, limit, label); // He walks up after the "!" and only then talks. ShortDialogWatcher opened(COLOR_CYAN); if (wait_until(m_stream, m_context, 5000ms, {{opened}}) != 0){ @@ -1352,7 +1353,7 @@ class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ virtual Outcome hold(Direction direction, int64_t ms, const std::string& label) override{ const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - const WalkResult walk = walk_for(m_stream, m_context, x, y, Milliseconds(ms), label); + const WalkResult walk = walk_for(m_stream, m_context, run_from_wild_battle, x, y, Milliseconds(ms), label); if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); } @@ -1365,7 +1366,7 @@ class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ } virtual Outcome leave_south(const std::string& label) override{ try{ - leave_door_south(m_stream, m_context, label); + leave_door_south(m_stream, m_context, run_from_wild_battle, label); }catch (OperationFailedException& e){ return outcome_failed("walk_failed", e.message()); } @@ -1373,12 +1374,12 @@ class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ } virtual void nudge(Direction direction, const std::string& label) override{ route_turn(m_stream, m_context, direction); - walk_for(m_stream, m_context, direction == Direction::EAST ? 1 : -1, 0, 80ms, label); + walk_for(m_stream, m_context, run_from_wild_battle, direction == Direction::EAST ? 1 : -1, 0, 80ms, label); } virtual Outcome tile(Direction direction, const std::string& label) override{ const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - const WalkResult walk = walk_for(m_stream, m_context, x, y, 160ms, label); + const WalkResult walk = walk_for(m_stream, m_context, run_from_wild_battle, x, y, 160ms, label); if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); } @@ -1416,7 +1417,7 @@ class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ virtual bool through_door_north(const std::string& label) override{ m_context.wait_for_all_requests(); VideoSnapshot before = m_stream.video().snapshot(); - const WalkResult up = walk_until(m_stream, m_context, 0, 1, 1200ms, label); + const WalkResult up = walk_until(m_stream, m_context, run_from_wild_battle, 0, 1, 1200ms, label); m_context.wait_for(1500ms); if (up.stop == WalkStop::TRANSITION){ return true; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index a6221ee38d..40e1eb537d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -43,6 +43,7 @@ #include "NintendoSwitch/Programs/DateManip/NintendoSwitch_DateManip.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" #include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" using namespace std::chrono_literals; @@ -663,12 +664,12 @@ namespace PokemonAutomation { const TilePos door = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); const TilePos front = door.next(Pokemon::Direction::SOUTH); const auto at_front = [&](const TilePos& pos) { return pos.y == front.y && std::abs(pos.x - front.x) <= 1; }; - walk_route(stream, context, maps, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), + walk_route(stream, context, run_from_wild_battle, maps, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), at_front, trainers, fight, "To the Poke Mart"); const std::vector short_of_door = tiles_short_of_door(stream, context, maps.world(), door, "To the Poke Mart"); if (!short_of_door.empty()) { route_status(stream, "To the Poke Mart: stopped short of the door; walking again."); - walk_route(stream, context, maps, short_of_door, at_front, trainers, fight, "To the Poke Mart again"); + walk_route(stream, context, run_from_wild_battle, maps, short_of_door, at_front, trainers, fight, "To the Poke Mart again"); } enter_lit_door(stream, context, "Sandgem Poke Mart: through the door"); } @@ -680,7 +681,7 @@ namespace PokemonAutomation { // name and price; the quantity is counted out blind. static void buy_potions_at_the_counter(VideoStream& stream, ProControllerContext& context) { route_status(stream, "Poke Mart: to the clerk."); - const WalkResult walk = walk_until(stream, context, 0, 1, 6000ms, "Poke Mart: to the clerk"); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "Poke Mart: to the clerk"); if (walk.stop != WalkStop::BLOCKED && walk.stop != WalkStop::DIALOG) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Poke Mart: the counter was not reached (" + std::string(walk_stop_name(walk.stop)) + ").", stream); @@ -690,7 +691,7 @@ namespace PokemonAutomation { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Poke Mart: could not buy " + std::to_string(POTIONS_TO_BUY) + " Potions.", stream); } - leave_door_south(stream, context, "Poke Mart: out the door"); + leave_door_south(stream, context, run_from_wild_battle, "Poke Mart: out the door"); } // Potions let us heal where we stand. Sandgem's Poke Mart is next @@ -742,7 +743,7 @@ namespace PokemonAutomation { using Pokemon::Direction; std::vector banners; for (size_t nudges = 0;; nudges++){ - const WalkResult north = walk_until(stream, context, 0, 1, 8000ms, "North through the gap", banner_keeper(banners)); + const WalkResult north = walk_until(stream, context, run_from_wild_battle, 0, 1, 8000ms, "North through the gap", banner_keeper(banners)); if (north.stop == WalkStop::TRANSITION || north.stop == WalkStop::DIALOG || north.wild_battles > 0){ OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "North through the gap: " + std::string(walk_stop_name(north.stop)) + " instead of a wall.", stream); @@ -757,7 +758,7 @@ namespace PokemonAutomation { } route_status(stream, "North through the gap: blocked at once, below a sign or the Mart; a tile west."); route_turn(stream, context, Direction::WEST); - walk_for(stream, context, -1, 0, route_open_hold(Direction::WEST, 1), "A tile west"); + walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 1), "A tile west"); route_turn(stream, context, Direction::NORTH); } context.wait_for_all_requests(); @@ -782,7 +783,7 @@ namespace PokemonAutomation { using Pokemon::Direction; route_status(stream, "To the gap between the Center's and the Mart's signs: west 6."); route_turn(stream, context, Direction::WEST); - walk_for(stream, context, -1, 0, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); + walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); const bool on_route = walk_north_through_gap(stream, context); std::vector ends; for (const TilePos& pos : SandgemTown::below_sign_gap()){ @@ -809,11 +810,11 @@ namespace PokemonAutomation { const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END); const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); if (!all_on_map(maps, at, MAP_ROUTE_202)) { - at = walk_route(stream, context, maps, std::move(at), + at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), [&](const TilePos& pos) { return pos == row_end; }, trainers, fight, "To the trees east of the Poke Mart"); at = north_through_sign_gap(stream, context, maps); } - walk_route(stream, context, maps, std::move(at), + walk_route(stream, context, run_from_wild_battle, maps, std::move(at), [&](const TilePos& pos) { return pos == save_spot; }, trainers, fight, "Back to Route 202"); } @@ -837,7 +838,7 @@ namespace PokemonAutomation { // are further west. The scene itself is the next step. static void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { for (size_t attempt = 0; attempt < 6; attempt++) { - const WalkResult walk = walk_until(stream, context, 0, 1, 4000ms, "to Dawn in Jubilife"); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 4000ms, "to Dawn in Jubilife"); if (walk.stop == WalkStop::TRANSITION) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Jubilife: left the area instead of meeting Dawn.", stream); @@ -872,8 +873,8 @@ namespace PokemonAutomation { // Onto the doors' row first: out of the door we stand a tile to // three below it (r261 walked west into the trees from three). route_turn(stream, context, Pokemon::Direction::EAST); - walk_tiles_dpad(stream, context, 1, 0, 1, "east under the Center's wall"); - const WalkResult up = walk_until(stream, context, 0, 1, 3000ms, "north to the Center's front"); + walk_tiles_dpad(stream, context, run_from_wild_battle, 1, 0, 1, "east under the Center's wall"); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); if (up.stop != WalkStop::BLOCKED) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + ".", stream); @@ -1098,8 +1099,8 @@ namespace PokemonAutomation { // From the save below the steps, against the wall east of them: // a tile west (the first press only turns), then up the steps. route_turn(stream, context, Pokemon::Direction::WEST); - walk_for(stream, context, -1, 0, 200ms, "To the steps"); - walk_for(stream, context, 0, 1, 480ms, "Up the steps"); + walk_for(stream, context, run_from_wild_battle, -1, 0, 200ms, "To the steps"); + walk_for(stream, context, run_from_wild_battle, 0, 1, 480ms, "Up the steps"); std::vector at; for (const TilePos& pos : JubilifeCity::up_the_steps()) { at.push_back(world(pos)); @@ -1115,8 +1116,8 @@ namespace PokemonAutomation { // front, which no wall pins. Measured on the console (2026-10-30): // from the top of the steps, east stops at the house east of the // road, and north from there stops in front of the Center. - walk_until(stream, context, 1, 0, 4000ms, "East to the house"); - walk_until(stream, context, 0, 1, 4000ms, "North to the Pokemon Center"); + walk_until(stream, context, run_from_wild_battle, 1, 0, 4000ms, "East to the house"); + walk_until(stream, context, run_from_wild_battle, 0, 1, 4000ms, "North to the Pokemon Center"); heal_at_pokemon_center(stream, context, "Jubilife Pokemon Center"); // From the doorstep the School's door is ten one-tile steps west // (measured). Nothing on that row stops a walk, so count the @@ -1125,14 +1126,14 @@ namespace PokemonAutomation { route_turn(stream, context, Pokemon::Direction::WEST); bool in_school = false; for (size_t tiles = 1; tiles <= 12 && !in_school; tiles++) { - walk_for(stream, context, -1, 0, 160ms, "West to the School's door"); + walk_for(stream, context, run_from_wild_battle, -1, 0, 160ms, "West to the School's door"); if (tiles < 8) { continue; } - const WalkResult up = walk_until(stream, context, 0, 1, 1200ms, "Trainers' School door"); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 1200ms, "Trainers' School door"); in_school = up.stop == WalkStop::TRANSITION; if (!in_school) { - walk_for(stream, context, 0, -1, 200ms, "Back to the doorsteps' row"); + walk_for(stream, context, run_from_wild_battle, 0, -1, 200ms, "Back to the doorsteps' row"); route_turn(stream, context, Pokemon::Direction::WEST); } } @@ -1145,7 +1146,7 @@ namespace PokemonAutomation { // step west, walk north until he stops us, talk. route_turn(stream, context, Pokemon::Direction::WEST); pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); - walk_until(stream, context, 0, 1, 3000ms, "to Barry"); + walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "to Barry"); pbf_press_button(context, BUTTON_A, 80ms, 200ms); context.wait_for_all_requests(); { @@ -1158,10 +1159,10 @@ namespace PokemonAutomation { // His scene ends with "You obtained a Town Map!", which A closes. finish_dialogue(stream, context, BUTTON_A, "Barry"); // Out: south to the front wall, a tile east (the door's column), out. - walk_until(stream, context, 0, -1, 4000ms, "to the school's front wall"); + walk_until(stream, context, run_from_wild_battle, 0, -1, 4000ms, "to the school's front wall"); route_turn(stream, context, Pokemon::Direction::EAST); pbf_move_left_joystick(context, {1, 0}, route_open_hold(Pokemon::Direction::EAST, 1), 200ms); - if (walk_until(stream, context, 0, -1, 2500ms, "out of the Trainers' School").stop != WalkStop::TRANSITION) { + if (walk_until(stream, context, run_from_wild_battle, 0, -1, 2500ms, "out of the Trainers' School").stop != WalkStop::TRANSITION) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Trainers' School: did not leave by the door.", stream); } @@ -1172,42 +1173,42 @@ namespace PokemonAutomation { // The president talks on with A after each last line; B moves his // lines on and never starts a new talk. const TilePos president = world(JubilifeCity::POKETCH_PRESIDENT); - talk_to_npc(stream, context, maps, at, president, + talk_to_npc(stream, context, run_from_wild_battle, maps, at, president, {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH}, trainers, in_the_way, "Poketch president"); finish_dialogue(stream, context, BUTTON_B, "Poketch president"); step(3); // Each clown asks a question whose default answer, Yes, is right, // and hands over a coupon with an "obtained" message A closes. - talk_to_npc(stream, context, maps, at, world(JubilifeCity::CLOWN_MART), + talk_to_npc(stream, context, run_from_wild_battle, maps, at, world(JubilifeCity::CLOWN_MART), {Pokemon::Direction::SOUTH, Pokemon::Direction::WEST, Pokemon::Direction::EAST}, trainers, in_the_way, "Clown by the Poke Mart"); finish_dialogue(stream, context, BUTTON_A, "Clown by the Poke Mart"); - talk_to_npc(stream, context, maps, at, world(JubilifeCity::CLOWN_TV_STATION), + talk_to_npc(stream, context, run_from_wild_battle, maps, at, world(JubilifeCity::CLOWN_TV_STATION), {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH, Pokemon::Direction::WEST}, trainers, in_the_way, "Clown by the TV station"); finish_dialogue(stream, context, BUTTON_A, "Clown by the TV station"); - talk_to_npc(stream, context, maps, at, world(JubilifeCity::CLOWN_POKETCH_COMPANY), + talk_to_npc(stream, context, run_from_wild_battle, maps, at, world(JubilifeCity::CLOWN_POKETCH_COMPANY), {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH, Pokemon::Direction::WEST}, trainers, in_the_way, "Clown by the Poketch Company"); finish_dialogue(stream, context, BUTTON_A, "Clown by the Poketch Company"); step(4); - talk_to_npc(stream, context, maps, at, president, + talk_to_npc(stream, context, run_from_wild_battle, maps, at, president, {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH}, trainers, in_the_way, "Poketch president"); finish_dialogue(stream, context, BUTTON_B, "Poketch president"); step(5); const TilePos center = world(JubilifeCity::POKEMON_CENTER_DOOR.next(Pokemon::Direction::SOUTH)); - at = walk_route(stream, context, maps, at, by_door(center), + at = walk_route(stream, context, run_from_wild_battle, maps, at, by_door(center), trainers, in_the_way, "To the Pokemon Center"); - enter_door_north(stream, context, "Pokemon Center door"); + enter_door_north(stream, context, run_from_wild_battle, "Pokemon Center door"); // Inside: north to the counter, three tiles east, north to the PC // at the back wall right of the counter. - walk_until(stream, context, 0, 1, 2500ms, "to the counter"); + walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "to the counter"); route_turn(stream, context, Pokemon::Direction::EAST); pbf_move_left_joystick(context, {1, 0}, route_open_hold(Pokemon::Direction::EAST, 3), 200ms); - walk_until(stream, context, 0, 1, 1500ms, "to the PC"); + walk_until(stream, context, run_from_wild_battle, 0, 1, 1500ms, "to the PC"); context.wait_for_all_requests(); } // --------------------------------------------------------------------------- diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp index 00cc3cf623..dd06b7c19b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp @@ -31,6 +31,7 @@ #include "CommonTools/VisualDetectors/BlackScreenDetector.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" #include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" #include @@ -180,7 +181,7 @@ void jubilife_to_barry(VideoStream& stream, ProControllerContext& context){ pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 2), 200ms); route_turn(stream, context, Direction::WEST); pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 3), 200ms); - if (walk_to_talk(stream, context, 0, 1, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ + if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ fail(stream, "Jubilife Pokemon Center: the nurse did not answer."); } // A throughout: Yes to "rest your Pokemon?" (B would answer No). @@ -194,7 +195,7 @@ void jubilife_to_barry(VideoStream& stream, ProControllerContext& context){ // door's fade may or may not read as a transition. WalkResult out{WalkStop::LIMIT, 0ms, 0}; for (size_t walk = 0; walk < 3 && out.stop != WalkStop::BLOCKED; walk++){ - out = walk_until(stream, context, 0, -1, 8000ms, "out of the Pokemon Center"); + out = walk_until(stream, context, run_from_wild_battle, 0, -1, 8000ms, "out of the Pokemon Center"); if (out.stop == WalkStop::DIALOG || out.stop == WalkStop::ARRIVED){ break; } @@ -233,7 +234,7 @@ void jubilife_to_barry(VideoStream& stream, ProControllerContext& context){ context.wait_for_all_requests(); WalkResult walk{WalkStop::LIMIT, 0ms, 0}; for (size_t stretch = 0; stretch < 3 && walk.stop == WalkStop::LIMIT; stretch++){ - walk = walk_until(stream, context, 1, 0, 9000ms, "east to Route 203"); + walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 9000ms, "east to Route 203"); } barry = walk.stop == WalkStop::DIALOG; } @@ -275,7 +276,7 @@ void route203_to_oreburgh(VideoStream& stream, ProControllerContext& context){ } }; for (size_t leg = 0; leg < 40 && !inside; leg++){ - const WalkResult walk = walk_until(stream, context, 1, 0, 10000ms, "east along Route 203"); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203"); if (walk.stop == WalkStop::TRANSITION){ inside = true; break; @@ -291,11 +292,11 @@ void route203_to_oreburgh(VideoStream& stream, ProControllerContext& context){ // first (a step north there does not move us, so rows are counted // from it), then a row at a time south. At the route's end this is the // corner by the second sign, with the Gate's opening two rows down. - walk_until(stream, context, 0, 1, 6000ms, "north to the top wall"); + walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "north to the top wall"); bool moved_on = false; for (size_t row = 1; row <= 10 && !moved_on; row++){ step_south(stream, context); - const WalkResult next = walk_until(stream, context, 1, 0, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); + const WalkResult next = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); if (next.stop == WalkStop::TRANSITION){ inside = true; } @@ -318,7 +319,7 @@ void route203_to_oreburgh(VideoStream& stream, ProControllerContext& context){ step(2); // The Hiker steps up to us as we walk east. if (!met_hiker){ - const WalkResult to_hiker = walk_until(stream, context, 1, 0, 4000ms, "to the Hiker"); + const WalkResult to_hiker = walk_until(stream, context, run_from_wild_battle, 1, 0, 4000ms, "to the Hiker"); if (to_hiker.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ fail(stream, "Oreburgh Gate: the Hiker did not stop us."); } @@ -338,7 +339,7 @@ void route203_to_oreburgh(VideoStream& stream, ProControllerContext& context){ bool city = false; size_t stuck = 0; for (size_t leg = 0; leg < 12 && !city; leg++){ - const WalkResult walk = walk_until(stream, context, 1, 0, 10000ms, "east through the Oreburgh Gate"); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east through the Oreburgh Gate"); if (walk.stop == WalkStop::LIMIT || walk.stop == WalkStop::TRANSITION){ continue; } @@ -386,7 +387,7 @@ WalkResult walk_dir( ){ const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; - return walk_until(stream, context, x, y, limit, label, arrived); + return walk_until(stream, context, run_from_wild_battle, x, y, limit, label, arrived); } // Out of a building whose door is straight south of us, a D-pad tile at a @@ -474,8 +475,8 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ // the Center's east corner stops a walk east on these rows a tile east of // the door (by hand): walk to it, north to the Center's front, tap back // one, and shift one tap either side. - walk_to_wall(stream, context, 1, 0, 5000ms, "east to the fence below the Center"); - walk_to_wall(stream, context, 0, 1, 3000ms, "north to the Center's front"); + walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 5000ms, "east to the fence below the Center"); + walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); // The door wants us near its middle: a turn and a tap west (half a tile // too far, by hand) or a turn alone leave north blocked by the facade // beside it. The facade blocks north for two tiles either side of the @@ -506,7 +507,7 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ // door's column, and the walk back out drifted into the yard (r188b). // Oreburgh's sand reads up to 34 % warm outside (r189a), the floor // 56 to 64 % inside. - const WalkResult in = walk_until(stream, context, 0, 1, 2500ms, "north into the Center's door", + const WalkResult in = walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && inside_now())){ inside = true; @@ -523,7 +524,7 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ context.wait_for(2000ms); // he runs off } // Talked in the door's column: the door is free now. - walk_until(stream, context, 0, 1, 2500ms, "north into the Center's door", + walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); inside = inside_now(); } @@ -536,7 +537,7 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ // and heal; ends at the counter facing her. The door is straight south of // her (by hand, 2026-10-31: north from the door stopped at the counter). void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context){ - if (walk_to_talk(stream, context, 0, 1, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ + if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ fail(stream, "Oreburgh Pokemon Center: the nurse did not answer."); } // A throughout: Yes to "rest your Pokemon?" (B would answer No). @@ -552,7 +553,7 @@ void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context // below it, facing south, on the door's column. Stops as soon as the floor // is gone: walking on runs down the column to the mine. void leave_center_onto_door_row(VideoStream& stream, ProControllerContext& context){ - walk_until(stream, context, 0, -1, 4000ms, "out of the Center's door", + walk_until(stream, context, run_from_wild_battle, 0, -1, 4000ms, "out of the Center's door", [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent < 30; }); context.wait_for(1500ms); } @@ -643,10 +644,10 @@ void oreburgh_to_roark(VideoStream& stream, ProControllerContext& context){ // him (on the console, r97). Then south to the bottom wall and two tiles // west puts us below him. const Pokemon::TilePos roark = world(OreburghMine::ROARK); - at = walk_route(stream, context, maps, at, + at = walk_route(stream, context, run_from_wild_battle, maps, at, [&](const Pokemon::TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }, no_trainers, in_the_way, "To the stretch east of Roark"); - walk_to_wall(stream, context, 0, 1, 2000ms, "north to the wall on Roark's row"); + walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); // His rock and the bottom wall end these legs (see walk_to_wall()). The // two taps west from there land within a third of a tile or so of his // column (s03e2 missed him by that much), so look for him with nudges @@ -658,27 +659,27 @@ void oreburgh_to_roark(VideoStream& stream, ProControllerContext& context){ for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ if (attempt > 0){ // The battle may leave us on his row or the bottom one. - walk_to_wall(stream, context, 0, -1, 2000ms, "south to the bottom row"); - walk_to_wall(stream, context, 1, 0, 6000ms, "east along the bottom row"); + walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom row"); + walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 6000ms, "east along the bottom row"); route_turn(stream, context, Direction::WEST); for (size_t tap = 0; tap < 3; tap++){ pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); } - walk_to_wall(stream, context, 0, 1, 2000ms, "north to the wall on Roark's row"); + walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); } // A wild battle on the way eats into the walk's time (r100): walk again. for (size_t walk = 0; walk < 3; walk++){ - if (walk_to_wall(stream, context, -1, 0, 6000ms, "west to the rock beside Roark").wild_battles == 0){ + if (walk_to_wall(stream, context, run_from_wild_battle, -1, 0, 6000ms, "west to the rock beside Roark").wild_battles == 0){ break; } } - walk_to_wall(stream, context, 0, -1, 2000ms, "south to the bottom wall"); + walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom wall"); route_turn(stream, context, Direction::WEST); pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); route_turn(stream, context, Direction::NORTH); context.wait_for_all_requests(); - if (run_from_late_encounter(stream, context, "below Roark")){ + if (run_from_late_encounter(stream, context, run_from_wild_battle, "below Roark")){ continue; } if (VideoSnapshot screen = stream.video().snapshot()){ @@ -691,7 +692,7 @@ void oreburgh_to_roark(VideoStream& stream, ProControllerContext& context){ // His rocks answer A too ("It's a rugged rock..."): his talk is the // one that zooms the camera in on us (by hand), which the rock's // message does not. Compare the view above the dialogue box. - talked = talk_with_nudges(stream, context, Direction::NORTH, "Roark", 8000ms, + talked = talk_with_nudges(stream, context, run_from_wild_battle, Direction::NORTH, "Roark", 8000ms, [&](const ImageViewRGB32& before, const ImageViewRGB32& after){ const ImageFloatBox above_box{0.0, 0.0, 1.0, 0.6}; const double change = walk_screen_change( @@ -732,7 +733,7 @@ void oreburgh_to_roark(VideoStream& stream, ProControllerContext& context){ // no wall stops us a row short of it, so the route ends on the corner // at (14,10), where that leg started, and the walk does the rest. const Pokemon::TilePos exit = world(OreburghMine::CORRIDOR_EXIT_START); - at = walk_route(stream, context, maps, at, + at = walk_route(stream, context, run_from_wild_battle, maps, at, [&](const Pokemon::TilePos& pos){ return pos == exit; }, no_trainers, in_the_way, "Back to the corridor's top"); // Up into the entrance hall (a transition, as on the way in), then on @@ -754,7 +755,7 @@ void oreburgh_to_roark(VideoStream& stream, ProControllerContext& context){ // of the mine we stand below its sign; a tile east is the Center door's // column, and north on it goes in, or stops on the front rows. hold_tiles(stream, context, Direction::EAST, 1); - walk_until(stream, context, 0, 1, 12000ms, "north into the Center's door", + walk_until(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into the Center's door", [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); size_t floor = 0; if (VideoSnapshot frame = stream.video().snapshot()){ @@ -929,7 +930,7 @@ bool through_door_north(VideoStream& stream, ProControllerContext& context, cons }; context.wait_for_all_requests(); const VideoSnapshot before = stream.video().snapshot(); - const WalkResult up = walk_for(stream, context, 0, 1, 240ms, label); + const WalkResult up = walk_for(stream, context, run_from_wild_battle, 0, 1, 240ms, label); // Look for the fade ourselves: the library's black screen check missed it // under the gym's door (r209d), while its frames read 7 against about // 100 outside. @@ -944,7 +945,7 @@ bool through_door_north(VideoStream& stream, ProControllerContext& context, cons // through (r213d: view change 160, no fade): step on north. if (moved > WALK_SCENE_CHANGE){ stream.log("[Route] " + label + ": the door opened (view change " + std::to_string(moved) + "); on north."); - walk_for(stream, context, 0, 1, 240ms, label + ": on through"); + walk_for(stream, context, run_from_wild_battle, 0, 1, 240ms, label + ": on through"); if (went_dark(5000ms)){ stream.log("[Route] " + label + ": the screen went dark; through the door."); context.wait_for(2000ms); @@ -956,7 +957,7 @@ bool through_door_north(VideoStream& stream, ProControllerContext& context, cons // (r208d drifted south along the gym's front). stream.log("[Route] " + label + ": no dark screen; not the door (view change " + std::to_string(moved) + ")."); if (moved > WALK_BLOCKED_CHANGE * 2){ - walk_for(stream, context, 0, -1, 240ms, label + ": back down"); + walk_for(stream, context, run_from_wild_battle, 0, -1, 240ms, label + ": back down"); } return false; } @@ -1172,7 +1173,7 @@ void route203_to_jubilife(VideoStream& stream, ProControllerContext& context){ auto fight = [&](const std::string& who){ talk_or_fight(stream, context, "Route 203: " + who); }; - const std::vector at = walk_route(stream, context, route203_maps(), Route203::gate_exit(), + const std::vector at = walk_route(stream, context, run_from_wild_battle, route203_maps(), Route203::gate_exit(), on_route203_west_line, trainers, fight, "Route 203 to row 18"); if (at.size() != 1){ fail(stream, "Route 203: the walk to row 18 did not end on a known tile."); @@ -1242,7 +1243,7 @@ void jubilife_to_ravaged_path(VideoStream& stream, ProControllerContext& context // The street's column 14 carries on as the route's; the location banner // names Route 204 as we cross. A D-pad walk keeps the column; it may // stop a tile or two past the border, which the entry set allows for. - const WalkResult out = walk_until_dpad(stream, context, 0, 1, 8000ms, "north out of Jubilife", location_banner_showing); + const WalkResult out = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 8000ms, "north out of Jubilife", location_banner_showing); if (out.stop != WalkStop::ARRIVED){ fail(stream, "Jubilife: no Route 204 banner walking north up the street."); } @@ -1260,7 +1261,7 @@ void jubilife_to_ravaged_path(VideoStream& stream, ProControllerContext& context { std::vector trainers = Route204South::trainer_sights(); auto fight = [&](const std::string& who){ talk_or_fight(stream, context, who); }; - const std::vector at = walk_route(stream, context, route204_south_maps(), + const std::vector at = walk_route(stream, context, run_from_wild_battle, route204_south_maps(), Route204South::jubilife_entry(), [](const Pokemon::TilePos& pos){ return pos == Route204South::BELOW_DOOR; }, trainers, fight, "Route 204 South to the Ravaged Path", Route204South::tiles_to_avoid(), town_plan_costs()); @@ -1326,14 +1327,14 @@ void ravaged_path(VideoStream& stream, ProControllerContext& context){ }; std::vector at{RavagedPath::ENTRANCE}; const Pokemon::MapSet rocks_in = ravaged_path_maps(); - at = walk_route(stream, context, rocks_in, at, + at = walk_route(stream, context, run_from_wild_battle, rocks_in, at, [&](const Pokemon::TilePos& pos){ const auto& before = RavagedPath::before_rocks(); return std::find(before.begin(), before.end(), pos) != before.end(); }, no_trainers, in_the_way, "To a rock across the corridor"); route_turn(stream, context, Direction::EAST); - if (!talk_with_nudges(stream, context, Direction::EAST, "the rock east of us", 2000ms)){ + if (!talk_with_nudges(stream, context, run_from_wild_battle, Direction::EAST, "the rock east of us", 2000ms)){ fail(stream, "Ravaged Path: nothing answered facing the rock east of us."); } if (!answer_rock_smash_prompt(stream, context, "the rock east of us")){ @@ -1346,7 +1347,7 @@ void ravaged_path(VideoStream& stream, ProControllerContext& context){ smashed.push_back(pos.next(Direction::EAST)); } const Pokemon::MapSet rocks_out = ravaged_path_maps(smashed); - at = walk_route(stream, context, rocks_out, at, + at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, [&](const Pokemon::TilePos& pos){ return pos == RavagedPath::EAST_END; }, no_trainers, in_the_way, "To the corridor's east end"); dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); @@ -1377,14 +1378,14 @@ void route204_to_floaroma(VideoStream& stream, ProControllerContext& context){ talk_or_fight(stream, context, who, false, doubles); }; std::vector at{Route204::CAVE_EXIT}; - at = walk_waypoints(stream, context, route204_north_maps(), at, Route204::floaroma_waypoints(), + at = walk_waypoints(stream, context, run_from_wild_battle, route204_north_maps(), at, Route204::floaroma_waypoints(), trainers, fight, "To the path into Floaroma"); if (!std::all_of(at.begin(), at.end(), Route204::in_floaroma_path)){ fail(stream, "Route 204: the walk did not end in the path into Floaroma."); } // North until the town's banner shows: the checkpoint ends right // there, where the save goes (r247a walked 20 s on and overshot). - const WalkResult in = walk_until_dpad(stream, context, 0, 1, 12000ms, "north into Floaroma", location_banner_showing); + const WalkResult in = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into Floaroma", location_banner_showing); if (in.stop != WalkStop::ARRIVED){ fail(stream, "Route 204: no location banner walking north into Floaroma."); } @@ -1417,13 +1418,13 @@ void floaroma_old_couple(VideoStream& stream, ProControllerContext& context){ step(1); { - const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, 8, "west 8 to the flower field"); + const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 8, "west 8 to the flower field"); if (west.stop != WalkStop::LIMIT || west.moved_chunks < 7){ fail(stream, "Floaroma: west 8 to the flower field moved " + std::to_string(west.moved_chunks) + " tiles."); } // Blocked, or out of taps with the last ones bumping: either way // she is in front of us (r250a: 3 steps, 2 bumps, "limit"). - const WalkResult north = walk_tiles_dpad(stream, context, 0, 1, 5, "north until the old woman"); + const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 5, "north until the old woman"); if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 5){ fail(stream, "Floaroma: nothing stopped the walk north to the old woman."); } @@ -1445,11 +1446,11 @@ void floaroma_old_couple(VideoStream& stream, ProControllerContext& context){ step(3); // One tile west, then a tap north: the old man stands there, so the // tap only turns. - const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, 1, "west 1 below the old man"); + const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 1, "west 1 below the old man"); if (west.moved_chunks < 1){ fail(stream, "Floaroma: the step west below the old man did not move."); } - const WalkResult north = walk_tiles_dpad(stream, context, 0, 1, 3, "north until the old man"); + const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 3, "north until the old man"); if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 3){ fail(stream, "Floaroma: nothing stopped the walk north to the old man."); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp index efd43585ea..fef4620ab1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -13,6 +13,7 @@ #include "PokemonBDSP_AutoStory_PokemonCenter.h" #include "PokemonBDSP_AutoStory_Segment_00_tools.h" #include "PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" #include "PokemonBDSP_AutoStory_StoryExecutor.h" namespace PokemonAutomation{ @@ -115,7 +116,7 @@ Outcome ConsoleStoryExecutor::route(RunState& state, const Pokemon::StoryStep& s const std::vector goal = Pokemon::tiles_of(*step.target); StoryConsole& c = console(); const std::vector at = walk_route( - c.env.console, c.context, m_maps, from, + c.env.console, c.context, run_from_wild_battle, m_maps, from, [&](const TilePos& pos){ return std::find(goal.begin(), goal.end(), pos) != goal.end(); }, m_trainers, fight(c), step.name, {}, *costs ); @@ -134,7 +135,7 @@ Outcome ConsoleStoryExecutor::legs(RunState& state, const Pokemon::StoryStep& st route_turn(c.env.console, c.context, leg.direction); const auto [x, y] = dpad_of(leg.direction); const size_t tiles = (size_t)leg.tiles; - const WalkResult walk = walk_tiles_dpad(c.env.console, c.context, x, y, tiles, step.name); + const WalkResult walk = walk_tiles_dpad(c.env.console, c.context, run_from_wild_battle, x, y, tiles, step.name); c.env.console.log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp index 7b425518b2..a8ba839234 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp @@ -222,6 +222,10 @@ void escape_wild_battle(VideoStream& stream, stream); } +void run_from_wild_battle(VideoStream& stream, ProControllerContext& context){ + escape_wild_battle(stream, context, BattleType::STANDARD); +} + void Barry_1_fight(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context){ stream.log("Starting Barry 1 battle..."); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h index 0a5d2378f8..80fad9bb8c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h @@ -21,6 +21,11 @@ void escape_wild_battle(VideoStream& stream, BattleType battle_type = BattleType::STANDARD, std::chrono::seconds timeout = std::chrono::seconds(30)); +// escape_wild_battle() for a standard wild battle, as the walker takes it +// (WildBattleEscape): how the story's walks leave the battles that +// interrupt them. +void run_from_wild_battle(VideoStream& stream, ProControllerContext& context); + // Shared legacy first-move battle routine; consolidation remains preliminary. void Barry_1_fight(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context); } From cfd3d2c06553f4c496681fa52831bbdf9f87b6c8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 13:54:12 -0800 Subject: [PATCH 05/98] BDSP AutoStory: the story as a table of story steps, with events for the scripted parts The program, segments 00 and 01, rewritten as FRLG's AutoStory is: one table of checkpoints (00.00 to 01.11) on the shared story steps, run by the shared engine with the start and end dropdowns and AutoStoryStats. - 00.01 to 00.04 are planned walks on the JSON maps and the five interiors, stepping onto the decomp's scene triggers; they replace the recorded stick holds and the landmark alignment (OverworldMatchers), and have not run on a console yet. - 00.04b to 00.04d keep their rows; 00.05's Jubilife beats, measured on the console, are rows of walks to walls and named events. - Segment 01's walks are events split at their old steps, unchanged. - Actions, Bag, Mart, Center, Battle, WildBattle and Settings hold what the steps use of AutoStoryTools and the segment tools, which are gone, with BDSPData, BattleOpponentNameReader, Dialog and Navigation. Tests: the table lints and every variant names an event; the simulation runs 00.00 to Floaroma in English and Japanese, each start pose chained and every route planned; the intro and screenshot tests moved by topic. Adapted in part from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_BattleOpponentNameReader.cpp | 75 - .../PokemonBDSP_BattleOpponentNameReader.h | 44 - .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 10 +- .../Data/PokemonBDSP_AutoStory_BDSPData.cpp | 250 -- .../Data/PokemonBDSP_AutoStory_BDSPData.h | 104 - .../AutoStory/PokemonBDSP_AutoStory.cpp | 901 +--- .../AutoStory/PokemonBDSP_AutoStory.h | 155 +- .../AutoStory/PokemonBDSP_AutoStoryTools.cpp | 2282 ---------- .../AutoStory/PokemonBDSP_AutoStoryTools.h | 538 --- .../PokemonBDSP_AutoStory_Actions.cpp | 340 ++ .../AutoStory/PokemonBDSP_AutoStory_Actions.h | 63 + .../PokemonBDSP_AutoStory_Actions_Tests.cpp | 271 ++ .../PokemonBDSP_AutoStory_Actions_Tests.h | 25 + .../AutoStory/PokemonBDSP_AutoStory_Bag.cpp | 403 ++ .../AutoStory/PokemonBDSP_AutoStory_Bag.h | 91 + .../PokemonBDSP_AutoStory_Battle.cpp | 602 +++ .../AutoStory/PokemonBDSP_AutoStory_Battle.h | 55 + ...r.cpp => PokemonBDSP_AutoStory_Center.cpp} | 12 +- ...enter.h => PokemonBDSP_AutoStory_Center.h} | 12 +- .../PokemonBDSP_AutoStory_Events.cpp | 40 + .../AutoStory/PokemonBDSP_AutoStory_Events.h | 57 + .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 447 ++ .../PokemonBDSP_AutoStory_Events_Lake.cpp | 155 + .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 492 +++ .../PokemonBDSP_AutoStory_Events_Route203.cpp | 231 + .../PokemonBDSP_AutoStory_Events_Route204.cpp | 242 ++ .../PokemonBDSP_AutoStory_Events_Sandgem.cpp | 345 ++ .../PokemonBDSP_AutoStory_Indoors_Tests.cpp | 110 - .../PokemonBDSP_AutoStory_Indoors_Tests.h | 26 - .../AutoStory/PokemonBDSP_AutoStory_Mart.cpp | 303 ++ .../AutoStory/PokemonBDSP_AutoStory_Mart.h | 61 + .../PokemonBDSP_AutoStory_NewGame.cpp | 0 .../PokemonBDSP_AutoStory_NewGame.h | 0 .../PokemonBDSP_AutoStory_Settings.cpp | 444 ++ .../PokemonBDSP_AutoStory_Settings.h | 31 + .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 342 ++ .../PokemonBDSP_AutoStory_StoryExecutor.h | 178 + .../PokemonBDSP_AutoStory_StoryPlaces.cpp | 180 + .../PokemonBDSP_AutoStory_StoryPlaces.h | 55 + ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 967 ++--- .../PokemonBDSP_AutoStory_StoryTable.cpp | 670 +-- .../PokemonBDSP_AutoStory_StoryTable.h | 75 +- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 213 + .../PokemonBDSP_AutoStory_StoryTable_Tests.h | 25 + ...okemonBDSP_AutoStory_StoryTables_Tests.cpp | 141 - .../PokemonBDSP_AutoStory_StoryTables_Tests.h | 27 - .../AutoStory/PokemonBDSP_AutoStory_Talks.cpp | 319 ++ .../AutoStory/PokemonBDSP_AutoStory_Talks.h | 92 + .../AutoStory/PokemonBDSP_AutoStory_Tests.cpp | 2001 --------- .../AutoStory/PokemonBDSP_AutoStory_Tests.h | 26 - .../AutoStory/PokemonBDSP_AutoStory_Walks.cpp | 319 ++ .../AutoStory/PokemonBDSP_AutoStory_Walks.h | 96 + .../PokemonBDSP_AutoStory_WildBattle.cpp | 444 ++ .../PokemonBDSP_AutoStory_WildBattle.h | 56 + ...emonBDSP_AutoStory_SandgemCenterEvents.cpp | 1485 ------- ...okemonBDSP_AutoStory_SandgemCenterEvents.h | 344 -- .../PokemonBDSP_AutoStory_Segment_00.cpp | 1387 ------- .../PokemonBDSP_AutoStory_Segment_00.h | 170 - .../PokemonBDSP_AutoStory_Segment_01.cpp | 1656 -------- .../PokemonBDSP_AutoStory_Segment_01.h | 189 - .../Utils/PokemonBDSP_AutoStory_Battle.cpp | 1024 ----- .../Utils/PokemonBDSP_AutoStory_Battle.h | 111 - .../Utils/PokemonBDSP_AutoStory_Dialog.cpp | 44 - .../Utils/PokemonBDSP_AutoStory_Dialog.h | 61 - .../PokemonBDSP_AutoStory_Navigation.cpp | 57 - .../Utils/PokemonBDSP_AutoStory_Navigation.h | 78 - ...PokemonBDSP_AutoStory_Segment_00_tools.cpp | 2082 ---------- .../PokemonBDSP_AutoStory_Segment_00_tools.h | 161 - .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 203 - .../PokemonBDSP_AutoStory_StoryExecutor.h | 123 - .../PokemonBDSP_AutoStory_StoryTables.cpp | 191 - .../Utils/PokemonBDSP_AutoStory_StoryTables.h | 119 - ...PokemonBDSP_Autostory_Segment_01_tools.cpp | 271 -- .../PokemonBDSP_Autostory_Segment_01_tools.h | 33 - .../Utils/PokemonBDSP_OverworldMatchers.cpp | 3697 ----------------- .../Utils/PokemonBDSP_OverworldMatchers.h | 101 - SerialPrograms/cmake/SourceFiles.cmake | 85 +- 77 files changed, 8226 insertions(+), 20889 deletions(-) delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.h rename SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/{Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp => PokemonBDSP_AutoStory_Center.cpp} (98%) rename SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/{Utils/PokemonBDSP_AutoStory_PokemonCenter.h => PokemonBDSP_AutoStory_Center.h} (91%) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h rename SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/{Utils => }/PokemonBDSP_AutoStory_NewGame.cpp (100%) rename SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/{Utils => }/PokemonBDSP_AutoStory_NewGame.h (100%) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp deleted file mode 100644 index 5934464203..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* BDSP Battle Opponent Name Reader - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/Tools/VideoStream.h" -#include "CommonTools/OCR/OCR_Routines.h" -#include "Pokemon/Inference/Pokemon_NameReader.h" -#include "PokemonBDSP_BattleOpponentNameReader.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace Pokemon; - - -// ⚠ These regions are estimated from BDSP's typical battle layout. -// Calibrate against a real capture if OCR reads are consistently wrong. -// -// In BDSP singles, the one opponent's info box is in the top-right quadrant. -// In BDSP doubles, two boxes sit side-by-side in the top half of the screen. -static const ImageFloatBox BOX_SINGLES_OPP (0.505, 0.010, 0.420, 0.085); -static const ImageFloatBox BOX_DOUBLES_LEFT (0.505, 0.010, 0.200, 0.085); -static const ImageFloatBox BOX_DOUBLES_RIGHT (0.710, 0.010, 0.260, 0.085); - - -static std::string ocr_name_box( - Logger& logger, - Language language, - const ImageViewRGB32& frame, - const ImageFloatBox& box -){ - ImageViewRGB32 cropped = extract_box_reference(frame, box); - OCR::StringMatchResult result = PokemonNameReader::instance().read_substring( - logger, - language, - cropped, - OCR::BLACK_OR_WHITE_TEXT_FILTERS() - ); - if (result.results.empty()){ - return ""; - } - // Use the best match (lowest log10p) if it meets the confidence threshold - double log10p = result.results.begin()->first; - if (log10p > PokemonNameReader::MAX_LOG10P){ - return ""; - } - return result.results.begin()->second.token; -} - - -std::vector read_battle_opponent_names( - Logger& logger, - Language language, - const ImageViewRGB32& frame, - BDSPBattleType battle_type -){ - std::vector names; - if (battle_type == BDSPBattleType::SINGLES){ - names.push_back(ocr_name_box(logger, language, frame, BOX_SINGLES_OPP)); - } else { - names.push_back(ocr_name_box(logger, language, frame, BOX_DOUBLES_LEFT)); - names.push_back(ocr_name_box(logger, language, frame, BOX_DOUBLES_RIGHT)); - } - return names; -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h b/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h deleted file mode 100644 index b1592c8705..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h +++ /dev/null @@ -1,44 +0,0 @@ -/* BDSP Battle Opponent Name Reader - * - * From: https://github.com/PokemonAutomation/ - * - * OCR-based detection of opponent pokemon names from the BDSP battle screen. - * Used by the outside-help battle handler to track which pokemon are currently - * on the field and update move selection accordingly. - * - * ⚠ Screen region coordinates are estimates and may require calibration - * against a real BDSP capture before they work reliably in production. - */ - -#ifndef PokemonAutomation_PokemonBDSP_BattleOpponentNameReader_H -#define PokemonAutomation_PokemonBDSP_BattleOpponentNameReader_H - -#include -#include -#include "CommonFramework/Language.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h" - -namespace PokemonAutomation{ - class Logger; - class ImageViewRGB32; -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Read the opponent pokemon name boxes at the top of the BDSP battle screen. -// -// Returns one slug per opponent slot (up to 1 for singles, 2 for doubles). -// An empty string in a slot means OCR confidence was too low to identify the pokemon. -// The caller should keep the previous state for any slot that returns empty string. -std::vector read_battle_opponent_names( - Logger& logger, - Language language, - const ImageViewRGB32& frame, - BDSPBattleType battle_type -); - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 5d7351999c..1b442bd1a6 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -10,10 +10,9 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.h" -#include "Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h" -#include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" -#include "Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h" #include "PokemonBDSP_Tests.h" @@ -28,11 +27,10 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_StoryWorld(database); add_tests_AutoStory_RoutePlanner(database); add_tests_AutoStory_StoryMaps(database); - add_tests_AutoStory(database); + add_tests_AutoStory_Actions(database); add_tests_AutoStory_Segment00(database); add_tests_AutoStory_StorySimulation(database); - add_tests_AutoStory_StoryTables(database); - add_tests_AutoStory_Indoors(database); + add_tests_AutoStory_StoryTable(database); add_tests_AutoStory_Walk(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp deleted file mode 100644 index f72ee6f780..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/* BDSP AutoStory - Game Data - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include - -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/Json/JsonArray.h" -#include "Common/Cpp/Json/JsonObject.h" -#include "Common/Cpp/Json/JsonValue.h" - -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/Globals.h" - -#include "PokemonBDSP_AutoStory_BDSPData.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - - -static const std::string RESOURCE_DIR = "PokemonBDSP/AutoStory/"; - - -// --------------------------------------------------------------------------- -// Shared parsing helpers -// --------------------------------------------------------------------------- - -static PokemonType parse_type(const std::string& slug, const std::string& path){ - static const std::map TABLE{ - {"normal", PokemonType::NORMAL}, - {"fire", PokemonType::FIRE}, - {"fighting", PokemonType::FIGHTING}, - {"water", PokemonType::WATER}, - {"flying", PokemonType::FLYING}, - {"grass", PokemonType::GRASS}, - {"poison", PokemonType::POISON}, - {"electric", PokemonType::ELECTRIC}, - {"ground", PokemonType::GROUND}, - {"psychic", PokemonType::PSYCHIC}, - {"rock", PokemonType::ROCK}, - {"ice", PokemonType::ICE}, - {"bug", PokemonType::BUG}, - {"dragon", PokemonType::DRAGON}, - {"ghost", PokemonType::GHOST}, - {"dark", PokemonType::DARK}, - {"steel", PokemonType::STEEL}, - {"fairy", PokemonType::FAIRY}, - }; - auto iter = TABLE.find(slug); - if (iter == TABLE.end()){ - throw FileException(nullptr, PA_CURRENT_FUNCTION, "Unknown type: " + slug, path); - } - return iter->second; -} - -static MoveCategory parse_category(const std::string& slug, const std::string& path){ - if (slug == "physical"){ - return MoveCategory::PHYSICAL; - } - if (slug == "special"){ - return MoveCategory::SPECIAL; - } - if (slug == "status"){ - return MoveCategory::STATUS; - } - throw FileException(nullptr, PA_CURRENT_FUNCTION, "Unknown move category: " + slug, path); -} - - -// --------------------------------------------------------------------------- -// Move database -// --------------------------------------------------------------------------- - -struct BDSPMoveDatabase { - std::map data; - - static const BDSPMoveDatabase& instance(){ - static BDSPMoveDatabase db; - return db; - } - - BDSPMoveDatabase(){ - std::string path = RESOURCE_PATH() + RESOURCE_DIR + "bdsp_move_data.json"; - JsonValue json = load_json_file(path); - JsonObject& root = json.to_object_throw(path); - - for (auto& item : root){ - const std::string& slug = item.first; - JsonObject& obj = item.second.to_object_throw(path); - - BDSPMoveData& move = data[slug]; - move.category = parse_category(obj.get_string_throw("category", path), path); - move.type = parse_type(obj.get_string_throw("type", path), path); - move.base_power = (uint8_t)obj.get_integer_throw("base_power", path); - move.pp = (uint8_t)obj.get_integer_throw("pp", path); - move.spread = obj.get_boolean_throw("spread", path); - move.accuracy = (uint8_t)obj.get_integer_default("accuracy", 100); - } - } -}; - -const BDSPMoveData& bdsp_move(const std::string& slug){ - const auto& db = BDSPMoveDatabase::instance().data; - auto iter = db.find(slug); - if (iter == db.end()){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown BDSP move slug: " + slug); - } - return iter->second; -} - - -// --------------------------------------------------------------------------- -// Pokemon database -// --------------------------------------------------------------------------- - -struct BDSPPokemonDatabase { - std::map data; - - static const BDSPPokemonDatabase& instance(){ - static BDSPPokemonDatabase db; - return db; - } - - BDSPPokemonDatabase(){ - std::string path = RESOURCE_PATH() + RESOURCE_DIR + "bdsp_pokemon_data.json"; - JsonValue json = load_json_file(path); - JsonObject& root = json.to_object_throw(path); - - for (auto& item : root){ - const std::string& slug = item.first; - JsonObject& obj = item.second.to_object_throw(path); - - BDSPPokemonData& mon = data[slug]; - mon.type[0] = PokemonType::NONE; - mon.type[1] = PokemonType::NONE; - { - JsonArray& type_arr = obj.get_array_throw("type", path); - if (type_arr.size() >= 1){ - mon.type[0] = parse_type(type_arr[0].to_string_throw(path), path); - } - if (type_arr.size() >= 2){ - mon.type[1] = parse_type(type_arr[1].to_string_throw(path), path); - } - } - { - JsonObject& stats = obj.get_object_throw("base_stats", path); - mon.hp = (uint8_t)stats.get_integer_throw("hp", path); - mon.attack = (uint8_t)stats.get_integer_throw("attack", path); - mon.defense = (uint8_t)stats.get_integer_throw("defense", path); - mon.spatk = (uint8_t)stats.get_integer_throw("spatk", path); - mon.spdef = (uint8_t)stats.get_integer_throw("spdef", path); - mon.speed = (uint8_t)stats.get_integer_throw("speed", path); - } - } - } -}; - -const BDSPPokemonData& bdsp_pokemon(const std::string& slug){ - const auto& db = BDSPPokemonDatabase::instance().data; - auto iter = db.find(slug); - if (iter == db.end()){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown BDSP pokemon slug: " + slug); - } - return iter->second; -} - - -// --------------------------------------------------------------------------- -// Trainer database -// --------------------------------------------------------------------------- - -struct BDSPTrainerDatabase { - std::map data; - - static const BDSPTrainerDatabase& instance(){ - static BDSPTrainerDatabase db; - return db; - } - - BDSPTrainerDatabase(){ - std::string path = RESOURCE_PATH() + RESOURCE_DIR + "trainers.json"; - JsonValue json = load_json_file(path); - JsonObject& root = json.to_object_throw(path); - - for (auto& item : root){ - const std::string& battle_id = item.first; - JsonObject& obj = item.second.to_object_throw(path); - - BDSPTrainerData& battle = data[battle_id]; - { - const std::string& type_str = obj.get_string_throw("battle_type", path); - if (type_str == "2v2"){ - battle.battle_type = BDSPBattleType::DOUBLES; - }else{ - battle.battle_type = BDSPBattleType::SINGLES; - } - } - { - JsonArray& trainers_arr = obj.get_array_throw("trainers", path); - for (auto& t_val : trainers_arr){ - JsonObject& t_obj = t_val.to_object_throw(path); - BDSPTrainer trainer; - JsonArray& pkmn_arr = t_obj.get_array_throw("pokemon", path); - for (auto& p_val : pkmn_arr){ - JsonObject& p_obj = p_val.to_object_throw(path); - BDSPTrainerPokemon tp; - tp.species = p_obj.get_string_throw("species", path); - tp.level = (uint8_t)p_obj.get_integer_throw("level", path); - - auto parse_stat_set = [&](BDSPStatSet& set, const char* key){ - JsonObject* s = p_obj.get_object(key); - if (!s){ - return; - } - set.hp = (uint8_t)s->get_integer_default("hp", 0); - set.attack = (uint8_t)s->get_integer_default("attack", 0); - set.defense = (uint8_t)s->get_integer_default("defense", 0); - set.spatk = (uint8_t)s->get_integer_default("spatk", 0); - set.spdef = (uint8_t)s->get_integer_default("spdef", 0); - set.speed = (uint8_t)s->get_integer_default("speed", 0); - }; - parse_stat_set(tp.ivs, "ivs"); - parse_stat_set(tp.evs, "evs"); - tp.has_intimidate = p_obj.get_boolean_default("intimidate", false); - - trainer.pokemon.push_back(std::move(tp)); - } - battle.trainers.push_back(std::move(trainer)); - } - } - } - } -}; - -const BDSPTrainerData& bdsp_trainer(const std::string& battle_id){ - const auto& db = BDSPTrainerDatabase::instance().data; - auto iter = db.find(battle_id); - if (iter == db.end()){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown BDSP battle ID: " + battle_id); - } - return iter->second; -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h deleted file mode 100644 index a8a6bce913..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h +++ /dev/null @@ -1,104 +0,0 @@ -/* BDSP AutoStory - Game Data - * - * From: https://github.com/PokemonAutomation/ - * - * Provides access to move metadata, pokemon base stats, and the trainer - * database used by the outside-help battle handler. - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_BDSPData_H -#define PokemonAutomation_PokemonBDSP_AutoStory_BDSPData_H - -#include -#include - -#include "Pokemon/Pokemon_Types.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - -using namespace Pokemon; - - -// --------------------------------------------------------------------------- -// Move data -// --------------------------------------------------------------------------- - -struct BDSPMoveData { - MoveCategory category; - PokemonType type; - uint8_t base_power; - uint8_t pp; - bool spread; - uint8_t accuracy; // 0 = never-miss (e.g. Swift); otherwise 1-100 -}; - -// Returns move data for the given slug. -// Throws InternalProgramError if the slug is not found. -const BDSPMoveData& bdsp_move(const std::string& slug); - - -// --------------------------------------------------------------------------- -// Pokemon base stats -// --------------------------------------------------------------------------- - -struct BDSPPokemonData { - PokemonType type[2]; // type[1] == NONE for mono-type pokemon - uint8_t hp; - uint8_t attack; - uint8_t defense; - uint8_t spatk; - uint8_t spdef; - uint8_t speed; -}; - -// Returns base stats for the given species slug (e.g. "beautifly"). -// Throws InternalProgramError if the slug is not found. -const BDSPPokemonData& bdsp_pokemon(const std::string& slug); - - -// --------------------------------------------------------------------------- -// Trainer database -// --------------------------------------------------------------------------- - -enum class BDSPBattleType { - SINGLES, - DOUBLES, -}; - -struct BDSPStatSet { - uint8_t hp = 0; - uint8_t attack = 0; - uint8_t defense = 0; - uint8_t spatk = 0; - uint8_t spdef = 0; - uint8_t speed = 0; -}; - -struct BDSPTrainerPokemon { - std::string species; - uint8_t level; - BDSPStatSet ivs; - BDSPStatSet evs; - bool has_intimidate = false; -}; - -struct BDSPTrainer { - std::vector pokemon; -}; - -struct BDSPTrainerData { - BDSPBattleType battle_type; - std::vector trainers; -}; - -// Returns trainer data for the given battle ID (e.g. "Veilstone_GalacticGrunts_2v2"). -// Throws InternalProgramError if the ID is not found. -const BDSPTrainerData& bdsp_trainer(const std::string& battle_id); - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index ba6801157d..6fede05b43 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -1,713 +1,188 @@ -#include -#include - -#include "PokemonBDSP_AutoStory.h" -#include "Utils/PokemonBDSP_AutoStory_Location.h" -#include "Utils/PokemonBDSP_AutoStory_NewGame.h" - -#include "CommonFramework/Globals.h" -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/Notifications/ProgramNotifications.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/ImageMatchDetector.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" - -#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" -#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" -#include "StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h" - -#include "CommonTools/Options/StringSelectOption.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; - - -// --------------------------------------------------------------------------- -// Segment list -// --------------------------------------------------------------------------- - -static std::vector> make_segment_list(){ - std::vector> segment_list; - segment_list.emplace_back(std::make_unique()); - segment_list.emplace_back(std::make_unique()); - return segment_list; -} - -static const std::vector>& ALL_SEGMENTS(){ - static std::vector> list = make_segment_list(); - return list; -} -static std::vector> make_checkpoint_list(){ - std::vector> checkpoint_list; - - //Segment 00 - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - - //Segment 01 - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - checkpoint_list.emplace_back(std::make_unique()); - - return checkpoint_list; -} - -static const std::vector>& ALL_CHECKPOINTS(){ - static std::vector> list = make_checkpoint_list(); - return list; -} - -// --------------------------------------------------------------------------- -// Segment database -// --------------------------------------------------------------------------- - -static const StringSelectDatabase& section_database(bool checkpoints){ - static const StringSelectDatabase segments = []{ - StringSelectDatabase db; - for (size_t i = 0; i < ALL_SEGMENTS().size(); i++) - db.add_entry(StringSelectEntry(std::to_string(i), ALL_SEGMENTS()[i]->name())); - return db; - }(); - static const StringSelectDatabase checkpoint_db = []{ - StringSelectDatabase db; - for (size_t i = 0; i < ALL_CHECKPOINTS().size(); i++) - db.add_entry(StringSelectEntry(std::to_string(i), ALL_CHECKPOINTS()[i]->name())); - return db; - }(); - return checkpoints ? checkpoint_db : segments; -} - -// --------------------------------------------------------------------------- -// Descriptor -// --------------------------------------------------------------------------- - -BDSPAutoStory_Descriptor::BDSPAutoStory_Descriptor() - : SingleSwitchProgramDescriptor( - "PokemonBDSP:AutoStory", - "Pokemon BDSP", - "Auto Story", - "Programs/PokemonBDSP/AutoStory.html", - "Automatically completes BDSP story segments.", - PanelQualityStanding::OFFICIAL_BETA, - ProgramControllerClass::StandardController_NoRestrictions, - FeedbackType::REQUIRED, - AllowCommandsWhenRunning::DISABLE_COMMANDS, - {"Tesseract"} - ) -{} - - - -std::unique_ptr -BDSPAutoStory_Descriptor::make_stats() const{ - return std::make_unique(); -} - - -// --------------------------------------------------------------------------- -// Program Instance -// --------------------------------------------------------------------------- - -BDSPAutoStory::~BDSPAutoStory(){ - OUTSIDE_HELP_POKEMON.remove_listener(*this); - HELP_MODE.remove_listener(*this); - START_CHECKPOINT_TUTORIAL.remove_listener(*this); - END_CHECKPOINT_TUTORIAL.remove_listener(*this); - START_SEGMENT.remove_listener(*this); - END_SEGMENT.remove_listener(*this); - ENABLE_ADVANCED_MODE.remove_listener(*this); - - -} - - -// --------------------------------------------------------------------------- -// Constructor -// --------------------------------------------------------------------------- - -BDSPAutoStory::BDSPAutoStory() - : SingleSwitchProgramInstance() - , DIAGNOSTIC_POST_CATCH_ONLY( - "Diagnostic only: Run 00.04 after catch through 00.05
" - "Start at the saved position immediately after catching the second Pokemon. " - "Applies only in advanced Tutorial mode when Start Checkpoint is 00.04. Runs its post-catch part, then 00.05. Other starting checkpoints run normally. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, - false - ) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_FAKE_CHECKPOINT_SAVE( - "Diagnostic only: Fake checkpoint saves
" - "Use the fake save helper instead of normal checkpoint saving. " - "Use an existing in-game save at the selected starting checkpoint or segment. " - "Any failure reloads that save and reruns the entire selected range from its start. " - "Checkpoint saves are not verified in this mode. Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER( - "Diagnostic only: Route 202 to Sandgem's Pokemon Center (events)
" - "Start at Route 202's save spot under the south ledge. Walks into Sandgem Town to the front of the " - "Pokemon Center's door, facing it, and stops there: no heal, no save. Runs only this, whatever the other options say " - "(with the Mart lady option too, runs both in turn). " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_SANDGEM_MART_LADY( - "Diagnostic only: Sandgem's Pokemon Center to the Poke Mart lady and back to Route 202 (events)
" - "Start in front of Sandgem's Pokemon Center door. Reads Route 201's border sign, talks to the Poke Mart lady, reads the sign " - "again, goes back to the Center's door, then to Route 202's save spot under the south ledge and saves there. " - "With the Route 202 option too, runs that first, from its Route 202 save. Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_JUBILIFE_CENTER( - "Diagnostic only: 00.05a Jubilife, steps to the Pokemon Center (events)
" - "Start at 00.05's save below Jubilife's steps. Walks to the Pokemon Center and heals; no save. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_JUBILIFE_SCHOOL( - "Diagnostic only: 00.05b Jubilife, Pokemon Center to the Trainers' School (events)
" - "Start on the Pokemon Center's doorstep (or after 00.05a). Walks into the Trainers' School; no save. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_JUBILIFE_BARRY( - "Diagnostic only: 00.05c Jubilife, Barry in the Trainers' School (events)
" - "Start inside the School at its door (or after 00.05b). Talks to Barry for the Town Map and walks out; no save. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_JUBILIFE_PRESIDENT( - "Diagnostic only: 00.05d Jubilife, the Poketch Company president (events)
" - "Start on the Trainers' School's doorstep (or after 00.05c). Walks into the road until the president talks; no save. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_JUBILIFE_CLOWN_MART( - "Diagnostic only: 00.05e Jubilife, the clown by the Poke Mart (events)
" - "Start where the president leaves us (or after 00.05d). Walks to the clown and gets Coupon 1; no save. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY( - "Diagnostic only: 00.05f Jubilife, the clown by the Poketch Company (events)
" - "Start beside the TV station clown (after his coupon). Walks to the third clown and gets Coupon 3; no save. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - // DIAGNOSTIC ONLY: Remove before pushing to production. - , DIAGNOSTIC_JUBILIFE_POKETCH( - "Diagnostic only: 00.05g Jubilife, the Poketch from the president (events)
" - "Start in the gap beside the Poketch Company's sign, with the three coupons (after 00.05f). " - "Gets the Poketch and goes up to the street east to Route 203; no save. " - "Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, false) - , ENABLE_ADVANCED_MODE( - "Advanced mode:
" - "Use checkpoints instead of segments.", - LockMode::UNLOCK_WHILE_RUNNING, - false - ) - , START_CHECKPOINT_TUTORIAL( - "Start Checkpoint:", - section_database(true), - LockMode::LOCK_WHILE_RUNNING, - "0" - ) - , END_CHECKPOINT_TUTORIAL( - "End Checkpoint:", - section_database(true), - LockMode::UNLOCK_WHILE_RUNNING, - "5" - ) - - , START_DESCRIPTION_SEGMENT("Start:
", "") - , END_DESCRIPTION_SEGMENT("End:
", "") - - , START_DESCRIPTION_CHECKPOINT("Start:
", "") - , END_DESCRIPTION_CHECKPOINT("End:
", "") - - , START_SEGMENT( - "Start Segment:", - section_database(false), - LockMode::LOCK_WHILE_RUNNING, - "0" - ) - - , END_SEGMENT( - "End Segment:", - section_database(false), - LockMode::UNLOCK_WHILE_RUNNING, - "0" - ) - - , NEW_GAME_INTRO( - "Start a new game first:
" - "For a game with no save: restart it and play the intro (language, appearance, names) " - "until the player is in control in the bedroom. Runs before the selected range.", - LockMode::LOCK_WHILE_RUNNING, false) - , PLAYER_NAME(false, "Player name:
For a new game. Letters and digits.", LockMode::LOCK_WHILE_RUNNING, "Kosmos", "Kosmos") - , STARTERCHOICE( - "Starter Pokemon:", - { - {StarterChoice::TURTWIG, "turtwig", "Turtwig"}, - {StarterChoice::CHIMCHAR, "chimchar", "Chimchar"}, - {StarterChoice::PIPLUP, "piplup", "Piplup"}, - }, - LockMode::LOCK_WHILE_RUNNING, - StarterChoice::CHIMCHAR - ) - - , STARTER_SHINY( - "Shiny Starter:", - LockMode::LOCK_WHILE_RUNNING, - false - ) - - , CATCH_LEGENDARY( - "Catch Box Legendary:", - LockMode::LOCK_WHILE_RUNNING, - false - ) - - , HELP_MODE( - "Help Mode:", - { - {HelpMode::NO_OUTSIDE_HELP, "no_outside_help", "No Outside Help"}, - {HelpMode::OUTSIDE_HELP, "outside_help", "Outside Help"}, - }, - LockMode::LOCK_WHILE_RUNNING, - HelpMode::NO_OUTSIDE_HELP - ) - - , OUTSIDE_HELP_POKEMON( - "Pokemon:", - { - {OutsideHelpPokemon::TORTERRA, "torterra", "Torterra"}, - }, - LockMode::LOCK_WHILE_RUNNING, - OutsideHelpPokemon::TORTERRA - ) - - , OUTSIDE_HELP_ATTACK_1( - "Attack 1:", - { - {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, - {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, - {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, - {TorterraMove::CRUNCH, "crunch", "Crunch"}, - {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, - {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, - {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, - {TorterraMove::FACADE, "facade", "Facade"}, - {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, - {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, - {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, - {TorterraMove::STRENGTH, "strength", "Strength"}, - {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, - {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, - }, - LockMode::LOCK_WHILE_RUNNING, - TorterraMove::EARTHQUAKE - ) - - , OUTSIDE_HELP_ATTACK_2( - "Attack 2:", - { - {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, - {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, - {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, - {TorterraMove::CRUNCH, "crunch", "Crunch"}, - {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, - {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, - {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, - {TorterraMove::FACADE, "facade", "Facade"}, - {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, - {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, - {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, - {TorterraMove::STRENGTH, "strength", "Strength"}, - {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, - {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, - }, - LockMode::LOCK_WHILE_RUNNING, - TorterraMove::ROCK_SLIDE - ) - - , OUTSIDE_HELP_ATTACK_3( - "Attack 3:", - { - {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, - {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, - {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, - {TorterraMove::CRUNCH, "crunch", "Crunch"}, - {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, - {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, - {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, - {TorterraMove::FACADE, "facade", "Facade"}, - {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, - {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, - {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, - {TorterraMove::STRENGTH, "strength", "Strength"}, - {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, - {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, - }, - LockMode::LOCK_WHILE_RUNNING, - TorterraMove::WOOD_HAMMER - ) - - , OUTSIDE_HELP_ATTACK_4( - "Attack 4:", - { - {TorterraMove::EARTHQUAKE, "earthquake", "Earthquake"}, - {TorterraMove::WOOD_HAMMER, "wood_hammer", "Wood Hammer"}, - {TorterraMove::RAZOR_LEAF, "razor_leaf", "Razor Leaf"}, - {TorterraMove::CRUNCH, "crunch", "Crunch"}, - {TorterraMove::GIGA_DRAIN, "giga_drain", "Giga Drain"}, - {TorterraMove::IRON_TAIL, "iron_tail", "Iron Tail"}, - {TorterraMove::ROCK_TOMB, "rock_tomb", "Rock Tomb"}, - {TorterraMove::FACADE, "facade", "Facade"}, - {TorterraMove::STONE_EDGE, "stone_edge", "Stone Edge"}, - {TorterraMove::ROCK_SLIDE, "rock_slide", "Rock Slide"}, - {TorterraMove::BULLDOZE, "bulldoze", "Bulldoze"}, - {TorterraMove::STRENGTH, "strength", "Strength"}, - {TorterraMove::ROCK_SMASH, "rock_smash", "Rock Smash"}, - {TorterraMove::ROCK_CLIMB, "rock_climb", "Rock Climb"}, - }, - LockMode::LOCK_WHILE_RUNNING, - TorterraMove::CRUNCH - ) - - , NOTIFICATION_STATUS_UPDATE("Status Update", true, false, std::chrono::seconds(30)) - , NOTIFICATIONS({ - &NOTIFICATION_STATUS_UPDATE, - &NOTIFICATION_PROGRAM_FINISH, - &NOTIFICATION_ERROR_RECOVERABLE, - &NOTIFICATION_ERROR_FATAL, - }) - -{ - // ----------------------------------------------------------------------- - // Options - // ----------------------------------------------------------------------- - - PA_ADD_OPTION(RETRIES); - PA_ADD_OPTION(DIAGNOSTIC_POST_CATCH_ONLY); - PA_ADD_OPTION(DIAGNOSTIC_FAKE_CHECKPOINT_SAVE); - PA_ADD_OPTION(DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER); - PA_ADD_OPTION(DIAGNOSTIC_SANDGEM_MART_LADY); - PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_CENTER); - PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_SCHOOL); - PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_BARRY); - PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_PRESIDENT); - PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_CLOWN_MART); - PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY); - PA_ADD_OPTION(DIAGNOSTIC_JUBILIFE_POKETCH); - PA_ADD_OPTION(ENABLE_ADVANCED_MODE); - - // Segment UI - PA_ADD_OPTION(START_SEGMENT); - PA_ADD_OPTION(START_DESCRIPTION_SEGMENT); - - PA_ADD_OPTION(END_SEGMENT); - PA_ADD_OPTION(END_DESCRIPTION_SEGMENT); - - // Checkpoint UI - PA_ADD_OPTION(START_CHECKPOINT_TUTORIAL); - PA_ADD_OPTION(START_DESCRIPTION_CHECKPOINT); - - PA_ADD_OPTION(END_CHECKPOINT_TUTORIAL); - PA_ADD_OPTION(END_DESCRIPTION_CHECKPOINT); - - - PA_ADD_OPTION(NEW_GAME_INTRO); - PA_ADD_OPTION(PLAYER_NAME); - PA_ADD_OPTION(STARTERCHOICE); - PA_ADD_OPTION(STARTER_SHINY); - PA_ADD_OPTION(CATCH_LEGENDARY); - - PA_ADD_OPTION(HELP_MODE); - PA_ADD_OPTION(OUTSIDE_HELP_POKEMON); - - PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_1); - PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_2); - PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_3); - PA_ADD_OPTION(OUTSIDE_HELP_ATTACK_4); - - PA_ADD_OPTION(NOTIFICATIONS); - - HELP_MODE.add_listener(*this); - OUTSIDE_HELP_POKEMON.add_listener(*this); - START_CHECKPOINT_TUTORIAL.add_listener(*this); - END_CHECKPOINT_TUTORIAL.add_listener(*this); - ENABLE_ADVANCED_MODE.add_listener(*this); - START_SEGMENT.add_listener(*this); - END_SEGMENT.add_listener(*this); - on_config_value_changed(nullptr); -} -void BDSPAutoStory::update_ui_state(){ - const bool advanced = ENABLE_ADVANCED_MODE; - auto visible = [](ConfigOption& option, bool show){ - option.set_visibility(show ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); - }; - visible(START_SEGMENT, !advanced); - visible(END_SEGMENT, !advanced); - visible(START_CHECKPOINT_TUTORIAL, advanced); - visible(END_CHECKPOINT_TUTORIAL, advanced); - visible(START_DESCRIPTION_SEGMENT, !advanced); - visible(END_DESCRIPTION_SEGMENT, !advanced); - visible(START_DESCRIPTION_CHECKPOINT, advanced); - visible(END_DESCRIPTION_CHECKPOINT, advanced); - - bool show_help = ((HelpMode)HELP_MODE == HelpMode::OUTSIDE_HELP); - - OUTSIDE_HELP_POKEMON.set_visibility( - show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN - ); - - OUTSIDE_HELP_ATTACK_1.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); - OUTSIDE_HELP_ATTACK_2.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); - OUTSIDE_HELP_ATTACK_3.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); - OUTSIDE_HELP_ATTACK_4.set_visibility(show_help ? ConfigOptionState::ENABLED : ConfigOptionState::HIDDEN); -} -void BDSPAutoStory::on_config_value_changed(void*){ - if (ENABLE_ADVANCED_MODE){ - const auto& checkpoints = ALL_CHECKPOINTS(); - - size_t start = std::stoul(START_CHECKPOINT_TUTORIAL.slug()); - size_t end = std::stoul(END_CHECKPOINT_TUTORIAL.slug()); - - if (start < checkpoints.size()){ - START_DESCRIPTION_CHECKPOINT.set_text( - "Start: " + checkpoints[start]->start_text() - ); - } - if (end < checkpoints.size()){ - END_DESCRIPTION_CHECKPOINT.set_text( - "End: " + checkpoints[end]->end_text() - ); - } - - }else{ - const auto& segments = ALL_SEGMENTS(); - - size_t start = std::stoul(START_SEGMENT.slug()); - size_t end = std::stoul(END_SEGMENT.slug()); - - if (start < segments.size()){ - START_DESCRIPTION_SEGMENT.set_text( - "Start: " + segments[start]->start_text() - ); - } - if (end < segments.size()){ - END_DESCRIPTION_SEGMENT.set_text( - "End: " + segments[end]->end_text() - ); - } - } - - update_ui_state(); -} -// --------------------------------------------------------------------------- -// Program logic -// --------------------------------------------------------------------------- - -void BDSPAutoStory::program( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context -){ - AutoStoryStats& stats = env.current_stats(); - // The checkpoints not yet in tables still run checkpoint_reattempt_loop(). - g_stop_at_first_failure = RETRIES.STOP_AT_FIRST_FAILURE; - g_story_language = RETRIES.language(); - - AutoStoryOptions options{ - RETRIES.language(), - STARTERCHOICE, - NOTIFICATION_STATUS_UPDATE, - (HelpMode)HELP_MODE, - OutsideHelpConfig{ - (OutsideHelpPokemon)OUTSIDE_HELP_POKEMON, - { - (TorterraMove)OUTSIDE_HELP_ATTACK_1, - (TorterraMove)OUTSIDE_HELP_ATTACK_2, - (TorterraMove)OUTSIDE_HELP_ATTACK_3, - (TorterraMove)OUTSIDE_HELP_ATTACK_4, - } - }, - DIAGNOSTIC_FAKE_CHECKPOINT_SAVE ? CheckpointSaveMode::FAKE : CheckpointSaveMode::REAL - }; - options.policy = RETRIES.policy(); - options.checks = RETRIES.checks(); - - const bool fake_saves = options.checkpoint_save_mode == CheckpointSaveMode::FAKE; - if (fake_saves) options.checkpoint_retry_scope = CheckpointRetryScope::DEFER_TO_SELECTED_RANGE; - // Logs what a checkpoint is for before it runs, and that it finished after. - auto run_checkpoint_logged = [&](size_t index, const AutoStory_Checkpoint& checkpoint){ - stats.m_current = index; - env.update_stats(); - env.log("[AutoStory] Objective: " + checkpoint.name() + ". Ends: " + checkpoint.end_text(), COLOR_BLUE); - add_wrapped_log(env.console.overlay(), "Now: " + checkpoint.name(), COLOR_WHITE); - add_wrapped_log(env.console.overlay(), "Goal: " + checkpoint.end_text(), COLOR_GRAY); - checkpoint.run_checkpoint(env, context, options, stats); - stats.m_completed++; - stats.m_last_completed = index; - env.update_stats(); - env.log("[AutoStory] Completed: " + checkpoint.name() + ".", COLOR_DARKGREEN); - add_wrapped_log(env.console.overlay(), "Done: " + checkpoint.name(), COLOR_GREEN); - }; - auto run_selected_range = [&](size_t attempt){ - if (attempt > 0) env.log("[Fake saves] Reloaded initial save; restarting the entire selected range from its start."); - // DIAGNOSTIC ONLY: Remove this branch before pushing to production. - if (DIAGNOSTIC_JUBILIFE_CENTER || DIAGNOSTIC_JUBILIFE_SCHOOL || DIAGNOSTIC_JUBILIFE_BARRY || DIAGNOSTIC_JUBILIFE_PRESIDENT || DIAGNOSTIC_JUBILIFE_CLOWN_MART || DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY || DIAGNOSTIC_JUBILIFE_POKETCH){ - const std::pair parts[] = { - {DIAGNOSTIC_JUBILIFE_CENTER, JUBILIFE_CENTER_NAME}, - {DIAGNOSTIC_JUBILIFE_SCHOOL, JUBILIFE_SCHOOL_NAME}, - {DIAGNOSTIC_JUBILIFE_BARRY, JUBILIFE_BARRY_NAME}, - {DIAGNOSTIC_JUBILIFE_PRESIDENT, JUBILIFE_PRESIDENT_NAME}, - {DIAGNOSTIC_JUBILIFE_CLOWN_MART, JUBILIFE_CLOWN_MART_NAME}, - {DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY, JUBILIFE_CLOWN_COMPANY_NAME}, - {DIAGNOSTIC_JUBILIFE_POKETCH, JUBILIFE_POKETCH_NAME}, - }; - for (const auto& [on, name] : parts){ - if (!on){ - continue; - } - env.log(std::string("[AutoStory] Objective: ") + name + ".", COLOR_BLUE); - add_wrapped_log(env.console.overlay(), std::string("Now: ") + name, COLOR_WHITE); - if (name == JUBILIFE_CENTER_NAME){ - diagnostic_jubilife_center(env, context); - }else if (name == JUBILIFE_SCHOOL_NAME){ - diagnostic_jubilife_school(env, context); - }else if (name == JUBILIFE_BARRY_NAME){ - diagnostic_jubilife_barry(env, context); - }else if (name == JUBILIFE_PRESIDENT_NAME){ - diagnostic_jubilife_president(env, context); - }else if (name == JUBILIFE_CLOWN_MART_NAME){ - diagnostic_jubilife_clown_mart(env, context); - }else if (name == JUBILIFE_CLOWN_COMPANY_NAME){ - diagnostic_jubilife_clown_company(env, context); - }else{ - diagnostic_jubilife_poketch(env, context); - } - env.log(std::string("[AutoStory] Completed: ") + name + ".", COLOR_DARKGREEN); - add_wrapped_log(env.console.overlay(), std::string("Done: ") + name, COLOR_GREEN); - } - return; - } - // DIAGNOSTIC ONLY: Remove this branch before pushing to production. - if (DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER || DIAGNOSTIC_SANDGEM_MART_LADY){ - if (DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER){ - const std::string name = ROUTE202_TO_SANDGEM_CENTER_NAME; - env.log("[AutoStory] Objective: " + name + ". Ends: in front of Sandgem Town's Pokemon Center door, not healed, not saved.", COLOR_BLUE); - add_wrapped_log(env.console.overlay(), "Now: " + name, COLOR_WHITE); - diagnostic_route202_to_sandgem_center(env, context); - env.log("[AutoStory] Completed: " + name + ".", COLOR_DARKGREEN); - add_wrapped_log(env.console.overlay(), "Done: " + name, COLOR_GREEN); - } - if (DIAGNOSTIC_SANDGEM_MART_LADY){ - const std::string name = SANDGEM_MART_LADY_NAME; - env.log("[AutoStory] Objective: " + name + ". Ends: at Route 202's save spot, saved.", COLOR_BLUE); - add_wrapped_log(env.console.overlay(), "Now: " + name, COLOR_WHITE); - diagnostic_sandgem_mart_lady(env, context); - env.log("[AutoStory] Completed: " + name + ".", COLOR_DARKGREEN); - add_wrapped_log(env.console.overlay(), "Done: " + name, COLOR_GREEN); - } - return; - } - // DIAGNOSTIC ONLY: Remove this branch before pushing to production. - if (DIAGNOSTIC_POST_CATCH_ONLY && ENABLE_ADVANCED_MODE && - START_CHECKPOINT_TUTORIAL.slug() == "4"){ - env.log("Diagnostic only: Running checkpoint 00.04 after catch."); - stats.m_current = 4; - env.update_stats(); - env.log("[AutoStory] Objective: " + AutoStory_Checkpoint_0004().name() + " (after the catch). Ends: " + - AutoStory_Checkpoint_0004().end_text(), COLOR_BLUE); - add_wrapped_log(env.console.overlay(), "Now: " + AutoStory_Checkpoint_0004().name() + " (after the catch)", COLOR_WHITE); - add_wrapped_log(env.console.overlay(), "Goal: " + AutoStory_Checkpoint_0004().end_text(), COLOR_GRAY); - diagnostic_checkpoint_0004_after_catch(env, context, options, stats); - stats.m_completed++; - stats.m_last_completed = 4; - env.update_stats(); - env.log("[AutoStory] Completed: " + AutoStory_Checkpoint_0004().name() + ".", COLOR_DARKGREEN); - env.log("Diagnostic continuation: Running checkpoint 00.05."); - run_checkpoint_logged(5, AutoStory_Checkpoint_0005()); - return; - } - - const auto& segments = ALL_SEGMENTS(); - - if (ENABLE_ADVANCED_MODE){ - size_t start = std::stoul(START_CHECKPOINT_TUTORIAL.slug()); - size_t end = std::stoul(END_CHECKPOINT_TUTORIAL.slug()); - - env.log("Starting AutoStory from checkpoint " + std::to_string(start) + - " to " + std::to_string(end)); - - const auto& checkpoints = ALL_CHECKPOINTS(); - - for (size_t i = start; i <= end && i < checkpoints.size(); i++){ - env.log("Running Checkpoint: " + std::to_string(i)); - run_checkpoint_logged(i, *checkpoints[i]); - } - - }else{ - size_t start = std::stoul(START_SEGMENT.slug()); - size_t end = std::stoul(END_SEGMENT.slug()); - - for (size_t i = start; i <= end && i < segments.size(); i++){ - env.log("Running Segment: " + std::to_string(i)); - segments[i]->run_segment(env, context, options, stats); - } - } - }; - try{ - if (NEW_GAME_INTRO){ - play_new_game_intro(env, context, PLAYER_NAME); - } - if (fake_saves){ - checkpoint_reattempt_loop(env, context, options.notif_status_update, stats, - std::move(run_selected_range), true, CheckpointSaveMode::FAKE, - CheckpointRetryScope::SELECTED_RANGE); - }else{ - run_selected_range(0); - } - }catch (OperationFailedException&){ - if (RETRIES.STOP_AT_FIRST_FAILURE){ - env.log("AutoStory failed: leaving the game where it failed (lab option).", COLOR_RED); - throw; - } - // Leave the Switch in a known state: on HOME with the game closed, so the next - // run (or the user) starts from there. The error already holds its screenshot. - env.log("AutoStory failed: going to HOME and closing the game.", COLOR_RED); - try{ - go_home(env.console, context); - close_game_from_home(env.console, context); - }catch (OperationFailedException& e){ - env.log("Could not close the game: " + e.message(), COLOR_RED); - } - throw; - } - - send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); -} - - - -} -} -} \ No newline at end of file +/* BDSP AutoStory + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#include +#include +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" +#include "Pokemon/AutoStory/Pokemon_AutoStoryStats.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +BDSPAutoStory_Descriptor::BDSPAutoStory_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonBDSP:AutoStory", + "Pokemon BDSP", "Auto Story", + "Programs/PokemonBDSP/AutoStory.html", + "Automatically completes BDSP story segments.", + PanelQualityStanding::OFFICIAL_BETA, + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS, + {"Tesseract"} + ) +{} + +std::unique_ptr BDSPAutoStory_Descriptor::make_stats() const{ + return std::make_unique(); +} + + +namespace{ + +// The start and end dropdowns' entries: segments 00 and 01 joined, 00.00 +// to 01.11. The options hold a reference to it. +const StringSelectDatabase& checkpoint_database(){ + static const StringSelectDatabase database = Pokemon::story_checkpoint_database(story_checkpoints()); + return database; +} + +std::set rows_that_stop(const std::vector& rows){ + std::set ids; + for (const StoryCheckpoint& row : rows){ + if (row.on_failure == StoryFailure::STOP){ + ids.insert(row.id); + } + } + return ids; +} + +// A failed attempt resets the game from HOME, which loads the save the +// row made at its start; the engine's lines go to the console's log. +Pokemon::SegmentHooks story_hooks(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + Pokemon::SegmentHooks hooks; + hooks.save = [](Pokemon::RunState&){ + return Pokemon::outcome_failed("no_save_hook", "story rows save at their start"); + }; + hooks.reload = [&env, &context](Pokemon::RunState&){ + env.console.log("Resetting game from HOME before retry...", COLOR_ORANGE); + if (!reset_game_from_home(env, env.console, context, true, 1000ms)){ + return Pokemon::outcome_failed("reload_failed", "Failed to reset the game from HOME."); + } + return Pokemon::outcome_ok(); + }; + hooks.log = [&env](const std::string& line){ + env.log(line, COLOR_BLUE); + }; + return hooks; +} + +// Leave the Switch in a known state after a failure: on HOME with the +// game closed, so the next run (or the user) starts from there. +void close_game_after_failure(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + env.log("AutoStory failed: going to HOME and closing the game.", COLOR_RED); + try{ + go_home(env.console, context); + close_game_from_home(env.console, context); + }catch (OperationFailedException& e){ + env.log("Could not close the game: " + e.message(), COLOR_RED); + } +} + +} + + +BDSPAutoStory::BDSPAutoStory() + : RUN(checkpoint_database()) + , NEW_GAME_INTRO( + "Start a new game first:
" + "For a game with no save: restart it and play the intro (language, appearance, names) " + "until the player is in control in the bedroom. Only when starting at 00.00.", + LockMode::LOCK_WHILE_RUNNING, false + ) + , PLAYER_NAME(false, "Player name:
For a new game. Letters and digits.", LockMode::LOCK_WHILE_RUNNING, "Kosmos", "Kosmos") + , STARTER( + "Starter Pokemon:", + { + {StarterChoice::TURTWIG, "turtwig", "Turtwig"}, + {StarterChoice::CHIMCHAR, "chimchar", "Chimchar"}, + {StarterChoice::PIPLUP, "piplup", "Piplup"}, + }, + LockMode::LOCK_WHILE_RUNNING, + StarterChoice::CHIMCHAR + ) + , NOTIFICATIONS({ + &NOTIFICATION_PROGRAM_FINISH, + &NOTIFICATION_ERROR_RECOVERABLE, + &NOTIFICATION_ERROR_FATAL, + }) +{ + PA_ADD_OPTION(RUN); + PA_ADD_OPTION(NEW_GAME_INTRO); + PA_ADD_OPTION(PLAYER_NAME); + PA_ADD_OPTION(STARTER); + PA_ADD_OPTION(NOTIFICATIONS); +} + +void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + const std::vector& checkpoints = story_checkpoints(); + const size_t first = RUN.start_index(); + const size_t last = RUN.end_index(); + if (first > last || last >= checkpoints.size()){ + throw UserSetupError(env.console, "The start checkpoint must not come after the end checkpoint."); + } + // The reads that use text (Location.h) take the game's language from here. + g_story_language = RUN.language(); + + StoryEnv story{env, context, STARTER, PLAYER_NAME}; + StoryExecutor executor(story); + executor.set_checks(RUN.checks()); + executor.set_language(RUN.language()); + executor.set_stop_rows(rows_that_stop(checkpoints)); + std::set saved; + Pokemon::Segment segment = make_story_segment(checkpoints, executor, saved); + Pokemon::SegmentHooks hooks = story_hooks(env, context); + + Pokemon::RunState state; + if (!(first == 0 && NEW_GAME_INTRO)){ + state.flags.insert("save_loaded"); + } + Pokemon::SegmentReport report; + try{ + report = run_segment(state, segment, Pokemon::SegmentRange{checkpoints[first].id, checkpoints[last].id}, RUN.policy(), hooks); + }catch (OperationFailedException&){ + if (!RUN.STOP_AT_FIRST_FAILURE){ + close_game_after_failure(env, context); + } + throw; + } + Pokemon::AutoStoryStats& stats = env.current_stats(); + Pokemon::record_story_stats(stats, report); + env.update_stats(); + if (!report.completed){ + if (!report.stopped_at_first_failure){ + close_game_after_failure(env, context); + } + throw OperationFailedException( + ErrorReportMode::NO_ERROR_REPORT, + std::format( + "AutoStory stopped at {} {}: {}", + report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure) + ) + ); + } + env.console.log("AutoStory: done through " + checkpoints[last].id + "."); + send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h index 6677d84e2e..118589ceed 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h @@ -1,96 +1,59 @@ -#pragma once - -#include -#include -#include "Common/Cpp/Options/BooleanCheckBoxOption.h" -#include "Common/Cpp/Options/EnumDropdownOption.h" -#include "Common/Cpp/Options/StaticTextOption.h" -#include "Common/Cpp/Options/StringOption.h" -#include "CommonFramework/Notifications/EventNotificationsTable.h" -#include "CommonTools/Options/StringSelectOption.h" -#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" -#include "PokemonBDSP_AutoStoryTools.h" -#include "Pokemon/Options/Pokemon_StoryRunOptions.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -class BDSPAutoStory_Descriptor : public SingleSwitchProgramDescriptor{ -public: - BDSPAutoStory_Descriptor(); - - virtual std::unique_ptr make_stats() const override; -}; - - -class BDSPAutoStory : public SingleSwitchProgramInstance, public ConfigOption::Listener{ -public: - using Descriptor = BDSPAutoStory_Descriptor; - ~BDSPAutoStory(); - BDSPAutoStory(); - - virtual void program( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context - ) override; - - -private: - virtual void on_config_value_changed(void* object) override; - void update_ui_state(); -private: - // Attempts per checkpoint and stop at the first failure, as in FRLG. - Pokemon::StoryRunOptions RETRIES; - BooleanCheckBoxOption DIAGNOSTIC_POST_CATCH_ONLY; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_FAKE_CHECKPOINT_SAVE; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_SANDGEM_MART_LADY; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_CENTER; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_SCHOOL; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_BARRY; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_PRESIDENT; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_CLOWN_MART; - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_CLOWN_COMPANY; - BooleanCheckBoxOption DIAGNOSTIC_JUBILIFE_POKETCH; - BooleanCheckBoxOption ENABLE_ADVANCED_MODE; - - StringSelectOption START_CHECKPOINT_TUTORIAL; - StringSelectOption END_CHECKPOINT_TUTORIAL; - StaticTextOption START_DESCRIPTION_SEGMENT; - StaticTextOption END_DESCRIPTION_SEGMENT; - - StaticTextOption START_DESCRIPTION_CHECKPOINT; - StaticTextOption END_DESCRIPTION_CHECKPOINT; - - StringSelectOption START_SEGMENT; - StringSelectOption END_SEGMENT; - - BooleanCheckBoxOption NEW_GAME_INTRO; - StringOption PLAYER_NAME; - EnumDropdownOption STARTERCHOICE; - BooleanCheckBoxOption STARTER_SHINY; - BooleanCheckBoxOption CATCH_LEGENDARY; - EnumDropdownOption HELP_MODE; - EnumDropdownOption OUTSIDE_HELP_POKEMON; - EnumDropdownOption OUTSIDE_HELP_ATTACK_1; - EnumDropdownOption OUTSIDE_HELP_ATTACK_2; - EnumDropdownOption OUTSIDE_HELP_ATTACK_3; - EnumDropdownOption OUTSIDE_HELP_ATTACK_4; - - EventNotificationOption NOTIFICATION_STATUS_UPDATE; - EventNotificationsOption NOTIFICATIONS; -}; - -} -} -} +/* BDSP AutoStory + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * Plays Brilliant Diamond / Shining Pearl from a new game to Floaroma Town: + * the story table (PokemonBDSP_AutoStory_StoryTable.h) run by the shared + * story engine, from the start checkpoint to the end one. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_H +#define PokemonAutomation_PokemonBDSP_AutoStory_H + +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/StringOption.h" +#include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Options/Pokemon_StoryRunOptions.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class BDSPAutoStory_Descriptor : public SingleSwitchProgramDescriptor{ +public: + BDSPAutoStory_Descriptor(); + + virtual std::unique_ptr make_stats() const override; +}; + + +class BDSPAutoStory : public SingleSwitchProgramInstance{ +public: + using Descriptor = BDSPAutoStory_Descriptor; + BDSPAutoStory(); + + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + // The start and end checkpoints, attempts, checks and language. + Pokemon::StoryRunOptions RUN; + BooleanCheckBoxOption NEW_GAME_INTRO; + StringOption PLAYER_NAME; + EnumDropdownOption STARTER; + + EventNotificationsOption NOTIFICATIONS; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp deleted file mode 100644 index 0a1be09564..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp +++ /dev/null @@ -1,2282 +0,0 @@ -#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "Common/Cpp/PrettyPrint.h" -/* BDSP AutoStory Tools - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include - -#include "Common/Cpp/Filesystem/Filesystem.h" - -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/Globals.h" -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/ImageTools/ImageDiff.h" -#include "CommonFramework/ImageTools/ImageStats.h" -#include "CommonFramework/Notifications/ProgramNotifications.h" -#include "CommonFramework/Tools/VideoStream.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" - -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/Images/ImageFilter.h" -#include "CommonTools/Images/SolidColorTest.h" -#include "CommonTools/OCR/OCR_Routines.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" -#include "CommonTools/VisualDetectors/ImageMatchDetector.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" - -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" -#include "PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h" -#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" -#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" -#include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" - -#include "Detect/PokemonBDSP_AutoStory_OverworldDetector.h" -#include "Utils/PokemonBDSP_AutoStory_Location.h" - -#include "PokemonBDSP_AutoStoryTools.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - - -const char* torterra_move_slug(TorterraMove move){ - switch (move){ - case TorterraMove::EARTHQUAKE: - return "earthquake"; - case TorterraMove::WOOD_HAMMER: - return "wood-hammer"; - case TorterraMove::RAZOR_LEAF: - return "razor-leaf"; - case TorterraMove::CRUNCH: - return "crunch"; - case TorterraMove::GIGA_DRAIN: - return "giga-drain"; - case TorterraMove::IRON_TAIL: - return "iron-tail"; - case TorterraMove::ROCK_TOMB: - return "rock-tomb"; - case TorterraMove::FACADE: - return "facade"; - case TorterraMove::STONE_EDGE: - return "stone-edge"; - case TorterraMove::ROCK_SLIDE: - return "rock-slide"; - case TorterraMove::BULLDOZE: - return "bulldoze"; - case TorterraMove::STRENGTH: - return "strength"; - case TorterraMove::ROCK_SMASH: - return "rock-smash"; - case TorterraMove::ROCK_CLIMB: - return "rock-climb"; - } - return "unknown"; -} - -const char* trainer_slug(Trainer t){ - switch (t){ - case Trainer::Veilstone_GalacticGrunts_2v2: - return "Veilstone_GalacticGrunts_2v2"; - } - return "unknown"; -} - -bool fake_save_game(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - pbf_mash_button(context, BUTTON_L, 1000ms); - - context.wait_for_all_requests(); - pbf_press_button(context, BUTTON_X, 80ms, 1000ms); - pbf_press_button(context, BUTTON_R, 80ms, 1000ms); - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - - return true; -} - -// Opens the save prompt from the overworld and keeps a screenshot of it: it -// shows the play time and badges, a record of where each checkpoint save -// stands. Returns the map its card names. -static std::string open_save_prompt(VideoStream& stream, ProControllerContext& context){ - overworld_to_menu(stream, context); - pbf_press_button(context, BUTTON_R, 80ms, 2000ms); - context.wait_for_all_requests(); - VideoSnapshot screen = stream.video().snapshot(); - if (!screen){ - return ""; - } - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/saves/"; - Filesystem::create_directories(Filesystem::Path(folder)); - const std::string path = folder + now_to_filestring() + ".png"; - screen.frame->save(path); - stream.log("Save prompt screenshot: " + path); - const std::string location = read_save_screen_location(*screen.frame); - stream.log("Save screen location: \"" + location + "\"."); - return location; -} - -// Backs out of the save prompt and its menu to the overworld. -static void leave_save_prompt(VideoStream& stream, ProControllerContext& context){ - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - OverworldWatcher overworld; - if (wait_until(stream, context, std::chrono::seconds(15), {overworld}) < 0){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Failed to detect the overworld after leaving the save prompt.", stream); - } -} - -std::string read_location_from_save_screen(VideoStream& stream, ProControllerContext& context){ - const std::string location = open_save_prompt(stream, context); - leave_save_prompt(stream, context); - return location; -} - -bool save_game( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - const std::string& expected_location -) { - static constexpr size_t MAX_RETRIES = 3; - - std::string wrong_place; - for (size_t attempt = 0; attempt < MAX_RETRIES && wrong_place.empty(); attempt++){ - try { - env.console.log("Checkpoint save attempt: " + std::to_string(attempt + 1)); - - // Only mash B on retries - no need to on first attempt - // if already cleanly in the overworld. - if (attempt > 0){ - pbf_mash_button(context, BUTTON_B, 1000ms); - } - - // The feedback-based save of save_game(VideoStream&, ...), split at the - // save prompt to check where it stands. - const std::string location = open_save_prompt(env.console, context); - if (!expected_location.empty() && !location_banner_matches(location, expected_location)){ - wrong_place = "About to save in \"" + location + "\", expected " + expected_location + "; not saving."; - env.console.log(wrong_place, COLOR_RED); - leave_save_prompt(env.console, context); - break; - } - pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); - - // Return to overworld cleanly. - pbf_mash_button(context, BUTTON_B, 1000ms); - - // Drain R/confirm/B before detection; an earlier overworld frame - // must not report success while saving commands are still queued. - context.wait_for_all_requests(); - - // Verify overworld. Generous timeout: save animation + fade can be slow. - OverworldWatcher overworld; - int ret = wait_until(env.console, context, std::chrono::seconds(15), {overworld}); - if (ret < 0){ - throw OperationFailedExceptionWithScreenshot(ErrorReportMode::SEND_ERROR_REPORT, - "Failed to detect overworld after save.", - env.console); - } - - env.console.log("Checkpoint save verified."); - return true; - - } catch (OperationFailedException& e){ - env.console.log("Checkpoint save failed: " + e.message(), COLOR_RED); - context.wait_for(1000ms); - } - } - // No blind fallback for the wrong place: it would save there. - if (!wrong_place.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, wrong_place, env.console); - } - - // Blind fallback: reuse the non-feedback save_game(context) from GameNavigation - // rather than duplicating the button sequence manually. - env.console.log("All save retries failed. Using blind fallback.", COLOR_ORANGE); - pbf_mash_button(context, BUTTON_B, 1000ms); - save_game(context); // PokemonBDSP_GameNavigation.h - non-feedback overload - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - - return false; -} - - -void checkpoint_save(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats, - CheckpointSaveMode save_mode){ - const bool fake = save_mode == CheckpointSaveMode::FAKE; - env.console.log( - fake ? "[Checkpoint save] DIAGNOSTIC FAKE mode: save is not verified." : "[Checkpoint save] REAL mode."); - bool success = fake ? fake_save_game(env, context) : save_game(env, context); - - stats.m_checkpoint++; - env.update_stats(); - - send_program_status_notification(env, - notif_status_update, - fake ? "Diagnostic fake checkpoint save (not verified)." - : success ? "Saved at checkpoint (verified)." - : "Saved at checkpoint (blind fallback)."); -} - - -void reload_backup_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - // TODO: implement backup save reload. - // This is called when a checkpoint with save=false fails. - // Load the game's automatic backup save rather than resetting from HOME. - (void)env; - (void)context; -} - - -bool g_stop_at_first_failure = false; - -void checkpoint_reattempt_loop(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats, - std::function&& action, - bool save, - CheckpointSaveMode save_mode, - CheckpointRetryScope retry_scope){ - static constexpr size_t MAX_ATTEMPTS = 20; - - for (size_t i = 0;; i++){ - try { - if (i == 0 && retry_scope != CheckpointRetryScope::SELECTED_RANGE){ - if (save || stats.m_first_run){ - checkpoint_save(env, context, notif_status_update, stats, save_mode); - } - stats.m_first_run = false; - } - - action(i); - return; // success - - }catch (OperationFailedException& e){ - if (retry_scope == CheckpointRetryScope::DEFER_TO_SELECTED_RANGE) throw; - env.console.log("checkpoint_reattempt_loop: attempt " + std::to_string(i) + " failed: " + e.message(), COLOR_RED); - // Keep what the screen showed at the failure; the retry below resets the game. - if (VideoSnapshot screen = env.console.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/failures/"; - Filesystem::create_directories(Filesystem::Path(folder)); - const std::string path = folder + now_to_filestring() + "-attempt" + std::to_string(i) + ".png"; - screen.frame->save(path); - env.console.log("Failure screenshot: " + path); - } - stats.m_reset++; - env.update_stats(); - - if (i + 1 >= MAX_ATTEMPTS){ - throw; - } - if (g_stop_at_first_failure){ - env.console.log("Stopping at the first failure; the game stays where it failed.", COLOR_ORANGE); - throw; - } - - if (save || retry_scope == CheckpointRetryScope::SELECTED_RANGE){ - env.console.log("Resetting game from HOME before retry...", COLOR_ORANGE); - bool ok = reset_game_from_home(env, env.console, context, true, 1000ms); - if (!ok){ - throw OperationFailedExceptionWithScreenshot(ErrorReportMode::SEND_ERROR_REPORT, - "Failed to reset game from HOME during checkpoint retry.", - env.console); - } - }else{ - env.console.log("Reloading from backup save before retry...", COLOR_ORANGE); - reload_backup_save(env, context); - } - } - } -} - - -void repeat(std::initializer_list> actions, size_t times){ - for (size_t i = 0; i < times; i++){ - for (const auto& action : actions){ - action(); - } - } -} - - -void wait_for_dialogue(VideoStream& stream, - ProControllerContext& context, - const std::string& label, - Milliseconds timeout){ - auto log_detected = [&] { - if (!label.empty()){ - stream.log(label + " dialogue detected."); - } - }; - - ShortDialogWatcher watcher_cyan(COLOR_CYAN); - int ret = run_until(stream, - context, - [timeout](ProControllerContext& context){ - context.wait_for(timeout); - }, - {{watcher_cyan}}); - if (ret == 0){ - log_detected(); - return; - } - - ShortDialogWatcher watcher_red(COLOR_RED); - ret = run_until(stream, - context, - [timeout](ProControllerContext& context){ - context.wait_for(timeout); - }, - {{watcher_red}}); - if (ret == 0){ - log_detected(); - return; - } - - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - (label.empty() ? "wait_for_dialogue" : label) + ": dialogue not detected within timeout.", - stream); -} - - -void mash_until_dialogue_ends(VideoStream& stream, ProControllerContext& context, Button button, Milliseconds timeout){ - OverworldWatcher overworld; - int ret = run_until(stream, - context, - [button, timeout](ProControllerContext& context){ - pbf_mash_button(context, button, timeout); - }, - {{overworld}}); - if (ret < 0){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "mash_until_dialogue_ends: overworld not reached within timeout.", - stream); - } -} - - -void repeat_dpad(ProControllerContext& context, - DpadState& state, - DpadPosition dir, - Milliseconds press, - Milliseconds hold, - size_t times, - bool include_last_dir){ - if (include_last_dir && state.last_dir != DPAD_NONE && state.last_dir != dir){ - times += 1; - } - state.last_dir = dir; - for (size_t i = 0; i < times; i++){ - pbf_press_dpad(context, dir, press, hold); - } -} - -static const char* shop_item_name(ShopItem item){ - switch (item){ - case ShopItem::FullRestore: - return "Full restore"; - case ShopItem::MaxPotion: - return "Max potion"; - case ShopItem::HyperPotion: - return "Hyper potion"; - case ShopItem::SuperPotion: - return "Super potion"; - case ShopItem::Potion: - return "Potion"; - case ShopItem::Antidote: - return "Antidote"; - case ShopItem::ParalyzHeal: - return "Paralyz heal"; - case ShopItem::Awakening: - return "Awakening"; - case ShopItem::Repel: - return "Repel"; - case ShopItem::SuperRepel: - return "Super repel"; - case ShopItem::MaxRepel: - return "Max repel"; - case ShopItem::EscapeRope: - return "Escape rope"; - default: - return "Unknown"; - } -} - -// Returns the OCR slug used as the dictionary key in ShopItemNameOCR.json. -static const char* shop_item_slug(ShopItem item){ - switch (item){ - case ShopItem::FullRestore: - return "full-restore"; - case ShopItem::MaxPotion: - return "max-potion"; - case ShopItem::HyperPotion: - return "hyper-potion"; - case ShopItem::SuperPotion: - return "super-potion"; - case ShopItem::Potion: - return "potion"; - case ShopItem::Antidote: - return "antidote"; - case ShopItem::ParalyzHeal: - return "paralyze-heal"; - case ShopItem::Awakening: - return "awakening"; - case ShopItem::Repel: - return "repel"; - case ShopItem::SuperRepel: - return "super-repel"; - case ShopItem::MaxRepel: - return "max-repel"; - case ShopItem::EscapeRope: - return "escape-rope"; - default: - return "unknown"; - } -} - -// {MaxRepel, 2, 4}, {FullRestore, 0, 3} → "4 Max repel, 3 Full restore" -static std::string make_buy_label(const std::vector& items){ - std::string label; - for (size_t i = 0; i < items.size(); i++){ - if (i > 0){ - label += ", "; - } - if (items[i].quantity == BUY_MAX){ - label += "max "; - }else{ - label += std::to_string(items[i].quantity) + " "; - } - label += shop_item_name(items[i].item); - } - return label; -} - -bool buy_items(VideoStream& stream, - ProControllerContext& context, - const std::vector& items, - Language language){ - if (items.empty()){ - return true; - } - - const std::string label = make_buy_label(items); - context.wait_for_all_requests(); - pbf_wait(context, 500ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - context.wait_for_all_requests(); - - - // ── Coordinate constants ───────────────────────────────────────────────── - - // Area where the selection arrow appears at the vendor greeting menu (Buy/Sell/Leave). - static const ImageFloatBox VENDOR_ARROW_BOX{0.630000, 0.550000, 0.070000, 0.120000}; - - // Area where the selection arrow appears at the YES/NO confirmation dialog. - static const ImageFloatBox CONFIRM_ARROW_BOX{0.700000, 0.620000, 0.070000, 0.120000}; - - // Full area containing all item rows in the shop list. - // The hover detector scans this region to dynamically locate the red outline. - static const ImageFloatBox ITEM_SCAN_AREA{0.417000, 0.100000, 0.540000, 0.625000}; - - static constexpr size_t MAX_OUTER = 50; - static constexpr size_t MAX_ITEM_SEARCH = 50; // max iterations per item before giving up - - // ── Outer state machine ────────────────────────────────────────────────── - for (size_t outer = 0; outer < MAX_OUTER; outer++){ - SelectionArrowFinder arrow_vendor(stream.overlay(), VENDOR_ARROW_BOX, COLOR_GREEN); - ShopBuyingWindowWatcher buying_window; - ShortDialogWatcher dialog(COLOR_RED); - - // case 0: no detectors fired — press A in a for-loop (5 presses) while watching. - stream.log("Starting to loop over items"); - int state = run_until(stream, - context, - [](ProControllerContext& ctx){ - for (int i = 0; i < 5; i++){ - pbf_press_button(ctx, BUTTON_A, 80ms, 1000ms); - } - }, - {{arrow_vendor}, {buying_window}, {dialog}}); - - context.wait_for_all_requests(); - pbf_wait(context, 500ms); - - switch (state){ - - case 0: // case 1: SelectionArrow at vendor menu → press A to choose "Buy" - stream.log("Found the selection arrow, pressing A to enter the buying window...", COLOR_GREEN); - pbf_press_button(context, BUTTON_A, 80ms, 500ms); - break; - - case 1: // case 2: buying window open → buy each requested item - stream.log("Found the buying window, searching for the right item...", COLOR_GREEN); - { - bool buying_failed = false; - - for (const ItemToBuy& item : items){ - stream.log("Buying " + std::to_string(item.quantity) + "x " + shop_item_slug(item.item)); - if (buying_failed){ - break; - } - - // Detector fires only when the red outline is found AND both - // name and price OCR match the expected item slug. - ShopItemHoverDetector hover(stream.logger(), - ITEM_SCAN_AREA, - shop_item_slug(item.item), - language, - COLOR_CYAN); - ShopQuantityWindowWatcher qty_window; - SelectionArrowFinder confirm_arrow(stream.overlay(), CONFIRM_ARROW_BOX, COLOR_GREEN); - - bool item_bought = false; - bool hover_confirmed = false; - for (size_t inner = 0; inner < MAX_ITEM_SEARCH && !item_bought; inner++){ - context.wait_for_all_requests(); - - int sub = run_until(stream, - context, - [](ProControllerContext& ctx){ - ctx.wait_for(1000ms); - }, - {{confirm_arrow}, {qty_window}, {hover}}); - - context.wait_for_all_requests(); - - stream.log("buy_items: sub=" + std::to_string(sub), COLOR_WHITE); - - switch (sub){ - - case 0: // YES/NO confirmation arrow → confirm purchase - stream.log("Found the confirmation to buy arrow, pressing A to buy", COLOR_GREEN); - { - context.wait_for_all_requests(); - ShopBuyingWindowWatcher back_in_shop; - run_until(stream, - context, - [](ProControllerContext& ctx){ - for (int i = 0; i < 10; i++){ - pbf_press_button(ctx, BUTTON_A, 80ms, 800ms); - } - }, - {{back_in_shop}}); - item_bought = true; - break; - } - - case 1: // quantity window - if (!hover_confirmed){ - // Quantity window appeared before we confirmed the right item — - // an earlier A press accidentally opened the wrong item's selector. - // Press B to go back to the list and continue searching. - stream.log("Unexpected quantity window (wrong item), pressing B...", COLOR_YELLOW); - pbf_press_button(context, BUTTON_B, 80ms, 800ms); - break; - } - stream.log("Found the quantity window", COLOR_GREEN); - if (item.quantity == BUY_MAX){ - stream.log("Buying max quantity"); - pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); - }else{ - stream.log("Buying " + std::to_string(item.quantity) + " of the item"); - for (int q = 1; q < item.quantity; q++){ - pbf_press_dpad(context, DPAD_UP, 80ms, 300ms); - } - } - pbf_press_button(context, BUTTON_A, 80ms, 300ms); - pbf_wait(context, 1000ms); - break; - - case 2: // correct item highlighted → press A to open quantity selector - stream.log("Found the right item, pressing A to buy a certain quantity...", COLOR_GREEN); - hover_confirmed = true; - pbf_press_button(context, BUTTON_A, 80ms, 300ms); - pbf_wait(context, 1000ms); - break; - - default: // outline not found or wrong item → advance to next row - stream.log("Not on the right item, moving...", COLOR_GREEN); - pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); - pbf_wait(context, 300ms); - break; - } - } - - if (!item_bought){ - stream.log(label + ": buy_items: could not locate " + shop_item_name(item.item) + - " in shop list.", - COLOR_RED); - buying_failed = true; - } - } - - if (!buying_failed){ - stream.log("All items bought, exiting shop...", COLOR_GREEN); - pbf_mash_button(context, BUTTON_B, 5000ms); - return true; - } - break; - } - - case 2: // case 3: unexpected ShortDialog → press B until back in buying window - { - ShopBuyingWindowWatcher back_in_shop; - run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_B, 5000ms); - }, - {{back_in_shop}}); - break; - } - - default: // outer default: unrecognised state → press B to recover - pbf_press_button(context, BUTTON_B, 80ms, 3000ms); - break; - } - } - - stream.log(label + ": buy_items: failed to complete after max iterations.", COLOR_RED); - return false; -} - - -bool heal_pokemon(VideoStream& stream, ProControllerContext& context, const std::string& label){ - context.wait_for_all_requests(); - pbf_wait(context, 2000ms); - - pbf_move_left_joystick(context, {0, +1}, 1800ms, 100ms); // 7+ - context.wait_for_all_requests(); - - pbf_mash_button(context, BUTTON_A, 5000ms); - context.wait_for_all_requests(); - pbf_wait(context, 3000ms); - wait_for_dialogue(stream, context, label + " - Pokemon healing"); - mash_until_dialogue_ends(stream, context, BUTTON_B); - context.wait_for_all_requests(); - pbf_wait(context, 1000ms); - - // Exiting the Pokemon Center - BlackScreenOverWatcher black_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); - int ret = run_until(stream, - context, - [](ProControllerContext& context){ - pbf_move_left_joystick(context, {0, -1}, 15000ms, 100ms); - }, - {{black_screen}}); - if (ret < 0){ - stream.log("heal_pokemon(" + label + "): black screen not detected", COLOR_RED); - return false; - } - - return true; -} - -std::string fly_point_name(FlyPoint place){ - switch (place){ - case FlyPoint::CanalavCity: - return "canalave_city"; - case FlyPoint::CelesticTown: - return "celestic_town"; - case FlyPoint::EternaCity: - return "eterna_city"; - case FlyPoint::FloaromaTown: - return "floaroma_town"; - case FlyPoint::HearthomeCity: - return "hearthome_city"; - case FlyPoint::JubilifeCity: - return "jubilife_city"; - case FlyPoint::OreburghCity: - return "oreburgh_city"; - case FlyPoint::PastoriaCity: - return "pastoria_city"; - case FlyPoint::PokemonLeagueLower: - return "pokemon_league_lower"; - case FlyPoint::PokemonLeagueUpper: - return "pokemon_league_upper"; - case FlyPoint::Route221: - return "route_221"; - case FlyPoint::SandgemTown: - return "sandgem_town"; - case FlyPoint::SolaceonTown: - return "solaceon_town"; - case FlyPoint::SnowpointCity: - return "snowpoint_city"; - case FlyPoint::SunyshoreCity: - return "sunyshore_city"; - case FlyPoint::TwinleafTown: - return "twinleaf_town"; - case FlyPoint::VeilstoneCity: - return "veilstone_city"; - case FlyPoint::CurrentCity: - return "current_city"; - } - return "unknown"; -} - -bool fly_to(VideoStream& stream, ProControllerContext& context, FlyPoint place){ - // Each city branch below should update this to the city icon's position on the fly map. - // Detection: when the cursor brackets overlap the icon, its normally uniform - // color becomes non-uniform (stddev rises). Box stays as a rough fallback - // until each city is calibrated. - ImageFloatBox city_icon_box{0.700000, 0.550000, 0.100000, 0.120000}; - - DpadState dpad; - context.wait_for_all_requests(); - pbf_wait(context, 1000ms); - open_menu(stream, context, MenuCursorPosition::MAP, 8); - - if (place == FlyPoint::CurrentCity){ - ImageFloatBox button_a_box{0.540000, 0.960000, 0.070000, 0.035000}; - struct FlyAButtonDetector : VisualInferenceCallback { - ImageFloatBox m_box; - FlyAButtonDetector(const ImageFloatBox& box) - : VisualInferenceCallback("FlyAButtonDetector") - , m_box(box){ - } - void make_overlays(VideoOverlaySet&) const override { - } - bool process_frame(const ImageViewRGB32& frame, WallClock) override { - ImageStats s = image_stats(extract_box_reference(frame, m_box)); - return s.stddev.sum() > 30.0; - } - }; - FlyAButtonDetector watcher(button_a_box); - int ret = wait_until(stream, context, 3000ms, {{watcher}}); - if (ret < 0){ - stream.log("CurrentCity: A button not found.", COLOR_RED); - return false; - } - BlackScreenOverWatcher flying_black_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); - int fly_ret = run_until(stream, - context, - [](ProControllerContext& context){ - pbf_mash_button(context, BUTTON_A, 200000ms); - }, - {{flying_black_screen}}); - if (fly_ret < 0){ - stream.log("CurrentCity: black screen not detected!", COLOR_RED); - return false; - } - stream.log("CurrentCity: black screen detected, arrived.", COLOR_GREEN); - return true; - } - - switch (place){ - case FlyPoint::CelesticTown: - pbf_move_left_joystick(context, {-1, +1}, 4000ms, 500ms); - break; - case FlyPoint::HearthomeCity: - case FlyPoint::OreburghCity: - pbf_move_left_joystick(context, {-1, -1}, 4000ms, 500ms); - break; - case FlyPoint::PastoriaCity: - case FlyPoint::PokemonLeagueLower: - case FlyPoint::PokemonLeagueUpper: - case FlyPoint::SolaceonTown: - case FlyPoint::SunyshoreCity: - case FlyPoint::VeilstoneCity: - pbf_move_left_joystick(context, {+1, -1}, 4000ms, 500ms); - break; - case FlyPoint::SnowpointCity: - pbf_move_left_joystick(context, {-1, +1}, 4000ms, 500ms); - break; - case FlyPoint::TwinleafTown: - pbf_move_left_joystick(context, {-1, -1}, 4000ms, 500ms); - break; - default: - pbf_move_left_joystick(context, {-1, -1}, 4000ms, 500ms); - break; - } - context.wait_for_all_requests(); - - ImageFloatBox button_a_box{0.540000, 0.960000, 0.070000, 0.035000}; - - struct FlyAButtonDetector : VisualInferenceCallback { - ImageFloatBox m_box; - FlyAButtonDetector(const ImageFloatBox& box) - : VisualInferenceCallback("FlyAButtonDetector") - , m_box(box){ - } - void make_overlays(VideoOverlaySet&) const override { - } - bool process_frame(const ImageViewRGB32& frame, WallClock) override { - ImageStats s = image_stats(extract_box_reference(frame, m_box)); - // Blue stripe is uniform when no button shown; A button disrupts it - return s.stddev.sum() > 30.0; - } - }; - FlyAButtonDetector watcher_1(button_a_box); - - switch (place){ - case FlyPoint::CanalavCity: - break; - case FlyPoint::CelesticTown: - city_icon_box = {0.660000, 0.470000, 0.012000, 0.020000}; - repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 15, false); - repeat_dpad(context, dpad, DPAD_DOWN, 80ms, 300ms, 13, false); - break; - case FlyPoint::EternaCity: - break; - case FlyPoint::FloaromaTown: - break; - case FlyPoint::HearthomeCity: - city_icon_box = {0.660000, 0.637000, 0.029000, 0.052000}; - repeat_dpad(context, dpad, DPAD_LEFT, 80ms, 300ms, 16, false); - repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 7, false); - break; - case FlyPoint::JubilifeCity: - city_icon_box = {0.453000, 0.702000, 0.016000, 0.054000}; - repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 4, false); - repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 5, false); - break; - case FlyPoint::OreburghCity: - city_icon_box = {0.566000, 0.702000, 0.012000, 0.054000}; - repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 10, false); - repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 5, false); - break; - case FlyPoint::PastoriaCity: - break; - case FlyPoint::PokemonLeagueLower: - break; - case FlyPoint::PokemonLeagueUpper: - break; - case FlyPoint::Route221: - break; - case FlyPoint::SandgemTown: - break; - case FlyPoint::SolaceonTown: - city_icon_box = {0.735000, 0.603000, 0.030000, 0.022000}; - repeat_dpad(context, dpad, DPAD_LEFT, 80ms, 300ms, 12, false); - repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 9, false); - break; - case FlyPoint::SnowpointCity: - city_icon_box = {0.602000, 0.070000, 0.013000, 0.054000}; - repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 12, false); - repeat_dpad(context, dpad, DPAD_DOWN, 80ms, 300ms, 1, false); - break; - case FlyPoint::SunyshoreCity: - break; - case FlyPoint::TwinleafTown: - city_icon_box = {0.415000, 0.870000, 0.013000, 0.022000}; - repeat_dpad(context, dpad, DPAD_RIGHT, 80ms, 300ms, 2, false); - repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 1, false); - break; - case FlyPoint::VeilstoneCity: - city_icon_box = {0.829000, 0.537000, 0.017000, 0.054000}; - repeat_dpad(context, dpad, DPAD_LEFT, 80ms, 300ms, 8, false); - repeat_dpad(context, dpad, DPAD_UP, 80ms, 300ms, 10, false); - break; - case FlyPoint::CurrentCity: - break; - } - - context.wait_for_all_requests(); - - struct CityIconDetector : VisualInferenceCallback { - ImageFloatBox m_box; - CityIconDetector(const ImageFloatBox& box) - : VisualInferenceCallback("CityIconDetector") - , m_box(box){ - } - void make_overlays(VideoOverlaySet&) const override { - } - bool process_frame(const ImageViewRGB32& frame, WallClock) override { - ImageStats s = image_stats(extract_box_reference(frame, m_box)); - // City icon is normally a uniform color; cursor brackets disrupt it - return s.stddev.sum() > 30.0; - } - }; - CityIconDetector watcher_2(city_icon_box); - - const std::string name = fly_point_name(place); - int ret_1 = wait_until(stream, context, 3000ms, {{watcher_1}}); - int ret_2 = wait_until(stream, context, 4000ms, {{watcher_2}}); - if (ret_1 < 0){ - stream.log("A button not found, retrying...", COLOR_RED); - }; - if (ret_2 < 0){ - stream.log(name + " not found, retrying...", COLOR_RED); - }; - if (ret_1 == 0 and ret_2 == 0){ - stream.log("Found " + name + ", flying towards it.", COLOR_GREEN); - WhiteScreenOverWatcher flying_white_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); - BlackScreenOverWatcher flying_black_screen(COLOR_RED, {0.1, 0.1, 0.8, 0.8}); - int ret = run_until(stream, - context, - [](ProControllerContext& context){ - pbf_mash_button(context, BUTTON_A, 200000ms); - }, - {{flying_white_screen}, {flying_black_screen}}); - if (ret < 0){ - stream.log("Flying to " + name + ": white / black screen not detected!", COLOR_RED); - return false; - }else if (ret >= 0){ - stream.log("Flying to " + name + ": white / black screen detected!", COLOR_GREEN); - return true; - } - } - return false; -} - -// --------------------------------------------------------------------------- -// Arrow - Based Potion Usage Helper - -// This version assumes : -// -// you are already inside the Bag -// Potion has already been selected -// the game is currently waiting for you to choose a Pokemon -// -// Flow : -// 1. Move LEFT once to Pokemon list -// 2. Press A on first Pokemon -// 3. Use SelectionArrow to detect arrow position -// 4. Move DOWN until arrow reaches slot 3 -// 5. Mash A for 3 seconds -// 6. Mash B for 10 seconds -// -// --------------------------------------------------------------------------- - -// Slot 1's colored HP bar in the Bag. Read fill position rather than OCR. -static double potion_hp_fraction(const ImageViewRGB32& frame){ - if (bag_pocket_position(frame) == 0){ - return -1; - } - const ImageViewRGB32 bar = extract_box_reference(frame, ImageFloatBox{0.137, 0.160, 0.202, 0.016}); - size_t filled = 0; - for (size_t x = 0; x < bar.width(); x++){ - size_t color = 0; - for (size_t y = 0; y < bar.height(); y++){ - uint32_t p = bar.pixel(x, y); - int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; - const bool green = g > r + 30 && g > b + 20; - const bool yellow = r > 150 && g > 120 && b < 100; - const bool red = r > 150 && r > g + 50 && r > b + 50; - color += green || yellow || red; - } - filled += color > bar.height() / 4; - } - return bar.width() ? (double)filled / bar.width() : -1; -} - -static double confirmed_potion_hp(VideoStream& stream, ProControllerContext& context){ - double previous = -1; - size_t stable = 0; - for (size_t check = 0; check < 20; check++){ - context.wait_for_all_requests(); - auto frame = stream.video().snapshot(); - double hp = frame ? potion_hp_fraction(frame) : -1; - if (hp >= 0 && previous >= 0 && std::abs(hp - previous) <= 0.015){ - if (++stable >= 3){ - return hp; - } - }else{ - stable = 0; - } - previous = hp; - pbf_wait(context, 150ms); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Potion: Slot 1 HP bar could not be confirmed.", - stream); -} - -bool bag_has_item(VideoStream& stream, ProControllerContext& context, const std::string& name, int target_pocket) { - const bool found = find_bag_item_by_name(stream, context, name, target_pocket, 5); - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - OverworldWatcher overworld; - if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Bag: Did not return to the overworld after looking for " + name + ".", stream); - } - return found; -} - -void use_potion_first_pokemon(VideoStream& stream, ProControllerContext& context, double heal_below) { - OverlayBoxScope hp_overlay(stream.overlay(), COLOR_GREEN, {0.137, 0.160, 0.202, 0.016}, ""); - bool healed = false; - for (size_t dose = 0; dose < 3; dose++){ - stream.log("[Potion] Finding Bag/Medicine/Potion; dose " + std::to_string(dose + 1) + "/3."); - if (!find_bag_item_by_name(stream, context, "potion", 1, 5)) { - if (dose == 0 && heal_below < 1.0) { - // Only a check: without Potions there is nothing to do here. - stream.log("[Potion] No Potion in the Bag; skipping the HP check.", COLOR_ORANGE); - healed = true; - break; - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Potion: Could not verify Potion in the Medicine pocket.", - stream); - } - const double before = confirmed_potion_hp(stream, context); - stream.log("[Potion] Slot 1 HP bar before use=" + std::to_string(before * 100) + "%."); - if (before >= 0.98) { healed = true; break; } - if (dose == 0 && before >= heal_below) { - stream.log("[Potion] Slot 1 HP is at least " + std::to_string(heal_below * 100) + "%; no Potion needed."); - healed = true; - break; - } - if (before <= 0.005) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Potion: Slot 1 appears fainted; a Potion cannot revive it.", - stream); - } - pbf_press_button(context, BUTTON_A, 80ms, 500ms); - pbf_press_button(context, BUTTON_A, 80ms, 500ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - OverworldWatcher overworld; - if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Potion: Did not return to the overworld after use.", - stream); - } - // Reopen Bag to verify the result, including when the last Potion was used. - open_menu(stream, context, MenuCursorPosition::BAG, 5); - pbf_wait(context, 750ms); - context.wait_for_all_requests(); - const double after = confirmed_potion_hp(stream, context); - stream.log("[Potion] Slot 1 HP bar after use=" + std::to_string(after * 100) + "%."); - if (after <= before + 0.005){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Potion: Slot 1 HP did not increase; refusing to continue movement.", - stream); - } - if (after >= 0.98){ - healed = true; - break; - } - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - } - if (!healed){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Potion: Full HP not confirmed after three uses.", - stream); - } - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - OverworldWatcher overworld; - if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Potion: Final overworld return not confirmed.", - stream); - } - stream.log("[Potion] Slot 1 HP bar full and overworld confirmed."); -} - - -namespace { - -struct IconGrid { - int row, col; -}; - -IconGrid icon_to_grid(MenuCursorPosition pos, int num_icons){ - switch (pos){ - case MenuCursorPosition::POKEDEX: - return {0, 0}; - case MenuCursorPosition::POKEMON: - return {0, 1}; - case MenuCursorPosition::BAG: - return {0, 2}; - case MenuCursorPosition::CARD: - return {0, 3}; - case MenuCursorPosition::OPTIONS: - return {1, num_icons <= 5 ? 0 : 2}; - case MenuCursorPosition::MAP: - return {1, 0}; - case MenuCursorPosition::CAPSULES: - return {1, 1}; - case MenuCursorPosition::MYSTERYGIFT: - return {1, 3}; - default: - return {-1, -1}; - } -} - -} // anonymous namespace - - -void open_menu(VideoStream& stream, ProControllerContext& context, MenuCursorPosition target, int num_icons){ - using namespace std::chrono_literals; - - // Previous single opening attempt: - // context.wait_for_all_requests(); - // overworld_to_menu(stream, context); - // pbf_wait(context, 1000ms); - // context.wait_for_all_requests(); - context.wait_for_all_requests(); - - MenuCursorDetector detector; - { - static const char* ICON_NAMES[MENU_CURSOR_NUM_ICONS] = { - "POKEDEX", - "POKEMON", - "BAG", - "CARD", - "OPTIONS", - "MAP", - "CAPSULES", - "MYSTERYGIFT", - }; - // Previous optional template directory: PokemonBDSP/AutoStory. - std::string ref_dir = RESOURCE_PATH() + "PokemonBDSP/Autostory"; - for (int i = 0; i < MENU_CURSOR_NUM_ICONS; i++){ - std::string path = ref_dir + "/" + ICON_NAMES[i] + ".png"; - // These templates are optional. The dark-pixel cursor detector - // remains available when a template is not packaged. - if (!Filesystem::is_regular_file(path)){ - continue; - } - // Previously constructed the image before checking its availability. - ImageRGB32 img(path); - if (img){ - detector.set_icon_reference(static_cast(i), std::move(img)); - } - } - } - // Previous single detection attempt retained for comparison. - // MenuCursorPosition cur = MenuCursorPosition::UNKNOWN; - // for (int i = 0; i < 10 && cur == MenuCursorPosition::UNKNOWN; i++){ - // context.wait_for(50ms); - // VideoSnapshot snap = stream.video().snapshot(); - // if (snap) cur = detector.detect(snap); - // } - // if (cur == MenuCursorPosition::UNKNOWN){ - // OperationFailedExceptionWithScreenshot::fire( - // ErrorReportMode::SEND_ERROR_REPORT, - // "Menu cursor not detected after opening menu.", - // stream - // ); - // } - - MenuCursorPosition cur = MenuCursorPosition::UNKNOWN; - bool menu_confirmed_open = false; - for (size_t attempt = 1; attempt <= 3; attempt++){ - stream.log("[Open menu] Open and detect attempt " + std::to_string(attempt) + "/3."); - try { - // A late-opening menu may already be visible after a timeout. - // Check before pressing X again, since X would close that menu. - if (!menu_confirmed_open){ - MenuWatcher already_open; - if (wait_until(stream, context, 300ms, {{already_open}}) < 0){ - overworld_to_menu(stream, context); - }else{ - stream.log("[Open menu] Menu already visible; skipping X."); - } - menu_confirmed_open = true; - }else{ - // Cursor detection retries send no input that could close the - // confirmed menu. Keep observing it instead of toggling X. - stream.log("[Open menu] Retrying cursor detection in the open menu."); - } - pbf_wait(context, 1000ms); - context.wait_for_all_requests(); - for (int frame = 0; frame < 10 && cur == MenuCursorPosition::UNKNOWN; frame++){ - context.wait_for(50ms); - const VideoSnapshot snapshot = stream.video().snapshot(); - if (snapshot){ - cur = detector.detect(snapshot); - } - } - if (cur != MenuCursorPosition::UNKNOWN){ - stream.log("[Open menu] Menu and cursor detected on attempt " + std::to_string(attempt) + ".", - COLOR_GREEN); - break; - } - stream.log("[Open menu] Menu cursor not detected on attempt " + std::to_string(attempt) + ".", - COLOR_ORANGE); - } catch (OperationFailedException& error){ - stream.log("[Open menu] Attempt " + std::to_string(attempt) + " failed: " + error.message(), COLOR_ORANGE); - } - } - if (cur == MenuCursorPosition::UNKNOWN){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Menu or cursor detection failed after 3 opening attempts.", - stream); - } - - IconGrid cur_g = icon_to_grid(cur, num_icons); - IconGrid tgt_g = icon_to_grid(target, num_icons); - if (tgt_g.row == -1){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Unknown target menu icon.", - stream); - } - - if (cur_g.row != tgt_g.row){ - DpadPosition vert = (cur_g.row < tgt_g.row) ? DPAD_DOWN : DPAD_UP; - pbf_press_dpad(context, vert, 80ms, 300ms); - context.wait_for_all_requests(); - } - - int col_diff = tgt_g.col - cur_g.col; - if (col_diff != 0){ - DpadPosition horiz = (col_diff > 0) ? DPAD_RIGHT : DPAD_LEFT; - for (int i = 0; i < std::abs(col_diff); i++){ - pbf_press_dpad(context, horiz, 80ms, 300ms); - context.wait_for_all_requests(); - } - } - - pbf_press_button(context, BUTTON_A, 80ms, 200ms); -} - - -// Search the selected item's fixed description header rather than guessing -// which list row is selected. This remains in place while the list scrolls. -// A cursor parked at row 2 or 6 does not indicate a list end: only repeated -// unchanged item headers after movement are used to detect an end. -// Previous selected-item-only search, retained for reference. -// bool find_bag_item( -// VideoStream& stream, -// ProControllerContext& context, -// const BagItemTemplate& item, -// int num_icons, -// size_t max_scrolls_per_pocket, -// std::chrono::milliseconds search_timeout -// ){ -// auto reference = std::make_shared( -// RESOURCE_PATH() + item.reference_path -// ); -// ImageMatchDetector icon(reference, item.icon_box, item.max_icon_rmsd, true); -// ImageMatchDetector name(reference, item.name_box, item.max_name_rmsd, true); -// ImageMatchDetector bag(reference, ImageFloatBox(0.055, 0.012, 0.044, 0.045), 35.0, true); -// const ImageFloatBox selected_header(0.428, 0.754, 0.382, 0.046); -// const auto deadline = std::chrono::steady_clock::now() + search_timeout; -// -// stream.log("[Bag search] Looking for " + item.label + "."); -// open_menu(stream, context, MenuCursorPosition::BAG, num_icons); -// pbf_wait(context, 1000ms); -// context.wait_for_all_requests(); -// -// for (size_t pocket = 0; pocket < 8; pocket++){ -// ImageRGB32 initial_header; -// ImageRGB32 previous_header; -// size_t unchanged = 0; -// size_t matching_frames = 0; -// size_t scrolls = 0; -// bool scanning_up = false; -// -// while (std::chrono::steady_clock::now() < deadline){ -// context.wait_for_all_requests(); -// const VideoSnapshot frame = stream.video().snapshot(); -// if (!frame || !bag.detect(frame)){ -// stream.log("[Bag search] Bag screen not detected.", COLOR_ORANGE); -// return false; -// } -// const double icon_score = icon.rmsd(frame); -// const double name_score = item.check_name ? name.rmsd(frame) : 0.0; -// stream.log( -// "[Bag search] Pocket " + std::to_string(pocket + 1) + -// ", scroll " + std::to_string(scrolls) + -// ": icon=" + std::to_string(icon_score) + -// ", name=" + std::to_string(name_score) -// ); -// if (icon_score <= item.max_icon_rmsd && -// name_score <= item.max_name_rmsd){ -// if (++matching_frames >= 3){ -// stream.log("[Bag search] " + item.label + " confirmed.", COLOR_GREEN); -// return true; -// } -// pbf_wait(context, 150ms); -// continue; -// } -// matching_frames = 0; -// -// const ImageRGB32 header = extract_box_reference(frame, selected_header).scale_to(382, 46); -// if (!initial_header){ -// initial_header = header.copy(); -// } -// else if (scrolls > 0){ -// const bool same_as_previous = -// ImageMatch::pixel_RMSD(previous_header, header) <= 3.0; -// const bool returned_to_start = -// ImageMatch::pixel_RMSD(initial_header, header) <= 3.0; -// if (returned_to_start && !same_as_previous){ -// break; -// } -// // Repeated unchanged rows mean an empty pocket or list end. -// // Returning to the initial row means the list wrapped. -// unchanged = same_as_previous || returned_to_start ? unchanged + 1 : 0; -// if (unchanged >= 3){ -// if (scanning_up){ -// break; -// } -// // A non-wrapping list may have opened in the middle. -// // Search upward as well before moving to another pocket. -// scanning_up = true; -// unchanged = 0; -// initial_header = header.copy(); -// } -// } -// previous_header = header.copy(); -// if (scrolls >= max_scrolls_per_pocket){ -// stream.log("[Bag search] Scroll limit reached; search incomplete.", COLOR_ORANGE); -// return false; -// } -// scrolls++; -// pbf_press_dpad(context, scanning_up ? DPAD_UP : DPAD_DOWN, 80ms, 350ms); -// } -// if (std::chrono::steady_clock::now() >= deadline){ -// stream.log("[Bag search] Search timed out; search incomplete.", COLOR_ORANGE); -// return false; -// } -// if (pocket < 7){ -// pbf_press_dpad(context, DPAD_RIGHT, 80ms, 500ms); -// context.wait_for_all_requests(); -// } -// } -// stream.log("[Bag search] Item not found in any pocket.", COLOR_ORANGE); -// return false; -// } -// - -// One narrow strip on the left side of the red selection border. Divide its -// height into the seven row bands; the item's art and NEW badge are outside it. -int bag_cursor_position(const ImageViewRGB32& frame){ - if (!frame){ - return 0; - } - const ImageViewRGB32 strip = extract_box_reference(frame, ImageFloatBox(0.431, 0.229, 0.005, 0.470)); - double best = 0.0; - double second = 0.0; - int position = 0; - for (int row = 0; row < 7; row++){ - const size_t top = static_cast(strip.height() * (row * 0.075 / 0.470)); - const size_t bottom = static_cast(strip.height() * ((row * 0.075 + 0.020) / 0.470)); - size_t red = 0; - size_t total = 0; - for (size_t y = top; y < bottom && y < strip.height(); y++){ - for (size_t x = 0; x < strip.width(); x++){ - const uint32_t pixel = strip.pixel(x, y); - const unsigned r = (pixel >> 16) & 255; - const unsigned g = (pixel >> 8) & 255; - const unsigned b = pixel & 255; - red += r > 180 && g < 120 && b < 120; - total++; - } - } - const double score = total == 0 ? 0.0 : static_cast(red) / total; - if (score > best){ - second = best; - best = score; - position = row + 1; - }else if (score > second){ - second = score; - } - } - return best >= 0.15 && best - second >= 0.10 ? position : 0; -} - -static ImageFloatBox bag_list_icon_box(size_t row){ - return ImageFloatBox(0.449, 0.212 + (row - 1) * 0.075, 0.033, 0.050); -} - -static ImageFloatBox bag_list_name_box(size_t row){ - // Previous height included the faint lower selection border in rows 2/6. - // return ImageFloatBox(0.492, 0.213 + (row - 1) * 0.075, 0.335, 0.052); - return ImageFloatBox(0.492, 0.213 + (row - 1) * 0.075, 0.335, 0.050); -} - -static double bag_row_patch_score(const ImageRGB32& reference, const ImageViewRGB32& frame, const ImageFloatBox& box){ - const ImageRGB32 live = extract_box_reference(frame, box).scale_to(reference.width(), reference.height()); - return ImageMatch::pixel_RMSD(reference, live); -} - -// Previous name-area empty-list check retained for reference. -// static bool bag_list_is_empty(const ImageViewRGB32& frame){ -// const ImageViewRGB32 names = extract_box_reference( -// frame, ImageFloatBox(0.492, 0.213, 0.335, 0.500) -// ); -// size_t dark = 0; -// for (size_t y = 0; y < names.height(); y++){ -// for (size_t x = 0; x < names.width(); x++){ -// const uint32_t pixel = names.pixel(x, y); -// dark += ((pixel >> 16) & 255) < 100 && -// ((pixel >> 8) & 255) < 100 && (pixel & 255) < 100; -// } -// } -// return dark < names.width() * names.height() * 0.001; -// } - -size_t bag_clear_tab_count(const ImageViewRGB32& frame){ - if (!frame || bag_pocket_position(frame) == 0){ - return 0; - } - static const std::vector> references = [] { - std::vector> refs; - for (const char* name : {"Normal", - "NormalMedium", - "NormalSmall", - "Alternate", - "AlternateMedium", - "AlternateSmall", - "Selected", - "SelectedMedium", - "SelectedSmall", - "SelectedBlue", - "SelectedBlueMedium", - "SelectedBlueSmall"}){ - refs.emplace_back( - std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/BagTabs/" + name + ".png")); - } - return refs; - }(); - size_t clear = 0; - for (size_t tab = 0; tab < 8; tab++){ - const double x = .570 + tab * .0421; - // A selected tab has a thin red outline. The NEW marker fills the - // interior of this upper-left patch, which the outline leaves white. - auto badge = extract_box_reference(frame, ImageFloatBox(x + .007, .074, .006, .014)); - size_t red = 0; - for (size_t y = 0; y < badge.height(); y++){ - for (size_t bx = 0; bx < badge.width(); bx++){ - const uint32_t pixel = badge.pixel(bx, y); - int rr = (pixel >> 16) & 255, gg = (pixel >> 8) & 255, bb = pixel & 255; - if (rr > 150 && rr > gg + 60 && rr > bb + 60){ - red++; - } - } - } - const size_t badge_area = badge.width() * badge.height(); - if (badge_area > 0 && (double)red / badge_area > .45){ - continue; - } - const ImageFloatBox box{x, .06, .038, .072}; - for (const auto& reference : references){ - ImageMatchDetector detector(reference, box, 50.0); - if (detector.detect(frame)){ - clear++; - break; - } - } - } - return clear; -} - -bool clear_new_item_flags(VideoStream& stream, ProControllerContext& context, int num_icons){ - stream.log("[Bag NEW flags] Opening bag to verify all eight tabs."); - open_menu(stream, context, MenuCursorPosition::BAG, num_icons); - pbf_wait(context, 1000ms); - auto all_clear = [&](){ - context.wait_for_all_requests(); - size_t confirmed = 0; - for (size_t check = 0; check < 3; check++){ - const auto frame = stream.video().snapshot(); - const size_t count = frame ? bag_clear_tab_count(*frame.frame) : 0; - stream.log("[Bag NEW flags] Clear tabs: " + std::to_string(count) + "/8."); - if (count != 8){ - return false; - } - confirmed++; - pbf_wait(context, 150ms); - context.wait_for_all_requests(); - } - return confirmed == 3; - }; - bool cleared = all_clear(); - for (size_t pocket = 0; pocket < 32 && !cleared; pocket++){ - if (pocket){ - pbf_press_dpad(context, DPAD_RIGHT, 80ms, 300ms); - } - // Let the initial selection and each subsequent item clear its NEW - // flag. Short taps avoid skipping rows through held-button repeat. - pbf_wait(context, 1000ms); - for (size_t tap = 0; tap < 12; tap++){ - pbf_press_dpad(context, DPAD_DOWN, 80ms, 1000ms); - } - cleared = all_clear(); - } - pbf_press_dpad(context, DPAD_UP, 3000ms, 250ms); - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - stream.log(cleared ? "[Bag NEW flags] All eight tabs clear; bag closed." - : "[Bag NEW flags] Full pocket pass did not clear all eight tabs; bag closed.", - cleared ? COLOR_GREEN : COLOR_ORANGE); - return cleared; -} - -int bag_pocket_position(const ImageViewRGB32& frame){ - if (!frame){ - return 0; - } - int selected = 0; - for (int pocket = 1; pocket <= 8; pocket++){ - // The lower left edge of each tab is below the red NEW badge. - // This strip includes both the normal outline and enlarged selection. - const auto edge = - extract_box_reference(frame, ImageFloatBox(0.570 + (pocket - 1) * 0.0421, 0.098, 0.0045, 0.021)); - size_t colored = 0; - for (size_t y = 0; y < edge.height(); y++){ - for (size_t x = 0; x < edge.width(); x++){ - const uint32_t pixel = edge.pixel(x, y); - const int r = (pixel >> 16) & 255; - const int g = (pixel >> 8) & 255; - const int b = pixel & 255; - // Red when focused, blue when the item list has focus. - colored += (r > 160 && r > g + 60 && r > b + 60) || (b > 160 && b > r + 40 && b > g + 20); - } - } - const double pixels = (double)edge.width() * edge.height(); - if (pixels > 0 && colored / pixels >= 0.18){ - if (selected != 0){ - return 0; - } - selected = pocket; - } - } - return selected; -} - -bool bag_list_is_empty(const ImageViewRGB32& frame){ - if (!frame || bag_pocket_position(frame) == 0 || bag_cursor_position(frame) != 0){ - return false; - } - // Empty pockets have no item icons. The central Empty label is outside - // this column; counting dark pixels in the name area included that label. - const auto icons = extract_box_reference(frame, ImageFloatBox(0.449, 0.213, 0.032, 0.500)); - const auto stats = image_stats(icons); - return stats.average.r > 220 && stats.average.g > 220 && stats.average.b > 220 && stats.stddev.sum() < 20; -} - - -std::array -match_bag_item_rows(const ImageViewRGB32& frame, const ImageViewRGB32& reference, const BagItemTemplate& item){ - std::array result; - if (!frame || !reference || item.reference_row < 1 || item.reference_row > 7){ - return result; - } - const ImageRGB32 icon = extract_box_reference(reference, bag_list_icon_box(item.reference_row)).copy(); - const ImageRGB32 name = extract_box_reference(reference, bag_list_name_box(item.reference_row)).copy(); - for (size_t row = 1; row <= 7; row++){ - auto& match = result[row - 1]; - match.icon_rmsd = bag_row_patch_score(icon, frame, bag_list_icon_box(row)); - match.name_rmsd = item.check_name ? bag_row_patch_score(name, frame, bag_list_name_box(row)) : 0.0; - match.matched = match.icon_rmsd <= item.max_icon_rmsd && match.name_rmsd <= item.max_name_rmsd; - } - return result; -} - -BagMovePlan plan_bag_item_move(int cursor_row, int item_row){ - if (cursor_row < 1 || cursor_row > 7 || item_row < 0 || item_row > 7){ - return {}; - } - if (item_row != 0){ - if (item_row == cursor_row){ - return {BagMoveAction::CONFIRM, 0}; - } - return {item_row > cursor_row ? BagMoveAction::DOWN : BagMoveAction::UP, - static_cast(std::abs(item_row - cursor_row))}; - } - if (cursor_row == 7){ - return {BagMoveAction::REWIND, 1}; - } - return {BagMoveAction::PAGE_DOWN, cursor_row == 1 ? 13 : static_cast(13 - cursor_row)}; -} - - -static int bag_confirm_pocket(VideoStream& stream, ProControllerContext& context){ - int previous = 0; - size_t matches = 0; - for (size_t check = 0; check < 10; check++){ - context.wait_for_all_requests(); - const auto frame = stream.video().snapshot(); - const int pocket = frame ? bag_pocket_position(frame) : 0; - matches = pocket != 0 && pocket == previous ? matches + 1 : pocket != 0 ? 1 : 0; - previous = pocket; - if (matches >= 3){ - return pocket; - } - pbf_wait(context, 100ms); - } - return 0; -} - -// static bool bag_advance_pocket(VideoStream& stream, ProControllerContext& context, int current){ -// const int expected = current % 8 + 1; -// for (size_t attempt = 0; attempt < 3; attempt++){ -// // Recheck before retrying: a delayed change must not cause a second step. -// const int observed = bag_confirm_pocket(stream, context); -// if (observed == expected) return true; -// if (observed != current){ -// stream.log("[Bag search] Pocket selection uncertain; stopping.", COLOR_ORANGE); -// return false; -// } -// pbf_press_dpad(context, DPAD_RIGHT, 80ms, 500ms); -// const int result = bag_confirm_pocket(stream, context); -// stream.log("[Bag search] Pocket change: " + std::to_string(current) + " -> " + -// std::to_string(result) + ", expected " + std::to_string(expected) + "."); -// if (result == expected) return true; -// } -// stream.log("[Bag search] Could not confirm pocket change after 3 attempts.", COLOR_ORANGE); -// return false; -// } - -BagPocketMovePlan plan_bag_pocket_move(int current, int target){ - if (current < 1 || current > 8 || target < 1 || target > 8){ - return {}; - } - const int right = (target - current + 8) % 8; - const int left = (current - target + 8) % 8; - if (right == 0){ - return {BagPocketAction::CONFIRM, 0}; - } - return right <= left ? BagPocketMovePlan{BagPocketAction::RIGHT, static_cast(right)} - : BagPocketMovePlan{BagPocketAction::LEFT, static_cast(left)}; -} - -static bool bag_navigate_to_pocket(VideoStream& stream, ProControllerContext& context, int target){ - int current = bag_confirm_pocket(stream, context); - for (size_t attempt = 0; attempt < 3; attempt++){ - const auto plan = plan_bag_pocket_move(current, target); - if (plan.action == BagPocketAction::CONFIRM){ - return true; - } - if (plan.action == BagPocketAction::UNKNOWN){ - stream.log("[Bag search] Selected pocket not detected.", COLOR_ORANGE); - return false; - } - stream.log("[Bag search] Pocket " + std::to_string(current) + " to " + std::to_string(target) + ": " + - (plan.action == BagPocketAction::RIGHT ? "right " : "left ") + std::to_string(plan.presses) + - " presses, attempt " + std::to_string(attempt + 1) + "/3."); - for (size_t press = 0; press < plan.presses; press++){ - pbf_press_dpad(context, plan.action == BagPocketAction::RIGHT ? DPAD_RIGHT : DPAD_LEFT, 80ms, 150ms); - } - pbf_wait(context, 350ms); - current = bag_confirm_pocket(stream, context); - stream.log( - "[Bag search] Final pocket=" + std::to_string(current) + ", expected=" + std::to_string(target) + "."); - if (current == target){ - return true; - } - } - stream.log("[Bag search] Target pocket not reached after 3 movement batches.", COLOR_ORANGE); - return false; -} - - -bool find_bag_item(VideoStream& stream, - ProControllerContext& context, - const BagItemTemplate& item, - int target_pocket, - int num_icons, - size_t max_scrolls_per_pocket, - std::chrono::milliseconds search_timeout){ - if (target_pocket < 1 || target_pocket > 8 || item.reference_row < 1 || item.reference_row > 7){ - stream.log("[Bag search] Invalid template reference row.", COLOR_ORANGE); - return false; - } - auto reference = std::make_shared(RESOURCE_PATH() + item.reference_path); - // const ImageRGB32 reference_icon = extract_box_reference( - // *reference, bag_list_icon_box(item.reference_row) - // ).copy(); - // const ImageRGB32 reference_name = extract_box_reference( - // *reference, bag_list_name_box(item.reference_row) - // ).copy(); - ImageMatchDetector bag(reference, ImageFloatBox(0.055, 0.012, 0.044, 0.045), 35.0, true); - const auto deadline = std::chrono::steady_clock::now() + search_timeout; - - open_menu(stream, context, MenuCursorPosition::BAG, num_icons); - pbf_wait(context, 1000ms); - context.wait_for_all_requests(); - - if (!bag_navigate_to_pocket(stream, context, target_pocket)){ - return false; - } - // Previously searched all eight pockets. Only inspect the requested pocket now. - // for (size_t pocket = 0; pocket < 8; pocket++){ - { - const int selected_pocket = bag_confirm_pocket(stream, context); - if (selected_pocket == 0){ - stream.log("[Bag search] Selected pocket not detected.", COLOR_ORANGE); - return false; - } - size_t commands = 0; - bool empty = false; - auto move = [&](DpadPosition direction, size_t count, bool check_rows = false){ - for (size_t press = 0; press < count; press++){ - if (commands >= max_scrolls_per_pocket || std::chrono::steady_clock::now() >= deadline){ - stream.log("[Bag search] Limit reached; search incomplete.", COLOR_ORANGE); - return false; - } - commands++; - pbf_press_dpad(context, direction, 80ms, 150ms); - if (check_rows){ - context.wait_for_all_requests(); - const VideoSnapshot page_frame = stream.video().snapshot(); - if (!page_frame || !bag.detect(page_frame)){ - return false; - } - const auto matches = match_bag_item_rows(page_frame, *reference, item); - for (const auto& match : matches){ - if (match.matched){ - // Stop paging; the main loop verifies and selects the row. - return true; - } - } - } - } - pbf_wait(context, 250ms); - context.wait_for_all_requests(); - return true; - }; - for (size_t pass = 0; pass < 3 && !empty; pass++){ - size_t matching_frames = 0; - size_t missing_cursor = 0; - size_t unchanged_pages = 0; - ImageRGB32 previous_page; - stream.log("[Bag search] Looking for " + item.label + ": pocket " + std::to_string(selected_pocket) + - ", pass " + std::to_string(pass + 1) + "/3."); - while (std::chrono::steady_clock::now() < deadline){ - const VideoSnapshot frame = stream.video().snapshot(); - if (!frame || !bag.detect(frame)){ - stream.log("[Bag search] Bag screen not detected.", COLOR_ORANGE); - return false; - } - const int cursor = bag_cursor_position(frame); - int target = 0; - double best = 1000.0; - bool ambiguous = false; - // The seven list ROIs move neither with the cursor nor with - // scrolling. Each contains an icon patch and a name patch. - // Previous per-row loop retained for reference. - // for (size_t row = 1; row <= 7; row++){ - // const double icon = bag_row_patch_score(reference_icon, frame, bag_list_icon_box(row)); - // const double name = item.check_name - // ? bag_row_patch_score(reference_name, frame, bag_list_name_box(row)) : 0.0; - // if (icon <= item.max_icon_rmsd && name <= item.max_name_rmsd){ - // if (target != 0){ - // ambiguous = true; - // } - // if (icon + name < best){ - // target = static_cast(row); - // best = icon + name; - // } - // } - // } - const auto matches = match_bag_item_rows(frame, *reference, item); - for (size_t row = 1; row <= 7; row++){ - const auto& match = matches[row - 1]; - if (match.matched){ - if (target != 0){ - ambiguous = true; - } - if (match.icon_rmsd + match.name_rmsd < best){ - target = static_cast(row); - best = match.icon_rmsd + match.name_rmsd; - } - } - } - stream.log("[Bag search] " + item.label + (target == 0 ? " is not detected" : " is detected") + - " on screen; item row=" + std::to_string(target) + ", cursor row=" + std::to_string(cursor) + "."); - if (ambiguous){ - stream.log("[Bag search] Multiple rows match; selection is uncertain.", COLOR_ORANGE); - return false; - } - if (cursor == 0){ - matching_frames = 0; - if (bag_list_is_empty(frame)){ - empty = true; - break; - } - if (++missing_cursor >= 3){ - stream.log("[Bag search] Cursor location uncertain.", COLOR_ORANGE); - return false; - } - pbf_wait(context, 200ms); - context.wait_for_all_requests(); - continue; - } - missing_cursor = 0; - if (target != 0){ - previous_page = ImageRGB32(); - unchanged_pages = 0; - // A unique list sprite/name match with the cursor on that - // row is sufficient; do not depend on the detail panel. - if (target == cursor){ - if (++matching_frames >= 3){ - stream.log("[Bag search] Item and cursor confirmed in row " + std::to_string(cursor) + ".", - COLOR_GREEN); - return true; - } - pbf_wait(context, 150ms); - context.wait_for_all_requests(); - continue; - } - matching_frames = 0; - const size_t count = plan_bag_item_move(cursor, target).presses; - stream.log("[Bag search] Moving " + std::string(target > cursor ? "down " : "up ") + - std::to_string(count) + " time(s), then checking again."); - if (!move(target > cursor ? DPAD_DOWN : DPAD_UP, count)){ - return false; - } - continue; - } - matching_frames = 0; - const ImageRGB32 page = - extract_box_reference(frame, ImageFloatBox(0.449, 0.212, 0.378, 0.500)).scale_to(378, 500); - if (previous_page && ImageMatch::pixel_RMSD(previous_page, page) <= 3.0){ - unchanged_pages++; - }else{ - unchanged_pages = 0; - } - previous_page = page.copy(); - // Row 7 signals the bottom of a long list. Repeated pages - // also cover shorter lists whose final row is less than 7. - if (cursor == 7 || unchanged_pages >= 2){ - stream.log("[Bag search] List end; returning to row 1."); - while (true){ - const VideoSnapshot rewind_frame = stream.video().snapshot(); - if (!rewind_frame || !bag.detect(rewind_frame)){ - return false; - } - const int rewind_cursor = bag_cursor_position(rewind_frame); - if (rewind_cursor == 1){ - break; - } - if (rewind_cursor == 0 || !move(DPAD_UP, 1)){ - return false; - } - } - break; - } - const size_t count = plan_bag_item_move(cursor, 0).presses; - stream.log("[Bag search] Advancing " + std::to_string(count) + " presses to another page."); - if (!move(DPAD_DOWN, count, true)){ - return false; - } - } - if (std::chrono::steady_clock::now() >= deadline){ - stream.log("[Bag search] Timeout; search incomplete.", COLOR_ORANGE); - return false; - } - } - // if (pocket < 7){ - // // pbf_press_dpad(context, DPAD_RIGHT, 80ms, 500ms); - // // context.wait_for_all_requests(); - // if (!bag_advance_pocket(stream, context, selected_pocket)) return false; - // } - } - stream.log("[Bag search] Item not found after three passes per nonempty pocket.", COLOR_ORANGE); - return false; -} - -bool find_bag_item_by_name( - VideoStream& stream, - ProControllerContext& context, - const std::string& name, - int target_pocket, - int num_icons -){ - open_menu(stream, context, MenuCursorPosition::BAG, num_icons); - pbf_wait(context, 1000ms); - return find_bag_item_in_open_bag(stream, context, name, target_pocket); -} - -bool find_bag_item_in_open_bag( - VideoStream& stream, - ProControllerContext& context, - const std::string& name, - int target_pocket -){ - context.wait_for_all_requests(); - if (!bag_navigate_to_pocket(stream, context, target_pocket)) return false; - - // No reader for the game's language: the cursor's row, the pocket's - // first, if the pocket has one. Early in the story the pocket holds only - // what was bought or given (Potions in Medicine). - if (!story_reads_text()){ - const VideoSnapshot frame = stream.video().snapshot(); - const bool any = frame && !bag_list_is_empty(*frame.frame) && bag_cursor_position(*frame.frame) != 0; - stream.log("[Bag search] " + name + ": no text reader for the game's language; " + - (any ? "taking the pocket's cursor row." : "the pocket is empty."), COLOR_ORANGE); - return any; - } - - // Walk down the list reading the cursor row. The list does not wrap, so - // turn around once the cursor stops moving. - DpadPosition direction = DPAD_DOWN; - std::string previous; - size_t same = 0; - size_t confirmed = 0; - size_t unknown_cursor = 0; - for (size_t scan = 0; scan < 128; scan++){ - context.wait_for_all_requests(); - const VideoSnapshot frame = stream.video().snapshot(); - if (!frame || bag_list_is_empty(*frame.frame)){ - stream.log("[Bag search] " + name + ": pocket is empty.", COLOR_ORANGE); - return false; - } - const int cursor = bag_cursor_position(*frame.frame); - if (cursor == 0){ - if (++unknown_cursor >= 3){ - stream.log("[Bag search] " + name + ": cursor not found.", COLOR_ORANGE); - return false; - } - pbf_wait(context, 200ms); - continue; - } - unknown_cursor = 0; - const std::string text = OCR::tesseract_ocr_read(Language::English, - extract_box_reference(*frame.frame, bag_list_name_box(cursor))); - std::string normalized; - for (unsigned char ch : text){ - if (std::isalpha(ch)) normalized.push_back((char)std::tolower(ch)); - } - stream.log("[Bag search] Row " + std::to_string(cursor) + " reads \"" + normalized + "\"."); - if (normalized == name){ - if (++confirmed >= 2){ - stream.log("[Bag search] " + name + " confirmed in row " + std::to_string(cursor) + ".", COLOR_GREEN); - return true; - } - pbf_wait(context, 150ms); - continue; - } - confirmed = 0; - same = normalized == previous ? same + 1 : 0; - previous = normalized; - if (same >= 2){ - if (direction == DPAD_UP) break; - direction = DPAD_UP; - same = 0; - } - pbf_press_dpad(context, direction, 80ms, 300ms); - } - stream.log("[Bag search] " + name + " not found.", COLOR_ORANGE); - return false; -} - -bool use_escape_rope( - VideoStream& stream, - ProControllerContext& context, - int num_icons -){ - const BagItemTemplate rope{ - // "PokemonBDSP/AutoStory/Bag_EscapeRope.png", "escape-rope" - "PokemonBDSP/Autostory/Bag_EscapeRope.png", "escape-rope" - }; - if (!find_bag_item(stream, context, rope, 5, num_icons)){ - return false; - } - - pbf_press_button(context, BUTTON_A, 80ms, 350ms); - context.wait_for_all_requests(); - SelectionArrowFinder use_menu(stream.overlay(), ImageFloatBox(0.660, 0.100, 0.310, 0.430), COLOR_GREEN); - if (wait_until(stream, context, 3s, {{use_menu}}) != 0){ - stream.log("[Escape Rope] Item action menu not detected.", COLOR_ORANGE); - return false; - } - - // Watch while selecting Use so the transition cannot happen before - // the watcher starts. Extra A presses handle a confirmation prompt. - BlackScreenWatcher transition; - // Escape Rope fades white in the supplied clips; support black transitions too. - WhiteScreenWatcher white_transition; - const int result = run_until(stream, - context, - [](ProControllerContext& ctx){ - for (size_t press = 0; press < 10; press++){ - pbf_press_button(ctx, BUTTON_A, 80ms, 500ms); - } - ctx.wait_for_all_requests(); - pbf_wait(ctx, 10s); - ctx.wait_for_all_requests(); - }, - {transition, white_transition}); - if (result < 0){ - stream.log("[Escape Rope] No exit transition detected.", COLOR_ORANGE); - return false; - } - BlackScreenWatcher finished(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 100, 10, BlackScreenWatcher::FinderType::GONE, 500ms); - WhiteScreenWatcher white_finished(COLOR_RED, - {0.1, 0.1, 0.8, 0.8}, - 500, - 10, - WhiteScreenWatcher::FinderType::GONE, - 500ms); - stream.log(std::string("[Escape Rope] ") + (result == 1 ? "White" : "Black") + " exit transition detected."); - // if (wait_until(stream, context, 15s, {{finished}}) != 0){ return false; } - const int finish_result = result == 1 ? wait_until(stream, context, 15s, {{white_finished}}) - : wait_until(stream, context, 15s, {{finished}}); - if (finish_result != 0){ - stream.log("[Escape Rope] Exit transition did not finish.", COLOR_ORANGE); - return false; - } - OverworldWatcher overworld; - if (wait_until(stream, context, 10s, {{overworld}}) != 0){ - stream.log("[Escape Rope] Overworld not detected after transition.", COLOR_ORANGE); - return false; - } - pbf_wait(context, 1000ms); - context.wait_for_all_requests(); - stream.log("[Escape Rope] Exit transition and overworld confirmed.", COLOR_GREEN); - return true; -} - - -bool activate_repel(VideoStream& stream, ProControllerContext& context, int num_icons, int repel_type){ - static const char* const slugs[] = {"repel", "super-repel", "max-repel"}; - if (repel_type < 0 || repel_type > 2){ - stream.log("activate_repel: type must be 0 (Repel), 1 (Super Repel), or 2 (Max Repel).", COLOR_RED); - return false; - } - const std::string slug = slugs[repel_type]; - stream.log("Activating " + slug); - open_menu(stream, context, MenuCursorPosition::BAG, num_icons); - context.wait_for_all_requests(); - pbf_wait(context, 1000ms); - - auto repel_tab_ref = std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTab.png"); - // A smaller reference covers capture-resize sampling differences without - // loosening the match threshold or changing shared image processing. - auto repel_tab_small_ref = - std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTabSmall.png"); - // Obtaining Honey adds a NEW badge to the same selected Other Items tab. - // Keep the original references and threshold for the earlier route stages. - auto repel_tab_honey_ref = - std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTabHoney.png"); - auto repel_tab_honey_small_ref = - std::make_shared(RESOURCE_PATH() + "PokemonBDSP/Autostory/Bag_RepelTabHoneySmall.png"); - const ImageFloatBox repel_tab_box{0.735000, 0.055000, 0.045000, 0.080000}; - const double repel_tab_rmsd = 50.0; - - // BDSP bag has 8 compartment tabs; check before each press so zero-press - // detection works when the bag already starts on the repel tab. - bool found = false; - for (int press = 0; press <= 8; press++){ - context.wait_for_all_requests(); - - ImageMatchWatcher watcher(repel_tab_ref, repel_tab_box, repel_tab_rmsd); - ImageMatchWatcher small_watcher(repel_tab_small_ref, repel_tab_box, repel_tab_rmsd); - ImageMatchWatcher honey_watcher(repel_tab_honey_ref, repel_tab_box, repel_tab_rmsd); - ImageMatchWatcher honey_small_watcher(repel_tab_honey_small_ref, repel_tab_box, repel_tab_rmsd); - int ret = - wait_until(stream, context, 400ms, {{watcher}, {small_watcher}, {honey_watcher}, {honey_small_watcher}}); - if (ret >= 0){ - stream.log("[AutoStory] activate_repel: repel tab found after " + std::to_string(press) + " press(es).", - COLOR_GREEN); - found = true; - break; - } - - if (press < 8){ - stream.log( - "[AutoStory] activate_repel: tab not found, pressing right (" + std::to_string(press + 1) + "/8)."); - pbf_press_dpad(context, DPAD_RIGHT, 80ms, 300ms); - } - } - - if (!found){ - stream.log("[AutoStory] activate_repel: failed to find repel compartment tab.", COLOR_RED); - return false; - } - - // Reuse the Bag cursor/name geometry and existing Tesseract backend. A later - // save may contain all three Repels and unrelated Other Items in any order. - bool item_found = false; - size_t confirmed = 0; - size_t unknown_cursor = 0; - for (size_t scan = 0; scan < 256; scan++){ - context.wait_for_all_requests(); - const auto frame = stream.video().snapshot(); - if (!frame || bag_list_is_empty(*frame.frame)){ - break; - } - const int cursor = bag_cursor_position(*frame.frame); - if (!cursor){ - if (++unknown_cursor >= 3){ - break; - } - pbf_wait(context, 200ms); - continue; - } - unknown_cursor = 0; - // Use the existing Tesseract entry point directly so item selection - // does not depend on the optional Paddle/ONNX runtime configuration. - const std::string text = - OCR::tesseract_ocr_read(Language::English, extract_box_reference(*frame.frame, bag_list_name_box(cursor))); - std::string normalized; - for (unsigned char ch : text){ - if (std::isalpha(ch)){ - normalized.push_back((char)std::tolower(ch)); - } - } - static const char* const names[] = {"repel", "superrepel", "maxrepel"}; - if (normalized == names[repel_type]){ - if (++confirmed >= 2){ - item_found = true; - break; - } - pbf_wait(context, 150ms); - }else{ - confirmed = 0; - // Search both directions so a non-wrapping list opened halfway - // down still reaches items above the initial cursor position. - pbf_press_dpad(context, scan < 128 ? DPAD_DOWN : DPAD_UP, 80ms, 300ms); - } - } - if (!item_found){ - stream.log("activate_repel: requested item not confirmed: " + slug, COLOR_RED); - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - return false; - } - stream.log("[AutoStory] activate_repel: selected item confirmed: " + slug, COLOR_GREEN); - - context.wait_for_all_requests(); - pbf_wait(context, 400ms); - pbf_press_button(context, BUTTON_A, 80ms, 300ms); - context.wait_for_all_requests(); - - // The Use-item popup follows the selected row, which may be anywhere - // in a later save's list. Exclude the Bag list cursor on the left. - const ImageFloatBox repel_item_box{0.700000, 0.100000, 0.280000, 0.650000}; - SelectionArrowFinder arrow_finder(stream.overlay(), repel_item_box, COLOR_GREEN); - int arrow_ret = wait_until(stream, context, 2000ms, {{arrow_finder}}); - if (arrow_ret != 0){ - stream.log("[AutoStory] activate_repel: selection arrow not found after pressing A.", COLOR_RED); - return false; - } - stream.log("[AutoStory] activate_repel: selection arrow confirmed.", COLOR_GREEN); - pbf_press_button(context, BUTTON_A, 80ms, 105ms); - pbf_mash_button(context, BUTTON_B, 2000ms); - return true; -} - - -void use_HM(VideoStream& stream, ProControllerContext& context, const std::string& label - -){ - stream.log("Using " + label); - const ImageFloatBox box{0.670000, 0.600000, 0.100000, 0.150000}; - SelectionArrowFinder arrow(stream.overlay(), box, COLOR_GREEN); - - int ret = run_until(stream, - context, - [](ProControllerContext& context){ - pbf_mash_button(context, BUTTON_A, 30000ms); - }, - {{arrow}}); - if (ret != 0){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Using " + label + ": HM confirmation arrow not found; stopping before further movement.", - stream); - } - - stream.log("Using " + label + ": selection arrow found, confirming.", COLOR_GREEN); - pbf_mash_button(context, BUTTON_A, 1000ms); - pbf_mash_button(context, BUTTON_B, 6000ms); -} - - -bool get_on_bicycle(VideoStream& stream, ProControllerContext& context, GearLevel /*gear*/){ - static const ImageFloatBox FRONT_BOX{0.440000, 0.470000, 0.035000, 0.054000}; - static const ImageFloatBox BACK_BOX{0.520000, 0.470000, 0.022000, 0.054000}; - // Spark color range: white (R=232,G=255,B=255) down to light green (R=163,G=217,B=181) - static const uint32_t SPARK_MIN = 0xFF000000u | (163u << 16) | (217u << 8) | 181u; - static const uint32_t SPARK_MAX = 0xFFFFFFFFu; - static const size_t SPARK_THRESHOLD = 13; - - stream.log("[AutoStory] get_on_bicycle: mounting bicycle.", COLOR_WHITE); - pbf_press_button(context, BUTTON_PLUS, 80ms, 800ms); - - // Press B, then poll up to 1 s for Fast-gear sparks. - // Logs the peak pixel counts in both boxes for threshold tuning. - // Returns true if sparks are detected (switched TO Fast gear). - auto toggle_and_check = [&]() -> bool { - context.wait_for_all_requests(); - pbf_press_button(context, BUTTON_B, 80ms, 800ms); - - size_t max_front = 0, max_back = 0; - auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000); - while (std::chrono::steady_clock::now() < deadline){ - context.wait_for(50ms); - VideoSnapshot screen = stream.video().snapshot(); - if (!screen){ - continue; - } - - size_t front_pixels = 0, back_pixels = 0; - filter_rgb32_range(front_pixels, - extract_box_reference(screen, FRONT_BOX), - SPARK_MIN, - SPARK_MAX, - Color(0), - false); - filter_rgb32_range(back_pixels, - extract_box_reference(screen, BACK_BOX), - SPARK_MIN, - SPARK_MAX, - Color(0), - false); - - if (front_pixels > max_front){ - max_front = front_pixels; - } - if (back_pixels > max_back){ - max_back = back_pixels; - } - - if (front_pixels >= SPARK_THRESHOLD || back_pixels >= SPARK_THRESHOLD){ - stream.log("[AutoStory] get_on_bicycle: sparks detected " - "(front=" + - std::to_string(front_pixels) + " back=" + std::to_string(back_pixels) + - " threshold=" + std::to_string(SPARK_THRESHOLD) + ").", - COLOR_GREEN); - return true; - } - } - stream.log("[AutoStory] get_on_bicycle: no sparks detected " - "(peak front=" + - std::to_string(max_front) + " back=" + std::to_string(max_back) + - " threshold=" + std::to_string(SPARK_THRESHOLD) + ").", - COLOR_ORANGE); - return false; - }; - - if (toggle_and_check()){ - stream.log("[AutoStory] get_on_bicycle: Fast gear confirmed.", COLOR_GREEN); - return true; - } - - // First toggle produced no sparks -> we switched to Slow gear; toggle back to Fast. - stream.log("[AutoStory] get_on_bicycle: Slow gear detected, toggling back to Fast.", COLOR_ORANGE); - if (toggle_and_check()){ - stream.log("[AutoStory] get_on_bicycle: Fast gear confirmed after correction.", COLOR_GREEN); - return true; - } - - stream.log("[AutoStory] get_on_bicycle: Failed to confirm Fast gear after two toggles.", COLOR_RED); - return false; -} - - -} -} -} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h deleted file mode 100644 index dd1f9ff41a..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h +++ /dev/null @@ -1,538 +0,0 @@ -/* BDSP AutoStory Tools - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStoryTools_H -#define PokemonAutomation_PokemonBDSP_AutoStoryTools_H - -#include -#include -#include -#include -#include - -#include "Common/Cpp/Options/EnumDropdownOption.h" -#include "Pokemon/Navigation/Pokemon_StorySegments.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" - -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/Language.h" -#include "CommonFramework/VideoPipeline/VideoOverlay.h" -#include "CommonFramework/Notifications/EventNotificationsTable.h" -#include "CommonFramework/ProgramStats/StatsTracking.h" -#include "CommonFramework/Tools/VideoStream.h" -#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" - -#include "CommonTools/VisualDetector.h" - -#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" - -#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" - -#include "Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h" -#include "Utils/PokemonBDSP_AutoStory_OverlayLog.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - - -// --------------------------------------------------------------------------- -// Stats -// --------------------------------------------------------------------------- - -struct AutoStoryStats : public StatsTracker { - AutoStoryStats() - : m_checkpoint(m_stats["Checkpoint"]) - , m_segment(m_stats["Segment"]) - , m_reset(m_stats["Reset"]) - , m_current(m_stats["Current Checkpoint (00.0X = X)"]) - , m_completed(m_stats["Checkpoints Completed"]) - , m_last_completed(m_stats["Last Completed (00.0X = X)"]) - { - m_display_order.emplace_back("Checkpoint"); - m_display_order.emplace_back("Segment"); - m_display_order.emplace_back("Reset"); - m_display_order.emplace_back("Current Checkpoint (00.0X = X)"); - m_display_order.emplace_back("Checkpoints Completed"); - m_display_order.emplace_back("Last Completed (00.0X = X)"); - } - std::atomic& m_checkpoint; - std::atomic& m_segment; - std::atomic& m_reset; - std::atomic& m_current; - std::atomic& m_completed; - std::atomic& m_last_completed; - bool m_first_run = true; -}; - - -// --------------------------------------------------------------------------- -// Options passed to every segment/checkpoint -// --------------------------------------------------------------------------- - -enum class StarterChoice { - TURTWIG, - CHIMCHAR, - PIPLUP, -}; - -enum class HelpMode { - NO_OUTSIDE_HELP, - OUTSIDE_HELP, -}; - -enum class OutsideHelpPokemon { - TORTERRA, -}; - -enum class TorterraMove { - EARTHQUAKE, - WOOD_HAMMER, - RAZOR_LEAF, - CRUNCH, - GIGA_DRAIN, - IRON_TAIL, - ROCK_TOMB, - FACADE, - STONE_EDGE, - ROCK_SLIDE, - BULLDOZE, - STRENGTH, - ROCK_SMASH, - ROCK_CLIMB, -}; - -// Returns the JSON slug for a configured helper move. -const char* torterra_move_slug(TorterraMove move); - -enum class Trainer { - Veilstone_GalacticGrunts_2v2, - // Extend with new trainer battle IDs as the AutoStory adds them. -}; - -// Returns the trainers.json battle-ID string for a Trainer value. -const char* trainer_slug(Trainer t); - -struct OutsideHelpConfig { - OutsideHelpPokemon pokemon; - TorterraMove attacks[4]; -}; - -enum class CheckpointSaveMode { - REAL, - FAKE -}; -// Real saves retry one checkpoint. Fake-save runs own one retry loop around -// the selected range; inner checkpoints propagate failures to that owner. -enum class CheckpointRetryScope { - CHECKPOINT, - SELECTED_RANGE, - DEFER_TO_SELECTED_RANGE -}; - -struct AutoStoryOptions { - Language language; - StarterChoice starter_choice; - EventNotificationOption& notif_status_update; - HelpMode help_mode; - OutsideHelpConfig outside_help; - CheckpointSaveMode checkpoint_save_mode = CheckpointSaveMode::REAL; - CheckpointRetryScope checkpoint_retry_scope = CheckpointRetryScope::CHECKPOINT; - // How the story tables retry a checkpoint (StoryRunOptions). - Pokemon::SegmentPolicy policy = {}; - // What the story steps confirm on screen (StoryRunOptions). - Pokemon::StoryChecks checks = Pokemon::StoryChecks::THOROUGH; -}; - - -// --------------------------------------------------------------------------- -// Base classes -// --------------------------------------------------------------------------- - -class AutoStory_Segment { -public: - virtual ~AutoStory_Segment() = default; - virtual std::string name() const = 0; - virtual std::string start_text() const = 0; - virtual std::string end_text() const = 0; - virtual void run_segment(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const = 0; -}; - -class AutoStory_Checkpoint { -public: - virtual ~AutoStory_Checkpoint() = default; - virtual std::string name() const = 0; - virtual std::string start_text() const = 0; - virtual std::string end_text() const = 0; - virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const = 0; -}; - - -// --------------------------------------------------------------------------- -// Shared utility functions (implementations in .cpp) -// --------------------------------------------------------------------------- - -// Save the game from the overworld with retries. Returns true if the save was -// verified via overworld detection, false if the blind fallback was used. -// With `expected_location`, the map named on the save screen's card must be -// it (compared as a location banner is), or the save is backed out of and -// the function fails with a screenshot: r124a saved in Sandgem Town after a -// walk that was sure it stood on Route 202. -bool save_game( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - const std::string& expected_location = "" -); - -// Opens the save screen from the overworld, reads where the game says we -// are (read_save_screen_location() in the Location module), and backs out -// without saving. -std::string read_location_from_save_screen(VideoStream& stream, ProControllerContext& context); - -// Fake save game to be replaced with save_game when program runs smoothly -void checkpoint_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - -// Save the game from the overworld, increment the checkpoint counter, -// update stats display, and send a status notification. -void checkpoint_save(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats, - CheckpointSaveMode save_mode = CheckpointSaveMode::REAL); - -// Reload the game from the most recent backup save (auto-save). -// Called on retry when a checkpoint has no hard save (save=false). -// TODO: implement this function. -void reload_backup_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - -// Lab: checkpoint_reattempt_loop() rethrows the first failure after its -// screenshot instead of resetting the game, so the game stays where it -// failed. Set from the program's option when it starts. -extern bool g_stop_at_first_failure; - -// If save=true (default): save the game before the first attempt, then run -// `action(attempt_number)`. On failure, reset from HOME and retry. -// If save=false: skip the hard save. On failure, reload from backup save and retry. -// SELECTED_RANGE skips the entry save and owns HOME resets for the range. -// DEFER_TO_SELECTED_RANGE lets an inner failure reach that outer retry loop. -// Throws a fatal error after too many consecutive failures. -void checkpoint_reattempt_loop(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats, - std::function&& action, - bool save = true, - CheckpointSaveMode save_mode = CheckpointSaveMode::REAL, - CheckpointRetryScope retry_scope = CheckpointRetryScope::CHECKPOINT); - -// Forward the explicit checkpoint save mode selected by AutoStory's options. -inline void checkpoint_reattempt_loop(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats, - std::function&& action, - bool save = true){ - checkpoint_reattempt_loop(env, - context, - options.notif_status_update, - stats, - std::move(action), - save, - options.checkpoint_save_mode, - options.checkpoint_retry_scope); -} - -// Execute every action in `actions` in order, repeated `times` times. -void repeat(std::initializer_list> actions, size_t times); - -// Tracks the last D-pad direction pressed. Pass one per navigation function -// so each function gets a clean slate on each attempt. -struct DpadState { - DpadPosition last_dir = DPAD_NONE; -}; - -// Press `dir` on the D-pad `times` times (plus one extra if the direction -// changed from the previous call, since the first press turns the character). -void repeat_dpad(ProControllerContext& context, - DpadState& state, - DpadPosition dir, - Milliseconds press, - Milliseconds hold, - size_t times, - bool include_last_dir = true); - -// Wait (without pressing any buttons) until a short dialogue box appears. -// Logs `label + " dialogue detected."` when found (omit label to skip logging). -// Throws OperationFailedException if no dialogue appears within `timeout`. -void wait_for_dialogue(VideoStream& stream, - ProControllerContext& context, - const std::string& label = "", - Milliseconds timeout = std::chrono::seconds(60)); - -// Mash `button` (default B) until the game returns to the free-roaming overworld -// (no dialogue, menu, or battle UI visible). -// Throws OperationFailedException if the overworld is not reached within `timeout`. -void mash_until_dialogue_ends(VideoStream& stream, - ProControllerContext& context, - Button button = BUTTON_B, - Milliseconds timeout = std::chrono::seconds(60)); - -// Scripted trainer battle: choose a 1-based move position each turn, then -// confirm the default target with A and clear narration with B. No target steering. -// A single position is reused for the whole battle. A list is consumed in -// order, then its final position is reused. Empty lists and slots outside 1-4 fail. -// post_battle_dialogue_cleanup must be chosen explicitly: false returns after -// the completed battle-exit transition; true also clears post-fight NPC dialogue. -// Examples: -// fight_trainer_using_moves(stream, context, 2, "Grunt", false); -// const std::vector moves{2, 1, 3}; -// fight_trainer_using_moves(stream, context, moves, "Gym leader", true); -void fight_trainer_using_moves(VideoStream& stream, - ProControllerContext& context, - size_t move, - const std::string& label, - bool post_battle_dialogue_cleanup); -void fight_trainer_using_moves(VideoStream& stream, - ProControllerContext& context, - const std::vector& moves, - const std::string& label, - bool post_battle_dialogue_cleanup); - -// A full Bag screenshot with the desired item highlighted. Paths are relative -// to RESOURCE_PATH(); quantities and selection borders are excluded. -struct BagItemTemplate { - std::string reference_path; - std::string label; - ImageFloatBox icon_box{0.428, 0.754, 0.029, 0.044}; - ImageFloatBox name_box{0.460, 0.755, 0.350, 0.045}; - double max_icon_rmsd = 25.0; - double max_name_rmsd = 20.0; - bool check_name = true; - // Selected item row in the reference screenshot (1-7). - size_t reference_row = 1; -}; - -// Screenshot-only helpers shared by live search and developer tests. -// Cursor 0 means unknown. Array index 0 corresponds to visible row 1. -struct BagItemRowMatch { - double icon_rmsd = 1000.0; - double name_rmsd = 1000.0; - bool matched = false; -}; - -// Pocket positions 1-8: Medicine, Balls, Battle Items, Berries, Other Items, -// TMs, Treasures, Key Items. Zero means the selected tab is uncertain. -int bag_pocket_position(const ImageViewRGB32& frame); -// Count normal, unobscured pocket icons; NEW badges prevent a tab match. -size_t bag_clear_tab_count(const ImageViewRGB32& frame); -// Open bag, pause 1s on the initial selection in each pocket, then scan -// with 80ms Down taps / 1s rest until all 8 tabs match, -// then restore the top of the item list and close the bag. -// Returns false after four bounded pocket passes. -bool clear_new_item_flags(VideoStream& stream, ProControllerContext& context, int num_icons = 8); -bool bag_list_is_empty(const ImageViewRGB32& frame); -int bag_cursor_position(const ImageViewRGB32& frame); -std::array -match_bag_item_rows(const ImageViewRGB32& frame, const ImageViewRGB32& reference, const BagItemTemplate& item); - -enum class BagMoveAction { - UNKNOWN, - CONFIRM, - UP, - DOWN, - PAGE_DOWN, - REWIND -}; -struct BagMovePlan { - BagMoveAction action = BagMoveAction::UNKNOWN; - size_t presses = 0; -}; - -// Describes the next input without sending it. REWIND means repeat Up and -// recheck until row 1; a screenshot cannot tell the total distance to the top. -BagMovePlan plan_bag_item_move(int cursor_row, int item_row); - -enum class BagPocketAction { - UNKNOWN, - CONFIRM, - LEFT, - RIGHT -}; -struct BagPocketMovePlan { - BagPocketAction action = BagPocketAction::UNKNOWN; - size_t presses = 0; -}; -// Circular eight-pocket route; ties prefer Right. Invalid positions send no input. -BagPocketMovePlan plan_bag_pocket_move(int current, int target); - - -// Starts in the overworld, opens the Bag, and leaves the requested item -// highlighted. Navigates to the requested pocket, then searches its item rows. -// Checks all seven visible rows and the cursor, then confirms the selected -// item in the green header. Makes up to three passes per pocket. -// Limits report an incomplete search, not absence. -// Returns false on missing frames, timeout, or no match; does not use an item. -bool find_bag_item(VideoStream& stream, - ProControllerContext& context, - const BagItemTemplate& item, - int target_pocket, - int num_icons = 8, - size_t max_scrolls_per_pocket = 512, - std::chrono::milliseconds search_timeout = std::chrono::minutes(10)); - -// Starts in the overworld, opens the Bag on `target_pocket`, and moves the cursor -// onto the item whose name reads `name` (letters only, lower case, e.g. "potion"). -// Reads the cursor row's name with OCR, so it does not depend on the capture -// card's colors or sharpness the way a template match does. -// Returns false if the pocket is empty or the name is not found. -bool find_bag_item_by_name( - VideoStream& stream, - ProControllerContext& context, - const std::string& name, - int target_pocket, - int num_icons = 8 -); - -// The same search in a Bag that is already open, such as the one a battle's -// Bag command opens. -bool find_bag_item_in_open_bag( - VideoStream& stream, - ProControllerContext& context, - const std::string& name, - int target_pocket -); - -// Find Escape Rope, choose Use, and verify a transition back to the overworld. -bool use_escape_rope(VideoStream& stream, ProControllerContext& context, int num_icons = 8); - -// Reuse the tutorial's verified Jubilife Center alignment and entry. -void align_and_enter_jubilife_center(VideoStream& stream, ProControllerContext& context); - -// Open the bag, navigate to the repel compartment tab, and use a repel. -// Returns false if the repel compartment tab could not be found. -// num_icons: early/late menu layout (preserved for existing callers). -// repel_type: 0=Repel, 1=Super Repel, 2=Max Repel. English item names. -bool activate_repel(VideoStream& stream, ProControllerContext& context, int num_icons = 8, int repel_type = 0); - -// Press A until the any HM selection arrow appears, confirm with A, then mash B for 3 s. -void use_HM(VideoStream& stream, ProControllerContext& context, const std::string& label); - -// Walk to the nurse, heal the party, and exit the Pokemon Center. -// `label` names the center for log messages (e.g. "Veilstone"). -// Returns false if the exit black screen was not detected. -bool heal_pokemon(VideoStream& stream, ProControllerContext& context, const std::string& label); - -enum class FlyPoint { - CanalavCity, - CelesticTown, - EternaCity, - FloaromaTown, - HearthomeCity, - JubilifeCity, - OreburghCity, - PastoriaCity, - PokemonLeagueLower, - PokemonLeagueUpper, - Route221, - SandgemTown, - SolaceonTown, - SnowpointCity, - SunyshoreCity, - TwinleafTown, - VeilstoneCity, - CurrentCity, // cursor already starts on the city; skip all map navigation -}; - -std::string fly_point_name(FlyPoint place); - -// Open the map, move the cursor to either bottom left, bottom right or -// top left, then move the cursor a number of steps, then checks for -// correctness, and mashes A until a black screen is detected -bool fly_to(VideoStream& stream, ProControllerContext& context, FlyPoint place); - -// Opens the X-menu from the overworld, navigates the cursor to `target`, and presses A. -// num_icons: 5 (early-game layout) or 8 (late-game layout). -void open_menu(VideoStream& stream, ProControllerContext& context, MenuCursorPosition target, int num_icons); - -// Open the Bag from the overworld, look for `name` in the pocket, and close -// it again. True if the item is there. -bool bag_has_item( - VideoStream& stream, - ProControllerContext& context, - const std::string& name, - int target_pocket -); - -// Read the lead's HP bar in the Bag and, if it is below `heal_below` (a -// fraction of full), use Potions until it is full. heal_below = 1 always -// heals; 0.5 only checks and heals when the lead is under half, and is -// skipped when the Bag has no Potion. -void use_potion_first_pokemon( - VideoStream& stream, - ProControllerContext& context, - double heal_below = 1.0 -); - -enum class GearLevel { - Fast, -}; - - -// --------------------------------------------------------------------------- -// Mart buying -// --------------------------------------------------------------------------- - -enum class ShopItem { - FullRestore, - MaxPotion, - HyperPotion, - SuperPotion, - Potion, - Antidote, - ParalyzHeal, - Awakening, - Repel, - SuperRepel, - MaxRepel, - EscapeRope, -}; - -// Pass as the `quantity` field to buy the maximum available amount. -static constexpr int BUY_MAX = -1; - -struct ItemToBuy { - ShopItem item; - int quantity; // positive count to buy, or BUY_MAX -}; - -// Walk up to a vendor (already talking to them), navigate through the Buy menu, -// and purchase each item in `items` in order. The function presses DPAD_DOWN -// until the matching item name is found via OCR, so items may appear in any -// list order. Returns true on success. -bool buy_items(VideoStream& stream, - ProControllerContext& context, - const std::vector& items, - Language language = Language::English); - - -// Mount the bicycle (press +) and confirm it is in the requested gear. -// Presses B to toggle gear and uses the Fast-gear spark visual effect to -// determine which gear was entered. If the wrong gear is entered, presses B -// again and re-checks. Player must be on foot when called. -// Returns false if gear state cannot be confirmed after two toggles. -bool get_on_bicycle(VideoStream& stream, ProControllerContext& context, GearLevel gear); - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.cpp new file mode 100644 index 0000000000..2e8826bbef --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.cpp @@ -0,0 +1,340 @@ +/* BDSP AutoStory - Actions + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_Actions.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +// Opens the save prompt from the overworld and keeps a screenshot of it: it +// shows the play time and badges, a record of where each checkpoint save +// stands. Returns the map its card names. +std::string open_save_prompt(VideoStream& stream, ProControllerContext& context){ + overworld_to_menu(stream, context); + pbf_press_button(context, BUTTON_R, 80ms, 2000ms); + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + if (!screen){ + return ""; + } + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/saves/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = folder + now_to_filestring() + ".png"; + screen.frame->save(path); + stream.log("Save prompt screenshot: " + path); + const std::string location = read_save_screen_location(*screen.frame); + stream.log("Save screen location: \"" + location + "\"."); + return location; +} + +// Backs out of the save prompt and its menu to the overworld. +void leave_save_prompt(VideoStream& stream, ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(15), {overworld}) < 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Failed to detect the overworld after leaving the save prompt.", stream); + } +} + +struct IconGrid { + int row, col; +}; + +IconGrid icon_to_grid(MenuCursorPosition pos, int num_icons){ + switch (pos){ + case MenuCursorPosition::POKEDEX: + return {0, 0}; + case MenuCursorPosition::POKEMON: + return {0, 1}; + case MenuCursorPosition::BAG: + return {0, 2}; + case MenuCursorPosition::CARD: + return {0, 3}; + case MenuCursorPosition::OPTIONS: + return {1, num_icons <= 5 ? 0 : 2}; + case MenuCursorPosition::MAP: + return {1, 0}; + case MenuCursorPosition::CAPSULES: + return {1, 1}; + case MenuCursorPosition::MYSTERYGIFT: + return {1, 3}; + default: + return {-1, -1}; + } +} + +} + +std::string read_location_from_save_screen(VideoStream& stream, ProControllerContext& context){ + const std::string location = open_save_prompt(stream, context); + leave_save_prompt(stream, context); + return location; +} + +bool save_game( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::string& expected_location +) { + static constexpr size_t MAX_RETRIES = 3; + + std::string wrong_place; + for (size_t attempt = 0; attempt < MAX_RETRIES && wrong_place.empty(); attempt++){ + try { + env.console.log("Checkpoint save attempt: " + std::to_string(attempt + 1)); + + // Only mash B on retries - no need to on first attempt + // if already cleanly in the overworld. + if (attempt > 0){ + pbf_mash_button(context, BUTTON_B, 1000ms); + } + + // The feedback-based save of save_game(VideoStream&, ...), split at the + // save prompt to check where it stands. + const std::string location = open_save_prompt(env.console, context); + if (!expected_location.empty() && !location_banner_matches(location, expected_location)){ + wrong_place = "About to save in \"" + location + "\", expected " + expected_location + "; not saving."; + env.console.log(wrong_place, COLOR_RED); + leave_save_prompt(env.console, context); + break; + } + pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); + + // Return to overworld cleanly. + pbf_mash_button(context, BUTTON_B, 1000ms); + + // Drain R/confirm/B before detection; an earlier overworld frame + // must not report success while saving commands are still queued. + context.wait_for_all_requests(); + + // Verify overworld. Generous timeout: save animation + fade can be slow. + OverworldWatcher overworld; + int ret = wait_until(env.console, context, std::chrono::seconds(15), {overworld}); + if (ret < 0){ + throw OperationFailedExceptionWithScreenshot(ErrorReportMode::SEND_ERROR_REPORT, + "Failed to detect overworld after save.", + env.console); + } + + env.console.log("Checkpoint save verified."); + return true; + + } catch (OperationFailedException& e){ + env.console.log("Checkpoint save failed: " + e.message(), COLOR_RED); + context.wait_for(1000ms); + } + } + // No blind fallback for the wrong place: it would save there. + if (!wrong_place.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, wrong_place, env.console); + } + + // Blind fallback: reuse the non-feedback save_game(context) from GameNavigation + // rather than duplicating the button sequence manually. + env.console.log("All save retries failed. Using blind fallback.", COLOR_ORANGE); + pbf_mash_button(context, BUTTON_B, 1000ms); + save_game(context); // PokemonBDSP_GameNavigation.h - non-feedback overload + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + + return false; +} + +void wait_for_dialogue(VideoStream& stream, + ProControllerContext& context, + const std::string& label, + Milliseconds timeout){ + auto log_detected = [&] { + if (!label.empty()){ + stream.log(label + " dialogue detected."); + } + }; + + ShortDialogWatcher watcher_cyan(COLOR_CYAN); + int ret = run_until(stream, + context, + [timeout](ProControllerContext& context){ + context.wait_for(timeout); + }, + {{watcher_cyan}}); + if (ret == 0){ + log_detected(); + return; + } + + ShortDialogWatcher watcher_red(COLOR_RED); + ret = run_until(stream, + context, + [timeout](ProControllerContext& context){ + context.wait_for(timeout); + }, + {{watcher_red}}); + if (ret == 0){ + log_detected(); + return; + } + + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + (label.empty() ? "wait_for_dialogue" : label) + ": dialogue not detected within timeout.", + stream); +} + +void open_menu(VideoStream& stream, ProControllerContext& context, MenuCursorPosition target, int num_icons){ + using namespace std::chrono_literals; + + // Previous single opening attempt: + // context.wait_for_all_requests(); + // overworld_to_menu(stream, context); + // pbf_wait(context, 1000ms); + // context.wait_for_all_requests(); + context.wait_for_all_requests(); + + MenuCursorDetector detector; + { + static const char* ICON_NAMES[MENU_CURSOR_NUM_ICONS] = { + "POKEDEX", + "POKEMON", + "BAG", + "CARD", + "OPTIONS", + "MAP", + "CAPSULES", + "MYSTERYGIFT", + }; + // Previous optional template directory: PokemonBDSP/AutoStory. + std::string ref_dir = RESOURCE_PATH() + "PokemonBDSP/Autostory"; + for (int i = 0; i < MENU_CURSOR_NUM_ICONS; i++){ + std::string path = ref_dir + "/" + ICON_NAMES[i] + ".png"; + // These templates are optional. The dark-pixel cursor detector + // remains available when a template is not packaged. + if (!Filesystem::is_regular_file(path)){ + continue; + } + // Previously constructed the image before checking its availability. + ImageRGB32 img(path); + if (img){ + detector.set_icon_reference(static_cast(i), std::move(img)); + } + } + } + // Previous single detection attempt retained for comparison. + // MenuCursorPosition cur = MenuCursorPosition::UNKNOWN; + // for (int i = 0; i < 10 && cur == MenuCursorPosition::UNKNOWN; i++){ + // context.wait_for(50ms); + // VideoSnapshot snap = stream.video().snapshot(); + // if (snap) cur = detector.detect(snap); + // } + // if (cur == MenuCursorPosition::UNKNOWN){ + // OperationFailedExceptionWithScreenshot::fire( + // ErrorReportMode::SEND_ERROR_REPORT, + // "Menu cursor not detected after opening menu.", + // stream + // ); + // } + + MenuCursorPosition cur = MenuCursorPosition::UNKNOWN; + bool menu_confirmed_open = false; + for (size_t attempt = 1; attempt <= 3; attempt++){ + stream.log("[Open menu] Open and detect attempt " + std::to_string(attempt) + "/3."); + try { + // A late-opening menu may already be visible after a timeout. + // Check before pressing X again, since X would close that menu. + if (!menu_confirmed_open){ + MenuWatcher already_open; + if (wait_until(stream, context, 300ms, {{already_open}}) < 0){ + overworld_to_menu(stream, context); + }else{ + stream.log("[Open menu] Menu already visible; skipping X."); + } + menu_confirmed_open = true; + }else{ + // Cursor detection retries send no input that could close the + // confirmed menu. Keep observing it instead of toggling X. + stream.log("[Open menu] Retrying cursor detection in the open menu."); + } + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + for (int frame = 0; frame < 10 && cur == MenuCursorPosition::UNKNOWN; frame++){ + context.wait_for(50ms); + const VideoSnapshot snapshot = stream.video().snapshot(); + if (snapshot){ + cur = detector.detect(snapshot); + } + } + if (cur != MenuCursorPosition::UNKNOWN){ + stream.log("[Open menu] Menu and cursor detected on attempt " + std::to_string(attempt) + ".", + COLOR_GREEN); + break; + } + stream.log("[Open menu] Menu cursor not detected on attempt " + std::to_string(attempt) + ".", + COLOR_ORANGE); + } catch (OperationFailedException& error){ + stream.log("[Open menu] Attempt " + std::to_string(attempt) + " failed: " + error.message(), COLOR_ORANGE); + } + } + if (cur == MenuCursorPosition::UNKNOWN){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Menu or cursor detection failed after 3 opening attempts.", + stream); + } + + IconGrid cur_g = icon_to_grid(cur, num_icons); + IconGrid tgt_g = icon_to_grid(target, num_icons); + if (tgt_g.row == -1){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Unknown target menu icon.", + stream); + } + + if (cur_g.row != tgt_g.row){ + DpadPosition vert = (cur_g.row < tgt_g.row) ? DPAD_DOWN : DPAD_UP; + pbf_press_dpad(context, vert, 80ms, 300ms); + context.wait_for_all_requests(); + } + + int col_diff = tgt_g.col - cur_g.col; + if (col_diff != 0){ + DpadPosition horiz = (col_diff > 0) ? DPAD_RIGHT : DPAD_LEFT; + for (int i = 0; i < std::abs(col_diff); i++){ + pbf_press_dpad(context, horiz, 80ms, 300ms); + context.wait_for_all_requests(); + } + } + + pbf_press_button(context, BUTTON_A, 80ms, 200ms); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.h new file mode 100644 index 0000000000..a2fa75786e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.h @@ -0,0 +1,63 @@ +/* BDSP AutoStory - Actions + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * The game's menus as the story uses them: saving, opening the X menu, and + * waiting for a dialogue. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Actions_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Actions_H + +#include +#include +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Save the game from the overworld with retries. Returns true if the save was +// verified via overworld detection, false if the blind fallback was used. +// With `expected_location`, the map named on the save screen's card must be +// it (compared as a location banner is), or the save is backed out of and +// the function fails with a screenshot: r124a saved in Sandgem Town after a +// walk that was sure it stood on Route 202. +bool save_game( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context, + const std::string& expected_location = "" +); + +// Opens the save screen from the overworld, reads where the game says we +// are (read_save_screen_location() in the Location module), and backs out +// without saving. +std::string read_location_from_save_screen(VideoStream& stream, ProControllerContext& context); + +// Wait (without pressing any buttons) until a short dialogue box appears. +// Logs `label + " dialogue detected."` when found (omit label to skip logging). +// Throws OperationFailedException if no dialogue appears within `timeout`. +void wait_for_dialogue( + VideoStream& stream, + ProControllerContext& context, + const std::string& label = "", + Milliseconds timeout = std::chrono::seconds(60) +); + +// Opens the X-menu from the overworld, navigates the cursor to `target`, and presses A. +// num_icons: 5 (early-game layout) or 8 (late-game layout). +void open_menu(VideoStream& stream, ProControllerContext& context, MenuCursorPosition target, int num_icons); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.cpp new file mode 100644 index 0000000000..6abbf23dde --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.cpp @@ -0,0 +1,271 @@ +/* BDSP AutoStory - Actions Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The new game's intro decisions, and the screenshot checks behind the + * Pokemon Center, the save card and the Jubilife School's lamp. Screenshots + * are in the unit test resources (the CommandLineTests checkout); a test + * whose screenshot is missing is skipped. + * + */ + +#include +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_Center.h" +#include "PokemonBDSP_AutoStory_NewGame.h" +#include "PokemonBDSP_AutoStory_Actions_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + + +class Test_IntroDecisions : public UnitTest{ +public: + Test_IntroDecisions() + : UnitTest("PokemonBDSP::AutoStory - new-game intro decisions") + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + struct Case{ + const char* screen; + IntroProgress progress; + IntroStep expected; + }; + const IntroProgress start{false, false}; + const IntroProgress named{true, false}; + const IntroProgress done{true, true}; + const Case cases[] = { + {"user-select", start, IntroStep::SELECT_USER}, + {"home", done, IntroStep::FAIL_NOT_IN_GAME}, + {"keyboard", start, IntroStep::ENTER_PLAYER_NAME}, + {"keyboard", named, IntroStep::CANCEL_RIVAL_KEYBOARD}, + {"rival-list-tell", named, IntroStep::MOVE_TO_PRESET}, + {"rival-list-preset", named, IntroStep::PICK_PRESET}, + {"menu", done, IntroStep::CLOSE_MENU_AND_FINISH}, + // The appearance grid has its prompt in a dialogue box. + {"dialog", start, IntroStep::ADVANCE}, + {"dialog", done, IntroStep::ADVANCE}, + // Language and the appearance preview: no X before the bedroom is possible. + {"other", start, IntroStep::ADVANCE}, + {"other", named, IntroStep::ADVANCE}, + {"other", done, IntroStep::PROBE_FOR_MENU}, + }; + for (const Case& c : cases){ + const IntroStep actual = decide_intro_step(c.screen, c.progress); + if (actual != c.expected){ + return std::string("Screen \"") + c.screen + "\" (player named " + + (c.progress.player_named ? "yes" : "no") + ", rival named " + + (c.progress.rival_named ? "yes" : "no") + "): step " + + std::to_string((int)actual) + ", expected " + std::to_string((int)c.expected) + "."; + } + } + if (advance_after_probe(true)){ + return "A after X changed the screen would pick from whatever X opened."; + } + if (!advance_after_probe(false)){ + return "A must advance a screen X left unchanged."; + } + return true; + } +}; + + +// The Mart's doorway on the failure screenshots of r121a and r122a, kept in +// the unit test resources (the CommandLineTests checkout): where it is +// relative to the player, who is in the middle of the screen. +class Test_LitDoorwayScreenshots : public UnitTest{ +public: + Test_LitDoorwayScreenshots() + : UnitTest("PokemonBDSP::AutoStory - lit doorway screenshots") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ + const char* file; + double tiles_across_min; + double tiles_across_max; + double rows_up_min; + double rows_up_max; + }; + const Case cases[] = { + // r121a: below-left of the door, which is part of a tile east, straight ahead. + {"PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", 0.2, 0.7, 0.5, 1.5}, + // r122a: a tile west of the door and three or four rows south of its front. + {"PokemonBDSP/AutoStory/SandgemMartDoorFarLeft-20261030-024648.png", -1.3, -0.5, 2.5, 4.5}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const ImageRGB32 screen(path); + const std::optional doorway = find_lit_doorway(screen); + if (!doorway){ + return std::string(c.file) + ": no lit doorway found."; + } + logger.log(std::string(c.file) + ": doorway at " + std::to_string(doorway->x) + ", " + + std::to_string(doorway->y) + ": " + std::to_string(doorway->tiles_across) + " tiles across, " + + std::to_string(doorway->rows_up) + " rows up."); + if (doorway->tiles_across < c.tiles_across_min || doorway->tiles_across > c.tiles_across_max){ + return std::string(c.file) + ": doorway " + std::to_string(doorway->tiles_across) + + " tiles across, expected " + std::to_string(c.tiles_across_min) + " to " + + std::to_string(c.tiles_across_max) + "."; + } + if (doorway->rows_up < c.rows_up_min || doorway->rows_up > c.rows_up_max){ + return std::string(c.file) + ": doorway " + std::to_string(doorway->rows_up) + + " rows up, expected " + std::to_string(c.rows_up_min) + " to " + + std::to_string(c.rows_up_max) + "."; + } + } + return true; + } +}; + + +// The save prompt r124a took beside Sandgem's Poke Mart, sure it stood on +// Route 202 (in CommandLineTests under PokemonBDSP/AutoStory): its card +// names Sandgem Town, which the save must read before confirming. +class Test_SaveScreenLocation : public UnitTest{ +public: + Test_SaveScreenLocation() + : UnitTest("PokemonBDSP::AutoStory - save screen location") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const std::string path = UNIT_TEST_RESOURCE_PATH() + "PokemonBDSP/AutoStory/SaveScreenSandgemTown-20261030-034444.png"; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const std::string read = read_save_screen_location(ImageRGB32(path)); + logger.log("Save screen location read: \"" + read + "\"."); + if (!location_banner_matches(read, MAP_SANDGEM_TOWN)){ + return "Save screen location \"" + read + "\" should match Sandgem Town."; + } + if (location_banner_matches(read, MAP_ROUTE_202)){ + return "Save screen location \"" + read + "\" should not match Route 202."; + } + return true; + } +}; + + +// The two shots around r126's door walk: below the Center's sign, outside, +// and on the entrance mat inside, where the lit floor read as a doorway and +// the walk kept lining up with it (runs 2 and 3). +class Test_PokemonCenterInside : public UnitTest{ +public: + Test_PokemonCenterInside() + : UnitTest("PokemonBDSP::AutoStory - Pokemon Center inside") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ + const char* file; + bool inside; + bool doorway; + }; + const Case cases[] = { + {.file = "PokemonBDSP/AutoStory/SandgemCenterInside-20261030-053555.png", .inside = true, .doorway = false}, + // r127c: walked in by the turn north at the door, facing north. + {.file = "PokemonBDSP/AutoStory/SandgemCenterInsideFacingNorth-r127c.png", .inside = true, .doorway = false}, + {.file = "PokemonBDSP/AutoStory/SandgemCenterBelowSign-20261030-053527.png", .inside = false, .doorway = true}, + {.file = "PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", .inside = false, .doorway = true}, + {.file = "PokemonBDSP/AutoStory/SandgemMartDoorFarLeft-20261030-024648.png", .inside = false, .doorway = true}, + // Jubilife (2026-10-30): inside its Center twice, at its door, and + // on the doorstep after walking out (the banner up). + {.file = "PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", .inside = true, .doorway = false}, + {.file = "PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", .inside = true, .doorway = false}, + {.file = "PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", .inside = false, .doorway = true}, + {.file = "PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", .inside = false, .doorway = true}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const ImageRGB32 screen(path); + const bool inside = pokemon_center_inside(screen); + const std::optional doorway = find_lit_doorway(screen); + logger.log(std::string(c.file) + ": inside " + std::to_string(inside) + ", doorway " + + (doorway ? std::to_string(doorway->tiles_across) + " across, " + std::to_string(doorway->rows_up) + " rows up" : "none")); + if (inside != c.inside){ + return std::string(c.file) + ": expected " + (c.inside ? "inside" : "outside") + + " a Pokemon Center, read " + (inside ? "inside" : "outside") + "."; + } + if (doorway.has_value() != c.doorway){ + return std::string(c.file) + ": expected " + (c.doorway ? "a doorway" : "no doorway") + + ", read " + (doorway ? "one" : "none") + "."; + } + } + return true; + } +}; + + +// The School's lamp at night, from its doorsteps' row (2026-10-30): where +// the door took us in, the lamp reads within half a tile of the door; a +// tile either side, more. +class Test_SchoolLamp : public UnitTest{ +public: + Test_SchoolLamp() + : UnitTest("PokemonBDSP::AutoStory - Jubilife School lamp") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const ImageFloatBox box(0.55, 0.20, 0.30, 0.10); + struct Case{ const char* file; double min; double max; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeSchoolRightOfDoor-20261030.png", -1.5, -0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolOneTileShort-20261030.png", -1.5, -0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolAtDoor-20261030.png", -0.5, 0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolAtDoor2-20261030.png", -0.5, 0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", -0.5, 0.5}, + {"PokemonBDSP/AutoStory/JubilifeSchoolOneTilePast-20261030.png", 0.5, 1.5}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const ImageRGB32 screen(path); + const std::optional lamp = find_lit_lamp_x(screen, box); + if (!lamp){ + return std::string(c.file) + ": no lit lamp found."; + } + const double across = tiles_across_at(*lamp) - tiles_across_at(0.67); + logger.log(std::string(c.file) + ": lamp at " + std::to_string(*lamp) + ", door " + std::to_string(across) + " tiles across."); + if (across < c.min || across > c.max){ + return std::string(c.file) + ": door " + std::to_string(across) + " tiles across, expected " + + std::to_string(c.min) + " to " + std::to_string(c.max) + "."; + } + } + return true; + } +}; + + +} + + +void add_tests_AutoStory_Actions(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.h new file mode 100644 index 0000000000..b6b09c7d32 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.h @@ -0,0 +1,25 @@ +/* BDSP AutoStory - Actions Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Actions_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Actions_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Tests that need no console: the new game's intro decisions, and the screenshot checks of the Pokemon Center, the save card and the School's lamp. +void add_tests_AutoStory_Actions(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.cpp new file mode 100644 index 0000000000..70a24e5350 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.cpp @@ -0,0 +1,403 @@ +/* BDSP AutoStory - Bag + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#include +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_Actions.h" +#include "PokemonBDSP_AutoStory_Bag.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +ImageFloatBox bag_list_icon_box(size_t row){ + return ImageFloatBox(0.449, 0.212 + (row - 1) * 0.075, 0.033, 0.050); +} + +ImageFloatBox bag_list_name_box(size_t row){ + // Previous height included the faint lower selection border in rows 2/6. + // return ImageFloatBox(0.492, 0.213 + (row - 1) * 0.075, 0.335, 0.052); + return ImageFloatBox(0.492, 0.213 + (row - 1) * 0.075, 0.335, 0.050); +} + +int bag_confirm_pocket(VideoStream& stream, ProControllerContext& context){ + int previous = 0; + size_t matches = 0; + for (size_t check = 0; check < 10; check++){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const int pocket = frame ? bag_pocket_position(frame) : 0; + matches = pocket != 0 && pocket == previous ? matches + 1 : pocket != 0 ? 1 : 0; + previous = pocket; + if (matches >= 3){ + return pocket; + } + pbf_wait(context, 100ms); + } + return 0; +} + +bool bag_navigate_to_pocket(VideoStream& stream, ProControllerContext& context, int target){ + int current = bag_confirm_pocket(stream, context); + for (size_t attempt = 0; attempt < 3; attempt++){ + const auto plan = plan_bag_pocket_move(current, target); + if (plan.action == BagPocketAction::CONFIRM){ + return true; + } + if (plan.action == BagPocketAction::UNKNOWN){ + stream.log("[Bag search] Selected pocket not detected.", COLOR_ORANGE); + return false; + } + stream.log("[Bag search] Pocket " + std::to_string(current) + " to " + std::to_string(target) + ": " + + (plan.action == BagPocketAction::RIGHT ? "right " : "left ") + std::to_string(plan.presses) + + " presses, attempt " + std::to_string(attempt + 1) + "/3."); + for (size_t press = 0; press < plan.presses; press++){ + pbf_press_dpad(context, plan.action == BagPocketAction::RIGHT ? DPAD_RIGHT : DPAD_LEFT, 80ms, 150ms); + } + pbf_wait(context, 350ms); + current = bag_confirm_pocket(stream, context); + stream.log( + "[Bag search] Final pocket=" + std::to_string(current) + ", expected=" + std::to_string(target) + "."); + if (current == target){ + return true; + } + } + stream.log("[Bag search] Target pocket not reached after 3 movement batches.", COLOR_ORANGE); + return false; +} + +// Slot 1's colored HP bar in the Bag. Read fill position rather than OCR. +double potion_hp_fraction(const ImageViewRGB32& frame){ + if (bag_pocket_position(frame) == 0){ + return -1; + } + const ImageViewRGB32 bar = extract_box_reference(frame, ImageFloatBox{0.137, 0.160, 0.202, 0.016}); + size_t filled = 0; + for (size_t x = 0; x < bar.width(); x++){ + size_t color = 0; + for (size_t y = 0; y < bar.height(); y++){ + uint32_t p = bar.pixel(x, y); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + const bool green = g > r + 30 && g > b + 20; + const bool yellow = r > 150 && g > 120 && b < 100; + const bool red = r > 150 && r > g + 50 && r > b + 50; + color += green || yellow || red; + } + filled += color > bar.height() / 4; + } + return bar.width() ? (double)filled / bar.width() : -1; +} + +double confirmed_potion_hp(VideoStream& stream, ProControllerContext& context){ + double previous = -1; + size_t stable = 0; + for (size_t check = 0; check < 20; check++){ + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + double hp = frame ? potion_hp_fraction(frame) : -1; + if (hp >= 0 && previous >= 0 && std::abs(hp - previous) <= 0.015){ + if (++stable >= 3){ + return hp; + } + }else{ + stable = 0; + } + previous = hp; + pbf_wait(context, 150ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Slot 1 HP bar could not be confirmed.", + stream); +} + +} + +int bag_pocket_position(const ImageViewRGB32& frame){ + if (!frame){ + return 0; + } + int selected = 0; + for (int pocket = 1; pocket <= 8; pocket++){ + // The lower left edge of each tab is below the red NEW badge. + // This strip includes both the normal outline and enlarged selection. + const auto edge = + extract_box_reference(frame, ImageFloatBox(0.570 + (pocket - 1) * 0.0421, 0.098, 0.0045, 0.021)); + size_t colored = 0; + for (size_t y = 0; y < edge.height(); y++){ + for (size_t x = 0; x < edge.width(); x++){ + const uint32_t pixel = edge.pixel(x, y); + const int r = (pixel >> 16) & 255; + const int g = (pixel >> 8) & 255; + const int b = pixel & 255; + // Red when focused, blue when the item list has focus. + colored += (r > 160 && r > g + 60 && r > b + 60) || (b > 160 && b > r + 40 && b > g + 20); + } + } + const double pixels = (double)edge.width() * edge.height(); + if (pixels > 0 && colored / pixels >= 0.18){ + if (selected != 0){ + return 0; + } + selected = pocket; + } + } + return selected; +} + +// One narrow strip on the left side of the red selection border. Divide its +// height into the seven row bands; the item's art and NEW badge are outside it. +int bag_cursor_position(const ImageViewRGB32& frame){ + if (!frame){ + return 0; + } + const ImageViewRGB32 strip = extract_box_reference(frame, ImageFloatBox(0.431, 0.229, 0.005, 0.470)); + double best = 0.0; + double second = 0.0; + int position = 0; + for (int row = 0; row < 7; row++){ + const size_t top = static_cast(strip.height() * (row * 0.075 / 0.470)); + const size_t bottom = static_cast(strip.height() * ((row * 0.075 + 0.020) / 0.470)); + size_t red = 0; + size_t total = 0; + for (size_t y = top; y < bottom && y < strip.height(); y++){ + for (size_t x = 0; x < strip.width(); x++){ + const uint32_t pixel = strip.pixel(x, y); + const unsigned r = (pixel >> 16) & 255; + const unsigned g = (pixel >> 8) & 255; + const unsigned b = pixel & 255; + red += r > 180 && g < 120 && b < 120; + total++; + } + } + const double score = total == 0 ? 0.0 : static_cast(red) / total; + if (score > best){ + second = best; + best = score; + position = row + 1; + }else if (score > second){ + second = score; + } + } + return best >= 0.15 && best - second >= 0.10 ? position : 0; +} + +bool bag_list_is_empty(const ImageViewRGB32& frame){ + if (!frame || bag_pocket_position(frame) == 0 || bag_cursor_position(frame) != 0){ + return false; + } + // Empty pockets have no item icons. The central Empty label is outside + // this column; counting dark pixels in the name area included that label. + const auto icons = extract_box_reference(frame, ImageFloatBox(0.449, 0.213, 0.032, 0.500)); + const auto stats = image_stats(icons); + return stats.average.r > 220 && stats.average.g > 220 && stats.average.b > 220 && stats.stddev.sum() < 20; +} + +BagPocketMovePlan plan_bag_pocket_move(int current, int target){ + if (current < 1 || current > 8 || target < 1 || target > 8){ + return {}; + } + const int right = (target - current + 8) % 8; + const int left = (current - target + 8) % 8; + if (right == 0){ + return {BagPocketAction::CONFIRM, 0}; + } + return right <= left ? BagPocketMovePlan{BagPocketAction::RIGHT, static_cast(right)} + : BagPocketMovePlan{BagPocketAction::LEFT, static_cast(left)}; +} + +bool find_bag_item_by_name( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket, + int num_icons +){ + open_menu(stream, context, MenuCursorPosition::BAG, num_icons); + pbf_wait(context, 1000ms); + return find_bag_item_in_open_bag(stream, context, name, target_pocket); +} + +bool find_bag_item_in_open_bag( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket +){ + context.wait_for_all_requests(); + if (!bag_navigate_to_pocket(stream, context, target_pocket)) return false; + + // No reader for the game's language: the cursor's row, the pocket's + // first, if the pocket has one. Early in the story the pocket holds only + // what was bought or given (Potions in Medicine). + if (!story_reads_text()){ + const VideoSnapshot frame = stream.video().snapshot(); + const bool any = frame && !bag_list_is_empty(*frame.frame) && bag_cursor_position(*frame.frame) != 0; + stream.log("[Bag search] " + name + ": no text reader for the game's language; " + + (any ? "taking the pocket's cursor row." : "the pocket is empty."), COLOR_ORANGE); + return any; + } + + // Walk down the list reading the cursor row. The list does not wrap, so + // turn around once the cursor stops moving. + DpadPosition direction = DPAD_DOWN; + std::string previous; + size_t same = 0; + size_t confirmed = 0; + size_t unknown_cursor = 0; + for (size_t scan = 0; scan < 128; scan++){ + context.wait_for_all_requests(); + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame || bag_list_is_empty(*frame.frame)){ + stream.log("[Bag search] " + name + ": pocket is empty.", COLOR_ORANGE); + return false; + } + const int cursor = bag_cursor_position(*frame.frame); + if (cursor == 0){ + if (++unknown_cursor >= 3){ + stream.log("[Bag search] " + name + ": cursor not found.", COLOR_ORANGE); + return false; + } + pbf_wait(context, 200ms); + continue; + } + unknown_cursor = 0; + const std::string text = OCR::tesseract_ocr_read(Language::English, + extract_box_reference(*frame.frame, bag_list_name_box(cursor))); + std::string normalized; + for (unsigned char ch : text){ + if (std::isalpha(ch)) normalized.push_back((char)std::tolower(ch)); + } + stream.log("[Bag search] Row " + std::to_string(cursor) + " reads \"" + normalized + "\"."); + if (normalized == name){ + if (++confirmed >= 2){ + stream.log("[Bag search] " + name + " confirmed in row " + std::to_string(cursor) + ".", COLOR_GREEN); + return true; + } + pbf_wait(context, 150ms); + continue; + } + confirmed = 0; + same = normalized == previous ? same + 1 : 0; + previous = normalized; + if (same >= 2){ + if (direction == DPAD_UP) break; + direction = DPAD_UP; + same = 0; + } + pbf_press_dpad(context, direction, 80ms, 300ms); + } + stream.log("[Bag search] " + name + " not found.", COLOR_ORANGE); + return false; +} + +bool bag_has_item(VideoStream& stream, ProControllerContext& context, const std::string& name, int target_pocket) { + const bool found = find_bag_item_by_name(stream, context, name, target_pocket, 5); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Bag: Did not return to the overworld after looking for " + name + ".", stream); + } + return found; +} + +void use_potion_first_pokemon(VideoStream& stream, ProControllerContext& context, double heal_below) { + OverlayBoxScope hp_overlay(stream.overlay(), COLOR_GREEN, {0.137, 0.160, 0.202, 0.016}, ""); + bool healed = false; + for (size_t dose = 0; dose < 3; dose++){ + stream.log("[Potion] Finding Bag/Medicine/Potion; dose " + std::to_string(dose + 1) + "/3."); + if (!find_bag_item_by_name(stream, context, "potion", 1, 5)) { + if (dose == 0 && heal_below < 1.0) { + // Only a check: without Potions there is nothing to do here. + stream.log("[Potion] No Potion in the Bag; skipping the HP check.", COLOR_ORANGE); + healed = true; + break; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Could not verify Potion in the Medicine pocket.", + stream); + } + const double before = confirmed_potion_hp(stream, context); + stream.log("[Potion] Slot 1 HP bar before use=" + std::to_string(before * 100) + "%."); + if (before >= 0.98) { healed = true; break; } + if (dose == 0 && before >= heal_below) { + stream.log("[Potion] Slot 1 HP is at least " + std::to_string(heal_below * 100) + "%; no Potion needed."); + healed = true; + break; + } + if (before <= 0.005) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Slot 1 appears fainted; a Potion cannot revive it.", + stream); + } + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Did not return to the overworld after use.", + stream); + } + // Reopen Bag to verify the result, including when the last Potion was used. + open_menu(stream, context, MenuCursorPosition::BAG, 5); + pbf_wait(context, 750ms); + context.wait_for_all_requests(); + const double after = confirmed_potion_hp(stream, context); + stream.log("[Potion] Slot 1 HP bar after use=" + std::to_string(after * 100) + "%."); + if (after <= before + 0.005){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Slot 1 HP did not increase; refusing to continue movement.", + stream); + } + if (after >= 0.98){ + healed = true; + break; + } + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + } + if (!healed){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Full HP not confirmed after three uses.", + stream); + } + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, std::chrono::seconds(10), {{overworld}}) != 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Potion: Final overworld return not confirmed.", + stream); + } + stream.log("[Potion] Slot 1 HP bar full and overworld confirmed."); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.h new file mode 100644 index 0000000000..8e0e73695e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.h @@ -0,0 +1,91 @@ +/* BDSP AutoStory - Bag + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * Finding an item in the Bag, and Potions on the lead. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Bag_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Bag_H + +#include +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Pocket positions 1-8: Medicine, Balls, Battle Items, Berries, Other Items, +// TMs, Treasures, Key Items. Zero means the selected tab is uncertain. +int bag_pocket_position(const ImageViewRGB32& frame); +// Whether the open pocket shows no item. +bool bag_list_is_empty(const ImageViewRGB32& frame); +// The cursor's row, 1-7, or 0 if unknown. +int bag_cursor_position(const ImageViewRGB32& frame); + +enum class BagPocketAction{ + UNKNOWN, + CONFIRM, + LEFT, + RIGHT, +}; +struct BagPocketMovePlan{ + BagPocketAction action = BagPocketAction::UNKNOWN; + size_t presses = 0; +}; +// Circular eight-pocket route; ties prefer Right. Invalid positions send no input. +BagPocketMovePlan plan_bag_pocket_move(int current, int target); + +// Starts in the overworld, opens the Bag on `target_pocket`, and moves the cursor +// onto the item whose name reads `name` (letters only, lower case, e.g. "potion"). +// Reads the cursor row's name with OCR, so it does not depend on the capture +// card's colors or sharpness the way a template match does. +// Returns false if the pocket is empty or the name is not found. +bool find_bag_item_by_name( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket, + int num_icons = 8 +); +// The same search in a Bag that is already open, such as the one a battle's +// Bag command opens. +bool find_bag_item_in_open_bag( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket +); + +// Open the Bag from the overworld, look for `name` in the pocket, and close +// it again. True if the item is there. +bool bag_has_item( + VideoStream& stream, + ProControllerContext& context, + const std::string& name, + int target_pocket +); + +// Read the lead's HP bar in the Bag and, if it is below `heal_below` (a +// fraction of full), use Potions until it is full. heal_below = 1 always +// heals; 0.5 only checks and heals when the lead is under half, and is +// skipped when the Bag has no Potion. +void use_potion_first_pokemon( + VideoStream& stream, + ProControllerContext& context, + double heal_below = 1.0 +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.cpp new file mode 100644 index 0000000000..8635622af2 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.cpp @@ -0,0 +1,602 @@ +/* BDSP AutoStory - Battle + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_Bag.h" +#include "PokemonBDSP_AutoStory_Battle.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +BattleDialogWatcher::BattleDialogWatcher(Color color) + : DetectorToFinder( + "BattleDialogWatcher", + std::chrono::milliseconds(250), + color + ) +{ +} + +namespace{ + +// Early moves the starters and their foes have, as type, power and +// most PP. A status move has power 0. +struct KnownMove { const char* name; const char* type; int power; int pp; }; +const KnownMove KNOWN_MOVES[] = { + {"pound", "normal", 40, 35}, {"tackle", "normal", 40, 35}, {"scratch", "normal", 40, 35}, + {"quickattack", "normal", 40, 30}, {"headbutt", "normal", 70, 15}, + {"watergun", "water", 40, 25}, {"bubble", "water", 40, 30}, {"bubblebeam", "water", 65, 20}, + {"ember", "fire", 40, 25}, {"flamewheel", "fire", 60, 25}, {"machpunch", "fighting", 40, 30}, + {"absorb", "grass", 20, 25}, {"razorleaf", "grass", 55, 25}, {"bite", "dark", 60, 25}, + {"peck", "flying", 35, 35}, {"wingattack", "flying", 60, 35}, {"metalclaw", "steel", 50, 35}, + {"growl", "normal", 0, 40}, {"leer", "normal", 0, 30}, {"tailwhip", "normal", 0, 30}, + {"withdraw", "water", 0, 40}, {"taunt", "dark", 0, 20}, +}; + +// A move button's PP left, "8/25" at its right end; -1 if it cannot be +// read. The text is dark, or yellow to red as PP runs low, so text is +// whatever differs from the button's own color. `max_pp` helps when +// the OCR reads the slash as a digit. +int read_move_pp(const ImageViewRGB32& screen, const ImageFloatBox& button, int max_pp) { + const ImageViewRGB32 box = extract_box_reference(screen, ImageFloatBox{ + button.x + button.width * 0.75, button.y + button.height * 0.10, + button.width * 0.21, button.height * 0.80}); + if (box.width() == 0 || box.height() == 0) return -1; + std::vector pixels; + for (size_t y = 0; y < box.height(); y++) for (size_t x = 0; x < box.width(); x++) pixels.push_back(box.pixel(x, y)); + auto brightness = [](uint32_t p) { return ((p >> 16) & 255) + ((p >> 8) & 255) + (p & 255); }; + std::vector sorted = pixels; + std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end(), + [&](uint32_t a, uint32_t b) { return brightness(a) < brightness(b); }); + const uint32_t background = sorted[sorted.size() / 2]; + const size_t pad = 8; + ImageRGB32 text(box.width() + 2 * pad, box.height() + 2 * pad); + text.fill(0xffffffff); + for (size_t y = 0; y < box.height(); y++) { + for (size_t x = 0; x < box.width(); x++) { + const uint32_t p = box.pixel(x, y); + int distance = 0; + for (int shift : {16, 8, 0}) distance += std::abs((int)((p >> shift) & 255) - (int)((background >> shift) & 255)); + if (distance > 120) text.pixel(x + pad, y + pad) = 0xff000000; + } + } + const std::string read = OCR::tesseract_ocr_read(Language::English, text); + const size_t slash = read.find('/'); + std::string digits; + for (size_t i = 0; i < read.size() && i < slash; i++) if (std::isdigit((unsigned char)read[i])) digits.push_back(read[i]); + if (slash == std::string::npos && max_pp > 0) { + // "29135" for 29/35: drop the most PP and the misread slash. + const std::string max = std::to_string(max_pp); + if (digits.size() > max.size() + 1 && digits.compare(digits.size() - max.size(), max.size(), max) == 0) { + digits.resize(digits.size() - max.size() - 1); + } else { + return -1; + } + } + if (digits.empty() || digits.size() > 2) return -1; + return std::stoi(digits); +} + +// The best move on the move menu: the most powerful one we know, and +// on a tie one that is not Normal (an early starter's own type). +// Reads the names with OCR; 1 if none is known. +size_t pick_best_move(VideoStream& stream, const ImageViewRGB32& screen, + const BattleMoveMenuState& moves, const std::string& label) { + // Keep the menu as it looked, to check the reads against. + { + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/moves/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.save(folder + now_to_filestring() + "-" + label + ".png"); + } + size_t best = 1; + int best_score = -1; + bool any_attack = false; + size_t last_not_status = 1; + for (size_t i = 0; i < moves.move_count; i++) { + // The name is dark text on a light button (grey, or light blue under the + // cursor). Take the box right of the type icon and left of the PP, tall + // enough for a status move's name, which sits lower: it has no "Effective" + // line under it. + const ImageFloatBox& button = moves.buttons[i]; + const ImageFloatBox name_box{button.x + button.width * 0.17, button.y + button.height * 0.08, + button.width * 0.53, button.height * 0.80}; + const ImageViewRGB32 box = extract_box_reference(screen, name_box); + // Text is darker than halfway between the background and the darkest pixel; + // a fixed cut thins the strokes on the blue button. + std::vector gray(box.width() * box.height()); + for (size_t y = 0; y < box.height(); y++) { + for (size_t x = 0; x < box.width(); x++) { + const uint32_t pixel = box.pixel(x, y); + gray[y * box.width() + x] = (int)(((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff)) / 3; + } + } + std::vector sorted = gray; + std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end()); + const int background = sorted[sorted.size() / 2]; + const int darkest = *std::min_element(gray.begin(), gray.end()); + const int threshold = (background + darkest) / 2; + // The name is the tallest band of text rows; "Effective" is smaller. + size_t band_top = 0, band_bottom = 0; + for (size_t y = 0; y < box.height();) { + auto has_text = [&](size_t row) { + for (size_t x = 0; x < box.width(); x++) if (gray[row * box.width() + x] < threshold) return true; + return false; + }; + if (!has_text(y)) { y++; continue; } + size_t end = y; + while (end < box.height() && has_text(end)) end++; + if (end - y > band_bottom - band_top) { band_top = y; band_bottom = end; } + y = end; + } + // Black text on white, with a margin, for the OCR. + const size_t pad = 6; + const size_t top = band_top >= 4 ? band_top - 4 : 0; + const size_t bottom = std::min(box.height(), band_bottom + 4); + ImageRGB32 text(box.width() + 2 * pad, bottom - top + 2 * pad); + text.fill(0xffffffff); + for (size_t y = top; y < bottom; y++) { + for (size_t x = 0; x < box.width(); x++) { + if (gray[y * box.width() + x] < threshold) text.pixel(x + pad, y - top + pad) = 0xff000000; + } + } + std::string name; + // Move names only where there is a reader for the game's + // language; an unread move counts as a plain attack. + for (unsigned char ch : story_reads_text() ? OCR::tesseract_ocr_read(g_story_language, text) : std::string()) { + if (std::isalpha(ch)) name.push_back((char)std::tolower(ch)); + } + // A slot read well once keeps its name when a later read fails. + static std::array last_read; + const KnownMove* known = nullptr; + for (int pass = 0; pass < 2 && !known; pass++) { + if (pass == 1) name = last_read[i]; + for (const KnownMove& move : KNOWN_MOVES) { + // The longest name that appears wins: "bubblebeam" over "bubble". + if (name.find(move.name) != std::string::npos && + (!known || std::string(move.name).size() > std::string(known->name).size())) { + known = &move; + } + } + } + if (known) last_read[i] = known->name; + // A move with no PP left cannot be chosen; the game says so and + // keeps the menu up. + const int pp = read_move_pp(screen, button, known ? known->pp : 0); + stream.log("[" + label + "] Move " + std::to_string(i + 1) + " PP left: " + (pp < 0 ? std::string("unread") : std::to_string(pp)) + "."); + if (pp == 0) { + continue; + } + // An unreadable move counts as a plain attack, ahead of known + // status moves. + const int score = known ? known->power * 2 + (std::string(known->type) != "normal" ? 1 : 0) : 40 * 2 - 1; + stream.log("[" + label + "] Move " + std::to_string(i + 1) + " reads \"" + name + "\"" + + (known ? std::string(" = ") + known->name + " (" + known->type + ", " + std::to_string(known->power) + ")" : " (unknown)") + "."); + if (score > best_score) { best_score = score; best = i + 1; } + if (known && known->power > 0) any_attack = true; + if (!known || known->power > 0) last_not_status = i + 1; + } + // No attack read: the newest move is the starter's own-type attack + // (Water Gun, Ember, Absorb), unless it is a status move. + if (!any_attack) best = last_not_status; + stream.log("[" + label + "] Best move: " + std::to_string(best) + "."); + return best; +} + +void select_battle_move_slot(VideoStream& stream, ProControllerContext& context, + BattleType battle_type, size_t target, const std::string& label, + bool confirm_default_target = false, size_t current_move = 1) { + BattleMenuDetector commands(battle_type); + VideoOverlaySet overlays(stream.overlay()); + commands.make_overlays(overlays); + overlays.add(COLOR_GREEN, {0.90, 0.48, 0.075, 0.505}); + // Reenter Fight from an already-open list without assuming + // that this resets the remembered move cursor. + if (confirm_default_target) { + context.wait_for_all_requests(); + const auto frame=stream.video().snapshot(); + if (frame && detect_battle_move_menu(frame).move_count) { + pbf_press_button(context,BUTTON_B,80ms,500ms); + } + } + bool fight = false; + size_t confirmed = 0; + for (size_t check = 0; check < 30; check++) { + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + BattleMenuOption selected = frame ? battle_menu_selected_option(commands, frame) : BattleMenuOption::UNKNOWN; + stream.log("[" + label + "] Command cursor=" + std::to_string((int)selected + 1) + "; target Battle=1."); + if (selected == BattleMenuOption::BATTLE) { + if (++confirmed >= 3) { fight = true; break; } + pbf_wait(context, 100ms); + } else { + confirmed = 0; + if (selected == BattleMenuOption::UNKNOWN) pbf_wait(context, 150ms); + else pbf_press_dpad(context, DPAD_UP, 80ms, 250ms); + } + } + if (!fight) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Could not confirm Battle command; A not pressed.", stream); + pbf_press_button(context,BUTTON_A,80ms,confirm_default_target ? 1000ms : 500ms); + if (confirm_default_target) { + // Reuse the farming move/target confirmation sequence, but + // navigate relative to this battle's retained move cursor. + // Reopening Fight does not reset it to move 1. + size_t available=0,stable=0; + for (size_t check=0;check<20 && stable<3;check++) { + context.wait_for_all_requests(); + const auto frame=stream.video().snapshot(); + const auto moves=frame ? detect_battle_move_menu(frame) : BattleMoveMenuState{}; + if (moves.move_count && moves.move_count==available) stable++; + else {available=moves.move_count;stable=moves.move_count ? 1:0;} + if (stable<3) pbf_wait(context,100ms); + } + if (stable<3 || available "+std::to_string(target)+"."); + for (size_t slot=current_move;slottarget;slot--) pbf_press_dpad(context,DPAD_UP,80ms,400ms); + pbf_press_button(context,BUTTON_A,80ms,1000ms); + pbf_press_button(context,BUTTON_A,80ms,3000ms); + return; + } + confirmed = 0; + size_t previous_move_count = 0, stable_move_count = 0; + for (size_t check = 0; check < 30; check++) { + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + const BattleMoveMenuState moves = frame ? detect_battle_move_menu(frame) : BattleMoveMenuState{}; + const int slot = (int)moves.selected_slot; + VideoOverlaySet move_overlays(stream.overlay()); + for (size_t i = 0; i < moves.move_count; i++) + move_overlays.add(moves.selected_slot == i + 1 ? COLOR_GREEN : COLOR_YELLOW, moves.buttons[i]); + stream.log("[" + label + "] Available moves=" + std::to_string(moves.move_count) + + "; selected slot=" + std::to_string(slot) + "/4; target=" + std::to_string(target) + "."); + if (moves.move_count == previous_move_count && moves.move_count > 0) stable_move_count++; + else { previous_move_count = moves.move_count; stable_move_count = 1; confirmed = 0; } + // Target 0: pick the best move once the menu has settled. + if (target == 0 && stable_move_count >= 3 && frame) { + target = pick_best_move(stream, *frame.frame, moves, label); + } + if (moves.move_count > 0 && moves.move_count < target && stable_move_count >= 3) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Move slot " + std::to_string(target) + " is absent; no move confirmed.", stream); + } + if (slot == (int)target && stable_move_count >= 3) { + if (++confirmed >= 3) { + stream.log("[" + label + "] Move " + std::to_string(target) + " confirmed; using it."); + pbf_press_button(context, BUTTON_A, 80ms, 3000ms); + return; + } + pbf_wait(context, 100ms); + } else { + confirmed = 0; + if (slot == 0 || slot == (int)target) pbf_wait(context, 150ms); + else pbf_press_dpad(context, slot < (int)target ? DPAD_DOWN : DPAD_UP, 80ms, 250ms); + } + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Could not confirm move " + std::to_string(target) + "; A not pressed.", stream); +} + +// Our HP bar in the battle's bottom-left panel, as a fraction; -1 if +// no bar is there. Measured on 1920x1080 frames: the bar runs from +// x 88 to 466 on rows 961 to 965. +double battle_player_hp(const ImageViewRGB32& frame) { + const ImageViewRGB32 bar = extract_box_reference(frame, ImageFloatBox{0.046, 0.890, 0.197, 0.004}); + size_t filled = 0; + for (size_t x = 0; x < bar.width(); x++) { + size_t color = 0; + for (size_t y = 0; y < bar.height(); y++) { + const uint32_t p = bar.pixel(x, y); + const int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + const bool green = g > r + 30 && g > b + 20; + const bool yellow = r > 150 && g > 120 && b < 100; + const bool red = r > 150 && r > g + 50 && r > b + 50; + color += green || yellow || red; + } + filled += color > bar.height() / 2; + } + return bar.width() && filled ? (double)filled / bar.width() : -1; +} + +enum class BattleHeal { NOT_NEEDED, USED, NONE_LEFT }; + +// At the battle menu: if our HP is under `heal_below`, use a Potion +// from the Bag on the lead, which takes the turn. Otherwise the +// battle menu is left up. +BattleHeal battle_heal_if_low(VideoStream& stream, ProControllerContext& context, + BattleType battle_type, double heal_below, const std::string& label) { + context.wait_for_all_requests(); + double hp = -1; + for (size_t check = 0; check < 5 && hp < 0; check++) { + const auto frame = stream.video().snapshot(); + if (frame) hp = battle_player_hp(*frame.frame); + if (hp < 0) pbf_wait(context, 150ms); + context.wait_for_all_requests(); + } + stream.log("[" + label + "] Our HP bar reads " + std::to_string((int)(hp * 100)) + "%."); + if (hp < 0 || hp >= heal_below) return BattleHeal::NOT_NEEDED; + BattleMenuDetector commands(battle_type); + bool on_bag = false; + for (size_t check = 0; check < 30 && !on_bag; check++) { + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const BattleMenuOption selected = frame ? battle_menu_selected_option(commands, frame) : BattleMenuOption::UNKNOWN; + if (selected == BattleMenuOption::BAG) on_bag = true; + else if (selected == BattleMenuOption::UNKNOWN) pbf_wait(context, 150ms); + else pbf_press_dpad(context, (int)selected < (int)BattleMenuOption::BAG ? DPAD_DOWN : DPAD_UP, 80ms, 300ms); + } + if (!on_bag) { + stream.log("[" + label + "] Could not reach the Bag command; fighting on.", COLOR_ORANGE); + return BattleHeal::NONE_LEFT; + } + pbf_press_button(context, BUTTON_A, 80ms, 2000ms); + if (!find_bag_item_in_open_bag(stream, context, "potion", 1)) { + stream.log("[" + label + "] No Potion left; fighting on.", COLOR_ORANGE); + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + context.wait_for_all_requests(); + return BattleHeal::NONE_LEFT; + } + // Potion, then the lead in the party list, then Use. + stream.log("[" + label + "] Using a Potion on the lead.", COLOR_BLUE); + pbf_press_button(context, BUTTON_A, 80ms, 1500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1500ms); + pbf_press_button(context, BUTTON_A, 80ms, 2500ms); + context.wait_for_all_requests(); + return BattleHeal::USED; +} + +void fight_using_move_list( + VideoStream& stream, ProControllerContext& context, + BattleType battle_type, const std::vector& move_slots, + const std::string& label, bool confirm_default_target, bool post_battle_dialogue_cleanup, + bool doubles = false, bool switch_on_faint = false +) { + if (move_slots.empty() || std::any_of(move_slots.begin(),move_slots.end(), + [](size_t slot) { return slot>4; })) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label+": Supply a nonempty move list containing only positions 0 (best) through 4.",stream); + } + size_t move_index=0; + // The first Fight menu starts at move 1. Later turns retain + // this Pokemon's previous selection; a new battle resets it. + size_t current_move=1; + stream.log( + "[AutoStory] Starting " + label + " battle...", + COLOR_BLUE + ); + + // Scripted battles wait on the menu detector below, so no + // extra fixed delay is needed after a caller confirms it. + // Preserve the existing early-game transition delay. + if (!confirm_default_target) pbf_wait(context,5000ms); + + bool battle_menu_seen = false; + bool out_of_potions = false; + size_t potions_used = 0; + size_t turns = 0; + size_t faint_switches = 0; + const bool watch_learn_move = confirm_default_target || doubles || switch_on_faint; + EndBattleWatcher end_battle; + + while (true) { + + context.wait_for_all_requests(); + + BattleMenuWatcher battle_menu(battle_type); + // Reuse the farming handlers' move-learning prompt recovery. + SelectionArrowFinder learn_move(stream.overlay(),{0.50,0.62,0.40,0.18},COLOR_YELLOW); + + int ret = run_until( + stream, + context, + [&](ProControllerContext& context) { + // Clear narration with B while watching for commands. + // A here can open Fight before selection begins. + // With faint switching, a quiet 30 s means the party + // screen after a faint, which B cannot leave. + pbf_mash_button(context,BUTTON_B,switch_on_faint ? 30000ms : 120000ms); + }, + { + {battle_menu}, + battle_menu_seen + ? PeriodicInferenceCallback{end_battle} + : PeriodicInferenceCallback{}, + watch_learn_move ? PeriodicInferenceCallback{learn_move} : PeriodicInferenceCallback{}, + } + ); + + switch (ret) { + + // ------------------------------------------------- + // Battle menu detected. + // ------------------------------------------------- + case 0: + battle_menu_seen = true; + // Early battles take a few turns; this many means the chosen + // move is not being used (no PP, a misread menu). + if (++turns > 25) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": still at the battle menu after 25 turns.", stream); + } + + stream.log( + "[AutoStory] Battle menu detected.", + COLOR_BLUE + ); + + // A trainer battle we may lose: a Potion when HP is low. + if (battle_type == BattleType::TRAINER && !out_of_potions && potions_used < 4) { + const BattleHeal heal = battle_heal_if_low(stream, context, battle_type, 0.35, label); + if (heal == BattleHeal::USED) { + potions_used++; + break; + } + out_of_potions = heal == BattleHeal::NONE_LEFT; + } + stream.log("["+label+"] Move list position "+std::to_string(move_index+1)+ + ": use move "+std::to_string(move_slots[move_index])+"."); + select_battle_move_slot(stream, context, battle_type, move_slots[move_index], label, + confirm_default_target,current_move); + current_move=move_slots[move_index]; + // A double battle asks for the target next; B would + // back out of it. The default target is an opponent. + if (doubles) { + pbf_wait(context,1200ms); + pbf_press_button(context,BUTTON_A,80ms,1500ms); + } + // Advance once per move selection; retain the final entry. + if (move_index+1(stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, {{overworld}}); + if (cleanup_ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Post-battle cleanup timed out.", stream); + } + context.wait_for_all_requests(); + confirm_battle_overworld(stream, context, label + " cleanup"); + return; + } + + case 2: + stream.log("["+label+"] Declining move learning; preserving scripted move positions."); + pbf_move_right_joystick(context,{0,-1},160ms,840ms); + pbf_press_button(context,BUTTON_ZL,160ms,840ms); + break; + + default: + // After a faint the game shows the party: Down to the + // next Pokemon, A, then A on "Send out" (by hand, + // 2026-10-29 against Roark). + if (switch_on_faint && faint_switches < 5) { + faint_switches++; + stream.log("[" + label + "] No battle menu for 30 s: a faint; sending out the next Pokemon.", COLOR_ORANGE); + pbf_press_dpad(context, DPAD_DOWN, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1200ms); + pbf_press_button(context, BUTTON_A, 80ms, 2000ms); + break; + } + stream.log( + "[AutoStory] " + label + " battle timeout.", + COLOR_RED + ); + + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "[AutoStory] " + label + " battle timeout.", stream); + } + } +} + +// Preserve the existing early-game callers and their B text cleanup. +void fight_using_move_slot(VideoStream& stream, ProControllerContext& context, + BattleType battle_type, size_t move_slot, const std::string& label) { + fight_using_move_list(stream,context,battle_type,{move_slot},label,false,true); +} + +} + +void confirm_battle_overworld(VideoStream& stream, ProControllerContext& context, + const std::string& label) { + OverworldDetector overworld; + ShortDialogDetector dialogue(COLOR_YELLOW); + BlackScreenDetector black; + VideoOverlaySet overlays(stream.overlay()); + overworld.make_overlays(overlays); + dialogue.make_overlays(overlays); + black.make_overlays(overlays); + const auto deadline = std::chrono::steady_clock::now() + 180s; + auto quiet_since = std::chrono::steady_clock::now(); + bool finished = false; + while (std::chrono::steady_clock::now() < deadline) { + const auto frame = stream.video().snapshot(); + if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No video frame.", stream); + const auto now = std::chrono::steady_clock::now(); + if (dialogue.detect(frame) || black.detect(frame) || !overworld.detect(frame)) { + quiet_since = now; + } else if (now - quiet_since >= 4s) { + finished = true; + break; + } + pbf_mash_button(context, BUTTON_B, 100ms); + context.wait_for_all_requests(); + } + if (!finished) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": Dialogue-free overworld not confirmed for four seconds.", stream); + context.wait_for_all_requests(); +} + +void fight_story_battle(VideoStream& stream, ProControllerContext& context, const std::string& label, bool doubles) { + // The best move each turn; the next Pokemon after a faint; no new moves. + fight_using_move_list(stream, context, BattleType::TRAINER, {0}, label, false, true, doubles, true); +} + +void fight_trainer(VideoStream& stream, ProControllerContext& context, const std::string& label) { + // Move 0: the best move on the menu each turn, whatever the starter. + fight_using_move_slot(stream, context, BattleType::TRAINER, 0, label); +} + +void fight_wild_battle(VideoStream& stream, ProControllerContext& context, const std::string& label){ + fight_using_move_slot(stream, context, BattleType::STANDARD, 0, label); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.h new file mode 100644 index 0000000000..efc7e47697 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.h @@ -0,0 +1,55 @@ +/* BDSP AutoStory - Battle + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * Fighting a battle with the best move on the menu: the move names are read + * and scored by power, so it works whatever the starter knows. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Battle_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Battle_H + +#include +#include "CommonFramework/Tools/VideoStream.h" +#include "CommonTools/VisualDetector.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// A battle's text box, as a watcher. +class BattleDialogWatcher : public DetectorToFinder{ +public: + BattleDialogWatcher(Color color = COLOR_RED); +}; + +// Mashes B until the overworld has shown with no dialogue for four seconds. +// Fails with a screenshot after three minutes. +void confirm_battle_overworld(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// A story battle that may go badly: the best move each turn, Potions +// under 35 %, the next Pokemon when one faints, and "Keep old moves" +// on level-up prompts. `doubles` confirms the default target each +// turn (tag and twin battles). Returns once the battle has ended, +// won or lost; after a loss we stand in the last Pokemon Center. +void fight_story_battle(VideoStream& stream, ProControllerContext& context, const std::string& label, bool doubles = false); + +// Any trainer battle, whatever the starter: the best move on the menu each +// turn until the battle ends, then the text after it. +void fight_trainer(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// A wild battle at its battle menu, fought with the best move until it ends. +void fight_wild_battle(VideoStream& stream, ProControllerContext& context, const std::string& label); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.cpp similarity index 98% rename from SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp rename to SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.cpp index 6242149723..35ed89c090 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.cpp @@ -15,10 +15,10 @@ #include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP_AutoStory_Walk.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" -#include "PokemonBDSP_AutoStory_PokemonCenter.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Center.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -289,7 +289,7 @@ std::optional find_lit_doorway(const ImageViewRGB32& screen){ std::vector tiles_short_of_door( VideoStream& stream, ProControllerContext& context, - const TileGrid& grid, const TilePos& door, const std::string& label + const StoryWorld& grid, const TilePos& door, const std::string& label ){ context.wait_for_all_requests(); VideoSnapshot screen = stream.video().snapshot(); @@ -305,7 +305,7 @@ std::vector tiles_short_of_door( return tiles_by_lit_doorway(*doorway, grid, door); } -std::vector tiles_by_lit_doorway(const LitDoorway& doorway, const TileGrid& grid, const TilePos& door){ +std::vector tiles_by_lit_doorway(const LitDoorway& doorway, const StoryWorld& grid, const TilePos& door){ const TilePos guess{door.x - (int)std::lround(doorway.tiles_across), door.y + (int)std::lround(doorway.rows_up)}; std::vector tiles; for (int y = guess.y - 1; y <= guess.y + 1; y++){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.h similarity index 91% rename from SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h rename to SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.h index 3b3c1110d2..f5e3577de5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.h @@ -4,8 +4,8 @@ * */ -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_PokemonCenter_H -#define PokemonAutomation_PokemonBDSP_AutoStory_PokemonCenter_H +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Center_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Center_H #include #include @@ -14,8 +14,8 @@ #include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "CommonFramework/Tools/VideoStream.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" -#include "Pokemon/Navigation/Pokemon_TileGrid.h" -#include "PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -66,7 +66,7 @@ void move_across(VideoStream& stream, ProControllerContext& context, double tile // stand on: the one its offset says, give or take one each way, since the // rows are a rough count. Only standable tiles of `grid`. std::vector tiles_by_lit_doorway( - const LitDoorway& doorway, const Pokemon::TileGrid& grid, const Pokemon::TilePos& door + const LitDoorway& doorway, const StoryWorld& grid, const Pokemon::TilePos& door ); // For a player who walked to the row below the door on world tile `door`: @@ -77,7 +77,7 @@ std::vector tiles_by_lit_doorway( // the front row, or no doorway shows. std::vector tiles_short_of_door( VideoStream& stream, ProControllerContext& context, - const Pokemon::TileGrid& grid, const Pokemon::TilePos& door, const std::string& label + const StoryWorld& grid, const Pokemon::TilePos& door, const std::string& label ); // For a player on the row below a building's door, its lit doorway on diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp new file mode 100644 index 0000000000..2348d76216 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp @@ -0,0 +1,40 @@ +/* BDSP AutoStory - Events + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +const StoryEvents& story_events(){ + static const StoryEvents events = []{ + StoryEvents ret; + add_lake_events(ret); + add_sandgem_events(ret); + add_jubilife_events(ret); + add_route203_events(ret); + add_oreburgh_events(ret); + add_route204_events(ret); + return ret; + }(); + return events; +} + +StoryEvent console_event(std::function action){ + return [action = std::move(action)](StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep&){ + action(executor.stream(), executor.context()); + return Pokemon::outcome_ok(); + }; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h new file mode 100644 index 0000000000..f3917b1c0c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -0,0 +1,57 @@ +/* BDSP AutoStory - Events + * + * From: https://github.com/PokemonAutomation/ + * + * The story's steps that only BDSP has, by name: a step of the table whose + * variant names one of these runs it (StoryExecutor::run()). Some are the + * scripted scenes (the intro's briefcase, the starter, the Starly battle, + * the Pokedex); most are segment 01's walks, which were measured on the + * console as holds, taps and walks until stopped rather than planned, and + * are kept as they ran there. Each fails by throwing, with a screenshot; + * the executor turns that into a failed step. + * + * They are in files by area: _Events_Lake (00.00 to 00.03), _Sandgem + * (00.04), _Jubilife (00.05, 01.01, 01.06 to 01.08), _Oreburgh (01.02 to + * 01.05) and _Route204 (01.09 to 01.11). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Events_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Events_H + +#include +#include +#include +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Runs a step on the console. Returns where it left the player when it +// knows; throws OperationFailedException when it fails. +using StoryEvent = std::function; +using StoryEvents = std::map; + +// Every event of the story, by name. +const StoryEvents& story_events(); + +// An event that only acts and says nothing of where it leaves us. +StoryEvent console_event(std::function action); + +void add_lake_events(StoryEvents& events); +void add_sandgem_events(StoryEvents& events); +void add_jubilife_events(StoryEvents& events); +void add_route203_events(StoryEvents& events); +void add_oreburgh_events(StoryEvents& events); +void add_route204_events(StoryEvents& events); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp new file mode 100644 index 0000000000..fc0002bd1c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -0,0 +1,447 @@ +/* BDSP AutoStory - Events: Jubilife City (00.05, 01.01, 01.07) + * + * From: https://github.com/PokemonAutomation/ + * + * The Jubilife beats of 00.05 (the Town Map, the president, the clowns' coupons, + * the Poketch), stepped through on the console on 2026-10-30, and + * segment 01's walks out of the Center to Barry and up to Rowan's scene. + * + */ + +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Center.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +namespace{ + +// 00.05: from the save below Jubilife's steps, against the wall east of +// them: a tile west (the first press only turns), then up the steps. +void up_the_steps(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::WEST); + walk_for(stream, context, run_from_wild_battle, -1, 0, 200ms, "To the steps"); + const WalkResult up = walk_for(stream, context, run_from_wild_battle, 0, 1, 480ms, "Up the steps"); + if (up.stop == WalkStop::DIALOG || up.stop == WalkStop::TRANSITION){ + fail_step(stream, std::string("Up the steps: ") + walk_stop_name(up.stop) + "."); + } +} + +// The School's lamp, east of its door, from its doorsteps' row at night +// (2026-10-30): at 0.65 to 0.69 of the screen across the door takes us in; +// at 0.61 (right of it) and 0.75 (past it) its front stops us. A tile is +// 0.065 of the screen. The box leaves out the Center's lamp further east. +const double SCHOOL_LAMP_X_AT_DOOR = 0.67; +const ImageFloatBox SCHOOL_LAMP_BOX(0.55, 0.20, 0.30, 0.10); +const size_t SCHOOL_TILES_WEST_OF_CENTER = 11; + +// At night the lamp east of the School's door says how many tiles across +// the door is; nothing by day. +std::optional school_door_across(VideoStream& stream, ProControllerContext& context){ + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + if (!screen){ + return std::nullopt; + } + const std::optional lamp = find_lit_lamp_x(*screen.frame, SCHOOL_LAMP_BOX); + if (!lamp){ + stream.log("[Route] School's door: no lit lamp on screen."); + return std::nullopt; + } + const double across = tiles_across_at(*lamp) - tiles_across_at(SCHOOL_LAMP_X_AT_DOOR); + stream.log("[Route] School's door: the lamp at " + std::to_string(*lamp) + " of the screen, the door " + + std::to_string(across) + " tiles across."); + return across; +} + +// 00.05: from the Center's doorstep into the Trainers' School. Measured on +// the console (2026-10-30): a tap to face west and eleven one-tile holds put +// us under the School's doorway; twelve is past it. A hold ends a part of a +// tile off (back east, eleven stopped beside the Center's door), so if the +// School's front stops us, nudge half a tile each way. +void into_the_school(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::WEST); + for (size_t tiles = 0; tiles < SCHOOL_TILES_WEST_OF_CENTER; tiles++){ + hold_for(stream, context, Direction::WEST, 160ms, "West to the School's door"); + } + for (size_t looks = 0; looks < 3; looks++){ + const std::optional across = school_door_across(stream, context); + // The door took us from -0.19 and 0.19 but not -0.34 (r148b). + if (!across || std::abs(*across) <= 0.3){ + break; + } + stream.log("[Route] Line up with the School's door by its lamp: " + std::to_string(*across) + " tiles across."); + move_across(stream, context, *across); + context.wait_for_all_requests(); + } + if (door_north_by_scene(stream, context, "Trainers' School door")){ + return; + } + for (Direction side : {Direction::EAST, Direction::WEST, Direction::WEST}){ + nudge(stream, context, side, "Line up with the School's door"); + if (door_north_by_scene(stream, context, "Trainers' School door")){ + return; + } + } + fail_step(stream, "The School's door did not take us from 11 tiles west of the Center's doorstep, or half a tile either side."); +} + +// 00.05: Barry stands at the blackboard a tile west of the door's column, +// and that column is clear to the back wall: north to it, face west, his +// scene to "You obtained a Town Map!" (A closes it). A tile west by a hold +// went two in r149a and the boy at the table stopped us. +void barry_town_map(VideoStream& stream, ProControllerContext& context){ + for (size_t attempt = 0; attempt < 2; attempt++){ + const DialogueRead read = talk_facing(stream, context, Direction::WEST, BUTTON_A, "Barry"); + if (read.opened && read.closed){ + return; + } + walk_until_stopped(stream, context, Direction::NORTH, 4, "North to the back wall"); + } + fail_step(stream, "Trainers' School: Barry did not answer from beside the back wall."); +} + +// 00.05: four tiles east is the main road; a couple of tiles north the +// Poketch Company's president sees us, walks up and talks (2026-10-30). +// He sends us for the clowns' coupons; B moves his lines on. +void meet_the_president(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::EAST); + for (size_t tiles = 0; tiles < 4; tiles++){ + hold_for(stream, context, Direction::EAST, 160ms, "East into the road"); + } + walk_until(stream, context, run_from_wild_battle, 0, 1, 6 * ROUTE_MS_PER_TILE_UP_DOWN + 800ms, "The Poketch Company president"); + // He walks up after the "!" and only then talks. + ShortDialogWatcher opened(COLOR_CYAN); + if (wait_until(stream, context, 5000ms, {{opened}}) != 0){ + fail_step(stream, "Jubilife: the Poketch Company president did not talk to us."); + } + finish_dialogue(stream, context, BUTTON_B, "The Poketch Company president"); +} + +// 00.05: a tile west of the lamp by the Center, then a second north onto +// the road between the Center and the Mart. +void up_to_the_mart_road(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::WEST); + hold_for(stream, context, Direction::WEST, 160ms, "West of the lamp"); + hold_for(stream, context, Direction::NORTH, 1000ms, "North to the Mart's road"); +} + +// 00.05: the clown at the east end of the road between the Center and the +// Mart. He may be what stopped the walk east (r155c, 2.8 s against 2.2): +// ask ahead first. Else he stands a row south and a tile west: try that +// row, then one more, facing west each time. His question's default +// answer, Yes, is right; A takes his coupon. +void clown_by_the_mart(VideoStream& stream, ProControllerContext& context){ + DialogueRead read = talk_facing(stream, context, Direction::EAST, BUTTON_A, "Clown by the Poke Mart"); + for (size_t rows = 0; rows < 2 && !read.opened; rows++){ + hold_for(stream, context, Direction::SOUTH, 200ms, "A row south"); + route_turn(stream, context, Direction::WEST); + read = talk_facing(stream, context, Direction::WEST, BUTTON_A, "Clown by the Poke Mart"); + } + if (!read.opened || !read.closed){ + fail_step(stream, "Jubilife: the clown by the Poke Mart did not talk."); + } +} + +// The Poketch Company's sign reads "...World..." or "...Company...". +bool is_company_sign(const DialogueRead& read){ + return read.text.find("World") != std::string::npos || read.text.find("Company") != std::string::npos; +} + +// Him north of us, or the sign. +bool ask_clown(VideoStream& stream, ProControllerContext& context, Direction direction){ + const DialogueRead read = talk_facing(stream, context, direction, BUTTON_A, "Clown by the Poketch Company"); + return read.opened && !is_company_sign(read); +} + +// 00.05: the clown wanders the plaza behind the hedges, and a walk can pass +// him by. Measured on the console (2026-10-30): from the lamp, the Poketch +// Company's sign is six tiles east along the hedges; it reads facing north, +// and the tile east of it is a gap into the plaza. A step into the gap, then +// A every second or so, talked to him within seconds: he comes by. So tap +// four tiles, then a tile at a time asking north (him, or the sign), and +// wait in the gap. A turn tap the way we already face is a step (r161c), so +// we face east between two asks north. +void clown_by_the_company(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::EAST); + tap_tiles(stream, context, Direction::EAST, 4, "East along the hedges"); + bool talked = false; + bool in_gap = false; + for (size_t tiles = 0; tiles < 5 && !in_gap && !talked; tiles++){ + talked = ask_clown(stream, context, Direction::NORTH); + if (talked){ + break; + } + route_turn(stream, context, Direction::EAST); + const DialogueRead sign = read_sign(stream, context, Direction::NORTH, "the Poketch Company's sign"); + if (sign.opened && !is_company_sign(sign)){ + // Him after all (or text OCR missed); his talk was closed with B: ask again. + route_turn(stream, context, Direction::EAST); + talked = ask_clown(stream, context, Direction::NORTH); + if (talked){ + break; + } + } + route_turn(stream, context, Direction::EAST); + tap_tiles(stream, context, Direction::EAST, 1, "On along the hedges"); + if (sign.opened){ + // East of the sign: north is the gap. + hold_for(stream, context, Direction::NORTH, 200ms, "Into the gap beside the sign"); + in_gap = true; + } + } + // Up to about a minute and a half; west of the gap is the sign. East + // first: the step into the gap left us facing north. + for (size_t round = 0; round < 18 && in_gap && !talked; round++){ + for (Direction direction : {Direction::EAST, Direction::NORTH, Direction::WEST}){ + talked = ask_clown(stream, context, direction); + if (talked){ + break; + } + } + } + if (!talked){ + fail_step(stream, "Jubilife: the clown by the Poketch Company did not talk."); + } +} + +// 00.05: from the gap beside the Poketch Company's sign, a row south, then +// east along the long road until the main road. A hold, not a walk until +// still: when the green-haired man who wanders the crossing is what stops +// us, his steps keep the screen moving and the stop is never seen (r167a). +// The stop took 5.9 s; holding on against it is harmless, and the Mart's +// wall puts the column right afterwards. +void to_the_main_road(VideoStream& stream, ProControllerContext& context){ + hold_for(stream, context, Direction::SOUTH, 200ms, "Out of the gap"); + hold_for(stream, context, Direction::EAST, 7500ms, "East along the long road"); +} + +// 00.05: six rows down the main road, east stops against the Poke Mart's +// west wall: the column between the Mart's lamp and the Center's, so south +// stops above the Center's lamp (2026-10-30). +void to_the_center_lamp(VideoStream& stream, ProControllerContext& context){ + for (size_t row = 0; row < 6; row++){ + hold_for(stream, context, Direction::SOUTH, 200ms, "Down the main road"); + } + walk_until_stopped(stream, context, Direction::EAST, 8, "East to the Mart's wall"); + walk_until_stopped(stream, context, Direction::SOUTH, 8, "South to the Center's lamp"); +} + +// 00.05: the president stands still a row or two north of the lamp's row, +// a few tiles west: from a row up, west a tile at a time, asking ahead and +// north. Turns only ever change the way we face, so none is a step. His +// last line asks us to try the R Button; a help panel about the Poketch +// follows, closed with B. +void poketch_from_the_president(VideoStream& stream, ProControllerContext& context){ + hold_for(stream, context, Direction::NORTH, 200ms, "A row up from the lamp"); + route_turn(stream, context, Direction::WEST); + DialogueRead read; + for (size_t tiles = 0; tiles < 8 && !read.opened; tiles++){ + read = talk_ahead(stream, context, BUTTON_B, "The Poketch Company president"); + if (read.opened){ + break; + } + route_turn(stream, context, Direction::NORTH); + read = talk_ahead(stream, context, BUTTON_B, "The Poketch Company president"); + if (read.opened){ + break; + } + route_turn(stream, context, Direction::WEST); + tap_tiles(stream, context, Direction::WEST, 1, "West along the road"); + } + if (!read.opened || !read.closed){ + fail_step(stream, "Jubilife: the president did not give us the Poketch."); + } + stream.log("[Talk] The Poketch's help: B to close the panel."); + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + context.wait_for_all_requests(); +} + +// 00.05 ends on the street east to Route 203, about ten rows north (the +// long road's crossing is six rows above the Mart's wall, which is four +// above the talk; r164a, by hand). A row off is fine: 01.01 walks east and +// tries the rows either side at a wall. The president walks off north after +// his talk, up our way (r164a): let him. +void up_to_the_route_203_street(VideoStream& stream, ProControllerContext& context){ + for (size_t presses = 0; presses < 8; presses++){ + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + } + context.wait_for_all_requests(); + for (size_t row = 0; row < 10; row++){ + hold_for(stream, context, Direction::NORTH, 200ms, "Up the main road"); + } +} + +// 00.05: straight south down the School door's column and out. +void out_of_the_school(VideoStream& stream, ProControllerContext& context){ + leave_door_south(stream, context, run_from_wild_battle, "Out of the Trainers' School"); +} + +// 01.07: Rowan's and Dawn's lines after the tag battle. 01.08 starts here, +// from a save at the top of the street. +void rowan_after_the_battle(VideoStream& stream, ProControllerContext& context){ + finish_dialogue(stream, context, BUTTON_B, "Rowan and Dawn after the battle"); +} + +// 01.01: from the Center's PC, the nurse and out, then the holds to the street east to Route 203; +// nothing when 00.05 has left us outdoors on that street. +void jubilife_center_to_street(VideoStream& stream, ProControllerContext& context){ + // From 00.05g we are already outdoors on the street east to Route 203, + // healed on arriving in Jubilife: straight to the walk east. + bool outdoors = false; + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + outdoors = !indoors(*screen.frame); + } + if (outdoors){ + stream.log("[Route] 01.01: outdoors; skipping the Pokemon Center."); + }else{ + // The save stands us at the PC, right of the counter, facing it; west + // is the counter's end. Two tiles south clears it, then three west is + // the floor emblem's column, with the nurse north and the door south. + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 2), 200ms); + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 3), 200ms); + if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ + fail_step(stream, "Jubilife Pokemon Center: the nurse did not answer."); + } + // A throughout: Yes to "rest your Pokemon?" (B would answer No). + finish_dialogue(stream, context, BUTTON_A, "Nurse"); + // Healing shows no text box for a few seconds, which reads as the end; + // her "Thank you for waiting" lines follow it. + if (dialogue_opens(stream, context, 6000ms)){ + finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); + } + // South through the door until a girl standing below it stops us. The + // door's fade may or may not read as a transition. + WalkResult out{WalkStop::LIMIT, 0ms, 0}; + for (size_t walk = 0; walk < 3 && out.stop != WalkStop::BLOCKED; walk++){ + out = walk_until(stream, context, run_from_wild_battle, 0, -1, 8000ms, "out of the Pokemon Center"); + if (out.stop == WalkStop::DIALOG || out.stop == WalkStop::ARRIVED){ + break; + } + } + if (out.stop != WalkStop::BLOCKED){ + fail_step(stream, std::string("Jubilife Pokemon Center: walking out ended with ") + walk_stop_name(out.stop) + ", not at the girls below the door."); + } + + // Jubilife's plaza has too few walls the grid gets right to plan on (a + // leg to a wall the map shows walked on 19 tiles). These are the holds + // that worked on the console: west onto the main road, north to the + // street east to Route 203, and back a couple of tiles. + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, 720ms, 300ms); + route_turn(stream, context, Direction::NORTH); + pbf_move_left_joystick(context, {0, 1}, 3960ms, 300ms); + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, 600ms, 300ms); + context.wait_for_all_requests(); + } +} + +// 01.01: east along the street until Barry stops us at the entrance to Route 203. +void east_to_barry(VideoStream& stream, ProControllerContext& context){ + // East along the street. The opening to the route is a few rows high; + // Barry stops us a little way in. A wall means we are a row off it: try + // rows to the south, then to the north. + static const int ROW_CHANGES[] = {0, 1, 1, -3, -1}; + bool barry = false; + for (int change : ROW_CHANGES){ + if (barry){ + break; + } + for (int i = 0; i < std::abs(change); i++){ + const Direction dir = change > 0 ? Direction::SOUTH : Direction::NORTH; + route_turn(stream, context, dir); + pbf_move_left_joystick(context, {0, change > 0 ? -1.0 : 1.0}, route_open_hold(dir, 1), 200ms); + } + context.wait_for_all_requests(); + WalkResult walk{WalkStop::LIMIT, 0ms, 0}; + for (size_t stretch = 0; stretch < 3 && walk.stop == WalkStop::LIMIT; stretch++){ + walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 9000ms, "east to Route 203"); + } + barry = walk.stop == WalkStop::DIALOG; + } + if (!barry){ + fail_step(stream, "Route 203: Barry did not stop us at the entrance."); + } +} + +// 01.07: west along Jubilife's long road to the crossroads. +void west_to_the_crossroads(VideoStream& stream, ProControllerContext& context){ + // Route 203's row 18 carries on as Jubilife's long road. From the first + // tile the crossroads with the street north to Route 204 is 14 tiles + // west; the street is about 4 tiles wide, so a stick hold does (by hand, + // 2026-10-31: 2 x 1120 ms landed mid-street; 7 D-pad taps stopped below + // a building, and 18 went on to the road down to Route 202, r230b). + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 14), 300ms); + context.wait_for_all_requests(); +} + +// 01.07: north up the street until Rowan's scene starts. +void north_to_rowan(VideoStream& stream, ProControllerContext& context){ + // North up the street: Rowan's scene starts a few tiles up (by hand, a + // 6 s walk). It opens with a cutscene, not a dialog box, so the walk + // reads as stopped; wait for his first line. The green-haired man walks + // about the crossroads and may stand in our way: wait and walk again. + bool scene = false; + for (size_t tries = 0; tries < 3 && !scene; tries++){ + const WalkResult up = walk_dir(stream, context, Direction::NORTH, 6000ms, "north up the street to Route 204"); + scene = up.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 8000ms); + if (!scene){ + pbf_wait(context, 2500ms); + } + } + if (!scene){ + fail_step(stream, "Jubilife: Rowan's scene did not start up the street north."); + } +} + +} + + +void add_jubilife_events(StoryEvents& events){ + events.emplace("up_the_steps", console_event(up_the_steps)); + events.emplace("into_the_school", console_event(into_the_school)); + events.emplace("barry_town_map", console_event(barry_town_map)); + events.emplace("meet_the_president", console_event(meet_the_president)); + events.emplace("up_to_the_mart_road", console_event(up_to_the_mart_road)); + events.emplace("clown_by_the_mart", console_event(clown_by_the_mart)); + events.emplace("clown_by_the_company", console_event(clown_by_the_company)); + events.emplace("to_the_main_road", console_event(to_the_main_road)); + events.emplace("to_the_center_lamp", console_event(to_the_center_lamp)); + events.emplace("poketch_from_the_president", console_event(poketch_from_the_president)); + events.emplace("up_to_the_route_203_street", console_event(up_to_the_route_203_street)); + events.emplace("out_of_the_school", console_event(out_of_the_school)); + events.emplace("rowan_after_the_battle", console_event(rowan_after_the_battle)); + events.emplace("jubilife_center_to_street", console_event(jubilife_center_to_street)); + events.emplace("east_to_barry", console_event(east_to_barry)); + events.emplace("west_to_the_crossroads", console_event(west_to_the_crossroads)); + events.emplace("north_to_rowan", console_event(north_to_rowan)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp new file mode 100644 index 0000000000..b15bcfc445 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp @@ -0,0 +1,155 @@ +/* BDSP AutoStory - Events: the lake and the Pokedex (00.02 to 00.04) + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * The scenes of the story's start that need scripted input: the briefcase + * at Lake Verity, the starter, the Starly battle and the scene after it, + * the Pokedex in Rowan's lab, Mom's and Barry's mother's lines, and Dawn's + * catching tutorial. The button sequences are Butters' (#1717); they ran on + * the console in his recordings, but not yet from the planned walks around + * them. + * + */ + +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/ImageMatchDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP_AutoStory_Actions.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +// 00.02: the lake's scene, from Barry's first line by the entrance to the +// starters' briefcase: B until the briefcase shows, then until it is open. +void starter_briefcase(VideoStream& stream, ProControllerContext& context){ + wait_for_dialogue(stream, context, "Barry before the starter selection"); + pbf_mash_button(context, BUTTON_A, 1000ms); + pbf_wait(context, 500ms); + std::shared_ptr briefcase = std::make_shared( + RESOURCE_PATH() + "PokemonBDSP/StarterBriefcase.png" + ); + ImageMatchWatcher detector(briefcase, {0.5, 0.1, 0.5, 0.7}, 100, true); + const int ret = run_until( + stream, context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 120000ms); + }, + {{detector}} + ); + if (ret != 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Failed to detect the starters' briefcase.", stream); + } + stream.log("Detected briefcase! Mashing B for it to open."); + pbf_mash_button(context, BUTTON_B, 2000ms); +} + +// 00.02: the starter from the program's option, in the briefcase: Turtwig +// is first, Chimchar a press right, Piplup two; ZL picks, then Up and ZL +// answer "this one?" with Yes. +Pokemon::Outcome choose_starter(StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep&){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + size_t rights = 0; + switch (executor.env().starter){ + case StarterChoice::TURTWIG: + rights = 0; + break; + case StarterChoice::CHIMCHAR: + rights = 1; + break; + case StarterChoice::PIPLUP: + rights = 2; + break; + } + for (size_t i = 0; i < rights; i++){ + pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); + } + pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); + context.wait_for_all_requests(); + ShortDialogWatcher watcher(COLOR_CYAN); + int ret = wait_until(stream, context, 5000ms, {{watcher}}); + if (ret != 0){ + ShortDialogWatcher watcher_red(COLOR_RED); + ret = wait_until(stream, context, 5000ms, {{watcher_red}}); + } + stream.log(ret == 0 ? "[Starter] Confirmation dialogue detected." : "[Starter] Dialogue detection failed, proceeding anyway.", ret == 0 ? COLOR_BLACK : COLOR_ORANGE); + pbf_wait(context, 400ms); + pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); + pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); + context.wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + +// 00.02: after the Starly battle, Rowan's lines and the walk out of the +// lake, B for a minute. +void after_starly(VideoStream&, ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 60000ms); + context.wait_for_all_requests(); +} + +// 00.03: Dawn walks us into Rowan's lab: his lines, "give a nickname?" +// (B: No), and the Pokedex, taken with A. +void rowan_gives_pokedex(VideoStream& stream, ProControllerContext& context){ + wait_for_dialogue(stream, context, "Dawn at Sandgem's entrance"); + pbf_mash_button(context, BUTTON_B, 50000ms); + wait_for_dialogue(stream, context, "Nickname: no"); + pbf_mash_button(context, BUTTON_B, 40000ms); + wait_for_dialogue(stream, context, "The Pokedex"); + pbf_mash_button(context, BUTTON_A, 40000ms); + context.wait_for_all_requests(); +} + +// 00.04: Barry's mother stands in the door with his parcel: four A, with +// the waits of Butters' recording between them. +void barrys_mother(VideoStream&, ProControllerContext& context){ + pbf_press_button(context, BUTTON_A, 180ms, 946ms); + pbf_press_button(context, BUTTON_A, 180ms, 9092ms); + pbf_press_button(context, BUTTON_A, 180ms, 740ms); + pbf_press_button(context, BUTTON_A, 180ms, 2998ms); + context.wait_for_all_requests(); +} + +// 00.04: Dawn's catching tutorial on Route 202: her lines and her demo +// battle, B for 70 s. +void catch_tutorial(VideoStream& stream, ProControllerContext& context){ + wait_for_dialogue(stream, context, "Dawn's catching tutorial"); + pbf_mash_button(context, BUTTON_B, 70000ms); + context.wait_for_all_requests(); + pbf_wait(context, 2000ms); +} + +} + + +void add_lake_events(StoryEvents& events){ + events.emplace("starter_briefcase", console_event(starter_briefcase)); + events.emplace("choose_starter", choose_starter); + events.emplace("fight_starly", console_event(fight_starly)); + events.emplace("after_starly", console_event(after_starly)); + events.emplace("rowan_gives_pokedex", console_event(rowan_gives_pokedex)); + events.emplace("barrys_mother", console_event(barrys_mother)); + events.emplace("catch_tutorial", console_event(catch_tutorial)); + events.emplace("catch_pokemon", console_event(catch_1_pokemon)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp new file mode 100644 index 0000000000..ec36462008 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -0,0 +1,492 @@ +/* BDSP AutoStory - Events: Oreburgh (01.03 to 01.05) + * + * From: https://github.com/PokemonAutomation/ + * + * Segment 01's walks in Oreburgh: the mine and Roark, the Gym, and back + * out of the Oreburgh Gate, as they ran on the console (2026-10-29 to + * 10-31). + * + */ + +#include +#include "CommonFramework/GlobalAutoPaths.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +namespace{ + +// Oreburgh: from a row or two below the Pokemon Center's front, west of +// the fence below its east corner, into the Center by its door, stopping +// just inside. Talks to Barry if he stands in the door (01.03, the first +// time). +void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ + // The save stands us a row or two below the Center's front, a little + // west of its door. The posts and the road beside the Center make north + // from there unreliable (r191a, r192a walked up the road to the city's + // north exit), and counted taps east drift. The fence running south from + // the Center's east corner stops a walk east on these rows a tile east of + // the door (by hand): walk to it, north to the Center's front, tap back + // one, and shift one tap either side. + walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 5000ms, "east to the fence below the Center"); + walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); + // The door wants us near its middle: a turn and a tap west (half a tile + // too far, by hand) or a turn alone leave north blocked by the facade + // beside it. The facade blocks north for two tiles either side of the + // door, so look for it in nudges of about a third of a tile. + route_turn(stream, context, Direction::WEST); + context.wait_for_all_requests(); + auto inside_now = [&](){ + const VideoSnapshot frame = stream.video().snapshot(); + size_t percent = 0; + return frame && (center_floor_on_screen(*frame.frame, &percent), percent >= 45); + }; + bool inside = false; + const int offsets[] = {0, -1, -2, 1, -3, 2, -4, 3}; // nudges from here + int nudged = 0; + for (size_t i = 0; i < 8 && !inside; i++){ + const int offset = offsets[i]; + if (offset != nudged){ + // No turn first: a turn is a tap too and moves us (see + // talk_with_nudges()); a nudge both turns and moves a little. + for (; nudged != offset; nudged += offset > nudged ? 1 : -1){ + pbf_move_left_joystick(context, {offset > nudged ? 1.0 : -1.0, 0}, ROUTE_NUDGE_TAP, 300ms); + } + context.wait_for_all_requests(); + } + // The door's fade often reads as the time limit, not a black screen + // (r183a went in and on to the counter): look at the floor. + // Stop as soon as the floor shows: walking on to the counter left the + // door's column, and the walk back out drifted into the yard (r188b). + // Oreburgh's sand reads up to 34 % warm outside (r189a), the floor + // 56 to 64 % inside. + const WalkResult in = walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", + [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); + if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && inside_now())){ + inside = true; + break; + } + if (in.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, "Someone at the Center's door"); + }else{ + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + if (!dialogue_opens(stream, context, 2000ms)){ + continue; + } + finish_dialogue(stream, context, BUTTON_B, "Barry at the Center's door"); + context.wait_for(2000ms); // he runs off + } + // Talked in the door's column: the door is free now. + walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", + [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); + inside = inside_now(); + } + if (!inside || !inside_now()){ + fail_step(stream, "Oreburgh: no column east of the save went into the Pokemon Center."); + } +} + +// Oreburgh: from just inside the Pokemon Center's door, up to the nurse +// and heal; ends at the counter facing her. The door is straight south of +// her (by hand, 2026-10-31: north from the door stopped at the counter). +void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context){ + if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ + fail_step(stream, "Oreburgh Pokemon Center: the nurse did not answer."); + } + // A throughout: Yes to "rest your Pokemon?" (B would answer No). + finish_dialogue(stream, context, BUTTON_A, "Nurse"); + // Healing shows no text box for a few seconds, which reads as the end; + // her "Thank you for waiting" lines follow it. + if (dialogue_opens(stream, context, 6000ms)){ + finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); + } +} + + + +// Oreburgh, from the street in front of the Pokemon Center (its door or its +// front wall) into the gym, as walked by hand on 2026-10-29: west to the +// cliff, south to the posts, a row up, then 17 tiles east is the gym door's +// column. The hold is checked by the door itself: walk north, and if the +// gym's wall stops us, try the columns either side. +// D-pad moves from the Center's doorstep (as walk_out_of_center() leaves us +// there) to below the gym's door: 20. The program's own runs landed 2 tiles +// west with 22 and 1 tile west with 21 (r217d, r218d, the same tiles each +// time). By hand, after a stick exit, it took 22 taps; the hand exit and +// count did not carry over to the program's. Stick holds drift: 160 ms moved +// 36 to 48 px, so 18 or 19 of them missed the door by a part of a tile. +constexpr size_t GYM_DPAD_TAPS_WEST_OF_CENTER = 20; + +void oreburgh_street_to_gym(VideoStream& stream, ProControllerContext& context){ + // From the Center's doorstep, straight west along its row to below the + // gym's door, then north through it, all on the D-pad (whole tiles; see + // GYM_DPAD_TAPS_WEST_OF_CENTER). + dpad_tiles(stream, context, DPAD_LEFT, GYM_DPAD_TAPS_WEST_OF_CENTER, "west to the gym's door"); + if (dpad_through_door_north(stream, context, "Oreburgh Gym door")){ + return; + } + fail_step(stream, "Oreburgh: no column near the gym's door went in."); +} + +// Inside the Oreburgh Gym: straight up the middle, fighting the trainers +// who stop us, to Roark on the top tier. Returns whether Roark was beaten; +// false after a whiteout, which leaves us in the Pokemon Center. +// Walking straight up meets both gym trainers (one at the entrance, one on +// the second tier); their experience helps an under-levelled team, and +// after a whiteout they stay beaten. Butters' route goes round them. +bool oreburgh_gym_battles(VideoStream& stream, ProControllerContext& context){ + // The entry camera pans for about 7.5 s (Butters' reference video). + context.wait_for(8000ms); + if (dialogue_opens(stream, context, 3000ms)){ + if (talk_or_fight(stream, context, "Gym trainer at the entrance") && in_pokemon_center(stream)){ + return false; + } + } + size_t stuck = 0; + for (size_t leg = 0; leg < 10; leg++){ + const WalkResult up = walk_until_no_wild(stream, context, 0, 1, 8000ms, "north up the gym"); + if (up.stop == WalkStop::DIALOG){ + if (talk_or_fight(stream, context, "Gym trainer") && in_pokemon_center(stream)){ + return false; + } + continue; + } + if (up.stop != WalkStop::BLOCKED){ + continue; + } + // Roark stands at the top facing us; a wall or a stair edge says + // nothing to A. Only Roark starts a battle from our A here. The walk + // left us facing north: a turn tap that way would be a step. + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + if (dialogue_opens(stream, context, 3000ms)){ + // His lines pause for more than 4 s before the battle (r210d: "only + // talked"), so the battle is expected. + if (talk_or_fight(stream, context, "Roark", true)){ + if (in_pokemon_center(stream)){ + return false; + } + finish_dialogue(stream, context, BUTTON_B, "Roark after the battle"); + return true; + } + continue; + } + if (up.walked <= 800ms && ++stuck >= 3){ + fail_step(stream, "Oreburgh Gym: stuck on the way up, and nobody answers A."); + } + } + fail_step(stream, "Oreburgh Gym: did not reach Roark."); +} + +// 01.03: into the Pokemon Center by its door (talking to Barry if he stands in it). +void oreburgh_center_door(VideoStream& stream, ProControllerContext& context){ + // Planned on Bulbapedia's Oreburgh map (devtools/maps/examples/ + // oreburgh_center_to_mine.png): the column of the Center's door runs + // straight south down the yard's stairs, between the conveyors, into + // the mine (walked north up it by hand, 2026-10-31). The door is the one + // place that pins that column: go in and back out, and we stand on it + // facing south. The save stands us two tiles west of the door and a row + // down; north stops against the Center's front or its post, then a tile + // at a time east until north goes in. Barry stands in the door the first + // time: walking into him reads as a wall, so A there; he tells us Roark + // went to the mine, which Roark needs before he answers (2026-10-29: he + // ignored A in r101 and r104 until then). A save after his talk has him + // gone already. + // + enter_oreburgh_center(stream, context); + context.wait_for(1000ms); +} + +// 01.03: straight south from the door into the mine. +void south_into_the_mine(VideoStream& stream, ProControllerContext& context){ + // Straight back south: out of the door and on into the mine. Fades go + // unseen at times (r184b missed the mine's, r185a the Center's and walked + // on into the mine hall), so count: the Center's door and the mine are + // two transitions, or about 1 s inside and 8.6 s outside of walking (by + // hand). A worker walks about on the sand path and stops the walk as a + // wall would (r95, r96): wait for him to move and walk on. Step 3 copes + // with ending in the hall or in the corridor. + bool in_mine = false; + size_t transitions = 0; + Milliseconds walked = 0ms; + for (size_t attempt = 0; attempt < 6 && !in_mine; attempt++){ + const WalkResult down = walk_dir(stream, context, Direction::SOUTH, 15000ms, "south out of the Center and into the mine"); + walked += down.walked; + if (down.stop == WalkStop::TRANSITION){ + transitions++; + context.wait_for(1000ms); + }else if (down.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, "Someone on the way to the mine"); + }else if (down.stop == WalkStop::BLOCKED){ + context.wait_for(2000ms); + } + // Counting alone took a walk that drifted into the yard (r188b) for the + // mine: it also has to look like the mine. + in_mine = (transitions >= 2 || walked >= 9500ms) && in_dim_cave(stream); + } + if (!in_mine){ + fail_step(stream, "Oreburgh: the walk south from the Center's door did not reach the mine."); + } + context.wait_for(1500ms); +} + +// 01.03: through the mine's entrance hall to the rocky corridor. +void mine_hall(VideoStream& stream, ProControllerContext& context){ + // If the fade into the mine was missed, we are already in the corridor + // and this walk stops at its boulder; otherwise it crosses the hall. + const WalkResult hall = walk_dir(stream, context, Direction::SOUTH, 10000ms, "through the mine's entrance hall"); + if (hall.stop == WalkStop::TRANSITION){ + context.wait_for(1500ms); + } +} + +// 01.03: the planned route down the corridor to Roark, his talk, and back out of the mine. +void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ + // Planned on the coal room's tile grid (Data/OreburghMine): the + // corridor's rocks zigzag a row at a time, the room's second row passes + // under the worker on the first, and the east side leads down to the + // bottom row below Roark. + const StoryWorld& maps = oreburgh_mine_maps(); + auto world = [&](const Pokemon::TilePos& pos){ return maps.to_world(MAP_OREBURGH_MINE, pos); }; + std::vector no_trainers; + auto in_the_way = [&](const std::string& who){ talk_or_fight(stream, context, who); }; + std::vector at; + for (const Pokemon::TilePos& pos : map_region("OreburghMine", "corridor_top")){ + at.push_back(world(pos)); + } + // Roark stands with a rock on each side, so the rock east of him is the + // anchor: plan to his row's open stretch east of the rock, walk north to + // the wall above it and west until the rock stops us, two tiles east of + // him (on the console, r97). Then south to the bottom wall and two tiles + // west puts us below him. + const Pokemon::TilePos roark = world(map_point("OreburghMine", "roark")); + at = walk_route(stream, context, run_from_wild_battle, maps, at, + [&](const Pokemon::TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }, + no_trainers, in_the_way, "To the stretch east of Roark"); + walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); + // His rock and the bottom wall end these legs (see walk_to_wall()). The + // two taps west from there land within a third of a tile or so of his + // column (s03e2 missed him by that much), so look for him with nudges + // either side. Any tap here can start a wild battle, which leaves us + // off his column (r180a, even on his row west of his rock): then line up + // again from the east end of the bottom row, three taps west and north to the wall on his row (Data/ + // OreburghMine: columns 23 to 25 have that wall above). + bool talked = false; + for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ + if (attempt > 0){ + // The battle may leave us on his row or the bottom one. + walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom row"); + walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 6000ms, "east along the bottom row"); + route_turn(stream, context, Direction::WEST); + for (size_t tap = 0; tap < 3; tap++){ + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + } + walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); + } + // A wild battle on the way eats into the walk's time (r100): walk again. + for (size_t walk = 0; walk < 3; walk++){ + if (walk_to_wall(stream, context, run_from_wild_battle, -1, 0, 6000ms, "west to the rock beside Roark").wild_battles == 0){ + break; + } + } + walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom wall"); + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + route_turn(stream, context, Direction::NORTH); + context.wait_for_all_requests(); + if (run_from_late_encounter(stream, context, run_from_wild_battle, "below Roark")){ + continue; + } + if (VideoSnapshot screen = stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-below-roark.png"); + } + // The camera zooms in and his first line, "OK, stand back and watch + // this.", can take some seconds (r98). + // His rocks answer A too ("It's a rugged rock..."): his talk is the + // one that zooms the camera in on us (by hand), which the rock's + // message does not. Compare the view above the dialogue box. + talked = talk_with_nudges(stream, context, run_from_wild_battle, Direction::NORTH, "Roark", 8000ms, + [&](const ImageViewRGB32& before, const ImageViewRGB32& after){ + const ImageFloatBox above_box{0.0, 0.0, 1.0, 0.6}; + const double change = walk_screen_change( + extract_box_reference(before, above_box), extract_box_reference(after, above_box) + ); + stream.log("[01.03] View change when the talk opened: " + std::to_string(change) + " (Roark zooms in)."); + return change > 35; // by hand: 50 for his zoom + }); + } + if (!talked){ + fail_step(stream, "Oreburgh Mine: no talk opened below Roark (has Barry told us about the mine?)."); + } + // His scene (by hand, 2026-10-31): the camera zooms in, about eight + // lines that each wait for a press ("OK, stand back and watch this.", + // Rock Smash, "...That'd be me!"), then he smashes his rock and leaves. + // finish_dialogue() ended early on it, and stepping onto his tile could + // not tell him from the time the stop takes to read (r184, r186 "passed" + // with him still there, and Barry kept the gym shut). Press B through it + // on a clock, then ask his tile: once he is gone, A there opens nothing. + // B and A in the overworld facing a wall do nothing. + bool gone = false; + for (size_t round = 0; round < 3 && !gone; round++){ + for (size_t press = 0; press < 14; press++){ + pbf_press_button(context, BUTTON_B, 80ms, 2420ms); + } + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + gone = !dialogue_opens(stream, context, 2500ms); + } + if (!gone){ + fail_step(stream, "Oreburgh Mine: Roark is still there after the talk."); + } + walk_dir(stream, context, Direction::SOUTH, 2000ms, "back to the bottom wall"); + at = {roark.next(Direction::SOUTH)}; + + // Back up the corridor, then north out through the hall. A route leg + // north to the top's wall walks on through it into the hall (r99), and + // no wall stops us a row short of it, so the route ends on the corner + // at (14,10), where that leg started, and the walk does the rest. + const Pokemon::TilePos exit = world(map_point("OreburghMine", "corridor_exit_start")); + at = walk_route(stream, context, run_from_wild_battle, maps, at, + [&](const Pokemon::TilePos& pos){ return pos == exit; }, + no_trainers, in_the_way, "Back to the corridor's top"); + // Up into the entrance hall (a transition, as on the way in), then on + // north out of the mine: in r103 that walk left the mine without a + // transition we saw and ended against the sign above the stairs, which + // the moving belts make read as the time limit. + if (walk_dir(stream, context, Direction::NORTH, 10000ms, "into the entrance hall").stop != WalkStop::TRANSITION){ + fail_step(stream, "Oreburgh Mine: walking north from the corridor did not reach the entrance hall."); + } + context.wait_for(1500ms); + if (walk_dir(stream, context, Direction::NORTH, 10000ms, "out of the mine").stop == WalkStop::DIALOG){ + fail_step(stream, "Oreburgh Mine: someone stopped us on the way out of the mine."); + } + context.wait_for(1500ms); +} + +// 01.03: into the Pokemon Center and heal at the counter. +void oreburgh_center_heal(VideoStream& stream, ProControllerContext& context){ + // End at the nurse, healed: the counter is a fixed start for 01.04, + // where the mine's mouth is not (2026-10-31, at the user's request). Out + // of the mine we stand below its sign; a tile east is the Center door's + // column, and north on it goes in, or stops on the front rows. + hold_tiles(stream, context, Direction::EAST, 1); + walk_until(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into the Center's door", + [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); + size_t floor = 0; + if (VideoSnapshot frame = stream.video().snapshot()){ + center_floor_on_screen(*frame.frame, &floor); + } + if (floor < 45){ + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, 480ms, 300ms); + enter_oreburgh_center(stream, context); + } + heal_at_oreburgh_counter(stream, context); +} + +// 01.04: out of the Pokemon Center from the counter. +void out_of_the_center(VideoStream& stream, ProControllerContext& context){ + // 01.03 leaves us at the Center's counter, facing the nurse; the door + // is straight south. + walk_out_of_center(stream, context, "Oreburgh"); +} + +// 01.04: west along the row into the gym and up it to Roark, again from the Center after a loss. +void gym_until_roark_beaten(VideoStream& stream, ProControllerContext& context){ + // Losing costs some money and keeps the experience, so each try comes + // back stronger: Roark won twice by hand against Piplup Lv 10 and 14 and + // lost the third time. + bool won = false; + for (size_t attempt = 0; attempt < 5 && !won; attempt++){ + oreburgh_street_to_gym(stream, context); + won = oreburgh_gym_battles(stream, context); + if (!won){ + stream.log("[01.04] Lost in the gym (try " + std::to_string(attempt + 1) + "); back from the Center.", COLOR_ORANGE); + walk_out_of_center(stream, context, "Oreburgh"); + } + } + if (!won){ + fail_step(stream, "Oreburgh Gym: lost to Roark five times."); + } +} + +// 01.05: out of the gym onto its doorstep. +void out_of_the_gym(VideoStream& stream, ProControllerContext& context){ + // 01.04 ends in front of Roark, just after his battle: its save at + // 01.05's start is right after the win (2026-10-31, at the user's + // request), so a failure later never has to beat him again. + // No trip back to the Oreburgh Center (2026-10-31, the user): talk_or_fight() + // gives a Potion after Roark when HP is low, and Jubilife's Center is on + // the way. Out of the gym onto its doorstep, which is on the Center door's + // row: west along it reaches the stairs' cliff. + if (!dpad_out_of_building(stream, context, 40, "out of the gym")){ + fail_step(stream, "Oreburgh Gym: did not leave by its door."); + } +} + +// 01.05: up to the platform by the Oreburgh Gate and into it (Barry bumps into us there). +void into_oreburgh_gate(VideoStream& stream, ProControllerContext& context){ + // From the gym's doorstep, as walked by hand on 2026-10-29. + walk_dir(stream, context, Direction::WEST, 20000ms, "west to the stairs' cliff"); + walk_dir(stream, context, Direction::NORTH, 10000ms, "north along the cliff"); + hold_tiles(stream, context, Direction::EAST, 2); + walk_dir(stream, context, Direction::NORTH, 5000ms, "north up the stairs"); + walk_dir(stream, context, Direction::WEST, 6000ms, "west along the platform"); + hold_tiles(stream, context, Direction::SOUTH, 1); + // Barry runs into us on the platform ("*thud*") the first time; his lines + // end with no battle. + west_until_transition(stream, context, "Into Oreburgh Gate", 8); +} + +// 01.05: west through the Gate until Route 203's banner shows. +void out_of_oreburgh_gate(VideoStream& stream, ProControllerContext& context){ + // The cave's fade out of the Gate does not always read as a transition + // (r89), so the walk west ends on Route 203's location banner, which + // shows for a few seconds as we step out. 01.06 starts there, from a save. + west_until_transition(stream, context, "West out of Oreburgh Gate", 8, location_banner_showing); +} + +} + + +void add_oreburgh_events(StoryEvents& events){ + events.emplace("oreburgh_center_door", console_event(oreburgh_center_door)); + events.emplace("south_into_the_mine", console_event(south_into_the_mine)); + events.emplace("mine_hall", console_event(mine_hall)); + events.emplace("roark_in_the_mine", console_event(roark_in_the_mine)); + events.emplace("oreburgh_center_heal", console_event(oreburgh_center_heal)); + events.emplace("out_of_the_center", console_event(out_of_the_center)); + events.emplace("gym_until_roark_beaten", console_event(gym_until_roark_beaten)); + events.emplace("out_of_the_gym", console_event(out_of_the_gym)); + events.emplace("into_oreburgh_gate", console_event(into_oreburgh_gate)); + events.emplace("out_of_oreburgh_gate", console_event(out_of_oreburgh_gate)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp new file mode 100644 index 0000000000..7445903086 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp @@ -0,0 +1,231 @@ +/* BDSP AutoStory - Events: Route 203 (01.02, 01.06) + * + * From: https://github.com/PokemonAutomation/ + * + * Segment 01's walks along Route 203: east to the Oreburgh Gate and through + * it to Oreburgh, and back west to Jubilife, as they ran on the console + * (2026-10-29 to 10-31). + * + */ + +#include +#include "CommonFramework/GlobalAutoPaths.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +namespace{ + +// 01.02: east along Route 203 into the Oreburgh Gate, fighting whoever stops us. +Pokemon::Outcome route_203_east_to_the_gate(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + // Route 203 only goes east, over three stretches joined by stairs, and + // the Gate's opening is in the cliff at its north-east end. Who stops us + // differs from run to run (a trainer's sight depends on our row), so + // there is no fixed list: walk east, talk or fight whoever stops us, and + // at a wall find a row from which east goes on. + size_t fights = 0; + bool inside = false; + // On Route 203 only trainers start a talk; one with no battle is the + // Hiker, who meets us inside the Gate (whose fade may not read as a + // transition). + bool met_hiker = false; + auto stopped_by = [&](){ + if (talk_or_fight(stream, context, "Route 203 trainer " + std::to_string(fights + 1))){ + fights++; + }else{ + route_status(stream, "No battle: that was the Hiker, inside the Oreburgh Gate."); + met_hiker = true; + inside = true; + } + }; + for (size_t leg = 0; leg < 40 && !inside; leg++){ + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203"); + if (walk.stop == WalkStop::TRANSITION){ + inside = true; + break; + } + if (walk.stop == WalkStop::LIMIT){ + continue; + } + if (walk.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 1500ms)){ + stopped_by(); + continue; + } + // A wall. Find the row from which east goes on: north to the top wall + // first (a step north there does not move us, so rows are counted + // from it), then a row at a time south. At the route's end this is the + // corner by the second sign, with the Gate's opening two rows down. + walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "north to the top wall"); + bool moved_on = false; + for (size_t row = 1; row <= 10 && !moved_on; row++){ + step_south(stream, context); + const WalkResult next = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); + if (next.stop == WalkStop::TRANSITION){ + inside = true; + } + if (next.stop == WalkStop::DIALOG || (next.stop == WalkStop::BLOCKED && dialogue_opens(stream, context, 1500ms))){ + stopped_by(); + } + // Two still chunks are the least a wall takes; more is progress. + moved_on = inside || next.stop != WalkStop::BLOCKED || next.walked > 1200ms; + } + if (!moved_on){ + fail_step(stream, "Route 203: walls east on every row down to ten below the top wall."); + } + } + if (!inside){ + fail_step(stream, "Route 203: did not reach the Oreburgh Gate."); + } + route_status(stream, "Route 203: " + std::to_string(fights) + " trainer battle(s) on the way."); + context.wait_for(1500ms); + if (met_hiker){ + state.flags.insert("met_hiker"); + } + return Pokemon::outcome_ok(); +} + +// 01.02: the Hiker, who gives HM Rock Smash, when the walk east has not met him. +void the_hiker(VideoStream& stream, ProControllerContext& context){ + // The Hiker steps up to us as we walk east. + const WalkResult to_hiker = walk_until(stream, context, run_from_wild_battle, 1, 0, 4000ms, "to the Hiker"); + if (to_hiker.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ + fail_step(stream, "Oreburgh Gate: the Hiker did not stop us."); + } + finish_dialogue(stream, context, BUTTON_B, "Hiker"); +} + +// 01.02: the "Hidden Moves" card after the Hiker. +void hidden_moves_card(VideoStream& stream, ProControllerContext& context){ + // Then a "Hidden Moves" tutorial card, closed with B. It is no dialogue + // box, and it can come up a moment after his last line. + pbf_wait(context, 1500ms); + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); +} + +// 01.02: east through the Gate past Curtis and Diana, out to Oreburgh City. +void gate_east_to_oreburgh(VideoStream& stream, ProControllerContext& context){ + // East: Camper Curtis and Picnicker Diana see us; past them the exit's + // white fade, and the man in Oreburgh stops us right outside. + size_t fights = 0; + bool city = false; + size_t stuck = 0; + for (size_t leg = 0; leg < 12 && !city; leg++){ + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east through the Oreburgh Gate"); + if (walk.stop == WalkStop::LIMIT || walk.stop == WalkStop::TRANSITION){ + continue; + } + if (walk.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ + // A card or menu left up holds the screen still: B once, then retry. + if (++stuck > 1){ + fail_step(stream, "Oreburgh Gate: a wall stopped us walking east."); + } + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + continue; + } + stuck = 0; + if (fights >= 2){ + city = true; + break; + } + fights += talk_or_fight(stream, context, "Oreburgh Gate trainer " + std::to_string(fights + 1)) ? 1 : 0; + } + if (!city){ + fail_step(stream, "Oreburgh Gate: did not reach Oreburgh City."); + } +} + +// 01.02: the man at the city's entrance and Barry, at the Pokemon Center. +void oreburgh_welcome(VideoStream& stream, ProControllerContext& context){ + // He walks us to the Pokemon Center between his lines, and Barry joins + // there: more lines after each walk. + finish_dialogue(stream, context, BUTTON_B, "Man at Oreburgh's entrance"); + for (size_t scene = 0; scene < 6 && dialogue_opens(stream, context, 6000ms); scene++){ + finish_dialogue(stream, context, BUTTON_B, "Oreburgh scene"); + } + context.wait_for_all_requests(); +} + +// 01.06: Route 203 on its grid to row 18, below the girl. +Pokemon::Outcome route_203_to_row_18(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + // On the grid (Data/PokemonBDSP_AutoStory_Route203, from the decomp, + // its east stretch checked by hand): south from the Gate down the sand, + // west along the sand's bottom row and on under the girl's row, which + // runs straight to Jubilife as row 18. Trainers the map does not list + // are fought where they stop us. + std::vector trainers; + auto fight = [&](const std::string& who){ + talk_or_fight(stream, context, "Route 203: " + who); + }; + const std::vector at = walk_route(stream, context, run_from_wild_battle, route203_maps(), map_region("Route203", "gate_exit"), + on_route203_west_line, trainers, fight, "Route 203 to row 18"); + if (at.size() != 1){ + fail_step(stream, "Route 203: the walk to row 18 did not end on a known tile."); + } + return Pokemon::outcome_ok(Pokemon::tile_pose("Route203", at)); +} + +// 01.06: west along row 18 into Jubilife, counting tiles. +Pokemon::Outcome row_18_into_jubilife(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + const std::vector at = Pokemon::tiles_of(state.pose); + if (at.size() != 1){ + fail_step(stream, "Route 203: row 18's walk needs the one tile the walk to it ended on."); + } + // Row 18 has no wall before Jubilife, so count the tiles: from column + // `x` it is x + 1 taps to Jubilife's first tile (by hand from column 31: + // 32 taps, Jubilife's banner on the last one). 01.07 starts there. + dpad_tiles(stream, context, DPAD_LEFT, (size_t)at[0].x + 1, "west along row 18 into Jubilife"); + context.wait_for_all_requests(); + if (!location_banner_showing(*stream.video().snapshot().frame)){ + fail_step(stream, "Route 203: no Jubilife banner after counting the tiles west."); + } + return Pokemon::outcome_ok(); +} + +} + + +void add_route203_events(StoryEvents& events){ + events.emplace("route_203_east_to_the_gate", route_203_east_to_the_gate); + events.emplace("route_203_to_row_18", route_203_to_row_18); + events.emplace("row_18_into_jubilife", row_18_into_jubilife); + events.emplace("the_hiker", console_event(the_hiker)); + events.emplace("hidden_moves_card", console_event(hidden_moves_card)); + events.emplace("gate_east_to_oreburgh", console_event(gate_east_to_oreburgh)); + events.emplace("oreburgh_welcome", console_event(oreburgh_welcome)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp new file mode 100644 index 0000000000..bdda697a2c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -0,0 +1,242 @@ +/* BDSP AutoStory - Events: Route 204 to Floaroma (01.08 to 01.11) + * + * From: https://github.com/PokemonAutomation/ + * + * Segment 01's walks up Route 204 South and through the Ravaged Path, up Route 204 North into + * Floaroma, and to the old couple in its flower field, as they ran on the + * console (2026-10-31). + * + */ + +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +namespace{ + +// "This rock seems breakable with a hidden move. Would you like to use Rock +// Smash?" is open, from an A at a smashable rock. Its Yes/No box comes only +// once the text is advanced again (by hand, 2026-10-31: the text complete +// 1.2 s after the first A, the box 1.2 s after a second), and r237a's timed +// A, A, B put its B on that box: No. So press A until the box's cursor +// shows, then once more for Yes, and wait out the move. False if no box +// shows: whatever is open is not this prompt. +bool answer_rock_smash_prompt(VideoStream& stream, ProControllerContext& context, const std::string& label){ + static const ImageFloatBox YES_NO_ARROW_BOX{0.700, 0.620, 0.070, 0.120}; + SelectionArrowFinder yes_no(stream.overlay(), YES_NO_ARROW_BOX, COLOR_GREEN); + for (size_t press = 0; press < 5; press++){ + context.wait_for(1200ms); + context.wait_for_all_requests(); + const VideoSnapshot screen = stream.video().snapshot(); + if (screen && yes_no.detect(*screen.frame)){ + stream.log("[Route] " + label + ": Rock Smash offered; Yes."); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for(5000ms); + if (dialogue_opens(stream, context, 500ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": after Rock Smash"); + } + return true; + } + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + } + stream.log("[Route] " + label + ": no Yes/No box for Rock Smash.", COLOR_RED); + return false; +} + +// 01.08: north out of Jubilife onto Route 204. +void north_out_of_jubilife(VideoStream& stream, ProControllerContext& context){ + // The street's column 14 carries on as the route's; the location banner + // names Route 204 as we cross. A D-pad walk keeps the column; it may + // stop a tile or two past the border, which the entry set allows for. + const WalkResult out = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 8000ms, "north out of Jubilife", location_banner_showing); + if (out.stop != WalkStop::ARRIVED){ + fail_step(stream, "Jubilife: no Route 204 banner walking north up the street."); + } + context.wait_for(1500ms); +} + +// 01.08: Route 204 South on its grid to below the Ravaged Path's door. +void route_204_south_to_the_door(VideoStream& stream, ProControllerContext& context){ + // On the grid decoded from the game's data (Data/..._Route204South): + // a tile west onto the tall grass column that runs up to the cliff, + // north up it, then west along the cliff to the signboard beside the + // door (the offline plan: 3 legs). Other trainers' sight lines are kept + // out of; town_plan_costs() lets the one-tile step land exactly. Lass Sarah's east line crosses + // the street, so she may stop us for a battle on the way; a Youngster + // who is not a trainer wanders in front of the door, and the walker + // waits for him. + { + std::vector trainers = route204_south_trainer_sights(); + auto fight = [&](const std::string& who){ talk_or_fight(stream, context, who); }; + const std::vector at = walk_route(stream, context, run_from_wild_battle, route204_south_maps(), + map_region("Route204South", "jubilife_entry"), + [](const Pokemon::TilePos& pos){ return pos == map_point("Route204South", "below_door"); }, + trainers, fight, "Route 204 South to the Ravaged Path", route204_south_tiles_to_avoid(), town_plan_costs()); + if (at.size() != 1 || !(at[0] == map_point("Route204South", "below_door"))){ + fail_step(stream, "Route 204: the walk did not end below the Ravaged Path's door."); + } + } +} + +// 01.08: north through the door. +void into_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ + if (!dpad_through_door_north(stream, context, "Ravaged Path door")){ + fail_step(stream, "Route 204: no dark screen walking north into the Ravaged Path."); + } + context.wait_for(2000ms); +} + +// 01.09: through the Ravaged Path, smashing its rocks. +void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ + // On the traced grid (Data/PokemonBDSP_AutoStory_RavagedPath): from the + // south door up the stairs to the tile west of one of the two rocks + // across the lower corridor, smash it, then east and south to the + // corridor's east end; the way out is a tile west of it, south. + { + std::vector no_trainers; + // Blocked short of a wall in here, it is a rock the plan counted on + // being gone (r237a: the walker took one for a person and said No). + auto in_the_way = [&](const std::string& who){ + if (!answer_rock_smash_prompt(stream, context, who)){ + talk_or_fight(stream, context, who); + } + }; + std::vector at{map_point("RavagedPath", "entrance")}; + const StoryWorld rocks_in = ravaged_path_maps(); + at = walk_route(stream, context, run_from_wild_battle, rocks_in, at, + [&](const Pokemon::TilePos& pos){ + const auto& before = map_region("RavagedPath", "before_rocks"); + return std::find(before.begin(), before.end(), pos) != before.end(); + }, + no_trainers, in_the_way, "To a rock across the corridor"); + route_turn(stream, context, Direction::EAST); + if (!talk_with_nudges(stream, context, run_from_wild_battle, Direction::EAST, "the rock east of us", 2000ms)){ + fail_step(stream, "Ravaged Path: nothing answered facing the rock east of us."); + } + if (!answer_rock_smash_prompt(stream, context, "the rock east of us")){ + fail_step(stream, "Ravaged Path: no Rock Smash offer facing the rock east of us."); + } + // We may be on either row; the rock east of the other stays, but no + // leg from our row crosses it. + std::vector smashed; + for (const Pokemon::TilePos& pos : map_region("RavagedPath", "before_rocks")){ + smashed.push_back(pos.next(Direction::EAST)); + } + const StoryWorld rocks_out = ravaged_path_maps(smashed); + at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, + [&](const Pokemon::TilePos& pos){ return pos == map_point("RavagedPath", "east_end"); }, + no_trainers, in_the_way, "To the corridor's east end"); + dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); + const WalkResult out = walk_dir(stream, context, Direction::SOUTH, 4000ms, "south out of the Ravaged Path"); + if (out.stop != WalkStop::TRANSITION && out.stop != WalkStop::LIMIT){ + fail_step(stream, "Ravaged Path: the walk south from a tile west of the corridor's end did not leave the cave."); + } + context.wait_for(2000ms); + } +} + +// 01.10: Route 204 north on the human's line, into Floaroma. +void route_204_to_floaroma(VideoStream& stream, ProControllerContext& context){ + // The line a human walked (Data/PokemonBDSP_AutoStory_Route204, + // floaroma_waypoints()): a tile south out of the cave's mouth, west along + // the row under the cliff past the Youngster who wanders there, up the + // stairs, east along the corridor into Aroma Lady Taylor's battle, north + // through the tall grass, west below the flower beds, and north between + // the beds into Floaroma. Whole tiles on the D-pad, corner to corner. + { + std::vector trainers = route204_north_trainer_sights(); + auto fight = [&](const std::string& who){ + const bool doubles = who.find("Twins") != std::string::npos || who.find("someone") != std::string::npos; + talk_or_fight(stream, context, who, false, doubles); + }; + std::vector at{map_point("Route204North", "cave_exit")}; + at = walk_waypoints(stream, context, run_from_wild_battle, route204_north_maps(), at, map_region("Route204North", "floaroma_waypoints"), + trainers, fight, "To the path into Floaroma"); + if (!std::all_of(at.begin(), at.end(), in_floaroma_path)){ + fail_step(stream, "Route 204: the walk did not end in the path into Floaroma."); + } + // North until the town's banner shows: the checkpoint ends right + // there, where the save goes (r247a walked 20 s on and overshot). + const WalkResult in = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into Floaroma", location_banner_showing); + if (in.stop != WalkStop::ARRIVED){ + fail_step(stream, "Route 204: no location banner walking north into Floaroma."); + } + context.wait_for(500ms); + const VideoSnapshot banner = stream.video().snapshot(); + const std::string read = banner ? read_location_banner(*banner.frame) : ""; + if (!location_banner_matches(read, "Floaroma Town")){ + fail_step(stream, "Route 204: the banner read \"" + read + "\", not Floaroma Town."); + } + stream.log("[01.10] In Floaroma Town; the banner read \"" + read + "\".", COLOR_GREEN); + } +} + +// 01.11: west 8 along the flower field's bottom row, north until the old woman stops us. +void west_8_north_to_the_old_woman(VideoStream& stream, ProControllerContext& context){ + { + const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 8, "west 8 to the flower field"); + if (west.stop != WalkStop::LIMIT || west.moved_chunks < 7){ + fail_step(stream, "Floaroma: west 8 to the flower field moved " + std::to_string(west.moved_chunks) + " tiles."); + } + // Blocked, or out of taps with the last ones bumping: either way + // she is in front of us (r250a: 3 steps, 2 bumps, "limit"). + const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 5, "north until the old woman"); + if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 5){ + fail_step(stream, "Floaroma: nothing stopped the walk north to the old woman."); + } + } +} + +// 01.11: one tile west, then a tap north: the old man stands there, so the tap only turns. +void below_the_old_man(VideoStream& stream, ProControllerContext& context){ + const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 1, "west 1 below the old man"); + if (west.moved_chunks < 1){ + fail_step(stream, "Floaroma: the step west below the old man did not move."); + } + const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 3, "north until the old man"); + if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 3){ + fail_step(stream, "Floaroma: nothing stopped the walk north to the old man."); + } +} + +} + + +void add_route204_events(StoryEvents& events){ + events.emplace("north_out_of_jubilife", console_event(north_out_of_jubilife)); + events.emplace("route_204_south_to_the_door", console_event(route_204_south_to_the_door)); + events.emplace("into_the_ravaged_path", console_event(into_the_ravaged_path)); + events.emplace("through_the_ravaged_path", console_event(through_the_ravaged_path)); + events.emplace("route_204_to_floaroma", console_event(route_204_to_floaroma)); + events.emplace("west_8_north_to_the_old_woman", console_event(west_8_north_to_the_old_woman)); + events.emplace("below_the_old_man", console_event(below_the_old_man)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp new file mode 100644 index 0000000000..50d28f0cad --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp @@ -0,0 +1,345 @@ +/* BDSP AutoStory - Events: Sandgem and Route 202 (00.04) + * + * From: https://github.com/PokemonAutomation/ + * + * 00.04's Potions in Sandgem (the Poke Mart lady, or the clerk) and the + * way back to Route 202's save spot, and the stairs into Jubilife. + * + */ + +#include +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Bag.h" +#include "PokemonBDSP_AutoStory_Center.h" +#include "PokemonBDSP_AutoStory_Mart.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Events.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::TilePos; + + +namespace{ + +// How many Potions to buy. A Potion is 300 and a new game starts with +// 3000; the money on screen cannot be read yet. Five heal the starter +// through Route 202's four trainers and leave half the money. +constexpr int POTIONS_TO_BUY = 5; + +// From below the Center's door to below the Poke Mart's, ten tiles +// east along the same row, and in. The last leg is open and movement +// is not locked to tiles, so we may stand part of a tile off the door: +// whole-tile steps either side missed it in r121a. The Mart's doorway +// is lit like the Center's, so it is lined up with by sight the same +// way. Two townspeople wander the dirt south of the Mart and stop a +// leg short without the walk knowing (r122a counted to the door's +// row from three rows south of it): where the doorway shows on screen +// says so, and the walk is made once more from there. +void enter_sandgem_poke_mart( + VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, + std::vector& trainers, const RouteTrainerFight& fight +) { + const TilePos door = maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "poke_mart_door")); + const TilePos front = door.next(Pokemon::Direction::SOUTH); + const auto at_front = [&](const TilePos& pos) { return pos.y == front.y && std::abs(pos.x - front.x) <= 1; }; + walk_route(stream, context, run_from_wild_battle, maps, story_door_front(MAP_SANDGEM_TOWN, map_point("SandgemTown", "pokemon_center_door")), + at_front, trainers, fight, "To the Poke Mart"); + const std::vector short_of_door = tiles_short_of_door(stream, context, maps, door, "To the Poke Mart"); + if (!short_of_door.empty()) { + route_status(stream, "To the Poke Mart: stopped short of the door; walking again."); + walk_route(stream, context, run_from_wild_battle, maps, short_of_door, at_front, trainers, fight, "To the Poke Mart again"); + } + enter_lit_door(stream, context, "Sandgem Poke Mart: through the door"); +} + +// Inside the Mart, the clerk's counter is straight ahead of the door +// (every Mart is laid out so; Oreburgh's and Eterna's are walked the +// same way). Walk up to it, buy the Potions, and walk back out. +// buy_items() opens the talk and finds the Potion row by OCR of its +// name and price; the quantity is counted out blind. +void buy_potions_at_the_counter(VideoStream& stream, ProControllerContext& context) { + route_status(stream, "Poke Mart: to the clerk."); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "Poke Mart: to the clerk"); + if (walk.stop != WalkStop::BLOCKED && walk.stop != WalkStop::DIALOG) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poke Mart: the counter was not reached (" + std::string(walk_stop_name(walk.stop)) + ").", stream); + } + route_status(stream, "Poke Mart: buying " + std::to_string(POTIONS_TO_BUY) + " Potions."); + if (!buy_items(stream, context, {{ShopItem::Potion, POTIONS_TO_BUY}})) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poke Mart: could not buy " + std::to_string(POTIONS_TO_BUY) + " Potions.", stream); + } + leave_door_south(stream, context, run_from_wild_battle, "Poke Mart: out the door"); +} + +// Potions let us heal where we stand. Sandgem's Poke Mart is next +// door east of its Center. The purchase is checked in the Bag after, +// since buy_items() counts the quantity out without reading it. +// Returns where we stand after: below the Mart's door. +std::vector buy_potions_in_sandgem( + VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, + std::vector& trainers, const RouteTrainerFight& fight +) { + enter_sandgem_poke_mart(stream, context, maps, trainers, fight); + buy_potions_at_the_counter(stream, context); + if (!bag_has_item(stream, context, "potion", 1)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Poke Mart: the purchase went through, but no Potion is in the Bag.", stream); + } + return story_door_front(MAP_SANDGEM_TOWN, map_point("SandgemTown", "poke_mart_door")); +} + +// A walk_until() callback that keeps every location banner it sees. +std::function banner_keeper(std::vector& banners) { + return [&banners](const ImageViewRGB32& screen) { + const std::string read = read_location_banner(screen); + if (!read.empty() && (banners.empty() || banners.back() != read)) { + banners.push_back(read); + } + return false; + }; +} + +bool any_banner_names(const std::vector& banners, const char* map) { + return std::any_of(banners.begin(), banners.end(), + [&](const std::string& read) { return location_banner_matches(read, map); }); +} + +// Whether all of `tiles` are on `map`. +bool all_on_map(const StoryWorld& maps, const std::vector& tiles, const char* map) { + return !tiles.empty() && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos) { + const std::optional tile = maps.to_local(pos); + return tile && tile->map == map; + }); +} + +// North from below the gap between the signs until a wall stops us, +// nudging a tile west, twice at most, when blocked at once (below a +// sign or the Mart). Returns whether Route 202's banner showed: only +// one of the gap's columns leads out of town before a wall. +bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context) { + using Pokemon::Direction; + std::vector banners; + for (size_t nudges = 0;; nudges++){ + const WalkResult north = walk_until(stream, context, run_from_wild_battle, 0, 1, 8000ms, "North through the gap", banner_keeper(banners)); + if (north.stop == WalkStop::TRANSITION || north.stop == WalkStop::DIALOG || north.wild_battles > 0){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "North through the gap: " + std::string(walk_stop_name(north.stop)) + " instead of a wall.", stream); + } + // Blocked at once: the wall reads after about 800ms still. + if (north.walked > ROUTE_START_DELAY + 800ms + 2 * ROUTE_MS_PER_TILE_UP_DOWN){ + break; + } + if (nudges == 2){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "North through the gap: still blocked after two tiles west.", stream); + } + route_status(stream, "North through the gap: blocked at once, below a sign or the Mart; a tile west."); + route_turn(stream, context, Direction::WEST); + walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 1), "A tile west"); + route_turn(stream, context, Direction::NORTH); + } + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + banner_keeper(banners)(*screen.frame); + } + return any_banner_names(banners, MAP_ROUTE_202); +} + +// From the trees at the east end of the doors' row, west into the gap +// between the Center's sign and the Mart's (below_sign_gap()) and +// north through it until a wall stops us. Six tiles west from column +// 29 ends on 23, and on 20 to 26 if two tiles off: north from all of +// those passes no door, and only the gap lets us through. A walk +// north that goes nowhere stood below a sign or the Mart: a tile +// west, twice at most, never reaches the Center's door at 17. The +// planner would rather go round the south of town on walls, where +// townspeople wander. Returns where the walk north may have ended: +// the gap's columns end on Sandgem's top row, bar one that runs on to +// Route 202, and its banner tells the two apart. +std::vector north_through_sign_gap(VideoStream& stream, ProControllerContext& context, const StoryWorld& maps){ + using Pokemon::Direction; + route_status(stream, "To the gap between the Center's and the Mart's signs: west 6."); + route_turn(stream, context, Direction::WEST); + walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); + const bool on_route = walk_north_through_gap(stream, context); + std::vector ends; + for (const TilePos& pos : map_region("SandgemTown", "below_sign_gap")){ + const TilePos end = walk_to_wall(maps, maps.to_world(MAP_SANDGEM_TOWN, pos), Direction::NORTH).stop; + if (all_on_map(maps, {end}, MAP_ROUTE_202) == on_route){ + ends.push_back(end); + } + } + route_status(stream, std::string("North through the gap: Route 202's banner ") + + (on_route ? "showed; on Route 202." : "did not show; on Sandgem Town's top row.")); + return ends; +} + +// Back up to Route 202's save spot under the ledge: it is where a +// retry may start (story_save_spots()), its walls pin it, and the +// trainers are north of it. First east to the trees that end the +// doors' row, so the way north starts from one known tile, then north +// through the gap between the signs: r123a, planned round the south +// of town on a map that had it wrong, walked into a house. +void walk_to_route_202_save_spot( + VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, + std::vector at, std::vector& trainers, const RouteTrainerFight& fight +) { + const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end")); + const TilePos save_spot = maps.to_world(MAP_ROUTE_202, map_point("Route202", "save_spot")); + if (!all_on_map(maps, at, MAP_ROUTE_202)) { + at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), + [&](const TilePos& pos) { return pos == row_end; }, trainers, fight, "To the trees east of the Poke Mart"); + at = north_through_sign_gap(stream, context, maps); + } + walk_route(stream, context, run_from_wild_battle, maps, std::move(at), + [&](const TilePos& pos) { return pos == save_spot; }, trainers, fight, "Back to Route 202"); +} + +// North into Jubilife until Dawn's scene starts (00.04d's +// into_jubilife). Jubilife's south edge is a wall with stairs in it, +// west of where the route may leave us (r78 stood at its east end). +// Entering starts a scene: Dawn walks over while we stand still. So +// walk north; if the wall stops us and no dialogue follows, the stairs +// are further west. The scene itself is the next step. +void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { + for (size_t attempt = 0; attempt < 6; attempt++) { + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 4000ms, "to Dawn in Jubilife"); + if (walk.stop == WalkStop::TRANSITION) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: left the area instead of meeting Dawn.", stream); + } + ShortDialogWatcher dialog(COLOR_CYAN); + if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0) { + return; + } + stream.log("[00.04] Jubilife: the wall stopped us; looking for the stairs one tile west."); + route_turn(stream, context, Pokemon::Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: no stairs found and no Dawn after six tiles west.", stream); +} + +// Reads a sign facing `facing`: its box must open and close, and its text, +// when there is a reader for it, have `key`. A walk that stopped short of +// it is put right by walking on until the sign stops us, once. +void read_sign_on_the_way(VideoStream& stream, ProControllerContext& context, Direction facing, const std::string& sign, const std::string& key){ + for (size_t attempt = 0; attempt < 2; attempt++){ + const DialogueRead read = read_sign(stream, context, facing, sign); + if (read.opened && read.closed && (read.text.empty() || sign_text_matches(read.text, key))){ + return; + } + walk_until_stopped(stream, context, facing, 2, "On to the sign"); + } + fail_step(stream, "Sandgem: " + sign + " did not read facing " + Pokemon::direction_name(facing) + "."); +} + +// From the doors' row in front of Sandgem's Pokemon Center to the Poke Mart +// lady and back to Route 202's save spot. Every leg but three one-tile +// steps walks until something stops it, as measured on the console +// (2026-10-30, CFR "walk"): west to the Lab's sign; south a tile, west to +// the grass's edge; north to the trees, on her row; east until she stops +// us (she gives new games 10 Potions); south a tile, east to Route 201's +// sign; south a tile, east to the trees east of the Mart, north a row; then +// the planned walk to the save spot. She stands on the top row of her +// grass, a tile or two in from its west edge: missed, back west to the +// edge and again, twice. +void mart_lady(VideoStream& stream, ProControllerContext& context){ + walk_until_stopped(stream, context, Direction::WEST, 20, "West to the Lab's sign"); + read_sign_on_the_way(stream, context, Direction::WEST, "the Lab's sign", "lab"); + hold_for(stream, context, Direction::SOUTH, 200ms, "South onto the path"); + walk_until_stopped(stream, context, Direction::WEST, 28, "West along the path"); + walk_until_stopped(stream, context, Direction::NORTH, 3, "North to the trees"); + bool talked = false; + for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ + if (attempt > 0){ + walk_until_stopped(stream, context, Direction::WEST, 6, "West to the grass's edge"); + } + const DialogueRead read = talk_walking(stream, context, Direction::EAST, 4, "Poke Mart lady"); + talked = read.opened && read.closed; + } + if (!talked){ + fail_step(stream, "Route 201: the Poke Mart lady did not talk."); + } + hold_for(stream, context, Direction::SOUTH, 200ms, "South onto the signs' row"); + walk_until_stopped(stream, context, Direction::EAST, 16, "East to the Route 201 sign"); + read_sign_on_the_way(stream, context, Direction::EAST, "Route 201's sign", "201"); + hold_for(stream, context, Direction::SOUTH, 200ms, "South onto the path"); + walk_until_stopped(stream, context, Direction::EAST, 40, "East along the path"); + walk_until_stopped(stream, context, Direction::NORTH, 2, "North to the front row"); + const StoryWorld& maps = story_maps(); + std::vector trainers = story_trainer_sights(); + const RouteTrainerFight fight = [&](const std::string& name){ fight_route_trainer(stream, context, name); }; + walk_to_route_202_save_spot(stream, context, maps, {maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end"))}, trainers, fight); +} + +// 00.04c's potions step, from the save out of Sandgem's Center: the Poke +// Mart lady's, or the clerk's after two misses this run, on to Route 202's +// save spot, where 00.04d's save goes. The Bag must have Potions after. +// The clerk's list is found by reading item names, which needs a reader +// for the game's language; without one, the lady every time. +Pokemon::Outcome sandgem_potions(StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep&){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + size_t& misses = executor.run_count("mart_lady_misses"); + if (misses < 2 || !story_reads_text()){ + // Onto the doors' row first: out of the door we stand a tile to + // three below it (r261 walked west into the trees from three). + route_turn(stream, context, Direction::EAST); + walk_tiles_dpad(stream, context, run_from_wild_battle, 1, 0, 1, "east under the Center's wall"); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); + if (up.stop != WalkStop::BLOCKED){ + fail_step(stream, std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + "."); + } + try{ + mart_lady(stream, context); + }catch (OperationFailedException&){ + misses++; + throw; + } + }else{ + const StoryWorld& maps = story_maps(); + std::vector trainers = story_trainer_sights(); + const RouteTrainerFight fight = [&](const std::string& name){ fight_route_trainer(stream, context, name); }; + const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); + walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); + } + if (!bag_has_item(stream, context, "potion", 1)){ + fail_step(stream, "00.04: no Potions in the Bag after the shop."); + } + return Pokemon::outcome_ok(); +} + +} + + +void add_sandgem_events(StoryEvents& events){ + events.emplace("sandgem_potions", sandgem_potions); + events.emplace("jubilife_stairs", console_event(up_jubilife_stairs)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp deleted file mode 100644 index 561b2cef1a..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* BDSP AutoStory Indoors Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "Pokemon/Navigation/Pokemon_MapSet.h" -#include "Pokemon/Navigation/Pokemon_TileGrid.h" -#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" -#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h" -#include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" -#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP_AutoStory_Indoors_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -using Pokemon::Direction; -using Pokemon::MapSet; -using Pokemon::TilePos; - -// 00.04c's Potions: the Mart lady's walk (after_catch_potions() and the Mart lady checkpoint) -// is walks until stopped, so on the grid each leg ends at its wall: from -// a tile to three below the Center's door, a tap east and north to the -// Center's front, west to the Lab's sign, a row south and west to -// her grass's edge, north to the trees on her row (she stands east of -// there), back a row south and east to Route 201's sign, a row south and -// east to the trees east of the Mart, and north to the front row's end. -struct LadyLeg{ - Direction direction; - size_t tiles; // 0: walk until the wall - TilePos end; // world tile - std::string name; -}; - -class Test_MartLadyWalk : public UnitTest{ -public: - Test_MartLadyWalk() - : UnitTest("PokemonBDSP::AutoStory - 00.04: the walk to the Poke Mart lady and back") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const MapSet& maps = story_maps(); - const auto sandgem = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; - const auto route201 = [&](const TilePos& pos){ return maps.to_world(MAP_ROUTE_201, pos); }; - const std::vector legs{ - {Direction::EAST, 1, sandgem({18, 0}), "a tap east under the Center's wall"}, - {Direction::NORTH, 0, sandgem({18, 11}), "north to the Center's front"}, - {Direction::WEST, 0, east_of_lab_sign(), "west to the Lab's sign"}, - {Direction::SOUTH, 1, sandgem({6, 12}), "a row south onto the path"}, - {Direction::WEST, 0, route201({42, 5}), "west to her grass's edge"}, - {Direction::NORTH, 0, route201({42, 3}), "north to the trees, on her row"}, - {Direction::SOUTH, 1, route201({42, 4}), "a row south onto the signs' row"}, - {Direction::EAST, 0, route201_sign_west(), "east to Route 201's sign"}, - {Direction::SOUTH, 1, route201({54, 5}), "a row south onto the path"}, - {Direction::EAST, 0, sandgem({29, 12}), "east to the trees east of the Mart"}, - {Direction::NORTH, 0, sandgem(SandgemTown::FRONT_ROW_EAST_END), "north to the front row's end"}, - }; - // Out of the Center's door we stand a tile to three below it (r261). - for (int below = 1; below <= 3; below++){ - const std::string error = follow(maps, legs, sandgem({17, 10 + below}), logger); - if (!error.empty()){ - return "From " + std::to_string(below) + " below the door: " + error; - } - } - return true; - } - -private: - static std::string follow(const MapSet& maps, const std::vector& legs, TilePos at, Logger& logger){ - for (const LadyLeg& leg : legs){ - if (leg.tiles == 0){ - at = Pokemon::walk_to_wall(maps.world(), at, leg.direction).stop; - }else{ - const std::optional next = maps.world().step(at, leg.direction); - if (!next){ - return leg.name + ": blocked at " + maps.describe(at) + "."; - } - at = *next; - } - // The tap east lands on whichever row we stood on. - if (&leg != &legs.front() && !(at == leg.end)){ - return leg.name + ": ends at " + maps.describe(at) + ", not " + maps.describe(leg.end) + "."; - } - logger.log(leg.name + ": " + maps.describe(at)); - } - return ""; - } -}; - -} - - -void add_tests_AutoStory_Indoors(UnitTestDatabase& database){ - database.add(); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h deleted file mode 100644 index 11f7c9f0ed..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h +++ /dev/null @@ -1,26 +0,0 @@ -/* BDSP AutoStory Indoors Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Indoors_Tests_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Indoors_Tests_H - -#include "Common/Cpp/TestRunners/UnitTest.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - - -// 00.04c's walk to the Poke Mart lady and back, leg by leg on the grid. -void add_tests_AutoStory_Indoors(UnitTestDatabase& database); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp new file mode 100644 index 0000000000..93e3504a7f --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp @@ -0,0 +1,303 @@ +/* BDSP AutoStory - Poke Mart + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#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/Inference/PokemonBDSP_SelectionArrow.h" +#include "PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h" +#include "PokemonBDSP_AutoStory_Mart.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +const char* shop_item_name(ShopItem item){ + switch (item){ + case ShopItem::FullRestore: + return "Full restore"; + case ShopItem::MaxPotion: + return "Max potion"; + case ShopItem::HyperPotion: + return "Hyper potion"; + case ShopItem::SuperPotion: + return "Super potion"; + case ShopItem::Potion: + return "Potion"; + case ShopItem::Antidote: + return "Antidote"; + case ShopItem::ParalyzHeal: + return "Paralyz heal"; + case ShopItem::Awakening: + return "Awakening"; + case ShopItem::Repel: + return "Repel"; + case ShopItem::SuperRepel: + return "Super repel"; + case ShopItem::MaxRepel: + return "Max repel"; + case ShopItem::EscapeRope: + return "Escape rope"; + default: + return "Unknown"; + } +} + +// Returns the OCR slug used as the dictionary key in ShopItemNameOCR.json. +const char* shop_item_slug(ShopItem item){ + switch (item){ + case ShopItem::FullRestore: + return "full-restore"; + case ShopItem::MaxPotion: + return "max-potion"; + case ShopItem::HyperPotion: + return "hyper-potion"; + case ShopItem::SuperPotion: + return "super-potion"; + case ShopItem::Potion: + return "potion"; + case ShopItem::Antidote: + return "antidote"; + case ShopItem::ParalyzHeal: + return "paralyze-heal"; + case ShopItem::Awakening: + return "awakening"; + case ShopItem::Repel: + return "repel"; + case ShopItem::SuperRepel: + return "super-repel"; + case ShopItem::MaxRepel: + return "max-repel"; + case ShopItem::EscapeRope: + return "escape-rope"; + default: + return "unknown"; + } +} + +// {MaxRepel, 2, 4}, {FullRestore, 0, 3} → "4 Max repel, 3 Full restore" +std::string make_buy_label(const std::vector& items){ + std::string label; + for (size_t i = 0; i < items.size(); i++){ + if (i > 0){ + label += ", "; + } + if (items[i].quantity == BUY_MAX){ + label += "max "; + }else{ + label += std::to_string(items[i].quantity) + " "; + } + label += shop_item_name(items[i].item); + } + return label; +} + +} + +bool buy_items(VideoStream& stream, + ProControllerContext& context, + const std::vector& items, + Language language){ + if (items.empty()){ + return true; + } + + const std::string label = make_buy_label(items); + context.wait_for_all_requests(); + pbf_wait(context, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + context.wait_for_all_requests(); + + + // ── Coordinate constants ───────────────────────────────────────────────── + + // Area where the selection arrow appears at the vendor greeting menu (Buy/Sell/Leave). + static const ImageFloatBox VENDOR_ARROW_BOX{0.630000, 0.550000, 0.070000, 0.120000}; + + // Area where the selection arrow appears at the YES/NO confirmation dialog. + static const ImageFloatBox CONFIRM_ARROW_BOX{0.700000, 0.620000, 0.070000, 0.120000}; + + // Full area containing all item rows in the shop list. + // The hover detector scans this region to dynamically locate the red outline. + static const ImageFloatBox ITEM_SCAN_AREA{0.417000, 0.100000, 0.540000, 0.625000}; + + static constexpr size_t MAX_OUTER = 50; + static constexpr size_t MAX_ITEM_SEARCH = 50; // max iterations per item before giving up + + // ── Outer state machine ────────────────────────────────────────────────── + for (size_t outer = 0; outer < MAX_OUTER; outer++){ + SelectionArrowFinder arrow_vendor(stream.overlay(), VENDOR_ARROW_BOX, COLOR_GREEN); + ShopBuyingWindowWatcher buying_window; + ShortDialogWatcher dialog(COLOR_RED); + + // case 0: no detectors fired — press A in a for-loop (5 presses) while watching. + stream.log("Starting to loop over items"); + int state = run_until(stream, + context, + [](ProControllerContext& ctx){ + for (int i = 0; i < 5; i++){ + pbf_press_button(ctx, BUTTON_A, 80ms, 1000ms); + } + }, + {{arrow_vendor}, {buying_window}, {dialog}}); + + context.wait_for_all_requests(); + pbf_wait(context, 500ms); + + switch (state){ + + case 0: // case 1: SelectionArrow at vendor menu → press A to choose "Buy" + stream.log("Found the selection arrow, pressing A to enter the buying window...", COLOR_GREEN); + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + break; + + case 1: // case 2: buying window open → buy each requested item + stream.log("Found the buying window, searching for the right item...", COLOR_GREEN); + { + bool buying_failed = false; + + for (const ItemToBuy& item : items){ + stream.log("Buying " + std::to_string(item.quantity) + "x " + shop_item_slug(item.item)); + if (buying_failed){ + break; + } + + // Detector fires only when the red outline is found AND both + // name and price OCR match the expected item slug. + ShopItemHoverDetector hover(stream.logger(), + ITEM_SCAN_AREA, + shop_item_slug(item.item), + language, + COLOR_CYAN); + ShopQuantityWindowWatcher qty_window; + SelectionArrowFinder confirm_arrow(stream.overlay(), CONFIRM_ARROW_BOX, COLOR_GREEN); + + bool item_bought = false; + bool hover_confirmed = false; + for (size_t inner = 0; inner < MAX_ITEM_SEARCH && !item_bought; inner++){ + context.wait_for_all_requests(); + + int sub = run_until(stream, + context, + [](ProControllerContext& ctx){ + ctx.wait_for(1000ms); + }, + {{confirm_arrow}, {qty_window}, {hover}}); + + context.wait_for_all_requests(); + + stream.log("buy_items: sub=" + std::to_string(sub), COLOR_WHITE); + + switch (sub){ + + case 0: // YES/NO confirmation arrow → confirm purchase + stream.log("Found the confirmation to buy arrow, pressing A to buy", COLOR_GREEN); + { + context.wait_for_all_requests(); + ShopBuyingWindowWatcher back_in_shop; + run_until(stream, + context, + [](ProControllerContext& ctx){ + for (int i = 0; i < 10; i++){ + pbf_press_button(ctx, BUTTON_A, 80ms, 800ms); + } + }, + {{back_in_shop}}); + item_bought = true; + break; + } + + case 1: // quantity window + if (!hover_confirmed){ + // Quantity window appeared before we confirmed the right item — + // an earlier A press accidentally opened the wrong item's selector. + // Press B to go back to the list and continue searching. + stream.log("Unexpected quantity window (wrong item), pressing B...", COLOR_YELLOW); + pbf_press_button(context, BUTTON_B, 80ms, 800ms); + break; + } + stream.log("Found the quantity window", COLOR_GREEN); + if (item.quantity == BUY_MAX){ + stream.log("Buying max quantity"); + pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); + }else{ + stream.log("Buying " + std::to_string(item.quantity) + " of the item"); + for (int q = 1; q < item.quantity; q++){ + pbf_press_dpad(context, DPAD_UP, 80ms, 300ms); + } + } + pbf_press_button(context, BUTTON_A, 80ms, 300ms); + pbf_wait(context, 1000ms); + break; + + case 2: // correct item highlighted → press A to open quantity selector + stream.log("Found the right item, pressing A to buy a certain quantity...", COLOR_GREEN); + hover_confirmed = true; + pbf_press_button(context, BUTTON_A, 80ms, 300ms); + pbf_wait(context, 1000ms); + break; + + default: // outline not found or wrong item → advance to next row + stream.log("Not on the right item, moving...", COLOR_GREEN); + pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); + pbf_wait(context, 300ms); + break; + } + } + + if (!item_bought){ + stream.log(label + ": buy_items: could not locate " + shop_item_name(item.item) + + " in shop list.", + COLOR_RED); + buying_failed = true; + } + } + + if (!buying_failed){ + stream.log("All items bought, exiting shop...", COLOR_GREEN); + pbf_mash_button(context, BUTTON_B, 5000ms); + return true; + } + break; + } + + case 2: // case 3: unexpected ShortDialog → press B until back in buying window + { + ShopBuyingWindowWatcher back_in_shop; + run_until(stream, + context, + [](ProControllerContext& ctx){ + pbf_mash_button(ctx, BUTTON_B, 5000ms); + }, + {{back_in_shop}}); + break; + } + + default: // outer default: unrecognised state → press B to recover + pbf_press_button(context, BUTTON_B, 80ms, 3000ms); + break; + } + } + + stream.log(label + ": buy_items: failed to complete after max iterations.", COLOR_RED); + return false; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h new file mode 100644 index 0000000000..4eead40649 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h @@ -0,0 +1,61 @@ +/* BDSP AutoStory - Poke Mart + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Mart_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Mart_H + +#include +#include "CommonFramework/Language.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class ShopItem{ + FullRestore, + MaxPotion, + HyperPotion, + SuperPotion, + Potion, + Antidote, + ParalyzHeal, + Awakening, + Repel, + SuperRepel, + MaxRepel, + EscapeRope, +}; + +// Pass as the `quantity` field to buy the maximum available amount. +inline constexpr int BUY_MAX = -1; + +struct ItemToBuy{ + ShopItem item = ShopItem::Potion; + int quantity = 0; // positive count to buy, or BUY_MAX +}; + +// Walk up to a vendor (already talking to them), navigate through the Buy menu, +// and purchase each item in `items` in order. The function presses DPAD_DOWN +// until the matching item name is found via OCR, so items may appear in any +// list order. Returns true on success. +bool buy_items( + VideoStream& stream, + ProControllerContext& context, + const std::vector& items, + Language language = Language::English +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.cpp similarity index 100% rename from SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp rename to SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.cpp diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.h similarity index 100% rename from SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h rename to SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.h diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.cpp new file mode 100644 index 0000000000..ac9a427ccd --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.cpp @@ -0,0 +1,444 @@ +/* BDSP AutoStory - Settings + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP_AutoStory_Bag.h" +#include "PokemonBDSP_AutoStory_Settings.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +// Options chrome and position detectors are independent of setting values. +class TutorialOptionsDetector { +public: + bool detect(const ImageViewRGB32& screen) const { + ImageViewRGB32 panel = extract_box_reference(screen, ImageFloatBox{0.04, 0.065, 0.93, 0.70}); + size_t white = 0; + for (size_t y = 0; y < panel.height(); y += 4) + for (size_t x = 0; x < panel.width(); x += 4) { + uint32_t p = panel.pixel(x, y); + if (((p >> 16) & 255) > 225 && ((p >> 8) & 255) > 225 && (p & 255) > 225) white++; + } + double samples = (double)(((panel.width() + 3) / 4) * ((panel.height() + 3) / 4)); + // Distinguish Options from the icon menu and bag using its + // blue heading and full-width white settings panel. + ImageViewRGB32 heading = extract_box_reference(screen, ImageFloatBox{0.01, 0.005, 0.20, 0.025}); + size_t blue = 0, total = 0; + for (size_t y = 0; y < heading.height(); y++) + for (size_t x = 0; x < heading.width(); x++) { + uint32_t p = heading.pixel(x, y); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + blue += b > r + 20 && g > r + 10; + total++; + } + return samples > 0 && white / samples > 0.70 && total > 0 && blue > total / 4; + } +}; + +// Options fades in after A on its icon: poll instead of judging one frame. +bool wait_for_options_screen(VideoStream& stream, ProControllerContext& context, + const TutorialOptionsDetector& options_screen, Milliseconds timeout) { + context.wait_for_all_requests(); + const WallClock deadline = current_time() + timeout; + while (current_time() < deadline) { + auto frame = stream.video().snapshot(); + if (frame && options_screen.detect(frame)) return true; + context.wait_for(100ms); + } + return false; +} + +class TutorialOptionPositionDetector { +public: + // Locate the two long red horizontal lines surrounding the + // selected row. Scrolling changes their position automatically. + double selected_row(const ImageViewRGB32& screen) const { + int first = -1, last = -1; + for (size_t y = (size_t)(screen.height() * 0.08); y < screen.height() * 0.75; y++) { + size_t red = 0, total = 0; + for (size_t x = (size_t)(screen.width() * 0.08); x < screen.width() * 0.90; x += 2) { + uint32_t p = screen.pixel(x, y); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + red += r > 150 && r > g + 40 && r > b + 40; + total++; + } + if (total && red > total * 0.70) { + if (first < 0) first = (int)y; + last = (int)y; + } + } + if (first < 0 || last - first < screen.height() * 0.04 || last - first > screen.height() * 0.12) return -1; + return (first + last) / (2.0 * screen.height()); + } + // Locate selected red value text separately from the row border. + bool selected_value(const ImageViewRGB32& screen, double y) const { + ImageViewRGB32 value = extract_box_reference(screen, ImageFloatBox{0.33, y - 0.025, 0.54, 0.05}); + size_t red = 0; + for (size_t py = 0; py < value.height(); py++) + for (size_t px = 0; px < value.width(); px++) { + uint32_t p = value.pixel(px, py); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + red += r > g + 20 && r > b + 20; + } + return red > value.width() * value.height() * 0.0005; + } +}; + + +// Compare binary visual shapes with small alignment tolerance. +// No text recognition is needed for fixed English option labels. +double option_image_score(const ImageViewRGB32& screen, + const ImageFloatBox& box, const ImageRGB32& reference, bool red_only) { + ImageRGB32 actual = extract_box_reference(screen, box).scale_to(384, 32); + ImageRGB32 expected = reference.scale_to(384, 32); + auto ink = [red_only](uint32_t p){ + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + return red_only ? r > g + 20 && r > b + 20 : r + g + b < 500; + }; + double best = 0; + for (int dy = -2; dy <= 2; dy++) for (int dx = -2; dx <= 2; dx++) { + size_t overlap = 0, count = 0; + for (int y = 2; y < 30; y++) for (int x = 2; x < 382; x++) { + bool a = ink(actual.pixel(x + dx, y + dy)); + bool b = ink(expected.pixel(x, y)); + overlap += a && b; + count += a + b; + } + if (count) best = std::max(best, 2.0 * overlap / count); + } + return best; +} + +bool autosave_off_highlight(const ImageViewRGB32& screen, double y) { + auto red_count = [&](const ImageFloatBox& box){ + ImageViewRGB32 image = extract_box_reference(screen, box); + size_t red = 0; + for (size_t py = 0; py < image.height(); py++) + for (size_t px = 0; px < image.width(); px++) { + uint32_t p = image.pixel(px, py); + int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; + red += r > g + 30 && r > b + 30; + } + return red; + }; + size_t off = red_count({0.63, y - 0.022, 0.055, 0.044}); + size_t on = red_count({0.34, y - 0.022, 0.055, 0.044}); + // Require a clear Off highlight and reject any competing On highlight. + return off >= 10 && on * 10 < off; +} + +void verify_tutorial_settings_once(VideoStream& stream, ProControllerContext& context) { + context.wait_for_all_requests(); + MenuDetector menu; + VideoOverlaySet detector_overlays(stream.overlay()); + menu.make_overlays(detector_overlays); + detector_overlays.add(COLOR_CYAN, {0.04, 0.065, 0.93, 0.70}, ""); + detector_overlays.add(COLOR_BLUE, {0.01, 0.005, 0.20, 0.025}, ""); + detector_overlays.add(COLOR_RED, {0.08, 0.08, 0.82, 0.67}, ""); + auto initial = stream.video().snapshot(); + if (!initial || !menu.detect(initial)) { + pbf_press_button(context, BUTTON_X, 80ms, 1000ms); + context.wait_for_all_requests(); + } + MenuWatcher menu_open; + if (wait_until(stream, context, 5s, {{menu_open}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Main menu not detected.", stream); + } + stream.log("[Tutorial settings] Main menu detected; settling for 500ms before Options navigation."); + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + TutorialOptionsDetector options_screen; + TutorialOptionPositionDetector position; + // Only two icons are available at this initial checkpoint. + // Verify the main menu before navigating to the neighboring icon. + bool options_open = false; + for (size_t attempt = 0; attempt < 2; attempt++) { + pbf_press_dpad(context, attempt == 0 ? DPAD_RIGHT : DPAD_LEFT, 80ms, 350ms); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + if (wait_for_options_screen(stream, context, options_screen, 5s)) { + options_open = true; + break; + } + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + MenuWatcher back_to_menu; + if (wait_until(stream, context, 5s, {{back_to_menu}}) != 0) break; + } + if (!options_open) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Options screen not detected.", stream); + } + stream.log("[Tutorial settings] Options screen detected."); + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + struct Setting { const char* label; const char* value; }; + const Setting settings[] = { + {"textspeed", "fast"}, {"changewindow", "type1"}, + {"battleeffects", "off"}, {"battlestyle", "set"}, + {"sendtoboxes", "automatic"}, {"givenicknames", "dontgive"}, + {"gyroscope", "use"}, {"autosave", "off"}, + {"shownicknames", "show"}, {"backgroundmusic", "8"}, + {"soundeffects", "8"}, + }; + for (size_t row = 0; row < sizeof(settings) / sizeof(settings[0]); row++) { + // Detect the selected row afresh after every navigation/correction. + double y = -1; + const std::string base = RESOURCE_PATH() + "PokemonBDSP/Autostory/Options/" + settings[row].label; + const ImageRGB32 label_reference(base + "_label.png"); + const ImageRGB32 value_reference(base + "_value.png"); + bool slider_reset = false; + bool correct = false; + for (size_t attempt = 0; attempt < 30; attempt++) { + context.wait_for_all_requests(); + auto frame = stream.video().snapshot(); + if (frame && options_screen.detect(frame)) y = position.selected_row(frame); + else y = -1; + const bool value_visible = y >= 0 && position.selected_value(frame, y); + const double label_score = y >= 0 ? option_image_score(frame, + {0.08, y - 0.025, 0.24, 0.05}, label_reference, false) : 0; + stream.log("[Tutorial settings] Expected row=" + std::string(settings[row].label) + + " row_y=" + std::to_string(y) + " highlighted_value=" + std::to_string(value_visible) + + " label_match=" + std::to_string(label_score)); + if (y < 0 || !value_visible || label_score < 0.80) { + // A menu can be detected while its row/text is still + // fading in. Re-read without directional inputs first. + if (attempt < 4) { + pbf_wait(context, 200ms); + context.wait_for_all_requests(); + continue; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Unable to confirm expected row " + std::string(settings[row].label), stream); + } + // These are regular video-overlay boxes, so the existing + // overlay display control can show/hide every active ROI. + OverlayBoxScope row_overlay(stream.overlay(), COLOR_RED, + {0.07, y - 0.04, 0.84, 0.08}, ""); + OverlayBoxScope label_overlay(stream.overlay(), COLOR_YELLOW, + {0.08, y - 0.025, 0.24, 0.05}, ""); + OverlayBoxScope value_overlay(stream.overlay(), COLOR_GREEN, + {0.33, y - 0.025, 0.54, 0.05}, ""); + VideoOverlaySet autosave_rois(stream.overlay()); + if (row == 7) { + autosave_rois.add(COLOR_GREEN, {0.63, y - 0.022, 0.055, 0.044}); + autosave_rois.add(COLOR_RED, {0.34, y - 0.022, 0.055, 0.044}); + } + double score; + if (row >= 9) { + // Compare the numeric value rather than the long slider: + // neighboring slider levels otherwise look too similar. + ImageViewRGB32 number = value_reference.sub_image( + value_reference.width() * 82 / 100, 0, + value_reference.width() - value_reference.width() * 82 / 100, + value_reference.height()); + ImageRGB32 number_reference = number.scale_to(96, 32); + score = option_image_score(frame, + {0.7728, y - 0.025, 0.0972, 0.05}, number_reference, true); + } else { + score = option_image_score(frame, + {0.33, y - 0.025, 0.54, 0.05}, value_reference, true); + } + stream.log("[Tutorial settings] " + std::string(settings[row].label) + + " target match=" + std::to_string(score)); + // The reference recording uses inactive pale-red text; + // the focused live row has a thicker dark-red glyph mask. + // Verified Fast scores 0.734 while Slow/Normal score 0. + if (score >= 0.65 && (row != 7 || autosave_off_highlight(frame, y))) { + if (row == 7) { + for (size_t confirmation = 0; confirmation < 3; confirmation++) { + pbf_wait(context, 200ms); + context.wait_for_all_requests(); + auto check = stream.video().snapshot(); + double check_y = check ? position.selected_row(check) : -1; + if (!check || !options_screen.detect(check) || check_y < 0 || + option_image_score(check, {0.08, check_y - 0.025, 0.24, 0.05}, label_reference, false) < 0.80 || + !autosave_off_highlight(check, check_y)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave Off could not be confirmed. Stopping before movement.", stream); + } + } + } + stream.log("[Tutorial settings] Verified " + std::string(settings[row].label) + "=" + settings[row].value); + correct = true; + break; + } + stream.log("[Tutorial settings] Correcting " + std::string(settings[row].label)); + if (row >= 9 && !slider_reset) { + // Sliders clamp at zero. Establish a known endpoint, + // then increment; verify the target image afterward. + for (size_t step = 0; step < 12; step++) + pbf_press_dpad(context, DPAD_LEFT, 80ms, 100ms); + slider_reset = true; + } else { + pbf_press_dpad(context, DPAD_RIGHT, 80ms, 350ms); + } + context.wait_for_all_requests(); + } + if (!correct) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Could not apply " + std::string(settings[row].label), stream); + } + if (row + 1 < sizeof(settings) / sizeof(settings[0])) + pbf_press_dpad(context, DPAD_DOWN, 80ms, 350ms); + } + // Confirm settings before exiting: A A A A B B. + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1200ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + OverworldWatcher overworld; + if (wait_until(stream, context, 10s, {{overworld}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Settings checkpoint: Did not return to the overworld after saving options.", stream); + } + // Reopen the saved Options screen and independently verify Autosave. + // No route movement is permitted until this persisted-value check passes. + pbf_press_button(context, BUTTON_X, 80ms, 1000ms); + context.wait_for_all_requests(); + MenuWatcher verify_menu; + if (wait_until(stream, context, 5s, {{verify_menu}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave verification: Main menu not detected.", stream); + } + bool reopened = false; + for (size_t attempt = 0; attempt < 2; attempt++) { + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + if (wait_for_options_screen(stream, context, options_screen, 5s)) { reopened = true; break; } + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + MenuWatcher return_menu; + if (wait_until(stream, context, 5s, {{return_menu}}) != 0) break; + pbf_press_dpad(context, DPAD_RIGHT, 80ms, 350ms); + } + if (!reopened) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave verification: Could not reopen saved Options.", stream); + } + // Options starts on Text Speed; Autosave is seven rows below it. + for (size_t step = 0; step < 7; step++) + pbf_press_dpad(context, DPAD_DOWN, 80ms, 350ms); + context.wait_for_all_requests(); + const ImageRGB32 autosave_label(RESOURCE_PATH() + "PokemonBDSP/Autostory/Options/autosave_label.png"); + for (size_t confirmation = 0; confirmation < 3; confirmation++) { + auto check = stream.video().snapshot(); + double check_y = check ? position.selected_row(check) : -1; + if (!check || !options_screen.detect(check) || check_y < 0 || + option_image_score(check, {0.08, check_y - 0.025, 0.24, 0.05}, autosave_label, false) < 0.80 || + !autosave_off_highlight(check, check_y)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Saved Autosave setting is not verified Off. Stopping before movement.", stream); + } + pbf_wait(context, 200ms); + context.wait_for_all_requests(); + } + stream.log("[Tutorial settings] Autosave = Off: confirmed after reopening saved Options (3 frames)."); + // Confirm settings before exiting: A A A A B B. + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + pbf_press_button(context, BUTTON_A, 80ms, 1200ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + OverworldWatcher verified_exit; + if (wait_until(stream, context, 10s, {{verified_exit}}) != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Autosave verification: Could not return to the overworld.", stream); + } + // Report inferred values explicitly: these are visual matches, + // allowing the user to compare our conclusions with the screen. + stream.log("[Tutorial settings] Completed. Values believed applied (visual matches before saving):"); + stream.log("[Tutorial settings] Text Speed = Fast; Change Window = Type 1; Battle Effects = Off; Battle Style = Set."); + stream.log("[Tutorial settings] Send to Boxes = Automatic; Give Nicknames = Don't give; Gyroscope = Use."); + stream.log("[Tutorial settings] Autosave = Off; Show Nicknames = Show; Background Music = 8; Sound Effects = 8."); +} + +} + +void verify_tutorial_settings(VideoStream& stream, ProControllerContext& context) { + // Settings-only recovery: initial attempt plus three retries. + // Never permit route movement unless the complete check succeeds. + constexpr size_t MAX_RETRIES = 3; + for (size_t attempt = 0; attempt <= MAX_RETRIES; attempt++) { + stream.log("[Tutorial settings] Attempt " + std::to_string(attempt + 1) + "/4."); + try { + verify_tutorial_settings_once(stream, context); + return; + } catch (OperationFailedException& error) { + stream.log("[Tutorial settings] Attempt failed: " + error.message(), COLOR_ORANGE); + if (attempt == MAX_RETRIES) { + stream.log("[Tutorial settings] All three retries exhausted; stopping before movement.", COLOR_RED); + throw; + } + // Back out of Options, save prompts, bag, or menu without + // moving the player or resetting/loading the game. + stream.log("[Tutorial settings] Returning to overworld before retry."); + context.wait_for_all_requests(); + TutorialOptionsDetector options_screen; + const auto failed_frame = stream.video().snapshot(); + if (failed_frame && options_screen.detect(failed_frame)) { + // Finish the known Options save/exit sequence before + // observing state; never cancel it on an Options frame + // misclassified as overworld by the generic detector. + for (size_t press = 0; press < 4; press++) + pbf_press_button(context, BUTTON_A, 80ms, 1000ms); + } + pbf_mash_button(context, BUTTON_B, 3000ms); + context.wait_for_all_requests(); + OverworldDetector overworld; + MenuDetector menu; + BlackScreenDetector black; + bool recovered = false; + size_t confirmations = 0; + for (size_t check = 0; check < 15; check++) { + const auto frame = stream.video().snapshot(); + const bool clear = frame && !options_screen.detect(frame) && + !menu.detect(frame) && !black.detect(frame) && bag_pocket_position(frame) == 0 && overworld.detect(frame); + if (clear) { + if (++confirmations >= 3) { recovered = true; break; } + pbf_wait(context, 200ms); + } else { + confirmations = 0; + pbf_mash_button(context, BUTTON_B, 500ms); + } + context.wait_for_all_requests(); + } + stream.log(recovered ? "[Tutorial settings] Recovery confirmed outside Options/menu; retrying." + : "[Tutorial settings] Recovery not confirmed; next attempt will recheck the screen."); + pbf_wait(context, 500ms); + context.wait_for_all_requests(); + } + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.h new file mode 100644 index 0000000000..2fa2d27641 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.h @@ -0,0 +1,31 @@ +/* BDSP AutoStory - Settings + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Settings_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Settings_H + +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// From the overworld: opens Options and sets what the story needs (fast +// text, Send to Boxes automatic, no nickname prompt, Autosave off), saves +// them and returns to the overworld. Tries four times, backing out to the +// overworld between tries, and fails with a screenshot before any movement. +void verify_tutorial_settings(VideoStream& stream, ProControllerContext& context); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp new file mode 100644 index 0000000000..6f132ad841 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -0,0 +1,342 @@ +/* BDSP AutoStory - Story executor + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Globals.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_TilePose.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Actions.h" +#include "PokemonBDSP_AutoStory_Center.h" +#include "PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP_AutoStory_NewGame.h" +#include "PokemonBDSP_AutoStory_Settings.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::RunState; +using Pokemon::StoryStep; +using Pokemon::TilePos; + + +std::optional story_route_costs(const std::string& variant){ + if (variant.empty()){ + return route_plan_costs(); + } + if (variant == "town"){ + return town_plan_costs(); + } + if (variant == "robust"){ + return RobustPlanCosts(); + } + return std::nullopt; +} + + +namespace{ + +// "to_catch_grass" as "to catch grass", for the logs. +std::string label_of(const StoryStep& step){ + std::string label = step.name; + std::replace(label.begin(), label.end(), '_', ' '); + return label; +} + +// The screen at a failed step, kept: the retry resets the game. +void save_failure_screenshot(VideoStream& stream, size_t attempt){ + VideoSnapshot screen = stream.video().snapshot(); + if (!screen){ + return; + } + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/failures/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = std::format("{}{}-attempt{}.png", folder, now_to_filestring(), attempt); + screen.frame->save(path); + stream.log("Failure screenshot: " + path); +} + +} + + +StoryExecutor::StoryExecutor(StoryEnv& env) + : m_env(env) +{} + +void StoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ + m_checkpoint = data.id; + const StoryWorld* south = story_plan_world("Route202", "Route202"); + m_trainers = south ? story_trainers(*south) : std::vector{}; +} + +Outcome StoryExecutor::run(RunState& state, const StoryStep& step){ + stream().log(std::format("[{}] {} {}: {}", m_checkpoint, Pokemon::story_step_kind_name(step.kind), step.name, step.why), COLOR_BLUE); + try{ + return run_guarded(state, step); + }catch (OperationFailedException& e){ + if (m_stop_rows.contains(m_checkpoint)){ + throw; + } + // Attempts are the segment's policy; this only counts them for the + // log line the lab's run watcher reads. + const size_t attempt = m_failures[m_checkpoint]++; + stream().log(std::format("checkpoint_reattempt_loop: attempt {} failed: {}: {}", attempt, m_checkpoint, e.message()), COLOR_RED); + save_failure_screenshot(stream(), attempt); + return Pokemon::outcome_failed("operation_failed", e.message()); + } +} + +Outcome StoryExecutor::run_guarded(RunState& state, const StoryStep& step){ + const StoryEvents& events = story_events(); + if (auto event = events.find(step.variant); event != events.end()){ + return event->second(*this, state, step); + } + return Pokemon::StoryStepExecutor::run(state, step); +} + +Pose StoryExecutor::walk_to(const Pose& from, const Pose& goal, const StoryStep& step){ + const StoryWorld* maps = story_plan_world(from.map, goal.map); + if (maps == nullptr){ + fail_step(stream(), std::format("{}: no maps join {} and {}.", step.name, from.map, goal.map)); + } + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown route costs: " + step.variant); + } + std::vector start; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + start.push_back(story_to_world(*maps, from.map, pos)); + } + std::vector targets; + for (const TilePos& pos : Pokemon::tiles_of(goal)){ + targets.push_back(story_to_world(*maps, goal.map, pos)); + } + std::vector no_trainers; + const bool south = maps == story_plan_world("Route202", "Route202"); + const std::vector at = walk_route( + stream(), context(), run_from_wild_battle, *maps, start, + [&](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, + south ? m_trainers : no_trainers, + [this](const std::string& trainer){ fight_route_trainer(stream(), context(), trainer); }, + label_of(step), {}, *costs + ); + std::vector local; + for (const TilePos& pos : at){ + const std::optional tile = story_to_local(*maps, goal.map, pos); + if (!tile){ + fail_step(stream(), std::format("{}: the walk ended off {}.", step.name, goal.map)); + } + local.push_back(*tile); + } + return Pokemon::tile_pose(goal.map, local, Pokemon::facing_of(goal)); +} + +Outcome StoryExecutor::new_game(RunState&, const StoryStep&){ + play_new_game_intro(m_env.env, context(), m_env.player_name); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::options(RunState&, const StoryStep&){ + verify_tutorial_settings(stream(), context()); + context().wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::route(RunState& state, const StoryStep& step){ + if (Pokemon::tiles_of(state.pose).empty()){ + return Pokemon::outcome_failed("no_pose", Pokemon::to_string(state.pose)); + } + return Pokemon::outcome_ok(walk_to(state.pose, *step.target, step)); +} + +Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ + if (step.variant == "to_wall"){ + for (const Pokemon::StoryLeg& leg : step.legs){ + walk_until_stopped(stream(), context(), leg.direction, (size_t)leg.tiles, label_of(step)); + } + return Pokemon::outcome_ok(); + } + const bool one_short_ok = step.variant == "one_short_ok"; + if (!one_short_ok && !step.variant.empty()){ + return Pokemon::outcome_failed("unknown_variant", step.variant); + } + std::vector at = Pokemon::tiles_of(state.pose); + for (const Pokemon::StoryLeg& leg : step.legs){ + // Facing another way, a tap only turns (r256): turn first. + route_turn(stream(), context(), leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const size_t tiles = (size_t)leg.tiles; + const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, x, y, tiles, step.name); + stream().log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); + if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ + return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); + } + // Where we may be: the end, or a tile short of it. + std::vector next; + for (const TilePos& start : at){ + TilePos end = start; + for (size_t i = 0; i < tiles; i++){ + if (one_short_ok && i + 1 == tiles){ + next.push_back(end); + } + end = end.next(leg.direction); + } + next.push_back(end); + } + at = std::move(next); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.legs.back().direction)); +} + +Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ + route_turn(stream(), context(), step.direction); + const auto [x, y] = dpad_of(step.direction); + const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, x, y, 1, step.name); + stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); + std::vector at; + for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ + at.push_back(pos.next(step.direction)); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.direction)); +} + +Outcome StoryExecutor::talk(RunState&, const StoryStep& step){ + pbf_press_button(context(), BUTTON_A, 80ms, 200ms); + context().wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream(), context(), 3000ms, {{dialog}}) != 0){ + fail_step(stream(), label_of(step) + ": nobody answered."); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ + wait_for_dialogue(stream(), context(), label_of(step)); + pbf_press_button(context(), step.yes ? BUTTON_A : BUTTON_B, 80ms, 1000ms); + context().wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::mash(RunState&, const StoryStep& step){ + wait_for_dialogue(stream(), context(), label_of(step)); + finish_dialogue(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step)); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::battle(RunState&, const StoryStep& step){ + if (!talk_or_fight(stream(), context(), label_of(step), true, step.variant == "doubles")){ + fail_step(stream(), label_of(step) + ": talked but did not battle."); + } + if (!step.may_lose && in_pokemon_center(stream())){ + fail_step(stream(), label_of(step) + ": lost the battle."); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::train(RunState&, const StoryStep& step){ + for (size_t battle = 0; battle < step.battles; battle++){ + const std::string label = std::format("Training battle {}/{}", battle + 1, step.battles); + route_status(stream(), label); + battle_1_pokemon(stream(), context()); + fail_if_blacked_out(stream(), context(), label); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ + heal_at_pokemon_center(stream(), context(), label_of(step)); + // r143a's heal said it had walked out while still inside. + context().wait_for_all_requests(); + VideoSnapshot screen = stream().video().snapshot(); + if (screen && indoors(*screen.frame)){ + fail_step(stream(), label_of(step) + ": still inside the Pokemon Center."); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::save(RunState&, const StoryStep&){ + save_game(m_env.env, context()); + return Pokemon::outcome_ok(); +} + +Outcome StoryExecutor::game(RunState&, const StoryStep& step){ + return Pokemon::outcome_failed("unknown_event", step.variant); +} + + +Pokemon::Segment make_story_segment( + const std::vector& rows, Pokemon::StoryStepExecutor& executor, + std::set& saved +){ + static const StoryStep SAVE_AT_START = []{ + StoryStep step; + step.kind = Pokemon::StoryStepKind::SAVE; + step.name = "save"; + step.why = "the save a failed step resets to"; + return step; + }(); + Pokemon::Segment segment; + segment.id = "BDSP"; + for (const StoryCheckpoint& row : rows){ + segment.checkpoints.push_back(Pokemon::CheckpointSpec{ + .id = row.id, .title = row.title, .saves = false, + .build = [&row, &executor, &saved]{ + Pokemon::Checkpoint steps = Pokemon::build_story_checkpoint(row, executor); + if (row.on_failure != StoryFailure::RELOAD_AND_RETRY){ + return steps; + } + Pokemon::Event save = Pokemon::story_event(SAVE_AT_START, executor); + save.skip_if.push_back(Pokemon::condition("saved at the start", [&saved, id = row.id](RunState&){ + return saved.contains(id); + })); + // A save that fails is a failed first attempt: the retry + // resets and does not save again. + save.act = [&executor, &saved, id = row.id](RunState& state){ + saved.insert(id); + return executor.run(state, SAVE_AT_START); + }; + Pokemon::Checkpoint checkpoint; + checkpoint.id = row.id; + checkpoint.events.push_back(std::move(save)); + for (Pokemon::Event& event : steps.events){ + checkpoint.events.push_back(std::move(event)); + } + return checkpoint; + }, + }); + } + return segment; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h new file mode 100644 index 0000000000..1da372f489 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -0,0 +1,178 @@ +/* BDSP AutoStory - Story executor + * + * From: https://github.com/PokemonAutomation/ + * + * BDSP's way of doing the shared story steps + * (Pokemon/Navigation/Pokemon_StorySteps.h) on the console. A route is + * planned on the joined maps that hold its start and its target + * (story_plan_world()), from the tiles the run's pose holds, and walked by + * the walker, which fights the trainers who see us and runs from wild + * battles. A step whose variant names one of the story's events + * (story_events(), PokemonBDSP_AutoStory_Events.h) runs that event, + * whatever its kind: the scripted scenes and the console-measured walks + * of segment 01. + * + * Every step runs inside a guard that turns a failed console step into a + * failed Outcome, with a screenshot and the "checkpoint_reattempt_loop: + * attempt N failed" line the lab's run watcher reads; the segment then + * reloads the row's save and runs it again. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H + +#include +#include +#include +#include +#include +#include +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +enum class StarterChoice{ + TURTWIG, + CHIMCHAR, + PIPLUP, +}; + +// What a row does when one of its steps fails. +enum class StoryFailure{ + // Save before the first step; on a failure reset to that save and run + // the row again from its first step. + RELOAD_AND_RETRY, + // No save and no retry: the failure ends the program (00.00, before + // the game has anything worth saving). + STOP, +}; + +// A row of the story table: the shared checkpoint, and what a failure does. +struct StoryCheckpoint : public Pokemon::StoryCheckpoint{ + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY; +}; + +// What the story's steps act on. +struct StoryEnv{ + SingleSwitchProgramEnvironment& env; + ProControllerContext& context; + StarterChoice starter = StarterChoice::CHIMCHAR; + std::string player_name; +}; + + +// The plan costs a ROUTE step's variant names: "" the route costs, +// "town" town_plan_costs(), "robust" the planner's defaults. Nothing for +// another name. +std::optional story_route_costs(const std::string& variant); + + +// Does BDSP's kinds of story step on the console. +class StoryExecutor : public Pokemon::StoryStepExecutor{ +public: + explicit StoryExecutor(StoryEnv& env); + + StoryEnv& env(){ + return m_env; + } + VideoStream& stream(){ + return m_env.env.console; + } + ProControllerContext& context(){ + return m_env.context; + } + // The row running now. + const std::string& checkpoint() const{ + return m_checkpoint; + } + // The rows whose failure ends the program (StoryFailure::STOP): a + // failed step there throws on instead of failing the step. + void set_stop_rows(std::set rows){ + m_stop_rows = std::move(rows); + } + // A count that lasts the whole program run, across reloads (e.g. the + // Poke Mart lady's misses, after which the clerk sells the Potions). + size_t& run_count(const std::string& name){ + return m_run_counts[name]; + } + // Walks from the tiles of `from` on its map to any of `goal`'s tiles, + // fighting the trainers who see us this attempt. Returns where it left + // us, as a pose on `goal`'s map. + Pokemon::Pose walk_to(const Pokemon::Pose& from, const Pokemon::Pose& goal, const Pokemon::StoryStep& step); + + // A new attempt of a row: every trainer can see us again. + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override; + virtual Pokemon::Outcome run(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + + // The intro of a new game, from the title screen to the bedroom. + virtual Pokemon::Outcome new_game(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // verify_tutorial_settings(): fast text, Autosave off. + virtual Pokemon::Outcome options(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // Variant: the plan's costs ("" route, "town", "robust"). + virtual Pokemon::Outcome route(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // Each leg in D-pad taps, turning first. Variant "one_short_ok": a leg + // may end a tile short (a door lined up by sight after). Variant + // "to_wall": each leg walks until something stops it, for at most its + // tiles' time, as measured on the console; the pose is then unknown + // unless the step gives one. + virtual Pokemon::Outcome legs(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // One D-pad step `direction`, turning first: onto a scene's trigger or + // into a door. + virtual Pokemon::Outcome step(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A at what we face; a dialogue must open. + virtual Pokemon::Outcome talk(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A for YES, B for NO, once the question's box shows. + virtual Pokemon::Outcome answer(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A dialogue opens (or has), then B through it to the overworld; + // variant "a": A through it, for lines that give an item. + virtual Pokemon::Outcome mash(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A trainer's text opens (or has) and ends in a battle, fought with the + // best move; one lost fails the step unless `may_lose`. Variant + // "doubles" for a tag battle. + virtual Pokemon::Outcome battle(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // `battles` wild battles in the grass we stand in, with the best move. + virtual Pokemon::Outcome train(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // In front of a Pokemon Center, its lit doorway on screen: in, the + // nurse, and out (heal_at_pokemon_center()). + virtual Pokemon::Outcome heal(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + virtual Pokemon::Outcome save(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // The event its variant names (story_events()). + virtual Pokemon::Outcome game(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + +private: + Pokemon::Outcome run_guarded(Pokemon::RunState& state, const Pokemon::StoryStep& step); + + StoryEnv& m_env; + std::string m_checkpoint; + // Route 202's trainers who can still see us, in story_plan_world()'s + // world tiles; all of them again at each attempt. + std::vector m_trainers; + std::map m_failures; + std::map m_run_counts; + std::set m_stop_rows; +}; + + +// `rows` (kept alive by the caller) as one segment. A row that reloads on +// failure saves first, unless `saved` says it has in this run: a retry +// starts from that save. `executor` and `saved` must outlive the segment. +Pokemon::Segment make_story_segment( + const std::vector& rows, Pokemon::StoryStepExecutor& executor, + std::set& saved +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp new file mode 100644 index 0000000000..951a69d4e3 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -0,0 +1,180 @@ +/* BDSP AutoStory - Story places + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::TilePos; + + +namespace{ + +// The JSON maps' names and the location banners'. +const std::map& banner_names(){ + static const std::map names{ + {"TwinleafTown", "Twinleaf Town"}, + {"Route201", MAP_ROUTE_201}, + {"VerityLakefront", "Verity Lakefront"}, + {"SandgemTown", MAP_SANDGEM_TOWN}, + {"Route202", MAP_ROUTE_202}, + {"JubilifeCity", MAP_JUBILIFE_CITY}, + }; + return names; +} + +const std::string& banner_name(const std::string& map){ + const auto& names = banner_names(); + if (auto iter = names.find(map); iter != names.end()){ + return iter->second; + } + return map; +} + +// Twinleaf to Route 202 in one world, at the decomp's world tiles (its +// cells are 32 tiles square; Route 201's map starts 7 rows into its cell +// and Route 202's a column west of its own). +const StoryWorld& south_world(){ + static const StoryWorld world = []{ + StoryWorld world; + world.add(banner_name("TwinleafTown"), story_map("TwinleafTown"), {96, 864}); + world.add(banner_name("Route201"), story_map("Route201"), {96, 839}); + world.add(banner_name("VerityLakefront"), story_map("VerityLakefront"), {64, 832}); + world.add(banner_name("SandgemTown"), story_map("SandgemTown"), {160, 832}); + world.add(banner_name("Route202"), story_map("Route202"), {159, 800}); + return world; + }(); + return world; +} + +// The interiors of 00.01 to 00.04, each its own world. +const std::map& room_worlds(){ + static const std::map rooms = []{ + std::map ret; + for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab"}){ + ret[room].add(room, story_map(room), {0, 0}); + } + return ret; + }(); + return rooms; +} + +bool on_world(const StoryWorld& world, const std::string& map){ + if (&world == &south_world()){ + static const std::set south{"TwinleafTown", "Route201", "VerityLakefront", "SandgemTown", "Route202"}; + return south.contains(map); + } + return false; +} + +// The tiles of Route 202 we can stand on in the box (x0, y0) to (x1, y1). +std::vector route202_open(int x0, int y0, int x1, int y1){ + const StoryWorld& world = south_world(); + std::vector tiles; + for (int y = y0; y <= y1; y++){ + for (int x = x0; x <= x1; x++){ + if (world.standable(story_to_world(world, "Route202", {x, y}))){ + tiles.push_back({x, y}); + } + } + } + return tiles; +} + +std::vector route202_region(const std::string& region){ + const TilePos catch_spot = map_point("Route202", "catch_spot"); + if (region == "catch_save_area"){ + // Wherever the catch's walks in the grass ended (story_catch_save_area()). + return route202_open(15, 25, 21, 29); + } + if (region == "after_training_area"){ + // The catch spot's column and one either side, two rows up and down. + return route202_open(catch_spot.x - 1, catch_spot.y - 2, catch_spot.x + 1, catch_spot.y + 2); + } + if (region == "catch_grass"){ + // The training area's tall grass, which training walks into. + std::vector grass; + for (const TilePos& pos : route202_region("after_training_area")){ + if (story_map("Route202").tiles.at(pos) == Pokemon::TileKind::TALL_GRASS){ + grass.push_back(pos); + } + } + return grass; + } + if (region == "jubilife_exit"){ + // The open rows at the top: north from any of them is Jubilife. + return route202_open(0, 0, story_map("Route202").tiles.width - 1, 3); + } + return {}; +} + +} + + +TilePos story_point(const std::string& map, const std::string& point){ + return map_point(map, point); +} + +std::vector story_region(const std::string& map, const std::string& region){ + if (map == "Route202"){ + std::vector tiles = route202_region(region); + if (!tiles.empty()){ + return tiles; + } + } + return map_region(map, region); +} + +const StoryWorld* story_plan_world(const std::string& from, const std::string& to){ + if (on_world(south_world(), from) && on_world(south_world(), to)){ + return &south_world(); + } + if (from == "JubilifeCity" && to == from){ + return &jubilife_maps(); + } + const auto& rooms = room_worlds(); + if (auto iter = rooms.find(from); iter != rooms.end() && to == from){ + return &iter->second; + } + return nullptr; +} + +TilePos story_to_world(const StoryWorld& world, const std::string& map, const TilePos& local){ + return world.to_world(banner_name(map), local); +} + +std::optional story_to_local(const StoryWorld& world, const std::string& map, const TilePos& pos){ + const std::optional tile = world.to_local(pos); + if (!tile || tile->map != banner_name(map)){ + return std::nullopt; + } + return tile->pos; +} + +std::vector story_trainers(const StoryWorld& world){ + if (!on_world(world, "Route202")){ + return {}; + } + std::vector sights = route202_trainer_sights(); + for (TrainerSight& sight : sights){ + sight.pos = story_to_world(world, "Route202", sight.pos); + } + return sights; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h new file mode 100644 index 0000000000..9160d76216 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h @@ -0,0 +1,55 @@ +/* BDSP AutoStory - Story places + * + * From: https://github.com/PokemonAutomation/ + * + * Where the story's table and steps stand: tiles of the JSON maps + * ("Route202"), by map and point name (map_point(), map_region()). A walk + * from one map to another is planned on a story world that holds both: the + * outdoors from Twinleaf to Route 202, Jubilife, and each interior of + * 00.01 to 00.04 on its own. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryPlaces_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryPlaces_H + +#include +#include +#include +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The point `point` of the map `map` (map_point()). +Pokemon::TilePos story_point(const std::string& map, const std::string& point); + +// The tiles of the region `region` of the map `map`: map_region(), and +// the areas of Route 202 that the maps have no region for yet (where the +// catch may leave us, the training grass, the exit to Jubilife). Throws +// InternalProgramError for an unknown map or region. +std::vector story_region(const std::string& map, const std::string& region); + +// The story world a walk from `from` to `to` is planned on, or nothing if +// no world holds both. Its maps are named as the game's location banners +// name them ("Route 202"), which the walker reads. +const StoryWorld* story_plan_world(const std::string& from, const std::string& to); + +// The tile `local` of the map `map` in `world`. +Pokemon::TilePos story_to_world(const StoryWorld& world, const std::string& map, const Pokemon::TilePos& local); + +// The world tile `pos` as a tile of `map`, or nothing if it is not on `map`. +std::optional story_to_local(const StoryWorld& world, const std::string& map, const Pokemon::TilePos& pos); + +// The trainers a walk on `world` may meet (Route 202's), in its tiles. +std::vector story_trainers(const StoryWorld& world); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp index a3b6f3f2b6..8e91db3f8d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -1,508 +1,459 @@ -/* BDSP AutoStory Story Simulation Tests - * - * From: https://github.com/PokemonAutomation/ - * - * A run of the whole story table, new game to Floaroma, without the - * console: segment_00_table() and segment_01_table() built into segments - * as the program builds them (story_table_segment()), run by the shared - * runner with the program's retry policy, against a fake game. The fake - * plans each ROUTE on the built-in JSON maps from where the run's pose - * says we stand, walks each LEGS tile by tile on them, and passes every - * other step unless told to fail it. Steps that still call an old - * function pass as a whole. - * - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/TestRunners/UnitTest.h" -#include "Pokemon/Navigation/Pokemon_MapSet.h" -#include "Pokemon/Navigation/Pokemon_StorySegments.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" -#include "Pokemon/Navigation/Pokemon_TileGrid.h" -#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" -#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" -#include "Utils/PokemonBDSP_AutoStory_Location.h" -#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "Utils/PokemonBDSP_AutoStory_StoryExecutor.h" -#include "Utils/PokemonBDSP_AutoStory_StoryTables.h" -#include "PokemonBDSP_AutoStory_StoryTable.h" -#include "PokemonBDSP_AutoStory_StorySimulation_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -using Pokemon::Outcome; -using Pokemon::RunState; -using Pokemon::StoryStepKind; -using Pokemon::TilePos; - -std::string describe(const std::vector& tiles){ - std::string text; - for (const TilePos& pos : tiles){ - const std::optional local = story_maps().to_local(pos); - text += " " + (local ? local->map + " " + Pokemon::to_string(local->pos) : Pokemon::to_string(pos)); - } - return text; -} - -std::string joined(const std::vector& lines){ - std::string text; - for (const std::string& line : lines){ - text += "\n " + line; - } - return text; -} - -bool contains(const std::vector& tiles, const TilePos& pos){ - return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); -} - -// Where a ROUTE step ends from `from`, planned as ConsoleStoryExecutor -// plans it, or why it does not plan. -struct Walked{ - std::vector at; - std::string error; - std::string plan; -}; -Walked plan_route(const std::vector& from, const Pokemon::StoryStep& step){ - const std::optional costs = story_route_costs(step.variant); - if (!costs){ - return {{}, step.name + ": no plan costs named \"" + step.variant + "\".", ""}; - } - const std::vector goal = Pokemon::tiles_of(*step.target); - const auto plan = Pokemon::plan_robust_route(story_maps().world(), from, - [&](const TilePos& pos){ return contains(goal, pos); }, *costs); - if (!plan){ - return {{}, step.name + ": no plan from" + describe(from) + ".", ""}; - } - Walked walked{from, "", std::format("{}: {} legs from{}", step.name, plan->size(), describe(from))}; - for (const Pokemon::PlannedLeg& leg : *plan){ - walked.plan += "\n " + to_string(leg.leg) + " to" + describe(leg.after); - } - if (!plan->empty()){ - walked.at = plan->back().after; - } - for (const TilePos& pos : walked.at){ - if (!contains(goal, pos)){ - return {{}, step.name + ": the plan may end off the target, at" + describe({pos}) + ".", walked.plan}; - } - } - return walked; -} - -// Where a LEGS step ends from `from`, walked tile by tile on the maps, a -// tile short too where the step allows it. -Walked walk_legs(const std::vector& from, const Pokemon::StoryStep& step){ - const Pokemon::TileGrid& world = story_maps().world(); - std::vector at = from; - for (const Pokemon::StoryLeg& leg : step.legs){ - std::vector next; - for (TilePos pos : at){ - for (int i = 0; i < leg.tiles; i++){ - const std::optional stepped = world.step(pos, leg.direction); - if (!stepped){ - return {{}, step.name + ": " + direction_name(leg.direction) + " is blocked at" + describe({pos}) + ".", ""}; - } - if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ - next.push_back(pos); - } - pos = *stepped; - } - next.push_back(pos); - } - at = std::move(next); - } - return {at, "", ""}; -} - - -// Saves at a row's start into the trace, as the console runner saves. -class FakeRunner : public StoryStepRunner{ -public: - std::vector& trace; - std::set saved; - - explicit FakeRunner(std::vector& trace) - : trace(trace) - {} - virtual bool saved_at_start(const std::string& id) const override{ - return saved.contains(id); - } - virtual Outcome save_at_start(const std::string& id) override{ - saved.insert(id); - trace.push_back("save " + id); - return Pokemon::outcome_ok(); - } - virtual Outcome run_step(const std::string&, StoryFailure, const StoryAction&) override{ - return Pokemon::outcome_failed("no_console"); - } -}; - -// The game: steps pass, routes and legs move the pose on the maps, and -// the steps in `failures` ("/") fail, so many times, as said. -class FakeGame : public Pokemon::StoryStepExecutor{ -public: - struct Failure{ - std::string failure; - size_t times = 0; - }; - - std::vector& trace; - std::map failures; - // Every route's plan, for the log. - std::vector plans; - - explicit FakeGame(std::vector& trace) - : trace(trace) - {} - - virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ - m_row = data.id; - trace.push_back("begin " + data.id); - } - virtual Outcome run(RunState& state, const Pokemon::StoryStep& step) override{ - const std::string key = m_row + "/" + step.name; - trace.push_back(key); - auto failure = failures.find(key); - if (failure != failures.end() && failure->second.times > 0){ - failure->second.times--; - return Pokemon::outcome_failed(failure->second.failure, key); - } - return Pokemon::StoryStepExecutor::run(state, step); - } - virtual Outcome route(RunState& state, const Pokemon::StoryStep& step) override{ - const Walked walked = plan_route(Pokemon::tiles_of(state.pose), step); - if (!walked.error.empty()){ - return Pokemon::outcome_failed("no_plan", walked.error); - } - plans.push_back(walked.plan); - return Pokemon::outcome_ok(Pokemon::tile_pose(step.target->map, walked.at)); - } - virtual Outcome legs(RunState& state, const Pokemon::StoryStep& step) override{ - const Walked walked = walk_legs(Pokemon::tiles_of(state.pose), step); - if (!walked.error.empty()){ - return Pokemon::outcome_failed("blocked", walked.error); - } - return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, walked.at)); - } - virtual Outcome step(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } - virtual Outcome mash(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } - virtual Outcome battle(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } - virtual Outcome train(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } - virtual Outcome heal(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } - virtual Outcome shop(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } - virtual Outcome save(RunState&, const Pokemon::StoryStep&) override{ - trace.push_back("save in " + m_row); - return Pokemon::outcome_ok(); - } - virtual Outcome game(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } - -private: - std::string m_row; -}; - - -// One simulated run of both segments, as the program runs them. Its -// executor has a language no reader supports (Japanese), so a step that -// needed text would show. (The console's global language is left alone: -// tests run in parallel.) -struct Simulation{ - std::vector trace; - std::vector lines; - FakeRunner runner{trace}; - std::shared_ptr game = std::make_shared(trace); - Pokemon::SegmentPolicy policy; - - Pokemon::SegmentReport run(const std::string& id, const std::vector& table, const Pokemon::SegmentRange& range){ - game->set_language(Language::Japanese); - Pokemon::Segment segment = story_table_segment(runner, id, table, game); - Pokemon::SegmentHooks hooks; - hooks.reload = [this](RunState&){ - trace.push_back("reload"); - return Pokemon::outcome_ok(); - }; - hooks.log = [this](const std::string& line){ lines.push_back(line); }; - RunState state; - return Pokemon::run_segment(state, segment, range, policy, hooks); - } - // The rows begun, in order, one per attempt. - std::vector begun() const{ - std::vector ids; - for (const std::string& item : trace){ - if (item.starts_with("begin ")){ - ids.push_back(item.substr(6)); - } - } - return ids; - } - size_t count(const std::string& item) const{ - return (size_t)std::count(trace.begin(), trace.end(), item); - } - bool logged(const std::string& line) const{ - return std::find(lines.begin(), lines.end(), line) != lines.end(); - } -}; - -std::vector ids_of(const std::vector& table){ - std::vector ids; - for (const StoryRow& row : table){ - ids.push_back(row.id); - } - return ids; -} - - -UnitTestResult test_whole_table(Logger& logger){ - Simulation sim; - const Pokemon::SegmentReport first = sim.run("00", segment_00_table(), {}); - const Pokemon::SegmentReport second = sim.run("01", segment_01_table(), {}); - for (const std::string& plan : sim.game->plans){ - logger.log(plan); - } - if (!first.completed || !second.completed){ - return "Expected both segments through:" + joined(sim.lines); - } - std::vector expected = ids_of(segment_00_table()); - for (const std::string& id : ids_of(segment_01_table())){ - expected.push_back(id); - } - if (sim.begun() != expected){ - return "Unexpected checkpoint order:" + joined(sim.begun()); - } - // Every row saves first but 00.00, which a failure ends; 01.10 and - // 01.11 save again at their ends. - for (const std::string& id : expected){ - const size_t saves = sim.count("save " + id); - if (saves != (id == "00.00" ? 0 : 1)){ - return std::format("{} saved {} times at its start:{}", id, saves, joined(sim.trace)); - } - } - if (sim.count("save in 01.10") != 1 || sim.count("save in 01.11") != 1){ - return "Expected 01.10 and 01.11 to save at their ends:" + joined(sim.trace); - } - if (first.one_shot_passes() != first.checkpoints_run || second.one_shot_passes() != second.checkpoints_run){ - return "Expected every checkpoint one-shot:" + joined(sim.lines); - } - return true; -} - -UnitTestResult test_start_and_end(Logger&){ - Simulation sim; - Pokemon::SegmentReport report = sim.run("00", segment_00_table(), {.from = "00.04b", .to = "00.04d"}); - if (!report.completed || sim.begun() != std::vector{"00.04b", "00.04c", "00.04d"}){ - return "Expected 00.04b to 00.04d only:" + joined(sim.begun()); - } - Simulation tail; - report = tail.run("01", segment_01_table(), {.from = "01.10", .to = std::nullopt}); - if (!report.completed || tail.begun() != std::vector{"01.10", "01.11"}){ - return "Expected 01.10 to the end:" + joined(tail.begun()); - } - Simulation one; - report = one.run("00", segment_00_table(), {.from = "00.02", .to = "00.02"}); - if (!report.completed || one.begun() != std::vector{"00.02"}){ - return "Expected 00.02 alone:" + joined(one.begun()); - } - return true; -} - -// r72: a blackout in 00.04b's training. The checkpoint resets to the save -// it made at its start, does not save again, and walks from that save's -// pose to the grass again. -UnitTestResult test_training_blackout(Logger&){ - Simulation sim; - sim.game->failures["00.04b/train"] = {.failure = "blackout", .times = 1}; - const Pokemon::SegmentReport report = sim.run("00", segment_00_table(), {.from = "00.04b", .to = "00.04d"}); - const std::vector expected{"00.04b", "00.04b", "00.04c", "00.04d"}; - if (!report.completed || report.reloads != 1 || sim.begun() != expected){ - return "Expected 00.04b reloaded once, then on:" + joined(sim.trace); - } - if (sim.count("save 00.04b") != 1 || sim.count("00.04b/to_catch_grass") != 2){ - return "Expected the retry to start from 00.04b's one save and walk to the grass again:" + joined(sim.trace); - } - if (!sim.logged("[Checkpoint] 00.04b: passed on attempt 2 (1 reset)")){ - return "Expected the retried pass logged:" + joined(sim.lines); - } - return true; -} - -// A step that fails every time: three attempts from the checkpoint's -// save, then the segment stops on it and runs nothing after. -UnitTestResult test_gives_up_after_attempts(Logger&){ - Simulation sim; - sim.game->failures["01.04/oreburgh_gym"] = {.failure = "operation_failed", .times = 10}; - const Pokemon::SegmentReport report = sim.run("01", segment_01_table(), {}); - if (report.completed || report.reloads != 2 || report.failed_checkpoint != "01.04" || report.failed_event != "oreburgh_gym" - || report.runs.empty() || report.runs.back().attempts != 3 || sim.count("01.04/oreburgh_gym") != 3 - || sim.count("save 01.04") != 1 || sim.count("begin 01.05") != 0 - ){ - return "Expected 01.04 tried 3 times from its save, then a stop:" + joined(sim.trace) + joined(sim.lines); - } - // The stop-at-first-failure option: no reload. - Simulation lab; - lab.policy.stop_at_first_failure = true; - lab.game->failures["01.04/oreburgh_gym"] = {.failure = "operation_failed", .times = 10}; - const Pokemon::SegmentReport stopped = lab.run("01", segment_01_table(), {}); - if (stopped.completed || !stopped.stopped_at_first_failure || lab.count("reload") != 0){ - return "Expected a stop at the first failure without a reload:" + joined(lab.trace); - } - return true; -} - -// Each row that has a start pose starts where the row before it leaves -// the player, by the same planning and walking the fake run does. -UnitTestResult test_poses_chain(Logger&){ - for (const std::vector* table : {&segment_00_table(), &segment_01_table()}){ - std::optional> at; - std::string last = ""; - for (const StoryRow& row : *table){ - if (row.start){ - const std::vector start = Pokemon::tiles_of(*row.start); - if (at && !std::ranges::all_of(*at, [&](const TilePos& pos){ return contains(start, pos); })){ - return std::format("{} starts at{}, but {} may end at{}.", row.id, describe(start), last, describe(*at)); - } - at = start; - }else{ - at.reset(); - } - for (const Pokemon::StoryStep& step : row.steps){ - if (step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS){ - if (!at){ - return row.id + " " + step.name + ": walks from an unknown pose."; - } - const Walked walked = step.kind == StoryStepKind::ROUTE ? plan_route(*at, step) : walk_legs(*at, step); - if (!walked.error.empty()){ - return row.id + " " + walked.error; - } - at = walked.at; - }else if (step.kind == StoryStepKind::GAME || step.kind == StoryStepKind::SHOP){ - // A call to an old function: where it ends is the table's to say. - at.reset(); - } - if (step.pose_after){ - at = Pokemon::tiles_of(*step.pose_after); - } - } - last = row.id; - } - } - return true; -} - -// Every step that calls an old function names one the segment registers, -// and each table lints clean. -UnitTestResult test_variants_registered(Logger&){ - const struct{ const std::vector* table; std::map variants; } segments[] = { - {.table = &segment_00_table(), .variants = segment_00_variants()}, - {.table = &segment_01_table(), .variants = segment_01_variants()}, - }; - const std::set built_in{"", "town", "robust", "one_short_ok"}; - for (const auto& segment : segments){ - for (const StoryRow& row : *segment.table){ - const std::vector problems = Pokemon::lint_story_checkpoint(row); - if (!problems.empty()){ - return problems[0]; - } - for (const Pokemon::StoryStep& step : row.steps){ - if (!built_in.contains(step.variant) && !segment.variants.contains(step.variant)){ - return std::format("{} {}: no variant \"{}\" is registered.", row.id, step.name, step.variant); - } - } - } - } - return true; -} - -// With no reader for the game's language, the reads that used text give -// way: a banner or save card that showed matches any map, so walks check -// where they stand by the grid. -UnitTestResult test_no_text_reader(Logger&){ - for (Language language : {Language::Japanese, Language::Korean, Language::French}){ - if (story_reads_text(language)){ - return std::string("Expected no text reader for ") + language_data(language).name + "."; - } - } - if (!location_banner_matches(UNREAD_LOCATION, MAP_ROUTE_202) || location_banner_matches("", MAP_ROUTE_202)){ - return "Expected a shown, unread banner to match and no banner not to."; - } - if (!story_reads_text(Language::English) || location_banner_matches("sandgemtown", MAP_ROUTE_202)){ - return "Expected English banners read and compared."; - } - return true; -} - -// r261: out of the Center the walk saw no dark fade and walked on three -// rows. The walk stops at the first look that is not a room, fade or not. -UnitTestResult test_door_exit_looks(Logger&){ - const struct{ const char* name; std::vector> looks; size_t stops_at; } cases[] = { - {.name = "no fade: the first look outside", .looks = {{180, true}, {175, true}, {230, false}}, .stops_at = 2}, - {.name = "the fade, then outside", .looks = {{180, true}, {40, true}, {210, false}}, .stops_at = 2}, - {.name = "a dim look outside", .looks = {{180, true}, {150, false}, {230, false}}, .stops_at = 1}, - {.name = "still a room", .looks = {{180, true}, {40, true}, {190, true}}, .stops_at = 3}, - }; - for (const auto& c : cases){ - DoorFadeWatch watch; - size_t stopped = c.looks.size(); - for (size_t i = 0; i < c.looks.size(); i++){ - if (door_walk_stops(watch, c.looks[i].first, c.looks[i].second)){ - stopped = i; - break; - } - } - if (stopped != c.stops_at){ - return std::format("{}: stopped at look {}, expected {}.", c.name, stopped, c.stops_at); - } - } - return true; -} - - -class Test_StorySimulation : public UnitTest{ -public: - Test_StorySimulation(std::string name, std::function body) - : UnitTest("PokemonBDSP::AutoStory - Simulation: " + std::move(name)) - , m_body(std::move(body)) - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - try{ - return m_body(logger); - }catch (Exception& e){ - return std::string(e.name()) + ": " + e.message(); - } - } - -private: - std::function m_body; -}; - -} - - -void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database){ - database.add("00.00 to Floaroma, in order, each row saved first", test_whole_table); - database.add("start and end checkpoints", test_start_and_end); - database.add("a blackout in 00.04b's training resets to its save", test_training_blackout); - database.add("a step that keeps failing stops after 3 attempts", test_gives_up_after_attempts); - database.add("each start pose is where the row before ends; routes plan on the maps", test_poses_chain); - database.add("every called function is registered; tables lint", test_variants_registered); - database.add("out of a door: the first look that is not a room", test_door_exit_looks); - database.add("no text reader for the game's language", test_no_text_reader); -} - - - -} -} -} +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + * A run of the whole story table, new game to Floaroma, without the + * console: story_checkpoints() built into one segment as the program builds + * it (make_story_segment()), run by the shared runner with the program's + * retry policy, against a fake game. The fake plans each ROUTE on the story + * world that holds its start and target, walks each LEGS tile by tile, and + * passes every other step unless told to fail it. Events pass as a whole. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/Navigation/Pokemon_TilePose.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StorySimulation_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::RunState; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + +std::string describe(const std::string& map, const std::vector& tiles){ + std::string text = " " + map; + for (const TilePos& pos : tiles){ + text += " " + Pokemon::to_string(pos); + } + return text; +} + +std::string joined(const std::vector& lines){ + std::string text; + for (const std::string& line : lines){ + text += "\n " + line; + } + return text; +} + +bool contains(const std::vector& tiles, const TilePos& pos){ + return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); +} + +// Where a step ends, or why it does not plan. +struct Walked{ + std::optional at; + std::string error; + std::string plan; +}; + +// A ROUTE step from `from`, planned as StoryExecutor::walk_to() walks it. +Walked plan_route(const Pose& from, const StoryStep& step){ + const Pose& goal = *step.target; + const StoryWorld* world = story_plan_world(from.map, goal.map); + const std::optional costs = story_route_costs(step.variant); + if (world == nullptr || !costs){ + return {std::nullopt, std::format("{}: no world joins {} and {}, or no costs \"{}\".", step.name, from.map, goal.map, step.variant), ""}; + } + std::vector start; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + start.push_back(story_to_world(*world, from.map, pos)); + } + std::vector targets; + for (const TilePos& pos : Pokemon::tiles_of(goal)){ + targets.push_back(story_to_world(*world, goal.map, pos)); + } + const auto plan = plan_robust_route(*world, start, [&](const TilePos& pos){ return contains(targets, pos); }, *costs); + if (!plan){ + return {std::nullopt, step.name + ": no plan from" + describe(from.map, Pokemon::tiles_of(from)) + ".", ""}; + } + Walked walked{std::nullopt, "", std::format("{}: {} legs", step.name, plan->size())}; + std::vector local; + for (const PlannedLeg& leg : *plan){ + walked.plan += " " + to_string(leg.leg); + } + for (const TilePos& pos : plan->empty() ? start : plan->back().after){ + const std::optional tile = story_to_local(*world, goal.map, pos); + if (!tile || !contains(Pokemon::tiles_of(goal), *tile)){ + return {std::nullopt, step.name + ": the plan may end off the target, at " + world->describe(pos) + ".", walked.plan}; + } + local.push_back(*tile); + } + walked.at = Pokemon::tile_pose(goal.map, local, Pokemon::facing_of(goal)); + return walked; +} + +// A LEGS step from `from`, walked tile by tile on its map, a tile short too +// where the step allows it. Walks to walls end where nothing is known. +Walked walk_legs(const Pose& from, const StoryStep& step){ + if (step.variant == "to_wall"){ + return {std::nullopt, "", ""}; + } + const StoryWorld* world = story_plan_world(from.map, from.map); + if (world == nullptr){ + return {std::nullopt, step.name + ": no world holds " + from.map + ".", ""}; + } + std::vector at; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + at.push_back(story_to_world(*world, from.map, pos)); + } + for (const Pokemon::StoryLeg& leg : step.legs){ + std::vector next; + for (TilePos pos : at){ + for (int i = 0; i < leg.tiles; i++){ + const std::optional stepped = world->step(pos, leg.direction); + if (!stepped){ + return {std::nullopt, step.name + ": " + direction_name(leg.direction) + " is blocked at " + world->describe(pos) + ".", ""}; + } + if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ + next.push_back(pos); + } + pos = *stepped; + } + next.push_back(pos); + } + at = std::move(next); + } + std::vector local; + for (const TilePos& pos : at){ + local.push_back(*story_to_local(*world, from.map, pos)); + } + return {Pokemon::tile_pose(from.map, local, step.legs.back().direction), "", ""}; +} + +// A STEP onto the next tile of the same map. +Walked step_on(const Pose& from, const StoryStep& step){ + std::vector at; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + at.push_back(pos.next(step.direction)); + } + return {Pokemon::tile_pose(from.map, at, step.direction), "", ""}; +} + + +// The game: steps pass, routes, legs and steps move the pose on the maps, +// and the steps in `failures` ("/") fail, so many times. +class FakeGame : public Pokemon::StoryStepExecutor{ +public: + struct Failure{ + std::string failure; + size_t times = 0; + }; + + std::vector& trace; + std::map failures; + std::vector plans; + + explicit FakeGame(std::vector& trace) + : trace(trace) + {} + + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ + m_row = data.id; + trace.push_back("begin " + data.id); + } + virtual Outcome run(RunState& state, const StoryStep& step) override{ + const std::string key = m_row + "/" + step.name; + trace.push_back(key); + auto failure = failures.find(key); + if (failure != failures.end() && failure->second.times > 0){ + failure->second.times--; + return Pokemon::outcome_failed(failure->second.failure, key); + } + return Pokemon::StoryStepExecutor::run(state, step); + } + virtual Outcome route(RunState& state, const StoryStep& step) override{ + return moved(plan_route(state.pose, step), "no_plan"); + } + virtual Outcome legs(RunState& state, const StoryStep& step) override{ + return moved(walk_legs(state.pose, step), "blocked"); + } + virtual Outcome step(RunState& state, const StoryStep& step) override{ + return moved(step_on(state.pose, step), "blocked"); + } + virtual Outcome new_game(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome options(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome talk(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome mash(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome battle(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome train(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome heal(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome shop(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome save(RunState&, const StoryStep&) override{ + trace.push_back("save in " + m_row); + return Pokemon::outcome_ok(); + } + virtual Outcome game(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } + +private: + Outcome moved(const Walked& walked, const char* failure){ + if (!walked.error.empty()){ + return Pokemon::outcome_failed(failure, walked.error); + } + if (!walked.plan.empty()){ + plans.push_back(walked.plan); + } + return walked.at ? Pokemon::outcome_ok(*walked.at) : Pokemon::outcome_ok(Pose{}); + } + + std::string m_row; +}; + + +// One simulated run, as the program runs it, in `language`. Japanese has +// no text reader, so a step that needed text would show. (The console's +// global language is left alone: tests run in parallel.) +struct Simulation{ + std::vector trace; + std::vector lines; + FakeGame game{trace}; + std::set saved; + Pokemon::SegmentPolicy policy; + + Pokemon::SegmentReport run(const Pokemon::SegmentRange& range, Language language = Language::Japanese){ + game.set_language(language); + Pokemon::Segment segment = make_story_segment(story_checkpoints(), game, saved); + Pokemon::SegmentHooks hooks; + hooks.reload = [this](RunState&){ + trace.push_back("reload"); + return Pokemon::outcome_ok(); + }; + hooks.log = [this](const std::string& line){ lines.push_back(line); }; + RunState state; + return Pokemon::run_segment(state, segment, range, policy, hooks); + } + std::vector begun() const{ + std::vector ids; + for (const std::string& item : trace){ + if (item.starts_with("begin ")){ + ids.push_back(item.substr(6)); + } + } + return ids; + } + size_t count(const std::string& item) const{ + return (size_t)std::count(trace.begin(), trace.end(), item); + } + bool logged(const std::string& line) const{ + return std::find(lines.begin(), lines.end(), line) != lines.end(); + } +}; + +std::vector all_ids(){ + std::vector ids; + for (const StoryCheckpoint& row : story_checkpoints()){ + ids.push_back(row.id); + } + return ids; +} + + +UnitTestResult test_whole_table(Logger& logger){ + for (Language language : {Language::English, Language::Japanese}){ + Simulation sim; + const Pokemon::SegmentReport report = sim.run({}, language); + for (const std::string& plan : sim.game.plans){ + logger.log(plan); + } + if (!report.completed){ + return std::string(language_data(language).name) + ": expected the whole story through:" + joined(sim.lines); + } + if (sim.begun() != all_ids()){ + return "Unexpected checkpoint order:" + joined(sim.begun()); + } + // Every row saves first but 00.00, which a failure ends; 01.10 and + // 01.11 save again at their ends. + for (const std::string& id : all_ids()){ + const size_t saves = sim.count("save in " + id); + const size_t expected = id == "00.00" ? 0 : id == "01.10" || id == "01.11" ? 2 : 1; + if (saves != expected){ + return std::format("{} saved {} times, expected {}:{}", id, saves, expected, joined(sim.trace)); + } + } + if (report.one_shot_passes() != report.checkpoints_run){ + return "Expected every checkpoint one-shot:" + joined(sim.lines); + } + } + return true; +} + +UnitTestResult test_start_and_end(Logger&){ + Simulation sim; + Pokemon::SegmentReport report = sim.run({.from = "00.04b", .to = "00.04d"}); + if (!report.completed || sim.begun() != std::vector{"00.04b", "00.04c", "00.04d"}){ + return "Expected 00.04b to 00.04d only:" + joined(sim.begun()); + } + Simulation tail; + report = tail.run({.from = "01.10", .to = std::nullopt}); + if (!report.completed || tail.begun() != std::vector{"01.10", "01.11"}){ + return "Expected 01.10 to the end:" + joined(tail.begun()); + } + Simulation one; + report = one.run({.from = "00.02", .to = "00.02"}); + if (!report.completed || one.begun() != std::vector{"00.02"}){ + return "Expected 00.02 alone:" + joined(one.begun()); + } + return true; +} + +// r72: a blackout in 00.04b's training. The checkpoint resets to the save +// it made at its start, does not save again, and walks from that save's +// pose to the grass again. +UnitTestResult test_training_blackout(Logger&){ + Simulation sim; + sim.game.failures["00.04b/train"] = {.failure = "blackout", .times = 1}; + const Pokemon::SegmentReport report = sim.run({.from = "00.04b", .to = "00.04d"}); + const std::vector expected{"00.04b", "00.04b", "00.04c", "00.04d"}; + if (!report.completed || report.reloads != 1 || sim.begun() != expected){ + return "Expected 00.04b reloaded once, then on:" + joined(sim.trace); + } + if (sim.count("save in 00.04b") != 1 || sim.count("00.04b/to_catch_grass") != 2){ + return "Expected the retry to start from 00.04b's one save and walk to the grass again:" + joined(sim.trace); + } + if (!sim.logged("[Checkpoint] 00.04b: passed on attempt 2 (1 reset)")){ + return "Expected the retried pass logged:" + joined(sim.lines); + } + return true; +} + +// A step that fails every time: three attempts from the checkpoint's +// save, then the segment stops on it and runs nothing after. +UnitTestResult test_gives_up_after_attempts(Logger&){ + const std::string step = "01.04/gym_until_roark_beaten"; + Simulation sim; + sim.game.failures[step] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport report = sim.run({.from = "01.01", .to = std::nullopt}); + if (report.completed || report.reloads != 2 || report.failed_checkpoint != "01.04" || report.failed_event != "gym_until_roark_beaten" + || report.runs.empty() || report.runs.back().attempts != 3 || sim.count(step) != 3 + || sim.count("save in 01.04") != 1 || sim.count("begin 01.05") != 0 + ){ + return "Expected 01.04 tried 3 times from its save, then a stop:" + joined(sim.trace) + joined(sim.lines); + } + Simulation lab; + lab.policy.stop_at_first_failure = true; + lab.game.failures[step] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport stopped = lab.run({.from = "01.01", .to = std::nullopt}); + if (stopped.completed || !stopped.stopped_at_first_failure || lab.count("reload") != 0){ + return "Expected a stop at the first failure without a reload:" + joined(lab.trace); + } + return true; +} + +// Each row that has a start pose starts where the row before it leaves +// the player, by the same planning and walking the fake run does. +UnitTestResult test_poses_chain(Logger&){ + std::optional at; + std::string last; + for (const StoryCheckpoint& row : story_checkpoints()){ + if (row.start){ + const std::vector start = Pokemon::tiles_of(*row.start); + if (at && (at->map != row.start->map || + !std::ranges::all_of(Pokemon::tiles_of(*at), [&](const TilePos& pos){ return contains(start, pos); })) + ){ + return std::format("{} starts at{}, but {} may end at{}.", row.id, describe(row.start->map, start), + last, describe(at->map, Pokemon::tiles_of(*at))); + } + at = row.start; + }else{ + at.reset(); + } + for (const StoryStep& step : row.steps){ + if (step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || step.kind == StoryStepKind::STEP){ + if (!at && step.variant != "to_wall" && step.variant != "jubilife_stairs"){ + return row.id + " " + step.name + ": walks from an unknown pose."; + } + const Walked walked = !at ? Walked{} : step.kind == StoryStepKind::ROUTE ? plan_route(*at, step) + : step.kind == StoryStepKind::LEGS ? walk_legs(*at, step) : step_on(*at, step); + if (!walked.error.empty()){ + return row.id + " " + walked.error; + } + at = walked.at; + }else if (step.kind == StoryStepKind::GAME || step.kind == StoryStepKind::SHOP || step.kind == StoryStepKind::MASH){ + // A scene or an event: where it ends is the table's to say. + at.reset(); + } + if (step.pose_after){ + at = step.pose_after; + } + } + last = row.id; + } + return true; +} + +// With no reader for the game's language, the reads that used text give +// way: a banner or save card that showed matches any map. +UnitTestResult test_no_text_reader(Logger&){ + for (Language language : {Language::Japanese, Language::Korean, Language::French}){ + if (story_reads_text(language)){ + return std::string("Expected no text reader for ") + language_data(language).name + "."; + } + } + if (!location_banner_matches(UNREAD_LOCATION, "Route 202") || location_banner_matches("", "Route 202")){ + return "Expected a shown, unread banner to match and no banner not to."; + } + if (!story_reads_text(Language::English) || location_banner_matches("sandgemtown", "Route 202")){ + return "Expected English banners read and compared."; + } + return true; +} + + +class Test_StorySimulation : public UnitTest{ +public: + Test_StorySimulation(std::string name, std::function body) + : UnitTest("PokemonBDSP::AutoStory - Simulation: " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + +} + + +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database){ + database.add("00.00 to Floaroma, in order, each row saved first, English and Japanese", test_whole_table); + database.add("start and end checkpoints", test_start_and_end); + database.add("a blackout in 00.04b's training resets to its save", test_training_blackout); + database.add("a step that keeps failing stops after 3 attempts", test_gives_up_after_attempts); + database.add("each start pose is where the row before ends; routes plan on the maps", test_poses_chain); + database.add("no text reader for the game's language", test_no_text_reader); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index cc6e2f4c9e..65df817a0a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -1,252 +1,418 @@ -/* BDSP AutoStory - Story table - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include "Data/PokemonBDSP_AutoStory_Route202.h" -#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" -#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "PokemonBDSP_AutoStory_StoryTable.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using Pokemon::Direction; -using Pokemon::Pose; -using Pokemon::StoryLeg; -using Pokemon::StoryStep; -using Pokemon::StoryStepKind; -using Pokemon::TilePos; - - -namespace{ - -constexpr Direction NORTH = Direction::NORTH; -constexpr Direction WEST = Direction::WEST; - -TilePos world(const char* map, const TilePos& local){ - return story_maps().to_world(map, local); -} - -// A pose on any of `tiles`, world tiles of story_maps(). -Pose on(const char* map, std::vector tiles, std::optional facing = std::nullopt){ - return Pokemon::tile_pose(map, std::move(tiles), facing); -} - -Pose save_spot(){ - return on(MAP_ROUTE_202, {world(MAP_ROUTE_202, Route202::SAVE_SPOT)}); -} - -// The catch grass training walks into: the tall grass of the training area. -std::vector catch_grass(){ - std::vector grass = story_after_training_area(); - std::erase_if(grass, [](const TilePos& pos){ - return story_maps().world().at(pos) != Pokemon::TileType::TALL_GRASS; - }); - return grass; -} - -// Route 202's open rows at the top: north from any of them is Jubilife. -std::vector jubilife_exit(){ - std::vector exit; - const Pokemon::TileGrid& grid = Route202::grid(); - for (int y = 0; y <= 3; y++){ - for (int x = 0; x < grid.width(); x++){ - const TilePos pos = world(MAP_ROUTE_202, {x, y}); - if (story_maps().world().standable(pos) && story_at_jubilife_exit(pos)){ - exit.push_back(pos); - } - } - } - return exit; -} - - -StoryStep make(StoryStepKind kind, std::string name, std::string why){ - StoryStep step; - step.kind = kind; - step.name = std::move(name); - step.why = std::move(why); - return step; -} - -// A step that calls the old function named `name` (the segment's variant). -StoryStep call(std::string name, std::string why, std::optional pose_after = std::nullopt){ - StoryStep step = make(StoryStepKind::GAME, name, std::move(why)); - step.variant = std::move(name); - step.pose_after = std::move(pose_after); - return step; -} - -StoryStep route(std::string name, Pose target, std::string why, std::string costs = ""){ - StoryStep step = make(StoryStepKind::ROUTE, std::move(name), std::move(why)); - step.target = std::move(target); - step.variant = std::move(costs); - return step; -} - -StoryStep legs(std::string name, std::vector legs, std::string why, std::string variant = ""){ - StoryStep step = make(StoryStepKind::LEGS, std::move(name), std::move(why)); - step.legs = std::move(legs); - step.variant = std::move(variant); - return step; -} - -StoryStep train(size_t battles, Pose pose_after, std::string why){ - StoryStep step = make(StoryStepKind::TRAIN, "train", std::move(why)); - step.battles = battles; - step.pose_after = std::move(pose_after); - return step; -} - -StoryStep heal(std::string name, Pose pose_after, std::string why){ - StoryStep step = make(StoryStepKind::HEAL, std::move(name), std::move(why)); - step.pose_after = std::move(pose_after); - return step; -} - -StoryStep with_variant(StoryStep step, std::string variant){ - step.variant = std::move(variant); - return step; -} - -StoryRow row(std::string id, std::string title, std::optional start, std::vector steps, - StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY -){ - StoryRow ret; - ret.id = std::move(id); - ret.title = std::move(title); - ret.start = std::move(start); - ret.steps = std::move(steps); - ret.on_failure = on_failure; - return ret; -} - - -std::vector make_segment_00(){ - const int to_center_door = SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x; - const Pose center_door_front = on(MAP_SANDGEM_TOWN, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR)); - StoryStep potions = make(StoryStepKind::SHOP, "potions", - "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp), the clerk's 5 after two misses"); - potions.items = {{.item = "potion", .quantity = 10}}; - potions.variant = "sandgem_potions"; - potions.pose_after = save_spot(); - StoryStep stairs = make(StoryStepKind::STEP, "into_jubilife", - "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); - stairs.direction = NORTH; - stairs.variant = "jubilife_stairs"; - - return { - row("00.00", "Verify Settings", std::nullopt, { - call("verify_settings", "before any route movement or checkpoint save: fail visibly if options cannot be read"), - }, StoryFailure::STOP), - row("00.01", "After TV", std::nullopt, { - call("tutorial_part_1_navigation", "intro and starter selection: Butters' recorded stick holds to the lake"), - }), - row("00.02", "Outside Verity before starter", std::nullopt, { - call("starter_nav", "into the lake's scene"), - call("select_starter", "the starter from the program's option"), - with_variant(make(StoryStepKind::BATTLE, "fight_starly", "the Starly battle"), "fight_starly"), - with_variant(make(StoryStepKind::MASH, "post_starly_cleanup", "the scene after the battle"), "post_starly_cleanup"), - }), - row("00.03", "Outside Verity after starter", std::nullopt, { - call("tutorial_part_2_navigation", "Pokedex acquisition: back through Route 201 to Rowan's lab"), - }), - row("00.04", "After Pokedex, to the catch", std::nullopt, { - call("tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", - on(MAP_ROUTE_202, story_catch_save_area())), - }), - // A Lv5 starter fainted to Route 202's trainers (r72): training, a - // heal, and Potions first, each row from the save the one before ends on. - row("00.04b", "After the catch: train and heal in Sandgem", on(MAP_ROUTE_202, story_catch_save_area()), { - route("to_catch_grass", on(MAP_ROUTE_202, catch_grass()), "into the training column's grass"), - train(3, on(MAP_ROUTE_202, story_after_training_area()), "three wild battles with the best move"), - // One walk round the houses ended two tiles short (r252): down - // the road first, then the town plan. - route("into_sandgem", on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP)}), - "south down the road to above the Mart", "robust"), - route("to_center_sign", on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH))}), - "below the Center's sign, its doorway in sight", "town"), - // From below the sign the finder read the signs' panels as - // doorways (r252, r255): taps instead. One tile short is fine. - legs("below_center_door", {{.direction = WEST, .tiles = to_center_door}}, "taps west below the Center's door", "one_short_ok"), - heal("heal_sandgem", center_door_front, "Sandgem Town Pokemon Center"), - }), - row("00.04c", "Potions, back to Route 202's save spot", center_door_front, { - potions, - }), - // From the one save spot: from both at once the planner gives up. - row("00.04d", "Route 202's trainers, Dawn in Jubilife", save_spot(), { - route("north_to_jubilife", on(MAP_ROUTE_202, jubilife_exit()), "north past Route 202's trainers, fighting who sees us"), - stairs, - make(StoryStepKind::MASH, "dawn_scene", "Dawn walks over while we stand still"), - }), - row("00.05", "Jubilife After Dawn", std::nullopt, { - call("tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC"), - }), - }; -} - -std::vector make_segment_01(){ - StoryStep save = make(StoryStepKind::SAVE, "save_here", "the next checkpoint starts here"); - return { - row("01.01", "Jubilife to Barry", std::nullopt, { - call("jubilife_to_barry", "from the Center's PC out to Barry on Route 203"), - }), - row("01.02", "Route 203 to Oreburgh", std::nullopt, { - call("route203_to_oreburgh", "east through the Oreburgh Gate into Oreburgh City"), - }), - row("01.03", "Oreburgh Mine to Roark", std::nullopt, { - call("oreburgh_to_roark", "into the Mine to Roark and back to the Center"), - }), - row("01.04", "Oreburgh Gym", std::nullopt, { - call("oreburgh_gym", "the Gym's trainers and Roark for the Coal Badge"), - }), - row("01.05", "Oreburgh to Route 203", std::nullopt, { - call("oreburgh_to_route203", "heal, then west out of the Oreburgh Gate"), - }), - row("01.06", "Route 203 to Jubilife", std::nullopt, { - call("route203_to_jubilife", "west along Route 203 into Jubilife"), - }), - row("01.07", "Jubilife through Rowan's scene", std::nullopt, { - call("jubilife_to_rowan", "to the north street, Rowan's scene and the tag battle"), - }), - row("01.08", "Jubilife to the Ravaged Path", std::nullopt, { - call("jubilife_to_ravaged_path", "up Route 204's south half into the Ravaged Path"), - }), - row("01.09", "Ravaged Path", std::nullopt, { - call("ravaged_path", "through the cave with Rock Smash, out onto Route 204 North"), - }), - row("01.10", "Route 204 to Floaroma", std::nullopt, { - call("route204_to_floaroma", "the human's line north into Floaroma"), - save, - }), - row("01.11", "Floaroma's old couple", std::nullopt, { - call("floaroma_old_couple", "the old woman and the old man in the flower field"), - save, - }), - }; -} - -} - - -const std::vector& segment_00_table(){ - static const std::vector table = make_segment_00(); - return table; -} - -const std::vector& segment_01_table(){ - static const std::vector table = make_segment_01(); - return table; -} - - - -} -} -} +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + * 00.00 to 00.05 are planned walks on the JSON maps, with the scenes' + * triggers from the Platinum and Diamond decomps (Segment00Lines) and + * Butters' button sequences for the scenes (#1717). They replace his + * recorded stick holds, and have NOT run on a console yet: where a scene + * leaves the player (pose_after) is the decomp's or a guess, marked + * "assumed", until a new-game run checks it. 00.04b to 00.04d and 00.05's + * Jubilife walks were measured on the console (r72 to r261, 2026-10-30); + * segment 01 ran there end to end (2026-10-31), its walks kept as events. + * + */ + +#include +#include +#include +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; +using Pokemon::Pose; +using Pokemon::StoryLeg; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +namespace{ + +constexpr Direction NORTH = Direction::NORTH; +constexpr Direction SOUTH = Direction::SOUTH; +constexpr Direction EAST = Direction::EAST; +constexpr Direction WEST = Direction::WEST; + +const char* const BEDROOM = "PlayerHouse2F"; +const char* const HOME = "PlayerHouse1F"; +const char* const BARRYS_HOUSE = "RivalHouse1F"; +const char* const BARRYS_ROOM = "RivalHouse2F"; +const char* const LAB = "RowansLab"; +const char* const TWINLEAF = "TwinleafTown"; +const char* const ROUTE_201 = "Route201"; +const char* const LAKEFRONT = "VerityLakefront"; +const char* const SANDGEM = "SandgemTown"; +const char* const ROUTE_202 = "Route202"; +const char* const JUBILIFE = "JubilifeCity"; + +Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, {TilePos{x, y}}, facing); +} +Pose at(const char* map, const char* point, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, {story_point(map, point)}, facing); +} +Pose on(const char* map, const char* region){ + return Pokemon::tile_pose(map, story_region(map, region)); +} +// The tile below the door `door` of `map` and its neighbours either side: +// a door is not lined up with one tile (Jubilife's Center, r85). +Pose door_front(const char* map, const char* door){ + const TilePos below = story_point(map, door).next(SOUTH); + return Pokemon::tile_pose(map, {below.next(WEST), below, below.next(EAST)}); +} + +StoryCheckpoint row( + std::string id, std::string title, std::optional start, std::vector steps, + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY +){ + StoryCheckpoint checkpoint; + checkpoint.id = std::move(id); + checkpoint.title = std::move(title); + checkpoint.start = std::move(start); + checkpoint.steps = std::move(steps); + checkpoint.on_failure = on_failure; + return checkpoint; +} + +StoryStep make(StoryStepKind kind, std::string name, std::string why){ + StoryStep step; + step.kind = kind; + step.name = std::move(name); + step.why = std::move(why); + return step; +} +StoryStep route(std::string name, Pose target, std::string why, std::string costs = ""){ + StoryStep step = make(StoryStepKind::ROUTE, std::move(name), std::move(why)); + step.target = std::move(target); + step.variant = std::move(costs); + return step; +} +StoryStep legs(std::string name, std::vector legs, std::string why, std::string variant = ""){ + StoryStep step = make(StoryStepKind::LEGS, std::move(name), std::move(why)); + step.legs = std::move(legs); + step.variant = std::move(variant); + return step; +} +// Legs that each walk until something stops them, as measured on the console. +StoryStep to_walls(std::string name, std::vector legs, std::string why){ + return PokemonBDSP::legs(std::move(name), std::move(legs), std::move(why), "to_wall"); +} +StoryStep step_into(std::string name, Direction direction, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::STEP, std::move(name), std::move(why)); + step.direction = direction; + step.pose_after = std::move(pose_after); + return step; +} +StoryStep talk(std::string name, std::string why){ + return make(StoryStepKind::TALK, std::move(name), std::move(why)); +} +// Text to the overworld, with B; with A ("a") for lines that give an item. +StoryStep scene(std::string name, std::optional pose_after, std::string why, std::string button = ""){ + StoryStep step = make(StoryStepKind::MASH, std::move(name), std::move(why)); + step.pose_after = std::move(pose_after); + step.variant = std::move(button); + return step; +} +StoryStep battle(std::string name, bool may_lose, std::string why, std::string variant = ""){ + StoryStep step = make(StoryStepKind::BATTLE, std::move(name), std::move(why)); + step.may_lose = may_lose; + step.variant = std::move(variant); + return step; +} +// One of the story's events (PokemonBDSP_AutoStory_Events.h), by name. +StoryStep event(std::string name, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::GAME, name, std::move(why)); + step.variant = std::move(name); + step.pose_after = std::move(pose_after); + return step; +} +StoryStep heal(std::string name, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::HEAL, std::move(name), std::move(why)); + step.pose_after = std::move(pose_after); + return step; +} +StoryStep save(){ + return make(StoryStepKind::SAVE, "save_here", "the next checkpoint starts here"); +} +StoryStep skip_if(StoryStep step, std::string flag){ + step.skip_if.push_back(std::move(flag)); + return step; +} + + +std::vector intro(){ + StoryStep new_game = make(StoryStepKind::NEW_GAME, "intro", "language, appearance, the player's name, Barry's default name"); + new_game.pose_after = at(BEDROOM, "start", NORTH); + return { + row("00.00", "New game: the intro and the settings", std::nullopt, { + skip_if(new_game, "save_loaded"), + make(StoryStepKind::OPTIONS, "settings", "fast text, Send to Boxes automatic, no nickname prompt, Autosave off"), + }, StoryFailure::STOP), + row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { + route("above_the_stairs", at(BEDROOM, 8, 5, NORTH), "the stairs (8, 4) east of the bed"), + // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). + step_into("down_the_stairs", NORTH, "onto the stairs", Pokemon::tile_pose(HOME, {story_point(HOME, "stairs"), story_point(HOME, "stairs").next(SOUTH)})), + // Butters' recording walked 1.1 s south before her first line. + scene("mom_calls", Pokemon::tile_pose(HOME, {story_point(HOME, "stairs"), story_point(HOME, "stairs").next(SOUTH)}), + "Mom calls the player over as they come down; we stay put (assumed)"), + route("above_the_door", at(HOME, 6, 9, SOUTH), "past Mom at (7, 8) to above the door"), + step_into("mom_warning", SOUTH, "the door's tile (6, 10): Mom's warning (coord event, VAR_PLAYER_HOUSE_STATE 1)"), + scene("mom_warns_us", at(HOME, "door", SOUTH), "\"don't go into the tall grass\""), + step_into("out_of_the_house", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), + route("short_of_barrys_door", at(TWINLEAF, 10, 12, WEST), "a tile east of Barry's doorstep"), + step_into("barrys_door", WEST, "the trigger on his doorstep (9, 12)"), + scene("barry_bursts_out", at(TWINLEAF, "barry_doorstep"), "Barry runs out, bumps into us and runs back in", "a"), + scene("barry_goes_home", at(TWINLEAF, "barry_doorstep"), "his second lines (Butters waited for two)", "a"), + step_into("into_barrys_house", NORTH, "his door", at(BARRYS_HOUSE, "door", NORTH)), + legs("off_barrys_mat", {{.direction = NORTH, .tiles = 1}}, "off the door's mat, which no route starts on"), + route("below_barrys_stairs", at(BARRYS_HOUSE, 2, 4, NORTH), "past his mother at (4, 6)"), + step_into("up_barrys_stairs", NORTH, "onto the stairs", at(BARRYS_ROOM, "stairs")), + route("beside_barry", at(BARRYS_ROOM, "barry_talk", EAST), "west of Barry, who faces his PC (assumed talk tile)"), + talk("barry", "Barry in his room"), + scene("barry_heads_for_the_lake", at(BARRYS_ROOM, "barry_talk", EAST), "he runs off to the lake", "a"), + route("above_barrys_stairs", at(BARRYS_ROOM, 3, 5, NORTH), "back to the stairs (3, 4)"), + step_into("down_barrys_stairs", NORTH, "onto the stairs", at(BARRYS_HOUSE, "stairs")), + route("above_barrys_door", at(BARRYS_HOUSE, 6, 9, SOUTH), "back to his door"), + step_into("out_of_barrys_house", SOUTH, "out of his door", at(TWINLEAF, "barry_doorstep", SOUTH)), + // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. + route("below_barrys_warning", at(ROUTE_201, 15, 19, NORTH), "up Twinleaf's exit path onto Route 201"), + step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), + scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; we stay on the trigger (assumed)"), + // Route 201's lower level, row 16, clear of the grass, then the Lakefront. + route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), + }), + }; +} + +std::vector lake_and_pokedex(){ + return { + row("00.02", "Lake Verity: the starter and the Starly battle", at(LAKEFRONT, "below_lake_entrance", NORTH), { + legs("to_the_lake_entrance", {{.direction = NORTH, .tiles = 1}}, "a tile short of the trigger"), + step_into("lake_entrance", NORTH, "the trigger (16..17, 12): Barry hears Rowan and the two walk in"), + event("starter_briefcase", "the lake's scene, to the open briefcase"), + event("choose_starter", "the starter from the program's option"), + event("fight_starly", "the Starly battle"), + // The scene walks us out; the game leaves us on the entrance tile facing south (assumed). + event("after_starly", "Rowan's lines and the walk out of the lake", at(LAKEFRONT, "lake_entrance", SOUTH)), + }), + row("00.03", "Back to Sandgem: Rowan, Dawn and the Pokedex", at(LAKEFRONT, "lake_entrance", SOUTH), { + route("short_of_rowan", at(ROUTE_201, 4, 14, EAST), "back east onto Route 201's lower level"), + step_into("rowans_stop", EAST, "the trigger (5, 14..16)"), + scene("rowan_and_dawn", at(ROUTE_201, "rowan_stop"), "Rowan and Dawn come back for the Pokedex's owner"), + // Through the grass now, with a starter: wild battles are run from. + route("short_of_dawn", at(SANDGEM, 3, 12, EAST), "east along Route 201 to Sandgem's edge"), + step_into("dawns_stop", EAST, "the trigger (4, 10..15)"), + event("rowan_gives_pokedex", "Dawn walks us to the lab: Rowan, no nickname, the Pokedex", + at(LAB, "rowan_talk", NORTH)), + }), + }; +} + +std::vector to_the_catch(){ + return { + row("00.04", "Home to Mom, then Dawn's catching tutorial on Route 202", at(LAB, "rowan_talk", NORTH), { + route("above_the_lab_door", at(LAB, 7, 14, SOUTH), "down the lab to its door (7, 15)"), + step_into("out_of_the_lab", SOUTH, "out of the lab", door_front(SANDGEM, "lab_door")), + // Dawn shows us the Center and the Mart; she leaves us by the Mart's door (assumed). + scene("dawn_shows_us_sandgem", door_front(SANDGEM, "poke_mart_door"), "Dawn's tour of Sandgem"), + // First to one known tile: the trees that end the doors' row. + route("to_the_row_end", at(SANDGEM, "front_row_east_end"), "east along the doors' row to the trees", "town"), + route("to_route_201", at(ROUTE_201, "route_201_east_end"), "west along the doors' row out of Sandgem"), + route("home", at(TWINLEAF, "house_doorstep", NORTH), "west along Route 201 to Twinleaf, running from wild battles"), + step_into("into_the_house", NORTH, "the house's door", at(HOME, "door", NORTH)), + legs("off_the_mat", {{.direction = NORTH, .tiles = 1}}, "off the door's mat, which no route starts on"), + route("beside_mom", at(HOME, 6, 8, EAST), "west of Mom at (7, 8)"), + talk("mom", "Mom"), + scene("mom_running_shoes", at(HOME, 6, 8, EAST), "her lines"), + route("back_above_the_door", at(HOME, 6, 9, SOUTH), "Barry's mother stands in the door"), + event("barrys_mother", "she gives us Barry's parcel", at(HOME, 6, 9, SOUTH)), + step_into("out_of_the_house_again", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), + route("short_of_the_catch_tutorial", at(ROUTE_202, 22, 27, WEST), "through Sandgem onto Route 202, east of its trigger column"), + step_into("catch_tutorial_trigger", WEST, "the trigger (21, 25..29), VAR_ROUTE_202_STATE 0"), + event("catch_tutorial", "Dawn's catching tutorial"), + event("catch_pokemon", "a Pokemon of our own from the grass", on(ROUTE_202, "catch_save_area")), + }), + // A Lv5 starter fainted to Route 202's trainers (r72): training, a + // heal, and Potions first, each row from the save the one before ends on. + row("00.04b", "After the catch: train and heal in Sandgem", on(ROUTE_202, "catch_save_area"), { + route("to_catch_grass", on(ROUTE_202, "catch_grass"), "into the training column's grass"), + []{ + StoryStep step = make(StoryStepKind::TRAIN, "train", "three wild battles with the best move"); + step.battles = 3; + step.pose_after = on(ROUTE_202, "after_training_area"); + return step; + }(), + // One walk round the houses ended two tiles short (r252): down + // the road first, then the town plan. + route("into_sandgem", at(SANDGEM, "route_202_entry_stop"), "south down the road to above the Mart", "robust"), + route("to_center_sign", at(SANDGEM, story_point(SANDGEM, "pokemon_center_sign").x, story_point(SANDGEM, "pokemon_center_sign").y + 1), + "below the Center's sign, its doorway in sight", "town"), + // From below the sign the finder read the signs' panels as + // doorways (r252, r255): taps instead. One tile short is fine. + legs("below_center_door", {{.direction = WEST, + .tiles = story_point(SANDGEM, "pokemon_center_sign").x - story_point(SANDGEM, "pokemon_center_door").x}}, + "taps west below the Center's door", "one_short_ok"), + heal("heal_sandgem", "Sandgem Town Pokemon Center", door_front(SANDGEM, "pokemon_center_door")), + }), + }; +} + +std::vector to_jubilife(){ + StoryStep potions = make(StoryStepKind::SHOP, "potions", + "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp), the clerk's 5 after two misses"); + potions.items = {{.item = "potion", .quantity = 10}}; + potions.variant = "sandgem_potions"; + potions.pose_after = at(ROUTE_202, "save_spot"); + StoryStep stairs = step_into("into_jubilife", NORTH, + "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); + stairs.variant = "jubilife_stairs"; + return { + row("00.04c", "Potions, back to Route 202's save spot", door_front(SANDGEM, "pokemon_center_door"), { + potions, + }), + // From the one save spot: from both at once the planner gives up. + row("00.04d", "Route 202's trainers, Dawn in Jubilife", at(ROUTE_202, "save_spot"), { + route("north_to_jubilife", on(ROUTE_202, "jubilife_exit"), "north past Route 202's trainers, fighting who sees us"), + stairs, + scene("dawn_scene", std::nullopt, "Dawn walks over while we stand still"), + }), + }; +} + +// The Jubilife beats, stepped through on the console on 2026-10-30: every +// leg walks until something stops it, and each talk stands beside the +// person; the coupons and the Poketch end on the street east to Route 203. +std::vector jubilife(){ + return { + row("00.05", "Jubilife: the Center, the Trainers' School and the Town Map", std::nullopt, { + event("up_the_steps", "a tile west of the save below the steps, then up them", on(JUBILIFE, "up_the_steps")), + to_walls("to_the_sign", {{.direction = WEST, .tiles = 8}, {.direction = NORTH, .tiles = 8}}, + "west to the fence's end, north to the sign by the lamp west of the road"), + to_walls("to_the_center", {{.direction = EAST, .tiles = 14}, {.direction = NORTH, .tiles = 14}}, + "east across the road to the house, north up the sidewalk to the Center's doorway"), + // The kids south of the Center can push us aside: the heal lines up by sight. + heal("heal_jubilife", "in, the nurse, out onto the doorstep (r143a: check we are out)"), + event("into_the_school", "eleven tiles west of the Center's doorstep, north through the School's door"), + to_walls("up_to_barry", {{.direction = NORTH, .tiles = 10}}, "the door's column to the back wall, beside Barry"), + event("barry_town_map", "Barry gives the Town Map"), + event("out_of_the_school", "straight down the door's column and out"), + }), + row("00.05b", "Jubilife: the president and the clowns' coupons", std::nullopt, { + event("meet_the_president", "four tiles east into the road; the president walks up and talks"), + to_walls("east_to_the_lamp", {{.direction = EAST, .tiles = 8}}, "east until the lamp by the Center"), + event("up_to_the_mart_road", "a tile west, then north onto the road between the Center and the Mart"), + to_walls("east_to_the_building", {{.direction = EAST, .tiles = 14}}, "east along it until the tall building"), + event("clown_by_the_mart", "his question, Yes, and a coupon"), + // The TV station's screen keeps the picture moving after we stop: + // r161 saw the stop only at 14 tiles' time, r162a not within it. + to_walls("to_the_west_lamp", {{.direction = WEST, .tiles = 20}, {.direction = SOUTH, .tiles = 10}, + {.direction = WEST, .tiles = 26}, {.direction = NORTH, .tiles = 6}}, + "west to the hedges, south to the long road, west to the lamp, north a row"), + event("clown_by_the_company", "along the hedges to the gap by the Poketch Company's sign; he comes by"), + }), + row("00.05c", "Jubilife: the Poketch, up to the street east to Route 203", std::nullopt, { + event("to_the_main_road", "a row south of the gap, east along the long road to the main road"), + event("to_the_center_lamp", "six rows down, east to the Mart's wall, south to the Center's lamp"), + event("poketch_from_the_president", "he trades the coupons for the Poketch"), + event("up_to_the_route_203_street", "let him walk off, then ten rows north"), + }), + }; +} + +std::vector to_oreburgh(){ + return { + row("01.01", "Jubilife to Barry", std::nullopt, { + event("jubilife_center_to_street", "out of the Center to the street east to Route 203, unless we stand there"), + event("east_to_barry", "east until Barry stops us at Route 203's entrance"), + battle("barry", true, "Barry's battle; a loss goes on, as it did on the console"), + }), + row("01.02", "Route 203 to Oreburgh", std::nullopt, { + event("route_203_east_to_the_gate", "east along Route 203, fighting the trainers who stop us"), + skip_if(event("the_hiker", "the Hiker in the Gate gives HM Rock Smash"), "met_hiker"), + event("hidden_moves_card", "the card after him, closed with B"), + event("gate_east_to_oreburgh", "east past Curtis and Diana into Oreburgh"), + event("oreburgh_welcome", "the man at the entrance, and Barry at the Center"), + }), + row("01.03", "Oreburgh Mine to Roark", std::nullopt, { + event("oreburgh_center_door", "in by the Center's door, talking to Barry in it"), + event("south_into_the_mine", "straight south from the door into the mine"), + event("mine_hall", "through the entrance hall"), + event("roark_in_the_mine", "down the corridor to Roark, his talk, and back out"), + event("oreburgh_center_heal", "the Center's nurse: 01.04 starts at her counter"), + }), + row("01.04", "Oreburgh Gym", std::nullopt, { + event("out_of_the_center", "out from the counter"), + event("gym_until_roark_beaten", "west into the gym, up it to Roark; again from the Center after a loss"), + }), + row("01.05", "Oreburgh to Route 203", std::nullopt, { + event("out_of_the_gym", "out of the gym onto its doorstep"), + event("into_oreburgh_gate", "up to the platform and west into the Gate"), + event("out_of_oreburgh_gate", "west until Route 203's banner shows"), + }), + }; +} + +std::vector to_floaroma(){ + return { + row("01.06", "Route 203 to Jubilife", std::nullopt, { + event("route_203_to_row_18", "on Route 203's map to row 18, below the girl"), + event("row_18_into_jubilife", "west along row 18, counting tiles"), + }), + row("01.07", "Jubilife through Rowan's scene", std::nullopt, { + event("west_to_the_crossroads", "west along the long road"), + event("north_to_rowan", "north up the street until Rowan's scene"), + battle("galactic_grunts", false, "the tag battle with Dawn", "doubles"), + event("rowan_after_the_battle", "Rowan's and Dawn's lines"), + }), + row("01.08", "Jubilife to the Ravaged Path", std::nullopt, { + event("north_out_of_jubilife", "north until Route 204's banner"), + event("route_204_south_to_the_door", "on Route 204 South's map to below the Ravaged Path's door"), + event("into_the_ravaged_path", "north through the door"), + }), + row("01.09", "Ravaged Path", std::nullopt, { + event("through_the_ravaged_path", "through the cave with Rock Smash, out onto Route 204 North"), + }), + row("01.10", "Route 204 to Floaroma", std::nullopt, { + event("route_204_to_floaroma", "the human's line north into Floaroma"), + save(), + }), + row("01.11", "Floaroma's old couple", std::nullopt, { + event("west_8_north_to_the_old_woman", "west 8, north until she stops us"), + talk("old_woman", "she gives Mew when a Let's Go save is on the console"), + scene("old_woman_talks", std::nullopt, "her lines"), + event("below_the_old_man", "a tile west, a tap north"), + talk("old_man", "he gives Jirachi with a Sword/Shield save"), + scene("old_man_talks", std::nullopt, "his lines"), + save(), + }), + }; +} + +std::vector make_table(){ + std::vector table; + for (auto part : {intro, lake_and_pokedex, to_the_catch, to_jubilife, jubilife, to_oreburgh, to_floaroma}){ + for (StoryCheckpoint& checkpoint : part()){ + table.push_back(std::move(checkpoint)); + } + } + return table; +} + +} + + +const std::vector& story_checkpoints(){ + static const std::vector checkpoints = make_table(); + return checkpoints; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h index 53a92dd035..bf9390ea77 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h @@ -1,42 +1,33 @@ -/* BDSP AutoStory - Story table - * - * From: https://github.com/PokemonAutomation/ - * - * Segments 00 and 01, new game to Floaroma, as rows of the shared story - * steps (Pokemon/Navigation/Pokemon_StorySteps.h): plain data, as FRLG's - * table is, so the tests can plan its routes and run it against a fake - * game. Each row saves at its start, which is where the row before it - * ends. - * - * Steps that have no declarative form yet call one of the old functions: - * a GAME step (or another kind with a variant) names it, and the segment - * registers it with its executor (segment_00_variants(), - * segment_01_variants()). Those are the next to split. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H -#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H - -#include -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// 00.00 to 00.05: the settings, the lake, the Pokedex, the catch, Sandgem -// and Route 202 (00.04b to 00.04d as story steps), Jubilife to the Poketch. -const std::vector& segment_00_table(); - -// 01.01 to 01.11: Jubilife to Oreburgh and the Coal Badge, back, and north -// to Floaroma's old couple. -const std::vector& segment_01_table(); - - - -} -} -} -#endif +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + * The story from a new game to Floaroma, 00.00 to 01.11, as rows of the + * shared story steps (Pokemon/Navigation/Pokemon_StorySteps.h): plain + * data, as FRLG's table is, so the tests plan its routes and run it + * against a fake game. Each row but 00.00 saves at its start, which is + * where the row before it ends. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H + +#include +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Checkpoints 00.00 to 01.11, in order: segment 00 (the intro to the +// Poketch) and segment 01 (Jubilife to Floaroma). +const std::vector& story_checkpoints(); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp new file mode 100644 index 0000000000..0ab553860a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -0,0 +1,213 @@ +/* BDSP AutoStory - Story table Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StoryTable_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Direction; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + +// The variants each kind does itself, besides the story's events. +const std::map>& built_in_variants(){ + static const std::map> variants{ + {StoryStepKind::ROUTE, {"", "town", "robust"}}, + {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall"}}, + {StoryStepKind::MASH, {"", "a"}}, + {StoryStepKind::BATTLE, {"", "doubles"}}, + }; + return variants; +} + +// The table: unique ids in order, each row lints, and every step's variant +// is one its kind does or one of the story's events. +class Test_StoryTableLints : public UnitTest{ +public: + Test_StoryTableLints() + : UnitTest("PokemonBDSP::AutoStory - Story table: rows lint, variants known") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const StoryEvents& events = story_events(); + std::set ids; + std::string previous; + for (const StoryCheckpoint& row : story_checkpoints()){ + if (!ids.insert(row.id).second || row.id <= previous){ + return row.id + ": repeated or out of order."; + } + previous = row.id; + const std::vector problems = Pokemon::lint_story_checkpoint(row); + if (!problems.empty()){ + return row.id + ": " + problems[0]; + } + for (const Pokemon::StoryStep& step : row.steps){ + const auto built_in = built_in_variants().find(step.kind); + const bool known = events.contains(step.variant) || + (step.kind != StoryStepKind::GAME && step.variant.empty()) || + (built_in != built_in_variants().end() && built_in->second.contains(step.variant)); + if (!known){ + return std::format("{} {}: no event or {} way named \"{}\".", + row.id, step.name, Pokemon::story_step_kind_name(step.kind), step.variant); + } + } + } + if (!ids.contains("00.00") || !ids.contains("01.11") || story_checkpoints().front().on_failure != StoryFailure::STOP){ + return "Expected 00.00 (which stops on a failure) to 01.11."; + } + return true; + } +}; + +struct LadyLeg{ + Direction direction; + size_t tiles; // 0: walk until the wall + TilePos end; // world tile + std::string name; +}; + +// 00.04c's Potions: the Mart lady's walk (mart_lady()) is walks until +// stopped, so on the map each leg ends at its wall: from a tile to three +// below the Center's door, a tap east and north to the Center's front, +// west to the Lab's sign, a row south and west to her grass's edge, north +// to the trees on her row (she stands east of there), back a row south and +// east to Route 201's sign, a row south and east to the trees east of the +// Mart, and north to the front row's end. +class Test_MartLadyWalk : public UnitTest{ +public: + Test_MartLadyWalk() + : UnitTest("PokemonBDSP::AutoStory - 00.04: the walk to the Poke Mart lady and back") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const StoryWorld& maps = story_maps(); + const auto sandgem = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; + const auto route201 = [&](const TilePos& pos){ return maps.to_world(MAP_ROUTE_201, pos); }; + const std::vector legs{ + {Direction::EAST, 1, sandgem({18, 0}), "a tap east under the Center's wall"}, + {Direction::NORTH, 0, sandgem({18, 11}), "north to the Center's front"}, + {Direction::WEST, 0, sandgem({6, 11}), "west to the Lab's sign"}, + {Direction::SOUTH, 1, sandgem({6, 12}), "a row south onto the path"}, + {Direction::WEST, 0, route201({42, 5}), "west to her grass's edge"}, + {Direction::NORTH, 0, route201({42, 3}), "north to the trees, on her row"}, + {Direction::SOUTH, 1, route201({42, 4}), "a row south onto the signs' row"}, + {Direction::EAST, 0, route201(map_point("Route201", "border_sign").next(Direction::WEST)), "east to Route 201's sign"}, + {Direction::SOUTH, 1, route201({54, 5}), "a row south onto the path"}, + {Direction::EAST, 0, sandgem({29, 12}), "east to the trees east of the Mart"}, + {Direction::NORTH, 0, sandgem(map_point("SandgemTown", "front_row_east_end")), "north to the front row's end"}, + }; + // Out of the Center's door we stand a tile to three below it (r261). + for (int below = 1; below <= 3; below++){ + const std::string error = follow(maps, legs, sandgem({17, 10 + below}), logger); + if (!error.empty()){ + return std::format("From {} below the door: {}", below, error); + } + } + return true; + } + +private: + static std::string follow(const StoryWorld& maps, const std::vector& legs, TilePos at, Logger& logger){ + for (const LadyLeg& leg : legs){ + if (leg.tiles == 0){ + at = walk_to_wall(maps, at, leg.direction).stop; + }else{ + const std::optional next = maps.step(at, leg.direction); + if (!next){ + return leg.name + ": blocked at " + maps.describe(at) + "."; + } + at = *next; + } + // The tap east lands on whichever row we stood on. + if (&leg != &legs.front() && !(at == leg.end)){ + return leg.name + ": ends at " + maps.describe(at) + ", not " + maps.describe(leg.end) + "."; + } + logger.log(leg.name + ": " + maps.describe(at)); + } + return ""; + } +}; + +// 01.10 on Route 204's north half: the line a human walked (2026-10-31), +// corner to corner, from the Ravaged Path's exit into Floaroma. Each +// stretch must plan as the one straight tap leg the human took, on the map +// as it is: a detour here means the map or the line changed. +class Test_Route204NorthLine : public UnitTest{ +public: + Test_Route204NorthLine() + : UnitTest("PokemonBDSP::AutoStory - Route 204 North: the line into Floaroma") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const StoryWorld& route = route204_north_maps(); + const std::vector waypoints = map_region("Route204North", "floaroma_waypoints"); + struct Expected{ Direction direction; size_t tiles; }; + const std::vector expected{ + {Direction::SOUTH, 1}, {Direction::WEST, 9}, {Direction::NORTH, 5}, {Direction::EAST, 8}, + {Direction::NORTH, 10}, {Direction::WEST, 8}, {Direction::NORTH, 7}, + }; + if (waypoints.size() != expected.size()){ + return std::format("Route 204 North: {} waypoints, expected {}.", waypoints.size(), expected.size()); + } + std::vector at{map_point("Route204North", "cave_exit")}; + for (size_t i = 0; i < waypoints.size(); i++){ + const TilePos goal = waypoints[i]; + const std::string name = std::format("Route 204 North stretch {} to {}", i + 1, route.describe(goal)); + const auto plan = plan_robust_route(route, at, [&](const TilePos& pos){ return pos == goal; }, waypoint_plan_costs()); + if (!plan){ + return name + ": no plan."; + } + if (plan->size() != 1){ + return std::format("{}: {} legs, expected the one straight leg.", name, plan->size()); + } + const TileLeg& leg = (*plan)[0].leg; + logger.log(name + ": " + to_string(leg)); + if (leg.ends_at_wall || leg.direction != expected[i].direction || leg.tiles != expected[i].tiles){ + return std::format("{}: planned {}, expected an open leg of {} tiles.", name, to_string(leg), expected[i].tiles); + } + if ((*plan)[0].after.size() != 1 || !((*plan)[0].after[0] == goal)){ + return name + ": the leg does not end exactly on the waypoint."; + } + at = {goal}; + } + if (!in_floaroma_path(at[0])){ + return "Route 204 North: the last waypoint is not in the path into Floaroma."; + } + return true; + } +}; + +} + + +void add_tests_AutoStory_StoryTable(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h new file mode 100644 index 0000000000..ffde11f4bb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h @@ -0,0 +1,25 @@ +/* BDSP AutoStory - StoryTable Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Tests that need no console: the story table: rows lint, every step's variant is known, and the walks its events plan. +void add_tests_AutoStory_StoryTable(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp deleted file mode 100644 index 82dcc701ed..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/* BDSP AutoStory Story Tables Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include -#include -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" -#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" -#include "Utils/PokemonBDSP_AutoStory_StoryTables.h" -#include "PokemonBDSP_AutoStory_StoryTables_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -// Builds tables without a console; nothing in them runs. -class DescribeOnlyRunner : public StoryStepRunner{ -public: - virtual bool saved_at_start(const std::string&) const override{ - return false; - } - virtual Pokemon::Outcome save_at_start(const std::string&) override{ - return Pokemon::outcome_failed("not_run"); - } - virtual Pokemon::Outcome run_step(const std::string&, StoryFailure, const StoryAction&) override{ - return Pokemon::outcome_failed("not_run"); - } -}; - -struct TableCase{ - std::string name; - std::function make; - std::string segment_id; - std::vector checkpoint_ids; - // Rows whose first event is not the save (they neither save nor retry). - std::vector unsaved; -}; - -const std::vector& table_cases(){ - static const std::vector cases{ - { - .name = "Segment 00", - .make = make_segment_00, - .segment_id = "00", - .checkpoint_ids = {"00.00", "00.01", "00.02", "00.03", "00.04", "00.04b", "00.04c", "00.04d", "00.05"}, - .unsaved = {"00.00"}, - }, - { - .name = "Segment 01", - .make = make_segment_01, - .segment_id = "01", - .checkpoint_ids = {"01.01", "01.02", "01.03", "01.04", "01.05", "01.06", "01.07", "01.08", "01.09", "01.10", "01.11"}, - .unsaved = {}, - }, - }; - return cases; -} - -// An empty string when `table` matches `expected`; else what differs. -std::string check_table(Pokemon::Segment& table, const TableCase& expected){ - if (table.id != expected.segment_id){ - return std::format("{}: id is \"{}\", expected \"{}\".", expected.name, table.id, expected.segment_id); - } - std::vector ids; - for (const Pokemon::CheckpointSpec& spec : table.checkpoints){ - ids.push_back(spec.id); - } - if (ids != expected.checkpoint_ids){ - std::string read; - for (const std::string& id : ids){ - read += " " + id; - } - return std::format("{}: {} rows,{}; expected {}.", expected.name, ids.size(), read, expected.checkpoint_ids.size()); - } - for (Pokemon::CheckpointSpec& spec : table.checkpoints){ - Pokemon::Checkpoint checkpoint = spec.build(); - if (checkpoint.id != spec.id){ - return std::format("{}: row {} builds checkpoint \"{}\".", expected.name, spec.id, checkpoint.id); - } - if (checkpoint.events.empty()){ - return std::format("{}: {} has no events.", expected.name, spec.id); - } - const bool unsaved = std::find(expected.unsaved.begin(), expected.unsaved.end(), spec.id) != expected.unsaved.end(); - const bool saves_first = checkpoint.events[0].name == "save"; - if (saves_first == unsaved){ - return std::format("{}: {}'s first event is \"{}\", expected {}.", - expected.name, spec.id, checkpoint.events[0].name, unsaved ? "a step" : "the save"); - } - const std::vector problems = Pokemon::lint_checkpoint(checkpoint); - if (!problems.empty()){ - return std::format("{}: {} lint: {}", expected.name, spec.id, problems[0]); - } - } - return ""; -} - -class Test_StoryTables : public UnitTest{ -public: - Test_StoryTables() - : UnitTest("PokemonBDSP::AutoStory - Story tables: rows, events and describe()") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - for (const TableCase& expected : table_cases()){ - DescribeOnlyRunner runner; - Pokemon::Segment table = expected.make(runner); - const std::string problem = check_table(table, expected); - if (!problem.empty()){ - return problem; - } - const std::string text = Pokemon::describe(table); - for (const std::string& id : expected.checkpoint_ids){ - if (text.find(id) == std::string::npos){ - return std::format("{}: describe() does not name {}:\n{}", expected.name, id, text); - } - } - logger.log(text); - } - return true; - } -}; - -} - - -void add_tests_AutoStory_StoryTables(UnitTestDatabase& database){ - database.add(); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h deleted file mode 100644 index c8066e5016..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h +++ /dev/null @@ -1,27 +0,0 @@ -/* BDSP AutoStory Story Tables Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_Tests_H -#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_Tests_H - -#include "Common/Cpp/TestRunners/UnitTest.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - - -// Offline checks of the Segment 00 and 01 tables: their rows in order, -// each checkpoint built and described without a console, none linted. -void add_tests_AutoStory_StoryTables(UnitTestDatabase& database); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp new file mode 100644 index 0000000000..da879b444c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp @@ -0,0 +1,319 @@ +/* BDSP AutoStory - Talks + * + * From: https://github.com/PokemonAutomation/ + * + * Talking to people and fighting the trainers among them, and the looks + * at the screen the story's events decide by. + * + */ + +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "CommonTools/OCR/OCR_Routines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Bag.h" +#include "PokemonBDSP_AutoStory_Battle.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_Talks.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +[[noreturn]] void fail_step(VideoStream& stream, const std::string& message){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, message, stream); +} + +// True if a dialogue opens within `timeout`: someone who stopped us as the +// walk ended reads as BLOCKED, and their box comes up a moment later. +bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Milliseconds timeout){ + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + return wait_until(stream, context, timeout, {{dialog}}) == 0; +} + +bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std::string& label, bool battle_expected, bool doubles){ + ShortDialogDetector dialog; + OverworldDetector overworld; + BattleMenuDetector battle_menu(BattleType::TRAINER); + // Roark's battle menu read as STANDARD, not TRAINER, and nothing fought + // it (r210d): either counts here, as only a trainer's battle can start. + BattleMenuDetector battle_menu_standard(BattleType::STANDARD); + BlackScreenDetector black; + auto quiet_since = std::chrono::steady_clock::now(); + for (size_t check = 0; check < 120; check++){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + if (!frame){ + fail_step(stream, label + ": No video frame."); + } + if (battle_menu.detect(*frame.frame) || battle_menu_standard.detect(*frame.frame) || black.detect(*frame.frame)){ + route_status(stream, "Fighting " + label + "."); + fight_story_battle(stream, context, label, doubles); + // Lines after the battle (Roark's badge, r211d) keep the menu + // from opening: finish them before the Potion. + if (dialogue_opens(stream, context, 3000ms)){ + finish_dialogue(stream, context, BUTTON_B, label + " after the battle"); + } + if (!in_pokemon_center(stream)){ + use_potion_first_pokemon(stream, context, 0.5); + } + return true; + } + const bool talking = dialog.detect(*frame.frame); + if (!talking && overworld.detect(*frame.frame)){ + if (!battle_expected && std::chrono::steady_clock::now() - quiet_since >= 4s){ + route_status(stream, label + " only talked."); + return false; + } + pbf_wait(context, 300ms); + continue; + } + quiet_since = std::chrono::steady_clock::now(); + if (talking){ + pbf_press_button(context, BUTTON_B, 80ms, 1100ms); + }else{ + pbf_wait(context, 300ms); + } + } + fail_step(stream, label + ": neither a battle nor the overworld after the dialogue."); +} + +// A rough check that we stand in a Pokemon Center: its floor fills the lower +// half of the screen with warm yellow, where the gym is grey and routes are +// green. After a battle it tells a loss (a whiteout ends at the counter) +// from a win. +bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent){ + const ImageRGB32 small = screen.scale_to(192, 108); + size_t warm = 0, total = 0; + for (size_t y = 54; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + const int r = (pixel >> 16) & 0xff, g = (pixel >> 8) & 0xff, b = pixel & 0xff; + total++; + if (r > 200 && g > 150 && b < 150 && r - b > 70){ + warm++; + } + } + } + if (percent){ + *percent = warm * 100 / total; + } + return warm * 10 > total * 3; +} + +bool in_pokemon_center(VideoStream& stream){ + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + size_t percent = 0; + const bool inside = center_floor_on_screen(*frame.frame, &percent); + stream.log("[Center check] warm floor " + std::to_string(percent) + " %: " + + (inside ? "in a Pokemon Center." : "not in a Pokemon Center.")); + return inside; +} + +// The Oreburgh Mine's hall and corridors are dim: mean brightness 45 to 64 +// on the console, against 99 to 116 for the city and its yard (2026-10-31). +bool in_dim_cave(VideoStream& stream, uint64_t below){ + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + const ImageRGB32 small = frame.frame->scale_to(192, 108); + uint64_t sum = 0; + for (size_t y = 0; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); + } + } + const uint64_t mean = sum / (3 * 192 * 108); + stream.log("[Cave check] mean brightness " + std::to_string(mean) + "."); + return mean < below; +} + +// Mean of the three channels over a small copy of the frame, 0 to 255. +uint64_t mean_brightness(const ImageViewRGB32& screen){ + const ImageRGB32 small = screen.scale_to(192, 108); + uint64_t sum = 0; + for (size_t y = 0; y < 108; y++){ + for (size_t x = 0; x < 192; x++){ + const uint32_t pixel = small.pixel(x, y); + sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); + } + } + return sum / (3 * 192 * 108); +} + +// Fights a trainer whose dialogue has opened, with whatever starter we +// have. Someone who only talks leaves no battle to fight. +void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name) { + pbf_mash_button(context, BUTTON_B, 5000ms); + context.wait_for_all_requests(); + BattleMenuWatcher battle_menu(BattleType::TRAINER, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + if (wait_until(stream, context, 8s, {{battle_menu}, {battle_dialog}}) < 0) { + stream.log("[Route] " + name + ": no battle followed the talk."); + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + return; + } + fight_trainer(stream, context, name); + context.wait_for_all_requests(); + // The next trainer may be a few tiles on: check HP now. With no + // Potion in the Bag this only looks. + use_potion_first_pokemon(stream, context, 0.5); +} + +bool sign_text_matches(const std::string& text, const std::string& key){ + std::string lower; + for (char ch : text){ + lower += (char)std::tolower((unsigned char)ch); + } + return lower.find(key) != std::string::npos; +} + +namespace{ + +// Where a sign's box shows: the bottom of the screen, as a dialogue's. The +// Lab's is grey, Route 201's green; opening either changes the area far +// more than the overworld behind it moves standing still. +const ImageFloatBox SIGN_BOX(0.19, 0.80, 0.64, 0.18); +// Its text, inside the margins. +const ImageFloatBox SIGN_TEXT(0.22, 0.82, 0.58, 0.15); +constexpr double SIGN_BOX_OPENED = 20.0; + +bool dialogue_showing(VideoStream& stream, ProControllerContext& context){ + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + return screen && ShortDialogDetector().detect(*screen.frame); +} + +// After a talk opened: reads the first box, then presses `next` once +// per line, 1.5 s apart, and only while a box shows: a press after it +// has closed would start the talk over. +void finish_talk(VideoStream& stream, ProControllerContext& context, DialogueRead& read, Button next, const std::string& label){ + // The first box's text, inside its white margins. + // For the log only; a talk is checked by its box opening and closing. + VideoSnapshot screen = stream.video().snapshot(); + if (screen && story_reads_text()){ + read.text = OCR::ocr_read(g_story_language, extract_box_reference(*screen.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + } + stream.log("[Talk] " + label + ": the dialogue reads \"" + read.text + "\"."); + size_t quiet = 0; + // Barry's scene, to the Town Map, took more than 20 (r150a). + for (size_t presses = 0; presses < 80 && quiet < 2; presses++){ + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 1500ms, {{dialog}}) == 0){ + quiet = 0; + pbf_press_button(context, next, 80ms, 0ms); + }else{ + quiet++; + } + } + read.closed = !dialogue_showing(stream, context); + stream.log("[Talk] " + label + (read.closed ? ": closed." : ": still open.")); +} + +} + +DialogueRead talk_facing(VideoStream& stream, ProControllerContext& context, Direction direction, Button next, const std::string& label){ + route_turn(stream, context, direction); + return talk_ahead(stream, context, next, label); +} + +DialogueRead talk_ahead(VideoStream& stream, ProControllerContext& context, Button next, const std::string& label){ + DialogueRead read; + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher opened(COLOR_CYAN); + if (wait_until(stream, context, 2000ms, {{opened}}) != 0){ + stream.log("[Talk] " + label + ": no dialogue opened."); + return read; + } + read.opened = true; + finish_talk(stream, context, read, next, label); + return read; +} + +DialogueRead talk_walking(VideoStream& stream, ProControllerContext& context, Direction direction, size_t tiles, const std::string& label){ + DialogueRead read; + const auto [x, y] = dpad_of(direction); + const bool across = direction == Direction::EAST || direction == Direction::WEST; + const Milliseconds limit = (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN) + 800ms; + const WalkResult walked = walk_to_talk(stream, context, run_from_wild_battle, x, y, limit, label); + if (walked.stop != WalkStop::DIALOG){ + stream.log("[Talk] " + label + ": nobody answered."); + return read; + } + read.opened = true; + finish_talk(stream, context, read, BUTTON_A, label); + return read; +} + +DialogueRead read_sign(VideoStream& stream, ProControllerContext& context, Direction facing, const std::string& label){ + DialogueRead read; + route_turn(stream, context, facing); + // Let the camera settle after the turn: its drift read as a box + // opening (r132a). + pbf_wait(context, 800ms); + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + // The text types out over about a second; B before it has finished + // only finishes it (r131a). + pbf_press_button(context, BUTTON_A, 80ms, 1500ms); + context.wait_for_all_requests(); + VideoSnapshot shown = stream.video().snapshot(); + if (!before || !shown){ + return read; + } + const double opened = walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*shown.frame, SIGN_BOX)); + read.opened = opened > SIGN_BOX_OPENED; + if (!read.opened){ + stream.log("[Sign] " + label + ": no box opened (change " + std::to_string(opened) + ")."); + return read; + } + // Tesseract read the Lab's white-on-grey text as "e R O e IO" (r131a). + // Only to say which sign, when there is a reader for the game's + // language; the box opening and closing is the check. + if (story_reads_text()){ + read.text = OCR::ocr_read(g_story_language, extract_box_reference(*shown.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + } + stream.log("[Sign] " + label + ": reads \"" + read.text + "\" (change " + std::to_string(opened) + ")."); + for (size_t presses = 0; presses < 3 && !read.closed; presses++){ + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + read.closed = after && walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*after.frame, SIGN_BOX)) < SIGN_BOX_OPENED; + } + stream.log("[Sign] " + label + (read.closed ? ": closed." : ": still open.")); + return read; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h new file mode 100644 index 0000000000..30c9c612e6 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h @@ -0,0 +1,92 @@ +/* BDSP AutoStory - Talks + * + * From: https://github.com/PokemonAutomation/ + * + * Talking to people and fighting the trainers among them, and the looks + * at the screen the story's events decide by. Measured on the console + * (segment 01, 2026-10-29 to 10-31; the Jubilife talks, 2026-10-30). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Talks_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Talks_H + +#include +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Fails the step with a screenshot of the screen now. +[[noreturn]] void fail_step(VideoStream& stream, const std::string& message); + +// True if a dialogue opens within `timeout`: someone who stopped us as the +// walk ended reads as BLOCKED, and their box comes up a moment later. +bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Milliseconds timeout); + +// For a dialogue that has opened: B through it. A trainer's ends in a +// battle, which is fought with the best move (Potions when HP runs low), +// followed by a Potion if HP is under half. Anyone else's ends back in the +// overworld. Returns whether there was a battle. Between a trainer's last +// line and the battle the overworld shows with no dialogue for a moment, +// so "only talked" needs 4 s of it, and never comes when `battle_expected`. +// `doubles` for tag and twin battles. A lost battle ends in the last +// Pokemon Center; see in_pokemon_center(). +bool talk_or_fight( + VideoStream& stream, ProControllerContext& context, const std::string& label, + bool battle_expected = false, bool doubles = false +); + +// Fights a trainer whose dialogue has opened, with whatever starter we +// have, then a Potion if HP is under half. Someone who only talks leaves +// no battle to fight. How the story's planned walks fight (RouteTrainerFight). +void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name); + +// A rough check that we stand in a Pokemon Center: its floor fills the lower +// half of the screen with warm yellow, where the gym is grey and routes are +// green. After a battle it tells a loss (a whiteout ends at the counter) +// from a win. `percent`, if given, gets the warm share of the lower half. +bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent = nullptr); +bool in_pokemon_center(VideoStream& stream); +// The Oreburgh Mine's hall and corridors are dim: mean brightness 45 to 64 +// on the console, against 99 to 116 for the city and its yard (2026-10-31). +bool in_dim_cave(VideoStream& stream, uint64_t below = 85); +// Mean of the three channels over a small copy of the frame, 0 to 255. +uint64_t mean_brightness(const ImageViewRGB32& screen); + +// What a talk showed: whether a dialogue opened, whether it had closed +// after, and what OCR read in its first box. +struct DialogueRead{ + bool opened = false; + bool closed = false; + std::string text; +}; +// Whether a sign's OCR text has `key` (lower case), ignoring case. +bool sign_text_matches(const std::string& text, const std::string& key); +// Faces `direction` and presses A. If a dialogue opens, reads its first +// box, then presses `next` once per line, 1.5 s apart, while it shows. +DialogueRead talk_facing(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, Button next, const std::string& label); +// Presses A the way we face, without the turn talk_facing() taps first: a +// turn tap the way we already face is a step. +DialogueRead talk_ahead(VideoStream& stream, ProControllerContext& context, Button next, const std::string& label); +// Walks `direction`, pressing A after each short stretch, for at most +// `tiles` tiles' time until someone answers; then one A per line, 1.5 s +// apart, while the box shows. +DialogueRead talk_walking(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, size_t tiles, const std::string& label); +// Faces `facing` and presses A: whether a box opened at the bottom of +// the screen (a sign's may be green, not a dialogue's), what OCR reads +// in it, and whether B closed it. +DialogueRead read_sign(VideoStream& stream, ProControllerContext& context, Pokemon::Direction facing, const std::string& label); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp deleted file mode 100644 index 047f6ceabf..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp +++ /dev/null @@ -1,2001 +0,0 @@ -/* BDSP AutoStory Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "Pokemon/Navigation/Pokemon_TileGrid.h" -#include "Data/PokemonBDSP_AutoStory_JubilifeCity.h" -#include "Data/PokemonBDSP_AutoStory_OreburghMine.h" -#include "Data/PokemonBDSP_AutoStory_RavagedPath.h" -#include "Data/PokemonBDSP_AutoStory_Route203.h" -#include "Data/PokemonBDSP_AutoStory_Route204.h" -#include "Data/PokemonBDSP_AutoStory_Route204South.h" -#include "Data/PokemonBDSP_AutoStory_RouteRecords.h" -#include "Data/PokemonBDSP_AutoStory_Route201.h" -#include "Data/PokemonBDSP_AutoStory_Route202.h" -#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" -#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "Utils/PokemonBDSP_AutoStory_Location.h" -#include "Utils/PokemonBDSP_AutoStory_NewGame.h" -#include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" -#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h" -#include "PokemonBDSP_AutoStory_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -using Pokemon::Direction; -using Pokemon::TileGrid; -using Pokemon::TileLeg; -using Pokemon::TilePos; -using Pokemon::TileType; - -// A small map with every tile type. -// Row 2 is a ledge with a gap at x = 4. The bottom row has a sign at (1,4), -// an NPC at (3,4) and a door at (5,4); (3,0) is off the map. -const TileGrid& small_grid(){ - static const TileGrid grid({ - R"(#..?..#)", - R"(#.."".#)", - R"(#vvv..#)", - R"(#.....#)", - R"(#S.N.D#)", - }); - return grid; -} - -class Test_TileGridParse : public UnitTest{ -public: - Test_TileGridParse() - : UnitTest("PokemonBDSP::AutoStory - tile grid parse") - {} - - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - const TileGrid& grid = small_grid(); - if (grid.width() != 7 || grid.height() != 5){ - return "Small grid is " + std::to_string(grid.width()) + "x" + std::to_string(grid.height()) + "."; - } - struct Case{ - TilePos pos; - TileType type; - }; - const Case cases[] = { - {{0, 0}, TileType::BLOCKED}, - {{1, 0}, TileType::WALKABLE}, - {{3, 1}, TileType::TALL_GRASS}, - {{2, 2}, TileType::LEDGE}, - {{3, 4}, TileType::NPC}, - {{1, 4}, TileType::SIGN}, - {{5, 4}, TileType::DOOR}, - {{3, 0}, TileType::UNKNOWN}, - {{-1, 0}, TileType::UNKNOWN}, - {{7, 4}, TileType::UNKNOWN}, - }; - for (const Case& c : cases){ - if (grid.at(c.pos) != c.type){ - return "Tile " + Pokemon::to_string(c.pos) + " parsed as " + std::to_string((int)grid.at(c.pos)) + "."; - } - } - for (const std::vector& bad : { - std::vector{"...", ".."}, - std::vector{"..x"}, - }){ - try{ - TileGrid parsed(bad); - return "A malformed grid was accepted."; - }catch (InternalProgramError&){} - } - - if (grid.rows()[4] != "#S.N.D#"){ - return "Small grid row 4 prints as " + grid.rows()[4] + "."; - } - - struct Map{ - const char* name; - const TileGrid& grid; - int width; - int height; - }; - const Map maps[] = { - {MAP_ROUTE_201, Route201::grid(), 64, 25}, - {MAP_SANDGEM_TOWN, SandgemTown::grid(), 34, 32}, - {MAP_ROUTE_202, Route202::grid(), 36, 32}, - }; - for (const Map& map : maps){ - if (map.grid.width() != map.width || map.grid.height() != map.height){ - return std::string(map.name) + " is " + std::to_string(map.grid.width()) + "x" + std::to_string(map.grid.height()) + "."; - } - } - - struct Named{ - const char* map; - TilePos pos; - TileType type; - }; - const Named named[] = { - {MAP_SANDGEM_TOWN, SandgemTown::LAB_DOOR, TileType::DOOR}, - {MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR, TileType::DOOR}, - {MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN, TileType::SIGN}, - {MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR, TileType::DOOR}, - {MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END, TileType::WALKABLE}, - {MAP_ROUTE_202, Route202::CATCH_SPOT, TileType::TALL_GRASS}, - {MAP_ROUTE_202, Route202::SAVE_SPOT, TileType::WALKABLE}, - {MAP_ROUTE_202, Route202::TRAINER_TRISTAN, TileType::NPC}, - {MAP_ROUTE_202, Route202::TRAINER_NATALIE, TileType::NPC}, - {MAP_ROUTE_202, Route202::TRAINER_LOGAN, TileType::NPC}, - {MAP_ROUTE_202, Route202::EXIT_JUBILIFE, TileType::WALKABLE}, - }; - const Pokemon::MapSet& world = story_maps(); - for (const Named& c : named){ - const TilePos pos = world.to_world(c.map, c.pos); - if (world.world().at(pos) != c.type){ - return std::string(c.map) + " tile " + Pokemon::to_string(c.pos) + " is type " + - std::to_string((int)world.world().at(pos)) + " in the joined maps."; - } - const std::optional local = world.to_local(pos); - if (!local || local->map != c.map || local->pos != c.pos){ - return std::string(c.map) + " tile " + Pokemon::to_string(c.pos) + " does not map back."; - } - } - // Sandgem's Poke Mart is next door east of its Center, on the same - // front row: 00.04 walks from the one door to the other. - { - const TilePos center = world.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); - const TilePos mart = world.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); - if (center.y != mart.y || mart.x <= center.x){ - return "The Poke Mart's door is not east of the Pokemon Center's on the same row."; - } - for (int x = center.x; x <= mart.x; x++){ - if (!world.world().standable({x, mart.y + 1})){ - return "The row below the Center's and the Mart's doors is not open at " + world.describe({x, mart.y + 1}) + "."; - } - } - } - - // The maps join where their exits meet: the grids are the game's cells. - struct Join{ - const char* from_map; - TilePos from; - Direction direction; - const char* to_map; - TilePos to; - }; - const Join joins[] = { - {MAP_ROUTE_201, {63, 4}, Direction::EAST, MAP_SANDGEM_TOWN, {0, 11}}, - {MAP_ROUTE_201, {63, 6}, Direction::EAST, MAP_SANDGEM_TOWN, {0, 13}}, - {MAP_SANDGEM_TOWN, {24, 0}, Direction::NORTH, MAP_ROUTE_202, {25, 31}}, - {MAP_SANDGEM_TOWN, {29, 0}, Direction::NORTH, MAP_ROUTE_202, {30, 31}}, - }; - for (const Join& join : joins){ - const TilePos from = world.to_world(join.from_map, join.from); - const std::optional next = world.world().step(from, join.direction); - if (!next || *next != world.to_world(join.to_map, join.to)){ - return "No step from " + world.describe(from) + " to " + join.to_map + " " + Pokemon::to_string(join.to) + "."; - } - } - Pokemon::MapSet overlapping; - overlapping.add("a", small_grid(), {0, 0}); - overlapping.add("b", small_grid(), {6, 4}); // shares one blocked tile - try{ - overlapping.add("c", small_grid(), {5, 4}); - return "Maps that overlap on a door were accepted."; - }catch (InternalProgramError&){} - return true; - } -}; - -class Test_TileGridWallHit : public UnitTest{ -public: - Test_TileGridWallHit() - : UnitTest("PokemonBDSP::AutoStory - tile grid wall hit") - {} - - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - struct Case{ - const TileGrid& grid; - TilePos from; - Direction direction; - TilePos stop; - size_t tiles; - bool anchored; - }; - const TileGrid& small = small_grid(); - const TileGrid& route = Route202::grid(); - const Case cases[] = { - {small, {1, 1}, Direction::EAST, {5, 1}, 4, true}, // through grass to the east wall - {small, {1, 0}, Direction::SOUTH, {1, 3}, 3, true}, // hops the ledge, stops at the sign - {small, {2, 3}, Direction::NORTH, {2, 3}, 0, true}, // a ledge stops a north walk - {small, {3, 3}, Direction::SOUTH, {3, 3}, 0, true}, // the NPC is a wall - {small, {4, 4}, Direction::NORTH, {4, 0}, 4, false}, // through the gap, off the top edge - {small, {4, 3}, Direction::EAST, {5, 3}, 1, true}, - {small, {4, 4}, Direction::EAST, {4, 4}, 0, false}, // into the door - {small, {2, 0}, Direction::EAST, {2, 0}, 0, false}, // off the map - {route, Route202::CATCH_SPOT, Direction::WEST, {3, 27}, 16, true}, - {route, Route202::CATCH_SPOT, Direction::NORTH, {19, 25}, 2, true}, - {route, Route202::CATCH_SPOT, Direction::EAST, {30, 27}, 11, true}, - // Measured on the console from the post-catch save. - {route, {30, 27}, Direction::NORTH, {30, 24}, 3, true}, - {route, {30, 24}, Direction::WEST, {22, 24}, 8, true}, - {route, Route202::TRAINER_TRISTAN.next(Direction::SOUTH), Direction::NORTH, {7, 14}, 0, true}, // Tristan holds the player - // After Natalie, as measured in r75: on the row right above the south - // ledge the east wall is at column 31; column 30 is open up to row 4. - {route, {22, 22}, Direction::EAST, {30, 22}, 8, true}, - {route, {30, 22}, Direction::NORTH, {30, 4}, 18, true}, - // Through the gap east of the north ledge, over the south ledge - // and off the bottom edge to Sandgem Town. - {route, {29, 4}, Direction::SOUTH, {29, 31}, 27, false}, - {route, {12, 8}, Direction::SOUTH, {12, 29}, 21, true}, // hops both ledges - {route, Route202::EXIT_JUBILIFE, Direction::NORTH, Route202::EXIT_JUBILIFE, 0, false}, - }; - auto check = [](const Case& c) -> std::string{ - const Pokemon::WallHit hit = Pokemon::walk_to_wall(c.grid, c.from, c.direction); - if (hit.stop != c.stop || hit.tiles != c.tiles || hit.anchored != c.anchored){ - return "Walking " + std::string(Pokemon::direction_name(c.direction)) + " from " + - Pokemon::to_string(c.from) + " stops at " + Pokemon::to_string(hit.stop) + " after " + - std::to_string(hit.tiles) + " tiles" + (hit.anchored ? "" : " (not anchored)") + - ", expected " + Pokemon::to_string(c.stop) + " after " + std::to_string(c.tiles) + "."; - } - return ""; - }; - for (const Case& c : cases){ - const std::string error = check(c); - if (!error.empty()){ - return error; - } - } - // Sandgem Town on the joined maps (00.04): the front row of the - // Center and the Mart runs east to the trees past the Mart, and the - // Mart's door is a warp, not a wall. - const Pokemon::MapSet& maps = story_maps(); - const TileGrid& world = maps.world(); - auto town = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; - const TilePos below_mart = town(SandgemTown::POKE_MART_DOOR.next(Direction::SOUTH)); - const TilePos row_end = town(SandgemTown::FRONT_ROW_EAST_END); - const Case sandgem[] = { - {world, maps.to_world(MAP_ROUTE_202, {22, 24}), Direction::SOUTH, maps.to_world(MAP_ROUTE_202, {22, 31}), 7, true}, // under the ledge, down to Sandgem's trees (a timed count said 5) - {world, town(SandgemTown::POKEMON_CENTER_DOOR.next(Direction::SOUTH)), Direction::EAST, row_end, 12, true}, - {world, below_mart, Direction::EAST, row_end, 2, true}, - {world, below_mart, Direction::NORTH, below_mart, 0, false}, - {world, row_end, Direction::NORTH, row_end, 0, true}, - }; - for (const Case& c : sandgem){ - const std::string error = check(c); - if (!error.empty()){ - return error; - } - } - return true; - } -}; - -// Plans from `from` to `goal` allowing open legs a tile short or long, and -// logs the legs with every map tile each may end on. -std::string check_robust_plan( - Logger& logger, const std::string& name, const Pokemon::MapSet& maps, - const std::vector& from, const std::function& goal, - const Pokemon::RobustPlanCosts& costs = Pokemon::RobustPlanCosts() -){ - const auto legs = Pokemon::plan_robust_route(maps.world(), from, goal, costs); - if (!legs){ - return name + ": no robust route found."; - } - std::string text = name + " (robust): from"; - for (const TilePos& pos : from){ - text += " " + maps.describe(pos); - } - for (const Pokemon::PlannedLeg& leg : *legs){ - text += "\n " + Pokemon::to_string(leg.leg) + " to"; - for (const TilePos& pos : leg.after){ - text += " " + maps.describe(pos); - } - } - logger.log(text); - const std::vector& end = legs->empty() ? from : legs->back().after; - for (const TilePos& pos : end){ - if (!goal(pos)){ - return name + ": the robust plan may end on " + maps.describe(pos) + "."; - } - } - return ""; -} - -// Checks that no leg of a robust plan from `from` to `goal` can stop in tall -// grass, where a walk cannot read that it has stopped (the sway keeps the -// screen changing): a leg that only runs out of time there would be taken -// as an anchor. -std::string check_plan_stops_readable( - const std::string& name, const Pokemon::MapSet& maps, - const std::vector& from, const std::function& goal, - const Pokemon::RobustPlanCosts& costs = Pokemon::RobustPlanCosts() -){ - const auto legs = Pokemon::plan_robust_route(maps.world(), from, goal, costs); - if (!legs){ - return name + ": no robust route found."; - } - for (const Pokemon::PlannedLeg& leg : *legs){ - if (leg.leg.ends_at_wall && !route_wall_readable(maps.world(), leg.after)){ - return name + ": " + Pokemon::to_string(leg.leg) + " ends on a wall in tall grass, where a stop cannot be read."; - } - } - return ""; -} - -// Checks that no leg of a robust plan from `from` to `goal` can walk into a -// door, from any tile the leg before may have left us on, even if an open -// leg runs on to twice its length plus two: r123a's legs in Sandgem ran -// past where the grid said and the last one entered a house. -std::string check_plan_avoids_doors( - const std::string& name, const Pokemon::MapSet& maps, - const std::vector& from, const std::function& goal, - const Pokemon::RobustPlanCosts& costs = Pokemon::RobustPlanCosts() -){ - const TileGrid& grid = maps.world(); - const auto legs = Pokemon::plan_robust_route(grid, from, goal, costs); - if (!legs){ - return name + ": no robust route found."; - } - std::vector starts = from; - for (const Pokemon::PlannedLeg& planned : *legs){ - const TileLeg& leg = planned.leg; - const size_t reach = leg.ends_at_wall ? (size_t)grid.width() + grid.height() : leg.tiles * 2 + 2; - for (TilePos pos : starts){ - for (size_t i = 0; i < reach; i++){ - const TilePos next = pos.next(leg.direction); - if (grid.at(next) == TileType::DOOR){ - return name + ": " + Pokemon::to_string(leg) + " may walk into the door at " + maps.describe(next) + "."; - } - const std::optional step = grid.step(pos, leg.direction); - if (!step){ - break; - } - pos = *step; - } - } - starts = planned.after; - } - return ""; -} - -// Checks a plan from `from` to `to`: every leg can be walked, ledges are -// crossed only southward, and each leg's wall flag matches the grid. -// With `maps`, legs are printed with the map tile they end on, and with -// the trainers whose sight they cross. -std::string check_plan( - Logger& logger, const std::string& name, - const TileGrid& grid, const TilePos& from, const TilePos& to, - const Pokemon::MapSet* maps = nullptr, - const std::vector& trainers = {} -){ - const std::optional> legs = Pokemon::plan_tile_route(grid, from, to); - if (!legs){ - return name + ": no route found."; - } - auto describe = [&](const TilePos& pos){ - return maps ? maps->describe(pos) : Pokemon::to_string(pos); - }; - const std::vector> seen = Pokemon::legs_crossing_sight(grid, from, *legs, trainers); - std::string text = name + ": from " + describe(from); - TilePos end = from; - for (size_t i = 0; i < legs->size(); i++){ - const TileLeg& leg = (*legs)[i]; - end = Pokemon::trace_legs(grid, end, {leg}).back(); - text += "\n " + Pokemon::to_string(leg) + " to " + describe(end); - for (const std::string& trainer : seen[i]){ - text += ", crosses " + trainer + "'s sight"; - } - } - logger.log(text); - - std::vector tiles; - try{ - tiles = Pokemon::trace_legs(grid, from, *legs); - }catch (InternalProgramError& e){ - return name + ": " + e.message(); - } - if (tiles.back() != to){ - return name + ": the legs end at " + Pokemon::to_string(tiles.back()) + "."; - } - for (size_t i = 1; i < tiles.size(); i++){ - const TilePos& a = tiles[i - 1]; - const TilePos& b = tiles[i]; - if (!grid.standable(b)){ - return name + ": the route stands on " + Pokemon::to_string(b) + "."; - } - const int dx = b.x - a.x; - const int dy = b.y - a.y; - if (std::abs(dx) + std::abs(dy) == 1){ - continue; - } - if (dx != 0 || dy != 2 || grid.at(a.next(Direction::SOUTH)) != TileType::LEDGE){ - return name + ": jump from " + Pokemon::to_string(a) + " to " + Pokemon::to_string(b) + " is not a south ledge hop."; - } - } - TilePos pos = from; - for (const TileLeg& leg : *legs){ - if (leg.tiles == 0){ - return name + ": empty leg."; - } - pos = Pokemon::trace_legs(grid, pos, {leg}).back(); - if (leg.ends_at_wall != grid.at_wall(pos, leg.direction)){ - return name + ": leg " + Pokemon::to_string(leg) + " has the wrong wall flag."; - } - } - for (size_t i = 1; i < legs->size(); i++){ - if ((*legs)[i].direction == (*legs)[i - 1].direction){ - return name + ": two legs in a row go " + Pokemon::direction_name((*legs)[i].direction) + "."; - } - } - return ""; -} - -class Test_TileGridPlans : public UnitTest{ -public: - Test_TileGridPlans() - : UnitTest("PokemonBDSP::AutoStory - tile grid plans") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const TileGrid& small = small_grid(); - - // South over the ledge is a straight hop; back north must use the gap. - std::string error = check_plan(logger, "Small south", small, {2, 0}, {2, 4}); - if (!error.empty()){ - return error; - } - error = check_plan(logger, "Small north", small, {2, 4}, {2, 0}); - if (!error.empty()){ - return error; - } - const auto south = Pokemon::plan_tile_route(small, {2, 0}, {2, 4}); - if (south->size() != 1 || south->front().direction != Direction::SOUTH){ - return "Small south should be one leg over the ledge."; - } - - // Walled in by ledges from below. - const TileGrid pit({ - R"(#.#)", - R"(#v#)", - R"(#.#)", - }); - if (Pokemon::plan_tile_route(pit, {1, 2}, {1, 0})){ - return "A route north over a ledge was found."; - } - - // 00.04 after the catch: every leg it walks, in order. - const Pokemon::MapSet& maps = story_maps(); - const std::vector trainers = story_trainer_sights(); - const TilePos catch_spot = maps.to_world(MAP_ROUTE_202, Route202::CATCH_SPOT); - const TilePos jubilife = maps.to_world(MAP_ROUTE_202, Route202::EXIT_JUBILIFE); - error = check_plan(logger, "Catch spot to Jubilife", maps.world(), catch_spot, jubilife, &maps, trainers); - if (!error.empty()){ - return error; - } - // 00.04's walks in order, from where the step before may leave us. - const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); - const std::vector grass = story_after_training_area(); - const TilePos below_sign = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH)); - const TilePos below_mart = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR.next(Direction::SOUTH)); - const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END); - // From the trees east of the Mart, 00.04 walks six tiles west, three - // off at most, and north (north_through_sign_gap()): from every - // column it may reach, north passes no door, and only the gap - // between the signs leads out of town. - std::vector above_sign_gap; - for (int x = row_end.x - 11; x <= row_end.x - 3; x++){ - const TilePos pos{x, row_end.y}; - const Pokemon::WallHit hit = Pokemon::walk_to_wall(maps.world(), pos, Direction::NORTH); - if (maps.world().at(hit.wall) == TileType::DOOR){ - return "North from " + maps.describe(pos) + " walks into a door."; - } - const std::vector gap = SandgemTown::below_sign_gap(); - const bool in_gap = std::find(gap.begin(), gap.end(), maps.to_local(pos)->pos) != gap.end(); - if (in_gap != (hit.tiles > 2)){ - return "North from " + maps.describe(pos) + " goes " + std::to_string(hit.tiles) + " tiles."; - } - if (in_gap){ - above_sign_gap.push_back(hit.stop); - } - } - // The walks that start or end in the catch grass must stop in it; - // the Sandgem walks (`readable`) never need to. Each is planned with - // the costs the program plans it with: walk_route()'s route costs - // for 00.04's walks, the Sandgem Center events' default costs for - // walk() and town costs for walk_in_town(). The planner gives up - // after costs.max_searched sets, and the costs decide whether a - // walk gets a plan: after training, 00.04 walks to Sandgem's entry - // with the default costs (the route costs find none within the cap), - // then to the Center with the town costs, checked leg by leg below. - const struct{ - const char* name; - std::vector from; - std::function goal; - bool readable; - Pokemon::RobustPlanCosts costs; - } walks[] = { - {"Save spots to the catch grass", story_save_spots(), - [&](const TilePos& pos){ return std::find(grass.begin(), grass.end(), pos) != grass.end(); }, false, route_plan_costs()}, - {"After training to Sandgem's entry", story_after_training_area(), - [&](const TilePos& pos){ return pos == sandgem_entry_stop(); }, false, Pokemon::RobustPlanCosts()}, - // The Mart's door takes a tile either side: enter_lit_door() lines up by sight. - {"Pokemon Center to the Poke Mart", story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), - [&](const TilePos& pos){ return pos.y == below_mart.y && std::abs(pos.x - below_mart.x) <= 1; }, true, route_plan_costs()}, - {"Poke Mart to the trees east of it", story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR), - [&](const TilePos& pos){ return pos == row_end; }, true, route_plan_costs()}, - {"North of the gap between the signs to the save spot", above_sign_gap, - [&](const TilePos& pos){ return pos == save_spot; }, true, route_plan_costs()}, - // Step 5 starts on the save spot (see tutorial_part_3_after_catch_navigation()). - // From both save spots at once the route costs find no plan within the - // cap: exact open legs move the pair in step without joining it. - {"Save spot to Jubilife", {save_spot}, story_at_jubilife_exit, false, route_plan_costs()}, - // The Sandgem Center test checkpoint: south into town, then to the Center's sign. - {"Save spot to Sandgem Town", {save_spot}, - [&](const TilePos& pos){ return pos == sandgem_entry_stop(); }, true, Pokemon::RobustPlanCosts()}, - {"Sandgem's entry to the Pokemon Center", {sandgem_entry_stop()}, - [&](const TilePos& pos){ return pos == below_sign; }, true, town_plan_costs()}, - // The Mart lady test checkpoint: out of the Center to the tile west - // of Route 201's border sign, from the lady back to it, and back. The - // sign's row has no wall west of it: a step onto it, then east to it. - // These legs are walked by hand (walk_until_stopped), so the plan is - // only a check of the grid; the one-tile steps need the town costs, - // the default costs find no plan. - {"Pokemon Center to west of the Route 201 sign", story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), - [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, - {"Poke Mart lady to west of the Route 201 sign", {maps.to_world(MAP_ROUTE_201, Route201::POTION_GIVER_TALK_FROM)}, - [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, - {"Route 201 sign to the Pokemon Center", {route201_sign_west()}, - [&](const TilePos& pos){ return pos == below_sign; }, true, Pokemon::RobustPlanCosts()}, - // The Center walk's re-anchor at the trees east of the Mart. - {"Trees east of the Mart to the Pokemon Center", {maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END)}, - [&](const TilePos& pos){ return pos == below_sign; }, true, Pokemon::RobustPlanCosts()}, - // From and back to where the Route 202 checkpoint ends: the doors' - // row by the signs' gap; westward it first walks to the Lab's sign. - {"East of the Lab's sign to west of the Route 201 sign", {east_of_lab_sign()}, - [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, - {"Below the Center's sign to the trees east of the Mart", {below_sandgem_center_sign()}, - [&](const TilePos& pos){ return pos == row_end; }, true, Pokemon::RobustPlanCosts()}, - {"East of the Lab's sign to the trees east of the Mart", {east_of_lab_sign()}, - [&](const TilePos& pos){ return pos == row_end; }, true, Pokemon::RobustPlanCosts()}, - // Its re-anchor after a wild battle in the lady's grass. - {"East end of the lady's row to the Route 201 sign", {route201_row_3_east_end()}, - [&](const TilePos& pos){ return pos == route201_sign_west(); }, false, town_plan_costs()}, - }; - for (const auto& walk : walks){ - error = check_robust_plan(logger, walk.name, maps, walk.from, walk.goal, walk.costs); - if (!error.empty()){ - return error; - } - error = check_plan_avoids_doors(walk.name, maps, walk.from, walk.goal, walk.costs); - if (!error.empty()){ - return error; - } - if (walk.readable){ - error = check_plan_stops_readable(walk.name, maps, walk.from, walk.goal, walk.costs); - if (!error.empty()){ - return error; - } - } - } - - // In town, short open legs are cheap: from where the walk south stops, - // the plan is the user's (2026-10-30): west to the wall, a tile east - // into the gap's column, straight south through the gap between the - // signs, a tile west, then north until the Center's sign stops us - // (r134a: "south 5" went 6; seven rows on the game's grid). Not round the town. - { - const auto legs = Pokemon::plan_robust_route(maps.world(), {sandgem_entry_stop()}, - [&](const TilePos& pos){ return pos == below_sign; }, town_plan_costs()); - std::string read; - for (const Pokemon::PlannedLeg& planned : legs ? *legs : std::vector()){ - read += " [" + Pokemon::to_string(planned.leg) + "]"; - } - const std::vector> expected{ - {Direction::WEST, 9}, {Direction::EAST, 1}, {Direction::SOUTH, 7}, {Direction::WEST, 1}, {Direction::NORTH, 2}, - }; - bool same = legs && legs->size() == expected.size(); - for (size_t i = 0; same && i < expected.size(); i++){ - same = (*legs)[i].leg.direction == expected[i].first && (*legs)[i].leg.tiles == expected[i].second; - } - if (!same){ - return "Sandgem's entry to below the Center's sign, town costs: expected west 9, east 1, south 7, west 1, north 2; planned" + - (read.empty() ? std::string(" nothing") : read) + "."; - } - logger.log("Sandgem's entry to below the Center's sign, town costs:" + read); - } - - // The walk south from the save spot keeps out of the tall grass, so - // no wild battle can stop it. - { - const auto legs = Pokemon::plan_robust_route(maps.world(), {save_spot}, sandgem_entry_stop()); - if (!legs){ - return "Save spot to Sandgem Town: no robust route found."; - } - std::vector starts{save_spot}; - for (const Pokemon::PlannedLeg& planned : *legs){ - for (TilePos pos : starts){ - for (size_t i = 0; i < planned.leg.tiles; i++){ - const std::optional step = maps.world().step(pos, planned.leg.direction); - if (!step){ - break; - } - pos = *step; - if (maps.world().at(pos) == TileType::TALL_GRASS){ - return "Save spot to Sandgem Town: " + Pokemon::to_string(planned.leg) + - " crosses tall grass at " + maps.describe(pos) + "."; - } - } - } - starts = planned.after; - } - } - - // 00.05 in Jubilife: every leg the segment walks, from where the one - // before it may have left us, with route_plan_costs() as talk_to_npc() plans. - { - using namespace JubilifeCity; - const Pokemon::MapSet& town = jubilife_maps(); - auto at = [&](const TilePos& pos){ return town.to_world(MAP_JUBILIFE_CITY, pos); }; - std::vector dawn; - for (const TilePos& pos : up_the_steps()){ - dawn.push_back(at(pos)); - } - const TilePos school = at(TRAINERS_SCHOOL_DOOR.next(Direction::SOUTH)); - const TilePos center = at(POKEMON_CENTER_DOOR.next(Direction::SOUTH)); - const std::vector out_of_school{school.next(Direction::WEST), school, school.next(Direction::EAST)}; - const TilePos president = at(POKETCH_PRESIDENT).next(Direction::EAST); - const TilePos clown_mart = at(CLOWN_MART).next(Direction::SOUTH); - const TilePos clown_tv = at(CLOWN_TV_STATION).next(Direction::EAST); - const TilePos clown_company = at(CLOWN_POKETCH_COMPANY).next(Direction::EAST); - // A door's goal is its front row within a tile of its column; an - // NPC's is the one tile beside them. 00.05 walks them with - // walk_route()'s route costs (talk_to_npc()), which the test - // plans with too: on the whole city the town costs find no plan - // for four of them within the planner's cap. The walk from the - // steps is made by hand, so its plan is only a check of the grid. - const struct{ const char* name; std::vector from; TilePos to; bool door; Pokemon::RobustPlanCosts costs; } legs[] = { - {"Jubilife: after Dawn to the Trainers' School", dawn, school, true, town_plan_costs()}, - {"Jubilife: school to the president", out_of_school, president, false, route_plan_costs()}, - {"Jubilife: president to the clown by the Mart", {president}, clown_mart, false, route_plan_costs()}, - {"Jubilife: Mart clown to the TV station clown", {clown_mart}, clown_tv, false, route_plan_costs()}, - {"Jubilife: TV station clown to the Poketch Company clown", {clown_tv}, clown_company, false, route_plan_costs()}, - {"Jubilife: Poketch Company clown to the president", {clown_company}, president, false, route_plan_costs()}, - {"Jubilife: president to the Pokemon Center", {president}, center, true, route_plan_costs()}, - }; - std::string failed; - // 01.03 in the Oreburgh Mine: the corridor's top to below Roark, and - // back, both walk_route() with its route costs. The way in finds no - // plan with them within the planner's cap and walk_route() falls back - // to the default costs, so that is the plan that runs; the way back - // plans with the route costs, and only those stay within the cap. - { - const Pokemon::MapSet& mine = oreburgh_mine_maps(); - auto m = [&](const TilePos& pos){ return mine.to_world(MAP_OREBURGH_MINE, pos); }; - std::vector top; - for (const TilePos& pos : OreburghMine::corridor_top()){ - top.push_back(m(pos)); - } - const TilePos below_roark = m(OreburghMine::ROARK).next(Direction::SOUTH); - const TilePos roark = m(OreburghMine::ROARK); - std::string leg_error = check_robust_plan(logger, "Mine: corridor top to the stretch east of Roark", mine, top, - [&](const TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }); - failed += leg_error.empty() ? "" : leg_error + " "; - const TilePos exit = m(OreburghMine::CORRIDOR_EXIT_START); - leg_error = check_robust_plan(logger, "Mine: below Roark to the corridor's corner", mine, {below_roark}, - [&](const TilePos& pos){ return pos == exit; }, route_plan_costs()); - failed += leg_error.empty() ? "" : leg_error + " "; - } - // 01.05 in the Ravaged Path: from the south door to a rock across the - // corridor, and once it is smashed, to the tile above the way out. - { - const Pokemon::MapSet cave = ravaged_path_maps(); - for (const TilePos& rock : RavagedPath::rocks()){ - if (cave.world().at(rock) != Pokemon::TileType::BLOCKED){ - failed += "Ravaged Path: rock " + cave.describe(rock) + " is not blocked. "; - } - } - const std::vector& before = RavagedPath::before_rocks(); - std::string leg_error = check_robust_plan(logger, "Ravaged Path: south door to a rock", cave, {RavagedPath::ENTRANCE}, - [&](const TilePos& pos){ return std::find(before.begin(), before.end(), pos) != before.end(); }); - failed += leg_error.empty() ? "" : leg_error + " "; - for (const TilePos& from : before){ - const TilePos rock = from.next(Direction::EAST); - const Pokemon::MapSet open = ravaged_path_maps({rock}); - leg_error = check_robust_plan(logger, "Ravaged Path: after smashing " + open.describe(rock) + ", to the corridor's east end", open, {from}, - [&](const TilePos& pos){ return pos == RavagedPath::EAST_END; }); - failed += leg_error.empty() ? "" : leg_error + " "; - } - // What 01.05 plans: from either tile, with both rocks open. - std::vector both; - for (const TilePos& from : before){ - both.push_back(from.next(Direction::EAST)); - } - leg_error = check_robust_plan(logger, "Ravaged Path: from either rock's tile to the corridor's east end", ravaged_path_maps(both), before, - [&](const TilePos& pos){ return pos == RavagedPath::EAST_END; }); - failed += leg_error.empty() ? "" : leg_error + " "; - if (!(RavagedPath::EAST_END.next(Direction::WEST) == RavagedPath::ABOVE_EXIT) || - cave.world().at(RavagedPath::ABOVE_EXIT.next(Direction::SOUTH)) != Pokemon::TileType::DOOR - ){ - failed += "Ravaged Path: the way out is not a door a tile west of the corridor's east end. "; - } - } - // 01.06 on Route 203, then Jubilife's north-east corner: from the - // Oreburgh Gate's west exit to its road near Jubilife, and from - // Jubilife's east stairs to the foot of its north road. - { - std::string leg_error = check_robust_plan(logger, "Route 203: Oreburgh Gate to row 18", route203_maps(), - Route203::gate_exit(), on_route203_west_line); - failed += leg_error.empty() ? "" : leg_error + " "; - leg_error = check_robust_plan(logger, "Jubilife: east stairs to the north road", jubilife_east_maps(), - jubilife_east_entrance(), on_jubilife_north_road, town_plan_costs()); - failed += leg_error.empty() ? "" : leg_error + " "; - } - // 01.08 on Route 204's south half: from the top of Jubilife's - // street to the tile below the Ravaged Path's door. - { - // Planned as the walker plans it: the sight lines it keeps - // out of are off the map. - Pokemon::TileGrid grid = Route204South::grid(); - for (const TilePos& pos : Route204South::tiles_to_avoid()){ - grid.set(pos, Pokemon::TileType::UNKNOWN); - } - Pokemon::MapSet route; - route.add(MAP_ROUTE_204_SOUTH, grid, {0, 0}); - const std::string leg_error = check_robust_plan(logger, "Route 204 South: Jubilife to the Ravaged Path door", route, - Route204South::jubilife_entry(), [](const TilePos& pos){ return pos == Route204South::BELOW_DOOR; }, town_plan_costs()); - failed += leg_error.empty() ? "" : leg_error + " "; - for (const Pokemon::TrainerSight& trainer : Route204South::trainer_sights()){ - if (route.world().at(trainer.pos) != Pokemon::TileType::NPC){ - failed += "Route 204 South: " + trainer.name + " is not on an NPC tile. "; - } - } - } - for (const auto& leg : legs){ - const TilePos to = leg.to; - const bool door = leg.door; - const std::string leg_error = check_robust_plan(logger, leg.name, town, leg.from, - [&](const TilePos& pos){ - return door ? pos.y == to.y && pos.x >= to.x - 1 && pos.x <= to.x + 1 : pos == to; - }, leg.costs); - failed += leg_error.empty() ? "" : leg_error + " "; - } - if (!failed.empty()){ - return failed; - } - } - return true; - } -}; - -class Test_IntroDecisions : public UnitTest{ -public: - Test_IntroDecisions() - : UnitTest("PokemonBDSP::AutoStory - new-game intro decisions") - {} - - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - struct Case{ - const char* screen; - IntroProgress progress; - IntroStep expected; - }; - const IntroProgress start{false, false}; - const IntroProgress named{true, false}; - const IntroProgress done{true, true}; - const Case cases[] = { - {"user-select", start, IntroStep::SELECT_USER}, - {"home", done, IntroStep::FAIL_NOT_IN_GAME}, - {"keyboard", start, IntroStep::ENTER_PLAYER_NAME}, - {"keyboard", named, IntroStep::CANCEL_RIVAL_KEYBOARD}, - {"rival-list-tell", named, IntroStep::MOVE_TO_PRESET}, - {"rival-list-preset", named, IntroStep::PICK_PRESET}, - {"menu", done, IntroStep::CLOSE_MENU_AND_FINISH}, - // The appearance grid has its prompt in a dialogue box. - {"dialog", start, IntroStep::ADVANCE}, - {"dialog", done, IntroStep::ADVANCE}, - // Language and the appearance preview: no X before the bedroom is possible. - {"other", start, IntroStep::ADVANCE}, - {"other", named, IntroStep::ADVANCE}, - {"other", done, IntroStep::PROBE_FOR_MENU}, - }; - for (const Case& c : cases){ - const IntroStep actual = decide_intro_step(c.screen, c.progress); - if (actual != c.expected){ - return std::string("Screen \"") + c.screen + "\" (player named " + - (c.progress.player_named ? "yes" : "no") + ", rival named " + - (c.progress.rival_named ? "yes" : "no") + "): step " + - std::to_string((int)actual) + ", expected " + std::to_string((int)c.expected) + "."; - } - } - if (advance_after_probe(true)){ - return "A after X changed the screen would pick from whatever X opened."; - } - if (!advance_after_probe(false)){ - return "A must advance a screen X left unchanged."; - } - return true; - } -}; - -} - - -// The walk to the Poke Mart may be stopped short by someone in the way, and -// is then made again from the tiles the doorway on screen puts us on, give -// 01.10 on Route 204's north half: the line a human walked (2026-10-31), -// corner to corner, from the Ravaged Path's exit into Floaroma. Each -// stretch must plan as the one straight tap leg the human took, on the -// grid as it is: a detour here means the grid or the line changed. -class Test_Route204NorthLine : public UnitTest{ -public: - Test_Route204NorthLine() - : UnitTest("PokemonBDSP::AutoStory - Route 204 North: the line into Floaroma") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const Pokemon::MapSet& route = route204_north_maps(); - const std::vector waypoints = Route204::floaroma_waypoints(); - struct Expected{ Direction direction; size_t tiles; }; - const std::vector expected{ - {Direction::SOUTH, 1}, {Direction::WEST, 9}, {Direction::NORTH, 5}, {Direction::EAST, 8}, - {Direction::NORTH, 10}, {Direction::WEST, 8}, {Direction::NORTH, 7}, - }; - if (waypoints.size() != expected.size()){ - return "Route 204 North: " + std::to_string(waypoints.size()) + " waypoints, expected " + std::to_string(expected.size()) + "."; - } - std::vector at{Route204::CAVE_EXIT}; - for (size_t i = 0; i < waypoints.size(); i++){ - const TilePos goal = waypoints[i]; - const std::string name = "Route 204 North stretch " + std::to_string(i + 1) + " to " + route.describe(goal); - const auto plan = Pokemon::plan_robust_route(route.world(), at, - [&](const TilePos& pos){ return pos == goal; }, waypoint_plan_costs()); - if (!plan){ - return name + ": no plan."; - } - std::string text = name + ":"; - for (const Pokemon::PlannedLeg& leg : *plan){ - text += " " + to_string(leg.leg); - } - logger.log(text); - if (plan->size() != 1){ - return name + ": " + std::to_string(plan->size()) + " legs, expected the one straight leg."; - } - const Pokemon::TileLeg& leg = (*plan)[0].leg; - if (leg.ends_at_wall || leg.direction != expected[i].direction || leg.tiles != expected[i].tiles){ - return name + ": planned " + to_string(leg) + ", expected an open leg of " + - std::to_string(expected[i].tiles) + " tiles."; - } - if ((*plan)[0].after.size() != 1 || !((*plan)[0].after[0] == goal)){ - return name + ": the leg does not end exactly on the waypoint."; - } - at = {goal}; - } - if (!Route204::in_floaroma_path(at[0])){ - return "Route 204 North: the last waypoint is not in the path into Floaroma."; - } - for (const Pokemon::TrainerSight& trainer : Route204::north_trainer_sights()){ - if (route.world().at(trainer.pos) != Pokemon::TileType::NPC){ - return "Route 204 North: " + trainer.name + " is not on an NPC tile."; - } - } - return true; - } -}; - -// Every walk a checkpoint makes without a plan is written down as legs -// (Data/PokemonBDSP_AutoStory_RouteRecords). A record with a start tile on -// a grid whose rows are the game's is traced leg by leg: counted legs stay -// on standable tiles, a leg to a wall finds one, a leg to a person or a -// door ends in front of one. The others are checked for shape and logged -// as documented, not traced, with why. -class Test_RouteRecords : public UnitTest{ -public: - Test_RouteRecords() - : UnitTest("PokemonBDSP::AutoStory - route records") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - size_t traced = 0, documented = 0; - for (const RouteRecords::RouteRecord& record : RouteRecords::all()){ - const std::string name = record.checkpoint + " " + record.title; - if (record.legs.empty() || record.source.empty() || record.title.empty()){ - return name + ": a record needs legs, a title and a source."; - } - for (const RouteRecords::RouteLeg& leg : record.legs){ - if (leg.end == RouteRecords::LegEnd::COUNT && leg.tiles == 0){ - return name + ": a counted leg of 0 tiles."; - } - if (!leg.exact && leg.end != RouteRecords::LegEnd::COUNT){ - return name + ": only a counted leg can be an estimate."; - } - } - const TileGrid* grid = record.grid ? record.grid() : nullptr; - if (!record.traceable || grid == nullptr || !record.start){ - logger.log(name + ": documented, not traced (" + - (grid == nullptr ? "no grid" : !record.traceable ? "grid rows not the game's" : "no start tile") + ")."); - documented++; - continue; - } - TilePos pos = *record.start; - if (!grid->standable(pos)){ - return name + ": the start tile is not standable."; - } - for (size_t i = 0; i < record.legs.size(); i++){ - const RouteRecords::RouteLeg& leg = record.legs[i]; - const std::string leg_name = name + " leg " + std::to_string(i + 1); - switch (leg.end){ - case RouteRecords::LegEnd::COUNT: - for (size_t k = 0; k < leg.tiles; k++){ - pos = pos.next(leg.direction); - if (!grid->standable(pos)){ - return leg_name + ": tile " + std::to_string(k + 1) + " is not standable."; - } - } - break; - case RouteRecords::LegEnd::WALL:{ - const Pokemon::WallHit hit = Pokemon::walk_to_wall(*grid, pos, leg.direction); - if (hit.tiles == 0){ - return leg_name + ": already at the wall."; - } - if (leg.tiles != 0 && hit.tiles != leg.tiles){ - return leg_name + ": the wall is " + std::to_string(hit.tiles) + " tiles away, the record says " + std::to_string(leg.tiles) + "."; - } - pos = hit.stop; - break; - } - case RouteRecords::LegEnd::NPC: - case RouteRecords::LegEnd::DOOR:{ - const Pokemon::WallHit hit = Pokemon::walk_to_wall(*grid, pos, leg.direction); - pos = hit.stop; - const Pokemon::TileType next = grid->at(pos.next(leg.direction)); - const Pokemon::TileType wanted = leg.end == RouteRecords::LegEnd::NPC ? Pokemon::TileType::NPC : Pokemon::TileType::DOOR; - if (next != wanted){ - return leg_name + ": does not end in front of " + (wanted == Pokemon::TileType::NPC ? "a person." : "a door."); - } - break; - } - case RouteRecords::LegEnd::BANNER: - case RouteRecords::LegEnd::TRANSITION: - case RouteRecords::LegEnd::SCENE: - // Leaves the map or the game takes over: nothing to trace. - break; - } - } - size_t estimates = 0; - for (const RouteRecords::RouteLeg& leg : record.legs){ - estimates += leg.exact ? 0 : 1; - } - logger.log(name + ": traced " + std::to_string(record.legs.size()) + " legs" + - (estimates ? ", " + std::to_string(estimates) + " of them stick-timed estimates" : "") + "."); - traced++; - } - logger.log("Route records: " + std::to_string(traced) + " traced, " + std::to_string(documented) + " documented only."); - return true; - } -}; - -// or take one each way: from every such block of tiles south of the Mart, -// a robust plan to the row below its door must exist. -class Test_PokeMartShortWalks : public UnitTest{ -public: - Test_PokeMartShortWalks() - : UnitTest("PokemonBDSP::AutoStory - walks short of the Poke Mart") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const Pokemon::MapSet& maps = story_maps(); - const TileGrid& grid = maps.world(); - const TilePos front = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR.next(Direction::SOUTH)); - const auto at_front = [&](const TilePos& pos){ return pos.y == front.y && std::abs(pos.x - front.x) <= 1; }; - // The open ground south of the Mart, to the trees east of town and - // the fence rows below. - for (int y = front.y + 1; y <= front.y + 7; y++){ - for (int x = front.x - 6; x <= front.x + 2; x++){ - std::vector block; - for (int dy = -1; dy <= 1; dy++){ - for (int dx = -1; dx <= 1; dx++){ - if (grid.standable({x + dx, y + dy})){ - block.push_back({x + dx, y + dy}); - } - } - } - if (block.empty()){ - continue; - } - const std::string error = check_robust_plan(logger, "Short of the Poke Mart about " + maps.describe({x, y}), - maps, block, at_front); - if (!error.empty()){ - return error; - } - } - } - return true; - } -}; - - -// The Mart's doorway on the failure screenshots of r121a and r122a, kept in -// the unit test resources (the CommandLineTests checkout): where it is -// relative to the player, who is in the middle of the screen. -class Test_LitDoorwayScreenshots : public UnitTest{ -public: - Test_LitDoorwayScreenshots() - : UnitTest("PokemonBDSP::AutoStory - lit doorway screenshots") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - struct Case{ - const char* file; - double tiles_across_min; - double tiles_across_max; - double rows_up_min; - double rows_up_max; - }; - const Case cases[] = { - // r121a: below-left of the door, which is part of a tile east, straight ahead. - {"PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", 0.2, 0.7, 0.5, 1.5}, - // r122a: a tile west of the door and three or four rows south of its front. - {"PokemonBDSP/AutoStory/SandgemMartDoorFarLeft-20261030-024648.png", -1.3, -0.5, 2.5, 4.5}, - }; - for (const Case& c : cases){ - const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; - if (!Filesystem::is_regular_file(path)){ - return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); - } - const ImageRGB32 screen(path); - const std::optional doorway = find_lit_doorway(screen); - if (!doorway){ - return std::string(c.file) + ": no lit doorway found."; - } - logger.log(std::string(c.file) + ": doorway at " + std::to_string(doorway->x) + ", " + - std::to_string(doorway->y) + ": " + std::to_string(doorway->tiles_across) + " tiles across, " + - std::to_string(doorway->rows_up) + " rows up."); - if (doorway->tiles_across < c.tiles_across_min || doorway->tiles_across > c.tiles_across_max){ - return std::string(c.file) + ": doorway " + std::to_string(doorway->tiles_across) + - " tiles across, expected " + std::to_string(c.tiles_across_min) + " to " + - std::to_string(c.tiles_across_max) + "."; - } - if (doorway->rows_up < c.rows_up_min || doorway->rows_up > c.rows_up_max){ - return std::string(c.file) + ": doorway " + std::to_string(doorway->rows_up) + - " rows up, expected " + std::to_string(c.rows_up_min) + " to " + - std::to_string(c.rows_up_max) + "."; - } - } - return true; - } -}; - - -// The save prompt r124a took beside Sandgem's Poke Mart, sure it stood on -// Route 202 (in CommandLineTests under PokemonBDSP/AutoStory): its card -// names Sandgem Town, which the save must read before confirming. -class Test_SaveScreenLocation : public UnitTest{ -public: - Test_SaveScreenLocation() - : UnitTest("PokemonBDSP::AutoStory - save screen location") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const std::string path = UNIT_TEST_RESOURCE_PATH() + "PokemonBDSP/AutoStory/SaveScreenSandgemTown-20261030-034444.png"; - if (!Filesystem::is_regular_file(path)){ - return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); - } - const std::string read = read_save_screen_location(ImageRGB32(path)); - logger.log("Save screen location read: \"" + read + "\"."); - if (!location_banner_matches(read, MAP_SANDGEM_TOWN)){ - return "Save screen location \"" + read + "\" should match Sandgem Town."; - } - if (location_banner_matches(read, MAP_ROUTE_202)){ - return "Save screen location \"" + read + "\" should not match Route 202."; - } - return true; - } -}; - - -// The Route 202 to Sandgem Center checkpoint's events against a scripted -// game: where the player is, and what goes wrong on the way. -class FakeSandgemCenter : public SandgemCenterActions{ -public: - enum class Where{ ROUTE_202, SANDGEM_ENTRY, SHORT_OF_DOOR, BELOW_SIGN, DOOR_FRONT, LOST, MART_ROW }; - - std::string save_location = "Route 202"; - // Walks to the Center that end three rows short (someone in the way). - size_t short_walks = 0; - // Walks that a townsperson's talk stops, with the dialogue left open. - size_t talking_walks = 0; - // Walks to the Center that end lost in town (a wall leg short, r127a). - size_t lost_walks = 0; - Where where = Where::ROUTE_202; - bool dialogue = false; - std::vector trace; - - virtual std::string save_screen_location() override{ - trace.push_back("read save screen"); - return save_location; - } - virtual Pokemon::Outcome walk( - std::vector& at, const std::function& goal, const std::string& label - ) override{ - trace.push_back("walk: " + label); - if (talking_walks > 0){ - talking_walks--; - dialogue = true; - return Pokemon::outcome_failed("walk_failed", "a dialogue no trainer explains"); - } - if (goal(sandgem_entry_stop())){ - at = {sandgem_entry_stop()}; - where = Where::SANDGEM_ENTRY; - return Pokemon::outcome_ok(); - } - if (goal(below_sandgem_center_sign())){ - if (lost_walks > 0){ - lost_walks--; - where = Where::LOST; - return Pokemon::outcome_failed("walk_failed", "ran out of time short of the wall"); - } - at = {below_sandgem_center_sign()}; - where = Where::BELOW_SIGN; - if (short_walks > 0){ - short_walks--; - where = Where::SHORT_OF_DOOR; - } - return Pokemon::outcome_ok(); - } - return Pokemon::outcome_failed("no_plan", label); - } - virtual bool east_then_south_into_sandgem() override{ - trace.push_back("east, then south"); - const bool entered = where == Where::ROUTE_202; - where = Where::SANDGEM_ENTRY; - return entered; - } - virtual Pokemon::Outcome walk_in_town( - std::vector& at, const std::function& goal, const std::string& label - ) override{ - return walk(at, goal, label); - } - virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ - trace.push_back("read: " + label); - if (where == Where::BELOW_SIGN && facing == Direction::NORTH){ - return DialogueRead{.opened = true, .closed = true, .text = "Pokemon Center"}; - } - return DialogueRead{}; - } - virtual std::optional lit_doorway() override{ - switch (where){ - case Where::BELOW_SIGN: - return LitDoorway{.x = 0.3, .y = 0.38, .tiles_across = -3.0, .rows_up = 1.0}; - case Where::SHORT_OF_DOOR: - return LitDoorway{.x = 0.3, .y = 0.14, .tiles_across = -3.0, .rows_up = 3.0}; - case Where::DOOR_FRONT: - return LitDoorway{.x = 0.5, .y = 0.38, .tiles_across = 0.1, .rows_up = 1.0}; - case Where::MART_ROW: - return LitDoorway{.x = 0.37, .y = 0.38, .tiles_across = -2.0, .rows_up = 1.0}; - default: - return std::nullopt; - } - } - virtual Pokemon::Outcome line_up_with_door() override{ - trace.push_back("line up"); - if (where != Where::BELOW_SIGN){ - return Pokemon::outcome_failed("not_lined_up", "no lit doorway on screen"); - } - where = Where::DOOR_FRONT; - return Pokemon::outcome_ok(); - } - virtual bool inside_pokemon_center() override{ - return false; - } - virtual Pokemon::Outcome east_then_north_in_sandgem() override{ - trace.push_back("east, then north"); - where = Where::MART_ROW; - return Pokemon::outcome_ok(); - } - virtual bool dialogue_showing() override{ - return dialogue; - } - virtual Pokemon::Outcome finish_dialogue() override{ - trace.push_back("finish dialogue"); - dialogue = false; - return Pokemon::outcome_ok(); - } -}; - -class Test_SandgemCenterEvents : public UnitTest{ -public: - Test_SandgemCenterEvents() - : UnitTest("PokemonBDSP::AutoStory - Sandgem Center events") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const std::string SOUTH = "walk: Route 202 to Sandgem Town"; - const std::string ROW = "walk: To below the Center's sign"; - const std::string READ = "read: the Center's sign"; - struct Case{ - const char* name; - std::string save_location; - size_t short_walks; - size_t talking_walks; - size_t lost_walks; - bool completed; - size_t failed_event; - std::vector trace; - }; - const Case cases[] = { - { - .name = "Straight through", - .save_location = "Route 202", - .short_walks = 0, - .talking_walks = 0, - .lost_walks = 0, - .completed = true, - .failed_event = 0, - .trace = {"read save screen", SOUTH, ROW, READ}, - }, - { - .name = "The save is in Sandgem Town", - .save_location = "Sandgem Town", - .short_walks = 0, - .talking_walks = 0, - .lost_walks = 0, - .completed = false, - .failed_event = 1, - .trace = {"read save screen", "read save screen"}, - }, - { - .name = "A row off: the sign does not read", - .save_location = "Route 202", - .short_walks = 1, - .talking_walks = 0, - .lost_walks = 0, - .completed = false, - .failed_event = 4, - .trace = {"read save screen", SOUTH, ROW, READ}, - }, - { - .name = "A talk stops the walk south", - .save_location = "Route 202", - .short_walks = 0, - .talking_walks = 1, - .lost_walks = 0, - .completed = true, - .failed_event = 0, - .trace = {"read save screen", SOUTH, "finish dialogue", "east, then south", ROW, READ}, - }, - { - .name = "The walk in town fails", - .save_location = "Route 202", - .short_walks = 0, - .talking_walks = 0, - .lost_walks = 1, - .completed = false, - .failed_event = 3, - .trace = {"read save screen", SOUTH, ROW}, - }, - }; - for (const Case& c : cases){ - FakeSandgemCenter game; - game.save_location = c.save_location; - game.short_walks = c.short_walks; - game.talking_walks = c.talking_walks; - game.lost_walks = c.lost_walks; - Pokemon::Checkpoint checkpoint = route202_to_sandgem_center_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(checkpoint); - if (!problems.empty()){ - return std::string(c.name) + ": lint: " + problems.front(); - } - std::string log_text; - Pokemon::EventLogger log = [&](const std::string& line){ log_text += "\n " + line; }; - Pokemon::RunState state; - const Pokemon::CheckpointReport report = Pokemon::run_checkpoint(state, checkpoint, log); - logger.log(std::string(c.name) + ":" + log_text); - if (report.completed != c.completed || report.failed_event != c.failed_event){ - return std::string(c.name) + ": expected " + (c.completed ? "completed" : "failed") + - " at event " + std::to_string(c.failed_event) + ", read " + (report.completed ? "completed" : "failed") + - " at event " + std::to_string(report.failed_event) + ":" + log_text; - } - if (game.trace != c.trace){ - std::string expected, read; - for (const std::string& step : c.trace){ - expected += " [" + step + "]"; - } - for (const std::string& step : game.trace){ - read += " [" + step + "]"; - } - return std::string(c.name) + ": expected steps" + expected + ", read" + read + "."; - } - if (c.completed && game.where != FakeSandgemCenter::Where::BELOW_SIGN){ - return std::string(c.name) + ": completed, but not on the doors' row."; - } - } - return true; - } -}; - - -// The two shots around r126's door walk: below the Center's sign, outside, -// and on the entrance mat inside, where the lit floor read as a doorway and -// the walk kept lining up with it (runs 2 and 3). -class Test_PokemonCenterInside : public UnitTest{ -public: - Test_PokemonCenterInside() - : UnitTest("PokemonBDSP::AutoStory - Pokemon Center inside") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - struct Case{ - const char* file; - bool inside; - bool doorway; - }; - const Case cases[] = { - {.file = "PokemonBDSP/AutoStory/SandgemCenterInside-20261030-053555.png", .inside = true, .doorway = false}, - // r127c: walked in by the turn north at the door, facing north. - {.file = "PokemonBDSP/AutoStory/SandgemCenterInsideFacingNorth-r127c.png", .inside = true, .doorway = false}, - {.file = "PokemonBDSP/AutoStory/SandgemCenterBelowSign-20261030-053527.png", .inside = false, .doorway = true}, - {.file = "PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", .inside = false, .doorway = true}, - {.file = "PokemonBDSP/AutoStory/SandgemMartDoorFarLeft-20261030-024648.png", .inside = false, .doorway = true}, - // Jubilife (2026-10-30): inside its Center twice, at its door, and - // on the doorstep after walking out (the banner up). - {.file = "PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", .inside = true, .doorway = false}, - {.file = "PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", .inside = true, .doorway = false}, - {.file = "PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", .inside = false, .doorway = true}, - {.file = "PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", .inside = false, .doorway = true}, - }; - for (const Case& c : cases){ - const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; - if (!Filesystem::is_regular_file(path)){ - return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); - } - const ImageRGB32 screen(path); - const bool inside = pokemon_center_inside(screen); - const std::optional doorway = find_lit_doorway(screen); - logger.log(std::string(c.file) + ": inside " + std::to_string(inside) + ", doorway " + - (doorway ? std::to_string(doorway->tiles_across) + " across, " + std::to_string(doorway->rows_up) + " rows up" : "none")); - if (inside != c.inside){ - return std::string(c.file) + ": expected " + (c.inside ? "inside" : "outside") + - " a Pokemon Center, read " + (inside ? "inside" : "outside") + "."; - } - if (doorway.has_value() != c.doorway){ - return std::string(c.file) + ": expected " + (c.doorway ? "a doorway" : "no doorway") + - ", read " + (doorway ? "one" : "none") + "."; - } - } - return true; - } -}; - - -// The Poke Mart lady checkpoint against a scripted game: where the player -// stands along the route measured on the console. -class FakeMartLady : public MartLadyActions{ -public: - enum class Where{ - DOORS_ROW, SHORT_OF_LAB, LAB_EAST, PATH_WEST, GRASS_EDGE, TOP_EDGE, LADY, - SIGNS_ROW, SHORT_OF_201, ROUTE_201_WEST, PATH_EAST, MART_TREES_LOW, MART_TREES, ROUTE_202, - }; - - // Walks back east that a wild battle ends short of the sign. - size_t short_walks = 0; - // Walks west along the doors' row that a townsperson ends short of the Lab's sign. - size_t short_lab_walks = 0; - // No text reader for the game's language: boxes open and close, unread. - bool unread = false; - // Talks to the lady before she is there to answer. - size_t lady_away = 0; - Where where = Where::DOORS_ROW; - std::vector trace; - - virtual std::string save_screen_location() override{ return ""; } - virtual Pokemon::Outcome walk( - std::vector&, const std::function&, const std::string& label - ) override{ - trace.push_back("walk: " + label); - return Pokemon::outcome_failed("no_plan", label); - } - virtual bool east_then_south_into_sandgem() override{ return false; } - virtual Pokemon::Outcome walk_in_town( - std::vector&, const std::function&, const std::string& label - ) override{ - trace.push_back("walk: " + label); - return Pokemon::outcome_failed("no_plan", label); - } - virtual std::optional lit_doorway() override{ return std::nullopt; } - virtual Pokemon::Outcome line_up_with_door() override{ return Pokemon::outcome_failed("no_plan", "not on this checkpoint"); } - virtual bool inside_pokemon_center() override{ return false; } - virtual Pokemon::Outcome east_then_north_in_sandgem() override{ return Pokemon::outcome_ok(); } - virtual bool dialogue_showing() override{ return false; } - virtual Pokemon::Outcome finish_dialogue() override{ return Pokemon::outcome_ok(); } - virtual Pokemon::Outcome walk_until_stopped(Direction direction, size_t, const std::string& label) override{ - trace.push_back("go: " + label); - const struct{ Where from; Direction direction; Where to; } walks[] = { - {Where::DOORS_ROW, Direction::WEST, Where::LAB_EAST}, - {Where::SHORT_OF_LAB, Direction::WEST, Where::LAB_EAST}, - {Where::PATH_WEST, Direction::WEST, Where::GRASS_EDGE}, - {Where::GRASS_EDGE, Direction::NORTH, Where::TOP_EDGE}, - {Where::LADY, Direction::WEST, Where::TOP_EDGE}, - {Where::TOP_EDGE, Direction::WEST, Where::TOP_EDGE}, - {Where::SIGNS_ROW, Direction::EAST, Where::ROUTE_201_WEST}, - {Where::SHORT_OF_201, Direction::EAST, Where::ROUTE_201_WEST}, - {Where::PATH_EAST, Direction::EAST, Where::MART_TREES_LOW}, - {Where::MART_TREES_LOW, Direction::NORTH, Where::MART_TREES}, - }; - for (const auto& walk : walks){ - if (walk.from == where && walk.direction == direction){ - where = walk.to; - if (where == Where::ROUTE_201_WEST && short_walks > 0 && walk.from == Where::SIGNS_ROW){ - short_walks--; - where = Where::SHORT_OF_201; - } - if (where == Where::LAB_EAST && short_lab_walks > 0 && walk.from == Where::DOORS_ROW){ - short_lab_walks--; - where = Where::SHORT_OF_LAB; - } - break; - } - } - return Pokemon::outcome_ok(); - } - virtual Pokemon::Outcome step(Direction direction, const std::string& label) override{ - trace.push_back("step: " + label); - if (direction != Direction::SOUTH){ - return Pokemon::outcome_ok(); - } - if (where == Where::LAB_EAST){ - where = Where::PATH_WEST; - }else if (where == Where::LADY || where == Where::TOP_EDGE){ - where = Where::SIGNS_ROW; - }else if (where == Where::ROUTE_201_WEST){ - where = Where::PATH_EAST; - } - return Pokemon::outcome_ok(); - } - virtual DialogueRead talk(Direction, Button, const std::string& label) override{ - trace.push_back("talk: " + label); - return DialogueRead{}; - } - virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ - trace.push_back("read: " + label); - if (where == Where::LAB_EAST && facing == Direction::WEST){ - return DialogueRead{.opened = true, .closed = true, .text = unread ? "" : "Pokemon Research Lab"}; - } - if (where == Where::ROUTE_201_WEST && facing == Direction::EAST){ - return DialogueRead{.opened = true, .closed = true, .text = unread ? "" : "Route 201 Sandgem Town"}; - } - return DialogueRead{}; - } - virtual DialogueRead talk_walking(Direction direction, size_t, const std::string& label) override{ - trace.push_back("talk: " + label); - if (direction != Direction::EAST || (where != Where::TOP_EDGE && where != Where::LADY)){ - return DialogueRead{}; - } - where = Where::LADY; - if (lady_away > 0){ - lady_away--; - return DialogueRead{}; - } - return DialogueRead{.opened = true, .closed = true, .text = unread ? "" : "Did you know"}; - } - virtual Pokemon::Outcome walk_to_route_202_save_spot() override{ - trace.push_back("to Route 202"); - if (where != Where::MART_TREES){ - return Pokemon::outcome_failed("wrong_place", "to Route 202"); - } - where = Where::ROUTE_202; - return Pokemon::outcome_ok(); - } - virtual Pokemon::Outcome save_on_route_202() override{ - trace.push_back("save"); - return where == Where::ROUTE_202 ? Pokemon::outcome_ok() : Pokemon::outcome_failed("wrong_place", "save"); - } -}; - -class Test_MartLadyEvents : public UnitTest{ -public: - Test_MartLadyEvents() - : UnitTest("PokemonBDSP::AutoStory - Mart lady events") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const std::vector TO_LADY = {"go: West to the Lab's sign", "read: the Lab's sign", - "step: South onto the path", "go: West along the path", "go: North to the trees"}; - const std::vector LADY = {"talk: Poke Mart lady"}; - const std::vector RETRY = {"go: West to the grass's edge"}; - const std::vector TO_SIGN = {"step: South onto the signs' row", "go: East to the Route 201 sign"}; - const std::vector SIGN = {"read: Route 201's sign"}; - const std::vector HOME = {"step: South onto the path", "go: East along the path", - "go: North to the front row", "to Route 202", "save"}; - const auto join = [](std::initializer_list> parts){ - std::vector all; - for (const std::vector& part : parts){ - all.insert(all.end(), part.begin(), part.end()); - } - return all; - }; - const std::vector TO_LAB = {"go: West to the Lab's sign", "read: the Lab's sign"}; - const std::vector LAB_TO_LADY = {"step: South onto the path", "go: West along the path", "go: North to the trees"}; - struct Case{ - const char* name; - size_t short_walks; - size_t short_lab_walks = 0; - size_t lady_away; - bool unread = false; - bool completed; - size_t failed_event; - std::vector trace; - }; - const Case cases[] = { - { - .name = "Straight through", - .short_walks = 0, - .short_lab_walks = 0, - .lady_away = 0, - .unread = false, - .completed = true, - .failed_event = 0, - .trace = join({TO_LADY, LADY, TO_SIGN, SIGN, HOME}), - }, - { - .name = "Back east, a tile short of the sign", - .short_walks = 1, - .short_lab_walks = 0, - .lady_away = 0, - .unread = false, - .completed = true, - .failed_event = 0, - .trace = join({TO_LADY, LADY, TO_SIGN, SIGN, {"go: On to the sign"}, SIGN, HOME}), - }, - { - .name = "West, a tile short of the Lab's sign: re-anchored on it", - .short_walks = 0, - .short_lab_walks = 1, - .lady_away = 0, - .unread = false, - .completed = true, - .failed_event = 0, - .trace = join({TO_LAB, {"go: On to the sign", "read: the Lab's sign"}, LAB_TO_LADY, LADY, TO_SIGN, SIGN, HOME}), - }, - { - .name = "No text reader for the game's language: boxes open and close, unread", - .short_walks = 1, - .short_lab_walks = 1, - .lady_away = 0, - .unread = true, - .completed = true, - .failed_event = 0, - .trace = join({TO_LAB, {"go: On to the sign", "read: the Lab's sign"}, LAB_TO_LADY, LADY, TO_SIGN, SIGN, {"go: On to the sign"}, SIGN, HOME}), - }, - { - .name = "The lady answers the second time", - .short_walks = 0, - .short_lab_walks = 0, - .lady_away = 1, - .unread = false, - .completed = true, - .failed_event = 0, - .trace = join({TO_LADY, LADY, RETRY, LADY, TO_SIGN, SIGN, HOME}), - }, - { - .name = "The lady never answers", - .short_walks = 0, - .short_lab_walks = 0, - .lady_away = 5, - .unread = false, - .completed = false, - .failed_event = 6, - .trace = join({TO_LADY, LADY, RETRY, LADY, RETRY, LADY}), - }, - }; - for (const Case& c : cases){ - FakeMartLady game; - game.short_walks = c.short_walks; - game.short_lab_walks = c.short_lab_walks; - game.unread = c.unread; - game.lady_away = c.lady_away; - Pokemon::Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(checkpoint); - if (!problems.empty()){ - return std::string(c.name) + ": lint: " + problems.front(); - } - std::string log_text; - Pokemon::EventLogger log = [&](const std::string& line){ log_text += "\n " + line; }; - Pokemon::RunState state; - const Pokemon::CheckpointReport report = Pokemon::run_checkpoint(state, checkpoint, log); - logger.log(std::string(c.name) + ":" + log_text); - if (report.completed != c.completed || report.failed_event != c.failed_event){ - return std::string(c.name) + ": expected " + (c.completed ? "completed" : "failed") + - " at event " + std::to_string(c.failed_event) + ", read " + (report.completed ? "completed" : "failed") + - " at event " + std::to_string(report.failed_event) + ":" + log_text; - } - if (game.trace != c.trace){ - std::string expected, read; - for (const std::string& step : c.trace){ - expected += " [" + step + "]"; - } - for (const std::string& step : game.trace){ - read += " [" + step + "]"; - } - return std::string(c.name) + ": expected steps" + expected + ", read" + read + "."; - } - } - return true; - } -}; - - -// 00.05a and 00.05b against a scripted game: the door is `door_at` tiles -// west of the Center's doorstep. -class FakeJubilife : public JubilifeActions{ -public: - size_t door_at = 10; - bool stuck_on_house = false; - bool heal_stays_inside = false; - bool inside = false; - size_t column = 0; // tiles west of the doorstep - bool on_doorstep = false; - bool in_school = false; - std::vector trace; - - virtual std::string save_screen_location() override{ return ""; } - virtual Pokemon::Outcome walk(std::vector&, const std::function&, const std::string& label) override{ - return Pokemon::outcome_failed("no_plan", label); - } - virtual bool east_then_south_into_sandgem() override{ return false; } - virtual Pokemon::Outcome walk_in_town(std::vector&, const std::function&, const std::string& label) override{ - return Pokemon::outcome_failed("no_plan", label); - } - // The Poketch Company's sign reads after `sign_reads_after` tries. - size_t sign_reads_after = 1; - size_t sign_tries = 0; - virtual DialogueRead read_sign(Direction, const std::string& label) override{ - trace.push_back("sign: " + label); - if (label == "the Poketch Company's sign" && ++sign_tries >= sign_reads_after){ - return DialogueRead{.opened = true, .closed = true, .text = "The Poketch Company"}; - } - return {}; - } - virtual std::optional lit_doorway() override{ return std::nullopt; } - virtual Pokemon::Outcome line_up_with_door() override{ return Pokemon::outcome_ok(); } - virtual bool inside_pokemon_center() override{ return inside; } - virtual bool indoors() override{ return inside; } - virtual Pokemon::Outcome hold(Direction, int64_t, const std::string& label) override{ - trace.push_back("hold: " + label); - return Pokemon::outcome_ok(); - } - bool president_talks = true; - virtual DialogueRead walk_until_talked_to(size_t, const std::string& label) override{ - trace.push_back("meet: " + label); - return president_talks ? DialogueRead{.opened = true, .closed = true, .text = "Oh?"} : DialogueRead{}; - } - virtual Pokemon::Outcome east_then_north_in_sandgem() override{ return Pokemon::outcome_ok(); } - virtual bool dialogue_showing() override{ return false; } - virtual Pokemon::Outcome finish_dialogue() override{ return Pokemon::outcome_ok(); } - virtual Pokemon::Outcome walk_until_stopped(Direction, size_t, const std::string& label) override{ - trace.push_back("go: " + label); - return stuck_on_house && label == "East to the house" - ? Pokemon::outcome_failed("walk_failed", label) - : Pokemon::outcome_ok(); - } - virtual DialogueRead talk(Direction, Button, const std::string& label) override{ - trace.push_back("talk: " + label); - if (label == "Clown by the Poke Mart"){ - return DialogueRead{.opened = true, .closed = true, .text = "Hi!"}; - } - if (label == "Barry" && barry_answers){ - return DialogueRead{.opened = true, .closed = true, .text = "Barry"}; - } - if (label == "Clown by the Poketch Company" && ++clown_asks >= clown_answers_at){ - return DialogueRead{.opened = true, .closed = true, .text = "Hi!"}; - } - return {}; - } - virtual Pokemon::Outcome step(Direction direction, const std::string& label) override{ - trace.push_back("step: " + label); - (void)direction; - return Pokemon::outcome_ok(); - } - // The clown answers the `clown_answers_at`th ask. - size_t clown_answers_at = 7; - size_t clown_asks = 0; - virtual DialogueRead talk_walking(Direction, size_t, const std::string& label) override{ - trace.push_back("talk walking: " + label); - return {}; - } - virtual Pokemon::Outcome walk_to_route_202_save_spot() override{ return Pokemon::outcome_ok(); } - virtual Pokemon::Outcome save_on_route_202() override{ return Pokemon::outcome_ok(); } - virtual Pokemon::Outcome climb_the_steps() override{ - trace.push_back("climb"); - return Pokemon::outcome_ok(); - } - virtual Pokemon::Outcome heal_at_center() override{ - trace.push_back("heal"); - inside = heal_stays_inside; - on_doorstep = true; - column = 0; - return Pokemon::outcome_ok(); - } - virtual void turn(Direction direction) override{ - trace.push_back(std::string("turn ") + Pokemon::direction_name(direction)); - } - // The lamp: off by `lamp_off` tiles after the counted walk, or unlit. - std::optional lamp_off; - virtual std::optional school_door_across() override{ - if (!lamp_off){ - return std::nullopt; - } - return (double)door_at - (double)column; - } - virtual void step_across(double tiles, const std::string&) override{ - column += (size_t)std::lround(tiles); - } - bool barry_answers = true; - bool left_school = false; - virtual Pokemon::Outcome leave_south(const std::string&) override{ - left_school = true; - return Pokemon::outcome_ok(); - } - // Half tiles: a nudge east then west lands back on the column. - int half = 0; - virtual void nudge(Direction direction, const std::string&) override{ - half += direction == Direction::EAST ? -1 : 1; - if (half == 2){ column++; half = 0; } - if (half == -2){ column--; half = 0; } - } - virtual Pokemon::Outcome tile(Direction direction, const std::string&) override{ - if (direction == Direction::WEST){ - column++; - } - return Pokemon::outcome_ok(); - } - virtual void close_panel(const std::string& label) override{ - trace.push_back("close: " + label); - } - // The president answers the `president_answers_at`th ask ahead. - size_t president_answers_at = 5; - size_t president_asks = 0; - virtual DialogueRead talk_ahead(Button, const std::string& label) override{ - trace.push_back("ask: " + label); - if (label == "The Poketch Company president" && ++president_asks >= president_answers_at){ - return DialogueRead{.opened = true, .closed = true, .text = "Bravo!"}; - } - return {}; - } - virtual Pokemon::Outcome taps(Direction direction, size_t tiles, const std::string& label) override{ - trace.push_back("taps: " + label); - if (direction == Direction::WEST){ - column += tiles; - } - return Pokemon::outcome_ok(); - } - virtual bool through_door_north(const std::string&) override{ - trace.push_back("north at " + std::to_string(column)); - in_school = column == door_at; - return in_school; - } -}; - -class Test_JubilifeEvents : public UnitTest{ -public: - Test_JubilifeEvents() - : UnitTest("PokemonBDSP::AutoStory - Jubilife 00.05a/b events") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - struct Case{ const char* name; size_t door_at; bool stuck; bool stays_inside; bool center_done; bool school_done; size_t tries; bool lamp = false; }; - const Case cases[] = { - {"Door eleven tiles west, as measured", 11, false, false, true, true, 1}, - {"Door a whole tile off: a map error, not found", 12, false, false, true, false, 4}, - {"Door off by two: not found", 14, false, false, true, false, 4}, - {"The house never stops us", 11, true, false, false, false, 0}, - {"The heal leaves us inside (r143a)", 11, false, true, false, false, 0}, - {"A tile off, the lamp lit: it lines us up", 12, false, false, true, true, 1, true}, - {"Two tiles off, the lamp lit: it lines us up", 9, false, false, true, true, 1, true}, - }; - for (const Case& c : cases){ - FakeJubilife game; - game.door_at = c.door_at; - game.stuck_on_house = c.stuck; - game.heal_stays_inside = c.stays_inside; - if (c.lamp){ - game.lamp_off = 0.0; - } - for (size_t which = 0; which < 2; which++){ - Pokemon::Checkpoint checkpoint = which == 0 ? jubilife_center_checkpoint(game) : jubilife_school_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(checkpoint); - if (!problems.empty()){ - return std::string(c.name) + ": lint: " + problems.front(); - } - } - Pokemon::EventLogger log = [&](const std::string& line){ logger.log(line); }; - Pokemon::RunState center_state; - Pokemon::Checkpoint center = jubilife_center_checkpoint(game); - const bool center_done = Pokemon::run_checkpoint(center_state, center, log).completed; - bool school_done = false; - if (center_done){ - Pokemon::RunState school_state; - Pokemon::Checkpoint school = jubilife_school_checkpoint(game); - school_done = Pokemon::run_checkpoint(school_state, school, log).completed; - } - if (school_done){ - Pokemon::RunState barry_state; - Pokemon::Checkpoint barry = jubilife_barry_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(barry); - if (!problems.empty()){ - return std::string(c.name) + ": 00.05c lint: " + problems.front(); - } - if (!Pokemon::run_checkpoint(barry_state, barry, log).completed || !game.left_school){ - return std::string(c.name) + ": 00.05c did not complete."; - } - } - if (school_done){ - Pokemon::RunState president_state; - Pokemon::Checkpoint president = jubilife_president_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(president); - if (!problems.empty()){ - return std::string(c.name) + ": 00.05d lint: " + problems.front(); - } - if (!Pokemon::run_checkpoint(president_state, president, log).completed){ - return std::string(c.name) + ": 00.05d did not complete."; - } - } - if (school_done){ - Pokemon::RunState clown_state; - Pokemon::Checkpoint clown = jubilife_clown_mart_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(clown); - if (!problems.empty()){ - return std::string(c.name) + ": 00.05e lint: " + problems.front(); - } - if (!Pokemon::run_checkpoint(clown_state, clown, log).completed){ - return std::string(c.name) + ": 00.05e did not complete."; - } - } - if (school_done){ - Pokemon::RunState clown_state; - Pokemon::Checkpoint clown = jubilife_clown_company_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(clown); - if (!problems.empty()){ - return std::string(c.name) + ": 00.05f lint: " + problems.front(); - } - if (!Pokemon::run_checkpoint(clown_state, clown, log).completed){ - return std::string(c.name) + ": 00.05f did not complete."; - } - } - if (school_done){ - Pokemon::RunState poketch_state; - Pokemon::Checkpoint poketch = jubilife_poketch_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(poketch); - if (!problems.empty()){ - return std::string(c.name) + ": 00.05g lint: " + problems.front(); - } - if (!Pokemon::run_checkpoint(poketch_state, poketch, log).completed){ - return std::string(c.name) + ": 00.05g did not complete."; - } - } - size_t tries = 0; - for (const std::string& step : game.trace){ - tries += step.rfind("north at", 0) == 0; - } - if (center_done != c.center_done || school_done != c.school_done || (school_done && tries != c.tries)){ - return std::string(c.name) + ": center " + (center_done ? "done" : "failed") + - ", school " + (school_done ? "done" : "failed") + ", " + std::to_string(tries) + " tries north."; - } - } - return true; - } -}; - - -// 00.05f: a tile at a time along the hedges asking north, then waiting in -// the gap beside the sign until the wandering clown steps next to us. -class Test_JubilifeClownCompany : public UnitTest{ -public: - Test_JubilifeClownCompany() - : UnitTest("PokemonBDSP::AutoStory - Jubilife 00.05f clown wait") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - struct Case{ const char* name; size_t sign_after; size_t clown_at; bool completed; }; - const Case cases[] = { - {"Clown north of the hedge walk", 1, 2, true}, - {"Sign at the first tile, clown comes by in the gap", 1, 5, true}, - {"Sign at the third tile, clown comes by late", 3, 40, true}, - {"Sign never reads: never in the gap", 100, 40, false}, - {"Clown never answers", 1, 1000, false}, - }; - for (const Case& c : cases){ - FakeJubilife game; - game.sign_reads_after = c.sign_after; - game.clown_answers_at = c.clown_at; - Pokemon::EventLogger log = [&](const std::string& line){ logger.log(line); }; - Pokemon::RunState state; - Pokemon::Checkpoint checkpoint = jubilife_clown_company_checkpoint(game); - const std::vector problems = Pokemon::lint_checkpoint(checkpoint); - if (!problems.empty()){ - return std::string(c.name) + ": lint: " + problems.front(); - } - const bool completed = Pokemon::run_checkpoint(state, checkpoint, log).completed; - if (completed != c.completed){ - return std::string(c.name) + (completed ? ": completed" : ": did not complete") + "."; - } - } - return true; - } -}; - - -// The School's lamp at night, from its doorsteps' row (2026-10-30): where -// the door took us in, the lamp reads within half a tile of the door; a -// tile either side, more. -class Test_SchoolLamp : public UnitTest{ -public: - Test_SchoolLamp() - : UnitTest("PokemonBDSP::AutoStory - Jubilife School lamp") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const ImageFloatBox box(0.55, 0.20, 0.30, 0.10); - struct Case{ const char* file; double min; double max; }; - const Case cases[] = { - {"PokemonBDSP/AutoStory/JubilifeSchoolRightOfDoor-20261030.png", -1.5, -0.5}, - {"PokemonBDSP/AutoStory/JubilifeSchoolOneTileShort-20261030.png", -1.5, -0.5}, - {"PokemonBDSP/AutoStory/JubilifeSchoolAtDoor-20261030.png", -0.5, 0.5}, - {"PokemonBDSP/AutoStory/JubilifeSchoolAtDoor2-20261030.png", -0.5, 0.5}, - {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", -0.5, 0.5}, - {"PokemonBDSP/AutoStory/JubilifeSchoolOneTilePast-20261030.png", 0.5, 1.5}, - }; - for (const Case& c : cases){ - const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; - if (!Filesystem::is_regular_file(path)){ - return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); - } - const ImageRGB32 screen(path); - const std::optional lamp = find_lit_lamp_x(screen, box); - if (!lamp){ - return std::string(c.file) + ": no lit lamp found."; - } - const double across = tiles_across_at(*lamp) - tiles_across_at(0.67); - logger.log(std::string(c.file) + ": lamp at " + std::to_string(*lamp) + ", door " + std::to_string(across) + " tiles across."); - if (across < c.min || across > c.max){ - return std::string(c.file) + ": door " + std::to_string(across) + " tiles across, expected " + - std::to_string(c.min) + " to " + std::to_string(c.max) + "."; - } - } - return true; - } -}; - - -// Out of a building, the town's location banner shows; inside, none -// (Jubilife, 2026-10-30): what says a walk out of a door got out. -// Indoors or out, wherever the camera is (2026-10-30 screenshots). -void add_tests_AutoStory(UnitTestDatabase& database){ - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h deleted file mode 100644 index 18b8c8a666..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h +++ /dev/null @@ -1,26 +0,0 @@ -/* BDSP AutoStory Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Tests_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Tests_H - -#include "Common/Cpp/TestRunners/UnitTest.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - - -// AutoStory tests that need no console: the new-game intro's decisions. -void add_tests_AutoStory(UnitTestDatabase& database); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp new file mode 100644 index 0000000000..eef6af021a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp @@ -0,0 +1,319 @@ +/* BDSP AutoStory - Walks + * + * From: https://github.com/PokemonAutomation/ + * + * Walks measured on the console that the planner does not make: straight + * holds and D-pad taps, walks until something stops us, and the ways in + * and out of doors. + * + */ + +#include +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_Walks.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +std::pair dpad_of(Direction direction){ + switch (direction){ + case Direction::NORTH: + return {0, 1}; + case Direction::EAST: + return {1, 0}; + case Direction::SOUTH: + return {0, -1}; + case Direction::WEST: + return {-1, 0}; + } + return {0, 0}; +} + +void walk_until_stopped(VideoStream& stream, ProControllerContext& context, Direction direction, size_t tiles, const std::string& label){ + const bool across = direction == Direction::EAST || direction == Direction::WEST; + const auto [x, y] = dpad_of(direction); + // Time for every tile, and for the wall to read as still. + const Milliseconds limit = ROUTE_START_DELAY + 800ms + + (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN); + // Turn first: the first press of a new way only turns us, the screen + // holds still meanwhile, and that read as a wall two tiles into the + // walk west to the Lab's sign (r132a). + route_turn(stream, context, direction); + const WalkResult walked = walk_until(stream, context, run_from_wild_battle, x, y, limit, label); + stream.log("[Walk] " + label + ": stopped on " + walk_stop_name(walked.stop) + "."); + if (walked.stop == WalkStop::TRANSITION || walked.stop == WalkStop::DIALOG){ + fail_step(stream, label + ": " + walk_stop_name(walked.stop) + " instead of a stop."); + } + // The leg was measured to end against a wall, so holding toward it + // for every tile's time leaves us there whatever the screen shows: + // people walking or a TV screen nearby kept it changing and the stop + // went unseen (r162a, r167a, r171a). Seeing the stop only ends the + // hold early; a wrong place is caught by the next gate (a sign, a + // door, someone who answers). + if (walked.stop == WalkStop::LIMIT){ + stream.log("[Walk] " + label + ": held for " + std::to_string(tiles) + + " tiles' time without seeing the stop; taking the wall as reached."); + } +} + +void hold_for(VideoStream& stream, ProControllerContext& context, Direction direction, Milliseconds hold, const std::string& label){ + const auto [x, y] = dpad_of(direction); + const WalkResult walk = walk_for(stream, context, run_from_wild_battle, x, y, hold, label); + if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ + fail_step(stream, label + ": " + walk_stop_name(walk.stop) + "."); + } +} + +void tap_tiles(VideoStream& stream, ProControllerContext& context, Direction direction, size_t tiles, const std::string& label){ + const auto [x, y] = dpad_of(direction); + stream.log("[Walk] " + label + ": " + std::to_string(tiles) + " taps."); + for (size_t i = 0; i < tiles; i++){ + pbf_move_left_joystick(context, {x, y}, 160ms, 400ms); + } + pbf_wait(context, 800ms); + context.wait_for_all_requests(); +} + +void nudge(VideoStream& stream, ProControllerContext& context, Direction direction, const std::string& label){ + route_turn(stream, context, direction); + walk_for(stream, context, run_from_wild_battle, direction == Direction::EAST ? 1 : -1, 0, 80ms, label); +} + +bool door_north_by_scene(VideoStream& stream, ProControllerContext& context, const std::string& label){ + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 1200ms, label); + context.wait_for(1500ms); + if (up.stop == WalkStop::TRANSITION){ + return true; + } + // The School's fade is not always black enough to read as one: r146b + // went in (changes 156, 261) and read "limit". A whole new scene is + // through, as leave_door_south() counts it. + VideoSnapshot after = stream.video().snapshot(); + const bool through = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE; + if (through){ + stream.log("[Route] " + label + ": a new scene; through the door."); + } + return through; +} + +// One tile south, for a player facing another way. +void step_south(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 1), 200ms); + context.wait_for_all_requests(); +} + +// Holds the stick toward `dir` for `tiles` tiles, turning first. +void hold_tiles(VideoStream& stream, ProControllerContext& context, Direction dir, size_t tiles){ + route_turn(stream, context, dir); + const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; + const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; + pbf_move_left_joystick(context, {x, y}, route_open_hold(dir, tiles), 200ms); + context.wait_for_all_requests(); +} + +WalkResult walk_dir( + VideoStream& stream, ProControllerContext& context, Direction dir, Milliseconds limit, const std::string& label, + const std::function& arrived +){ + const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; + const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; + return walk_until(stream, context, run_from_wild_battle, x, y, limit, label, arrived); +} + +// `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the +// player moves a whole tile per tap (measured above), so counted legs do +// not drift the way stick holds do. The first tap toward a new way only +// turns the player, so turn first. +void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label){ + stream.log("[Walk] " + label + ": " + std::to_string(taps) + " D-pad taps."); + pbf_press_dpad(context, dpad, 160ms, 700ms); // turn + // Count only taps that moved the view: one after the turn was lost and + // the leg ended a tile short (r215d). A tile's scroll changes the view by + // about 100; standing still, under 15. A tap that does not move is someone + // in the way: people walk about these streets (r220g: blocked on tap 9 + // for 12 s while a passer-by stood in our row). Wait for them to move on, + // up to 30 s, before giving up. + size_t moved = 0; + const auto deadline = current_time() + std::chrono::milliseconds(taps * 1000 + 30000); + while (moved < taps && current_time() < deadline){ + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + pbf_press_dpad(context, dpad, 160ms, 700ms); + context.wait_for_all_requests(); + const VideoSnapshot after = stream.video().snapshot(); + const double change = before && after ? walk_screen_change(*before.frame, *after.frame) : 100; + if (change > 30){ + moved++; + continue; + } + stream.log("[Walk] " + label + ": tap " + std::to_string(moved + 1) + " did not move (view change " + std::to_string(change) + "); someone in the way? Waiting."); + if (dialogue_opens(stream, context, 300ms)){ + talk_or_fight(stream, context, label + ": someone in the way"); + } + context.wait_for(1500ms); + } + if (moved < taps){ + fail_step(stream, label + ": only " + std::to_string(moved) + " of " + std::to_string(taps) + " D-pad taps moved us."); + } +} + +// Through a door straight north of us: tap up to face it, hold up into it, +// and wait for its dark fade (by hand: brightness 7, about 1 s after). +bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label){ + pbf_press_dpad(context, DPAD_UP, 160ms, 300ms); + pbf_press_dpad(context, DPAD_UP, 300ms, 0ms); + context.wait_for_all_requests(); + const auto start = current_time(); + while (current_time() - start < 5000ms){ + context.wait_for(100ms); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && mean_brightness(*frame.frame) < 25){ + stream.log("[Route] " + label + ": the screen went dark; through the door."); + context.wait_for(2000ms); + return true; + } + } + stream.log("[Route] " + label + ": no dark screen after the D-pad step north."); + return false; +} + +// Out of a building whose door is straight south of us, a D-pad tile at a +// time, until the location banner shows: it names the map we step into +// (e.g. "Oreburgh City") for a few seconds, and never shows inside. Ends in +// the doorway, facing south. Out of the Oreburgh Gym from in front of Roark +// (by hand, 2026-10-31): 18 taps inside, 2 taps of black fade, the banner on +// the 21st. The door's fade alone was missed between steps (r219g walked on +// south to the rocks below the gym). +bool dpad_out_of_building(VideoStream& stream, ProControllerContext& context, size_t max_taps, const std::string& label){ + pbf_press_dpad(context, DPAD_DOWN, 160ms, 700ms); // turn + for (size_t tap = 0; tap < max_taps; tap++){ + pbf_press_dpad(context, DPAD_DOWN, 160ms, 900ms); + context.wait_for_all_requests(); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && location_banner_showing(*frame.frame)){ + stream.log("[Walk] " + label + ": the location banner shows after " + std::to_string(tap + 1) + " taps; outside."); + context.wait_for(1000ms); + return true; + } + if (dialogue_opens(stream, context, 100ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": someone in the way"); + } + } + return false; +} + +// For a player inside a Pokemon Center after a whiteout, at the counter: +// out through the door. The door is straight south of the nurse in most +// Centers; in Oreburgh's it was two tiles east of where we stood (by hand). +void walk_out_of_center(VideoStream& stream, ProControllerContext& context, const std::string& label){ + finish_dialogue(stream, context, BUTTON_B, label + ": the nurse"); + const int shifts[] = {0, 2, -4}; + for (int shift : shifts){ + if (shift != 0){ + hold_tiles(stream, context, shift > 0 ? Direction::EAST : Direction::WEST, (size_t)std::abs(shift)); + } + // A tile at a time, looking after each, so we stop on the doorstep: + // walk_until() looks only between 400 ms holds, and r205d held on + // through the fade and stepped a tile further south. + bool out = false; + for (size_t tile = 0; tile < 10 && !out; tile++){ + // D-pad taps: whole tiles (the first only turns us from the nurse). + pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); + context.wait_for(1500ms); + if (dialogue_opens(stream, context, 100ms)){ + finish_dialogue(stream, context, BUTTON_B, label + ": someone in the Center"); + } + const VideoSnapshot frame = stream.video().snapshot(); + size_t percent = 100; + if (frame){ + center_floor_on_screen(*frame.frame, &percent); + } + out = percent < 30; + } + if (out){ + // "No floor" also reads true on the door's black fade, and the game + // takes no input until it ends: the first D-pad taps after it were + // lost and the gym's door ended a tile away (r215d, r216d; a shot + // per press by hand showed the black frame). Wait for daylight. + const auto start = current_time(); + while (current_time() - start < 6000ms){ + context.wait_for(100ms); + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && mean_brightness(*frame.frame) > 40){ + break; + } + } + context.wait_for(1000ms); + return; + } + } + fail_step(stream, label + ": could not find the Pokemon Center's door."); +} + +// West until a transition, by the user's rule for a wall in the way: walk +// north to the top wall, which pins the row, then try the rows south of it +// one at a time. Trainers who stop us are fought. Returns at the first +// transition (a building, a cave, a gate), or when `arrived` matches. +void west_until_transition( + VideoStream& stream, ProControllerContext& context, const std::string& label, size_t max_legs, + const std::function& arrived +){ + for (size_t leg = 0; leg < max_legs; leg++){ + const WalkResult west = walk_dir(stream, context, Direction::WEST, 20000ms, label, arrived); + if (west.stop == WalkStop::ARRIVED){ + return; + } + if (west.stop == WalkStop::TRANSITION){ + context.wait_for(2000ms); + return; + } + if (west.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, label + ": someone in the way"); + continue; + } + if (west.stop != WalkStop::BLOCKED){ + continue; + } + walk_dir(stream, context, Direction::NORTH, 4000ms, label + ": north to the top wall"); + bool moved = false; + for (size_t row = 0; row < 10 && !moved; row++){ + hold_tiles(stream, context, Direction::SOUTH, 1); + const WalkResult probe = walk_dir(stream, context, Direction::WEST, 1500ms, label + ": west on the next row", arrived); + if (probe.stop == WalkStop::ARRIVED){ + return; + } + moved = probe.stop != WalkStop::BLOCKED || probe.walked > 1200ms; + if (probe.stop == WalkStop::TRANSITION){ + context.wait_for(2000ms); + return; + } + if (probe.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, label + ": someone in the way"); + } + } + if (!moved){ + fail_step(stream, label + ": no row below the top wall leads west."); + } + } + fail_step(stream, label + ": no transition after " + std::to_string(max_legs) + " legs."); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h new file mode 100644 index 0000000000..07b9174beb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h @@ -0,0 +1,96 @@ +/* BDSP AutoStory - Walks + * + * From: https://github.com/PokemonAutomation/ + * + * Walks measured on the console that the planner does not make: straight + * holds and D-pad taps, walks until something stops us, and the ways in + * and out of doors. Each fails the step with a screenshot when the walk + * ends where it should not (a dialogue or a door on a walk to a wall). + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walks_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Walks_H + +#include +#include +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The stick's (x, y) for `direction`: y is up the screen. +std::pair dpad_of(Pokemon::Direction direction); + +// Walks `direction` until something stops us, for at most `tiles` tiles' +// time, turning first. Holding on for the whole time counts as stopped: +// the leg was measured to end against a wall. Fails on a transition or a +// dialogue. +void walk_until_stopped(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, size_t tiles, const std::string& label); +// Holds the stick `direction` for `hold` from standing still (200 ms is a +// row, turning included; 160 ms a tile across, facing that way already). +// Fails on a transition or a dialogue. +void hold_for(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, Milliseconds hold, const std::string& label); +// `tiles` one-tile taps `direction` in a row, facing that way already, +// without waiting for the screen to settle in between: six of these moved +// exactly six tiles on the console, where six single holds moved seven +// (2026-10-30). +void tap_tiles(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, size_t tiles, const std::string& label); +// Half a tile `direction` (EAST or WEST), facing it first. +void nudge(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction, const std::string& label); +// Walks north; true if a door took us through, by its fade or by a whole +// new scene (the Trainers' School's fade is not always black, r146b). +bool door_north_by_scene(VideoStream& stream, ProControllerContext& context, const std::string& label); + +// One tile south, for a player facing another way. +void step_south(VideoStream& stream, ProControllerContext& context); +// Holds the stick toward `dir` for `tiles` tiles, turning first. +void hold_tiles(VideoStream& stream, ProControllerContext& context, Pokemon::Direction dir, size_t tiles); +// walk_until() toward `dir`. +WalkResult walk_dir( + VideoStream& stream, ProControllerContext& context, Pokemon::Direction dir, Milliseconds limit, const std::string& label, + const std::function& arrived = nullptr +); + +// `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the +// player moves a whole tile per tap, so counted legs do not drift the way +// stick holds do. The first tap toward a new way only turns the player, so +// turn first. +void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label); +// Through a door straight north of us: tap up to face it, hold up into it, +// and wait for its dark fade (by hand: brightness 7, about 1 s after). +bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label); +// Out of a building whose door is straight south of us, a D-pad tile at a +// time, until the location banner shows: it names the map we step into +// (e.g. "Oreburgh City") for a few seconds, and never shows inside. Ends in +// the doorway, facing south. Out of the Oreburgh Gym from in front of Roark +// (by hand, 2026-10-31): 18 taps inside, 2 taps of black fade, the banner on +// the 21st. The door's fade alone was missed between steps (r219g walked on +// south to the rocks below the gym). +bool dpad_out_of_building(VideoStream& stream, ProControllerContext& context, size_t max_taps, const std::string& label); +// For a player inside a Pokemon Center after a whiteout, at the counter: +// out through the door. The door is straight south of the nurse in most +// Centers; in Oreburgh's it was two tiles east of where we stood (by hand). +void walk_out_of_center(VideoStream& stream, ProControllerContext& context, const std::string& label); +// West until a transition, by the user's rule for a wall in the way: walk +// north to the top wall, which pins the row, then try the rows south of it +// one at a time. Trainers who stop us are fought. Returns at the first +// transition (a building, a cave, a gate), or when `arrived` matches. +void west_until_transition( + VideoStream& stream, ProControllerContext& context, const std::string& label, size_t max_legs, + const std::function& arrived = nullptr +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.cpp new file mode 100644 index 0000000000..b0333a7963 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.cpp @@ -0,0 +1,444 @@ +/* BDSP AutoStory - Wild battles + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_ReceivePokemonDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Battle.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +namespace{ + +// Two grass rows at 240 ms each, with room to spare: the wall stops us. +constexpr Milliseconds CATCH_GRASS_WALK = 900ms; + +void encounter_one_wild_pokemon(VideoStream& stream, ProControllerContext& context) { + context.wait_for_all_requests(); + pbf_wait(context, 1000ms); + + bool encounter_found = false; + const auto deadline = std::chrono::steady_clock::now() + 180s; + + // Walk up and down the catch grass until a battle starts. Its three rows + // sit between tree walls, so each walk ends at a wall, on the same tile + // every time, after two new grass tiles. Short sideways steps drift and + // can rock across one tile edge without entering new grass. + while (!encounter_found && std::chrono::steady_clock::now() < deadline) { + BlackScreenWatcher battle_start; + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + + route_turn(stream, context, Pokemon::Direction::NORTH); + pbf_move_left_joystick(context, {0, 1}, CATCH_GRASS_WALK, 200ms); + context.wait_for_all_requests(); + + int ret = wait_until( + stream, + context, + 1500ms, + { + {battle_start}, + {battle_menu}, + {battle_dialog}, + } + ); + + if (ret >= 0) { + encounter_found = true; + break; + } + + route_turn(stream, context, Pokemon::Direction::SOUTH); + pbf_move_left_joystick(context, {0, -1}, CATCH_GRASS_WALK, 200ms); + context.wait_for_all_requests(); + + ret = wait_until( + stream, + context, + 1500ms, + { + {battle_start}, + {battle_menu}, + {battle_dialog}, + } + ); + + if (ret >= 0) { + encounter_found = true; + break; + } + } + + if (!encounter_found) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Wild encounter: walking up and down the grass found no battle within three minutes.", stream); + stream.log("[AutoStory] Wild battle detected!", COLOR_GREEN); + + // Wait until the actual battle menu appears. + BattleMenuWatcher confirm_menu(BattleType::STANDARD); + + int menu_ret = wait_until( + stream, + context, + 15s, + { + {confirm_menu}, + } + ); + + if (menu_ret < 0) { + stream.log( + "[AutoStory] Failed to detect battle menu.", + COLOR_RED + ); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "[AutoStory] Failed to detect battle menu.", stream); + } + + stream.log("[AutoStory] Battle menu detected.", COLOR_GREEN); + + context.wait_for_all_requests(); + pbf_wait(context, 500ms); + +} + +bool mine_confirm_escape_via_menu(VideoStream& stream, ProControllerContext& context, BattleType battle_type){ + MenuWatcher game_menu; + OverworldWatcher overworld; + BattleMenuWatcher battle_menu(battle_type, COLOR_YELLOW); + for (size_t attempt = 1; attempt <= 3; attempt++){ + // Do not accept the absence-based overworld detector before menu proof. + const int state = wait_until(stream, context, 300ms, {{game_menu}, {battle_menu}}); + if (state == 1){ + stream.log("[Mine escape] Battle menu still present; retrying Run."); + return false; + } + int detected = state; + if (state != 0){ + stream.log("[Mine escape] Main menu fallback attempt " + std::to_string(attempt) + "/3."); + detected = run_until(stream, + context, + [](ProControllerContext& controller){ + pbf_press_button(controller, BUTTON_X, 160ms, 840ms); + pbf_wait(controller, 3s); + controller.wait_for_all_requests(); + }, + {game_menu, battle_menu}); + if (detected == 1){ + return false; + } + } + if (detected != 0){ + continue; + } + stream.log("[Mine escape] Main game menu confirmed; battle has ended.", COLOR_GREEN); + // Close the confirmed menu before returning control to navigation. + MenuDetector menu_visible; + for (size_t close = 0; close < 3; close++){ + pbf_press_button(context, BUTTON_B, 80ms, 500ms); + context.wait_for_all_requests(); + if (wait_until(stream, context, 1s, {{overworld}}) == 0){ + return true; + } + BlackScreenWatcher visible(COLOR_YELLOW, + {0.1, 0.1, 0.8, 0.8}, + 100, + 10, + BlackScreenWatcher::FinderType::GONE, + 500ms); + if (wait_until(stream, context, 2s, {{visible}}) == 0){ + const VideoSnapshot frame = stream.video().snapshot(); + if (frame && !menu_visible.detect(frame)){ + stream.log("[Mine escape] Confirmed game menu closed; returning to navigation.", COLOR_GREEN); + return true; + } + } + } + stream.log("[Mine escape] Main menu detected but could not confirm it closed.", COLOR_ORANGE); + return false; + } + return false; +} + +bool mine_select_run(VideoStream& stream, ProControllerContext& context, BattleType battle_type){ + BattleMenuDetector cursor(battle_type, COLOR_YELLOW); + size_t matching_frames = 0; + for (size_t check = 0; check < 16; check++){ + context.wait_for_all_requests(); + const VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + return false; + } + const BattleMenuOption selected = battle_menu_selected_option(cursor, frame); + stream.log( + "[Mine escape] Battle cursor row=" + std::to_string(static_cast(selected) + 1) + "; target Run=4."); + if (selected == BattleMenuOption::RUN){ + if (++matching_frames >= 3){ + stream.log("[Mine escape] Run cursor confirmed.", COLOR_GREEN); + return true; + } + pbf_wait(context, 100ms); + }else{ + matching_frames = 0; + if (selected == BattleMenuOption::UNKNOWN){ + // Do not send a direction or A without a known cursor. + pbf_wait(context, 150ms); + }else{ + const DpadPosition direction = selected == BattleMenuOption::BATTLE ? DPAD_UP : DPAD_DOWN; + pbf_press_dpad(context, direction, 80ms, 250ms); + } + } + } + stream.log("[Mine escape] Could not confirm Run selection; A was not pressed.", COLOR_ORANGE); + return false; +} + +} + +void fight_starly(VideoStream& stream, ProControllerContext& context) { + stream.log("[AutoStory] Starting Starly battle...", COLOR_BLUE); + + // Declare OUTSIDE the loop so accumulated detector state + // (trigger counts, black-screen transitions) is preserved across frames. + // STARTER type: player has no Pokeballs, so ball-count areas won't + // be white. STARTER skips that check and only verifies status bars + // and the Fight button color. + BattleMenuWatcher battle_menu(BattleType::STARTER, COLOR_YELLOW); + EndBattleWatcher end_battle; + + while (true) { + context.wait_for_all_requests(); + + // Phase 1: wait for either battle menu or battle end. + int ret = run_until( + stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, +{ + {battle_menu}, + {end_battle}, +} +); + + switch (ret) { + case 0: + { + stream.log("[AutoStory] Battle menu detected. Selecting move...", COLOR_BLUE); + + // Select Fight, then move slot 1. + pbf_press_button(context, BUTTON_A, 80ms, 500ms); + pbf_press_button(context, BUTTON_A, 80ms, 3000ms); + // Loop back - mash B through animations until next menu or end. + break; + } + case 1: + { + stream.log("[AutoStory] Starly battle complete.", COLOR_GREEN); + + // Mash through XP / level-up / dialog screens back to overworld. + OverworldWatcher overworld; + int cleanup_ret = run_until( + stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, + { {overworld} } + ); + if (cleanup_ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Starly battle: Overworld cleanup timed out.", stream); + } + return; + } + default: + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Starly battle timed out.", stream); + } + } +} + +void battle_1_pokemon(VideoStream& stream, ProControllerContext& context) { + stream.log("[Training] Finding one wild Pokemon using the catch encounter wiggle.", COLOR_BLUE); + encounter_one_wild_pokemon(stream, context); + // The best move on the menu, whatever the starter. + fight_wild_battle(stream, context, "Training"); + context.wait_for_all_requests(); + stream.log("[Training] One wild battle completed.", COLOR_GREEN); +} + +void catch_1_pokemon( + VideoStream& stream, + ProControllerContext& context +) { + stream.log("[AutoStory] Starting catch_1_pokemon helper...", COLOR_BLUE); + + encounter_one_wild_pokemon(stream, context); + + // Catch the pokemon. + stream.log("[AutoStory] Throwing Poke Ball (manual bag open)...", COLOR_BLUE); + ReceivePokemonDetector received_pokemon; + // Preserve exit-transition state from the ball outcome through + // receipt/experience cleanup; do not recreate it between phases. + EndBattleWatcher battle_ended; + bool caught = false; + bool exit_seen = false; + for (int throw_attempt = 0; throw_attempt < 10; throw_attempt++) { + // Open bag from battle menu (X), confirm first slot (ZL). + pbf_press_button(context, BUTTON_X, 160ms, 840ms); + context.wait_for_all_requests(); + pbf_press_button(context, BUTTON_ZL, 160ms, 1000ms); + context.wait_for_all_requests(); + + // Wait to see if the battle menu comes back (ball failed) + // or if the battle ends (caught or fainted). + BattleMenuWatcher menu_back(BattleType::STANDARD, COLOR_YELLOW); + int throw_ret = run_until( + stream, context, + [](ProControllerContext& ctx) { + pbf_mash_button(ctx, BUTTON_B, 120000ms); + }, + {{menu_back}, {received_pokemon}, {battle_ended}} + ); + + if (throw_ret == 1 || (throw_ret == 2 && received_pokemon.received())) { + caught = true; + exit_seen = throw_ret == 2; + stream.log("[AutoStory] Pokemon receipt verified.", COLOR_GREEN); + break; + } + if (throw_ret < 0 || throw_ret == 2) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Catch failed: No verified Pokemon receipt before battle ended or timed out.", stream); + } + stream.log( + "[AutoStory] Ball failed, retrying (attempt " + std::to_string(throw_attempt + 2) + ")...", + COLOR_ORANGE + ); + } + + context.wait_for_all_requests(); + + if (!caught) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Catch failed: Exhausted ten ball attempts.", stream); + } + // Receipt alone is not an overworld return: clear experience, + // automatic move learning, dex registration and nickname screens + // until the same completed-black-transition watcher used in battles fires. + if (!exit_seen) { + stream.log("[Catch cleanup] Receipt confirmed; waiting for battle exit transition."); + const int cleanup_ret = run_until(stream, context, + [](ProControllerContext& ctx) { pbf_mash_button(ctx, BUTTON_B, 120000ms); }, + {{battle_ended}}); + if (cleanup_ret != 0) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Catch cleanup: Battle exit transition did not finish.", stream); + } + } + context.wait_for_all_requests(); + stream.log("[Catch cleanup] Battle exit transition confirmed; checking stable overworld."); + confirm_battle_overworld(stream, context, "Catch cleanup"); + stream.log("[Catch cleanup] Verified receipt, completed battle exit, and four seconds of clear overworld.", COLOR_GREEN); +} + +void escape_wild_battle(VideoStream& stream, + ProControllerContext& context, + BattleType battle_type, + std::chrono::seconds timeout){ + EndBattleWatcher battle_end; + bool exit_transition = false; + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline){ + pbf_press_button(context, BUTTON_B, 40ms, 200ms); + context.wait_for_all_requests(); + BattleMenuWatcher menu_ready(battle_type, COLOR_YELLOW); + const int ready = wait_until(stream, context, 3s, {{battle_end}, {menu_ready}}); + if (ready == 0){ + exit_transition = true; + break; + } + if (ready != 1){ + continue; + } + if (!mine_select_run(stream, context, battle_type)){ + continue; + } + stream.log("[Mine escape] Run input sent; watching for black-screen exit."); + const int result = run_until(stream, + context, + [](ProControllerContext& controller){ + pbf_press_button(controller, BUTTON_A, 60ms, 0ms); + pbf_wait(controller, 4s); + controller.wait_for_all_requests(); + }, + {battle_end}); + if (result == 0){ + exit_transition = true; + break; + } + } + if (!exit_transition && wait_until(stream, context, 10s, {{battle_end}}) == 0){ + exit_transition = true; + } + if (exit_transition){ + // EndBattleWatcher requires black followed by a visible screen. + stream.log("[Mine escape] Black-screen exit transition confirmed.", COLOR_GREEN); + pbf_wait(context, 800ms); + context.wait_for_all_requests(); + return; + } + // Only probe X after normal escape attempts and the final transition wait. + // Absence of battle UI by itself is not evidence of an overworld return. + stream.log("[Mine escape] Exit transition missed; trying late main-menu fallback."); + if (mine_confirm_escape_via_menu(stream, context, battle_type)){ + return; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "escape_wild_battle: Exit transition and late main-menu fallback could not confirm escape.", + stream); +} + +void run_from_wild_battle(VideoStream& stream, ProControllerContext& context){ + escape_wild_battle(stream, context, BattleType::STANDARD); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.h new file mode 100644 index 0000000000..5377a3c1b4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.h @@ -0,0 +1,56 @@ +/* BDSP AutoStory - Wild battles + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * The story's wild battles: the Starly the starter is chosen against, one + * found in the grass to fight or to catch, and running from one that + * interrupts a walk. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_WildBattle_H +#define PokemonAutomation_PokemonBDSP_AutoStory_WildBattle_H + +#include +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The Starly battle at Lake Verity, from its first text to the overworld. +void fight_starly(VideoStream& stream, ProControllerContext& context); + +// Finds one wild Pokemon in the grass by walking up and down a column +// between two tree walls, and fights it with the best move. Does not heal. +void battle_1_pokemon(VideoStream& stream, ProControllerContext& context); + +// Finds one wild Pokemon the same way and catches it with the first ball +// in the Bag, up to ten throws; ends on the overworld. +void catch_1_pokemon(VideoStream& stream, ProControllerContext& context); + +// Runs from the wild battle on screen: Run on the battle menu until the +// exit's black screen shows. Fails with a screenshot after `timeout`. +void escape_wild_battle( + VideoStream& stream, + ProControllerContext& context, + BattleType battle_type = BattleType::STANDARD, + std::chrono::seconds timeout = std::chrono::seconds(30) +); + +// escape_wild_battle() for a standard wild battle, as the walker takes it +// (WildBattleEscape): how the story's walks leave the battles that +// interrupt them. +void run_from_wild_battle(VideoStream& stream, ProControllerContext& context); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp deleted file mode 100644 index 2f6f37c43f..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp +++ /dev/null @@ -1,1485 +0,0 @@ -/* BDSP AutoStory - Route 202 to Sandgem's Pokemon Center, as events - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include -#include -#include "Common/Cpp/PrettyPrint.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/Language.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/OCR/OCR_RawTesseractOCR.h" -#include "CommonTools/OCR/OCR_Routines.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route201.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" -#include "PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" -#include "PokemonBDSP_AutoStory_SandgemCenterEvents.h" -#include "Pokemon/Navigation/Pokemon_TilePose.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; -using Pokemon::Checkpoint; -using Pokemon::Condition; -using Pokemon::Direction; -using Pokemon::Event; -using Pokemon::Interrupt; -using Pokemon::Outcome; -using Pokemon::Pose; -using Pokemon::TilePose; -using Pokemon::tiles_of; -using Pokemon::PoseCertainty; -using Pokemon::Recovery; -using Pokemon::RecoveryKind; -using Pokemon::RunState; -using Pokemon::TilePos; -using Pokemon::condition; -using Pokemon::outcome_failed; -using Pokemon::outcome_ok; -using Pokemon::pose_known; - - -const char* const ROUTE202_TO_SANDGEM_CENTER_NAME = "Route 202 to Sandgem's Pokemon Center (events)"; - -TilePos sandgem_entry_stop(){ - return story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); -} - -TilePos below_sandgem_center_sign(){ - return story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH)); -} - -TilePos east_of_lab_sign(){ - return story_maps().to_world(MAP_SANDGEM_TOWN, {6, 11}); -} - -bool sign_text_matches(const std::string& text, const std::string& key){ - std::string lower; - for (char ch : text){ - lower += (char)std::tolower((unsigned char)ch); - } - return lower.find(key) != std::string::npos; -} - -bool doorway_lined_up(const LitDoorway& doorway){ - return doorway.rows_up > 0 && doorway.rows_up <= 1.5 && std::abs(doorway.tiles_across) <= 0.5; -} - -bool center_doorway_at_front_row(const LitDoorway& doorway){ - // From below the sign the door is three tiles west; a tile either way - // for where the walk stopped, and the rows as tiles_short_of_door() - // counts the front row (1.5). - // Inside, the lit floor reads as a doorway well under a row up (0.28 - // in r129c). - return doorway.rows_up >= 0.7 && doorway.rows_up <= 1.5 && - doorway.tiles_across >= -4.5 && doorway.tiles_across <= 1.5; -} - - - -namespace{ - -// Where a sign's box shows: the bottom of the screen, as a dialogue's. The -// Lab's is grey, Route 201's green; opening either changes the area far -// more than the overworld behind it moves standing still. -const ImageFloatBox SIGN_BOX(0.19, 0.80, 0.64, 0.18); -// Its text, inside the margins. -const ImageFloatBox SIGN_TEXT(0.22, 0.82, 0.58, 0.15); -constexpr double SIGN_BOX_OPENED = 20.0; - -// The pose of a player who may be on any of `tiles`: on their map if they -// all share one. -Pose pose_on(const std::vector& tiles, std::optional facing = std::nullopt){ - const Pokemon::MapSet& maps = story_maps(); - Pose pose; - pose.where = std::make_shared(tiles, facing); - if (tiles.empty()){ - return pose; - } - const std::optional first = maps.to_local(tiles.front()); - const bool one_map = first && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos){ - const std::optional tile = maps.to_local(pos); - return tile && tile->map == first->map; - }); - pose.map = one_map ? first->map : ""; - pose.certainty = tiles.size() == 1 ? PoseCertainty::TILE : PoseCertainty::SET; - return pose; -} - -Recovery recovery(std::string on, RecoveryKind kind, size_t attempts, Pokemon::Act prepare, std::string why){ - Recovery recovery; - recovery.on = std::move(on); - recovery.kind = kind; - recovery.attempts = attempts; - recovery.prepare = std::move(prepare); - recovery.why = std::move(why); - return recovery; -} - -Condition outside(SandgemCenterActions& game){ - return condition("outside, not in a Pokemon Center", [&game](RunState&){ - return !game.inside_pokemon_center(); - }); -} - -// The doorway alone cannot tell the Center's from the Mart's, so the pose -// must also be in front of the Center: on its doors' row or a few rows -// south, within a few columns of its door. -Condition center_doorway_ahead(SandgemCenterActions& game){ - return condition("the Center's doorway at the front row", [&game](RunState& state){ - const TilePos door = story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); - const bool near = state.pose.map == MAP_SANDGEM_TOWN && !tiles_of(state.pose).empty() && - std::all_of(tiles_of(state.pose).begin(), tiles_of(state.pose).end(), [&](const TilePos& pos){ - return std::abs(pos.x - door.x) <= 4 && pos.y > door.y && pos.y <= door.y + 4; - }); - if (!near){ - return false; - } - const std::optional doorway = game.lit_doorway(); - return doorway && center_doorway_at_front_row(*doorway); - }); -} - -// 1. The save the checkpoint starts from is Route 202's save spot: the -// save screen's card says which map, and its walls pin the tile. -Event confirm_on_route_202(SandgemCenterActions& game){ - std::shared_ptr read = std::make_shared(); - Event event; - event.name = "confirm_on_route_202"; - event.why = "the save must stand at Route 202's save spot"; - event.act = [&game, read](RunState&){ - *read = game.save_screen_location(); - return outcome_ok(); - }; - event.expect.push_back(condition("the save screen reads Route 202", [read](RunState&){ - return location_banner_matches(*read, MAP_ROUTE_202); - })); - event.pose_learned = pose_on({story_maps().to_world(MAP_ROUTE_202, Route202::SAVE_SPOT)}, Direction::EAST); - event.recoveries.push_back(recovery("expect_failed", RecoveryKind::WAIT, 1, nullptr, - "OCR may have misread the card; read it again")); - return event; -} - -// 2. South along the trees east of Route 202, into Sandgem Town. Column -// 30 has no tall grass, and no trainer's sight reaches it below the ledge. -// walk_route() fails unless Sandgem's banner shows on the way. -Event walk_to_sandgem(SandgemCenterActions& game){ - Event event; - event.name = "walk_to_sandgem"; - event.why = "south out of Route 202, past Sandgem's banner"; - event.skip_if.push_back(pose_known(MAP_SANDGEM_TOWN, PoseCertainty::SET)); - event.require.push_back(pose_known(MAP_ROUTE_202, PoseCertainty::SET)); - event.act = [&game](RunState& state){ - std::vector at = tiles_of(state.pose); - const TilePos stop = sandgem_entry_stop(); - const Outcome walked = game.walk(at, [stop](const TilePos& pos){ return pos == stop; }, "Route 202 to Sandgem Town"); - if (walked.succeeded()){ - return outcome_ok(pose_on(at, Direction::SOUTH)); - } - return walked; - }; - event.expect.push_back(pose_known(MAP_SANDGEM_TOWN, PoseCertainty::SET)); - // From anywhere on Route 202 under the ledge, or Sandgem's top rows, - // east to the trees and south to the wall ends at the same tile. - event.recoveries.push_back(recovery("", RecoveryKind::REANCHOR, 1, [&game](RunState& state){ - if (!game.east_then_south_into_sandgem()){ - return outcome_failed("no_sandgem_banner", "east to the trees and south showed no Sandgem Town banner"); - } - return outcome_ok(pose_on({sandgem_entry_stop()}, Direction::SOUTH)); - }, "east to the trees and south to the wall, watching for Sandgem's banner")); - return event; -} - -// 3. From where the walk south stops to below the Center's sign, planned -// with town costs: west to the wall, a tile east into the gap's column, -// straight south through the gap between the signs, a tile west (the -// user's route, 2026-10-30; the plan test checks the planner finds it). -// With walls only, the plan went round the town in 13 legs, into the Lab -// (r127a) and the Mart (r128b); judging the row by the doorway's height put -// us a row up, level with the signs (r132a, r133a). -Event walk_to_center_sign(SandgemCenterActions& game){ - Event event; - event.name = "walk_to_center_sign"; - event.why = "through the signs' gap to below the Center's sign"; - event.require.push_back(pose_known(MAP_SANDGEM_TOWN, PoseCertainty::SET)); - event.act = [&game](RunState& state){ - std::vector at = tiles_of(state.pose); - const TilePos goal = below_sandgem_center_sign(); - const Outcome walked = game.walk_in_town(at, [goal](const TilePos& pos){ return pos == goal; }, "To below the Center's sign"); - if (walked.succeeded()){ - return outcome_ok(pose_on(at)); - } - return walked; - }; - event.expect.push_back(outside(game)); - return event; -} - -// 4. The gate: the Center's sign, at the building's bottom right, reads -// from the tile below it facing north, and only from there. -Event read_center_sign(SandgemCenterActions& game){ - std::shared_ptr read = std::make_shared(); - Event event; - event.name = "read_center_sign"; - event.why = "read the Center's sign from below it"; - event.act = [&game, read](RunState&){ - *read = game.read_sign(Direction::NORTH, "the Center's sign"); - return outcome_ok(); - }; - event.expect.push_back(condition("the Center's sign's box opened and closed", [read](RunState&){ - return read->opened && read->closed; - })); - event.expect.push_back(condition("the Center's sign's text, if read, has \"center\"", [read](RunState&){ - return read->text.empty() || sign_text_matches(read->text, "center"); - })); - event.pose_learned = pose_on({below_sandgem_center_sign()}, Direction::NORTH); - return event; -} - -// A townsperson who talks stops a walk where it stands, and walk_route() -// fails on a talk no trainer explains: close it and run the event again, -// from wherever its recoveries can place us. -Interrupt dialogue_interrupt(SandgemCenterActions& game){ - Interrupt interrupt; - interrupt.name = "dialogue"; - interrupt.detect = [&game](RunState&){ return game.dialogue_showing(); }; - interrupt.handle = [&game](RunState&){ return game.finish_dialogue(); }; - interrupt.pose = Pokemon::InterruptPose::LOST; - interrupt.resume = Pokemon::ResumePolicy::RETRY_EVENT; - return interrupt; -} - - -// The console's side of the checkpoint. -class ConsoleSandgemCenterActions : public SandgemCenterActions{ -public: - ConsoleSandgemCenterActions(VideoStream& stream, ProControllerContext& context) - : m_stream(stream) - , m_context(context) - , m_trainers(story_trainer_sights()) - {} - - virtual std::string save_screen_location() override{ - return read_location_from_save_screen(m_stream, m_context); - } - - virtual Outcome walk( - std::vector& at, - const std::function& goal, - const std::string& label - ) override{ - return walk_planned(at, goal, label, Pokemon::RobustPlanCosts()); - } - - Outcome walk_planned( - std::vector& at, - const std::function& goal, - const std::string& label, - const Pokemon::RobustPlanCosts& costs - ){ - // No trainer sees this way; one who does is a failure to look at. - const RouteTrainerFight fight = [this](const std::string& trainer){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Trainer " + trainer + " saw us on a way no trainer should see.", m_stream); - }; - try{ - at = walk_route(m_stream, m_context, run_from_wild_battle, story_maps(), at, goal, m_trainers, fight, label, {}, costs); - return outcome_ok(); - }catch (OperationFailedException& e){ - // Through a door: nothing to re-anchor on in there. - const bool through_door = e.message().find("transition") != std::string::npos; - return outcome_failed(through_door ? "walk_transition" : "walk_failed", e.message()); - } - } - - virtual bool east_then_south_into_sandgem() override{ - bool saw_sandgem = false; - const auto banner = [&saw_sandgem](const ImageViewRGB32& screen){ - saw_sandgem = saw_sandgem || location_banner_matches(read_location_banner(screen), MAP_SANDGEM_TOWN); - return false; - }; - const WalkResult east = walk_until(m_stream, m_context, run_from_wild_battle, 1, 0, 6000ms, "East to the trees"); - const WalkResult south = walk_until(m_stream, m_context, run_from_wild_battle, 0, -1, 9000ms, "South to the wall", banner); - for (const WalkResult& walk : {east, south}){ - if (walk.stop == WalkStop::TRANSITION || walk.stop == WalkStop::DIALOG){ - route_status(m_stream, std::string("Re-anchor: ") + walk_stop_name(walk.stop) + " instead of a wall."); - return false; - } - } - return saw_sandgem; - } - - virtual Outcome walk_in_town( - std::vector& at, - const std::function& goal, - const std::string& label - ) override{ - return walk_planned(at, goal, label, town_plan_costs()); - } - - virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ - VideoStream& stream = m_stream; - DialogueRead read; - route_turn(stream, m_context, facing); - // Let the camera settle after the turn: its drift read as a box - // opening (r132a). - pbf_wait(m_context, 800ms); - m_context.wait_for_all_requests(); - VideoSnapshot before = stream.video().snapshot(); - // The text types out over about a second; B before it has finished - // only finishes it (r131a). - pbf_press_button(m_context, BUTTON_A, 80ms, 1500ms); - m_context.wait_for_all_requests(); - VideoSnapshot shown = stream.video().snapshot(); - if (!before || !shown){ - return read; - } - const double opened = walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*shown.frame, SIGN_BOX)); - read.opened = opened > SIGN_BOX_OPENED; - if (!read.opened){ - stream.log("[Sign] " + label + ": no box opened (change " + std::to_string(opened) + ")."); - return read; - } - // Tesseract read the Lab's white-on-grey text as "e R O e IO" (r131a). - // Only to say which sign, when there is a reader for the game's - // language; the box opening and closing is the check. - if (story_reads_text()){ - read.text = OCR::ocr_read(g_story_language, extract_box_reference(*shown.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); - } - stream.log("[Sign] " + label + ": reads \"" + read.text + "\" (change " + std::to_string(opened) + ")."); - for (size_t presses = 0; presses < 3 && !read.closed; presses++){ - pbf_press_button(m_context, BUTTON_B, 80ms, 1000ms); - m_context.wait_for_all_requests(); - VideoSnapshot after = stream.video().snapshot(); - read.closed = after && walk_screen_change(extract_box_reference(*before.frame, SIGN_BOX), extract_box_reference(*after.frame, SIGN_BOX)) < SIGN_BOX_OPENED; - } - stream.log("[Sign] " + label + (read.closed ? ": closed." : ": still open.")); - return read; - } - - virtual std::optional lit_doorway() override{ - m_context.wait_for_all_requests(); - VideoSnapshot screen = m_stream.video().snapshot(); - return screen ? find_lit_doorway(*screen.frame) : std::nullopt; - } - - virtual Outcome line_up_with_door() override{ - if (!line_up_with_lit_door(m_stream, m_context, "Sandgem Pokemon Center: in front of the door")){ - return outcome_failed("not_lined_up", "no lit doorway, inside, or not lined up after 6 steps"); - } - return outcome_ok(); - } - - virtual bool inside_pokemon_center() override{ - m_context.wait_for_all_requests(); - VideoSnapshot screen = m_stream.video().snapshot(); - return screen && pokemon_center_inside(*screen.frame); - } - - virtual Outcome east_then_north_in_sandgem() override{ - const WalkResult east = walk_until(m_stream, m_context, run_from_wild_battle, 1, 0, 6000ms, "Sandgem: east to a wall"); - const WalkResult north = walk_until(m_stream, m_context, run_from_wild_battle, 0, 1, 6000ms, "Sandgem: north to a wall"); - for (const WalkResult& walk : {east, north}){ - if (walk.stop != WalkStop::BLOCKED){ - return outcome_failed("not_anchored", std::string("Sandgem: ") + walk_stop_name(walk.stop) + " instead of a wall"); - } - } - return outcome_ok(); - } - - virtual bool dialogue_showing() override{ - m_context.wait_for_all_requests(); - VideoSnapshot screen = m_stream.video().snapshot(); - return screen && ShortDialogDetector().detect(*screen.frame); - } - - virtual Outcome finish_dialogue() override{ - try{ - PokemonBDSP::finish_dialogue(m_stream, m_context, BUTTON_B, "Sandgem: a talk on the way"); - return outcome_ok(); - }catch (OperationFailedException& e){ - return outcome_failed("dialogue_failed", e.message()); - } - } - -protected: - VideoStream& m_stream; - ProControllerContext& m_context; - std::vector m_trainers; -}; - - -// --------------------------------------------------------------------------- -// The Poke Mart lady -// --------------------------------------------------------------------------- - -// A step of 00.04 that Segment 00 walks as one function, wrapped as an -// event: done once `flag` is set. -Event wrapped_step(std::string name, std::string why, std::string flag, Pokemon::Act step){ - Event event; - event.name = std::move(name); - event.why = std::move(why); - event.skip_if.push_back(condition(flag, [flag](RunState& state){ return state.flags.count(flag) > 0; })); - event.act = [flag, step = std::move(step)](RunState& state) mutable{ - const Outcome done = step(state); - if (done.succeeded()){ - state.flags.insert(flag); - } - return done; - }; - event.expect.push_back(condition(flag, [flag](RunState& state){ return state.flags.count(flag) > 0; })); - return event; -} - -// A gate: the sign we walked into, read with A facing it. Only the tiles -// beside it read it, so the read says which row we are on; the text, when -// OCR gets it, says which sign. A walk that stopped short of it is put -// right by walking on until the sign stops us. -Event read_sign(MartLadyActions& game, std::string name, std::string sign, Direction facing, std::string key){ - std::shared_ptr read = std::make_shared(); - Event event; - event.name = std::move(name); - event.why = "read " + sign + " facing " + std::string(Pokemon::direction_name(facing)); - event.act = [&game, read, sign, facing](RunState&){ - *read = game.read_sign(facing, sign); - return outcome_ok(); - }; - event.expect.push_back(condition(sign + "'s box opened and closed", [read](RunState&){ - return read->opened && read->closed; - })); - event.expect.push_back(condition(sign + "'s text, if read, has \"" + key + "\"", [read, key](RunState&){ - return read->text.empty() || sign_text_matches(read->text, key); - })); - event.recoveries.push_back(recovery("expect_failed", RecoveryKind::REANCHOR, 1, [&game, facing](RunState&){ - return game.walk_until_stopped(facing, 2, "On to the sign"); - }, "not at the sign: on until it stops us")); - return event; -} - -// She stands on the top row of her grass, under the trees, a tile or two -// in from its west edge. From the edge's top tile: east until she stops us, -// pressing A; then one A per line. Missed: back west to the edge, again. -Event talk_to_mart_lady(MartLadyActions& game){ - std::shared_ptr read = std::make_shared(); - Event event; - event.name = "talk_to_mart_lady"; - event.why = "the Poke Mart lady on Route 201 talks (she gives new games Potions)"; - event.act = [&game, read](RunState&){ - *read = game.talk_walking(Direction::EAST, 4, "Poke Mart lady"); - return outcome_ok(); - }; - event.expect.push_back(condition("a dialogue opened and was closed", [read](RunState&){ - return read->opened && read->closed; - })); - event.recoveries.push_back(recovery("expect_failed", RecoveryKind::REANCHOR, 2, [&game](RunState&){ - return game.walk_until_stopped(Direction::WEST, 6, "West to the grass's edge"); - }, "missed her: back west to the grass's edge and again")); - return event; -} - - -// The console's side of the Mart lady checkpoint; Jubilife's actions add -// to it. -template -class ConsoleMartLadyActionsT : public Base{ -public: - ConsoleMartLadyActionsT(SingleSwitchProgramEnvironment& env, ProControllerContext& context) - : m_env(env) - , m_context(context) - , m_center(env.console, context) - {} - - virtual std::string save_screen_location() override{ return m_center.save_screen_location(); } - virtual Outcome walk( - std::vector& at, - const std::function& goal, - const std::string& label - ) override{ - return m_center.walk(at, goal, label); - } - virtual bool east_then_south_into_sandgem() override{ return m_center.east_then_south_into_sandgem(); } - virtual Outcome walk_in_town( - std::vector& at, const std::function& goal, const std::string& label - ) override{ - return m_center.walk_in_town(at, goal, label); - } - virtual DialogueRead read_sign(Direction facing, const std::string& label) override{ return m_center.read_sign(facing, label); } - virtual std::optional lit_doorway() override{ return m_center.lit_doorway(); } - virtual Outcome line_up_with_door() override{ return m_center.line_up_with_door(); } - virtual bool inside_pokemon_center() override{ return m_center.inside_pokemon_center(); } - virtual Outcome east_then_north_in_sandgem() override{ return m_center.east_then_north_in_sandgem(); } - virtual bool dialogue_showing() override{ return m_center.dialogue_showing(); } - virtual Outcome finish_dialogue() override{ return m_center.finish_dialogue(); } - - virtual Outcome walk_until_stopped(Direction direction, size_t tiles, const std::string& label) override{ - const bool across = direction == Direction::EAST || direction == Direction::WEST; - const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; - const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - // Time for every tile, and for the wall to read as still. - const Milliseconds limit = ROUTE_START_DELAY + 800ms + - (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN); - // Turn first: the first press of a new way only turns us, the screen - // holds still meanwhile, and that read as a wall two tiles into the - // walk west to the Lab's sign (r132a). - route_turn(m_env.console, m_context, direction); - const WalkResult walked = walk_until(m_env.console, m_context, run_from_wild_battle, x, y, limit, label); - m_env.console.log("[Walk] " + label + ": stopped on " + walk_stop_name(walked.stop) + "."); - if (walked.stop == WalkStop::TRANSITION || walked.stop == WalkStop::DIALOG){ - return outcome_failed("walk_failed", label + ": " + walk_stop_name(walked.stop) + " instead of a stop"); - } - // The leg was measured to end against a wall, so holding toward it - // for every tile's time leaves us there whatever the screen shows: - // people walking or a TV screen nearby kept it changing and the stop - // went unseen (r162a, r167a, r171a). Seeing the stop only ends the - // hold early; a wrong place is caught by the next gate (a sign, a - // door, someone who answers). - if (walked.stop == WalkStop::LIMIT){ - m_env.console.log("[Walk] " + label + ": held for " + std::to_string(tiles) + - " tiles' time without seeing the stop; taking the wall as reached."); - } - return outcome_ok(); - } - - virtual DialogueRead talk(Direction direction, Button next, const std::string& label) override{ - VideoStream& stream = m_env.console; - DialogueRead read; - route_turn(stream, m_context, direction); - pbf_press_button(m_context, BUTTON_A, 80ms, 0ms); - m_context.wait_for_all_requests(); - ShortDialogWatcher opened(COLOR_CYAN); - if (wait_until(stream, m_context, 2000ms, {{opened}}) != 0){ - stream.log("[Talk] " + label + ": no dialogue opened."); - return read; - } - read.opened = true; - finish_talk(read, next, label); - return read; - } - - virtual DialogueRead talk_walking(Direction direction, size_t tiles, const std::string& label) override{ - DialogueRead read; - const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; - const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - const bool across = direction == Direction::EAST || direction == Direction::WEST; - const Milliseconds limit = (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN) + 800ms; - const WalkResult walked = walk_to_talk(m_env.console, m_context, run_from_wild_battle, x, y, limit, label); - if (walked.stop != WalkStop::DIALOG){ - m_env.console.log("[Talk] " + label + ": nobody answered."); - return read; - } - read.opened = true; - finish_talk(read, BUTTON_A, label); - return read; - } - - virtual Outcome step(Direction direction, const std::string& label) override{ - // 200 ms took exactly one row on the console, turning included; 300 ms - // took one or two (2026-10-30). - const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; - const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - const WalkResult walk = walk_for(m_env.console, m_context, run_from_wild_battle, x, y, 200ms, label); - if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ - return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); - } - return outcome_ok(); - } - - - virtual Outcome walk_to_route_202_save_spot() override{ - return wrap("walk_failed", [&]{ walk_from_sandgem_center_to_route_202_save_spot( - m_env.console, m_context, {story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END)} - ); }); - } - virtual Outcome save_on_route_202() override{ - bool saved = false; - const Outcome outcome = wrap("save_failed", [&]{ saved = save_game(m_env, m_context, MAP_ROUTE_202); }); - if (outcome.succeeded() && !saved){ - return outcome_failed("save_failed", "the save screen did not read Route 202, or the save did not finish"); - } - return outcome; - } - -protected: - // After a talk opened: reads the first box, then presses `next` once - // per line, 1.5 s apart, and only while a box shows: a press after it - // has closed would start the talk over. - void finish_talk(DialogueRead& read, Button next, const std::string& label){ - VideoStream& stream = m_env.console; - // The first box's text, inside its white margins. - // For the log only; a talk is checked by its box opening and closing. - VideoSnapshot screen = stream.video().snapshot(); - if (screen && story_reads_text()){ - read.text = OCR::ocr_read(g_story_language, extract_box_reference(*screen.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); - } - stream.log("[Talk] " + label + ": the dialogue reads \"" + read.text + "\"."); - size_t quiet = 0; - // Barry's scene, to the Town Map, took more than 20 (r150a). - for (size_t presses = 0; presses < 80 && quiet < 2; presses++){ - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream, m_context, 1500ms, {{dialog}}) == 0){ - quiet = 0; - pbf_press_button(m_context, next, 80ms, 0ms); - }else{ - quiet++; - } - } - read.closed = !dialogue_showing(); - stream.log("[Talk] " + label + (read.closed ? ": closed." : ": still open.")); - } - -private: - static Outcome wrap(const char* failure, const std::function& step){ - try{ - step(); - return outcome_ok(); - }catch (OperationFailedException& e){ - return outcome_failed(failure, e.message()); - } - } - - SingleSwitchProgramEnvironment& m_env; - ProControllerContext& m_context; - ConsoleSandgemCenterActions m_center; -}; -using ConsoleMartLadyActions = ConsoleMartLadyActionsT; - - -void save_end_screenshot(SingleSwitchProgramEnvironment& env, const std::string& name){ - if (VideoSnapshot screen = env.console.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; - Filesystem::create_directories(Filesystem::Path(folder)); - const std::string path = folder + now_to_filestring() + "-" + name + ".png"; - screen.frame->save(path); - env.log("[Checkpoint] Screenshot: " + path); - } -} - -// Runs `checkpoint`, logging its events, and fails with a screenshot -// naming the event and why if it does not complete. -void run_on_console(SingleSwitchProgramEnvironment& env, Checkpoint& checkpoint, const char* name){ - const std::vector problems = Pokemon::lint_checkpoint(checkpoint); - for (const std::string& problem : problems){ - env.log("[Checkpoint] lint: " + problem, COLOR_RED); - } - RunState state; - // A screenshot as each event ends, so a run that passes can be - // checked step by step, not only by its end. - Pokemon::EventLogger log = [&env, &checkpoint](const std::string& line){ - env.log(line, COLOR_BLUE); - add_wrapped_log(env.console.overlay(), line, COLOR_WHITE); - const size_t done = line.rfind(": done"); - const size_t failed = line.find(": failed"); - if ((done != std::string::npos && done + 6 == line.size()) || failed != std::string::npos){ - const size_t slash = line.find(checkpoint.id + "/"); - const size_t colon = line.find(':', slash == std::string::npos ? 0 : slash); - const std::string event = slash == std::string::npos || colon == std::string::npos - ? "event" : line.substr(slash + checkpoint.id.size() + 1, colon - slash - checkpoint.id.size() - 1); - std::string name = "step-" + checkpoint.id + "-" + event; - for (char& c : name){ - if (c == ' ' || c == '/'){ - c = '_'; - } - } - save_end_screenshot(env, name); - } - }; - const Pokemon::CheckpointReport report = Pokemon::run_checkpoint(state, checkpoint, log); - // Where it ended, to check by eye. - save_end_screenshot(env, "end-" + checkpoint.id); - if (!report.completed){ - const std::string where = report.failed_event == 0 ? "" : " at event " + - std::to_string(report.failed_event) + " " + report.failed_name; - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - std::string(name) + ": failed" + where + ": " + - report.failure.failure + (report.failure.detail.empty() ? "" : ": " + report.failure.detail), env.console); - } -} - -} - - -Checkpoint route202_to_sandgem_center_checkpoint(SandgemCenterActions& game){ - Checkpoint checkpoint; - checkpoint.id = "sandgem_center"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - checkpoint.events.push_back(confirm_on_route_202(game)); - checkpoint.events.push_back(walk_to_sandgem(game)); - // It ends below the Center's sign, facing it: lining up with the door - // itself overshot or walked in (r128, r129), and nothing after needs it. - checkpoint.events.push_back(walk_to_center_sign(game)); - checkpoint.events.push_back(read_center_sign(game)); - // No save: the save stays on Route 202, so the run can be repeated. - return checkpoint; -} - -void diagnostic_route202_to_sandgem_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleSandgemCenterActions game(env.console, context); - Checkpoint checkpoint = route202_to_sandgem_center_checkpoint(game); - run_on_console(env, checkpoint, ROUTE202_TO_SANDGEM_CENTER_NAME); -} - - -const char* const SANDGEM_MART_LADY_NAME = "Sandgem's Pokemon Center to the Poke Mart lady and back to Route 202 (events)"; - -TilePos route201_sign_west(){ - return story_maps().to_world(MAP_ROUTE_201, Route201::BORDER_SIGN.next(Direction::WEST)); -} - -TilePos route201_row_3_east_end(){ - // Row 3 is open east of her grass to Sandgem's fence: (64,3) is - // Sandgem's (0,6), a wall. - return story_maps().to_world(MAP_ROUTE_201, {63, 3}); -} - -// Where the Route 202 checkpoint ends: below the Center's sign. -Event on_doors_row(SandgemCenterActions& game){ - Event event; - event.name = "on_doors_row"; - event.why = "below the Center's sign, where the Route 202 checkpoint ends"; - event.act = [](RunState&){ - return outcome_ok(pose_on({below_sandgem_center_sign()}, Direction::NORTH)); - }; - event.expect.push_back(outside(game)); - return event; -} - -Checkpoint sandgem_mart_lady_checkpoint(MartLadyActions& game){ - // Walks that stop on their own, measured on the console (2026-10-30) and - // traced on the grid by Test_MartLadyWalk. - Checkpoint checkpoint; - checkpoint.id = "mart_lady"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - checkpoint.events.push_back(on_doors_row(game)); - checkpoint.events.push_back(wrapped_step("west_to_lab_sign", - "west along the doors' row until the Lab's sign stops us", "at_lab_sign", - [&game](RunState&){ return game.walk_until_stopped(Direction::WEST, 20, "West to the Lab's sign"); })); - checkpoint.events.push_back(read_sign(game, "read_lab_sign", "the Lab's sign", Direction::WEST, "lab")); - checkpoint.events.push_back(wrapped_step("west_along_the_path", - "a tile south onto the path, then west to the grass's edge", "at_grass_edge", - [&game](RunState&){ - const Outcome down = game.step(Direction::SOUTH, "South onto the path"); - return down.succeeded() ? game.walk_until_stopped(Direction::WEST, 28, "West along the path") : down; - })); - checkpoint.events.push_back(wrapped_step("north_to_the_trees", - "north until the trees stop us, on her row", "on_her_row", - [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 3, "North to the trees"); })); - checkpoint.events.push_back(talk_to_mart_lady(game)); - checkpoint.events.push_back(wrapped_step("back_to_route_201_sign", - "a tile south onto the signs' row, then east until Route 201's sign stops us", "at_route_201_sign", - [&game](RunState&){ - const Outcome down = game.step(Direction::SOUTH, "South onto the signs' row"); - return down.succeeded() ? game.walk_until_stopped(Direction::EAST, 16, "East to the Route 201 sign") : down; - })); - checkpoint.events.push_back(read_sign(game, "read_route_201_sign", "Route 201's sign", Direction::EAST, "201")); - checkpoint.events.push_back(wrapped_step("east_along_the_path", - "a tile south onto the path, east to the trees east of the Mart, north a row", "at_front_row_east_end", - [&game](RunState&){ - const Outcome down = game.step(Direction::SOUTH, "South onto the path"); - if (!down.succeeded()){ - return down; - } - const Outcome east = game.walk_until_stopped(Direction::EAST, 40, "East along the path"); - return east.succeeded() ? game.walk_until_stopped(Direction::NORTH, 2, "North to the front row") : east; - })); - // Back to where 00.04 saves with its Potions, as Segment 00 walks it - // from the trees east of the Mart. - checkpoint.events.push_back(wrapped_step("walk_to_route_202_save_spot", - "from the trees east of the Mart to Route 202's save spot", "at_route_202_save_spot", - [&game](RunState& state){ - const Outcome walked = game.walk_to_route_202_save_spot(); - if (walked.succeeded()){ - return outcome_ok(pose_on({story_maps().to_world(MAP_ROUTE_202, Route202::SAVE_SPOT)})); - } - return walked; - })); - // The save the checkpoint starts from stands there, so it can be repeated. - checkpoint.save = [&game](RunState&){ return game.save_on_route_202(); }; - return checkpoint; -} - -void mart_lady_without_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleMartLadyActions game(env, context); - Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); - checkpoint.save = {}; - run_on_console(env, checkpoint, SANDGEM_MART_LADY_NAME); -} -void diagnostic_sandgem_mart_lady(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleMartLadyActions game(env, context); - Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); - run_on_console(env, checkpoint, SANDGEM_MART_LADY_NAME); -} - - - - -// --------------------------------------------------------------------------- -// Jubilife: 00.05a and 00.05b -// --------------------------------------------------------------------------- - -const char* const JUBILIFE_CENTER_NAME = "00.05a Jubilife: steps to the Pokemon Center (events)"; -const size_t SCHOOL_TILES_WEST_OF_CENTER = 11; -// The School's lamp, east of its door, from its doorsteps' row at night -// (2026-10-30): at 0.65 to 0.69 of the screen across the door takes us in; -// at 0.61 (right of it) and 0.75 (past it) its front stops us. A tile is -// 0.065 of the screen. The box leaves out the Center's lamp further east. -const double SCHOOL_LAMP_X_AT_DOOR = 0.67; -const ImageFloatBox SCHOOL_LAMP_BOX(0.55, 0.20, 0.30, 0.10); -const char* const JUBILIFE_SCHOOL_NAME = "00.05b Jubilife: Pokemon Center to the Trainers' School (events)"; - -Checkpoint jubilife_center_checkpoint(JubilifeActions& game){ - Checkpoint checkpoint; - checkpoint.id = "jubilife_center"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - checkpoint.events.push_back(wrapped_step("up_the_steps", - "a tile west of the save, then up the steps onto the main road", "on_main_road", - [&game](RunState&){ return game.climb_the_steps(); })); - // Every leg walks until a wall stops it, measured on the console - // (2026-10-30): the fence's end west of the steps; the sign by the lamp - // west of the road; across the road, the house east of it; and north up - // the east sidewalk, the Center's doorway. The kids south of the Center - // can push us part of a tile aside, so the heal lines up by sight. - checkpoint.events.push_back(wrapped_step("west_to_the_fence", - "west until the fence's end stops us", "at_fence_end", - [&game](RunState&){ return game.walk_until_stopped(Direction::WEST, 8, "West to the fence's end"); })); - checkpoint.events.push_back(wrapped_step("north_to_the_sign", - "north until the sign by the lamp west of the road stops us", "at_sign", - [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 8, "North to the sign"); })); - checkpoint.events.push_back(wrapped_step("east_to_the_house", - "east across the road until the house east of it stops us", "at_east_house", - [&game](RunState&){ return game.walk_until_stopped(Direction::EAST, 14, "East to the house"); })); - checkpoint.events.push_back(wrapped_step("north_to_the_center", - "north up the east sidewalk until the Pokemon Center stops us", "at_center", - [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 14, "North to the Pokemon Center"); })); - { - Event heal = wrapped_step("heal", - "in, the nurse heals, out onto the doorstep", "healed", - [&game](RunState&){ return game.heal_at_center(); }); - // r143a's heal said it had walked out while still inside. - heal.expect.push_back(condition("out of the Pokemon Center, in the overworld", [&game](RunState&){ - return !game.indoors(); - })); - checkpoint.events.push_back(std::move(heal)); - } - return checkpoint; -} - -Checkpoint jubilife_school_checkpoint(JubilifeActions& game){ - Checkpoint checkpoint; - checkpoint.id = "jubilife_school"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - Event into = wrapped_step("into_the_school", - "west from the Center's doorstep, counting tiles, north through the School's door", "in_school", - [&game](RunState&){ - // Measured on the console (2026-10-30): from the Center's doorstep, - // a tap to face west and eleven one-tile holds put us under the - // School's doorway; twelve is past it. A hold ends a part of a - // tile off (back east, eleven stopped beside the Center's door), - // so if the School's front stops us, nudge half a tile each way. - game.turn(Direction::WEST); - for (size_t tiles = 0; tiles < SCHOOL_TILES_WEST_OF_CENTER; tiles++){ - const Outcome moved = game.tile(Direction::WEST, "West to the School's door"); - if (!moved.succeeded()){ - return moved; - } - } - // At night the lamp east of the door says how far off we are. - for (size_t looks = 0; looks < 3; looks++){ - const std::optional across = game.school_door_across(); - // The door took us from -0.19 and 0.19 but not -0.34 (r148b). - if (!across || std::abs(*across) <= 0.3){ - break; - } - game.step_across(*across, "Line up with the School's door by its lamp"); - } - if (game.through_door_north("Trainers' School door")){ - return outcome_ok(); - } - for (Direction side : {Direction::EAST, Direction::WEST, Direction::WEST}){ - game.nudge(side, "Line up with the School's door"); - if (game.through_door_north("Trainers' School door")){ - return outcome_ok(); - } - } - return outcome_failed("door_failed", "the School's door did not take us from 11 tiles west of the Center's doorstep, or half a tile either side"); - }); - // It starts on the Center's doorstep: outside, in the overworld. - into.require.push_back(condition("out of the Pokemon Center, in the overworld", [&game](RunState&){ - return !game.indoors(); - })); - checkpoint.events.push_back(std::move(into)); - return checkpoint; -} - -const char* const JUBILIFE_BARRY_NAME = "00.05c Jubilife: Barry in the Trainers' School (events)"; - -Checkpoint jubilife_barry_checkpoint(JubilifeActions& game){ - Checkpoint checkpoint; - checkpoint.id = "jubilife_barry"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - // Barry stands at the blackboard a tile west of the door's column, and - // that column is clear to the back wall (shots from inside the door). - // A tile west by a hold went two in r149a and the boy at the table - // stopped us: stay on the door's column, north to the wall, face west. - checkpoint.events.push_back(wrapped_step("up_to_barry", - "north up the door's column until the back wall stops us, beside Barry", "at_barry", - [&game](RunState&){ return game.walk_until_stopped(Direction::NORTH, 10, "North to the back wall"); })); - { - std::shared_ptr read = std::make_shared(); - Event talk; - talk.name = "talk_to_barry"; - talk.why = "Barry gives the Town Map"; - talk.skip_if.push_back(condition("talked_to_barry", [](RunState& state){ return state.flags.count("talked_to_barry") > 0; })); - talk.act = [&game, read](RunState& state){ - *read = game.talk(Direction::WEST, BUTTON_A, "Barry"); - if (read->opened && read->closed){ - state.flags.insert("talked_to_barry"); - } - return outcome_ok(); - }; - talk.expect.push_back(condition("his dialogue opened and was closed", [read](RunState&){ - return read->opened && read->closed; - })); - talk.recoveries.push_back(recovery("expect_failed", RecoveryKind::REANCHOR, 1, [&game](RunState&){ - return game.walk_until_stopped(Direction::NORTH, 4, "North to the back wall"); - }, "not beside him: north until the back wall stops us")); - checkpoint.events.push_back(std::move(talk)); - } - checkpoint.events.push_back(wrapped_step("out_of_the_school", - "straight south down the door's column and out", "out_of_school", - [&game](RunState&){ return game.leave_south("Out of the Trainers' School"); })); - return checkpoint; -} - -const char* const JUBILIFE_PRESIDENT_NAME = "00.05d Jubilife: the Poketch Company president (events)"; - -Checkpoint jubilife_president_checkpoint(JubilifeActions& game){ - Checkpoint checkpoint; - checkpoint.id = "jubilife_president"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - checkpoint.events.push_back(wrapped_step("into_the_road", - "a tap to face east, four one-tile steps into the main road", "in_road", - [&game](RunState&){ - game.turn(Direction::EAST); - for (size_t tiles = 0; tiles < 4; tiles++){ - const Outcome moved = game.tile(Direction::EAST, "East into the road"); - if (!moved.succeeded()){ - return moved; - } - } - return outcome_ok(); - })); - { - std::shared_ptr read = std::make_shared(); - Event meet; - meet.name = "meet_the_president"; - meet.why = "the president sends us for the three clowns' coupons"; - meet.skip_if.push_back(condition("met_president", [](RunState& state){ return state.flags.count("met_president") > 0; })); - meet.act = [&game, read](RunState& state){ - *read = game.walk_until_talked_to(6, "The Poketch Company president"); - if (read->opened && read->closed){ - state.flags.insert("met_president"); - } - return outcome_ok(); - }; - meet.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ - return read->opened && read->closed; - })); - checkpoint.events.push_back(std::move(meet)); - } - return checkpoint; -} - -const char* const JUBILIFE_CLOWN_MART_NAME = "00.05e Jubilife: the clown by the Poke Mart (events)"; - -Checkpoint jubilife_clown_mart_checkpoint(JubilifeActions& game){ - Checkpoint checkpoint; - checkpoint.id = "jubilife_clown_mart"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - checkpoint.events.push_back(wrapped_step("east_to_the_lamp", - "east until the lamp by the Pokemon Center stops us", "at_center_lamp", - [&game](RunState&){ return game.walk_until_stopped(Direction::EAST, 8, "East to the Center's lamp"); })); - checkpoint.events.push_back(wrapped_step("up_to_the_mart_road", - "a tile west, then a second north onto the road between the Center and the Mart", "on_mart_road", - [&game](RunState&){ - game.turn(Direction::WEST); - const Outcome west = game.tile(Direction::WEST, "West of the lamp"); - return west.succeeded() ? game.hold(Direction::NORTH, 1000, "North to the Mart's road") : west; - })); - checkpoint.events.push_back(wrapped_step("east_to_the_building", - "east along the road until the tall building stops us", "at_east_building", - [&game](RunState&){ return game.walk_until_stopped(Direction::EAST, 14, "East to the building"); })); - { - std::shared_ptr read = std::make_shared(); - Event talk; - talk.name = "talk_to_clown"; - talk.why = "the clown asks a question (Yes is right) and gives Coupon 1"; - talk.skip_if.push_back(condition("coupon_1", [](RunState& state){ return state.flags.count("coupon_1") > 0; })); - talk.act = [&game, read](RunState& state){ - // He may be what stopped the walk east (r155c, 2.8 s against - // 2.2): ask ahead first. Else he stands a row south and a tile - // west: try that row, then one more, facing west each time. - *read = game.talk(Direction::EAST, BUTTON_A, "Clown by the Poke Mart"); - for (size_t rows = 0; rows < 2 && !read->opened; rows++){ - const Outcome down = game.step(Direction::SOUTH, "A row south"); - if (!down.succeeded()){ - return down; - } - game.turn(Direction::WEST); - *read = game.talk(Direction::WEST, BUTTON_A, "Clown by the Poke Mart"); - } - if (read->opened && read->closed){ - state.flags.insert("coupon_1"); - } - return outcome_ok(); - }; - talk.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ - return read->opened && read->closed; - })); - checkpoint.events.push_back(std::move(talk)); - } - return checkpoint; -} - -const char* const JUBILIFE_CLOWN_COMPANY_NAME = "00.05f Jubilife: the clown by the Poketch Company (events)"; - -Checkpoint jubilife_clown_company_checkpoint(JubilifeActions& game){ - Checkpoint checkpoint; - checkpoint.id = "jubilife_clown_company"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - checkpoint.events.push_back(wrapped_step("to_the_lamp", - "west, south and west to the lamp at the long road's west end, north a row", "at_west_lamp", - [&game](RunState&){ - for (const auto& [direction, tiles, label] : { - // The TV station's screen keeps the picture moving after we - // stop: r161 saw the stop only at 14 tiles' time, r162a not - // within it. - std::tuple{Direction::WEST, 20, "West to the hedges"}, - {Direction::SOUTH, 10, "South to the long road"}, - {Direction::WEST, 26, "West to the lamp"}, - {Direction::NORTH, 6, "North by the lamp"}, - }){ - const Outcome walked = game.walk_until_stopped(direction, tiles, label); - if (!walked.succeeded()){ - return walked; - } - } - return outcome_ok(); - })); - // The clown wanders the plaza behind the hedges, and a walk can pass him - // by. Measured on the console (2026-10-30): from the lamp, the - // Poketch Company's sign is six tiles east along the hedges (taps put us - // there or a tile past it); it reads facing north, and the tile east of - // it is a gap in the hedges into the plaza. A step into the gap, then A - // every second or so, talked to him within seconds: he comes by. So we - // tap four tiles, then go a tile at a time asking north (him, or the - // sign), and wait in the gap. `progress` keeps a rerun after a talk - // interrupt from walking the tiles again. - { - struct Progress{ bool tapped = false; size_t tiles = 0; bool in_gap = false; }; - std::shared_ptr progress = std::make_shared(); - std::shared_ptr read = std::make_shared(); - Event talk; - talk.name = "talk_to_clown"; - talk.why = "the clown asks a question (Yes is right) and gives Coupon 3"; - talk.skip_if.push_back(condition("coupon_3", [](RunState& state){ return state.flags.count("coupon_3") > 0; })); - talk.act = [&game, read, progress](RunState& state){ - const auto is_sign = [](const DialogueRead& r){ - return r.text.find("World") != std::string::npos || r.text.find("Company") != std::string::npos; - }; - // Him north of us, or the sign. - const auto ask = [&](Direction direction) -> bool{ - *read = game.talk(direction, BUTTON_A, "Clown by the Poketch Company"); - if (read->opened && is_sign(*read)){ - *read = DialogueRead(); - } - return read->opened; - }; - if (!progress->tapped){ - game.turn(Direction::EAST); - const Outcome moved = game.taps(Direction::EAST, 4, "East along the hedges"); - if (!moved.succeeded()){ - return moved; - } - progress->tapped = true; - } - // A turn tap the way we already face is a step (r161c walked into - // the plaza so), so we face east between two asks north. - while (!progress->in_gap && progress->tiles < 5){ - if (ask(Direction::NORTH)){ - break; - } - game.turn(Direction::EAST); - const DialogueRead sign = game.read_sign(Direction::NORTH, "the Poketch Company's sign"); - if (sign.opened && !is_sign(sign)){ - // Him after all (or text OCR missed); his talk was - // closed with B: ask again. - game.turn(Direction::EAST); - if (ask(Direction::NORTH)){ - break; - } - } - game.turn(Direction::EAST); - const Outcome moved = game.taps(Direction::EAST, 1, "On along the hedges"); - if (!moved.succeeded()){ - return moved; - } - progress->tiles++; - if (sign.opened){ - // East of the sign: north is the gap. - const Outcome up = game.step(Direction::NORTH, "Into the gap beside the sign"); - if (!up.succeeded()){ - return up; - } - progress->in_gap = true; - } - } - // Up to about a minute and a half; west of the gap is the sign. - // East first: the step into the gap left us facing north. - for (size_t round = 0; round < 18 && progress->in_gap && !read->opened; round++){ - for (Direction direction : {Direction::EAST, Direction::NORTH, Direction::WEST}){ - if (ask(direction)){ - break; - } - } - } - if (read->opened && read->closed){ - state.flags.insert("coupon_3"); - } - return outcome_ok(); - }; - talk.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ - return read->opened && read->closed; - })); - checkpoint.events.push_back(std::move(talk)); - } - return checkpoint; -} - -const char* const JUBILIFE_POKETCH_NAME = "00.05g Jubilife: the Poketch from the president, then up to the street east to Route 203 (events)"; - -// Measured on the console (2026-10-30), from the gap beside the Poketch -// Company's sign: a row south, then east along the long road stops at the -// main road (on the building north of the Mart, or on the green-haired man -// who wanders the crossing; either leaves us in the main road). Six rows -// down that road, east stops against the Poke Mart's west wall: the column -// between the Mart's lamp and the Center's, so south stops above the -// Center's lamp. The president stands still a row or two north of that -// row, a few tiles west, so from a row up we go west a tile at a time, -// asking ahead and north. Back the same way to the lamp; the Center's lit -// door is then on screen a tile west and a few rows south, and the heal -// lines up with it. -Checkpoint jubilife_poketch_checkpoint(JubilifeActions& game){ - Checkpoint checkpoint; - checkpoint.id = "jubilife_poketch"; - checkpoint.interrupts.push_back(dialogue_interrupt(game)); - const auto steps = [&game](Direction direction, size_t rows, const char* label){ - for (size_t i = 0; i < rows; i++){ - const Outcome moved = game.step(direction, label); - if (!moved.succeeded()){ - return moved; - } - } - return outcome_ok(); - }; - checkpoint.events.push_back(wrapped_step("to_the_main_road", - "a row south of the gap, then east along the long road until the main road stops us", "on_main_road", - [&game](RunState&){ - const Outcome down = game.step(Direction::SOUTH, "Out of the gap"); - // A hold, not a walk until still: when the wanderer is what stops - // us, his steps keep the screen moving and the stop is never seen - // (r167a). The stop took 5.9 s; holding on against it is harmless, - // and the Mart's wall puts the column right afterwards. - return down.succeeded() ? game.hold(Direction::EAST, 7500, "East along the long road") : down; - })); - checkpoint.events.push_back(wrapped_step("to_the_center_lamp", - "six rows down the main road, east to the Mart's wall, south to the Center's lamp", "at_center_lamp_north", - [&game, steps](RunState&){ - const Outcome down = steps(Direction::SOUTH, 6, "Down the main road"); - if (!down.succeeded()){ - return down; - } - const Outcome east = game.walk_until_stopped(Direction::EAST, 8, "East to the Mart's wall"); - return east.succeeded() ? game.walk_until_stopped(Direction::SOUTH, 8, "South to the Center's lamp") : east; - })); - { - std::shared_ptr read = std::make_shared(); - Event talk; - talk.name = "talk_to_president"; - talk.why = "the president trades the three coupons for the Poketch"; - talk.skip_if.push_back(condition("poketch", [](RunState& state){ return state.flags.count("poketch") > 0; })); - talk.act = [&game, read](RunState& state){ - const Outcome up = game.step(Direction::NORTH, "A row up from the lamp"); - if (!up.succeeded()){ - return up; - } - game.turn(Direction::WEST); - // Facing west: ask ahead, turn north and ask, turn back, one tile - // on. Turns only ever change the way we face, so none is a step. - for (size_t tiles = 0; tiles < 8 && !read->opened; tiles++){ - *read = game.talk_ahead(BUTTON_B, "The Poketch Company president"); - if (read->opened){ - break; - } - game.turn(Direction::NORTH); - *read = game.talk_ahead(BUTTON_B, "The Poketch Company president"); - if (read->opened){ - break; - } - game.turn(Direction::WEST); - const Outcome moved = game.taps(Direction::WEST, 1, "West along the road"); - if (!moved.succeeded()){ - return moved; - } - } - if (read->opened){ - // His last line asks us to try the R Button; a help panel - // about the Poketch follows, closed with B. - game.close_panel("The Poketch's help"); - } - if (read->opened && read->closed){ - state.flags.insert("poketch"); - } - return outcome_ok(); - }; - talk.expect.push_back(condition("his talk opened and was closed", [read](RunState&){ - return read->opened && read->closed; - })); - checkpoint.events.push_back(std::move(talk)); - } - // 00.05 ends here: we healed at the Center on arriving (00.05a) and have - // not fought since. 01.01 starts on the street east to Route 203, about - // ten rows north (the long road's crossing is six rows above the Mart's - // wall, which is four above the talk; r164a, by hand). A row off is - // fine: 01.01 walks east and tries the rows either side at a wall. The - // president walks off north after his talk, up our way (r164a): let him. - checkpoint.events.push_back(wrapped_step("up_to_the_route_203_street", - "let the president walk off, then ten rows north to the street east to Route 203", "on_route_203_street", - [&game, steps](RunState&){ - for (size_t presses = 0; presses < 8; presses++){ - game.close_panel("Waiting for the president to walk off"); - } - return steps(Direction::NORTH, 10, "Up the main road"); - })); - return checkpoint; -} - -class ConsoleJubilifeActions : public ConsoleMartLadyActionsT{ -public: - ConsoleJubilifeActions(SingleSwitchProgramEnvironment& env, ProControllerContext& context) - : ConsoleMartLadyActionsT(env, context) - , m_stream(env.console) - , m_context(context) - {} - virtual Outcome climb_the_steps() override{ - route_turn(m_stream, m_context, Direction::WEST); - walk_for(m_stream, m_context, run_from_wild_battle, -1, 0, 200ms, "To the steps"); - const WalkResult up = walk_for(m_stream, m_context, run_from_wild_battle, 0, 1, 480ms, "Up the steps"); - if (up.stop == WalkStop::DIALOG || up.stop == WalkStop::TRANSITION){ - return outcome_failed("walk_failed", std::string("up the steps: ") + walk_stop_name(up.stop)); - } - return outcome_ok(); - } - virtual Outcome heal_at_center() override{ - try{ - heal_at_pokemon_center(m_stream, m_context, "Jubilife Pokemon Center"); - }catch (OperationFailedException& e){ - return outcome_failed("heal_failed", e.message()); - } - return outcome_ok(); - } - virtual void turn(Direction direction) override{ - route_turn(m_stream, m_context, direction); - } - virtual std::optional school_door_across() override{ - m_context.wait_for_all_requests(); - VideoSnapshot screen = m_stream.video().snapshot(); - if (!screen){ - return std::nullopt; - } - const std::optional lamp = find_lit_lamp_x(*screen.frame, SCHOOL_LAMP_BOX); - if (!lamp){ - m_stream.log("[Route] School's door: no lit lamp on screen."); - return std::nullopt; - } - const double across = tiles_across_at(*lamp) - tiles_across_at(SCHOOL_LAMP_X_AT_DOOR); - m_stream.log("[Route] School's door: the lamp at " + std::to_string(*lamp) + " of the screen, the door " + - std::to_string(across) + " tiles across."); - return across; - } - virtual void step_across(double tiles, const std::string& label) override{ - m_stream.log("[Route] " + label + ": " + std::to_string(tiles) + " tiles across."); - move_across(m_stream, m_context, tiles); - m_context.wait_for_all_requests(); - } - virtual DialogueRead walk_until_talked_to(size_t tiles, const std::string& label) override{ - DialogueRead read; - const Milliseconds limit = (int64_t)tiles * ROUTE_MS_PER_TILE_UP_DOWN + 800ms; - walk_until(m_stream, m_context, run_from_wild_battle, 0, 1, limit, label); - // He walks up after the "!" and only then talks. - ShortDialogWatcher opened(COLOR_CYAN); - if (wait_until(m_stream, m_context, 5000ms, {{opened}}) != 0){ - m_stream.log("[Talk] " + label + ": no one talked."); - return read; - } - read.opened = true; - PokemonBDSP::finish_dialogue(m_stream, m_context, BUTTON_B, label); - read.closed = true; - return read; - } - virtual Outcome hold(Direction direction, int64_t ms, const std::string& label) override{ - const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; - const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - const WalkResult walk = walk_for(m_stream, m_context, run_from_wild_battle, x, y, Milliseconds(ms), label); - if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ - return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); - } - return outcome_ok(); - } - virtual bool indoors() override{ - m_context.wait_for_all_requests(); - VideoSnapshot screen = m_stream.video().snapshot(); - return screen && PokemonBDSP::indoors(*screen.frame); - } - virtual Outcome leave_south(const std::string& label) override{ - try{ - leave_door_south(m_stream, m_context, run_from_wild_battle, label); - }catch (OperationFailedException& e){ - return outcome_failed("walk_failed", e.message()); - } - return outcome_ok(); - } - virtual void nudge(Direction direction, const std::string& label) override{ - route_turn(m_stream, m_context, direction); - walk_for(m_stream, m_context, run_from_wild_battle, direction == Direction::EAST ? 1 : -1, 0, 80ms, label); - } - virtual Outcome tile(Direction direction, const std::string& label) override{ - const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; - const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - const WalkResult walk = walk_for(m_stream, m_context, run_from_wild_battle, x, y, 160ms, label); - if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ - return outcome_failed("walk_failed", label + ": " + walk_stop_name(walk.stop)); - } - return outcome_ok(); - } - virtual Outcome taps(Direction direction, size_t tiles, const std::string& label) override{ - const double x = direction == Direction::EAST ? 1 : direction == Direction::WEST ? -1 : 0; - const double y = direction == Direction::NORTH ? 1 : direction == Direction::SOUTH ? -1 : 0; - m_stream.log("[Walk] " + label + ": " + std::to_string(tiles) + " taps."); - for (size_t i = 0; i < tiles; i++){ - pbf_move_left_joystick(m_context, {x, y}, 160ms, 400ms); - } - pbf_wait(m_context, 800ms); - m_context.wait_for_all_requests(); - return outcome_ok(); - } - virtual DialogueRead talk_ahead(Button next, const std::string& label) override{ - DialogueRead read; - pbf_press_button(m_context, BUTTON_A, 80ms, 0ms); - m_context.wait_for_all_requests(); - ShortDialogWatcher opened(COLOR_CYAN); - if (wait_until(m_stream, m_context, 2000ms, {{opened}}) != 0){ - m_stream.log("[Talk] " + label + ": no dialogue opened."); - return read; - } - read.opened = true; - this->finish_talk(read, next, label); - return read; - } - virtual void close_panel(const std::string& label) override{ - m_stream.log("[Talk] " + label + ": B to close the panel."); - pbf_press_button(m_context, BUTTON_B, 80ms, 1500ms); - m_context.wait_for_all_requests(); - } - virtual bool through_door_north(const std::string& label) override{ - m_context.wait_for_all_requests(); - VideoSnapshot before = m_stream.video().snapshot(); - const WalkResult up = walk_until(m_stream, m_context, run_from_wild_battle, 0, 1, 1200ms, label); - m_context.wait_for(1500ms); - if (up.stop == WalkStop::TRANSITION){ - return true; - } - // The School's fade is not always black enough to read as one: r146b - // went in (changes 156, 261) and read "limit". A whole new scene is - // through, as leave_door_south() counts it. - VideoSnapshot after = m_stream.video().snapshot(); - const bool through = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE; - if (through){ - m_stream.log("[Route] " + label + ": a new scene; through the door."); - } - return through; - } -private: - VideoStream& m_stream; - ProControllerContext& m_context; -}; - -void diagnostic_jubilife_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleJubilifeActions game(env, context); - Checkpoint checkpoint = jubilife_center_checkpoint(game); - run_on_console(env, checkpoint, JUBILIFE_CENTER_NAME); -} - -void diagnostic_jubilife_clown_company(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleJubilifeActions game(env, context); - Checkpoint checkpoint = jubilife_clown_company_checkpoint(game); - run_on_console(env, checkpoint, JUBILIFE_CLOWN_COMPANY_NAME); -} - -void diagnostic_jubilife_poketch(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleJubilifeActions game(env, context); - Checkpoint checkpoint = jubilife_poketch_checkpoint(game); - run_on_console(env, checkpoint, JUBILIFE_POKETCH_NAME); -} - -void diagnostic_jubilife_clown_mart(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleJubilifeActions game(env, context); - Checkpoint checkpoint = jubilife_clown_mart_checkpoint(game); - run_on_console(env, checkpoint, JUBILIFE_CLOWN_MART_NAME); -} - -void diagnostic_jubilife_president(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleJubilifeActions game(env, context); - Checkpoint checkpoint = jubilife_president_checkpoint(game); - run_on_console(env, checkpoint, JUBILIFE_PRESIDENT_NAME); -} - -void diagnostic_jubilife_barry(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleJubilifeActions game(env, context); - Checkpoint checkpoint = jubilife_barry_checkpoint(game); - run_on_console(env, checkpoint, JUBILIFE_BARRY_NAME); -} - -void diagnostic_jubilife_school(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - ConsoleJubilifeActions game(env, context); - Checkpoint checkpoint = jubilife_school_checkpoint(game); - run_on_console(env, checkpoint, JUBILIFE_SCHOOL_NAME); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h deleted file mode 100644 index 4415b1b61b..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h +++ /dev/null @@ -1,344 +0,0 @@ -/* BDSP AutoStory - Route 202 to Sandgem's Pokemon Center, as events - * - * From: https://github.com/PokemonAutomation/ - * - * A test checkpoint, and the first written with the story event model - * (Pokemon/Navigation/Pokemon_StoryEvents.h): from Route 202's save spot - * under the south ledge, south into Sandgem Town and in through the - * Pokemon Center's door. It neither heals nor saves, so the save stays on - * Route 202 and the run can be repeated. - * - * The game steps are behind SandgemCenterActions, so the events and their - * recoveries can be run against a scripted fake. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_SandgemCenterEvents_H -#define PokemonAutomation_PokemonBDSP_AutoStory_SandgemCenterEvents_H - -#include -#include -#include -#include -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" -#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" -#include "Pokemon/Navigation/Pokemon_StoryEvents.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// What a talk showed: whether a dialogue opened, whether it had closed -// after, and what OCR read in its first box. -struct DialogueRead{ - bool opened = false; - bool closed = false; - std::string text; -}; - -// The game steps the checkpoint is made of. -class SandgemCenterActions{ -public: - virtual ~SandgemCenterActions() = default; - - // The map named on the save screen's card, as read (see - // read_location_from_save_screen()); back on the overworld after. - virtual std::string save_screen_location() = 0; - - // Walks the story maps from any of `at` until every tile we may be on - // is `goal`, and sets `at` to those tiles. A walk onto another map must - // see its location banner on the way (walk_route()). - virtual Pokemon::Outcome walk( - std::vector& at, - const std::function& goal, - const std::string& label - ) = 0; - - // East until the trees stop us, then south until a wall does. Returns - // whether Sandgem Town's location banner showed on the way. - virtual bool east_then_south_into_sandgem() = 0; - - // walk() with town_plan_costs(): short open legs allowed, so the plan - // can step a tile across and go straight through a gap. - virtual Pokemon::Outcome walk_in_town( - std::vector& at, - const std::function& goal, - const std::string& label - ) = 0; - - // Faces `facing` and presses A: whether a box opened at the bottom of - // the screen (a sign's may be green, not a dialogue's), what OCR reads - // in it, and whether B closed it. - virtual DialogueRead read_sign(Pokemon::Direction facing, const std::string& label) = 0; - - // The lit doorway nearest the middle of the screen, if any. - virtual std::optional lit_doorway() = 0; - - // From the doors' row: lines up with the lit doorway on screen, across, - // without going in (line_up_with_lit_door()). - virtual Pokemon::Outcome line_up_with_door() = 0; - - // Whether a Pokemon Center's inside shows (pokemon_center_inside()). - virtual bool inside_pokemon_center() = 0; - - // Anywhere in Sandgem Town: east until a wall stops us, then north - // until one does, which ends on the doors' row or above it, and never - // walks into a door. - virtual Pokemon::Outcome east_then_north_in_sandgem() = 0; - - virtual bool dialogue_showing() = 0; - virtual Pokemon::Outcome finish_dialogue() = 0; -}; - - -// Where the walk south stops in Sandgem Town, in world tiles of -// story_maps(): Route 202's column 30 runs into Sandgem's column 29, and -// walking south from Route 202 stops at (29,5), above the Poke Mart, where -// the road narrows (the console counted it as two rows above the signs' row -// on the image grid; the decomp has three, with (29,6) a wall). -Pokemon::TilePos sandgem_entry_stop(); - -// Below the Center's sign, three tiles east of its door: where 00.04's -// heal walks to (r75), with the doorway in sight. -Pokemon::TilePos below_sandgem_center_sign(); - -// On the doors' row, east of the Lab's sign, where a walk west along the -// row stops (checked on the console). -Pokemon::TilePos east_of_lab_sign(); - -// Whether `doorway` is straight ahead on the row below it: about a row up, -// within half a tile across. -bool doorway_lined_up(const LitDoorway& doorway); - -// Whether a sign's OCR text has `key` (lower case), ignoring case. -bool sign_text_matches(const std::string& text, const std::string& key); - -// Whether `doorway` is the Center's, seen from the row below its door: up -// the screen by about a row, and no further across than the Center's sign. -// The Mart's doorway, seven tiles east of the sign, is too far off the -// middle to be found from there (find_lit_doorway()). -bool center_doorway_at_front_row(const LitDoorway& doorway); - - - -// The checkpoint's events, run against `game`, which must outlive it. It -// ends in front of the Center's door, not inside. -// Events: -// 1. confirm_on_route_202 the save screen reads Route 202 -// 2. walk_to_sandgem the pose is on Sandgem Town -// 3. walk_to_center_sign below the Center's sign (planned, town costs) -// 4. read_center_sign it reads, facing north -// 4. at_center_door the doorway straight ahead, a row up, outside -Pokemon::Checkpoint route202_to_sandgem_center_checkpoint(SandgemCenterActions& game); - -extern const char* const ROUTE202_TO_SANDGEM_CENTER_NAME; - -// DIAGNOSTIC ONLY: Remove before pushing to production. -// Runs the checkpoint on the console, from Route 202's save spot. Fails -// with a screenshot, naming the event and why, if it does not complete. -void diagnostic_route202_to_sandgem_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - - - -// The game steps of the Poke Mart lady checkpoint. -class MartLadyActions : public SandgemCenterActions{ -public: - // Walks `direction` until something stops us, for at most `tiles` - // tiles' time. Fails on a transition or a dialogue. - virtual Pokemon::Outcome walk_until_stopped(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; - - // Faces `direction` and presses A. If a dialogue opens, reads its first - // box, then presses `next` once per line, 1.5 s apart, while it shows. - virtual DialogueRead talk(Pokemon::Direction direction, Button next, const std::string& label) = 0; - - // One tile `direction`: the one hold that is not a walk until stopped. - // The walk after it stops on something that tells whether it took. - virtual Pokemon::Outcome step(Pokemon::Direction direction, const std::string& label) = 0; - - // Walks `direction`, pressing A after each short stretch, for at most - // `tiles` tiles' time until someone answers; then one A per line, 1.5 s - // apart, while the box shows. - virtual DialogueRead talk_walking(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; - - // As 00.04 walks it after the Potions, from the trees east of the Mart - // (the front row's east end) to Route 202's save spot, and the save - // there, which must read Route 202. - virtual Pokemon::Outcome walk_to_route_202_save_spot() = 0; - virtual Pokemon::Outcome save_on_route_202() = 0; -}; - -// West of Route 201's border sign, in world tiles of story_maps(): the -// only tile it is read from facing east. -Pokemon::TilePos route201_sign_west(); - -// The east end of the lady's row, against Sandgem's fence. -Pokemon::TilePos route201_row_3_east_end(); - -// From below Sandgem's Pokemon Center sign (where the Center checkpoint -// ends): to the Poke Mart lady and back to Route 202's save spot, where the -// save it starts from stands, and save there, so it can be repeated. Every -// leg but three one-tile steps walks until something stops it, as measured -// on the console (2026-10-30, CFR "walk"): -// 1. on_doors_row below the Center's sign -// 2. west_to_lab_sign west until the Lab's sign stops us -// 3. read_lab_sign facing west, "Pokemon Research Lab" -// 4. west_along_the_path south a tile, west to the grass's edge -// 5. north_to_the_trees north until the trees stop us (2 rows) -// 6. talk_to_mart_lady east until she stops us; she talks -// 7. back_to_route_201_sign south a tile, east until the sign stops us -// 8. read_route_201_sign facing east, "Route 201 -> Sandgem Town" -// 9. east_along_the_path south a tile, east to the trees, north a row -// 10. walk_to_route_202_save_spot the walk ended there (Segment 00's) -// then the save, which must read Route 202. -Pokemon::Checkpoint sandgem_mart_lady_checkpoint(MartLadyActions& game); - -extern const char* const SANDGEM_MART_LADY_NAME; - -// 00.04c's Potions: the Mart lady checkpoint, from the doors' row in front -// of Sandgem's Pokemon Center, without its save: it ends on Route 202's -// save spot, where 00.04d saves. -void mart_lady_without_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - -// DIAGNOSTIC ONLY: Remove before pushing to production. -// Runs the Mart lady checkpoint on the console, from in front of Sandgem's -// Pokemon Center door. -void diagnostic_sandgem_mart_lady(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - - -// The game steps of 00.05's first two checkpoints, in Jubilife. -class JubilifeActions : public MartLadyActions{ -public: - // From 00.05's save below the steps, against the wall east of them: a - // tile west and up the steps onto the main road. - virtual Pokemon::Outcome climb_the_steps() = 0; - // From in front of a Pokemon Center, its lit doorway on screen: heals - // and walks out. Ends on the Center's doorstep. - virtual Pokemon::Outcome heal_at_center() = 0; - // Faces `direction` without moving: a tap the game takes as a turn. - virtual void turn(Pokemon::Direction direction) = 0; - // Half a tile `direction` (EAST or WEST), facing it first. - virtual void nudge(Pokemon::Direction direction, const std::string& label) = 0; - // On the School's doorsteps' row, near its door: tiles across from the - // player to the door's column, read from the lit lamp east of it; - // nothing if no lamp is lit (by day). - virtual std::optional school_door_across() = 0; - // Moves across by `tiles` (positive: east). - virtual void step_across(double tiles, const std::string& label) = 0; - // Whether the screen shows a building's inside. - virtual bool indoors() = 0; - // Holds the stick `direction` for `ms`, from standing still. - virtual Pokemon::Outcome hold(Pokemon::Direction direction, int64_t ms, const std::string& label) = 0; - // Walks north for at most `tiles` tiles' time until someone's talk - // opens (a person who sees us walks up and talks), then reads it on - // with B, which never starts a talk over. - virtual DialogueRead walk_until_talked_to(size_t tiles, const std::string& label) = 0; - // Inside a building whose door is straight south: out through it. Fails - // if three walks south do not change the scene. - virtual Pokemon::Outcome leave_south(const std::string& label) = 0; - // One tile `direction` from standing still, facing that way already. - virtual Pokemon::Outcome tile(Pokemon::Direction direction, const std::string& label) = 0; - // `tiles` one-tile taps `direction` in a row, facing that way already, - // without waiting for the screen to settle in between: six of these - // moved exactly six tiles on the console, where six tile() calls moved - // seven (2026-10-30). - virtual Pokemon::Outcome taps(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; - // Presses A the way we face, without the turn talk() taps first: a turn - // tap the way we already face is a step. Reads a talk as talk() does. - virtual DialogueRead talk_ahead(Button next, const std::string& label) = 0; - // Presses B once: closes a help panel (the Poketch's) that is not a - // dialogue box. Does nothing in the overworld. - virtual void close_panel(const std::string& label) = 0; - // Walks north; true if a door took us through. - virtual bool through_door_north(const std::string& label) = 0; -}; - -// 00.05a: from 00.05's save below Jubilife's steps to the Pokemon Center, -// healed, on its doorstep. Measured on the console (2026-10-30): -// 1. up_the_steps a tile west, then north up the steps -// 2. west_to_the_fence west until the fence's end stops us -// 3. north_to_the_sign north until the sign by the west lamp stops us -// 4. east_to_the_house east across the road until the house stops us -// 5. north_to_the_center north until the Center's doorway stops us -// 6. heal in, the nurse, out onto the doorstep -// No save: the save stays below the steps, so it can be repeated. -Pokemon::Checkpoint jubilife_center_checkpoint(JubilifeActions& game); -extern const char* const JUBILIFE_CENTER_NAME; - -// 00.05b: from the Center's doorstep into the Trainers' School. Nothing on -// the doorsteps' row stops a walk, so the School's door is counted: eleven -// one-tile steps west (measured both ways), then north; half a tile either -// side if the School's front stops us. -// 1. into_the_school through the School's door -Pokemon::Checkpoint jubilife_school_checkpoint(JubilifeActions& game); -extern const char* const JUBILIFE_SCHOOL_NAME; - -// 00.05c: in the Trainers' School, from its door: Barry, who gives the Town -// Map, and back out onto the School's doorstep. -// 1. up_to_barry north up the door's column to the back wall -// 2. talk_to_barry face west: his scene, to "You obtained a Town Map!" -// 3. out_of_the_school south down the door's column and out -Pokemon::Checkpoint jubilife_barry_checkpoint(JubilifeActions& game); -extern const char* const JUBILIFE_BARRY_NAME; - -// 00.05d: from the School's doorstep, the Poketch Company president on the -// main road, who sends us for the clowns' coupons. Stepped through on the -// console (2026-10-30): four tiles east is the road; a couple of tiles -// north he sees us, walks up and talks. -// 1. into_the_road a tap east, four one-tile steps -// 2. meet_the_president north until his talk opens; read it with B -Pokemon::Checkpoint jubilife_president_checkpoint(JubilifeActions& game); -extern const char* const JUBILIFE_PRESIDENT_NAME; - -// 00.05e: from where the president leaves us, the clown by the Poke Mart, -// for Coupon 1. Stepped through on the console (2026-10-30): east stops -// at the lamp by the Center; a tile west and a second north put us on the -// road between the Center and the Mart; east along it stops at the tall -// building, a tile east and a row north of the clown. -// 1. east_to_the_lamp east until the lamp by the Center stops us -// 2. up_to_the_mart_road a tile west, north about four rows -// 3. east_to_the_building east until the building stops us -// 4. talk_to_clown a row south, face west, A; his question's -// default answer is right; Coupon 1 -Pokemon::Checkpoint jubilife_clown_mart_checkpoint(JubilifeActions& game); -extern const char* const JUBILIFE_CLOWN_MART_NAME; - -// 00.05f: from beside the TV station clown (after 00.05e and his coupon), -// the clown in front of the Poketch Company, for Coupon 3. Stepped through -// on the console (2026-10-30): west, south and west stop at the lamp at the -// long road's west end; north stops a row up; seven tiles east and north -// stop at the Poketch Company's front wall, between its doors. The clown -// wanders a tile or two by the sign south-west of there: sweep the rows -// below, pressing A. -// 1. to_the_lamp west, south, west, north: the lamp's corner -// 2. to_the_company seven tiles east, north to the front wall -// 3. talk_to_clown a row south: east 3, west 6 pressing A; a -// second row the same; Coupon 3 -Pokemon::Checkpoint jubilife_clown_company_checkpoint(JubilifeActions& game); -extern const char* const JUBILIFE_CLOWN_COMPANY_NAME; -// 00.05g: from where 00.05f leaves us (in the gap beside the Poketch -// Company's sign) to the president, who trades the coupons for the Poketch, -// then up the main road to the street east to Route 203. -Pokemon::Checkpoint jubilife_poketch_checkpoint(JubilifeActions& game); -extern const char* const JUBILIFE_POKETCH_NAME; -// DIAGNOSTIC ONLY: Remove before pushing to production. -void diagnostic_jubilife_clown_company(SingleSwitchProgramEnvironment& env, ProControllerContext& context); -void diagnostic_jubilife_poketch(SingleSwitchProgramEnvironment& env, ProControllerContext& context); -// DIAGNOSTIC ONLY: Remove before pushing to production. -void diagnostic_jubilife_clown_mart(SingleSwitchProgramEnvironment& env, ProControllerContext& context); -// DIAGNOSTIC ONLY: Remove before pushing to production. -void diagnostic_jubilife_president(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - -// DIAGNOSTIC ONLY: Remove before pushing to production. -void diagnostic_jubilife_barry(SingleSwitchProgramEnvironment& env, ProControllerContext& context); -// DIAGNOSTIC ONLY: Remove before pushing to production. -void diagnostic_jubilife_center(SingleSwitchProgramEnvironment& env, ProControllerContext& context); -void diagnostic_jubilife_school(SingleSwitchProgramEnvironment& env, ProControllerContext& context); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp deleted file mode 100644 index 40e1eb537d..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ /dev/null @@ -1,1387 +0,0 @@ -/* BDSP AutoStory - Segment 00 - * From player's bedroom to pokedex acquisition. - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "CommonFramework/GlobalAutoPaths.h" -#include "PokemonBDSP_AutoStory_Segment_00.h" -#include "PokemonBDSP_AutoStory_SandgemCenterEvents.h" -#include "../Data/PokemonBDSP_AutoStory_JubilifeCity.h" -#include "../Data/PokemonBDSP_AutoStory_Route202.h" -#include "../Data/PokemonBDSP_AutoStory_SandgemTown.h" -#include "../Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "../Utils/PokemonBDSP_AutoStory_Location.h" -#include "../Utils/PokemonBDSP_AutoStory_PokemonCenter.h" -#include "../Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "../Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" -#include "../Utils/PokemonBDSP_AutoStory_StoryExecutor.h" -#include "../PokemonBDSP_AutoStory_StoryTable.h" -#include "../Utils/PokemonBDSP_AutoStory_Walk.h" -#include "../Utils/PokemonBDSP_OverworldMatchers.h" - -#include -#include -#include -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" - -#include "Pokemon/Pokemon_Strings.h" -#include "Pokemon/Pokemon_Notification.h" - -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" -#include "PokemonBDSP/Programs/ShinyHunting/PokemonBDSP_StarterReset.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" -#include "CommonTools/VisualDetectors/ImageMatchDetector.h" - -#include "NintendoSwitch/Programs/DateManip/NintendoSwitch_DateManip.h" - -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" -#include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" - -using namespace std::chrono_literals; -using PokemonAutomation::Pokemon::MapSet; -using PokemonAutomation::Pokemon::TilePos; -using PokemonAutomation::Pokemon::TrainerSight; - -namespace PokemonAutomation { - namespace NintendoSwitch { - namespace PokemonBDSP { - // --------------------------------------------------------------------------- - // Starter selection - // --------------------------------------------------------------------------- - - // Watch before movement starts, release on the fade, and hold the - // following recorded wait until the destination is visible again. - static void recorded_move_until_transition( - VideoStream& stream, ProControllerContext& context, - double x, double y, Milliseconds duration, const std::string& label - ) { - context.wait_for_all_requests(); - stream.log("[Tutorial transition] " + label + ": approaching fade."); - BlackScreenWatcher black; - int ret = run_until(stream, context, - [x, y, duration](ProControllerContext& ctx) { - pbf_move_left_joystick(ctx, {x, y}, duration, 0ms); - // Keep observing a delayed fade after the recorded hold. - pbf_wait(ctx, 5000ms); - }, {{black}}); - if (ret != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Black transition not detected.", stream); - } - BlackScreenWatcher visible(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, - 100, 10, BlackScreenWatcher::FinderType::GONE, 500ms); - if (wait_until(stream, context, 15s, {{visible}}) != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Transition did not finish.", stream); - } - stream.log("[Tutorial transition] " + label + ": destination visible."); - } - - static void select_turtwig(VideoStream& stream, ProControllerContext& context) { - stream.log("[DEBUG] Turtwig Selection: Starting starter selection"); - - pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); - context.wait_for_all_requests(); - - ShortDialogWatcher watcher(COLOR_CYAN); - int ret = run_until( - stream, context, - [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, - { watcher } - ); - if (ret != 0) { - ShortDialogWatcher watcher_red(COLOR_RED); - ret = run_until( - stream, context, - [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, - { watcher_red } - ); - } - stream.log(ret == 0 ? "[DEBUG] Turtwig Selection: Confirmation dialogue detected" - : "[WARNING] Turtwig Selection: Dialogue detection failed, proceeding anyway"); - - pbf_wait(context, 400ms); - pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); - pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); - context.wait_for_all_requests(); - stream.log("[DEBUG] Turtwig Selection: Selection complete"); - } - - static void select_chimchar(VideoStream& stream, ProControllerContext& context) { - stream.log("[DEBUG] Chimchar Selection: Starting starter selection"); - - pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); - pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); - context.wait_for_all_requests(); - - ShortDialogWatcher watcher(COLOR_CYAN); - int ret = run_until( - stream, context, - [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, - { watcher } - ); - if (ret != 0) { - ShortDialogWatcher watcher_red(COLOR_RED); - ret = run_until( - stream, context, - [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, - { watcher_red } - ); - } - stream.log(ret == 0 ? "[DEBUG] Chimchar Selection: Confirmation dialogue detected" - : "[WARNING] Chimchar Selection: Dialogue detection failed, proceeding anyway"); - - pbf_wait(context, 400ms); - pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); - pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); - context.wait_for_all_requests(); - stream.log("[DEBUG] Chimchar Selection: Selection complete"); - } - - static void select_piplup(VideoStream& stream, ProControllerContext& context) { - stream.log("[DEBUG] Piplup Selection: Starting starter selection"); - - pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); - pbf_press_dpad(context, DPAD_RIGHT, 160ms, 840ms); - pbf_press_button(context, BUTTON_ZL, 160ms, 240ms); - context.wait_for_all_requests(); - - ShortDialogWatcher watcher(COLOR_CYAN); - int ret = run_until( - stream, context, - [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, - { watcher } - ); - if (ret != 0) { - ShortDialogWatcher watcher_red(COLOR_RED); - ret = run_until( - stream, context, - [](ProControllerContext& ctx) { ctx.wait_for(std::chrono::milliseconds(5000)); }, - { watcher_red } - ); - } - stream.log(ret == 0 ? "[DEBUG] Piplup Selection: Confirmation dialogue detected" - : "[WARNING] Piplup Selection: Dialogue detection failed, proceeding anyway"); - - pbf_wait(context, 400ms); - pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); - pbf_press_button(context, BUTTON_ZL, 80ms, 5000ms); - context.wait_for_all_requests(); - stream.log("[DEBUG] Piplup Selection: Selection complete"); - } - - // Navigation functions for each part of the tutorial, used in order; each relies on the game state the one before left. - // TODO (task #22): they replay Butters' recorded stick holds; the same walks are lines on the decomp grids in Data/PokemonBDSP_AutoStory_Segment00Lines, - // tested offline with the route records. Swap in walk_waypoints() over Segment00Lines::lines() only after a new-game run has confirmed where each scene leaves the player, with walk_through_grass()'s battle handling on the Route 201 grass. - - // --------------------------------------------------------------------------- - // Segment 00-01 begins - // Navigates from player's room to Route 201 through Mom/Barry dialogues. - // --------------------------------------------------------------------------- - - static void tutorial_part_1_navigation(SingleSwitchProgramEnvironment& env, - VideoStream& stream, ProControllerContext& context) { - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 527ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, 1.000000 }, 456ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 915ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, 1.000000 }, 642ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 557ms, 0ms); - pbf_wait(context, 978ms); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1112ms, 0ms); - pbf_wait(context, 4967ms); - - wait_for_dialogue(stream, context, "Phase 1 (Mom)"); - - pbf_mash_button(context, BUTTON_A, 5000ms); - pbf_wait(context, 300ms); - - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1662ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, -1.000000 }, 49ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1215ms, 0ms); - pbf_wait(context, 1419ms); - - wait_for_dialogue(stream, context, "Phase 2 (Mom Warning)"); - - pbf_mash_button(context, BUTTON_A, 2500ms); - pbf_wait(context, 300ms); - context.wait_for_all_requests(); - - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 680ms, 0ms); - - pbf_wait(context, 4000ms); - - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1748ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 18ms, 0ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 2763ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 58ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1364ms, 0ms); - pbf_wait(context, 4000ms); - context.wait_for_all_requests(); - - wait_for_dialogue(stream, context, "Phase 3 (Barry 1st)"); - pbf_mash_button(context, BUTTON_A, 6000ms); - pbf_wait(context, 800ms); - wait_for_dialogue(stream, context, "Phase 3 (Barry 1st)"); - pbf_mash_button(context, BUTTON_A, 6000ms); - pbf_wait(context, 800ms); - - pbf_wait(context, 1306ms); - context.wait_for_all_requests(); - stream.log("[Barry house] Entering; waiting for entry fade."); - BlackScreenWatcher entry_black; - int entry_ret = run_until(stream, context, - [](ProControllerContext& ctx) { - pbf_move_left_joystick(ctx, {0, 1}, 5000ms, 0ms); - }, {{entry_black}}); - if (entry_ret != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Barry house: Entry fade not detected.", stream); - } - // Release the joystick during loading; resume indoor movement only - // after the screen has stayed non-black for half a second. - BlackScreenWatcher entry_visible(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, - 100, 10, BlackScreenWatcher::FinderType::GONE, 500ms); - if (wait_until(stream, context, 15s, {{entry_visible}}) != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Barry house: Entry fade did not finish.", stream); - } - pbf_wait(context, 500ms); - context.wait_for_all_requests(); - stream.log("[Barry house] Entry settled; starting stair approach."); - // Align left after entry before the next upward stair approach. - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 400ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 1600ms, 0ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 2722ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1600ms, 0ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 501ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 652ms, 0ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 1817ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 975ms, 0ms); - pbf_wait(context, 1422ms); - - wait_for_dialogue(stream, context, "Phase 8 (Barry 2nd dialog)"); - pbf_mash_button(context, BUTTON_A, 10000ms); - - pbf_wait(context, 856ms); - - - // Recorded route from the top of Barry's stairs. Moves rounded to - // 100ms, waits to 50ms; transition waits gate both floor changes. - recorded_move_until_transition(stream, context, -1.0, 0.0, 900ms, - "Barry house downstairs"); - pbf_wait(context, 750ms); - pbf_move_left_joystick(context, {-1.0, 0.0}, 2000ms, 0ms); - pbf_wait(context, 250ms); - pbf_move_left_joystick(context, {0.0, -1.0}, 2000ms, 0ms); - pbf_wait(context, 150ms); - pbf_move_left_joystick(context, {1.0, 0.0}, 1400ms, 0ms); - pbf_wait(context, 500ms); - recorded_move_until_transition(stream, context, 0.0, -1.0, 1200ms, - "Barry house exit"); - // First-run video placed the player east of the northbound path. - // Shorten the rightward approach; tune further from the next recording. - stream.log("[Route 201] Aligning east for 2300ms before northbound approach."); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 2300ms, 0ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 5861ms, 0ms); - pbf_wait(context, 134ms); - - wait_for_dialogue(stream, context, "Phase 9 (Barry Route 201)"); - - pbf_mash_button(context, BUTTON_B, 6000ms); - pbf_wait(context, 800ms); - - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 700ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 279ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 10ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 145ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 7703ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 140ms, 0ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 1700ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 39ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 0.000000 }, 544ms, 1000ms); - - pbf_wait(context, 1000ms); - - // save_game(stream, context); // Disabled: checkpoint save mode controls saving. - context.wait_for_all_requests(); - } - // --------------------------------------------------------------------------- - // End of Segment 00 - 01 - // Standing in front lake varity ready to enter lake Verity for starter selection. - // --------------------------------------------------------------------------- - // --------------------------------------------------------------------------- - // Segment 00-02 begins - // Navigates into lake Verity to recieve starter - // --------------------------------------------------------------------------- - static void starter_nav(VideoStream& stream, ProControllerContext& context) { - // re-sync control - pbf_press_button(context, BUTTON_B, 20ms, 200ms); - pbf_wait(context, 2000ms); - context.wait_for_all_requests(); - - // continue movement - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 300ms, 0ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 300ms, 0ms); - - wait_for_dialogue(stream, context, "Phase 10 (Barry before the starter selection)"); - - pbf_mash_button(context, BUTTON_A, 1000ms); - - pbf_wait(context, 500ms); - std::shared_ptr briefcase = - std::make_shared( - RESOURCE_PATH() + "PokemonBDSP/StarterBriefcase.png" - ); - - // Mash B until we see the briefcase. - ImageMatchWatcher detector(briefcase, { 0.5, 0.1, 0.5, 0.7 }, 100, true); - - int ret = run_until( - stream, context, - [](ProControllerContext& context) { - pbf_mash_button(context, BUTTON_B, 120000ms); - }, - { {detector} } - ); - - if (ret == 0) { - stream.log("Detected briefcase!"); - } - else { - stream.log("Timed out waiting for briefcase."); - throw OperationFailedExceptionWithScreenshot( - ErrorReportMode::SEND_ERROR_REPORT, - "Failed to detect briefcase in Segment 00.", - stream - ); - } - - // Wait for briefcase to fully open. - stream.log("Mashing B for briefcase to fully open."); - pbf_mash_button(context, BUTTON_B, 2000ms); - } - static void post_starly_clenaup(VideoStream& stream, ProControllerContext& context) { - pbf_mash_button(context, BUTTON_B, 60000ms); - } - // --------------------------------------------------------------------------- - // End of Segment 00 - 02 - // Standing in front lake varity after starter selection. - // --------------------------------------------------------------------------- - // --------------------------------------------------------------------------- - // Segment 00-03 begins - // Navigates from Exit of lake Verity to receipt of the Pokedex - // --------------------------------------------------------------------------- - - static void tutorial_part_2_navigation(SingleSwitchProgramEnvironment& env, - VideoStream& stream, ProControllerContext& context) { - pbf_wait(context, 2000ms); - context.wait_for_all_requests(); - - pbf_wait(context, 563ms); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1700ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, -1.000000 }, 510ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 50ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, -1.000000 }, 310ms, 0ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 5500ms, 0ms); - pbf_wait(context, 3963ms); - context.wait_for_all_requests(); - - wait_for_dialogue(stream, context, "Phase 10 (Professor dialog to mom)"); - pbf_mash_button(context, BUTTON_B, 40000ms); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 820ms, 0ms); - pbf_wait(context, 4000ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 800ms, 0ms); - pbf_wait(context, 555ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 50ms, 0ms); - pbf_wait(context, 23ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 5020ms, 0ms); - pbf_wait(context, 169ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 745ms, 0ms); - walk_through_grass(stream, context, 1, 0, 20, "right through grass to path", BattleType::STARTER); - walk_through_grass(stream, context, 0, 1, 18, "up through grass 2", BattleType::STARTER); - walk_through_grass(stream, context, 1, 0, 10, "right through grass 2", BattleType::STARTER); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 730ms, 0ms); - pbf_wait(context, 941ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 1950ms, 0ms); - wait_for_dialogue(stream, context, "Phase 11 (dawn conversation)"); - pbf_mash_button(context, BUTTON_B, 50000ms); - wait_for_dialogue(stream, context, "Phase 12 (Nickname reject)"); - pbf_mash_button(context, BUTTON_B, 40000ms); - wait_for_dialogue(stream, context, "Phase 13 (Pokedex accept)"); - pbf_mash_button(context, BUTTON_A, 40000ms); - - - // pokedex received - } - - - // --------------------------------------------------------------------------- - // End of Segment 00 - 03 - // Standing in front of the professor after receiving the pokedex. - // --------------------------------------------------------------------------- - - // --------------------------------------------------------------------------- - // Begin Segment 00 - 04 - // Will heal at Sandgem pokeCenter to reliably catch a second pokemon for Home - // transfer. - // Collects poketech and stands in front of the PC to end the tutorial section - // and send the starter to pokemon HOME. - // --------------------------------------------------------------------------- - - // Inside the player's house, just through the door: Mom watches TV a - // column east of the door and two rows north of the mat. The door fixes - // our column, but counted taps north lost a row (r108b stood a row below - // her). So find her row: face east and press A, and step one tile north - // at a time until she answers. A press from a row off talks to nobody, - // and the TV stand stops us a row above her. - static void talk_to_mom_at_home(VideoStream& stream, ProControllerContext& context) { - for (size_t row = 0; row < 4; row++) { - if (talk_to_adjacent_npc(stream, context, {Pokemon::Direction::EAST}, "Home: Mom")) { - stream.log("[00.04] Mom answered after " + std::to_string(row) + " step(s) north."); - return; - } - // Facing east, a short tap north only turns; facing north, it is one tile. - route_turn(stream, context, Pokemon::Direction::NORTH); - pbf_press_dpad(context, DPAD_UP, 80ms, 400ms); - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Home: Mom did not answer from any of four rows north of the door.", stream); - } - - static void tutorial_part_3_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { - // Arm exit detection before movement and finish the fade before - // starting the dialogue watcher, which cancels queued commands. - recorded_move_until_transition(stream, context, 0.0, -1.0, 2010ms, - "Rowan lab exit"); - context.wait_for_all_requests(); - - wait_for_dialogue(stream, context, "Phase 14 (dawn tour)"); - - pbf_mash_button(context, BUTTON_B, 25000ms); - - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 5125ms, 0ms); - pbf_wait(context, 336ms); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1700ms, 0ms); - pbf_wait(context, 213ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 2325ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, 1.000000 }, 950ms, 0ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 2360ms, 0ms); - pbf_wait(context, 253ms); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 4610ms, 0ms); - pbf_wait(context, 954ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 220ms, 0ms); - pbf_wait(context, 587ms); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 1375ms, 0ms); - pbf_wait(context, 464ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 775ms, 0ms); - pbf_wait(context, 1655ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 640ms, 0ms); - pbf_wait(context, 3128ms); - talk_to_mom_at_home(stream, context); - - wait_for_dialogue(stream, context, "Phase 15 (Mom conversation to accept parcel)"); - - pbf_mash_button(context, BUTTON_B, 40000ms); - - context.wait_for_all_requests(); - pbf_wait(context, 1500ms); - pbf_press_dpad(context, DPAD_LEFT, 150ms, 0ms); - pbf_wait(context, 1143ms); - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 875ms, 0ms); - pbf_wait(context, 1169ms); - pbf_press_button(context, BUTTON_A, 180ms, 0ms); - pbf_wait(context, 946ms); - pbf_press_button(context, BUTTON_A, 180ms, 0ms); - pbf_wait(context, 9092ms); - pbf_press_button(context, BUTTON_A, 180ms, 0ms); - pbf_wait(context, 740ms); - pbf_press_button(context, BUTTON_A, 180ms, 0ms); - pbf_wait(context, 2998ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 240ms, 0ms); - pbf_wait(context, 893ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 5300ms, 0ms); - pbf_wait(context, 282ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 260ms, 0ms); - context.wait_for_all_requests(); - pbf_wait(context, 1500ms); - - walk_through_grass(stream, context, 1, 0, 20, "right through grass to path", BattleType::STARTER); - walk_through_grass(stream, context, 0, 1, 18, "up through grass 2", BattleType::STARTER); - walk_through_grass(stream, context, 1, 0, 10, "right through grass 2", BattleType::STARTER); - context.wait_for_all_requests(); - pbf_wait(context, 1500ms); - - pbf_move_left_joystick(context, { 0.000000, -1.000000 }, 560ms, 0ms); - pbf_wait(context, 1500ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 5000ms, 0ms); - pbf_wait(context, 1500ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1000ms, 0ms); - pbf_wait(context, 1500ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 1855ms, 0ms); - pbf_wait(context, 1500ms); - pbf_move_left_joystick(context, { 1.000000, -0.000000 }, 430ms, 0ms); - pbf_wait(context, 1500ms); - pbf_move_left_joystick(context, { 0.000000, 1.000000 }, 850ms, 0ms); - pbf_wait(context, 1500ms); - pbf_move_left_joystick(context, { -1.000000, -0.000000 }, 1120ms, 0ms); - - wait_for_dialogue(stream, context, "Phase 16 (catch tutorial)"); - - pbf_mash_button(context, BUTTON_B, 70000ms); - context.wait_for_all_requests(); - pbf_wait(context, 2000ms); - catch_1_pokemon(stream, context); - pbf_wait(context, 1500ms); - context.wait_for_all_requests(); - } - - // Align to a route reference before continuing recorded navigation. - // Matcher offsets and thresholds are fractions of screen width/height. - static void align_to_tutorial_landmarks( - VideoStream& stream, ProControllerContext& context, - OverworldMatchResult (*matcher)(const ImageRGB32&), const std::string& label - ) { - context.wait_for_all_requests(); - pbf_wait(context, 700ms); - context.wait_for_all_requests(); - size_t confirmations = 0, coarse_confirmations = 0; - double previous_dx = 0, previous_dy = 0; - for (size_t attempt = 0; attempt < 60; attempt++) { - const auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": No video frame.", stream); - const auto result = matcher(*frame.frame); - VideoOverlaySet overlays(stream.overlay()); - for (const auto& landmark : result.landmarks) { - overlays.add(COLOR_CYAN, landmark.search); - if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); - stream.log("[" + label + "] " + landmark.name + - " score=" + std::to_string(landmark.score) + - " margin=" + std::to_string(landmark.margin) + - " accepted=" + std::to_string(landmark.accepted) + - " screen-relative offset=(" + std::to_string(landmark.dx) + - "," + std::to_string(landmark.dy) + ")"); - } - stream.log("[" + label + "] located=" + std::to_string(result.located) + - " mode=" + (result.coarse ? "vertical acquisition" : "fine alignment") + - " reference screen-relative offset=(" + std::to_string(result.dx) + - "," + std::to_string(result.dy) + ")"); - if (result.coarse && result.located) { - if (coarse_confirmations && std::abs(result.dx-previous_dx)<=0.00625 && - std::abs(result.dy-previous_dy)<=1.0/90) coarse_confirmations++; - else coarse_confirmations=1; - previous_dx=result.dx; - previous_dy=result.dy; - } else coarse_confirmations=0; - if (!result.located) { - confirmations = 0; - pbf_wait(context, 250ms); - } else if (result.coarse && coarse_confirmations<3) { - confirmations = 0; - stream.log("[" + label + "] Waiting for consistent vertical acquisition."); - pbf_wait(context, 250ms); - } else if (result.at_target) { - if (++confirmations >= 3) { - stream.log("[" + label + "] Reference position confirmed."); - return; - } - pbf_wait(context, 300ms); - } else { - confirmations = 0; - const bool horizontal = !result.coarse && - std::abs(result.dx) >= std::abs(result.dy)*9.0/16; - const double error = horizontal ? result.dx : result.dy*9.0/16; - const auto duration = std::abs(error) > 1.0/24 ? 80ms : - std::abs(error) > 0.015625 ? 40ms : 16ms; - stream.log("[" + label + "] Correcting " + - std::string(horizontal ? "horizontal" : "vertical") + " axis " + - (horizontal ? (result.dx>0 ? "right" : "left") : (result.dy<0 ? "up" : "down")) + - " for " + std::to_string(duration.count()) + "ms."); - // Reuse confirmed-Run encounter handling if correction enters grass. - walk_through_grass(stream, context, - horizontal ? (result.dx > 0 ? 1 : -1) : 0, - horizontal ? 0 : (result.dy < 0 ? 1 : -1), - 1, label, BattleType::STANDARD, duration); - } - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Reference position not confirmed; route movement stopped.", stream); - } - - static void align_to_sandgem_junction(VideoStream& stream, ProControllerContext& context) { - align_to_tutorial_landmarks(stream, context, match_sandgem_junction, "Sandgem junction"); - } - - // Fights a trainer whose dialogue has opened, with whatever starter we - // have. Someone who only talks leaves no battle to fight. - static void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name) { - pbf_mash_button(context, BUTTON_B, 5000ms); - context.wait_for_all_requests(); - BattleMenuWatcher battle_menu(BattleType::TRAINER, COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - if (wait_until(stream, context, 8s, {{battle_menu}, {battle_dialog}}) < 0) { - stream.log("[00.04] " + name + ": no battle followed the talk."); - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - return; - } - fight_trainer(stream, context, name); - context.wait_for_all_requests(); - // The next trainer may be a few tiles on: check HP now. With no - // Potion in the Bag this only looks. - use_potion_first_pokemon(stream, context, 0.5); - } - - // How many Potions to buy. A Potion is 300 and a new game starts with - // 3000; the money on screen cannot be read yet. Five heal the starter - // through Route 202's four trainers and leave half the money. - constexpr int POTIONS_TO_BUY = 5; - - // From below the Center's door to below the Poke Mart's, ten tiles - // east along the same row, and in. The last leg is open and movement - // is not locked to tiles, so we may stand part of a tile off the door: - // whole-tile steps either side missed it in r121a. The Mart's doorway - // is lit like the Center's, so it is lined up with by sight the same - // way. Two townspeople wander the dirt south of the Mart and stop a - // leg short without the walk knowing (r122a counted to the door's - // row from three rows south of it): where the doorway shows on screen - // says so, and the walk is made once more from there. - static void enter_sandgem_poke_mart( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector& trainers, const RouteTrainerFight& fight - ) { - const TilePos door = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); - const TilePos front = door.next(Pokemon::Direction::SOUTH); - const auto at_front = [&](const TilePos& pos) { return pos.y == front.y && std::abs(pos.x - front.x) <= 1; }; - walk_route(stream, context, run_from_wild_battle, maps, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), - at_front, trainers, fight, "To the Poke Mart"); - const std::vector short_of_door = tiles_short_of_door(stream, context, maps.world(), door, "To the Poke Mart"); - if (!short_of_door.empty()) { - route_status(stream, "To the Poke Mart: stopped short of the door; walking again."); - walk_route(stream, context, run_from_wild_battle, maps, short_of_door, at_front, trainers, fight, "To the Poke Mart again"); - } - enter_lit_door(stream, context, "Sandgem Poke Mart: through the door"); - } - - // Inside the Mart, the clerk's counter is straight ahead of the door - // (every Mart is laid out so; Oreburgh's and Eterna's are walked the - // same way). Walk up to it, buy the Potions, and walk back out. - // buy_items() opens the talk and finds the Potion row by OCR of its - // name and price; the quantity is counted out blind. - static void buy_potions_at_the_counter(VideoStream& stream, ProControllerContext& context) { - route_status(stream, "Poke Mart: to the clerk."); - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "Poke Mart: to the clerk"); - if (walk.stop != WalkStop::BLOCKED && walk.stop != WalkStop::DIALOG) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poke Mart: the counter was not reached (" + std::string(walk_stop_name(walk.stop)) + ").", stream); - } - route_status(stream, "Poke Mart: buying " + std::to_string(POTIONS_TO_BUY) + " Potions."); - if (!buy_items(stream, context, {{ShopItem::Potion, POTIONS_TO_BUY}})) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poke Mart: could not buy " + std::to_string(POTIONS_TO_BUY) + " Potions.", stream); - } - leave_door_south(stream, context, run_from_wild_battle, "Poke Mart: out the door"); - } - - // Potions let us heal where we stand. Sandgem's Poke Mart is next - // door east of its Center. The purchase is checked in the Bag after, - // since buy_items() counts the quantity out without reading it. - // Returns where we stand after: below the Mart's door. - static std::vector buy_potions_in_sandgem( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector& trainers, const RouteTrainerFight& fight - ) { - enter_sandgem_poke_mart(stream, context, maps, trainers, fight); - buy_potions_at_the_counter(stream, context); - if (!bag_has_item(stream, context, "potion", 1)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poke Mart: the purchase went through, but no Potion is in the Bag.", stream); - } - return story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); - } - - // A walk_until() callback that keeps every location banner it sees. - static std::function banner_keeper(std::vector& banners) { - return [&banners](const ImageViewRGB32& screen) { - const std::string read = read_location_banner(screen); - if (!read.empty() && (banners.empty() || banners.back() != read)) { - banners.push_back(read); - } - return false; - }; - } - - static bool any_banner_names(const std::vector& banners, const char* map) { - return std::any_of(banners.begin(), banners.end(), - [&](const std::string& read) { return location_banner_matches(read, map); }); - } - - // Whether all of `tiles` are on `map`. - static bool all_on_map(const MapSet& maps, const std::vector& tiles, const char* map) { - return !tiles.empty() && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos) { - const std::optional tile = maps.to_local(pos); - return tile && tile->map == map; - }); - } - - // North from below the gap between the signs until a wall stops us, - // nudging a tile west, twice at most, when blocked at once (below a - // sign or the Mart). Returns whether Route 202's banner showed: only - // one of the gap's columns leads out of town before a wall. - static bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context) { - using Pokemon::Direction; - std::vector banners; - for (size_t nudges = 0;; nudges++){ - const WalkResult north = walk_until(stream, context, run_from_wild_battle, 0, 1, 8000ms, "North through the gap", banner_keeper(banners)); - if (north.stop == WalkStop::TRANSITION || north.stop == WalkStop::DIALOG || north.wild_battles > 0){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "North through the gap: " + std::string(walk_stop_name(north.stop)) + " instead of a wall.", stream); - } - // Blocked at once: the wall reads after about 800ms still. - if (north.walked > ROUTE_START_DELAY + 800ms + 2 * ROUTE_MS_PER_TILE_UP_DOWN){ - break; - } - if (nudges == 2){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "North through the gap: still blocked after two tiles west.", stream); - } - route_status(stream, "North through the gap: blocked at once, below a sign or the Mart; a tile west."); - route_turn(stream, context, Direction::WEST); - walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 1), "A tile west"); - route_turn(stream, context, Direction::NORTH); - } - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - banner_keeper(banners)(*screen.frame); - } - return any_banner_names(banners, MAP_ROUTE_202); - } - - // From the trees at the east end of the doors' row, west into the gap - // between the Center's sign and the Mart's (below_sign_gap()) and - // north through it until a wall stops us. Six tiles west from column - // 29 ends on 23, and on 20 to 26 if two tiles off: north from all of - // those passes no door, and only the gap lets us through. A walk - // north that goes nowhere stood below a sign or the Mart: a tile - // west, twice at most, never reaches the Center's door at 17. The - // planner would rather go round the south of town on walls, where - // townspeople wander. Returns where the walk north may have ended: - // the gap's columns end on Sandgem's top row, bar one that runs on to - // Route 202, and its banner tells the two apart. - static std::vector north_through_sign_gap(VideoStream& stream, ProControllerContext& context, const MapSet& maps){ - using Pokemon::Direction; - route_status(stream, "To the gap between the Center's and the Mart's signs: west 6."); - route_turn(stream, context, Direction::WEST); - walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); - const bool on_route = walk_north_through_gap(stream, context); - std::vector ends; - for (const TilePos& pos : SandgemTown::below_sign_gap()){ - const TilePos end = Pokemon::walk_to_wall(maps.world(), maps.to_world(MAP_SANDGEM_TOWN, pos), Direction::NORTH).stop; - if (all_on_map(maps, {end}, MAP_ROUTE_202) == on_route){ - ends.push_back(end); - } - } - route_status(stream, std::string("North through the gap: Route 202's banner ") + - (on_route ? "showed; on Route 202." : "did not show; on Sandgem Town's top row.")); - return ends; - } - - // Back up to Route 202's save spot under the ledge: it is where a - // retry may start (story_save_spots()), its walls pin it, and the - // trainers are north of it. First east to the trees that end the - // doors' row, so the way north starts from one known tile, then north - // through the gap between the signs: r123a, planned round the south - // of town on a map that had it wrong, walked into a house. - static void walk_to_route_202_save_spot( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector at, std::vector& trainers, const RouteTrainerFight& fight - ) { - const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::FRONT_ROW_EAST_END); - const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); - if (!all_on_map(maps, at, MAP_ROUTE_202)) { - at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), - [&](const TilePos& pos) { return pos == row_end; }, trainers, fight, "To the trees east of the Poke Mart"); - at = north_through_sign_gap(stream, context, maps); - } - walk_route(stream, context, run_from_wild_battle, maps, std::move(at), - [&](const TilePos& pos) { return pos == save_spot; }, trainers, fight, "Back to Route 202"); - } - - // DIAGNOSTIC ONLY (the Mart lady checkpoint): 00.04's walk back to - // Route 202's save spot, from below Sandgem Center's door, without saving. - void walk_from_sandgem_center_to_route_202_save_spot( - VideoStream& stream, ProControllerContext& context, - const std::vector& at - ) { - const MapSet& maps = story_maps(); - std::vector trainers = story_trainer_sights(); - const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; - walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); - } - - // North into Jubilife until Dawn's scene starts (00.04d's - // into_jubilife). Jubilife's south edge is a wall with stairs in it, - // west of where the route may leave us (r78 stood at its east end). - // Entering starts a scene: Dawn walks over while we stand still. So - // walk north; if the wall stops us and no dialogue follows, the stairs - // are further west. The scene itself is the next step. - static void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { - for (size_t attempt = 0; attempt < 6; attempt++) { - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 4000ms, "to Dawn in Jubilife"); - if (walk.stop == WalkStop::TRANSITION) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Jubilife: left the area instead of meeting Dawn.", stream); - } - ShortDialogWatcher dialog(COLOR_CYAN); - if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0) { - return; - } - stream.log("[00.04] Jubilife: the wall stopped us; looking for the stairs one tile west."); - route_turn(stream, context, Pokemon::Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Jubilife: no stairs found and no Dawn after six tiles west.", stream); - } - - // Mart lady runs that failed this program run: the clerk after two. - static size_t g_mart_lady_misses = 0; - // 00.04c's potions step, from the save out of Sandgem's Center: the - // Poke Mart lady's (her checkpoint without its own save), or the - // clerk's after two misses, on to Route 202's save spot, where - // 00.04d's save goes. The Bag must have Potions after. - static Pokemon::Outcome sandgem_potions( - ConsoleStoryExecutor& executor, StoryConsole& c, Pokemon::RunState&, const Pokemon::StoryStep& - ) { - VideoStream& stream = c.env.console; - ProControllerContext& context = c.context; - // The clerk's list is found by reading item names, which needs a - // reader for the game's language; without one, the lady every time. - if (g_mart_lady_misses < 2 || !story_reads_text()) { - // Onto the doors' row first: out of the door we stand a tile to - // three below it (r261 walked west into the trees from three). - route_turn(stream, context, Pokemon::Direction::EAST); - walk_tiles_dpad(stream, context, run_from_wild_battle, 1, 0, 1, "east under the Center's wall"); - const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); - if (up.stop != WalkStop::BLOCKED) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + ".", stream); - } - try { mart_lady_without_save(c.env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } - } else { - const std::vector at = buy_potions_in_sandgem(stream, context, executor.maps(), executor.trainers(), executor.fight(c)); - walk_to_route_202_save_spot(stream, context, executor.maps(), at, executor.trainers(), executor.fight(c)); - } - if (!bag_has_item(stream, context, "potion", 1)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after the shop.", stream); - } - return Pokemon::outcome_ok(); - } - // --------------------------------------------------------------------------- - // End of Segment 00 - 04 - // Standing in Jubilife after speaking to dawn. - // --------------------------------------------------------------------------- - // --------------------------------------------------------------------------- - // Begin Segment 00 - 05 - // Standing at the entrance to jubilife after speaking to dawn, will navigate to the poketech challenge. - // Collects poketech and stands in front of the PC to end the tutorial section - // and send the starter to pokemon HOME. - // --------------------------------------------------------------------------- - - // Stop the approach as soon as buildings can guide it, instead of - // overshooting south and then blindly walking back north. - static void approach_poketch_landmarks(VideoStream& stream, ProControllerContext& context) { - context.wait_for_all_requests(); - Milliseconds walked = 0ms; - while (walked < 1900ms) { - auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poketch approach: No video frame.", stream); - const auto result = match_poketch_landmarks(*frame.frame); - VideoOverlaySet overlays(stream.overlay()); - for (const auto& landmark : result.landmarks) { - overlays.add(COLOR_CYAN, landmark.search); - if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); - stream.log("[Poketch approach] " + landmark.name + " score=" + std::to_string(landmark.score) + - " margin=" + std::to_string(landmark.margin)); - } - if (result.located) { - stream.log("[Poketch approach] Landmarks acquired after " + std::to_string(walked.count()) + "ms; ending downward approach."); - return; - } - const Milliseconds step = std::min(200ms, 1900ms - walked); - pbf_move_left_joystick(context, {0, -1}, step, 0ms); - walked += step; - pbf_wait(context, 700ms); - context.wait_for_all_requests(); - } - stream.log("[Poketch approach] Reached 1900ms downward limit; handing off to locator."); - } - - static void align_and_talk_to_poketch_man(VideoStream& stream, ProControllerContext& context) { - context.wait_for_all_requests(); - pbf_wait(context, 700ms); - context.wait_for_all_requests(); - size_t confirmed = 0, coarse_confirmed = 0; - double previous_dx = 0, previous_dy = 0; - size_t acquisition_misses = 0, vertical_probe = 0; - // Two small excursions around the approach position. This is - // visibility recovery, not permission to interact without a match. - const int vertical_probes[] = {-80, 160, -240, 320, -160, -120, 240, -360, 480, -240}; - for (size_t attempt = 0; attempt < 80; attempt++) { - auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poketch interaction: No video frame.", stream); - const OverworldMatchResult result = match_poketch_landmarks(*frame.frame); - stream.log(result.coarse ? "[Poketch landmarks] Coarse acquisition." : "[Poketch landmarks] Fine alignment."); - VideoOverlaySet overlays(stream.overlay()); - for (const auto& landmark : result.landmarks) { - overlays.add(COLOR_CYAN, landmark.search); - if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); - stream.log("[Poketch landmarks] " + landmark.name + " score=" + std::to_string(landmark.score) + - " margin=" + std::to_string(landmark.margin) + " accepted=" + std::to_string(landmark.accepted) + - " screen-relative offset=(" + std::to_string(landmark.dx) + "," + std::to_string(landmark.dy) + ")"); - } - if (result.located) acquisition_misses = 0; - if (result.coarse && result.located) { - if (coarse_confirmed && std::abs(result.dx - previous_dx) <= 0.00625 && std::abs(result.dy - previous_dy) <= 1.0/90) - coarse_confirmed++; - else coarse_confirmed = 1; - previous_dx = result.dx; previous_dy = result.dy; - } else coarse_confirmed = 0; - if (!result.located) { - confirmed = 0; - if (++acquisition_misses >= 4 && vertical_probe < sizeof(vertical_probes) / sizeof(vertical_probes[0])) { - int probe = vertical_probes[vertical_probe++]; - acquisition_misses = 0; - stream.log("[Poketch landmarks] Bounded vertical visibility probe " + std::to_string(vertical_probe) + - "/10: " + (probe < 0 ? "down " : "up ") + std::to_string(std::abs(probe)) + "ms."); - pbf_move_left_joystick(context, {0, probe < 0 ? -1.0 : 1.0}, Milliseconds(std::abs(probe)), 0ms); - pbf_wait(context, 700ms); - } else { - stream.log("[Poketch landmarks] No trustworthy acquisition; waiting for another frame."); - pbf_wait(context, 250ms); - } - } else if (result.coarse && coarse_confirmed < 3) { - confirmed = 0; - stream.log("[Poketch landmarks] Waiting for consistent coarse offset before movement."); - pbf_wait(context, 250ms); - } else if (result.at_target) { - if (++confirmed >= 3) { - stream.log("[Poketch landmarks] Above-NPC reference confirmed; turning down."); - pbf_press_dpad(context, DPAD_DOWN, 40ms, 400ms); - context.wait_for_all_requests(); - pbf_press_button(context, BUTTON_A, 80ms, 500ms); - context.wait_for_all_requests(); - wait_for_dialogue(stream, context, "Phase 27 (poketech challenge completed dialog)", 10s); - return; - } - pbf_wait(context, 300ms); - } else { - confirmed = 0; - const double dx = result.dx, dy = result.dy; - stream.log("[Poketch landmarks] Reference screen-relative offset=(" + std::to_string(result.dx) + "," + std::to_string(result.dy) + ")."); - const bool horizontal = std::abs(dx) > 7.0/960 && - (std::abs(dy) <= 7.0/540 || std::abs(dx) >= std::abs(dy)*9.0/16); - double error = horizontal ? dx : dy*9.0/16; - auto duration = std::abs(error) > 0.03125 ? 80ms : std::abs(error) > 0.015625 ? 40ms : 16ms; - if (std::abs(dx) <= 7.0/960 && std::abs(dy) <= 7.0/540) { - stream.log("[Poketch landmarks] Coarse position near target; waiting for fine confirmation."); - pbf_wait(context, 300ms); - context.wait_for_all_requests(); - continue; - } - stream.log("[Poketch landmarks] Correcting " + std::string(horizontal ? "horizontal" : "vertical") + - " axis for " + std::to_string(duration.count()) + "ms."); - pbf_move_left_joystick(context, - {horizontal ? (dx > 0 ? 1.0 : -1.0) : 0.0, - horizontal ? 0.0 : (dy < 0 ? 1.0 : -1.0)}, duration, 0ms); - pbf_wait(context, 700ms); - } - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poketch interaction: Building landmarks did not confirm the reference position; A not pressed.", stream); - } - - void align_and_enter_jubilife_center(VideoStream& stream, ProControllerContext& context) { - context.wait_for_all_requests(); - pbf_wait(context, 700ms); - context.wait_for_all_requests(); - size_t confirmed = 0; - for (size_t attempt = 0; attempt < 60; attempt++) { - const auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Jubilife Center alignment: No video frame.", stream); - const auto result = match_jubilife_center_entrance(*frame.frame); - VideoOverlaySet overlays(stream.overlay()); - for (const auto& landmark : result.landmarks) { - overlays.add(COLOR_CYAN, landmark.search); - if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); - stream.log("[Jubilife Center alignment] score=" + std::to_string(landmark.score) + - " margin=" + std::to_string(landmark.margin) + " screen-relative offset=(" + - std::to_string(landmark.dx) + "," + std::to_string(landmark.dy) + ")"); - } - if (!result.located) { - confirmed = 0; - pbf_wait(context, 250ms); - } else if (result.at_target) { - if (++confirmed >= 3) { - stream.log("[Jubilife Center alignment] Door position confirmed; entering."); - recorded_move_until_transition(stream, context, 0, 1, 2000ms, "Jubilife Pokemon Center entry"); - pbf_wait(context, 700ms); - context.wait_for_all_requests(); - return; - } - pbf_wait(context, 300ms); - } else { - confirmed = 0; - bool horizontal = std::abs(result.dx) >= std::abs(result.dy)*9.0/16; - double error = horizontal ? result.dx : result.dy*9.0/16; - const auto duration = std::abs(error) > 1.0/24 ? 80ms : std::abs(error) > 0.015625 ? 40ms : 16ms; - pbf_move_left_joystick(context, - {horizontal ? (result.dx > 0 ? 1.0 : -1.0) : 0.0, - horizontal ? 0.0 : (result.dy < 0 ? 1.0 : -1.0)}, duration, 0ms); - pbf_wait(context, 700ms); - } - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Jubilife Center alignment: Door reference not confirmed; entry not attempted.", stream); - } - - static void tutorial_part_4_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { - // What 00.05 does, stepped through on the console before writing it. - // Every walk is planned on Jubilife's tile grid; every talk stands - // beside the person, faces them and presses A once. - static const char* const STEPS[] = { - "Trainers' School: talk to Barry, who gives the Town Map", - "Talk to the Poketch Company president on the main road", - "Get the three coupons from the clowns by the Poke Mart, the TV station and the Poketch Company", - "Show the coupons to the president for the Poketch", - "Walk into the Pokemon Center and stand at the PC", - }; - constexpr size_t STEP_COUNT = sizeof(STEPS) / sizeof(STEPS[0]); - { - std::string text = "[00.05] In Jubilife, the steps are:"; - for (size_t i = 0; i < STEP_COUNT; i++) { - text += "\n " + std::to_string(i + 1) + ". " + STEPS[i]; - } - stream.log(text, COLOR_BLUE); - } - auto step = [&](size_t number) { - std::string text = "[00.05 step " + std::to_string(number) + "/" + std::to_string(STEP_COUNT) + "] " + STEPS[number - 1]; - text += number < STEP_COUNT ? std::string(" -- next: ") + STEPS[number] : std::string(" -- last step"); - stream.log(text, COLOR_BLUE); - add_wrapped_log(stream.overlay(), "00.05 step " + std::to_string(number) + "/" + std::to_string(STEP_COUNT) + ": " + STEPS[number - 1], COLOR_WHITE); - if (number < STEP_COUNT) { - add_wrapped_log(stream.overlay(), "Next: " + std::string(STEPS[number]), COLOR_GRAY); - } - }; - - const MapSet& maps = jubilife_maps(); - auto world = [&](const TilePos& pos) { return maps.to_world(MAP_JUBILIFE_CITY, pos); }; - // Jubilife has no trainers; someone in the way only talks. - std::vector trainers; - auto in_the_way = [&](const std::string&) { finish_dialogue(stream, context, BUTTON_B, "Someone in the way"); }; - // From the save below the steps, against the wall east of them: - // a tile west (the first press only turns), then up the steps. - route_turn(stream, context, Pokemon::Direction::WEST); - walk_for(stream, context, run_from_wild_battle, -1, 0, 200ms, "To the steps"); - walk_for(stream, context, run_from_wild_battle, 0, 1, 480ms, "Up the steps"); - std::vector at; - for (const TilePos& pos : JubilifeCity::up_the_steps()) { - at.push_back(world(pos)); - } - - step(1); - const TilePos school = world(JubilifeCity::TRAINERS_SCHOOL_DOOR.next(Pokemon::Direction::SOUTH)); - auto by_door = [](const TilePos& front) { - return [front](const TilePos& pos) { return pos.y == front.y && pos.x >= front.x - 1 && pos.x <= front.x + 1; }; - }; - // To the Pokemon Center first, and heal: going in proves where we - // are, and coming out leaves us on its doorstep, the door row's - // front, which no wall pins. Measured on the console (2026-10-30): - // from the top of the steps, east stops at the house east of the - // road, and north from there stops in front of the Center. - walk_until(stream, context, run_from_wild_battle, 1, 0, 4000ms, "East to the house"); - walk_until(stream, context, run_from_wild_battle, 0, 1, 4000ms, "North to the Pokemon Center"); - heal_at_pokemon_center(stream, context, "Jubilife Pokemon Center"); - // From the doorstep the School's door is ten one-tile steps west - // (measured). Nothing on that row stops a walk, so count the - // steps, and from the eighth on try north: a wall there is the - // School's front, the door takes us in. - route_turn(stream, context, Pokemon::Direction::WEST); - bool in_school = false; - for (size_t tiles = 1; tiles <= 12 && !in_school; tiles++) { - walk_for(stream, context, run_from_wild_battle, -1, 0, 160ms, "West to the School's door"); - if (tiles < 8) { - continue; - } - const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 1200ms, "Trainers' School door"); - in_school = up.stop == WalkStop::TRANSITION; - if (!in_school) { - walk_for(stream, context, run_from_wild_battle, 0, -1, 200ms, "Back to the doorsteps' row"); - route_turn(stream, context, Pokemon::Direction::WEST); - } - } - if (!in_school) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Trainers' School door: not found 8 to 12 tiles west of the Center's doorstep.", stream); - } - context.wait_for(1500ms); - // Barry stands at the blackboard, a tile west of the door's column: - // step west, walk north until he stops us, talk. - route_turn(stream, context, Pokemon::Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); - walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "to Barry"); - 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) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Trainers' School: Barry did not answer.", stream); - } - } - // His scene ends with "You obtained a Town Map!", which A closes. - finish_dialogue(stream, context, BUTTON_A, "Barry"); - // Out: south to the front wall, a tile east (the door's column), out. - walk_until(stream, context, run_from_wild_battle, 0, -1, 4000ms, "to the school's front wall"); - route_turn(stream, context, Pokemon::Direction::EAST); - pbf_move_left_joystick(context, {1, 0}, route_open_hold(Pokemon::Direction::EAST, 1), 200ms); - if (walk_until(stream, context, run_from_wild_battle, 0, -1, 2500ms, "out of the Trainers' School").stop != WalkStop::TRANSITION) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Trainers' School: did not leave by the door.", stream); - } - context.wait_for(1500ms); - at = {school.next(Pokemon::Direction::WEST), school, school.next(Pokemon::Direction::EAST)}; - - step(2); - // The president talks on with A after each last line; B moves his - // lines on and never starts a new talk. - const TilePos president = world(JubilifeCity::POKETCH_PRESIDENT); - talk_to_npc(stream, context, run_from_wild_battle, maps, at, president, - {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH}, trainers, in_the_way, "Poketch president"); - finish_dialogue(stream, context, BUTTON_B, "Poketch president"); - - step(3); - // Each clown asks a question whose default answer, Yes, is right, - // and hands over a coupon with an "obtained" message A closes. - talk_to_npc(stream, context, run_from_wild_battle, maps, at, world(JubilifeCity::CLOWN_MART), - {Pokemon::Direction::SOUTH, Pokemon::Direction::WEST, Pokemon::Direction::EAST}, - trainers, in_the_way, "Clown by the Poke Mart"); - finish_dialogue(stream, context, BUTTON_A, "Clown by the Poke Mart"); - talk_to_npc(stream, context, run_from_wild_battle, maps, at, world(JubilifeCity::CLOWN_TV_STATION), - {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH, Pokemon::Direction::WEST}, - trainers, in_the_way, "Clown by the TV station"); - finish_dialogue(stream, context, BUTTON_A, "Clown by the TV station"); - talk_to_npc(stream, context, run_from_wild_battle, maps, at, world(JubilifeCity::CLOWN_POKETCH_COMPANY), - {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH, Pokemon::Direction::WEST}, - trainers, in_the_way, "Clown by the Poketch Company"); - finish_dialogue(stream, context, BUTTON_A, "Clown by the Poketch Company"); - - step(4); - talk_to_npc(stream, context, run_from_wild_battle, maps, at, president, - {Pokemon::Direction::EAST, Pokemon::Direction::SOUTH}, trainers, in_the_way, "Poketch president"); - finish_dialogue(stream, context, BUTTON_B, "Poketch president"); - - step(5); - const TilePos center = world(JubilifeCity::POKEMON_CENTER_DOOR.next(Pokemon::Direction::SOUTH)); - at = walk_route(stream, context, run_from_wild_battle, maps, at, by_door(center), - trainers, in_the_way, "To the Pokemon Center"); - enter_door_north(stream, context, run_from_wild_battle, "Pokemon Center door"); - // Inside: north to the counter, three tiles east, north to the PC - // at the back wall right of the counter. - walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "to the counter"); - route_turn(stream, context, Pokemon::Direction::EAST); - pbf_move_left_joystick(context, {1, 0}, route_open_hold(Pokemon::Direction::EAST, 3), 200ms); - walk_until(stream, context, run_from_wild_battle, 0, 1, 1500ms, "to the PC"); - context.wait_for_all_requests(); - } - // --------------------------------------------------------------------------- - // Segment 00-05 ends - // Standing in front of the Jubilife City Pokemon Center PC. - // Player will need to deposit pokemon in the pc and transfer to Pokemon Home - // and level to 100 before starting the next segment. - // --------------------------------------------------------------------------- - - // --------------------------------------------------------------------------- - // Segment / Checkpoint classes - // --------------------------------------------------------------------------- - - std::string AutoStory_Segment_00::name() const { return "00: Intro to Poketech"; } - std::string AutoStory_Segment_00::start_text() const { return "Start: Game launched, Standing in the player's room after tv stops playing"; } - std::string AutoStory_Segment_00::end_text() const { return "End: Starter Pokemon chosen, received, and Pokedex acquired, standing in front of the PC."; } - - void AutoStory_Segment_00::run_segment( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const { - stats.m_segment++; - env.update_stats(); - context.wait_for_all_requests(); - env.console.log("Start Segment " + name(), COLOR_ORANGE); - - run_story_table(env, context, options, stats, make_segment_00, {}); - - context.wait_for_all_requests(); - env.console.log("End Segment " + name(), COLOR_GREEN); - } - - std::string AutoStory_Checkpoint_0000::name() const { return "00.00 - Verify Settings"; } - std::string AutoStory_Checkpoint_0000::start_text() const { return "Standing in the initial bedroom before beginning the tutorial."; } - std::string AutoStory_Checkpoint_0000::end_text() const { return "Required game settings verified and saved; ready for 00.01."; } - void AutoStory_Checkpoint_0000::run_checkpoint( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, - AutoStoryOptions options, AutoStoryStats& stats - ) const { - run_story_table(env, context, options, stats, make_segment_00, {"00.00", "00.00"}); - } - - static void verify_settings_step(StoryConsole& c) { - verify_tutorial_settings(c.env.console, c.context); - c.context.wait_for_all_requests(); - } - static void select_starter_step(StoryConsole& c) { - switch (c.options.starter_choice) { - case StarterChoice::TURTWIG: select_turtwig(c.env.console, c.context); break; - case StarterChoice::CHIMCHAR: select_chimchar(c.env.console, c.context); break; - case StarterChoice::PIPLUP: select_piplup(c.env.console, c.context); break; - } - } - - std::map segment_00_variants() { - return { - {"verify_settings", story_call(verify_settings_step)}, - {"tutorial_part_1_navigation", story_call([](StoryConsole& c) { tutorial_part_1_navigation(c.env, c.env.console, c.context); })}, - {"starter_nav", story_call([](StoryConsole& c) { starter_nav(c.env.console, c.context); })}, - {"select_starter", story_call(select_starter_step)}, - {"fight_starly", story_call([](StoryConsole& c) { fight_starly(c.env.console, c.context); })}, - {"post_starly_cleanup", story_call([](StoryConsole& c) { post_starly_clenaup(c.env.console, c.context); })}, - {"tutorial_part_2_navigation", story_call([](StoryConsole& c) { tutorial_part_2_navigation(c.env, c.env.console, c.context); })}, - {"tutorial_part_3_navigation", story_call([](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); })}, - {"sandgem_potions", sandgem_potions}, - {"jubilife_stairs", story_call([](StoryConsole& c) { up_jubilife_stairs(c.env.console, c.context); })}, - {"tutorial_part_4_navigation", story_call([](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); })}, - }; - } - - // The table is segment_00_table() (PokemonBDSP_AutoStory_StoryTable.h). - Pokemon::Segment make_segment_00(StoryStepRunner& runner) { - const std::vector& table = segment_00_table(); - return story_table_segment(runner, "00", table, std::make_shared( - runner, rows_that_stop(table), story_maps(), story_trainer_sights(), - [](StoryConsole& c, const std::string& name) { fight_route_trainer(c.env.console, c.context, name); }, - segment_00_variants() - )); - } - - // DIAGNOSTIC ONLY: Remove before pushing to production. - void diagnostic_checkpoint_0004_after_catch( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, - AutoStoryOptions options, AutoStoryStats& stats - ) { - run_story_table(env, context, options, stats, make_segment_00, {"00.04b", "00.04d"}); - } - - // ======================== - // Checkpoint 0001 Wrapper - // ======================== - std::string AutoStory_Checkpoint_0001::name() const { return "00.01 - After TV"; } - std::string AutoStory_Checkpoint_0001::start_text() const { return "Game launched, Standing in the player's room after tv stops playing"; } - std::string AutoStory_Checkpoint_0001::end_text() const { return "Standing outside Lake Verity before picking starter."; } - - void AutoStory_Checkpoint_0001::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const { - run_story_table(env, context, options, stats, make_segment_00, {"00.01", "00.01"}); - } - - // ======================== - // Checkpoint 0002 Wrapper - // ======================== - - std::string AutoStory_Checkpoint_0002::name() const { return "00.02 - Outside Verity before starter"; } - std::string AutoStory_Checkpoint_0002::start_text() const { return AutoStory_Checkpoint_0001().end_text(); } - std::string AutoStory_Checkpoint_0002::end_text() const { return "Standing outside Lake Verity after picking starter."; } - - void AutoStory_Checkpoint_0002::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const { - run_story_table(env, context, options, stats, make_segment_00, {"00.02", "00.02"}); - } - - - // ======================== - // Checkpoint 0003 Wrapper - // ======================== - - std::string AutoStory_Checkpoint_0003::name() const { return "00.03 - Outside Verity after starter"; } - std::string AutoStory_Checkpoint_0003::start_text() const { return AutoStory_Checkpoint_0002().end_text(); } - std::string AutoStory_Checkpoint_0003::end_text() const { return "Standing in front of The professor after recieving the Pokedex"; } - - void AutoStory_Checkpoint_0003::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const { - run_story_table(env, context, options, stats, make_segment_00, {"00.03", "00.03"}); - } - // ======================== - // Checkpoint 0004 Wrapper - // ======================== - - std::string AutoStory_Checkpoint_0004::name() const { return "00.04 - After Pokedex"; } - std::string AutoStory_Checkpoint_0004::start_text() const { return AutoStory_Checkpoint_0003().end_text(); } - std::string AutoStory_Checkpoint_0004::end_text() const { return "Standing in in jubilife after speaking to dawn. Next: give town map to barry"; } - - void AutoStory_Checkpoint_0004::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const { - run_story_table(env, context, options, stats, make_segment_00, {"00.04", "00.04d"}); - } - // ======================== - // Checkpoint 0005 Wrapper - // ======================== - - std::string AutoStory_Checkpoint_0005::name() const { return "00.05 - Jubilife After Dawn"; } - std::string AutoStory_Checkpoint_0005::start_text() const { return AutoStory_Checkpoint_0004().end_text(); } - std::string AutoStory_Checkpoint_0005::end_text() const { return "Standing in front of PC. Recieved the Poketech. Ready to deposit starter"; } - - void AutoStory_Checkpoint_0005::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const { - run_story_table(env, context, options, stats, make_segment_00, {"00.05", "00.05"}); - } - - } - } -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h deleted file mode 100644 index a789a26969..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h +++ /dev/null @@ -1,170 +0,0 @@ -/* BDSP AutoStory - Segment 00 - * From beginning cinematic until after choosing the starter Pokemon. - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment00_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Segment00_H - -#include -#include -#include -#include "Pokemon/Navigation/Pokemon_Grid.h" -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - - -// Segment 00 as a table of checkpoints, segment_00_table(): 00.00 to 00.05, -// with 00.04 in four rows (to the catch, then 00.04b to 00.04d as story -// steps), each saving at its start. -Pokemon::Segment make_segment_00(StoryStepRunner& runner); - -// How segment_00_table()'s steps that call an old function run on the -// console, by their variants. -std::map segment_00_variants(); - -// DIAGNOSTIC ONLY: Remove before pushing to production. -// Starts at the post-catch save and runs only the rest of checkpoint 00.04. -void diagnostic_checkpoint_0004_after_catch(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats); - -// DIAGNOSTIC ONLY: Remove before pushing to production. -// 00.04's walk after the Potions, for the event-model diagnostic -// (PokemonBDSP_AutoStory_SandgemCenterEvents.h): from below Sandgem's -// Pokemon Center to Route 202's save spot, without saving. Fails with -// a screenshot as 00.04 does. `at`: the tiles in Sandgem we may stand on. -void walk_from_sandgem_center_to_route_202_save_spot( - VideoStream& stream, ProControllerContext& context, - const std::vector& at -); - -class AutoStory_Segment_00 : public AutoStory_Segment { -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_segment(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const override; -}; - -class AutoStory_Checkpoint_0000 : public AutoStory_Checkpoint { -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const override; -}; - -class AutoStory_Checkpoint_0001 : public AutoStory_Checkpoint { -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const override; -}; - -class AutoStory_Checkpoint_0002 : public AutoStory_Checkpoint { -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const override; -}; - -class AutoStory_Checkpoint_0003 : public AutoStory_Checkpoint { -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const override; -}; - -class AutoStory_Checkpoint_0004 : public AutoStory_Checkpoint { -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const override; -}; - -class AutoStory_Checkpoint_0005 : public AutoStory_Checkpoint { -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - - virtual void run_checkpoint(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats) const override; -}; - -// start: game launched, in front of the TV. -// end: Standing outside lake varety before starter -//Segment 00-00 begins -void checkpoint_000(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats); - -// start: Standing outside lake varety before starter -// end: Standing outside lake varety after starter battle -//Segment 00-001 begins -void checkpoint_001(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats); - -// start: Standing outside lake varety after starter battle -// end: recieved Pokedex, standing in front of the professor -//Segment 00-002 begins -void checkpoint_002(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats); - -// start: recieved Pokedex, standing in front of the professor -// end: Standing in the entrance to Jubilife City after speaking to dawn -//Segment 00-003 begins -void checkpoint_003(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats); - -// start: Standing in the entrance to Jubilife City after speaking to dawn -// end: Standing in the Pokemon Center ready to deposit pokemon after recieving poketech. -//Segment 00-004 begins -void checkpoint_004(SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - EventNotificationOption& notif_status_update, - AutoStoryStats& stats); -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp deleted file mode 100644 index dd06b7c19b..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp +++ /dev/null @@ -1,1656 +0,0 @@ -/* BDSP AutoStory - Segment 01 - * From the Jubilife Pokemon Center to Oreburgh City. - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "PokemonBDSP_AutoStory_Segment_01.h" -#include "../Data/PokemonBDSP_AutoStory_OreburghMine.h" -#include "../Data/PokemonBDSP_AutoStory_RavagedPath.h" -#include "../Data/PokemonBDSP_AutoStory_Route203.h" -#include "../Data/PokemonBDSP_AutoStory_Route204.h" -#include "../Data/PokemonBDSP_AutoStory_Route204South.h" -#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" -#include "../Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "../Detect/PokemonBDSP_AutoStory_OverworldDetector.h" -#include "../Utils/PokemonBDSP_AutoStory_PokemonCenter.h" -#include "../Utils/PokemonBDSP_AutoStory_Location.h" -#include "../Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "../Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" -#include "../Utils/PokemonBDSP_AutoStory_Walk.h" -#include "../PokemonBDSP_AutoStory_StoryTable.h" -#include "Common/Cpp/PrettyPrint.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "CommonFramework/Globals.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" - -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" - -#include -#include - -using namespace std::chrono_literals; - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using Pokemon::Direction; - - -namespace{ - -[[noreturn]] void fail(VideoStream& stream, const std::string& message){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, message, stream); -} - -// One tile south, for a player facing another way. -void step_south(VideoStream& stream, ProControllerContext& context){ - route_turn(stream, context, Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 1), 200ms); - context.wait_for_all_requests(); -} - -// True if a dialogue opens within `timeout`: someone who stopped us as the -// walk ended reads as BLOCKED, and their box comes up a moment later. -bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Milliseconds timeout){ - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_CYAN); - return wait_until(stream, context, timeout, {{dialog}}) == 0; -} - -// For a dialogue that has opened: B through it. A trainer's ends in a -// battle, which is fought with the best move (Potions when HP runs low), -// followed by a Potion if HP is under half. Anyone else's ends back in the -// overworld. Returns whether there was a battle. Between a trainer's last -// line and the battle the overworld shows with no dialogue for a moment, -// so "only talked" needs 4 s of it, and never comes when `battle_expected`. -// `doubles` for tag and twin battles. A lost battle ends in the last -// Pokemon Center; see in_pokemon_center(). -bool in_pokemon_center(VideoStream& stream); -uint64_t mean_brightness(const ImageViewRGB32& screen); -bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent = nullptr); -bool in_dim_cave(VideoStream& stream, uint64_t below = 85); -bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std::string& label, bool battle_expected = false, bool doubles = false){ - ShortDialogDetector dialog; - OverworldDetector overworld; - BattleMenuDetector battle_menu(BattleType::TRAINER); - // Roark's battle menu read as STANDARD, not TRAINER, and nothing fought - // it (r210d): either counts here, as only a trainer's battle can start. - BattleMenuDetector battle_menu_standard(BattleType::STANDARD); - BlackScreenDetector black; - auto quiet_since = std::chrono::steady_clock::now(); - for (size_t check = 0; check < 120; check++){ - context.wait_for_all_requests(); - const auto frame = stream.video().snapshot(); - if (!frame){ - fail(stream, label + ": No video frame."); - } - if (battle_menu.detect(*frame.frame) || battle_menu_standard.detect(*frame.frame) || black.detect(*frame.frame)){ - route_status(stream, "Fighting " + label + "."); - fight_story_battle(stream, context, label, doubles); - // Lines after the battle (Roark's badge, r211d) keep the menu - // from opening: finish them before the Potion. - if (dialogue_opens(stream, context, 3000ms)){ - finish_dialogue(stream, context, BUTTON_B, label + " after the battle"); - } - if (!in_pokemon_center(stream)){ - use_potion_first_pokemon(stream, context, 0.5); - } - return true; - } - const bool talking = dialog.detect(*frame.frame); - if (!talking && overworld.detect(*frame.frame)){ - if (!battle_expected && std::chrono::steady_clock::now() - quiet_since >= 4s){ - route_status(stream, label + " only talked."); - return false; - } - pbf_wait(context, 300ms); - continue; - } - quiet_since = std::chrono::steady_clock::now(); - if (talking){ - pbf_press_button(context, BUTTON_B, 80ms, 1100ms); - }else{ - pbf_wait(context, 300ms); - } - } - fail(stream, label + ": neither a battle nor the overworld after the dialogue."); -} - -// Logs a checkpoint's steps up front, then each step as it starts, with the -// one after it, in the log and the overlay. -class StepLog{ -public: - StepLog(VideoStream& stream, std::string checkpoint, std::string intro, std::vector steps) - : m_stream(stream), m_checkpoint(std::move(checkpoint)), m_steps(std::move(steps)) - { - std::string text = "[" + m_checkpoint + "] " + intro + ", the steps are:"; - for (size_t i = 0; i < m_steps.size(); i++){ - text += "\n " + std::to_string(i + 1) + ". " + m_steps[i]; - } - m_stream.log(text, COLOR_BLUE); - } - void operator()(size_t number){ - const std::string count = std::to_string(number) + "/" + std::to_string(m_steps.size()); - const bool last = number >= m_steps.size(); - m_stream.log("[" + m_checkpoint + " step " + count + "] " + m_steps[number - 1] + - (last ? std::string(" -- last step") : " -- next: " + m_steps[number]), COLOR_BLUE); - add_wrapped_log(m_stream.overlay(), m_checkpoint + " step " + count + ": " + m_steps[number - 1], COLOR_WHITE); - if (!last){ - add_wrapped_log(m_stream.overlay(), "Next: " + m_steps[number], COLOR_GRAY); - } - } - -private: - VideoStream& m_stream; - std::string m_checkpoint; - std::vector m_steps; -}; - -void jubilife_to_barry(VideoStream& stream, ProControllerContext& context){ - // What 01.01 does, stepped through on the console before writing it. - StepLog step(stream, "01.01", "From the Jubilife Pokemon Center to Barry", { - "Heal at the nurse, then walk out of the Pokemon Center", - "Walk to Jubilife's east exit and battle Barry at the Route 203 entrance", - }); - - // From 00.05g we are already outdoors on the street east to Route 203, - // healed on arriving in Jubilife: straight to the walk east. - bool outdoors = false; - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - outdoors = !indoors(*screen.frame); - } - if (outdoors){ - stream.log("[Route] 01.01: outdoors; skipping the Pokemon Center."); - }else{ - step(1); - // The save stands us at the PC, right of the counter, facing it; west - // is the counter's end. Two tiles south clears it, then three west is - // the floor emblem's column, with the nurse north and the door south. - route_turn(stream, context, Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 2), 200ms); - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 3), 200ms); - if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ - fail(stream, "Jubilife Pokemon Center: the nurse did not answer."); - } - // A throughout: Yes to "rest your Pokemon?" (B would answer No). - finish_dialogue(stream, context, BUTTON_A, "Nurse"); - // Healing shows no text box for a few seconds, which reads as the end; - // her "Thank you for waiting" lines follow it. - if (dialogue_opens(stream, context, 6000ms)){ - finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); - } - // South through the door until a girl standing below it stops us. The - // door's fade may or may not read as a transition. - WalkResult out{WalkStop::LIMIT, 0ms, 0}; - for (size_t walk = 0; walk < 3 && out.stop != WalkStop::BLOCKED; walk++){ - out = walk_until(stream, context, run_from_wild_battle, 0, -1, 8000ms, "out of the Pokemon Center"); - if (out.stop == WalkStop::DIALOG || out.stop == WalkStop::ARRIVED){ - break; - } - } - if (out.stop != WalkStop::BLOCKED){ - fail(stream, std::string("Jubilife Pokemon Center: walking out ended with ") + walk_stop_name(out.stop) + ", not at the girls below the door."); - } - - step(2); - // Jubilife's plaza has too few walls the grid gets right to plan on (a - // leg to a wall the map shows walked on 19 tiles). These are the holds - // that worked on the console: west onto the main road, north to the - // street east to Route 203, and back a couple of tiles. - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, 720ms, 300ms); - route_turn(stream, context, Direction::NORTH); - pbf_move_left_joystick(context, {0, 1}, 3960ms, 300ms); - route_turn(stream, context, Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, 600ms, 300ms); - context.wait_for_all_requests(); - } - // East along the street. The opening to the route is a few rows high; - // Barry stops us a little way in. A wall means we are a row off it: try - // rows to the south, then to the north. - static const int ROW_CHANGES[] = {0, 1, 1, -3, -1}; - bool barry = false; - for (int change : ROW_CHANGES){ - if (barry){ - break; - } - for (int i = 0; i < std::abs(change); i++){ - const Direction dir = change > 0 ? Direction::SOUTH : Direction::NORTH; - route_turn(stream, context, dir); - pbf_move_left_joystick(context, {0, change > 0 ? -1.0 : 1.0}, route_open_hold(dir, 1), 200ms); - } - context.wait_for_all_requests(); - WalkResult walk{WalkStop::LIMIT, 0ms, 0}; - for (size_t stretch = 0; stretch < 3 && walk.stop == WalkStop::LIMIT; stretch++){ - walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 9000ms, "east to Route 203"); - } - barry = walk.stop == WalkStop::DIALOG; - } - if (!barry){ - fail(stream, "Route 203: Barry did not stop us at the entrance."); - } - if (!talk_or_fight(stream, context, "Barry", true)){ - fail(stream, "Route 203: Barry talked but did not battle."); - } -} - -void route203_to_oreburgh(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.02", "From Barry on Route 203 to Oreburgh City", { - "Walk east along Route 203 into the Oreburgh Gate, fighting the trainers who see us", - "Listen to the Hiker, who gives HM Rock Smash", - "Walk east through the Gate past Curtis and Diana, out to Oreburgh City", - "Listen to the man who meets us at the city's entrance", - }); - - step(1); - // Route 203 only goes east, over three stretches joined by stairs, and - // the Gate's opening is in the cliff at its north-east end. Who stops us - // differs from run to run (a trainer's sight depends on our row), so - // there is no fixed list: walk east, talk or fight whoever stops us, and - // at a wall find a row from which east goes on. - size_t fights = 0; - bool inside = false; - // On Route 203 only trainers start a talk; one with no battle is the - // Hiker, who meets us inside the Gate (whose fade may not read as a - // transition). - bool met_hiker = false; - auto stopped_by = [&](){ - if (talk_or_fight(stream, context, "Route 203 trainer " + std::to_string(fights + 1))){ - fights++; - }else{ - route_status(stream, "No battle: that was the Hiker, inside the Oreburgh Gate."); - met_hiker = true; - inside = true; - } - }; - for (size_t leg = 0; leg < 40 && !inside; leg++){ - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203"); - if (walk.stop == WalkStop::TRANSITION){ - inside = true; - break; - } - if (walk.stop == WalkStop::LIMIT){ - continue; - } - if (walk.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 1500ms)){ - stopped_by(); - continue; - } - // A wall. Find the row from which east goes on: north to the top wall - // first (a step north there does not move us, so rows are counted - // from it), then a row at a time south. At the route's end this is the - // corner by the second sign, with the Gate's opening two rows down. - walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "north to the top wall"); - bool moved_on = false; - for (size_t row = 1; row <= 10 && !moved_on; row++){ - step_south(stream, context); - const WalkResult next = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); - if (next.stop == WalkStop::TRANSITION){ - inside = true; - } - if (next.stop == WalkStop::DIALOG || (next.stop == WalkStop::BLOCKED && dialogue_opens(stream, context, 1500ms))){ - stopped_by(); - } - // Two still chunks are the least a wall takes; more is progress. - moved_on = inside || next.stop != WalkStop::BLOCKED || next.walked > 1200ms; - } - if (!moved_on){ - fail(stream, "Route 203: walls east on every row down to ten below the top wall."); - } - } - if (!inside){ - fail(stream, "Route 203: did not reach the Oreburgh Gate."); - } - route_status(stream, "Route 203: " + std::to_string(fights) + " trainer battle(s) on the way."); - context.wait_for(1500ms); - - step(2); - // The Hiker steps up to us as we walk east. - if (!met_hiker){ - const WalkResult to_hiker = walk_until(stream, context, run_from_wild_battle, 1, 0, 4000ms, "to the Hiker"); - if (to_hiker.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ - fail(stream, "Oreburgh Gate: the Hiker did not stop us."); - } - finish_dialogue(stream, context, BUTTON_B, "Hiker"); - } - // Then a "Hidden Moves" tutorial card, closed with B. It is no dialogue - // box, and it can come up a moment after his last line. - pbf_wait(context, 1500ms); - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - context.wait_for_all_requests(); - - step(3); - // East: Camper Curtis and Picnicker Diana see us; past them the exit's - // white fade, and the man in Oreburgh stops us right outside. - fights = 0; - bool city = false; - size_t stuck = 0; - for (size_t leg = 0; leg < 12 && !city; leg++){ - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east through the Oreburgh Gate"); - if (walk.stop == WalkStop::LIMIT || walk.stop == WalkStop::TRANSITION){ - continue; - } - if (walk.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ - // A card or menu left up holds the screen still: B once, then retry. - if (++stuck > 1){ - fail(stream, "Oreburgh Gate: a wall stopped us walking east."); - } - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - continue; - } - stuck = 0; - if (fights >= 2){ - city = true; - break; - } - fights += talk_or_fight(stream, context, "Oreburgh Gate trainer " + std::to_string(fights + 1)) ? 1 : 0; - } - if (!city){ - fail(stream, "Oreburgh Gate: did not reach Oreburgh City."); - } - - step(4); - // He walks us to the Pokemon Center between his lines, and Barry joins - // there: more lines after each walk. - finish_dialogue(stream, context, BUTTON_B, "Man at Oreburgh's entrance"); - for (size_t scene = 0; scene < 6 && dialogue_opens(stream, context, 6000ms); scene++){ - finish_dialogue(stream, context, BUTTON_B, "Oreburgh scene"); - } - context.wait_for_all_requests(); -} - -// Holds the stick toward `dir` for `tiles` tiles, turning first. -void hold_tiles(VideoStream& stream, ProControllerContext& context, Direction dir, size_t tiles){ - route_turn(stream, context, dir); - const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; - const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; - pbf_move_left_joystick(context, {x, y}, route_open_hold(dir, tiles), 200ms); - context.wait_for_all_requests(); -} - -WalkResult walk_dir( - VideoStream& stream, ProControllerContext& context, Direction dir, Milliseconds limit, const std::string& label, - const std::function& arrived = nullptr -){ - const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; - const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; - return walk_until(stream, context, run_from_wild_battle, x, y, limit, label, arrived); -} - -// Out of a building whose door is straight south of us, a D-pad tile at a -// time, until the location banner shows: it names the map we step into -// (e.g. "Oreburgh City") for a few seconds, and never shows inside. Ends in -// the doorway, facing south. Out of the Oreburgh Gym from in front of Roark -// (by hand, 2026-10-31): 18 taps inside, 2 taps of black fade, the banner on -// the 21st. The door's fade alone was missed between steps (r219g walked on -// south to the rocks below the gym). -bool dpad_out_of_building(VideoStream& stream, ProControllerContext& context, size_t max_taps, const std::string& label){ - pbf_press_dpad(context, DPAD_DOWN, 160ms, 700ms); // turn - for (size_t tap = 0; tap < max_taps; tap++){ - pbf_press_dpad(context, DPAD_DOWN, 160ms, 900ms); - context.wait_for_all_requests(); - const VideoSnapshot frame = stream.video().snapshot(); - if (frame && location_banner_showing(*frame.frame)){ - stream.log("[Walk] " + label + ": the location banner shows after " + std::to_string(tap + 1) + " taps; outside."); - context.wait_for(1000ms); - return true; - } - if (dialogue_opens(stream, context, 100ms)){ - finish_dialogue(stream, context, BUTTON_B, label + ": someone in the way"); - } - } - return false; -} - -// For a player inside a Pokemon Center after a whiteout, at the counter: -// out through the door. The door is straight south of the nurse in most -// Centers; in Oreburgh's it was two tiles east of where we stood (by hand). -void walk_out_of_center(VideoStream& stream, ProControllerContext& context, const std::string& label){ - finish_dialogue(stream, context, BUTTON_B, label + ": the nurse"); - const int shifts[] = {0, 2, -4}; - for (int shift : shifts){ - if (shift != 0){ - hold_tiles(stream, context, shift > 0 ? Direction::EAST : Direction::WEST, (size_t)std::abs(shift)); - } - // A tile at a time, looking after each, so we stop on the doorstep: - // walk_until() looks only between 400 ms holds, and r205d held on - // through the fade and stepped a tile further south. - bool out = false; - for (size_t tile = 0; tile < 10 && !out; tile++){ - // D-pad taps: whole tiles (the first only turns us from the nurse). - pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); - context.wait_for(1500ms); - if (dialogue_opens(stream, context, 100ms)){ - finish_dialogue(stream, context, BUTTON_B, label + ": someone in the Center"); - } - const VideoSnapshot frame = stream.video().snapshot(); - size_t percent = 100; - if (frame){ - center_floor_on_screen(*frame.frame, &percent); - } - out = percent < 30; - } - if (out){ - // "No floor" also reads true on the door's black fade, and the game - // takes no input until it ends: the first D-pad taps after it were - // lost and the gym's door ended a tile away (r215d, r216d; a shot - // per press by hand showed the black frame). Wait for daylight. - const auto start = current_time(); - while (current_time() - start < 6000ms){ - context.wait_for(100ms); - const VideoSnapshot frame = stream.video().snapshot(); - if (frame && mean_brightness(*frame.frame) > 40){ - break; - } - } - context.wait_for(1000ms); - return; - } - } - fail(stream, label + ": could not find the Pokemon Center's door."); -} - -// Oreburgh: from a row or two below the Pokemon Center's front, west of -// the fence below its east corner, into the Center by its door, stopping -// just inside. Talks to Barry if he stands in the door (01.03, the first -// time). -void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ - // The save stands us a row or two below the Center's front, a little - // west of its door. The posts and the road beside the Center make north - // from there unreliable (r191a, r192a walked up the road to the city's - // north exit), and counted taps east drift. The fence running south from - // the Center's east corner stops a walk east on these rows a tile east of - // the door (by hand): walk to it, north to the Center's front, tap back - // one, and shift one tap either side. - walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 5000ms, "east to the fence below the Center"); - walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); - // The door wants us near its middle: a turn and a tap west (half a tile - // too far, by hand) or a turn alone leave north blocked by the facade - // beside it. The facade blocks north for two tiles either side of the - // door, so look for it in nudges of about a third of a tile. - route_turn(stream, context, Direction::WEST); - context.wait_for_all_requests(); - auto inside_now = [&](){ - const VideoSnapshot frame = stream.video().snapshot(); - size_t percent = 0; - return frame && (center_floor_on_screen(*frame.frame, &percent), percent >= 45); - }; - bool inside = false; - const int offsets[] = {0, -1, -2, 1, -3, 2, -4, 3}; // nudges from here - int nudged = 0; - for (size_t i = 0; i < 8 && !inside; i++){ - const int offset = offsets[i]; - if (offset != nudged){ - // No turn first: a turn is a tap too and moves us (see - // talk_with_nudges()); a nudge both turns and moves a little. - for (; nudged != offset; nudged += offset > nudged ? 1 : -1){ - pbf_move_left_joystick(context, {offset > nudged ? 1.0 : -1.0, 0}, ROUTE_NUDGE_TAP, 300ms); - } - context.wait_for_all_requests(); - } - // The door's fade often reads as the time limit, not a black screen - // (r183a went in and on to the counter): look at the floor. - // Stop as soon as the floor shows: walking on to the counter left the - // door's column, and the walk back out drifted into the yard (r188b). - // Oreburgh's sand reads up to 34 % warm outside (r189a), the floor - // 56 to 64 % inside. - const WalkResult in = walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", - [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); - if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && inside_now())){ - inside = true; - break; - } - if (in.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, "Someone at the Center's door"); - }else{ - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - if (!dialogue_opens(stream, context, 2000ms)){ - continue; - } - finish_dialogue(stream, context, BUTTON_B, "Barry at the Center's door"); - context.wait_for(2000ms); // he runs off - } - // Talked in the door's column: the door is free now. - walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", - [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); - inside = inside_now(); - } - if (!inside || !inside_now()){ - fail(stream, "Oreburgh: no column east of the save went into the Pokemon Center."); - } -} - -// Oreburgh: from just inside the Pokemon Center's door, up to the nurse -// and heal; ends at the counter facing her. The door is straight south of -// her (by hand, 2026-10-31: north from the door stopped at the counter). -void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context){ - if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ - fail(stream, "Oreburgh Pokemon Center: the nurse did not answer."); - } - // A throughout: Yes to "rest your Pokemon?" (B would answer No). - finish_dialogue(stream, context, BUTTON_A, "Nurse"); - // Healing shows no text box for a few seconds, which reads as the end; - // her "Thank you for waiting" lines follow it. - if (dialogue_opens(stream, context, 6000ms)){ - finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); - } -} - -// Oreburgh: from just inside the Pokemon Center's door, out onto the tile -// below it, facing south, on the door's column. Stops as soon as the floor -// is gone: walking on runs down the column to the mine. -void leave_center_onto_door_row(VideoStream& stream, ProControllerContext& context){ - walk_until(stream, context, run_from_wild_battle, 0, -1, 4000ms, "out of the Center's door", - [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent < 30; }); - context.wait_for(1500ms); -} - -void oreburgh_to_roark(VideoStream& stream, ProControllerContext& context){ - // What 01.03 does, stepped through on the console before writing it. - StepLog step(stream, "01.03", "From the Oreburgh Pokemon Center to Roark in the mine", { - "Into the Pokemon Center by its door (talking to Barry if he stands in it) and back out onto the door's column", - "Straight south from the door into the mine", - "Through the mine's entrance hall to the rocky corridor", - "Walk the planned route down the corridor and round the coal block, and talk to Roark", - "Walk back to the corridor's top and out of the mine", - "Into the Pokemon Center and heal at the counter", - }); - - step(1); - // Planned on Bulbapedia's Oreburgh map (devtools/maps/examples/ - // oreburgh_center_to_mine.png): the column of the Center's door runs - // straight south down the yard's stairs, between the conveyors, into - // the mine (walked north up it by hand, 2026-10-31). The door is the one - // place that pins that column: go in and back out, and we stand on it - // facing south. The save stands us two tiles west of the door and a row - // down; north stops against the Center's front or its post, then a tile - // at a time east until north goes in. Barry stands in the door the first - // time: walking into him reads as a wall, so A there; he tells us Roark - // went to the mine, which Roark needs before he answers (2026-10-29: he - // ignored A in r101 and r104 until then). A save after his talk has him - // gone already. - // - enter_oreburgh_center(stream, context); - context.wait_for(1000ms); - - step(2); - // Straight back south: out of the door and on into the mine. Fades go - // unseen at times (r184b missed the mine's, r185a the Center's and walked - // on into the mine hall), so count: the Center's door and the mine are - // two transitions, or about 1 s inside and 8.6 s outside of walking (by - // hand). A worker walks about on the sand path and stops the walk as a - // wall would (r95, r96): wait for him to move and walk on. Step 3 copes - // with ending in the hall or in the corridor. - bool in_mine = false; - size_t transitions = 0; - Milliseconds walked = 0ms; - for (size_t attempt = 0; attempt < 6 && !in_mine; attempt++){ - const WalkResult down = walk_dir(stream, context, Direction::SOUTH, 15000ms, "south out of the Center and into the mine"); - walked += down.walked; - if (down.stop == WalkStop::TRANSITION){ - transitions++; - context.wait_for(1000ms); - }else if (down.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, "Someone on the way to the mine"); - }else if (down.stop == WalkStop::BLOCKED){ - context.wait_for(2000ms); - } - // Counting alone took a walk that drifted into the yard (r188b) for the - // mine: it also has to look like the mine. - in_mine = (transitions >= 2 || walked >= 9500ms) && in_dim_cave(stream); - } - if (!in_mine){ - fail(stream, "Oreburgh: the walk south from the Center's door did not reach the mine."); - } - context.wait_for(1500ms); - - step(3); - // If the fade into the mine was missed, we are already in the corridor - // and this walk stops at its boulder; otherwise it crosses the hall. - const WalkResult hall = walk_dir(stream, context, Direction::SOUTH, 10000ms, "through the mine's entrance hall"); - if (hall.stop == WalkStop::TRANSITION){ - context.wait_for(1500ms); - } - - step(4); - // Planned on the coal room's tile grid (Data/OreburghMine): the - // corridor's rocks zigzag a row at a time, the room's second row passes - // under the worker on the first, and the east side leads down to the - // bottom row below Roark. - const Pokemon::MapSet& maps = oreburgh_mine_maps(); - auto world = [&](const Pokemon::TilePos& pos){ return maps.to_world(MAP_OREBURGH_MINE, pos); }; - std::vector no_trainers; - auto in_the_way = [&](const std::string& who){ talk_or_fight(stream, context, who); }; - std::vector at; - for (const Pokemon::TilePos& pos : OreburghMine::corridor_top()){ - at.push_back(world(pos)); - } - // Roark stands with a rock on each side, so the rock east of him is the - // anchor: plan to his row's open stretch east of the rock, walk north to - // the wall above it and west until the rock stops us, two tiles east of - // him (on the console, r97). Then south to the bottom wall and two tiles - // west puts us below him. - const Pokemon::TilePos roark = world(OreburghMine::ROARK); - at = walk_route(stream, context, run_from_wild_battle, maps, at, - [&](const Pokemon::TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }, - no_trainers, in_the_way, "To the stretch east of Roark"); - walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); - // His rock and the bottom wall end these legs (see walk_to_wall()). The - // two taps west from there land within a third of a tile or so of his - // column (s03e2 missed him by that much), so look for him with nudges - // either side. Any tap here can start a wild battle, which leaves us - // off his column (r180a, even on his row west of his rock): then line up - // again from the east end of the bottom row, three taps west and north to the wall on his row (Data/ - // OreburghMine: columns 23 to 25 have that wall above). - bool talked = false; - for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ - if (attempt > 0){ - // The battle may leave us on his row or the bottom one. - walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom row"); - walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 6000ms, "east along the bottom row"); - route_turn(stream, context, Direction::WEST); - for (size_t tap = 0; tap < 3; tap++){ - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - } - walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); - } - // A wild battle on the way eats into the walk's time (r100): walk again. - for (size_t walk = 0; walk < 3; walk++){ - if (walk_to_wall(stream, context, run_from_wild_battle, -1, 0, 6000ms, "west to the rock beside Roark").wild_battles == 0){ - break; - } - } - walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom wall"); - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - route_turn(stream, context, Direction::NORTH); - context.wait_for_all_requests(); - if (run_from_late_encounter(stream, context, run_from_wild_battle, "below Roark")){ - continue; - } - if (VideoSnapshot screen = stream.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; - Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-below-roark.png"); - } - // The camera zooms in and his first line, "OK, stand back and watch - // this.", can take some seconds (r98). - // His rocks answer A too ("It's a rugged rock..."): his talk is the - // one that zooms the camera in on us (by hand), which the rock's - // message does not. Compare the view above the dialogue box. - talked = talk_with_nudges(stream, context, run_from_wild_battle, Direction::NORTH, "Roark", 8000ms, - [&](const ImageViewRGB32& before, const ImageViewRGB32& after){ - const ImageFloatBox above_box{0.0, 0.0, 1.0, 0.6}; - const double change = walk_screen_change( - extract_box_reference(before, above_box), extract_box_reference(after, above_box) - ); - stream.log("[01.03] View change when the talk opened: " + std::to_string(change) + " (Roark zooms in)."); - return change > 35; // by hand: 50 for his zoom - }); - } - if (!talked){ - fail(stream, "Oreburgh Mine: no talk opened below Roark (has Barry told us about the mine?)."); - } - // His scene (by hand, 2026-10-31): the camera zooms in, about eight - // lines that each wait for a press ("OK, stand back and watch this.", - // Rock Smash, "...That'd be me!"), then he smashes his rock and leaves. - // finish_dialogue() ended early on it, and stepping onto his tile could - // not tell him from the time the stop takes to read (r184, r186 "passed" - // with him still there, and Barry kept the gym shut). Press B through it - // on a clock, then ask his tile: once he is gone, A there opens nothing. - // B and A in the overworld facing a wall do nothing. - bool gone = false; - for (size_t round = 0; round < 3 && !gone; round++){ - for (size_t press = 0; press < 14; press++){ - pbf_press_button(context, BUTTON_B, 80ms, 2420ms); - } - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - gone = !dialogue_opens(stream, context, 2500ms); - } - if (!gone){ - fail(stream, "Oreburgh Mine: Roark is still there after the talk."); - } - walk_dir(stream, context, Direction::SOUTH, 2000ms, "back to the bottom wall"); - at = {roark.next(Direction::SOUTH)}; - - step(5); - // Back up the corridor, then north out through the hall. A route leg - // north to the top's wall walks on through it into the hall (r99), and - // no wall stops us a row short of it, so the route ends on the corner - // at (14,10), where that leg started, and the walk does the rest. - const Pokemon::TilePos exit = world(OreburghMine::CORRIDOR_EXIT_START); - at = walk_route(stream, context, run_from_wild_battle, maps, at, - [&](const Pokemon::TilePos& pos){ return pos == exit; }, - no_trainers, in_the_way, "Back to the corridor's top"); - // Up into the entrance hall (a transition, as on the way in), then on - // north out of the mine: in r103 that walk left the mine without a - // transition we saw and ended against the sign above the stairs, which - // the moving belts make read as the time limit. - if (walk_dir(stream, context, Direction::NORTH, 10000ms, "into the entrance hall").stop != WalkStop::TRANSITION){ - fail(stream, "Oreburgh Mine: walking north from the corridor did not reach the entrance hall."); - } - context.wait_for(1500ms); - if (walk_dir(stream, context, Direction::NORTH, 10000ms, "out of the mine").stop == WalkStop::DIALOG){ - fail(stream, "Oreburgh Mine: someone stopped us on the way out of the mine."); - } - context.wait_for(1500ms); - - step(6); - // End at the nurse, healed: the counter is a fixed start for 01.04, - // where the mine's mouth is not (2026-10-31, at the user's request). Out - // of the mine we stand below its sign; a tile east is the Center door's - // column, and north on it goes in, or stops on the front rows. - hold_tiles(stream, context, Direction::EAST, 1); - walk_until(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into the Center's door", - [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); - size_t floor = 0; - if (VideoSnapshot frame = stream.video().snapshot()){ - center_floor_on_screen(*frame.frame, &floor); - } - if (floor < 45){ - route_turn(stream, context, Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, 480ms, 300ms); - enter_oreburgh_center(stream, context); - } - heal_at_oreburgh_counter(stream, context); -} - - -// A rough check that we stand in a Pokemon Center: its floor fills the lower -// half of the screen with warm yellow, where the gym is grey and routes are -// green. After a battle it tells a loss (a whiteout ends at the counter) -// from a win. -bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent){ - const ImageRGB32 small = screen.scale_to(192, 108); - size_t warm = 0, total = 0; - for (size_t y = 54; y < 108; y++){ - for (size_t x = 0; x < 192; x++){ - const uint32_t pixel = small.pixel(x, y); - const int r = (pixel >> 16) & 0xff, g = (pixel >> 8) & 0xff, b = pixel & 0xff; - total++; - if (r > 200 && g > 150 && b < 150 && r - b > 70){ - warm++; - } - } - } - if (percent){ - *percent = warm * 100 / total; - } - return warm * 10 > total * 3; -} -bool in_pokemon_center(VideoStream& stream){ - const VideoSnapshot frame = stream.video().snapshot(); - if (!frame){ - return false; - } - size_t percent = 0; - const bool inside = center_floor_on_screen(*frame.frame, &percent); - stream.log("[Center check] warm floor " + std::to_string(percent) + " %: " + - (inside ? "in a Pokemon Center." : "not in a Pokemon Center.")); - return inside; -} - -// The Oreburgh Mine's hall and corridors are dim: mean brightness 45 to 64 -// on the console, against 99 to 116 for the city and its yard (2026-10-31). -bool in_dim_cave(VideoStream& stream, uint64_t below){ - const VideoSnapshot frame = stream.video().snapshot(); - if (!frame){ - return false; - } - const ImageRGB32 small = frame.frame->scale_to(192, 108); - uint64_t sum = 0; - for (size_t y = 0; y < 108; y++){ - for (size_t x = 0; x < 192; x++){ - const uint32_t pixel = small.pixel(x, y); - sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); - } - } - const uint64_t mean = sum / (3 * 192 * 108); - stream.log("[Cave check] mean brightness " + std::to_string(mean) + "."); - return mean < below; -} - - -// Oreburgh, from the street in front of the Pokemon Center (its door or its -// front wall) into the gym, as walked by hand on 2026-10-29: west to the -// cliff, south to the posts, a row up, then 17 tiles east is the gym door's -// column. The hold is checked by the door itself: walk north, and if the -// gym's wall stops us, try the columns either side. -// D-pad moves from the Center's doorstep (as walk_out_of_center() leaves us -// there) to below the gym's door: 20. The program's own runs landed 2 tiles -// west with 22 and 1 tile west with 21 (r217d, r218d, the same tiles each -// time). By hand, after a stick exit, it took 22 taps; the hand exit and -// count did not carry over to the program's. Stick holds drift: 160 ms moved -// 36 to 48 px, so 18 or 19 of them missed the door by a part of a tile. -constexpr size_t GYM_DPAD_TAPS_WEST_OF_CENTER = 20; -uint64_t mean_brightness(const ImageViewRGB32& screen); - -// `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the -// player moves a whole tile per tap (measured above), so counted legs do -// not drift the way stick holds do. The first tap toward a new way only -// turns the player, so turn first. -void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label){ - stream.log("[Walk] " + label + ": " + std::to_string(taps) + " D-pad taps."); - pbf_press_dpad(context, dpad, 160ms, 700ms); // turn - // Count only taps that moved the view: one after the turn was lost and - // the leg ended a tile short (r215d). A tile's scroll changes the view by - // about 100; standing still, under 15. A tap that does not move is someone - // in the way: people walk about these streets (r220g: blocked on tap 9 - // for 12 s while a passer-by stood in our row). Wait for them to move on, - // up to 30 s, before giving up. - size_t moved = 0; - const auto deadline = current_time() + std::chrono::milliseconds(taps * 1000 + 30000); - while (moved < taps && current_time() < deadline){ - context.wait_for_all_requests(); - const VideoSnapshot before = stream.video().snapshot(); - pbf_press_dpad(context, dpad, 160ms, 700ms); - context.wait_for_all_requests(); - const VideoSnapshot after = stream.video().snapshot(); - const double change = before && after ? walk_screen_change(*before.frame, *after.frame) : 100; - if (change > 30){ - moved++; - continue; - } - stream.log("[Walk] " + label + ": tap " + std::to_string(moved + 1) + " did not move (view change " + std::to_string(change) + "); someone in the way? Waiting."); - if (dialogue_opens(stream, context, 300ms)){ - talk_or_fight(stream, context, label + ": someone in the way"); - } - context.wait_for(1500ms); - } - if (moved < taps){ - fail(stream, label + ": only " + std::to_string(moved) + " of " + std::to_string(taps) + " D-pad taps moved us."); - } -} - -// Through a door straight north of us: tap up to face it, hold up into it, -// and wait for its dark fade (by hand: brightness 7, about 1 s after). -bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label){ - pbf_press_dpad(context, DPAD_UP, 160ms, 300ms); - pbf_press_dpad(context, DPAD_UP, 300ms, 0ms); - context.wait_for_all_requests(); - const auto start = current_time(); - while (current_time() - start < 5000ms){ - context.wait_for(100ms); - const VideoSnapshot frame = stream.video().snapshot(); - if (frame && mean_brightness(*frame.frame) < 25){ - stream.log("[Route] " + label + ": the screen went dark; through the door."); - context.wait_for(2000ms); - return true; - } - } - stream.log("[Route] " + label + ": no dark screen after the D-pad step north."); - return false; -} - -// Mean of the three channels over a small copy of the frame, 0 to 255. -uint64_t mean_brightness(const ImageViewRGB32& screen){ - const ImageRGB32 small = screen.scale_to(192, 108); - uint64_t sum = 0; - for (size_t y = 0; y < 108; y++){ - for (size_t x = 0; x < 192; x++){ - const uint32_t pixel = small.pixel(x, y); - sum += ((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff); - } - } - return sum / (3 * 192 * 108); -} - -// North through a door from just below it: one tile's step, then watch -// for the door's black fade. Entering the Oreburgh Gym (by hand, -// 2026-10-31): the door opens and the camera zooms in, and about 2 s after -// the step the screen is black (brightness 7) for 1 to 1.5 s. The step's -// own walk stops watching when the stick is let go, so it missed that -// black; a scene comparison could not tell a tile's scroll (106 to 119) -// from going in (121, r205d, r207d). A miss steps back onto the row. -bool through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label){ - auto went_dark = [&](Milliseconds timeout){ - const auto start = current_time(); - while (current_time() - start < timeout){ - context.wait_for(100ms); - const VideoSnapshot frame = stream.video().snapshot(); - if (frame && mean_brightness(*frame.frame) < 25){ - return true; - } - } - return false; - }; - context.wait_for_all_requests(); - const VideoSnapshot before = stream.video().snapshot(); - const WalkResult up = walk_for(stream, context, run_from_wild_battle, 0, 1, 240ms, label); - // Look for the fade ourselves: the library's black screen check missed it - // under the gym's door (r209d), while its frames read 7 against about - // 100 outside. - if (up.stop == WalkStop::TRANSITION || went_dark(5000ms)){ - stream.log("[Route] " + label + ": the screen went dark; through the door."); - context.wait_for(2000ms); - return true; - } - const VideoSnapshot after = stream.video().snapshot(); - const double moved = before && after ? walk_screen_change(*before.frame, *after.frame) : 0; - // The door opened and the camera zoomed in, but one step did not take us - // through (r213d: view change 160, no fade): step on north. - if (moved > WALK_SCENE_CHANGE){ - stream.log("[Route] " + label + ": the door opened (view change " + std::to_string(moved) + "); on north."); - walk_for(stream, context, run_from_wild_battle, 0, 1, 240ms, label + ": on through"); - if (went_dark(5000ms)){ - stream.log("[Route] " + label + ": the screen went dark; through the door."); - context.wait_for(2000ms); - return true; - } - } - // Back down only if the step moved us: against the building's front it - // does not, and stepping south anyway took us a row down each try - // (r208d drifted south along the gym's front). - stream.log("[Route] " + label + ": no dark screen; not the door (view change " + std::to_string(moved) + ")."); - if (moved > WALK_BLOCKED_CHANGE * 2){ - walk_for(stream, context, run_from_wild_battle, 0, -1, 240ms, label + ": back down"); - } - return false; -} - -void oreburgh_street_to_gym(VideoStream& stream, ProControllerContext& context){ - // From the Center's doorstep, straight west along its row to below the - // gym's door, then north through it, all on the D-pad (whole tiles; see - // GYM_DPAD_TAPS_WEST_OF_CENTER). - dpad_tiles(stream, context, DPAD_LEFT, GYM_DPAD_TAPS_WEST_OF_CENTER, "west to the gym's door"); - if (dpad_through_door_north(stream, context, "Oreburgh Gym door")){ - return; - } - fail(stream, "Oreburgh: no column near the gym's door went in."); -} - -// Inside the Oreburgh Gym: straight up the middle, fighting the trainers -// who stop us, to Roark on the top tier. Returns whether Roark was beaten; -// false after a whiteout, which leaves us in the Pokemon Center. -// Walking straight up meets both gym trainers (one at the entrance, one on -// the second tier); their experience helps an under-levelled team, and -// after a whiteout they stay beaten. Butters' route goes round them. -bool oreburgh_gym_battles(VideoStream& stream, ProControllerContext& context){ - // The entry camera pans for about 7.5 s (Butters' reference video). - context.wait_for(8000ms); - if (dialogue_opens(stream, context, 3000ms)){ - if (talk_or_fight(stream, context, "Gym trainer at the entrance") && in_pokemon_center(stream)){ - return false; - } - } - size_t stuck = 0; - for (size_t leg = 0; leg < 10; leg++){ - const WalkResult up = walk_until_no_wild(stream, context, 0, 1, 8000ms, "north up the gym"); - if (up.stop == WalkStop::DIALOG){ - if (talk_or_fight(stream, context, "Gym trainer") && in_pokemon_center(stream)){ - return false; - } - continue; - } - if (up.stop != WalkStop::BLOCKED){ - continue; - } - // Roark stands at the top facing us; a wall or a stair edge says - // nothing to A. Only Roark starts a battle from our A here. The walk - // left us facing north: a turn tap that way would be a step. - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - if (dialogue_opens(stream, context, 3000ms)){ - // His lines pause for more than 4 s before the battle (r210d: "only - // talked"), so the battle is expected. - if (talk_or_fight(stream, context, "Roark", true)){ - if (in_pokemon_center(stream)){ - return false; - } - finish_dialogue(stream, context, BUTTON_B, "Roark after the battle"); - return true; - } - continue; - } - if (up.walked <= 800ms && ++stuck >= 3){ - fail(stream, "Oreburgh Gym: stuck on the way up, and nobody answers A."); - } - } - fail(stream, "Oreburgh Gym: did not reach Roark."); -} - -void oreburgh_gym(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.04", "From the Oreburgh Mine to the Coal Badge", { - "Out of the Pokemon Center from the counter", - "West along the row into the gym", - "Up the gym, fighting its trainers, and beat Roark (after a loss: from the Center, again)", - }); - step(1); - // 01.03 leaves us at the Center's counter, facing the nurse; the door - // is straight south. - walk_out_of_center(stream, context, "Oreburgh"); - - // Losing costs some money and keeps the experience, so each try comes - // back stronger: Roark won twice by hand against Piplup Lv 10 and 14 and - // lost the third time. - bool won = false; - for (size_t attempt = 0; attempt < 5 && !won; attempt++){ - step(2); - oreburgh_street_to_gym(stream, context); - step(3); - won = oreburgh_gym_battles(stream, context); - if (!won){ - stream.log("[01.04] Lost in the gym (try " + std::to_string(attempt + 1) + "); back from the Center.", COLOR_ORANGE); - walk_out_of_center(stream, context, "Oreburgh"); - } - } - if (!won){ - fail(stream, "Oreburgh Gym: lost to Roark five times."); - } - -} - -// West until a transition, by the user's rule for a wall in the way: walk -// north to the top wall, which pins the row, then try the rows south of it -// one at a time. Trainers who stop us are fought. Returns at the first -// transition (a building, a cave, a gate), or when `arrived` matches. -void west_until_transition( - VideoStream& stream, ProControllerContext& context, const std::string& label, size_t max_legs, - const std::function& arrived = nullptr -){ - for (size_t leg = 0; leg < max_legs; leg++){ - const WalkResult west = walk_dir(stream, context, Direction::WEST, 20000ms, label, arrived); - if (west.stop == WalkStop::ARRIVED){ - return; - } - if (west.stop == WalkStop::TRANSITION){ - context.wait_for(2000ms); - return; - } - if (west.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, label + ": someone in the way"); - continue; - } - if (west.stop != WalkStop::BLOCKED){ - continue; - } - walk_dir(stream, context, Direction::NORTH, 4000ms, label + ": north to the top wall"); - bool moved = false; - for (size_t row = 0; row < 10 && !moved; row++){ - hold_tiles(stream, context, Direction::SOUTH, 1); - const WalkResult probe = walk_dir(stream, context, Direction::WEST, 1500ms, label + ": west on the next row", arrived); - if (probe.stop == WalkStop::ARRIVED){ - return; - } - moved = probe.stop != WalkStop::BLOCKED || probe.walked > 1200ms; - if (probe.stop == WalkStop::TRANSITION){ - context.wait_for(2000ms); - return; - } - if (probe.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, label + ": someone in the way"); - } - } - if (!moved){ - fail(stream, label + ": no row below the top wall leads west."); - } - } - fail(stream, label + ": no transition after " + std::to_string(max_legs) + " legs."); -} - -// Oreburgh: from in front of Roark at the top of the gym, back down and out -// of its door a tile at a time, east along the row to the Pokemon Center -// and heal; ends at the counter. -void oreburgh_gym_to_center(VideoStream& stream, ProControllerContext& context){ - if (!dpad_out_of_building(stream, context, 40, "out of the gym")){ - fail(stream, "Oreburgh Gym: did not leave by its door."); - } - - // Heal at the Center, so 01.05 starts at its counter: the gym's doorstep - // row runs east to the Center's door, the way 01.04 came. - // The way 01.04 came, reversed, on the D-pad. Untested on the console: - // the gym's doorstep is assumed to be the tile below its door. - dpad_tiles(stream, context, DPAD_RIGHT, GYM_DPAD_TAPS_WEST_OF_CENTER, "east to the Center's door"); - if (!dpad_through_door_north(stream, context, "Oreburgh Pokemon Center door")){ - fail(stream, "Oreburgh: the Pokemon Center's door did not take us from the gym's row."); - } - heal_at_oreburgh_counter(stream, context); -} - -void oreburgh_to_route203(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.05", "From the Oreburgh Gym out of the Oreburgh Gate onto Route 203", { - "Out of the gym onto its doorstep", - "Up to the platform by Oreburgh Gate and into it (Barry bumps into us there)", - "West through the Gate until Route 203's banner shows", - }); - step(1); - // 01.04 ends in front of Roark, just after his battle: its save at - // 01.05's start is right after the win (2026-10-31, at the user's - // request), so a failure later never has to beat him again. - // No trip back to the Oreburgh Center (2026-10-31, the user): talk_or_fight() - // gives a Potion after Roark when HP is low, and Jubilife's Center is on - // the way. Out of the gym onto its doorstep, which is on the Center door's - // row: west along it reaches the stairs' cliff. - if (!dpad_out_of_building(stream, context, 40, "out of the gym")){ - fail(stream, "Oreburgh Gym: did not leave by its door."); - } - - step(2); - // From the gym's doorstep, as walked by hand on 2026-10-29. - walk_dir(stream, context, Direction::WEST, 20000ms, "west to the stairs' cliff"); - walk_dir(stream, context, Direction::NORTH, 10000ms, "north along the cliff"); - hold_tiles(stream, context, Direction::EAST, 2); - walk_dir(stream, context, Direction::NORTH, 5000ms, "north up the stairs"); - walk_dir(stream, context, Direction::WEST, 6000ms, "west along the platform"); - hold_tiles(stream, context, Direction::SOUTH, 1); - // Barry runs into us on the platform ("*thud*") the first time; his lines - // end with no battle. - west_until_transition(stream, context, "Into Oreburgh Gate", 8); - - step(3); - // The cave's fade out of the Gate does not always read as a transition - // (r89), so the walk west ends on Route 203's location banner, which - // shows for a few seconds as we step out. 01.06 starts there, from a save. - west_until_transition(stream, context, "West out of Oreburgh Gate", 8, location_banner_showing); -} - -void route203_to_jubilife(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.06", "From the Oreburgh Gate's west exit into Jubilife City", { - "Route 203 on its grid to row 18, below the girl", - "West along row 18 into Jubilife, counting tiles", - }); - - step(1); - // On the grid (Data/PokemonBDSP_AutoStory_Route203, from the decomp, - // its east stretch checked by hand): south from the Gate down the sand, - // west along the sand's bottom row and on under the girl's row, which - // runs straight to Jubilife as row 18. Trainers the map does not list - // are fought where they stop us. - std::vector trainers; - auto fight = [&](const std::string& who){ - talk_or_fight(stream, context, "Route 203: " + who); - }; - const std::vector at = walk_route(stream, context, run_from_wild_battle, route203_maps(), Route203::gate_exit(), - on_route203_west_line, trainers, fight, "Route 203 to row 18"); - if (at.size() != 1){ - fail(stream, "Route 203: the walk to row 18 did not end on a known tile."); - } - - step(2); - // Row 18 has no wall before Jubilife, so count the tiles: from column - // `x` it is x + 1 taps to Jubilife's first tile (by hand from column 31: - // 32 taps, Jubilife's banner on the last one). 01.07 starts there. - dpad_tiles(stream, context, DPAD_LEFT, (size_t)at[0].x + 1, "west along row 18 into Jubilife"); - context.wait_for_all_requests(); - if (!location_banner_showing(*stream.video().snapshot().frame)){ - fail(stream, "Route 203: no Jubilife banner after counting the tiles west."); - } -} - -void jubilife_to_rowan(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.07", "From Jubilife City's east edge through Rowan's scene", { - "West along Jubilife's long road to the crossroads", - "North up the street to Rowan (Dawn and two Galactic grunts: a tag battle)", - }); - - step(1); - // Route 203's row 18 carries on as Jubilife's long road. From the first - // tile the crossroads with the street north to Route 204 is 14 tiles - // west; the street is about 4 tiles wide, so a stick hold does (by hand, - // 2026-10-31: 2 x 1120 ms landed mid-street; 7 D-pad taps stopped below - // a building, and 18 went on to the road down to Route 202, r230b). - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 14), 300ms); - context.wait_for_all_requests(); - - step(2); - // North up the street: Rowan's scene starts a few tiles up (by hand, a - // 6 s walk). It opens with a cutscene, not a dialog box, so the walk - // reads as stopped; wait for his first line. The green-haired man walks - // about the crossroads and may stand in our way: wait and walk again. - bool scene = false; - for (size_t tries = 0; tries < 3 && !scene; tries++){ - const WalkResult up = walk_dir(stream, context, Direction::NORTH, 6000ms, "north up the street to Route 204"); - scene = up.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 8000ms); - if (!scene){ - pbf_wait(context, 2500ms); - } - } - if (!scene){ - fail(stream, "Jubilife: Rowan's scene did not start up the street north."); - } - if (!talk_or_fight(stream, context, "Galactic grunts with Dawn", true, true)){ - fail(stream, "Jubilife: Rowan's scene ended without the tag battle."); - } - if (in_pokemon_center(stream)){ - fail(stream, "Jubilife: lost the tag battle with Dawn."); - } - finish_dialogue(stream, context, BUTTON_B, "Rowan and Dawn after the battle"); - // 01.08 starts here, from a save at the top of the street. -} - -void jubilife_to_ravaged_path(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.08", "From the top of Jubilife's north street into the Ravaged Path", { - "North out of Jubilife onto Route 204", - "Route 204 South on its grid to below the Ravaged Path's door", - "North through the door", - }); - - step(1); - // The street's column 14 carries on as the route's; the location banner - // names Route 204 as we cross. A D-pad walk keeps the column; it may - // stop a tile or two past the border, which the entry set allows for. - const WalkResult out = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 8000ms, "north out of Jubilife", location_banner_showing); - if (out.stop != WalkStop::ARRIVED){ - fail(stream, "Jubilife: no Route 204 banner walking north up the street."); - } - context.wait_for(1500ms); - - step(2); - // On the grid decoded from the game's data (Data/..._Route204South): - // a tile west onto the tall grass column that runs up to the cliff, - // north up it, then west along the cliff to the signboard beside the - // door (the offline plan: 3 legs). Other trainers' sight lines are kept - // out of; town_plan_costs() lets the one-tile step land exactly. Lass Sarah's east line crosses - // the street, so she may stop us for a battle on the way; a Youngster - // who is not a trainer wanders in front of the door, and the walker - // waits for him. - { - std::vector trainers = Route204South::trainer_sights(); - auto fight = [&](const std::string& who){ talk_or_fight(stream, context, who); }; - const std::vector at = walk_route(stream, context, run_from_wild_battle, route204_south_maps(), - Route204South::jubilife_entry(), - [](const Pokemon::TilePos& pos){ return pos == Route204South::BELOW_DOOR; }, - trainers, fight, "Route 204 South to the Ravaged Path", Route204South::tiles_to_avoid(), town_plan_costs()); - if (at.size() != 1 || !(at[0] == Route204South::BELOW_DOOR)){ - fail(stream, "Route 204: the walk did not end below the Ravaged Path's door."); - } - } - - step(3); - if (!dpad_through_door_north(stream, context, "Ravaged Path door")){ - fail(stream, "Route 204: no dark screen walking north into the Ravaged Path."); - } - context.wait_for(2000ms); -} - -// "This rock seems breakable with a hidden move. Would you like to use Rock -// Smash?" is open, from an A at a smashable rock. Its Yes/No box comes only -// once the text is advanced again (by hand, 2026-10-31: the text complete -// 1.2 s after the first A, the box 1.2 s after a second), and r237a's timed -// A, A, B put its B on that box: No. So press A until the box's cursor -// shows, then once more for Yes, and wait out the move. False if no box -// shows: whatever is open is not this prompt. -bool answer_rock_smash_prompt(VideoStream& stream, ProControllerContext& context, const std::string& label){ - static const ImageFloatBox YES_NO_ARROW_BOX{0.700, 0.620, 0.070, 0.120}; - SelectionArrowFinder yes_no(stream.overlay(), YES_NO_ARROW_BOX, COLOR_GREEN); - for (size_t press = 0; press < 5; press++){ - context.wait_for(1200ms); - context.wait_for_all_requests(); - const VideoSnapshot screen = stream.video().snapshot(); - if (screen && yes_no.detect(*screen.frame)){ - stream.log("[Route] " + label + ": Rock Smash offered; Yes."); - pbf_press_button(context, BUTTON_A, 80ms, 0ms); - context.wait_for(5000ms); - if (dialogue_opens(stream, context, 500ms)){ - finish_dialogue(stream, context, BUTTON_B, label + ": after Rock Smash"); - } - return true; - } - pbf_press_button(context, BUTTON_A, 80ms, 0ms); - } - stream.log("[Route] " + label + ": no Yes/No box for Rock Smash.", COLOR_RED); - return false; -} - -void ravaged_path(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.09", "Through the Ravaged Path", { - "Through the Ravaged Path, smashing its rocks", - }); - - step(1); - // On the traced grid (Data/PokemonBDSP_AutoStory_RavagedPath): from the - // south door up the stairs to the tile west of one of the two rocks - // across the lower corridor, smash it, then east and south to the - // corridor's east end; the way out is a tile west of it, south. - { - std::vector no_trainers; - // Blocked short of a wall in here, it is a rock the plan counted on - // being gone (r237a: the walker took one for a person and said No). - auto in_the_way = [&](const std::string& who){ - if (!answer_rock_smash_prompt(stream, context, who)){ - talk_or_fight(stream, context, who); - } - }; - std::vector at{RavagedPath::ENTRANCE}; - const Pokemon::MapSet rocks_in = ravaged_path_maps(); - at = walk_route(stream, context, run_from_wild_battle, rocks_in, at, - [&](const Pokemon::TilePos& pos){ - const auto& before = RavagedPath::before_rocks(); - return std::find(before.begin(), before.end(), pos) != before.end(); - }, - no_trainers, in_the_way, "To a rock across the corridor"); - route_turn(stream, context, Direction::EAST); - if (!talk_with_nudges(stream, context, run_from_wild_battle, Direction::EAST, "the rock east of us", 2000ms)){ - fail(stream, "Ravaged Path: nothing answered facing the rock east of us."); - } - if (!answer_rock_smash_prompt(stream, context, "the rock east of us")){ - fail(stream, "Ravaged Path: no Rock Smash offer facing the rock east of us."); - } - // We may be on either row; the rock east of the other stays, but no - // leg from our row crosses it. - std::vector smashed; - for (const Pokemon::TilePos& pos : RavagedPath::before_rocks()){ - smashed.push_back(pos.next(Direction::EAST)); - } - const Pokemon::MapSet rocks_out = ravaged_path_maps(smashed); - at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, - [&](const Pokemon::TilePos& pos){ return pos == RavagedPath::EAST_END; }, - no_trainers, in_the_way, "To the corridor's east end"); - dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); - const WalkResult out = walk_dir(stream, context, Direction::SOUTH, 4000ms, "south out of the Ravaged Path"); - if (out.stop != WalkStop::TRANSITION && out.stop != WalkStop::LIMIT){ - fail(stream, "Ravaged Path: the walk south from a tile west of the corridor's end did not leave the cave."); - } - context.wait_for(2000ms); - } -} - -void route204_to_floaroma(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.10", "From the Ravaged Path's north exit to Floaroma Town", { - "Route 204 north on the human's line, into Floaroma", - }); - - step(1); - // The line a human walked (Data/PokemonBDSP_AutoStory_Route204, - // floaroma_waypoints()): a tile south out of the cave's mouth, west along - // the row under the cliff past the Youngster who wanders there, up the - // stairs, east along the corridor into Aroma Lady Taylor's battle, north - // through the tall grass, west below the flower beds, and north between - // the beds into Floaroma. Whole tiles on the D-pad, corner to corner. - { - std::vector trainers = Route204::north_trainer_sights(); - auto fight = [&](const std::string& who){ - const bool doubles = who.find("Twins") != std::string::npos || who.find("someone") != std::string::npos; - talk_or_fight(stream, context, who, false, doubles); - }; - std::vector at{Route204::CAVE_EXIT}; - at = walk_waypoints(stream, context, run_from_wild_battle, route204_north_maps(), at, Route204::floaroma_waypoints(), - trainers, fight, "To the path into Floaroma"); - if (!std::all_of(at.begin(), at.end(), Route204::in_floaroma_path)){ - fail(stream, "Route 204: the walk did not end in the path into Floaroma."); - } - // North until the town's banner shows: the checkpoint ends right - // there, where the save goes (r247a walked 20 s on and overshot). - const WalkResult in = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into Floaroma", location_banner_showing); - if (in.stop != WalkStop::ARRIVED){ - fail(stream, "Route 204: no location banner walking north into Floaroma."); - } - context.wait_for(500ms); - const VideoSnapshot banner = stream.video().snapshot(); - const std::string read = banner ? read_location_banner(*banner.frame) : ""; - if (!location_banner_matches(read, "Floaroma Town")){ - fail(stream, "Route 204: the banner read \"" + read + "\", not Floaroma Town."); - } - stream.log("[01.10] In Floaroma Town; the banner read \"" + read + "\".", COLOR_GREEN); - } - -} - -// 01.11: the old couple in Floaroma's flower field, left of the path in -// from Route 204. Walked by hand on 2026-10-31 from 01.10's save: west 8 -// along the row (the field's bottom, purple flowers), north until the old -// woman stops us (r249a counted two and stood a row short: no count, a -// clip), talk (she gives Mew when a Let's Go save is on the console; -// otherwise she only talks), one tile west, north until the old man stops -// us (a tap that cannot step still turns), talk (Jirachi with a -// Sword/Shield save). B advances their lines and never re-talks; A at the -// end re-opens the talk (m5, m6). -void floaroma_old_couple(VideoStream& stream, ProControllerContext& context){ - StepLog step(stream, "01.11", "Floaroma's old couple in the flower field", { - "West 8, north until the old woman stops us", - "Talk to the old woman", - "Talk to the old man", - }); - - step(1); - { - const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 8, "west 8 to the flower field"); - if (west.stop != WalkStop::LIMIT || west.moved_chunks < 7){ - fail(stream, "Floaroma: west 8 to the flower field moved " + std::to_string(west.moved_chunks) + " tiles."); - } - // Blocked, or out of taps with the last ones bumping: either way - // she is in front of us (r250a: 3 steps, 2 bumps, "limit"). - const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 5, "north until the old woman"); - if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 5){ - fail(stream, "Floaroma: nothing stopped the walk north to the old woman."); - } - } - - auto talk = [&](const std::string& who){ - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream, context, 3000ms, {{dialog}}) != 0){ - fail(stream, "Floaroma: " + who + " did not answer."); - } - finish_dialogue(stream, context, BUTTON_B, who); - }; - - step(2); - talk("the old woman"); - - step(3); - // One tile west, then a tap north: the old man stands there, so the - // tap only turns. - const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 1, "west 1 below the old man"); - if (west.moved_chunks < 1){ - fail(stream, "Floaroma: the step west below the old man did not move."); - } - const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 3, "north until the old man"); - if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 3){ - fail(stream, "Floaroma: nothing stopped the walk north to the old man."); - } - talk("the old man"); -} - -} - - -std::map segment_01_variants(){ - return { - {"jubilife_to_barry", story_call([](StoryConsole& c){ jubilife_to_barry(c.env.console, c.context); })}, - {"route203_to_oreburgh", story_call([](StoryConsole& c){ route203_to_oreburgh(c.env.console, c.context); })}, - {"oreburgh_to_roark", story_call([](StoryConsole& c){ oreburgh_to_roark(c.env.console, c.context); })}, - {"oreburgh_gym", story_call([](StoryConsole& c){ oreburgh_gym(c.env.console, c.context); })}, - {"oreburgh_to_route203", story_call([](StoryConsole& c){ oreburgh_to_route203(c.env.console, c.context); })}, - {"route203_to_jubilife", story_call([](StoryConsole& c){ route203_to_jubilife(c.env.console, c.context); })}, - {"jubilife_to_rowan", story_call([](StoryConsole& c){ jubilife_to_rowan(c.env.console, c.context); })}, - {"jubilife_to_ravaged_path", story_call([](StoryConsole& c){ jubilife_to_ravaged_path(c.env.console, c.context); })}, - {"ravaged_path", story_call([](StoryConsole& c){ ravaged_path(c.env.console, c.context); })}, - {"route204_to_floaroma", story_call([](StoryConsole& c){ route204_to_floaroma(c.env.console, c.context); })}, - {"floaroma_old_couple", story_call([](StoryConsole& c){ floaroma_old_couple(c.env.console, c.context); })}, - }; -} - -// The table is segment_01_table() (PokemonBDSP_AutoStory_StoryTable.h). Its -// steps only call functions, so the executor plans nothing itself. -Pokemon::Segment make_segment_01(StoryStepRunner& runner){ - const std::vector& table = segment_01_table(); - return story_table_segment(runner, "01", table, std::make_shared( - runner, rows_that_stop(table), story_maps(), std::vector{}, - [](StoryConsole&, const std::string&){}, segment_01_variants() - )); -} - - -std::string AutoStory_Segment_01::name() const { return "01: Jubilife to Floaroma"; } -std::string AutoStory_Segment_01::start_text() const { return "Start: Standing in front of the Jubilife City Pokemon Center PC."; } -std::string AutoStory_Segment_01::end_text() const { return "End: In Floaroma's flower field below the old couple, with the Coal Badge, saved."; } - -void AutoStory_Segment_01::run_segment( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - stats.m_segment++; - env.update_stats(); - env.console.log("Start Segment " + name(), COLOR_ORANGE); - run_story_table(env, context, options, stats, make_segment_01, {}); - env.console.log("End Segment " + name(), COLOR_GREEN); -} - -std::string AutoStory_Checkpoint_0101::name() const { return "01.01 - Jubilife to Barry"; } -std::string AutoStory_Checkpoint_0101::start_text() const { return "Standing in front of the Jubilife City Pokemon Center PC."; } -std::string AutoStory_Checkpoint_0101::end_text() const { return "On Route 203 by its sign, after beating Barry."; } - -void AutoStory_Checkpoint_0101::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.01", "01.01"}); -} - -std::string AutoStory_Checkpoint_0102::name() const { return "01.02 - Route 203 to Oreburgh"; } -std::string AutoStory_Checkpoint_0102::start_text() const { return AutoStory_Checkpoint_0101().end_text(); } -std::string AutoStory_Checkpoint_0102::end_text() const { return "In front of the Oreburgh Pokemon Center, after the man's tour and Barry."; } - -void AutoStory_Checkpoint_0102::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.02", "01.02"}); -} - - - -std::string AutoStory_Checkpoint_0103::name() const { return "01.03 - Oreburgh Mine to Roark"; } -std::string AutoStory_Checkpoint_0103::start_text() const { return AutoStory_Checkpoint_0102().end_text(); } -std::string AutoStory_Checkpoint_0103::end_text() const { return "At the Oreburgh Pokemon Center counter, healed, after talking to Roark."; } - -void AutoStory_Checkpoint_0103::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.03", "01.03"}); -} - - - -std::string AutoStory_Checkpoint_0104::name() const { return "01.04 - Oreburgh Gym"; } -std::string AutoStory_Checkpoint_0104::start_text() const { return AutoStory_Checkpoint_0103().end_text(); } -std::string AutoStory_Checkpoint_0104::end_text() const { return "In the Oreburgh Gym, in front of Roark, just after beating him (Coal Badge)."; } - -void AutoStory_Checkpoint_0104::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.04", "01.04"}); -} - -std::string AutoStory_Checkpoint_0105::name() const { return "01.05 - Oreburgh to Route 203"; } -std::string AutoStory_Checkpoint_0105::start_text() const { return AutoStory_Checkpoint_0104().end_text(); } -std::string AutoStory_Checkpoint_0105::end_text() const { return "On Route 203, just out of the Oreburgh Gate's west exit."; } - -void AutoStory_Checkpoint_0105::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.05", "01.05"}); -} - -std::string AutoStory_Checkpoint_0106::name() const { return "01.06 - Route 203 to Jubilife"; } -std::string AutoStory_Checkpoint_0106::start_text() const { return AutoStory_Checkpoint_0105().end_text(); } -std::string AutoStory_Checkpoint_0106::end_text() const { return "In Jubilife City, on the first tile of its long road in from Route 203."; } - -void AutoStory_Checkpoint_0106::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.06", "01.06"}); -} - -std::string AutoStory_Checkpoint_0107::name() const { return "01.07 - Jubilife through Rowan's scene"; } -std::string AutoStory_Checkpoint_0107::start_text() const { return AutoStory_Checkpoint_0106().end_text(); } -std::string AutoStory_Checkpoint_0107::end_text() const { return "In Jubilife City, at the top of the street north to Route 204, after the tag battle with Dawn."; } - -void AutoStory_Checkpoint_0107::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.07", "01.07"}); -} - -std::string AutoStory_Checkpoint_0108::name() const { return "01.08 - Jubilife to the Ravaged Path"; } -std::string AutoStory_Checkpoint_0108::start_text() const { return AutoStory_Checkpoint_0107().end_text(); } -std::string AutoStory_Checkpoint_0108::end_text() const { return "In the Ravaged Path, just inside its south entrance."; } - -void AutoStory_Checkpoint_0108::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.08", "01.08"}); -} - -std::string AutoStory_Checkpoint_0109::name() const { return "01.09 - Ravaged Path"; } -std::string AutoStory_Checkpoint_0109::start_text() const { return AutoStory_Checkpoint_0108().end_text(); } -std::string AutoStory_Checkpoint_0109::end_text() const { return "On Route 204, just out of the Ravaged Path's north exit."; } - -void AutoStory_Checkpoint_0109::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.09", "01.09"}); -} - -std::string AutoStory_Checkpoint_0110::name() const { return "01.10 - Route 204 to Floaroma"; } -std::string AutoStory_Checkpoint_0110::start_text() const { return AutoStory_Checkpoint_0109().end_text(); } -std::string AutoStory_Checkpoint_0110::end_text() const { return "Just inside Floaroma Town from Route 204, saved."; } - -void AutoStory_Checkpoint_0110::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.10", "01.10"}); -} - -std::string AutoStory_Checkpoint_0111::name() const { return "01.11 - Floaroma's old couple"; } -std::string AutoStory_Checkpoint_0111::start_text() const { return AutoStory_Checkpoint_0110().end_text(); } -std::string AutoStory_Checkpoint_0111::end_text() const { return "In Floaroma's flower field below the old couple, both talked to (Mew and Jirachi when the console has the saves), saved."; } - -void AutoStory_Checkpoint_0111::run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -) const{ - run_story_table(env, context, options, stats, make_segment_01, {"01.11", "01.11"}); -} - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h deleted file mode 100644 index ccb65f4ca8..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h +++ /dev/null @@ -1,189 +0,0 @@ -/* BDSP AutoStory - Segment 01 - * From the Jubilife Pokemon Center to Oreburgh City. - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment01_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Segment01_H - -#include "../PokemonBDSP_AutoStoryTools.h" -#include -#include -#include "../Utils/PokemonBDSP_AutoStory_StoryExecutor.h" -#include "../Utils/PokemonBDSP_AutoStory_StoryTables.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Segment 01 as a table of checkpoints, segment_01_table(): 01.01 to 01.11. -Pokemon::Segment make_segment_01(StoryStepRunner& runner); - -// How segment_01_table()'s steps that call an old function run on the -// console, by their variants. -std::map segment_01_variants(); - -class AutoStory_Segment_01 : public AutoStory_Segment{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_segment( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0101 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0102 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0103 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0104 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0105 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0106 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0107 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0108 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0109 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0110 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - -class AutoStory_Checkpoint_0111 : public AutoStory_Checkpoint{ -public: - virtual std::string name() const override; - virtual std::string start_text() const override; - virtual std::string end_text() const override; - virtual void run_checkpoint( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) const override; -}; - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp deleted file mode 100644 index 883fa21a6c..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp +++ /dev/null @@ -1,1024 +0,0 @@ -/* BDSP AutoStory - Battle Utilities - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include -#include -#include - -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" - -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/Images/SolidColorTest.h" -#include "CommonTools/OCR/OCR_Routines.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" - -#include "Pokemon/Inference/Pokemon_NameReader.h" -#include "Pokemon/Pokemon_StatsCalculation.h" - -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_ExperienceGainDetector.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h" -#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" - -#include "VideoGames/PokemonSwSh/Resources/PokemonSwSh_TypeMatchup.h" - -#include "PokemonBDSP_AutoStory_Battle.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - -using namespace Pokemon; -using namespace PokemonSwSh; -using namespace std::chrono_literals; - - -// --------------------------------------------------------------------------- -// run_battle_press_A -// --------------------------------------------------------------------------- - -void run_battle_press_A(const ProgramInfo& /*info*/, VideoStream& stream, ProControllerContext& context){ - EndBattleWatcher end_battle; - run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_A, 120000ms); - }, - {{end_battle}}); - - OverworldWatcher overworld; - run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_A, 120000ms); - }, - {{overworld}}); -} - - -// --------------------------------------------------------------------------- -// handle_unexpected_battle -// --------------------------------------------------------------------------- - -void handle_unexpected_battle(const ProgramInfo& info, - VideoStream& stream, - ProControllerContext& context, - std::function&& action, - std::function&& recovery_action, - size_t max_attempts){ - for (size_t attempt = 0; attempt < max_attempts; attempt++){ - StartBattleDetector start_battle(stream.overlay()); - int result = run_until(stream, - context, - [&](ProControllerContext& ctx){ - action(info, stream, ctx); - }, - {{start_battle}}); - if (result < 0){ - return; - } - run_battle_press_A(info, stream, context); - recovery_action(info, stream, context); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "handle_unexpected_battle: exceeded max attempts", - stream); -} - - -// --------------------------------------------------------------------------- -// Internal helpers for handle_battle_with_outside_help -// --------------------------------------------------------------------------- - -static constexpr int TORTERRA_ATK = 238; -static constexpr int TORTERRA_SPATK = 170; - -// ⚠️ Estimated PP regions (fight menu, 2×2 move grid); require calibration. -static const ImageFloatBox PP_BOX[4] = { - {0.36, 0.590, 0.10, 0.060}, - {0.75, 0.590, 0.10, 0.060}, - {0.36, 0.730, 0.10, 0.060}, - {0.75, 0.730, 0.10, 0.060}, -}; - -static int ocr_pp_slot(Logger& logger, const ImageViewRGB32& frame, const ImageFloatBox& box){ - ImageViewRGB32 region = extract_box_reference(frame, box); - std::string text = OCR::ocr_read(Language::English, region, OCR::PageSegMode::SINGLE_LINE); - text.erase(0, text.find_first_not_of(" \t\r\n")); - if (!text.empty()){ - text.erase(text.find_last_not_of(" \t\r\n") + 1); - } - - size_t slash = text.find('/'); - std::string num_str = (slash != std::string::npos) ? text.substr(0, slash) : text; - size_t digit_pos = num_str.find_first_of("0123456789"); - if (digit_pos == std::string::npos){ - logger.log("PP OCR: no digit in \"" + text + "\"", COLOR_YELLOW); - return -1; - } - try { - return std::stoi(num_str.substr(digit_pos)); - } catch (...){ - logger.log("PP OCR: parse failed \"" + num_str + "\"", COLOR_YELLOW); - return -1; - } -} - -static std::array read_all_pp(Logger& logger, VideoStream& stream){ - VideoSnapshot frame = stream.video().snapshot(); - std::array result; - for (int i = 0; i < 4; i++){ - result[i] = ocr_pp_slot(logger, frame, PP_BOX[i]); - } - return result; -} - -static std::pair damage_range(int level, int base_power, int atk, int def, double type_mult){ - if (base_power == 0){ - return {0, 0}; - } - int base = (((2 * level) / 5 + 2) * base_power * atk / def) / 50 + 2; - return {(int)(base * 85.0 / 100.0 * type_mult), (int)(base * type_mult)}; -} - -struct MoveScore { - int lower_sum = 0; - int upper_sum = 0; - bool ohko_all = false; -}; - -static MoveScore -score_move(const BDSPMoveData& mdata, const std::vector& on_field, const BDSPTrainerData& trainer_data){ - if (mdata.category == MoveCategory::STATUS){ - return {}; - } - - int atk_stat = (mdata.category == MoveCategory::PHYSICAL) ? TORTERRA_ATK : TORTERRA_SPATK; - - MoveScore score; - score.ohko_all = !on_field.empty(); - - for (const std::string& slug : on_field){ - if (slug.empty()){ - score.ohko_all = false; - continue; - } - - const BDSPTrainerPokemon* tp = nullptr; - for (const BDSPTrainer& trainer : trainer_data.trainers){ - for (const BDSPTrainerPokemon& mon : trainer.pokemon){ - if (mon.species == slug){ - tp = &mon; - break; - } - } - if (tp){ - break; - } - } - if (!tp){ - score.ohko_all = false; - continue; - } - - const BDSPPokemonData& pdata = bdsp_pokemon(slug); - uint16_t hp = calc_stats_hp(pdata.hp, tp->level, tp->ivs.hp, tp->evs.hp); - uint16_t def = (mdata.category == MoveCategory::PHYSICAL) - ? calc_stats_nonhp(pdata.defense, tp->level, tp->ivs.defense, tp->evs.defense, NatureAdjustment::NEUTRAL) - : calc_stats_nonhp(pdata.spdef, tp->level, tp->ivs.spdef, tp->evs.spdef, NatureAdjustment::NEUTRAL); - - double mult = type_multiplier(mdata.type, pdata.type[0], pdata.type[1]); - if (mdata.spread && trainer_data.battle_type == BDSPBattleType::DOUBLES){ - mult *= 0.75; - } - - auto [lo, hi] = damage_range(100, (int)mdata.base_power, atk_stat, (int)def, mult); - score.lower_sum += lo; - score.upper_sum += hi; - if (lo < (int)hp){ - score.ohko_all = false; - } - } - return score; -} - -static void navigate_to_slot(ProControllerContext& context, int slot){ - if (slot == 1 || slot == 3){ - pbf_press_dpad(context, DPAD_RIGHT, 10ms, 40ms); - } - if (slot == 2 || slot == 3){ - pbf_press_dpad(context, DPAD_DOWN, 10ms, 40ms); - } -} - - -// --------------------------------------------------------------------------- -// handle_battle_with_outside_help -// --------------------------------------------------------------------------- - -void handle_battle_with_outside_help(const ProgramInfo& /*info*/, - VideoStream& stream, - ProControllerContext& context, - const std::string& trainer_id, - const AutoStoryOptions& options){ - Logger& logger = stream.logger(); - const BDSPTrainerData& trainer_data = bdsp_trainer(trainer_id); - const OutsideHelpConfig& help = options.outside_help; - - std::array move_slugs; - for (int i = 0; i < 4; i++){ - move_slugs[i] = torterra_move_slug(help.attacks[i]); - } - - std::array pp_remaining; - for (int i = 0; i < 4; i++){ - pp_remaining[i] = (int)bdsp_move(move_slugs[i]).pp; - } - - std::vector on_field; - for (const BDSPTrainer& trainer : trainer_data.trainers){ - if (!trainer.pokemon.empty()){ - on_field.push_back(trainer.pokemon[0].species); - } - } - - bool battle_active = true; - while (battle_active){ - - // Wait for the lead Pokemon's battle menu - { - BattleMenuWatcher menu(BattleType::TRAINER); - EndBattleWatcher end; - int r = wait_until(stream, context, 120s, {{menu}, {end}}); - if (r == 1 || r < 0){ - battle_active = false; - break; - } - } - - // Open Fight menu - pbf_press_button(context, BUTTON_A, 10ms, 60ms); - context.wait_for_all_requests(); - context.wait_for(300ms); - - // OCR current PP - { - auto ocr_pp = read_all_pp(logger, stream); - for (int i = 0; i < 4; i++){ - if (ocr_pp[i] >= 0){ - pp_remaining[i] = ocr_pp[i]; - }else{ - logger.log("PP OCR failed slot " + std::to_string(i) + "; using tracked", COLOR_YELLOW); - } - } - } - - // Check for all-zero PP (Struggle) - bool all_zero = true; - for (int pp : pp_remaining){ - if (pp > 0){ - all_zero = false; - break; - } - } - - if (all_zero){ - logger.log("All PP at 0 — using Struggle", COLOR_YELLOW); - pbf_press_button(context, BUTTON_B, 10ms, 60ms); - context.wait_for_all_requests(); - }else{ - // Score all usable moves - int best_slot = -1, best_hi = -1; - bool best_ohko = false; - - for (int i = 0; i < 4; i++){ - if (pp_remaining[i] <= 0){ - continue; - } - MoveScore s = score_move(bdsp_move(move_slugs[i]), on_field, trainer_data); - if (bdsp_move(move_slugs[i]).category == MoveCategory::STATUS){ - continue; - } - - bool prefer = (best_slot < 0) || (s.ohko_all && !best_ohko) || - (s.ohko_all && best_ohko && s.upper_sum < best_hi) || - (!s.ohko_all && !best_ohko && s.upper_sum > best_hi); - - if (prefer){ - best_slot = i; - best_hi = s.upper_sum; - best_ohko = s.ohko_all; - } - } - if (best_slot < 0){ - for (int i = 0; i < 4; i++){ - if (pp_remaining[i] > 0){ - best_slot = i; - break; - } - } - } - - logger.log("Move slot " + std::to_string(best_slot) + " (" + move_slugs[best_slot] + ")" + - (best_ohko ? " [OHKO]" : " [best effort]"), - COLOR_WHITE); - - navigate_to_slot(context, best_slot); - pbf_mash_button(context, BUTTON_A, 500ms); - context.wait_for_all_requests(); - pp_remaining[best_slot]--; - - // Single-target in doubles: confirm default target - if (!bdsp_move(move_slugs[best_slot]).spread && trainer_data.battle_type == BDSPBattleType::DOUBLES){ - pbf_press_button(context, BUTTON_A, 10ms, 60ms); - context.wait_for_all_requests(); - } - - // Mash through target confirmation and partner's move selection - pbf_mash_button(context, BUTTON_A, 3000ms); - context.wait_for_all_requests(); - } - - // Wait for next event - { - BattleMenuWatcher menu2(BattleType::TRAINER); - ExperienceGainWatcher exp; - EndBattleWatcher end; - - int event = wait_until(stream, context, 60s, {{menu2}, {exp}, {end}}); - - if (event == 2 || event < 0){ - battle_active = false; - }else if (event == 1){ - // XP gain — mash through, then update on_field via OCR - BattleMenuWatcher menu3(BattleType::TRAINER); - EndBattleWatcher end2; - int after = run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_A, 120000ms); - }, - {{menu3}, {end2}}); - if (after == 1 || after < 0){ - battle_active = false; - }else{ - // Update on_field via OCR - VideoSnapshot frame = stream.video().snapshot(); - std::vector names = - read_battle_opponent_names(logger, options.language, frame, trainer_data.battle_type); - for (size_t i = 0; i < names.size() && i < on_field.size(); i++){ - if (!names[i].empty() && names[i] != on_field[i]){ - logger.log("Slot " + std::to_string(i) + ": " + on_field[i] + " → " + names[i], - COLOR_WHITE); - on_field[i] = names[i]; - } - } - } - } - // event == 0: next turn, loop continues - } - } - - // Mash through XP/dialog until overworld - OverworldWatcher overworld; - run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_A, 120000ms); - }, - {{overworld}}); -} - - -// --------------------------------------------------------------------------- -// Internal helpers for handle_expert_battle -// --------------------------------------------------------------------------- - -// ⚠️ Estimated player-side HP bar region — calibrate against a real capture. -static const ImageFloatBox PLAYER_HP_BAR{0.057, 0.652, 0.168, 0.008}; - -// ⚠️ Estimated player-side status badge region — calibrate against a real capture. -static const ImageFloatBox PLAYER_STATUS_BOX{0.140, 0.710, 0.080, 0.018}; - -// ⚠️ Estimated battle-bag item name region (highlighted row) — calibrate. -static const ImageFloatBox BATTLE_BAG_ITEM_NAME_BOX{0.470, 0.150, 0.350, 0.050}; - -// Returns the fraction [0.0, 1.0] of the player's HP bar that is filled. -// Scans each pixel column; a column with high colour saturation counts as "HP present". -static double read_player_hp_ratio(const ImageViewRGB32& frame){ - ImageViewRGB32 strip = extract_box_reference(frame, PLAYER_HP_BAR); - int total = (int)strip.width(); - int mid_row = (int)(strip.height() / 2); - if (total <= 0){ - return 1.0; - } - - int colored_cols = 0; - for (int x = 0; x < total; x++){ - uint32_t px = strip.pixel(x, mid_row); - uint8_t r = (px >> 16) & 0xFF; - uint8_t g = (px >> 8) & 0xFF; - uint8_t b = px & 0xFF; - int mx = std::max({(int)r, (int)g, (int)b}); - int mn = std::min({(int)r, (int)g, (int)b}); - if (mx - mn > 30){ - colored_cols++; - } - } - return (double)colored_cols / total; -} - -// Returns true if a coloured status badge is visible on the player's active pokemon. -static bool detect_player_status(const ImageViewRGB32& frame){ - return !is_white(extract_box_reference(frame, PLAYER_STATUS_BOX)); -} - -// Cycle through battle-bag pockets with BUTTON_R until the highlighted item -// matches "full-restore", then use it on the first pokemon. -// Called while the battle-bag is already open (navigate to Bag from battle menu first). -static void use_full_restore_in_open_bag(VideoStream& stream, ProControllerContext& context, Language language){ - Logger& logger = stream.logger(); - static constexpr size_t MAX_POCKETS = 9; - static constexpr size_t MAX_SCROLL = 25; - - // Cycle pockets until we see Full Restore in the highlighted row - bool pocket_found = false; - for (size_t tab = 0; tab < MAX_POCKETS; tab++){ - context.wait_for(500ms); - VideoSnapshot frame = stream.video().snapshot(); - ImageViewRGB32 region = extract_box_reference(frame, BATTLE_BAG_ITEM_NAME_BOX); - OCR::StringMatchResult result = ShopItemNameReader::instance().read_substring(logger, language, region); - for (const auto& [score, match] : result.results){ - if (match.token == "full-restore" && score <= ShopItemNameReader::MAX_LOG10P){ - pocket_found = true; - break; - } - } - if (pocket_found){ - break; - } - pbf_press_button(context, BUTTON_R, 10ms, 400ms); - } - - if (!pocket_found){ - logger.log("Full Restore pocket not found — scanning list", COLOR_YELLOW); - } - - // Scroll down within the pocket until the highlighted row is Full Restore - for (size_t i = 0; i < MAX_SCROLL; i++){ - context.wait_for(400ms); - VideoSnapshot frame = stream.video().snapshot(); - ImageViewRGB32 region = extract_box_reference(frame, BATTLE_BAG_ITEM_NAME_BOX); - OCR::StringMatchResult result = ShopItemNameReader::instance().read_substring(logger, language, region); - bool found = false; - for (const auto& [score, match] : result.results){ - if (match.token == "full-restore" && score <= ShopItemNameReader::MAX_LOG10P){ - found = true; - break; - } - } - if (found){ - pbf_press_button(context, BUTTON_A, 10ms, 600ms); // select item - pbf_press_button(context, BUTTON_A, 10ms, 600ms); // "Use" - pbf_press_button(context, BUTTON_A, 10ms, 800ms); // confirm on first pokemon - context.wait_for(500ms); - logger.log("Full Restore used", COLOR_WHITE); - pbf_mash_button(context, BUTTON_B, 1500ms); - return; - } - pbf_press_dpad(context, DPAD_DOWN, 10ms, 200ms); - } - - logger.log("Full Restore not found in bag — skipping", COLOR_YELLOW); - pbf_mash_button(context, BUTTON_B, 1500ms); -} - -// From the battle menu (Fight/Pokemon/Bag/Run), navigate to Bag and open it. -// In BDSP the battle menu is a 2×2 grid: Fight(top-left) Bag(top-right) -// Pokemon(bottom-left) Run(bottom-right). Cursor starts at Fight. -static void open_battle_bag(ProControllerContext& context){ - pbf_press_dpad(context, DPAD_RIGHT, 10ms, 200ms); - pbf_press_button(context, BUTTON_A, 10ms, 800ms); - context.wait_for(500ms); -} - -// Use a Full Restore on the first party pokemon from the battle menu. -// Returns immediately after item use; the caller is responsible for resuming the turn. -static void use_full_restore_in_battle(VideoStream& stream, ProControllerContext& context, Language language){ - open_battle_bag(context); - use_full_restore_in_open_bag(stream, context, language); - // Mash B to ensure we're back at the battle menu - context.wait_for(300ms); -} - -// OCR the opponent name box and restrict matches to `candidates`. -// Returns the best matching slug in candidates, or the best unrestricted match -// if no candidate qualifies. -static std::string ocr_opponent_restricted(Logger& logger, - Language language, - const ImageViewRGB32& frame, - const ImageFloatBox& box, - const std::vector& candidates){ - using namespace Pokemon; - ImageViewRGB32 cropped = extract_box_reference(frame, box); - OCR::StringMatchResult result = - PokemonNameReader::instance().read_substring(logger, language, cropped, OCR::BLACK_OR_WHITE_TEXT_FILTERS()); - if (result.results.empty()){ - return ""; - } - - // Prefer the best candidate-restricted match - for (const auto& [score, match] : result.results){ - if (score > PokemonNameReader::MAX_LOG10P){ - break; - } - for (const std::string& c : candidates){ - if (match.token == c){ - return c; - } - } - } - // Fallback: best unrestricted match (if it meets threshold) - const auto& best = result.results.begin(); - if (best->first <= PokemonNameReader::MAX_LOG10P){ - return best->second.token; - } - return ""; -} - - -// --------------------------------------------------------------------------- -// Pre-plan computation -// --------------------------------------------------------------------------- - -struct BattlePlan { - // species slug → move slot to use against that pokemon (for current trainer) - std::map move_for_species; - // Estimated remaining PP after the full trainer sequence is played out - std::array estimated_pp_after{}; -}; - -// Returns the Intimidate attack-stage multiplier for `count` Intimidate activations. -// Stage formula: 2 / (2 + count), capped at 6 drops. -static double intimidate_atk_mult(int count){ - if (count <= 0){ - return 1.0; - } - int capped = std::min(count, 6); - return 2.0 / (2.0 + capped); -} - -// Score move `slot` (0-3) against a single opponent pokemon. -// Returns {lower_damage, upper_damage, is_ohko}. -static std::tuple score_move_expert(const BDSPMoveData& mdata, - const BDSPTrainerPokemon& tp, - int effective_atk, - int effective_spatk, - bool doubles){ - if (mdata.category == MoveCategory::STATUS){ - return {0, 0, false}; - } - - const BDSPPokemonData& pdata = bdsp_pokemon(tp.species); - uint16_t hp = calc_stats_hp(pdata.hp, tp.level, tp.ivs.hp, tp.evs.hp); - - int atk_stat = (mdata.category == MoveCategory::PHYSICAL) ? effective_atk : effective_spatk; - uint16_t def = (mdata.category == MoveCategory::PHYSICAL) - ? calc_stats_nonhp(pdata.defense, tp.level, tp.ivs.defense, tp.evs.defense, NatureAdjustment::NEUTRAL) - : calc_stats_nonhp(pdata.spdef, tp.level, tp.ivs.spdef, tp.evs.spdef, NatureAdjustment::NEUTRAL); - - double mult = type_multiplier(mdata.type, pdata.type[0], pdata.type[1]); - if (mdata.spread && doubles){ - mult *= 0.75; - } - - // Attacker level is always 100 (helper Pokemon); defender level is tp.level - auto [lo, hi] = damage_range(100, (int)mdata.base_power, atk_stat, (int)def, mult); - bool ohko = (lo >= (int)hp); - return {lo, hi, ohko}; -} - -static BattlePlan build_expert_plan(Logger& logger, - const std::array& move_slugs, - const std::array& current_pp, - const std::vector& all_trainers, - Trainer current_trainer, - int base_atk, - int base_spatk){ - // Running PP budget across the full sequence - std::array pp = current_pp; - - BattlePlan plan; - plan.estimated_pp_after = pp; - - int intimidate_count = 0; - - // Iterate through all listed trainers in sequence order. - // For current_trainer, populate move_for_species. - for (Trainer t : all_trainers){ - const std::string slug = trainer_slug(t); - const BDSPTrainerData& tdata = bdsp_trainer(slug); - bool is_current = (t == current_trainer); - bool doubles = (tdata.battle_type == BDSPBattleType::DOUBLES); - - for (const BDSPTrainer& side : tdata.trainers){ - // Check first-out pokemon for Intimidate (activates before we attack) - if (!side.pokemon.empty() && side.pokemon[0].has_intimidate){ - intimidate_count++; - } - } - - int eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); - int eff_spatk = base_spatk; // Intimidate doesn't affect SpAtk - - for (const BDSPTrainer& side : tdata.trainers){ - for (const BDSPTrainerPokemon& tp : side.pokemon){ - // Find best move for this pokemon - int best_slot = -1; - int best_hi = -1; - bool best_ohko = false; - - for (int i = 0; i < 4; i++){ - if (pp[i] <= 0){ - continue; - } - const BDSPMoveData& mdata = bdsp_move(move_slugs[i]); - if (mdata.category == MoveCategory::STATUS){ - continue; - } - - auto [lo, hi, ohko] = score_move_expert(mdata, tp, eff_atk, eff_spatk, doubles); - (void)lo; - - // Log adjusted accuracy (informational only, not used in OHKO decisions) - uint8_t acc = mdata.accuracy; - if (acc > 0 && acc < 100){ - int adj = 100 - 2 * (100 - acc); - logger.log(move_slugs[i] + " vs " + tp.species + ": accuracy=" + std::to_string(acc) + - "% (adjusted=" + std::to_string(adj) + "%)", - COLOR_YELLOW); - } - - bool prefer = (best_slot < 0) || (ohko && !best_ohko) // new OHKO > non-OHKO - || (ohko && best_ohko && hi < best_hi) // both OHKO: lower overkill - || (!ohko && !best_ohko && hi > best_hi); // no OHKO: higher damage - - if (prefer){ - best_slot = i; - best_hi = hi; - best_ohko = ohko; - } - } - - if (best_slot < 0){ - // All PP at 0 — Struggle will be used in practice - logger.log("PP exhausted for " + tp.species + " — Struggle expected", COLOR_YELLOW); - }else{ - pp[best_slot]--; - logger.log(std::string(is_current ? "[plan] " : "[pre-plan] ") + tp.species + " → slot " + - std::to_string(best_slot) + " (" + move_slugs[best_slot] + ")" + - (best_ohko ? " [OHKO]" : " [best effort, hi=" + std::to_string(best_hi) + "]"), - COLOR_WHITE); - if (is_current){ - // Only override if this species hasn't been planned yet - // (in case the same species appears twice — first occurrence wins) - if (plan.move_for_species.find(tp.species) == plan.move_for_species.end()){ - plan.move_for_species[tp.species] = best_slot; - } - } - } - - // Track Intimidate for subsequent pokemon (after the initial send-out) - if (tp.has_intimidate){ - intimidate_count++; - eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); - } - } - } - } - - plan.estimated_pp_after = pp; - return plan; -} - - -// --------------------------------------------------------------------------- -// handle_expert_battle -// --------------------------------------------------------------------------- - -// Snapshot the current frame, OCR on-field names, and update `on_field`. -static void update_on_field(Logger& logger, - Language language, - VideoStream& stream, - BDSPBattleType battle_type, - const std::vector& candidates, - std::vector& on_field){ - VideoSnapshot frame = stream.video().snapshot(); - if (battle_type == BDSPBattleType::SINGLES){ - static const ImageFloatBox BOX(0.505, 0.010, 0.420, 0.085); - std::string name = ocr_opponent_restricted(logger, language, frame, BOX, candidates); - if (!name.empty() && !on_field.empty()){ - on_field[0] = name; - } - }else{ - static const ImageFloatBox BOX_L(0.505, 0.010, 0.200, 0.085); - static const ImageFloatBox BOX_R(0.710, 0.010, 0.260, 0.085); - std::string l = ocr_opponent_restricted(logger, language, frame, BOX_L, candidates); - std::string r = ocr_opponent_restricted(logger, language, frame, BOX_R, candidates); - if (!l.empty() && on_field.size() > 0){ - on_field[0] = l; - } - if (!r.empty() && on_field.size() > 1){ - on_field[1] = r; - } - } -} - - -void handle_expert_battle(const ProgramInfo& /*info*/, - VideoStream& stream, - ProControllerContext& context, - const std::vector& all_trainers, - Trainer current_trainer, - const AutoStoryOptions& options){ - using namespace Pokemon; - - Logger& logger = stream.logger(); - const std::string current_slug = trainer_slug(current_trainer); - const BDSPTrainerData& trainer_data = bdsp_trainer(current_slug); - const OutsideHelpConfig& help = options.outside_help; - bool doubles = (trainer_data.battle_type == BDSPBattleType::DOUBLES); - - // Move slugs for the helper's 4 moves - std::array move_slugs; - for (int i = 0; i < 4; i++){ - move_slugs[i] = torterra_move_slug(help.attacks[i]); - } - - // Helper stats — 0IV/0EV at level 100, neutral nature - const std::string helper_base_slug = "torterra"; - const BDSPPokemonData& helper_base = bdsp_pokemon(helper_base_slug); - int base_atk = (int)calc_stats_nonhp(helper_base.attack, 100, 0, 0, NatureAdjustment::NEUTRAL); - int base_spatk = (int)calc_stats_nonhp(helper_base.spatk, 100, 0, 0, NatureAdjustment::NEUTRAL); - - // Candidate species slugs for restricted OCR (all pokemon in current trainer's party) - std::vector candidates; - for (const BDSPTrainer& side : trainer_data.trainers){ - for (const BDSPTrainerPokemon& tp : side.pokemon){ - candidates.push_back(tp.species); - } - } - - // PP tracking — initialised to max PP from JSON; OCR updates each turn - std::array pp_remaining; - for (int i = 0; i < 4; i++){ - pp_remaining[i] = (int)bdsp_move(move_slugs[i]).pp; - } - - // Intimidate tracking across turns - int intimidate_count = 0; - std::set intimidate_seen; - - // Current on-field opponents (initialised to first pokemon of each side) - std::vector on_field; - for (const BDSPTrainer& side : trainer_data.trainers){ - on_field.push_back(side.pokemon.empty() ? "" : side.pokemon[0].species); - } - - // plan is built on the first turn after PP are OCR'd - BattlePlan plan; - bool plan_built = false; - - // ── Wait for the initial battle menu ───────────────────────────────────── - bool battle_active = true; - { - BattleMenuWatcher menu(BattleType::TRAINER); - EndBattleWatcher end; - int r = wait_until(stream, context, 60s, {{menu}, {end}}); - if (r != 0 || r < 0){ - battle_active = false; - } - } - - // ── Main battle loop ────────────────────────────────────────────────────── - // Each iteration starts with the battle menu (Fight/Pokemon/Bag/Run) visible. - // We check HP/status, open the fight menu, OCR PP, identify the opponent, - // select a move, then wait for the next event. - while (battle_active){ - - // 1. Check HP / status → use Full Restore if needed - { - VideoSnapshot frame = stream.video().snapshot(); - double hp = read_player_hp_ratio(frame); - bool has_status = detect_player_status(frame); - if (hp < 0.33 || has_status){ - logger.log("Full Restore needed (HP=" + std::to_string((int)(hp * 100)) + - "%, status=" + (has_status ? "yes" : "no") + ")", - COLOR_WHITE); - use_full_restore_in_battle(stream, context, options.language); - } - } - - // 2. Open fight menu and re-read PP - pbf_press_button(context, BUTTON_A, 10ms, 60ms); - context.wait_for_all_requests(); - context.wait_for(300ms); - { - auto ocr_pp = read_all_pp(logger, stream); - for (int i = 0; i < 4; i++){ - if (ocr_pp[i] >= 0){ - pp_remaining[i] = ocr_pp[i]; - }else{ - logger.log("PP OCR failed slot " + std::to_string(i), COLOR_YELLOW); - } - } - } - - // 3. Build plan on first turn (after we have real PP values from OCR) - if (!plan_built){ - plan = build_expert_plan(logger, - move_slugs, - pp_remaining, - all_trainers, - current_trainer, - base_atk, - base_spatk); - plan_built = true; - } - - // 4. Identify current on-field opponent(s) via restricted OCR - // (Fight menu is open at bottom; opponent info boxes are at the top — still readable) - update_on_field(logger, options.language, stream, trainer_data.battle_type, candidates, on_field); - - // 5. Track Intimidate from newly-observed opponents - { - int eff_atk = base_atk; - for (const std::string& slug : on_field){ - if (slug.empty() || intimidate_seen.count(slug)){ - continue; - } - for (const BDSPTrainer& side : trainer_data.trainers){ - for (const BDSPTrainerPokemon& tp : side.pokemon){ - if (tp.species == slug && tp.has_intimidate){ - intimidate_count++; - intimidate_seen.insert(slug); - eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); - logger.log(slug + " has Intimidate — ATK stage -" + std::to_string(intimidate_count) + - " (effective ATK=" + std::to_string(eff_atk) + ")", - COLOR_YELLOW); - } - } - } - } - } - - // 6. Select move — prefer pre-plan; fall back to live scoring if plan misses - int best_slot = -1; - if (!on_field.empty() && !on_field[0].empty()){ - auto it = plan.move_for_species.find(on_field[0]); - if (it != plan.move_for_species.end() && pp_remaining[it->second] > 0){ - best_slot = it->second; - } - } - - if (best_slot < 0){ - logger.log("Plan miss for " + (on_field.empty() ? "?" : on_field[0]) + " — live scoring", COLOR_YELLOW); - int eff_atk = (int)(base_atk * intimidate_atk_mult(intimidate_count)); - int eff_spatk = base_spatk; - int best_hi = -1; - bool best_ohko = false; - - for (int i = 0; i < 4; i++){ - if (pp_remaining[i] <= 0){ - continue; - } - const BDSPMoveData& mdata = bdsp_move(move_slugs[i]); - if (mdata.category == MoveCategory::STATUS){ - continue; - } - - int hi = 0; - bool ohko = false; - if (!on_field.empty() && !on_field[0].empty()){ - for (const BDSPTrainer& side : trainer_data.trainers){ - for (const BDSPTrainerPokemon& tp : side.pokemon){ - if (tp.species != on_field[0]){ - continue; - } - auto [lo, h, ok] = score_move_expert(mdata, tp, eff_atk, eff_spatk, doubles); - hi = h; - ohko = ok; - } - } - } - - bool prefer = (best_slot < 0) || (ohko && !best_ohko) || (ohko && best_ohko && hi < best_hi) || - (!ohko && !best_ohko && hi > best_hi); - - if (prefer){ - best_slot = i; - best_hi = hi; - best_ohko = ohko; - } - } - // Last resort: first move with any PP remaining - if (best_slot < 0){ - for (int i = 0; i < 4; i++){ - if (pp_remaining[i] > 0){ - best_slot = i; - break; - } - } - } - } - - // 7. Execute selected move (or Struggle if all PP are gone) - if (best_slot < 0){ - logger.log("All PP at 0 — Struggle", COLOR_YELLOW); - pbf_press_button(context, BUTTON_B, 10ms, 60ms); // close fight menu - context.wait_for_all_requests(); - }else{ - logger.log("Move slot " + std::to_string(best_slot) + " (" + move_slugs[best_slot] + ") vs " + - (on_field.empty() ? "?" : on_field[0]), - COLOR_WHITE); - navigate_to_slot(context, best_slot); - pbf_mash_button(context, BUTTON_A, 500ms); - context.wait_for_all_requests(); - pp_remaining[best_slot]--; - - // Single-target move in doubles: confirm default target - if (!bdsp_move(move_slugs[best_slot]).spread && doubles){ - pbf_press_button(context, BUTTON_A, 10ms, 60ms); - context.wait_for_all_requests(); - } - // Mash through target confirm and partner's move selection - pbf_mash_button(context, BUTTON_A, 3000ms); - context.wait_for_all_requests(); - } - - // 8. Wait for next event: next battle menu, XP gain, or battle end - { - BattleMenuWatcher menu2(BattleType::TRAINER); - ExperienceGainWatcher exp; - EndBattleWatcher end; - - int event = wait_until(stream, context, 60s, {{menu2}, {exp}, {end}}); - - if (event == 2 || event < 0){ - // Battle over - break; - }else if (event == 1){ - // XP gain — mash until the NEXT battle menu appears (or battle ends) - BattleMenuWatcher menu3(BattleType::TRAINER); - EndBattleWatcher end2; - int after = run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_A, 120000ms); - }, - {{menu3}, {end2}}); - if (after != 0){ - // Battle ended during XP screens - break; - } - // Update on-field now that the next pokemon is visible - update_on_field(logger, options.language, stream, trainer_data.battle_type, candidates, on_field); - for (size_t i = 0; i < on_field.size(); i++){ - logger.log("On-field[" + std::to_string(i) + "] = " + on_field[i], COLOR_WHITE); - } - // Continue loop — we're now at the next battle menu - } - // event == 0: next battle menu appeared (mid-turn switch or next turn) - // Loop continues from the top, which is the battle menu state - } - } - - // Mash through any remaining XP/dialog until we're back in the overworld - OverworldWatcher overworld; - run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_A, 120000ms); - }, - {{overworld}}); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h deleted file mode 100644 index 1ab3a67b78..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h +++ /dev/null @@ -1,111 +0,0 @@ -/* BDSP AutoStory - Battle Utilities - * - * From: https://github.com/PokemonAutomation/ - * - * Helpers for handling trainer battles and unexpected wild encounters. - * These wrap existing BDSP battle detectors: - * - BattleMenuDetector / BattleMenuWatcher (PokemonBDSP_BattleMenuDetector.h) - * - EndBattleWatcher (PokemonBDSP_EndBattleDetector.h) - * - StartBattleDetector (PokemonBDSP_StartBattleDetector.h) - * - ExperienceGainWatcher (PokemonBDSP_ExperienceGainDetector.h) - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Battle_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Battle_H - -#include -#include -#include - -#include "CommonFramework/Tools/VideoStream.h" - -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" - -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" - -namespace PokemonAutomation { -struct ProgramInfo; -namespace NintendoSwitch { -namespace PokemonBDSP { - - -// --------------------------------------------------------------------------- -// Scripted (expected) battles -// --------------------------------------------------------------------------- - -// Mash A through a battle (trainer or scripted wild) until EndBattleWatcher fires. -// This does not attempt to catch — it simply presses A to select the first move -// repeatedly until the battle ends. -// After the black-screen transition and experience gain, returns to the caller. -void run_battle_press_A(const ProgramInfo& info, VideoStream& stream, ProControllerContext& context); - - -// --------------------------------------------------------------------------- -// Unexpected battle handling -// --------------------------------------------------------------------------- - -// Run `action`. If a StartBattleDetector fires during `action`, the action -// is aborted, the battle is resolved with run_battle_press_A(), and then -// `recovery_action` is called to restore the player's position before -// retrying `action`. -// The whole sequence retries up to `max_attempts` times. -// Throws OperationFailedException if max_attempts is exceeded. -void handle_unexpected_battle(const ProgramInfo& info, - VideoStream& stream, - ProControllerContext& context, - std::function&& action, - std::function&& recovery_action, - size_t max_attempts = 5); - - -// --------------------------------------------------------------------------- -// Outside-help battle handler -// --------------------------------------------------------------------------- - -// Handle a scripted trainer battle where the configured human helper joins. -// Each turn it: -// 1. OCRs current PP for all 4 configured moves. -// 2. Calculates damage against all on-field opponents. -// 3. Navigates to and selects the optimal move (guaranteed OHKO if possible; -// highest total damage otherwise). -// 4. Mashes A to cover target selection and partner's move. -// 5. After XP gain, updates on-field opponents via OCR. -// Returns when the battle ends and the player is back in the overworld. -void handle_battle_with_outside_help(const ProgramInfo& info, - VideoStream& stream, - ProControllerContext& context, - const std::string& trainer_id, - const AutoStoryOptions& options); - - -// --------------------------------------------------------------------------- -// Expert battle handler (sequence-aware, per-turn status/HP check) -// --------------------------------------------------------------------------- - -// Handle one scripted trainer battle with intelligent per-turn move selection. -// -// `all_trainers` — the full upcoming sequence of trainer battles. Used to -// pre-plan PP budget so moves are allocated efficiently across the sequence. -// `current_trainer` — the trainer being fought in this call. -// -// Each turn the function: -// 1. Checks player HP (<33%) and status condition; uses a Full Restore if needed. -// 2. OCRs current PP for all 4 configured moves. -// 3. Identifies the current opponent via restricted-dictionary OCR. -// 4. Tracks Intimidate activations and reduces effective ATK accordingly. -// 5. Selects the pre-planned optimal move and executes it. -// 6. After XP gain, OCRs the new opponent and loops. -// Returns when the battle ends and the player is back in the overworld. -void handle_expert_battle(const ProgramInfo& info, - VideoStream& stream, - ProControllerContext& context, - const std::vector& all_trainers, - Trainer current_trainer, - const AutoStoryOptions& options); - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp deleted file mode 100644 index 5d1fde2c18..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* BDSP AutoStory - Dialog Utilities - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "CommonFramework/Exceptions/OperationFailedException.h" - -#include "CommonTools/Async/InferenceRoutines.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" - -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" - -#include "PokemonBDSP_AutoStory_Dialog.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - - -void clear_dialog(VideoStream& stream, ProControllerContext& context, ClearDialogMode mode, uint16_t timeout_sec){ - // TODO: implement - // - use ShortDialogWatcher + (optionally) ShortDialogPromptDetector - // + OverworldDetector (from Detect/) with run_until<> - // - loop mashing A until stop condition fires - (void)stream; - (void)context; - (void)mode; - (void)timeout_sec; -} - -void wait_for_dialog(VideoStream& stream, ProControllerContext& context, uint16_t timeout_sec){ - // TODO: implement - // - use wait_until() with ShortDialogWatcher - (void)stream; - (void)context; - (void)timeout_sec; -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h deleted file mode 100644 index 50fc7d7204..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h +++ /dev/null @@ -1,61 +0,0 @@ -/* BDSP AutoStory - Dialog Utilities - * - * From: https://github.com/PokemonAutomation/ - * - * Helpers for clearing NPC dialog and waiting for dialog to appear. - * These wrap the existing BDSP dialog detectors: - * - ShortDialogDetector / ShortDialogWatcher (PokemonBDSP_DialogDetector.h) - * - ShortDialogPromptDetector (PokemonBDSP_DialogDetector.h) - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Dialog_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Dialog_H - -#include - -#include "CommonFramework/Tools/VideoStream.h" - -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" - -namespace PokemonAutomation { -struct ProgramInfo; -namespace NintendoSwitch { -namespace PokemonBDSP { - - -// --------------------------------------------------------------------------- -// Stop conditions for clear_dialog() -// --------------------------------------------------------------------------- - -enum class ClearDialogMode { - // Stop once the dialog has closed and the overworld is detected. - STOP_OVERWORLD, - - // Stop when a YES/NO prompt (ShortDialogPromptDetector) is detected. - STOP_PROMPT, - - // Run for exactly `timeout_sec` seconds regardless of screen state. - STOP_TIMEOUT, -}; - - -// --------------------------------------------------------------------------- -// Dialog clearing -// --------------------------------------------------------------------------- - -// Mash A through NPC dialog until `mode` stop condition is met. -// Uses ShortDialogWatcher as the primary "still in dialog" signal. -// Throws OperationFailedException if no dialog is ever detected, or if -// the stop condition is not met within `timeout_sec` seconds. -void clear_dialog(VideoStream& stream, ProControllerContext& context, ClearDialogMode mode, uint16_t timeout_sec = 60); - -// Block until a short dialog box appears, then return. -// Throws OperationFailedException if no dialog appears within `timeout_sec`. -void wait_for_dialog(VideoStream& stream, ProControllerContext& context, uint16_t timeout_sec = 30); - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp deleted file mode 100644 index 28a871f34a..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* BDSP AutoStory - Navigation Utilities - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "CommonFramework/Exceptions/OperationFailedException.h" - -#include "CommonTools/Async/InferenceRoutines.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" - -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" - -#include "PokemonBDSP_AutoStory_Navigation.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - - -void walk_steps(ProControllerContext& context, uint16_t direction, uint16_t steps){ - // TODO: implement — press D-pad `direction` for `steps` tiles - (void)context; - (void)direction; - (void)steps; -} - -void walk_until_dialog(VideoStream& stream, ProControllerContext& context, uint16_t direction, uint16_t timeout_sec){ - // TODO: implement — walk in direction; stop when ShortDialogWatcher fires - (void)stream; - (void)context; - (void)direction; - (void)timeout_sec; -} - -void walk_until_battle(VideoStream& stream, ProControllerContext& context, uint16_t direction, uint16_t timeout_sec){ - // TODO: implement — walk in direction; stop when StartBattleDetector fires - (void)stream; - (void)context; - (void)direction; - (void)timeout_sec; -} - -void recalibrate_after_battle(const ProgramInfo& /*info*/, - VideoStream& /*stream*/, - ProControllerContext& context, - std::function&& recovery_action){ - // TODO: implement — wait for overworld, then run recovery_action - recovery_action(context); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h deleted file mode 100644 index 55bdab77f9..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h +++ /dev/null @@ -1,78 +0,0 @@ -/* BDSP AutoStory - Navigation Utilities - * - * From: https://github.com/PokemonAutomation/ - * - * Helpers for moving the player character in the BDSP overworld. - * BDSP uses a tile-based grid, so most movement is fixed D-pad sequences. - * Detection-based stop conditions and post-battle recalibration are also here. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Navigation_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Navigation_H - -#include - -#include "CommonFramework/Tools/VideoStream.h" - -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" - -namespace PokemonAutomation { -struct ProgramInfo; -namespace NintendoSwitch { -namespace PokemonBDSP { - - -// --------------------------------------------------------------------------- -// Fixed-sequence walking (no detection — pure tile counting) -// --------------------------------------------------------------------------- - -// Walk `steps` tiles in the given D-pad direction. -// direction must be one of DPAD_UP / DPAD_DOWN / DPAD_LEFT / DPAD_RIGHT. -void walk_steps(ProControllerContext& context, uint16_t direction, uint16_t steps); - - -// --------------------------------------------------------------------------- -// Detection-guided walking (walk until a screen state is detected) -// --------------------------------------------------------------------------- - -// Walk in `direction` until a short dialog box is detected, or `timeout_sec` elapses. -// Throws OperationFailedException on timeout. -void walk_until_dialog(VideoStream& stream, - ProControllerContext& context, - uint16_t direction, - uint16_t timeout_sec = 30); - -// Walk in `direction` until a battle starts (StartBattleDetector fires), -// or `timeout_sec` elapses. -// Throws OperationFailedException on timeout. -void walk_until_battle(VideoStream& stream, - ProControllerContext& context, - uint16_t direction, - uint16_t timeout_sec = 30); - - -// --------------------------------------------------------------------------- -// Post-battle recalibration -// -// BDSP movement is deterministic on a grid, but a random encounter shifts -// the player's position unpredictably (post-battle exit tile varies). -// These functions restore the player to a known reference tile by navigating -// to a fixed landmark (e.g. a building entrance). -// --------------------------------------------------------------------------- - -// After an unexpected battle, navigate to the nearest known landmark and -// re-establish a known tile position. The exact recovery sequence depends -// on the calling segment and is passed as a lambda. -// `recovery_action` should move the player from wherever they ended up back -// to a defined starting tile for the current checkpoint. -void recalibrate_after_battle(const ProgramInfo& info, - VideoStream& stream, - ProControllerContext& context, - std::function&& recovery_action); - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp deleted file mode 100644 index 51c42a038a..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp +++ /dev/null @@ -1,2082 +0,0 @@ -/* BDSP AutoStory - Segment 00 Tools - * - * Helper implementations for Segment 00. - * - * From: https://github.com/PokemonAutomation/ - * - */ - - -#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include -#include -#include -#include -#include - -#include "PokemonBDSP_AutoStory_Location.h" -#include "PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP_AutoStory_Segment_00_tools.h" -#include "PokemonBDSP_Autostory_Segment_01_tools.h" - -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "Common/Cpp/PrettyPrint.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonTools/OCR/OCR_RawTesseractOCR.h" -#include "CommonFramework/Tools/ErrorDumper.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" - -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" -#include "CommonTools/VisualDetectors/ImageMatchDetector.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "NintendoSwitch/NintendoSwitch_Settings.h" - -#include "NintendoSwitch/Programs/DateManip/NintendoSwitch_DateManip.h" -#include "NintendoSwitch/Programs/DateSpam/NintendoSwitch_HomeToDateTime.h" -#include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" - -#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" - -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_ReceivePokemonDetector.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_ExperienceGainDetector.h" - -#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" - -#include "PokemonBDSP/Programs/PokemonBDSP_GameNavigation.h" -#include "PokemonBDSP/Programs/PokemonBDSP_BasicCatcher.h" -#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" - -using namespace std::chrono_literals; - -namespace PokemonAutomation { - namespace NintendoSwitch { - namespace PokemonBDSP { - - // --------------------------------------------------------------------------- - // BattleDialogWatcher - // --------------------------------------------------------------------------- - - BattleDialogWatcher::BattleDialogWatcher(Color color) - : DetectorToFinder( - "BattleDialogWatcher", - std::chrono::milliseconds(250), - color - ) - { - } - - // --------------------------------------------------------------------------- - // Poketech Clown dialog helper - // --------------------------------------------------------------------------- - - int detect_clown_choice_arrow( - VideoOverlay& overlay, - VideoSnapshot snapshot - ) { - SelectionArrowFinder top_arrow( - overlay, - { 0.62, 0.37, 0.08, 0.10 }, - COLOR_RED - ); - - SelectionArrowFinder bottom_arrow( - overlay, - { 0.62, 0.48, 0.08, 0.10 }, - COLOR_RED - ); - - if (top_arrow.detect(snapshot)) { - return 0; - } - - if (bottom_arrow.detect(snapshot)) { - return 1; - } - - return -1; - } - - void clown_dialog_helper( - VideoStream& stream, - ProControllerContext& context, - DpadPosition direction - ) { - stream.log( - "[AutoStory] Starting clown dialog helper...", - COLOR_BLUE - ); - - const auto overall_deadline = - std::chrono::steady_clock::now() + 30s; - - // ------------------------------------------------- - // Phase 1: Move + interact until dialog is detected. - // ------------------------------------------------- - ShortDialogWatcher dialog(COLOR_YELLOW); - bool dialog_started = false; - - while (!dialog_started) { - - if (std::chrono::steady_clock::now() > overall_deadline) { - stream.log( - "[AutoStory] Clown helper timed out finding dialog.", - COLOR_RED - ); - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "[AutoStory] Clown helper timed out finding dialog.", stream); - } - - context.wait_for_all_requests(); - pbf_press_dpad(context, direction, 80ms, 200ms); - pbf_press_button(context, BUTTON_A, 80ms, 500ms); - - if (wait_until(stream, context, 1500ms, { {dialog} }) == 0) { - dialog_started = true; - stream.log("[AutoStory] Dialog detected.", COLOR_GREEN); - } - } - - // ------------------------------------------------- - // Phase 2: A mash for 2 seconds. - // ------------------------------------------------- - stream.log("[AutoStory] Mashing A to advance dialog...", COLOR_BLUE); - pbf_mash_button(context, BUTTON_A, 4000ms); - context.wait_for_all_requests(); - - // ------------------------------------------------- - // Phase 3: Clear remaining dialog. - // ------------------------------------------------- - pbf_mash_button(context, BUTTON_B, 4000ms); - - context.wait_for_all_requests(); - - stream.log( - "[AutoStory] Clown dialog helper complete.", - COLOR_GREEN - ); - } - // --------------------------------------------------------------------------- - // Time helper - // --------------------------------------------------------------------------- - - // Options chrome and position detectors are independent of setting values. - class TutorialOptionsDetector { - public: - bool detect(const ImageViewRGB32& screen) const { - ImageViewRGB32 panel = extract_box_reference(screen, ImageFloatBox{0.04, 0.065, 0.93, 0.70}); - size_t white = 0; - for (size_t y = 0; y < panel.height(); y += 4) - for (size_t x = 0; x < panel.width(); x += 4) { - uint32_t p = panel.pixel(x, y); - if (((p >> 16) & 255) > 225 && ((p >> 8) & 255) > 225 && (p & 255) > 225) white++; - } - double samples = (double)(((panel.width() + 3) / 4) * ((panel.height() + 3) / 4)); - // Distinguish Options from the icon menu and bag using its - // blue heading and full-width white settings panel. - ImageViewRGB32 heading = extract_box_reference(screen, ImageFloatBox{0.01, 0.005, 0.20, 0.025}); - size_t blue = 0, total = 0; - for (size_t y = 0; y < heading.height(); y++) - for (size_t x = 0; x < heading.width(); x++) { - uint32_t p = heading.pixel(x, y); - int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; - blue += b > r + 20 && g > r + 10; - total++; - } - return samples > 0 && white / samples > 0.70 && total > 0 && blue > total / 4; - } - }; - - // Options fades in after A on its icon: poll instead of judging one frame. - static bool wait_for_options_screen(VideoStream& stream, ProControllerContext& context, - const TutorialOptionsDetector& options_screen, Milliseconds timeout) { - context.wait_for_all_requests(); - const WallClock deadline = current_time() + timeout; - while (current_time() < deadline) { - auto frame = stream.video().snapshot(); - if (frame && options_screen.detect(frame)) return true; - context.wait_for(100ms); - } - return false; - } - - class TutorialOptionPositionDetector { - public: - // Locate the two long red horizontal lines surrounding the - // selected row. Scrolling changes their position automatically. - double selected_row(const ImageViewRGB32& screen) const { - int first = -1, last = -1; - for (size_t y = (size_t)(screen.height() * 0.08); y < screen.height() * 0.75; y++) { - size_t red = 0, total = 0; - for (size_t x = (size_t)(screen.width() * 0.08); x < screen.width() * 0.90; x += 2) { - uint32_t p = screen.pixel(x, y); - int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; - red += r > 150 && r > g + 40 && r > b + 40; - total++; - } - if (total && red > total * 0.70) { - if (first < 0) first = (int)y; - last = (int)y; - } - } - if (first < 0 || last - first < screen.height() * 0.04 || last - first > screen.height() * 0.12) return -1; - return (first + last) / (2.0 * screen.height()); - } - // Locate selected red value text separately from the row border. - bool selected_value(const ImageViewRGB32& screen, double y) const { - ImageViewRGB32 value = extract_box_reference(screen, ImageFloatBox{0.33, y - 0.025, 0.54, 0.05}); - size_t red = 0; - for (size_t py = 0; py < value.height(); py++) - for (size_t px = 0; px < value.width(); px++) { - uint32_t p = value.pixel(px, py); - int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; - red += r > g + 20 && r > b + 20; - } - return red > value.width() * value.height() * 0.0005; - } - }; - - - // Compare binary visual shapes with small alignment tolerance. - // No text recognition is needed for fixed English option labels. - static double option_image_score(const ImageViewRGB32& screen, - const ImageFloatBox& box, const ImageRGB32& reference, bool red_only) { - ImageRGB32 actual = extract_box_reference(screen, box).scale_to(384, 32); - ImageRGB32 expected = reference.scale_to(384, 32); - auto ink = [red_only](uint32_t p){ - int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; - return red_only ? r > g + 20 && r > b + 20 : r + g + b < 500; - }; - double best = 0; - for (int dy = -2; dy <= 2; dy++) for (int dx = -2; dx <= 2; dx++) { - size_t overlap = 0, count = 0; - for (int y = 2; y < 30; y++) for (int x = 2; x < 382; x++) { - bool a = ink(actual.pixel(x + dx, y + dy)); - bool b = ink(expected.pixel(x, y)); - overlap += a && b; - count += a + b; - } - if (count) best = std::max(best, 2.0 * overlap / count); - } - return best; - } - - static bool autosave_off_highlight(const ImageViewRGB32& screen, double y) { - auto red_count = [&](const ImageFloatBox& box){ - ImageViewRGB32 image = extract_box_reference(screen, box); - size_t red = 0; - for (size_t py = 0; py < image.height(); py++) - for (size_t px = 0; px < image.width(); px++) { - uint32_t p = image.pixel(px, py); - int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; - red += r > g + 30 && r > b + 30; - } - return red; - }; - size_t off = red_count({0.63, y - 0.022, 0.055, 0.044}); - size_t on = red_count({0.34, y - 0.022, 0.055, 0.044}); - // Require a clear Off highlight and reject any competing On highlight. - return off >= 10 && on * 10 < off; - } - - static void verify_tutorial_settings_once(VideoStream& stream, ProControllerContext& context) { - context.wait_for_all_requests(); - MenuDetector menu; - VideoOverlaySet detector_overlays(stream.overlay()); - menu.make_overlays(detector_overlays); - detector_overlays.add(COLOR_CYAN, {0.04, 0.065, 0.93, 0.70}, ""); - detector_overlays.add(COLOR_BLUE, {0.01, 0.005, 0.20, 0.025}, ""); - detector_overlays.add(COLOR_RED, {0.08, 0.08, 0.82, 0.67}, ""); - auto initial = stream.video().snapshot(); - if (!initial || !menu.detect(initial)) { - pbf_press_button(context, BUTTON_X, 80ms, 1000ms); - context.wait_for_all_requests(); - } - MenuWatcher menu_open; - if (wait_until(stream, context, 5s, {{menu_open}}) != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Settings checkpoint: Main menu not detected.", stream); - } - stream.log("[Tutorial settings] Main menu detected; settling for 500ms before Options navigation."); - pbf_wait(context, 500ms); - context.wait_for_all_requests(); - TutorialOptionsDetector options_screen; - TutorialOptionPositionDetector position; - // Only two icons are available at this initial checkpoint. - // Verify the main menu before navigating to the neighboring icon. - bool options_open = false; - for (size_t attempt = 0; attempt < 2; attempt++) { - pbf_press_dpad(context, attempt == 0 ? DPAD_RIGHT : DPAD_LEFT, 80ms, 350ms); - pbf_press_button(context, BUTTON_A, 80ms, 0ms); - if (wait_for_options_screen(stream, context, options_screen, 5s)) { - options_open = true; - break; - } - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - context.wait_for_all_requests(); - MenuWatcher back_to_menu; - if (wait_until(stream, context, 5s, {{back_to_menu}}) != 0) break; - } - if (!options_open) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Settings checkpoint: Options screen not detected.", stream); - } - stream.log("[Tutorial settings] Options screen detected."); - pbf_wait(context, 500ms); - context.wait_for_all_requests(); - struct Setting { const char* label; const char* value; }; - const Setting settings[] = { - {"textspeed", "fast"}, {"changewindow", "type1"}, - {"battleeffects", "off"}, {"battlestyle", "set"}, - {"sendtoboxes", "automatic"}, {"givenicknames", "dontgive"}, - {"gyroscope", "use"}, {"autosave", "off"}, - {"shownicknames", "show"}, {"backgroundmusic", "8"}, - {"soundeffects", "8"}, - }; - for (size_t row = 0; row < sizeof(settings) / sizeof(settings[0]); row++) { - // Detect the selected row afresh after every navigation/correction. - double y = -1; - const std::string base = RESOURCE_PATH() + "PokemonBDSP/Autostory/Options/" + settings[row].label; - const ImageRGB32 label_reference(base + "_label.png"); - const ImageRGB32 value_reference(base + "_value.png"); - bool slider_reset = false; - bool correct = false; - for (size_t attempt = 0; attempt < 30; attempt++) { - context.wait_for_all_requests(); - auto frame = stream.video().snapshot(); - if (frame && options_screen.detect(frame)) y = position.selected_row(frame); - else y = -1; - const bool value_visible = y >= 0 && position.selected_value(frame, y); - const double label_score = y >= 0 ? option_image_score(frame, - {0.08, y - 0.025, 0.24, 0.05}, label_reference, false) : 0; - stream.log("[Tutorial settings] Expected row=" + std::string(settings[row].label) + - " row_y=" + std::to_string(y) + " highlighted_value=" + std::to_string(value_visible) + - " label_match=" + std::to_string(label_score)); - if (y < 0 || !value_visible || label_score < 0.80) { - // A menu can be detected while its row/text is still - // fading in. Re-read without directional inputs first. - if (attempt < 4) { - pbf_wait(context, 200ms); - context.wait_for_all_requests(); - continue; - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Settings checkpoint: Unable to confirm expected row " + std::string(settings[row].label), stream); - } - // These are regular video-overlay boxes, so the existing - // overlay display control can show/hide every active ROI. - OverlayBoxScope row_overlay(stream.overlay(), COLOR_RED, - {0.07, y - 0.04, 0.84, 0.08}, ""); - OverlayBoxScope label_overlay(stream.overlay(), COLOR_YELLOW, - {0.08, y - 0.025, 0.24, 0.05}, ""); - OverlayBoxScope value_overlay(stream.overlay(), COLOR_GREEN, - {0.33, y - 0.025, 0.54, 0.05}, ""); - VideoOverlaySet autosave_rois(stream.overlay()); - if (row == 7) { - autosave_rois.add(COLOR_GREEN, {0.63, y - 0.022, 0.055, 0.044}); - autosave_rois.add(COLOR_RED, {0.34, y - 0.022, 0.055, 0.044}); - } - double score; - if (row >= 9) { - // Compare the numeric value rather than the long slider: - // neighboring slider levels otherwise look too similar. - ImageViewRGB32 number = value_reference.sub_image( - value_reference.width() * 82 / 100, 0, - value_reference.width() - value_reference.width() * 82 / 100, - value_reference.height()); - ImageRGB32 number_reference = number.scale_to(96, 32); - score = option_image_score(frame, - {0.7728, y - 0.025, 0.0972, 0.05}, number_reference, true); - } else { - score = option_image_score(frame, - {0.33, y - 0.025, 0.54, 0.05}, value_reference, true); - } - stream.log("[Tutorial settings] " + std::string(settings[row].label) + - " target match=" + std::to_string(score)); - // The reference recording uses inactive pale-red text; - // the focused live row has a thicker dark-red glyph mask. - // Verified Fast scores 0.734 while Slow/Normal score 0. - if (score >= 0.65 && (row != 7 || autosave_off_highlight(frame, y))) { - if (row == 7) { - for (size_t confirmation = 0; confirmation < 3; confirmation++) { - pbf_wait(context, 200ms); - context.wait_for_all_requests(); - auto check = stream.video().snapshot(); - double check_y = check ? position.selected_row(check) : -1; - if (!check || !options_screen.detect(check) || check_y < 0 || - option_image_score(check, {0.08, check_y - 0.025, 0.24, 0.05}, label_reference, false) < 0.80 || - !autosave_off_highlight(check, check_y)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Autosave Off could not be confirmed. Stopping before movement.", stream); - } - } - } - stream.log("[Tutorial settings] Verified " + std::string(settings[row].label) + "=" + settings[row].value); - correct = true; - break; - } - stream.log("[Tutorial settings] Correcting " + std::string(settings[row].label)); - if (row >= 9 && !slider_reset) { - // Sliders clamp at zero. Establish a known endpoint, - // then increment; verify the target image afterward. - for (size_t step = 0; step < 12; step++) - pbf_press_dpad(context, DPAD_LEFT, 80ms, 100ms); - slider_reset = true; - } else { - pbf_press_dpad(context, DPAD_RIGHT, 80ms, 350ms); - } - context.wait_for_all_requests(); - } - if (!correct) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Settings checkpoint: Could not apply " + std::string(settings[row].label), stream); - } - if (row + 1 < sizeof(settings) / sizeof(settings[0])) - pbf_press_dpad(context, DPAD_DOWN, 80ms, 350ms); - } - // Confirm settings before exiting: A A A A B B. - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1200ms); - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - context.wait_for_all_requests(); - OverworldWatcher overworld; - if (wait_until(stream, context, 10s, {{overworld}}) != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Settings checkpoint: Did not return to the overworld after saving options.", stream); - } - // Reopen the saved Options screen and independently verify Autosave. - // No route movement is permitted until this persisted-value check passes. - pbf_press_button(context, BUTTON_X, 80ms, 1000ms); - context.wait_for_all_requests(); - MenuWatcher verify_menu; - if (wait_until(stream, context, 5s, {{verify_menu}}) != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Autosave verification: Main menu not detected.", stream); - } - bool reopened = false; - for (size_t attempt = 0; attempt < 2; attempt++) { - pbf_press_button(context, BUTTON_A, 80ms, 0ms); - if (wait_for_options_screen(stream, context, options_screen, 5s)) { reopened = true; break; } - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - context.wait_for_all_requests(); - MenuWatcher return_menu; - if (wait_until(stream, context, 5s, {{return_menu}}) != 0) break; - pbf_press_dpad(context, DPAD_RIGHT, 80ms, 350ms); - } - if (!reopened) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Autosave verification: Could not reopen saved Options.", stream); - } - // Options starts on Text Speed; Autosave is seven rows below it. - for (size_t step = 0; step < 7; step++) - pbf_press_dpad(context, DPAD_DOWN, 80ms, 350ms); - context.wait_for_all_requests(); - const ImageRGB32 autosave_label(RESOURCE_PATH() + "PokemonBDSP/Autostory/Options/autosave_label.png"); - for (size_t confirmation = 0; confirmation < 3; confirmation++) { - auto check = stream.video().snapshot(); - double check_y = check ? position.selected_row(check) : -1; - if (!check || !options_screen.detect(check) || check_y < 0 || - option_image_score(check, {0.08, check_y - 0.025, 0.24, 0.05}, autosave_label, false) < 0.80 || - !autosave_off_highlight(check, check_y)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Saved Autosave setting is not verified Off. Stopping before movement.", stream); - } - pbf_wait(context, 200ms); - context.wait_for_all_requests(); - } - stream.log("[Tutorial settings] Autosave = Off: confirmed after reopening saved Options (3 frames)."); - // Confirm settings before exiting: A A A A B B. - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1200ms); - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - context.wait_for_all_requests(); - OverworldWatcher verified_exit; - if (wait_until(stream, context, 10s, {{verified_exit}}) != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Autosave verification: Could not return to the overworld.", stream); - } - // Report inferred values explicitly: these are visual matches, - // allowing the user to compare our conclusions with the screen. - stream.log("[Tutorial settings] Completed. Values believed applied (visual matches before saving):"); - stream.log("[Tutorial settings] Text Speed = Fast; Change Window = Type 1; Battle Effects = Off; Battle Style = Set."); - stream.log("[Tutorial settings] Send to Boxes = Automatic; Give Nicknames = Don't give; Gyroscope = Use."); - stream.log("[Tutorial settings] Autosave = Off; Show Nicknames = Show; Background Music = 8; Sound Effects = 8."); - } - - - void verify_tutorial_settings(VideoStream& stream, ProControllerContext& context) { - // Settings-only recovery: initial attempt plus three retries. - // Never permit route movement unless the complete check succeeds. - constexpr size_t MAX_RETRIES = 3; - for (size_t attempt = 0; attempt <= MAX_RETRIES; attempt++) { - stream.log("[Tutorial settings] Attempt " + std::to_string(attempt + 1) + "/4."); - try { - verify_tutorial_settings_once(stream, context); - return; - } catch (OperationFailedException& error) { - stream.log("[Tutorial settings] Attempt failed: " + error.message(), COLOR_ORANGE); - if (attempt == MAX_RETRIES) { - stream.log("[Tutorial settings] All three retries exhausted; stopping before movement.", COLOR_RED); - throw; - } - // Back out of Options, save prompts, bag, or menu without - // moving the player or resetting/loading the game. - stream.log("[Tutorial settings] Returning to overworld before retry."); - context.wait_for_all_requests(); - TutorialOptionsDetector options_screen; - const auto failed_frame = stream.video().snapshot(); - if (failed_frame && options_screen.detect(failed_frame)) { - // Finish the known Options save/exit sequence before - // observing state; never cancel it on an Options frame - // misclassified as overworld by the generic detector. - for (size_t press = 0; press < 4; press++) - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - } - pbf_mash_button(context, BUTTON_B, 3000ms); - context.wait_for_all_requests(); - OverworldDetector overworld; - MenuDetector menu; - BlackScreenDetector black; - bool recovered = false; - size_t confirmations = 0; - for (size_t check = 0; check < 15; check++) { - const auto frame = stream.video().snapshot(); - const bool clear = frame && !options_screen.detect(frame) && - !menu.detect(frame) && !black.detect(frame) && bag_pocket_position(frame) == 0 && overworld.detect(frame); - if (clear) { - if (++confirmations >= 3) { recovered = true; break; } - pbf_wait(context, 200ms); - } else { - confirmations = 0; - pbf_mash_button(context, BUTTON_B, 500ms); - } - context.wait_for_all_requests(); - } - stream.log(recovered ? "[Tutorial settings] Recovery confirmed outside Options/menu; retrying." - : "[Tutorial settings] Recovery not confirmed; next attempt will recheck the screen."); - pbf_wait(context, 500ms); - context.wait_for_all_requests(); - } - } - } - - void set_time_fast( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - const DateTime& target, - bool preserve_date - ) { - env.log("[AutoStory] Fast date set starting...", COLOR_ORANGE); - - // 1) Home - NintendoSwitch::go_home(env.console, context); - - // 2) Navigate to Date/Time settings quickly. - NintendoSwitch::home_to_date_time(env.console, context, true); - - // 3) Enter date editor. - pbf_press_button(context, BUTTON_A, 80ms, 240ms); - - // 4) Wait for date screen and set exact time. - NintendoSwitch::DateChangeWatcher date_reader(env.console); - int ret = wait_until( - env.console, context, 10s, - { date_reader } - ); - if (ret < 0) { - OperationFailedExceptionWithScreenshot::fire( - ErrorReportMode::SEND_ERROR_REPORT, - "Failed to enter Date Change screen.", - env.console - ); - } - - { - VideoOverlaySet overlays(env.console.overlay()); - date_reader.make_overlays(overlays); - DateTime effective_target = target; - if (preserve_date) { - context.wait_for_all_requests(); - const auto snapshot = env.console.video().snapshot(); - if (!snapshot) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Clock-only setup: No date-editor frame.", env.console); - const DateTime current = date_reader.read_date(env.console, snapshot.frame).second; - if (current.year < 2000 || current.year > 2060 || current.month < 1 || current.month > 12 || - current.day < 1 || current.day > 31) - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Clock-only setup: Could not reliably read the current console date.", env.console); - effective_target.year = current.year; - effective_target.month = current.month; - effective_target.day = current.day; - env.log("[AutoStory] Clock-only target=" + std::to_string((int)target.hour) + ":" + - std::to_string((int)target.minute) + "; preserving date=" + std::to_string(current.year) + "-" + - std::to_string((int)current.month) + "-" + std::to_string((int)current.day) + "."); - } - date_reader.set_date(env.program_info(), env.console, context, effective_target); - } - - // 5) Commit date. - pbf_press_button(context, BUTTON_A, 160ms, 240ms); - - // 6) Back to game. - pbf_press_button( - context, BUTTON_HOME, 160ms, - ConsoleSettings::instance().SETTINGS_TO_HOME_DELAY0 - ); - NintendoSwitch::resume_game_from_home(env.console, context, false); - - context.wait_for_all_requests(); - context.wait_for(500ms); - } - - - // --------------------------------------------------------------------------- - // Natalie battle - // --------------------------------------------------------------------------- - // Early moves the starters and their foes have, as type, power and - // most PP. A status move has power 0. - struct KnownMove { const char* name; const char* type; int power; int pp; }; - static const KnownMove KNOWN_MOVES[] = { - {"pound", "normal", 40, 35}, {"tackle", "normal", 40, 35}, {"scratch", "normal", 40, 35}, - {"quickattack", "normal", 40, 30}, {"headbutt", "normal", 70, 15}, - {"watergun", "water", 40, 25}, {"bubble", "water", 40, 30}, {"bubblebeam", "water", 65, 20}, - {"ember", "fire", 40, 25}, {"flamewheel", "fire", 60, 25}, {"machpunch", "fighting", 40, 30}, - {"absorb", "grass", 20, 25}, {"razorleaf", "grass", 55, 25}, {"bite", "dark", 60, 25}, - {"peck", "flying", 35, 35}, {"wingattack", "flying", 60, 35}, {"metalclaw", "steel", 50, 35}, - {"growl", "normal", 0, 40}, {"leer", "normal", 0, 30}, {"tailwhip", "normal", 0, 30}, - {"withdraw", "water", 0, 40}, {"taunt", "dark", 0, 20}, - }; - - // A move button's PP left, "8/25" at its right end; -1 if it cannot be - // read. The text is dark, or yellow to red as PP runs low, so text is - // whatever differs from the button's own color. `max_pp` helps when - // the OCR reads the slash as a digit. - static int read_move_pp(const ImageViewRGB32& screen, const ImageFloatBox& button, int max_pp) { - const ImageViewRGB32 box = extract_box_reference(screen, ImageFloatBox{ - button.x + button.width * 0.75, button.y + button.height * 0.10, - button.width * 0.21, button.height * 0.80}); - if (box.width() == 0 || box.height() == 0) return -1; - std::vector pixels; - for (size_t y = 0; y < box.height(); y++) for (size_t x = 0; x < box.width(); x++) pixels.push_back(box.pixel(x, y)); - auto brightness = [](uint32_t p) { return ((p >> 16) & 255) + ((p >> 8) & 255) + (p & 255); }; - std::vector sorted = pixels; - std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end(), - [&](uint32_t a, uint32_t b) { return brightness(a) < brightness(b); }); - const uint32_t background = sorted[sorted.size() / 2]; - const size_t pad = 8; - ImageRGB32 text(box.width() + 2 * pad, box.height() + 2 * pad); - text.fill(0xffffffff); - for (size_t y = 0; y < box.height(); y++) { - for (size_t x = 0; x < box.width(); x++) { - const uint32_t p = box.pixel(x, y); - int distance = 0; - for (int shift : {16, 8, 0}) distance += std::abs((int)((p >> shift) & 255) - (int)((background >> shift) & 255)); - if (distance > 120) text.pixel(x + pad, y + pad) = 0xff000000; - } - } - const std::string read = OCR::tesseract_ocr_read(Language::English, text); - const size_t slash = read.find('/'); - std::string digits; - for (size_t i = 0; i < read.size() && i < slash; i++) if (std::isdigit((unsigned char)read[i])) digits.push_back(read[i]); - if (slash == std::string::npos && max_pp > 0) { - // "29135" for 29/35: drop the most PP and the misread slash. - const std::string max = std::to_string(max_pp); - if (digits.size() > max.size() + 1 && digits.compare(digits.size() - max.size(), max.size(), max) == 0) { - digits.resize(digits.size() - max.size() - 1); - } else { - return -1; - } - } - if (digits.empty() || digits.size() > 2) return -1; - return std::stoi(digits); - } - - // The best move on the move menu: the most powerful one we know, and - // on a tie one that is not Normal (an early starter's own type). - // Reads the names with OCR; 1 if none is known. - static size_t pick_best_move(VideoStream& stream, const ImageViewRGB32& screen, - const BattleMoveMenuState& moves, const std::string& label) { - // Keep the menu as it looked, to check the reads against. - { - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/moves/"; - Filesystem::create_directories(Filesystem::Path(folder)); - screen.save(folder + now_to_filestring() + "-" + label + ".png"); - } - size_t best = 1; - int best_score = -1; - bool any_attack = false; - size_t last_not_status = 1; - for (size_t i = 0; i < moves.move_count; i++) { - // The name is dark text on a light button (grey, or light blue under the - // cursor). Take the box right of the type icon and left of the PP, tall - // enough for a status move's name, which sits lower: it has no "Effective" - // line under it. - const ImageFloatBox& button = moves.buttons[i]; - const ImageFloatBox name_box{button.x + button.width * 0.17, button.y + button.height * 0.08, - button.width * 0.53, button.height * 0.80}; - const ImageViewRGB32 box = extract_box_reference(screen, name_box); - // Text is darker than halfway between the background and the darkest pixel; - // a fixed cut thins the strokes on the blue button. - std::vector gray(box.width() * box.height()); - for (size_t y = 0; y < box.height(); y++) { - for (size_t x = 0; x < box.width(); x++) { - const uint32_t pixel = box.pixel(x, y); - gray[y * box.width() + x] = (int)(((pixel >> 16) & 0xff) + ((pixel >> 8) & 0xff) + (pixel & 0xff)) / 3; - } - } - std::vector sorted = gray; - std::nth_element(sorted.begin(), sorted.begin() + sorted.size() / 2, sorted.end()); - const int background = sorted[sorted.size() / 2]; - const int darkest = *std::min_element(gray.begin(), gray.end()); - const int threshold = (background + darkest) / 2; - // The name is the tallest band of text rows; "Effective" is smaller. - size_t band_top = 0, band_bottom = 0; - for (size_t y = 0; y < box.height();) { - auto has_text = [&](size_t row) { - for (size_t x = 0; x < box.width(); x++) if (gray[row * box.width() + x] < threshold) return true; - return false; - }; - if (!has_text(y)) { y++; continue; } - size_t end = y; - while (end < box.height() && has_text(end)) end++; - if (end - y > band_bottom - band_top) { band_top = y; band_bottom = end; } - y = end; - } - // Black text on white, with a margin, for the OCR. - const size_t pad = 6; - const size_t top = band_top >= 4 ? band_top - 4 : 0; - const size_t bottom = std::min(box.height(), band_bottom + 4); - ImageRGB32 text(box.width() + 2 * pad, bottom - top + 2 * pad); - text.fill(0xffffffff); - for (size_t y = top; y < bottom; y++) { - for (size_t x = 0; x < box.width(); x++) { - if (gray[y * box.width() + x] < threshold) text.pixel(x + pad, y - top + pad) = 0xff000000; - } - } - std::string name; - // Move names only where there is a reader for the game's - // language; an unread move counts as a plain attack. - for (unsigned char ch : story_reads_text() ? OCR::tesseract_ocr_read(g_story_language, text) : std::string()) { - if (std::isalpha(ch)) name.push_back((char)std::tolower(ch)); - } - // A slot read well once keeps its name when a later read fails. - static std::array last_read; - const KnownMove* known = nullptr; - for (int pass = 0; pass < 2 && !known; pass++) { - if (pass == 1) name = last_read[i]; - for (const KnownMove& move : KNOWN_MOVES) { - // The longest name that appears wins: "bubblebeam" over "bubble". - if (name.find(move.name) != std::string::npos && - (!known || std::string(move.name).size() > std::string(known->name).size())) { - known = &move; - } - } - } - if (known) last_read[i] = known->name; - // A move with no PP left cannot be chosen; the game says so and - // keeps the menu up. - const int pp = read_move_pp(screen, button, known ? known->pp : 0); - stream.log("[" + label + "] Move " + std::to_string(i + 1) + " PP left: " + (pp < 0 ? std::string("unread") : std::to_string(pp)) + "."); - if (pp == 0) { - continue; - } - // An unreadable move counts as a plain attack, ahead of known - // status moves. - const int score = known ? known->power * 2 + (std::string(known->type) != "normal" ? 1 : 0) : 40 * 2 - 1; - stream.log("[" + label + "] Move " + std::to_string(i + 1) + " reads \"" + name + "\"" + - (known ? std::string(" = ") + known->name + " (" + known->type + ", " + std::to_string(known->power) + ")" : " (unknown)") + "."); - if (score > best_score) { best_score = score; best = i + 1; } - if (known && known->power > 0) any_attack = true; - if (!known || known->power > 0) last_not_status = i + 1; - } - // No attack read: the newest move is the starter's own-type attack - // (Water Gun, Ember, Absorb), unless it is a status move. - if (!any_attack) best = last_not_status; - stream.log("[" + label + "] Best move: " + std::to_string(best) + "."); - return best; - } - - static void select_battle_move_slot(VideoStream& stream, ProControllerContext& context, - BattleType battle_type, size_t target, const std::string& label, - bool confirm_default_target = false, size_t current_move = 1) { - BattleMenuDetector commands(battle_type); - VideoOverlaySet overlays(stream.overlay()); - commands.make_overlays(overlays); - overlays.add(COLOR_GREEN, {0.90, 0.48, 0.075, 0.505}); - // Reenter Fight from an already-open list without assuming - // that this resets the remembered move cursor. - if (confirm_default_target) { - context.wait_for_all_requests(); - const auto frame=stream.video().snapshot(); - if (frame && detect_battle_move_menu(frame).move_count) { - pbf_press_button(context,BUTTON_B,80ms,500ms); - } - } - bool fight = false; - size_t confirmed = 0; - for (size_t check = 0; check < 30; check++) { - context.wait_for_all_requests(); - auto frame = stream.video().snapshot(); - BattleMenuOption selected = frame ? battle_menu_selected_option(commands, frame) : BattleMenuOption::UNKNOWN; - stream.log("[" + label + "] Command cursor=" + std::to_string((int)selected + 1) + "; target Battle=1."); - if (selected == BattleMenuOption::BATTLE) { - if (++confirmed >= 3) { fight = true; break; } - pbf_wait(context, 100ms); - } else { - confirmed = 0; - if (selected == BattleMenuOption::UNKNOWN) pbf_wait(context, 150ms); - else pbf_press_dpad(context, DPAD_UP, 80ms, 250ms); - } - } - if (!fight) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Could not confirm Battle command; A not pressed.", stream); - pbf_press_button(context,BUTTON_A,80ms,confirm_default_target ? 1000ms : 500ms); - if (confirm_default_target) { - // Reuse the farming move/target confirmation sequence, but - // navigate relative to this battle's retained move cursor. - // Reopening Fight does not reset it to move 1. - size_t available=0,stable=0; - for (size_t check=0;check<20 && stable<3;check++) { - context.wait_for_all_requests(); - const auto frame=stream.video().snapshot(); - const auto moves=frame ? detect_battle_move_menu(frame) : BattleMoveMenuState{}; - if (moves.move_count && moves.move_count==available) stable++; - else {available=moves.move_count;stable=moves.move_count ? 1:0;} - if (stable<3) pbf_wait(context,100ms); - } - if (stable<3 || available "+std::to_string(target)+"."); - for (size_t slot=current_move;slottarget;slot--) pbf_press_dpad(context,DPAD_UP,80ms,400ms); - pbf_press_button(context,BUTTON_A,80ms,1000ms); - pbf_press_button(context,BUTTON_A,80ms,3000ms); - return; - } - confirmed = 0; - size_t previous_move_count = 0, stable_move_count = 0; - for (size_t check = 0; check < 30; check++) { - context.wait_for_all_requests(); - auto frame = stream.video().snapshot(); - const BattleMoveMenuState moves = frame ? detect_battle_move_menu(frame) : BattleMoveMenuState{}; - const int slot = (int)moves.selected_slot; - VideoOverlaySet move_overlays(stream.overlay()); - for (size_t i = 0; i < moves.move_count; i++) - move_overlays.add(moves.selected_slot == i + 1 ? COLOR_GREEN : COLOR_YELLOW, moves.buttons[i]); - stream.log("[" + label + "] Available moves=" + std::to_string(moves.move_count) + - "; selected slot=" + std::to_string(slot) + "/4; target=" + std::to_string(target) + "."); - if (moves.move_count == previous_move_count && moves.move_count > 0) stable_move_count++; - else { previous_move_count = moves.move_count; stable_move_count = 1; confirmed = 0; } - // Target 0: pick the best move once the menu has settled. - if (target == 0 && stable_move_count >= 3 && frame) { - target = pick_best_move(stream, *frame.frame, moves, label); - } - if (moves.move_count > 0 && moves.move_count < target && stable_move_count >= 3) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Move slot " + std::to_string(target) + " is absent; no move confirmed.", stream); - } - if (slot == (int)target && stable_move_count >= 3) { - if (++confirmed >= 3) { - stream.log("[" + label + "] Move " + std::to_string(target) + " confirmed; using it."); - pbf_press_button(context, BUTTON_A, 80ms, 3000ms); - return; - } - pbf_wait(context, 100ms); - } else { - confirmed = 0; - if (slot == 0 || slot == (int)target) pbf_wait(context, 150ms); - else pbf_press_dpad(context, slot < (int)target ? DPAD_DOWN : DPAD_UP, 80ms, 250ms); - } - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Could not confirm move " + std::to_string(target) + "; A not pressed.", stream); - } - - // --------------------------------------------------------------------------- - // Starly battle - // --------------------------------------------------------------------------- - - void fight_starly(VideoStream& stream, ProControllerContext& context) { - stream.log("[AutoStory] Starting Starly battle...", COLOR_BLUE); - - // Declare OUTSIDE the loop so accumulated detector state - // (trigger counts, black-screen transitions) is preserved across frames. - // STARTER type: player has no Pokeballs, so ball-count areas won't - // be white. STARTER skips that check and only verifies status bars - // and the Fight button color. - BattleMenuWatcher battle_menu(BattleType::STARTER, COLOR_YELLOW); - EndBattleWatcher end_battle; - - while (true) { - context.wait_for_all_requests(); - - // Phase 1: wait for either battle menu or battle end. - int ret = run_until( - stream, context, - [](ProControllerContext& ctx) { - pbf_mash_button(ctx, BUTTON_B, 120000ms); - }, - { - {battle_menu}, - {end_battle}, - } - ); - - switch (ret) { - case 0: - { - stream.log("[AutoStory] Battle menu detected. Selecting move...", COLOR_BLUE); - - // Select Fight, then move slot 1. - pbf_press_button(context, BUTTON_A, 80ms, 500ms); - pbf_press_button(context, BUTTON_A, 80ms, 3000ms); - // Loop back - mash B through animations until next menu or end. - break; - } - case 1: - { - stream.log("[AutoStory] Starly battle complete.", COLOR_GREEN); - - // Mash through XP / level-up / dialog screens back to overworld. - OverworldWatcher overworld; - int cleanup_ret = run_until( - stream, context, - [](ProControllerContext& ctx) { - pbf_mash_button(ctx, BUTTON_B, 120000ms); - }, - { {overworld} } - ); - if (cleanup_ret != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Starly battle: Overworld cleanup timed out.", stream); - } - return; - } - default: - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Starly battle timed out.", stream); - } - } - } - - // --------------------------------------------------------------------------- - // Tristan battle - // --------------------------------------------------------------------------- - // Two grass rows at 240 ms each, with room to spare: the wall stops us. - static constexpr Milliseconds CATCH_GRASS_WALK = 900ms; - - static void encounter_one_wild_pokemon(VideoStream& stream, ProControllerContext& context) { - context.wait_for_all_requests(); - pbf_wait(context, 1000ms); - - bool encounter_found = false; - const auto deadline = std::chrono::steady_clock::now() + 180s; - - // Walk up and down the catch grass until a battle starts. Its three rows - // sit between tree walls, so each walk ends at a wall, on the same tile - // every time, after two new grass tiles. Short sideways steps drift and - // can rock across one tile edge without entering new grass. - while (!encounter_found && std::chrono::steady_clock::now() < deadline) { - BlackScreenWatcher battle_start; - BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - - route_turn(stream, context, Pokemon::Direction::NORTH); - pbf_move_left_joystick(context, {0, 1}, CATCH_GRASS_WALK, 200ms); - context.wait_for_all_requests(); - - int ret = wait_until( - stream, - context, - 1500ms, - { - {battle_start}, - {battle_menu}, - {battle_dialog}, - } - ); - - if (ret >= 0) { - encounter_found = true; - break; - } - - route_turn(stream, context, Pokemon::Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, CATCH_GRASS_WALK, 200ms); - context.wait_for_all_requests(); - - ret = wait_until( - stream, - context, - 1500ms, - { - {battle_start}, - {battle_menu}, - {battle_dialog}, - } - ); - - if (ret >= 0) { - encounter_found = true; - break; - } - } - - if (!encounter_found) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Wild encounter: walking up and down the grass found no battle within three minutes.", stream); - stream.log("[AutoStory] Wild battle detected!", COLOR_GREEN); - - // Wait until the actual battle menu appears. - BattleMenuWatcher confirm_menu(BattleType::STANDARD); - - int menu_ret = wait_until( - stream, - context, - 15s, - { - {confirm_menu}, - } - ); - - if (menu_ret < 0) { - stream.log( - "[AutoStory] Failed to detect battle menu.", - COLOR_RED - ); - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "[AutoStory] Failed to detect battle menu.", stream); - } - - stream.log("[AutoStory] Battle menu detected.", COLOR_GREEN); - - context.wait_for_all_requests(); - pbf_wait(context, 500ms); - - } - - static void confirm_battle_overworld(VideoStream& stream, ProControllerContext& context, - const std::string& label) { - OverworldDetector overworld; - ShortDialogDetector dialogue(COLOR_YELLOW); - BlackScreenDetector black; - VideoOverlaySet overlays(stream.overlay()); - overworld.make_overlays(overlays); - dialogue.make_overlays(overlays); - black.make_overlays(overlays); - const auto deadline = std::chrono::steady_clock::now() + 180s; - auto quiet_since = std::chrono::steady_clock::now(); - bool finished = false; - while (std::chrono::steady_clock::now() < deadline) { - const auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": No video frame.", stream); - const auto now = std::chrono::steady_clock::now(); - if (dialogue.detect(frame) || black.detect(frame) || !overworld.detect(frame)) { - quiet_since = now; - } else if (now - quiet_since >= 4s) { - finished = true; - break; - } - pbf_mash_button(context, BUTTON_B, 100ms); - context.wait_for_all_requests(); - } - if (!finished) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Dialogue-free overworld not confirmed for four seconds.", stream); - context.wait_for_all_requests(); - } - - // Our HP bar in the battle's bottom-left panel, as a fraction; -1 if - // no bar is there. Measured on 1920x1080 frames: the bar runs from - // x 88 to 466 on rows 961 to 965. - static double battle_player_hp(const ImageViewRGB32& frame) { - const ImageViewRGB32 bar = extract_box_reference(frame, ImageFloatBox{0.046, 0.890, 0.197, 0.004}); - size_t filled = 0; - for (size_t x = 0; x < bar.width(); x++) { - size_t color = 0; - for (size_t y = 0; y < bar.height(); y++) { - const uint32_t p = bar.pixel(x, y); - const int r = (p >> 16) & 255, g = (p >> 8) & 255, b = p & 255; - const bool green = g > r + 30 && g > b + 20; - const bool yellow = r > 150 && g > 120 && b < 100; - const bool red = r > 150 && r > g + 50 && r > b + 50; - color += green || yellow || red; - } - filled += color > bar.height() / 2; - } - return bar.width() && filled ? (double)filled / bar.width() : -1; - } - - enum class BattleHeal { NOT_NEEDED, USED, NONE_LEFT }; - - // At the battle menu: if our HP is under `heal_below`, use a Potion - // from the Bag on the lead, which takes the turn. Otherwise the - // battle menu is left up. - static BattleHeal battle_heal_if_low(VideoStream& stream, ProControllerContext& context, - BattleType battle_type, double heal_below, const std::string& label) { - context.wait_for_all_requests(); - double hp = -1; - for (size_t check = 0; check < 5 && hp < 0; check++) { - const auto frame = stream.video().snapshot(); - if (frame) hp = battle_player_hp(*frame.frame); - if (hp < 0) pbf_wait(context, 150ms); - context.wait_for_all_requests(); - } - stream.log("[" + label + "] Our HP bar reads " + std::to_string((int)(hp * 100)) + "%."); - if (hp < 0 || hp >= heal_below) return BattleHeal::NOT_NEEDED; - BattleMenuDetector commands(battle_type); - bool on_bag = false; - for (size_t check = 0; check < 30 && !on_bag; check++) { - context.wait_for_all_requests(); - const auto frame = stream.video().snapshot(); - const BattleMenuOption selected = frame ? battle_menu_selected_option(commands, frame) : BattleMenuOption::UNKNOWN; - if (selected == BattleMenuOption::BAG) on_bag = true; - else if (selected == BattleMenuOption::UNKNOWN) pbf_wait(context, 150ms); - else pbf_press_dpad(context, (int)selected < (int)BattleMenuOption::BAG ? DPAD_DOWN : DPAD_UP, 80ms, 300ms); - } - if (!on_bag) { - stream.log("[" + label + "] Could not reach the Bag command; fighting on.", COLOR_ORANGE); - return BattleHeal::NONE_LEFT; - } - pbf_press_button(context, BUTTON_A, 80ms, 2000ms); - if (!find_bag_item_in_open_bag(stream, context, "potion", 1)) { - stream.log("[" + label + "] No Potion left; fighting on.", COLOR_ORANGE); - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - context.wait_for_all_requests(); - return BattleHeal::NONE_LEFT; - } - // Potion, then the lead in the party list, then Use. - stream.log("[" + label + "] Using a Potion on the lead.", COLOR_BLUE); - pbf_press_button(context, BUTTON_A, 80ms, 1500ms); - pbf_press_button(context, BUTTON_A, 80ms, 1500ms); - pbf_press_button(context, BUTTON_A, 80ms, 2500ms); - context.wait_for_all_requests(); - return BattleHeal::USED; - } - - static void fight_using_move_list( - VideoStream& stream, ProControllerContext& context, - BattleType battle_type, const std::vector& move_slots, - const std::string& label, bool confirm_default_target, bool post_battle_dialogue_cleanup, - bool doubles = false, bool switch_on_faint = false - ) { - if (move_slots.empty() || std::any_of(move_slots.begin(),move_slots.end(), - [](size_t slot) { return slot>4; })) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label+": Supply a nonempty move list containing only positions 0 (best) through 4.",stream); - } - size_t move_index=0; - // The first Fight menu starts at move 1. Later turns retain - // this Pokemon's previous selection; a new battle resets it. - size_t current_move=1; - stream.log( - "[AutoStory] Starting " + label + " battle...", - COLOR_BLUE - ); - - // Scripted battles wait on the menu detector below, so no - // extra fixed delay is needed after a caller confirms it. - // Preserve the existing early-game transition delay. - if (!confirm_default_target) pbf_wait(context,5000ms); - - bool battle_menu_seen = false; - bool out_of_potions = false; - size_t potions_used = 0; - size_t turns = 0; - size_t faint_switches = 0; - const bool watch_learn_move = confirm_default_target || doubles || switch_on_faint; - EndBattleWatcher end_battle; - - while (true) { - - context.wait_for_all_requests(); - - BattleMenuWatcher battle_menu(battle_type); - // Reuse the farming handlers' move-learning prompt recovery. - SelectionArrowFinder learn_move(stream.overlay(),{0.50,0.62,0.40,0.18},COLOR_YELLOW); - - int ret = run_until( - stream, - context, - [&](ProControllerContext& context) { - // Clear narration with B while watching for commands. - // A here can open Fight before selection begins. - // With faint switching, a quiet 30 s means the party - // screen after a faint, which B cannot leave. - pbf_mash_button(context,BUTTON_B,switch_on_faint ? 30000ms : 120000ms); - }, - { - {battle_menu}, - battle_menu_seen - ? PeriodicInferenceCallback{end_battle} - : PeriodicInferenceCallback{}, - watch_learn_move ? PeriodicInferenceCallback{learn_move} : PeriodicInferenceCallback{}, - } - ); - - switch (ret) { - - // ------------------------------------------------- - // Battle menu detected. - // ------------------------------------------------- - case 0: - battle_menu_seen = true; - // Early battles take a few turns; this many means the chosen - // move is not being used (no PP, a misread menu). - if (++turns > 25) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": still at the battle menu after 25 turns.", stream); - } - - stream.log( - "[AutoStory] Battle menu detected.", - COLOR_BLUE - ); - - // A trainer battle we may lose: a Potion when HP is low. - if (battle_type == BattleType::TRAINER && !out_of_potions && potions_used < 4) { - const BattleHeal heal = battle_heal_if_low(stream, context, battle_type, 0.35, label); - if (heal == BattleHeal::USED) { - potions_used++; - break; - } - out_of_potions = heal == BattleHeal::NONE_LEFT; - } - stream.log("["+label+"] Move list position "+std::to_string(move_index+1)+ - ": use move "+std::to_string(move_slots[move_index])+"."); - select_battle_move_slot(stream, context, battle_type, move_slots[move_index], label, - confirm_default_target,current_move); - current_move=move_slots[move_index]; - // A double battle asks for the target next; B would - // back out of it. The default target is an opponent. - if (doubles) { - pbf_wait(context,1200ms); - pbf_press_button(context,BUTTON_A,80ms,1500ms); - } - // Advance once per move selection; retain the final entry. - if (move_index+1(stream, context, - [](ProControllerContext& ctx) { - pbf_mash_button(ctx, BUTTON_B, 120000ms); - }, {{overworld}}); - if (cleanup_ret != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Post-battle cleanup timed out.", stream); - } - context.wait_for_all_requests(); - confirm_battle_overworld(stream, context, label + " cleanup"); - return; - } - - case 2: - stream.log("["+label+"] Declining move learning; preserving scripted move positions."); - pbf_move_right_joystick(context,{0,-1},160ms,840ms); - pbf_press_button(context,BUTTON_ZL,160ms,840ms); - break; - - default: - // After a faint the game shows the party: Down to the - // next Pokemon, A, then A on "Send out" (by hand, - // 2026-10-29 against Roark). - if (switch_on_faint && faint_switches < 5) { - faint_switches++; - stream.log("[" + label + "] No battle menu for 30 s: a faint; sending out the next Pokemon.", COLOR_ORANGE); - pbf_press_dpad(context, DPAD_DOWN, 80ms, 500ms); - pbf_press_button(context, BUTTON_A, 80ms, 1200ms); - pbf_press_button(context, BUTTON_A, 80ms, 2000ms); - break; - } - stream.log( - "[AutoStory] " + label + " battle timeout.", - COLOR_RED - ); - - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "[AutoStory] " + label + " battle timeout.", stream); - } - } - } - - // Preserve the existing early-game callers and their B text cleanup. - static void fight_using_move_slot(VideoStream& stream, ProControllerContext& context, - BattleType battle_type, size_t move_slot, const std::string& label) { - fight_using_move_list(stream,context,battle_type,{move_slot},label,false,true); - } - - void clear_post_battle_dialogue(VideoStream& stream, ProControllerContext& context, - Button button, Milliseconds quiet_period, - const char* instruction_card) { - context.wait_for_all_requests(); - std::unique_ptr card; - if (instruction_card) card = std::make_unique( - std::make_shared(RESOURCE_PATH()+instruction_card), - ImageFloatBox{.04,.32,.92,.34},30.0); - ShortDialogDetector dialogue(COLOR_YELLOW); - BlackScreenDetector black; - VideoOverlaySet overlays(stream.overlay()); - dialogue.make_overlays(overlays); - black.make_overlays(overlays); - const auto deadline = std::chrono::steady_clock::now() + 120s; - auto absent_since = std::chrono::steady_clock::now(); - while (std::chrono::steady_clock::now() < deadline) { - const auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Post-battle dialogue: No video frame.", stream); - const auto now = std::chrono::steady_clock::now(); - const bool dialogue_visible = dialogue.detect(*frame.frame); - const bool card_visible = card && card->detect(*frame.frame); - if (dialogue_visible || card_visible || black.detect(*frame.frame)) absent_since = now; - else if (now - absent_since >= quiet_period) { - stream.log("[Post-battle dialogue] " + std::to_string(quiet_period.count()) + - "ms without dialogue confirmed."); - return; - } - // A can interact again once dialogue disappears. Only confirm - // visible prompts; B retains the existing post-battle behavior. - if (card_visible) pbf_press_button(context, BUTTON_B, 80ms, 100ms); - else if (button == BUTTON_A && !dialogue_visible) pbf_wait(context, 100ms); - else pbf_mash_button(context, button, 100ms); - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Post-battle dialogue: Three clear seconds not confirmed.", stream); - } - - void align_to_overworld_landmarks(VideoStream& stream, ProControllerContext& context, - OverworldMatchResult (*matcher)(const ImageRGB32&), const std::string& label, bool allow_story_start, bool horizontal_first, - bool search_down_first, - OverworldMatchResult (*entry_matcher)(const ImageRGB32&), - bool* entry_detected, - double arrival_tolerance_x, double arrival_tolerance_y, - double moderate_entry_confidence, bool* rock_acquisition_timed_out) { - if (rock_acquisition_timed_out) *rock_acquisition_timed_out=false; - auto missing_rocks_since=std::chrono::steady_clock::time_point::max(); - if (entry_detected) *entry_detected=false; - context.wait_for_all_requests(); - pbf_wait(context, 500ms); - context.wait_for_all_requests(); - ShortDialogDetector dialogue(COLOR_YELLOW); - BattleMenuDetector battle(BattleType::TRAINER, COLOR_GREEN); - size_t confirmations = 0, missing = 0; - for (size_t attempt = 0; attempt < 80; attempt++) { - const auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": No video frame.", stream); - if (entry_matcher){ - BlackScreenDetector transition; - if (entry_matcher(*frame.frame).located || transition.detect(*frame.frame)){ - BlackScreenWatcher visible(COLOR_RED,{0.1,0.1,0.8,0.8}, - 100,10,BlackScreenWatcher::FinderType::GONE,500ms); - if (wait_until(stream,context,30s,{{visible}})!=0) - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label+": Building transition did not finish.",stream); - const auto inside=stream.video().snapshot(); - if (!inside || !entry_matcher(*inside.frame).located) - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label+": Expected interior not confirmed after transition.",stream); - if(entry_detected)*entry_detected=true; - stream.log("["+label+"] Expected interior confirmed; ending outdoor alignment.",COLOR_GREEN); - return; - } - } - // The story can start before the exact reference point. Hand off - // to battle preparation instead of steering during dialogue. - if (dialogue.detect(*frame.frame) || battle.detect(*frame.frame)) { - if (allow_story_start) { - stream.log("[" + label + "] Story dialogue/battle started; handing off without further movement."); - return; - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Unexpected dialogue/battle during alignment.", stream); - } - const auto result = matcher(*frame.frame); - if (result.located) - missing_rocks_since=std::chrono::steady_clock::time_point::max(); - VideoOverlaySet overlays(stream.overlay()); - for (const auto& landmark : result.landmarks) { - overlays.add(COLOR_CYAN, landmark.search); - if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); - stream.log("[" + label + "] " + landmark.name + " score=" + - std::to_string(landmark.score) + ", margin=" + std::to_string(landmark.margin) + - ", offset=(" + std::to_string(landmark.dx) + "," + std::to_string(landmark.dy) + ")"); - } - stream.log("[" + label + "] located=" + std::to_string(result.located) + - ", coarse=" + std::to_string(result.coarse) + ", at_target=" + std::to_string(result.at_target) + - ", consensus offset=(" + std::to_string(result.dx) + "," + std::to_string(result.dy) + ")"); - const double entry_confidence=moderate_entry_confidence>0 ? overworld_entry_confidence(result) : 0; - const bool moderate_arrival=moderate_entry_confidence>0 && entry_confidence>=moderate_entry_confidence; - if(moderate_entry_confidence>0) stream.log("["+label+"] Exterior entry confidence="+ - std::to_string(entry_confidence)+", moderate threshold="+std::to_string(moderate_entry_confidence)+ - ", strict target="+std::to_string(result.at_target)); - // Explicit loose bounds are for non-interacting route handoffs. - // Default callers still require the matcher's precise arrival. - const bool rough_arrival=arrival_tolerance_x>0 && arrival_tolerance_y>0 && - result.located && std::abs(result.dx)<=arrival_tolerance_x && - std::abs(result.dy)<=arrival_tolerance_y; - if (result.at_target || rough_arrival || moderate_arrival) { - if (++confirmations >= 3) { stream.log("[" + label + "] Position confirmed."); return; } - pbf_wait(context, 250ms); - } else if (result.located) { - confirmations = 0; - missing = 0; - // Wait at an aligned pose while strict visual support - // recovers, rather than stepping away under a cloud. - if ((matcher==match_galactic_hq_jupiter || - matcher==match_route208_hannah_departure || matcher==match_route208_stair_top) && result.coarse && - std::abs(result.dx)<=1.0/128 && std::abs(result.dy)<=1.0/54){ - pbf_wait(context,500ms); - context.wait_for_all_requests(); - continue; - } - // A coarse match cannot confirm position, but two accepted - // anchors can establish a safe horizontal direction when both - // offsets are well outside the uncertainty in their spacing. - bool coarse_horizontal = false; - if (horizontal_first && result.coarse && result.landmarks.size() == 2) { - const auto& a = result.landmarks[0]; - const auto& b = result.landmarks[1]; - const double nearest = std::min(std::abs(a.dx), std::abs(b.dx)); - coarse_horizontal = a.accepted && b.accepted && a.dx*b.dx > 0 && - nearest > 1.0/64 && nearest > 2*std::abs(a.dx-b.dx); - } - const bool plaza_anchor_horizontal = matcher == match_jubilife_plaza_approach && - result.coarse && std::abs(result.dx)>1.0/50 && std::abs(result.dy)<=1.0/54; - // A uniquely matched Eterna ball may correct the door - // column while the sign/bush enter the frame. Arrival - // still requires the matcher's full three-anchor check. - const bool eterna_anchor_horizontal = (matcher == match_eterna_center_entrance || - matcher == match_eterna_underground_house || matcher == match_eterna_underground_man) && - result.coarse && std::abs(result.dx)>1.0/128; - // Recover the gym's doorway row before moving right if - // the south approach overshot toward the fence/cliff. - const bool gym_anchor_horizontal=matcher==match_eterna_gym_entrance && - result.coarse && std::abs(result.dx)>1.0/128 && std::abs(result.dy)<=.06; - const bool gym_row_recovery=matcher==match_eterna_gym_entrance && - result.coarse && std::abs(result.dy)>.06; - // Reach the row north of Jupiter before entering her - // column. From above that row, align sideways before - // moving south so the NPC cannot block the correction. - const bool jupiter_row_recovery=matcher==match_galactic_hq_jupiter && - result.dy < -1.0/54; - const bool jupiter_anchor_horizontal=matcher==match_galactic_hq_jupiter && - !jupiter_row_recovery && result.coarse && std::abs(result.dx)>1.0/128; - // Stay south of the Contest Hall wall/sign before crossing to Fantina. - const bool contest_row_recovery=matcher==match_hearthome_contest_fantina && result.dy>1.0/54; - // Keep the stair approach horizontal-first until close to - // its column. Recorded fine poses otherwise spent repeated - // 16ms sideways turns while a larger vertical error waited. - // Fine correction changes axis priority, never arrival gates. - const bool stair_fine_correction=matcher==match_route208_stair_top && - !result.coarse && std::abs(result.dx)<=1.0/64; - const bool horizontal = !contest_row_recovery && !gym_row_recovery && !jupiter_row_recovery && (jupiter_anchor_horizontal || coarse_horizontal || plaza_anchor_horizontal || eterna_anchor_horizontal || gym_anchor_horizontal || (!result.coarse && - ((horizontal_first && !stair_fine_correction && std::abs(result.dx)>1.0/128) || - std::abs(result.dx)>=std::abs(result.dy)*9.0/16))); - if (stair_fine_correction && horizontal_first && !horizontal && std::abs(result.dx)>1.0/128) - stream.log("["+label+"] Near stair column; correcting the larger vertical error first."); - if (coarse_horizontal) stream.log("[" + label + - "] Both landmarks agree on a large horizontal offset; correcting the street lane first."); - const double error = horizontal ? result.dx : result.dy*9.0/16; - auto duration = std::abs(error) > 1.0/24 ? 80ms : std::abs(error) > 1.0/64 ? 40ms : 16ms; - // A 16ms turn may not complete a step. Use a bounded - // full correction while reacquiring the second plaza anchor. - if (matcher == match_jubilife_plaza_approach && result.coarse) - duration = std::max(duration, 60ms); - if (matcher==match_galactic_hq_jupiter){ - for (const auto& landmark:result.landmarks){ - // The player stands around y=.74. Do not steer - // sideways/north when the stair opening is close - // to that row; a southward correction moves away. - if (landmark.name=="StairBoundary" && landmark.accepted && - landmark.found.y+landmark.found.height>.60 && - (horizontal || result.dy<0)) - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label+": Correction would approach the stair boundary.",stream); - } - } - pbf_move_left_joystick(context, - {horizontal ? (result.dx > 0 ? 1.0 : -1.0) : 0.0, - horizontal ? 0.0 : (result.dy < 0 ? 1.0 : -1.0)}, duration, 0ms); - pbf_wait(context, 500ms); - } else { - confirmations = 0; - if (matcher==match_hearthome_center_entrance || matcher==match_hearthome_contest_fantina){ - // Do not sweep into a different doorway or around the Hall's - // side when a cloud, NPC or fade hides the required anchors. - if (++missing>20) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label+": Scene anchors missing; stationary reacquisition exhausted.",stream); - pbf_wait(context,500ms); - context.wait_for_all_requests(); - continue; - } - if (matcher==match_galactic_hq_jupiter || matcher==match_route208_stair_top){ - // Keep generic missing-anchor behavior unchanged. Route208's - // explicit caller can instead request its recorded recovery. - const bool stair_rocks=matcher==match_route208_stair_top; - if (stair_rocks && rock_acquisition_timed_out) { - const auto now=std::chrono::steady_clock::now(); - if (missing_rocks_since==std::chrono::steady_clock::time_point::max()) - missing_rocks_since=now; - // Matcher processing can take seconds per observation; - // measure real elapsed time rather than counting waits. - if (now-missing_rocks_since>=10s) { - context.wait_for_all_requests(); - *rock_acquisition_timed_out=true; - stream.log("["+label+"] Rock acquisition timed out after 10s; handing off to the requested cliff recovery."); - return; - } - } else if (++missing>(stair_rocks ? 20 : 6)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label+(stair_rocks ? ": Rock anchors missing; no blind search near stairs." : - ": Chair/stair anchors missing; no blind search near stairs."),stream); - } - pbf_wait(context,500ms); - context.wait_for_all_requests(); - continue; - } - // Search right first, then across the starting point to the - // left and back. Each complete sweep returns to its origin. - // Try that sweep at the initial height, above it, and below it; - // no missing/weak landmark ever confirms the destination. - if (++missing > (search_down_first ? 78 : 74)) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Landmarks missing during bounded bidirectional search.", stream); - if (search_down_first && missing <= 4) { - stream.log("[" + label + "] Post-battle acquisition: moving down before lateral search."); - pbf_move_left_joystick(context, {0.0, -1.0}, 100ms, 0ms); - } else { - const size_t search_step = missing - 1 - (search_down_first ? 4 : 0); - const size_t level = search_step / 25; - const size_t slot = search_step % 25; - const bool lateral = slot < 24; - const double direction = slot < 6 || slot >= 18 ? 1.0 : -1.0; - const auto search_duration = lateral ? 100ms : level == 0 ? 600ms : 1200ms; - stream.log("[" + label + "] Acquisition search step=" + std::to_string(missing) + - ", direction=" + (lateral ? (direction>0 ? "right" : "left") : (level==0 ? "up" : "down"))); - // On the street, a northward search from the east - // edge can stop at the building/NPC collision row. - // Step west around it before changing height, then - // return to the same lane for the next bounded sweep. - const bool street_height_search=!lateral && matcher==match_jubilife_street_approach; - if (street_height_search) pbf_move_left_joystick(context,{-1.0,0.0},150ms,0ms); - pbf_move_left_joystick(context, - {lateral ? direction : 0.0, lateral ? 0.0 : (level == 0 ? 1.0 : -1.0)}, search_duration, 0ms); - if (street_height_search) pbf_move_left_joystick(context,{1.0,0.0},150ms,0ms); - } - pbf_wait(context, 500ms); - } - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": Could not confirm the reference position.", stream); - } - - void fight_trainer_using_moves(VideoStream& stream, ProControllerContext& context, - const std::vector& moves, const std::string& label, bool post_battle_dialogue_cleanup) { - fight_using_move_list(stream,context,BattleType::TRAINER,moves,label,true,post_battle_dialogue_cleanup); - } - - void fight_trainer_using_moves(VideoStream& stream, ProControllerContext& context, - size_t move, const std::string& label, bool post_battle_dialogue_cleanup) { - fight_trainer_using_moves(stream,context,std::vector{move},label,post_battle_dialogue_cleanup); - } - - void fight_trainer_using_move_slot(VideoStream& stream, ProControllerContext& context, - size_t move_slot, const std::string& label) { - fight_using_move_slot(stream,context,BattleType::TRAINER,move_slot,label); - } - - void fight_story_battle(VideoStream& stream, ProControllerContext& context, const std::string& label, bool doubles) { - // The best move each turn; the next Pokemon after a faint; no new moves. - fight_using_move_list(stream, context, BattleType::TRAINER, {0}, label, false, true, doubles, true); - } - - void fight_trainer(VideoStream& stream, ProControllerContext& context, const std::string& label) { - // Move 0: the best move on the menu each turn, whatever the starter. - fight_using_move_slot(stream, context, BattleType::TRAINER, 0, label); - } - - void battle_1_pokemon(VideoStream& stream, ProControllerContext& context) { - stream.log("[Training] Finding one wild Pokemon using the catch encounter wiggle.", COLOR_BLUE); - encounter_one_wild_pokemon(stream, context); - // The best move on the menu, whatever the starter. - fight_using_move_slot(stream, context, BattleType::STANDARD, 0, "Training"); - context.wait_for_all_requests(); - stream.log("[Training] One wild battle completed.", COLOR_GREEN); - } - - // --------------------------------------------------------------------------- - // Catch 1 Pokemon - // --------------------------------------------------------------------------- - - void catch_1_pokemon( - VideoStream& stream, - ProControllerContext& context - ) { - stream.log("[AutoStory] Starting catch_1_pokemon helper...", COLOR_BLUE); - - encounter_one_wild_pokemon(stream, context); - - // Catch the pokemon. - stream.log("[AutoStory] Throwing Poke Ball (manual bag open)...", COLOR_BLUE); - ReceivePokemonDetector received_pokemon; - // Preserve exit-transition state from the ball outcome through - // receipt/experience cleanup; do not recreate it between phases. - EndBattleWatcher battle_ended; - bool caught = false; - bool exit_seen = false; - for (int throw_attempt = 0; throw_attempt < 10; throw_attempt++) { - // Open bag from battle menu (X), confirm first slot (ZL). - pbf_press_button(context, BUTTON_X, 160ms, 840ms); - context.wait_for_all_requests(); - pbf_press_button(context, BUTTON_ZL, 160ms, 1000ms); - context.wait_for_all_requests(); - - // Wait to see if the battle menu comes back (ball failed) - // or if the battle ends (caught or fainted). - BattleMenuWatcher menu_back(BattleType::STANDARD, COLOR_YELLOW); - int throw_ret = run_until( - stream, context, - [](ProControllerContext& ctx) { - pbf_mash_button(ctx, BUTTON_B, 120000ms); - }, - {{menu_back}, {received_pokemon}, {battle_ended}} - ); - - if (throw_ret == 1 || (throw_ret == 2 && received_pokemon.received())) { - caught = true; - exit_seen = throw_ret == 2; - stream.log("[AutoStory] Pokemon receipt verified.", COLOR_GREEN); - break; - } - if (throw_ret < 0 || throw_ret == 2) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Catch failed: No verified Pokemon receipt before battle ended or timed out.", stream); - } - stream.log( - "[AutoStory] Ball failed, retrying (attempt " + std::to_string(throw_attempt + 2) + ")...", - COLOR_ORANGE - ); - } - - context.wait_for_all_requests(); - - if (!caught) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Catch failed: Exhausted ten ball attempts.", stream); - } - // Receipt alone is not an overworld return: clear experience, - // automatic move learning, dex registration and nickname screens - // until the same completed-black-transition watcher used in battles fires. - if (!exit_seen) { - stream.log("[Catch cleanup] Receipt confirmed; waiting for battle exit transition."); - const int cleanup_ret = run_until(stream, context, - [](ProControllerContext& ctx) { pbf_mash_button(ctx, BUTTON_B, 120000ms); }, - {{battle_ended}}); - if (cleanup_ret != 0) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Catch cleanup: Battle exit transition did not finish.", stream); - } - } - context.wait_for_all_requests(); - stream.log("[Catch cleanup] Battle exit transition confirmed; checking stable overworld."); - confirm_battle_overworld(stream, context, "Catch cleanup"); - stream.log("[Catch cleanup] Verified receipt, completed battle exit, and four seconds of clear overworld.", COLOR_GREEN); - } - - // --------------------------------------------------------------------------- - // catch_1_pokemon_v2 - burst wiggle variant for testing. - // Uses WIGGLES_PER_BURST consecutive left/right moves before checking for a - // battle, reducing total wait time compared to checking after every move. - // Commented out - enable for testing when ready. - // --------------------------------------------------------------------------- - // void catch_1_pokemon_v2( - // SingleSwitchProgramEnvironment& env, - // VideoStream& stream, - // ProControllerContext& context - // ) { - // stream.log("[AutoStory] Starting catch_1_pokemon_v2 helper...", COLOR_BLUE); - // - // // Save once before attempting to catch. On failure the game - // // resets back to this save, so the save lives outside the loop. - // stream.log("[AutoStory] Saving game before catch attempt...", COLOR_BLUE); - // { - // static constexpr int SAVE_MAX_RETRIES = 3; - // bool save_succeeded = false; - // for (int save_attempt = 0; save_attempt < SAVE_MAX_RETRIES; save_attempt++) { - // try { - // if (save_attempt > 0) { - // stream.log( - // "[AutoStory] Retrying save (attempt " + std::to_string(save_attempt + 1) + ")...", - // COLOR_ORANGE - // ); - // pbf_mash_button(context, BUTTON_B, 1000ms); - // context.wait_for_all_requests(); - // } - // save_game(stream, context); - // save_succeeded = true; - // break; - // } - // catch (OperationFailedException& e) { - // stream.log( - // "[AutoStory] Save attempt " + std::to_string(save_attempt + 1) + " failed: " + e.message(), - // COLOR_RED - // ); - // context.wait_for(1000ms); - // } - // } - // if (!save_succeeded) { - // stream.log("[AutoStory] All save attempts failed. Using blind fallback save.", COLOR_ORANGE); - // pbf_mash_button(context, BUTTON_B, 1000ms); - // save_game(context); - // pbf_mash_button(context, BUTTON_B, 2000ms); - // } - // } - // context.wait_for_all_requests(); - // pbf_wait(context, 1000ms); - // - // // ------------------------------------------------------- - // // Mini-checkpoint loop: retry from the save above if we - // // run out of balls or our Pokemon faints. - // // ------------------------------------------------------- - // while (true) { - // - // BlackScreenWatcher battle_start; - // BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); - // BattleDialogWatcher battle_dialog(COLOR_YELLOW); - // - // bool encounter_found = false; - // - // // Wiggle left/right in bursts, checking for a battle after each burst. - // // Doing multiple moves before checking avoids waiting after every step. - // static constexpr int WIGGLES_PER_BURST = 3; - // while (!encounter_found) { - // for (int i = 0; i < WIGGLES_PER_BURST; i++) { - // pbf_move_left_joystick(context, {-1, 0}, 120ms, 80ms); - // pbf_move_left_joystick(context, { 1, 0}, 120ms, 80ms); - // } - // context.wait_for_all_requests(); - // - // int ret = wait_until( - // stream, - // context, - // 1500ms, - // { - // {battle_start}, - // {battle_menu}, - // {battle_dialog}, - // } - // ); - // - // if (ret >= 0) { - // encounter_found = true; - // } - // } - // - // stream.log("[AutoStory] Wild battle detected!", COLOR_GREEN); - // - // // Wait until the actual battle menu appears. - // BattleMenuWatcher confirm_menu(BattleType::STANDARD); - // - // int menu_ret = wait_until( - // stream, - // context, - // 15s, - // { - // {confirm_menu}, - // } - // ); - // - // if (menu_ret < 0) { - // stream.log( - // "[AutoStory] Failed to detect battle menu. Resetting to checkpoint...", - // COLOR_RED - // ); - // reset_game_from_home(env, env.console, context, true); - // continue; - // } - // - // stream.log("[AutoStory] Battle menu detected.", COLOR_GREEN); - // - // context.wait_for_all_requests(); - // pbf_wait(context, 500ms); - // - // // --- Throw balls --- - // CatchResults result = basic_catcher( - // stream, - // context, - // Language::English, - // "poke-ball", - // 10 - // ); - // - // switch (result.result) { - // case CatchResult::POKEMON_CAUGHT: - // stream.log( - // "[AutoStory] Pokemon successfully caught after " + - // std::to_string(result.balls_used) + " ball(s).", - // COLOR_GREEN - // ); - // { - // OverworldWatcher overworld; - // run_until( - // stream, context, - // [](ProControllerContext& ctx) { - // pbf_mash_button(ctx, BUTTON_B, 120000ms); - // }, - // {{overworld}} - // ); - // } - // return; - // - // case CatchResult::POKEMON_FAINTED: - // stream.log("[AutoStory] Wild Pokemon fainted. Exiting battle.", COLOR_ORANGE); - // { - // OverworldWatcher overworld; - // run_until( - // stream, context, - // [](ProControllerContext& ctx) { - // pbf_mash_button(ctx, BUTTON_B, 120000ms); - // }, - // {{overworld}} - // ); - // } - // return; - // - // case CatchResult::OUT_OF_BALLS: - // stream.log( - // "[AutoStory] Ran out of Poke Balls after " + - // std::to_string(result.balls_used) + " throw(s). Resetting to checkpoint...", - // COLOR_RED - // ); - // reset_game_from_home(env, env.console, context, true); - // continue; - // - // case CatchResult::OWN_FAINTED: - // stream.log( - // "[AutoStory] Own Pokemon fainted. Resetting to checkpoint...", - // COLOR_RED - // ); - // reset_game_from_home(env, env.console, context, true); - // continue; - // - // case CatchResult::BALL_LIMIT_REACHED: - // stream.log( - // "[AutoStory] Ball limit reached (" + - // std::to_string(result.balls_used) + " throw(s)). Resetting to checkpoint...", - // COLOR_RED - // ); - // reset_game_from_home(env, env.console, context, true); - // continue; - // - // case CatchResult::CANNOT_THROW_BALL: - // stream.log( - // "[AutoStory] Cannot throw ball (menu timing issue). Resetting to checkpoint...", - // COLOR_RED - // ); - // reset_game_from_home(env, env.console, context, true); - // continue; - // - // default: - // stream.log( - // "[AutoStory] Catch timed out after " + - // std::to_string(result.balls_used) + " ball(s). Resetting to checkpoint...", - // COLOR_RED - // ); - // reset_game_from_home(env, env.console, context, true); - // continue; - // } - // - // } // end while(true) - // } - - // --------------------------------------------------------------------------- - // Movement Helpers - // --------------------------------------------------------------------------- - - bool detect_black_screen_after_movement( - VideoStream& stream, - ProControllerContext& context, - Milliseconds timeout - ) { - BlackScreenWatcher black_screen; - return wait_until(stream, context, timeout, {{black_screen}}) == 0; - } - - bool try_right_down_exit_search( - VideoStream& stream, - ProControllerContext& context, - size_t iterations - ) { - for (size_t i = 0; i < iterations; i++) { - pbf_move_left_joystick(context, { 1.0, 0.0 }, 80ms, 0ms); - pbf_move_left_joystick(context, { 0.0, -1.0 }, 220ms, 0ms); - - if (detect_black_screen_after_movement(stream, context, 1000ms)) { - return true; - } - } - return false; - } - - void leave_pokemon_center_before_barry_1( - VideoStream& stream, - ProControllerContext& context - ) { - // Original down movement to leave Pokemon Center. - pbf_move_left_joystick(context, { 0.0, -1.0 }, 814ms, 0ms); - - if (detect_black_screen_after_movement(stream, context, 1200ms)) { - pbf_wait(context, 2340ms); - return; - } - - stream.log("[AutoStory] Pokemon Center exit black screen not detected. Searching with right/down movement.", COLOR_ORANGE); - - if (try_right_down_exit_search(stream, context, 20)) { - pbf_wait(context, 2340ms); - return; - } - - stream.log("[AutoStory] First exit search failed. Repositioning left and retrying.", COLOR_ORANGE); - pbf_move_left_joystick(context, { -1.0, 0.0 }, 5000ms, 0ms); - - if (try_right_down_exit_search(stream, context, 30)) { - pbf_wait(context, 2340ms); - return; - } - - OperationFailedExceptionWithScreenshot::fire( - ErrorReportMode::SEND_ERROR_REPORT, - "Failed to find Pokemon Center exit black screen before Barry 1. Triggering checkpoint retry.", - stream - ); - } - - // --------------------------------------------------------------------------- - // Generic Movement Helper - Walk through grass in specified direction for - // specified steps, attempting to run from any battles that are encountered. - // --------------------------------------------------------------------------- - void walk_through_grass( - VideoStream& stream, ProControllerContext& context, - int8_t x, int8_t y, size_t steps, const std::string& direction_name, - BattleType battle_type, Milliseconds movement_duration, - OverworldMatchResult (*destination)(const ImageRGB32&) - ) { - size_t movements = 0, confirmations = 0; - while (true) { - context.wait_for_all_requests(); - // Destination checks happen before input and after each step/escape. - // With no matcher, retain the original fixed-step behavior. - VideoOverlaySet overlays(stream.overlay()); - OverworldMatchResult location; - if (destination) { - const auto frame = stream.video().snapshot(); - if (!frame) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Grass destination: No video frame.", stream); - location = destination(*frame.frame); - for (const auto& landmark : location.landmarks) { - overlays.add(COLOR_CYAN, landmark.search); - if (landmark.accepted) overlays.add(COLOR_GREEN, landmark.found); - stream.log("[Grass destination] " + direction_name + " " + landmark.name + - " score=" + std::to_string(landmark.score) + - " margin=" + std::to_string(landmark.margin) + - " accepted=" + std::to_string(landmark.accepted)); - } - stream.log("[Grass destination] located=" + std::to_string(location.located) + - " at_target=" + std::to_string(location.at_target) + - " screen-relative offset=(" + std::to_string(location.dx) + - "," + std::to_string(location.dy) + ")"); - if (location.at_target) { - if (++confirmations >= 3) { - stream.log("[Grass] Destination confirmed; stopped " + direction_name + - " after " + std::to_string(movements) + "/" + std::to_string(steps) + " movements.", COLOR_GREEN); - return; - } - pbf_wait(context, 250ms); - continue; - } - confirmations = 0; - } - if (movements >= steps) { - if (destination) OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Grass destination not confirmed within movement limit: " + direction_name, stream); - stream.log("[Grass] Completed " + direction_name + " (" + std::to_string(steps) + " steps).", COLOR_GREEN); - return; - } - int8_t move_x=x, move_y=y; - Milliseconds duration=movement_duration; - if (destination && location.located && !location.coarse && - std::abs(location.dx)<=0.10 && std::abs(location.dy)<=0.12) { - // Small corrections prevent stepping past a narrow stop target. - const bool horizontal=std::abs(location.dx)>=std::abs(location.dy)*9.0/16; - const double error=horizontal ? location.dx : location.dy*9.0/16; - duration=std::min(duration, std::abs(error)>0.03125 ? 80ms : - std::abs(error)>0.015625 ? 40ms : 16ms); - move_x=horizontal ? (location.dx>0 ? 1 : -1) : 0; - move_y=horizontal ? 0 : (location.dy<0 ? 1 : -1); - stream.log("[Grass destination] Correcting " + std::string(horizontal ? "horizontal" : "vertical") + - " axis for " + std::to_string(duration.count()) + "ms."); - } - // Each movement gets fresh watcher state. A completed white - // transition from a prior encounter must not trigger again. - BlackScreenWatcher battle_start; - WhiteScreenOverWatcher white_screen(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BattleMenuWatcher battle_menu(battle_type, COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - pbf_move_left_joystick(context, {(double)move_x, (double)move_y}, duration, 0ms); - movements++; - context.wait_for_all_requests(); - int ret = wait_until(stream, context, 2500ms, - {{white_screen}, {battle_start}, {battle_dialog}, {battle_menu}}); - if (ret >= 0) { - confirmations = 0; - stream.log("[Grass] " + direction_name + " step " + std::to_string(movements) + - "/" + std::to_string(steps) + ": encounter trigger=" + std::to_string(ret) + - "; using shared confirmed-Run escape."); - escape_wild_battle(stream, context, battle_type); - context.wait_for_all_requests(); - stream.log("[Grass] Escape confirmed; resuming " + direction_name + "."); - } - } - } - - } - } -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h deleted file mode 100644 index 351ed5ab5b..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h +++ /dev/null @@ -1,161 +0,0 @@ -#pragma once - -#include "PokemonBDSP_OverworldMatchers.h" - -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" -#include "../StorySegments/PokemonBDSP_AutoStory_Segment_00.h" -#include "CommonTools/Async/InferenceRoutines.h" - -#include "NintendoSwitch/Programs/DateManip/NintendoSwitch_DateManip.h" - -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" - -namespace PokemonAutomation { - namespace NintendoSwitch { - namespace PokemonBDSP { - - // --------------------------------------------------------------------------- - // Utility Watchers - // --------------------------------------------------------------------------- - - class BattleDialogWatcher : public DetectorToFinder { - public: - BattleDialogWatcher(Color color = COLOR_RED); - }; - - // --------------------------------------------------------------------------- - // Time / setup helpers - // --------------------------------------------------------------------------- - - // Shared visual alignment and dialogue cleanup used by story routes. - void clear_post_battle_dialogue(VideoStream& stream, ProControllerContext& context, - Button button = BUTTON_B, Milliseconds quiet_period = Milliseconds(3000), - const char* instruction_card = nullptr); - void align_to_overworld_landmarks(VideoStream& stream, ProControllerContext& context, - OverworldMatchResult (*matcher)(const ImageRGB32&), const std::string& label, bool allow_story_start = false, bool horizontal_first = false, - bool search_down_first = false, - OverworldMatchResult (*entry_matcher)(const ImageRGB32&) = nullptr, - bool* entry_detected = nullptr, - double arrival_tolerance_x = 0, double arrival_tolerance_y = 0, - double moderate_entry_confidence = 0, - bool* rock_acquisition_timed_out = nullptr); - - void verify_tutorial_settings(VideoStream& stream, ProControllerContext& context); - - void set_time_fast( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - const DateTime& target, - bool preserve_date = false // Keep current year/month/day; set only target time. - ); - - // --------------------------------------------------------------------------- - // Battles - // --------------------------------------------------------------------------- - - void fight_starly( - VideoStream& stream, - ProControllerContext& context - ); - - // Select the requested 1-based move slot using visual cursor confirmation. - void fight_trainer_using_move_slot(VideoStream& stream, ProControllerContext& context, - size_t move_slot, const std::string& label); - - // Any trainer battle, whatever the starter: uses move slot 1 until the - // battle ends. Slot 1 is a damaging move (Tackle, Scratch or Pound) for - // every starter. - void fight_trainer(VideoStream& stream, ProControllerContext& context, const std::string& label); - - // A story battle that may go badly: the best move each turn, Potions - // under 35 %, the next Pokemon when one faints, and "Keep old moves" - // on level-up prompts. `doubles` confirms the default target each - // turn (tag and twin battles). Returns once the battle has ended, - // won or lost; after a loss we stand in the last Pokemon Center. - void fight_story_battle(VideoStream& stream, ProControllerContext& context, const std::string& label, bool doubles = false); - - // --------------------------------------------------------------------------- - // Catching / healing - // --------------------------------------------------------------------------- - - void catch_1_pokemon( - VideoStream& stream, - ProControllerContext& context - ); - - // Finds one wild Pokemon in the grass by stepping left and right, and - // fights it with the best move on the menu. Does not heal. - void battle_1_pokemon(VideoStream& stream, ProControllerContext& context); - - // Burst wiggle variant of catch_1_pokemon for testing. - // Commented out in the .cpp - uncomment both to enable. - // void catch_1_pokemon_v2( - // SingleSwitchProgramEnvironment& env, - // VideoStream& stream, - // ProControllerContext& context - // ); - - // --------------------------------------------------------------------------- - // Grass movement helpers - // --------------------------------------------------------------------------- - - void walk_through_grass( - VideoStream& stream, - ProControllerContext& context, - int8_t x, - int8_t y, - size_t steps, - const std::string& direction_name, - BattleType battle_type = BattleType::STANDARD, - Milliseconds movement_duration = Milliseconds(200), - OverworldMatchResult (*destination)(const ImageRGB32&) = nullptr - ); - - bool detect_black_screen_after_movement( - VideoStream& stream, - ProControllerContext& context, - Milliseconds timeout - ); - - bool try_right_down_exit_search( - VideoStream& stream, - ProControllerContext& context, - size_t iterations - ); - - void leave_pokemon_center_before_barry_1( - VideoStream& stream, - ProControllerContext& context - ); - // --------------------------------------------------------------------------- - // Helper function to navigate the poketech challenge clown dialogue. - // Example usage - // Move up and talk to clown. - // clown_dialog_helper( - // stream, - // context, - // DPAD_UP - // ); - - // Move left and talk to clown. - // clown_dialog_helper( - // stream, - // context, - // DPAD_LEFT - // ); - - // Move right and talk to clown. - // clown_dialog_helper( - // stream, - // context, - // DPAD_RIGHT - // ); - // ---------------------------------------------------------------------------- - void clown_dialog_helper( - VideoStream& stream, - ProControllerContext& context, - DpadPosition direction - ); - } - } -} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp deleted file mode 100644 index fef4620ab1..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ /dev/null @@ -1,203 +0,0 @@ -/* BDSP AutoStory - Story step executor - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" -#include "PokemonBDSP_AutoStory_PokemonCenter.h" -#include "PokemonBDSP_AutoStory_Segment_00_tools.h" -#include "PokemonBDSP_AutoStory_Walk.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h" -#include "PokemonBDSP_AutoStory_StoryExecutor.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; -using Pokemon::Direction; -using Pokemon::Outcome; -using Pokemon::RunState; -using Pokemon::TilePos; - - -std::optional story_route_costs(const std::string& variant){ - if (variant.empty()){ - return route_plan_costs(); - } - if (variant == "town"){ - return town_plan_costs(); - } - if (variant == "robust"){ - return Pokemon::RobustPlanCosts(); - } - return std::nullopt; -} - -std::pair dpad_of(Direction direction){ - switch (direction){ - case Direction::NORTH: - return {0, 1}; - case Direction::EAST: - return {1, 0}; - case Direction::SOUTH: - return {0, -1}; - case Direction::WEST: - return {-1, 0}; - } - return {0, 0}; -} - - -ConsoleStoryExecutor::ConsoleStoryExecutor( - StoryStepRunner& runner, std::set stop_rows, - const Pokemon::MapSet& maps, std::vector trainers, - TrainerFight fight, std::map variants -) - : m_runner(runner) - , m_stop_rows(std::move(stop_rows)) - , m_maps(maps) - , m_all_trainers(std::move(trainers)) - , m_trainers(m_all_trainers) - , m_fight(std::move(fight)) - , m_variants(std::move(variants)) -{} - -void ConsoleStoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ - m_checkpoint = data.id; - m_trainers = m_all_trainers; -} - -RouteTrainerFight ConsoleStoryExecutor::fight(StoryConsole& console){ - return [this, &console](const std::string& trainer){ - m_fight(console, trainer); - }; -} - -StoryConsole& ConsoleStoryExecutor::console(){ - if (m_console == nullptr){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "A story step ran outside ConsoleStoryExecutor::run()."); - } - return *m_console; -} - -Outcome ConsoleStoryExecutor::run(RunState& state, const Pokemon::StoryStep& step){ - Outcome result = Pokemon::outcome_ok(); - const StoryFailure on_failure = m_stop_rows.contains(m_checkpoint) ? StoryFailure::STOP : StoryFailure::RELOAD_AND_RETRY; - const Outcome ran = m_runner.run_step(m_checkpoint, on_failure, [&](StoryConsole& console){ - m_console = &console; - set_checks(console.options.checks); - set_language(console.options.language); - const auto variant = m_variants.find(step.variant); - result = variant == m_variants.end() - ? Pokemon::StoryStepExecutor::run(state, step) - : variant->second(*this, console, state, step); - m_console = nullptr; - }); - m_console = nullptr; - return ran.succeeded() ? result : ran; -} - -Outcome ConsoleStoryExecutor::route(RunState& state, const Pokemon::StoryStep& step){ - const std::vector from = Pokemon::tiles_of(state.pose); - if (from.empty()){ - return Pokemon::outcome_failed("no_pose", Pokemon::to_string(state.pose)); - } - const std::optional costs = story_route_costs(step.variant); - if (!costs){ - return Pokemon::outcome_failed("unknown_variant", step.variant); - } - const std::vector goal = Pokemon::tiles_of(*step.target); - StoryConsole& c = console(); - const std::vector at = walk_route( - c.env.console, c.context, run_from_wild_battle, m_maps, from, - [&](const TilePos& pos){ return std::find(goal.begin(), goal.end(), pos) != goal.end(); }, - m_trainers, fight(c), step.name, {}, *costs - ); - return Pokemon::outcome_ok(Pokemon::tile_pose(step.target->map, at)); -} - -Outcome ConsoleStoryExecutor::legs(RunState& state, const Pokemon::StoryStep& step){ - const bool one_short_ok = step.variant == "one_short_ok"; - if (!one_short_ok && !step.variant.empty()){ - return Pokemon::outcome_failed("unknown_variant", step.variant); - } - StoryConsole& c = console(); - std::vector at = Pokemon::tiles_of(state.pose); - for (const Pokemon::StoryLeg& leg : step.legs){ - // Facing another way, a tap only turns (r256): turn first. - route_turn(c.env.console, c.context, leg.direction); - const auto [x, y] = dpad_of(leg.direction); - const size_t tiles = (size_t)leg.tiles; - const WalkResult walk = walk_tiles_dpad(c.env.console, c.context, run_from_wild_battle, x, y, tiles, step.name); - c.env.console.log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); - if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ - return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); - } - // Where we may be: the end, or a tile short of it. - std::vector next; - for (const TilePos& start : at){ - TilePos end = start; - for (size_t i = 0; i < tiles; i++){ - if (one_short_ok && i + 1 == tiles){ - next.push_back(end); - } - end = end.next(leg.direction); - } - next.push_back(end); - } - at = std::move(next); - } - return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.legs.back().direction)); -} - -Outcome ConsoleStoryExecutor::train(RunState&, const Pokemon::StoryStep& step){ - StoryConsole& c = console(); - for (size_t battle = 0; battle < step.battles; battle++){ - const std::string label = std::format("Training battle {}/{}", battle + 1, step.battles); - route_status(c.env.console, label); - battle_1_pokemon(c.env.console, c.context); - fail_if_blacked_out(c.env.console, c.context, label); - } - return Pokemon::outcome_ok(); -} - -Outcome ConsoleStoryExecutor::heal(RunState&, const Pokemon::StoryStep& step){ - StoryConsole& c = console(); - heal_at_pokemon_center(c.env.console, c.context, step.why.empty() ? step.name : step.why); - return Pokemon::outcome_ok(); -} - -Outcome ConsoleStoryExecutor::save(RunState&, const Pokemon::StoryStep&){ - StoryConsole& c = console(); - checkpoint_save(c.env, c.context, c.options.notif_status_update, c.stats, c.options.checkpoint_save_mode); - return Pokemon::outcome_ok(); -} - -Outcome ConsoleStoryExecutor::mash(RunState&, const Pokemon::StoryStep& step){ - StoryConsole& c = console(); - wait_for_dialogue(c.env.console, c.context, step.name); - pbf_mash_button(c.context, BUTTON_B, 5000ms); - c.context.wait_for_all_requests(); - return Pokemon::outcome_ok(); -} - - -ConsoleStoryExecutor::Variant story_call(StoryAction action){ - return [action = std::move(action)](ConsoleStoryExecutor&, StoryConsole& console, RunState&, const Pokemon::StoryStep&){ - action(console); - return Pokemon::outcome_ok(); - }; -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h deleted file mode 100644 index 48a07aa476..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h +++ /dev/null @@ -1,123 +0,0 @@ -/* BDSP AutoStory - Story step executor - * - * From: https://github.com/PokemonAutomation/ - * - * BDSP's way of doing the shared story steps - * (Pokemon/Navigation/Pokemon_StorySteps.h) on the console. Routes are - * planned on the story's map set from the tiles the run's pose holds, - * with the trainers who can still see us fought on the way; a route's - * variant names its plan costs. A segment adds its own steps by variant - * (the Mart lady, a walk through a gap in town): a variant registered - * here is used for its step whatever the kind. - * - * Each step runs through the table's StoryStepRunner, so a failed step - * is answered as the table's other rows are: a screenshot, the - * "checkpoint_reattempt_loop: attempt N failed" line the lab watches - * for, and a failed Outcome that the segment's policy answers. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H -#define PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H - -#include -#include -#include -#include -#include -#include -#include -#include "Pokemon/Navigation/Pokemon_MapSet.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" -#include "Pokemon/Navigation/Pokemon_TileGrid.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// The plan costs a ROUTE step's variant names: "" the route costs, -// "town" town_plan_costs(), "robust" the planner's defaults. Nothing for -// another name. -std::optional story_route_costs(const std::string& variant); - -// The D-pad direction (x, y) of `direction`: y is up the screen. -std::pair dpad_of(Pokemon::Direction direction); - - -// Does BDSP's kinds of story step for one checkpoint of a table. -class ConsoleStoryExecutor : public Pokemon::StoryStepExecutor{ -public: - // A segment's own way of doing a step, by the step's variant. It may - // set run flags for later steps' skip_if, and returns where it left - // the player if it knows. - using Variant = std::function; - // Fights a trainer whose dialogue has opened. - using TrainerFight = std::function; - - // `stop_rows`: the rows whose failure ends the program (rows_that_stop()). - ConsoleStoryExecutor( - StoryStepRunner& runner, std::set stop_rows, - const Pokemon::MapSet& maps, std::vector trainers, - TrainerFight fight, std::map variants - ); - - // A new attempt of a row: every trainer can see us again. - virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override; - - const Pokemon::MapSet& maps() const{ - return m_maps; - } - // The trainers who can still see us, and how to fight one, for a - // variant that walks with walk_route(). - std::vector& trainers(){ - return m_trainers; - } - RouteTrainerFight fight(StoryConsole& console); - - virtual Pokemon::Outcome run(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - - virtual Pokemon::Outcome route(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - // Each leg in D-pad taps, turning first. Variant "one_short_ok": a leg - // may end a tile short (a door lined up by sight after). - virtual Pokemon::Outcome legs(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - virtual Pokemon::Outcome train(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - // In front of a Pokemon Center, its lit doorway on screen: in, the - // nurse, and out (heal_at_pokemon_center()). - virtual Pokemon::Outcome heal(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - // The checkpoint save (checkpoint_save(), real or fake as the options say). - virtual Pokemon::Outcome save(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - // A dialogue opens (or has), then B through it. - virtual Pokemon::Outcome mash(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - -private: - StoryConsole& console(); - - StoryStepRunner& m_runner; - std::set m_stop_rows; - std::string m_checkpoint; - const Pokemon::MapSet& m_maps; - std::vector m_all_trainers; - std::vector m_trainers; - TrainerFight m_fight; - std::map m_variants; - // The console of the step running now. - StoryConsole* m_console = nullptr; -}; - - -// A variant that calls an old console function, for a step that has no -// declarative form yet. -ConsoleStoryExecutor::Variant story_call(StoryAction action); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp deleted file mode 100644 index 4ff9530c50..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/* BDSP AutoStory - Story tables - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "Common/Cpp/PrettyPrint.h" -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/Globals.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" -#include "PokemonBDSP_AutoStory_StoryTables.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; -using Pokemon::Checkpoint; -using Pokemon::Event; -using Pokemon::Outcome; -using Pokemon::RunState; - - -namespace{ - -Event save_event(StoryStepRunner& runner, const std::string& id){ - Event event; - event.name = "save"; - event.why = "the save a failed step resets to"; - event.skip_if.push_back(Pokemon::condition("saved at the start", [&runner, id](RunState&){ - return runner.saved_at_start(id); - })); - event.act = [&runner, id](RunState&){ - return runner.save_at_start(id); - }; - return event; -} - -} - - -Pokemon::Segment story_table_segment( - StoryStepRunner& runner, std::string id, const std::vector& rows, - std::shared_ptr executor -){ - Pokemon::Segment segment; - segment.id = std::move(id); - for (const StoryRow& row : rows){ - segment.checkpoints.push_back(Pokemon::CheckpointSpec{ - .id = row.id, .title = row.title, .saves = false, - .build = [&runner, &row, executor]{ - Checkpoint steps = Pokemon::build_story_checkpoint(row, *executor); - if (row.on_failure != StoryFailure::RELOAD_AND_RETRY){ - return steps; - } - Checkpoint checkpoint; - checkpoint.id = row.id; - checkpoint.events.push_back(save_event(runner, row.id)); - for (Event& event : steps.events){ - checkpoint.events.push_back(std::move(event)); - } - return checkpoint; - }, - }); - } - return segment; -} - -std::set rows_that_stop(const std::vector& rows){ - std::set ids; - for (const StoryRow& row : rows){ - if (row.on_failure == StoryFailure::STOP){ - ids.insert(row.id); - } - } - return ids; -} - - -ConsoleStoryRunner::ConsoleStoryRunner(StoryConsole console) - : m_console(console) -{} - -bool ConsoleStoryRunner::saved_at_start(const std::string& id) const{ - return m_saved.contains(id); -} - -// A save that fails is a failed first attempt, as in the loop: the retry -// resets and does not save again. -Outcome ConsoleStoryRunner::save_at_start(const std::string& id){ - m_saved.insert(id); - return run_step(id, StoryFailure::RELOAD_AND_RETRY, [](StoryConsole& console){ - checkpoint_save(console.env, console.context, console.options.notif_status_update, console.stats, console.options.checkpoint_save_mode); - console.stats.m_first_run = false; - }); -} - -Outcome ConsoleStoryRunner::run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step){ - if (on_failure == StoryFailure::STOP){ - step(m_console); - return Pokemon::outcome_ok(); - } - SingleSwitchProgramEnvironment& env = m_console.env; - try{ - step(m_console); - return Pokemon::outcome_ok(); - }catch (OperationFailedException& e){ - // The fake-save mode's loop around the whole range retries it. - if (m_console.options.checkpoint_retry_scope == CheckpointRetryScope::DEFER_TO_SELECTED_RANGE){ - throw; - } - // Attempts are the segment's policy (AutoStoryOptions::policy); this - // only counts them for the log line the lab's run watcher reads. - const size_t attempt = m_failures[id]++; - env.console.log(std::format("checkpoint_reattempt_loop: attempt {} failed: {}: {}", attempt, id, e.message()), COLOR_RED); - // Keep what the screen showed at the failure; the retry resets the game. - if (VideoSnapshot screen = env.console.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/failures/"; - Filesystem::create_directories(Filesystem::Path(folder)); - const std::string path = std::format("{}{}-attempt{}.png", folder, now_to_filestring(), attempt); - screen.frame->save(path); - env.console.log("Failure screenshot: " + path); - } - m_console.stats.m_reset++; - env.update_stats(); - return Pokemon::outcome_failed("operation_failed", e.message()); - } -} - -Outcome ConsoleStoryRunner::reload(){ - SingleSwitchProgramEnvironment& env = m_console.env; - env.console.log("Resetting game from HOME before retry...", COLOR_ORANGE); - if (!reset_game_from_home(env, env.console, m_console.context, true, 1000ms)){ - throw OperationFailedExceptionWithScreenshot( - ErrorReportMode::SEND_ERROR_REPORT, - "Failed to reset game from HOME during checkpoint retry.", - env.console - ); - } - return Pokemon::outcome_ok(); -} - -void ConsoleStoryRunner::run(Pokemon::Segment& segment, const Pokemon::SegmentRange& range){ - SingleSwitchProgramEnvironment& env = m_console.env; - Pokemon::SegmentHooks hooks; - hooks.save = [](RunState&){ - return Pokemon::outcome_failed("no_save_hook", "story checkpoints save at their start"); - }; - hooks.reload = [this](RunState&){ - return reload(); - }; - hooks.log = [&env](const std::string& line){ - env.log(line, COLOR_BLUE); - }; - const Pokemon::SegmentPolicy& policy = m_console.options.policy; - RunState state; - const Pokemon::SegmentReport report = Pokemon::run_segment(state, segment, range, policy, hooks); - if (!report.completed){ - throw OperationFailedExceptionWithScreenshot( - ErrorReportMode::SEND_ERROR_REPORT, - std::format( - "AutoStory segment {}: failed at {} {}: {}", - segment.id, report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure) - ), - env.console - ); - } -} - - -void run_story_table( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, - const AutoStoryOptions& options, AutoStoryStats& stats, - const StorySegmentFactory& make_segment, const Pokemon::SegmentRange& range -){ - ConsoleStoryRunner runner(StoryConsole{env, context, options, stats}); - Pokemon::Segment segment = make_segment(runner); - runner.run(segment, range); -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h deleted file mode 100644 index 165cc3cbd9..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h +++ /dev/null @@ -1,119 +0,0 @@ -/* BDSP AutoStory - Story tables - * - * From: https://github.com/PokemonAutomation/ - * - * A segment of the story as a table of checkpoints on the shared engine - * (Pokemon_StorySegments.h). Each checkpoint is the list of step functions - * it called in order before it was a table, each step one event. The - * checkpoint saves first, as checkpoint_reattempt_loop() did before its - * first attempt; a step that fails is answered the way that loop answered - * it: a screenshot, a reset to the save, and the checkpoint again from its - * first step, up to the attempts of the shared policy the program's retry - * options set (AutoStoryOptions::policy; 3 by default, where the loop had 20). - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_H -#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_H - -#include -#include -#include -#include -#include -#include -#include "Pokemon/Navigation/Pokemon_StorySegments.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// What a step runs on on the console. -struct StoryConsole{ - SingleSwitchProgramEnvironment& env; - ProControllerContext& context; - const AutoStoryOptions& options; - AutoStoryStats& stats; -}; -// A console action: what a step that calls an old function runs. -using StoryAction = std::function; - -// What a checkpoint does when a step fails. -enum class StoryFailure{ - // Save before the first step; on a failure reset to that save and run - // the checkpoint again (checkpoint_reattempt_loop with save=true). - RELOAD_AND_RETRY, - // No save and no retry: the failure ends the program (00.00). - STOP, -}; - -// A row of a story table: the shared checkpoint, and what a failure does. -struct StoryRow : public Pokemon::StoryCheckpoint{ - StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY; -}; - -// How a table saves at a row's start and runs a console action: the -// console runner below, or a fake in the tests. -class StoryStepRunner{ -public: - virtual ~StoryStepRunner() = default; - // Whether checkpoint `id` has saved at its start in this run; a retry - // starts from that save and does not save again. - virtual bool saved_at_start(const std::string& id) const = 0; - virtual Pokemon::Outcome save_at_start(const std::string& id) = 0; - virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step) = 0; -}; - -// The table `rows` (kept alive by the caller) as segment `id`: each row -// that retries saves first, through `runner`, unless this run has saved it -// already (a retry starts from that save); then its steps run with -// `executor`, which the segment keeps. -Pokemon::Segment story_table_segment( - StoryStepRunner& runner, std::string id, const std::vector& rows, - std::shared_ptr executor -); - -// The ids of the rows that stop on a failure instead of retrying. -std::set rows_that_stop(const std::vector& rows); - -using StorySegmentFactory = std::function; - - -// Runs tables on the Switch. -class ConsoleStoryRunner : public StoryStepRunner{ -public: - explicit ConsoleStoryRunner(StoryConsole console); - - virtual bool saved_at_start(const std::string& id) const override; - virtual Pokemon::Outcome save_at_start(const std::string& id) override; - virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step) override; - - // Runs the rows of `segment` in `range`; throws as the checkpoint - // loop did once a checkpoint has used up its attempts. - void run(Pokemon::Segment& segment, const Pokemon::SegmentRange& range); - -private: - Pokemon::Outcome reload(); - - StoryConsole m_console; - std::set m_saved; - std::map m_failures; -}; - -// Builds the table with a console runner and runs the checkpoints from -// `range.from` to `range.to` (the whole segment when both are unset). -void run_story_table( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, - const AutoStoryOptions& options, AutoStoryStats& stats, - const StorySegmentFactory& make_segment, const Pokemon::SegmentRange& range -); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp deleted file mode 100644 index a8ba839234..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp +++ /dev/null @@ -1,271 +0,0 @@ -/* BDSP AutoStory - Segment 01 - * From after choosing the starter Pokemon until after acquiring the first badge. - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include - -#include "CommonFramework/Exceptions/OperationFailedException.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/ImageTools/ImageDiff.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" - -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" -#include "CommonTools/VisualDetectors/ImageMatchDetector.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" - -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_EndBattleDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" -#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" - -#include "PokemonBDSP_AutoStory_Segment_00_tools.h" - -#include "PokemonBDSP_Autostory_Segment_01_tools.h" - -using namespace std::chrono_literals; - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { - -// Extracted from the Segment 01 Oreburgh Gate approach. Both callers use -// the same bounded down/right search and cancel queued input at the black fade. -// Oreburgh preserves its original north sweep; Coronet uses only down/right. -void enter_cave_with_down_right_search(VideoStream& stream, - ProControllerContext& context, - const std::string& label, - Milliseconds north_fallback){ - context.wait_for_all_requests(); - for (size_t attempt = 0; attempt < 3; attempt++){ - BlackScreenWatcher black_screen; - const int result = run_until(stream, - context, - [north_fallback](ProControllerContext& ctx){ - for (size_t i = 0; i < 10; i++){ - pbf_press_button(ctx, BUTTON_DOWN, 180ms, 0ms); - pbf_press_button(ctx, BUTTON_RIGHT, 180ms, 0ms); - pbf_press_button(ctx, BUTTON_RIGHT, 180ms, 0ms); - } - if (north_fallback > 0ms){ - pbf_move_left_joystick(ctx, {0, 1}, north_fallback, 0ms); - } - }, - {{black_screen}}); - context.wait_for_all_requests(); - if (result == 0){ - stream.log(label + ": Black screen detected."); - return; - } - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": No black screen after 3 attempts.", - stream); -} - -static bool mine_confirm_escape_via_menu(VideoStream& stream, ProControllerContext& context, BattleType battle_type){ - MenuWatcher game_menu; - OverworldWatcher overworld; - BattleMenuWatcher battle_menu(battle_type, COLOR_YELLOW); - for (size_t attempt = 1; attempt <= 3; attempt++){ - // Do not accept the absence-based overworld detector before menu proof. - const int state = wait_until(stream, context, 300ms, {{game_menu}, {battle_menu}}); - if (state == 1){ - stream.log("[Mine escape] Battle menu still present; retrying Run."); - return false; - } - int detected = state; - if (state != 0){ - stream.log("[Mine escape] Main menu fallback attempt " + std::to_string(attempt) + "/3."); - detected = run_until(stream, - context, - [](ProControllerContext& controller){ - pbf_press_button(controller, BUTTON_X, 160ms, 840ms); - pbf_wait(controller, 3s); - controller.wait_for_all_requests(); - }, - {game_menu, battle_menu}); - if (detected == 1){ - return false; - } - } - if (detected != 0){ - continue; - } - stream.log("[Mine escape] Main game menu confirmed; battle has ended.", COLOR_GREEN); - // Close the confirmed menu before returning control to navigation. - MenuDetector menu_visible; - for (size_t close = 0; close < 3; close++){ - pbf_press_button(context, BUTTON_B, 80ms, 500ms); - context.wait_for_all_requests(); - if (wait_until(stream, context, 1s, {{overworld}}) == 0){ - return true; - } - BlackScreenWatcher visible(COLOR_YELLOW, - {0.1, 0.1, 0.8, 0.8}, - 100, - 10, - BlackScreenWatcher::FinderType::GONE, - 500ms); - if (wait_until(stream, context, 2s, {{visible}}) == 0){ - const VideoSnapshot frame = stream.video().snapshot(); - if (frame && !menu_visible.detect(frame)){ - stream.log("[Mine escape] Confirmed game menu closed; returning to navigation.", COLOR_GREEN); - return true; - } - } - } - stream.log("[Mine escape] Main menu detected but could not confirm it closed.", COLOR_ORANGE); - return false; - } - return false; -} - -static bool mine_select_run(VideoStream& stream, ProControllerContext& context, BattleType battle_type){ - BattleMenuDetector cursor(battle_type, COLOR_YELLOW); - size_t matching_frames = 0; - for (size_t check = 0; check < 16; check++){ - context.wait_for_all_requests(); - const VideoSnapshot frame = stream.video().snapshot(); - if (!frame){ - return false; - } - const BattleMenuOption selected = battle_menu_selected_option(cursor, frame); - stream.log( - "[Mine escape] Battle cursor row=" + std::to_string(static_cast(selected) + 1) + "; target Run=4."); - if (selected == BattleMenuOption::RUN){ - if (++matching_frames >= 3){ - stream.log("[Mine escape] Run cursor confirmed.", COLOR_GREEN); - return true; - } - pbf_wait(context, 100ms); - }else{ - matching_frames = 0; - if (selected == BattleMenuOption::UNKNOWN){ - // Do not send a direction or A without a known cursor. - pbf_wait(context, 150ms); - }else{ - const DpadPosition direction = selected == BattleMenuOption::BATTLE ? DPAD_UP : DPAD_DOWN; - pbf_press_dpad(context, direction, 80ms, 250ms); - } - } - } - stream.log("[Mine escape] Could not confirm Run selection; A was not pressed.", COLOR_ORANGE); - return false; -} - -void escape_wild_battle(VideoStream& stream, - ProControllerContext& context, - BattleType battle_type, - std::chrono::seconds timeout){ - EndBattleWatcher battle_end; - bool exit_transition = false; - const auto deadline = std::chrono::steady_clock::now() + timeout; - while (std::chrono::steady_clock::now() < deadline){ - pbf_press_button(context, BUTTON_B, 40ms, 200ms); - context.wait_for_all_requests(); - BattleMenuWatcher menu_ready(battle_type, COLOR_YELLOW); - const int ready = wait_until(stream, context, 3s, {{battle_end}, {menu_ready}}); - if (ready == 0){ - exit_transition = true; - break; - } - if (ready != 1){ - continue; - } - if (!mine_select_run(stream, context, battle_type)){ - continue; - } - stream.log("[Mine escape] Run input sent; watching for black-screen exit."); - const int result = run_until(stream, - context, - [](ProControllerContext& controller){ - pbf_press_button(controller, BUTTON_A, 60ms, 0ms); - pbf_wait(controller, 4s); - controller.wait_for_all_requests(); - }, - {battle_end}); - if (result == 0){ - exit_transition = true; - break; - } - } - if (!exit_transition && wait_until(stream, context, 10s, {{battle_end}}) == 0){ - exit_transition = true; - } - if (exit_transition){ - // EndBattleWatcher requires black followed by a visible screen. - stream.log("[Mine escape] Black-screen exit transition confirmed.", COLOR_GREEN); - pbf_wait(context, 800ms); - context.wait_for_all_requests(); - return; - } - // Only probe X after normal escape attempts and the final transition wait. - // Absence of battle UI by itself is not evidence of an overworld return. - stream.log("[Mine escape] Exit transition missed; trying late main-menu fallback."); - if (mine_confirm_escape_via_menu(stream, context, battle_type)){ - return; - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "escape_wild_battle: Exit transition and late main-menu fallback could not confirm escape.", - stream); -} - -void run_from_wild_battle(VideoStream& stream, ProControllerContext& context){ - escape_wild_battle(stream, context, BattleType::STANDARD); -} - -void Barry_1_fight(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context){ - stream.log("Starting Barry 1 battle..."); - - // State machine: loop until EndBattleWatcher fires - for (size_t c = 0; c < 20; c++){ - context.wait_for_all_requests(); - - BattleMenuWatcher battle_menu(BattleType::STANDARD); - EndBattleWatcher end_battle; - int ret = run_until(stream, - context, - [](ProControllerContext& context){ - pbf_mash_button(context, BUTTON_ZL, 120000ms); - }, - { - battle_menu, - end_battle, - }); - switch (ret){ - case 0: - stream.log("Battle menu detected - using first move."); - pbf_press_button(context, BUTTON_ZL, 160ms, 1000ms); - break; - case 1: - stream.log("Barry 1 battle complete."); - pbf_mash_button(context, BUTTON_B, 5000ms); - context.wait_for_all_requests(); - return; - default: - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Barry_1_fight: Timed out waiting for battle menu or end.", - stream); - } - } - - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Barry_1_fight: Battle did not end after 20 turns.", - stream); -} - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h deleted file mode 100644 index 80fad9bb8c..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h +++ /dev/null @@ -1,33 +0,0 @@ -// Shared battle helpers used by AutoStory routes. -#pragma once - -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" -#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { -// Reuse the Oreburgh Gate down/right entry search. Returns at fade start; -// caller owns destination-specific fade completion and interior confirmation. -// north_fallback preserves Oreburgh's final north sweep when explicitly set. -void enter_cave_with_down_right_search(VideoStream& stream, - ProControllerContext& context, - const std::string& label, - Milliseconds north_fallback = Milliseconds(0)); - -// Shared confirmed-Run escape: verifies exit transition or main menu. -void escape_wild_battle(VideoStream& stream, - ProControllerContext& context, - BattleType battle_type = BattleType::STANDARD, - std::chrono::seconds timeout = std::chrono::seconds(30)); - -// escape_wild_battle() for a standard wild battle, as the walker takes it -// (WildBattleEscape): how the story's walks leave the battles that -// interrupt them. -void run_from_wild_battle(VideoStream& stream, ProControllerContext& context); - -// Shared legacy first-move battle routine; consolidation remains preliminary. -void Barry_1_fight(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context); -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp deleted file mode 100644 index 6b7c2b8182..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp +++ /dev/null @@ -1,3697 +0,0 @@ - - -#include -#include -#include - -#include - -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/Globals.h" - -#include "PokemonBDSP_OverworldMatchers.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { -enum class LandmarkFeatures { - GRAYSCALE, - LOCAL_CONTRAST, - BLUE_FRACTION -}; -struct LandmarkConfig { - const char* name; - const char* path; - ImageFloatBox target, search; - double threshold, margin; - LandmarkFeatures features = LandmarkFeatures::GRAYSCALE; -}; -// Screen-relative half-widths of the accepted position box, and how closely -// two landmarks (or reference variants) must agree to be trusted together. -static constexpr double POSITION_TOLERANCE_X = 1.0 / 50; -static constexpr double POSITION_TOLERANCE_Y = 1.0 / 54; -static constexpr double AGREEMENT_X = 1.0 / 64; -static constexpr double AGREEMENT_Y = 1.0 / 54; -static cv::Mat gray_image(const ImageRGB32& image){ - cv::Mat pixels((int)image.height(), - (int)image.width(), - CV_8UC4, - const_cast(image.data()), - image.bytes_per_row()); - cv::Mat gray; - cv::cvtColor(pixels, gray, cv::COLOR_BGRA2GRAY); - return gray; -} -// Opt-in for selected outdoor structural anchors. Divide out slow local lighting -// changes so a moving cloud does not alter the printed sign/emblem structure. -static cv::Mat normalize_landmark_illumination(const cv::Mat& gray){ - cv::Mat pixels, background, normalized; - gray.convertTo(pixels, CV_32F); - cv::GaussianBlur(pixels, background, cv::Size(0, 0), 3.0); - cv::divide(pixels, background + 1.0, normalized); - cv::Mat structure; - normalized.convertTo(structure, CV_8U, 128.0); - return structure; -} -// White Pokeball versus yellow fascia is a color-ratio boundary. A cast -// shadow changes their brightness, so compare blue fraction rather than luma. -static cv::Mat landmark_blue_fraction(const ImageRGB32& image){ - cv::Mat pixels((int)image.height(), - (int)image.width(), - CV_8UC4, - const_cast(image.data()), - image.bytes_per_row()); - cv::Mat small, floating; - cv::resize(pixels, small, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - small.convertTo(floating, CV_32F); - std::vector channels; - cv::split(floating, channels); - cv::Mat ratio, structure; - cv::divide(channels[0], channels[0] + channels[1] + channels[2] + 3.0, ratio); - ratio.convertTo(structure, CV_8U, 768.0); - return structure; -} -static OverworldMatchResult match_landmarks(const ImageRGB32& frame, - const std::vector& configs, - size_t required, - double tolerance_x, - double tolerance_y, - bool smooth_resize = false){ - OverworldMatchResult result; - if (!frame){ - return result; - } - cv::Mat gray; - if (smooth_resize){ - // Center matching uses explicit area averaging for stable emblem edges. - // Other matchers retain their existing ImageRGB32 resize behavior. - cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - }else{ - ImageRGB32 scaled = frame.scale_to(960, 540); - gray = gray_image(scaled); - } - cv::Mat normalized_gray, chromaticity; - for (const auto& config : configs){ - OverworldLandmarkResult landmark; - landmark.name = config.name; - landmark.search = config.search; - ImageRGB32 reference(RESOURCE_PATH() + config.path); - // Convert normalized geometry to pixels only at the image-processing boundary. - const int template_width = std::max(1, (int)std::lround(config.target.width * gray.cols)); - const int template_height = std::max(1, (int)std::lround(config.target.height * gray.rows)); - cv::Mat templ; - if (smooth_resize){ - cv::resize(gray_image(reference), templ, cv::Size(template_width, template_height), 0, 0, cv::INTER_AREA); - }else{ - ImageRGB32 small = reference.scale_to(template_width, template_height); - templ = gray_image(small); - } - cv::Rect search((int)(config.search.x * 960), - (int)(config.search.y * 540), - (int)(config.search.width * 960), - (int)(config.search.height * 540)); - search &= cv::Rect(0, 0, 960, 540); - if (search.width < templ.cols || search.height < templ.rows){ - result.landmarks.push_back(landmark); - continue; - } - cv::Mat scores; - if (config.features == LandmarkFeatures::BLUE_FRACTION){ - if (chromaticity.empty()){ - chromaticity = landmark_blue_fraction(frame); - } - cv::matchTemplate(chromaticity(search), templ, scores, cv::TM_CCOEFF_NORMED); - }else if (config.features == LandmarkFeatures::LOCAL_CONTRAST){ - if (normalized_gray.empty()){ - normalized_gray = normalize_landmark_illumination(gray); - } - // These opt-in references are normalized from their full source - // frame before cropping, avoiding false edges at template borders. - cv::matchTemplate(normalized_gray(search), templ, scores, cv::TM_CCOEFF_NORMED); - }else{ - cv::matchTemplate(gray(search), templ, scores, cv::TM_CCOEFF_NORMED); - } - cv::Point location; - cv::minMaxLoc(scores, nullptr, &landmark.score, nullptr, &location); - cv::Mat competitors = scores.clone(); - const int peak_radius_x = (int)std::lround(0.015625 * gray.cols); - const int peak_radius_y = (int)std::lround((1.0 / 36) * gray.rows); - cv::Rect exclusion = cv::Rect(location.x - peak_radius_x, - location.y - peak_radius_y, - 2 * peak_radius_x + 1, - 2 * peak_radius_y + 1) & - cv::Rect(0, 0, scores.cols, scores.rows); - competitors(exclusion).setTo(-1); - double runner_up; - cv::minMaxLoc(competitors, nullptr, &runner_up); - landmark.margin = landmark.score - runner_up; - location += search.tl(); - landmark.dx = (double)location.x / gray.cols - config.target.x; - landmark.dy = (double)location.y / gray.rows - config.target.y; - landmark.accepted = landmark.score >= config.threshold && landmark.margin >= config.margin; - landmark.found = {(double)location.x / 960, - (double)location.y / 540, - (double)templ.cols / 960, - (double)templ.rows / 540}; - result.landmarks.push_back(landmark); - } - if (required == 1 && result.landmarks.size() == 1){ - result.located = result.landmarks[0].accepted; - result.dx = result.landmarks[0].dx; - result.dy = result.landmarks[0].dy; - }else{ - size_t best_support = 0; - bool ambiguous = false; - for (const auto& candidate : result.landmarks){ - if (!candidate.accepted){ - continue; - } - size_t support = 0; - double dx = 0, dy = 0; - for (const auto& other : result.landmarks){ - if (other.accepted && std::abs(candidate.dx - other.dx) <= AGREEMENT_X && - std::abs(candidate.dy - other.dy) <= AGREEMENT_Y){ - support++; - dx += other.dx; - dy += other.dy; - } - } - if (support < required){ - continue; - } - dx /= support; - dy /= support; - if (support > best_support){ - best_support = support; - result.dx = dx; - result.dy = dy; - ambiguous = false; - }else if (support == best_support && - (std::abs(dx - result.dx) > AGREEMENT_X || std::abs(dy - result.dy) > AGREEMENT_Y)){ - ambiguous = true; - } - } - result.located = best_support >= required && !ambiguous; - } - result.at_target = result.located && std::abs(result.dx) <= tolerance_x && std::abs(result.dy) <= tolerance_y; - return result; -} -OverworldMatchResult match_poketch_landmarks(const ImageRGB32& frame){ - // Same cropped reference emblems and local search windows as production. - OverworldMatchResult fine = match_landmarks(frame, - {{"Mart", - "PokemonBDSP/Autostory/PoketchInteraction/Mart.png", - {0.768750000000, 0.044444444444, 0.079166666667, 0.088888888889}, - {598.0 / 960, 0, 356.0 / 960, 212.0 / 540}, - .90, - 0}, - {"Center", - "PokemonBDSP/Autostory/PoketchInteraction/Center.png", - {0.900000000000, 0.748148148148, 0.075000000000, 0.137037037037}, - {724.0 / 960, 264.0 / 540, 236.0 / 960, 276.0 / 540}, - .90, - 0}, - {"School", - "PokemonBDSP/Autostory/PoketchInteraction/School.png", - {0.006250000000, 0.755555555556, 0.097916666667, 0.133333333333}, - {0, 268.0 / 540, 240.0 / 960, 272.0 / 540}, - .90, - 0}}, - 2, - 7.0 / 960, - 7.0 / 540); - if (fine.located){ - return fine; - } - // Coarse acquisition searches the whole frame. Agreement and a peak margin - // guard against confusing the Mart/Center emblems with another building. - OverworldMatchResult coarse = match_landmarks(frame, - {{"Mart", - "PokemonBDSP/Autostory/PoketchInteraction/Mart.png", - {0.768750000000, 0.044444444444, 0.079166666667, 0.088888888889}, - {0, 0, 1, 1}, - .93, - .12}, - {"Center", - "PokemonBDSP/Autostory/PoketchInteraction/Center.png", - {0.900000000000, 0.748148148148, 0.075000000000, 0.137037037037}, - {0, 0, 1, 1}, - .93, - .12}, - {"School", - "PokemonBDSP/Autostory/PoketchInteraction/School.png", - {0.006250000000, 0.755555555556, 0.097916666667, 0.133333333333}, - {0, 0, 1, 1}, - .92, - .40}}, - 2, - 7.0 / 960, - 7.0 / 540); - if (!coarse.located){ - // Acquisition may expose just one building. School keeps its strong - // spatial margin; Mart/Center require a higher score than pair matching. - // A single anchor guides coarse movement only, never interaction. - const OverworldLandmarkResult* anchor = nullptr; - for (const auto& landmark : coarse.landmarks){ - const bool strong = - landmark.accepted && (landmark.name == "School" || (landmark.score >= .95 && landmark.margin >= .12)); - if (strong && (!anchor || landmark.score > anchor->score)){ - anchor = &landmark; - } - } - if (anchor){ - bool contradictory = false; - for (const auto& other : coarse.landmarks){ - if (other.accepted && - (std::abs(other.dx - anchor->dx) > 1.0 / 192 || std::abs(other.dy - anchor->dy) > 1.0 / 108)){ - contradictory = true; - } - } - if (!contradictory){ - coarse.located = true; - coarse.dx = anchor->dx; - coarse.dy = anchor->dy; - } - } - } - coarse.coarse = true; - // A coarse match may guide movement, but never authorize interaction. - // Final position must pass the normal local two-landmark confirmation. - coarse.at_target = false; - return coarse; -} -OverworldMatchResult match_oreburgh_mart(const ImageRGB32& frame){ - const ImageFloatBox target{0.502083333333, 0.235185185185, 0.056250000000, 0.079629629630}; - auto original = match_landmarks(frame, - {{"Mart emblem", - "PokemonBDSP/Autostory/OreburghMart/MartEmblem.png", - target, - {0, 0, 1, 360.0 / 540}, - .93, - .10}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y); - // The recorded daytime roof appearance narrowly missed the old template. - // Preserve both confidence gates and reject disagreement between variants. - auto recorded = match_landmarks(frame, - {{"Mart emblem", - "PokemonBDSP/Autostory/OreburghMart/MartEmblemRecorded.png", - target, - {0, 0, 1, 360.0 / 540}, - .93, - .10}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y); - if (original.located && recorded.located && - (std::abs(original.dx - recorded.dx) > AGREEMENT_X || std::abs(original.dy - recorded.dy) > AGREEMENT_Y)){ - original.located = false; - original.at_target = false; - return original; - } - if ((recorded.located && !original.located) || - (recorded.located == original.located && recorded.landmarks[0].score > original.landmarks[0].score)){ - return recorded; - } - return original; -} -OverworldMatchResult match_jubilife_center_entrance(const ImageRGB32& frame){ - // Native lighting-test captures avoid resizing artifacts in the reference. - // The emblem is about 5.73% of screen width left of the player at approach: - // shift its target right to center the door, preserving the approach height. - OverworldMatchResult clear = match_landmarks(frame, - {{"Center entrance", - "PokemonBDSP/Autostory/JubilifeCenter/CenterEmblem.png", - {0.462500000000, 0.109259259259, 0.072916666667, 0.111111111111}, - {0, 0, 1, 1}, - .90, - .20}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - OverworldMatchResult shadow = match_landmarks(frame, - {{"Center entrance", - "PokemonBDSP/Autostory/JubilifeCenter/CenterEmblemShadow.png", - {0.462500000000, 0.109259259259, 0.072916666667, 0.111111111111}, - {0, 0, 1, 1}, - .90, - .20}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - // Lighting alternatives must identify the same position when both pass. - if (clear.located && shadow.located && - (std::abs(clear.dx - shadow.dx) > AGREEMENT_X || std::abs(clear.dy - shadow.dy) > AGREEMENT_Y)){ - clear.located = false; - clear.at_target = false; - return clear; - } - if (shadow.located && (!clear.located || shadow.landmarks[0].score > clear.landmarks[0].score)){ - return shadow; - } - return clear; -} -OverworldMatchResult match_route201_potion_exit(const ImageRGB32& frame){ - // The sign face is stable across lighting; its post and surrounding grass are not. - // Path shadows change shape, so validate the corner against four lighting states. - // Each alternative still requires the sign and path to agree on position. - const char* corners[] = {"PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour0.png", - "PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour9.png", - "PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour12.png", - "PokemonBDSP/Autostory/Route201PotionExit/PathCornerHour17.png"}; - OverworldMatchResult best; - double best_score = -1; - for (const char* corner : corners){ - auto candidate = match_landmarks(frame, - {{"Route sign face", - "PokemonBDSP/Autostory/Route201PotionExit/RouteSignFace.png", - {0.292708333333, 0.470370370370, 0.109375, 0.072222222222}, - {0, 0, 1, 1}, - .94, - .20}, - {"Path corner", - corner, - {0.202083333333, 0.648148148148, 0.133333333333, 0.174074074074}, - {0, 0, 1, 1}, - .90, - .10}}, - 2, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if (best.located && candidate.located && - (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ - best.located = false; - best.at_target = false; - return best; - } - if (candidate.landmarks.size() != 2){ - continue; - } - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - return best; -} -OverworldMatchResult match_sandgem_junction(const ImageRGB32& frame){ - // Building emblems contain no localized words. Ignore NPCs and door lights. - auto result = match_landmarks(frame, - {{"Center emblem", - "PokemonBDSP/Autostory/SandgemJunction/Center.png", - {0.171875, 0.2, 0.058333333333, 0.092592592593}, - {0, 0, 1, 1}, - .90, - .08}, - {"Mart emblem", - "PokemonBDSP/Autostory/SandgemJunction/Mart.png", - {0.726041666667, 0.222222222222, 0.0625, 0.088888888889}, - {0, 0, 1, 1}, - .90, - .08}}, - 2, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if (result.located || result.landmarks.size() != 2){ - return result; - } - const auto& center = result.landmarks[0]; - const auto& mart = result.landmarks[1]; - const double dy = (center.dy + mart.dy) / 2; - const double horizontal_disagreement = center.dx - mart.dx; - // At a different approach height, perspective changes the buildings' spacing. - // Trust matching height offsets for vertical acquisition only. Bound the - // horizontal discrepancy and require its direction to agree with perspective: - // scenery above target contracts, scenery below target expands. - if (center.accepted && mart.accepted && std::abs(center.dy - mart.dy) <= AGREEMENT_Y && std::abs(dy) > 1.0 / 72 && - std::abs(dy) <= 0.20 && std::abs(horizontal_disagreement) <= 0.04 && - std::abs(horizontal_disagreement) <= 0.35 * std::abs(dy) + 1.0 / 192 && horizontal_disagreement * dy <= 0){ - result.located = true; - result.coarse = true; - result.dx = (center.dx + mart.dx) / 2; - result.dy = dy; - // Acquisition may move vertically; it never confirms the route endpoint. - result.at_target = false; - } - return result; -} -OverworldMatchResult match_route202_grass_exit(const ImageRGB32& frame){ - // Route marker and upper path bend avoid the player, NPCs, and localized text. - // Morning tree shadows need a second corner reference. Each alternative - // must still agree with the sign; neither can confirm the destination alone. - const char* corners[] = {"PokemonBDSP/Autostory/Route202GrassExit/PathCorner.png", - "PokemonBDSP/Autostory/Route202GrassExit/PathCornerMorning.png"}; - OverworldMatchResult best; - double best_score = -1; - for (const char* corner : corners){ - auto candidate = match_landmarks(frame, - {{"Route 202 sign", - "PokemonBDSP/Autostory/Route202GrassExit/RouteSign.png", - {0.59375, 0.194444444444, 0.091666666667, 0.059259259259}, - {0, 0, 1, 1}, - .94, - .20}, - {"Path corner", - corner, - {0.405208333333, 0.331481481481, 0.1375, 0.062962962963}, - {0, 0, 1, 1}, - .90, - .08}}, - 2, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if (best.located && candidate.located && - (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ - best.located = false; - best.at_target = false; - return best; - } - if (candidate.landmarks.size() != 2){ - continue; - } - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - return best; -} -// Lighting alternatives describe one physical landmark, not extra votes. -// Select one reference per landmark before requiring the two positions to agree. -static OverworldMatchResult match_lighting_pair(const ImageRGB32& frame, - const std::vector& variants, - const char* first, - const char* second, - double tolerance_x = POSITION_TOLERANCE_X, - bool allow_vertical_acquisition = false, - double agreement_x = AGREEMENT_X){ - auto candidates = - match_landmarks(frame, variants, variants.size() + 1, POSITION_TOLERANCE_X, POSITION_TOLERANCE_Y, true); - OverworldMatchResult result; - for (const char* name : {first, second}){ - const OverworldLandmarkResult* best = nullptr; - for (const auto& candidate : candidates.landmarks){ - if (candidate.name != name){ - continue; - } - if (!best || (candidate.accepted && !best->accepted) || - (candidate.accepted == best->accepted && candidate.score > best->score)){ - best = &candidate; - } - } - if (!best){ - return result; - } - result.landmarks.push_back(*best); - auto& chosen = result.landmarks.back(); - // Similar strong references must not disagree about location. - for (const auto& candidate : candidates.landmarks){ - if (candidate.name == name && candidate.accepted && candidate.score >= chosen.score - 0.02 && - (std::abs(candidate.dx - chosen.dx) > AGREEMENT_X || - std::abs(candidate.dy - chosen.dy) > AGREEMENT_Y)){ - chosen.accepted = false; - } - } - } - const auto& a = result.landmarks[0]; - const auto& b = result.landmarks[1]; - result.located = - a.accepted && b.accepted && std::abs(a.dx - b.dx) <= agreement_x && std::abs(a.dy - b.dy) <= AGREEMENT_Y; - result.dx = (a.dx + b.dx) / 2; - result.dy = (a.dy + b.dy) / 2; - result.at_target = result.located && std::abs(a.dx - b.dx) <= AGREEMENT_X && std::abs(result.dx) <= tolerance_x && - std::abs(result.dy) <= POSITION_TOLERANCE_Y; - const double spacing_difference = a.dx - b.dx; - // At the street approach height, perspective can change column spacing. - // Agreeing vertical offsets may guide an upward correction, but cannot - // confirm the destination or authorize a horizontal correction yet. - if (!result.located && allow_vertical_acquisition && a.accepted && b.accepted && - std::abs(a.dy - b.dy) <= AGREEMENT_Y && std::abs(result.dy) > POSITION_TOLERANCE_Y && - std::abs(result.dy) <= 0.20 && std::abs(spacing_difference) <= 0.04 && - std::abs(spacing_difference) <= 0.35 * std::abs(result.dy) + 1.0 / 192 && spacing_difference * result.dy <= 0){ - result.located = true; - result.coarse = true; - result.at_target = false; - } - return result; -} -OverworldMatchResult match_eterna_center_entrance(const ImageRGB32& frame){ - // Native screenshot-20261016-214300767696 is the intended doorway pose. - // The Pokeball is primary. Sign and bush assist when visible, but cloud - // shadows must not prevent arrival from a strong, unique emblem match. - OverworldMatchResult best; - double best_score = -1; - bool best_primary_only = false; - // Recorded evening/daytime appearances retain the same destination and gates. - for (int appearance = 0; appearance < 5; appearance++){ - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - std::vector configs = {{"Center Pokeball", - "PokemonBDSP/Autostory/EternaCenter/BallTarget.png", - {904.0 / 1920, 144.0 / 1080, 112.0 / 1920, 104.0 / 1080}, - {0, 0, 1, .65}, - .93, - .20}, - {"Right sign", - "PokemonBDSP/Autostory/EternaCenter/RightSignTarget.png", - {1762.0 / 1920, 304.0 / 1080, 128.0 / 1920, 138.0 / 1080}, - {0, 0, 1, .85}, - .93, - .20}, - {"Lower left bush", - "PokemonBDSP/Autostory/EternaCenter/BushTarget.png", - {248.0 / 1920, 854.0 / 1080, 180.0 / 1920, 146.0 / 1080}, - {0, .35, 1, .65}, - .93, - .20}}; - if (appearance == 1 || appearance == 2){ - configs = {{"Center Pokeball", - appearance == 1 ? "PokemonBDSP/Autostory/EternaCenter/BallEvening1.png" - : "PokemonBDSP/Autostory/EternaCenter/BallEvening2.png", - {920.0 / 1920, 154.0 / 1080, 82.0 / 1920, 84.0 / 1080}, - {0, 0, 1, .65}, - .93, - .20}, - {"Right sign", - appearance == 1 ? "PokemonBDSP/Autostory/EternaCenter/RightSignEvening1.png" - : "PokemonBDSP/Autostory/EternaCenter/RightSignEvening2.png", - {1770.0 / 1920, 310.0 / 1080, 116.0 / 1920, 63.0 / 1080}, - {0, 0, 1, .85}, - .93, - .20}, - {"Lower left bush", - appearance == 1 ? "PokemonBDSP/Autostory/EternaCenter/BushEvening1.png" - : "PokemonBDSP/Autostory/EternaCenter/BushEvening2.png", - {248.0 / 1920, 932.0 / 1080, 180.0 / 1920, 68.0 / 1080}, - {0, .35, 1, .65}, - .93, - .20}}; - } - if (appearance >= 3){ - // Recorded daylight changes the bush/planter contrast. These - // crops retain the original doorway geometry and all three votes. - configs[0].path = appearance == 3 ? "PokemonBDSP/Autostory/EternaCenter/BallDaytime1.png" - : "PokemonBDSP/Autostory/EternaCenter/BallDaytime2.png"; - configs[1].path = appearance == 3 ? "PokemonBDSP/Autostory/EternaCenter/RightSignDaytime1.png" - : "PokemonBDSP/Autostory/EternaCenter/RightSignDaytime2.png"; - configs[2].path = appearance == 3 ? "PokemonBDSP/Autostory/EternaCenter/BushDaytime1.png" - : "PokemonBDSP/Autostory/EternaCenter/BushDaytime2.png"; - } - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (candidate.located){ - double score = 1; - for (const auto& landmark : candidate.landmarks){ - score = std::min(score, landmark.score); - } - if (!best.located || best_primary_only || score > best_score){ - best = candidate; - best_score = score; - best_primary_only = false; - } - }else if (!best.located || best_primary_only){ - // Optional sign/bush can disappear under clouds or at frame edges. - // Retain the existing stricter primary-emblem confidence gate. - const auto& ball = candidate.landmarks[0]; - if (ball.accepted && ball.score >= .95 && (!best.located || ball.score > best_score)){ - best = candidate; - best.located = true; - best.coarse = false; - best.dx = ball.dx; - best.dy = ball.dy; - best.at_target = std::abs(best.dx) <= 1.0 / 128 && std::abs(best.dy) <= POSITION_TOLERANCE_Y; - best_score = ball.score; - best_primary_only = true; - }else if (best.landmarks.empty()){ - best = candidate; - } - } - } - } - return best; -} -OverworldMatchResult match_route208_stair_top(const ImageRGB32& frame){ - // Native screenshot 20261022-213928168268: centered above the east stairs. - // The isolated upper rock and lower rock pair must both agree before descent. - OverworldMatchResult best; - double best_score = -1; - // Stationary hourly captures supply lighting appearances, not new goals. - // Both physical rock groups still need the same confidence and agreement. - const char* references[][2] = { - {"PokemonBDSP/Autostory/Route208Stairs/UpperRock.png", "PokemonBDSP/Autostory/Route208Stairs/LowerRocks.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Hour0Frame1Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Hour0Frame1Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Hour4Frame1Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Hour4Frame1Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Hour17Frame1Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Hour17Frame1Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Hour11Frame3Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Hour11Frame3Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Hour15Frame1Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Hour15Frame1Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Hour11Frame5Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Hour11Frame5Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Hour15Frame3Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Hour15Frame3Lower.png"}, - }; - for (const auto& reference : references){ - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - std::vector configs = {{"Stair-top upper rock", - reference[0], - {170.0 / 1920, 350.0 / 1080, 120.0 / 1920, 120.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20}, - {"Stair-top lower rock pair", - reference[1], - {240.0 / 1920, 425.0 / 1080, 130.0 / 1920, 180.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20}}; - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - double score = 1; - for (const auto& landmark : candidate.landmarks){ - score = std::min(score, landmark.score); - } - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - } - if (best.located){ - return best; - } - const char* approaches[][2] = { - {"PokemonBDSP/Autostory/Route208Stairs/Approach0507Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Approach0507Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Approach0538Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Approach0538Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/Approach0549Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/Approach0549Lower.png"}, - // The eastern cliff pose changes rock perspective and the local shadow - // pattern. These real-run appearances guide acquisition only: retain - // .92 acquisition NCC/.20 uniqueness and two-rock offset agreement. - // Arrival still requires the original .93 strict reference bank. - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive01Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive01Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive02Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive02Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive03Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive03Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive04Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive04Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive05Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive05Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive06Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive06Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive07Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive07Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive08Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive08Lower.png"}, - {"PokemonBDSP/Autostory/Route208Stairs/ApproachLive09Upper.png", - "PokemonBDSP/Autostory/Route208Stairs/ApproachLive09Lower.png"}, - }; - for (const auto& reference : approaches){ - for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ - std::vector configs = {{"Stair-top approach upper rock", - reference[0], - {170.0 / 1920, 350.0 / 1080, 120.0 / 1920, 120.0 / 1080}, - {0, 0, .50, .85}, - .92, - .20}, - {"Stair-top approach lower rock pair", - reference[1], - {240.0 / 1920, 425.0 / 1080, 130.0 / 1920, 180.0 / 1080}, - {0, 0, .50, .85}, - .92, - .20}}; - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - candidate.coarse = true; - candidate.at_target = false; - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if (candidate.located && (!best.located || score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - } - return best; -} -OverworldMatchResult match_route208_lower_stair_lane(const ImageRGB32& frame){ - // Successful 1825ms descent endpoint at video 230s (2026-10-24 15-38-06). - // Independent cliff/tree anchors must agree before movement is authorized. - OverworldMatchResult best; - double best_score = -1; - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - std::vector configs = {{"Lower stair cliff corner", - "PokemonBDSP/Autostory/Route208LowerLane/CliffCorner.png", - {690.0 / 1920, 540.0 / 1080, 110.0 / 1920, 150.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20}, - {"Honey tree", - "PokemonBDSP/Autostory/Route208LowerLane/HoneyTree.png", - {1510.0 / 1920, 600.0 / 1080, 100.0 / 1920, 170.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20}}; - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (candidate.landmarks.size() != 2){ - continue; - } - const auto& cliff = candidate.landmarks[0]; - const auto& tree = candidate.landmarks[1]; - // Perspective changes anchor spacing farther down the field. Two strong - // anchors can authorize north-only recovery while bracketing the player. - // This cue never confirms arrival or authorizes east travel. - if (!candidate.located && cliff.accepted && tree.accepted && cliff.dy < -1.0 / 24 && tree.dy < -1.0 / 24 && - std::abs(cliff.dy - tree.dy) <= AGREEMENT_Y && cliff.found.x + cliff.found.width < .5 && - tree.found.x > .65){ - candidate.located = true; - candidate.coarse = true; - candidate.at_target = false; - candidate.dx = 0; - candidate.dy = (cliff.dy + tree.dy) / 2; - } - double score = 1; - for (const auto& landmark : candidate.landmarks){ - score = std::min(score, landmark.score); - } - if ((candidate.located && !best.located) || - (candidate.located && best.located && best.coarse && !candidate.coarse) || - (candidate.located == best.located && candidate.coarse == best.coarse && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - return best; -} -OverworldMatchResult match_route209_repel_point(const ImageRGB32& frame){ - // butters' marked sign frame and rock pair; the cloud-sensitive pillar is excluded. - // Only the sign's left frame/post is used; lettering is excluded. - OverworldMatchResult best; - double best_score = -1; - // Lighting samples are translated back to the original marked target. - // Appearance 1 remains acquisition-only; all others describe that target. - static const std::array appearances[] = { - {"PokemonBDSP/Autostory/Route209Repel/SignFrame.png", "PokemonBDSP/Autostory/Route209Repel/RockPair.png"}, - {"PokemonBDSP/Autostory/Route209Repel/EastSignFrame.png", - "PokemonBDSP/Autostory/Route209Repel/EastRockPair.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H04F1.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H04F1.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H18F4.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H18F4.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H12F1.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H12F1.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H02F3.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H02F3.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F4.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F4.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H12F4.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H12F4.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H12F5.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H12F5.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H02F5.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H02F5.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F2.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F2.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F3.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F3.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F4.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F4.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F5.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F5.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F1.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F1.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F2.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F2.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F3.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F3.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H02F4.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H02F4.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H03F5.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H03F5.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H10F1.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H10F1.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H11F5.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H11F5.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H13F5.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H13F5.png"}, - {"PokemonBDSP/Autostory/Route209Repel/SignFrame-H14F5.png", - "PokemonBDSP/Autostory/Route209Repel/RockPair-H14F5.png"}, - }; - for (double scale : {1.0, .95, 1.05, .90, 1.10}){ - std::vector lighting_landmarks(2); - for (size_t appearance = 0; appearance < std::size(appearances); appearance++){ - std::vector configs = {{"Sign frame", - "PokemonBDSP/Autostory/Route209Repel/SignFrame.png", - {1242.0 / 1920, 580.0 / 1080, 28.0 / 1920, 80.0 / 1080}, - {0, 0, 1, 1}, - .93, - .15}, - {"Rock pair", - "PokemonBDSP/Autostory/Route209Repel/RockPair.png", - {580.0 / 1920, 690.0 / 1080, 155.0 / 1920, 165.0 / 1080}, - {0, 0, 1, 1}, - .93, - .15}}; - for (size_t i = 0; i < configs.size(); i++){ - configs[i].path = appearances[appearance][i]; - } - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (appearance != 1){ - for (size_t i = 0; i < candidate.landmarks.size(); i++){ - const auto& landmark = candidate.landmarks[i]; - if (landmark.accepted && - (!lighting_landmarks[i].accepted || landmark.score > lighting_landmarks[i].score)){ - lighting_landmarks[i] = landmark; - } - } - } - double score = 1; - // Both remaining anchors must pass confidence and agree spatially. - for (const auto& landmark : candidate.landmarks){ - score = std::min(score, landmark.score); - } - candidate.at_target = appearance != 1 && candidate.located && std::abs(candidate.dx) <= 1.0 / 128 && - std::abs(candidate.dy) <= POSITION_TOLERANCE_Y; - // East-side appearance is acquisition-only; the marked pose owns arrival. - if (appearance == 1 && std::abs(candidate.dx) <= 1.0 / 128 && - std::abs(candidate.dy) <= POSITION_TOLERANCE_Y){ - candidate.located = false; - } - if (candidate.located){ - return candidate; - } - if ((candidate.at_target && !best.at_target) || (candidate.located && !best.located) || - (candidate.at_target == best.at_target && candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - // Combine lighting references only when the sign and rock agree. - OverworldMatchResult combined; - combined.landmarks = std::move(lighting_landmarks); - const auto& sign = combined.landmarks[0]; - const auto& rock = combined.landmarks[1]; - combined.located = sign.accepted && rock.accepted && std::abs(sign.dx - rock.dx) <= AGREEMENT_X && - std::abs(sign.dy - rock.dy) <= AGREEMENT_Y; - if (combined.located){ - combined.dx = (sign.dx + rock.dx) / 2; - combined.dy = (sign.dy + rock.dy) / 2; - combined.at_target = std::abs(combined.dx) <= 1.0 / 128 && std::abs(combined.dy) <= POSITION_TOLERANCE_Y; - return combined; - } - } - return best; -} -OverworldMatchResult match_route208_hannah_departure(const ImageRGB32& frame){ - OverworldMatchResult best; - double best_score = -1; - for (int appearance : {0, 865, 930, 965, 978}){ - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - std::vector configs = {{"Field stair foot", - "PokemonBDSP/Autostory/Route208Hannah/StairFoot.png", - {225.0 / 1920, 275.0 / 1080, 250.0 / 1920, 110.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Field sign frame", - "PokemonBDSP/Autostory/Route208Hannah/TreeSign.png", - {1000.0 / 1920, 125.0 / 1080, 100.0 / 1920, 80.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20}, - {"Field house sign frame", - "PokemonBDSP/Autostory/Route208Hannah/HouseSign.png", - {1440.0 / 1920, 110.0 / 1080, 110.0 / 1920, 110.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20}}; - if (appearance){ - // Recorded dodges stand farther east, with the stairs clipped. - // These agreeing signs guide acquisition only; the successful - // departure reference above alone may confirm arrival. - configs.erase(configs.begin()); - configs[0].path = appearance == 865 ? "PokemonBDSP/Autostory/Route208Hannah/FieldSign865.png" - : appearance == 930 ? "PokemonBDSP/Autostory/Route208Hannah/FieldSign930.png" - : appearance == 965 ? "PokemonBDSP/Autostory/Route208Hannah/FieldSign965.png" - : "PokemonBDSP/Autostory/Route208Hannah/FieldSign978.png"; - configs[1].path = appearance == 865 ? "PokemonBDSP/Autostory/Route208Hannah/HouseSign865.png" - : appearance == 930 ? "PokemonBDSP/Autostory/Route208Hannah/HouseSign930.png" - : appearance == 965 ? "PokemonBDSP/Autostory/Route208Hannah/HouseSign965.png" - : "PokemonBDSP/Autostory/Route208Hannah/HouseSign978.png"; - } - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (appearance){ - candidate.coarse = true; - candidate.at_target = false; - } - double score = 1; - size_t accepted = 0; - for (const auto& landmark : candidate.landmarks){ - if (landmark.accepted){ - score = std::min(score, landmark.score); - accepted++; - } - } - if (!accepted){ - score = -1; - } - if ((candidate.located && !best.located) || - (candidate.located && best.located && !candidate.coarse && best.coarse) || - (candidate.located == best.located && candidate.coarse == best.coarse && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - } - return best; -} -OverworldMatchResult match_hearthome_center_entrance(const ImageRGB32& frame){ - // Real stationary hourly views cover daylight, night, dusk and cloud edges. - // The target is butters' street pose below the door; entry remains watched. - struct Reference { - const char* ball; - const char* awning; - double ball_width, ball_height; - }; - const Reference references[] = {{"PokemonBDSP/Autostory/HearthomeCenter/Lighting00Ball.png", - "PokemonBDSP/Autostory/HearthomeCenter/Lighting00Awning.png", - 144.0 / 1920, - 128.0 / 1080}, - {"PokemonBDSP/Autostory/HearthomeCenter/Lighting01Ball.png", - "PokemonBDSP/Autostory/HearthomeCenter/Lighting01Awning.png", - 144.0 / 1920, - 128.0 / 1080}, - {"PokemonBDSP/Autostory/HearthomeCenter/Lighting02Ball.png", - "PokemonBDSP/Autostory/HearthomeCenter/Lighting02Awning.png", - 144.0 / 1920, - 128.0 / 1080}, - {"PokemonBDSP/Autostory/HearthomeCenter/Lighting03Ball.png", - "PokemonBDSP/Autostory/HearthomeCenter/Lighting03Awning.png", - 144.0 / 1920, - 128.0 / 1080}, - {"PokemonBDSP/Autostory/HearthomeCenter/Lighting04Ball.png", - "PokemonBDSP/Autostory/HearthomeCenter/Lighting04Awning.png", - 112.0 / 1920, - 108.0 / 1080}}; - std::vector variants; - for (const auto& reference : references){ - for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ - const double w = reference.ball_width * scale, h = reference.ball_height * scale; - const double aw = 272.0 / 1920 * scale, ah = 80.0 / 1080 * scale; - variants.push_back({"Hearthome Center roof ball", - reference.ball, - {972.0 / 1920 - w / 2, 208.0 / 1080 - h / 2, w, h}, - {0, 0, 1, .65}, - .95, - .20}); - variants.push_back({"Hearthome Center orange awning", - reference.awning, - {968.0 / 1920 - aw / 2, 308.0 / 1080 - ah / 2, aw, ah}, - {0, 0, 1, .65}, - .95, - .20}); - } - } - return match_lighting_pair(frame, - variants, - "Hearthome Center roof ball", - "Hearthome Center orange awning", - 1.0 / 128); -} -OverworldMatchResult match_hearthome_center_interior(const ImageRGB32& frame){ - // Three observed entry-camera angles. Perspective changes the machine - // and register independently, so require a pair from the same real view. - struct View { - const char* machine; - const char* desk; - ImageFloatBox a, b; - }; - const View views[] = {{"InteriorMachine.png", - "InteriorRegister.png", - {660.0 / 1920, 160.0 / 1080, 220.0 / 1920, 185.0 / 1080}, - {1150.0 / 1920, 155.0 / 1080, 140.0 / 1920, 175.0 / 1080}}, - {"EntryRightMachine.png", - "EntryRightRegister.png", - {390.0 / 1920, 160.0 / 1080, 240.0 / 1920, 185.0 / 1080}, - {905.0 / 1920, 155.0 / 1080, 130.0 / 1920, 175.0 / 1080}}, - {"EntryLeftMachine.png", - "EntryLeftRegister.png", - {1045.0 / 1920, 160.0 / 1080, 240.0 / 1920, 185.0 / 1080}, - {1550.0 / 1920, 220.0 / 1080, 155.0 / 1920, 110.0 / 1080}}}; - OverworldMatchResult best; - double best_score = -1; - for (const auto& view : views){ - const std::string machine = "PokemonBDSP/Autostory/HearthomeCenter/" + std::string(view.machine); - const std::string desk = "PokemonBDSP/Autostory/HearthomeCenter/" + std::string(view.desk); - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - auto a = view.a, b = view.b; - a.x = .5 + (a.x - .5) * scale; - a.y = .5 + (a.y - .5) * scale; - a.width *= scale; - a.height *= scale; - b.x = .5 + (b.x - .5) * scale; - b.y = .5 + (b.y - .5) * scale; - b.width *= scale; - b.height *= scale; - auto candidate = match_landmarks(frame, - {{"Center healing machine", machine.c_str(), a, {0, 0, 1, .8}, .93, .20}, - {"Center register", desk.c_str(), b, {0, 0, 1, .8}, .93, .20}}, - 2, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - double score = 1; - for (const auto& anchor : candidate.landmarks){ - score = std::min(score, anchor.score); - } - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - } - return best; -} -OverworldMatchResult match_hearthome_contest_fantina(const ImageRGB32& frame){ - // Static sign rim above localized text and roof emblem; Fantina can move. - // Hourly captures vary the sign illumination. The roof remains reliable. - const char* signs[] = {"PokemonBDSP/Autostory/HearthomeContest/SignRim.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting00Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting01Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting02Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting03Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting04Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting05Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting06Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting07Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting08Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting09Sign.png", - "PokemonBDSP/Autostory/HearthomeContest/RimLighting10Sign.png"}; - std::vector variants; - for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ - const double sw = 162.0 / 1920 * scale, sh = 62.0 / 1080 * scale; - const double rw = 136.0 / 1920 * scale, rh = 98.0 / 1080 * scale; - for (const char* sign : signs){ - variants.push_back({"Contest Hall sign frame", - sign, - {595.0 / 1920 - sw / 2, 467.0 / 1080 - sh / 2, sw, sh}, - {0, .15, .75, .65}, - .93, - .15}); - } - variants.push_back({"Contest Hall roof emblem", - "PokemonBDSP/Autostory/HearthomeContest/RoofEmblem.png", - {940.0 / 1920 - rw / 2, 175.0 / 1080 - rh / 2, rw, rh}, - {.1, 0, .8, .65}, - .93, - .15}); - } - return match_lighting_pair(frame, variants, "Contest Hall sign frame", "Contest Hall roof emblem", 1.0 / 128); -} -// Fixed city landmarks preserve the recorded passage and western stair lane. -OverworldMatchResult match_veilstone_city_passage(const ImageRGB32& frame){ - auto fine = match_landmarks(frame, - {{"Passage house", - "PokemonBDSP/Autostory/VeilstoneDawn/PassageHouse.png", - {42.0 / 960, 222.0 / 540, 210.0 / 960, 110.0 / 540}, - {0, 0, .55, 1}, - .93, - .12}, - {"Game Corner facade", - "PokemonBDSP/Autostory/VeilstoneDawn/PassageGameCorner.png", - {592.0 / 960, 227.0 / 540, 230.0 / 960, 110.0 / 540}, - {.45, 0, .55, 1}, - .93, - .12}}, - 2, - 1.0 / 128, - 1.0 / 108, - true); - if (fine.located){ - return fine; - } - // The former reference column lies just inside the left cliff in the - // live corridor. Recenter eight pixels horizontally and two vertically in the - // matcher image; keep both arrival tolerances unchanged. - // A farther-east expiry may expose only the house's right window. - std::vector variants; - for (double scale : {.85, .90, .95, 1.0, 1.05, 1.10}){ - const double hw = 95.0 / 960 * scale, hh = 80.0 / 540 * scale; - const double gw = 230.0 / 960 * scale, gh = 110.0 / 540 * scale; - variants.push_back({"House window", - "PokemonBDSP/Autostory/VeilstoneDawn/PassageHouseWindow.png", - {(165.0 + 95.0 / 2 - 8) / 960 - hw / 2, (250.0 + 80.0 / 2 + 2) / 540 - hh / 2, hw, hh}, - {0, 0, .55, 1}, - .94, - .15}); - variants.push_back({"Game Corner facade", - "PokemonBDSP/Autostory/VeilstoneDawn/PassageGameCorner.png", - {(600.0 + 230.0 / 2 - 8) / 960 - gw / 2, (225.0 + 110.0 / 2 + 2) / 540 - gh / 2, gw, gh}, - {.25, 0, .75, 1}, - .93, - .12}); - } - // At the east-side roof obstruction, perspective changes the facade. - // Calibrate this acquisition-only view to the simultaneously visible house - // window. It guides west off that roof corner; fine arrival stays mandatory. - variants.push_back({"Game Corner facade", - "PokemonBDSP/Autostory/VeilstoneDawn/GameCornerEastApproach.png", - {644.0 / 960, 228.0 / 540, 240.0 / 960, 100.0 / 540}, - {.25, 0, .75, 1}, - .93, - .12}); - auto approach = match_lighting_pair(frame, variants, "House window", "Game Corner facade", 1.0 / 128); - approach.coarse = true; - approach.at_target = false; - return approach; -} -OverworldMatchResult match_veilstone_dawn_stairs(const ImageRGB32& frame){ - // The residential terrace to the east has similar stairs. Require its - // distinct west corner together with the western staircase, not NPCs. - return match_landmarks(frame, - {{"Western stairs", - "PokemonBDSP/Autostory/VeilstoneDawn/GymStairs.png", - {445.0 / 960, 40.0 / 540, 165.0 / 960, 55.0 / 540}, - {.10, 0, .65, .70}, - .93, - .12}, - {"East terrace corner", - "PokemonBDSP/Autostory/VeilstoneDawn/EastTerraceCorner.png", - {620.0 / 960, 155.0 / 540, 200.0 / 960, 110.0 / 540}, - {.35, 0, .65, .85}, - .93, - .12}}, - 2, - 1.0 / 128, - 1.0 / 108, - true); -} - -OverworldMatchResult match_hearthome_gym_entrance(const ImageRGB32& frame){ - // Wider fixed facade distinguishes the window bays at night. Real hourly - // references cover the illuminated windows; cursor, NPCs and text stay out. - const char* facades[] = {"PokemonBDSP/Autostory/HearthomeGym/LeftFacade.png", - "PokemonBDSP/Autostory/HearthomeGym/Lighting00Facade.png", - "PokemonBDSP/Autostory/HearthomeGym/Lighting01Facade.png", - "PokemonBDSP/Autostory/HearthomeGym/Lighting02Facade.png", - "PokemonBDSP/Autostory/HearthomeGym/Lighting03Facade.png", - "PokemonBDSP/Autostory/HearthomeGym/Lighting04Facade.png"}; - std::vector variants; - for (double scale : {.90, .925, .95, .975, 1.0, 1.025, 1.05, 1.075, 1.10}){ - const double aw = 160.0 / 1920 * scale, ah = 84.0 / 1080 * scale; - const double fw = 218.0 / 1920 * scale, fh = 222.0 / 1080 * scale; - variants.push_back({"Hearthome Gym doorway arch", - "PokemonBDSP/Autostory/HearthomeGym/DoorArch.png", - {966.0 / 1920 - aw / 2, 236.0 / 1080 - ah / 2, aw, ah}, - {.15, 0, .75, .65}, - .93, - .20}); - for (const char* facade : facades){ - variants.push_back({"Hearthome Gym left facade", - facade, - {719.0 / 1920 - fw / 2, 315.0 / 1080 - fh / 2, fw, fh}, - {0, 0, .55, .65}, - .93, - .20}); - } - } - return match_lighting_pair(frame, variants, "Hearthome Gym doorway arch", "Hearthome Gym left facade", 1.0 / 128); -} - -OverworldMatchResult match_eterna_gym_entrance(const ImageRGB32& frame){ - // Recorded centered doorway pose. All three stationary landmarks must agree - // before entering; two can guide acquisition without confirming arrival. - OverworldMatchResult best; - double best_score = -1; - for (int appearance : {19, 16}){ - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - std::vector configs = {{"Left gym sign", - appearance == 19 - ? "PokemonBDSP/Autostory/EternaGym/LeftSign19.png" - : "PokemonBDSP/Autostory/EternaGym/LeftSign16.png", - {458.0 / 1920, 358.0 / 1080, 140.0 / 1920, 150.0 / 1080}, - {0, 0, .65, .85}, - .93, - .20}, - {"Gym Pokeball", - appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/Ball19.png" - : "PokemonBDSP/Autostory/EternaGym/Ball16.png", - {880.0 / 1920, 264.0 / 1080, 150.0 / 1920, 80.0 / 1080}, - {.15, 0, .7, .70}, - .93, - .20}, - {"Right bike post", - appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/BikePost19.png" - : "PokemonBDSP/Autostory/EternaGym/BikePost16.png", - {1468.0 / 1920, 460.0 / 1080, 130.0 / 1920, 178.0 / 1080}, - {.35, .15, .65, .80}, - .93, - .20}}; - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (!candidate.located){ - candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - candidate.coarse = true; - candidate.at_target = false; - } - double score = 1; - for (const auto& landmark : candidate.landmarks){ - if (landmark.accepted){ - score = std::min(score, landmark.score); - } - } - if (candidate.located && - (!best.located || (best.coarse && !candidate.coarse) || - (best.coarse == candidate.coarse && score > best_score))){ - best = candidate; - best_score = score; - }else if (best.landmarks.empty()){ - best = candidate; - } - } - } - if (best.located){ - return best; - } - // The gym first appears at the far right of the approach street. Search - // the whole frame for its left sign and emblem; the bike post may be offscreen. - // These broad matches guide travel only. Arrival still needs all three above. - double coarse_score = -1; - for (int appearance : {680, 19, 333, 303, 306, 339}){ - for (double scale : {.85, .90, 1.0, 1.10, 1.15}){ - std::vector configs = { - {"Left gym sign", - appearance == 680 ? "PokemonBDSP/Autostory/EternaGym/LeftSignApproach.png" - : appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/LeftSign19.png" - : appearance == 333 ? "PokemonBDSP/Autostory/EternaGym/LeftSignWest.png" - : appearance == 306 ? "PokemonBDSP/Autostory/EternaGym/LeftSignSouthShadow.png" - : appearance == 339 ? "PokemonBDSP/Autostory/EternaGym/LeftSignWestShadow.png" - : "PokemonBDSP/Autostory/EternaGym/LeftSignSouth.png", - {458.0 / 1920, 358.0 / 1080, 140.0 / 1920, 150.0 / 1080}, - {0, 0, 1, .95}, - .93, - .20}, - {"Gym Pokeball", - appearance == 680 ? "PokemonBDSP/Autostory/EternaGym/BallApproach.png" - : appearance == 19 ? "PokemonBDSP/Autostory/EternaGym/Ball19.png" - : "PokemonBDSP/Autostory/EternaGym/BallWest.png", - {880.0 / 1920, 264.0 / 1080, 150.0 / 1920, 80.0 / 1080}, - {0, 0, 1, .90}, - .93, - .20}}; - if (appearance == 680){ - configs[0].target.width = 148.0 / 1920; - configs[0].target.height = 158.0 / 1080; - configs[1].target.width = 190.0 / 1920; - } - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - const auto& sign = candidate.landmarks[0]; - const auto& ball = candidate.landmarks[1]; - double score = std::min(sign.score, ball.score); - // Perspective changes their spacing while the gym is far right. - // Both accepted anchors must still agree on the rightward direction - // and vertical row before using that directional-only acquisition. - if (!candidate.located && sign.accepted && ball.accepted && sign.dx > .08 && ball.dx > .08 && - std::abs(sign.dy - ball.dy) <= .04 && std::min(sign.dx, ball.dx) > 2 * std::abs(sign.dx - ball.dx)){ - candidate.located = true; - candidate.dx = (sign.dx + ball.dx) / 2; - candidate.dy = (sign.dy + ball.dy) / 2; - } - if (!candidate.located){ - // One fully visible unique landmark is enough to guide the - // approach when the other is clipped at the right edge. - for (const auto& anchor : candidate.landmarks){ - if (anchor.accepted && anchor.score >= .97 && anchor.margin >= .30 && anchor.dx > .08 && - std::abs(anchor.dy) <= .22 && anchor.score > score){ - candidate.located = true; - candidate.dx = anchor.dx; - candidate.dy = anchor.dy; - score = anchor.score; - } - } - } - candidate.coarse = true; - candidate.at_target = false; - if (candidate.located && score > coarse_score){ - best = candidate; - coarse_score = score; - } - } - } - if (best.located){ - return best; - } - // The yellow-roofed house opposite the gym remains visible when the - // gym itself is clipped offscreen. Reuse paired landmark agreement to - // recover the doorway row and guide east until gym anchors take over. - // Its virtual target is off the left edge: these approach anchors can - // never satisfy near-door moderate confidence or confirm gym arrival. - double house_score = -1; - for (int perspective : {720, 734, 708}){ - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - std::vector configs = { - {"Gym approach house door", - perspective == 708 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseDoorFarWest.png" - : perspective == 734 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseDoorWest.png" - : "PokemonBDSP/Autostory/EternaGym/ApproachHouseDoor.png", - {548.0 / 1920 - .55, 448.0 / 1080, 128.0 / 1920, 156.0 / 1080}, - {0, .15, .75, .70}, - .93, - .20}, - {"Gym approach house window", - perspective == 708 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseWindowFarWest.png" - : perspective == 734 ? "PokemonBDSP/Autostory/EternaGym/ApproachHouseWindowWest.png" - : "PokemonBDSP/Autostory/EternaGym/ApproachHouseWindow.png", - {718.0 / 1920 - .55, 462.0 / 1080, 132.0 / 1920, 112.0 / 1080}, - {0, .15, .85, .70}, - .93, - .20}}; - if (perspective == 734){ - configs[0].target = {338.0 / 1920 - .55, 450.0 / 1080, 132.0 / 1920, 150.0 / 1080}; - configs[1].target = {496.0 / 1920 - .55, 464.0 / 1080, 122.0 / 1920, 102.0 / 1080}; - } - if (perspective == 708){ - configs[0].target = {240.0 / 1920 - .55, 464.0 / 1080, 150.0 / 1920, 150.0 / 1080}; - configs[1].target = {416.0 / 1920 - .55, 478.0 / 1080, 118.0 / 1920, 90.0 / 1080}; - } - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - candidate.coarse = true; - candidate.at_target = false; - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if (candidate.located && candidate.dx > .08 && score > house_score){ - best = candidate; - house_score = score; - } - } - } - return best; -} -double overworld_entry_confidence(const OverworldMatchResult& result, - double tolerance_x, - double tolerance_y, - double minimum_margin){ - double confidence = 0; - for (size_t i = 0; i < result.landmarks.size(); i++){ - const auto& a = result.landmarks[i]; - if (a.margin < minimum_margin || std::abs(a.dx) > tolerance_x || std::abs(a.dy) > tolerance_y){ - continue; - } - for (size_t j = i + 1; j < result.landmarks.size(); j++){ - const auto& b = result.landmarks[j]; - if (b.margin < minimum_margin || std::abs(b.dx) > tolerance_x || std::abs(b.dy) > tolerance_y){ - continue; - } - if (std::abs(a.dx - b.dx) > AGREEMENT_X || std::abs(a.dy - b.dy) > AGREEMENT_Y){ - continue; - } - confidence = std::max(confidence, std::min(a.score, b.score)); - } - } - return confidence; -} -OverworldMatchResult match_galactic_hq_jupiter(const ImageRGB32& frame){ - // The supplied reference is the interaction pose directly north of Jupiter. - // Confirm the table-facing chair sides, adjacent table edges, and stairs. - // Exclude the NPC-facing chair edges where Buneary can overlap the reference. - const std::vector configs{{"LeftChairs", - "PokemonBDSP/Autostory/GalacticHQ/AboveLeftChairTableEdge.png", - {300.0 / 1920, 780.0 / 1080, 156.0 / 1920, 206.0 / 1080}, - {0, .40, .45, .60}, - .93, - .20}, - {"RightChairs", - "PokemonBDSP/Autostory/GalacticHQ/AboveRightChairTableEdge.png", - {1286.0 / 1920, 780.0 / 1080, 154.0 / 1920, 206.0 / 1080}, - {.50, .40, .50, .60}, - .93, - .20}, - {"StairBoundary", - "PokemonBDSP/Autostory/GalacticHQ/AboveStairs.png", - {424.0 / 1920, 310.0 / 1080, 214.0 / 1920, 198.0 / 1080}, - {0, 0, 1, .85}, - .93, - .20}}; - auto result = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (!result.located){ - // Two agreeing points guide the approach; all three still own arrival. - auto coarse = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (coarse.located){ - coarse.coarse = true; - coarse.at_target = false; - result = coarse; - } - } - if (!result.located){ - // Existing recorded chair views can guide recovery from the old row. - // Their coordinates still point to the new north-side destination; - // only the three canonical above-Jupiter references confirm arrival. - auto approach = std::vector{configs[0], configs[1]}; - approach[0].path = "PokemonBDSP/Autostory/GalacticHQ/LeftChairs.png"; - approach[1].path = "PokemonBDSP/Autostory/GalacticHQ/RightChairs.png"; - // Retain the original geometry for the old-row guide-only references. - approach[0].target = {334.0 / 1920, 756.0 / 1080, 156.0 / 1920, 228.0 / 1080}; - approach[1].target = {1238.0 / 1920, 756.0 / 1080, 164.0 / 1920, 230.0 / 1080}; - for (auto& chair : approach){ - chair.threshold = .95; - chair.margin = .25; - } - auto coarse = match_landmarks(frame, approach, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (coarse.located){ - result.located = true; - result.coarse = true; - result.at_target = false; - result.dx = coarse.dx; - result.dy = coarse.dy; - result.landmarks[0] = coarse.landmarks[0]; - result.landmarks[1] = coarse.landmarks[1]; - } - } - return result; -} - -OverworldMatchResult match_galactic_hq_cut(const ImageRGB32& frame){ - // The sign face and Center emblem identify the cuttable tree's column. - // Exclude the HQ door, whose open/night appearance changed in the failed run. - // Tight face/emblem crops avoid grass and moving cloud shadows on the roof. - // The stronger emblem needs a corroborating sign, with a lower identity - // gate for the smaller sign face. Both still must agree on the same pose. - auto result = match_lighting_pair(frame, - {{"HQ sign", - "PokemonBDSP/Autostory/GalacticHQ/CutSignStructure.png", - {810.0 / 1920, 500.0 / 1080, 80.0 / 1920, 58.0 / 1080}, - {0, .25, 1, .45}, - .85, - .10, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Center Pokeball", - "PokemonBDSP/Autostory/GalacticHQ/CutCenterBallStructure.png", - {1048.0 / 1920, 1004.0 / 1080, 136.0 / 1920, 66.0 / 1080}, - {0, .65, 1, .35}, - .93, - .20, - LandmarkFeatures::BLUE_FRACTION}}, - "HQ sign", - "Center Pokeball", - 1.0 / 128); - if (!result.located && result.landmarks.size() == 2){ - const auto& sign = result.landmarks[0]; - const auto& ball = result.landmarks[1]; - // In the recorded far-west misses, the low fascia emblem shifts 1.31 - // screen pixels per sign pixel. Calibrate that acquisition observation - // before applying the same position-agreement bound; never use it for arrival. - const double calibrated_ball_dx = ball.dx / 1.31; - if (sign.accepted && ball.accepted && sign.dx * ball.dx > 0 && std::abs(sign.dx) > .03 && - std::abs(sign.dx) <= .20 && std::abs(sign.dx - calibrated_ball_dx) <= AGREEMENT_X && - std::abs(sign.dy - ball.dy) <= AGREEMENT_Y){ - result.located = true; - result.coarse = true; - result.at_target = false; - result.dx = (sign.dx + calibrated_ball_dx) / 2; - } - } - // The recorded approach already clamps against the hedge. Correct only - // its column; the watched Up/A loop owns the final north interaction. - result.dy = 0; - result.at_target = result.located && !result.coarse && std::abs(result.dx) <= 1.0 / 128; - return result; -} - -OverworldMatchResult match_eterna_cycle_shop_entrance(const ImageRGB32& frame){ - const std::vector configs{ - {"Shop sign", - "PokemonBDSP/Autostory/EternaBikeShop/eterna_cycle_shop_entrance-0.png", - {555.0 / 1920, 350.0 / 1080, 100.0 / 1920, 120.0 / 1080}, - {0, 0.1, 0.65, 0.75}, - .93, - .20}, - {"Shop roof braces", - "PokemonBDSP/Autostory/EternaBikeShop/ShopRoofBraces.png", - {680.0 / 1920, 145.0 / 1080, 470.0 / 1920, 36.0 / 1080}, - {0, 0, .95, .75}, - .93, - .20}}; - return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); -} - -OverworldMatchResult match_eterna_cycle_shop_interior(const ImageRGB32& frame){ - // Entry mat view: fixed wall tools and register, clear of NPCs/Pokemon. - // This identifies the room; it is not the counter interaction position. - const std::vector configs{{"Shop wall tools", - "PokemonBDSP/Autostory/EternaBikeShop/InteriorWallTools.png", - {920.0 / 1920, 20.0 / 1080, 350.0 / 1920, 150.0 / 1080}, - {.15, 0, .60, .55}, - .93, - .20}, - {"Shop register", - "PokemonBDSP/Autostory/EternaBikeShop/InteriorRegister.png", - {760.0 / 1920, 160.0 / 1080, 120.0 / 1920, 145.0 / 1080}, - {.20, 0, .55, .65}, - .93, - .20}}; - auto result = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (result.located){ - return result; - } - const std::vector north_configs{ - {"Shop wall tools", - "PokemonBDSP/Autostory/EternaBikeShop/InteriorNorthWallTools.png", - {928.0 / 1920, 84.0 / 1080, 350.0 / 1920, 150.0 / 1080}, - {.15, 0, .60, .55}, - .93, - .20}, - {"Shop register", - "PokemonBDSP/Autostory/EternaBikeShop/InteriorNorthRegister.png", - {754.0 / 1920, 230.0 / 1080, 120.0 / 1920, 145.0 / 1080}, - {.20, 0, .55, .65}, - .93, - .20}}; - return match_landmarks(frame, north_configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); -} - -OverworldMatchResult match_eterna_cycle_shop_reward(const ImageRGB32& frame){ - // Reward returns the player outside on the bicycle, at a separate pose. - // Reuse the stable roof braces; the nearby post changes appearance/shading. - const std::vector configs{{"Shop sign", - "PokemonBDSP/Autostory/EternaBikeShop/reward-sign.png", - {580.0 / 1920, 330.0 / 1080, 100.0 / 1920, 120.0 / 1080}, - {0, .1, .65, .75}, - .93, - .20}, - {"Shop roof braces", - "PokemonBDSP/Autostory/EternaBikeShop/ShopRoofBraces.png", - {706.0 / 1920, 128.0 / 1080, 470.0 / 1920, 36.0 / 1080}, - {0, 0, .95, .75}, - .93, - .20}}; - return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); -} - -OverworldMatchResult match_eterna_bike_riding_card(const ImageRGB32& frame){ - // Fixed UI card: require both the blue illustration and B Close prompt. - // Small search boxes tolerate capture offsets without matching other menus. - const std::vector configs{{"Blue Bike illustration", - "PokemonBDSP/Autostory/EternaBikeShop/BikeRidingBlueFeature.png", - {104.0 / 1920, 314.0 / 1080, 520.0 / 1920, 450.0 / 1080}, - {.035, .26, .32, .48}, - .93, - .20, - LandmarkFeatures::BLUE_FRACTION}, - {"B Close prompt", - "PokemonBDSP/Autostory/EternaBikeShop/BikeRidingClosePrompt.png", - {1570.0 / 1920, 730.0 / 1080, 160.0 / 1920, 60.0 / 1080}, - {.79, .65, .14, .10}, - .93, - .20}}; - return match_landmarks(frame, configs, 2, 1.0 / 64, 1.0 / 54, true); -} - -OverworldMatchResult match_eterna_cycle_shop_counter(const ImageRGB32& frame){ - const std::vector configs{ - {"Red frame", - "PokemonBDSP/Autostory/EternaBikeShop/eterna_cycle_shop_counter-0.png", - {620.0 / 1920, 65.0 / 1080, 390.0 / 1920, 240.0 / 1080}, - {0.1, 0, 0.65, 0.55}, - .93, - .20}, - {"Counter detail", - "PokemonBDSP/Autostory/EternaBikeShop/eterna_cycle_shop_counter-1.png", - {1110.0 / 1920, 486.0 / 1080, 86.0 / 1920, 38.0 / 1080}, - {0.3, 0.15, 0.65, 0.65}, - .93, - .20}}; - return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); -} - -// Two stationary architectural features confirm the intended gate interior. -// The doorway and west window avoid the moving player, NPCs and localized text. -OverworldMatchResult match_cycling_road_south_gate_interior(const ImageRGB32& frame){ - return match_landmarks(frame, - {{"Gate north doorway", - "PokemonBDSP/Autostory/CyclingRoadSouthGate/NorthDoorway.png", - {630.0 / 1920, 118.0 / 1080, 220.0 / 1920, 270.0 / 1080}, - {.15, 0, .70, .55}, - .93, - .20}, - {"Gate west window", - "PokemonBDSP/Autostory/CyclingRoadSouthGate/WestWindow.png", - {302.0 / 1920, 140.0 / 1080, 157.0 / 1920, 145.0 / 1080}, - {.05, 0, .35, .55}, - .93, - .20}}, - 2, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y); -} -OverworldMatchResult match_cycling_road_bridge_approach(const ImageRGB32& frame){ - // Use the actual bridge entrance: cyan right rail with a broad metal deck - // immediately to its left. Neither the Route 206 sign nor gate is involved. - OverworldMatchResult best; - if (!frame){ - return best; - } - cv::Mat pixels((int)frame.height(), - (int)frame.width(), - CV_8UC4, - const_cast(frame.data()), - frame.bytes_per_row()); - cv::Mat small, bgr, hsv, cyan; - cv::resize(pixels, small, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - cv::cvtColor(small, bgr, cv::COLOR_BGRA2BGR); - cv::cvtColor(bgr, hsv, cv::COLOR_BGR2HSV); - // Hue/saturation retain the cyan rail under the recorded moving shadows. - cv::inRange(hsv, cv::Scalar(78, 95, 55), cv::Scalar(105, 255, 255), cyan); - cv::morphologyEx(cyan, cyan, cv::MORPH_CLOSE, cv::Mat::ones(3, 3, CV_8U)); - const int w = hsv.cols, h = hsv.rows; - std::vector> contours; - cv::findContours(cyan, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); - double best_support = 0; - bool ambiguous = false; - for (const auto& contour : contours){ - const auto rail = cv::boundingRect(contour); - if (rail.x < w * .25 || rail.x > w * .85 || rail.height < h * .10){ - continue; - } - // A left-hand rail has grass/cliff on this side and fails the deck test. - const int left = std::max(0, rail.x - (int)(w * .22)); - const int right = rail.x - (int)(w * .02); - if (right - left < w * .12){ - continue; - } - const int begin = std::max(0, rail.y + (int)(h * .035)); - const int end = std::min(h - (int)(h * .05), rail.y + (int)(h * .24)); - const int sustained = std::max(3, (int)(h * .018)); - int deck_y = -1; - double deck_support = 0; - for (int y = begin; y < end; y++){ - bool supported = true; - double total = 0; - // Both halves must see the same broad deck edge, not a flower, - // narrow painted line or one white patch beside a cyan object. - for (int half = 0; half < 2; half++){ - int neutral = 0; - const int x0 = left + (right - left) * half / 2; - const int x1 = left + (right - left) * (half + 1) / 2; - for (int row = y; row < y + sustained; row++){ - for (int x = x0; x < x1; x++){ - const auto p = hsv.at(row, x); - // Shadowed metal remains low-saturation; the grass edge - // and paired rail reject unrelated dark neutral panels. - neutral += p[1] <= 80 && p[2] >= 60; - } - } - const double fraction = (double)neutral / ((x1 - x0) * sustained); - if (fraction < .72){ - supported = false; - } - total += fraction / 2; - } - if (supported){ - deck_y = y; - deck_support = total; - break; - } - } - if (deck_y < 0 || deck_y < h * .15){ - continue; - } - // The entrance is a grass-to-metal boundary. A road panel or sky - // behind an unrelated cyan object cannot stand in for this edge. - int grass = 0; - const int above_begin = std::max(0, deck_y - (int)(h * .05)); - const int above_end = deck_y - (int)(h * .02); - if (above_end <= above_begin){ - continue; - } - for (int row = above_begin; row < above_end; row++){ - for (int x = left; x < right; x++){ - const auto p = hsv.at(row, x); - grass += p[0] >= 25 && p[0] <= 85 && p[1] >= 60 && p[2] >= 30; - } - } - if ((double)grass / ((right - left) * (above_end - above_begin)) < .45){ - continue; - } - // Measure the inner rail boundary at deck height, rather than using - // the cliff wall or the rail cap's perspective-dependent bounding box. - double inner = 0, ink = 0; - size_t rows = 0; - const int measure_begin = std::max(deck_y + (int)(h * .015), (int)(h * .52)); - const int measure_end = std::min(h, measure_begin + (int)(h * .02)); - for (int y = measure_begin; y < measure_end; y++){ - for (int x = rail.x; x < std::min(w, rail.x + rail.width); x++){ - if (cyan.at(y, x)){ - int last = x, count = 0; - for (int edge = x; edge < std::min(w, rail.x + rail.width); edge++){ - if (cyan.at(y, edge)){ - last = edge; - ++count; - } - } - const int width = last - x + 1; - if (width >= w * .008 && width <= w * .055 && (double)count / width >= .80){ - inner += x; - ink += (double)count / width; - ++rows; - } - break; - } - } - } - if (rows < 3 || rows < (measure_end - measure_begin) * .75){ - continue; - } - inner /= rows; - OverworldMatchResult candidate; - candidate.located = true; - candidate.dx = inner / w - .55; - candidate.dy = (double)deck_y / h - .36; - // Park on the upper white platform beside the right rail. The user - // demonstrated this stationary pose in the 12:33:35 recording; the - // downhill section starts well beyond it. Measure the rail at wheel - // height, not at its distant cap or the offset cliff wall. - candidate.at_target = std::abs(candidate.dx) <= .012 && std::abs(candidate.dy) <= 1.0 / 54; - OverworldLandmarkResult rail_anchor; - rail_anchor.name = "Bridge right rail inner edge"; - rail_anchor.accepted = true; - rail_anchor.score = ink / rows; - rail_anchor.dx = candidate.dx; - rail_anchor.dy = candidate.dy; - rail_anchor.search = {.25, 0, .68, 1}; - rail_anchor.found = {(double)inner / w, (double)rail.y / h, (double)rail.width / w, (double)rail.height / h}; - OverworldLandmarkResult deck_anchor; - deck_anchor.name = "Bridge deck leading edge"; - deck_anchor.accepted = true; - deck_anchor.score = deck_support; - deck_anchor.dx = candidate.dx; - deck_anchor.dy = candidate.dy; - deck_anchor.search = {0, (double)begin / h, (double)right / w, (double)(end - begin) / h}; - deck_anchor.found = {(double)left / w, (double)deck_y / h, (double)(right - left) / w, (double)sustained / h}; - candidate.landmarks = {rail_anchor, deck_anchor}; - const double support = ink / rows + deck_support; - if (best.located && - (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ - ambiguous = true; - } - if (support > best_support){ - best_support = support; - best = std::move(candidate); - } - } - if (ambiguous){ - best.located = false; - best.at_target = false; - } - return best; -} - - -OverworldMatchResult match_galactic_hq_exterior(const ImageRGB32& frame){ - const std::vector configs{{"HQ sign", - "PokemonBDSP/Autostory/GalacticHQ/galactic_hq_exterior-0.png", - {660.0 / 1920, 615.0 / 1080, 118.0 / 1920, 124.0 / 1080}, - {0, 0.2, 0.65, 0.8}, - .93, - .20}, - {"HQ doorway lintel", - "PokemonBDSP/Autostory/GalacticHQ/galactic_hq_exterior-1.png", - {874.0 / 1920, 350.0 / 1080, 172.0 / 1920, 78.0 / 1080}, - {0.2, 0, 0.6, 0.7}, - .93, - .20}}; - return match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); -} - -OverworldMatchResult match_eterna_center_departure(const ImageRGB32& frame){ - // Center-exit pose in the recorded Center->Mart and bicycle-shop route. - // Reuse the existing shadow/appearance-aware Center identification. - auto result = match_eterna_center_entrance(frame); - result.dy -= .070463; - result.at_target = result.located && !result.coarse && std::abs(result.dx) <= 1.0 / 128 && - std::abs(result.dy) <= POSITION_TOLERANCE_Y; - return result; -} - -OverworldMatchResult match_eterna_gym_statues(const ImageRGB32& frame){ - // Actual crops from screenshot-20261018-171535781091. Separate left/right - // search regions prevent one identical statue from satisfying both votes. - OverworldMatchResult best; - double best_score = -1; - if (!frame){ - return best; - } - // Normalized correlation also matches almost-black fade frames. Require - // visible statue regions before allowing those shapes to verify entry. - const cv::Mat gray = gray_image(frame); - const auto visible = [&](double x, double width){ - const cv::Rect box((int)(x * gray.cols), - (int)(458.0 / 1080 * gray.rows), - (int)(width * gray.cols), - (int)(350.0 / 1080 * gray.rows)); - return cv::mean(gray(box))[0] >= 60; - }; - const bool statues_visible = visible(448.0 / 1920, 146.0 / 1920) && visible(1322.0 / 1920, 154.0 / 1920); - for (double scale : {.95, 1.0, 1.05}){ - std::vector configs = {{"Left gym statue", - "PokemonBDSP/Autostory/EternaGym/LeftStatue.png", - {448.0 / 1920, 458.0 / 1080, 146.0 / 1920, 350.0 / 1080}, - {.12, .30, .27, .53}, - .93, - .20}, - {"Right gym statue", - "PokemonBDSP/Autostory/EternaGym/RightStatue.png", - {1322.0 / 1920, 458.0 / 1080, 154.0 / 1920, 350.0 / 1080}, - {.60, .30, .28, .53}, - .93, - .20}}; - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, .04, .06, true); - // Interior verification requires both statues near the expected entry - // positions, not merely anywhere in the room or during a fade. - candidate.at_target = candidate.at_target && statues_visible; - candidate.located = candidate.at_target; - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = candidate; - best_score = score; - } - } - return best; -} -OverworldMatchResult match_eterna_underground_house(const ImageRGB32& frame){ - // User's doorway pose: the Center ball and the same house sign, viewed - // from the east. The sign is optional when a strong unique ball confirms - // the doorway pose; cloud shadow on the sign must not block entry. - OverworldMatchResult best; - double best_score = -1; - bool best_primary_only = false; - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - std::vector configs{{"Center Pokeball", - "PokemonBDSP/Autostory/EternaUndergroundMan/HouseBall.png", - {332.0 / 1920, 224.0 / 1080, 112.0 / 1920, 104.0 / 1080}, - {0, 0, .65, .7}, - .93, - .20}, - {"House sign", - "PokemonBDSP/Autostory/EternaUndergroundMan/HouseSign.png", - {1240.0 / 1920, 396.0 / 1080, 110.0 / 1920, 106.0 / 1080}, - {.35, 0, .65, .85}, - .93, - .20}}; - for (auto& config : configs){ - const double w = config.target.width, h = config.target.height; - config.target.width *= scale; - config.target.height *= scale; - config.target.x += (w - config.target.width) / 2; - config.target.y += (h - config.target.height) / 2; - } - auto candidate = match_landmarks(frame, configs, 2, 1.0 / 128, POSITION_TOLERANCE_Y, true); - double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if (candidate.located){ - if (!best.located || best_primary_only || score > best_score){ - best = candidate; - best_score = score; - best_primary_only = false; - } - }else if (!best.located || best_primary_only){ - // The optional door-side sign can change perspective or disappear in - // cloud shadow. Preserve the high uniqueness margin on the primary ball. - const auto& ball = candidate.landmarks[0]; - if (ball.accepted && ball.score >= .95 && ball.margin >= .30 && std::abs(ball.dx) < .20 && - std::abs(ball.dy) < .20 && (!best.located || ball.score > best_score)){ - best = candidate; - best.located = true; - best.coarse = false; - best.dx = ball.dx; - best.dy = ball.dy; - best.at_target = std::abs(best.dx) <= 1.0 / 128 && std::abs(best.dy) <= POSITION_TOLERANCE_Y; - best_score = ball.score; - best_primary_only = true; - }else if (best.landmarks.empty()){ - best = candidate; - } - } - } - return best; -} -OverworldMatchResult match_eterna_underground_man(const ImageRGB32& frame){ - // Reuse fixed furniture, excluding people and the animated TV picture. - // The right-window search excludes the identical window above the stool. - const std::vector configs{{"Stool", - "PokemonBDSP/Autostory/EternaUndergroundMan/Stool.png", - {380.0 / 1920, 610.0 / 1080, 86.0 / 1920, 70.0 / 1080}, - {0, .3, .65, .5}, - .93, - .20}, - {"Right tree top", - "PokemonBDSP/Autostory/EternaUndergroundMan/TreeTop.png", - {1372.0 / 1920, 320.0 / 1080, 102.0 / 1920, 96.0 / 1080}, - {.5, 0, .4, .7}, - .93, - .20}, - {"Wall sign", - "PokemonBDSP/Autostory/EternaUndergroundMan/WallSign.png", - {642.0 / 1920, 354.0 / 1080, 114.0 / 1920, 70.0 / 1080}, - {.2, 0, .5, .6}, - .93, - .20}, - {"Window beside TV", - "PokemonBDSP/Autostory/EternaUndergroundMan/RightWindow.png", - {906.0 / 1920, 260.0 / 1080, 162.0 / 1920, 160.0 / 1080}, - {.35, 0, .55, .55}, - .93, - .20}}; - auto result = match_landmarks(frame, configs, 4, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (!result.located){ - // Three agreeing furniture anchors identify the room during entry and - // guide acquisition when a fourth is clipped. Never authorize talking. - auto coarse = match_landmarks(frame, configs, 3, 1.0 / 128, POSITION_TOLERANCE_Y, true); - if (coarse.located){ - coarse.coarse = true; - coarse.at_target = false; - return coarse; - } - } - return result; -} -OverworldMatchResult match_jubilife_street_approach(const ImageRGB32& frame){ - // The lighting-run pose was 1/108 screen height south of the original - // screenshot. Targets preserve the original destination height. - // Capture-derived references cover lamp, daylight, and moving tree shadows. - return match_lighting_pair(frame, - {{"BuildingCorner", - "PokemonBDSP/Autostory/JubilifeStreet/BuildingCornerHour3Frame4.png", - {0.588541666667, 0.351851851852, 0.088541666667, 0.259259259259}, - {0, 0, 1, 1}, - .93, - .08}, - {"BuildingCorner", - "PokemonBDSP/Autostory/JubilifeStreet/BuildingCornerHour14Frame2.png", - {0.588541666667, 0.351851851852, 0.088541666667, 0.259259259259}, - {0, 0, 1, 1}, - .93, - .08}, - {"BuildingCorner", - "PokemonBDSP/Autostory/JubilifeStreet/BuildingCornerHour17Frame4.png", - {0.588541666667, 0.351851851852, 0.088541666667, 0.259259259259}, - {0, 0, 1, 1}, - .93, - .08}, - {"BuildingBase", - "PokemonBDSP/Autostory/JubilifeStreet/BuildingBaseHour15Frame2.png", - {0.812500000000, 0.388888888889, 0.078125000000, 0.203703703704}, - {0, 0, 1, 1}, - .93, - .08}, - {"BuildingBase", - "PokemonBDSP/Autostory/JubilifeStreet/BuildingBaseHour17Frame5.png", - {0.812500000000, 0.388888888889, 0.078125000000, 0.203703703704}, - {0, 0, 1, 1}, - .93, - .08}, - {"BuildingBase", - "PokemonBDSP/Autostory/JubilifeStreet/BuildingBaseHour20Frame5.png", - {0.812500000000, 0.388888888889, 0.078125000000, 0.203703703704}, - {0, 0, 1, 1}, - .93, - .08}}, - "BuildingCorner", - "BuildingBase", - POSITION_TOLERANCE_X, - true); -} -OverworldMatchResult match_jubilife_plaza_approach(const ImageRGB32& frame){ - // Capture-derived references cover lamp, daylight, and moving tree shadows. - auto result = match_lighting_pair(frame, - { - {"RoofEdge", - "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour2Frame2.png", - {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, - {0, 0, 1, 1}, - .93, - .08}, - {"RoofEdge", - "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour11Frame1.png", - {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, - {0, 0, 1, 1}, - .93, - .08}, - {"RoofEdge", - "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour11Frame2.png", - {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, - {0, 0, 1, 1}, - .93, - .08}, - {"RoofEdge", - "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour11Frame5.png", - {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, - {0, 0, 1, 1}, - .93, - .08}, - {"RoofEdge", - "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour16Frame5.png", - {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, - {0, 0, 1, 1}, - .93, - .08}, - {"RoofEdge", - "PokemonBDSP/Autostory/JubilifePlaza/RoofEdgeHour23Frame2.png", - {0.614583333333, 0.203703703704, 0.299479166667, 0.111111111111}, - {0, 0, 1, 1}, - .93, - .08}, - {"Fence", - "PokemonBDSP/Autostory/JubilifePlaza/FenceHour3Frame1.png", - {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, - {0, 0, 1, 1}, - .93, - .08}, - {"Fence", - "PokemonBDSP/Autostory/JubilifePlaza/FenceHour5Frame4.png", - {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, - {0, 0, 1, 1}, - .93, - .08}, - {"Fence", - "PokemonBDSP/Autostory/JubilifePlaza/FenceHour11Frame3.png", - {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, - {0, 0, 1, 1}, - .93, - .08}, - {"Fence", - "PokemonBDSP/Autostory/JubilifePlaza/FenceHour14Frame1.png", - {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, - {0, 0, 1, 1}, - .93, - .08}, - {"Fence", - "PokemonBDSP/Autostory/JubilifePlaza/FenceHour19Frame1.png", - {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, - {0, 0, 1, 1}, - .93, - .08}, - {"Fence", - "PokemonBDSP/Autostory/JubilifePlaza/FenceHour21Frame4.png", - {0.044270833333, 0.328703703704, 0.169270833333, 0.106481481481}, - {0, 0, 1, 1}, - .93, - .08} // Small screen-relative quantization allowance avoids bouncing across the - // plaza target when one movement step spans both sides of the tolerance. - }, - "RoofEdge", - "Fence", - POSITION_TOLERANCE_X, - false, - 1.0 / 32); - if (!result.located && result.landmarks.size() == 2){ - const auto& fence = result.landmarks[1]; - // A moving roof shadow must not trigger a blind lateral sweep when the - // distinctive fence still gives a strong, nearby position estimate. - // Fence-only guidance can reacquire the roof; it cannot confirm arrival. - if (fence.accepted && fence.score >= .95 && fence.margin >= .30 && std::abs(fence.dx) <= .20 && - std::abs(fence.dy) <= .20){ - result.located = true; - result.coarse = true; - result.at_target = false; - result.dx = fence.dx; - result.dy = fence.dy; - } - } - return result; -} -OverworldMatchResult match_route204_sign(const ImageRGB32& frame){ - // One vote per physical landmark. Lighting variants avoid treating the - // moving tree shadow over the fence as a missing route destination. - const ImageFloatBox sign_target{0.468750000000, 0.376851851852, 0.072916666667, 0.052777777778}; - const ImageFloatBox fence_target{0.598958333333, 0.421296296296, 0.041666666667, 0.152777777778}; - std::vector variants{ - {"Route sign", "PokemonBDSP/Autostory/Route204Sign/Sign.png", sign_target, {0, 0, 1, 1}, .93, .10}, - {"Fence corner", "PokemonBDSP/Autostory/Route204Sign/FenceCorner.png", fence_target, {0, 0, 1, 1}, .93, .10}, - {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour0Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, - {"Fence corner", - "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour0Frame3.png", - fence_target, - {0, 0, 1, 1}, - .93, - .10}, - {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour8Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, - {"Fence corner", - "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour8Frame3.png", - fence_target, - {0, 0, 1, 1}, - .93, - .10}, - {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour13Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, - {"Fence corner", - "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour13Frame3.png", - fence_target, - {0, 0, 1, 1}, - .93, - .10}, - {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour17Frame3.png", sign_target, {0, 0, 1, 1}, .93, .10}, - {"Fence corner", - "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour17Frame3.png", - fence_target, - {0, 0, 1, 1}, - .93, - .10}, - {"Route sign", "PokemonBDSP/Autostory/Route204Sign/SignHour2Frame1.png", sign_target, {0, 0, 1, 1}, .93, .10}, - {"Fence corner", - "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour2Frame1.png", - fence_target, - {0, 0, 1, 1}, - .93, - .10}, - {"Fence corner", - "PokemonBDSP/Autostory/Route204Sign/FenceCornerHour10Frame3.png", - fence_target, - {0, 0, 1, 1}, - .93, - .10}, - {"Fence corner", - "PokemonBDSP/Autostory/Route204Sign/FenceCornerRecordedCloud.png", - fence_target, - {0, 0, 1, 1}, - .93, - .10}, - }; - auto result = match_lighting_pair(frame, variants, "Route sign", "Fence corner"); - if (!result.located && result.landmarks.size() == 2){ - const auto& sign = result.landmarks[0]; - // A distinctive high-confidence sign can guide bounded acquisition. - // It cannot confirm the endpoint without the fence agreeing as well. - if (sign.accepted && sign.score >= .95 && sign.margin >= .30 && std::abs(sign.dx) <= .20 && - std::abs(sign.dy) <= .25){ - result.located = true; - result.coarse = true; - result.at_target = false; - result.dx = sign.dx; - result.dy = sign.dy; - } - } - return result; -} -static OverworldMatchResult match_route205_marker(const ImageRGB32& frame, double cx, double cy){ - OverworldMatchResult best; - // Native sign faces at the two route positions. The post, moving NPCs, - // grass and water are excluded from the acquisition references. - for (int variant : {0, 1, 2, 3, 4, 5, 6}){ - for (double scale : {.9, 1.0, 1.1, 1.2}){ - const double w = (variant == 6 ? 188.0 - : variant == 5 ? 184.0 - : variant == 4 ? 220.0 - : variant == 3 ? 211.0 - : variant == 2 ? 208.0 - : variant == 1 ? 206.0 - : 205.0) / - 1920 * scale; - const double h = (variant == 6 ? 74.0 : variant >= 3 ? 72.0 : variant ? 80.0 : 82.0) / 1080 * scale; - auto candidate = match_landmarks(frame, - {{"Route 205 marker", - variant == 6 ? "PokemonBDSP/Autostory/Route205/BridgeSignMidCloud.png" - : variant == 5 ? "PokemonBDSP/Autostory/Route205/BridgeSignCloud.png" - : variant == 4 ? "PokemonBDSP/Autostory/Route205/BridgeSignLow.png" - : variant == 3 ? "PokemonBDSP/Autostory/Route205/BridgeSignLive.png" - : variant == 2 ? "PokemonBDSP/Autostory/Route205/NorthSignRecorded.png" - : variant == 1 ? "PokemonBDSP/Autostory/Route205/NorthSign.png" - : "PokemonBDSP/Autostory/Route205/BridgeSign.png", - {cx - w / 2, cy - h / 2, w, h}, - {0, 0, 1, 1}, - .93, - .20}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if (best.landmarks.empty() || (candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - } - return best; -} -OverworldMatchResult match_route205_zackary(const ImageRGB32& frame){ - OverworldMatchResult best; - double best_score = -1; - for (int view : {0, 1, 2, 3}){ - for (double scale : {.9, 1.0, 1.1}){ - const double sw = 91.0 / 1920 * scale, sh = 65.0 / 1080 * scale; - // Include the fixed cliff edge beside both pipes: the repeated blue caps - // alone produce a second strong peak in cloud shadow. - const double cw = 300.0 / 1920 * scale, ch = 300.0 / 1080 * scale; - auto candidate = match_landmarks(frame, - {{"Zackary bridge sign", - view == 2 ? "PokemonBDSP/Autostory/Route205/PatrolSignCloud.png" - : view >= 1 ? "PokemonBDSP/Autostory/Route205/PatrolSignLive.png" - : "PokemonBDSP/Autostory/Route205/PatrolSign.png", - {959.5 / 1920 - sw / 2, 67.5 / 1080 - sh / 2, sw, sh}, - {0, 0, 1, 1}, - .93, - .20}, - {"Zackary twin chimneys", - view == 3 ? "PokemonBDSP/Autostory/Route205/PatrolChimneysPartialCloud.png" - : view == 2 ? "PokemonBDSP/Autostory/Route205/PatrolChimneysCloud.png" - : view == 1 ? "PokemonBDSP/Autostory/Route205/PatrolChimneysLive.png" - : "PokemonBDSP/Autostory/Route205/PatrolChimneys.png", - {1210.0 / 1920 - cw / 2, 290.0 / 1080 - ch / 2, cw, ch}, - {0, 0, 1, 1}, - .93, - .20}}, - 2, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if (best.located && candidate.located && - (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ - best.located = false; - best.at_target = false; - return best; - } - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - } - return best; -} -OverworldMatchResult match_route205_kelsey(const ImageRGB32& frame){ - OverworldMatchResult best; - // This item is retained on the route. Restrict acquisition to the left of - // the player, excluding the red cap and the northern bridge's item ball. - for (double scale : {.9, 1.0, 1.1}){ - const double w = 56.0 / 1920 * scale, h = 53.0 / 1080 * scale; - auto candidate = match_landmarks(frame, - {{"Kelsey west item ball", - "PokemonBDSP/Autostory/Route205/KelseyItem.png", - {280.0 / 1920 - w / 2, 574.5 / 1080 - h / 2, w, h}, - {0, .2, .48, .65}, - .93, - .20}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if (best.landmarks.empty() || (candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = std::move(candidate); - } - } - return best; -} -OverworldMatchResult match_route205_bank_tree(const ImageRGB32& frame){ - // Use the tree crown above the player's reach, plus the stationary child. - // Recorded cloud-edge views retain both anchor gates and agreement. - // Trunk crops can include the player's cap when he stands under the tree. - OverworldMatchResult best; - double best_score = -1; - for (int view : {0, 1, 2, 3, 4, 5, 6, 7, 8}){ - for (double scale : {.9, 1.0, 1.1}){ - const double tw = 140.0 / 1920 * scale, th = 130.0 / 1080 * scale; - const double nw = 120.0 / 1920 * scale, nh = 170.0 / 1080 * scale; - auto candidate = match_landmarks(frame, - {{"Bank tree crown", - view == 8 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownWest.png" - : view == 7 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud333.png" - : view == 6 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud76.png" - : view == 5 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud336.png" - : view == 4 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud328.png" - : view == 3 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud80.png" - : view == 2 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownCloud72.png" - : view == 1 ? "PokemonBDSP/Autostory/Route205/BankTreeCrownLive.png" - : "PokemonBDSP/Autostory/Route205/BankTreeCrown.png", - {770.0 / 1920 - tw / 2, 235.0 / 1080 - th / 2, tw, th}, - {0, 0, 1, 1}, - .93, - .20}, - {"Bank blue-hat child", - view == 8 ? "PokemonBDSP/Autostory/Route205/BankBlueHatWest.png" - : view == 7 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud333.png" - : view == 6 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud76.png" - : view == 5 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud336.png" - : view == 4 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud328.png" - : view == 3 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud80.png" - : view == 2 ? "PokemonBDSP/Autostory/Route205/BankBlueHatCloud72.png" - : view == 1 ? "PokemonBDSP/Autostory/Route205/BankBlueHatLive.png" - : "PokemonBDSP/Autostory/Route205/BankBlueHat.png", - {700.0 / 1920 - nw / 2, 525.0 / 1080 - nh / 2, nw, nh}, - {0, 0, 1, 1}, - .93, - .20}}, - 2, - 1.0 / 96, - POSITION_TOLERANCE_Y, - true); - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if (best.located && candidate.located && - (std::abs(best.dx - candidate.dx) > AGREEMENT_X || std::abs(best.dy - candidate.dy) > AGREEMENT_Y)){ - best.located = false; - best.at_target = false; - return best; - } - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - } - return best; -} -OverworldMatchResult match_route205_bridge(const ImageRGB32& frame){ - return match_route205_marker(frame, 722.5 / 1920, 465.0 / 1080); -} -OverworldMatchResult match_route205_forest_entrance(const ImageRGB32& frame){ - return match_route205_marker(frame, 1245.0 / 1920, 508.0 / 1080); -} -OverworldMatchResult match_windworks_interior_grunt(const ImageRGB32& frame){ - OverworldMatchResult result; - cv::Mat pixels((int)frame.height(), (int)frame.width(), CV_8UC4, (void*)frame.data(), frame.bytes_per_row()); - cv::Mat small, bgr, hsv; - cv::resize(pixels, small, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - cv::cvtColor(small, bgr, cv::COLOR_BGRA2BGR); - cv::cvtColor(bgr, hsv, cv::COLOR_BGR2HSV); - // Grey indoor tiles distinguish this scene from the outdoor grunt, battle - // screens and fades. Pale turquoise partitions have too little saturation - // to qualify as the grunt's hair. - cv::Mat floor; - cv::inRange(hsv, cv::Scalar(0, 0, 100), cv::Scalar(179, 65, 255), floor); - const cv::Rect room(288, 81, 624, 405); - if ((double)cv::countNonZero(floor(room)) / room.area() < .30){ - return result; - } - auto head = [&](cv::Scalar low, cv::Scalar high, bool player, cv::Point2d& center){ - cv::Mat mask, labels, stats, centroids; - cv::inRange(hsv, low, high, mask); - const int count = cv::connectedComponentsWithStats(mask, labels, stats, centroids); - int best = -1, best_area = 0, runner_area = 0; - for (int i = 1; i < count; i++){ - int area = stats.at(i, cv::CC_STAT_AREA); - int w = stats.at(i, cv::CC_STAT_WIDTH), h = stats.at(i, cv::CC_STAT_HEIGHT); - double x = centroids.at(i, 0), y = centroids.at(i, 1); - if (area < 80 || area > 4000 || w < 12 || w > 110 || h < 8 || h > 90 || (double)area / (w * h) < .35 || - x < 288 || x > (player ? 672 : 864) || y < (player ? 135 : 15) || y > 459){ - continue; - } - if (area > best_area){ - runner_area = best_area; - best_area = area; - best = i; - }else{ - runner_area = std::max(runner_area, area); - } - } - if (best < 0 || runner_area > best_area * .70){ - return false; - } - center = {centroids.at(best, 0), centroids.at(best, 1)}; - return true; - }; - cv::Point2d player, grunt; - if (!head({0, 120, 120}, {22, 255, 255}, true, player) || !head({75, 100, 120}, {105, 255, 255}, false, grunt)){ - return result; - } - if (std::abs(player.x - grunt.x) > 288 || std::abs(player.y - grunt.y) > 350){ - return result; - } - result.located = true; - result.dx = (grunt.x - player.x) / 960; - result.dy = (grunt.y - player.y) / 540; - return result; -} -OverworldMatchResult match_windworks_sign_approach(const ImageRGB32& frame){ - // Sign beside the open lane west of the doorway turbine. Keep the player - // east of the sign/grass before the blind building-wall approach. - OverworldMatchResult best; - // The sign grows as it approaches the bottom of the camera view. Scale - // acquisition templates around the same target center, keeping all gates. - for (int view : {0, 1, 2}){ - for (double scale : {1.0, 1.1, 1.2, 1.3}){ - const double w = (view == 2 ? 124.0 : view == 1 ? 144.0 : 121.0) / 1920 * scale; - const double h = (view == 1 ? 140.0 : 122.0) / 1080 * scale; - auto candidate = match_landmarks(frame, - {{"Windworks approach sign", - view == 2 ? "PokemonBDSP/Autostory/WindworksApproach/SignNight.png" - : view == 1 ? "PokemonBDSP/Autostory/WindworksApproach/SignLow.png" - : "PokemonBDSP/Autostory/WindworksApproach/Sign.png", - {836.5 / 1920 - w / 2, 515.0 / 1080 - h / 2, w, h}, - {0, 0, 1, 1}, - .93, - .20}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if (best.landmarks.empty() || (candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - } - // Face-only references cover the high/mid camera perspectives without - // relying on the moving grass that can cover the sign's lower border. - for (bool high_view : {false, true}){ - for (double scale : {.9, 1.0, 1.1}){ - const double w = (high_view ? 86.0 : 106.0) / 1920 * scale; - const double h = (high_view ? 72.0 : 94.0) / 1080 * scale; - auto candidate = match_landmarks(frame, - {{"Windworks approach sign face", - high_view ? "PokemonBDSP/Autostory/WindworksApproach/SignHighFace.png" - : "PokemonBDSP/Autostory/WindworksApproach/SignMidFace.png", - {838.0 / 1920 - w / 2, 516.0 / 1080 - h / 2, w, h}, - {0, 0, 1, 1}, - .93, - .20}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if ((candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - } - return best; -} -OverworldMatchResult match_route204_floaroma_turn(const ImageRGB32& frame){ - OverworldMatchResult best; - // The northern 204 marker is wider in this camera view. Match its face, - // placing it above the player so the westward turn clears its post and - // the flower-bed fence. Align horizontally with the sign before the - // subsequent westward route, rather than staying in the eastern lane. - for (double scale_x : {1.0, 1.05, 1.10, 1.15, 1.20}){ - for (double scale_y : {.90, 1.0, 1.10}){ - const double w = 140.0 / 1920 * scale_x, h = 57.0 / 1080 * scale_y; - auto candidate = match_landmarks(frame, - {{"Northern Route 204 sign", - "PokemonBDSP/Autostory/Route204Sign/SignHour13Frame3.png", - {.5 - w / 2, 420.0 / 1080 - h / 2, w, h}, - {0, 0, 1, 1}, - .93, - .20}}, - 1, - POSITION_TOLERANCE_X, - POSITION_TOLERANCE_Y, - true); - if (best.landmarks.empty() || (candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - } - return best; -} -OverworldMatchResult match_route204_north_approach(const ImageRGB32& frame){ - auto result = match_route204_sign(frame); - // The pre-battle reference stands below the sign. After Tyler, place - // the player one body-width west of it before starting north. - constexpr double west_offset = .04; - for (auto& anchor : result.landmarks){ - anchor.dx -= west_offset; - } - result.dx -= west_offset; - if (!result.located && !result.landmarks.empty()){ - const auto& sign = result.landmarks[0]; - if (sign.accepted && sign.score >= .93 && sign.margin >= .30 && std::abs(sign.dx) <= .45 && - std::abs(sign.dy) <= .25){ - result.located = true; - result.coarse = true; - result.dx = sign.dx; - result.dy = sign.dy; - } - } - result.at_target = result.located && !result.coarse; - for (const auto& anchor : result.landmarks){ - if (!anchor.accepted || std::abs(anchor.dx) > 1.0 / 128 || std::abs(anchor.dy) > POSITION_TOLERANCE_Y){ - result.at_target = false; - } - } - return result; -} -OverworldMatchResult match_floaroma_house_entrance(const ImageRGB32& frame){ - // Fixed facade details above/aside the player avoid door/player occlusion. - // Both must agree on the pose recorded just outside the central house. - return match_landmarks(frame, - {{"House window", - "PokemonBDSP/Autostory/FloaromaHouse/Window.png", - {1060.0 / 1920, 370.0 / 1080, 128.0 / 1920, 106.0 / 1080}, - {0, 0, 1, 1}, - .93, - .10}, - {"House gable", - "PokemonBDSP/Autostory/FloaromaHouse/Gable.png", - {796.0 / 1920, 196.0 / 1080, 460.0 / 1920, 154.0 / 1080}, - {0, 0, 1, 1}, - .93, - .10}}, - 2, - 1.0 / 64, - 1.0 / 54, - true); -} -OverworldMatchResult match_floaroma_center_entrance(const ImageRGB32& frame){ - OverworldMatchResult best; - // Perspective changes the emblem size as we approach the doorway gap. - for (double scale : {.90, .95, 1.0, 1.05, 1.10}){ - const double w = 105.0 / 1920 * scale, h = 86.0 / 1080 * scale; - auto candidate = match_landmarks(frame, - {{"Center emblem", - "PokemonBDSP/Autostory/FloaromaCenter/Emblem.png", - {.5 - w / 2, 135.0 / 1080 - h / 2, w, h}, - {0, 0, 1, 1}, - .93, - .20}}, - 1, - 1.0 / 64, - 1.0 / 36, - true); - if (best.landmarks.empty() || (candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - // At the west wall the camera makes the emblem nearly circular, unlike - // the vertically compressed doorway reference. Search that recorded - // perspective without lowering the score or uniqueness requirements. - if (!best.located){ - for (double scale_x : {1.0, 1.05, 1.10, 1.15, 1.20}){ - for (double scale_y : {1.15, 1.25, 1.35, 1.45}){ - const double w = 105.0 / 1920 * scale_x, h = 86.0 / 1080 * scale_y; - auto candidate = match_landmarks(frame, - {{"Center emblem west perspective", - "PokemonBDSP/Autostory/FloaromaCenter/Emblem.png", - {.5 - w / 2, 135.0 / 1080 - h / 2, w, h}, - {0, 0, 1, 1}, - .93, - .20}}, - 1, - 1.0 / 64, - 1.0 / 36, - true); - candidate.coarse = true; - candidate.at_target = false; - if ((candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - } - } - // A lower-face reference excludes the tester's burned-in overlay line - // and covers the doorway-facing perspective, retaining score/margin gates. - auto lower = match_landmarks(frame, - {{"Center emblem lower face", - "PokemonBDSP/Autostory/FloaromaCenter/EmblemLower.png", - {.5 - 52.0 / 1920, 114.0 / 1080, 104.0 / 1920, 65.0 / 1080}, - {0, 0, 1, 1}, - .93, - .20}}, - 1, - 1.0 / 64, - 1.0 / 36, - true); - if ((lower.located && (!best.located || best.coarse)) || - (lower.located == best.located && lower.landmarks[0].score > best.landmarks[0].score)){ - best = lower; - } - return best; -} -OverworldMatchResult match_route204_north_stairs(const ImageRGB32& frame){ - OverworldMatchResult best; - // Fixed rails/steps, with sunny and cloud-covered perspectives. - for (const char* path : {"PokemonBDSP/Autostory/Route204NorthStairs/Stairs.png", - "PokemonBDSP/Autostory/Route204NorthStairs/StairsCloud.png"}){ - // Camera pitch changes height and width by different amounts. - for (double scale_x : {.90, 1.0, 1.10}){ - for (double scale_y : {.80, .90, 1.0, 1.10}){ - const double w = 298.0 / 1920 * scale_x, h = 282.0 / 1080 * scale_y; - auto candidate = match_landmarks(frame, - {{"North route stairs", path, {.5 - w / 2, 650.0 / 1080, w, h}, {0, 0, 1, 1}, .93, .10}}, - 1, - 1.0 / 64, - 1.0 / 54, - true); - if (best.landmarks.empty() || (candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - } - } - if (!best.located){ - // The lower steps distort as the camera moves north. The rail tops, - // grass edge and first steps retain a stable physical upper edge. - for (const auto& config : - std::vector{{"North route stairs top", - "PokemonBDSP/Autostory/Route204NorthStairs/Top.png", - {.5 - 149.0 / 1920, 640.0 / 1080, 298.0 / 1920, 70.0 / 1080}, - {0, 0, 1, 1}, - .93, - .10}, - {"North route stairs top", - "PokemonBDSP/Autostory/Route204NorthStairs/TopRaised.png", - {.5 - 157.0 / 1920, 640.0 / 1080, 314.0 / 1920, 70.0 / 1080}, - {0, 0, 1, 1}, - .93, - .10}}){ - for (double scale_x : {.95, 1.0, 1.05}){ - for (double scale_y : {.90, 1.0, 1.10}){ - auto variant = config; - variant.target.width *= scale_x; - variant.target.height *= scale_y; - variant.target.x = .5 - variant.target.width / 2; - auto candidate = match_landmarks(frame, {variant}, 1, 1.0 / 64, 1.0 / 54, true); - if ((candidate.located && !best.located) || - (candidate.located == best.located && candidate.landmarks[0].score > best.landmarks[0].score)){ - best = candidate; - } - } - } - } - } - return best; -} -OverworldMatchResult match_teighler_alcove_corner(const ImageRGB32& frame){ - auto result = match_route204_north_stairs(frame); - // Reuse the fixed stairs only as an anchor. The destination is the - // bottom-left of Teighler's alcove, east of the stairs, not their center. - // Geometry comes from the recorded 1920x1080 corner reference. - constexpr double shift_x = 393.0 / 1920, shift_y = 182.0 / 1080; - result.dx += shift_x; - result.dy += shift_y; - for (auto& anchor : result.landmarks){ - anchor.dx += shift_x; - anchor.dy += shift_y; - } - result.at_target = result.located && std::abs(result.dx) <= 1.0 / 64 && std::abs(result.dy) <= 1.0 / 54; - return result; -} -OverworldMatchResult match_ravaged_path_south_entrance(const ImageRGB32& frame){ - const ImageFloatBox mouth{860.0 / 1920, 220.0 / 1080, 200.0 / 1920, 155.0 / 1080}; - const ImageFloatBox sign{810.0 / 1920, 380.0 / 1080, 100.0 / 1920, 110.0 / 1080}; - std::vector variants; - // A different approach height changes perspective. Compare a small range - // of sizes, keeping each physical anchor's reference center fixed. - for (double scale : {.90, 1.0, 1.10}){ - for (const auto& config : - std::vector{{"Cave mouth", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/Mouth.png", - mouth, - {0, 0, 1, 1}, - .93, - .10}, - {"Entrance sign", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/Sign.png", - sign, - {0, 0, 1, 1}, - .93, - .10}, - {"Cave mouth", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/MouthShadow.png", - mouth, - {0, 0, 1, 1}, - .93, - .10}, - {"Entrance sign", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignShadow.png", - sign, - {0, 0, 1, 1}, - .93, - .10}, - {"Entrance sign", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignPartialShadow.png", - sign, - {0, 0, 1, 1}, - .93, - .10}, - {"Entrance sign", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignFace.png", - {828.0 / 1920, 400.0 / 1080, 64.0 / 1920, 62.0 / 1080}, - {0, 0, 1, 1}, - .93, - .10}, - {"Entrance sign", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignFaceLowApproach.png", - {828.0 / 1920, 400.0 / 1080, 64.0 / 1920, 62.0 / 1080}, - {0, 0, 1, 1}, - .93, - .10}, - {"Entrance sign", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/SignFaceCloud.png", - {828.0 / 1920, 400.0 / 1080, 64.0 / 1920, 62.0 / 1080}, - {0, 0, 1, 1}, - .93, - .10}}){ - auto variant = config; - variant.target.width *= scale; - variant.target.height *= scale; - variant.target.x += (config.target.width - variant.target.width) / 2; - variant.target.y += (config.target.height - variant.target.height) / 2; - variants.push_back(variant); - } - } - auto result = match_lighting_pair(frame, variants, "Cave mouth", "Entrance sign", 1.0 / 64, true); - if (!result.located && result.landmarks.size() == 2){ - for (const auto& landmark : result.landmarks){ - if (landmark.accepted && landmark.score >= .95 && landmark.margin >= .20 && std::abs(landmark.dx) <= .35 && - std::abs(landmark.dy) <= .40){ - result.located = true; - result.coarse = true; - result.at_target = false; - result.dx = landmark.dx; - result.dy = landmark.dy; - break; - } - } - } - // The moving blue-hatted boy is deliberately absent from the references. - // The lass is optional evidence for the southern movement boundary, never - // a replacement for the fixed mouth/sign arrival anchors. - auto boundary = match_landmarks(frame, - {{"Lass south boundary", - "PokemonBDSP/Autostory/RavagedPathSouthEntrance/Lass.png", - {910.0 / 1920, 788.0 / 1080, 100.0 / 1920, 142.0 / 1080}, - {0, 0, 1, 1}, - .88, - .08}}, - 1, - 1.0 / 64, - 1.0 / 36, - true); - if (!boundary.landmarks.empty()){ - result.landmarks.push_back(boundary.landmarks[0]); - } - return result; -} -OverworldMatchResult match_route215_bridge_renewal(const ImageRGB32& frame){ - // Restore the successful west bridge approach after variable expiry movement. - if (!frame){ - return {}; - } - cv::Mat gray, smoothed; - cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); - ImageRGB32 filtered(960, 540); - cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); - cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); - auto result = match_lighting_pair(filtered, - {{"Bridge west edge", - "PokemonBDSP/Autostory/Route215Renewal/BridgeEdge.png", - {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, - {.15, .25, .85, .75}, - .90, - .10}, - {"Landing cliff corner", - "PokemonBDSP/Autostory/Route215Renewal/BridgeCliff.png", - {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, - {.10, .25, .85, .75}, - .90, - .10}, - {"Bridge west edge", - "PokemonBDSP/Autostory/Route215Renewal/BridgeFarEdge.png", - {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, - {.15, .25, .85, .75}, - .90, - .10}, - {"Landing cliff corner", - "PokemonBDSP/Autostory/Route215Renewal/BridgeFarCliff.png", - {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, - {.10, .25, .85, .75}, - .90, - .10}, - {"Bridge west edge", - "PokemonBDSP/Autostory/Route215Renewal/BridgeNightEdge.png", - {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, - {.15, .25, .85, .75}, - .90, - .10}, - {"Landing cliff corner", - "PokemonBDSP/Autostory/Route215Renewal/BridgeNightCliff.png", - {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, - {.10, .25, .85, .75}, - .90, - .10}, - {"Bridge west edge", - "PokemonBDSP/Autostory/Route215Renewal/BridgeNightFarEdge.png", - {538.0 / 960, 302.0 / 540, 50.0 / 960, 100.0 / 540}, - {.15, .25, .85, .75}, - .90, - .10}, - {"Landing cliff corner", - "PokemonBDSP/Autostory/Route215Renewal/BridgeNightFarCliff.png", - {333.0 / 960, 410.0 / 540, 64.0 / 960, 88.0 / 540}, - {.10, .25, .85, .75}, - .90, - .10}}, - "Bridge west edge", - "Landing cliff corner", - 1.0 / 128); - result.at_target = result.located && std::abs(result.dx) <= 1.0 / 128 && std::abs(result.dy) <= 1.0 / 108; - return result; -} -OverworldMatchResult match_route215_ace_renewal(const ImageRGB32& frame){ - // The road bend and fence corner exclude the patrolling trainers. - if (!frame){ - return {}; - } - cv::Mat gray, smoothed; - cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); - ImageRGB32 filtered(960, 540); - cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); - cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); - auto result = match_landmarks(filtered, - {{"Road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceRoad.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {.10, .15, .85, .80}, - .92, - .08}, - {"Fence corner", - "PokemonBDSP/Autostory/Route215Renewal/AceFence.png", - {165.0 / 960, 389.0 / 540, 110.0 / 960, 78.0 / 540}, - {.05, .25, .90, .75}, - .90, - .10, - LandmarkFeatures::LOCAL_CONTRAST}}, - 2, - 1.0 / 128, - 1.0 / 108, - true); - if (result.located){ - return result; - } - // Expiry may finish east of the fence's visible range. Two separate road - // bends establish a horizontal recovery direction along this clear row. - // Perspective changes their spacing, so this pair cannot confirm arrival. - // Suppress thin rain streaks and puddle rings in the broader road shapes; - // retain the original input and confidence gates for the fine fence pair. - cv::medianBlur(gray, smoothed, 5); - cv::GaussianBlur(smoothed, smoothed, cv::Size(0, 0), .8); - cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); - auto approach = match_lighting_pair(filtered, - {{"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadGoal.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadNear.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadFar.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWestFar.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWestNear.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWest.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadWestNearRain.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Upper road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireRoadFarRain.png", - {352.0 / 960, 180.0 / 540, 60.0 / 960, 80.0 / 540}, - {0, .25, 1, .30}, - .92, - .08}, - {"Lower road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireLowerRoadEast.png", - {500.0 / 960, 420.0 / 540, 80.0 / 960, 70.0 / 540}, - {0, .70, 1, .30}, - .93, - .10}, - {"Lower road bend", - "PokemonBDSP/Autostory/Route215Renewal/AceAcquireLowerRoad.png", - {500.0 / 960, 420.0 / 540, 80.0 / 960, 70.0 / 540}, - {0, .70, 1, .30}, - .93, - .10}}, - "Upper road bend", - "Lower road bend", - 1.0 / 128); - // At the eastern extreme, the original bends leave the screen. The two - // corners of the same grass bed remain visible above the clear road. - auto east = match_lighting_pair(filtered, - {{"Grass west corner", - "PokemonBDSP/Autostory/Route215Renewal/AceGrassWest.png", - {665.0 / 960, 145.0 / 540, 110.0 / 960, 75.0 / 540}, - {0, .20, 1, .40}, - .92, - .10}, - {"Grass east corner", - "PokemonBDSP/Autostory/Route215Renewal/AceGrassEast.png", - {890.0 / 960, 175.0 / 540, 65.0 / 960, 62.0 / 540}, - {0, .20, 1, .40}, - .92, - .10}, - {"Grass west corner", - "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarWest.png", - {665.0 / 960, 145.0 / 540, 110.0 / 960, 75.0 / 540}, - {0, .20, 1, .40}, - .92, - .10}, - {"Grass east corner", - "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarEast.png", - {890.0 / 960, 175.0 / 540, 65.0 / 960, 62.0 / 540}, - {0, .20, 1, .40}, - .92, - .10}, - {"Grass east corner", - "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarEastShoulder.png", - {850.0 / 960, 145.0 / 540, 105.0 / 960, 80.0 / 540}, - {0, .20, 1, .40}, - .92, - .10}}, - "Grass west corner", - "Grass east corner", - 1.0 / 128); - const auto& west = east.landmarks[0]; - const auto& right = east.landmarks[1]; - const double east_nearest = std::min(std::abs(west.dx), std::abs(right.dx)); - east.located = west.accepted && right.accepted && std::abs(west.dy - right.dy) <= AGREEMENT_Y && - west.dx < -1.0 / 12 && right.dx < -1.0 / 12 && east_nearest > 2 * std::abs(west.dx - right.dx) && - east.dy >= -1.0 / 27 && east.dy <= 1.0 / 9; - if (east.located){ - east.coarse = true; - east.at_target = false; - return east; - } - // From the eastern end, the fixed route sign remains unique while the - // road bends are off-screen. Its virtual goal lies beyond the right edge; - // this pair guides acquisition only and can never confirm arrival. - auto far_east = match_lighting_pair(filtered, - {{"Grass west corner", - "PokemonBDSP/Autostory/Route215Renewal/AceGrassFarWest.png", - {665.0 / 960, 145.0 / 540, 110.0 / 960, 75.0 / 540}, - {0, .20, 1, .40}, - .92, - .10}, - {"Route 215 sign", - "PokemonBDSP/Autostory/Route215Renewal/AceRoute215Sign.png", - {1128.0 / 960, 359.0 / 540, 90.0 / 960, 45.0 / 540}, - {.50, .40, .50, .50}, - .94, - .20}, - {"Route 215 sign", - "PokemonBDSP/Autostory/Route215Renewal/AceRoute215SignRain.png", - {1128.0 / 960, 359.0 / 540, 90.0 / 960, 45.0 / 540}, - {.50, .40, .50, .50}, - .94, - .20}}, - "Grass west corner", - "Route 215 sign", - 1.0 / 128); - const auto& corner = far_east.landmarks[0]; - const auto& sign = far_east.landmarks[1]; - const double far_nearest = std::min(std::abs(corner.dx), std::abs(sign.dx)); - far_east.located = corner.accepted && sign.accepted && std::abs(corner.dy - sign.dy) <= AGREEMENT_Y && - corner.dx < -1.0 / 4 && sign.dx < -1.0 / 4 && far_nearest > 2 * std::abs(corner.dx - sign.dx) && - far_east.dy >= -1.0 / 27 && far_east.dy <= 1.0 / 9; - if (far_east.located){ - far_east.coarse = true; - far_east.at_target = false; - return far_east; - } - const auto& upper = approach.landmarks[0]; - const auto& lower = approach.landmarks[1]; - // Keep the shared row when both bends already agree. In the recorded - // perspective-shifted rows the nearer bend moves 1.35 times as far; - // compare those acquisition errors in the farther bend's plane. - // Neither model changes the strict fine-arrival checks above. - const double lower_plane_dy = std::abs(upper.dy - lower.dy) <= AGREEMENT_Y ? lower.dy : lower.dy / 1.35; - approach.dy = (upper.dy + lower_plane_dy) / 2; - const double nearest = std::min(std::abs(upper.dx), std::abs(lower.dx)); - const bool horizontal_support = - upper.dx * lower.dx > 0 && nearest > 1.0 / 64 && nearest > 2 * std::abs(upper.dx - lower.dx); - // Expiry can also finish one row south, already near the goal column. - // Both accepted bends must agree before correcting that row vertically. - const bool vertical_support = std::abs(upper.dx - lower.dx) <= AGREEMENT_X && - std::max(std::abs(upper.dx), std::abs(lower.dx)) <= 1.0 / 12 && - std::abs(upper.dy - lower_plane_dy) <= 1.0 / 108 && upper.dy * lower_plane_dy > 0 && - std::abs(approach.dy) > 1.0 / 54; - approach.located = upper.accepted && lower.accepted && std::abs(upper.dy - lower_plane_dy) <= AGREEMENT_Y && - std::abs(approach.dy) <= 1.0 / 9 && (horizontal_support || vertical_support); - approach.coarse = true; - approach.at_target = false; - return approach; -} -OverworldMatchResult match_route215_derek_stairs_renewal(const ImageRGB32& frame){ - if (!frame){ - return {}; - } - cv::Mat gray, smoothed; - cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); - ImageRGB32 filtered(960, 540); - cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); - cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); - // Clear landing below the short stairs; neither tree nor player is an anchor. - auto result = match_lighting_pair(filtered, - {{"White stair railing", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairsRailing.png", - {500.0 / 960, 78.0 / 540, 90.0 / 960, 110.0 / 540}, - {.10, .05, .85, .85}, - .93, - .12}, - {"Lower stair corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairsCorner.png", - {589.0 / 960, 315.0 / 540, 95.0 / 960, 105.0 / 540}, - {.10, .20, .85, .80}, - .93, - .12}, - {"White stair railing", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairsNightRailing.png", - {500.0 / 960, 78.0 / 540, 90.0 / 960, 110.0 / 540}, - {.10, .05, .85, .85}, - .93, - .12}, - {"Lower stair corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairsNightCorner.png", - {589.0 / 960, 315.0 / 540, 95.0 / 960, 105.0 / 540}, - {.10, .20, .85, .80}, - .93, - .12}}, - "White stair railing", - "Lower stair corner", - 1.0 / 128); - result.at_target = result.located && std::abs(result.dx) <= 1.0 / 128 && std::abs(result.dy) <= 1.0 / 108; - if (result.located){ - return result; - } - // Far west of this landing, perspective shifts the two structures by - // different amounts. Use that view only to acquire the horizontal column; - // the original close pair above remains the sole arrival check. - auto approach = match_landmarks(filtered, - {{"White stair railing", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairsWestRailing.png", - {500.0 / 960, 78.0 / 540, 90.0 / 960, 110.0 / 540}, - {.10, .05, .85, .85}, - .93, - .12}, - {"Lower stair corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairsWestCorner.png", - {589.0 / 960, 315.0 / 540, 95.0 / 960, 105.0 / 540}, - {.10, .20, .85, .80}, - .93, - .12}}, - 2, - 1.0 / 128, - 1.0 / 108, - true); - const auto& rail = approach.landmarks[0]; - const auto& stair = approach.landmarks[1]; - const double nearest = std::min(std::abs(rail.dx), std::abs(stair.dx)); - approach.located = rail.accepted && stair.accepted && std::abs(rail.dy - stair.dy) <= AGREEMENT_Y && - std::abs((rail.dy + stair.dy) / 2) <= 1.0 / 108 && rail.dx * stair.dx > 0 && nearest > 1.0 / 64 && - nearest > 2 * std::abs(rail.dx - stair.dx); - approach.dx = (rail.dx + stair.dx) / 2; - approach.dy = (rail.dy + stair.dy) / 2; - approach.coarse = true; - approach.at_target = false; - return approach; -} -OverworldMatchResult match_route215_derek_stair_top_renewal(const ImageRGB32& frame){ - if (!frame){ - return {}; - } - cv::Mat gray, smoothed; - cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); - ImageRGB32 filtered(960, 540); - cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); - cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); - // The road row above the stairs is clear west/east, below the tall grass. - auto result = match_lighting_pair(filtered, - {{"Stair-top barrier", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopBarrier.png", - {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, - {.05, .15, .90, .85}, - .93, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Stair-top barrier", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopBarrierEast.png", - {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, - {.05, .15, .90, .85}, - .93, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFenceEast.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Stair-top barrier", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightBarrier.png", - {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, - {.05, .15, .90, .85}, - .93, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Stair-top barrier", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightEastBarrier.png", - {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, - {.05, .15, .90, .85}, - .93, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightEastFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Stair-top barrier", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightWestBarrier.png", - {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, - {.05, .15, .90, .85}, - .93, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNightWestFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopNorthFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Stair-top barrier", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastNorthBarrier.png", - {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, - {.05, .15, .90, .85}, - .93, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastNorthFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Stair-top barrier", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastBarrier.png", - {384.0 / 960, 401.0 / 540, 127.0 / 960, 70.0 / 540}, - {.05, .15, .90, .85}, - .93, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST} // Raw-luminance alternatives preserve fence structure when local - // contrast normalization amplifies a passing rain streak. - , - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastNorthGrayFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12}, - {"West fence corner", - "PokemonBDSP/Autostory/Route215Renewal/DerekStairTopFarEastGrayFence.png", - {274.0 / 960, 275.0 / 540, 69.0 / 960, 125.0 / 540}, - {.05, .15, .90, .85}, - .90, - .12}}, - "Stair-top barrier", - "West fence corner", - 1.0 / 128); - result.at_target = result.located && std::abs(result.dx) <= 1.0 / 128 && std::abs(result.dy) <= 1.0 / 108; - if (!result.located && result.landmarks.size() == 2){ - const auto& barrier = result.landmarks[0]; - const auto& fence = result.landmarks[1]; - const double nearest = std::min(std::abs(barrier.dx), std::abs(fence.dx)); - // Far-east perspective changes spacing: acquire the column first, - // then use the original strict two-anchor arrival check. - if (barrier.accepted && fence.accepted && std::abs(barrier.dy - fence.dy) <= AGREEMENT_Y && - std::abs(result.dy) <= 1.0 / 20 && barrier.dx * fence.dx > 0 && nearest > 1.0 / 64 && - nearest > 2 * std::abs(barrier.dx - fence.dx)){ - result.located = true; - result.coarse = true; - result.at_target = false; - } - } - return result; -} -OverworldMatchResult match_route215_jogger_corner(const ImageRGB32& frame){ - if (!frame){ - return {}; - } - // These references describe the game capture, without the recording canvas. - // Smooth only this locator before contrast normalization to suppress rain - // streaks and capture resampling noise; other detectors retain their inputs. - cv::Mat gray, smoothed; - cv::resize(gray_image(frame), gray, cv::Size(960, 540), 0, 0, cv::INTER_AREA); - cv::GaussianBlur(gray, smoothed, cv::Size(0, 0), .8); - ImageRGB32 filtered(960, 540); - cv::Mat pixels(540, 960, CV_8UC4, filtered.data(), filtered.bytes_per_row()); - cv::cvtColor(smoothed, pixels, cv::COLOR_GRAY2BGRA); - // Fixed fence cap and road bend; neither the moving jogger nor the item - // supplies position evidence. Each view maps back to the same lane-4 corner. - // Search only the cap/bend height of lanes 4 and 3. This excludes similar - // lower fence sections; a missing cap never authorizes a vertical search. - // Low-frequency cap/cliff alternatives require higher correlation (.95/.97) - // as well as a unique peak and an independently agreeing structural anchor. - const std::vector appearances = { - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceGoal.png", - {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceRainWest.png", - {0.535416666667, 0.311111111111, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceCloudWest.png", - {0.535416666667, 0.311111111111, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceLane3.png", - {0.535416666667, 0.312962962963, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceMid.png", - {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceNear.png", - {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceApproachFar.png", - {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceApproachMiddle.png", - {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceApproachNear.png", - {0.534375, 0.312962962963, 32.0 / 960, 42.0 / 540}, - {0, .24, 1, .26}, - .93, - .08, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Fence top", - "PokemonBDSP/Autostory/Route215JoggerV2/FenceGrayLane3.png", - {0.535416666667, 0.312962962963, 32.0 / 960, 24.0 / 540}, - {0, .24, 1, .26}, - .95, - .05}, - {"Road bend", - "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperDayGoal.png", - {422.0 / 960, 164.0 / 540, 80.0 / 960, 25.0 / 540}, - {0, .24, 1, .26}, - .90, - .15}, - // In lane 3 the cap overlaps the lower part of the full road patch. - // Upper-bend alternatives keep position evidence clear of the player. - {"Road bend", - "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperGoal.png", - {0.439583333333, 0.303703703704, 80.0 / 960, 25.0 / 540}, - {0, .24, 1, .26}, - .90, - .15}, - {"Road bend", - "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperRainWest.png", - {0.439583333333, 0.303703703704, 80.0 / 960, 25.0 / 540}, - {0, .24, 1, .26}, - .90, - .15}, - {"Road bend", - "PokemonBDSP/Autostory/Route215JoggerV2/RoadUpperCloudWest.png", - {0.447916666667, 0.307407407407, 80.0 / 960, 25.0 / 540}, - {0, .24, 1, .26}, - .90, - .15}, - {"Road bend", - "PokemonBDSP/Autostory/Route215JoggerV2/RoadGoal.png", - {0.439583333333, 0.303703703704, 80.0 / 960, 41.0 / 540}, - {0, .24, 1, .26}, - .90, - .15}, - {"Road bend", - "PokemonBDSP/Autostory/Route215JoggerV2/RoadRainWest.png", - {0.439583333333, 0.303703703704, 80.0 / 960, 41.0 / 540}, - {0, .24, 1, .26}, - .90, - .15}, - {"Road bend", - "PokemonBDSP/Autostory/Route215JoggerV2/RoadCloudWest.png", - {0.447916666667, 0.307407407407, 80.0 / 960, 41.0 / 540}, - {0, .24, 1, .26}, - .90, - .15}, - }; - std::vector variants; - for (const auto& appearance : appearances){ - if (std::string(appearance.name) == "Road bend"){ - variants.push_back(appearance); - continue; - } - // Horizontal perspective changes as the fence crosses the screen. - for (double scale : {.96, 1.0, 1.04}){ - auto variant = appearance; - variant.target.width *= scale; - variant.target.x += (appearance.target.width - variant.target.width) / 2; - variants.push_back(variant); - } - } - // A road bend can disambiguate repeated fence sections. Choose complete - // agreeing pairs, rather than rejecting one anchor before considering the other. - const auto candidates = - match_landmarks(filtered, variants, variants.size() + 1, 1.0 / 128, POSITION_TOLERANCE_Y, true); - OverworldMatchResult best; - double best_score = -1; - bool supported = false, ambiguous = false; - double first_x = 0, first_y = 0; - for (const auto& fence : candidates.landmarks){ - if (fence.name != "Fence top"){ - continue; - } - for (const auto& road : candidates.landmarks){ - if (road.name != "Road bend"){ - continue; - } - const double score = std::min(fence.score, road.score); - const bool agrees = - std::abs(fence.dx - road.dx) <= AGREEMENT_X && std::abs(fence.dy - road.dy) <= AGREEMENT_Y; - const bool valid = fence.accepted && road.accepted && agrees; - const double dx = (fence.dx + road.dx) / 2, dy = (fence.dy + road.dy) / 2; - if (valid){ - if (supported && (std::abs(dx - first_x) > AGREEMENT_X || std::abs(dy - first_y) > AGREEMENT_Y)){ - ambiguous = true; - } - if (!supported){ - first_x = dx; - first_y = dy; - } - } - if ((valid && !supported) || (valid == supported && score > best_score)){ - best.landmarks = {fence, road}; - best.dx = dx; - best.dy = dy; - best_score = score; - } - supported |= valid; - } - } - best.located = supported && !ambiguous; - best.at_target = best.located && std::abs(best.dx) <= 1.0 / 128 && std::abs(best.dy) <= POSITION_TOLERANCE_Y; - if (best.located){ - return best; - } - // West of the visible fence, bridge/cliff pairs establish the walking row. - // Their x coordinates are acquisition-only and can never confirm arrival. - const std::vector> approaches = { - {// The observed lane-4 to lane-3 step changes the bridge-foot view. - // Both corners must agree; this pair never confirms arrival. - {"Bridge west corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestNightLane3.png", - {106.0 / 960 - .435625, 297.0 / 540 - 70.0 / 1080, 60.0 / 960, 75.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Bridge east corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeEastNightLane3.png", - {260.0 / 960 - .435625, 295.0 / 540 - 70.0 / 1080, 65.0 / 960, 75.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}}, - {// When the east fence is off-screen, these two full bridge-foot - // corners establish lane 4. They never authorize arrival. - {"Bridge west corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestCurrent.png", - {106.0 / 960 - .435, 257.0 / 540, 60.0 / 960, 75.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Bridge east corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeEastCurrent.png", - {260.0 / 960 - .435, 255.0 / 540, 65.0 / 960, 75.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}}, - { - - {"Bridge west corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestBridgeFar.png", - {240.0 / 960 - .61, 246.0 / 540, 64.0 / 960, 60.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"Bridge east corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeEastBridgeFar.png", - {388.0 / 960 - .61, 246.0 / 540, 64.0 / 960, 60.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}}, - {{"Bridge west corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestBridgeMid.png", - {108.0 / 960 - .445, 245.0 / 540, 64.0 / 960, 60.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"South cliff corner", - "PokemonBDSP/Autostory/Route215JoggerV2/SouthCliffBridgeMid.png", - {77.0 / 960 - .445, 295.0 / 540, 90.0 / 960, 80.0 / 540}, - {0, .2, 1, .6}, - .97, - .05}}, - {{"Bridge west corner", - "PokemonBDSP/Autostory/Route215JoggerV2/BridgeWestBridgeNear.png", - {68.0 / 960 - .405, 245.0 / 540, 64.0 / 960, 60.0 / 540}, - {0, .2, 1, .6}, - .90, - .12, - LandmarkFeatures::LOCAL_CONTRAST}, - {"South cliff corner", - "PokemonBDSP/Autostory/Route215JoggerV2/SouthCliffBridgeNear.png", - {37.0 / 960 - .405, 295.0 / 540, 90.0 / 960, 80.0 / 540}, - {0, .2, 1, .6}, - .97, - .05}}}; - for (const auto& approach : approaches){ - auto candidate = match_landmarks(filtered, approach, 2, 1.0 / 128, 1.0 / 54, true); - const double score = std::min(candidate.landmarks[0].score, candidate.landmarks[1].score); - if (best.located && candidate.located && std::abs(best.dy - candidate.dy) > AGREEMENT_Y){ - best.located = false; - best.at_target = false; - best.coarse = true; - return best; - } - candidate.coarse = true; - candidate.at_target = false; - if ((candidate.located && !best.located) || (candidate.located == best.located && score > best_score)){ - best = std::move(candidate); - best_score = score; - } - } - return best; -} -const std::vector& overworld_matchers(){ - static const std::vector matchers = { - {"route204-north-approach", "Route 204 left of sign after Tyler", match_route204_north_approach}, - {"teighler-alcove-corner", "Teighler alcove bottom-left corner", match_teighler_alcove_corner}, - {"route204-north-stairs", "Route 204 north stairs junction", match_route204_north_stairs}, - {"floaroma-center-entrance", "Floaroma Pokemon Center entrance", match_floaroma_center_entrance}, - {"ravaged-path-south-entrance", "Ravaged Path south entrance", match_ravaged_path_south_entrance}, - {"route204-sign", "Route 204 sign before the next trainer", match_route204_sign}, - {"windworks-sign", "Valley Windworks west of the door grunt", match_windworks_sign_approach}, - {"route205-bridge", "Route 205 south of the bridge", match_route205_bridge}, - {"route205-bank-tree", "Route 205 north bank beside the golden tree", match_route205_bank_tree}, - {"route205-zackary", "Route 205 Zackary patrol intercept", match_route205_zackary}, - {"route205-kelsey", "Route 205 Kelsey patrol intercept", match_route205_kelsey}, - {"route205-forest", "Route 205 outside Eterna Forest", match_route205_forest_entrance}, - {"jubilife-street-approach", "Jubilife street before Galactic plaza", match_jubilife_street_approach}, - {"jubilife-plaza-approach", "Jubilife Galactic plaza approach", match_jubilife_plaza_approach}, - {"poketch-landmarks", "Jubilife Poketch landmarks", match_poketch_landmarks}, - {"oreburgh-mart", "Oreburgh Mart emblem", match_oreburgh_mart}, - {"jubilife-center", "Jubilife Pokemon Center entrance", match_jubilife_center_entrance}, - {"eterna-center", "Eterna Pokemon Center doorway", match_eterna_center_entrance}, - {"hearthome-center", "Hearthome Pokemon Center doorway", match_hearthome_center_entrance}, - {"hearthome-center-interior", "Hearthome Pokemon Center interior", match_hearthome_center_interior}, - {"hearthome-fantina", "Hearthome Contest Hall sign + roof emblem (Fantina)", match_hearthome_contest_fantina}, - {"veilstone-city-passage", "Veilstone city passage before the west turn", match_veilstone_city_passage}, - {"veilstone-dawn-stairs", "Veilstone western stairs toward Dawn", match_veilstone_dawn_stairs}, - {"hearthome-gym", "Hearthome Gym doorway (arch + facade)", match_hearthome_gym_entrance}, - {"route208-stair-top", "Route 208 stair-top rocks", match_route208_stair_top}, - {"route209-repel-point", "Route 209 Repel renewal point", match_route209_repel_point}, - {"route215-jogger-corner", "Route 215 east corner beyond the jogger", match_route215_jogger_corner}, - {"route215-bridge-renewal", "Route 215 west bridge renewal", match_route215_bridge_renewal}, - {"route215-ace-renewal", "Route 215 renewal before Ace Trainers", match_route215_ace_renewal}, - {"route215-derek-stairs-renewal", "Route 215 renewal below Derek stairs", match_route215_derek_stairs_renewal}, - {"route215-derek-stair-top-renewal", - "Route 215 renewal above Derek stairs", - match_route215_derek_stair_top_renewal}, - {"route208-hannah-departure", "Route 208 field after Hannah", match_route208_hannah_departure}, - {"eterna-gym", "Eterna Gym doorway", match_eterna_gym_entrance}, - {"galactic-hq-jupiter", "Galactic HQ Jupiter interaction", match_galactic_hq_jupiter}, - {"eterna-gym-statues", "Eterna Gym interior statues", match_eterna_gym_statues}, - {"eterna-underground-house", "Eterna Underground Man house doorway", match_eterna_underground_house}, - {"eterna-underground-man", "Eterna Underground Man interaction", match_eterna_underground_man}, - {"route201-potion-exit", "Route 201 after Potion pickup", match_route201_potion_exit}, - {"sandgem-junction", "Sandgem Center/Mart junction", match_sandgem_junction}, - {"route202-grass-exit", "Route 202 after catching: grass exit", match_route202_grass_exit}, - }; - return matchers; -} -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h deleted file mode 100644 index 6377da938c..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h +++ /dev/null @@ -1,101 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" - -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { -struct OverworldLandmarkResult { - std::string name; - double score = 0, margin = 0; - // Signed fractions of screen width/height, independent of capture resolution. - double dx = 0, dy = 0; - bool accepted = false; - ImageFloatBox search, found; -}; -struct OverworldMatchResult { - std::vector landmarks; - bool located = false, at_target = false; - bool coarse = false; - // Signed fractions of screen width/height, independent of capture resolution. - double dx = 0, dy = 0; -}; -struct OverworldMatcher { - std::string slug, name; - std::function match; -}; -// Register additional functions here; each owns its landmarks and coordinate system. -const std::vector& overworld_matchers(); -OverworldMatchResult match_route204_north_approach(const ImageRGB32& frame); -OverworldMatchResult match_teighler_alcove_corner(const ImageRGB32& frame); -OverworldMatchResult match_route204_north_stairs(const ImageRGB32& frame); -OverworldMatchResult match_floaroma_center_entrance(const ImageRGB32& frame); -OverworldMatchResult match_floaroma_house_entrance(const ImageRGB32& frame); -OverworldMatchResult match_ravaged_path_south_entrance(const ImageRGB32& frame); -OverworldMatchResult match_route204_sign(const ImageRGB32& frame); -OverworldMatchResult match_windworks_sign_approach(const ImageRGB32& frame); -// Interior only: offsets from the player's red cap to the grunt's cyan hair. -// Positive dy > .025 means the player is above him; otherwise below/level. -OverworldMatchResult match_windworks_interior_grunt(const ImageRGB32& frame); -OverworldMatchResult match_route205_bridge(const ImageRGB32& frame); -OverworldMatchResult match_route205_bank_tree(const ImageRGB32& frame); -OverworldMatchResult match_route205_zackary(const ImageRGB32& frame); -OverworldMatchResult match_route205_kelsey(const ImageRGB32& frame); -OverworldMatchResult match_route205_forest_entrance(const ImageRGB32& frame); -// Northern sign: align primarily westward, then correct height below the flower bed. -OverworldMatchResult match_route204_floaroma_turn(const ImageRGB32& frame); -OverworldMatchResult match_jubilife_street_approach(const ImageRGB32& frame); -OverworldMatchResult match_jubilife_plaza_approach(const ImageRGB32& frame); -OverworldMatchResult match_poketch_landmarks(const ImageRGB32& frame); -OverworldMatchResult match_oreburgh_mart(const ImageRGB32& frame); -OverworldMatchResult match_jubilife_center_entrance(const ImageRGB32& frame); -OverworldMatchResult match_eterna_center_entrance(const ImageRGB32& frame); -OverworldMatchResult match_hearthome_center_entrance(const ImageRGB32& frame); -OverworldMatchResult match_hearthome_center_interior(const ImageRGB32& frame); -OverworldMatchResult match_hearthome_contest_fantina(const ImageRGB32& frame); -OverworldMatchResult match_veilstone_city_passage(const ImageRGB32& frame); -OverworldMatchResult match_veilstone_dawn_stairs(const ImageRGB32& frame); -OverworldMatchResult match_hearthome_gym_entrance(const ImageRGB32& frame); -OverworldMatchResult match_route208_stair_top(const ImageRGB32& frame); -OverworldMatchResult match_route208_lower_stair_lane(const ImageRGB32& frame); -// User-marked sign frame and rock pair at the Route 209 renewal point. -OverworldMatchResult match_route209_repel_point(const ImageRGB32& frame); -// Lane 4 east corner; coarse bridge references only guide approach acquisition. -OverworldMatchResult match_route215_jogger_corner(const ImageRGB32& frame); -OverworldMatchResult match_route215_bridge_renewal(const ImageRGB32& frame); -OverworldMatchResult match_route215_ace_renewal(const ImageRGB32& frame); -OverworldMatchResult match_route215_derek_stairs_renewal(const ImageRGB32& frame); -OverworldMatchResult match_route215_derek_stair_top_renewal(const ImageRGB32& frame); -OverworldMatchResult match_route208_hannah_departure(const ImageRGB32& frame); -OverworldMatchResult match_eterna_gym_entrance(const ImageRGB32& frame); -OverworldMatchResult match_galactic_hq_cut(const ImageRGB32& frame); -OverworldMatchResult match_galactic_hq_jupiter(const ImageRGB32& frame); -OverworldMatchResult match_eterna_cycle_shop_entrance(const ImageRGB32& frame); -OverworldMatchResult match_eterna_cycle_shop_interior(const ImageRGB32& frame); -OverworldMatchResult match_eterna_cycle_shop_reward(const ImageRGB32& frame); -OverworldMatchResult match_eterna_bike_riding_card(const ImageRGB32& frame); -OverworldMatchResult match_eterna_cycle_shop_counter(const ImageRGB32& frame); -OverworldMatchResult match_cycling_road_bridge_approach(const ImageRGB32& frame); -OverworldMatchResult match_cycling_road_south_gate_interior(const ImageRGB32& frame); -OverworldMatchResult match_galactic_hq_exterior(const ImageRGB32& frame); -OverworldMatchResult match_eterna_center_departure(const ImageRGB32& frame); -OverworldMatchResult match_eterna_gym_statues(const ImageRGB32& frame); -// Two unique, agreeing landmarks close to the doorway; this confidence only -// authorizes a watched entry attempt, never proves the interior location. -double overworld_entry_confidence(const OverworldMatchResult& result, - double tolerance_x = 1.0 / 50, - double tolerance_y = 1.0 / 27, - double minimum_margin = .15); -OverworldMatchResult match_eterna_underground_house(const ImageRGB32& frame); -OverworldMatchResult match_eterna_underground_man(const ImageRGB32& frame); -OverworldMatchResult match_route201_potion_exit(const ImageRGB32& frame); -OverworldMatchResult match_sandgem_junction(const ImageRGB32& frame); -OverworldMatchResult match_route202_grass_exit(const ImageRGB32& frame); -} -} -} diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 1d73f23833..6326efb75e 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -729,8 +729,6 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.5.h Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.cpp Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.h - Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickView.cpp - Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickView.h Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickWidget.cpp Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6_QQuickWidget.h Source/CommonFramework/VideoPipeline/Backends/MediaServicesQt6.cpp @@ -947,7 +945,6 @@ file(GLOB LIBRARY_SOURCES Source/Controllers/NullControllerWidget.h Source/Controllers/PABotBase2/SerialPABotBase2_SelectorWidget.cpp Source/Controllers/PABotBase2/SerialPABotBase2_SelectorWidget.h - Source/Controllers/PABotBase2/SerialPABotBase_StatusThread.h Source/Controllers/SerialPort/SerialPortPollerQt.cpp Source/Controllers/SerialPort/SerialPortPollerQt.h Source/Integrations/DiscordIntegrationSettings.cpp @@ -1126,7 +1123,6 @@ file(GLOB LIBRARY_SOURCES Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x16_x64_AVX2.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x32_x64_AVX512.h - Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x4_Default.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x64_x64_AVX512.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x8_arm64_NEON.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64x8_x64_SSE42.h @@ -1148,7 +1144,6 @@ file(GLOB LIBRARY_SOURCES Source/Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_64x8_x64_SSE42.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_64xH_Default.h Source/Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_Debugging.h - Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrix.h Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.h Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.tpp Source/Kernels/BinaryMatrix/Kernels_SparseBinaryMatrixCore.h @@ -1554,36 +1549,24 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_OverworldMatchers.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.cpp Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.cpp @@ -1594,8 +1577,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/Battles/PokemonBDSP_ExperienceGainDetector.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.cpp Source/PokemonBDSP/Inference/Battles/PokemonBDSP_StartBattleDetector.h - Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.cpp - Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleOpponentNameReader.h Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxDetector.cpp Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxDetector.h Source/PokemonBDSP/Inference/BoxSystem/PokemonBDSP_BoxGenderDetector.cpp @@ -1664,30 +1645,12 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Options/PokemonBDSP_PlayerModelOption.h Source/PokemonBDSP/Options/PokemonBDSP_ShortcutDirection.cpp Source/PokemonBDSP/Options/PokemonBDSP_ShortcutDirection.h - Source/PokemonBDSP/Panels_PokemonBDSP.cpp - Source/PokemonBDSP/Panels_PokemonBDSP.h Source/PokemonBDSP/PokemonBDSP_Panels.cpp Source/PokemonBDSP/PokemonBDSP_Panels.h Source/PokemonBDSP/PokemonBDSP_Settings.cpp Source/PokemonBDSP/PokemonBDSP_Settings.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_BDSPData.h - Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp - Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h - Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp - Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h - Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp - Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Battle.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Dialog.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Navigation.h Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.cpp Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_BattleMenuReader.h Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp @@ -1696,14 +1659,42 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h Source/PokemonBDSP/PokemonBDSP_Tests.cpp Source/PokemonBDSP/PokemonBDSP_Tests.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Bag.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Battle.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Center.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp @@ -2136,8 +2127,6 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonLA/Options/PokemonLA_TravelLocation.h Source/VideoGames/PokemonLA/Options/QtWidgets/PokemonLA_CustomPathTableWidget.cpp Source/VideoGames/PokemonLA/Options/QtWidgets/PokemonLA_CustomPathTableWidget.h - Source/VideoGames/PokemonLA/Panels_PokemonLA.cpp - Source/VideoGames/PokemonLA/Panels_PokemonLA.h Source/VideoGames/PokemonLA/PokemonLA_Locations.cpp Source/VideoGames/PokemonLA/PokemonLA_Locations.h Source/VideoGames/PokemonLA/PokemonLA_Panels.cpp @@ -2366,8 +2355,6 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonLZA/Programs/PokemonLZA_GameEntry.h Source/VideoGames/PokemonLZA/Programs/PokemonLZA_HyperspaceNavigation.cpp Source/VideoGames/PokemonLZA/Programs/PokemonLZA_HyperspaceNavigation.h - Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MegaShardFarmer.cpp - Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MegaShardFarmer.h Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MenuNavigation.cpp Source/VideoGames/PokemonLZA/Programs/PokemonLZA_MenuNavigation.h Source/VideoGames/PokemonLZA/Programs/PokemonLZA_PostKillCatcher.cpp From 6f5b3d002faa62909d9033fd862d036abd865c6f Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 13:55:51 -0800 Subject: [PATCH 06/98] BDSP AutoStory: the Route 202 regions and slide_to_wall from walk-v4 The table reads points and regions with map_point() and map_region(); the places file keeps only the story worlds a walk plans on. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Sandgem.cpp | 2 +- .../PokemonBDSP_AutoStory_StoryPlaces.cpp | 56 +------------------ .../PokemonBDSP_AutoStory_StoryPlaces.h | 14 +---- .../PokemonBDSP_AutoStory_StoryTable.cpp | 18 +++--- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 +- 5 files changed, 15 insertions(+), 77 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp index 50d28f0cad..dd9bb42eef 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp @@ -183,7 +183,7 @@ std::vector north_through_sign_gap(VideoStream& stream, ProControllerCo const bool on_route = walk_north_through_gap(stream, context); std::vector ends; for (const TilePos& pos : map_region("SandgemTown", "below_sign_gap")){ - const TilePos end = walk_to_wall(maps, maps.to_world(MAP_SANDGEM_TOWN, pos), Direction::NORTH).stop; + const TilePos end = slide_to_wall(maps, maps.to_world(MAP_SANDGEM_TOWN, pos), Direction::NORTH).stop; if (all_on_map(maps, {end}, MAP_ROUTE_202) == on_route){ ends.push_back(end); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp index 951a69d4e3..475acd37e4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -42,6 +42,7 @@ const std::string& banner_name(const std::string& map){ return map; } +// TODO(#114 multi-map view): MapData has no joined view of maps yet. // Twinleaf to Route 202 in one world, at the decomp's world tiles (its // cells are 32 tiles square; Route 201's map starts 7 rows into its cell // and Route 202's a column west of its own). @@ -78,63 +79,8 @@ bool on_world(const StoryWorld& world, const std::string& map){ return false; } -// The tiles of Route 202 we can stand on in the box (x0, y0) to (x1, y1). -std::vector route202_open(int x0, int y0, int x1, int y1){ - const StoryWorld& world = south_world(); - std::vector tiles; - for (int y = y0; y <= y1; y++){ - for (int x = x0; x <= x1; x++){ - if (world.standable(story_to_world(world, "Route202", {x, y}))){ - tiles.push_back({x, y}); - } - } - } - return tiles; -} - -std::vector route202_region(const std::string& region){ - const TilePos catch_spot = map_point("Route202", "catch_spot"); - if (region == "catch_save_area"){ - // Wherever the catch's walks in the grass ended (story_catch_save_area()). - return route202_open(15, 25, 21, 29); - } - if (region == "after_training_area"){ - // The catch spot's column and one either side, two rows up and down. - return route202_open(catch_spot.x - 1, catch_spot.y - 2, catch_spot.x + 1, catch_spot.y + 2); - } - if (region == "catch_grass"){ - // The training area's tall grass, which training walks into. - std::vector grass; - for (const TilePos& pos : route202_region("after_training_area")){ - if (story_map("Route202").tiles.at(pos) == Pokemon::TileKind::TALL_GRASS){ - grass.push_back(pos); - } - } - return grass; - } - if (region == "jubilife_exit"){ - // The open rows at the top: north from any of them is Jubilife. - return route202_open(0, 0, story_map("Route202").tiles.width - 1, 3); - } - return {}; } -} - - -TilePos story_point(const std::string& map, const std::string& point){ - return map_point(map, point); -} - -std::vector story_region(const std::string& map, const std::string& region){ - if (map == "Route202"){ - std::vector tiles = route202_region(region); - if (!tiles.empty()){ - return tiles; - } - } - return map_region(map, region); -} const StoryWorld* story_plan_world(const std::string& from, const std::string& to){ if (on_world(south_world(), from) && on_world(south_world(), to)){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h index 9160d76216..171c3db300 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h @@ -2,9 +2,8 @@ * * From: https://github.com/PokemonAutomation/ * - * Where the story's table and steps stand: tiles of the JSON maps - * ("Route202"), by map and point name (map_point(), map_region()). A walk - * from one map to another is planned on a story world that holds both: the + * Where the story's steps walk: poses are tiles of the JSON maps + * ("Route202"), named by map_point() and map_region(). A walk from one map to another is planned on a story world that holds both: the * outdoors from Twinleaf to Route 202, Jubilife, and each interior of * 00.01 to 00.04 on its own. * @@ -24,15 +23,6 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ -// The point `point` of the map `map` (map_point()). -Pokemon::TilePos story_point(const std::string& map, const std::string& point); - -// The tiles of the region `region` of the map `map`: map_region(), and -// the areas of Route 202 that the maps have no region for yet (where the -// catch may leave us, the training grass, the exit to Jubilife). Throws -// InternalProgramError for an unknown map or region. -std::vector story_region(const std::string& map, const std::string& region); - // The story world a walk from `from` to `to` is planned on, or nothing if // no world holds both. Its maps are named as the game's location banners // name them ("Route 202"), which the walker reads. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 65df817a0a..ad172d0d27 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -16,7 +16,7 @@ #include #include #include -#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" #include "PokemonBDSP_AutoStory_StoryTable.h" namespace PokemonAutomation{ @@ -54,15 +54,15 @@ Pose at(const char* map, int x, int y, std::optional facing = std::nu return Pokemon::tile_pose(map, {TilePos{x, y}}, facing); } Pose at(const char* map, const char* point, std::optional facing = std::nullopt){ - return Pokemon::tile_pose(map, {story_point(map, point)}, facing); + return Pokemon::tile_pose(map, {map_point(map, point)}, facing); } Pose on(const char* map, const char* region){ - return Pokemon::tile_pose(map, story_region(map, region)); + return Pokemon::tile_pose(map, map_region(map, region)); } // The tile below the door `door` of `map` and its neighbours either side: // a door is not lined up with one tile (Jubilife's Center, r85). Pose door_front(const char* map, const char* door){ - const TilePos below = story_point(map, door).next(SOUTH); + const TilePos below = map_point(map, door).next(SOUTH); return Pokemon::tile_pose(map, {below.next(WEST), below, below.next(EAST)}); } @@ -102,6 +102,8 @@ StoryStep legs(std::string name, std::vector legs, std::string why, st StoryStep to_walls(std::string name, std::vector legs, std::string why){ return PokemonBDSP::legs(std::move(name), std::move(legs), std::move(why), "to_wall"); } +// TODO(#114 coord events): the map schema has no scene triggers yet, so +// each row steps onto the decomp's trigger tile, named in its reason. StoryStep step_into(std::string name, Direction direction, std::string why, std::optional pose_after = std::nullopt){ StoryStep step = make(StoryStepKind::STEP, std::move(name), std::move(why)); step.direction = direction; @@ -156,9 +158,9 @@ std::vector intro(){ row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { route("above_the_stairs", at(BEDROOM, 8, 5, NORTH), "the stairs (8, 4) east of the bed"), // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). - step_into("down_the_stairs", NORTH, "onto the stairs", Pokemon::tile_pose(HOME, {story_point(HOME, "stairs"), story_point(HOME, "stairs").next(SOUTH)})), + step_into("down_the_stairs", NORTH, "onto the stairs", Pokemon::tile_pose(HOME, {map_point(HOME, "stairs"), map_point(HOME, "stairs").next(SOUTH)})), // Butters' recording walked 1.1 s south before her first line. - scene("mom_calls", Pokemon::tile_pose(HOME, {story_point(HOME, "stairs"), story_point(HOME, "stairs").next(SOUTH)}), + scene("mom_calls", Pokemon::tile_pose(HOME, {map_point(HOME, "stairs"), map_point(HOME, "stairs").next(SOUTH)}), "Mom calls the player over as they come down; we stay put (assumed)"), route("above_the_door", at(HOME, 6, 9, SOUTH), "past Mom at (7, 8) to above the door"), step_into("mom_warning", SOUTH, "the door's tile (6, 10): Mom's warning (coord event, VAR_PLAYER_HOUSE_STATE 1)"), @@ -250,12 +252,12 @@ std::vector to_the_catch(){ // One walk round the houses ended two tiles short (r252): down // the road first, then the town plan. route("into_sandgem", at(SANDGEM, "route_202_entry_stop"), "south down the road to above the Mart", "robust"), - route("to_center_sign", at(SANDGEM, story_point(SANDGEM, "pokemon_center_sign").x, story_point(SANDGEM, "pokemon_center_sign").y + 1), + route("to_center_sign", at(SANDGEM, map_point(SANDGEM, "pokemon_center_sign").x, map_point(SANDGEM, "pokemon_center_sign").y + 1), "below the Center's sign, its doorway in sight", "town"), // From below the sign the finder read the signs' panels as // doorways (r252, r255): taps instead. One tile short is fine. legs("below_center_door", {{.direction = WEST, - .tiles = story_point(SANDGEM, "pokemon_center_sign").x - story_point(SANDGEM, "pokemon_center_door").x}}, + .tiles = map_point(SANDGEM, "pokemon_center_sign").x - map_point(SANDGEM, "pokemon_center_door").x}}, "taps west below the Center's door", "one_short_ok"), heal("heal_sandgem", "Sandgem Town Pokemon Center", door_front(SANDGEM, "pokemon_center_door")), }), diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 0ab553860a..bab836f96b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -131,7 +131,7 @@ class Test_MartLadyWalk : public UnitTest{ static std::string follow(const StoryWorld& maps, const std::vector& legs, TilePos at, Logger& logger){ for (const LadyLeg& leg : legs){ if (leg.tiles == 0){ - at = walk_to_wall(maps, at, leg.direction).stop; + at = slide_to_wall(maps, at, leg.direction).stop; }else{ const std::optional next = maps.step(at, leg.direction); if (!next){ From 60180322b25de1844362ad886c70fdc3662c11f5 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 13:58:02 -0800 Subject: [PATCH 07/98] BDSP AutoStory: 00.04c's Potions from the Poke Mart lady only The clerk's 5 Potions after two missed visits to the lady needed the shop detectors and item readers from #1717; the lady is the way the console runs took (r261). Drops the Mart, ShopDetectors, ShopItemNameReader and ShopItemPriceReader. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Inference/PokemonBDSP_ShopDetectors.cpp | 273 ---------------- .../Inference/PokemonBDSP_ShopDetectors.h | 123 ------- .../PokemonBDSP_ShopItemNameReader.cpp | 41 --- .../PokemonBDSP_ShopItemNameReader.h | 40 --- .../PokemonBDSP_ShopItemPriceReader.cpp | 41 --- .../PokemonBDSP_ShopItemPriceReader.h | 40 --- .../PokemonBDSP_AutoStory_Events_Sandgem.cpp | 114 +------ .../AutoStory/PokemonBDSP_AutoStory_Mart.cpp | 303 ------------------ .../AutoStory/PokemonBDSP_AutoStory_Mart.h | 61 ---- .../PokemonBDSP_AutoStory_StoryExecutor.h | 5 - .../PokemonBDSP_AutoStory_StoryTable.cpp | 2 +- SerialPrograms/cmake/SourceFiles.cmake | 8 - 12 files changed, 14 insertions(+), 1037 deletions(-) delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp deleted file mode 100644 index fc48e45fa5..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp +++ /dev/null @@ -1,273 +0,0 @@ -/* Shop Detectors - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/ImageTools/ImageStats.h" -#include "CommonFramework/VideoPipeline/VideoOverlay.h" -#include "CommonTools/Images/BinaryImage_FilterRgb32.h" -#include "CommonTools/Images/SolidColorTest.h" -#include "Kernels/Waterfill/Kernels_Waterfill_Session.h" -#include "Kernels/Waterfill/Kernels_Waterfill_Types.h" -#include "PokemonBDSP_ShopItemNameReader.h" -#include "PokemonBDSP_ShopItemPriceReader.h" -#include "PokemonBDSP_ShopDetectors.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// --------------------------------------------------------------------------- -// ShopBuyingWindowDetector -// --------------------------------------------------------------------------- - -// TODO: Fill in the actual coordinates of the blue bottom border strip. -// At 1280x720, the buying-list bottom bar appears near y ≈ 0.93. -// The colour ratio below matches BoxDetector's bottom bar ({0.190, 0.327, 0.482}). -static const ImageFloatBox SHOP_BUYING_WINDOW_BOX{0.020000, 0.960000, 0.750000, 0.035000}; -static const FloatPixel SHOP_BLUE_RATIO{0.190, 0.327, 0.483}; -static constexpr double SHOP_BLUE_MAX_DISTANCE = 0.10; -static constexpr double SHOP_BLUE_MAX_STDDEV = 15.0; - -ShopBuyingWindowDetector::ShopBuyingWindowDetector(Color color) - : m_color(color) - , m_box(SHOP_BUYING_WINDOW_BOX) -{} -void ShopBuyingWindowDetector::make_overlays(VideoOverlaySet& items) const{ - items.add(m_color, m_box); -} -bool ShopBuyingWindowDetector::detect(const ImageViewRGB32& screen){ - ImageStats stats = image_stats(extract_box_reference(screen, m_box)); - return is_solid(stats, SHOP_BLUE_RATIO, SHOP_BLUE_MAX_DISTANCE, SHOP_BLUE_MAX_STDDEV); -} - - -// --------------------------------------------------------------------------- -// ShopQuantityWindowDetector -// --------------------------------------------------------------------------- - -// TODO: Fill in the coordinates of the yellow quantity-input box. -// It typically appears in the centre-right of the screen when selecting quantity. -static const ImageFloatBox SHOP_QUANTITY_BOX{0.800000, 0.570000, 0.100000, 0.140000}; -// Expected colour ratio for the gold/yellow quantity box. -// High R, high G, low B — adjust if the box colour looks different in your capture. -static const FloatPixel SHOP_YELLOW_RATIO{0.58, 0.38, 0.04}; -static constexpr double SHOP_YELLOW_MAX_DISTANCE = 0.20; -static constexpr double SHOP_YELLOW_MAX_STDDEV = 20.0; - -ShopQuantityWindowDetector::ShopQuantityWindowDetector(Color color) - : m_color(color) - , m_dialog(color) - , m_qty_box(SHOP_QUANTITY_BOX) -{} -void ShopQuantityWindowDetector::make_overlays(VideoOverlaySet& items) const{ - m_dialog.make_overlays(items); - items.add(m_color, m_qty_box); -} -bool ShopQuantityWindowDetector::detect(const ImageViewRGB32& screen){ - if (!m_dialog.detect(screen)){ - return false; - } - ImageStats stats = image_stats(extract_box_reference(screen, m_qty_box)); - return is_solid(stats, SHOP_YELLOW_RATIO, SHOP_YELLOW_MAX_DISTANCE, SHOP_YELLOW_MAX_STDDEV); -} - - -// --------------------------------------------------------------------------- -// ShopItemHoverDetector -// --------------------------------------------------------------------------- - -// Color filter for the red hover rectangle. -static constexpr uint32_t RED_MIN = 0xff800000; // R≥128, G=0, B=0 -static constexpr uint32_t RED_MAX = 0xffff6060; // R≤255, G≤96, B≤96 - -// Minimum pixel area for a red blob to be considered (filters noise). -static constexpr size_t RED_MIN_AREA = 200; - -// The hover rectangle must span at least this fraction of the scan area width. -static constexpr double RED_MIN_WIDTH_RATIO = 0.80; - -// Minimum aspect ratio (width/height) — the row is much wider than tall. -static constexpr double RED_MIN_ASPECT = 4.0; - -// Minimum consecutive frames that must confirm before the detector fires. -static constexpr size_t HOVER_MIN_CONSECUTIVE = 3; - -// OCR region X offsets (as fractions of outline row width). -// Tune NAME_X_OFFSET if the first character of the item name is being cut off. -// Name: [ 5%, 70%] → x_offset=0.05, width_fraction=0.65 -// Price: [70%, 100%] → x_offset=0.70, width_fraction=0.30 -static constexpr double NAME_X_OFFSET = 0.08; -static constexpr double NAME_X_WIDTH = 0.65; -static constexpr double PRICE_X_OFFSET = 0.70; -static constexpr double PRICE_X_WIDTH = 0.30; -// Fixed OCR box height in normalized screen coordinates. The waterfill detects -// only the thin red border lines, so outline_box.height is just the border -// thickness. This constant covers the full item row height instead. -static constexpr double OCR_ROW_HEIGHT = 0.06; - -// Maps item slug → expected price slug. -static const std::map ITEM_PRICE_TABLE{ - {"poke-ball", "200"}, - {"great-ball", "600"}, - {"ultra-ball", "1200"}, - {"heal-ball", "300"}, - {"net-ball", "1000"}, - {"nest-ball", "1000"}, - {"dusk-ball", "1000"}, - {"quick-ball", "1000"}, - {"timer-ball", "1000"}, - {"repeat-ball", "1000"}, - {"luxury-ball", "1000"}, - {"potion", "300"}, - {"super-potion", "700"}, - {"hyper-potion", "1200"}, - {"max-potion", "2500"}, - {"full-restore", "3000"}, - {"revive", "1500"}, - {"antidote", "100"}, - {"paralyze-heal", "200"}, - {"awakening", "250"}, - {"burn-heal", "250"}, - {"ice-heal", "250"}, - {"full-heal", "600"}, - {"escape-rope", "550"}, - {"repel", "350"}, - {"super-repel", "500"}, - {"max-repel", "700"}, -}; - -ShopItemHoverDetector::ShopItemHoverDetector( - Logger& logger, - const ImageFloatBox& scan_area, - const std::string& item_slug, - Language language, - Color color -) - : VisualInferenceCallback("ShopItemHoverDetector") - , m_logger(logger) - , m_color(color) - , m_language(language) - , m_scan_area(scan_area) - , m_item_slug(item_slug) - , m_price_slug(ITEM_PRICE_TABLE.at(item_slug)) -{} - -bool ShopItemHoverDetector::find_outline(const ImageViewRGB32& screen, ImageFloatBox& out_box) const{ - using namespace Kernels::Waterfill; - - const ImageViewRGB32 region = extract_box_reference(screen, m_scan_area); - PackedBinaryMatrix matrix = compress_rgb32_to_binary_range(region, RED_MIN, RED_MAX); - - auto session = make_WaterfillSession(matrix); - auto iter = session->make_iterator(RED_MIN_AREA); - - WaterfillObject object; - while (iter->find_next(object, false)){ - if (object.aspect_ratio() < RED_MIN_ASPECT) continue; - if ((double)object.width() / region.width() < RED_MIN_WIDTH_RATIO) continue; - - out_box = ImageFloatBox{ - m_scan_area.x + (double)object.min_x / region.width() * m_scan_area.width, - m_scan_area.y + (double)object.min_y / region.height() * m_scan_area.height, - (double)object.width() / region.width() * m_scan_area.width, - (double)object.height() / region.height() * m_scan_area.height - }; - return true; - } - return false; -} - -bool ShopItemHoverDetector::match_ocr(const ImageViewRGB32& screen, const ImageFloatBox& outline_box) const{ - m_last_name_box = ImageFloatBox{ - outline_box.x + NAME_X_OFFSET * outline_box.width, - outline_box.y, - NAME_X_WIDTH * outline_box.width, - OCR_ROW_HEIGHT - }; - m_last_price_box = ImageFloatBox{ - outline_box.x + PRICE_X_OFFSET * outline_box.width, - outline_box.y, - PRICE_X_WIDTH * outline_box.width, - OCR_ROW_HEIGHT - }; - const ImageFloatBox& name_box = m_last_name_box; - const ImageFloatBox& price_box = m_last_price_box; - - const ShopItemNameReader& name_reader = ShopItemNameReader::instance(); - OCR::StringMatchResult name_result = name_reader.read_substring( - m_logger, m_language, extract_box_reference(screen, name_box) - ); - name_result.clear_beyond_log10p(ShopItemNameReader::MAX_LOG10P); - if (name_result.results.empty()) return false; - if (name_result.results.begin()->second.token != m_item_slug) return false; - - const ShopItemPriceReader& price_reader = ShopItemPriceReader::instance(); - OCR::StringMatchResult price_result = price_reader.read_substring( - m_logger, m_language, extract_box_reference(screen, price_box) - ); - price_result.clear_beyond_log10p(ShopItemPriceReader::MAX_LOG10P); - if (price_result.results.empty()) return false; - if (price_result.results.begin()->second.token != m_price_slug) return false; - - if (!m_ocr_logged){ - m_ocr_logged = true; - name_result.log(m_logger, ShopItemNameReader::MAX_LOG10P, "want: " + m_item_slug); - price_result.log(m_logger, ShopItemPriceReader::MAX_LOG10P, "want: " + m_price_slug); - } - return true; -} - -bool ShopItemHoverDetector::detect(const ImageViewRGB32& screen){ - ImageFloatBox outline_box; - if (!find_outline(screen, outline_box)){ - m_detected_box = ImageFloatBox{0, 0, 0, 0}; - m_last_name_box = ImageFloatBox{0, 0, 0, 0}; - m_last_price_box = ImageFloatBox{0, 0, 0, 0}; - m_ocr_logged = false; - return false; - } - if (!match_ocr(screen, outline_box)){ - m_last_name_box = ImageFloatBox{0, 0, 0, 0}; - m_last_price_box = ImageFloatBox{0, 0, 0, 0}; - return false; - } - m_detected_box = outline_box; - return true; -} - -void ShopItemHoverDetector::make_overlays(VideoOverlaySet& items) const{ - items.add(m_color, m_scan_area); - if (m_detected_box.width > 0){ - items.add(m_color, m_detected_box); - } - if (m_last_name_box.width > 0){ - items.add(COLOR_BLUE, m_last_name_box); - } - if (m_last_price_box.width > 0){ - items.add(COLOR_GREEN, m_last_price_box); - } -} - -bool ShopItemHoverDetector::process_frame(const ImageViewRGB32& frame, WallClock /*timestamp*/){ - // Only run OCR every 5th frame to reduce CPU load. - if (m_frame_count++ % 5 != 0){ - return false; - } - if (!detect(frame)){ - m_trigger_count = 0; - return false; - } - m_trigger_count++; - return m_trigger_count >= HOVER_MIN_CONSECUTIVE; -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h deleted file mode 100644 index bfe3298e66..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h +++ /dev/null @@ -1,123 +0,0 @@ -/* Shop Detectors - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_ShopDetectors_H -#define PokemonAutomation_PokemonBDSP_ShopDetectors_H - -#include -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/Language.h" -#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" -#include "CommonTools/VisualDetector.h" -#include "PokemonBDSP_DialogDetector.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Detects the blue decorative border at the bottom of the screen that appears -// when the mart buying list is open. -class ShopBuyingWindowDetector : public StaticScreenDetector{ -public: - // box: a thin strip covering the blue bottom border (user fills in coordinates). - ShopBuyingWindowDetector(Color color = COLOR_RED); - - virtual void make_overlays(VideoOverlaySet& items) const override; - virtual bool detect(const ImageViewRGB32& screen) override; - -private: - Color m_color; - ImageFloatBox m_box; -}; -class ShopBuyingWindowWatcher : public DetectorToFinder{ -public: - ShopBuyingWindowWatcher(Color color = COLOR_RED) - : DetectorToFinder("ShopBuyingWindowWatcher", std::chrono::milliseconds(250), color) - {} -}; - - -// Detects the quantity-input overlay (yellow/gold box + ShortDialog) that appears -// after selecting an item to buy. -class ShopQuantityWindowDetector : public StaticScreenDetector{ -public: - // box: the area containing the yellow quantity-input box (user fills in coordinates). - ShopQuantityWindowDetector(Color color = COLOR_RED); - - virtual void make_overlays(VideoOverlaySet& items) const override; - virtual bool detect(const ImageViewRGB32& screen) override; - -private: - Color m_color; - ShortDialogDetector m_dialog; - ImageFloatBox m_qty_box; -}; -class ShopQuantityWindowWatcher : public DetectorToFinder{ -public: - ShopQuantityWindowWatcher(Color color = COLOR_RED) - : DetectorToFinder("ShopQuantityWindowWatcher", std::chrono::milliseconds(250), color) - {} -}; - - -// Scans `scan_area` to dynamically locate the red selection outline, then -// verifies the highlighted item by running OCR on the item name and price -// regions within the row and comparing against the expected `item_slug`. -// Fires only when the outline is found AND both OCR checks pass. -class ShopItemHoverDetector : public VisualInferenceCallback{ -public: - // scan_area: normalized screen region that covers the entire shop item list. - // item_slug: identifies the expected item (e.g. "full-restore"). Must match - // a key in ShopItemNameOCR.json and have a known price in the - // internal lookup table. - // language: language used for OCR dictionary lookup. - ShopItemHoverDetector( - Logger& logger, - const ImageFloatBox& scan_area, - const std::string& item_slug, - Language language = Language::English, - Color color = COLOR_CYAN - ); - - bool detect(const ImageViewRGB32& screen); - - // Returns the last outline box confirmed by detect() / process_frame(). - // Valid only after a successful detection; width == 0 if nothing detected yet. - const ImageFloatBox& detected_box() const { return m_detected_box; } - - virtual void make_overlays(VideoOverlaySet& items) const override; - virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; - -private: - // Scans scan_area for a horizontal band of red edges (left + right) that - // indicate the selection outline. Fills out_box with the outline's screen- - // normalized bounds and returns true when found. - bool find_outline(const ImageViewRGB32& screen, ImageFloatBox& out_box) const; - - // Runs OCR on the name and price sub-regions of outline_box. - // Returns true when both match the expected item slug and its price. - bool match_ocr(const ImageViewRGB32& screen, const ImageFloatBox& outline_box) const; - - Logger& m_logger; - Color m_color; - Language m_language; - ImageFloatBox m_scan_area; - std::string m_item_slug; - std::string m_price_slug; - mutable ImageFloatBox m_detected_box{0, 0, 0, 0}; - mutable ImageFloatBox m_last_name_box{0, 0, 0, 0}; - mutable ImageFloatBox m_last_price_box{0, 0, 0, 0}; - mutable bool m_ocr_logged = false; - size_t m_trigger_count = 0; - size_t m_frame_count = 0; -}; - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp deleted file mode 100644 index 96fa48eae0..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/* Shop Item Name Reader - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "CommonTools/OCR/OCR_Routines.h" -#include "PokemonBDSP_ShopItemNameReader.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -ShopItemNameReader::ShopItemNameReader() - : SmallDictionaryMatcher("PokemonBDSP/ShopItemNameOCR.json") -{} - -const ShopItemNameReader& ShopItemNameReader::instance(){ - static ShopItemNameReader reader; - return reader; -} - -OCR::StringMatchResult ShopItemNameReader::read_substring( - Logger& logger, - Language language, - const ImageViewRGB32& image, - double min_text_ratio, double max_text_ratio -) const{ - return match_substring_from_image_multifiltered( - &logger, language, image, - OCR::BLACK_TEXT_FILTERS(), - MAX_LOG10P, MAX_LOG10P_SPREAD, - min_text_ratio, max_text_ratio - ); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h deleted file mode 100644 index ae32b1dda5..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h +++ /dev/null @@ -1,40 +0,0 @@ -/* Shop Item Name Reader - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_ShopItemNameReader_H -#define PokemonAutomation_PokemonBDSP_ShopItemNameReader_H - -#include "CommonFramework/Language.h" -#include "CommonTools/OCR/OCR_SmallDictionaryMatcher.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -class ShopItemNameReader : public OCR::SmallDictionaryMatcher{ -public: - static constexpr double MAX_LOG10P = -1.40; - static constexpr double MAX_LOG10P_SPREAD = 0.50; - -public: - ShopItemNameReader(); - - static const ShopItemNameReader& instance(); - - OCR::StringMatchResult read_substring( - Logger& logger, - Language language, - const ImageViewRGB32& image, - double min_text_ratio = 0.01, double max_text_ratio = 0.50 - ) const; -}; - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp deleted file mode 100644 index 82d6cfba3d..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/* Shop Item Price Reader - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "CommonTools/OCR/OCR_Routines.h" -#include "PokemonBDSP_ShopItemPriceReader.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -ShopItemPriceReader::ShopItemPriceReader() - : SmallDictionaryMatcher("PokemonBDSP/ShopItemPriceOCR.json") -{} - -const ShopItemPriceReader& ShopItemPriceReader::instance(){ - static ShopItemPriceReader reader; - return reader; -} - -OCR::StringMatchResult ShopItemPriceReader::read_substring( - Logger& logger, - Language language, - const ImageViewRGB32& image, - double min_text_ratio, double max_text_ratio -) const{ - return match_substring_from_image_multifiltered( - &logger, language, image, - OCR::BLACK_TEXT_FILTERS(), - MAX_LOG10P, MAX_LOG10P_SPREAD, - min_text_ratio, max_text_ratio - ); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h deleted file mode 100644 index 0d5cc49303..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h +++ /dev/null @@ -1,40 +0,0 @@ -/* Shop Item Price Reader - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_ShopItemPriceReader_H -#define PokemonAutomation_PokemonBDSP_ShopItemPriceReader_H - -#include "CommonFramework/Language.h" -#include "CommonTools/OCR/OCR_SmallDictionaryMatcher.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -class ShopItemPriceReader : public OCR::SmallDictionaryMatcher{ -public: - static constexpr double MAX_LOG10P = -1.40; - static constexpr double MAX_LOG10P_SPREAD = 0.50; - -public: - ShopItemPriceReader(); - - static const ShopItemPriceReader& instance(); - - OCR::StringMatchResult read_substring( - Logger& logger, - Language language, - const ImageViewRGB32& image, - double min_text_ratio = 0.01, double max_text_ratio = 0.50 - ) const; -}; - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp index dd9bb42eef..f65d8c0032 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp @@ -2,7 +2,7 @@ * * From: https://github.com/PokemonAutomation/ * - * 00.04's Potions in Sandgem (the Poke Mart lady, or the clerk) and the + * 00.04's Potions from the Poke Mart lady on Route 201's east end, the * way back to Route 202's save spot, and the stairs into Jubilife. * */ @@ -22,7 +22,6 @@ #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Bag.h" #include "PokemonBDSP_AutoStory_Center.h" -#include "PokemonBDSP_AutoStory_Mart.h" #include "PokemonBDSP_AutoStory_Talks.h" #include "PokemonBDSP_AutoStory_Walks.h" #include "PokemonBDSP_AutoStory_WildBattle.h" @@ -39,74 +38,6 @@ using Pokemon::TilePos; namespace{ -// How many Potions to buy. A Potion is 300 and a new game starts with -// 3000; the money on screen cannot be read yet. Five heal the starter -// through Route 202's four trainers and leave half the money. -constexpr int POTIONS_TO_BUY = 5; - -// From below the Center's door to below the Poke Mart's, ten tiles -// east along the same row, and in. The last leg is open and movement -// is not locked to tiles, so we may stand part of a tile off the door: -// whole-tile steps either side missed it in r121a. The Mart's doorway -// is lit like the Center's, so it is lined up with by sight the same -// way. Two townspeople wander the dirt south of the Mart and stop a -// leg short without the walk knowing (r122a counted to the door's -// row from three rows south of it): where the doorway shows on screen -// says so, and the walk is made once more from there. -void enter_sandgem_poke_mart( - VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, - std::vector& trainers, const RouteTrainerFight& fight -) { - const TilePos door = maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "poke_mart_door")); - const TilePos front = door.next(Pokemon::Direction::SOUTH); - const auto at_front = [&](const TilePos& pos) { return pos.y == front.y && std::abs(pos.x - front.x) <= 1; }; - walk_route(stream, context, run_from_wild_battle, maps, story_door_front(MAP_SANDGEM_TOWN, map_point("SandgemTown", "pokemon_center_door")), - at_front, trainers, fight, "To the Poke Mart"); - const std::vector short_of_door = tiles_short_of_door(stream, context, maps, door, "To the Poke Mart"); - if (!short_of_door.empty()) { - route_status(stream, "To the Poke Mart: stopped short of the door; walking again."); - walk_route(stream, context, run_from_wild_battle, maps, short_of_door, at_front, trainers, fight, "To the Poke Mart again"); - } - enter_lit_door(stream, context, "Sandgem Poke Mart: through the door"); -} - -// Inside the Mart, the clerk's counter is straight ahead of the door -// (every Mart is laid out so; Oreburgh's and Eterna's are walked the -// same way). Walk up to it, buy the Potions, and walk back out. -// buy_items() opens the talk and finds the Potion row by OCR of its -// name and price; the quantity is counted out blind. -void buy_potions_at_the_counter(VideoStream& stream, ProControllerContext& context) { - route_status(stream, "Poke Mart: to the clerk."); - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "Poke Mart: to the clerk"); - if (walk.stop != WalkStop::BLOCKED && walk.stop != WalkStop::DIALOG) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poke Mart: the counter was not reached (" + std::string(walk_stop_name(walk.stop)) + ").", stream); - } - route_status(stream, "Poke Mart: buying " + std::to_string(POTIONS_TO_BUY) + " Potions."); - if (!buy_items(stream, context, {{ShopItem::Potion, POTIONS_TO_BUY}})) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poke Mart: could not buy " + std::to_string(POTIONS_TO_BUY) + " Potions.", stream); - } - leave_door_south(stream, context, run_from_wild_battle, "Poke Mart: out the door"); -} - -// Potions let us heal where we stand. Sandgem's Poke Mart is next -// door east of its Center. The purchase is checked in the Bag after, -// since buy_items() counts the quantity out without reading it. -// Returns where we stand after: below the Mart's door. -std::vector buy_potions_in_sandgem( - VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, - std::vector& trainers, const RouteTrainerFight& fight -) { - enter_sandgem_poke_mart(stream, context, maps, trainers, fight); - buy_potions_at_the_counter(stream, context); - if (!bag_has_item(stream, context, "potion", 1)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Poke Mart: the purchase went through, but no Potion is in the Bag.", stream); - } - return story_door_front(MAP_SANDGEM_TOWN, map_point("SandgemTown", "poke_mart_door")); -} - // A walk_until() callback that keeps every location banner it sees. std::function banner_keeper(std::vector& banners) { return [&banners](const ImageViewRGB32& screen) { @@ -294,47 +225,28 @@ void mart_lady(VideoStream& stream, ProControllerContext& context){ } // 00.04c's potions step, from the save out of Sandgem's Center: the Poke -// Mart lady's, or the clerk's after two misses this run, on to Route 202's -// save spot, where 00.04d's save goes. The Bag must have Potions after. -// The clerk's list is found by reading item names, which needs a reader -// for the game's language; without one, the lady every time. -Pokemon::Outcome sandgem_potions(StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep&){ - VideoStream& stream = executor.stream(); - ProControllerContext& context = executor.context(); - size_t& misses = executor.run_count("mart_lady_misses"); - if (misses < 2 || !story_reads_text()){ - // Onto the doors' row first: out of the door we stand a tile to - // three below it (r261 walked west into the trees from three). - route_turn(stream, context, Direction::EAST); - walk_tiles_dpad(stream, context, run_from_wild_battle, 1, 0, 1, "east under the Center's wall"); - const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); - if (up.stop != WalkStop::BLOCKED){ - fail_step(stream, std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + "."); - } - try{ - mart_lady(stream, context); - }catch (OperationFailedException&){ - misses++; - throw; - } - }else{ - const StoryWorld& maps = story_maps(); - std::vector trainers = story_trainer_sights(); - const RouteTrainerFight fight = [&](const std::string& name){ fight_route_trainer(stream, context, name); }; - const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); - walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); +// Mart lady's 10, on to Route 202's save spot, where 00.04d's save goes. +// The Bag must have Potions after. +void sandgem_potions(VideoStream& stream, ProControllerContext& context){ + // Onto the doors' row first: out of the door we stand a tile to + // three below it (r261 walked west into the trees from three). + route_turn(stream, context, Direction::EAST); + walk_tiles_dpad(stream, context, run_from_wild_battle, 1, 0, 1, "east under the Center's wall"); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); + if (up.stop != WalkStop::BLOCKED){ + fail_step(stream, std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + "."); } + mart_lady(stream, context); if (!bag_has_item(stream, context, "potion", 1)){ fail_step(stream, "00.04: no Potions in the Bag after the shop."); } - return Pokemon::outcome_ok(); } } void add_sandgem_events(StoryEvents& events){ - events.emplace("sandgem_potions", sandgem_potions); + events.emplace("sandgem_potions", console_event(sandgem_potions)); events.emplace("jubilife_stairs", console_event(up_jubilife_stairs)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp deleted file mode 100644 index 93e3504a7f..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp +++ /dev/null @@ -1,303 +0,0 @@ -/* BDSP AutoStory - Poke Mart - * - * From: https://github.com/PokemonAutomation/ - * - * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. - * - */ - -#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/Inference/PokemonBDSP_SelectionArrow.h" -#include "PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h" -#include "PokemonBDSP_AutoStory_Mart.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; - - -namespace{ - -const char* shop_item_name(ShopItem item){ - switch (item){ - case ShopItem::FullRestore: - return "Full restore"; - case ShopItem::MaxPotion: - return "Max potion"; - case ShopItem::HyperPotion: - return "Hyper potion"; - case ShopItem::SuperPotion: - return "Super potion"; - case ShopItem::Potion: - return "Potion"; - case ShopItem::Antidote: - return "Antidote"; - case ShopItem::ParalyzHeal: - return "Paralyz heal"; - case ShopItem::Awakening: - return "Awakening"; - case ShopItem::Repel: - return "Repel"; - case ShopItem::SuperRepel: - return "Super repel"; - case ShopItem::MaxRepel: - return "Max repel"; - case ShopItem::EscapeRope: - return "Escape rope"; - default: - return "Unknown"; - } -} - -// Returns the OCR slug used as the dictionary key in ShopItemNameOCR.json. -const char* shop_item_slug(ShopItem item){ - switch (item){ - case ShopItem::FullRestore: - return "full-restore"; - case ShopItem::MaxPotion: - return "max-potion"; - case ShopItem::HyperPotion: - return "hyper-potion"; - case ShopItem::SuperPotion: - return "super-potion"; - case ShopItem::Potion: - return "potion"; - case ShopItem::Antidote: - return "antidote"; - case ShopItem::ParalyzHeal: - return "paralyze-heal"; - case ShopItem::Awakening: - return "awakening"; - case ShopItem::Repel: - return "repel"; - case ShopItem::SuperRepel: - return "super-repel"; - case ShopItem::MaxRepel: - return "max-repel"; - case ShopItem::EscapeRope: - return "escape-rope"; - default: - return "unknown"; - } -} - -// {MaxRepel, 2, 4}, {FullRestore, 0, 3} → "4 Max repel, 3 Full restore" -std::string make_buy_label(const std::vector& items){ - std::string label; - for (size_t i = 0; i < items.size(); i++){ - if (i > 0){ - label += ", "; - } - if (items[i].quantity == BUY_MAX){ - label += "max "; - }else{ - label += std::to_string(items[i].quantity) + " "; - } - label += shop_item_name(items[i].item); - } - return label; -} - -} - -bool buy_items(VideoStream& stream, - ProControllerContext& context, - const std::vector& items, - Language language){ - if (items.empty()){ - return true; - } - - const std::string label = make_buy_label(items); - context.wait_for_all_requests(); - pbf_wait(context, 500ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - pbf_press_button(context, BUTTON_A, 80ms, 1000ms); - context.wait_for_all_requests(); - - - // ── Coordinate constants ───────────────────────────────────────────────── - - // Area where the selection arrow appears at the vendor greeting menu (Buy/Sell/Leave). - static const ImageFloatBox VENDOR_ARROW_BOX{0.630000, 0.550000, 0.070000, 0.120000}; - - // Area where the selection arrow appears at the YES/NO confirmation dialog. - static const ImageFloatBox CONFIRM_ARROW_BOX{0.700000, 0.620000, 0.070000, 0.120000}; - - // Full area containing all item rows in the shop list. - // The hover detector scans this region to dynamically locate the red outline. - static const ImageFloatBox ITEM_SCAN_AREA{0.417000, 0.100000, 0.540000, 0.625000}; - - static constexpr size_t MAX_OUTER = 50; - static constexpr size_t MAX_ITEM_SEARCH = 50; // max iterations per item before giving up - - // ── Outer state machine ────────────────────────────────────────────────── - for (size_t outer = 0; outer < MAX_OUTER; outer++){ - SelectionArrowFinder arrow_vendor(stream.overlay(), VENDOR_ARROW_BOX, COLOR_GREEN); - ShopBuyingWindowWatcher buying_window; - ShortDialogWatcher dialog(COLOR_RED); - - // case 0: no detectors fired — press A in a for-loop (5 presses) while watching. - stream.log("Starting to loop over items"); - int state = run_until(stream, - context, - [](ProControllerContext& ctx){ - for (int i = 0; i < 5; i++){ - pbf_press_button(ctx, BUTTON_A, 80ms, 1000ms); - } - }, - {{arrow_vendor}, {buying_window}, {dialog}}); - - context.wait_for_all_requests(); - pbf_wait(context, 500ms); - - switch (state){ - - case 0: // case 1: SelectionArrow at vendor menu → press A to choose "Buy" - stream.log("Found the selection arrow, pressing A to enter the buying window...", COLOR_GREEN); - pbf_press_button(context, BUTTON_A, 80ms, 500ms); - break; - - case 1: // case 2: buying window open → buy each requested item - stream.log("Found the buying window, searching for the right item...", COLOR_GREEN); - { - bool buying_failed = false; - - for (const ItemToBuy& item : items){ - stream.log("Buying " + std::to_string(item.quantity) + "x " + shop_item_slug(item.item)); - if (buying_failed){ - break; - } - - // Detector fires only when the red outline is found AND both - // name and price OCR match the expected item slug. - ShopItemHoverDetector hover(stream.logger(), - ITEM_SCAN_AREA, - shop_item_slug(item.item), - language, - COLOR_CYAN); - ShopQuantityWindowWatcher qty_window; - SelectionArrowFinder confirm_arrow(stream.overlay(), CONFIRM_ARROW_BOX, COLOR_GREEN); - - bool item_bought = false; - bool hover_confirmed = false; - for (size_t inner = 0; inner < MAX_ITEM_SEARCH && !item_bought; inner++){ - context.wait_for_all_requests(); - - int sub = run_until(stream, - context, - [](ProControllerContext& ctx){ - ctx.wait_for(1000ms); - }, - {{confirm_arrow}, {qty_window}, {hover}}); - - context.wait_for_all_requests(); - - stream.log("buy_items: sub=" + std::to_string(sub), COLOR_WHITE); - - switch (sub){ - - case 0: // YES/NO confirmation arrow → confirm purchase - stream.log("Found the confirmation to buy arrow, pressing A to buy", COLOR_GREEN); - { - context.wait_for_all_requests(); - ShopBuyingWindowWatcher back_in_shop; - run_until(stream, - context, - [](ProControllerContext& ctx){ - for (int i = 0; i < 10; i++){ - pbf_press_button(ctx, BUTTON_A, 80ms, 800ms); - } - }, - {{back_in_shop}}); - item_bought = true; - break; - } - - case 1: // quantity window - if (!hover_confirmed){ - // Quantity window appeared before we confirmed the right item — - // an earlier A press accidentally opened the wrong item's selector. - // Press B to go back to the list and continue searching. - stream.log("Unexpected quantity window (wrong item), pressing B...", COLOR_YELLOW); - pbf_press_button(context, BUTTON_B, 80ms, 800ms); - break; - } - stream.log("Found the quantity window", COLOR_GREEN); - if (item.quantity == BUY_MAX){ - stream.log("Buying max quantity"); - pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); - }else{ - stream.log("Buying " + std::to_string(item.quantity) + " of the item"); - for (int q = 1; q < item.quantity; q++){ - pbf_press_dpad(context, DPAD_UP, 80ms, 300ms); - } - } - pbf_press_button(context, BUTTON_A, 80ms, 300ms); - pbf_wait(context, 1000ms); - break; - - case 2: // correct item highlighted → press A to open quantity selector - stream.log("Found the right item, pressing A to buy a certain quantity...", COLOR_GREEN); - hover_confirmed = true; - pbf_press_button(context, BUTTON_A, 80ms, 300ms); - pbf_wait(context, 1000ms); - break; - - default: // outline not found or wrong item → advance to next row - stream.log("Not on the right item, moving...", COLOR_GREEN); - pbf_press_dpad(context, DPAD_DOWN, 80ms, 300ms); - pbf_wait(context, 300ms); - break; - } - } - - if (!item_bought){ - stream.log(label + ": buy_items: could not locate " + shop_item_name(item.item) + - " in shop list.", - COLOR_RED); - buying_failed = true; - } - } - - if (!buying_failed){ - stream.log("All items bought, exiting shop...", COLOR_GREEN); - pbf_mash_button(context, BUTTON_B, 5000ms); - return true; - } - break; - } - - case 2: // case 3: unexpected ShortDialog → press B until back in buying window - { - ShopBuyingWindowWatcher back_in_shop; - run_until(stream, - context, - [](ProControllerContext& ctx){ - pbf_mash_button(ctx, BUTTON_B, 5000ms); - }, - {{back_in_shop}}); - break; - } - - default: // outer default: unrecognised state → press B to recover - pbf_press_button(context, BUTTON_B, 80ms, 3000ms); - break; - } - } - - stream.log(label + ": buy_items: failed to complete after max iterations.", COLOR_RED); - return false; -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h deleted file mode 100644 index 4eead40649..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h +++ /dev/null @@ -1,61 +0,0 @@ -/* BDSP AutoStory - Poke Mart - * - * From: https://github.com/PokemonAutomation/ - * - * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Mart_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Mart_H - -#include -#include "CommonFramework/Language.h" -#include "CommonFramework/Tools/VideoStream.h" -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -enum class ShopItem{ - FullRestore, - MaxPotion, - HyperPotion, - SuperPotion, - Potion, - Antidote, - ParalyzHeal, - Awakening, - Repel, - SuperRepel, - MaxRepel, - EscapeRope, -}; - -// Pass as the `quantity` field to buy the maximum available amount. -inline constexpr int BUY_MAX = -1; - -struct ItemToBuy{ - ShopItem item = ShopItem::Potion; - int quantity = 0; // positive count to buy, or BUY_MAX -}; - -// Walk up to a vendor (already talking to them), navigate through the Buy menu, -// and purchase each item in `items` in order. The function presses DPAD_DOWN -// until the matching item name is found via OCR, so items may appear in any -// list order. Returns true on success. -bool buy_items( - VideoStream& stream, - ProControllerContext& context, - const std::vector& items, - Language language = Language::English -); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h index 1da372f489..74de2ce4ff 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -99,11 +99,6 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ void set_stop_rows(std::set rows){ m_stop_rows = std::move(rows); } - // A count that lasts the whole program run, across reloads (e.g. the - // Poke Mart lady's misses, after which the clerk sells the Potions). - size_t& run_count(const std::string& name){ - return m_run_counts[name]; - } // Walks from the tiles of `from` on its map to any of `goal`'s tiles, // fighting the trainers who see us this attempt. Returns where it left // us, as a pose on `goal`'s map. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index ad172d0d27..6980ece6a6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -266,7 +266,7 @@ std::vector to_the_catch(){ std::vector to_jubilife(){ StoryStep potions = make(StoryStepKind::SHOP, "potions", - "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp), the clerk's 5 after two misses"); + "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp)"); potions.items = {{.item = "potion", .quantity = 10}}; potions.variant = "sandgem_potions"; potions.pose_after = at(ROUTE_202, "save_spot"); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 6326efb75e..38983cdbce 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1603,12 +1603,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/PokemonBDSP_ReceivePokemonDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.cpp Source/PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h - Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.cpp - Source/PokemonBDSP/Inference/PokemonBDSP_ShopDetectors.h - Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.cpp - Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemNameReader.h - Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.cpp - Source/PokemonBDSP/Inference/PokemonBDSP_ShopItemPriceReader.h Source/PokemonBDSP/Inference/PokemonBDSP_VSSeekerReaction.cpp Source/PokemonBDSP/Inference/PokemonBDSP_VSSeekerReaction.h Source/PokemonBDSP/Inference/Rng/PokemonBDSP_BlinkExtraction.cpp @@ -1677,8 +1671,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Mart.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.cpp From e617908eeedd5e17d773c32ebb08c9be73495f97 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:09:15 -0800 Subject: [PATCH 08/98] BDSP AutoStory events: point the comments at the JSON maps Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 6 +++--- .../AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp | 2 +- .../AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index ec36462008..ea7a0736d5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -263,7 +263,7 @@ void mine_hall(VideoStream& stream, ProControllerContext& context){ // 01.03: the planned route down the corridor to Roark, his talk, and back out of the mine. void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ - // Planned on the coal room's tile grid (Data/OreburghMine): the + // Planned on the coal room's tile grid (Maps/OreburghMine.json): the // corridor's rocks zigzag a row at a time, the room's second row passes // under the worker on the first, and the east side leads down to the // bottom row below Roark. @@ -290,8 +290,8 @@ void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ // column (s03e2 missed him by that much), so look for him with nudges // either side. Any tap here can start a wild battle, which leaves us // off his column (r180a, even on his row west of his rock): then line up - // again from the east end of the bottom row, three taps west and north to the wall on his row (Data/ - // OreburghMine: columns 23 to 25 have that wall above). + // again from the east end of the bottom row, three taps west and north to the wall on his row + // (Maps/OreburghMine.json: columns 23 to 25 have that wall above). bool talked = false; for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ if (attempt > 0){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp index 7445903086..fa8ccc06ff 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp @@ -175,7 +175,7 @@ void oreburgh_welcome(VideoStream& stream, ProControllerContext& context){ Pokemon::Outcome route_203_to_row_18(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ VideoStream& stream = executor.stream(); ProControllerContext& context = executor.context(); - // On the grid (Data/PokemonBDSP_AutoStory_Route203, from the decomp, + // On the grid (Maps/Route203.json, from the decomp, // its east stretch checked by hand): south from the Gate down the sand, // west along the sand's bottom row and on under the girl's row, which // runs straight to Jubilife as row 18. Trainers the map does not list diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index bdda697a2c..075d997103 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -80,7 +80,7 @@ void north_out_of_jubilife(VideoStream& stream, ProControllerContext& context){ // 01.08: Route 204 South on its grid to below the Ravaged Path's door. void route_204_south_to_the_door(VideoStream& stream, ProControllerContext& context){ - // On the grid decoded from the game's data (Data/..._Route204South): + // On the grid decoded from the game's data (Maps/Route204South.json): // a tile west onto the tall grass column that runs up to the cliff, // north up it, then west along the cliff to the signboard beside the // door (the offline plan: 3 legs). Other trainers' sight lines are kept @@ -111,7 +111,7 @@ void into_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ // 01.09: through the Ravaged Path, smashing its rocks. void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ - // On the traced grid (Data/PokemonBDSP_AutoStory_RavagedPath): from the + // On the traced grid (Maps/RavagedPath.json): from the // south door up the stairs to the tile west of one of the two rocks // across the lower corridor, smash it, then east and south to the // corridor's east end; the way out is a tile west of it, south. @@ -160,8 +160,8 @@ void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context // 01.10: Route 204 north on the human's line, into Floaroma. void route_204_to_floaroma(VideoStream& stream, ProControllerContext& context){ - // The line a human walked (Data/PokemonBDSP_AutoStory_Route204, - // floaroma_waypoints()): a tile south out of the cave's mouth, west along + // The line a human walked (Maps/Route204North.json, + // the region floaroma_waypoints): a tile south out of the cave's mouth, west along // the row under the cliff past the Youngster who wanders there, up the // stairs, east along the corridor into Aroma Lady Taylor's battle, north // through the tall grass, west below the flower beds, and north between From a3b6ec822f407e1d1b44a528d8d4df27c15a2644 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:10:26 -0800 Subject: [PATCH 09/98] BDSP AutoStory: CRLF in the simulation tests' header Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- ...emonBDSP_AutoStory_StorySimulation_Tests.h | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h index b893232ca3..9af37ca9b4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h @@ -1,27 +1,27 @@ -/* BDSP AutoStory Story Simulation Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H -#define PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H - -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Segments 00 and 01 run end to end without a console: the story table, -// the shared segment runner and its retry policy, against a fake game -// that plans every route and walk on the built-in maps. -void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database); - - - -} -} -} -#endif +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Segments 00 and 01 run end to end without a console: the story table, +// the shared segment runner and its retry policy, against a fake game +// that plans every route and walk on the built-in maps. +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database); + + + +} +} +} +#endif From dc496e38d93610257650bcee07db290766221e7e Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:52:37 -0800 Subject: [PATCH 10/98] BDSP AutoStory: talk_or_fight passes the battle format to fight_story_battle Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp index da879b444c..56a36230a1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp @@ -69,7 +69,7 @@ bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std } if (battle_menu.detect(*frame.frame) || battle_menu_standard.detect(*frame.frame) || black.detect(*frame.frame)){ route_status(stream, "Fighting " + label + "."); - fight_story_battle(stream, context, label, doubles); + fight_story_battle(stream, context, label, doubles ? BattleFormat::DOUBLES : BattleFormat::SINGLES); // Lines after the battle (Roark's badge, r211d) keep the menu // from opening: finish them before the Potion. if (dialogue_opens(stream, context, 3000ms)){ From 5fcbf6debad28a610d13cba03872977549d785f1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:05:54 -0800 Subject: [PATCH 11/98] BDSP AutoStory: nav/story-steps' include paths and protected step kinds; walk-v4's walker API Story engine includes move to Pokemon/AutoStory/; the executor's per-kind methods are protected, as in StoryStepExecutor. Walks take a stick position, planned walks take RouteTrainers and a plan, StoryWorld's queries are free functions, and the program sets the story language once. A planned route step keeps the Route 202 trainers it fought off m_trainers until the next attempt, as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory.cpp | 4 +- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 12 +++--- .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 38 ++++++++--------- .../PokemonBDSP_AutoStory_Events_Route203.cpp | 19 ++++----- .../PokemonBDSP_AutoStory_Events_Route204.cpp | 41 ++++++++----------- .../PokemonBDSP_AutoStory_Events_Sandgem.cpp | 26 ++++++------ .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 25 +++++++---- .../PokemonBDSP_AutoStory_StoryExecutor.h | 41 ++++++++++--------- ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 13 +++--- .../PokemonBDSP_AutoStory_StoryTable.h | 2 +- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 12 +++--- .../AutoStory/PokemonBDSP_AutoStory_Talks.cpp | 6 +-- .../AutoStory/PokemonBDSP_AutoStory_Walks.cpp | 10 ++--- SerialPrograms/cmake/SourceFiles.cmake | 12 ------ 14 files changed, 122 insertions(+), 139 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index 6fede05b43..b988667453 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -12,7 +12,7 @@ #include "CommonFramework/Notifications/ProgramNotifications.h" #include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" #include "Pokemon/AutoStory/Pokemon_AutoStoryStats.h" -#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" #include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP_AutoStory_StoryTable.h" @@ -138,7 +138,7 @@ void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerCo throw UserSetupError(env.console, "The start checkpoint must not come after the end checkpoint."); } // The reads that use text (Location.h) take the game's language from here. - g_story_language = RUN.language(); + set_story_language(RUN.language()); StoryEnv story{env, context, STARTER, PLAYER_NAME}; StoryExecutor executor(story); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index fc0002bd1c..f5be33bb75 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -38,8 +38,8 @@ namespace{ // them: a tile west (the first press only turns), then up the steps. void up_the_steps(VideoStream& stream, ProControllerContext& context){ route_turn(stream, context, Direction::WEST); - walk_for(stream, context, run_from_wild_battle, -1, 0, 200ms, "To the steps"); - const WalkResult up = walk_for(stream, context, run_from_wild_battle, 0, 1, 480ms, "Up the steps"); + walk_for(stream, context, run_from_wild_battle, {-1, 0}, 200ms, "To the steps"); + const WalkResult up = walk_for(stream, context, run_from_wild_battle, {0, 1}, 480ms, "Up the steps"); if (up.stop == WalkStop::DIALOG || up.stop == WalkStop::TRANSITION){ fail_step(stream, std::string("Up the steps: ") + walk_stop_name(up.stop) + "."); } @@ -127,7 +127,7 @@ void meet_the_president(VideoStream& stream, ProControllerContext& context){ for (size_t tiles = 0; tiles < 4; tiles++){ hold_for(stream, context, Direction::EAST, 160ms, "East into the road"); } - walk_until(stream, context, run_from_wild_battle, 0, 1, 6 * ROUTE_MS_PER_TILE_UP_DOWN + 800ms, "The Poketch Company president"); + walk_until(stream, context, run_from_wild_battle, {0, 1}, 6 * ROUTE_MS_PER_TILE_UP_DOWN + 800ms, "The Poketch Company president"); // He walks up after the "!" and only then talks. ShortDialogWatcher opened(COLOR_CYAN); if (wait_until(stream, context, 5000ms, {{opened}}) != 0){ @@ -321,7 +321,7 @@ void jubilife_center_to_street(VideoStream& stream, ProControllerContext& contex pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 2), 200ms); route_turn(stream, context, Direction::WEST); pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 3), 200ms); - if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ + if (walk_to_talk(stream, context, run_from_wild_battle, {0, 1}, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ fail_step(stream, "Jubilife Pokemon Center: the nurse did not answer."); } // A throughout: Yes to "rest your Pokemon?" (B would answer No). @@ -335,7 +335,7 @@ void jubilife_center_to_street(VideoStream& stream, ProControllerContext& contex // door's fade may or may not read as a transition. WalkResult out{WalkStop::LIMIT, 0ms, 0}; for (size_t walk = 0; walk < 3 && out.stop != WalkStop::BLOCKED; walk++){ - out = walk_until(stream, context, run_from_wild_battle, 0, -1, 8000ms, "out of the Pokemon Center"); + out = walk_until(stream, context, run_from_wild_battle, {0, -1}, 8000ms, "out of the Pokemon Center"); if (out.stop == WalkStop::DIALOG || out.stop == WalkStop::ARRIVED){ break; } @@ -377,7 +377,7 @@ void east_to_barry(VideoStream& stream, ProControllerContext& context){ context.wait_for_all_requests(); WalkResult walk{WalkStop::LIMIT, 0ms, 0}; for (size_t stretch = 0; stretch < 3 && walk.stop == WalkStop::LIMIT; stretch++){ - walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 9000ms, "east to Route 203"); + walk = walk_until(stream, context, run_from_wild_battle, {1, 0}, 9000ms, "east to Route 203"); } barry = walk.stop == WalkStop::DIALOG; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index ea7a0736d5..0136ae9258 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -21,6 +21,7 @@ #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Talks.h" @@ -50,8 +51,8 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ // the Center's east corner stops a walk east on these rows a tile east of // the door (by hand): walk to it, north to the Center's front, tap back // one, and shift one tap either side. - walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 5000ms, "east to the fence below the Center"); - walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); + walk_to_wall(stream, context, run_from_wild_battle, {1, 0}, 5000ms, "east to the fence below the Center"); + walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 3000ms, "north to the Center's front"); // The door wants us near its middle: a turn and a tap west (half a tile // too far, by hand) or a turn alone leave north blocked by the facade // beside it. The facade blocks north for two tiles either side of the @@ -82,7 +83,7 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ // door's column, and the walk back out drifted into the yard (r188b). // Oreburgh's sand reads up to 34 % warm outside (r189a), the floor // 56 to 64 % inside. - const WalkResult in = walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", + const WalkResult in = walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && inside_now())){ inside = true; @@ -99,7 +100,7 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ context.wait_for(2000ms); // he runs off } // Talked in the door's column: the door is free now. - walk_until(stream, context, run_from_wild_battle, 0, 1, 2500ms, "north into the Center's door", + walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); inside = inside_now(); } @@ -112,7 +113,7 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ // and heal; ends at the counter facing her. The door is straight south of // her (by hand, 2026-10-31: north from the door stopped at the counter). void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context){ - if (walk_to_talk(stream, context, run_from_wild_battle, 0, 1, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ + if (walk_to_talk(stream, context, run_from_wild_battle, {0, 1}, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ fail_step(stream, "Oreburgh Pokemon Center: the nurse did not answer."); } // A throughout: Yes to "rest your Pokemon?" (B would answer No). @@ -166,7 +167,7 @@ bool oreburgh_gym_battles(VideoStream& stream, ProControllerContext& context){ } size_t stuck = 0; for (size_t leg = 0; leg < 10; leg++){ - const WalkResult up = walk_until_no_wild(stream, context, 0, 1, 8000ms, "north up the gym"); + const WalkResult up = walk_until_no_wild(stream, context, {0, 1}, 8000ms, "north up the gym"); if (up.stop == WalkStop::DIALOG){ if (talk_or_fight(stream, context, "Gym trainer") && in_pokemon_center(stream)){ return false; @@ -269,8 +270,7 @@ void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ // bottom row below Roark. const StoryWorld& maps = oreburgh_mine_maps(); auto world = [&](const Pokemon::TilePos& pos){ return maps.to_world(MAP_OREBURGH_MINE, pos); }; - std::vector no_trainers; - auto in_the_way = [&](const std::string& who){ talk_or_fight(stream, context, who); }; + RouteTrainers no_trainers{{}, [&](const std::string& who){ talk_or_fight(stream, context, who); }}; std::vector at; for (const Pokemon::TilePos& pos : map_region("OreburghMine", "corridor_top")){ at.push_back(world(pos)); @@ -281,10 +281,8 @@ void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ // him (on the console, r97). Then south to the bottom wall and two tiles // west puts us below him. const Pokemon::TilePos roark = world(map_point("OreburghMine", "roark")); - at = walk_route(stream, context, run_from_wild_battle, maps, at, - [&](const Pokemon::TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }, - no_trainers, in_the_way, "To the stretch east of Roark"); - walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); + at = walk_route(stream, context, run_from_wild_battle, maps, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }, "To the stretch east of Roark", {}, route_plan_costs()}); + walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 2000ms, "north to the wall on Roark's row"); // His rock and the bottom wall end these legs (see walk_to_wall()). The // two taps west from there land within a third of a tile or so of his // column (s03e2 missed him by that much), so look for him with nudges @@ -296,21 +294,21 @@ void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ if (attempt > 0){ // The battle may leave us on his row or the bottom one. - walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom row"); - walk_to_wall(stream, context, run_from_wild_battle, 1, 0, 6000ms, "east along the bottom row"); + walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom row"); + walk_to_wall(stream, context, run_from_wild_battle, {1, 0}, 6000ms, "east along the bottom row"); route_turn(stream, context, Direction::WEST); for (size_t tap = 0; tap < 3; tap++){ pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); } - walk_to_wall(stream, context, run_from_wild_battle, 0, 1, 2000ms, "north to the wall on Roark's row"); + walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 2000ms, "north to the wall on Roark's row"); } // A wild battle on the way eats into the walk's time (r100): walk again. for (size_t walk = 0; walk < 3; walk++){ - if (walk_to_wall(stream, context, run_from_wild_battle, -1, 0, 6000ms, "west to the rock beside Roark").wild_battles == 0){ + if (walk_to_wall(stream, context, run_from_wild_battle, {-1, 0}, 6000ms, "west to the rock beside Roark").wild_battles == 0){ break; } } - walk_to_wall(stream, context, run_from_wild_battle, 0, -1, 2000ms, "south to the bottom wall"); + walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom wall"); route_turn(stream, context, Direction::WEST); pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); @@ -369,9 +367,7 @@ void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ // no wall stops us a row short of it, so the route ends on the corner // at (14,10), where that leg started, and the walk does the rest. const Pokemon::TilePos exit = world(map_point("OreburghMine", "corridor_exit_start")); - at = walk_route(stream, context, run_from_wild_battle, maps, at, - [&](const Pokemon::TilePos& pos){ return pos == exit; }, - no_trainers, in_the_way, "Back to the corridor's top"); + at = walk_route(stream, context, run_from_wild_battle, maps, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == exit; }, "Back to the corridor's top", {}, route_plan_costs()}); // Up into the entrance hall (a transition, as on the way in), then on // north out of the mine: in r103 that walk left the mine without a // transition we saw and ended against the sign above the stairs, which @@ -393,7 +389,7 @@ void oreburgh_center_heal(VideoStream& stream, ProControllerContext& context){ // of the mine we stand below its sign; a tile east is the Center door's // column, and north on it goes in, or stops on the front rows. hold_tiles(stream, context, Direction::EAST, 1); - walk_until(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into the Center's door", + walk_until(stream, context, run_from_wild_battle, {0, 1}, 12000ms, "north into the Center's door", [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); size_t floor = 0; if (VideoSnapshot frame = stream.video().snapshot()){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp index fa8ccc06ff..97cba07a08 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp @@ -20,6 +20,7 @@ #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Talks.h" @@ -63,7 +64,7 @@ Pokemon::Outcome route_203_east_to_the_gate(StoryExecutor& executor, Pokemon::Ru } }; for (size_t leg = 0; leg < 40 && !inside; leg++){ - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203"); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203"); if (walk.stop == WalkStop::TRANSITION){ inside = true; break; @@ -79,11 +80,11 @@ Pokemon::Outcome route_203_east_to_the_gate(StoryExecutor& executor, Pokemon::Ru // first (a step north there does not move us, so rows are counted // from it), then a row at a time south. At the route's end this is the // corner by the second sign, with the Gate's opening two rows down. - walk_until(stream, context, run_from_wild_battle, 0, 1, 6000ms, "north to the top wall"); + walk_until(stream, context, run_from_wild_battle, {0, 1}, 6000ms, "north to the top wall"); bool moved_on = false; for (size_t row = 1; row <= 10 && !moved_on; row++){ step_south(stream, context); - const WalkResult next = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); + const WalkResult next = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); if (next.stop == WalkStop::TRANSITION){ inside = true; } @@ -111,7 +112,7 @@ Pokemon::Outcome route_203_east_to_the_gate(StoryExecutor& executor, Pokemon::Ru // 01.02: the Hiker, who gives HM Rock Smash, when the walk east has not met him. void the_hiker(VideoStream& stream, ProControllerContext& context){ // The Hiker steps up to us as we walk east. - const WalkResult to_hiker = walk_until(stream, context, run_from_wild_battle, 1, 0, 4000ms, "to the Hiker"); + const WalkResult to_hiker = walk_until(stream, context, run_from_wild_battle, {1, 0}, 4000ms, "to the Hiker"); if (to_hiker.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ fail_step(stream, "Oreburgh Gate: the Hiker did not stop us."); } @@ -136,7 +137,7 @@ void gate_east_to_oreburgh(VideoStream& stream, ProControllerContext& context){ bool city = false; size_t stuck = 0; for (size_t leg = 0; leg < 12 && !city; leg++){ - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 1, 0, 10000ms, "east through the Oreburgh Gate"); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east through the Oreburgh Gate"); if (walk.stop == WalkStop::LIMIT || walk.stop == WalkStop::TRANSITION){ continue; } @@ -180,12 +181,10 @@ Pokemon::Outcome route_203_to_row_18(StoryExecutor& executor, Pokemon::RunState& // west along the sand's bottom row and on under the girl's row, which // runs straight to Jubilife as row 18. Trainers the map does not list // are fought where they stop us. - std::vector trainers; - auto fight = [&](const std::string& who){ + RouteTrainers trainers{{}, [&](const std::string& who){ talk_or_fight(stream, context, "Route 203: " + who); - }; - const std::vector at = walk_route(stream, context, run_from_wild_battle, route203_maps(), map_region("Route203", "gate_exit"), - on_route203_west_line, trainers, fight, "Route 203 to row 18"); + }}; + const std::vector at = walk_route(stream, context, run_from_wild_battle, route203_maps(), map_region("Route203", "gate_exit"), trainers, RouteWalkPlan{on_route203_west_line, "Route 203 to row 18", {}, route_plan_costs()}); if (at.size() != 1){ fail_step(stream, "Route 203: the walk to row 18 did not end on a known tile."); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index 075d997103..77856738e0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -20,6 +20,8 @@ #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Talks.h" @@ -71,7 +73,7 @@ void north_out_of_jubilife(VideoStream& stream, ProControllerContext& context){ // The street's column 14 carries on as the route's; the location banner // names Route 204 as we cross. A D-pad walk keeps the column; it may // stop a tile or two past the border, which the entry set allows for. - const WalkResult out = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 8000ms, "north out of Jubilife", location_banner_showing); + const WalkResult out = walk_until_dpad(stream, context, run_from_wild_battle, {0, 1}, 8000ms, "north out of Jubilife", {.arrived = location_banner_showing}); if (out.stop != WalkStop::ARRIVED){ fail_step(stream, "Jubilife: no Route 204 banner walking north up the street."); } @@ -89,12 +91,9 @@ void route_204_south_to_the_door(VideoStream& stream, ProControllerContext& cont // who is not a trainer wanders in front of the door, and the walker // waits for him. { - std::vector trainers = route204_south_trainer_sights(); - auto fight = [&](const std::string& who){ talk_or_fight(stream, context, who); }; + RouteTrainers trainers{route204_south_trainer_sights(), [&](const std::string& who){ talk_or_fight(stream, context, who); }}; const std::vector at = walk_route(stream, context, run_from_wild_battle, route204_south_maps(), - map_region("Route204South", "jubilife_entry"), - [](const Pokemon::TilePos& pos){ return pos == map_point("Route204South", "below_door"); }, - trainers, fight, "Route 204 South to the Ravaged Path", route204_south_tiles_to_avoid(), town_plan_costs()); + map_region("Route204South", "jubilife_entry"), trainers, RouteWalkPlan{[](const Pokemon::TilePos& pos){ return pos == map_point("Route204South", "below_door"); }, "Route 204 South to the Ravaged Path", route204_south_tiles_to_avoid(), town_plan_costs()}); if (at.size() != 1 || !(at[0] == map_point("Route204South", "below_door"))){ fail_step(stream, "Route 204: the walk did not end below the Ravaged Path's door."); } @@ -116,7 +115,6 @@ void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context // across the lower corridor, smash it, then east and south to the // corridor's east end; the way out is a tile west of it, south. { - std::vector no_trainers; // Blocked short of a wall in here, it is a rock the plan counted on // being gone (r237a: the walker took one for a person and said No). auto in_the_way = [&](const std::string& who){ @@ -124,14 +122,13 @@ void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context talk_or_fight(stream, context, who); } }; + RouteTrainers no_trainers{{}, in_the_way}; std::vector at{map_point("RavagedPath", "entrance")}; const StoryWorld rocks_in = ravaged_path_maps(); - at = walk_route(stream, context, run_from_wild_battle, rocks_in, at, - [&](const Pokemon::TilePos& pos){ + at = walk_route(stream, context, run_from_wild_battle, rocks_in, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ const auto& before = map_region("RavagedPath", "before_rocks"); return std::find(before.begin(), before.end(), pos) != before.end(); - }, - no_trainers, in_the_way, "To a rock across the corridor"); + }, "To a rock across the corridor", {}, route_plan_costs()}); route_turn(stream, context, Direction::EAST); if (!talk_with_nudges(stream, context, run_from_wild_battle, Direction::EAST, "the rock east of us", 2000ms)){ fail_step(stream, "Ravaged Path: nothing answered facing the rock east of us."); @@ -146,9 +143,7 @@ void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context smashed.push_back(pos.next(Direction::EAST)); } const StoryWorld rocks_out = ravaged_path_maps(smashed); - at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, - [&](const Pokemon::TilePos& pos){ return pos == map_point("RavagedPath", "east_end"); }, - no_trainers, in_the_way, "To the corridor's east end"); + at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == map_point("RavagedPath", "east_end"); }, "To the corridor's east end", {}, route_plan_costs()}); dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); const WalkResult out = walk_dir(stream, context, Direction::SOUTH, 4000ms, "south out of the Ravaged Path"); if (out.stop != WalkStop::TRANSITION && out.stop != WalkStop::LIMIT){ @@ -167,20 +162,18 @@ void route_204_to_floaroma(VideoStream& stream, ProControllerContext& context){ // through the tall grass, west below the flower beds, and north between // the beds into Floaroma. Whole tiles on the D-pad, corner to corner. { - std::vector trainers = route204_north_trainer_sights(); - auto fight = [&](const std::string& who){ + RouteTrainers trainers{route204_north_trainer_sights(), [&](const std::string& who){ const bool doubles = who.find("Twins") != std::string::npos || who.find("someone") != std::string::npos; talk_or_fight(stream, context, who, false, doubles); - }; + }}; std::vector at{map_point("Route204North", "cave_exit")}; - at = walk_waypoints(stream, context, run_from_wild_battle, route204_north_maps(), at, map_region("Route204North", "floaroma_waypoints"), - trainers, fight, "To the path into Floaroma"); + at = walk_waypoints(stream, context, run_from_wild_battle, route204_north_maps(), at, trainers, WaypointWalkPlan{map_region("Route204North", "floaroma_waypoints"), "To the path into Floaroma", waypoint_plan_costs()}); if (!std::all_of(at.begin(), at.end(), in_floaroma_path)){ fail_step(stream, "Route 204: the walk did not end in the path into Floaroma."); } // North until the town's banner shows: the checkpoint ends right // there, where the save goes (r247a walked 20 s on and overshot). - const WalkResult in = walk_until_dpad(stream, context, run_from_wild_battle, 0, 1, 12000ms, "north into Floaroma", location_banner_showing); + const WalkResult in = walk_until_dpad(stream, context, run_from_wild_battle, {0, 1}, 12000ms, "north into Floaroma", {.arrived = location_banner_showing}); if (in.stop != WalkStop::ARRIVED){ fail_step(stream, "Route 204: no location banner walking north into Floaroma."); } @@ -197,13 +190,13 @@ void route_204_to_floaroma(VideoStream& stream, ProControllerContext& context){ // 01.11: west 8 along the flower field's bottom row, north until the old woman stops us. void west_8_north_to_the_old_woman(VideoStream& stream, ProControllerContext& context){ { - const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 8, "west 8 to the flower field"); + const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, {-1, 0}, 8, "west 8 to the flower field"); if (west.stop != WalkStop::LIMIT || west.moved_chunks < 7){ fail_step(stream, "Floaroma: west 8 to the flower field moved " + std::to_string(west.moved_chunks) + " tiles."); } // Blocked, or out of taps with the last ones bumping: either way // she is in front of us (r250a: 3 steps, 2 bumps, "limit"). - const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 5, "north until the old woman"); + const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, {0, 1}, 5, "north until the old woman"); if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 5){ fail_step(stream, "Floaroma: nothing stopped the walk north to the old woman."); } @@ -212,11 +205,11 @@ void west_8_north_to_the_old_woman(VideoStream& stream, ProControllerContext& co // 01.11: one tile west, then a tap north: the old man stands there, so the tap only turns. void below_the_old_man(VideoStream& stream, ProControllerContext& context){ - const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, -1, 0, 1, "west 1 below the old man"); + const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, {-1, 0}, 1, "west 1 below the old man"); if (west.moved_chunks < 1){ fail_step(stream, "Floaroma: the step west below the old man did not move."); } - const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, 0, 1, 3, "north until the old man"); + const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, {0, 1}, 3, "north until the old man"); if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 3){ fail_step(stream, "Floaroma: nothing stopped the walk north to the old man."); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp index f65d8c0032..3982b68686 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp @@ -18,6 +18,7 @@ #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Bag.h" @@ -70,7 +71,7 @@ bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context) using Pokemon::Direction; std::vector banners; for (size_t nudges = 0;; nudges++){ - const WalkResult north = walk_until(stream, context, run_from_wild_battle, 0, 1, 8000ms, "North through the gap", banner_keeper(banners)); + const WalkResult north = walk_until(stream, context, run_from_wild_battle, {0, 1}, 8000ms, "North through the gap", banner_keeper(banners)); if (north.stop == WalkStop::TRANSITION || north.stop == WalkStop::DIALOG || north.wild_battles > 0){ OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "North through the gap: " + std::string(walk_stop_name(north.stop)) + " instead of a wall.", stream); @@ -85,7 +86,7 @@ bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context) } route_status(stream, "North through the gap: blocked at once, below a sign or the Mart; a tile west."); route_turn(stream, context, Direction::WEST); - walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 1), "A tile west"); + walk_for(stream, context, run_from_wild_battle, {-1, 0}, route_open_hold(Direction::WEST, 1), "A tile west"); route_turn(stream, context, Direction::NORTH); } context.wait_for_all_requests(); @@ -110,7 +111,7 @@ std::vector north_through_sign_gap(VideoStream& stream, ProControllerCo using Pokemon::Direction; route_status(stream, "To the gap between the Center's and the Mart's signs: west 6."); route_turn(stream, context, Direction::WEST); - walk_for(stream, context, run_from_wild_battle, -1, 0, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); + walk_for(stream, context, run_from_wild_battle, {-1, 0}, route_open_hold(Direction::WEST, 6), "To the gap between the signs"); const bool on_route = walk_north_through_gap(stream, context); std::vector ends; for (const TilePos& pos : map_region("SandgemTown", "below_sign_gap")){ @@ -132,17 +133,15 @@ std::vector north_through_sign_gap(VideoStream& stream, ProControllerCo // of town on a map that had it wrong, walked into a house. void walk_to_route_202_save_spot( VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, - std::vector at, std::vector& trainers, const RouteTrainerFight& fight + std::vector at, RouteTrainers& trainers ) { const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end")); const TilePos save_spot = maps.to_world(MAP_ROUTE_202, map_point("Route202", "save_spot")); if (!all_on_map(maps, at, MAP_ROUTE_202)) { - at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), - [&](const TilePos& pos) { return pos == row_end; }, trainers, fight, "To the trees east of the Poke Mart"); + at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos) { return pos == row_end; }, "To the trees east of the Poke Mart", {}, route_plan_costs()}); at = north_through_sign_gap(stream, context, maps); } - walk_route(stream, context, run_from_wild_battle, maps, std::move(at), - [&](const TilePos& pos) { return pos == save_spot; }, trainers, fight, "Back to Route 202"); + walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos) { return pos == save_spot; }, "Back to Route 202", {}, route_plan_costs()}); } // North into Jubilife until Dawn's scene starts (00.04d's @@ -153,7 +152,7 @@ void walk_to_route_202_save_spot( // are further west. The scene itself is the next step. void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { for (size_t attempt = 0; attempt < 6; attempt++) { - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, 0, 1, 4000ms, "to Dawn in Jubilife"); + const WalkResult walk = walk_until(stream, context, run_from_wild_battle, {0, 1}, 4000ms, "to Dawn in Jubilife"); if (walk.stop == WalkStop::TRANSITION) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Jubilife: left the area instead of meeting Dawn.", stream); @@ -219,9 +218,8 @@ void mart_lady(VideoStream& stream, ProControllerContext& context){ walk_until_stopped(stream, context, Direction::EAST, 40, "East along the path"); walk_until_stopped(stream, context, Direction::NORTH, 2, "North to the front row"); const StoryWorld& maps = story_maps(); - std::vector trainers = story_trainer_sights(); - const RouteTrainerFight fight = [&](const std::string& name){ fight_route_trainer(stream, context, name); }; - walk_to_route_202_save_spot(stream, context, maps, {maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end"))}, trainers, fight); + RouteTrainers trainers{story_trainer_sights(), [&](const std::string& name){ fight_route_trainer(stream, context, name); }}; + walk_to_route_202_save_spot(stream, context, maps, {maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end"))}, trainers); } // 00.04c's potions step, from the save out of Sandgem's Center: the Poke @@ -231,8 +229,8 @@ void sandgem_potions(VideoStream& stream, ProControllerContext& context){ // Onto the doors' row first: out of the door we stand a tile to // three below it (r261 walked west into the trees from three). route_turn(stream, context, Direction::EAST); - walk_tiles_dpad(stream, context, run_from_wild_battle, 1, 0, 1, "east under the Center's wall"); - const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 3000ms, "north to the Center's front"); + walk_tiles_dpad(stream, context, run_from_wild_battle, {1, 0}, 1, "east under the Center's wall"); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, {0, 1}, 3000ms, "north to the Center's front"); if (up.stop != WalkStop::BLOCKED){ fail_step(stream, std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + "."); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 6f132ad841..c331439a69 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -17,10 +17,11 @@ #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "Pokemon/Navigation/Pokemon_TilePose.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Actions.h" @@ -138,15 +139,21 @@ Pose StoryExecutor::walk_to(const Pose& from, const Pose& goal, const StoryStep& for (const TilePos& pos : Pokemon::tiles_of(goal)){ targets.push_back(story_to_world(*maps, goal.map, pos)); } - std::vector no_trainers; + // Only Route 202's trainers are known; the ones fought stay off + // m_trainers until the next attempt. const bool south = maps == story_plan_world("Route202", "Route202"); - const std::vector at = walk_route( - stream(), context(), run_from_wild_battle, *maps, start, + RouteTrainers trainers{ + south ? m_trainers : std::vector{}, + [this](const std::string& trainer){ fight_route_trainer(stream(), context(), trainer); } + }; + const RouteWalkPlan plan{ [&](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, - south ? m_trainers : no_trainers, - [this](const std::string& trainer){ fight_route_trainer(stream(), context(), trainer); }, label_of(step), {}, *costs - ); + }; + const std::vector at = walk_route(stream(), context(), run_from_wild_battle, *maps, start, trainers, plan); + if (south){ + m_trainers = std::move(trainers.sights); + } std::vector local; for (const TilePos& pos : at){ const std::optional tile = story_to_local(*maps, goal.map, pos); @@ -193,7 +200,7 @@ Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ route_turn(stream(), context(), leg.direction); const auto [x, y] = dpad_of(leg.direction); const size_t tiles = (size_t)leg.tiles; - const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, x, y, tiles, step.name); + const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, tiles, step.name); stream().log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); @@ -218,7 +225,7 @@ Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ route_turn(stream(), context(), step.direction); const auto [x, y] = dpad_of(step.direction); - const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, x, y, 1, step.name); + const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); std::vector at; for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h index 74de2ce4ff..74042cb605 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -3,7 +3,7 @@ * From: https://github.com/PokemonAutomation/ * * BDSP's way of doing the shared story steps - * (Pokemon/Navigation/Pokemon_StorySteps.h) on the console. A route is + * (Pokemon/AutoStory/Pokemon_StorySteps.h) on the console. A route is * planned on the joined maps that hold its start and its target * (story_plan_world()), from the tiles the run's pose holds, and walked by * the walker, which fights the trainers who see us and runs from wild @@ -31,8 +31,8 @@ #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" -#include "Pokemon/Navigation/Pokemon_StorySegments.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" namespace PokemonAutomation{ @@ -70,12 +70,12 @@ struct StoryEnv{ }; -// The plan costs a ROUTE step's variant names: "" the route costs, -// "town" town_plan_costs(), "robust" the planner's defaults. Nothing for -// another name. -std::optional story_route_costs(const std::string& variant); - - +// The plan costs a ROUTE step's variant names: "" the route costs, +// "town" town_plan_costs(), "robust" the planner's defaults. Nothing for +// another name. +std::optional story_route_costs(const std::string& variant); + + // Does BDSP's kinds of story step on the console. class StoryExecutor : public Pokemon::StoryStepExecutor{ public: @@ -90,24 +90,25 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ ProControllerContext& context(){ return m_env.context; } + // The rows whose failure ends the program (StoryFailure::STOP): a + // failed step there throws on instead of failing the step. + void set_stop_rows(std::set rows){ + m_stop_rows = std::move(rows); + } + // A new attempt of a row: every trainer can see us again. + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override; + virtual Pokemon::Outcome run(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + +protected: // The row running now. const std::string& checkpoint() const{ return m_checkpoint; } - // The rows whose failure ends the program (StoryFailure::STOP): a - // failed step there throws on instead of failing the step. - void set_stop_rows(std::set rows){ - m_stop_rows = std::move(rows); - } // Walks from the tiles of `from` on its map to any of `goal`'s tiles, // fighting the trainers who see us this attempt. Returns where it left // us, as a pose on `goal`'s map. Pokemon::Pose walk_to(const Pokemon::Pose& from, const Pokemon::Pose& goal, const Pokemon::StoryStep& step); - // A new attempt of a row: every trainer can see us again. - virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override; - virtual Pokemon::Outcome run(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - // The intro of a new game, from the title screen to the bedroom. virtual Pokemon::Outcome new_game(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; // verify_tutorial_settings(): fast text, Autosave off. @@ -151,8 +152,8 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ // Route 202's trainers who can still see us, in story_plan_world()'s // world tiles; all of them again at each attempt. std::vector m_trainers; - std::map m_failures; - std::map m_run_counts; + std::map m_failures; + std::map m_run_counts; std::set m_stop_rows; }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp index 8e91db3f8d..cbde99eed3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -21,9 +21,9 @@ #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" -#include "Pokemon/Navigation/Pokemon_StorySegments.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" -#include "Pokemon/Navigation/Pokemon_TilePose.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "Utils/PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP_AutoStory_StoryPlaces.h" #include "PokemonBDSP_AutoStory_StoryTable.h" @@ -98,7 +98,7 @@ Walked plan_route(const Pose& from, const StoryStep& step){ for (const TilePos& pos : plan->empty() ? start : plan->back().after){ const std::optional tile = story_to_local(*world, goal.map, pos); if (!tile || !contains(Pokemon::tiles_of(goal), *tile)){ - return {std::nullopt, step.name + ": the plan may end off the target, at " + world->describe(pos) + ".", walked.plan}; + return {std::nullopt, step.name + ": the plan may end off the target, at " + describe(*world, pos) + ".", walked.plan}; } local.push_back(*tile); } @@ -124,9 +124,9 @@ Walked walk_legs(const Pose& from, const StoryStep& step){ std::vector next; for (TilePos pos : at){ for (int i = 0; i < leg.tiles; i++){ - const std::optional stepped = world->step(pos, leg.direction); + const std::optional stepped = PokemonBDSP::step(*world, pos, leg.direction); if (!stepped){ - return {std::nullopt, step.name + ": " + direction_name(leg.direction) + " is blocked at " + world->describe(pos) + ".", ""}; + return {std::nullopt, step.name + ": " + direction_name(leg.direction) + " is blocked at " + describe(*world, pos) + ".", ""}; } if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ next.push_back(pos); @@ -185,6 +185,7 @@ class FakeGame : public Pokemon::StoryStepExecutor{ } return Pokemon::StoryStepExecutor::run(state, step); } +protected: virtual Outcome route(RunState& state, const StoryStep& step) override{ return moved(plan_route(state.pose, step), "no_plan"); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h index bf9390ea77..db2b8f0683 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h @@ -3,7 +3,7 @@ * From: https://github.com/PokemonAutomation/ * * The story from a new game to Floaroma, 00.00 to 01.11, as rows of the - * shared story steps (Pokemon/Navigation/Pokemon_StorySteps.h): plain + * shared story steps (Pokemon/AutoStory/Pokemon_StorySteps.h): plain * data, as FRLG's table is, so the tests plan its routes and run it * against a fake game. Each row but 00.00 saves at its start, which is * where the row before it ends. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index bab836f96b..00a9d683d0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -12,7 +12,7 @@ #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "Data/PokemonBDSP_AutoStory_Maps.h" #include "Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "Data/PokemonBDSP_AutoStory_RoutePlanner.h" @@ -133,17 +133,17 @@ class Test_MartLadyWalk : public UnitTest{ if (leg.tiles == 0){ at = slide_to_wall(maps, at, leg.direction).stop; }else{ - const std::optional next = maps.step(at, leg.direction); + const std::optional next = PokemonBDSP::step(maps, at, leg.direction); if (!next){ - return leg.name + ": blocked at " + maps.describe(at) + "."; + return leg.name + ": blocked at " + describe(maps, at) + "."; } at = *next; } // The tap east lands on whichever row we stood on. if (&leg != &legs.front() && !(at == leg.end)){ - return leg.name + ": ends at " + maps.describe(at) + ", not " + maps.describe(leg.end) + "."; + return leg.name + ": ends at " + describe(maps, at) + ", not " + describe(maps, leg.end) + "."; } - logger.log(leg.name + ": " + maps.describe(at)); + logger.log(leg.name + ": " + describe(maps, at)); } return ""; } @@ -172,7 +172,7 @@ class Test_Route204NorthLine : public UnitTest{ std::vector at{map_point("Route204North", "cave_exit")}; for (size_t i = 0; i < waypoints.size(); i++){ const TilePos goal = waypoints[i]; - const std::string name = std::format("Route 204 North stretch {} to {}", i + 1, route.describe(goal)); + const std::string name = std::format("Route 204 North stretch {} to {}", i + 1, describe(route, goal)); const auto plan = plan_robust_route(route, at, [&](const TilePos& pos){ return pos == goal; }, waypoint_plan_costs()); if (!plan){ return name + ": no plan."; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp index 56a36230a1..71fa16bc24 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp @@ -219,7 +219,7 @@ void finish_talk(VideoStream& stream, ProControllerContext& context, DialogueRea // For the log only; a talk is checked by its box opening and closing. VideoSnapshot screen = stream.video().snapshot(); if (screen && story_reads_text()){ - read.text = OCR::ocr_read(g_story_language, extract_box_reference(*screen.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + read.text = OCR::ocr_read(story_language(), extract_box_reference(*screen.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); } stream.log("[Talk] " + label + ": the dialogue reads \"" + read.text + "\"."); size_t quiet = 0; @@ -263,7 +263,7 @@ DialogueRead talk_walking(VideoStream& stream, ProControllerContext& context, Di const auto [x, y] = dpad_of(direction); const bool across = direction == Direction::EAST || direction == Direction::WEST; const Milliseconds limit = (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN) + 800ms; - const WalkResult walked = walk_to_talk(stream, context, run_from_wild_battle, x, y, limit, label); + const WalkResult walked = walk_to_talk(stream, context, run_from_wild_battle, {x, y}, limit, label); if (walked.stop != WalkStop::DIALOG){ stream.log("[Talk] " + label + ": nobody answered."); return read; @@ -299,7 +299,7 @@ DialogueRead read_sign(VideoStream& stream, ProControllerContext& context, Direc // Only to say which sign, when there is a reader for the game's // language; the box opening and closing is the check. if (story_reads_text()){ - read.text = OCR::ocr_read(g_story_language, extract_box_reference(*shown.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); + read.text = OCR::ocr_read(story_language(), extract_box_reference(*shown.frame, SIGN_TEXT), OCR::PageSegMode::SINGLE_BLOCK); } stream.log("[Sign] " + label + ": reads \"" + read.text + "\" (change " + std::to_string(opened) + ")."); for (size_t presses = 0; presses < 3 && !read.closed; presses++){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp index eef6af021a..ac59adaad6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp @@ -50,7 +50,7 @@ void walk_until_stopped(VideoStream& stream, ProControllerContext& context, Dire // holds still meanwhile, and that read as a wall two tiles into the // walk west to the Lab's sign (r132a). route_turn(stream, context, direction); - const WalkResult walked = walk_until(stream, context, run_from_wild_battle, x, y, limit, label); + const WalkResult walked = walk_until(stream, context, run_from_wild_battle, {x, y}, limit, label); stream.log("[Walk] " + label + ": stopped on " + walk_stop_name(walked.stop) + "."); if (walked.stop == WalkStop::TRANSITION || walked.stop == WalkStop::DIALOG){ fail_step(stream, label + ": " + walk_stop_name(walked.stop) + " instead of a stop."); @@ -69,7 +69,7 @@ void walk_until_stopped(VideoStream& stream, ProControllerContext& context, Dire void hold_for(VideoStream& stream, ProControllerContext& context, Direction direction, Milliseconds hold, const std::string& label){ const auto [x, y] = dpad_of(direction); - const WalkResult walk = walk_for(stream, context, run_from_wild_battle, x, y, hold, label); + const WalkResult walk = walk_for(stream, context, run_from_wild_battle, {x, y}, hold, label); if (walk.stop == WalkStop::DIALOG || walk.stop == WalkStop::TRANSITION){ fail_step(stream, label + ": " + walk_stop_name(walk.stop) + "."); } @@ -87,13 +87,13 @@ void tap_tiles(VideoStream& stream, ProControllerContext& context, Direction dir void nudge(VideoStream& stream, ProControllerContext& context, Direction direction, const std::string& label){ route_turn(stream, context, direction); - walk_for(stream, context, run_from_wild_battle, direction == Direction::EAST ? 1 : -1, 0, 80ms, label); + walk_for(stream, context, run_from_wild_battle, {direction == Direction::EAST ? 1.0 : -1.0, 0.0}, 80ms, label); } bool door_north_by_scene(VideoStream& stream, ProControllerContext& context, const std::string& label){ context.wait_for_all_requests(); VideoSnapshot before = stream.video().snapshot(); - const WalkResult up = walk_until(stream, context, run_from_wild_battle, 0, 1, 1200ms, label); + const WalkResult up = walk_until(stream, context, run_from_wild_battle, {0, 1}, 1200ms, label); context.wait_for(1500ms); if (up.stop == WalkStop::TRANSITION){ return true; @@ -131,7 +131,7 @@ WalkResult walk_dir( ){ const double x = dir == Direction::EAST ? 1 : dir == Direction::WEST ? -1 : 0; const double y = dir == Direction::NORTH ? 1 : dir == Direction::SOUTH ? -1 : 0; - return walk_until(stream, context, run_from_wild_battle, x, y, limit, label, arrived); + return walk_until(stream, context, run_from_wild_battle, {x, y}, limit, label, arrived); } // `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 7c7d4e1119..39070432d0 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1704,20 +1704,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp - Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.cpp Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomousState.cpp From 2ebc3a30eb1655e66b4ec03019c492558f810360 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:11:21 -0800 Subject: [PATCH 12/98] BDSP AutoStory: code health, without changing what the steps do talk_or_fight() takes TalkExpect and BattleFormat for its two bools; the long event functions (Jubilife's Center, Oreburgh's Center door and Roark, Route 203's walk east) are split into their parts, unchanged; the beta and developer panels move to a helper so the AutoStory panel fits make_panels(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Panels.cpp | 31 ++- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 32 ++- .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 252 ++++++++++-------- .../PokemonBDSP_AutoStory_Events_Route203.cpp | 108 ++++---- .../PokemonBDSP_AutoStory_Events_Route204.cpp | 2 +- .../PokemonBDSP_AutoStory_Events_Sandgem.cpp | 32 +-- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 2 +- ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 35 ++- .../AutoStory/PokemonBDSP_AutoStory_Talks.cpp | 10 +- .../AutoStory/PokemonBDSP_AutoStory_Talks.h | 13 +- 10 files changed, 303 insertions(+), 214 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp index c35f8a047a..21c0c74c6c 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Panels.cpp @@ -59,6 +59,25 @@ PanelListFactory::PanelListFactory() ) {} +namespace{ + +// The programs shown only in beta builds or developer mode. +void add_beta_and_developer_panels(std::vector& ret){ + if (IS_BETA_VERSION || STATIC_GLOBALS.DEVELOPER_MODE){ + ret.emplace_back("---- Untested/Beta/WIP ----"); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back(make_SingleSwitchProgram()); + } + if (STATIC_GLOBALS.DEVELOPER_MODE){ + ret.emplace_back("---- Developer Tools ----"); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back(make_SingleSwitchProgram()); + } +} + +} + std::vector PanelListFactory::make_panels() const{ std::vector ret; @@ -103,17 +122,7 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back("---- Glitches (v1.1.2) ----"); ret.emplace_back(make_SingleSwitchProgram()); - if (IS_BETA_VERSION || STATIC_GLOBALS.DEVELOPER_MODE){ - ret.emplace_back("---- Untested/Beta/WIP ----"); - ret.emplace_back(make_SingleSwitchProgram()); - ret.emplace_back(make_SingleSwitchProgram()); - } - if (STATIC_GLOBALS.DEVELOPER_MODE){ - ret.emplace_back("---- Developer Tools ----"); - ret.emplace_back(make_SingleSwitchProgram()); - ret.emplace_back(make_SingleSwitchProgram()); - ret.emplace_back(make_SingleSwitchProgram()); - } + add_beta_and_developer_panels(ret); return ret; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index f5be33bb75..b2eb52a1da 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -301,19 +301,9 @@ void rowan_after_the_battle(VideoStream& stream, ProControllerContext& context){ finish_dialogue(stream, context, BUTTON_B, "Rowan and Dawn after the battle"); } -// 01.01: from the Center's PC, the nurse and out, then the holds to the street east to Route 203; -// nothing when 00.05 has left us outdoors on that street. -void jubilife_center_to_street(VideoStream& stream, ProControllerContext& context){ - // From 00.05g we are already outdoors on the street east to Route 203, - // healed on arriving in Jubilife: straight to the walk east. - bool outdoors = false; - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - outdoors = !indoors(*screen.frame); - } - if (outdoors){ - stream.log("[Route] 01.01: outdoors; skipping the Pokemon Center."); - }else{ +// 01.01 from the save at the Jubilife Center's PC: healed at the nurse, +// out of the door and through the plaza to the street east to Route 203. +void jubilife_heal_and_out_to_street(VideoStream& stream, ProControllerContext& context){ // The save stands us at the PC, right of the counter, facing it; west // is the counter's end. Two tiles south clears it, then three west is // the floor emblem's column, with the nurse north and the door south. @@ -355,6 +345,22 @@ void jubilife_center_to_street(VideoStream& stream, ProControllerContext& contex route_turn(stream, context, Direction::SOUTH); pbf_move_left_joystick(context, {0, -1}, 600ms, 300ms); context.wait_for_all_requests(); +} + +// 01.01: from the Center's PC, the nurse and out, then the holds to the street east to Route 203; +// nothing when 00.05 has left us outdoors on that street. +void jubilife_center_to_street(VideoStream& stream, ProControllerContext& context){ + // From 00.05g we are already outdoors on the street east to Route 203, + // healed on arriving in Jubilife: straight to the walk east. + bool outdoors = false; + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + outdoors = !indoors(*screen.frame); + } + if (outdoors){ + stream.log("[Route] 01.01: outdoors; skipping the Pokemon Center."); + }else{ + jubilife_heal_and_out_to_street(stream, context); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index 0136ae9258..d4b4c7d2b5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -39,6 +39,47 @@ using Pokemon::Direction; namespace{ +// The Center's floor: Oreburgh's sand reads up to 34 % warm outside +// (r189a), the floor 56 to 64 % inside. +bool oreburgh_center_floor(const ImageViewRGB32& screen){ + size_t percent = 0; + center_floor_on_screen(screen, &percent); + return percent >= 45; +} +bool oreburgh_center_inside_now(VideoStream& stream){ + const VideoSnapshot frame = stream.video().snapshot(); + return frame && oreburgh_center_floor(*frame.frame); +} + +// North into the Center's door from this column: whether we are inside. +// Someone in the door (Barry, the first time) is talked to and the walk +// tried again. +bool into_oreburgh_center_door(VideoStream& stream, ProControllerContext& context){ + // The door's fade often reads as the time limit, not a black screen + // (r183a went in and on to the counter): look at the floor. + // Stop as soon as the floor shows: walking on to the counter left the + // door's column, and the walk back out drifted into the yard (r188b). + const WalkResult in = walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", + oreburgh_center_floor); + if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && oreburgh_center_inside_now(stream))){ + return true; + } + if (in.stop == WalkStop::DIALOG){ + talk_or_fight(stream, context, "Someone at the Center's door"); + }else{ + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + if (!dialogue_opens(stream, context, 2000ms)){ + return false; + } + finish_dialogue(stream, context, BUTTON_B, "Barry at the Center's door"); + context.wait_for(2000ms); // he runs off + } + // Talked in the door's column: the door is free now. + walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", + oreburgh_center_floor); + return oreburgh_center_inside_now(stream); +} + // Oreburgh: from a row or two below the Pokemon Center's front, west of // the fence below its east corner, into the Center by its door, stopping // just inside. Talks to Barry if he stands in the door (01.03, the first @@ -59,11 +100,6 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ // door, so look for it in nudges of about a third of a tile. route_turn(stream, context, Direction::WEST); context.wait_for_all_requests(); - auto inside_now = [&](){ - const VideoSnapshot frame = stream.video().snapshot(); - size_t percent = 0; - return frame && (center_floor_on_screen(*frame.frame, &percent), percent >= 45); - }; bool inside = false; const int offsets[] = {0, -1, -2, 1, -3, 2, -4, 3}; // nudges from here int nudged = 0; @@ -77,34 +113,9 @@ void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ } context.wait_for_all_requests(); } - // The door's fade often reads as the time limit, not a black screen - // (r183a went in and on to the counter): look at the floor. - // Stop as soon as the floor shows: walking on to the counter left the - // door's column, and the walk back out drifted into the yard (r188b). - // Oreburgh's sand reads up to 34 % warm outside (r189a), the floor - // 56 to 64 % inside. - const WalkResult in = walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", - [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); - if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && inside_now())){ - inside = true; - break; - } - if (in.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, "Someone at the Center's door"); - }else{ - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - if (!dialogue_opens(stream, context, 2000ms)){ - continue; - } - finish_dialogue(stream, context, BUTTON_B, "Barry at the Center's door"); - context.wait_for(2000ms); // he runs off - } - // Talked in the door's column: the door is free now. - walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", - [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); - inside = inside_now(); + inside = into_oreburgh_center_door(stream, context); } - if (!inside || !inside_now()){ + if (!inside || !oreburgh_center_inside_now(stream)){ fail_step(stream, "Oreburgh: no column east of the save went into the Pokemon Center."); } } @@ -184,7 +195,7 @@ bool oreburgh_gym_battles(VideoStream& stream, ProControllerContext& context){ if (dialogue_opens(stream, context, 3000ms)){ // His lines pause for more than 4 s before the battle (r210d: "only // talked"), so the battle is expected. - if (talk_or_fight(stream, context, "Roark", true)){ + if (talk_or_fight(stream, context, "Roark", TalkExpect::BATTLE)){ if (in_pokemon_center(stream)){ return false; } @@ -262,6 +273,104 @@ void mine_hall(VideoStream& stream, ProControllerContext& context){ } } +// After a wild battle on the way: back to the east end of the bottom row, +// three taps west and north to the wall on his row (Maps/OreburghMine.json: +// columns 23 to 25 have that wall above). +void line_up_below_roark_again(VideoStream& stream, ProControllerContext& context){ + // The battle may leave us on his row or the bottom one. + walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom row"); + walk_to_wall(stream, context, run_from_wild_battle, {1, 0}, 6000ms, "east along the bottom row"); + route_turn(stream, context, Direction::WEST); + for (size_t tap = 0; tap < 3; tap++){ + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + } + walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 2000ms, "north to the wall on Roark's row"); +} + +// From his row east of his rock: west to the rock, down and two taps west +// below him, and A with nudges. Whether his talk opened. +bool talk_to_roark_from_his_row(VideoStream& stream, ProControllerContext& context){ + // A wild battle on the way eats into the walk's time (r100): walk again. + for (size_t walk = 0; walk < 3; walk++){ + if (walk_to_wall(stream, context, run_from_wild_battle, {-1, 0}, 6000ms, "west to the rock beside Roark").wild_battles == 0){ + break; + } + } + walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom wall"); + route_turn(stream, context, Direction::WEST); + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); + route_turn(stream, context, Direction::NORTH); + context.wait_for_all_requests(); + if (run_from_late_encounter(stream, context, run_from_wild_battle, "below Roark")){ + return false; + } + if (VideoSnapshot screen = stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-below-roark.png"); + } + // The camera zooms in and his first line, "OK, stand back and watch + // this.", can take some seconds (r98). + // His rocks answer A too ("It's a rugged rock..."): his talk is the + // one that zooms the camera in on us (by hand), which the rock's + // message does not. Compare the view above the dialogue box. + return talk_with_nudges(stream, context, run_from_wild_battle, Direction::NORTH, "Roark", 8000ms, + [&](const ImageViewRGB32& before, const ImageViewRGB32& after){ + const ImageFloatBox above_box{0.0, 0.0, 1.0, 0.6}; + const double change = walk_screen_change( + extract_box_reference(before, above_box), extract_box_reference(after, above_box) + ); + stream.log("[01.03] View change when the talk opened: " + std::to_string(change) + " (Roark zooms in)."); + return change > 35; // by hand: 50 for his zoom + }); +} + +// His scene (by hand, 2026-10-31): see roark_in_the_mine(). Fails if he +// is still there after it. +void wait_out_roarks_scene(VideoStream& stream, ProControllerContext& context){ + bool gone = false; + for (size_t round = 0; round < 3 && !gone; round++){ + for (size_t press = 0; press < 14; press++){ + pbf_press_button(context, BUTTON_B, 80ms, 2420ms); + } + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + gone = !dialogue_opens(stream, context, 2500ms); + } + if (!gone){ + fail_step(stream, "Oreburgh Mine: Roark is still there after the talk."); + } +} + +// From below Roark, back up the corridor and out of the mine. +void out_of_oreburgh_mine( + VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, + RouteTrainers& no_trainers, const Pokemon::TilePos& roark +){ + const auto world = [&](const Pokemon::TilePos& pos){ return maps.to_world(MAP_OREBURGH_MINE, pos); }; + walk_dir(stream, context, Direction::SOUTH, 2000ms, "back to the bottom wall"); + std::vector at{roark.next(Direction::SOUTH)}; + + // Back up the corridor, then north out through the hall. A route leg + // north to the top's wall walks on through it into the hall (r99), and + // no wall stops us a row short of it, so the route ends on the corner + // at (14,10), where that leg started, and the walk does the rest. + const Pokemon::TilePos exit = world(map_point("OreburghMine", "corridor_exit_start")); + at = walk_route(stream, context, run_from_wild_battle, maps, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == exit; }, "Back to the corridor's top", {}, route_plan_costs()}); + // Up into the entrance hall (a transition, as on the way in), then on + // north out of the mine: in r103 that walk left the mine without a + // transition we saw and ended against the sign above the stairs, which + // the moving belts make read as the time limit. + if (walk_dir(stream, context, Direction::NORTH, 10000ms, "into the entrance hall").stop != WalkStop::TRANSITION){ + fail_step(stream, "Oreburgh Mine: walking north from the corridor did not reach the entrance hall."); + } + context.wait_for(1500ms); + if (walk_dir(stream, context, Direction::NORTH, 10000ms, "out of the mine").stop == WalkStop::DIALOG){ + fail_step(stream, "Oreburgh Mine: someone stopped us on the way out of the mine."); + } + context.wait_for(1500ms); +} + // 01.03: the planned route down the corridor to Roark, his talk, and back out of the mine. void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ // Planned on the coal room's tile grid (Maps/OreburghMine.json): the @@ -294,48 +403,9 @@ void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ if (attempt > 0){ // The battle may leave us on his row or the bottom one. - walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom row"); - walk_to_wall(stream, context, run_from_wild_battle, {1, 0}, 6000ms, "east along the bottom row"); - route_turn(stream, context, Direction::WEST); - for (size_t tap = 0; tap < 3; tap++){ - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - } - walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 2000ms, "north to the wall on Roark's row"); + line_up_below_roark_again(stream, context); } - // A wild battle on the way eats into the walk's time (r100): walk again. - for (size_t walk = 0; walk < 3; walk++){ - if (walk_to_wall(stream, context, run_from_wild_battle, {-1, 0}, 6000ms, "west to the rock beside Roark").wild_battles == 0){ - break; - } - } - walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom wall"); - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - route_turn(stream, context, Direction::NORTH); - context.wait_for_all_requests(); - if (run_from_late_encounter(stream, context, run_from_wild_battle, "below Roark")){ - continue; - } - if (VideoSnapshot screen = stream.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; - Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-below-roark.png"); - } - // The camera zooms in and his first line, "OK, stand back and watch - // this.", can take some seconds (r98). - // His rocks answer A too ("It's a rugged rock..."): his talk is the - // one that zooms the camera in on us (by hand), which the rock's - // message does not. Compare the view above the dialogue box. - talked = talk_with_nudges(stream, context, run_from_wild_battle, Direction::NORTH, "Roark", 8000ms, - [&](const ImageViewRGB32& before, const ImageViewRGB32& after){ - const ImageFloatBox above_box{0.0, 0.0, 1.0, 0.6}; - const double change = walk_screen_change( - extract_box_reference(before, above_box), extract_box_reference(after, above_box) - ); - stream.log("[01.03] View change when the talk opened: " + std::to_string(change) + " (Roark zooms in)."); - return change > 35; // by hand: 50 for his zoom - }); + talked = talk_to_roark_from_his_row(stream, context); } if (!talked){ fail_step(stream, "Oreburgh Mine: no talk opened below Roark (has Barry told us about the mine?)."); @@ -348,38 +418,8 @@ void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ // with him still there, and Barry kept the gym shut). Press B through it // on a clock, then ask his tile: once he is gone, A there opens nothing. // B and A in the overworld facing a wall do nothing. - bool gone = false; - for (size_t round = 0; round < 3 && !gone; round++){ - for (size_t press = 0; press < 14; press++){ - pbf_press_button(context, BUTTON_B, 80ms, 2420ms); - } - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - gone = !dialogue_opens(stream, context, 2500ms); - } - if (!gone){ - fail_step(stream, "Oreburgh Mine: Roark is still there after the talk."); - } - walk_dir(stream, context, Direction::SOUTH, 2000ms, "back to the bottom wall"); - at = {roark.next(Direction::SOUTH)}; - - // Back up the corridor, then north out through the hall. A route leg - // north to the top's wall walks on through it into the hall (r99), and - // no wall stops us a row short of it, so the route ends on the corner - // at (14,10), where that leg started, and the walk does the rest. - const Pokemon::TilePos exit = world(map_point("OreburghMine", "corridor_exit_start")); - at = walk_route(stream, context, run_from_wild_battle, maps, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == exit; }, "Back to the corridor's top", {}, route_plan_costs()}); - // Up into the entrance hall (a transition, as on the way in), then on - // north out of the mine: in r103 that walk left the mine without a - // transition we saw and ended against the sign above the stairs, which - // the moving belts make read as the time limit. - if (walk_dir(stream, context, Direction::NORTH, 10000ms, "into the entrance hall").stop != WalkStop::TRANSITION){ - fail_step(stream, "Oreburgh Mine: walking north from the corridor did not reach the entrance hall."); - } - context.wait_for(1500ms); - if (walk_dir(stream, context, Direction::NORTH, 10000ms, "out of the mine").stop == WalkStop::DIALOG){ - fail_step(stream, "Oreburgh Mine: someone stopped us on the way out of the mine."); - } - context.wait_for(1500ms); + wait_out_roarks_scene(stream, context); + out_of_oreburgh_mine(stream, context, maps, no_trainers, roark); } // 01.03: into the Pokemon Center and heal at the counter. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp index 97cba07a08..6c3b272bab 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp @@ -39,6 +39,56 @@ using Pokemon::Direction; namespace{ +// The walk east along Route 203: who has stopped us so far. +struct Route203Walk{ + VideoStream& stream; + ProControllerContext& context; + size_t fights = 0; + bool inside = false; + // On Route 203 only trainers start a talk; one with no battle is the + // Hiker, who meets us inside the Gate (whose fade may not read as a + // transition). + bool met_hiker = false; +}; + +// Whoever stopped the walk: a trainer to fight, or the Hiker. +void route_203_stopped_by(Route203Walk& walk){ + if (talk_or_fight(walk.stream, walk.context, "Route 203 trainer " + std::to_string(walk.fights + 1))){ + walk.fights++; + }else{ + route_status(walk.stream, "No battle: that was the Hiker, inside the Oreburgh Gate."); + walk.met_hiker = true; + walk.inside = true; + } +} + +// At a wall: the row from which east goes on. North to the top wall first +// (a step north there does not move us, so rows are counted from it), then +// a row at a time south. At the route's end this is the corner by the +// second sign, with the Gate's opening two rows down. +void route_203_row_east_on(Route203Walk& walk){ + VideoStream& stream = walk.stream; + ProControllerContext& context = walk.context; + walk_until(stream, context, run_from_wild_battle, {0, 1}, 6000ms, "north to the top wall"); + bool moved_on = false; + for (size_t row = 1; row <= 10 && !moved_on; row++){ + step_south(stream, context); + const WalkResult next = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); + if (next.stop == WalkStop::TRANSITION){ + walk.inside = true; + } + if (next.stop == WalkStop::DIALOG || (next.stop == WalkStop::BLOCKED && dialogue_opens(stream, context, 1500ms))){ + route_203_stopped_by(walk); + } + // Two still chunks are the least a wall takes; more is progress. + moved_on = walk.inside || next.stop != WalkStop::BLOCKED || next.walked > 1200ms; + } + if (!moved_on){ + fail_step(stream, "Route 203: walls east on every row down to ten below the top wall."); + } +} + + // 01.02: east along Route 203 into the Oreburgh Gate, fighting whoever stops us. Pokemon::Outcome route_203_east_to_the_gate(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ VideoStream& stream = executor.stream(); @@ -48,62 +98,28 @@ Pokemon::Outcome route_203_east_to_the_gate(StoryExecutor& executor, Pokemon::Ru // differs from run to run (a trainer's sight depends on our row), so // there is no fixed list: walk east, talk or fight whoever stops us, and // at a wall find a row from which east goes on. - size_t fights = 0; - bool inside = false; - // On Route 203 only trainers start a talk; one with no battle is the - // Hiker, who meets us inside the Gate (whose fade may not read as a - // transition). - bool met_hiker = false; - auto stopped_by = [&](){ - if (talk_or_fight(stream, context, "Route 203 trainer " + std::to_string(fights + 1))){ - fights++; - }else{ - route_status(stream, "No battle: that was the Hiker, inside the Oreburgh Gate."); - met_hiker = true; - inside = true; - } - }; - for (size_t leg = 0; leg < 40 && !inside; leg++){ - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203"); - if (walk.stop == WalkStop::TRANSITION){ - inside = true; + Route203Walk walk{stream, context}; + for (size_t leg = 0; leg < 40 && !walk.inside; leg++){ + const WalkResult east = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203"); + if (east.stop == WalkStop::TRANSITION){ + walk.inside = true; break; } - if (walk.stop == WalkStop::LIMIT){ + if (east.stop == WalkStop::LIMIT){ continue; } - if (walk.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 1500ms)){ - stopped_by(); + if (east.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 1500ms)){ + route_203_stopped_by(walk); continue; } - // A wall. Find the row from which east goes on: north to the top wall - // first (a step north there does not move us, so rows are counted - // from it), then a row at a time south. At the route's end this is the - // corner by the second sign, with the Gate's opening two rows down. - walk_until(stream, context, run_from_wild_battle, {0, 1}, 6000ms, "north to the top wall"); - bool moved_on = false; - for (size_t row = 1; row <= 10 && !moved_on; row++){ - step_south(stream, context); - const WalkResult next = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); - if (next.stop == WalkStop::TRANSITION){ - inside = true; - } - if (next.stop == WalkStop::DIALOG || (next.stop == WalkStop::BLOCKED && dialogue_opens(stream, context, 1500ms))){ - stopped_by(); - } - // Two still chunks are the least a wall takes; more is progress. - moved_on = inside || next.stop != WalkStop::BLOCKED || next.walked > 1200ms; - } - if (!moved_on){ - fail_step(stream, "Route 203: walls east on every row down to ten below the top wall."); - } + route_203_row_east_on(walk); } - if (!inside){ + if (!walk.inside){ fail_step(stream, "Route 203: did not reach the Oreburgh Gate."); } - route_status(stream, "Route 203: " + std::to_string(fights) + " trainer battle(s) on the way."); + route_status(stream, "Route 203: " + std::to_string(walk.fights) + " trainer battle(s) on the way."); context.wait_for(1500ms); - if (met_hiker){ + if (walk.met_hiker){ state.flags.insert("met_hiker"); } return Pokemon::outcome_ok(); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index 77856738e0..9988c24e87 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -164,7 +164,7 @@ void route_204_to_floaroma(VideoStream& stream, ProControllerContext& context){ { RouteTrainers trainers{route204_north_trainer_sights(), [&](const std::string& who){ const bool doubles = who.find("Twins") != std::string::npos || who.find("someone") != std::string::npos; - talk_or_fight(stream, context, who, false, doubles); + talk_or_fight(stream, context, who, TalkExpect::TALK_OR_BATTLE, doubles ? BattleFormat::DOUBLES : BattleFormat::SINGLES); }}; std::vector at{map_point("Route204North", "cave_exit")}; at = walk_waypoints(stream, context, run_from_wild_battle, route204_north_maps(), at, trainers, WaypointWalkPlan{map_region("Route204North", "floaroma_waypoints"), "To the path into Floaroma", waypoint_plan_costs()}); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp index 3982b68686..e5b30ec0c0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp @@ -40,24 +40,24 @@ using Pokemon::TilePos; namespace{ // A walk_until() callback that keeps every location banner it sees. -std::function banner_keeper(std::vector& banners) { - return [&banners](const ImageViewRGB32& screen) { +std::function banner_keeper(std::vector& banners){ + return [&banners](const ImageViewRGB32& screen){ const std::string read = read_location_banner(screen); - if (!read.empty() && (banners.empty() || banners.back() != read)) { + if (!read.empty() && (banners.empty() || banners.back() != read)){ banners.push_back(read); } return false; }; } -bool any_banner_names(const std::vector& banners, const char* map) { +bool any_banner_names(const std::vector& banners, const char* map){ return std::any_of(banners.begin(), banners.end(), - [&](const std::string& read) { return location_banner_matches(read, map); }); + [&](const std::string& read){ return location_banner_matches(read, map); }); } // Whether all of `tiles` are on `map`. -bool all_on_map(const StoryWorld& maps, const std::vector& tiles, const char* map) { - return !tiles.empty() && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos) { +bool all_on_map(const StoryWorld& maps, const std::vector& tiles, const char* map){ + return !tiles.empty() && std::all_of(tiles.begin(), tiles.end(), [&](const TilePos& pos){ const std::optional tile = maps.to_local(pos); return tile && tile->map == map; }); @@ -67,7 +67,7 @@ bool all_on_map(const StoryWorld& maps, const std::vector& tiles, const // nudging a tile west, twice at most, when blocked at once (below a // sign or the Mart). Returns whether Route 202's banner showed: only // one of the gap's columns leads out of town before a wall. -bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context) { +bool walk_north_through_gap(VideoStream& stream, ProControllerContext& context){ using Pokemon::Direction; std::vector banners; for (size_t nudges = 0;; nudges++){ @@ -134,14 +134,14 @@ std::vector north_through_sign_gap(VideoStream& stream, ProControllerCo void walk_to_route_202_save_spot( VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, std::vector at, RouteTrainers& trainers -) { +){ const TilePos row_end = maps.to_world(MAP_SANDGEM_TOWN, map_point("SandgemTown", "front_row_east_end")); const TilePos save_spot = maps.to_world(MAP_ROUTE_202, map_point("Route202", "save_spot")); - if (!all_on_map(maps, at, MAP_ROUTE_202)) { - at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos) { return pos == row_end; }, "To the trees east of the Poke Mart", {}, route_plan_costs()}); + if (!all_on_map(maps, at, MAP_ROUTE_202)){ + at = walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos){ return pos == row_end; }, "To the trees east of the Poke Mart", {}, route_plan_costs()}); at = north_through_sign_gap(stream, context, maps); } - walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos) { return pos == save_spot; }, "Back to Route 202", {}, route_plan_costs()}); + walk_route(stream, context, run_from_wild_battle, maps, std::move(at), trainers, RouteWalkPlan{[&](const TilePos& pos){ return pos == save_spot; }, "Back to Route 202", {}, route_plan_costs()}); } // North into Jubilife until Dawn's scene starts (00.04d's @@ -150,15 +150,15 @@ void walk_to_route_202_save_spot( // Entering starts a scene: Dawn walks over while we stand still. So // walk north; if the wall stops us and no dialogue follows, the stairs // are further west. The scene itself is the next step. -void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { - for (size_t attempt = 0; attempt < 6; attempt++) { +void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context){ + for (size_t attempt = 0; attempt < 6; attempt++){ const WalkResult walk = walk_until(stream, context, run_from_wild_battle, {0, 1}, 4000ms, "to Dawn in Jubilife"); - if (walk.stop == WalkStop::TRANSITION) { + if (walk.stop == WalkStop::TRANSITION){ OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Jubilife: left the area instead of meeting Dawn.", stream); } ShortDialogWatcher dialog(COLOR_CYAN); - if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0) { + if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0){ return; } stream.log("[00.04] Jubilife: the wall stopped us; looking for the stairs one tile west."); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index c331439a69..8e96080e6a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -258,7 +258,7 @@ Outcome StoryExecutor::mash(RunState&, const StoryStep& step){ } Outcome StoryExecutor::battle(RunState&, const StoryStep& step){ - if (!talk_or_fight(stream(), context(), label_of(step), true, step.variant == "doubles")){ + if (!talk_or_fight(stream(), context(), label_of(step), TalkExpect::BATTLE, step.variant == "doubles" ? BattleFormat::DOUBLES : BattleFormat::SINGLES)){ fail_step(stream(), label_of(step) + ": talked but did not battle."); } if (!step.may_lose && in_pokemon_center(stream())){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp index cbde99eed3..0f2aeaf9da 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -106,6 +106,26 @@ Walked plan_route(const Pose& from, const StoryStep& step){ return walked; } +// `leg` of `step` from `pos`: adds where it ends to `next` (and a tile +// short where the step allows it). Returns why it is blocked, or "". +std::string walk_leg_from( + const StoryWorld& world, TilePos pos, const Pokemon::StoryLeg& leg, const StoryStep& step, + std::vector& next +){ + for (int i = 0; i < leg.tiles; i++){ + const std::optional stepped = PokemonBDSP::step(world, pos, leg.direction); + if (!stepped){ + return step.name + ": " + direction_name(leg.direction) + " is blocked at " + describe(world, pos) + "."; + } + if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ + next.push_back(pos); + } + pos = *stepped; + } + next.push_back(pos); + return ""; +} + // A LEGS step from `from`, walked tile by tile on its map, a tile short too // where the step allows it. Walks to walls end where nothing is known. Walked walk_legs(const Pose& from, const StoryStep& step){ @@ -122,18 +142,11 @@ Walked walk_legs(const Pose& from, const StoryStep& step){ } for (const Pokemon::StoryLeg& leg : step.legs){ std::vector next; - for (TilePos pos : at){ - for (int i = 0; i < leg.tiles; i++){ - const std::optional stepped = PokemonBDSP::step(*world, pos, leg.direction); - if (!stepped){ - return {std::nullopt, step.name + ": " + direction_name(leg.direction) + " is blocked at " + describe(*world, pos) + ".", ""}; - } - if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ - next.push_back(pos); - } - pos = *stepped; + for (const TilePos& pos : at){ + const std::string blocked = walk_leg_from(*world, pos, leg, step, next); + if (!blocked.empty()){ + return {std::nullopt, blocked, ""}; } - next.push_back(pos); } at = std::move(next); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp index 71fa16bc24..f821fdb0a6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp @@ -52,7 +52,7 @@ bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Millisec return wait_until(stream, context, timeout, {{dialog}}) == 0; } -bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std::string& label, bool battle_expected, bool doubles){ +bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std::string& label, TalkExpect expect, BattleFormat format){ ShortDialogDetector dialog; OverworldDetector overworld; BattleMenuDetector battle_menu(BattleType::TRAINER); @@ -69,7 +69,7 @@ bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std } if (battle_menu.detect(*frame.frame) || battle_menu_standard.detect(*frame.frame) || black.detect(*frame.frame)){ route_status(stream, "Fighting " + label + "."); - fight_story_battle(stream, context, label, doubles ? BattleFormat::DOUBLES : BattleFormat::SINGLES); + fight_story_battle(stream, context, label, format); // Lines after the battle (Roark's badge, r211d) keep the menu // from opening: finish them before the Potion. if (dialogue_opens(stream, context, 3000ms)){ @@ -82,7 +82,7 @@ bool talk_or_fight(VideoStream& stream, ProControllerContext& context, const std } const bool talking = dialog.detect(*frame.frame); if (!talking && overworld.detect(*frame.frame)){ - if (!battle_expected && std::chrono::steady_clock::now() - quiet_since >= 4s){ + if (expect != TalkExpect::BATTLE && std::chrono::steady_clock::now() - quiet_since >= 4s){ route_status(stream, label + " only talked."); return false; } @@ -169,12 +169,12 @@ uint64_t mean_brightness(const ImageViewRGB32& screen){ // Fights a trainer whose dialogue has opened, with whatever starter we // have. Someone who only talks leaves no battle to fight. -void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name) { +void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name){ pbf_mash_button(context, BUTTON_B, 5000ms); context.wait_for_all_requests(); BattleMenuWatcher battle_menu(BattleType::TRAINER, COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); - if (wait_until(stream, context, 8s, {{battle_menu}, {battle_dialog}}) < 0) { + if (wait_until(stream, context, 8s, {{battle_menu}, {battle_dialog}}) < 0){ stream.log("[Route] " + name + ": no battle followed the talk."); pbf_mash_button(context, BUTTON_B, 2000ms); context.wait_for_all_requests(); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h index 30c9c612e6..02f185e855 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h @@ -17,6 +17,7 @@ #include "CommonFramework/Tools/VideoStream.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" #include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP_AutoStory_Battle.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -35,12 +36,16 @@ bool dialogue_opens(VideoStream& stream, ProControllerContext& context, Millisec // followed by a Potion if HP is under half. Anyone else's ends back in the // overworld. Returns whether there was a battle. Between a trainer's last // line and the battle the overworld shows with no dialogue for a moment, -// so "only talked" needs 4 s of it, and never comes when `battle_expected`. -// `doubles` for tag and twin battles. A lost battle ends in the last -// Pokemon Center; see in_pokemon_center(). +// so "only talked" needs 4 s of it, and never comes when `expect` is +// BATTLE. `format` DOUBLES for tag and twin battles. A lost battle ends in +// the last Pokemon Center; see in_pokemon_center(). +enum class TalkExpect{ + TALK_OR_BATTLE, + BATTLE, +}; bool talk_or_fight( VideoStream& stream, ProControllerContext& context, const std::string& label, - bool battle_expected = false, bool doubles = false + TalkExpect expect = TalkExpect::TALK_OR_BATTLE, BattleFormat format = BattleFormat::SINGLES ); // Fights a trainer whose dialogue has opened, with whatever starter we From a70e79e0ceea4e865614e22d0faf603213c47d5d Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:13:37 -0800 Subject: [PATCH 13/98] BDSP AutoStory: story lines on the video overlay through SegmentHooks::status Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index b988667453..c859415790 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -10,6 +10,7 @@ #include #include "CommonFramework/Exceptions/OperationFailedException.h" #include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" #include "NintendoSwitch/Programs/NintendoSwitch_GameEntry.h" #include "Pokemon/AutoStory/Pokemon_AutoStoryStats.h" #include "Pokemon/AutoStory/Pokemon_StorySegments.h" @@ -80,6 +81,9 @@ Pokemon::SegmentHooks story_hooks(SingleSwitchProgramEnvironment& env, ProContro hooks.log = [&env](const std::string& line){ env.log(line, COLOR_BLUE); }; + hooks.status = [&env](const std::string& line){ + env.console.overlay().add_log(line); + }; return hooks; } From af15312f8087534a98b20ab0b4c4ff73a47a5733 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:16:58 -0800 Subject: [PATCH 14/98] BDSP AutoStory table: train(), next_to() and at_or_next_to() helpers, so rows stay data 00.04b's training is train(3, ...), its sign and door legs use next_to() and a named column count, and the stairs pose is at_or_next_to(); the rows' poses and legs are the same. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryTable.cpp | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 6980ece6a6..87cc313203 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -56,9 +56,22 @@ Pose at(const char* map, int x, int y, std::optional facing = std::nu Pose at(const char* map, const char* point, std::optional facing = std::nullopt){ return Pokemon::tile_pose(map, {map_point(map, point)}, facing); } +// The tile next to `point` of `map` on its `side`. +Pose next_to(const char* map, const char* point, Direction side, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, {map_point(map, point).next(side)}, facing); +} +// `point` of `map` or the tile on its `side`: where a scene may leave us. +Pose at_or_next_to(const char* map, const char* point, Direction side){ + const TilePos tile = map_point(map, point); + return Pokemon::tile_pose(map, {tile, tile.next(side)}); +} Pose on(const char* map, const char* region){ return Pokemon::tile_pose(map, map_region(map, region)); } +// Columns from below Sandgem's Pokemon Center sign west to below its door. +int sandgem_sign_to_center_door(){ + return map_point(SANDGEM, "pokemon_center_sign").x - map_point(SANDGEM, "pokemon_center_door").x; +} // The tile below the door `door` of `map` and its neighbours either side: // a door is not lined up with one tile (Jubilife's Center, r85). Pose door_front(const char* map, const char* door){ @@ -138,6 +151,13 @@ StoryStep heal(std::string name, std::string why, std::optional pose_after step.pose_after = std::move(pose_after); return step; } +// `battles` wild battles in the grass we stand in. +StoryStep train(size_t battles, Pose pose_after, std::string why){ + StoryStep step = make(StoryStepKind::TRAIN, "train", std::move(why)); + step.battles = battles; + step.pose_after = std::move(pose_after); + return step; +} StoryStep save(){ return make(StoryStepKind::SAVE, "save_here", "the next checkpoint starts here"); } @@ -158,9 +178,9 @@ std::vector intro(){ row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { route("above_the_stairs", at(BEDROOM, 8, 5, NORTH), "the stairs (8, 4) east of the bed"), // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). - step_into("down_the_stairs", NORTH, "onto the stairs", Pokemon::tile_pose(HOME, {map_point(HOME, "stairs"), map_point(HOME, "stairs").next(SOUTH)})), + step_into("down_the_stairs", NORTH, "onto the stairs", at_or_next_to(HOME, "stairs", SOUTH)), // Butters' recording walked 1.1 s south before her first line. - scene("mom_calls", Pokemon::tile_pose(HOME, {map_point(HOME, "stairs"), map_point(HOME, "stairs").next(SOUTH)}), + scene("mom_calls", at_or_next_to(HOME, "stairs", SOUTH), "Mom calls the player over as they come down; we stay put (assumed)"), route("above_the_door", at(HOME, 6, 9, SOUTH), "past Mom at (7, 8) to above the door"), step_into("mom_warning", SOUTH, "the door's tile (6, 10): Mom's warning (coord event, VAR_PLAYER_HOUSE_STATE 1)"), @@ -243,22 +263,14 @@ std::vector to_the_catch(){ // heal, and Potions first, each row from the save the one before ends on. row("00.04b", "After the catch: train and heal in Sandgem", on(ROUTE_202, "catch_save_area"), { route("to_catch_grass", on(ROUTE_202, "catch_grass"), "into the training column's grass"), - []{ - StoryStep step = make(StoryStepKind::TRAIN, "train", "three wild battles with the best move"); - step.battles = 3; - step.pose_after = on(ROUTE_202, "after_training_area"); - return step; - }(), + train(3, on(ROUTE_202, "after_training_area"), "three wild battles with the best move"), // One walk round the houses ended two tiles short (r252): down // the road first, then the town plan. route("into_sandgem", at(SANDGEM, "route_202_entry_stop"), "south down the road to above the Mart", "robust"), - route("to_center_sign", at(SANDGEM, map_point(SANDGEM, "pokemon_center_sign").x, map_point(SANDGEM, "pokemon_center_sign").y + 1), - "below the Center's sign, its doorway in sight", "town"), + route("to_center_sign", next_to(SANDGEM, "pokemon_center_sign", SOUTH), "below the Center's sign, its doorway in sight", "town"), // From below the sign the finder read the signs' panels as // doorways (r252, r255): taps instead. One tile short is fine. - legs("below_center_door", {{.direction = WEST, - .tiles = map_point(SANDGEM, "pokemon_center_sign").x - map_point(SANDGEM, "pokemon_center_door").x}}, - "taps west below the Center's door", "one_short_ok"), + legs("below_center_door", {{.direction = WEST, .tiles = sandgem_sign_to_center_door()}}, "taps west below the Center's door", "one_short_ok"), heal("heal_sandgem", "Sandgem Town Pokemon Center", door_front(SANDGEM, "pokemon_center_door")), }), }; From 490e6aad0a1dc4a5805e7d57f7c2126744708f36 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:49:03 -0800 Subject: [PATCH 15/98] BDSP AutoStory: go down the bedroom stairs moving east On console B (r8) the stairs are entered by stepping east from the mat at (8, 5); stepping north only bumps the wall, so 00.01 waited for Mom's call upstairs. The decomp's stairs point (8, 4) is a row off. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 87cc313203..8155e2b48b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -176,9 +176,11 @@ std::vector intro(){ make(StoryStepKind::OPTIONS, "settings", "fast text, Send to Boxes automatic, no nickname prompt, Autosave off"), }, StoryFailure::STOP), row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { - route("above_the_stairs", at(BEDROOM, 8, 5, NORTH), "the stairs (8, 4) east of the bed"), + route("above_the_stairs", at(BEDROOM, 8, 5, EAST), "the mat west of the stairs"), + // On the console the stairs are entered moving east from the mat at (8, 5), + // not north (r8); the decomp's stairs point (8, 4) is a row off. // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). - step_into("down_the_stairs", NORTH, "onto the stairs", at_or_next_to(HOME, "stairs", SOUTH)), + step_into("down_the_stairs", EAST, "onto the stairs", at_or_next_to(HOME, "stairs", SOUTH)), // Butters' recording walked 1.1 s south before her first line. scene("mom_calls", at_or_next_to(HOME, "stairs", SOUTH), "Mom calls the player over as they come down; we stay put (assumed)"), From 4545416f50fcbf94fdfe8862436b7346d652b0d4 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:55:46 -0800 Subject: [PATCH 16/98] BDSP AutoStory: take the bedroom stairs from the top of the mat The mat west of the stairs is two tiles tall, and only its top tile, the decomp's stairs point (8, 4), leads onto them moving east (r9: east from (8, 5) and north from (8, 5) both bump a wall). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 8155e2b48b..f1fb4e7ce9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -176,9 +176,9 @@ std::vector intro(){ make(StoryStepKind::OPTIONS, "settings", "fast text, Send to Boxes automatic, no nickname prompt, Autosave off"), }, StoryFailure::STOP), row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { - route("above_the_stairs", at(BEDROOM, 8, 5, EAST), "the mat west of the stairs"), - // On the console the stairs are entered moving east from the mat at (8, 5), - // not north (r8); the decomp's stairs point (8, 4) is a row off. + route("above_the_stairs", at(BEDROOM, "stairs", EAST), "the top of the mat, west of the stairs"), + // The stairs are entered moving east from the top of the two-tile mat, + // the decomp's stairs point (8, 4); east from (8, 5) or north are walls (r8, r9). // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). step_into("down_the_stairs", EAST, "onto the stairs", at_or_next_to(HOME, "stairs", SOUTH)), // Butters' recording walked 1.1 s south before her first line. From 6e186c81443499639a616fe6f004ad227dab9ef6 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 16:03:10 -0800 Subject: [PATCH 17/98] BDSP AutoStory: a step through a door waits for the fade At Barry's door (r10) the fade started after the step's 800 ms window, so the next leg, off his mat, stopped on it and moved 0 tiles. A step whose end pose is on another map now waits up to 5 s for the black screen to come and go, and fails if the map never changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 8e96080e6a..f64bcbd32d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -16,6 +16,7 @@ #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" @@ -227,6 +228,15 @@ Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ const auto [x, y] = dpad_of(step.direction); const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); + // A step onto a door changes the map, and the fade can start after the + // walk's window (r10: Barry's door). Wait it out here, so the next step + // doesn't take it for its own stop. + if (walk.stop != WalkStop::TRANSITION && step.pose_after && step.pose_after->map != state.pose.map){ + BlackScreenOverWatcher faded(COLOR_RED); + if (wait_until(stream(), context(), 5000ms, {{faded}}) != 0){ + fail_step(stream(), label_of(step) + ": the map didn't change."); + } + } std::vector at; for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ at.push_back(pos.next(step.direction)); From 6c101b8470640e3c07eebd5f5d272981527b6013 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 16:09:37 -0800 Subject: [PATCH 18/98] BDSP AutoStory: take Barry's stairs sideways from the top of their mat Same as the player's house: stairs are entered moving east (1F) or west (2F, mirrored) from the stairs point, the top of a two-tile mat. r11 stepped north from (2, 4) and only bumped the wall. The 2F direction is assumed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index f1fb4e7ce9..5f1c227261 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -194,13 +194,15 @@ std::vector intro(){ scene("barry_goes_home", at(TWINLEAF, "barry_doorstep"), "his second lines (Butters waited for two)", "a"), step_into("into_barrys_house", NORTH, "his door", at(BARRYS_HOUSE, "door", NORTH)), legs("off_barrys_mat", {{.direction = NORTH, .tiles = 1}}, "off the door's mat, which no route starts on"), - route("below_barrys_stairs", at(BARRYS_HOUSE, 2, 4, NORTH), "past his mother at (4, 6)"), - step_into("up_barrys_stairs", NORTH, "onto the stairs", at(BARRYS_ROOM, "stairs")), + // Like ours: stairs are entered sideways from the top of their mat, the + // stairs point (r11: north from (2, 4) only bumps the wall). + route("below_barrys_stairs", at(BARRYS_HOUSE, "stairs", EAST), "past his mother at (4, 6) to the top of the stairs' mat"), + step_into("up_barrys_stairs", EAST, "onto the stairs", at(BARRYS_ROOM, "stairs")), route("beside_barry", at(BARRYS_ROOM, "barry_talk", EAST), "west of Barry, who faces his PC (assumed talk tile)"), talk("barry", "Barry in his room"), scene("barry_heads_for_the_lake", at(BARRYS_ROOM, "barry_talk", EAST), "he runs off to the lake", "a"), - route("above_barrys_stairs", at(BARRYS_ROOM, 3, 5, NORTH), "back to the stairs (3, 4)"), - step_into("down_barrys_stairs", NORTH, "onto the stairs", at(BARRYS_HOUSE, "stairs")), + route("above_barrys_stairs", at(BARRYS_ROOM, "stairs", WEST), "back to the top of the stairs' mat (assumed: mirrors ours)"), + step_into("down_barrys_stairs", WEST, "onto the stairs", at(BARRYS_HOUSE, "stairs")), route("above_barrys_door", at(BARRYS_HOUSE, 6, 9, SOUTH), "back to his door"), step_into("out_of_barrys_house", SOUTH, "out of his door", at(TWINLEAF, "barry_doorstep", SOUTH)), // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. From bd15e1d8f0823f737220c1793ebea389a9496dd8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 16:15:57 -0800 Subject: [PATCH 19/98] BDSP AutoStory: a door step without a fade after it carries on Leaving the player's house (r12) the fade ended inside the step's own window, so waiting for one afterwards failed a step that had worked. Now a missing fade is logged and the next step's checks decide. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index f64bcbd32d..c23ff959ee 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -228,13 +228,14 @@ Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ const auto [x, y] = dpad_of(step.direction); const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); - // A step onto a door changes the map, and the fade can start after the - // walk's window (r10: Barry's door). Wait it out here, so the next step - // doesn't take it for its own stop. + // A step onto a door changes the map. The fade can start after the walk's + // window (r10: Barry's door), so wait for it here, so the next step doesn't + // take it for its own stop. It can also end inside the window (r12: our + // door), so no fade is fine: the next step checks where we are. if (walk.stop != WalkStop::TRANSITION && step.pose_after && step.pose_after->map != state.pose.map){ BlackScreenOverWatcher faded(COLOR_RED); if (wait_until(stream(), context(), 5000ms, {{faded}}) != 0){ - fail_step(stream(), label_of(step) + ": the map didn't change."); + stream().log(label_of(step) + ": no fade after the step; it was likely inside the step.", COLOR_ORANGE); } } std::vector at; From 2c039737514dc4bd47077335508d5456379406ed Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 16:56:57 -0800 Subject: [PATCH 20/98] BDSP AutoStory: 00.01 follows the game's scenes, from the event timeline From the decomp's scripts, Butters' recording and our runs: Mom's call needs no input; Barry knocks us back a tile at his door, so his house takes two steps north; his room's scene starts on arrival and walks us below the stairs, with no talk; and a door's fade can take 3 to 5 s, so a door step now waits up to 10 s. 00.01 moves into its own function (intro() was 54 lines). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 3 +- .../PokemonBDSP_AutoStory_StoryTable.cpp | 86 +++++++++++-------- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index c23ff959ee..f4b069b919 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -229,12 +229,13 @@ Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); // A step onto a door changes the map. The fade can start after the walk's + // window and take 3 to 5 s (r10, and by hand: black at +3.2 s, outside at +5.4 s), // window (r10: Barry's door), so wait for it here, so the next step doesn't // take it for its own stop. It can also end inside the window (r12: our // door), so no fade is fine: the next step checks where we are. if (walk.stop != WalkStop::TRANSITION && step.pose_after && step.pose_after->map != state.pose.map){ BlackScreenOverWatcher faded(COLOR_RED); - if (wait_until(stream(), context(), 5000ms, {{faded}}) != 0){ + if (wait_until(stream(), context(), 10000ms, {{faded}}) != 0){ stream().log(label_of(step) + ": no fade after the step; it was likely inside the step.", COLOR_ORANGE); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 5f1c227261..f928705d99 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -167,6 +167,54 @@ StoryStep skip_if(StoryStep step, std::string flag){ } +// 00.01: down from the bedroom, Barry's house, and out to the Lakefront. +StoryCheckpoint bedroom_to_lakefront(){ + return row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { + route("above_the_stairs", at(BEDROOM, "stairs", EAST), "the top of the mat, west of the stairs"), + // The stairs are entered moving east from the top of the two-tile mat, + // the decomp's stairs point (8, 4); east from (8, 5) or north are walls (r8, r9). + // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). + step_into("down_the_stairs", EAST, "onto the stairs", at_or_next_to(HOME, "stairs", SOUTH)), + // Automatic on arrival (1F frame script): Mom walks over, talks, walks back to + // (7, 8). No input; Butters' 1.1 s south hold fell inside the fade (timeline 00.01 #2). + scene("mom_calls", at_or_next_to(HOME, "stairs", SOUTH), + "Mom calls the player over as they come down; we stay put"), + route("above_the_door", at(HOME, 6, 9, SOUTH), "past Mom at (7, 8) to above the door"), + step_into("mom_warning", SOUTH, "the door's tile (6, 10): Mom's warning (coord event, VAR_PLAYER_HOUSE_STATE 1)"), + scene("mom_warns_us", at(HOME, "door", SOUTH), "\"don't go into the tall grass\""), + step_into("out_of_the_house", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), + route("short_of_barrys_door", at(TWINLEAF, 10, 12, WEST), "a tile east of Barry's doorstep"), + step_into("barrys_door", WEST, "the trigger on his doorstep (9, 12)"), + // Barry bumps into us and knocks us back a tile, still facing north (decomp; + // r10: the first north tap after the scene gave no warp, the second did). + scene("barry_bursts_out", at(TWINLEAF, 9, 13, NORTH), "Barry runs out, bumps into us and runs back in", "a"), + scene("barry_goes_home", at(TWINLEAF, 9, 13, NORTH), "his second lines (Butters waited for two)", "a"), + legs("back_onto_barrys_doorstep", {{.direction = NORTH, .tiles = 1}}, "back to (9, 12); its trigger is spent"), + step_into("into_barrys_house", NORTH, "his door", at(BARRYS_HOUSE, "door", NORTH)), + legs("off_barrys_mat", {{.direction = NORTH, .tiles = 1}}, "off the door's mat, which no route starts on"), + // Like ours: stairs are entered sideways from the top of their mat, the + // stairs point (r11: north from (2, 4) only bumps the wall). + route("below_barrys_stairs", at(BARRYS_HOUSE, "stairs", EAST), "past his mother at (4, 6) to the top of the stairs' mat"), + step_into("up_barrys_stairs", EAST, "onto the stairs", at(BARRYS_ROOM, "stairs")), + // Automatic on arrival (2F frame script), not a talk: Barry runs past us down + // the stairs and the game walks us a tile south of them (decomp; Butters + // never walked to him). + scene("barry_runs_to_the_lake", at(BARRYS_ROOM, 3, 5, NORTH), "Barry's lines; he runs off to the lake", "a"), + // Butters went west from there straight into the stairs; ours are entered from + // the mat's top tile, so go there first (unchecked: which works). + route("above_barrys_stairs", at(BARRYS_ROOM, "stairs", WEST), "back to the top of the stairs' mat"), + step_into("down_barrys_stairs", WEST, "onto the stairs", at(BARRYS_HOUSE, "stairs")), + route("above_barrys_door", at(BARRYS_HOUSE, 6, 9, SOUTH), "back to his door"), + step_into("out_of_barrys_house", SOUTH, "out of his door", at(TWINLEAF, "barry_doorstep", SOUTH)), + // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. + route("below_barrys_warning", at(ROUTE_201, 15, 19, NORTH), "up Twinleaf's exit path onto Route 201"), + step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), + scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; we stay on the trigger (assumed)"), + // Route 201's lower level, row 16, clear of the grass, then the Lakefront. + route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), + }); +} + std::vector intro(){ StoryStep new_game = make(StoryStepKind::NEW_GAME, "intro", "language, appearance, the player's name, Barry's default name"); new_game.pose_after = at(BEDROOM, "start", NORTH); @@ -175,43 +223,7 @@ std::vector intro(){ skip_if(new_game, "save_loaded"), make(StoryStepKind::OPTIONS, "settings", "fast text, Send to Boxes automatic, no nickname prompt, Autosave off"), }, StoryFailure::STOP), - row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { - route("above_the_stairs", at(BEDROOM, "stairs", EAST), "the top of the mat, west of the stairs"), - // The stairs are entered moving east from the top of the two-tile mat, - // the decomp's stairs point (8, 4); east from (8, 5) or north are walls (r8, r9). - // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). - step_into("down_the_stairs", EAST, "onto the stairs", at_or_next_to(HOME, "stairs", SOUTH)), - // Butters' recording walked 1.1 s south before her first line. - scene("mom_calls", at_or_next_to(HOME, "stairs", SOUTH), - "Mom calls the player over as they come down; we stay put (assumed)"), - route("above_the_door", at(HOME, 6, 9, SOUTH), "past Mom at (7, 8) to above the door"), - step_into("mom_warning", SOUTH, "the door's tile (6, 10): Mom's warning (coord event, VAR_PLAYER_HOUSE_STATE 1)"), - scene("mom_warns_us", at(HOME, "door", SOUTH), "\"don't go into the tall grass\""), - step_into("out_of_the_house", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), - route("short_of_barrys_door", at(TWINLEAF, 10, 12, WEST), "a tile east of Barry's doorstep"), - step_into("barrys_door", WEST, "the trigger on his doorstep (9, 12)"), - scene("barry_bursts_out", at(TWINLEAF, "barry_doorstep"), "Barry runs out, bumps into us and runs back in", "a"), - scene("barry_goes_home", at(TWINLEAF, "barry_doorstep"), "his second lines (Butters waited for two)", "a"), - step_into("into_barrys_house", NORTH, "his door", at(BARRYS_HOUSE, "door", NORTH)), - legs("off_barrys_mat", {{.direction = NORTH, .tiles = 1}}, "off the door's mat, which no route starts on"), - // Like ours: stairs are entered sideways from the top of their mat, the - // stairs point (r11: north from (2, 4) only bumps the wall). - route("below_barrys_stairs", at(BARRYS_HOUSE, "stairs", EAST), "past his mother at (4, 6) to the top of the stairs' mat"), - step_into("up_barrys_stairs", EAST, "onto the stairs", at(BARRYS_ROOM, "stairs")), - route("beside_barry", at(BARRYS_ROOM, "barry_talk", EAST), "west of Barry, who faces his PC (assumed talk tile)"), - talk("barry", "Barry in his room"), - scene("barry_heads_for_the_lake", at(BARRYS_ROOM, "barry_talk", EAST), "he runs off to the lake", "a"), - route("above_barrys_stairs", at(BARRYS_ROOM, "stairs", WEST), "back to the top of the stairs' mat (assumed: mirrors ours)"), - step_into("down_barrys_stairs", WEST, "onto the stairs", at(BARRYS_HOUSE, "stairs")), - route("above_barrys_door", at(BARRYS_HOUSE, 6, 9, SOUTH), "back to his door"), - step_into("out_of_barrys_house", SOUTH, "out of his door", at(TWINLEAF, "barry_doorstep", SOUTH)), - // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. - route("below_barrys_warning", at(ROUTE_201, 15, 19, NORTH), "up Twinleaf's exit path onto Route 201"), - step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), - scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; we stay on the trigger (assumed)"), - // Route 201's lower level, row 16, clear of the grass, then the Lakefront. - route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), - }), + bedroom_to_lakefront(), }; } From d6af718a1c3a2f017a602e318c367547e4fc21bd Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 17:21:14 -0800 Subject: [PATCH 21/98] BDSP AutoStory: Route 201 stops at Barry's warning and turns west His trigger spans the exit path, and the planner reached the row below it by running north to a wall and back through it (r14), into the row-19 pull-back. Exact legs now end on row 19, step onto the warning, then go west at once, away from Barry at (15, 17). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index f928705d99..2b4fce10f5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -207,9 +207,15 @@ StoryCheckpoint bedroom_to_lakefront(){ route("above_barrys_door", at(BARRYS_HOUSE, 6, 9, SOUTH), "back to his door"), step_into("out_of_barrys_house", SOUTH, "out of his door", at(TWINLEAF, "barry_doorstep", SOUTH)), // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. - route("below_barrys_warning", at(ROUTE_201, 15, 19, NORTH), "up Twinleaf's exit path onto Route 201"), + // Barry's warning covers the whole exit path (14..19, 18), so it ends our walk + // north: exact legs to the row below it, never a run to a wall through it (r14 + // ran north to (15, 13) and back, into it and the row-19 pull-back, in a loop). + legs("below_barrys_warning", {{.direction = EAST, .tiles = 6}, {.direction = NORTH, .tiles = 18}}, + "along row 12, then up Twinleaf's exit to Route 201 (15, 19)"), step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), - scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; we stay on the trigger (assumed)"), + scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; he then stands north of us at (15, 17)"), + // West at once, along row 18: north is Barry, south is row 19, which pulls us back. + legs("away_from_barry", {{.direction = WEST, .tiles = 3}}, "to (12, 18), clear of Barry's column"), // Route 201's lower level, row 16, clear of the grass, then the Lakefront. route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), }); From 61b5bdd3c9a769a8ef46385f7a0048d258c97b30 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 17:22:38 -0800 Subject: [PATCH 22/98] BDSP AutoStory: 00.01's Route 201 stretch in its own function (52 lines) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryTable.cpp | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 2b4fce10f5..fe5c4a6a63 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -167,9 +167,27 @@ StoryStep skip_if(StoryStep step, std::string flag){ } +// 00.01's last stretch: Barry's Route 201 warning, then west to the Lakefront. +std::vector route_201_to_the_lakefront(){ + return { + // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. + // Barry's warning covers the whole exit path (14..19, 18), so it ends our walk + // north: exact legs to the row below it, never a run to a wall through it (r14 + // ran north to (15, 13) and back, into it and the row-19 pull-back, in a loop). + legs("below_barrys_warning", {{.direction = EAST, .tiles = 6}, {.direction = NORTH, .tiles = 18}}, + "along row 12, then up Twinleaf's exit to Route 201 (15, 19)"), + step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), + scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; he then stands north of us at (15, 17)"), + // West at once, along row 18: north is Barry, south is row 19, which pulls us back. + legs("away_from_barry", {{.direction = WEST, .tiles = 3}}, "to (12, 18), clear of Barry's column"), + // Route 201's lower level, row 16, clear of the grass, then the Lakefront. + route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), + }; +} + // 00.01: down from the bedroom, Barry's house, and out to the Lakefront. StoryCheckpoint bedroom_to_lakefront(){ - return row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), { + std::vector steps{ route("above_the_stairs", at(BEDROOM, "stairs", EAST), "the top of the mat, west of the stairs"), // The stairs are entered moving east from the top of the two-tile mat, // the decomp's stairs point (8, 4); east from (8, 5) or north are walls (r8, r9). @@ -206,19 +224,11 @@ StoryCheckpoint bedroom_to_lakefront(){ step_into("down_barrys_stairs", WEST, "onto the stairs", at(BARRYS_HOUSE, "stairs")), route("above_barrys_door", at(BARRYS_HOUSE, 6, 9, SOUTH), "back to his door"), step_into("out_of_barrys_house", SOUTH, "out of his door", at(TWINLEAF, "barry_doorstep", SOUTH)), - // The guitarist's trigger on Twinleaf's row 3 fires only before his talk upstairs. - // Barry's warning covers the whole exit path (14..19, 18), so it ends our walk - // north: exact legs to the row below it, never a run to a wall through it (r14 - // ran north to (15, 13) and back, into it and the row-19 pull-back, in a loop). - legs("below_barrys_warning", {{.direction = EAST, .tiles = 6}, {.direction = NORTH, .tiles = 18}}, - "along row 12, then up Twinleaf's exit to Route 201 (15, 19)"), - step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), - scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; he then stands north of us at (15, 17)"), - // West at once, along row 18: north is Barry, south is row 19, which pulls us back. - legs("away_from_barry", {{.direction = WEST, .tiles = 3}}, "to (12, 18), clear of Barry's column"), - // Route 201's lower level, row 16, clear of the grass, then the Lakefront. - route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), - }); + }; + for (StoryStep& step : route_201_to_the_lakefront()){ + steps.push_back(std::move(step)); + } + return row("00.01", "Bedroom to the Verity Lakefront", at(BEDROOM, "start", NORTH), std::move(steps)); } std::vector intro(){ From 35ae70369db3f2866949dbcf3b0a2defb96b6e71 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 17:31:48 -0800 Subject: [PATCH 23/98] BDSP AutoStory: take the Azure Flute if the save has one, then the top row to the stairs BDSP leaves an Azure Flute on the bedroom floor at (7, 4) when the console has Legends: Arceus data (found by hand on console B). One tolerant step faces that tile and presses A, B-ing through any text; without the ball nothing happens. Then up onto (7, 4) and east over the stairs' mat, one turn fewer than before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Lake.cpp | 18 ++++++++++++++++++ .../PokemonBDSP_AutoStory_StoryTable.cpp | 6 +++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp index b15bcfc445..cfb3177f9f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp @@ -96,6 +96,23 @@ Pokemon::Outcome choose_starter(StoryExecutor& executor, Pokemon::RunState&, con return Pokemon::outcome_ok(); } +// 00.01: BDSP puts an Azure Flute on the bedroom floor at (7, 4) when the console +// has Legends: Arceus save data, and nothing there otherwise. Facing that tile: +// A, and if text follows ("You found an Azure Flute!", then the pocket line), B +// through it. Without the ball the A does nothing and we go on. +void azure_flute_if_there(VideoStream& stream, ProControllerContext& context){ + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + for (int presses = 0; presses < 6; presses++){ + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) != 0){ + return; + } + pbf_press_button(context, BUTTON_B, 80ms, 1000ms); + context.wait_for_all_requests(); + } +} + // 00.02: after the Starly battle, Rowan's lines and the walk out of the // lake, B for a minute. void after_starly(VideoStream&, ProControllerContext& context){ @@ -138,6 +155,7 @@ void catch_tutorial(VideoStream& stream, ProControllerContext& context){ void add_lake_events(StoryEvents& events){ + events.emplace("azure_flute_if_there", console_event(azure_flute_if_there)); events.emplace("starter_briefcase", console_event(starter_briefcase)); events.emplace("choose_starter", choose_starter); events.emplace("fight_starly", console_event(fight_starly)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index fe5c4a6a63..36cf7fc72a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -188,7 +188,11 @@ std::vector route_201_to_the_lakefront(){ // 00.01: down from the bedroom, Barry's house, and out to the Lakefront. StoryCheckpoint bedroom_to_lakefront(){ std::vector steps{ - route("above_the_stairs", at(BEDROOM, "stairs", EAST), "the top of the mat, west of the stairs"), + // The top row: the Azure Flute's tile (7, 4), then east over the stairs' mat. + route("below_the_flute", at(BEDROOM, 7, 5, NORTH), "below the Azure Flute's tile"), + event("azure_flute_if_there", "take the Azure Flute if this save has one", at(BEDROOM, 7, 5, NORTH)), + legs("onto_the_flute_tile", {{.direction = NORTH, .tiles = 1}, {.direction = EAST, .tiles = 1}}, + "up onto (7, 4), then east onto the top of the stairs' mat (8, 4)"), // The stairs are entered moving east from the top of the two-tile mat, // the decomp's stairs point (8, 4); east from (8, 5) or north are walls (r8, r9). // The stairs' warp lands on 1F's stairs; a tile below is allowed (assumed). From fa65519f50e39dc271cb5f06d47e48bd8f6e1659 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 17:47:59 -0800 Subject: [PATCH 24/98] BDSP AutoStory: face the Azure Flute's tile before pressing A The route to (7, 5) ends facing east, its last leg's way (r15), so A hit nothing and the ball still blocked the step north. A short tap north only turns. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp index cfb3177f9f..a762206b69 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp @@ -101,6 +101,9 @@ Pokemon::Outcome choose_starter(StoryExecutor& executor, Pokemon::RunState&, con // A, and if text follows ("You found an Azure Flute!", then the pocket line), B // through it. Without the ball the A does nothing and we go on. void azure_flute_if_there(VideoStream& stream, ProControllerContext& context){ + // The route ends facing the way its last leg went (east, r15): a short tap + // north only turns us to face the tile. + pbf_press_dpad(context, DPAD_UP, 40ms, 400ms); pbf_press_button(context, BUTTON_A, 80ms, 0ms); context.wait_for_all_requests(); for (int presses = 0; presses < 6; presses++){ From 4fe10eee0e3f4848d528de7401e6997e56a00c4e Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 18:04:08 -0800 Subject: [PATCH 25/98] BDSP AutoStory: Route 201's path west of Barry's warning, measured on console B r16 by hand: west of the exit path, row 18 is trees and the 201 sign stands at (13, 18); the dirt goes on along row 17, where the map had the sign. After the warning: west 1, north 1, then west. The map, the route record and the segment-00 line follow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp | 6 ++++-- .../AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp | 2 +- .../PokemonBDSP/Programs/AutoStory/Maps/Route201.json | 6 +++--- .../Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 5 +++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp index 9736436658..e0ee6d0579 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RouteRecords.cpp @@ -32,8 +32,10 @@ std::vector checkpoint_0001_records(){ }, "Segment00Lines line 2, not walked by the program yet"}, {"00.01", "Route 201: Barry's tall-grass warning, then west to the Lakefront", "Route 201", "Route201", true, map_point("Route201", "route_201_arrival"), { {Direction::NORTH, 6, LegEnd::COUNT, "up the exit path to (15, 18): Barry's warning fires on that row"}, - {Direction::WEST, 3, LegEnd::COUNT, "after the scene, along row 18 to column 12"}, - {Direction::NORTH, 2, LegEnd::COUNT, "to row 16, the lower level's row to the Lakefront"}, + {Direction::WEST, 1, LegEnd::COUNT, "after the scene, to (14, 18); west of it is the 201 sign and trees (r16)"}, + {Direction::NORTH, 1, LegEnd::COUNT, "onto row 17, the top of the exit path's T"}, + {Direction::WEST, 2, LegEnd::COUNT, "along row 17 to column 12"}, + {Direction::NORTH, 1, LegEnd::COUNT, "to row 16, the lower level's row to the Lakefront"}, {Direction::WEST, 12, LegEnd::COUNT, "to (0, 16), the route's last tile"}, {Direction::WEST, 0, LegEnd::BANNER, "on west onto the Verity Lakefront"}, }, "Segment00Lines lines 2 and 3, not walked by the program yet; traced on the Route201 map, the decomp's since #111"}, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp index 70b9d9bac1..251c399195 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp @@ -124,7 +124,7 @@ const std::vector& lines(){ twinleaf_point("barry_doorstep"), twinleaf({15, 12}), twinleaf_point("twinleaf_north_exit"), route201_point("barry_warning"), }, "Barry's tall-grass warning", {"Barry's tall-grass warning"}), walked("00.01", "Route 201's west half to the lake's entrance", { - route201_point("barry_warning"), route201({12, 18}), route201({12, 16}), route201({0, 16}), + route201_point("barry_warning"), route201({14, 18}), route201({14, 17}), route201({12, 17}), route201({12, 16}), route201({0, 16}), lakefront({20, 23}), lakefront({20, 21}), lakefront({17, 21}), lakefront_point("lake_entrance"), }, "Barry at the lake's entrance", {"Barry at the lake's entrance", "Route 201's row 19 after the warning"}), walked("00.03", "the lake's entrance back to Rowan", { diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json index 88f0b99f7e..05b22e405a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json @@ -1,6 +1,6 @@ { "name": "Route201", - "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0005.bin, files/fielddata/land_data/land_data_release/narc_0006.bin; zone_event narc_0327", "revision": "default branch", "date": "2026-10-31"}, + "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0005.bin, files/fielddata/land_data/land_data_release/narc_0006.bin; zone_event narc_0327", "revision": "default branch", "console": "row 18 west of the exit path (8..13, 18) is trees on the console (r16, by hand), open in the decomp; the 201 sign stands at (13, 18), not (13, 17)", "date": "2026-10-31"}, "tiles": [ "################################################################", "################################################################", @@ -19,8 +19,8 @@ "....................~~~~~~~~~~............................######", "....................~~~~~~~~~~............................######", "....................~~~~~~~~~~............................######", - "########.....S......##########............................######", - "########............##########............~~~~~~~~~~~~~~~~######", + "########............##########............................######", + "#############S......##########............~~~~~~~~~~~~~~~~######", "##############....############################~~~~~~~~~~~~######", "##############....############################~~~~~~~~~~~~######", "##############....##############################################", diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 36cf7fc72a..2f9d74af3b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -178,8 +178,9 @@ std::vector route_201_to_the_lakefront(){ "along row 12, then up Twinleaf's exit to Route 201 (15, 19)"), step_into("barrys_warning", NORTH, "the trigger (14..19, 18)"), scene("barry_warns_us", at(ROUTE_201, "barry_warning"), "Barry's tall-grass warning; he then stands north of us at (15, 17)"), - // West at once, along row 18: north is Barry, south is row 19, which pulls us back. - legs("away_from_barry", {{.direction = WEST, .tiles = 3}}, "to (12, 18), clear of Barry's column"), + // West at once, never back south onto row 19, which pulls us back. + legs("away_from_barry", {{.direction = WEST, .tiles = 1}, {.direction = NORTH, .tiles = 1}, {.direction = WEST, .tiles = 2}}, + "to (12, 17): row 18 west of the path is trees (r16), the dirt goes on along row 17"), // Route 201's lower level, row 16, clear of the grass, then the Lakefront. route("below_the_lake_entrance", at(LAKEFRONT, "below_lake_entrance", NORTH), "west along Route 201's lower level to the Lakefront"), }; From 3c830ff3e657f68eb47012276ad1284d44dce2df Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 18:12:19 -0800 Subject: [PATCH 26/98] BDSP AutoStory: 00.02 ends where the Lakefront's scene leaves us After the lake, the Lakefront's frame script walks the player 3 south, to (17, 14) or a tile below (decomp, event timeline 00.02 #5), not onto the entrance (17, 12). 00.02's end and 00.03's start take either. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 2f9d74af3b..80f4f3d8b0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -256,10 +256,12 @@ std::vector lake_and_pokedex(){ event("starter_briefcase", "the lake's scene, to the open briefcase"), event("choose_starter", "the starter from the program's option"), event("fight_starly", "the Starly battle"), - // The scene walks us out; the game leaves us on the entrance tile facing south (assumed). - event("after_starly", "Rowan's lines and the walk out of the lake", at(LAKEFRONT, "lake_entrance", SOUTH)), + // The game walks us out of the lake, then the Lakefront's scene walks us 3 + // south: (17, 14) facing south, or a tile below (decomp; timeline 00.02 #5). + event("after_starly", "Rowan's lines and the walk out of the lake", + at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH)), }), - row("00.03", "Back to Sandgem: Rowan, Dawn and the Pokedex", at(LAKEFRONT, "lake_entrance", SOUTH), { + row("00.03", "Back to Sandgem: Rowan, Dawn and the Pokedex", at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH), { route("short_of_rowan", at(ROUTE_201, 4, 14, EAST), "back east onto Route 201's lower level"), step_into("rowans_stop", EAST, "the trigger (5, 14..16)"), scene("rowan_and_dawn", at(ROUTE_201, "rowan_stop"), "Rowan and Dawn come back for the Pokedex's owner"), From 9c6520b6e8db6b7847a0e83756a3271033bd3669 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:07:23 -0800 Subject: [PATCH 27/98] BDSP AutoStory: finish a scene's late lines before the next walk Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index f4b069b919..f347bc1137 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -264,8 +264,20 @@ Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ } Outcome StoryExecutor::mash(RunState&, const StoryStep& step){ + const Button button = step.variant == "a" ? BUTTON_A : BUTTON_B; wait_for_dialogue(stream(), context(), label_of(step)); - finish_dialogue(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step)); + finish_dialogue(stream(), context(), button, label_of(step)); + // A scene can go quiet while someone walks off, then speak again: Dawn's + // "please wait for me!" comes after Rowan leaves (r23). A walk that + // meets such a line takes it for a trainer, so take a few more. + for (size_t tail = 0; tail < 3; tail++){ + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream(), context(), 4000ms, {{dialog}}) != 0){ + break; + } + stream().log("[Story] " + label_of(step) + ": the scene spoke again; finishing it."); + finish_dialogue(stream(), context(), button, label_of(step)); + } return Pokemon::outcome_ok(); } From 34a05faa9514e8b5d6c3462fc217bec4b952a3fe Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:15:42 -0800 Subject: [PATCH 28/98] BDSP AutoStory: 00.03 goes home for the Running Shoes after Rowan's scene BDSP fades to the house after Barry's line, where Mom gives the Running Shoes (console r24); Diamond's decomp has no such scene here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 80f4f3d8b0..09a917ce30 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -264,9 +264,16 @@ std::vector lake_and_pokedex(){ row("00.03", "Back to Sandgem: Rowan, Dawn and the Pokedex", at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH), { route("short_of_rowan", at(ROUTE_201, 4, 14, EAST), "back east onto Route 201's lower level"), step_into("rowans_stop", EAST, "the trigger (5, 14..16)"), - scene("rowan_and_dawn", at(ROUTE_201, "rowan_stop"), "Rowan and Dawn come back for the Pokedex's owner"), + // BDSP, unlike Diamond: after Rowan leaves, Dawn's and Barry's lines, then + // the game fades to the house, where Mom gives the Running Shoes. The + // scene ends beside her on the rug, facing her (console, r24). + scene("rowan_and_dawn", at(HOME, 6, 8, EAST), "Rowan and Dawn come back; Barry, then home for the Running Shoes"), + // Mom's door warning (var 1) was spent in 00.01: the mat is a plain door now (assumed). + route("above_the_door_again", at(HOME, 6, 9, SOUTH), "round Mom to above the door"), + step_into("onto_the_door_mat", SOUTH, "the door's tile (6, 10)", at(HOME, "door", SOUTH)), + step_into("out_of_the_house_again", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), // Through the grass now, with a starter: wild battles are run from. - route("short_of_dawn", at(SANDGEM, 3, 12, EAST), "east along Route 201 to Sandgem's edge"), + route("short_of_dawn", at(SANDGEM, 3, 12, EAST), "Twinleaf, then east along Route 201 to Sandgem's edge"), step_into("dawns_stop", EAST, "the trigger (4, 10..15)"), event("rowan_gives_pokedex", "Dawn walks us to the lab: Rowan, no nickname, the Pokedex", at(LAB, "rowan_talk", NORTH)), From d6709c56a22869c62219c39d0966c6fa192829ee Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:20:59 -0800 Subject: [PATCH 29/98] BDSP AutoStory: a scene that ends on another map waits for the fade and the lines after it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index f347bc1137..6344653ff9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -263,20 +263,43 @@ Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ return Pokemon::outcome_ok(); } -Outcome StoryExecutor::mash(RunState&, const StoryStep& step){ +namespace{ + +// The text to the overworld, then any lines that follow a quiet spell: a +// scene can go quiet while someone walks off, then speak again (Dawn's +// "please wait for me!" after Rowan leaves, r23). A walk that meets such a +// line takes it for a trainer. +void finish_scene_text(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label){ + finish_dialogue(stream, context, button, label); + for (size_t tail = 0; tail < 3; tail++){ + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, 4000ms, {{dialog}}) != 0){ + return; + } + stream.log("[Story] " + label + ": the scene spoke again; finishing it."); + finish_dialogue(stream, context, button, label); + } +} + +} + +Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ const Button button = step.variant == "a" ? BUTTON_A : BUTTON_B; wait_for_dialogue(stream(), context(), label_of(step)); - finish_dialogue(stream(), context(), button, label_of(step)); - // A scene can go quiet while someone walks off, then speak again: Dawn's - // "please wait for me!" comes after Rowan leaves (r23). A walk that - // meets such a line takes it for a trainer, so take a few more. - for (size_t tail = 0; tail < 3; tail++){ + finish_scene_text(stream(), context(), button, label_of(step)); + // A scene that ends on another map fades there after a quiet spell, and + // can speak again on arrival (r25: Barry, more than 4 s, the fade home, + // then Mom). Wait for the fade, then for the lines after it. + if (step.pose_after && step.pose_after->map != state.pose.map){ + BlackScreenOverWatcher faded(COLOR_RED); + if (wait_until(stream(), context(), 20000ms, {{faded}}) != 0){ + stream().log(label_of(step) + ": no fade to " + step.pose_after->map + " after the scene.", COLOR_ORANGE); + return Pokemon::outcome_ok(); + } ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream(), context(), 4000ms, {{dialog}}) != 0){ - break; + if (wait_until(stream(), context(), 10000ms, {{dialog}}) == 0){ + finish_scene_text(stream(), context(), button, label_of(step)); } - stream().log("[Story] " + label_of(step) + ": the scene spoke again; finishing it."); - finish_dialogue(stream(), context(), button, label_of(step)); } return Pokemon::outcome_ok(); } From 660c657760527934c9c9574098f3885428f1698b Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:27:06 -0800 Subject: [PATCH 30/98] BDSP AutoStory: a scene that ends on another map presses B until the lines on arrival The fade home is a dim, not a black screen, and the scene waited for a press on a line the detector missed (r26). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 6344653ff9..9b00d74147 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -287,19 +287,22 @@ Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ const Button button = step.variant == "a" ? BUTTON_A : BUTTON_B; wait_for_dialogue(stream(), context(), label_of(step)); finish_scene_text(stream(), context(), button, label_of(step)); - // A scene that ends on another map fades there after a quiet spell, and - // can speak again on arrival (r25: Barry, more than 4 s, the fade home, - // then Mom). Wait for the fade, then for the lines after it. + // A scene that ends on another map can stall on a line our detector + // misses, and only goes on (to the dim fade home, then Mom) at a press: + // r26 sat 30 s until the next walk's first press. B is safe on the + // overworld, so press it until the lines on arrival start. if (step.pose_after && step.pose_after->map != state.pose.map){ - BlackScreenOverWatcher faded(COLOR_RED); - if (wait_until(stream(), context(), 20000ms, {{faded}}) != 0){ - stream().log(label_of(step) + ": no fade to " + step.pose_after->map + " after the scene.", COLOR_ORANGE); - return Pokemon::outcome_ok(); - } ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream(), context(), 10000ms, {{dialog}}) == 0){ - finish_scene_text(stream(), context(), button, label_of(step)); + const int ret = run_until(stream(), context(), [](ProControllerContext& context){ + for (size_t press = 0; press < 25; press++){ + pbf_press_button(context, BUTTON_B, 80ms, 1500ms); + } + }, {{dialog}}); + if (ret != 0){ + stream().log(label_of(step) + ": no lines on arrival at " + step.pose_after->map + ".", COLOR_ORANGE); + return Pokemon::outcome_ok(); } + finish_scene_text(stream(), context(), button, label_of(step)); } return Pokemon::outcome_ok(); } From 9a79be3c5e679a8b40d2a338f207ba998fdee80f Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:34:58 -0800 Subject: [PATCH 31/98] BDSP AutoStory: one settle rule for scenes and door steps Scenes and steps through a door press through text and keep pressing B until the overworld has been quiet for 5 s: scenes pause, fade dimly and wait on boxes the detector misses (r23-r26). Stepping onto the house's door tile goes out once Mom's warning is spent (r27). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 101 +++++++++--------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 6 +- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 9b00d74147..12fa9cd64d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -11,15 +11,16 @@ #include "Common/Cpp/Filesystem/Filesystem.h" #include "Common/Cpp/PrettyPrint.h" #include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/Globals.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" @@ -223,21 +224,52 @@ Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.legs.back().direction)); } +namespace{ + +// One rule for scenes and map changes: v2's long B mashes, with an end +// check. Press through the text, and keep pressing B while the screen is +// quiet: a scene can pause while someone walks off (Dawn and Barry after +// Rowan leaves, r23), fade dimly, not to black, to another map (r25), or +// wait on a box the detector misses (r26). B never starts a talk on the +// overworld. Done once the overworld has shown, with no text, for 5 s of +// presses. +void settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label){ + const WallClock none = WallClock::max(); + const WallClock deadline = current_time() + 120s; + ShortDialogDetector dialog; + OverworldDetector overworld; + WallClock quiet_since = none; + while (current_time() < deadline){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const WallClock now = current_time(); + const bool talking = frame && dialog.detect(frame); + if (!talking && frame && overworld.detect(frame)){ + quiet_since = quiet_since == none ? now : quiet_since; + if (now - quiet_since >= 5000ms){ + return; + } + }else{ + quiet_since = none; + } + pbf_press_button(context, talking ? button : BUTTON_B, 80ms, 1200ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": not back on a quiet overworld within 120 s.", stream); +} + +} + Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ route_turn(stream(), context(), step.direction); const auto [x, y] = dpad_of(step.direction); const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); - // A step onto a door changes the map. The fade can start after the walk's - // window and take 3 to 5 s (r10, and by hand: black at +3.2 s, outside at +5.4 s), - // window (r10: Barry's door), so wait for it here, so the next step doesn't - // take it for its own stop. It can also end inside the window (r12: our - // door), so no fade is fine: the next step checks where we are. - if (walk.stop != WalkStop::TRANSITION && step.pose_after && step.pose_after->map != state.pose.map){ - BlackScreenOverWatcher faded(COLOR_RED); - if (wait_until(stream(), context(), 10000ms, {{faded}}) != 0){ - stream().log(label_of(step) + ": no fade after the step; it was likely inside the step.", COLOR_ORANGE); - } + // A step through a door: the fade takes 3 to 5 s and can start after the + // walk's window (r10), and arrival can start a scene (Mom's, 00.01). Settle + // as a scene does, so the next step starts on a quiet overworld. + if (step.pose_after && step.pose_after->map != state.pose.map){ + settle(stream(), context(), BUTTON_B, label_of(step)); } std::vector at; for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ @@ -263,47 +295,14 @@ Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ return Pokemon::outcome_ok(); } -namespace{ - -// The text to the overworld, then any lines that follow a quiet spell: a -// scene can go quiet while someone walks off, then speak again (Dawn's -// "please wait for me!" after Rowan leaves, r23). A walk that meets such a -// line takes it for a trainer. -void finish_scene_text(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label){ - finish_dialogue(stream, context, button, label); - for (size_t tail = 0; tail < 3; tail++){ - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream, context, 4000ms, {{dialog}}) != 0){ - return; - } - stream.log("[Story] " + label + ": the scene spoke again; finishing it."); - finish_dialogue(stream, context, button, label); - } -} - -} - -Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ - const Button button = step.variant == "a" ? BUTTON_A : BUTTON_B; - wait_for_dialogue(stream(), context(), label_of(step)); - finish_scene_text(stream(), context(), button, label_of(step)); - // A scene that ends on another map can stall on a line our detector - // misses, and only goes on (to the dim fade home, then Mom) at a press: - // r26 sat 30 s until the next walk's first press. B is safe on the - // overworld, so press it until the lines on arrival start. - if (step.pose_after && step.pose_after->map != state.pose.map){ - ShortDialogWatcher dialog(COLOR_CYAN); - const int ret = run_until(stream(), context(), [](ProControllerContext& context){ - for (size_t press = 0; press < 25; press++){ - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - } - }, {{dialog}}); - if (ret != 0){ - stream().log(label_of(step) + ": no lines on arrival at " + step.pose_after->map + ".", COLOR_ORANGE); - return Pokemon::outcome_ok(); - } - finish_scene_text(stream(), context(), button, label_of(step)); +Outcome StoryExecutor::mash(RunState&, const StoryStep& step){ + // The step before may have played the scene already (a door step settles + // its arrival scene), so its first line is not required. + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream(), context(), 15000ms, {{dialog}}) != 0){ + stream().log(label_of(step) + ": no line within 15 s; the step before played it.", COLOR_ORANGE); } + settle(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step)); return Pokemon::outcome_ok(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 09a917ce30..852b6cd2b4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -268,10 +268,10 @@ std::vector lake_and_pokedex(){ // the game fades to the house, where Mom gives the Running Shoes. The // scene ends beside her on the rug, facing her (console, r24). scene("rowan_and_dawn", at(HOME, 6, 8, EAST), "Rowan and Dawn come back; Barry, then home for the Running Shoes"), - // Mom's door warning (var 1) was spent in 00.01: the mat is a plain door now (assumed). + // Mom's warning was spent in 00.01, so the door's tile (6, 10) is a plain + // door now: stepping onto it goes out (r27). route("above_the_door_again", at(HOME, 6, 9, SOUTH), "round Mom to above the door"), - step_into("onto_the_door_mat", SOUTH, "the door's tile (6, 10)", at(HOME, "door", SOUTH)), - step_into("out_of_the_house_again", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), + step_into("out_of_the_house_again", SOUTH, "onto the door's tile (6, 10), and out", at(TWINLEAF, "house_doorstep", SOUTH)), // Through the grass now, with a starter: wild battles are run from. route("short_of_dawn", at(SANDGEM, 3, 12, EAST), "Twinleaf, then east along Route 201 to Sandgem's edge"), step_into("dawns_stop", EAST, "the trigger (4, 10..15)"), From 8df4b0ac46872876b3dcc9edf1720f73093730d9 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:40:42 -0800 Subject: [PATCH 32/98] BDSP AutoStory: routes run once Mom has given the Running Shoes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 11 +++++++++-- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h | 2 ++ .../AutoStory/PokemonBDSP_AutoStory_StoryTable.h | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 12fa9cd64d..5590809fe5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -32,6 +32,7 @@ #include "PokemonBDSP_AutoStory_NewGame.h" #include "PokemonBDSP_AutoStory_Settings.h" #include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" #include "PokemonBDSP_AutoStory_Talks.h" #include "PokemonBDSP_AutoStory_Walks.h" #include "PokemonBDSP_AutoStory_WildBattle.h" @@ -95,6 +96,8 @@ StoryExecutor::StoryExecutor(StoryEnv& env) void StoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ m_checkpoint = data.id; + // A row starts from its save: after the shoes' row, the save has them. + m_can_run = data.id > std::string(RUNNING_SHOES_ROW); const StoryWorld* south = story_plan_world("Route202", "Route202"); m_trainers = south ? story_trainers(*south) : std::vector{}; } @@ -102,7 +105,11 @@ void StoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ Outcome StoryExecutor::run(RunState& state, const StoryStep& step){ stream().log(std::format("[{}] {} {}: {}", m_checkpoint, Pokemon::story_step_kind_name(step.kind), step.name, step.why), COLOR_BLUE); try{ - return run_guarded(state, step); + Outcome outcome = run_guarded(state, step); + if (outcome.succeeded() && m_checkpoint == RUNNING_SHOES_ROW && step.name == RUNNING_SHOES_STEP){ + m_can_run = true; + } + return outcome; }catch (OperationFailedException& e){ if (m_stop_rows.contains(m_checkpoint)){ throw; @@ -150,7 +157,7 @@ Pose StoryExecutor::walk_to(const Pose& from, const Pose& goal, const StoryStep& }; const RouteWalkPlan plan{ [&](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, - label_of(step), {}, *costs + label_of(step), {}, *costs, m_can_run ? MoveSpeed::RUN : MoveSpeed::WALK }; const std::vector at = walk_route(stream(), context(), run_from_wild_battle, *maps, start, trainers, plan); if (south){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h index 74042cb605..88dbb00c09 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -149,6 +149,8 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ StoryEnv& m_env; std::string m_checkpoint; + // Whether the player has the Running Shoes: the pace routes are timed at. + bool m_can_run = false; // Route 202's trainers who can still see us, in story_plan_world()'s // world tiles; all of them again at each attempt. std::vector m_trainers; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h index db2b8f0683..a7cce9665d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h @@ -25,6 +25,11 @@ namespace PokemonBDSP{ // Poketch) and segment 01 (Jubilife to Floaroma). const std::vector& story_checkpoints(); +// Mom gives the Running Shoes in this row's step: from then on, B held +// with the D-pad runs (r24, r27). +constexpr const char* RUNNING_SHOES_ROW = "00.03"; +constexpr const char* RUNNING_SHOES_STEP = "rowan_and_dawn"; + } From cd29268efe43293f3d8cdf901f92a13474d762d4 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:52:54 -0800 Subject: [PATCH 33/98] BDSP AutoStory: a step or scene onto another map must see a fade r28 stood a tile beside the door, took the step for the way out and walked a whole route inside the house. Mom's scene leaves us at (5, 8) or (6, 8). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 33 +++++++++++++++---- .../PokemonBDSP_AutoStory_StoryTable.cpp | 6 ++-- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 5590809fe5..9b1831f013 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -13,6 +13,7 @@ #include "CommonFramework/Exceptions/OperationFailedException.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTools/ImageStats.h" #include "CommonFramework/Globals.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" @@ -239,22 +240,27 @@ namespace{ // Rowan leaves, r23), fade dimly, not to black, to another map (r25), or // wait on a box the detector misses (r26). B never starts a talk on the // overworld. Done once the overworld has shown, with no text, for 5 s of -// presses. -void settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label){ +// presses. Returns the darkest frame's brightness (0 to 255): a change of +// map fades, if only dimly (r24's fade home). +double settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label){ const WallClock none = WallClock::max(); const WallClock deadline = current_time() + 120s; ShortDialogDetector dialog; OverworldDetector overworld; WallClock quiet_since = none; + double darkest = 255; while (current_time() < deadline){ context.wait_for_all_requests(); const auto frame = stream.video().snapshot(); const WallClock now = current_time(); + if (frame){ + darkest = std::min(darkest, image_average(*frame.frame).sum() / 3); + } const bool talking = frame && dialog.detect(frame); if (!talking && frame && overworld.detect(frame)){ quiet_since = quiet_since == none ? now : quiet_since; if (now - quiet_since >= 5000ms){ - return; + return darkest; } }else{ quiet_since = none; @@ -265,6 +271,17 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c label + ": not back on a quiet overworld within 120 s.", stream); } +// A step or scene that ends on another map must have faded on the way: r28 +// stood beside the door, "went out", and walked a whole route in the house. +// Brightness 0 to 255; r24's dim fade home and every door go darker. +constexpr double FADE_DARKNESS = 40; +void require_fade(VideoStream& stream, const std::string& label, const std::string& map, bool faded){ + if (!faded){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no fade, so we are not on " + map + " (map mismatch).", stream); + } +} + } Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ @@ -276,7 +293,8 @@ Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ // walk's window (r10), and arrival can start a scene (Mom's, 00.01). Settle // as a scene does, so the next step starts on a quiet overworld. if (step.pose_after && step.pose_after->map != state.pose.map){ - settle(stream(), context(), BUTTON_B, label_of(step)); + const double darkest = settle(stream(), context(), BUTTON_B, label_of(step)); + require_fade(stream(), label_of(step), step.pose_after->map, walk.stop == WalkStop::TRANSITION || darkest < FADE_DARKNESS); } std::vector at; for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ @@ -302,14 +320,17 @@ Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ return Pokemon::outcome_ok(); } -Outcome StoryExecutor::mash(RunState&, const StoryStep& step){ +Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ // The step before may have played the scene already (a door step settles // its arrival scene), so its first line is not required. ShortDialogWatcher dialog(COLOR_CYAN); if (wait_until(stream(), context(), 15000ms, {{dialog}}) != 0){ stream().log(label_of(step) + ": no line within 15 s; the step before played it.", COLOR_ORANGE); } - settle(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step)); + const double darkest = settle(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step)); + if (step.pose_after && step.pose_after->map != state.pose.map){ + require_fade(stream(), label_of(step), step.pose_after->map, darkest < FADE_DARKNESS); + } return Pokemon::outcome_ok(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 852b6cd2b4..af0222569d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -266,8 +266,10 @@ std::vector lake_and_pokedex(){ step_into("rowans_stop", EAST, "the trigger (5, 14..16)"), // BDSP, unlike Diamond: after Rowan leaves, Dawn's and Barry's lines, then // the game fades to the house, where Mom gives the Running Shoes. The - // scene ends beside her on the rug, facing her (console, r24). - scene("rowan_and_dawn", at(HOME, 6, 8, EAST), "Rowan and Dawn come back; Barry, then home for the Running Shoes"), + // scene ends beside her on the rug, facing her: (6, 8) in r24 and r27, + // (5, 8) in r28. + scene("rowan_and_dawn", Pokemon::tile_pose(HOME, {TilePos{5, 8}, TilePos{6, 8}}, EAST), + "Rowan and Dawn come back; Barry, then home for the Running Shoes"), // Mom's warning was spent in 00.01, so the door's tile (6, 10) is a plain // door now: stepping onto it goes out (r27). route("above_the_door_again", at(HOME, 6, 9, SOUTH), "round Mom to above the door"), From f186ff94058c699d42a8a19d5db2a5cbe25749dd Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:57:34 -0800 Subject: [PATCH 34/98] BDSP AutoStory: line up on the counter before the door, away from Mom Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index af0222569d..5a1bd291c9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -272,7 +272,10 @@ std::vector lake_and_pokedex(){ "Rowan and Dawn come back; Barry, then home for the Running Shoes"), // Mom's warning was spent in 00.01, so the door's tile (6, 10) is a plain // door now: stepping onto it goes out (r27). - route("above_the_door_again", at(HOME, 6, 9, SOUTH), "round Mom to above the door"), + // Line up on the counter's wall west first: a plan from either tile + // ran east into Mom at (7, 8), who talks (r29). + route("by_the_counter", at(HOME, 5, 8, WEST), "west against the counter's end (4, 8)"), + route("above_the_door_again", at(HOME, 6, 9, SOUTH), "to above the door"), step_into("out_of_the_house_again", SOUTH, "onto the door's tile (6, 10), and out", at(TWINLEAF, "house_doorstep", SOUTH)), // Through the grass now, with a starter: wild battles are run from. route("short_of_dawn", at(SANDGEM, 3, 12, EAST), "Twinleaf, then east along Route 201 to Sandgem's edge"), From 600373ce13d5a4dfcc5bbd3d30859d2dd62b6579 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 20:03:07 -0800 Subject: [PATCH 35/98] BDSP AutoStory: settle counts quiet only after the fade on a change of map r30's scene went quiet for 5 s before the fade home, and the next walk met Mom's lines. No fade within the settle's 120 s is a map mismatch. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 9b1831f013..61f46799f3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -240,9 +240,14 @@ namespace{ // Rowan leaves, r23), fade dimly, not to black, to another map (r25), or // wait on a box the detector misses (r26). B never starts a talk on the // overworld. Done once the overworld has shown, with no text, for 5 s of -// presses. Returns the darkest frame's brightness (0 to 255): a change of -// map fades, if only dimly (r24's fade home). -double settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label){ +// presses. +// +// A step or scene that ends on another map (`fade`) must fade on the way, +// and only the quiet after the fade counts: r30's scene went quiet for 5 s +// before the fade home, and r28 "went out" a tile beside the door and +// walked a whole route in the house. No fade is a map mismatch. +constexpr double FADE_DARKNESS = 40; // brightness 0 to 255; r24's dim fade home goes darker +void settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label, bool fade){ const WallClock none = WallClock::max(); const WallClock deadline = current_time() + 120s; ShortDialogDetector dialog; @@ -257,10 +262,11 @@ double settle(VideoStream& stream, ProControllerContext& context, Button button, darkest = std::min(darkest, image_average(*frame.frame).sum() / 3); } const bool talking = frame && dialog.detect(frame); - if (!talking && frame && overworld.detect(frame)){ + if (!talking && frame && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS)){ quiet_since = quiet_since == none ? now : quiet_since; if (now - quiet_since >= 5000ms){ - return darkest; + stream.log(std::format("[Story] {}: settled; darkest frame {:.0f}.", label, darkest)); + return; } }else{ quiet_since = none; @@ -268,18 +274,10 @@ double settle(VideoStream& stream, ProControllerContext& context, Button button, pbf_press_button(context, talking ? button : BUTTON_B, 80ms, 1200ms); } OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": not back on a quiet overworld within 120 s.", stream); -} - -// A step or scene that ends on another map must have faded on the way: r28 -// stood beside the door, "went out", and walked a whole route in the house. -// Brightness 0 to 255; r24's dim fade home and every door go darker. -constexpr double FADE_DARKNESS = 40; -void require_fade(VideoStream& stream, const std::string& label, const std::string& map, bool faded){ - if (!faded){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": no fade, so we are not on " + map + " (map mismatch).", stream); - } + fade && darkest >= FADE_DARKNESS + ? label + ": no fade within 120 s, so we are not where the step goes (map mismatch)." + : label + ": not back on a quiet overworld within 120 s.", + stream); } } @@ -293,8 +291,7 @@ Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ // walk's window (r10), and arrival can start a scene (Mom's, 00.01). Settle // as a scene does, so the next step starts on a quiet overworld. if (step.pose_after && step.pose_after->map != state.pose.map){ - const double darkest = settle(stream(), context(), BUTTON_B, label_of(step)); - require_fade(stream(), label_of(step), step.pose_after->map, walk.stop == WalkStop::TRANSITION || darkest < FADE_DARKNESS); + settle(stream(), context(), BUTTON_B, label_of(step), walk.stop != WalkStop::TRANSITION); } std::vector at; for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ @@ -327,10 +324,8 @@ Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ if (wait_until(stream(), context(), 15000ms, {{dialog}}) != 0){ stream().log(label_of(step) + ": no line within 15 s; the step before played it.", COLOR_ORANGE); } - const double darkest = settle(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step)); - if (step.pose_after && step.pose_after->map != state.pose.map){ - require_fade(stream(), label_of(step), step.pose_after->map, darkest < FADE_DARKNESS); - } + settle(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step), + step.pose_after && step.pose_after->map != state.pose.map); return Pokemon::outcome_ok(); } From 12051d0ec96854d81d13c0e6bcd013a383160c74 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 20:08:24 -0800 Subject: [PATCH 36/98] BDSP AutoStory: settle's quiet needs a still screen too After r31's fade home a scripted walk-in ran 5 s with no text before Mom spoke, and the next walk met her lines. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 61f46799f3..9ff5bdf381 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -239,8 +239,9 @@ namespace{ // quiet: a scene can pause while someone walks off (Dawn and Barry after // Rowan leaves, r23), fade dimly, not to black, to another map (r25), or // wait on a box the detector misses (r26). B never starts a talk on the -// overworld. Done once the overworld has shown, with no text, for 5 s of -// presses. +// overworld. Done once the overworld has shown, with no text and a still +// screen, for 5 s of presses: after r31's fade home a scripted walk-in ran +// 5 s with no text before Mom spoke. // // A step or scene that ends on another map (`fade`) must fade on the way, // and only the quiet after the fade counts: r30's scene went quiet for 5 s @@ -254,6 +255,7 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c OverworldDetector overworld; WallClock quiet_since = none; double darkest = 255; + VideoSnapshot previous; while (current_time() < deadline){ context.wait_for_all_requests(); const auto frame = stream.video().snapshot(); @@ -262,7 +264,9 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c darkest = std::min(darkest, image_average(*frame.frame).sum() / 3); } const bool talking = frame && dialog.detect(frame); - if (!talking && frame && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS)){ + const bool still = frame && previous && walk_screen_change(*previous.frame, *frame.frame) < WALK_TAP_BLOCKED_CHANGE; + previous = frame; + if (!talking && still && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS)){ quiet_since = quiet_since == none ? now : quiet_since; if (now - quiet_since >= 5000ms){ stream.log(std::format("[Story] {}: settled; darkest frame {:.0f}.", label, darkest)); From fdf4fbff3777144fd438e3590c183f1c1011433a Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 20:19:04 -0800 Subject: [PATCH 37/98] BDSP AutoStory: line up on the table's corner, which the console puts at x 3 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 5a1bd291c9..beb2ee4710 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -272,9 +272,9 @@ std::vector lake_and_pokedex(){ "Rowan and Dawn come back; Barry, then home for the Running Shoes"), // Mom's warning was spent in 00.01, so the door's tile (6, 10) is a plain // door now: stepping onto it goes out (r27). - // Line up on the counter's wall west first: a plan from either tile + // Line up on the table's corner west first: a plan from either tile // ran east into Mom at (7, 8), who talks (r29). - route("by_the_counter", at(HOME, 5, 8, WEST), "west against the counter's end (4, 8)"), + route("by_the_counter", at(HOME, 4, 8, WEST), "west against the table's corner (3, 8)"), route("above_the_door_again", at(HOME, 6, 9, SOUTH), "to above the door"), step_into("out_of_the_house_again", SOUTH, "onto the door's tile (6, 10), and out", at(TWINLEAF, "house_doorstep", SOUTH)), // Through the grass now, with a starter: wild battles are run from. From 887d1defe7d134e00e1cc6dc06e41d7e2beeb124 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 20:26:54 -0800 Subject: [PATCH 38/98] BDSP AutoStory: a step through a door taps once and settles, which sees the fade r33 went out, but the fade fell inside the step's walk and settle saw none. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 9ff5bdf381..ba6663be24 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -288,15 +288,22 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ route_turn(stream(), context(), step.direction); + // A step through a door: one tap, then settle, which must see the fade + // (3 to 5 s) and then any scene on arrival (Mom's, 00.01). A walk's + // window could swallow the fade unseen (r33 was outside, "no fade"). + if (step.pose_after && step.pose_after->map != state.pose.map){ + const DpadPosition dpad = + step.direction == Direction::NORTH ? DPAD_UP : + step.direction == Direction::SOUTH ? DPAD_DOWN : + step.direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + pbf_press_dpad(context(), dpad, 160ms, 0ms); + stream().log(std::format("[{}] {}: a step {} through the door.", m_checkpoint, step.name, direction_name(step.direction))); + settle(stream(), context(), BUTTON_B, label_of(step), true); + return Pokemon::outcome_ok(); + } const auto [x, y] = dpad_of(step.direction); const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); - // A step through a door: the fade takes 3 to 5 s and can start after the - // walk's window (r10), and arrival can start a scene (Mom's, 00.01). Settle - // as a scene does, so the next step starts on a quiet overworld. - if (step.pose_after && step.pose_after->map != state.pose.map){ - settle(stream(), context(), BUTTON_B, label_of(step), walk.stop != WalkStop::TRANSITION); - } std::vector at; for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ at.push_back(pos.next(step.direction)); From 1039a3a13892479ad005efd42f5edbfaa8368d92 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 20:49:13 -0800 Subject: [PATCH 39/98] BDSP AutoStory: walk into Sandgem until Dawn's scene starts A leg variant, until_scene, taps until a dialogue opens. The route ends on Route 201's east edge, so no banner races her scene (r35, r36). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 10 ++++++++++ .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 6 ++++-- .../PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index ba6663be24..a4284592d2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -200,6 +200,16 @@ Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ } return Pokemon::outcome_ok(); } + // Into a scene that starts on the way, wherever it starts (Dawn's at + // Sandgem's edge, r35/r36: an exact tile and banner raced her scene). + if (step.variant == "until_scene"){ + const Pokemon::StoryLeg& leg = step.legs.front(); + route_turn(stream(), context(), leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, (size_t)leg.tiles, step.name); + stream().log(std::format("[{}] {}: taps {} until the scene: {} tiles, {}.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, walk_stop_name(walk.stop))); + return walk.stop == WalkStop::DIALOG ? Pokemon::outcome_ok() : Pokemon::outcome_failed("no_scene", step.name); + } const bool one_short_ok = step.variant == "one_short_ok"; if (!one_short_ok && !step.variant.empty()){ return Pokemon::outcome_failed("unknown_variant", step.variant); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index beb2ee4710..cffa1c16e8 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -278,8 +278,10 @@ std::vector lake_and_pokedex(){ route("above_the_door_again", at(HOME, 6, 9, SOUTH), "to above the door"), step_into("out_of_the_house_again", SOUTH, "onto the door's tile (6, 10), and out", at(TWINLEAF, "house_doorstep", SOUTH)), // Through the grass now, with a starter: wild battles are run from. - route("short_of_dawn", at(SANDGEM, 3, 12, EAST), "Twinleaf, then east along Route 201 to Sandgem's edge"), - step_into("dawns_stop", EAST, "the trigger (4, 10..15)"), + route("short_of_dawn", at(ROUTE_201, 63, 5, EAST), "Twinleaf, then east along Route 201 to its east edge"), + // Dawn's scene starts as we come into Sandgem (trigger (4, 10..15)); + // walk until it does rather than to a tile (r35, r36). + legs("into_sandgem", {{.direction = EAST, .tiles = 8}}, "east into Sandgem until Dawn's scene starts", "until_scene"), event("rowan_gives_pokedex", "Dawn walks us to the lab: Rowan, no nickname, the Pokedex", at(LAB, "rowan_talk", NORTH)), }), diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 00a9d683d0..94ff2044bd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -36,7 +36,7 @@ using Pokemon::TilePos; const std::map>& built_in_variants(){ static const std::map> variants{ {StoryStepKind::ROUTE, {"", "town", "robust"}}, - {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall"}}, + {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "until_scene"}}, {StoryStepKind::MASH, {"", "a"}}, {StoryStepKind::BATTLE, {"", "doubles"}}, }; From 9eb23d3bad671c98368318051785de9e9a6a3b24 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 20:56:31 -0800 Subject: [PATCH 40/98] BDSP AutoStory: to_dialogue legs, which also take a stop that a line follows Renamed from until_scene: the leg ends where a dialogue starts, and the next step plays the scene. Dawn locks us before her first line (r37). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 35 ++++++++++++++----- .../PokemonBDSP_AutoStory_StoryTable.cpp | 2 +- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 +- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index a4284592d2..a7bc8e834a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -193,6 +193,30 @@ Outcome StoryExecutor::route(RunState& state, const StoryStep& step){ return Pokemon::outcome_ok(walk_to(state.pose, *step.target, step)); } +namespace{ + +// A LEGS step "to_dialogue": taps its one leg until a dialogue starts on the +// way, wherever it starts (Dawn's at Sandgem's edge, r35/r36: an exact tile +// and banner raced her scene). The scene's lines are the next step's. +Outcome leg_to_dialogue(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ + const Pokemon::StoryLeg& leg = step.legs.front(); + route_turn(stream, context, leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const WalkResult walk = walk_tiles_dpad(stream, context, run_from_wild_battle, {x, y}, (size_t)leg.tiles, step.name); + stream.log(std::format("[{}] {}: taps {} to a dialogue: {} tiles, {}.", checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, walk_stop_name(walk.stop))); + if (walk.stop == WalkStop::DIALOG){ + return Pokemon::outcome_ok(); + } + // A scene can lock us before its first line: Dawn walks over first (r37). + ShortDialogWatcher dialog(COLOR_CYAN); + if (walk.stop == WalkStop::BLOCKED && wait_until(stream, context, 10000ms, {{dialog}}) == 0){ + return Pokemon::outcome_ok(); + } + return Pokemon::outcome_failed("no_dialogue", step.name); +} + +} + Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ if (step.variant == "to_wall"){ for (const Pokemon::StoryLeg& leg : step.legs){ @@ -200,15 +224,8 @@ Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ } return Pokemon::outcome_ok(); } - // Into a scene that starts on the way, wherever it starts (Dawn's at - // Sandgem's edge, r35/r36: an exact tile and banner raced her scene). - if (step.variant == "until_scene"){ - const Pokemon::StoryLeg& leg = step.legs.front(); - route_turn(stream(), context(), leg.direction); - const auto [x, y] = dpad_of(leg.direction); - const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, (size_t)leg.tiles, step.name); - stream().log(std::format("[{}] {}: taps {} until the scene: {} tiles, {}.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, walk_stop_name(walk.stop))); - return walk.stop == WalkStop::DIALOG ? Pokemon::outcome_ok() : Pokemon::outcome_failed("no_scene", step.name); + if (step.variant == "to_dialogue"){ + return leg_to_dialogue(stream(), context(), step, m_checkpoint); } const bool one_short_ok = step.variant == "one_short_ok"; if (!one_short_ok && !step.variant.empty()){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index cffa1c16e8..ee1eedb01b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -281,7 +281,7 @@ std::vector lake_and_pokedex(){ route("short_of_dawn", at(ROUTE_201, 63, 5, EAST), "Twinleaf, then east along Route 201 to its east edge"), // Dawn's scene starts as we come into Sandgem (trigger (4, 10..15)); // walk until it does rather than to a tile (r35, r36). - legs("into_sandgem", {{.direction = EAST, .tiles = 8}}, "east into Sandgem until Dawn's scene starts", "until_scene"), + legs("into_sandgem", {{.direction = EAST, .tiles = 8}}, "east into Sandgem until Dawn's scene starts", "to_dialogue"), event("rowan_gives_pokedex", "Dawn walks us to the lab: Rowan, no nickname, the Pokedex", at(LAB, "rowan_talk", NORTH)), }), diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 94ff2044bd..1919c4fdf9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -36,7 +36,7 @@ using Pokemon::TilePos; const std::map>& built_in_variants(){ static const std::map> variants{ {StoryStepKind::ROUTE, {"", "town", "robust"}}, - {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "until_scene"}}, + {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "to_dialogue"}}, {StoryStepKind::MASH, {"", "a"}}, {StoryStepKind::BATTLE, {"", "doubles"}}, }; From 9659b556c48d74f032f59697a6ba9f7cb27d936d Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 21:07:55 -0800 Subject: [PATCH 41/98] BDSP AutoStory: Dawn's tour leaves us a row or two below the Mart's door r39's walk west from row 11 ran on along row 12. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index ee1eedb01b..80eb74932c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -293,8 +293,12 @@ std::vector to_the_catch(){ row("00.04", "Home to Mom, then Dawn's catching tutorial on Route 202", at(LAB, "rowan_talk", NORTH), { route("above_the_lab_door", at(LAB, 7, 14, SOUTH), "down the lab to its door (7, 15)"), step_into("out_of_the_lab", SOUTH, "out of the lab", door_front(SANDGEM, "lab_door")), - // Dawn shows us the Center and the Mart; she leaves us by the Mart's door (assumed). - scene("dawn_shows_us_sandgem", door_front(SANDGEM, "poke_mart_door"), "Dawn's tour of Sandgem"), + // Automatic on arrival, so out_of_the_lab's settle plays it. She leaves us + // below the Mart's door (27, 10), a row or two down: r39's walk west + // from "row 11" ran on along row 12, which is open to Route 201. + scene("dawn_shows_us_sandgem", Pokemon::tile_pose(SANDGEM, { + TilePos{26, 11}, TilePos{27, 11}, TilePos{28, 11}, TilePos{26, 12}, TilePos{27, 12}, TilePos{28, 12} + }, NORTH), "Dawn's tour of Sandgem"), // First to one known tile: the trees that end the doors' row. route("to_the_row_end", at(SANDGEM, "front_row_east_end"), "east along the doors' row to the trees", "town"), route("to_route_201", at(ROUTE_201, "route_201_east_end"), "west along the doors' row out of Sandgem"), From 4833a7da01b70e9e65a42585d1897b1b50adc48e Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 21:14:18 -0800 Subject: [PATCH 42/98] BDSP AutoStory: a talk turns to the pose's facing first A route ends facing its last leg: r40 stood west of Mom facing north. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index a7bc8e834a..4d10e257d3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -338,7 +338,12 @@ Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.direction)); } -Outcome StoryExecutor::talk(RunState&, const StoryStep& step){ +Outcome StoryExecutor::talk(RunState& state, const StoryStep& step){ + // Face whom we talk to: a route ends facing its last leg, not its goal + // (r40 reached Mom facing north). They block the tile, so the tap only turns. + if (const std::optional facing = Pokemon::facing_of(state.pose)){ + route_turn(stream(), context(), *facing); + } pbf_press_button(context(), BUTTON_A, 80ms, 200ms); context().wait_for_all_requests(); ShortDialogWatcher dialog(COLOR_YELLOW); From 456be5fe5ba7b0e5c38ba0812d1d15b0baf9e891 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 21:16:48 -0800 Subject: [PATCH 43/98] BDSP AutoStory: entering the house sets us a tile inside, at (6, 9) r40's extra step off the mat put us beside the TV, above Mom. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 80eb74932c..335591903c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -303,8 +303,9 @@ std::vector to_the_catch(){ route("to_the_row_end", at(SANDGEM, "front_row_east_end"), "east along the doors' row to the trees", "town"), route("to_route_201", at(ROUTE_201, "route_201_east_end"), "west along the doors' row out of Sandgem"), route("home", at(TWINLEAF, "house_doorstep", NORTH), "west along Route 201 to Twinleaf, running from wild battles"), - step_into("into_the_house", NORTH, "the house's door", at(HOME, "door", NORTH)), - legs("off_the_mat", {{.direction = NORTH, .tiles = 1}}, "off the door's mat, which no route starts on"), + // The game sets us a tile inside, off the mat: (6, 9) (r40: the old extra + // step north put us beside the TV, above Mom). + step_into("into_the_house", NORTH, "the house's door", at(HOME, 6, 9, NORTH)), route("beside_mom", at(HOME, 6, 8, EAST), "west of Mom at (7, 8)"), talk("mom", "Mom"), scene("mom_running_shoes", at(HOME, 6, 8, EAST), "her lines"), From 980737e6264414ee7ab60129e4e48add95f353e7 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 21:32:51 -0800 Subject: [PATCH 44/98] BDSP AutoStory: leave Sandgem along the path, to Route 201's banner A to_map leg runs until a map's banner shows. Running the doors' row took us into the lab at its mat (r41, r42). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 21 +++++++++++++++++++ .../PokemonBDSP_AutoStory_StoryTable.cpp | 18 +++++++++++++--- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 4d10e257d3..853fb7d7d6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -24,6 +24,7 @@ #include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" @@ -215,6 +216,23 @@ Outcome leg_to_dialogue(VideoStream& stream, ProControllerContext& context, cons return Pokemon::outcome_failed("no_dialogue", step.name); } +// A LEGS step "to_map": runs its one leg until the location banner of +// pose_after's map shows, wherever it shows. A straight run along an open +// row needs no tile count (r41, r42: Sandgem's row 11 took us into the lab). +Outcome leg_to_map(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ + const Pokemon::StoryLeg& leg = step.legs.front(); + const std::string map = step.pose_after ? step.pose_after->map : ""; + route_turn(stream, context, leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const WalkResult walk = walk_until_dpad(stream, context, run_from_wild_battle, {x, y}, + Milliseconds(250) * leg.tiles + 2000ms, step.name, { + .arrived = [&](const ImageViewRGB32& screen){ return location_banner_matches(read_location_banner(screen), map); }, + .pace = DpadPace::RUN, + }); + stream.log(std::format("[{}] {}: ran {} to {}'s banner: {}.", checkpoint, step.name, direction_name(leg.direction), map, walk_stop_name(walk.stop))); + return walk.stop == WalkStop::ARRIVED ? Pokemon::outcome_ok() : Pokemon::outcome_failed("no_banner", map); +} + } Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ @@ -227,6 +245,9 @@ Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ if (step.variant == "to_dialogue"){ return leg_to_dialogue(stream(), context(), step, m_checkpoint); } + if (step.variant == "to_map"){ + return leg_to_map(stream(), context(), step, m_checkpoint); + } const bool one_short_ok = step.variant == "one_short_ok"; if (!one_short_ok && !step.variant.empty()){ return Pokemon::outcome_failed("unknown_variant", step.variant); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 335591903c..4f0f93de6d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -288,6 +288,16 @@ std::vector lake_and_pokedex(){ }; } +// West along Sandgem's path (row 12) until Route 201's banner shows: it +// shows a tile or a few past the border, Sandgem (-1, 12) = Route 201 (63, 5). +StoryStep route_201_by_the_path(){ + StoryStep step = legs("to_route_201", {{.direction = WEST, .tiles = 40}}, "run west along the path until Route 201's banner", "to_map"); + step.pose_after = Pokemon::tile_pose(ROUTE_201, { + TilePos{57, 5}, TilePos{58, 5}, TilePos{59, 5}, TilePos{60, 5}, TilePos{61, 5}, TilePos{62, 5}, TilePos{63, 5} + }, WEST); + return step; +} + std::vector to_the_catch(){ return { row("00.04", "Home to Mom, then Dawn's catching tutorial on Route 202", at(LAB, "rowan_talk", NORTH), { @@ -299,9 +309,11 @@ std::vector to_the_catch(){ scene("dawn_shows_us_sandgem", Pokemon::tile_pose(SANDGEM, { TilePos{26, 11}, TilePos{27, 11}, TilePos{28, 11}, TilePos{26, 12}, TilePos{27, 12}, TilePos{28, 12} }, NORTH), "Dawn's tour of Sandgem"), - // First to one known tile: the trees that end the doors' row. - route("to_the_row_end", at(SANDGEM, "front_row_east_end"), "east along the doors' row to the trees", "town"), - route("to_route_201", at(ROUTE_201, "route_201_east_end"), "west along the doors' row out of Sandgem"), + // Not west along the doors' row: running it took us into the lab at its + // mat (8, 11) (r41, r42). Row 12 is open into Route 201: run it until + // Route 201's banner shows, a few tiles in. + route("onto_the_path", at(SANDGEM, 29, 12, WEST), "to the east end of the path below the doors' row", "town"), + route_201_by_the_path(), route("home", at(TWINLEAF, "house_doorstep", NORTH), "west along Route 201 to Twinleaf, running from wild battles"), // The game sets us a tile inside, off the mat: (6, 9) (r40: the old extra // step north put us beside the TV, above Mom). diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 1919c4fdf9..00dad54088 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -36,7 +36,7 @@ using Pokemon::TilePos; const std::map>& built_in_variants(){ static const std::map> variants{ {StoryStepKind::ROUTE, {"", "town", "robust"}}, - {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "to_dialogue"}}, + {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "to_dialogue", "to_map"}}, {StoryStepKind::MASH, {"", "a"}}, {StoryStepKind::BATTLE, {"", "doubles"}}, }; From daed3845d57d242bd3e15ee9dd50b0189275bcf1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 21:47:12 -0800 Subject: [PATCH 45/98] BDSP AutoStory: door steps run until the fade; Mom at (7, 7) A door step walks (runs, after the shoes) until the screen goes black, from anywhere in the door's line: out of the lab from Rowan's spot, into the house from the end of the south run. Dawn's tour plays inside the lab exit's settle, so its scene row goes. Mom stands a row above the decomp's (7, 8) (r43, by hand). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 20 +++++++------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 26 ++++++++++++------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 853fb7d7d6..5da1ff2e87 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -336,17 +336,17 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ route_turn(stream(), context(), step.direction); - // A step through a door: one tap, then settle, which must see the fade - // (3 to 5 s) and then any scene on arrival (Mom's, 00.01). A walk's - // window could swallow the fade unseen (r33 was outside, "no fade"). + // A step through a door: walk (or run) on until the screen goes black, + // from next to the door or from anywhere in its line (r43: tapping 8 tiles + // down the lab took 7 s), then settle for any scene on arrival (Mom's, + // 00.01; Dawn's tour, 00.04). Without a black screen, settle must see the + // fade itself (r33 was outside, "no fade"). if (step.pose_after && step.pose_after->map != state.pose.map){ - const DpadPosition dpad = - step.direction == Direction::NORTH ? DPAD_UP : - step.direction == Direction::SOUTH ? DPAD_DOWN : - step.direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - pbf_press_dpad(context(), dpad, 160ms, 0ms); - stream().log(std::format("[{}] {}: a step {} through the door.", m_checkpoint, step.name, direction_name(step.direction))); - settle(stream(), context(), BUTTON_B, label_of(step), true); + const auto [x, y] = dpad_of(step.direction); + const WalkResult walk = walk_until_dpad(stream(), context(), run_from_wild_battle, {x, y}, 12000ms, step.name, + {.arrived = nullptr, .pace = m_can_run ? DpadPace::RUN : DpadPace::WALK}); + stream().log(std::format("[{}] {}: {} through the door: {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); + settle(stream(), context(), BUTTON_B, label_of(step), walk.stop != WalkStop::TRANSITION); return Pokemon::outcome_ok(); } const auto [x, y] = dpad_of(step.direction); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 4f0f93de6d..e6a654acec 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -301,26 +301,32 @@ StoryStep route_201_by_the_path(){ std::vector to_the_catch(){ return { row("00.04", "Home to Mom, then Dawn's catching tutorial on Route 202", at(LAB, "rowan_talk", NORTH), { - route("above_the_lab_door", at(LAB, 7, 14, SOUTH), "down the lab to its door (7, 15)"), - step_into("out_of_the_lab", SOUTH, "out of the lab", door_front(SANDGEM, "lab_door")), - // Automatic on arrival, so out_of_the_lab's settle plays it. She leaves us - // below the Mart's door (27, 10), a row or two down: r39's walk west - // from "row 11" ran on along row 12, which is open to Route 201. - scene("dawn_shows_us_sandgem", Pokemon::tile_pose(SANDGEM, { + // Rowan's spot is in the door's column: run south out of the door. Dawn's + // tour plays on arrival, inside the step's settle (r43: a scene row after + // it waited 20 s for lines already played). She leaves us below the Mart's + // door (27, 10), a row or two down: r39's walk west from "row 11" ran on + // along row 12, which is open to Route 201. + step_into("out_of_the_lab", SOUTH, "out of the lab; Dawn's tour of Sandgem", Pokemon::tile_pose(SANDGEM, { TilePos{26, 11}, TilePos{27, 11}, TilePos{28, 11}, TilePos{26, 12}, TilePos{27, 12}, TilePos{28, 12} - }, NORTH), "Dawn's tour of Sandgem"), + }, NORTH)), // Not west along the doors' row: running it took us into the lab at its // mat (8, 11) (r41, r42). Row 12 is open into Route 201: run it until // Route 201's banner shows, a few tiles in. route("onto_the_path", at(SANDGEM, 29, 12, WEST), "to the east end of the path below the doors' row", "town"), route_201_by_the_path(), - route("home", at(TWINLEAF, "house_doorstep", NORTH), "west along Route 201 to Twinleaf, running from wild battles"), + // Anywhere in the door's column: the step runs north into the door, so the + // route can end where the south run along x 14 stops (row 26) and go east. + route("home", Pokemon::tile_pose(TWINLEAF, { + TilePos{20, 22}, TilePos{20, 23}, TilePos{20, 24}, TilePos{20, 25}, TilePos{20, 26} + }, NORTH), "west along Route 201 to Twinleaf, running from wild battles"), // The game sets us a tile inside, off the mat: (6, 9) (r40: the old extra // step north put us beside the TV, above Mom). step_into("into_the_house", NORTH, "the house's door", at(HOME, 6, 9, NORTH)), - route("beside_mom", at(HOME, 6, 8, EAST), "west of Mom at (7, 8)"), + // Mom stands at (7, 7) now, a row above the decomp's (7, 8): from (6, 8) + // she was up and to the right; a tap north, east and A answered (r43). + route("beside_mom", at(HOME, 6, 7, EAST), "west of Mom at (7, 7)"), talk("mom", "Mom"), - scene("mom_running_shoes", at(HOME, 6, 8, EAST), "her lines"), + scene("mom_heals_us", at(HOME, 6, 7, EAST), "her lines; she heals the party"), route("back_above_the_door", at(HOME, 6, 9, SOUTH), "Barry's mother stands in the door"), event("barrys_mother", "she gives us Barry's parcel", at(HOME, 6, 9, SOUTH)), step_into("out_of_the_house_again", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), From 027b6241e8c7dbebd98cd541cbf48f018243656b Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 21:48:05 -0800 Subject: [PATCH 46/98] BDSP AutoStory: the lab exit in its own helper, under the function limit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryTable.cpp | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index e6a654acec..f253a696b1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -298,17 +298,21 @@ StoryStep route_201_by_the_path(){ return step; } +StoryStep out_of_the_lab_into_dawns_tour(){ + // Rowan's spot is in the door's column: run south out of the door. Dawn's + // tour plays on arrival, inside the step's settle (r43: a scene row after + // it waited 20 s for lines already played). She leaves us below the Mart's + // door (27, 10), a row or two down: r39's walk west from "row 11" ran on + // along row 12, which is open to Route 201. + return step_into("out_of_the_lab", SOUTH, "out of the lab; Dawn's tour of Sandgem", Pokemon::tile_pose(SANDGEM, { + TilePos{26, 11}, TilePos{27, 11}, TilePos{28, 11}, TilePos{26, 12}, TilePos{27, 12}, TilePos{28, 12} + }, NORTH)); +} + std::vector to_the_catch(){ return { row("00.04", "Home to Mom, then Dawn's catching tutorial on Route 202", at(LAB, "rowan_talk", NORTH), { - // Rowan's spot is in the door's column: run south out of the door. Dawn's - // tour plays on arrival, inside the step's settle (r43: a scene row after - // it waited 20 s for lines already played). She leaves us below the Mart's - // door (27, 10), a row or two down: r39's walk west from "row 11" ran on - // along row 12, which is open to Route 201. - step_into("out_of_the_lab", SOUTH, "out of the lab; Dawn's tour of Sandgem", Pokemon::tile_pose(SANDGEM, { - TilePos{26, 11}, TilePos{27, 11}, TilePos{28, 11}, TilePos{26, 12}, TilePos{27, 12}, TilePos{28, 12} - }, NORTH)), + out_of_the_lab_into_dawns_tour(), // Not west along the doors' row: running it took us into the lab at its // mat (8, 11) (r41, r42). Row 12 is open into Route 201: run it until // Route 201's banner shows, a few tiles in. From 1bece88414f30e0516ade047641b890f1ad0b35b Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 21:56:09 -0800 Subject: [PATCH 47/98] BDSP AutoStory: a door step watches for the fade itself The walker's black-screen watch missed the lab's fade into Sandgem (r44 stopped at Dawn's first line), and settle never saw it. Hold the D-pad under a watcher at settle's own darkness. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 5da1ff2e87..74b107fe82 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -18,7 +18,9 @@ #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" #include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" @@ -334,19 +336,40 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c } +namespace{ + +// Holds the D-pad toward a door (with B after the shoes) until the screen goes +// dark, up to 12 s. Returns whether it did. The walker's black-screen watch +// missed the lab's fade into Sandgem (r44, "dialog"), and settle's samples +// 1.3 s apart can fall either side of it, so watch at settle's own darkness. +bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction, bool run){ + const DpadPosition dpad = + direction == Direction::NORTH ? DPAD_UP : + direction == Direction::SOUTH ? DPAD_DOWN : + direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + BlackScreenWatcher dark(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 3 * FADE_DARKNESS, 60); + const int ret = run_until(stream, context, [&](ProControllerContext& context){ + if (run){ + ssf_press_button(context, BUTTON_B, 0ms, 12000ms, 0ms); + } + pbf_press_dpad(context, dpad, 12000ms, 0ms); + }, {{dark}}); + return ret == 0; +} + +} + Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ route_turn(stream(), context(), step.direction); - // A step through a door: walk (or run) on until the screen goes black, + // A step through a door: walk (or run) on until the screen goes dark, // from next to the door or from anywhere in its line (r43: tapping 8 tiles // down the lab took 7 s), then settle for any scene on arrival (Mom's, - // 00.01; Dawn's tour, 00.04). Without a black screen, settle must see the - // fade itself (r33 was outside, "no fade"). + // 00.01; Dawn's tour, 00.04). Without the fade, settle must see it itself + // (r33 was outside, "no fade"). if (step.pose_after && step.pose_after->map != state.pose.map){ - const auto [x, y] = dpad_of(step.direction); - const WalkResult walk = walk_until_dpad(stream(), context(), run_from_wild_battle, {x, y}, 12000ms, step.name, - {.arrived = nullptr, .pace = m_can_run ? DpadPace::RUN : DpadPace::WALK}); - stream().log(std::format("[{}] {}: {} through the door: {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); - settle(stream(), context(), BUTTON_B, label_of(step), walk.stop != WalkStop::TRANSITION); + const bool faded = run_into_door(stream(), context(), step.direction, m_can_run); + stream().log(std::format("[{}] {}: {} through the door: {}.", m_checkpoint, step.name, direction_name(step.direction), faded ? "faded" : "no fade yet")); + settle(stream(), context(), BUTTON_B, label_of(step), !faded); return Pokemon::outcome_ok(); } const auto [x, y] = dpad_of(step.direction); From f3580ff3e933509dd83741e0f4d5a6ab7e39d5eb Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 22:07:24 -0800 Subject: [PATCH 48/98] BDSP AutoStory: Mom's heal brings Barry's mom; her hat at the door Mom's heal scene plays Barry's mom and the parcel, and leaves Mom below us: go around her. At the door Mom calls us back with the hat and walks out with us, a tile west of the doorstep (r45). A door step stops at a line. The blind barrys_mother presses go. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp | 11 ----------- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 6 ++++-- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 12 ++++++++---- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp index a762206b69..80b271cd1e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp @@ -135,16 +135,6 @@ void rowan_gives_pokedex(VideoStream& stream, ProControllerContext& context){ context.wait_for_all_requests(); } -// 00.04: Barry's mother stands in the door with his parcel: four A, with -// the waits of Butters' recording between them. -void barrys_mother(VideoStream&, ProControllerContext& context){ - pbf_press_button(context, BUTTON_A, 180ms, 946ms); - pbf_press_button(context, BUTTON_A, 180ms, 9092ms); - pbf_press_button(context, BUTTON_A, 180ms, 740ms); - pbf_press_button(context, BUTTON_A, 180ms, 2998ms); - context.wait_for_all_requests(); -} - // 00.04: Dawn's catching tutorial on Route 202: her lines and her demo // battle, B for 70 s. void catch_tutorial(VideoStream& stream, ProControllerContext& context){ @@ -164,7 +154,6 @@ void add_lake_events(StoryEvents& events){ events.emplace("fight_starly", console_event(fight_starly)); events.emplace("after_starly", console_event(after_starly)); events.emplace("rowan_gives_pokedex", console_event(rowan_gives_pokedex)); - events.emplace("barrys_mother", console_event(barrys_mother)); events.emplace("catch_tutorial", console_event(catch_tutorial)); events.emplace("catch_pokemon", console_event(catch_1_pokemon)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 74b107fe82..699b955dec 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -339,7 +339,8 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c namespace{ // Holds the D-pad toward a door (with B after the shoes) until the screen goes -// dark, up to 12 s. Returns whether it did. The walker's black-screen watch +// dark or a line shows, up to 12 s. Returns whether it went dark. A line +// stops it: holding on under Mom's "Oh, Kosmos!" wasted 12 s (r45). The walker's black-screen watch // missed the lab's fade into Sandgem (r44, "dialog"), and settle's samples // 1.3 s apart can fall either side of it, so watch at settle's own darkness. bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction, bool run){ @@ -348,12 +349,13 @@ bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction == Direction::SOUTH ? DPAD_DOWN : direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; BlackScreenWatcher dark(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 3 * FADE_DARKNESS, 60); + ShortDialogWatcher dialog(COLOR_YELLOW); const int ret = run_until(stream, context, [&](ProControllerContext& context){ if (run){ ssf_press_button(context, BUTTON_B, 0ms, 12000ms, 0ms); } pbf_press_dpad(context, dpad, 12000ms, 0ms); - }, {{dark}}); + }, {{dark}, {dialog}}); return ret == 0; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index f253a696b1..43fc323164 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -330,10 +330,14 @@ std::vector to_the_catch(){ // she was up and to the right; a tap north, east and A answered (r43). route("beside_mom", at(HOME, 6, 7, EAST), "west of Mom at (7, 7)"), talk("mom", "Mom"), - scene("mom_heals_us", at(HOME, 6, 7, EAST), "her lines; she heals the party"), - route("back_above_the_door", at(HOME, 6, 9, SOUTH), "Barry's mother stands in the door"), - event("barrys_mother", "she gives us Barry's parcel", at(HOME, 6, 9, SOUTH)), - step_into("out_of_the_house_again", SOUTH, "out of the door", at(TWINLEAF, "house_doorstep", SOUTH)), + // Mom heals us, then Barry's mom comes in with his parcel; the scene + // leaves Mom at (6, 8), below us (r45). + scene("mom_heals_us", at(HOME, 6, 7, EAST), "Mom heals us; Barry's mom gives us his parcel"), + legs("around_mom", {{.direction = WEST, .tiles = 1}, {.direction = SOUTH, .tiles = 2}, {.direction = EAST, .tiles = 1}}, + "around Mom at (6, 8) to above the door"), + // At the door Mom calls us back ("Oh, Kosmos!"), gives us the hat and walks + // out with us; we end a tile west of the doorstep, beside her (r45). + step_into("out_of_the_house_again", SOUTH, "out of the door; Mom's hat", at(TWINLEAF, 19, 22, EAST)), route("short_of_the_catch_tutorial", at(ROUTE_202, 22, 27, WEST), "through Sandgem onto Route 202, east of its trigger column"), step_into("catch_tutorial_trigger", WEST, "the trigger (21, 25..29), VAR_ROUTE_202_STATE 0"), event("catch_tutorial", "Dawn's catching tutorial"), From 20ada648b1f0e39f36eaa357e7f411b2d501977f Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 22:16:28 -0800 Subject: [PATCH 49/98] BDSP AutoStory: Mom calls us back as we step above the door Stepping onto (6, 9) starts Mom's "Oh, Kosmos!" (r46), so walk there until her line, then her hat scene walks us out. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryTable.cpp | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 43fc323164..c0b5d2b338 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -309,6 +309,14 @@ StoryStep out_of_the_lab_into_dawns_tour(){ }, NORTH)); } +// Anywhere in the door's column: the step runs north into the door, so the +// route can end where the south run along x 14 stops (row 26) and go east. +StoryStep home_to_the_doors_column(){ + return route("home", Pokemon::tile_pose(TWINLEAF, { + TilePos{20, 22}, TilePos{20, 23}, TilePos{20, 24}, TilePos{20, 25}, TilePos{20, 26} + }, NORTH), "west along Route 201 to Twinleaf, running from wild battles"); +} + std::vector to_the_catch(){ return { row("00.04", "Home to Mom, then Dawn's catching tutorial on Route 202", at(LAB, "rowan_talk", NORTH), { @@ -318,11 +326,7 @@ std::vector to_the_catch(){ // Route 201's banner shows, a few tiles in. route("onto_the_path", at(SANDGEM, 29, 12, WEST), "to the east end of the path below the doors' row", "town"), route_201_by_the_path(), - // Anywhere in the door's column: the step runs north into the door, so the - // route can end where the south run along x 14 stops (row 26) and go east. - route("home", Pokemon::tile_pose(TWINLEAF, { - TilePos{20, 22}, TilePos{20, 23}, TilePos{20, 24}, TilePos{20, 25}, TilePos{20, 26} - }, NORTH), "west along Route 201 to Twinleaf, running from wild battles"), + home_to_the_doors_column(), // The game sets us a tile inside, off the mat: (6, 9) (r40: the old extra // step north put us beside the TV, above Mom). step_into("into_the_house", NORTH, "the house's door", at(HOME, 6, 9, NORTH)), @@ -333,11 +337,13 @@ std::vector to_the_catch(){ // Mom heals us, then Barry's mom comes in with his parcel; the scene // leaves Mom at (6, 8), below us (r45). scene("mom_heals_us", at(HOME, 6, 7, EAST), "Mom heals us; Barry's mom gives us his parcel"), - legs("around_mom", {{.direction = WEST, .tiles = 1}, {.direction = SOUTH, .tiles = 2}, {.direction = EAST, .tiles = 1}}, - "around Mom at (6, 8) to above the door"), - // At the door Mom calls us back ("Oh, Kosmos!"), gives us the hat and walks - // out with us; we end a tile west of the doorstep, beside her (r45). - step_into("out_of_the_house_again", SOUTH, "out of the door; Mom's hat", at(TWINLEAF, 19, 22, EAST)), + legs("around_mom", {{.direction = WEST, .tiles = 1}, {.direction = SOUTH, .tiles = 2}}, + "around Mom at (6, 8), to beside the door's column"), + // Stepping onto (6, 9), above the door, Mom calls us back ("Oh, Kosmos!", + // r46), gives us the hat and walks out with us; we end a tile west of the + // doorstep, beside her (r45). + legs("to_moms_call", {{.direction = EAST, .tiles = 1}}, "east above the door until Mom calls", "to_dialogue"), + scene("moms_hat", at(TWINLEAF, 19, 22, EAST), "Mom's hat; she walks out with us"), route("short_of_the_catch_tutorial", at(ROUTE_202, 22, 27, WEST), "through Sandgem onto Route 202, east of its trigger column"), step_into("catch_tutorial_trigger", WEST, "the trigger (21, 25..29), VAR_ROUTE_202_STATE 0"), event("catch_tutorial", "Dawn's catching tutorial"), From 8fccf5cc246e977656e22ed94fd292d4cb664f49 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 22:34:52 -0800 Subject: [PATCH 50/98] BDSP AutoStory: heal by running north into the Center from the planned tile The lit-doorway lineup read 0.7 tile off below Sandgem's door and stepped us off it (r48). Run in like any door; the lineup stays the fallback. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 699b955dec..e18f5386e7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -439,7 +439,17 @@ Outcome StoryExecutor::train(RunState&, const StoryStep& step){ } Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ - heal_at_pokemon_center(stream(), context(), label_of(step)); + // From the planned tile below the door, run in like any door. The lit- + // doorway lineup is for a guess of where we stand: below Sandgem's door + // it read 0.7 tile off and stepped us off it (r48). + route_turn(stream(), context(), Direction::NORTH); + if (run_into_door(stream(), context(), Direction::NORTH, m_can_run)){ + stream().log(std::format("[{}] {}: north into the Center: faded.", m_checkpoint, step.name)); + pbf_wait(context(), 1500ms); + heal_inside_pokemon_center(stream(), context(), label_of(step)); + }else{ + heal_at_pokemon_center(stream(), context(), label_of(step)); + } // r143a's heal said it had walked out while still inside. context().wait_for_all_requests(); VideoSnapshot screen = stream().video().snapshot(); From cb4d9a659dea95b623cc7f80feaef6831df01c18 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:01:06 -0800 Subject: [PATCH 51/98] BDSP AutoStory: a table test that each row starts where the last one ends Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 00dad54088..8e8bd57129 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -4,6 +4,7 @@ * */ +#include #include #include #include @@ -12,6 +13,7 @@ #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/AutoStory/Pokemon_StoryEvents.h" #include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "Data/PokemonBDSP_AutoStory_Maps.h" #include "Data/PokemonBDSP_AutoStory_PlanCosts.h" @@ -81,6 +83,46 @@ class Test_StoryTableLints : public UnitTest{ } }; +// Where a row's last step says it leaves the player: the end of its scene, +// or where its route (or heal) goes. None for a step that does not say. +std::optional end_pose(const StoryCheckpoint& row){ + if (row.steps.empty()){ + return std::nullopt; + } + const Pokemon::StoryStep& last = row.steps.back(); + return last.pose_after ? last.pose_after : last.target; +} + +// Each row starts where the row before it ends: every tile its start allows +// is one the last row's end allows, on the same map. Checked where the last +// step says where it ends. +class Test_StoryTableRowsJoin : public UnitTest{ +public: + Test_StoryTableRowsJoin() + : UnitTest("PokemonBDSP::AutoStory - Story table: each row starts where the last one ends") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector& rows = story_checkpoints(); + for (size_t i = 1; i < rows.size(); i++){ + const std::optional end = end_pose(rows[i - 1]); + const std::optional& start = rows[i].start; + if (!end || !start){ + continue; + } + const std::vector ends = Pokemon::tiles_of(*end); + bool joins = start->map == end->map; + for (const TilePos& pos : Pokemon::tiles_of(*start)){ + joins = joins && std::find(ends.begin(), ends.end(), pos) != ends.end(); + } + if (!joins){ + return std::format("{} starts at {}, but {} ends at {}.", + rows[i].id, Pokemon::to_string(*start), rows[i - 1].id, Pokemon::to_string(*end)); + } + } + return true; + } +}; + struct LadyLeg{ Direction direction; size_t tiles; // 0: walk until the wall @@ -202,6 +244,7 @@ class Test_Route204NorthLine : public UnitTest{ void add_tests_AutoStory_StoryTable(UnitTestDatabase& database){ database.add(); + database.add(); database.add(); database.add(); } From 635d7eef8a9c75f28693dab4ff60f98d8725d42b Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:02:04 -0800 Subject: [PATCH 52/98] BDSP AutoStory: out of the Center the way we went in, south until the fade The walk out's indoor check read "not a room" a step from the counter, so 00.04b ended inside (r50) and 00.04c walked back up to the nurse (r51). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index e18f5386e7..9f4321bd31 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -446,7 +446,14 @@ Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ if (run_into_door(stream(), context(), Direction::NORTH, m_can_run)){ stream().log(std::format("[{}] {}: north into the Center: faded.", m_checkpoint, step.name)); pbf_wait(context(), 1500ms); - heal_inside_pokemon_center(stream(), context(), label_of(step)); + talk_to_nurse(stream(), context(), label_of(step)); + // Out the same way: south until the fade. The walk out's indoor check + // read "not a room" a step from the counter, and 00.04b ended inside + // (r50, so r51's first steps went back up to the nurse). + route_turn(stream(), context(), Direction::SOUTH); + const bool out = run_into_door(stream(), context(), Direction::SOUTH, m_can_run); + stream().log(std::format("[{}] {}: south out of the Center: {}.", m_checkpoint, step.name, out ? "faded" : "no fade yet")); + settle(stream(), context(), BUTTON_B, label_of(step), !out); }else{ heal_at_pokemon_center(stream(), context(), label_of(step)); } From 923aabe6a1bd20167bd42f257ea1d6772e1f98ee Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:39:24 -0800 Subject: [PATCH 53/98] BDSP AutoStory: no save right after loading the first checkpoint's save The console's save is the run's first checkpoint start; saving it again cost about 10 s a run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index c859415790..5d99f0e046 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -134,6 +134,20 @@ BDSPAutoStory::BDSPAutoStory() PA_ADD_OPTION(NOTIFICATIONS); } +namespace{ + +// The checkpoints whose start is already saved: the first one, unless the +// run starts a new game. The console's save is the run's start, so saving +// again right after loading it only cost ~10 s. +std::set saved_before_the_run(const std::vector& checkpoints, size_t first, bool new_game){ + if (first == 0 && new_game){ + return {}; + } + return {checkpoints[first].id}; +} + +} + void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ const std::vector& checkpoints = story_checkpoints(); const size_t first = RUN.start_index(); @@ -149,7 +163,7 @@ void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerCo executor.set_checks(RUN.checks()); executor.set_language(RUN.language()); executor.set_stop_rows(rows_that_stop(checkpoints)); - std::set saved; + std::set saved = saved_before_the_run(checkpoints, first, NEW_GAME_INTRO); Pokemon::Segment segment = make_story_segment(checkpoints, executor, saved); Pokemon::SegmentHooks hooks = story_hooks(env, context); From aff0305c5459e0f35b28270f9449c12ce445ccd8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:44:34 -0800 Subject: [PATCH 54/98] BDSP AutoStory: skip the start save only when the game was just loaded A run that follows play must save its first checkpoint's start; a JUST_LOADED option says the game was just loaded from that save. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory.cpp | 19 +++++++++++++------ .../AutoStory/PokemonBDSP_AutoStory.h | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index 5d99f0e046..c0cd6c3a82 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -110,6 +110,12 @@ BDSPAutoStory::BDSPAutoStory() "until the player is in control in the bedroom. Only when starting at 00.00.", LockMode::LOCK_WHILE_RUNNING, false ) + , JUST_LOADED( + "Just loaded the start's save:
" + "The game was just loaded from the start checkpoint's save, so its save at the start is skipped (about 10 s). " + "Leave off when the game has moved since its last save.", + LockMode::LOCK_WHILE_RUNNING, false + ) , PLAYER_NAME(false, "Player name:
For a new game. Letters and digits.", LockMode::LOCK_WHILE_RUNNING, "Kosmos", "Kosmos") , STARTER( "Starter Pokemon:", @@ -129,6 +135,7 @@ BDSPAutoStory::BDSPAutoStory() { PA_ADD_OPTION(RUN); PA_ADD_OPTION(NEW_GAME_INTRO); + PA_ADD_OPTION(JUST_LOADED); PA_ADD_OPTION(PLAYER_NAME); PA_ADD_OPTION(STARTER); PA_ADD_OPTION(NOTIFICATIONS); @@ -136,11 +143,11 @@ BDSPAutoStory::BDSPAutoStory() namespace{ -// The checkpoints whose start is already saved: the first one, unless the -// run starts a new game. The console's save is the run's start, so saving -// again right after loading it only cost ~10 s. -std::set saved_before_the_run(const std::vector& checkpoints, size_t first, bool new_game){ - if (first == 0 && new_game){ +// The checkpoints whose start is already saved: the first one when the game +// was just loaded from its save (saving again cost ~10 s). After play, the +// save is older and the first checkpoint must save its start. +std::set saved_before_the_run(const std::vector& checkpoints, size_t first, bool just_loaded){ + if (!just_loaded){ return {}; } return {checkpoints[first].id}; @@ -163,7 +170,7 @@ void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerCo executor.set_checks(RUN.checks()); executor.set_language(RUN.language()); executor.set_stop_rows(rows_that_stop(checkpoints)); - std::set saved = saved_before_the_run(checkpoints, first, NEW_GAME_INTRO); + std::set saved = saved_before_the_run(checkpoints, first, JUST_LOADED && !(first == 0 && NEW_GAME_INTRO)); Pokemon::Segment segment = make_story_segment(checkpoints, executor, saved); Pokemon::SegmentHooks hooks = story_hooks(env, context); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h index 118589ceed..36fc8ea581 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h @@ -45,6 +45,7 @@ class BDSPAutoStory : public SingleSwitchProgramInstance{ // The start and end checkpoints, attempts, checks and language. Pokemon::StoryRunOptions RUN; BooleanCheckBoxOption NEW_GAME_INTRO; + BooleanCheckBoxOption JUST_LOADED; StringOption PLAYER_NAME; EnumDropdownOption STARTER; From 73dde7e4faed39be4d652b69ba8c6f08703e049d Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:47:00 -0800 Subject: [PATCH 55/98] BDSP AutoStory: twice the time for a walk until stopped Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp index ac59adaad6..900e7aec84 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp @@ -43,9 +43,12 @@ std::pair dpad_of(Direction direction){ void walk_until_stopped(VideoStream& stream, ProControllerContext& context, Direction direction, size_t tiles, const std::string& label){ const bool across = direction == Direction::EAST || direction == Direction::WEST; const auto [x, y] = dpad_of(direction); - // Time for every tile, and for the wall to read as still. + // Twice every tile's time, and time for the wall to read as still: it + // stops at the wall anyway. One tile's time each fell short once the + // turn stopped stepping us ahead (r57: west to Jubilife's sign ended + // on the time, a column short). const Milliseconds limit = ROUTE_START_DELAY + 800ms + - (int64_t)tiles * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN); + (int64_t)tiles * 2 * (across ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN); // Turn first: the first press of a new way only turns us, the screen // holds still meanwhile, and that read as a wall two tiles into the // walk west to the Lab's sign (r132a). From 8a5dfc768ce139310ca0f17c1f32af36b6015c4d Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:50:48 -0800 Subject: [PATCH 56/98] BDSP AutoStory: plan the walk to Jubilife's Center on the map The hand-measured walls leaned on the old turn's nudge and ended beside the Trainers' School (r57, r58). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index c0b5d2b338..c0f508be59 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -395,10 +395,11 @@ std::vector jubilife(){ return { row("00.05", "Jubilife: the Center, the Trainers' School and the Town Map", std::nullopt, { event("up_the_steps", "a tile west of the save below the steps, then up them", on(JUBILIFE, "up_the_steps")), - to_walls("to_the_sign", {{.direction = WEST, .tiles = 8}, {.direction = NORTH, .tiles = 8}}, - "west to the fence's end, north to the sign by the lamp west of the road"), - to_walls("to_the_center", {{.direction = EAST, .tiles = 14}, {.direction = NORTH, .tiles = 14}}, - "east across the road to the house, north up the sidewalk to the Center's doorway"), + // Planned on the map: the hand-measured walls (west to the sign, east + // across the road, north) leaned on the old turn's nudge and ended + // beside the Trainers' School once turns stopped stepping (r57, r58). + route("to_the_center", at(JUBILIFE, 52, 41, NORTH), + "below the Center's door (52, 40)", "town"), // The kids south of the Center can push us aside: the heal lines up by sight. heal("heal_jubilife", "in, the nurse, out onto the doorstep (r143a: check we are out)"), event("into_the_school", "eleven tiles west of the Center's doorstep, north through the School's door"), From 615d5e08cb105729f228996081102d88cb96488d Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:57:00 -0800 Subject: [PATCH 57/98] BDSP AutoStory: one way out of the Center, whichever way we got in The lit-doorway fallback walked out with the old indoor check, which stopped inside Jubilife's Center (r59). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 9f4321bd31..cbf4fb0d42 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -440,23 +440,24 @@ Outcome StoryExecutor::train(RunState&, const StoryStep& step){ Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ // From the planned tile below the door, run in like any door. The lit- - // doorway lineup is for a guess of where we stand: below Sandgem's door - // it read 0.7 tile off and stepped us off it (r48). + // doorway lineup is for when the run in finds no door: people on the + // way can leave the route unsure of the column (r59, Jubilife). Below + // Sandgem's door the lineup read 0.7 tile off and stepped us off it (r48). route_turn(stream(), context(), Direction::NORTH); - if (run_into_door(stream(), context(), Direction::NORTH, m_can_run)){ - stream().log(std::format("[{}] {}: north into the Center: faded.", m_checkpoint, step.name)); + const bool in = run_into_door(stream(), context(), Direction::NORTH, m_can_run); + stream().log(std::format("[{}] {}: north into the Center: {}.", m_checkpoint, step.name, in ? "faded" : "no door; lining up by sight")); + if (in){ pbf_wait(context(), 1500ms); - talk_to_nurse(stream(), context(), label_of(step)); - // Out the same way: south until the fade. The walk out's indoor check - // read "not a room" a step from the counter, and 00.04b ended inside - // (r50, so r51's first steps went back up to the nurse). - route_turn(stream(), context(), Direction::SOUTH); - const bool out = run_into_door(stream(), context(), Direction::SOUTH, m_can_run); - stream().log(std::format("[{}] {}: south out of the Center: {}.", m_checkpoint, step.name, out ? "faded" : "no fade yet")); - settle(stream(), context(), BUTTON_B, label_of(step), !out); }else{ - heal_at_pokemon_center(stream(), context(), label_of(step)); - } + enter_lit_door(stream(), context(), label_of(step)); + } + talk_to_nurse(stream(), context(), label_of(step)); + // Out the same way: south until the fade. The walk out's indoor check + // read "not a room" a step from the counter and stopped inside (r50, r59). + route_turn(stream(), context(), Direction::SOUTH); + const bool out = run_into_door(stream(), context(), Direction::SOUTH, m_can_run); + stream().log(std::format("[{}] {}: south out of the Center: {}.", m_checkpoint, step.name, out ? "faded" : "no fade yet")); + settle(stream(), context(), BUTTON_B, label_of(step), !out); // r143a's heal said it had walked out while still inside. context().wait_for_all_requests(); VideoSnapshot screen = stream().video().snapshot(); From 8052f9d13512b35a7ba611f882bccd80ae2ccea5 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:10:58 -0800 Subject: [PATCH 58/98] BDSP AutoStory: up Jubilife's steps on the D-pad Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index b2eb52a1da..7bd035a125 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -37,13 +37,19 @@ namespace{ // 00.05: from the save below Jubilife's steps, against the wall east of // them: a tile west (the first press only turns), then up the steps. void up_the_steps(VideoStream& stream, ProControllerContext& context){ + // D-pad taps, a tile each: the stick holds this was (200 ms west, 480 ms + // north) left us between tiles, and every leg after it a column off + // (r57 to r61). By hand: a tap west and taps north from the save land + // on the map's tiles (between the lamps at x 43 and 49 after six). route_turn(stream, context, Direction::WEST); - walk_for(stream, context, run_from_wild_battle, {-1, 0}, 200ms, "To the steps"); - const WalkResult up = walk_for(stream, context, run_from_wild_battle, {0, 1}, 480ms, "Up the steps"); + walk_tiles_dpad(stream, context, run_from_wild_battle, {-1, 0}, 1, "To the steps"); + route_turn(stream, context, Direction::NORTH); + const WalkResult up = walk_tiles_dpad(stream, context, run_from_wild_battle, {0, 1}, 2, "Up the steps"); if (up.stop == WalkStop::DIALOG || up.stop == WalkStop::TRANSITION){ fail_step(stream, std::string("Up the steps: ") + walk_stop_name(up.stop) + "."); } } +} // The School's lamp, east of its door, from its doorsteps' row at night // (2026-10-30): at 0.65 to 0.69 of the screen across the door takes us in; From c068144ff9c0fe9039040fc7c5a27ff54fa60da4 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:12:14 -0800 Subject: [PATCH 59/98] BDSP AutoStory: drop a stray brace after up_the_steps Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index 7bd035a125..a70b7660ed 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -49,7 +49,6 @@ void up_the_steps(VideoStream& stream, ProControllerContext& context){ fail_step(stream, std::string("Up the steps: ") + walk_stop_name(up.stop) + "."); } } -} // The School's lamp, east of its door, from its doorsteps' row at night // (2026-10-30): at 0.65 to 0.69 of the screen across the door takes us in; From 45224056e6989e6aaef521df52b63ebafaaa9898 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:18:45 -0800 Subject: [PATCH 60/98] BDSP AutoStory: past the nurse's last line before running out Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index cbf4fb0d42..d3cdd08b85 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -452,6 +452,17 @@ Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ enter_lit_door(stream(), context(), label_of(step)); } talk_to_nurse(stream(), context(), label_of(step)); + // Her last line ("We hope to see you again!") can still be up, and a + // line stops the run out at once (r62): B until none shows. + ShortDialogDetector line; + for (size_t presses = 0; presses < 10; presses++){ + context().wait_for_all_requests(); + const VideoSnapshot screen = stream().video().snapshot(); + if (!screen || !line.detect(*screen.frame)){ + break; + } + pbf_press_button(context(), BUTTON_B, 80ms, 700ms); + } // Out the same way: south until the fade. The walk out's indoor check // read "not a room" a step from the counter and stopped inside (r50, r59). route_turn(stream(), context(), Direction::SOUTH); From a38836ee294aed0f3d77730db1ed808200e6f4b5 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:25:16 -0800 Subject: [PATCH 61/98] BDSP AutoStory: plan the walk to the Trainers' School's door "Eleven tiles west" was measured when a turn also stepped; the map puts the door twelve west of the Center's. Route to the tile below it and run in, as for the Center, and drop the lamp/nudge line-up. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 60 ------------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 6 +- 2 files changed, 5 insertions(+), 61 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index a70b7660ed..2ba1c105f5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -50,65 +50,6 @@ void up_the_steps(VideoStream& stream, ProControllerContext& context){ } } -// The School's lamp, east of its door, from its doorsteps' row at night -// (2026-10-30): at 0.65 to 0.69 of the screen across the door takes us in; -// at 0.61 (right of it) and 0.75 (past it) its front stops us. A tile is -// 0.065 of the screen. The box leaves out the Center's lamp further east. -const double SCHOOL_LAMP_X_AT_DOOR = 0.67; -const ImageFloatBox SCHOOL_LAMP_BOX(0.55, 0.20, 0.30, 0.10); -const size_t SCHOOL_TILES_WEST_OF_CENTER = 11; - -// At night the lamp east of the School's door says how many tiles across -// the door is; nothing by day. -std::optional school_door_across(VideoStream& stream, ProControllerContext& context){ - context.wait_for_all_requests(); - VideoSnapshot screen = stream.video().snapshot(); - if (!screen){ - return std::nullopt; - } - const std::optional lamp = find_lit_lamp_x(*screen.frame, SCHOOL_LAMP_BOX); - if (!lamp){ - stream.log("[Route] School's door: no lit lamp on screen."); - return std::nullopt; - } - const double across = tiles_across_at(*lamp) - tiles_across_at(SCHOOL_LAMP_X_AT_DOOR); - stream.log("[Route] School's door: the lamp at " + std::to_string(*lamp) + " of the screen, the door " + - std::to_string(across) + " tiles across."); - return across; -} - -// 00.05: from the Center's doorstep into the Trainers' School. Measured on -// the console (2026-10-30): a tap to face west and eleven one-tile holds put -// us under the School's doorway; twelve is past it. A hold ends a part of a -// tile off (back east, eleven stopped beside the Center's door), so if the -// School's front stops us, nudge half a tile each way. -void into_the_school(VideoStream& stream, ProControllerContext& context){ - route_turn(stream, context, Direction::WEST); - for (size_t tiles = 0; tiles < SCHOOL_TILES_WEST_OF_CENTER; tiles++){ - hold_for(stream, context, Direction::WEST, 160ms, "West to the School's door"); - } - for (size_t looks = 0; looks < 3; looks++){ - const std::optional across = school_door_across(stream, context); - // The door took us from -0.19 and 0.19 but not -0.34 (r148b). - if (!across || std::abs(*across) <= 0.3){ - break; - } - stream.log("[Route] Line up with the School's door by its lamp: " + std::to_string(*across) + " tiles across."); - move_across(stream, context, *across); - context.wait_for_all_requests(); - } - if (door_north_by_scene(stream, context, "Trainers' School door")){ - return; - } - for (Direction side : {Direction::EAST, Direction::WEST, Direction::WEST}){ - nudge(stream, context, side, "Line up with the School's door"); - if (door_north_by_scene(stream, context, "Trainers' School door")){ - return; - } - } - fail_step(stream, "The School's door did not take us from 11 tiles west of the Center's doorstep, or half a tile either side."); -} - // 00.05: Barry stands at the blackboard a tile west of the door's column, // and that column is clear to the back wall: north to it, face west, his // scene to "You obtained a Town Map!" (A closes it). A tile west by a hold @@ -433,7 +374,6 @@ void north_to_rowan(VideoStream& stream, ProControllerContext& context){ void add_jubilife_events(StoryEvents& events){ events.emplace("up_the_steps", console_event(up_the_steps)); - events.emplace("into_the_school", console_event(into_the_school)); events.emplace("barry_town_map", console_event(barry_town_map)); events.emplace("meet_the_president", console_event(meet_the_president)); events.emplace("up_to_the_mart_road", console_event(up_to_the_mart_road)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index c0f508be59..6cb3159866 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -402,7 +402,11 @@ std::vector jubilife(){ "below the Center's door (52, 40)", "town"), // The kids south of the Center can push us aside: the heal lines up by sight. heal("heal_jubilife", "in, the nurse, out onto the doorstep (r143a: check we are out)"), - event("into_the_school", "eleven tiles west of the Center's doorstep, north through the School's door"), + // Same rule as the Center: plan to the tile below the door, then run in + // (r63: "eleven tiles west" counted on the old turn's step; the map says twelve). + route("to_the_school", next_to(JUBILIFE, "trainers_school_door", SOUTH, NORTH), + "below the School's door (40, 40)", "town"), + step_into("into_the_school", NORTH, "the School's door"), to_walls("up_to_barry", {{.direction = NORTH, .tiles = 10}}, "the door's column to the back wall, beside Barry"), event("barry_town_map", "Barry gives the Town Map"), event("out_of_the_school", "straight down the door's column and out"), From f03cb22a96018ab94f6615ad3f594da6e0a19a66 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:43:25 -0800 Subject: [PATCH 62/98] BDSP AutoStory: plan Jubilife's president, clowns and Poketch on the map 00.05b and 00.05c counted tiles after turns that used to step; now each beat routes to the tile below its person from the Diamond decomp's zone events and scripts: the president's trigger (44..48, 40), the clowns by the Mart (55, 33), the TV station (36, 16) and the Poketch Company (15, 18), and the president back at (46, 35). The TV station clown, done by hand until now, is in the row. A talk to a wandering clown asks until he comes by. The School's door steps both ways, so 00.05b starts below it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 206 ++---------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 140 ++++++++---- 2 files changed, 118 insertions(+), 228 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index 2ba1c105f5..384ca9879b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -2,13 +2,14 @@ * * From: https://github.com/PokemonAutomation/ * - * The Jubilife beats of 00.05 (the Town Map, the president, the clowns' coupons, - * the Poketch), stepped through on the console on 2026-10-30, and - * segment 01's walks out of the Center to Barry and up to Rowan's scene. + * The Jubilife beats of 00.05 the story table cannot plan (up the steps, + * Barry's Town Map, the talk to a wandering clown), and segment 01's walks + * out of the Center to Barry and up to Rowan's scene. * */ #include +#include #include #include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" @@ -65,180 +66,27 @@ void barry_town_map(VideoStream& stream, ProControllerContext& context){ fail_step(stream, "Trainers' School: Barry did not answer from beside the back wall."); } -// 00.05: four tiles east is the main road; a couple of tiles north the -// Poketch Company's president sees us, walks up and talks (2026-10-30). -// He sends us for the clowns' coupons; B moves his lines on. -void meet_the_president(VideoStream& stream, ProControllerContext& context){ - route_turn(stream, context, Direction::EAST); - for (size_t tiles = 0; tiles < 4; tiles++){ - hold_for(stream, context, Direction::EAST, 160ms, "East into the road"); +// 00.05b: a talk to someone who wanders through the tile we face (the +// clowns by the Mart and the Poketch Company: WANDER_AROUND a tile either +// side of home, so through it every other move). Faces the pose's way, +// then A until a line shows, for up to a minute; the scene after it +// answers and closes the talk. +Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep& step){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + if (const std::optional facing = Pokemon::facing_of(state.pose)){ + route_turn(stream, context, *facing); } - walk_until(stream, context, run_from_wild_battle, {0, 1}, 6 * ROUTE_MS_PER_TILE_UP_DOWN + 800ms, "The Poketch Company president"); - // He walks up after the "!" and only then talks. - ShortDialogWatcher opened(COLOR_CYAN); - if (wait_until(stream, context, 5000ms, {{opened}}) != 0){ - fail_step(stream, "Jubilife: the Poketch Company president did not talk to us."); - } - finish_dialogue(stream, context, BUTTON_B, "The Poketch Company president"); -} - -// 00.05: a tile west of the lamp by the Center, then a second north onto -// the road between the Center and the Mart. -void up_to_the_mart_road(VideoStream& stream, ProControllerContext& context){ - route_turn(stream, context, Direction::WEST); - hold_for(stream, context, Direction::WEST, 160ms, "West of the lamp"); - hold_for(stream, context, Direction::NORTH, 1000ms, "North to the Mart's road"); -} - -// 00.05: the clown at the east end of the road between the Center and the -// Mart. He may be what stopped the walk east (r155c, 2.8 s against 2.2): -// ask ahead first. Else he stands a row south and a tile west: try that -// row, then one more, facing west each time. His question's default -// answer, Yes, is right; A takes his coupon. -void clown_by_the_mart(VideoStream& stream, ProControllerContext& context){ - DialogueRead read = talk_facing(stream, context, Direction::EAST, BUTTON_A, "Clown by the Poke Mart"); - for (size_t rows = 0; rows < 2 && !read.opened; rows++){ - hold_for(stream, context, Direction::SOUTH, 200ms, "A row south"); - route_turn(stream, context, Direction::WEST); - read = talk_facing(stream, context, Direction::WEST, BUTTON_A, "Clown by the Poke Mart"); - } - if (!read.opened || !read.closed){ - fail_step(stream, "Jubilife: the clown by the Poke Mart did not talk."); - } -} - -// The Poketch Company's sign reads "...World..." or "...Company...". -bool is_company_sign(const DialogueRead& read){ - return read.text.find("World") != std::string::npos || read.text.find("Company") != std::string::npos; -} - -// Him north of us, or the sign. -bool ask_clown(VideoStream& stream, ProControllerContext& context, Direction direction){ - const DialogueRead read = talk_facing(stream, context, direction, BUTTON_A, "Clown by the Poketch Company"); - return read.opened && !is_company_sign(read); -} - -// 00.05: the clown wanders the plaza behind the hedges, and a walk can pass -// him by. Measured on the console (2026-10-30): from the lamp, the Poketch -// Company's sign is six tiles east along the hedges; it reads facing north, -// and the tile east of it is a gap into the plaza. A step into the gap, then -// A every second or so, talked to him within seconds: he comes by. So tap -// four tiles, then a tile at a time asking north (him, or the sign), and -// wait in the gap. A turn tap the way we already face is a step (r161c), so -// we face east between two asks north. -void clown_by_the_company(VideoStream& stream, ProControllerContext& context){ - route_turn(stream, context, Direction::EAST); - tap_tiles(stream, context, Direction::EAST, 4, "East along the hedges"); - bool talked = false; - bool in_gap = false; - for (size_t tiles = 0; tiles < 5 && !in_gap && !talked; tiles++){ - talked = ask_clown(stream, context, Direction::NORTH); - if (talked){ - break; - } - route_turn(stream, context, Direction::EAST); - const DialogueRead sign = read_sign(stream, context, Direction::NORTH, "the Poketch Company's sign"); - if (sign.opened && !is_company_sign(sign)){ - // Him after all (or text OCR missed); his talk was closed with B: ask again. - route_turn(stream, context, Direction::EAST); - talked = ask_clown(stream, context, Direction::NORTH); - if (talked){ - break; - } - } - route_turn(stream, context, Direction::EAST); - tap_tiles(stream, context, Direction::EAST, 1, "On along the hedges"); - if (sign.opened){ - // East of the sign: north is the gap. - hold_for(stream, context, Direction::NORTH, 200ms, "Into the gap beside the sign"); - in_gap = true; - } - } - // Up to about a minute and a half; west of the gap is the sign. East - // first: the step into the gap left us facing north. - for (size_t round = 0; round < 18 && in_gap && !talked; round++){ - for (Direction direction : {Direction::EAST, Direction::NORTH, Direction::WEST}){ - talked = ask_clown(stream, context, direction); - if (talked){ - break; - } - } - } - if (!talked){ - fail_step(stream, "Jubilife: the clown by the Poketch Company did not talk."); - } -} - -// 00.05: from the gap beside the Poketch Company's sign, a row south, then -// east along the long road until the main road. A hold, not a walk until -// still: when the green-haired man who wanders the crossing is what stops -// us, his steps keep the screen moving and the stop is never seen (r167a). -// The stop took 5.9 s; holding on against it is harmless, and the Mart's -// wall puts the column right afterwards. -void to_the_main_road(VideoStream& stream, ProControllerContext& context){ - hold_for(stream, context, Direction::SOUTH, 200ms, "Out of the gap"); - hold_for(stream, context, Direction::EAST, 7500ms, "East along the long road"); -} - -// 00.05: six rows down the main road, east stops against the Poke Mart's -// west wall: the column between the Mart's lamp and the Center's, so south -// stops above the Center's lamp (2026-10-30). -void to_the_center_lamp(VideoStream& stream, ProControllerContext& context){ - for (size_t row = 0; row < 6; row++){ - hold_for(stream, context, Direction::SOUTH, 200ms, "Down the main road"); - } - walk_until_stopped(stream, context, Direction::EAST, 8, "East to the Mart's wall"); - walk_until_stopped(stream, context, Direction::SOUTH, 8, "South to the Center's lamp"); -} - -// 00.05: the president stands still a row or two north of the lamp's row, -// a few tiles west: from a row up, west a tile at a time, asking ahead and -// north. Turns only ever change the way we face, so none is a step. His -// last line asks us to try the R Button; a help panel about the Poketch -// follows, closed with B. -void poketch_from_the_president(VideoStream& stream, ProControllerContext& context){ - hold_for(stream, context, Direction::NORTH, 200ms, "A row up from the lamp"); - route_turn(stream, context, Direction::WEST); - DialogueRead read; - for (size_t tiles = 0; tiles < 8 && !read.opened; tiles++){ - read = talk_ahead(stream, context, BUTTON_B, "The Poketch Company president"); - if (read.opened){ - break; - } - route_turn(stream, context, Direction::NORTH); - read = talk_ahead(stream, context, BUTTON_B, "The Poketch Company president"); - if (read.opened){ - break; + for (size_t ask = 0; ask < 40; ask++){ + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 1500ms, {{dialog}}) == 0){ + stream.log(std::format("[Talk] {}: answered after {} asks.", step.name, ask + 1)); + return Pokemon::outcome_ok(); } - route_turn(stream, context, Direction::WEST); - tap_tiles(stream, context, Direction::WEST, 1, "West along the road"); - } - if (!read.opened || !read.closed){ - fail_step(stream, "Jubilife: the president did not give us the Poketch."); - } - stream.log("[Talk] The Poketch's help: B to close the panel."); - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - context.wait_for_all_requests(); -} - -// 00.05 ends on the street east to Route 203, about ten rows north (the -// long road's crossing is six rows above the Mart's wall, which is four -// above the talk; r164a, by hand). A row off is fine: 01.01 walks east and -// tries the rows either side at a wall. The president walks off north after -// his talk, up our way (r164a): let him. -void up_to_the_route_203_street(VideoStream& stream, ProControllerContext& context){ - for (size_t presses = 0; presses < 8; presses++){ - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - } - context.wait_for_all_requests(); - for (size_t row = 0; row < 10; row++){ - hold_for(stream, context, Direction::NORTH, 200ms, "Up the main road"); } -} - -// 00.05: straight south down the School door's column and out. -void out_of_the_school(VideoStream& stream, ProControllerContext& context){ - leave_door_south(stream, context, run_from_wild_battle, "Out of the Trainers' School"); + fail_step(stream, step.name + ": nobody came by to talk within a minute."); } // 01.07: Rowan's and Dawn's lines after the tag battle. 01.08 starts here, @@ -375,15 +223,7 @@ void north_to_rowan(VideoStream& stream, ProControllerContext& context){ void add_jubilife_events(StoryEvents& events){ events.emplace("up_the_steps", console_event(up_the_steps)); events.emplace("barry_town_map", console_event(barry_town_map)); - events.emplace("meet_the_president", console_event(meet_the_president)); - events.emplace("up_to_the_mart_road", console_event(up_to_the_mart_road)); - events.emplace("clown_by_the_mart", console_event(clown_by_the_mart)); - events.emplace("clown_by_the_company", console_event(clown_by_the_company)); - events.emplace("to_the_main_road", console_event(to_the_main_road)); - events.emplace("to_the_center_lamp", console_event(to_the_center_lamp)); - events.emplace("poketch_from_the_president", console_event(poketch_from_the_president)); - events.emplace("up_to_the_route_203_street", console_event(up_to_the_route_203_street)); - events.emplace("out_of_the_school", console_event(out_of_the_school)); + events.emplace("talk_to_wanderer", talk_to_wanderer); events.emplace("rowan_after_the_battle", console_event(rowan_after_the_battle)); events.emplace("jubilife_center_to_street", console_event(jubilife_center_to_street)); events.emplace("east_to_barry", console_event(east_to_barry)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 6cb3159866..b7c4c477db 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -7,9 +7,11 @@ * Butters' button sequences for the scenes (#1717). They replace his * recorded stick holds, and have NOT run on a console yet: where a scene * leaves the player (pose_after) is the decomp's or a guess, marked - * "assumed", until a new-game run checks it. 00.04b to 00.04d and 00.05's - * Jubilife walks were measured on the console (r72 to r261, 2026-10-30); - * segment 01 ran there end to end (2026-10-31), its walks kept as events. + * "assumed", until a new-game run checks it. 00.04b to 00.04d were measured + * on the console (r72 to r261, 2026-10-30); 00.05 to 00.05c, measured there + * first, are now planned on Jubilife's map with the people and triggers of + * the Diamond decomp's zone events and scripts. Segment 01 ran on the + * console end to end (2026-10-31), its walks kept as events. * */ @@ -49,6 +51,8 @@ const char* const LAKEFRONT = "VerityLakefront"; const char* const SANDGEM = "SandgemTown"; const char* const ROUTE_202 = "Route202"; const char* const JUBILIFE = "JubilifeCity"; +// No map of it: a pose there names the room, not its tiles. +const char* const TRAINERS_SCHOOL = "TrainersSchool"; Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ return Pokemon::tile_pose(map, {TilePos{x, y}}, facing); @@ -68,6 +72,11 @@ Pose at_or_next_to(const char* map, const char* point, Direction side){ Pose on(const char* map, const char* region){ return Pokemon::tile_pose(map, map_region(map, region)); } +// Somewhere in `map`, which has no tiles here: enough for a door step to +// know it goes out to another map. +Pose inside(const char* map){ + return Pokemon::tile_pose(map, {}); +} // Columns from below Sandgem's Pokemon Center sign west to below its door. int sandgem_sign_to_center_door(){ return map_point(SANDGEM, "pokemon_center_sign").x - map_point(SANDGEM, "pokemon_center_door").x; @@ -126,6 +135,13 @@ StoryStep step_into(std::string name, Direction direction, std::string why, std: StoryStep talk(std::string name, std::string why){ return make(StoryStepKind::TALK, std::move(name), std::move(why)); } +// A talk to someone who wanders through the tile we face: A until they +// stand there (the "talk_to_wanderer" event). +StoryStep talk_to_wanderer(std::string name, std::string why){ + StoryStep step = talk(std::move(name), std::move(why)); + step.variant = "talk_to_wanderer"; + return step; +} // Text to the overworld, with B; with A ("a") for lines that give an item. StoryStep scene(std::string name, std::optional pose_after, std::string why, std::string button = ""){ StoryStep step = make(StoryStepKind::MASH, std::move(name), std::move(why)); @@ -388,49 +404,83 @@ std::vector to_jubilife(){ }; } -// The Jubilife beats, stepped through on the console on 2026-10-30: every -// leg walks until something stops it, and each talk stands beside the -// person; the coupons and the Poketch end on the street east to Route 203. +// Below the Trainers' School's door, where it lets us out (40, 41). +Pose below_the_school(){ + return next_to(JUBILIFE, "trainers_school_door", SOUTH, SOUTH); +} + +// 00.05: the steps were measured on the console (2026-10-30); the rest is +// planned on the map. +StoryCheckpoint the_town_map(){ + return row("00.05", "Jubilife: the Center, the Trainers' School and the Town Map", std::nullopt, { + event("up_the_steps", "a tile west of the save below the steps, then up them", on(JUBILIFE, "up_the_steps")), + // Planned on the map: the hand-measured walls (west to the sign, east + // across the road, north) leaned on the old turn's nudge and ended + // beside the Trainers' School once turns stopped stepping (r57, r58). + route("to_the_center", at(JUBILIFE, 52, 41, NORTH), + "below the Center's door (52, 40)", "town"), + // The kids south of the Center can push us aside: the heal lines up by sight. + heal("heal_jubilife", "in, the nurse, out onto the doorstep (r143a: check we are out)"), + // Same rule as the Center: plan to the tile below the door, then run in + // (r63: "eleven tiles west" counted on the old turn's step; the map says twelve). + route("to_the_school", next_to(JUBILIFE, "trainers_school_door", SOUTH, NORTH), + "below the School's door (40, 40)", "town"), + step_into("into_the_school", NORTH, "the School's door", inside(TRAINERS_SCHOOL)), + to_walls("up_to_barry", {{.direction = NORTH, .tiles = 10}}, "the door's column to the back wall, beside Barry"), + event("barry_town_map", "Barry gives the Town Map", inside(TRAINERS_SCHOOL)), + // Barry stands beside the door's column, which is clear down to the door. + step_into("out_of_the_school", SOUTH, "straight down the door's column and out", below_the_school()), + }); +} + +// 00.05b, from the Diamond decomp (zone event narc_0002, scripts +// scr_seq narc_0002; local = the decomp's world tile - (128, 736)): the +// president's coord event, row 40 from x 44 to 48 while +// VAR_POKETCH_CAMPAIGN_STATE is 1, walks him from (46, 35) to above us; +// then the three clowns, each with a question whose answer is Yes. +StoryCheckpoint the_coupons(){ + return row("00.05b", "Jubilife: the president and the clowns' coupons", below_the_school(), { + // From x 46 he walks straight down 4 to (46, 39); from the others he turns. + route("below_the_presidents_trigger", at(JUBILIFE, 46, 41, NORTH), "east along the School's row", "town"), + step_into("presidents_trigger", NORTH, "the trigger (44..48, 40)"), + scene("president_walks_up", at(JUBILIFE, 46, 40, NORTH), "he walks down to (46, 39) and sends us for the coupons"), + // He stays at (46, 39) until the TV station clown's coupon sends him + // home; a plan north from his column would walk into him. + legs("past_the_president", {{.direction = EAST, .tiles = 2}}, "east of his column"), + // He wanders (54..56, 33); he comes through (55, 33) every other move. + route("below_the_mart_clown", next_to(JUBILIFE, "clown_mart", SOUTH, NORTH), "up the road east of the Center", "town"), + talk_to_wanderer("mart_clown", "the clown by the Mart, north of us when he is home"), + scene("mart_clowns_coupon", next_to(JUBILIFE, "clown_mart", SOUTH, NORTH), "\"Do Pokemon grow by defeating others?\" Yes; Coupon 1", "a"), + // He stands still below the TV station's door, facing south. + route("below_the_tv_clown", next_to(JUBILIFE, "clown_tv_station", SOUTH, NORTH), "west and north to the TV station", "town"), + talk("tv_clown", "the clown below the TV station's door"), + // His script also puts the president back at (46, 35), facing south. + scene("tv_clowns_coupon", next_to(JUBILIFE, "clown_tv_station", SOUTH, NORTH), "\"Can a Pokemon hold an item?\" Yes; Coupon 3", "a"), + // He wanders (14..16, 18), behind the Poketch Company's sign (13, 19). + route("below_the_company_clown", next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), "west along the long road, north beside the sign", "town"), + talk_to_wanderer("company_clown", "the clown by the Poketch Company, north of us when he is home"), + scene("company_clowns_coupon", next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), "\"Do moves have types?\" Yes; Coupon 2", "a"), + }); +} + +// 00.05c: the president trades the coupons for the Poketch, walks 9 north +// and is gone (decomp); then up to the street east to Route 203, where +// 01.01 walks east until Barry stops us. +StoryCheckpoint the_poketch(){ + const Pose below_the_president = next_to(JUBILIFE, "poketch_president", SOUTH, NORTH); + return row("00.05c", "Jubilife: the Poketch, up to the street east to Route 203", + next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), { + route("below_the_president", below_the_president, "east along the long road, down the main road", "town"), + talk("president", "the Poketch Company's president at (46, 35)"), + // BDSP adds a help panel on the Poketch; B closes it. + scene("the_poketch", below_the_president, "the coupons for the Poketch; he walks off north"), + // The street's rows 21 to 24 lead onto Route 203 at x 63. + route("to_the_route_203_street", at(JUBILIFE, 58, 22, EAST), "up the main road and east along the street", "town"), + }); +} + std::vector jubilife(){ - return { - row("00.05", "Jubilife: the Center, the Trainers' School and the Town Map", std::nullopt, { - event("up_the_steps", "a tile west of the save below the steps, then up them", on(JUBILIFE, "up_the_steps")), - // Planned on the map: the hand-measured walls (west to the sign, east - // across the road, north) leaned on the old turn's nudge and ended - // beside the Trainers' School once turns stopped stepping (r57, r58). - route("to_the_center", at(JUBILIFE, 52, 41, NORTH), - "below the Center's door (52, 40)", "town"), - // The kids south of the Center can push us aside: the heal lines up by sight. - heal("heal_jubilife", "in, the nurse, out onto the doorstep (r143a: check we are out)"), - // Same rule as the Center: plan to the tile below the door, then run in - // (r63: "eleven tiles west" counted on the old turn's step; the map says twelve). - route("to_the_school", next_to(JUBILIFE, "trainers_school_door", SOUTH, NORTH), - "below the School's door (40, 40)", "town"), - step_into("into_the_school", NORTH, "the School's door"), - to_walls("up_to_barry", {{.direction = NORTH, .tiles = 10}}, "the door's column to the back wall, beside Barry"), - event("barry_town_map", "Barry gives the Town Map"), - event("out_of_the_school", "straight down the door's column and out"), - }), - row("00.05b", "Jubilife: the president and the clowns' coupons", std::nullopt, { - event("meet_the_president", "four tiles east into the road; the president walks up and talks"), - to_walls("east_to_the_lamp", {{.direction = EAST, .tiles = 8}}, "east until the lamp by the Center"), - event("up_to_the_mart_road", "a tile west, then north onto the road between the Center and the Mart"), - to_walls("east_to_the_building", {{.direction = EAST, .tiles = 14}}, "east along it until the tall building"), - event("clown_by_the_mart", "his question, Yes, and a coupon"), - // The TV station's screen keeps the picture moving after we stop: - // r161 saw the stop only at 14 tiles' time, r162a not within it. - to_walls("to_the_west_lamp", {{.direction = WEST, .tiles = 20}, {.direction = SOUTH, .tiles = 10}, - {.direction = WEST, .tiles = 26}, {.direction = NORTH, .tiles = 6}}, - "west to the hedges, south to the long road, west to the lamp, north a row"), - event("clown_by_the_company", "along the hedges to the gap by the Poketch Company's sign; he comes by"), - }), - row("00.05c", "Jubilife: the Poketch, up to the street east to Route 203", std::nullopt, { - event("to_the_main_road", "a row south of the gap, east along the long road to the main road"), - event("to_the_center_lamp", "six rows down, east to the Mart's wall, south to the Center's lamp"), - event("poketch_from_the_president", "he trades the coupons for the Poketch"), - event("up_to_the_route_203_street", "let him walk off, then ten rows north"), - }), - }; + return {the_town_map(), the_coupons(), the_poketch()}; } std::vector to_oreburgh(){ From f1d8c3a2e3d848689658428517b169ff0abca26b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:46:05 -0800 Subject: [PATCH 63/98] BDSP AutoStory: town routes take the short way; test for detours The town costs still treated open legs as a tile off either way, as stick holds were, so the plan to the Trainers' School went 38 tiles round the town to end every leg on a wall, for 12 straight. Open legs go on the D-pad now and land exactly. The story simulation now fails a route that walks much further than the shortest way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Data/PokemonBDSP_AutoStory_PlanCosts.cpp | 13 ++++---- ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 33 ++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp index ce7b7bc124..386089d180 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.cpp @@ -29,13 +29,14 @@ RobustPlanCosts waypoint_plan_costs(){ } RobustPlanCosts town_plan_costs(){ + // Open legs go on the D-pad now, a whole tile a tap, so they land exactly + // at any length ("south 5" went 6 in r134a on stick holds). A wall at a + // leg's end is still worth a little: r64 went 38 tiles round the town + // to end each leg on one, for 12 straight. RobustPlanCosts costs; - // A tile's hold moves exactly a tile; a longer open leg may end a tile - // either way ("south 5" went 6 in r134a), so the plan ends on a wall. - costs.exact_open_tiles = 1; - costs.open_error_per = 8; - costs.leg_not_at_wall = 6; - costs.per_open_tile = 2; + costs.exact_open_tiles = costs.max_open_tiles; + costs.per_open_tile = 1; + costs.leg_not_at_wall = 4; return costs; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp index 0f2aeaf9da..f1d5ee3489 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -25,6 +25,7 @@ #include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP_AutoStory_StoryPlaces.h" #include "PokemonBDSP_AutoStory_StoryTable.h" #include "PokemonBDSP_AutoStory_StorySimulation_Tests.h" @@ -70,6 +71,31 @@ struct Walked{ std::string plan; }; +// The tiles a plan walks. +int tiles_walked(const std::vector& plan){ + int tiles = 0; + for (const PlannedLeg& leg : plan){ + tiles += leg.leg.tiles; + } + return tiles; +} + +// A route may walk a little further than the shortest way, for a wall at a +// leg's end, but not round the town: r64 went 38 tiles to the School's door +// for 12 straight (the "town" costs still feared stick-era open legs). +std::string detour_error(const StoryWorld& world, const std::vector& start, + const std::function& at_goal, const std::vector& plan +){ + const auto shortest = plan_robust_route(world, start, at_goal, waypoint_plan_costs()); + const int walked = tiles_walked(plan); + const int least = shortest ? tiles_walked(*shortest) : walked; + if (walked <= least + least / 2 + 4){ + return ""; + } + return std::format("the plan walks {} tiles where {} would do.", walked, least); +} + + // A ROUTE step from `from`, planned as StoryExecutor::walk_to() walks it. Walked plan_route(const Pose& from, const StoryStep& step){ const Pose& goal = *step.target; @@ -86,7 +112,8 @@ Walked plan_route(const Pose& from, const StoryStep& step){ for (const TilePos& pos : Pokemon::tiles_of(goal)){ targets.push_back(story_to_world(*world, goal.map, pos)); } - const auto plan = plan_robust_route(*world, start, [&](const TilePos& pos){ return contains(targets, pos); }, *costs); + const std::function at_goal = [&](const TilePos& pos){ return contains(targets, pos); }; + const auto plan = plan_robust_route(*world, start, at_goal, *costs); if (!plan){ return {std::nullopt, step.name + ": no plan from" + describe(from.map, Pokemon::tiles_of(from)) + ".", ""}; } @@ -95,6 +122,10 @@ Walked plan_route(const Pose& from, const StoryStep& step){ for (const PlannedLeg& leg : *plan){ walked.plan += " " + to_string(leg.leg); } + const std::string detour = detour_error(*world, start, at_goal, *plan); + if (!detour.empty()){ + return {std::nullopt, step.name + ": " + detour, walked.plan}; + } for (const TilePos& pos : plan->empty() ? start : plan->back().after){ const std::optional tile = story_to_local(*world, goal.map, pos); if (!tile || !contains(Pokemon::tiles_of(goal), *tile)){ From 299032560cbbaa21e17f4432746f5c5a80498318 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 01:49:16 -0800 Subject: [PATCH 64/98] BDSP AutoStory: talk to a wandering clown from under his whole range The Mart clown wanders a tile either side of home; asking north from below home for a minute missed him (r66: he stood a tile east). Walk the row below his range a D-pad tile at a time and ask from each; answer Yes with A, then step back below home so the next route starts on a known tile. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 57 ++++++++++++++----- .../PokemonBDSP_AutoStory_StoryTable.cpp | 16 +++--- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index 384ca9879b..e3d62f952f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -66,29 +66,58 @@ void barry_town_map(VideoStream& stream, ProControllerContext& context){ fail_step(stream, "Trainers' School: Barry did not answer from beside the back wall."); } -// 00.05b: a talk to someone who wanders through the tile we face (the -// clowns by the Mart and the Poketch Company: WANDER_AROUND a tile either -// side of home, so through it every other move). Faces the pose's way, -// then A until a line shows, for up to a minute; the scene after it -// answers and closes the talk. -Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep& step){ - VideoStream& stream = executor.stream(); - ProControllerContext& context = executor.context(); - if (const std::optional facing = Pokemon::facing_of(state.pose)){ - route_turn(stream, context, *facing); - } - for (size_t ask = 0; ask < 40; ask++){ +// 00.05b: a talk to someone who wanders a tile either side of home along a +// row (the clowns by the Mart and the Poketch Company, WANDER_AROUND with +// x range 1). Waiting under home alone missed him for a minute (r66: he +// stood a tile east), so walk the row below his range, a whole D-pad tile +// at a time, asking north from each tile. His question's answer is Yes, so +// A answers and takes the coupon; then back under his home tile. +bool ask_north(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::NORTH); + for (size_t ask = 0; ask < 2; ask++){ pbf_press_button(context, BUTTON_A, 80ms, 0ms); context.wait_for_all_requests(); ShortDialogWatcher dialog(COLOR_YELLOW); if (wait_until(stream, context, 1500ms, {{dialog}}) == 0){ - stream.log(std::format("[Talk] {}: answered after {} asks.", step.name, ask + 1)); + return true; + } + } + return false; +} +// One tile along the row, east (+1) or west (-1). +void step_along(VideoStream& stream, ProControllerContext& context, int east, const std::string& label){ + route_turn(stream, context, east > 0 ? Direction::EAST : Direction::WEST); + walk_tiles_dpad(stream, context, run_from_wild_battle, {east > 0 ? 1.0 : -1.0, 0}, 1, label); +} +Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState&, const Pokemon::StoryStep& step){ + VideoStream& stream = executor.stream(); + ProControllerContext& context = executor.context(); + const std::string label = "Along the row below " + step.name; + // Home, east, west, west, east: each move one tile on the D-pad. + const int moves[] = {0, 1, -1, -1, 1}; + int at = 0; + for (size_t round = 0; round < 6; round++){ + for (int move : moves){ + if (move != 0){ + step_along(stream, context, move, label); + at += move; + } + if (!ask_north(stream, context)){ + continue; + } + stream.log(std::format("[Talk] {}: answered in round {}, {} tiles east of home.", step.name, round + 1, at)); + finish_dialogue(stream, context, BUTTON_A, step.name); + for (; at != 0; at += at > 0 ? -1 : 1){ + step_along(stream, context, at > 0 ? -1 : 1, "Back below " + step.name + "'s home"); + } + route_turn(stream, context, Direction::NORTH); return Pokemon::outcome_ok(); } } - fail_step(stream, step.name + ": nobody came by to talk within a minute."); + fail_step(stream, step.name + ": no answer from any tile below his range."); } + // 01.07: Rowan's and Dawn's lines after the tag battle. 01.08 starts here, // from a save at the top of the street. void rowan_after_the_battle(VideoStream& stream, ProControllerContext& context){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index b7c4c477db..5c80713fba 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -135,11 +135,13 @@ StoryStep step_into(std::string name, Direction direction, std::string why, std: StoryStep talk(std::string name, std::string why){ return make(StoryStepKind::TALK, std::move(name), std::move(why)); } -// A talk to someone who wanders through the tile we face: A until they -// stand there (the "talk_to_wanderer" event). -StoryStep talk_to_wanderer(std::string name, std::string why){ +// A talk to someone who wanders a tile either side of home along a row: +// asks north from each tile below the range, answers with A and steps back +// below home (the "talk_to_wanderer" event). +StoryStep talk_to_wanderer(std::string name, Pose pose_after, std::string why){ StoryStep step = talk(std::move(name), std::move(why)); step.variant = "talk_to_wanderer"; + step.pose_after = std::move(pose_after); return step; } // Text to the overworld, with B; with A ("a") for lines that give an item. @@ -449,8 +451,8 @@ StoryCheckpoint the_coupons(){ legs("past_the_president", {{.direction = EAST, .tiles = 2}}, "east of his column"), // He wanders (54..56, 33); he comes through (55, 33) every other move. route("below_the_mart_clown", next_to(JUBILIFE, "clown_mart", SOUTH, NORTH), "up the road east of the Center", "town"), - talk_to_wanderer("mart_clown", "the clown by the Mart, north of us when he is home"), - scene("mart_clowns_coupon", next_to(JUBILIFE, "clown_mart", SOUTH, NORTH), "\"Do Pokemon grow by defeating others?\" Yes; Coupon 1", "a"), + talk_to_wanderer("mart_clown", next_to(JUBILIFE, "clown_mart", SOUTH, NORTH), + "the clown by the Mart: 'Do Pokemon grow by defeating others?' Yes; Coupon 1"), // He stands still below the TV station's door, facing south. route("below_the_tv_clown", next_to(JUBILIFE, "clown_tv_station", SOUTH, NORTH), "west and north to the TV station", "town"), talk("tv_clown", "the clown below the TV station's door"), @@ -458,8 +460,8 @@ StoryCheckpoint the_coupons(){ scene("tv_clowns_coupon", next_to(JUBILIFE, "clown_tv_station", SOUTH, NORTH), "\"Can a Pokemon hold an item?\" Yes; Coupon 3", "a"), // He wanders (14..16, 18), behind the Poketch Company's sign (13, 19). route("below_the_company_clown", next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), "west along the long road, north beside the sign", "town"), - talk_to_wanderer("company_clown", "the clown by the Poketch Company, north of us when he is home"), - scene("company_clowns_coupon", next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), "\"Do moves have types?\" Yes; Coupon 2", "a"), + talk_to_wanderer("company_clown", next_to(JUBILIFE, "clown_poketch_company", SOUTH, NORTH), + "the clown by the Poketch Company: 'Do moves have types?' Yes; Coupon 2"), }); } From 23d9efa9db1818ee04bdd26d5937b3c00afe5373 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 02:36:33 -0800 Subject: [PATCH 65/98] BDSP AutoStory: Oreburgh's Center door step does nothing when already inside As 01.01 does for Jubilife's Center: a run started from a save inside the door goes straight on to the walk south into the mine. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index d4b4c7d2b5..a0f06ba030 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -20,6 +20,7 @@ #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" @@ -226,6 +227,15 @@ void oreburgh_center_door(VideoStream& stream, ProControllerContext& context){ // ignored A in r101 and r104 until then). A save after his talk has him // gone already. // + // Already in the Center (a save inside its door): nothing to do, as + // 01.01 does for its Center. + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + if (indoors(*screen.frame)){ + stream.log("[Route] 01.03: already in the Pokemon Center."); + return; + } + } enter_oreburgh_center(stream, context); context.wait_for(1000ms); } From 6786b015688f677522b476991bc6482f8b390f6e Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 02:43:04 -0800 Subject: [PATCH 66/98] BDSP AutoStory: 01.03 starts in the mine when the save is there The Center's door step and the walk south skip themselves when the mine's dim hall is already on screen, so a run can start from a save in the mine's entrance hall (Barry stands in the Center's door on this save). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index a0f06ba030..017599dff9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -236,6 +236,10 @@ void oreburgh_center_door(VideoStream& stream, ProControllerContext& context){ return; } } + if (in_dim_cave(stream)){ + stream.log("[Route] 01.03: already in the mine."); + return; + } enter_oreburgh_center(stream, context); context.wait_for(1000ms); } @@ -249,6 +253,11 @@ void south_into_the_mine(VideoStream& stream, ProControllerContext& context){ // hand). A worker walks about on the sand path and stops the walk as a // wall would (r95, r96): wait for him to move and walk on. Step 3 copes // with ending in the hall or in the corridor. + // A save in the mine's entrance hall: already there. + if (in_dim_cave(stream)){ + stream.log("[Route] 01.03: already in the mine."); + return; + } bool in_mine = false; size_t transitions = 0; Milliseconds walked = 0ms; From 4e0f7c3437b905aaa873773cbb4a9cd0a55d80f0 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 03:08:57 -0800 Subject: [PATCH 67/98] BDSP AutoStory: 01.04 starts in the gym when the save is there Leaving the Center does nothing off the Center's floor, and the walk to the gym does nothing indoors, so a run can start from a save inside the gym's door. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index 017599dff9..42b3e1efed 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -155,7 +155,14 @@ constexpr size_t GYM_DPAD_TAPS_WEST_OF_CENTER = 20; void oreburgh_street_to_gym(VideoStream& stream, ProControllerContext& context){ // From the Center's doorstep, straight west along its row to below the // gym's door, then north through it, all on the D-pad (whole tiles; see - // GYM_DPAD_TAPS_WEST_OF_CENTER). + // GYM_DPAD_TAPS_WEST_OF_CENTER). A save inside the gym: already there. + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + if (indoors(*screen.frame)){ + stream.log("[Route] 01.04: already in the gym."); + return; + } + } dpad_tiles(stream, context, DPAD_LEFT, GYM_DPAD_TAPS_WEST_OF_CENTER, "west to the gym's door"); if (dpad_through_door_north(stream, context, "Oreburgh Gym door")){ return; @@ -465,7 +472,15 @@ void oreburgh_center_heal(VideoStream& stream, ProControllerContext& context){ // 01.04: out of the Pokemon Center from the counter. void out_of_the_center(VideoStream& stream, ProControllerContext& context){ // 01.03 leaves us at the Center's counter, facing the nurse; the door - // is straight south. + // is straight south. Not on the Center's floor (a save in the gym): + // nothing to leave. + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + if (!center_floor_on_screen(*screen.frame)){ + stream.log("[Route] 01.04: not in the Center."); + return; + } + } walk_out_of_center(stream, context, "Oreburgh"); } From 4cc3bd0388a2ba63d81ff01e914efdfb4c3419a4 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 03:15:37 -0800 Subject: [PATCH 68/98] BDSP AutoStory: tell the Oreburgh gym from the street by brightness indoors() reads the gym's rock floor as outdoors, so a run from a save in the gym walked west in it (r74). The gym is darker than the street (mean 75 against 104). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index 42b3e1efed..163993a003 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -152,16 +152,17 @@ void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context // 36 to 48 px, so 18 or 19 of them missed the door by a part of a tile. constexpr size_t GYM_DPAD_TAPS_WEST_OF_CENTER = 20; +const uint64_t GYM_DARKER_THAN = 90; void oreburgh_street_to_gym(VideoStream& stream, ProControllerContext& context){ // From the Center's doorstep, straight west along its row to below the // gym's door, then north through it, all on the D-pad (whole tiles; see // GYM_DPAD_TAPS_WEST_OF_CENTER). A save inside the gym: already there. + // The gym's rock floor is darker than the street (mean brightness 75 + // against 104, r74), which indoors() does not tell apart. context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - if (indoors(*screen.frame)){ - stream.log("[Route] 01.04: already in the gym."); - return; - } + if (in_dim_cave(stream, GYM_DARKER_THAN)){ + stream.log("[Route] 01.04: already in the gym."); + return; } dpad_tiles(stream, context, DPAD_LEFT, GYM_DPAD_TAPS_WEST_OF_CENTER, "west to the gym's door"); if (dpad_through_door_north(stream, context, "Oreburgh Gym door")){ From f1bd5ac3ae3a5c7c7c185e5471267448b52c67c8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 05:33:01 -0800 Subject: [PATCH 69/98] BDSP AutoStory: plan 01.05 to 01.10 on the maps Segment 01's walks from Oreburgh to Floaroma were events tuned when a turn also stepped. They are now route, step and scene rows on the JSON maps, with the triggers and warps of the Diamond decomp's zone events: Barry's bump in Oreburgh (6, 12..15), the Gate's row 22, Route 203 into Jubilife, Rowan's trigger (45..47, 7), Route 204 up to the Ravaged Path, and the human's line up Route 204's north half into Floaroma. Routes cross maps on joined worlds and fight the trainers of Routes 203 and 204 who see us. Only the Rock Smash stays an event. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events.h | 5 +- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 46 +---- .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 47 +---- .../PokemonBDSP_AutoStory_Events_Route203.cpp | 49 +---- .../PokemonBDSP_AutoStory_Events_Route204.cpp | 145 ++------------ .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 29 ++- .../PokemonBDSP_AutoStory_StoryExecutor.h | 12 +- .../PokemonBDSP_AutoStory_StoryPlaces.cpp | 187 +++++++++++++++--- .../PokemonBDSP_AutoStory_StoryPlaces.h | 10 +- .../PokemonBDSP_AutoStory_StoryTable.cpp | 141 ++++++++++--- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 +- .../AutoStory/PokemonBDSP_AutoStory_Walks.cpp | 74 ------- .../AutoStory/PokemonBDSP_AutoStory_Walks.h | 16 -- 13 files changed, 330 insertions(+), 433 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h index f3917b1c0c..388b396eae 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -11,8 +11,9 @@ * the executor turns that into a failed step. * * They are in files by area: _Events_Lake (00.00 to 00.03), _Sandgem - * (00.04), _Jubilife (00.05, 01.01, 01.06 to 01.08), _Oreburgh (01.02 to - * 01.05) and _Route204 (01.09 to 01.11). + * (00.04), _Jubilife (00.05, 01.01), _Route203 (01.02), _Oreburgh (01.03 + * and 01.04) and _Route204 (01.09's Rock Smash, 01.11). 01.05 to 01.10 + * are planned walks in the story table. * */ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index e3d62f952f..c1dc95e0dd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -1,10 +1,10 @@ -/* BDSP AutoStory - Events: Jubilife City (00.05, 01.01, 01.07) +/* BDSP AutoStory - Events: Jubilife City (00.05, 01.01) * * From: https://github.com/PokemonAutomation/ * * The Jubilife beats of 00.05 the story table cannot plan (up the steps, - * Barry's Town Map, the talk to a wandering clown), and segment 01's walks - * out of the Center to Barry and up to Rowan's scene. + * Barry's Town Map, the talk to a wandering clown), and segment 01's walk + * out of the Center to Barry. * */ @@ -118,12 +118,6 @@ Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState&, c } -// 01.07: Rowan's and Dawn's lines after the tag battle. 01.08 starts here, -// from a save at the top of the street. -void rowan_after_the_battle(VideoStream& stream, ProControllerContext& context){ - finish_dialogue(stream, context, BUTTON_B, "Rowan and Dawn after the battle"); -} - // 01.01 from the save at the Jubilife Center's PC: healed at the nurse, // out of the door and through the plaza to the street east to Route 203. void jubilife_heal_and_out_to_street(VideoStream& stream, ProControllerContext& context){ @@ -215,37 +209,6 @@ void east_to_barry(VideoStream& stream, ProControllerContext& context){ } } -// 01.07: west along Jubilife's long road to the crossroads. -void west_to_the_crossroads(VideoStream& stream, ProControllerContext& context){ - // Route 203's row 18 carries on as Jubilife's long road. From the first - // tile the crossroads with the street north to Route 204 is 14 tiles - // west; the street is about 4 tiles wide, so a stick hold does (by hand, - // 2026-10-31: 2 x 1120 ms landed mid-street; 7 D-pad taps stopped below - // a building, and 18 went on to the road down to Route 202, r230b). - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 14), 300ms); - context.wait_for_all_requests(); -} - -// 01.07: north up the street until Rowan's scene starts. -void north_to_rowan(VideoStream& stream, ProControllerContext& context){ - // North up the street: Rowan's scene starts a few tiles up (by hand, a - // 6 s walk). It opens with a cutscene, not a dialog box, so the walk - // reads as stopped; wait for his first line. The green-haired man walks - // about the crossroads and may stand in our way: wait and walk again. - bool scene = false; - for (size_t tries = 0; tries < 3 && !scene; tries++){ - const WalkResult up = walk_dir(stream, context, Direction::NORTH, 6000ms, "north up the street to Route 204"); - scene = up.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 8000ms); - if (!scene){ - pbf_wait(context, 2500ms); - } - } - if (!scene){ - fail_step(stream, "Jubilife: Rowan's scene did not start up the street north."); - } -} - } @@ -253,11 +216,8 @@ void add_jubilife_events(StoryEvents& events){ events.emplace("up_the_steps", console_event(up_the_steps)); events.emplace("barry_town_map", console_event(barry_town_map)); events.emplace("talk_to_wanderer", talk_to_wanderer); - events.emplace("rowan_after_the_battle", console_event(rowan_after_the_battle)); events.emplace("jubilife_center_to_street", console_event(jubilife_center_to_street)); events.emplace("east_to_barry", console_event(east_to_barry)); - events.emplace("west_to_the_crossroads", console_event(west_to_the_crossroads)); - events.emplace("north_to_rowan", console_event(north_to_rowan)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index 163993a003..7628d3ce8c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -1,10 +1,9 @@ -/* BDSP AutoStory - Events: Oreburgh (01.03 to 01.05) +/* BDSP AutoStory - Events: Oreburgh (01.03 to 01.04) * * From: https://github.com/PokemonAutomation/ * - * Segment 01's walks in Oreburgh: the mine and Roark, the Gym, and back - * out of the Oreburgh Gate, as they ran on the console (2026-10-29 to - * 10-31). + * Segment 01's walks in Oreburgh: the mine and Roark, and the Gym, as + * they ran on the console (2026-10-29 to 10-31). * */ @@ -21,7 +20,6 @@ #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" @@ -504,42 +502,6 @@ void gym_until_roark_beaten(VideoStream& stream, ProControllerContext& context){ } } -// 01.05: out of the gym onto its doorstep. -void out_of_the_gym(VideoStream& stream, ProControllerContext& context){ - // 01.04 ends in front of Roark, just after his battle: its save at - // 01.05's start is right after the win (2026-10-31, at the user's - // request), so a failure later never has to beat him again. - // No trip back to the Oreburgh Center (2026-10-31, the user): talk_or_fight() - // gives a Potion after Roark when HP is low, and Jubilife's Center is on - // the way. Out of the gym onto its doorstep, which is on the Center door's - // row: west along it reaches the stairs' cliff. - if (!dpad_out_of_building(stream, context, 40, "out of the gym")){ - fail_step(stream, "Oreburgh Gym: did not leave by its door."); - } -} - -// 01.05: up to the platform by the Oreburgh Gate and into it (Barry bumps into us there). -void into_oreburgh_gate(VideoStream& stream, ProControllerContext& context){ - // From the gym's doorstep, as walked by hand on 2026-10-29. - walk_dir(stream, context, Direction::WEST, 20000ms, "west to the stairs' cliff"); - walk_dir(stream, context, Direction::NORTH, 10000ms, "north along the cliff"); - hold_tiles(stream, context, Direction::EAST, 2); - walk_dir(stream, context, Direction::NORTH, 5000ms, "north up the stairs"); - walk_dir(stream, context, Direction::WEST, 6000ms, "west along the platform"); - hold_tiles(stream, context, Direction::SOUTH, 1); - // Barry runs into us on the platform ("*thud*") the first time; his lines - // end with no battle. - west_until_transition(stream, context, "Into Oreburgh Gate", 8); -} - -// 01.05: west through the Gate until Route 203's banner shows. -void out_of_oreburgh_gate(VideoStream& stream, ProControllerContext& context){ - // The cave's fade out of the Gate does not always read as a transition - // (r89), so the walk west ends on Route 203's location banner, which - // shows for a few seconds as we step out. 01.06 starts there, from a save. - west_until_transition(stream, context, "West out of Oreburgh Gate", 8, location_banner_showing); -} - } @@ -551,9 +513,6 @@ void add_oreburgh_events(StoryEvents& events){ events.emplace("oreburgh_center_heal", console_event(oreburgh_center_heal)); events.emplace("out_of_the_center", console_event(out_of_the_center)); events.emplace("gym_until_roark_beaten", console_event(gym_until_roark_beaten)); - events.emplace("out_of_the_gym", console_event(out_of_the_gym)); - events.emplace("into_oreburgh_gate", console_event(into_oreburgh_gate)); - events.emplace("out_of_oreburgh_gate", console_event(out_of_oreburgh_gate)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp index 6c3b272bab..6b15500f78 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp @@ -1,10 +1,9 @@ -/* BDSP AutoStory - Events: Route 203 (01.02, 01.06) +/* BDSP AutoStory - Events: Route 203 (01.02) * * From: https://github.com/PokemonAutomation/ * * Segment 01's walks along Route 203: east to the Oreburgh Gate and through - * it to Oreburgh, and back west to Jubilife, as they ran on the console - * (2026-10-29 to 10-31). + * it to Oreburgh, as they ran on the console (2026-10-29 to 10-31). * */ @@ -18,16 +17,12 @@ #include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Talks.h" #include "PokemonBDSP_AutoStory_Walks.h" #include "PokemonBDSP_AutoStory_WildBattle.h" #include "PokemonBDSP_AutoStory_Events.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -188,51 +183,11 @@ void oreburgh_welcome(VideoStream& stream, ProControllerContext& context){ context.wait_for_all_requests(); } -// 01.06: Route 203 on its grid to row 18, below the girl. -Pokemon::Outcome route_203_to_row_18(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ - VideoStream& stream = executor.stream(); - ProControllerContext& context = executor.context(); - // On the grid (Maps/Route203.json, from the decomp, - // its east stretch checked by hand): south from the Gate down the sand, - // west along the sand's bottom row and on under the girl's row, which - // runs straight to Jubilife as row 18. Trainers the map does not list - // are fought where they stop us. - RouteTrainers trainers{{}, [&](const std::string& who){ - talk_or_fight(stream, context, "Route 203: " + who); - }}; - const std::vector at = walk_route(stream, context, run_from_wild_battle, route203_maps(), map_region("Route203", "gate_exit"), trainers, RouteWalkPlan{on_route203_west_line, "Route 203 to row 18", {}, route_plan_costs()}); - if (at.size() != 1){ - fail_step(stream, "Route 203: the walk to row 18 did not end on a known tile."); - } - return Pokemon::outcome_ok(Pokemon::tile_pose("Route203", at)); -} - -// 01.06: west along row 18 into Jubilife, counting tiles. -Pokemon::Outcome row_18_into_jubilife(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ - VideoStream& stream = executor.stream(); - ProControllerContext& context = executor.context(); - const std::vector at = Pokemon::tiles_of(state.pose); - if (at.size() != 1){ - fail_step(stream, "Route 203: row 18's walk needs the one tile the walk to it ended on."); - } - // Row 18 has no wall before Jubilife, so count the tiles: from column - // `x` it is x + 1 taps to Jubilife's first tile (by hand from column 31: - // 32 taps, Jubilife's banner on the last one). 01.07 starts there. - dpad_tiles(stream, context, DPAD_LEFT, (size_t)at[0].x + 1, "west along row 18 into Jubilife"); - context.wait_for_all_requests(); - if (!location_banner_showing(*stream.video().snapshot().frame)){ - fail_step(stream, "Route 203: no Jubilife banner after counting the tiles west."); - } - return Pokemon::outcome_ok(); -} - } void add_route203_events(StoryEvents& events){ events.emplace("route_203_east_to_the_gate", route_203_east_to_the_gate); - events.emplace("route_203_to_row_18", route_203_to_row_18); - events.emplace("row_18_into_jubilife", row_18_into_jubilife); events.emplace("the_hiker", console_event(the_hiker)); events.emplace("hidden_moves_card", console_event(hidden_moves_card)); events.emplace("gate_east_to_oreburgh", console_event(gate_east_to_oreburgh)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index 9988c24e87..438bf8e3c6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -1,27 +1,18 @@ -/* BDSP AutoStory - Events: Route 204 to Floaroma (01.08 to 01.11) +/* BDSP AutoStory - Events: Route 204 to Floaroma (01.09, 01.11) * * From: https://github.com/PokemonAutomation/ * - * Segment 01's walks up Route 204 South and through the Ravaged Path, up Route 204 North into - * Floaroma, and to the old couple in its flower field, as they ran on the - * console (2026-10-31). + * The Ravaged Path's Rock Smash, and segment 01's walks to the old couple + * in Floaroma's flower field, as they ran on the console (2026-10-31). * */ -#include #include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Inference/PokemonBDSP_SelectionArrow.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Talks.h" @@ -68,122 +59,16 @@ bool answer_rock_smash_prompt(VideoStream& stream, ProControllerContext& context return false; } -// 01.08: north out of Jubilife onto Route 204. -void north_out_of_jubilife(VideoStream& stream, ProControllerContext& context){ - // The street's column 14 carries on as the route's; the location banner - // names Route 204 as we cross. A D-pad walk keeps the column; it may - // stop a tile or two past the border, which the entry set allows for. - const WalkResult out = walk_until_dpad(stream, context, run_from_wild_battle, {0, 1}, 8000ms, "north out of Jubilife", {.arrived = location_banner_showing}); - if (out.stop != WalkStop::ARRIVED){ - fail_step(stream, "Jubilife: no Route 204 banner walking north up the street."); +// 01.09: Rock Smash on the rock east of us, from the tile the route ends on +// facing it. Only the prompt's Yes/No box tells the rock from someone in +// the way (r237a: the walker took one for a person and said No). +void smash_the_rock(VideoStream& stream, ProControllerContext& context){ + route_turn(stream, context, Direction::EAST); + if (!talk_with_nudges(stream, context, run_from_wild_battle, Direction::EAST, "the rock east of us", 2000ms)){ + fail_step(stream, "Ravaged Path: nothing answered facing the rock east of us."); } - context.wait_for(1500ms); -} - -// 01.08: Route 204 South on its grid to below the Ravaged Path's door. -void route_204_south_to_the_door(VideoStream& stream, ProControllerContext& context){ - // On the grid decoded from the game's data (Maps/Route204South.json): - // a tile west onto the tall grass column that runs up to the cliff, - // north up it, then west along the cliff to the signboard beside the - // door (the offline plan: 3 legs). Other trainers' sight lines are kept - // out of; town_plan_costs() lets the one-tile step land exactly. Lass Sarah's east line crosses - // the street, so she may stop us for a battle on the way; a Youngster - // who is not a trainer wanders in front of the door, and the walker - // waits for him. - { - RouteTrainers trainers{route204_south_trainer_sights(), [&](const std::string& who){ talk_or_fight(stream, context, who); }}; - const std::vector at = walk_route(stream, context, run_from_wild_battle, route204_south_maps(), - map_region("Route204South", "jubilife_entry"), trainers, RouteWalkPlan{[](const Pokemon::TilePos& pos){ return pos == map_point("Route204South", "below_door"); }, "Route 204 South to the Ravaged Path", route204_south_tiles_to_avoid(), town_plan_costs()}); - if (at.size() != 1 || !(at[0] == map_point("Route204South", "below_door"))){ - fail_step(stream, "Route 204: the walk did not end below the Ravaged Path's door."); - } - } -} - -// 01.08: north through the door. -void into_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ - if (!dpad_through_door_north(stream, context, "Ravaged Path door")){ - fail_step(stream, "Route 204: no dark screen walking north into the Ravaged Path."); - } - context.wait_for(2000ms); -} - -// 01.09: through the Ravaged Path, smashing its rocks. -void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ - // On the traced grid (Maps/RavagedPath.json): from the - // south door up the stairs to the tile west of one of the two rocks - // across the lower corridor, smash it, then east and south to the - // corridor's east end; the way out is a tile west of it, south. - { - // Blocked short of a wall in here, it is a rock the plan counted on - // being gone (r237a: the walker took one for a person and said No). - auto in_the_way = [&](const std::string& who){ - if (!answer_rock_smash_prompt(stream, context, who)){ - talk_or_fight(stream, context, who); - } - }; - RouteTrainers no_trainers{{}, in_the_way}; - std::vector at{map_point("RavagedPath", "entrance")}; - const StoryWorld rocks_in = ravaged_path_maps(); - at = walk_route(stream, context, run_from_wild_battle, rocks_in, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ - const auto& before = map_region("RavagedPath", "before_rocks"); - return std::find(before.begin(), before.end(), pos) != before.end(); - }, "To a rock across the corridor", {}, route_plan_costs()}); - route_turn(stream, context, Direction::EAST); - if (!talk_with_nudges(stream, context, run_from_wild_battle, Direction::EAST, "the rock east of us", 2000ms)){ - fail_step(stream, "Ravaged Path: nothing answered facing the rock east of us."); - } - if (!answer_rock_smash_prompt(stream, context, "the rock east of us")){ - fail_step(stream, "Ravaged Path: no Rock Smash offer facing the rock east of us."); - } - // We may be on either row; the rock east of the other stays, but no - // leg from our row crosses it. - std::vector smashed; - for (const Pokemon::TilePos& pos : map_region("RavagedPath", "before_rocks")){ - smashed.push_back(pos.next(Direction::EAST)); - } - const StoryWorld rocks_out = ravaged_path_maps(smashed); - at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == map_point("RavagedPath", "east_end"); }, "To the corridor's east end", {}, route_plan_costs()}); - dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); - const WalkResult out = walk_dir(stream, context, Direction::SOUTH, 4000ms, "south out of the Ravaged Path"); - if (out.stop != WalkStop::TRANSITION && out.stop != WalkStop::LIMIT){ - fail_step(stream, "Ravaged Path: the walk south from a tile west of the corridor's end did not leave the cave."); - } - context.wait_for(2000ms); - } -} - -// 01.10: Route 204 north on the human's line, into Floaroma. -void route_204_to_floaroma(VideoStream& stream, ProControllerContext& context){ - // The line a human walked (Maps/Route204North.json, - // the region floaroma_waypoints): a tile south out of the cave's mouth, west along - // the row under the cliff past the Youngster who wanders there, up the - // stairs, east along the corridor into Aroma Lady Taylor's battle, north - // through the tall grass, west below the flower beds, and north between - // the beds into Floaroma. Whole tiles on the D-pad, corner to corner. - { - RouteTrainers trainers{route204_north_trainer_sights(), [&](const std::string& who){ - const bool doubles = who.find("Twins") != std::string::npos || who.find("someone") != std::string::npos; - talk_or_fight(stream, context, who, TalkExpect::TALK_OR_BATTLE, doubles ? BattleFormat::DOUBLES : BattleFormat::SINGLES); - }}; - std::vector at{map_point("Route204North", "cave_exit")}; - at = walk_waypoints(stream, context, run_from_wild_battle, route204_north_maps(), at, trainers, WaypointWalkPlan{map_region("Route204North", "floaroma_waypoints"), "To the path into Floaroma", waypoint_plan_costs()}); - if (!std::all_of(at.begin(), at.end(), in_floaroma_path)){ - fail_step(stream, "Route 204: the walk did not end in the path into Floaroma."); - } - // North until the town's banner shows: the checkpoint ends right - // there, where the save goes (r247a walked 20 s on and overshot). - const WalkResult in = walk_until_dpad(stream, context, run_from_wild_battle, {0, 1}, 12000ms, "north into Floaroma", {.arrived = location_banner_showing}); - if (in.stop != WalkStop::ARRIVED){ - fail_step(stream, "Route 204: no location banner walking north into Floaroma."); - } - context.wait_for(500ms); - const VideoSnapshot banner = stream.video().snapshot(); - const std::string read = banner ? read_location_banner(*banner.frame) : ""; - if (!location_banner_matches(read, "Floaroma Town")){ - fail_step(stream, "Route 204: the banner read \"" + read + "\", not Floaroma Town."); - } - stream.log("[01.10] In Floaroma Town; the banner read \"" + read + "\".", COLOR_GREEN); + if (!answer_rock_smash_prompt(stream, context, "the rock east of us")){ + fail_step(stream, "Ravaged Path: no Rock Smash offer facing the rock east of us."); } } @@ -219,11 +104,7 @@ void below_the_old_man(VideoStream& stream, ProControllerContext& context){ void add_route204_events(StoryEvents& events){ - events.emplace("north_out_of_jubilife", console_event(north_out_of_jubilife)); - events.emplace("route_204_south_to_the_door", console_event(route_204_south_to_the_door)); - events.emplace("into_the_ravaged_path", console_event(into_the_ravaged_path)); - events.emplace("through_the_ravaged_path", console_event(through_the_ravaged_path)); - events.emplace("route_204_to_floaroma", console_event(route_204_to_floaroma)); + events.emplace("smash_the_rock", console_event(smash_the_rock)); events.emplace("west_8_north_to_the_old_woman", console_event(west_8_north_to_the_old_woman)); events.emplace("below_the_old_man", console_event(below_the_old_man)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index d3cdd08b85..d0c1982e26 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -65,6 +65,9 @@ std::optional story_route_costs(const std::string& variant){ if (variant == "robust"){ return RobustPlanCosts(); } + if (variant == "line"){ + return waypoint_plan_costs(); + } return std::nullopt; } @@ -91,6 +94,16 @@ void save_failure_screenshot(VideoStream& stream, size_t attempt){ stream.log("Failure screenshot: " + path); } +// A trainer who saw us on a route: Route 204's Twins battle as a pair +// (01.10), everyone else alone. +void fight_story_trainer(VideoStream& stream, ProControllerContext& context, const std::string& trainer){ + if (trainer.find("Twins") != std::string::npos){ + talk_or_fight(stream, context, trainer, TalkExpect::BATTLE, BattleFormat::DOUBLES); + return; + } + fight_route_trainer(stream, context, trainer); +} + } @@ -102,8 +115,7 @@ void StoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ m_checkpoint = data.id; // A row starts from its save: after the shoes' row, the save has them. m_can_run = data.id > std::string(RUNNING_SHOES_ROW); - const StoryWorld* south = story_plan_world("Route202", "Route202"); - m_trainers = south ? story_trainers(*south) : std::vector{}; + m_trainers.clear(); } Outcome StoryExecutor::run(RunState& state, const StoryStep& step){ @@ -152,21 +164,18 @@ Pose StoryExecutor::walk_to(const Pose& from, const Pose& goal, const StoryStep& for (const TilePos& pos : Pokemon::tiles_of(goal)){ targets.push_back(story_to_world(*maps, goal.map, pos)); } - // Only Route 202's trainers are known; the ones fought stay off - // m_trainers until the next attempt. - const bool south = maps == story_plan_world("Route202", "Route202"); + // The ones fought stay off m_trainers until the next attempt. + std::vector& sights = m_trainers.try_emplace(maps, story_trainers(*maps)).first->second; RouteTrainers trainers{ - south ? m_trainers : std::vector{}, - [this](const std::string& trainer){ fight_route_trainer(stream(), context(), trainer); } + sights, + [this](const std::string& trainer){ fight_story_trainer(stream(), context(), trainer); } }; const RouteWalkPlan plan{ [&](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, label_of(step), {}, *costs, m_can_run ? MoveSpeed::RUN : MoveSpeed::WALK }; const std::vector at = walk_route(stream(), context(), run_from_wild_battle, *maps, start, trainers, plan); - if (south){ - m_trainers = std::move(trainers.sights); - } + sights = std::move(trainers.sights); std::vector local; for (const TilePos& pos : at){ const std::optional tile = story_to_local(*maps, goal.map, pos); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h index 88dbb00c09..cae0c9e8a7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -71,7 +71,8 @@ struct StoryEnv{ // The plan costs a ROUTE step's variant names: "" the route costs, -// "town" town_plan_costs(), "robust" the planner's defaults. Nothing for +// "town" town_plan_costs(), "robust" the planner's defaults, "line" +// waypoint_plan_costs() (a stretch of a line a human walked). Nothing for // another name. std::optional story_route_costs(const std::string& variant); @@ -113,7 +114,7 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ virtual Pokemon::Outcome new_game(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; // verify_tutorial_settings(): fast text, Autosave off. virtual Pokemon::Outcome options(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - // Variant: the plan's costs ("" route, "town", "robust"). + // Variant: the plan's costs ("" route, "town", "robust", "line"). virtual Pokemon::Outcome route(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; // Each leg in D-pad taps, turning first. Variant "one_short_ok": a leg // may end a tile short (a door lined up by sight after). Variant @@ -151,9 +152,10 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ std::string m_checkpoint; // Whether the player has the Running Shoes: the pace routes are timed at. bool m_can_run = false; - // Route 202's trainers who can still see us, in story_plan_world()'s - // world tiles; all of them again at each attempt. - std::vector m_trainers; + // The trainers who can still see us on each story_plan_world() world, + // in its tiles, from its first walk this attempt; all of them again at + // each attempt. + std::map> m_trainers; std::map m_failures; std::map m_run_counts; std::set m_stop_rows; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp index 475acd37e4..f8863b4c87 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -16,12 +16,14 @@ namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ +using Pokemon::Direction; using Pokemon::TilePos; namespace{ -// The JSON maps' names and the location banners'. +// The JSON maps' names and the location banners'. Both halves of Route +// 204 show "Route 204"; no world holds both. const std::map& banner_names(){ static const std::map names{ {"TwinleafTown", "Twinleaf Town"}, @@ -30,6 +32,11 @@ const std::map& banner_names(){ {"SandgemTown", MAP_SANDGEM_TOWN}, {"Route202", MAP_ROUTE_202}, {"JubilifeCity", MAP_JUBILIFE_CITY}, + {"Route203", MAP_ROUTE_203}, + {"OreburghCity", MAP_OREBURGH_CITY}, + {"Route204South", "Route 204"}, + {"Route204North", "Route 204"}, + {"FloaromaTown", MAP_FLOAROMA_TOWN}, }; return names; } @@ -42,56 +49,173 @@ const std::string& banner_name(const std::string& map){ return map; } +// `maps` (JSON names) in one world, each with its tile (0,0) at the corner +// of its decomp rectangle (devtools/maps/bdsp_maps.py), so maps that meet +// in the game meet here. +StoryWorld joined(const std::vector>& maps){ + StoryWorld world; + for (const auto& [map, corner] : maps){ + world.add(banner_name(map), story_map(map), corner); + } + return world; +} + // TODO(#114 multi-map view): MapData has no joined view of maps yet. // Twinleaf to Route 202 in one world, at the decomp's world tiles (its // cells are 32 tiles square; Route 201's map starts 7 rows into its cell // and Route 202's a column west of its own). const StoryWorld& south_world(){ + static const StoryWorld world = joined({ + {"TwinleafTown", {96, 864}}, + {"Route201", {96, 839}}, + {"VerityLakefront", {64, 832}}, + {"SandgemTown", {160, 832}}, + {"Route202", {159, 800}}, + }); + return world; +} + +// Jubilife without its east column: Route 203's map starts on that column +// (world x 191, its column 0), and two maps may not both hold open ground. +// Jubilife's tiles keep their coordinates. +Pokemon::MapData jubilife_west_of_route_203(){ + const Pokemon::MapData& city = story_map("JubilifeCity"); + Pokemon::MapData cut; + cut.name = city.name; + cut.source = city.source; + cut.tiles = Pokemon::Grid{city.tiles.width - 1, city.tiles.height, {}}; + for (int y = 0; y < cut.tiles.height; y++){ + for (int x = 0; x < cut.tiles.width; x++){ + cut.tiles.cells.push_back(city.tiles.at({x, y})); + } + } + for (const Pokemon::MapObject& object : city.objects){ + if (cut.tiles.contains({object.x, object.y})){ + cut.objects.push_back(object); + } + } + return cut; +} + +// 01.06: Route 203 west into Jubilife (decomp rectangles (191, 740) and +// (128, 736)): Route 203's rows 17..20 are Jubilife's street, rows 21..24. +const StoryWorld& route_203_world(){ static const StoryWorld world = []{ - StoryWorld world; - world.add(banner_name("TwinleafTown"), story_map("TwinleafTown"), {96, 864}); - world.add(banner_name("Route201"), story_map("Route201"), {96, 839}); - world.add(banner_name("VerityLakefront"), story_map("VerityLakefront"), {64, 832}); - world.add(banner_name("SandgemTown"), story_map("SandgemTown"), {160, 832}); - world.add(banner_name("Route202"), story_map("Route202"), {159, 800}); + StoryWorld world = joined({{"Route203", {191, 740}}}); + world.add(MAP_JUBILIFE_CITY, jubilife_west_of_route_203(), {128, 736}); return world; }(); return world; } -// The interiors of 00.01 to 00.04, each its own world. +// 01.08: Jubilife north onto Route 204's south half (160, 704), whose street +// (12..15, 31) carries on Jubilife's (44..47, 0). +const StoryWorld& route_204_south_world(){ + static const StoryWorld world = joined({{"JubilifeCity", {128, 736}}, {"Route204South", {160, 704}}}); + return world; +} + +// 01.10: Route 204's north half (160, 672) up into Floaroma (160, 640). +const StoryWorld& route_204_north_world(){ + static const StoryWorld world = joined({{"Route204North", {160, 672}}, {"FloaromaTown", {160, 640}}}); + return world; +} + +// The Ravaged Path with the rock 01.09 smashes, (22, 43), open: no route +// before the smash can use it, since it only leads east, where the rocks +// shut everything else off. +Pokemon::MapData ravaged_path_smashed(){ + Pokemon::MapData map = story_map("RavagedPath"); + Pokemon::cell_at(map.tiles, map_point("RavagedPath", "rocks.13")) = Pokemon::TileKind::OPEN; + return map; +} + +// The interiors (houses, the lab, the Gym, the Gate, the Ravaged Path), +// each its own world, named by its JSON map. const std::map& room_worlds(){ static const std::map rooms = []{ std::map ret; - for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab"}){ + for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab", "OreburghGym", "OreburghGate"}){ ret[room].add(room, story_map(room), {0, 0}); } + ret["RavagedPath"].add("RavagedPath", ravaged_path_smashed(), {0, 0}); return ret; }(); return rooms; } -bool on_world(const StoryWorld& world, const std::string& map){ - if (&world == &south_world()){ - static const std::set south{"TwinleafTown", "Route201", "VerityLakefront", "SandgemTown", "Route202"}; - return south.contains(map); +// A world and the JSON maps it holds. +struct PlanWorld{ + const StoryWorld* world; + std::set maps; +}; + +// In order: the first that holds both ends of a walk plans it. Jubilife +// alone comes before the worlds that join it to a route, so 00.05's walks +// plan on the city alone. +const std::vector& plan_worlds(){ + static const std::vector worlds = []{ + std::vector ret{ + {&south_world(), {"TwinleafTown", "Route201", "VerityLakefront", "SandgemTown", "Route202"}}, + {&jubilife_maps(), {"JubilifeCity"}}, + {&oreburgh_city_maps(), {"OreburghCity"}}, + {&route_203_world(), {"Route203", "JubilifeCity"}}, + {&route_204_south_world(), {"JubilifeCity", "Route204South"}}, + {&route_204_north_world(), {"Route204North", "FloaromaTown"}}, + }; + for (const auto& [name, world] : room_worlds()){ + ret.push_back({&world, {name}}); + } + return ret; + }(); + return worlds; +} + +// Route 203's trainers, from the Diamond decomp's events (zone event +// narc_0329): where each stands on the JSON map, the way they look and +// how far. The one by the Gate wanders in BDSP and is off the map. +std::vector route_203_trainer_sights(){ + const auto at = [](const char* name){ + for (const Pokemon::MapObject& object : story_map("Route203").objects){ + if (object.name == name){ + return TilePos{object.x, object.y}; + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::string("Route 203 has no ") + name + "."); + }; + return { + {.name = "Youngster (west)", .pos = at("youngster_1"), .facing = Direction::WEST, .range = 2}, + {.name = "Lass (above row 18)", .pos = at("lass_2"), .facing = Direction::WEST, .range = 5}, + {.name = "Youngster (above row 18)", .pos = at("youngster_3"), .facing = Direction::SOUTH, .range = 4}, + {.name = "Lass (by the Gate)", .pos = at("lass_4"), .facing = Direction::SOUTH, .range = 5}, + }; +} + +// The trainers of the JSON map `map`, in its own tiles. +std::vector map_trainers(const std::string& map){ + if (map == "Route202"){ + return route202_trainer_sights(); + } + if (map == "Route203"){ + return route_203_trainer_sights(); + } + if (map == "Route204South"){ + return route204_south_trainer_sights(); } - return false; + if (map == "Route204North"){ + return route204_north_trainer_sights(); + } + return {}; } } const StoryWorld* story_plan_world(const std::string& from, const std::string& to){ - if (on_world(south_world(), from) && on_world(south_world(), to)){ - return &south_world(); - } - if (from == "JubilifeCity" && to == from){ - return &jubilife_maps(); - } - const auto& rooms = room_worlds(); - if (auto iter = rooms.find(from); iter != rooms.end() && to == from){ - return &iter->second; + for (const PlanWorld& world : plan_worlds()){ + if (world.maps.contains(from) && world.maps.contains(to)){ + return world.world; + } } return nullptr; } @@ -109,12 +233,17 @@ std::optional story_to_local(const StoryWorld& world, const std::string } std::vector story_trainers(const StoryWorld& world){ - if (!on_world(world, "Route202")){ - return {}; - } - std::vector sights = route202_trainer_sights(); - for (TrainerSight& sight : sights){ - sight.pos = story_to_world(world, "Route202", sight.pos); + std::vector sights; + for (const PlanWorld& plan : plan_worlds()){ + if (plan.world != &world){ + continue; + } + for (const std::string& map : plan.maps){ + for (TrainerSight sight : map_trainers(map)){ + sight.pos = story_to_world(world, map, sight.pos); + sights.push_back(std::move(sight)); + } + } } return sights; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h index 171c3db300..5a4dc7d8ab 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h @@ -3,9 +3,11 @@ * From: https://github.com/PokemonAutomation/ * * Where the story's steps walk: poses are tiles of the JSON maps - * ("Route202"), named by map_point() and map_region(). A walk from one map to another is planned on a story world that holds both: the - * outdoors from Twinleaf to Route 202, Jubilife, and each interior of - * 00.01 to 00.04 on its own. + * ("Route202"), named by map_point() and map_region(). A walk from one map + * to another is planned on a story world that holds both: the outdoors + * from Twinleaf to Route 202, Jubilife, Oreburgh, Route 203 into Jubilife, + * Jubilife up Route 204's south half, Route 204's north half into + * Floaroma, and each interior on its own. * */ @@ -34,7 +36,7 @@ Pokemon::TilePos story_to_world(const StoryWorld& world, const std::string& map, // The world tile `pos` as a tile of `map`, or nothing if it is not on `map`. std::optional story_to_local(const StoryWorld& world, const std::string& map, const Pokemon::TilePos& pos); -// The trainers a walk on `world` may meet (Route 202's), in its tiles. +// The trainers a walk on `world` may meet (Routes 202 to 204's), in its tiles. std::vector story_trainers(const StoryWorld& world); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 5c80713fba..a5361459cb 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -11,10 +11,13 @@ * on the console (r72 to r261, 2026-10-30); 00.05 to 00.05c, measured there * first, are now planned on Jubilife's map with the people and triggers of * the Diamond decomp's zone events and scripts. Segment 01 ran on the - * console end to end (2026-10-31), its walks kept as events. + * console end to end (2026-10-31), its walks as events; 01.05 to 01.10 + * are now planned on the maps the same way (2026-11-02), and have not + * run on a console as plans yet. * */ +#include #include #include #include @@ -53,6 +56,14 @@ const char* const ROUTE_202 = "Route202"; const char* const JUBILIFE = "JubilifeCity"; // No map of it: a pose there names the room, not its tiles. const char* const TRAINERS_SCHOOL = "TrainersSchool"; +const char* const OREBURGH = "OreburghCity"; +const char* const GYM = "OreburghGym"; +const char* const GATE = "OreburghGate"; +const char* const ROUTE_203 = "Route203"; +const char* const ROUTE_204_SOUTH = "Route204South"; +const char* const RAVAGED_PATH = "RavagedPath"; +const char* const ROUTE_204_NORTH = "Route204North"; +const char* const FLOAROMA = "FloaromaTown"; Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ return Pokemon::tile_pose(map, {TilePos{x, y}}, facing); @@ -485,6 +496,92 @@ std::vector jubilife(){ return {the_town_map(), the_coupons(), the_poketch()}; } +// Where the Oreburgh Gate lets us out onto Route 203: its warp tile (55, 9) +// (Diamond's warp (246, 749), zone event narc_0329), walking west. +Pose out_of_oreburgh_gate(){ + return at(ROUTE_203, 55, 9, WEST); +} + +// 01.05, planned on Oreburgh's and the Gate's maps with the Diamond decomp's +// zone events (narc_0044, narc_0250) and Platinum's script for the scene +// (scripts_oreburgh_city.s, the same trigger and variable). Barry's coord +// event, column 6 on rows 12 to 15 while VAR_OREBURGH_CITY_STATE is 2 (after +// Roark: the old walk met his "*thud*" there, 2026-10-29), runs him into us +// from the east: the game pushes us a tile west, facing east, and he +// leaves. The Gate's way west is row 22, across the Hiker's trigger (7, 22), +// spent in 01.02. +StoryCheckpoint oreburgh_to_route_203(){ + return row("01.05", "Oreburgh to Route 203", next_to(GYM, "roark", SOUTH, NORTH), { + // 01.04 talks to Roark from below him, up the clear middle column. + step_into("out_of_the_gym", SOUTH, "down the middle column (5, 4..23) and out of the door (5, 24)", + at(OREBURGH, "gym_doorstep", SOUTH)), + route("east_of_barrys_trigger", at(OREBURGH, 7, 13, WEST), + "west along the street, north up the stairs west of the houses, west along the Gate's row", "town"), + step_into("barrys_trigger", WEST, "the trigger (6, 12..15)"), + scene("barry_bumps_into_us", at(OREBURGH, 5, 13, EAST), "Barry runs into us, talks of Eterna, and leaves (assumed: the push)"), + route("by_the_gate", next_to(OREBURGH, "gate_mouth", EAST, WEST), "west to the Gate's warp tile (2, 13)"), + // The warp's other end is the Gate's (27, 22), beside its east mouth. + step_into("into_oreburgh_gate", WEST, "the Gate's mouth (1, 13)", at(GATE, 27, 22, WEST)), + route("through_the_gate", at(GATE, 4, 22, WEST), "west along row 22 to the warp tile by the west mouth (3, 22)"), + step_into("out_of_oreburgh_gate", WEST, "the west mouth (3, 22)", out_of_oreburgh_gate()), + }); +} + +// Below Rowan's trigger in Jubilife: (47, 8), the east tile under the +// trigger (45..47, 7), which (48, 7) beside it is not. +Pose below_rowans_trigger(){ + return at(JUBILIFE, 47, 8, NORTH); +} +// Where Rowan's scene leaves us: walked up to him at (47, 5), then a tile +// west in front of the grunt (Platinum's movements; assumed for BDSP). +Pose after_the_grunts(){ + return Pokemon::tile_pose(JUBILIFE, {TilePos{46, 5}, TilePos{47, 5}}); +} + +// 01.07, from the Diamond decomp (zone event narc_0002) and Platinum's +// script (scripts_jubilife_city.s, the same trigger and variable): the +// coord event (45..47, 7) while VAR_JUBILIFE_CITY_STATE is 3 walks us up to +// Rowan, his and the grunts' lines, then the tag battle with Dawn; after +// it they leave. +StoryCheckpoint rowan_and_the_grunts(){ + return row("01.07", "Jubilife: Rowan, Dawn and the Galactic grunts", below_rowans_trigger(), { + step_into("rowans_trigger", NORTH, "the trigger (45..47, 7): the game walks us up to Rowan"), + battle("galactic_grunts", false, "the lines, then the tag battle with Dawn", "doubles"), + scene("rowan_and_dawn_leave", after_the_grunts(), "Rowan's and Dawn's lines; they walk off"), + }); +} + +// 01.09 on the Ravaged Path's map (Platinum's, checked on the console in +// r235b): up the stairs to west of the rock (22, 43), smash it, east to +// above the way out (28, 45). Its other end is Route 204's (20, 26). +StoryCheckpoint through_the_ravaged_path(){ + const Pose west_of_the_rock = at(RAVAGED_PATH, "before_rocks.2", EAST); + return row("01.09", "Ravaged Path", at(RAVAGED_PATH, "entrance", NORTH), { + route("west_of_the_rock", west_of_the_rock, "up the stairs and along the corridor"), + event("smash_the_rock", "Rock Smash on the rock east of us", west_of_the_rock), + route("above_the_way_out", at(RAVAGED_PATH, "above_exit", SOUTH), "east through the rock's tile"), + step_into("out_of_the_ravaged_path", SOUTH, "the way out (28, 45)", at(ROUTE_204_NORTH, "cave_exit", SOUTH)), + }); +} + +// 01.10: the line a human walked up Route 204's north half (2026-10-31, +// r246a to r251a), corner to corner (Test_Route204NorthLine checks each +// stretch is one straight leg), then north between the flower beds into +// Floaroma. Its trainers who see us are fought: Taylor on the line, the +// Twins as a pair. +StoryCheckpoint route_204_to_floaroma(){ + std::vector steps; + const std::vector line = map_region(ROUTE_204_NORTH, "floaroma_waypoints"); + for (size_t i = 0; i < line.size(); i++){ + steps.push_back(route(std::format("line_{}", i + 1), Pokemon::tile_pose(ROUTE_204_NORTH, {line[i]}), + "a corner of the human's line", "line")); + } + // 01.11 walks on from here: west 8 along row 29, north to the old woman. + steps.push_back(route("into_floaroma", at(FLOAROMA, "route_204_arrival", NORTH), "north up column 11 into Floaroma", "line")); + steps.push_back(save()); + return row("01.10", "Route 204 to Floaroma", at(ROUTE_204_NORTH, "cave_exit", SOUTH), std::move(steps)); +} + std::vector to_oreburgh(){ return { row("01.01", "Jubilife to Barry", std::nullopt, { @@ -510,38 +607,30 @@ std::vector to_oreburgh(){ event("out_of_the_center", "out from the counter"), event("gym_until_roark_beaten", "west into the gym, up it to Roark; again from the Center after a loss"), }), - row("01.05", "Oreburgh to Route 203", std::nullopt, { - event("out_of_the_gym", "out of the gym onto its doorstep"), - event("into_oreburgh_gate", "up to the platform and west into the Gate"), - event("out_of_oreburgh_gate", "west until Route 203's banner shows"), - }), + oreburgh_to_route_203(), }; } std::vector to_floaroma(){ return { - row("01.06", "Route 203 to Jubilife", std::nullopt, { - event("route_203_to_row_18", "on Route 203's map to row 18, below the girl"), - event("row_18_into_jubilife", "west along row 18, counting tiles"), - }), - row("01.07", "Jubilife through Rowan's scene", std::nullopt, { - event("west_to_the_crossroads", "west along the long road"), - event("north_to_rowan", "north up the street until Rowan's scene"), - battle("galactic_grunts", false, "the tag battle with Dawn", "doubles"), - event("rowan_after_the_battle", "Rowan's and Dawn's lines"), - }), - row("01.08", "Jubilife to the Ravaged Path", std::nullopt, { - event("north_out_of_jubilife", "north until Route 204's banner"), - event("route_204_south_to_the_door", "on Route 204 South's map to below the Ravaged Path's door"), - event("into_the_ravaged_path", "north through the door"), + row("01.06", "Route 203 to Jubilife", out_of_oreburgh_gate(), { + // Trainers who saw us going east in 01.02 are beaten; any other + // sees us here and is fought (route_203_trainer_sights()). + route("below_rowans_trigger", below_rowans_trigger(), + "down Route 203 (its ledges only south), west along it into Jubilife's street, north up the road", "town"), }), - row("01.09", "Ravaged Path", std::nullopt, { - event("through_the_ravaged_path", "through the cave with Rock Smash, out onto Route 204 North"), - }), - row("01.10", "Route 204 to Floaroma", std::nullopt, { - event("route_204_to_floaroma", "the human's line north into Floaroma"), - save(), + rowan_and_the_grunts(), + row("01.08", "Jubilife to the Ravaged Path", after_the_grunts(), { + // Lass Sarah's east sight crosses the street (r233a, r234a): she + // may stop us for a battle. + route("below_the_ravaged_path", at(ROUTE_204_SOUTH, "below_door", NORTH), + "north out of Jubilife, up Route 204's street and grass to below the door (11, 1)", "town"), + // Its warp (Diamond's zone event narc_0330) lands us on the + // cave's (19, 50), as in r237a. + step_into("into_the_ravaged_path", NORTH, "the door (11, 1)", at(RAVAGED_PATH, "entrance", NORTH)), }), + through_the_ravaged_path(), + route_204_to_floaroma(), row("01.11", "Floaroma's old couple", std::nullopt, { event("west_8_north_to_the_old_woman", "west 8, north until she stops us"), talk("old_woman", "she gives Mew when a Let's Go save is on the console"), diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 8e8bd57129..45f4341f79 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -37,7 +37,7 @@ using Pokemon::TilePos; // The variants each kind does itself, besides the story's events. const std::map>& built_in_variants(){ static const std::map> variants{ - {StoryStepKind::ROUTE, {"", "town", "robust"}}, + {StoryStepKind::ROUTE, {"", "town", "robust", "line"}}, {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "to_dialogue", "to_map"}}, {StoryStepKind::MASH, {"", "a"}}, {StoryStepKind::BATTLE, {"", "doubles"}}, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp index 900e7aec84..4300a33e77 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp @@ -12,7 +12,6 @@ #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Talks.h" #include "PokemonBDSP_AutoStory_WildBattle.h" @@ -194,31 +193,6 @@ bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, return false; } -// Out of a building whose door is straight south of us, a D-pad tile at a -// time, until the location banner shows: it names the map we step into -// (e.g. "Oreburgh City") for a few seconds, and never shows inside. Ends in -// the doorway, facing south. Out of the Oreburgh Gym from in front of Roark -// (by hand, 2026-10-31): 18 taps inside, 2 taps of black fade, the banner on -// the 21st. The door's fade alone was missed between steps (r219g walked on -// south to the rocks below the gym). -bool dpad_out_of_building(VideoStream& stream, ProControllerContext& context, size_t max_taps, const std::string& label){ - pbf_press_dpad(context, DPAD_DOWN, 160ms, 700ms); // turn - for (size_t tap = 0; tap < max_taps; tap++){ - pbf_press_dpad(context, DPAD_DOWN, 160ms, 900ms); - context.wait_for_all_requests(); - const VideoSnapshot frame = stream.video().snapshot(); - if (frame && location_banner_showing(*frame.frame)){ - stream.log("[Walk] " + label + ": the location banner shows after " + std::to_string(tap + 1) + " taps; outside."); - context.wait_for(1000ms); - return true; - } - if (dialogue_opens(stream, context, 100ms)){ - finish_dialogue(stream, context, BUTTON_B, label + ": someone in the way"); - } - } - return false; -} - // For a player inside a Pokemon Center after a whiteout, at the counter: // out through the door. The door is straight south of the nurse in most // Centers; in Oreburgh's it was two tiles east of where we stood (by hand). @@ -267,54 +241,6 @@ void walk_out_of_center(VideoStream& stream, ProControllerContext& context, cons fail_step(stream, label + ": could not find the Pokemon Center's door."); } -// West until a transition, by the user's rule for a wall in the way: walk -// north to the top wall, which pins the row, then try the rows south of it -// one at a time. Trainers who stop us are fought. Returns at the first -// transition (a building, a cave, a gate), or when `arrived` matches. -void west_until_transition( - VideoStream& stream, ProControllerContext& context, const std::string& label, size_t max_legs, - const std::function& arrived -){ - for (size_t leg = 0; leg < max_legs; leg++){ - const WalkResult west = walk_dir(stream, context, Direction::WEST, 20000ms, label, arrived); - if (west.stop == WalkStop::ARRIVED){ - return; - } - if (west.stop == WalkStop::TRANSITION){ - context.wait_for(2000ms); - return; - } - if (west.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, label + ": someone in the way"); - continue; - } - if (west.stop != WalkStop::BLOCKED){ - continue; - } - walk_dir(stream, context, Direction::NORTH, 4000ms, label + ": north to the top wall"); - bool moved = false; - for (size_t row = 0; row < 10 && !moved; row++){ - hold_tiles(stream, context, Direction::SOUTH, 1); - const WalkResult probe = walk_dir(stream, context, Direction::WEST, 1500ms, label + ": west on the next row", arrived); - if (probe.stop == WalkStop::ARRIVED){ - return; - } - moved = probe.stop != WalkStop::BLOCKED || probe.walked > 1200ms; - if (probe.stop == WalkStop::TRANSITION){ - context.wait_for(2000ms); - return; - } - if (probe.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, label + ": someone in the way"); - } - } - if (!moved){ - fail_step(stream, label + ": no row below the top wall leads west."); - } - } - fail_step(stream, label + ": no transition after " + std::to_string(max_legs) + " legs."); -} - } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h index 07b9174beb..5672d71d00 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h @@ -67,26 +67,10 @@ void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition // Through a door straight north of us: tap up to face it, hold up into it, // and wait for its dark fade (by hand: brightness 7, about 1 s after). bool dpad_through_door_north(VideoStream& stream, ProControllerContext& context, const std::string& label); -// Out of a building whose door is straight south of us, a D-pad tile at a -// time, until the location banner shows: it names the map we step into -// (e.g. "Oreburgh City") for a few seconds, and never shows inside. Ends in -// the doorway, facing south. Out of the Oreburgh Gym from in front of Roark -// (by hand, 2026-10-31): 18 taps inside, 2 taps of black fade, the banner on -// the 21st. The door's fade alone was missed between steps (r219g walked on -// south to the rocks below the gym). -bool dpad_out_of_building(VideoStream& stream, ProControllerContext& context, size_t max_taps, const std::string& label); // For a player inside a Pokemon Center after a whiteout, at the counter: // out through the door. The door is straight south of the nurse in most // Centers; in Oreburgh's it was two tiles east of where we stood (by hand). void walk_out_of_center(VideoStream& stream, ProControllerContext& context, const std::string& label); -// West until a transition, by the user's rule for a wall in the way: walk -// north to the top wall, which pins the row, then try the rows south of it -// one at a time. Trainers who stop us are fought. Returns at the first -// transition (a building, a cave, a gate), or when `arrived` matches. -void west_until_transition( - VideoStream& stream, ProControllerContext& context, const std::string& label, size_t max_legs, - const std::function& arrived = nullptr -); From aa4855a0886aaf453fa81412e296bca43b9604be Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:00:45 -0800 Subject: [PATCH 70/98] BDSP AutoStory: no loss check when the battle starts on Center-like floor Jubilife's plaza reads as Pokemon Center floor, so r79 won Rowan's tag battle and was called lost. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index d3cdd08b85..1b36c7f0bc 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -419,10 +419,17 @@ Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ } Outcome StoryExecutor::battle(RunState&, const StoryStep& step){ + // A loss sends us to a Pokemon Center, so ending on its floor means a + // loss - unless the floor check already passed where the battle began: + // Jubilife's yellow plaza reads as Center floor (63 %, r79 won Rowan's + // tag battle there and was called lost). + const bool center_before = !step.may_lose && in_pokemon_center(stream()); if (!talk_or_fight(stream(), context(), label_of(step), TalkExpect::BATTLE, step.variant == "doubles" ? BattleFormat::DOUBLES : BattleFormat::SINGLES)){ fail_step(stream(), label_of(step) + ": talked but did not battle."); } - if (!step.may_lose && in_pokemon_center(stream())){ + if (center_before){ + stream().log(label_of(step) + ": the floor read as a Center's before the battle too; no loss check here.", COLOR_ORANGE); + }else if (!step.may_lose && in_pokemon_center(stream())){ fail_step(stream(), label_of(step) + ": lost the battle."); } return Pokemon::outcome_ok(); From 22a8bc75d7d2a84fc696d41492964cd1bf8248fb Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:03:58 -0800 Subject: [PATCH 71/98] BDSP AutoStory: tap through 01.05's doors to their banners r75's walk west out of the Oreburgh Gate missed the fade and went on into a pocket by Route 203's pond. The Gym's door and the Gate's and Ravaged Path's mouths are now D-pad taps, each looked at, that stop at the fade and fail on no banner of the map beyond. The Gate's exit is its one warp tile, which 01.06 plans from. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 66 ++++++++++++++++++- .../PokemonBDSP_AutoStory_StoryExecutor.h | 3 +- .../PokemonBDSP_AutoStory_StoryTable.cpp | 39 +++++++++-- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 1 + 4 files changed, 99 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index d0c1982e26..af9efebdc4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -352,11 +352,15 @@ namespace{ // stops it: holding on under Mom's "Oh, Kosmos!" wasted 12 s (r45). The walker's black-screen watch // missed the lab's fade into Sandgem (r44, "dialog"), and settle's samples // 1.3 s apart can fall either side of it, so watch at settle's own darkness. -bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction, bool run){ - const DpadPosition dpad = +DpadPosition dpad_toward(Direction direction){ + return direction == Direction::NORTH ? DPAD_UP : direction == Direction::SOUTH ? DPAD_DOWN : direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; +} + +bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction, bool run){ + const DpadPosition dpad = dpad_toward(direction); BlackScreenWatcher dark(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 3 * FADE_DARKNESS, 60); ShortDialogWatcher dialog(COLOR_YELLOW); const int ret = run_until(stream, context, [&](ProControllerContext& context){ @@ -368,10 +372,68 @@ bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction return ret == 0; } +// What the screen showed after a tap toward a door or a cave mouth. +enum class TapLook{ + NOTHING, + DARK, + BANNER, +}; + +// Looks for `map`'s location banner (and, if `dark_too`, the fade) for up +// to `wait`. +TapLook look_after_tap(VideoStream& stream, ProControllerContext& context, const std::string& map, Milliseconds wait, bool dark_too){ + const WallClock deadline = current_time() + wait; + while (true){ + context.wait_for_all_requests(); + const VideoSnapshot screen = stream.video().snapshot(); + if (screen && location_banner_matches(read_location_banner(*screen.frame), map)){ + return TapLook::BANNER; + } + if (dark_too && screen && image_average(*screen.frame).sum() / 3 < FADE_DARKNESS){ + return TapLook::DARK; + } + if (current_time() >= deadline){ + return TapLook::NOTHING; + } + context.wait_for(100ms); + } +} + +// A STEP "to_banner": D-pad taps one at a time toward a door or cave mouth, +// each followed by a look, until the fade (then no more taps) and +// pose_after's map's location banner. A held walk can carry on past a +// fade that goes unseen: out of the Gym it walked on south (r219g), and +// through the Oreburgh Gate's west mouth on west into a pocket by Route +// 203's pond (r75). Taps during the fade are eaten. No banner fails the +// step right there. +Outcome tap_to_banner(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ + const std::string map = step.pose_after ? step.pose_after->map : ""; + for (size_t tap = 1; tap <= 30; tap++){ + pbf_press_dpad(context, dpad_toward(step.direction), 160ms, 0ms); + TapLook look = look_after_tap(stream, context, map, 1500ms, true); + const bool faded = look == TapLook::DARK; + if (faded){ + look = look_after_tap(stream, context, map, 8000ms, false); + } + if (look == TapLook::BANNER){ + stream.log(std::format("[{}] {}: {}'s banner after {} taps {}.", checkpoint, step.name, map, tap, direction_name(step.direction))); + context.wait_for(1000ms); + return Pokemon::outcome_ok(); + } + if (faded){ + break; + } + } + return Pokemon::outcome_failed("no_banner", map); +} + } Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ route_turn(stream(), context(), step.direction); + if (step.variant == "to_banner"){ + return tap_to_banner(stream(), context(), step, m_checkpoint); + } // A step through a door: walk (or run) on until the screen goes dark, // from next to the door or from anywhere in its line (r43: tapping 8 tiles // down the lab took 7 s), then settle for any scene on arrival (Mom's, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h index cae0c9e8a7..319fea22cb 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -123,7 +123,8 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ // unless the step gives one. virtual Pokemon::Outcome legs(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; // One D-pad step `direction`, turning first: onto a scene's trigger or - // into a door. + // into a door. Variant "to_banner": one tap into a cave mouth, then + // wait for pose_after's map's location banner. virtual Pokemon::Outcome step(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; // A at what we face; a dialogue must open. virtual Pokemon::Outcome talk(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index a5361459cb..fc0bcb1359 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -143,6 +143,16 @@ StoryStep step_into(std::string name, Direction direction, std::string why, std: step.pose_after = std::move(pose_after); return step; } +// Through a door or a cave mouth `direction` a D-pad tap at a time, each +// looked at, ending on the far map's location banner (the executor's +// "to_banner"): a hold can carry on past a fade it missed (r75, r219g). +// A tap after an unseen fade may step once more on the far side, so +// `pose_after` allows for it. +StoryStep tap_into(std::string name, Direction direction, std::string why, Pose pose_after){ + StoryStep step = step_into(std::move(name), direction, std::move(why), std::move(pose_after)); + step.variant = "to_banner"; + return step; +} StoryStep talk(std::string name, std::string why){ return make(StoryStepKind::TALK, std::move(name), std::move(why)); } @@ -496,11 +506,24 @@ std::vector jubilife(){ return {the_town_map(), the_coupons(), the_poketch()}; } +// `tile` of `map`, or the tile past it `direction`: where a tap_into() may +// leave us. +Pose arrival(const char* map, TilePos tile, Direction direction){ + return Pokemon::tile_pose(map, {tile, tile.next(direction)}, direction); +} // Where the Oreburgh Gate lets us out onto Route 203: its warp tile (55, 9) -// (Diamond's warp (246, 749), zone event narc_0329), walking west. +// (Diamond's warp (246, 749), zone event narc_0329), walking west. The one +// tile only: from it and (54, 9) together the joined Route 203 and Jubilife +// find no plan. The tap stops at the fade, which puts us on the warp tile; +// r78 passed 01.06 from a save there (by the sign, the cave mouth east). Pose out_of_oreburgh_gate(){ return at(ROUTE_203, 55, 9, WEST); } +// Where Route 204's door (11, 1) lets us into the Ravaged Path: (19, 50), +// as in r237a. +Pose into_the_ravaged_path(){ + return arrival(RAVAGED_PATH, map_point(RAVAGED_PATH, "entrance"), NORTH); +} // 01.05, planned on Oreburgh's and the Gate's maps with the Diamond decomp's // zone events (narc_0044, narc_0250) and Platinum's script for the scene @@ -513,17 +536,19 @@ Pose out_of_oreburgh_gate(){ StoryCheckpoint oreburgh_to_route_203(){ return row("01.05", "Oreburgh to Route 203", next_to(GYM, "roark", SOUTH, NORTH), { // 01.04 talks to Roark from below him, up the clear middle column. - step_into("out_of_the_gym", SOUTH, "down the middle column (5, 4..23) and out of the door (5, 24)", - at(OREBURGH, "gym_doorstep", SOUTH)), + tap_into("out_of_the_gym", SOUTH, "down the middle column (5, 4..23) and out of the door (5, 24)", + arrival(OREBURGH, map_point(OREBURGH, "gym_doorstep"), SOUTH)), route("east_of_barrys_trigger", at(OREBURGH, 7, 13, WEST), "west along the street, north up the stairs west of the houses, west along the Gate's row", "town"), step_into("barrys_trigger", WEST, "the trigger (6, 12..15)"), scene("barry_bumps_into_us", at(OREBURGH, 5, 13, EAST), "Barry runs into us, talks of Eterna, and leaves (assumed: the push)"), route("by_the_gate", next_to(OREBURGH, "gate_mouth", EAST, WEST), "west to the Gate's warp tile (2, 13)"), // The warp's other end is the Gate's (27, 22), beside its east mouth. - step_into("into_oreburgh_gate", WEST, "the Gate's mouth (1, 13)", at(GATE, 27, 22, WEST)), + tap_into("into_oreburgh_gate", WEST, "the Gate's mouth (1, 13)", arrival(GATE, {27, 22}, WEST)), route("through_the_gate", at(GATE, 4, 22, WEST), "west along row 22 to the warp tile by the west mouth (3, 22)"), - step_into("out_of_oreburgh_gate", WEST, "the west mouth (3, 22)", out_of_oreburgh_gate()), + // Route 203's banner or a failure here: the old walk west "until the + // banner" went on into the pond's pocket (r75). 01.06 heads south. + tap_into("out_of_oreburgh_gate", WEST, "the west mouth (3, 22)", out_of_oreburgh_gate()), }); } @@ -556,7 +581,7 @@ StoryCheckpoint rowan_and_the_grunts(){ // above the way out (28, 45). Its other end is Route 204's (20, 26). StoryCheckpoint through_the_ravaged_path(){ const Pose west_of_the_rock = at(RAVAGED_PATH, "before_rocks.2", EAST); - return row("01.09", "Ravaged Path", at(RAVAGED_PATH, "entrance", NORTH), { + return row("01.09", "Ravaged Path", into_the_ravaged_path(), { route("west_of_the_rock", west_of_the_rock, "up the stairs and along the corridor"), event("smash_the_rock", "Rock Smash on the rock east of us", west_of_the_rock), route("above_the_way_out", at(RAVAGED_PATH, "above_exit", SOUTH), "east through the rock's tile"), @@ -627,7 +652,7 @@ std::vector to_floaroma(){ "north out of Jubilife, up Route 204's street and grass to below the door (11, 1)", "town"), // Its warp (Diamond's zone event narc_0330) lands us on the // cave's (19, 50), as in r237a. - step_into("into_the_ravaged_path", NORTH, "the door (11, 1)", at(RAVAGED_PATH, "entrance", NORTH)), + tap_into("into_the_ravaged_path", NORTH, "the door (11, 1)", into_the_ravaged_path()), }), through_the_ravaged_path(), route_204_to_floaroma(), diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 45f4341f79..807dc4d536 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -39,6 +39,7 @@ const std::map>& built_in_variants(){ static const std::map> variants{ {StoryStepKind::ROUTE, {"", "town", "robust", "line"}}, {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "to_dialogue", "to_map"}}, + {StoryStepKind::STEP, {"", "to_banner"}}, {StoryStepKind::MASH, {"", "a"}}, {StoryStepKind::BATTLE, {"", "doubles"}}, }; From 87f4296ef4c18fbaec41a7eea45cd3ace7ac6822 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:10:25 -0800 Subject: [PATCH 72/98] BDSP AutoStory: dpad_tiles turns with a short tap The 160 ms turn tap was a whole step, so every dpad_tiles call walked one tile too many (r80 went two tiles west in the Ravaged Path, not one). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Walks.cpp | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp index 900e7aec84..353d28c98f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp @@ -139,18 +139,32 @@ WalkResult walk_dir( // `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the // player moves a whole tile per tap (measured above), so counted legs do -// not drift the way stick holds do. The first tap toward a new way only -// turns the player, so turn first. +// not drift the way stick holds do. A short tap turns us first. void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label){ stream.log("[Walk] " + label + ": " + std::to_string(taps) + " D-pad taps."); - pbf_press_dpad(context, dpad, 160ms, 700ms); // turn + // Turn with a short tap: a 160 ms tap is a whole step even from another + // facing, so the old 160 ms "turn" walked one tile too many on every + // call (r80: two tiles west in the Ravaged Path, not one; the gym door's + // counts of 21 and 22 landing 1 and 2 tiles west). A short tap the way we + // already face can still step, so count it when the view moved. + size_t moved = 0; + { + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + pbf_press_dpad(context, dpad, 50ms, 500ms); + context.wait_for_all_requests(); + const VideoSnapshot after = stream.video().snapshot(); + if (before && after && walk_screen_change(*before.frame, *after.frame) > 30){ + stream.log("[Walk] " + label + ": the turn tap stepped; counting it."); + moved = 1; + } + } // Count only taps that moved the view: one after the turn was lost and // the leg ended a tile short (r215d). A tile's scroll changes the view by // about 100; standing still, under 15. A tap that does not move is someone // in the way: people walk about these streets (r220g: blocked on tap 9 // for 12 s while a passer-by stood in our row). Wait for them to move on, // up to 30 s, before giving up. - size_t moved = 0; const auto deadline = current_time() + std::chrono::milliseconds(taps * 1000 + 30000); while (moved < taps && current_time() < deadline){ context.wait_for_all_requests(); From bd37d0cce0bcf7cd3766f5d6bfa7a3dfd12efba3 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:11:14 -0800 Subject: [PATCH 73/98] BDSP AutoStory: 01.08 starts from the wall west of Rowan's scene From both tiles Rowan's scene may leave us, the joined Jubilife and Route 204 found no plan. A walk west along row 5 to its wall puts us on (44, 5). The Ravaged Path's arrival is its one tile, as the Gate's. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryTable.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index fc0bcb1359..5a64d37ecd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -135,6 +135,12 @@ StoryStep legs(std::string name, std::vector legs, std::string why, st StoryStep to_walls(std::string name, std::vector legs, std::string why){ return PokemonBDSP::legs(std::move(name), std::move(legs), std::move(why), "to_wall"); } +// `step`, after which the player stands on `pose`: a walk to a wall from +// a few tiles ends on one. +StoryStep anchored(StoryStep step, Pose pose){ + step.pose_after = std::move(pose); + return step; +} // TODO(#114 coord events): the map schema has no scene triggers yet, so // each row steps onto the decomp's trigger tile, named in its reason. StoryStep step_into(std::string name, Direction direction, std::string why, std::optional pose_after = std::nullopt){ @@ -520,9 +526,9 @@ Pose out_of_oreburgh_gate(){ return at(ROUTE_203, 55, 9, WEST); } // Where Route 204's door (11, 1) lets us into the Ravaged Path: (19, 50), -// as in r237a. +// as in r237a. The tap stops at the fade, on that tile. Pose into_the_ravaged_path(){ - return arrival(RAVAGED_PATH, map_point(RAVAGED_PATH, "entrance"), NORTH); + return at(RAVAGED_PATH, "entrance", NORTH); } // 01.05, planned on Oreburgh's and the Gate's maps with the Diamond decomp's @@ -646,6 +652,11 @@ std::vector to_floaroma(){ }), rowan_and_the_grunts(), row("01.08", "Jubilife to the Ravaged Path", after_the_grunts(), { + // One known tile to plan from: from both of the scene's ends + // the joined Jubilife and Route 204 find no plan. Row 5 is open + // from x 44 to 49, a wall at 43. + anchored(to_walls("west_to_the_wall", {{.direction = WEST, .tiles = 4}}, "west along row 5 to the wall"), + at(JUBILIFE, 44, 5, WEST)), // Lass Sarah's east sight crosses the street (r233a, r234a): she // may stop us for a battle. route("below_the_ravaged_path", at(ROUTE_204_SOUTH, "below_door", NORTH), From e2ae1bdddb24048f236dc5be90304e5af59df959 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:12:48 -0800 Subject: [PATCH 74/98] BDSP AutoStory: dpad_tiles without a turn tap A 160 ms D-pad tap steps from any facing; only talks need a turn, and the person blocks like a wall. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Walks.cpp | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp index 353d28c98f..f3423bede7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.cpp @@ -139,26 +139,14 @@ WalkResult walk_dir( // `taps` D-pad taps toward `dpad`, settling between them. On the D-pad the // player moves a whole tile per tap (measured above), so counted legs do -// not drift the way stick holds do. A short tap turns us first. +// not drift the way stick holds do. void dpad_tiles(VideoStream& stream, ProControllerContext& context, DpadPosition dpad, size_t taps, const std::string& label){ stream.log("[Walk] " + label + ": " + std::to_string(taps) + " D-pad taps."); - // Turn with a short tap: a 160 ms tap is a whole step even from another - // facing, so the old 160 ms "turn" walked one tile too many on every - // call (r80: two tiles west in the Ravaged Path, not one; the gym door's - // counts of 21 and 22 landing 1 and 2 tiles west). A short tap the way we - // already face can still step, so count it when the view moved. + // No turn tap: a 160 ms tap is a whole step from any facing, so N taps + // are N tiles (r80: the old 160 ms "turn" tap walked one tile too many, + // two tiles west in the Ravaged Path, not one). Turning only matters to + // talk to someone, and they block like a wall, so a tap toward them turns. size_t moved = 0; - { - context.wait_for_all_requests(); - const VideoSnapshot before = stream.video().snapshot(); - pbf_press_dpad(context, dpad, 50ms, 500ms); - context.wait_for_all_requests(); - const VideoSnapshot after = stream.video().snapshot(); - if (before && after && walk_screen_change(*before.frame, *after.frame) > 30){ - stream.log("[Walk] " + label + ": the turn tap stepped; counting it."); - moved = 1; - } - } // Count only taps that moved the view: one after the turn was lost and // the leg ended a tile short (r215d). A tile's scroll changes the view by // about 100; standing still, under 15. A tap that does not move is someone From 77d9ff4e2c7bb755966db65db81cf40057fd2368 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:16:06 -0800 Subject: [PATCH 75/98] BDSP AutoStory: leave the Ravaged Path one tap at a time r81 came out of the cave but the timed walk read the fade as nothing and went on south to the cliff, past 01.10's start tile. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Route204.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index 9988c24e87..87f0504144 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -145,9 +145,18 @@ void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context const StoryWorld rocks_out = ravaged_path_maps(smashed); at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == map_point("RavagedPath", "east_end"); }, "To the corridor's east end", {}, route_plan_costs()}); dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); - const WalkResult out = walk_dir(stream, context, Direction::SOUTH, 4000ms, "south out of the Ravaged Path"); - if (out.stop != WalkStop::TRANSITION && out.stop != WalkStop::LIMIT){ - fail_step(stream, "Ravaged Path: the walk south from a tile west of the corridor's end did not leave the cave."); + // One tap at a time until daylight, so we stop on the tile below the + // mouth (cave_exit) where 01.10 starts: a timed walk south read the + // fade as nothing and walked on to the cliff (r81). + bool out = false; + for (size_t tap = 0; tap < 4 && !out; tap++){ + pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); + context.wait_for(2500ms); + context.wait_for_all_requests(); + out = !in_dim_cave(stream); + } + if (!out){ + fail_step(stream, "Ravaged Path: tapping south from a tile west of the corridor's end did not leave the cave."); } context.wait_for(2000ms); } From daa3567ec0ecc07abb04134a4b24df5ebbbd9bb2 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:17:55 -0800 Subject: [PATCH 76/98] BDSP AutoStory: leave the Ravaged Path with one tap and its banner Brightness can't tell the cave from Route 204 at night; the banner shows either way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Route204.cpp | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index 87f0504144..a925759f8c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -145,18 +145,21 @@ void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context const StoryWorld rocks_out = ravaged_path_maps(smashed); at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == map_point("RavagedPath", "east_end"); }, "To the corridor's east end", {}, route_plan_costs()}); dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); - // One tap at a time until daylight, so we stop on the tile below the - // mouth (cave_exit) where 01.10 starts: a timed walk south read the - // fade as nothing and walked on to the cliff (r81). + // One tap onto the way out, then wait for Route 204's banner: a timed + // walk south read the fade as nothing and walked on to the cliff, past + // cave_exit where 01.10 starts (r81). Not brightness: outside can be + // as dark as the cave at night. + pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); + context.wait_for_all_requests(); bool out = false; - for (size_t tap = 0; tap < 4 && !out; tap++){ - pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); - context.wait_for(2500ms); - context.wait_for_all_requests(); - out = !in_dim_cave(stream); + const auto deadline = current_time() + 8000ms; + while (!out && current_time() < deadline){ + context.wait_for(200ms); + const VideoSnapshot frame = stream.video().snapshot(); + out = frame && location_banner_showing(*frame.frame); } if (!out){ - fail_step(stream, "Ravaged Path: tapping south from a tile west of the corridor's end did not leave the cave."); + fail_step(stream, "Ravaged Path: no location banner after one tap south from a tile west of the corridor's end."); } context.wait_for(2000ms); } From 1bb6634d8b06681fc856460cb46c40be7cc991f9 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:31:38 -0800 Subject: [PATCH 77/98] BDSP AutoStory: settle on 15 s of calm overworld when the screen keeps moving Floaroma's cloud shadows never let the screen go still: r83 got Mew and then timed out. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 1b36c7f0bc..2130e6d833 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -304,6 +304,7 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c ShortDialogDetector dialog; OverworldDetector overworld; WallClock quiet_since = none; + WallClock calm_since = none; // overworld, no text, still or not double darkest = 255; VideoSnapshot previous; while (current_time() < deadline){ @@ -316,6 +317,15 @@ void settle(VideoStream& stream, ProControllerContext& context, Button button, c const bool talking = frame && dialog.detect(frame); const bool still = frame && previous && walk_screen_change(*previous.frame, *frame.frame) < WALK_TAP_BLOCKED_CHANGE; previous = frame; + // Cloud shadows drift over Floaroma, so its screen is never still: r83 + // got Mew and then waited out the 120 s on a calm overworld. 15 s of + // overworld with no text is three times r31's silent walk-in. + const bool calm = !talking && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS); + calm_since = calm ? (calm_since == none ? now : calm_since) : none; + if (calm && now - calm_since >= 15000ms){ + stream.log(std::format("[Story] {}: settled on 15 s of overworld without text (the screen kept moving); darkest frame {:.0f}.", label, darkest)); + return; + } if (!talking && still && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS)){ quiet_since = quiet_since == none ? now : quiet_since; if (now - quiet_since >= 5000ms){ From fe17bc88a980ecc0f9321fe5bd8a9001695a9cd6 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:34:31 -0800 Subject: [PATCH 78/98] BDSP AutoStory: turn to Floaroma's old man with one tap Cloud shadows made blocked taps read as moves (r84). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index a925759f8c..9289b2c52b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -221,10 +221,11 @@ void below_the_old_man(VideoStream& stream, ProControllerContext& context){ if (west.moved_chunks < 1){ fail_step(stream, "Floaroma: the step west below the old man did not move."); } - const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, {0, 1}, 3, "north until the old man"); - if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 3){ - fail_step(stream, "Floaroma: nothing stopped the walk north to the old man."); - } + // He stands there, so one tap only turns us to him; the talk after fails + // if nobody answers. No counting: Floaroma's cloud shadows read as moves + // (r84: blocked taps counted, view change 20). + pbf_press_dpad(context, DPAD_UP, 160ms, 500ms); + context.wait_for_all_requests(); } } From e073a2d082f0e399410ca47eec5886f1e403b91f Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:39:56 -0800 Subject: [PATCH 79/98] BDSP AutoStory: end 01.07 with the settle scene, through Bebe's lines finish_dialogue stopped at the pause before Bebe walks over, so 01.08's save opened on her lines and missed the menu (r80). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp | 7 ------- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index e3d62f952f..c8ede10192 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -118,12 +118,6 @@ Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState&, c } -// 01.07: Rowan's and Dawn's lines after the tag battle. 01.08 starts here, -// from a save at the top of the street. -void rowan_after_the_battle(VideoStream& stream, ProControllerContext& context){ - finish_dialogue(stream, context, BUTTON_B, "Rowan and Dawn after the battle"); -} - // 01.01 from the save at the Jubilife Center's PC: healed at the nurse, // out of the door and through the plaza to the street east to Route 203. void jubilife_heal_and_out_to_street(VideoStream& stream, ProControllerContext& context){ @@ -253,7 +247,6 @@ void add_jubilife_events(StoryEvents& events){ events.emplace("up_the_steps", console_event(up_the_steps)); events.emplace("barry_town_map", console_event(barry_town_map)); events.emplace("talk_to_wanderer", talk_to_wanderer); - events.emplace("rowan_after_the_battle", console_event(rowan_after_the_battle)); events.emplace("jubilife_center_to_street", console_event(jubilife_center_to_street)); events.emplace("east_to_barry", console_event(east_to_barry)); events.emplace("west_to_the_crossroads", console_event(west_to_the_crossroads)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 5c80713fba..b60f28ca77 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -528,7 +528,7 @@ std::vector to_floaroma(){ event("west_to_the_crossroads", "west along the long road"), event("north_to_rowan", "north up the street until Rowan's scene"), battle("galactic_grunts", false, "the tag battle with Dawn", "doubles"), - event("rowan_after_the_battle", "Rowan's and Dawn's lines"), + scene("rowan_after_the_battle", std::nullopt, "Rowan's and Dawn's lines, then Bebe walks over with hers (r80 saved over her lines)"), }), row("01.08", "Jubilife to the Ravaged Path", std::nullopt, { event("north_out_of_jubilife", "north until Route 204's banner"), From 43ebd5b38cfe254587bbe446c74e9f798eb2c31f Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:40:21 -0800 Subject: [PATCH 80/98] BDSP AutoStory: the Ravaged Path exit in its own function Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Route204.cpp | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index 9289b2c52b..d17300478a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -108,6 +108,26 @@ void into_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ context.wait_for(2000ms); } +// 01.09: from the tile above the way out, one tap south out of the cave. +void out_of_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ + // One tap onto the way out, then wait for Route 204's banner: a timed + // walk south read the fade as nothing and walked on to the cliff, past + // cave_exit where 01.10 starts (r81). Not brightness: outside can be + // as dark as the cave at night. + pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); + context.wait_for_all_requests(); + bool out = false; + const auto deadline = current_time() + 8000ms; + while (!out && current_time() < deadline){ + context.wait_for(200ms); + const VideoSnapshot frame = stream.video().snapshot(); + out = frame && location_banner_showing(*frame.frame); + } + if (!out){ + fail_step(stream, "Ravaged Path: no location banner after one tap south from a tile west of the corridor's end."); + } +} + // 01.09: through the Ravaged Path, smashing its rocks. void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context){ // On the traced grid (Maps/RavagedPath.json): from the @@ -145,22 +165,7 @@ void through_the_ravaged_path(VideoStream& stream, ProControllerContext& context const StoryWorld rocks_out = ravaged_path_maps(smashed); at = walk_route(stream, context, run_from_wild_battle, rocks_out, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == map_point("RavagedPath", "east_end"); }, "To the corridor's east end", {}, route_plan_costs()}); dpad_tiles(stream, context, DPAD_LEFT, 1, "west onto the tile above the way out"); - // One tap onto the way out, then wait for Route 204's banner: a timed - // walk south read the fade as nothing and walked on to the cliff, past - // cave_exit where 01.10 starts (r81). Not brightness: outside can be - // as dark as the cave at night. - pbf_press_dpad(context, DPAD_DOWN, 160ms, 0ms); - context.wait_for_all_requests(); - bool out = false; - const auto deadline = current_time() + 8000ms; - while (!out && current_time() < deadline){ - context.wait_for(200ms); - const VideoSnapshot frame = stream.video().snapshot(); - out = frame && location_banner_showing(*frame.frame); - } - if (!out){ - fail_step(stream, "Ravaged Path: no location banner after one tap south from a tile west of the corridor's end."); - } + out_of_the_ravaged_path(stream, context); context.wait_for(2000ms); } } From 6517171a9d87b0d6db1d2436932cc73e364c81b2 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:46:29 -0800 Subject: [PATCH 81/98] BDSP maps: list every cell a map's rectangle touches in its source The cell list stepped 32 tiles from the rectangle's corner, so a rectangle not starting on a cell edge missed its last cell: Route203.json named narc_0016 and narc_0019 but not narc_0020. Its tiles are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP/Programs/AutoStory/Maps/Route203.json | 2 +- devtools/maps/bdsp_maps.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route203.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route203.json index 88f443c610..2e3c409907 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route203.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route203.json @@ -1,6 +1,6 @@ { "name": "Route203", - "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0016.bin, files/fielddata/land_data/land_data_release/narc_0019.bin; zone_event narc_0329", "revision": "default branch", "date": "2026-10-31"}, + "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0016.bin, files/fielddata/land_data/land_data_release/narc_0019.bin, files/fielddata/land_data/land_data_release/narc_0020.bin; zone_event narc_0329", "revision": "default branch", "date": "2026-10-31"}, "tiles": [ "#############################################################", "#############################################################", diff --git a/devtools/maps/bdsp_maps.py b/devtools/maps/bdsp_maps.py index 01e01b1ebc..4e027f0a28 100644 --- a/devtools/maps/bdsp_maps.py +++ b/devtools/maps/bdsp_maps.py @@ -517,8 +517,10 @@ def decode_map(name): for x, tile in enumerate(row): if tile == 'W': row[x] = 'D' if (x, y) in doors else '.' - lands = sorted({world.land_id(x, z) for z in range(z0, z0 + height, decomp.CELL) - for x in range(x0, x0 + width, decomp.CELL)} - {None}) + # Every cell the rectangle touches, also when it does not start on a cell's edge. + C = decomp.CELL + lands = sorted({world.land_id(C * col, C * row) for row in range(z0 // C, (z0 + height - 1) // C + 1) + for col in range(x0 // C, (x0 + width - 1) // C + 1)} - {None}) unverified = '; decomp only, not checked on the console' if spec.get('unverified') else '' source = { 'decomp_file': f'{repo}: ' + ', '.join(game.land_path(land) for land in lands) From 1b8fced9aef02b1044d51e5e9f50c19494412fc7 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 06:57:43 -0800 Subject: [PATCH 82/98] BDSP AutoStory: refuse a row's start save away from its map Each row that saves first now names the map the save screen's card shows at its start (saves_in, read from lab-B's console logs). The start save passes it to save_game(), which backs out unsaved when the card names another map; the executor turns that into a FatalProgramException, so the program stops with the game open instead of resetting to an older save and running the row from there. r76/r77 started 01.06 a walk away from its start and saved over the good save. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 29 ++++++++++++--- .../PokemonBDSP_AutoStory_StoryExecutor.h | 5 +++ ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 11 +++++- .../PokemonBDSP_AutoStory_StoryTable.cpp | 37 +++++++++++++++++++ .../PokemonBDSP_AutoStory_StoryTable.h | 3 +- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 3 ++ 6 files changed, 81 insertions(+), 7 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 2130e6d833..3d9868ef59 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -10,6 +10,7 @@ #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Filesystem/Filesystem.h" #include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/FatalProgramException.h" #include "CommonFramework/Exceptions/OperationFailedException.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/GlobalAutoPaths.h" @@ -495,8 +496,24 @@ Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ return Pokemon::outcome_ok(); } -Outcome StoryExecutor::save(RunState&, const StoryStep&){ - save_game(m_env.env, context()); +Outcome StoryExecutor::save(RunState&, const StoryStep& step){ + if (step.variant.empty()){ + save_game(m_env.env, context()); + return Pokemon::outcome_ok(); + } + // A row's start save, its variant the map the card must name. With a + // map, save_game() throws only when the card names another one, having + // backed out unsaved. That stops the program: a reset would load an + // older save and run this row from wherever that is. + try{ + save_game(m_env.env, context(), step.variant); + }catch (OperationFailedException& e){ + throw FatalProgramException( + ErrorReportMode::NO_ERROR_REPORT, + std::format("[{}] Not at the checkpoint's start, so not saving: {}", m_checkpoint, e.message()), + stream() + ); + } return Pokemon::outcome_ok(); } @@ -526,15 +543,17 @@ Pokemon::Segment make_story_segment( if (row.on_failure != StoryFailure::RELOAD_AND_RETRY){ return steps; } - Pokemon::Event save = Pokemon::story_event(SAVE_AT_START, executor); + StoryStep save_at_start = SAVE_AT_START; + save_at_start.variant = row.saves_in; + Pokemon::Event save = Pokemon::story_event(save_at_start, executor); save.skip_if.push_back(Pokemon::condition("saved at the start", [&saved, id = row.id](RunState&){ return saved.contains(id); })); // A save that fails is a failed first attempt: the retry // resets and does not save again. - save.act = [&executor, &saved, id = row.id](RunState& state){ + save.act = [&executor, &saved, id = row.id, save_at_start](RunState& state){ saved.insert(id); - return executor.run(state, SAVE_AT_START); + return executor.run(state, save_at_start); }; Pokemon::Checkpoint checkpoint; checkpoint.id = row.id; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h index 88dbb00c09..abad7f441b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -59,6 +59,11 @@ enum class StoryFailure{ // A row of the story table: the shared checkpoint, and what a failure does. struct StoryCheckpoint : public Pokemon::StoryCheckpoint{ StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY; + // The map the save screen's card names where the row starts ("Route + // 203"). The save at its start is refused anywhere else and the program + // stops: r76/r77 started 01.06 a walk away from its start and saved over + // the good save there. Empty for a row that does not save first. + std::string saves_in; }; // What the story's steps act on. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp index f1d5ee3489..febf93d8cb 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -247,8 +247,11 @@ class FakeGame : public Pokemon::StoryStepExecutor{ virtual Outcome train(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } virtual Outcome heal(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } virtual Outcome shop(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } - virtual Outcome save(RunState&, const StoryStep&) override{ + virtual Outcome save(RunState&, const StoryStep& step) override{ trace.push_back("save in " + m_row); + if (!step.variant.empty()){ + trace.push_back("card for " + m_row + ": " + step.variant); + } return Pokemon::outcome_ok(); } virtual Outcome game(RunState&, const StoryStep&) override{ return Pokemon::outcome_ok(); } @@ -338,6 +341,12 @@ UnitTestResult test_whole_table(Logger& logger){ return std::format("{} saved {} times, expected {}:{}", id, saves, expected, joined(sim.trace)); } } + // Each start save checks the card for its row's map. + for (const StoryCheckpoint& row : story_checkpoints()){ + if (!row.saves_in.empty() && sim.count("card for " + row.id + ": " + row.saves_in) != 1){ + return std::format("{}'s start save did not check the card for {}:{}", row.id, row.saves_in, joined(sim.trace)); + } + } if (report.one_shot_passes() != report.checkpoints_run){ return "Expected every checkpoint one-shot:" + joined(sim.lines); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index b60f28ca77..b9660db99e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -15,6 +15,7 @@ * */ +#include #include #include #include @@ -554,6 +555,36 @@ std::vector to_floaroma(){ }; } +// The map the save screen's card names at each row's start, as the console +// read it there (lab-B's logs, 2026-10-31 to 11-02). A building's card +// names its town: 00.04 starts in Rowan's lab, 01.04 at Oreburgh's nurse. +const std::map& start_save_cards(){ + static const std::map cards{ + {"00.01", "Twinleaf Town"}, + {"00.02", "Verity Lakefront"}, + {"00.03", "Verity Lakefront"}, + {"00.04", "Sandgem Town"}, + {"00.04b", "Route 202"}, + {"00.04c", "Sandgem Town"}, + {"00.04d", "Route 202"}, + {"00.05", "Jubilife City"}, + {"00.05b", "Jubilife City"}, + {"00.05c", "Jubilife City"}, + {"01.01", "Jubilife City"}, + {"01.02", "Route 203"}, + {"01.03", "Oreburgh City"}, + {"01.04", "Oreburgh City"}, + {"01.05", "Oreburgh City"}, + {"01.06", "Route 203"}, + {"01.07", "Jubilife City"}, + {"01.08", "Jubilife City"}, + {"01.09", "Ravaged Path"}, + {"01.10", "Route 204"}, + {"01.11", "Floaroma Town"}, + }; + return cards; +} + std::vector make_table(){ std::vector table; for (auto part : {intro, lake_and_pokedex, to_the_catch, to_jubilife, jubilife, to_oreburgh, to_floaroma}){ @@ -561,6 +592,12 @@ std::vector make_table(){ table.push_back(std::move(checkpoint)); } } + for (StoryCheckpoint& checkpoint : table){ + const auto card = start_save_cards().find(checkpoint.id); + if (checkpoint.on_failure == StoryFailure::RELOAD_AND_RETRY && card != start_save_cards().end()){ + checkpoint.saves_in = card->second; + } + } return table; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h index a7cce9665d..cd29187eb4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h @@ -6,7 +6,8 @@ * shared story steps (Pokemon/AutoStory/Pokemon_StorySteps.h): plain * data, as FRLG's table is, so the tests plan its routes and run it * against a fake game. Each row but 00.00 saves at its start, which is - * where the row before it ends. + * where the row before it ends, and only if the save screen's card names + * the row's map (saves_in). * */ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 8e8bd57129..9b5e4748df 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -61,6 +61,9 @@ class Test_StoryTableLints : public UnitTest{ return row.id + ": repeated or out of order."; } previous = row.id; + if ((row.on_failure == StoryFailure::RELOAD_AND_RETRY) == row.saves_in.empty()){ + return row.id + ": a row names the map its start save is made in (saves_in) exactly when it saves first."; + } const std::vector problems = Pokemon::lint_story_checkpoint(row); if (!problems.empty()){ return row.id + ": " + problems[0]; From 1f14bca446c165429b75a809098c0a5fefc81589 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 07:11:13 -0800 Subject: [PATCH 83/98] BDSP maps: PlayerHouse1F and Route201's console fixes as generator patches Both files had their console measurements (r32's open tile in the house, r16's trees and sign on Route 201) edited into the tiles by hand, so bdsp_maps.py --check failed for them. The generator's PATCHES table now holds them; the tiles go back to the decomp's and the loader applies the patches, so the walked grid is unchanged. --check passes for all 21 maps. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/Maps/PlayerHouse1F.json | 7 +++++-- .../Programs/AutoStory/Maps/Route201.json | 15 ++++++++++++--- devtools/maps/bdsp_maps.py | 12 ++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json index 2a7a89546d..a97ce8bbc3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json @@ -1,6 +1,6 @@ { "name": "PlayerHouse1F", - "source": {"decomp_file": "pret/pokeplatinum: res/field/maps/data/map_data_186.bin; events_twinleaf_town_player_house_1f.json; decomp, but (4, 8) is open on the console: west along row 8 stops at x 4 (r32, 2026-11-01)", "revision": "c6689696d5fb", "date": "2026-11-01"}, + "source": {"decomp_file": "pret/pokeplatinum: res/field/maps/data/map_data_186.bin; events_twinleaf_town_player_house_1f.json; decomp only, not checked on the console", "revision": "c6689696d5fb", "date": "2026-11-01"}, "tiles": [ "#############", "#############", @@ -10,7 +10,7 @@ "#SSS..SS....#", "#...........#", "#...........#", - "#..#........#", + "#..##.......#", "##.##......##", "##....D....##", "#############" @@ -25,5 +25,8 @@ ], "objects": [ {"name": "mom_0", "kind": "npc", "x": 7, "y": 8, "facing": "north"} + ], + "patches": [ + {"x": 4, "y": 8, "glyph": ".", "measured_by": "r32, 2026-11-01"} ] } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json index 05b22e405a..91ba7195f7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route201.json @@ -1,6 +1,6 @@ { "name": "Route201", - "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0005.bin, files/fielddata/land_data/land_data_release/narc_0006.bin; zone_event narc_0327", "revision": "default branch", "console": "row 18 west of the exit path (8..13, 18) is trees on the console (r16, by hand), open in the decomp; the 201 sign stands at (13, 18), not (13, 17)", "date": "2026-10-31"}, + "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0005.bin, files/fielddata/land_data/land_data_release/narc_0006.bin; zone_event narc_0327", "revision": "default branch", "date": "2026-10-31"}, "tiles": [ "################################################################", "################################################################", @@ -19,8 +19,8 @@ "....................~~~~~~~~~~............................######", "....................~~~~~~~~~~............................######", "....................~~~~~~~~~~............................######", - "########............##########............................######", - "#############S......##########............~~~~~~~~~~~~~~~~######", + "########.....S......##########............................######", + "########............##########............~~~~~~~~~~~~~~~~######", "##############....############################~~~~~~~~~~~~######", "##############....############################~~~~~~~~~~~~######", "##############....##############################################", @@ -42,5 +42,14 @@ {"name": "pokemon_breeder_m_0", "kind": "npc", "x": 27, "y": 15, "wander": {"x0": 26, "y0": 14, "x1": 28, "y1": 16}}, {"name": "school_kid_m_9", "kind": "npc", "x": 57, "y": 8, "facing": "west"}, {"name": "lass_11", "kind": "npc", "x": 46, "y": 16, "wander": {"x0": 44, "y0": 16, "x1": 48, "y1": 16}} + ], + "patches": [ + {"x": 8, "y": 18, "glyph": "#", "measured_by": "r16, 2026-10-31"}, + {"x": 9, "y": 18, "glyph": "#", "measured_by": "r16, 2026-10-31"}, + {"x": 10, "y": 18, "glyph": "#", "measured_by": "r16, 2026-10-31"}, + {"x": 11, "y": 18, "glyph": "#", "measured_by": "r16, 2026-10-31"}, + {"x": 12, "y": 18, "glyph": "#", "measured_by": "r16, 2026-10-31"}, + {"x": 13, "y": 18, "glyph": "S", "measured_by": "r16, 2026-10-31"}, + {"x": 13, "y": 17, "glyph": ".", "measured_by": "r16, 2026-10-31"} ] } diff --git a/devtools/maps/bdsp_maps.py b/devtools/maps/bdsp_maps.py index 4e027f0a28..4a12ed05b0 100644 --- a/devtools/maps/bdsp_maps.py +++ b/devtools/maps/bdsp_maps.py @@ -421,6 +421,18 @@ def box(x0, y0, x1, y1): # was blocked at once. + [(21, 30, '#', 'r254, 2026-11-01')] ), + 'PlayerHouse1F': [ + # Open on the console, a wall in the data: west along row 8 stops + # at x 4 (r32). + (4, 8, '.', 'r32, 2026-11-01'), + ], + 'Route201': ( + # Row 18 west of the exit path is trees on the console, open in the + # decomp, and the 201 sign stands at (13, 18), not (13, 17) (r16, + # walked by hand). + [(x, 18, '#', 'r16, 2026-10-31') for x in range(8, 13)] + + [(13, 18, 'S', 'r16, 2026-10-31'), (13, 17, '.', 'r16, 2026-10-31')] + ), 'Route204North': [ # Grass in the data, blocked on the console twice. (12, 6, '#', 'r248a, 2026-10-31'), From 0387b383477ba75bd0a9cbbd24f7baf06bfa9492 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 07:32:22 -0800 Subject: [PATCH 84/98] BDSP AutoStory: a Pokemon Center is its floor in a room in_pokemon_center() read only the floor's warm yellow, so Jubilife's plaza (63 %, r79: a won tag battle called lost) and Sandgem's and Oreburgh's streets (30 to 35 %) passed. It now also needs indoors(). Checked offline on 635 lab frames (failure shots, route shots, r79's video): the two together pick out exactly the Center interiors. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Talks.cpp | 10 ++++- .../AutoStory/PokemonBDSP_AutoStory_Talks.h | 5 +++ .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 42 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp index f821fdb0a6..487994981c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp @@ -23,6 +23,7 @@ #include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" @@ -122,14 +123,19 @@ bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent){ return warm * 10 > total * 3; } +bool pokemon_center_on_screen(const ImageViewRGB32& screen, size_t* percent){ + return center_floor_on_screen(screen, percent) && indoors(screen); +} + bool in_pokemon_center(VideoStream& stream){ const VideoSnapshot frame = stream.video().snapshot(); if (!frame){ return false; } size_t percent = 0; - const bool inside = center_floor_on_screen(*frame.frame, &percent); - stream.log("[Center check] warm floor " + std::to_string(percent) + " %: " + + const bool inside = pokemon_center_on_screen(*frame.frame, &percent); + stream.log("[Center check] warm floor " + std::to_string(percent) + " %, " + + (indoors(*frame.frame) ? "a room" : "outside") + ": " + (inside ? "in a Pokemon Center." : "not in a Pokemon Center.")); return inside; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h index 02f185e855..8f64bf9a1b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.h @@ -58,6 +58,11 @@ void fight_route_trainer(VideoStream& stream, ProControllerContext& context, con // green. After a battle it tells a loss (a whiteout ends at the counter) // from a win. `percent`, if given, gets the warm share of the lower half. bool center_floor_on_screen(const ImageViewRGB32& screen, size_t* percent = nullptr); +// In a Pokemon Center: its floor on screen, and a room (indoors()). Floor +// colour alone read Jubilife's yellow plaza as a Center (63 %, r79) and +// Sandgem's and Oreburgh's streets at 30 to 35 %; of 635 lab frames, the +// two together pick out exactly the Center interiors. +bool pokemon_center_on_screen(const ImageViewRGB32& screen, size_t* percent = nullptr); bool in_pokemon_center(VideoStream& stream); // The Oreburgh Mine's hall and corridors are dim: mean brightness 45 to 64 // on the console, against 99 to 116 for the city and its yard (2026-10-31). diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp index c38ff46daf..0c62971e4e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -14,6 +14,7 @@ #include "Utils/PokemonBDSP_AutoStory_Indoors.h" #include "Utils/PokemonBDSP_AutoStory_Location.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Talks.h" #include "PokemonBDSP_AutoStory_Walk_Tests.h" namespace PokemonAutomation{ @@ -237,6 +238,46 @@ class Test_LocationBannerShowing : public UnitTest{ } }; +// pokemon_center_on_screen(): the Center's warm floor in a room. r79 read +// Jubilife's yellow plaza as a Center and called a won battle lost. +class Test_PokemonCenterOnScreen : public UnitTest{ +public: + Test_PokemonCenterOnScreen() + : UnitTest("PokemonBDSP::AutoStory - a Pokemon Center is its floor in a room, not a yellow plaza") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + constexpr uint32_t FLOOR = 0xfff0c860; // the Center's warm yellow + constexpr uint32_t WALL = 0xffe08030; // its orange back wall + constexpr uint32_t GRASS = 0xff60b040; + struct Case{ + std::string name; + uint32_t top; + uint32_t bottom; + double dark_bottom; // how much of the screen's height, from the bottom, is black + bool center; + }; + const std::vector cases{ + {"a Center: wall over floor, black below it", WALL, FLOOR, 0.15, true}, + {"Jubilife's plaza: yellow paving, no black edge", FLOOR, FLOOR, 0, false}, + {"a street: grass over yellow paving", GRASS, FLOOR, 0, false}, + {"a grey gym in a room", 0xff808080, 0xff909090, 0.15, false}, + }; + for (const Case& item : cases){ + ImageRGB32 screen(1920, 1080); + const size_t dark_from = (size_t)(1080 * (1 - item.dark_bottom)); + for (size_t y = 0; y < 1080; y++){ + for (size_t x = 0; x < 1920; x++){ + screen.pixel(x, y) = y >= dark_from ? 0xff000000 : y < 540 ? item.top : item.bottom; + } + } + if (pokemon_center_on_screen(screen) != item.center){ + return item.name + (item.center ? ": not read as a Center." : ": read as a Center."); + } + } + return true; + } +}; + } @@ -248,6 +289,7 @@ void add_tests_AutoStory_Walk(UnitTestDatabase& database){ database.add(); database.add(); database.add(); + database.add(); } From ef520096f4ee9aef93a027f6a823702d7858b987 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 11:12:22 -0800 Subject: [PATCH 85/98] BDSP AutoStory: 00.02 on the shared steps The Starly battle is a BATTLE step, variant "starter" (fight_starly(): no talk first, no Poke Balls, and the scene goes on either way), and the walk out of the lake after it a MASH scene that ends on the Lakefront's quiet overworld, instead of Butters' fixed 60 s of B. Stays named: starter_briefcase (B until an image match of the open briefcase) and choose_starter (the briefcase's cursor from the program's starter option): menus only BDSP has. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Lake.cpp | 15 ++++----------- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 7 +++++++ .../PokemonBDSP_AutoStory_StoryTable.cpp | 8 +++++--- .../PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp index 80b271cd1e..f0343fff40 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp @@ -5,9 +5,9 @@ * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. * * The scenes of the story's start that need scripted input: the briefcase - * at Lake Verity, the starter, the Starly battle and the scene after it, - * the Pokedex in Rowan's lab, Mom's and Barry's mother's lines, and Dawn's - * catching tutorial. The button sequences are Butters' (#1717); they ran on + * at Lake Verity, the starter, the Pokedex in Rowan's lab, Mom's and Barry's + * mother's lines, and Dawn's catching tutorial. The button sequences are + * Butters' (#1717); they ran on * the console in his recordings, but not yet from the planned walks around * them. * @@ -116,12 +116,6 @@ void azure_flute_if_there(VideoStream& stream, ProControllerContext& context){ } } -// 00.02: after the Starly battle, Rowan's lines and the walk out of the -// lake, B for a minute. -void after_starly(VideoStream&, ProControllerContext& context){ - pbf_mash_button(context, BUTTON_B, 60000ms); - context.wait_for_all_requests(); -} // 00.03: Dawn walks us into Rowan's lab: his lines, "give a nickname?" // (B: No), and the Pokedex, taken with A. @@ -151,8 +145,7 @@ void add_lake_events(StoryEvents& events){ events.emplace("azure_flute_if_there", console_event(azure_flute_if_there)); events.emplace("starter_briefcase", console_event(starter_briefcase)); events.emplace("choose_starter", choose_starter); - events.emplace("fight_starly", console_event(fight_starly)); - events.emplace("after_starly", console_event(after_starly)); + events.emplace("rowan_gives_pokedex", console_event(rowan_gives_pokedex)); events.emplace("catch_tutorial", console_event(catch_tutorial)); events.emplace("catch_pokemon", console_event(catch_1_pokemon)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index c7a5c8a090..aab4658b3d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -501,6 +501,13 @@ Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ } Outcome StoryExecutor::battle(RunState&, const StoryStep& step){ + // "starter": the Starly battle at the lake (00.02). It opens on its own, + // with no talk, and the starter has no Poke Balls to throw; the scene + // goes on whoever wins. + if (step.variant == "starter"){ + fight_starly(stream(), context()); + return Pokemon::outcome_ok(); + } // A loss sends us to a Pokemon Center, so ending on its floor means a // loss - unless the floor check already passed where the battle began: // Jubilife's yellow plaza reads as Center floor (63 %, r79 won Rowan's diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 021fb6d3c5..1b50e75a73 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -301,11 +301,13 @@ std::vector lake_and_pokedex(){ step_into("lake_entrance", NORTH, "the trigger (16..17, 12): Barry hears Rowan and the two walk in"), event("starter_briefcase", "the lake's scene, to the open briefcase"), event("choose_starter", "the starter from the program's option"), - event("fight_starly", "the Starly battle"), + battle("starly", true, "the Starly battle, with the starter alone", "starter"), // The game walks us out of the lake, then the Lakefront's scene walks us 3 // south: (17, 14) facing south, or a tile below (decomp; timeline 00.02 #5). - event("after_starly", "Rowan's lines and the walk out of the lake", - at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH)), + // Butters' sequence was B for a fixed 60 s; the scene rule ends it on + // the quiet overworld instead (unchecked on the console). + scene("after_starly", at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH), + "Rowan's lines and the walk out of the lake"), }), row("00.03", "Back to Sandgem: Rowan, Dawn and the Pokedex", at_or_next_to(LAKEFRONT, "below_lake_entrance", SOUTH), { route("short_of_rowan", at(ROUTE_201, 4, 14, EAST), "back east onto Route 201's lower level"), diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 1debf3d6bc..43d2818e0b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -41,7 +41,7 @@ const std::map>& built_in_variants(){ {StoryStepKind::LEGS, {"", "one_short_ok", "to_wall", "to_dialogue", "to_map"}}, {StoryStepKind::STEP, {"", "to_banner"}}, {StoryStepKind::MASH, {"", "a"}}, - {StoryStepKind::BATTLE, {"", "doubles"}}, + {StoryStepKind::BATTLE, {"", "doubles", "starter"}}, }; return variants; } From 2452838752d6b0bc7c7035f6e8449a6c8b7dc3c0 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 11:13:10 -0800 Subject: [PATCH 86/98] BDSP AutoStory: 00.03 on the shared steps Rowan's Pokedex is a MASH scene ending at Rowan in the lab, through the fade into it. Butters' sequence pressed B through Dawn's and Rowan's lines and the nickname question, then A for the Pokedex; the scene's B answers the question No too, and closes an item's lines as it closed Barry's parcel in 00.04 (r45). Stays named: nothing in 00.03. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events.h | 4 ++-- .../PokemonBDSP_AutoStory_Events_Lake.cpp | 17 +++-------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 8 ++++++-- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h index 388b396eae..49bbc296c7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -4,8 +4,8 @@ * * The story's steps that only BDSP has, by name: a step of the table whose * variant names one of these runs it (StoryExecutor::run()). Some are the - * scripted scenes (the intro's briefcase, the starter, the Starly battle, - * the Pokedex); most are segment 01's walks, which were measured on the + * scripted scenes (the lake's briefcase, the starter, Dawn's catching + * tutorial); most are segment 01's walks, which were measured on the * console as holds, taps and walks until stopped rather than planned, and * are kept as they ran there. Each fails by throwing, with a screenshot; * the executor turns that into a failed step. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp index f0343fff40..0ecd9e3f77 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp @@ -5,8 +5,8 @@ * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. * * The scenes of the story's start that need scripted input: the briefcase - * at Lake Verity, the starter, the Pokedex in Rowan's lab, Mom's and Barry's - * mother's lines, and Dawn's catching tutorial. The button sequences are + * at Lake Verity, the starter, Mom's and Barry's mother's lines, and Dawn's + * catching tutorial. The button sequences are * Butters' (#1717); they ran on * the console in his recordings, but not yet from the planned walks around * them. @@ -117,17 +117,6 @@ void azure_flute_if_there(VideoStream& stream, ProControllerContext& context){ } -// 00.03: Dawn walks us into Rowan's lab: his lines, "give a nickname?" -// (B: No), and the Pokedex, taken with A. -void rowan_gives_pokedex(VideoStream& stream, ProControllerContext& context){ - wait_for_dialogue(stream, context, "Dawn at Sandgem's entrance"); - pbf_mash_button(context, BUTTON_B, 50000ms); - wait_for_dialogue(stream, context, "Nickname: no"); - pbf_mash_button(context, BUTTON_B, 40000ms); - wait_for_dialogue(stream, context, "The Pokedex"); - pbf_mash_button(context, BUTTON_A, 40000ms); - context.wait_for_all_requests(); -} // 00.04: Dawn's catching tutorial on Route 202: her lines and her demo // battle, B for 70 s. @@ -146,7 +135,7 @@ void add_lake_events(StoryEvents& events){ events.emplace("starter_briefcase", console_event(starter_briefcase)); events.emplace("choose_starter", choose_starter); - events.emplace("rowan_gives_pokedex", console_event(rowan_gives_pokedex)); + events.emplace("catch_tutorial", console_event(catch_tutorial)); events.emplace("catch_pokemon", console_event(catch_1_pokemon)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 1b50e75a73..2637eae876 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -330,8 +330,12 @@ std::vector lake_and_pokedex(){ // Dawn's scene starts as we come into Sandgem (trigger (4, 10..15)); // walk until it does rather than to a tile (r35, r36). legs("into_sandgem", {{.direction = EAST, .tiles = 8}}, "east into Sandgem until Dawn's scene starts", "to_dialogue"), - event("rowan_gives_pokedex", "Dawn walks us to the lab: Rowan, no nickname, the Pokedex", - at(LAB, "rowan_talk", NORTH)), + // Butters' sequence pressed B through Dawn's and Rowan's lines and + // the nickname question (No), then A for the Pokedex. The scene's + // B answers No too, and closes an item's lines as it did Barry's + // parcel (r45). Into the lab is a fade. + scene("rowan_gives_pokedex", at(LAB, "rowan_talk", NORTH), + "Dawn walks us to the lab: Rowan, no nickname, the Pokedex"), }), }; } From d9040cb194f66b6498ae3c9c5ecdae81898a31fd Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 11:13:49 -0800 Subject: [PATCH 87/98] BDSP AutoStory: 00.05 on the shared steps Barry's Town Map is a TALK west from beside him at the School's back wall (the walk there now says it ends facing him) and a MASH scene with A to "You obtained a Town Map!". The event's second try, north to the wall again when he did not answer, is not kept: the walk north already ends on the wall. Stays named: up_the_steps. It taps a tile west and two north from 00.05's save below Jubilife's steps, and that save's tile is not known (the map has only where the taps end, "up_the_steps", column 46 or 47 a few rows up), so the row has no start pose for a LEGS step to walk from. It needs the save's tile measured on the console, as a point on JubilifeCity's map. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 16 +--------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 11 +++++++++-- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index c1dc95e0dd..04c36c9aa1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -51,20 +51,6 @@ void up_the_steps(VideoStream& stream, ProControllerContext& context){ } } -// 00.05: Barry stands at the blackboard a tile west of the door's column, -// and that column is clear to the back wall: north to it, face west, his -// scene to "You obtained a Town Map!" (A closes it). A tile west by a hold -// went two in r149a and the boy at the table stopped us. -void barry_town_map(VideoStream& stream, ProControllerContext& context){ - for (size_t attempt = 0; attempt < 2; attempt++){ - const DialogueRead read = talk_facing(stream, context, Direction::WEST, BUTTON_A, "Barry"); - if (read.opened && read.closed){ - return; - } - walk_until_stopped(stream, context, Direction::NORTH, 4, "North to the back wall"); - } - fail_step(stream, "Trainers' School: Barry did not answer from beside the back wall."); -} // 00.05b: a talk to someone who wanders a tile either side of home along a // row (the clowns by the Mart and the Poketch Company, WANDER_AROUND with @@ -214,7 +200,7 @@ void east_to_barry(VideoStream& stream, ProControllerContext& context){ void add_jubilife_events(StoryEvents& events){ events.emplace("up_the_steps", console_event(up_the_steps)); - events.emplace("barry_town_map", console_event(barry_town_map)); + events.emplace("talk_to_wanderer", talk_to_wanderer); events.emplace("jubilife_center_to_street", console_event(jubilife_center_to_street)); events.emplace("east_to_barry", console_event(east_to_barry)); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 2637eae876..9d913d9c95 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -462,8 +462,15 @@ StoryCheckpoint the_town_map(){ route("to_the_school", next_to(JUBILIFE, "trainers_school_door", SOUTH, NORTH), "below the School's door (40, 40)", "town"), step_into("into_the_school", NORTH, "the School's door", inside(TRAINERS_SCHOOL)), - to_walls("up_to_barry", {{.direction = NORTH, .tiles = 10}}, "the door's column to the back wall, beside Barry"), - event("barry_town_map", "Barry gives the Town Map", inside(TRAINERS_SCHOOL)), + // Barry stands at the blackboard a tile west of the door's column, + // which is clear to the back wall: beside him there, face west and + // talk. A tile west by a hold went two in r149a, and the boy at the + // table stopped us. (The event walked to the wall again once when he + // did not answer.) + anchored(to_walls("up_to_barry", {{.direction = NORTH, .tiles = 10}}, "the door's column to the back wall, beside Barry"), + Pokemon::tile_pose(TRAINERS_SCHOOL, {}, WEST)), + talk("barry", "Barry, west of us at the blackboard"), + scene("barry_town_map", inside(TRAINERS_SCHOOL), "his lines to \"You obtained a Town Map!\", closed with A", "a"), // Barry stands beside the door's column, which is clear down to the door. step_into("out_of_the_school", SOUTH, "straight down the door's column and out", below_the_school()), }); From de371f7b7e96816261d9ded5dcc1acedb697d04d Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 11:15:20 -0800 Subject: [PATCH 88/98] BDSP AutoStory: 01.01 on the shared steps 01.01 starts where 00.05c ends, (58, 22) on Jubilife's street east to Route 203, and is planned on the joined Route 203 and Jubilife: a route to (4, 18) on Route 203, a step east onto Barry's coord event (column 5, rows 17 to 20, VAR_ROUTE_203_RIVAL_STATE 0, Diamond's zone event narc_0329), then his battle, which leaves us on the trigger (assumed). Dropped with jubilife_center_to_street: its start from a save at the Jubilife Center's PC (two tiles south, three west, the nurse, then stick holds to the street). A save there no longer starts 01.01; the row's save is made on the street. Dropped with east_to_barry: its walk east row by row past walls, which the plan replaces. Stays named: nothing in 01.01. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 105 +----------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 16 ++- 2 files changed, 15 insertions(+), 106 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index 04c36c9aa1..9919b71635 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -1,14 +1,12 @@ -/* BDSP AutoStory - Events: Jubilife City (00.05, 01.01) +/* BDSP AutoStory - Events: Jubilife City (00.05) * * From: https://github.com/PokemonAutomation/ * - * The Jubilife beats of 00.05 the story table cannot plan (up the steps, - * Barry's Town Map, the talk to a wandering clown), and segment 01's walk - * out of the Center to Barry. + * The Jubilife beats of 00.05 the story table cannot plan: up the steps, + * and the talk to a wandering clown. * */ -#include #include #include #include "CommonFramework/ImageTools/ImageBoxes.h" @@ -16,10 +14,8 @@ #include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" -#include "PokemonBDSP_AutoStory_Center.h" #include "PokemonBDSP_AutoStory_Talks.h" #include "PokemonBDSP_AutoStory_Walks.h" #include "PokemonBDSP_AutoStory_WildBattle.h" @@ -103,107 +99,12 @@ Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState&, c fail_step(stream, step.name + ": no answer from any tile below his range."); } - -// 01.01 from the save at the Jubilife Center's PC: healed at the nurse, -// out of the door and through the plaza to the street east to Route 203. -void jubilife_heal_and_out_to_street(VideoStream& stream, ProControllerContext& context){ - // The save stands us at the PC, right of the counter, facing it; west - // is the counter's end. Two tiles south clears it, then three west is - // the floor emblem's column, with the nurse north and the door south. - route_turn(stream, context, Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, route_open_hold(Direction::SOUTH, 2), 200ms); - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Direction::WEST, 3), 200ms); - if (walk_to_talk(stream, context, run_from_wild_battle, {0, 1}, 3000ms, "to the nurse").stop != WalkStop::DIALOG){ - fail_step(stream, "Jubilife Pokemon Center: the nurse did not answer."); - } - // A throughout: Yes to "rest your Pokemon?" (B would answer No). - finish_dialogue(stream, context, BUTTON_A, "Nurse"); - // Healing shows no text box for a few seconds, which reads as the end; - // her "Thank you for waiting" lines follow it. - if (dialogue_opens(stream, context, 6000ms)){ - finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); - } - // South through the door until a girl standing below it stops us. The - // door's fade may or may not read as a transition. - WalkResult out{WalkStop::LIMIT, 0ms, 0}; - for (size_t walk = 0; walk < 3 && out.stop != WalkStop::BLOCKED; walk++){ - out = walk_until(stream, context, run_from_wild_battle, {0, -1}, 8000ms, "out of the Pokemon Center"); - if (out.stop == WalkStop::DIALOG || out.stop == WalkStop::ARRIVED){ - break; - } - } - if (out.stop != WalkStop::BLOCKED){ - fail_step(stream, std::string("Jubilife Pokemon Center: walking out ended with ") + walk_stop_name(out.stop) + ", not at the girls below the door."); - } - - // Jubilife's plaza has too few walls the grid gets right to plan on (a - // leg to a wall the map shows walked on 19 tiles). These are the holds - // that worked on the console: west onto the main road, north to the - // street east to Route 203, and back a couple of tiles. - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, 720ms, 300ms); - route_turn(stream, context, Direction::NORTH); - pbf_move_left_joystick(context, {0, 1}, 3960ms, 300ms); - route_turn(stream, context, Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, 600ms, 300ms); - context.wait_for_all_requests(); -} - -// 01.01: from the Center's PC, the nurse and out, then the holds to the street east to Route 203; -// nothing when 00.05 has left us outdoors on that street. -void jubilife_center_to_street(VideoStream& stream, ProControllerContext& context){ - // From 00.05g we are already outdoors on the street east to Route 203, - // healed on arriving in Jubilife: straight to the walk east. - bool outdoors = false; - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - outdoors = !indoors(*screen.frame); - } - if (outdoors){ - stream.log("[Route] 01.01: outdoors; skipping the Pokemon Center."); - }else{ - jubilife_heal_and_out_to_street(stream, context); - } -} - -// 01.01: east along the street until Barry stops us at the entrance to Route 203. -void east_to_barry(VideoStream& stream, ProControllerContext& context){ - // East along the street. The opening to the route is a few rows high; - // Barry stops us a little way in. A wall means we are a row off it: try - // rows to the south, then to the north. - static const int ROW_CHANGES[] = {0, 1, 1, -3, -1}; - bool barry = false; - for (int change : ROW_CHANGES){ - if (barry){ - break; - } - for (int i = 0; i < std::abs(change); i++){ - const Direction dir = change > 0 ? Direction::SOUTH : Direction::NORTH; - route_turn(stream, context, dir); - pbf_move_left_joystick(context, {0, change > 0 ? -1.0 : 1.0}, route_open_hold(dir, 1), 200ms); - } - context.wait_for_all_requests(); - WalkResult walk{WalkStop::LIMIT, 0ms, 0}; - for (size_t stretch = 0; stretch < 3 && walk.stop == WalkStop::LIMIT; stretch++){ - walk = walk_until(stream, context, run_from_wild_battle, {1, 0}, 9000ms, "east to Route 203"); - } - barry = walk.stop == WalkStop::DIALOG; - } - if (!barry){ - fail_step(stream, "Route 203: Barry did not stop us at the entrance."); - } -} - } void add_jubilife_events(StoryEvents& events){ events.emplace("up_the_steps", console_event(up_the_steps)); - events.emplace("talk_to_wanderer", talk_to_wanderer); - events.emplace("jubilife_center_to_street", console_event(jubilife_center_to_street)); - events.emplace("east_to_barry", console_event(east_to_barry)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 9d913d9c95..5e294cfc30 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -539,6 +539,13 @@ Pose arrival(const char* map, TilePos tile, Direction direction){ Pose out_of_oreburgh_gate(){ return at(ROUTE_203, 55, 9, WEST); } +// Barry's coord event on Route 203 (Diamond's zone event narc_0329), column +// 5 on rows 17 to 20 (the street's rows 21 to 24) while +// VAR_ROUTE_203_RIVAL_STATE is 0. He walks up, battles and runs off, and +// we stay on it (assumed; the event's walk east stopped there, on any row). +Pose on_barrys_trigger(){ + return Pokemon::tile_pose(ROUTE_203, {TilePos{5, 17}, TilePos{5, 18}, TilePos{5, 19}, TilePos{5, 20}}, EAST); +} // Where Route 204's door (11, 1) lets us into the Ravaged Path: (19, 50), // as in r237a. The tap stops at the fade, on that tile. Pose into_the_ravaged_path(){ @@ -632,10 +639,11 @@ StoryCheckpoint route_204_to_floaroma(){ std::vector to_oreburgh(){ return { - row("01.01", "Jubilife to Barry", std::nullopt, { - event("jubilife_center_to_street", "out of the Center to the street east to Route 203, unless we stand there"), - event("east_to_barry", "east until Barry stops us at Route 203's entrance"), - battle("barry", true, "Barry's battle; a loss goes on, as it did on the console"), + // From 00.05c's end on the street east to Route 203. + row("01.01", "Jubilife to Barry", at(JUBILIFE, 58, 22, EAST), { + route("west_of_barrys_trigger", at(ROUTE_203, 4, 18, EAST), "east along the street onto Route 203", "town"), + step_into("barrys_trigger", EAST, "the trigger (5, 17..20): Barry walks over"), + anchored(battle("barry", true, "Barry's battle; a loss goes on, as it did on the console"), on_barrys_trigger()), }), row("01.02", "Route 203 to Oreburgh", std::nullopt, { event("route_203_east_to_the_gate", "east along Route 203, fighting the trainers who stop us"), From d839c4ba1777994a5887ac7a7a409878ebe6356a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 11:17:25 -0800 Subject: [PATCH 89/98] BDSP AutoStory: 01.02 on the shared steps 01.02 starts on Barry's trigger, where 01.01 ends, and is planned on Route 203's, the Gate's and Oreburgh's maps with the Diamond decomp's zone events (narc_0329, narc_0250, narc_0044): - a route east along Route 203 to the Gate's warp tile (55, 9), fighting the trainers who see us, then a tap east to the Gate's banner; - a route to (6, 22), a step onto the Hiker's coord event (7, 22), and a MASH scene for his HM Rock Smash and the Hidden Moves card after it (no dialogue box; the scene's B closes it, as the event's two B did); - a route east along row 22 to (27, 22), fighting Picnicker Diana and Camper Curtis, now on the Gate's trainer list with ranges that reach the row; a tap east to Oreburgh's banner; - a route to (9, 13), a step onto the coord event of the man at the entrance (10, 12..15), and a MASH scene for his walk to the Center and Barry's lines. Where it leaves us is not known, so the row ends there. Dropped: the met_hiker flag (the walk east meeting the Hiker on its way; the step onto his trigger is explicit now), and the walk east row by row past walls. Events_Route203.cpp had only these events and is gone. Stays named: nothing in 01.02. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events.cpp | 1 - .../AutoStory/PokemonBDSP_AutoStory_Events.h | 9 +- .../PokemonBDSP_AutoStory_Events_Route203.cpp | 201 ------------------ .../PokemonBDSP_AutoStory_StoryPlaces.cpp | 31 ++- .../PokemonBDSP_AutoStory_StoryTable.cpp | 43 +++- SerialPrograms/cmake/SourceFiles.cmake | 1 - 6 files changed, 63 insertions(+), 223 deletions(-) delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp index 2348d76216..0037449ea4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.cpp @@ -18,7 +18,6 @@ const StoryEvents& story_events(){ add_lake_events(ret); add_sandgem_events(ret); add_jubilife_events(ret); - add_route203_events(ret); add_oreburgh_events(ret); add_route204_events(ret); return ret; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h index 49bbc296c7..3923c369e3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -10,10 +10,10 @@ * are kept as they ran there. Each fails by throwing, with a screenshot; * the executor turns that into a failed step. * - * They are in files by area: _Events_Lake (00.00 to 00.03), _Sandgem - * (00.04), _Jubilife (00.05, 01.01), _Route203 (01.02), _Oreburgh (01.03 - * and 01.04) and _Route204 (01.09's Rock Smash, 01.11). 01.05 to 01.10 - * are planned walks in the story table. + * They are in files by area: _Events_Lake (00.00 to 00.04), _Sandgem + * (00.04), _Jubilife (00.05), _Oreburgh (01.03 and 01.04) and _Route204 + * (01.09's Rock Smash, 01.11). 01.01, 01.02 and 01.05 to 01.10 are + * planned walks in the story table. * */ @@ -46,7 +46,6 @@ StoryEvent console_event(std::function -#include "CommonFramework/GlobalAutoPaths.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "Common/Cpp/PrettyPrint.h" -#include "CommonFramework/Globals.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" -#include "PokemonBDSP_AutoStory_Talks.h" -#include "PokemonBDSP_AutoStory_Walks.h" -#include "PokemonBDSP_AutoStory_WildBattle.h" -#include "PokemonBDSP_AutoStory_Events.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; -using Pokemon::Direction; - - -namespace{ - -// The walk east along Route 203: who has stopped us so far. -struct Route203Walk{ - VideoStream& stream; - ProControllerContext& context; - size_t fights = 0; - bool inside = false; - // On Route 203 only trainers start a talk; one with no battle is the - // Hiker, who meets us inside the Gate (whose fade may not read as a - // transition). - bool met_hiker = false; -}; - -// Whoever stopped the walk: a trainer to fight, or the Hiker. -void route_203_stopped_by(Route203Walk& walk){ - if (talk_or_fight(walk.stream, walk.context, "Route 203 trainer " + std::to_string(walk.fights + 1))){ - walk.fights++; - }else{ - route_status(walk.stream, "No battle: that was the Hiker, inside the Oreburgh Gate."); - walk.met_hiker = true; - walk.inside = true; - } -} - -// At a wall: the row from which east goes on. North to the top wall first -// (a step north there does not move us, so rows are counted from it), then -// a row at a time south. At the route's end this is the corner by the -// second sign, with the Gate's opening two rows down. -void route_203_row_east_on(Route203Walk& walk){ - VideoStream& stream = walk.stream; - ProControllerContext& context = walk.context; - walk_until(stream, context, run_from_wild_battle, {0, 1}, 6000ms, "north to the top wall"); - bool moved_on = false; - for (size_t row = 1; row <= 10 && !moved_on; row++){ - step_south(stream, context); - const WalkResult next = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203, row " + std::to_string(row) + " below the top"); - if (next.stop == WalkStop::TRANSITION){ - walk.inside = true; - } - if (next.stop == WalkStop::DIALOG || (next.stop == WalkStop::BLOCKED && dialogue_opens(stream, context, 1500ms))){ - route_203_stopped_by(walk); - } - // Two still chunks are the least a wall takes; more is progress. - moved_on = walk.inside || next.stop != WalkStop::BLOCKED || next.walked > 1200ms; - } - if (!moved_on){ - fail_step(stream, "Route 203: walls east on every row down to ten below the top wall."); - } -} - - -// 01.02: east along Route 203 into the Oreburgh Gate, fighting whoever stops us. -Pokemon::Outcome route_203_east_to_the_gate(StoryExecutor& executor, Pokemon::RunState& state, const Pokemon::StoryStep&){ - VideoStream& stream = executor.stream(); - ProControllerContext& context = executor.context(); - // Route 203 only goes east, over three stretches joined by stairs, and - // the Gate's opening is in the cliff at its north-east end. Who stops us - // differs from run to run (a trainer's sight depends on our row), so - // there is no fixed list: walk east, talk or fight whoever stops us, and - // at a wall find a row from which east goes on. - Route203Walk walk{stream, context}; - for (size_t leg = 0; leg < 40 && !walk.inside; leg++){ - const WalkResult east = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east along Route 203"); - if (east.stop == WalkStop::TRANSITION){ - walk.inside = true; - break; - } - if (east.stop == WalkStop::LIMIT){ - continue; - } - if (east.stop == WalkStop::DIALOG || dialogue_opens(stream, context, 1500ms)){ - route_203_stopped_by(walk); - continue; - } - route_203_row_east_on(walk); - } - if (!walk.inside){ - fail_step(stream, "Route 203: did not reach the Oreburgh Gate."); - } - route_status(stream, "Route 203: " + std::to_string(walk.fights) + " trainer battle(s) on the way."); - context.wait_for(1500ms); - if (walk.met_hiker){ - state.flags.insert("met_hiker"); - } - return Pokemon::outcome_ok(); -} - -// 01.02: the Hiker, who gives HM Rock Smash, when the walk east has not met him. -void the_hiker(VideoStream& stream, ProControllerContext& context){ - // The Hiker steps up to us as we walk east. - const WalkResult to_hiker = walk_until(stream, context, run_from_wild_battle, {1, 0}, 4000ms, "to the Hiker"); - if (to_hiker.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ - fail_step(stream, "Oreburgh Gate: the Hiker did not stop us."); - } - finish_dialogue(stream, context, BUTTON_B, "Hiker"); -} - -// 01.02: the "Hidden Moves" card after the Hiker. -void hidden_moves_card(VideoStream& stream, ProControllerContext& context){ - // Then a "Hidden Moves" tutorial card, closed with B. It is no dialogue - // box, and it can come up a moment after his last line. - pbf_wait(context, 1500ms); - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - pbf_press_button(context, BUTTON_B, 80ms, 1000ms); - context.wait_for_all_requests(); -} - -// 01.02: east through the Gate past Curtis and Diana, out to Oreburgh City. -void gate_east_to_oreburgh(VideoStream& stream, ProControllerContext& context){ - // East: Camper Curtis and Picnicker Diana see us; past them the exit's - // white fade, and the man in Oreburgh stops us right outside. - size_t fights = 0; - bool city = false; - size_t stuck = 0; - for (size_t leg = 0; leg < 12 && !city; leg++){ - const WalkResult walk = walk_until(stream, context, run_from_wild_battle, {1, 0}, 10000ms, "east through the Oreburgh Gate"); - if (walk.stop == WalkStop::LIMIT || walk.stop == WalkStop::TRANSITION){ - continue; - } - if (walk.stop != WalkStop::DIALOG && !dialogue_opens(stream, context, 2000ms)){ - // A card or menu left up holds the screen still: B once, then retry. - if (++stuck > 1){ - fail_step(stream, "Oreburgh Gate: a wall stopped us walking east."); - } - pbf_press_button(context, BUTTON_B, 80ms, 1500ms); - continue; - } - stuck = 0; - if (fights >= 2){ - city = true; - break; - } - fights += talk_or_fight(stream, context, "Oreburgh Gate trainer " + std::to_string(fights + 1)) ? 1 : 0; - } - if (!city){ - fail_step(stream, "Oreburgh Gate: did not reach Oreburgh City."); - } -} - -// 01.02: the man at the city's entrance and Barry, at the Pokemon Center. -void oreburgh_welcome(VideoStream& stream, ProControllerContext& context){ - // He walks us to the Pokemon Center between his lines, and Barry joins - // there: more lines after each walk. - finish_dialogue(stream, context, BUTTON_B, "Man at Oreburgh's entrance"); - for (size_t scene = 0; scene < 6 && dialogue_opens(stream, context, 6000ms); scene++){ - finish_dialogue(stream, context, BUTTON_B, "Oreburgh scene"); - } - context.wait_for_all_requests(); -} - -} - - -void add_route203_events(StoryEvents& events){ - events.emplace("route_203_east_to_the_gate", route_203_east_to_the_gate); - events.emplace("the_hiker", console_event(the_hiker)); - events.emplace("hidden_moves_card", console_event(hidden_moves_card)); - events.emplace("gate_east_to_oreburgh", console_event(gate_east_to_oreburgh)); - events.emplace("oreburgh_welcome", console_event(oreburgh_welcome)); -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp index 3c05fa52b1..6496aa2f3d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -171,17 +171,22 @@ const std::vector& plan_worlds(){ return worlds; } +// Where the object `name` of the JSON map `map` stands. +TilePos object_tile(const char* map, const char* name){ + for (const Pokemon::MapObject& object : story_map(map).objects){ + if (object.name == name){ + return TilePos{object.x, object.y}; + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::string(map) + " has no " + name + "."); +} + // Route 203's trainers, from the Diamond decomp's events (zone event // narc_0329): where each stands on the JSON map, the way they look and // how far. The one by the Gate wanders in BDSP and is off the map. std::vector route_203_trainer_sights(){ const auto at = [](const char* name){ - for (const Pokemon::MapObject& object : story_map("Route203").objects){ - if (object.name == name){ - return TilePos{object.x, object.y}; - } - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::string("Route 203 has no ") + name + "."); + return object_tile("Route203", name); }; return { {.name = "Youngster (west)", .pos = at("youngster_1"), .facing = Direction::WEST, .range = 2}, @@ -191,6 +196,17 @@ std::vector route_203_trainer_sights(){ }; } +// The Oreburgh Gate's trainers (Diamond's zone event narc_0250): Picnicker +// Diana above row 22 and Camper Curtis below it. Both stopped the walk +// east along that row on the console (2026-10-29 to 10-31); the maps have +// no ranges, and 4 reaches the row from either. +std::vector oreburgh_gate_trainer_sights(){ + return { + {.name = "Picnicker Diana", .pos = object_tile("OreburghGate", "picnicker_0"), .facing = Direction::SOUTH, .range = 4}, + {.name = "Camper Curtis", .pos = object_tile("OreburghGate", "camper_1"), .facing = Direction::NORTH, .range = 4}, + }; +} + // The trainers of the JSON map `map`, in its own tiles. std::vector map_trainers(const std::string& map){ if (map == "Route202"){ @@ -199,6 +215,9 @@ std::vector map_trainers(const std::string& map){ if (map == "Route203"){ return route_203_trainer_sights(); } + if (map == "OreburghGate"){ + return oreburgh_gate_trainer_sights(); + } if (map == "Route204South"){ return route204_south_trainer_sights(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 5e294cfc30..dd14b44002 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -536,8 +536,8 @@ Pose arrival(const char* map, TilePos tile, Direction direction){ // tile only: from it and (54, 9) together the joined Route 203 and Jubilife // find no plan. The tap stops at the fade, which puts us on the warp tile; // r78 passed 01.06 from a save there (by the sign, the cave mouth east). -Pose out_of_oreburgh_gate(){ - return at(ROUTE_203, 55, 9, WEST); +Pose out_of_oreburgh_gate(Direction facing = WEST){ + return at(ROUTE_203, 55, 9, facing); } // Barry's coord event on Route 203 (Diamond's zone event narc_0329), column // 5 on rows 17 to 20 (the street's rows 21 to 24) while @@ -579,6 +579,37 @@ StoryCheckpoint oreburgh_to_route_203(){ }); } +// 01.02, planned on Route 203's, the Gate's and Oreburgh's maps with the +// Diamond decomp's zone events (narc_0329, narc_0250, narc_0044): east +// along Route 203 to the Gate, fighting the trainers who see us +// (route_203_trainer_sights()); the Hiker's coord event (7, 22) while +// VAR_OREBURGH_GATE_1F_HIKER_STATE is 1; east along row 22 past Diana and +// Curtis (oreburgh_gate_trainer_sights()); then the coord event of the man +// at Oreburgh's entrance, column 10 on rows 12 to 15 while +// VAR_OREBURGH_CITY_STATE is 0. Each mouth is a tap to its banner, as in +// 01.05; its warp's other end is the tile beside the far mouth. +StoryCheckpoint route_203_to_oreburgh(){ + return row("01.02", "Route 203 to Oreburgh", on_barrys_trigger(), { + route("by_the_gate", out_of_oreburgh_gate(EAST), "east along Route 203 to the Gate's warp tile (55, 9)"), + tap_into("into_oreburgh_gate", EAST, "the Gate's west mouth (56, 9)", arrival(GATE, {4, 22}, EAST)), + route("west_of_the_hikers_trigger", at(GATE, 6, 22, EAST), "east along row 22"), + step_into("hikers_trigger", EAST, "the trigger (7, 22): the Hiker walks down to us"), + // The "Hidden Moves" card after his lines is no dialogue box, and + // can come up a moment after them; the scene's B closes it. + scene("the_hiker", at(GATE, 7, 22, EAST), "HM Rock Smash, then the Hidden Moves card"), + route("by_the_east_mouth", at(GATE, 27, 22, EAST), "east along row 22, fighting Diana and Curtis"), + tap_into("into_oreburgh", EAST, "the Gate's east mouth (28, 22)", + arrival(OREBURGH, map_point(OREBURGH, "gate_mouth").next(EAST), EAST)), + route("west_of_the_welcome", at(OREBURGH, 9, 13, EAST), "east along row 13"), + step_into("welcome_trigger", EAST, "the trigger (10, 12..15): the man walks over"), + // He walks us to the Pokemon Center between his lines; Barry joins + // there with more lines after each walk. Where it leaves us is + // not known: "a row or two below the Center's front, a little west + // of its door" (01.03's save, 2026-10-29). + scene("oreburgh_welcome", std::nullopt, "the man's lines, the walk to the Center, and Barry's"), + }); +} + // Below Rowan's trigger in Jubilife: (47, 8), the east tile under the // trigger (45..47, 7), which (48, 7) beside it is not. Pose below_rowans_trigger(){ @@ -645,13 +676,7 @@ std::vector to_oreburgh(){ step_into("barrys_trigger", EAST, "the trigger (5, 17..20): Barry walks over"), anchored(battle("barry", true, "Barry's battle; a loss goes on, as it did on the console"), on_barrys_trigger()), }), - row("01.02", "Route 203 to Oreburgh", std::nullopt, { - event("route_203_east_to_the_gate", "east along Route 203, fighting the trainers who stop us"), - skip_if(event("the_hiker", "the Hiker in the Gate gives HM Rock Smash"), "met_hiker"), - event("hidden_moves_card", "the card after him, closed with B"), - event("gate_east_to_oreburgh", "east past Curtis and Diana into Oreburgh"), - event("oreburgh_welcome", "the man at the entrance, and Barry at the Center"), - }), + route_203_to_oreburgh(), row("01.03", "Oreburgh Mine to Roark", std::nullopt, { event("oreburgh_center_door", "in by the Center's door, talking to Barry in it"), event("south_into_the_mine", "straight south from the door into the mine"), diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index b8984e26f2..1c8f2bc453 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1680,7 +1680,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Lake.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route203.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_NewGame.cpp From 382d151e7eff53d8d6364c84eca4245b7e89b825 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 11:39:27 -0800 Subject: [PATCH 90/98] BDSP AutoStory: 01.02 plans Route 203 from one tile From all four of Barry's trigger rows at once the planner found no way east (StorySimulation's poses chain): walk south down the trigger's column to the cliff below row 20 first, as 01.08 does at its start. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index dd14b44002..728b7d44c3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -590,6 +590,11 @@ StoryCheckpoint oreburgh_to_route_203(){ // 01.05; its warp's other end is the tile beside the far mouth. StoryCheckpoint route_203_to_oreburgh(){ return row("01.02", "Route 203 to Oreburgh", on_barrys_trigger(), { + // One known tile to plan from: from all four of the trigger's rows + // at once the planner finds no plan. Its column is open from row + // 17 down to the cliff below row 20. + anchored(to_walls("south_to_the_cliff", {{.direction = SOUTH, .tiles = 4}}, "down the trigger's column to the cliff"), + at(ROUTE_203, 5, 20, SOUTH)), route("by_the_gate", out_of_oreburgh_gate(EAST), "east along Route 203 to the Gate's warp tile (55, 9)"), tap_into("into_oreburgh_gate", EAST, "the Gate's west mouth (56, 9)", arrival(GATE, {4, 22}, EAST)), route("west_of_the_hikers_trigger", at(GATE, 6, 22, EAST), "east along row 22"), From a68d2c1eef699a3a4d61fec94c7f399506bf953b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 11:44:35 -0800 Subject: [PATCH 91/98] BDSP AutoStory: 01.02 meets the Gate's trainers one route each In the grid sim (bdsp/sim, merged locally), the walk east along the Gate planned one leg to the wall on row 23, where both Curtis and Diana see it: Curtis was fought under Diana's name, and her stop on the walk on to the wall went unseen, so the route ended at (24, 23). Two routes now, to (20, 22) past Curtis's column and on to (27, 22) past Diana's. They come off the trainer list again: the grid sim does not know them beaten in 01.05, and they stopped its walk back west. The walker fights them as trainers the map lacks, one per route. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryPlaces.cpp | 31 ++++--------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 9 +++++- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp index 6496aa2f3d..3c05fa52b1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -171,22 +171,17 @@ const std::vector& plan_worlds(){ return worlds; } -// Where the object `name` of the JSON map `map` stands. -TilePos object_tile(const char* map, const char* name){ - for (const Pokemon::MapObject& object : story_map(map).objects){ - if (object.name == name){ - return TilePos{object.x, object.y}; - } - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::string(map) + " has no " + name + "."); -} - // Route 203's trainers, from the Diamond decomp's events (zone event // narc_0329): where each stands on the JSON map, the way they look and // how far. The one by the Gate wanders in BDSP and is off the map. std::vector route_203_trainer_sights(){ const auto at = [](const char* name){ - return object_tile("Route203", name); + for (const Pokemon::MapObject& object : story_map("Route203").objects){ + if (object.name == name){ + return TilePos{object.x, object.y}; + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::string("Route 203 has no ") + name + "."); }; return { {.name = "Youngster (west)", .pos = at("youngster_1"), .facing = Direction::WEST, .range = 2}, @@ -196,17 +191,6 @@ std::vector route_203_trainer_sights(){ }; } -// The Oreburgh Gate's trainers (Diamond's zone event narc_0250): Picnicker -// Diana above row 22 and Camper Curtis below it. Both stopped the walk -// east along that row on the console (2026-10-29 to 10-31); the maps have -// no ranges, and 4 reaches the row from either. -std::vector oreburgh_gate_trainer_sights(){ - return { - {.name = "Picnicker Diana", .pos = object_tile("OreburghGate", "picnicker_0"), .facing = Direction::SOUTH, .range = 4}, - {.name = "Camper Curtis", .pos = object_tile("OreburghGate", "camper_1"), .facing = Direction::NORTH, .range = 4}, - }; -} - // The trainers of the JSON map `map`, in its own tiles. std::vector map_trainers(const std::string& map){ if (map == "Route202"){ @@ -215,9 +199,6 @@ std::vector map_trainers(const std::string& map){ if (map == "Route203"){ return route_203_trainer_sights(); } - if (map == "OreburghGate"){ - return oreburgh_gate_trainer_sights(); - } if (map == "Route204South"){ return route204_south_trainer_sights(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 728b7d44c3..6f6e9667b2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -588,6 +588,12 @@ StoryCheckpoint oreburgh_to_route_203(){ // at Oreburgh's entrance, column 10 on rows 12 to 15 while // VAR_OREBURGH_CITY_STATE is 0. Each mouth is a tap to its banner, as in // 01.05; its warp's other end is the tile beside the far mouth. +// +// Diana and Curtis are not on the trainer list: the walker fights them as +// trainers the map lacks, one per route (in the grid sim, the second of +// two on one leg stopped its walk on to the wall unseen). On the list, +// the grid sim, which does not know them beaten, had them stop 01.05's +// walk back. StoryCheckpoint route_203_to_oreburgh(){ return row("01.02", "Route 203 to Oreburgh", on_barrys_trigger(), { // One known tile to plan from: from all four of the trigger's rows @@ -602,7 +608,8 @@ StoryCheckpoint route_203_to_oreburgh(){ // The "Hidden Moves" card after his lines is no dialogue box, and // can come up a moment after them; the scene's B closes it. scene("the_hiker", at(GATE, 7, 22, EAST), "HM Rock Smash, then the Hidden Moves card"), - route("by_the_east_mouth", at(GATE, 27, 22, EAST), "east along row 22, fighting Diana and Curtis"), + route("past_curtis", at(GATE, 20, 22, EAST), "east along row 22 past Camper Curtis's column (17), fighting him"), + route("by_the_east_mouth", at(GATE, 27, 22, EAST), "east past Picnicker Diana's column (24), fighting her"), tap_into("into_oreburgh", EAST, "the Gate's east mouth (28, 22)", arrival(OREBURGH, map_point(OREBURGH, "gate_mouth").next(EAST), EAST)), route("west_of_the_welcome", at(OREBURGH, 9, 13, EAST), "east along row 13"), From 534dc28119378c5b4b5b162f4f7536eac0f883dc Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:17:36 -0800 Subject: [PATCH 92/98] BDSP AutoStory: 01.03 on the shared steps 01.03 starts where Oreburgh's welcome scene leaves us (now 01.02's end too) and is planned on Oreburgh's map and the mine's coal room (Diamond's zone events narc_0044 and narc_0197): - east to the fence below the Center and north, the one known tile to plan from; two taps west below the door; Barry in it (TALK, MASH); - a route down the door's column, round the Hiker's patch, to above the mine's warp tiles (44..48, 59) and a step onto them. The generator writes those tiles as doors (WARP_DOORS), so no plan stands on them; - the entrance hall, which has no map, on 19 counted taps each way and a step onto its warp row: no hold that a wild battle could end; - in the coal room (now a room world): down the corridor to the boulder, a route to below Roark, his talk and scene, a route back to the warp; - out below the mine's sign (to the wall), a route to the Center's doorstep (46, 21), and the heal. 01.04 now starts there, outside, and its out_of_the_center event is gone. TALK waits 8 s for a line, not 3: Roark's first comes after the camera zooms in on us (r98). Dropped: the nudges round the Center's door and below Roark (they made up for stick holds; taps are whole tiles), the Roark-gone check by A after his scene (the scene rule's quiet overworld ends it), and the walks until a transition. Stays named: 01.04's gym_until_roark_beaten. After a loss it walks back from the Center keeping the battles' experience, which a reload of the row's save would take away. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/Maps/OreburghCity.json | 2 +- .../AutoStory/PokemonBDSP_AutoStory_Events.h | 4 +- .../PokemonBDSP_AutoStory_Events_Oreburgh.cpp | 390 +----------------- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 4 +- .../PokemonBDSP_AutoStory_StoryPlaces.cpp | 6 +- .../PokemonBDSP_AutoStory_StoryTable.cpp | 103 ++++- devtools/maps/bdsp_maps.py | 3 + 7 files changed, 105 insertions(+), 407 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json index 0043b0bea2..5df7f3b6d4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghCity.json @@ -61,7 +61,7 @@ "#####################################..####.#S....###.....######", "###########################################.......####.......###", "############################################.....#####.......###", - "############################################.....#####.....#####", + "############################################DDDDD#####.....#####", "######################################################..#..#####", "################################################################", "################################################################", diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h index 3923c369e3..8a47ffc628 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -11,8 +11,8 @@ * the executor turns that into a failed step. * * They are in files by area: _Events_Lake (00.00 to 00.04), _Sandgem - * (00.04), _Jubilife (00.05), _Oreburgh (01.03 and 01.04) and _Route204 - * (01.09's Rock Smash, 01.11). 01.01, 01.02 and 01.05 to 01.10 are + * (00.04), _Jubilife (00.05), _Oreburgh (01.04's Gym) and _Route204 + * (01.09's Rock Smash, 01.11). 01.01 to 01.03 and 01.05 to 01.10 are * planned walks in the story table. * */ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp index 7628d3ce8c..c7c85493a2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Oreburgh.cpp @@ -1,26 +1,17 @@ -/* BDSP AutoStory - Events: Oreburgh (01.03 to 01.04) +/* BDSP AutoStory - Events: Oreburgh Gym (01.04) * * From: https://github.com/PokemonAutomation/ * - * Segment 01's walks in Oreburgh: the mine and Roark, and the Gym, as - * they ran on the console (2026-10-29 to 10-31). + * 01.04's walk into the Oreburgh Gym and up it to Roark, again from the + * Pokemon Center after each loss, as it ran on the console (2026-10-29 to + * 10-31). A loss keeps the experience, which a reload of the row's save + * would not. * */ -#include -#include "CommonFramework/GlobalAutoPaths.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "Common/Cpp/PrettyPrint.h" -#include "CommonFramework/Globals.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h" -#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Talks.h" @@ -33,110 +24,10 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ using namespace std::chrono_literals; -using Pokemon::Direction; namespace{ -// The Center's floor: Oreburgh's sand reads up to 34 % warm outside -// (r189a), the floor 56 to 64 % inside. -bool oreburgh_center_floor(const ImageViewRGB32& screen){ - size_t percent = 0; - center_floor_on_screen(screen, &percent); - return percent >= 45; -} -bool oreburgh_center_inside_now(VideoStream& stream){ - const VideoSnapshot frame = stream.video().snapshot(); - return frame && oreburgh_center_floor(*frame.frame); -} - -// North into the Center's door from this column: whether we are inside. -// Someone in the door (Barry, the first time) is talked to and the walk -// tried again. -bool into_oreburgh_center_door(VideoStream& stream, ProControllerContext& context){ - // The door's fade often reads as the time limit, not a black screen - // (r183a went in and on to the counter): look at the floor. - // Stop as soon as the floor shows: walking on to the counter left the - // door's column, and the walk back out drifted into the yard (r188b). - const WalkResult in = walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", - oreburgh_center_floor); - if (in.stop == WalkStop::ARRIVED || ((in.stop == WalkStop::TRANSITION || in.stop == WalkStop::LIMIT) && oreburgh_center_inside_now(stream))){ - return true; - } - if (in.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, "Someone at the Center's door"); - }else{ - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - if (!dialogue_opens(stream, context, 2000ms)){ - return false; - } - finish_dialogue(stream, context, BUTTON_B, "Barry at the Center's door"); - context.wait_for(2000ms); // he runs off - } - // Talked in the door's column: the door is free now. - walk_until(stream, context, run_from_wild_battle, {0, 1}, 2500ms, "north into the Center's door", - oreburgh_center_floor); - return oreburgh_center_inside_now(stream); -} - -// Oreburgh: from a row or two below the Pokemon Center's front, west of -// the fence below its east corner, into the Center by its door, stopping -// just inside. Talks to Barry if he stands in the door (01.03, the first -// time). -void enter_oreburgh_center(VideoStream& stream, ProControllerContext& context){ - // The save stands us a row or two below the Center's front, a little - // west of its door. The posts and the road beside the Center make north - // from there unreliable (r191a, r192a walked up the road to the city's - // north exit), and counted taps east drift. The fence running south from - // the Center's east corner stops a walk east on these rows a tile east of - // the door (by hand): walk to it, north to the Center's front, tap back - // one, and shift one tap either side. - walk_to_wall(stream, context, run_from_wild_battle, {1, 0}, 5000ms, "east to the fence below the Center"); - walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 3000ms, "north to the Center's front"); - // The door wants us near its middle: a turn and a tap west (half a tile - // too far, by hand) or a turn alone leave north blocked by the facade - // beside it. The facade blocks north for two tiles either side of the - // door, so look for it in nudges of about a third of a tile. - route_turn(stream, context, Direction::WEST); - context.wait_for_all_requests(); - bool inside = false; - const int offsets[] = {0, -1, -2, 1, -3, 2, -4, 3}; // nudges from here - int nudged = 0; - for (size_t i = 0; i < 8 && !inside; i++){ - const int offset = offsets[i]; - if (offset != nudged){ - // No turn first: a turn is a tap too and moves us (see - // talk_with_nudges()); a nudge both turns and moves a little. - for (; nudged != offset; nudged += offset > nudged ? 1 : -1){ - pbf_move_left_joystick(context, {offset > nudged ? 1.0 : -1.0, 0}, ROUTE_NUDGE_TAP, 300ms); - } - context.wait_for_all_requests(); - } - inside = into_oreburgh_center_door(stream, context); - } - if (!inside || !oreburgh_center_inside_now(stream)){ - fail_step(stream, "Oreburgh: no column east of the save went into the Pokemon Center."); - } -} - -// Oreburgh: from just inside the Pokemon Center's door, up to the nurse -// and heal; ends at the counter facing her. The door is straight south of -// her (by hand, 2026-10-31: north from the door stopped at the counter). -void heal_at_oreburgh_counter(VideoStream& stream, ProControllerContext& context){ - if (walk_to_talk(stream, context, run_from_wild_battle, {0, 1}, 6000ms, "to the nurse").stop != WalkStop::DIALOG){ - fail_step(stream, "Oreburgh Pokemon Center: the nurse did not answer."); - } - // A throughout: Yes to "rest your Pokemon?" (B would answer No). - finish_dialogue(stream, context, BUTTON_A, "Nurse"); - // Healing shows no text box for a few seconds, which reads as the end; - // her "Thank you for waiting" lines follow it. - if (dialogue_opens(stream, context, 6000ms)){ - finish_dialogue(stream, context, BUTTON_B, "Nurse, after healing"); - } -} - - - // Oreburgh, from the street in front of the Pokemon Center (its door or its // front wall) into the gym, as walked by hand on 2026-10-29: west to the // cliff, south to the posts, a row up, then 17 tiles east is the gym door's @@ -218,271 +109,6 @@ bool oreburgh_gym_battles(VideoStream& stream, ProControllerContext& context){ fail_step(stream, "Oreburgh Gym: did not reach Roark."); } -// 01.03: into the Pokemon Center by its door (talking to Barry if he stands in it). -void oreburgh_center_door(VideoStream& stream, ProControllerContext& context){ - // Planned on Bulbapedia's Oreburgh map (devtools/maps/examples/ - // oreburgh_center_to_mine.png): the column of the Center's door runs - // straight south down the yard's stairs, between the conveyors, into - // the mine (walked north up it by hand, 2026-10-31). The door is the one - // place that pins that column: go in and back out, and we stand on it - // facing south. The save stands us two tiles west of the door and a row - // down; north stops against the Center's front or its post, then a tile - // at a time east until north goes in. Barry stands in the door the first - // time: walking into him reads as a wall, so A there; he tells us Roark - // went to the mine, which Roark needs before he answers (2026-10-29: he - // ignored A in r101 and r104 until then). A save after his talk has him - // gone already. - // - // Already in the Center (a save inside its door): nothing to do, as - // 01.01 does for its Center. - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - if (indoors(*screen.frame)){ - stream.log("[Route] 01.03: already in the Pokemon Center."); - return; - } - } - if (in_dim_cave(stream)){ - stream.log("[Route] 01.03: already in the mine."); - return; - } - enter_oreburgh_center(stream, context); - context.wait_for(1000ms); -} - -// 01.03: straight south from the door into the mine. -void south_into_the_mine(VideoStream& stream, ProControllerContext& context){ - // Straight back south: out of the door and on into the mine. Fades go - // unseen at times (r184b missed the mine's, r185a the Center's and walked - // on into the mine hall), so count: the Center's door and the mine are - // two transitions, or about 1 s inside and 8.6 s outside of walking (by - // hand). A worker walks about on the sand path and stops the walk as a - // wall would (r95, r96): wait for him to move and walk on. Step 3 copes - // with ending in the hall or in the corridor. - // A save in the mine's entrance hall: already there. - if (in_dim_cave(stream)){ - stream.log("[Route] 01.03: already in the mine."); - return; - } - bool in_mine = false; - size_t transitions = 0; - Milliseconds walked = 0ms; - for (size_t attempt = 0; attempt < 6 && !in_mine; attempt++){ - const WalkResult down = walk_dir(stream, context, Direction::SOUTH, 15000ms, "south out of the Center and into the mine"); - walked += down.walked; - if (down.stop == WalkStop::TRANSITION){ - transitions++; - context.wait_for(1000ms); - }else if (down.stop == WalkStop::DIALOG){ - talk_or_fight(stream, context, "Someone on the way to the mine"); - }else if (down.stop == WalkStop::BLOCKED){ - context.wait_for(2000ms); - } - // Counting alone took a walk that drifted into the yard (r188b) for the - // mine: it also has to look like the mine. - in_mine = (transitions >= 2 || walked >= 9500ms) && in_dim_cave(stream); - } - if (!in_mine){ - fail_step(stream, "Oreburgh: the walk south from the Center's door did not reach the mine."); - } - context.wait_for(1500ms); -} - -// 01.03: through the mine's entrance hall to the rocky corridor. -void mine_hall(VideoStream& stream, ProControllerContext& context){ - // If the fade into the mine was missed, we are already in the corridor - // and this walk stops at its boulder; otherwise it crosses the hall. - const WalkResult hall = walk_dir(stream, context, Direction::SOUTH, 10000ms, "through the mine's entrance hall"); - if (hall.stop == WalkStop::TRANSITION){ - context.wait_for(1500ms); - } -} - -// After a wild battle on the way: back to the east end of the bottom row, -// three taps west and north to the wall on his row (Maps/OreburghMine.json: -// columns 23 to 25 have that wall above). -void line_up_below_roark_again(VideoStream& stream, ProControllerContext& context){ - // The battle may leave us on his row or the bottom one. - walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom row"); - walk_to_wall(stream, context, run_from_wild_battle, {1, 0}, 6000ms, "east along the bottom row"); - route_turn(stream, context, Direction::WEST); - for (size_t tap = 0; tap < 3; tap++){ - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - } - walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 2000ms, "north to the wall on Roark's row"); -} - -// From his row east of his rock: west to the rock, down and two taps west -// below him, and A with nudges. Whether his talk opened. -bool talk_to_roark_from_his_row(VideoStream& stream, ProControllerContext& context){ - // A wild battle on the way eats into the walk's time (r100): walk again. - for (size_t walk = 0; walk < 3; walk++){ - if (walk_to_wall(stream, context, run_from_wild_battle, {-1, 0}, 6000ms, "west to the rock beside Roark").wild_battles == 0){ - break; - } - } - walk_to_wall(stream, context, run_from_wild_battle, {0, -1}, 2000ms, "south to the bottom wall"); - route_turn(stream, context, Direction::WEST); - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - pbf_move_left_joystick(context, {-1, 0}, 120ms, 300ms); - route_turn(stream, context, Direction::NORTH); - context.wait_for_all_requests(); - if (run_from_late_encounter(stream, context, run_from_wild_battle, "below Roark")){ - return false; - } - if (VideoSnapshot screen = stream.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; - Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-below-roark.png"); - } - // The camera zooms in and his first line, "OK, stand back and watch - // this.", can take some seconds (r98). - // His rocks answer A too ("It's a rugged rock..."): his talk is the - // one that zooms the camera in on us (by hand), which the rock's - // message does not. Compare the view above the dialogue box. - return talk_with_nudges(stream, context, run_from_wild_battle, Direction::NORTH, "Roark", 8000ms, - [&](const ImageViewRGB32& before, const ImageViewRGB32& after){ - const ImageFloatBox above_box{0.0, 0.0, 1.0, 0.6}; - const double change = walk_screen_change( - extract_box_reference(before, above_box), extract_box_reference(after, above_box) - ); - stream.log("[01.03] View change when the talk opened: " + std::to_string(change) + " (Roark zooms in)."); - return change > 35; // by hand: 50 for his zoom - }); -} - -// His scene (by hand, 2026-10-31): see roark_in_the_mine(). Fails if he -// is still there after it. -void wait_out_roarks_scene(VideoStream& stream, ProControllerContext& context){ - bool gone = false; - for (size_t round = 0; round < 3 && !gone; round++){ - for (size_t press = 0; press < 14; press++){ - pbf_press_button(context, BUTTON_B, 80ms, 2420ms); - } - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - gone = !dialogue_opens(stream, context, 2500ms); - } - if (!gone){ - fail_step(stream, "Oreburgh Mine: Roark is still there after the talk."); - } -} - -// From below Roark, back up the corridor and out of the mine. -void out_of_oreburgh_mine( - VideoStream& stream, ProControllerContext& context, const StoryWorld& maps, - RouteTrainers& no_trainers, const Pokemon::TilePos& roark -){ - const auto world = [&](const Pokemon::TilePos& pos){ return maps.to_world(MAP_OREBURGH_MINE, pos); }; - walk_dir(stream, context, Direction::SOUTH, 2000ms, "back to the bottom wall"); - std::vector at{roark.next(Direction::SOUTH)}; - - // Back up the corridor, then north out through the hall. A route leg - // north to the top's wall walks on through it into the hall (r99), and - // no wall stops us a row short of it, so the route ends on the corner - // at (14,10), where that leg started, and the walk does the rest. - const Pokemon::TilePos exit = world(map_point("OreburghMine", "corridor_exit_start")); - at = walk_route(stream, context, run_from_wild_battle, maps, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos == exit; }, "Back to the corridor's top", {}, route_plan_costs()}); - // Up into the entrance hall (a transition, as on the way in), then on - // north out of the mine: in r103 that walk left the mine without a - // transition we saw and ended against the sign above the stairs, which - // the moving belts make read as the time limit. - if (walk_dir(stream, context, Direction::NORTH, 10000ms, "into the entrance hall").stop != WalkStop::TRANSITION){ - fail_step(stream, "Oreburgh Mine: walking north from the corridor did not reach the entrance hall."); - } - context.wait_for(1500ms); - if (walk_dir(stream, context, Direction::NORTH, 10000ms, "out of the mine").stop == WalkStop::DIALOG){ - fail_step(stream, "Oreburgh Mine: someone stopped us on the way out of the mine."); - } - context.wait_for(1500ms); -} - -// 01.03: the planned route down the corridor to Roark, his talk, and back out of the mine. -void roark_in_the_mine(VideoStream& stream, ProControllerContext& context){ - // Planned on the coal room's tile grid (Maps/OreburghMine.json): the - // corridor's rocks zigzag a row at a time, the room's second row passes - // under the worker on the first, and the east side leads down to the - // bottom row below Roark. - const StoryWorld& maps = oreburgh_mine_maps(); - auto world = [&](const Pokemon::TilePos& pos){ return maps.to_world(MAP_OREBURGH_MINE, pos); }; - RouteTrainers no_trainers{{}, [&](const std::string& who){ talk_or_fight(stream, context, who); }}; - std::vector at; - for (const Pokemon::TilePos& pos : map_region("OreburghMine", "corridor_top")){ - at.push_back(world(pos)); - } - // Roark stands with a rock on each side, so the rock east of him is the - // anchor: plan to his row's open stretch east of the rock, walk north to - // the wall above it and west until the rock stops us, two tiles east of - // him (on the console, r97). Then south to the bottom wall and two tiles - // west puts us below him. - const Pokemon::TilePos roark = world(map_point("OreburghMine", "roark")); - at = walk_route(stream, context, run_from_wild_battle, maps, at, no_trainers, RouteWalkPlan{[&](const Pokemon::TilePos& pos){ return pos.y >= roark.y && pos.y <= roark.y + 1 && pos.x >= roark.x + 2 && pos.x <= roark.x + 7; }, "To the stretch east of Roark", {}, route_plan_costs()}); - walk_to_wall(stream, context, run_from_wild_battle, {0, 1}, 2000ms, "north to the wall on Roark's row"); - // His rock and the bottom wall end these legs (see walk_to_wall()). The - // two taps west from there land within a third of a tile or so of his - // column (s03e2 missed him by that much), so look for him with nudges - // either side. Any tap here can start a wild battle, which leaves us - // off his column (r180a, even on his row west of his rock): then line up - // again from the east end of the bottom row, three taps west and north to the wall on his row - // (Maps/OreburghMine.json: columns 23 to 25 have that wall above). - bool talked = false; - for (size_t attempt = 0; attempt < 3 && !talked; attempt++){ - if (attempt > 0){ - // The battle may leave us on his row or the bottom one. - line_up_below_roark_again(stream, context); - } - talked = talk_to_roark_from_his_row(stream, context); - } - if (!talked){ - fail_step(stream, "Oreburgh Mine: no talk opened below Roark (has Barry told us about the mine?)."); - } - // His scene (by hand, 2026-10-31): the camera zooms in, about eight - // lines that each wait for a press ("OK, stand back and watch this.", - // Rock Smash, "...That'd be me!"), then he smashes his rock and leaves. - // finish_dialogue() ended early on it, and stepping onto his tile could - // not tell him from the time the stop takes to read (r184, r186 "passed" - // with him still there, and Barry kept the gym shut). Press B through it - // on a clock, then ask his tile: once he is gone, A there opens nothing. - // B and A in the overworld facing a wall do nothing. - wait_out_roarks_scene(stream, context); - out_of_oreburgh_mine(stream, context, maps, no_trainers, roark); -} - -// 01.03: into the Pokemon Center and heal at the counter. -void oreburgh_center_heal(VideoStream& stream, ProControllerContext& context){ - // End at the nurse, healed: the counter is a fixed start for 01.04, - // where the mine's mouth is not (2026-10-31, at the user's request). Out - // of the mine we stand below its sign; a tile east is the Center door's - // column, and north on it goes in, or stops on the front rows. - hold_tiles(stream, context, Direction::EAST, 1); - walk_until(stream, context, run_from_wild_battle, {0, 1}, 12000ms, "north into the Center's door", - [](const ImageViewRGB32& screen){ size_t percent = 0; center_floor_on_screen(screen, &percent); return percent >= 45; }); - size_t floor = 0; - if (VideoSnapshot frame = stream.video().snapshot()){ - center_floor_on_screen(*frame.frame, &floor); - } - if (floor < 45){ - route_turn(stream, context, Direction::SOUTH); - pbf_move_left_joystick(context, {0, -1}, 480ms, 300ms); - enter_oreburgh_center(stream, context); - } - heal_at_oreburgh_counter(stream, context); -} - -// 01.04: out of the Pokemon Center from the counter. -void out_of_the_center(VideoStream& stream, ProControllerContext& context){ - // 01.03 leaves us at the Center's counter, facing the nurse; the door - // is straight south. Not on the Center's floor (a save in the gym): - // nothing to leave. - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - if (!center_floor_on_screen(*screen.frame)){ - stream.log("[Route] 01.04: not in the Center."); - return; - } - } - walk_out_of_center(stream, context, "Oreburgh"); -} - // 01.04: west along the row into the gym and up it to Roark, again from the Center after a loss. void gym_until_roark_beaten(VideoStream& stream, ProControllerContext& context){ // Losing costs some money and keeps the experience, so each try comes @@ -506,12 +132,6 @@ void gym_until_roark_beaten(VideoStream& stream, ProControllerContext& context){ void add_oreburgh_events(StoryEvents& events){ - events.emplace("oreburgh_center_door", console_event(oreburgh_center_door)); - events.emplace("south_into_the_mine", console_event(south_into_the_mine)); - events.emplace("mine_hall", console_event(mine_hall)); - events.emplace("roark_in_the_mine", console_event(roark_in_the_mine)); - events.emplace("oreburgh_center_heal", console_event(oreburgh_center_heal)); - events.emplace("out_of_the_center", console_event(out_of_the_center)); events.emplace("gym_until_roark_beaten", console_event(gym_until_roark_beaten)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index aab4658b3d..e87167f910 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -474,8 +474,10 @@ Outcome StoryExecutor::talk(RunState& state, const StoryStep& step){ } pbf_press_button(context(), BUTTON_A, 80ms, 200ms); context().wait_for_all_requests(); + // Roark's first line in the mine comes after the camera zooms in on + // us, which can take some seconds (r98; the old event waited 8 s). ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream(), context(), 3000ms, {{dialog}}) != 0){ + if (wait_until(stream(), context(), 8000ms, {{dialog}}) != 0){ fail_step(stream(), label_of(step) + ": nobody answered."); } return Pokemon::outcome_ok(); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp index 3c05fa52b1..78c7b02584 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -130,12 +130,12 @@ Pokemon::MapData ravaged_path_smashed(){ return map; } -// The interiors (houses, the lab, the Gym, the Gate, the Ravaged Path), -// each its own world, named by its JSON map. +// The interiors (houses, the lab, the Gym, the Gate, the mine's coal room, +// the Ravaged Path), each its own world, named by its JSON map. const std::map& room_worlds(){ static const std::map rooms = []{ std::map ret; - for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab", "OreburghGym", "OreburghGate"}){ + for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab", "OreburghGym", "OreburghGate", "OreburghMine"}){ ret[room].add(room, story_map(room), {0, 0}); } ret["RavagedPath"].add("RavagedPath", ravaged_path_smashed(), {0, 0}); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 6f6e9667b2..4476af2198 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -59,6 +59,10 @@ const char* const JUBILIFE = "JubilifeCity"; const char* const TRAINERS_SCHOOL = "TrainersSchool"; const char* const OREBURGH = "OreburghCity"; const char* const GYM = "OreburghGym"; +// The mine's entrance hall (Diamond's MAP_OREBURGH_MINE_B1F) has no map: +// a pose there names it. The coal room below it, Roark's, does. +const char* const MINE_HALL = "OreburghMineB1F"; +const char* const MINE = "OreburghMine"; const char* const GATE = "OreburghGate"; const char* const ROUTE_203 = "Route203"; const char* const ROUTE_204_SOUTH = "Route204South"; @@ -552,6 +556,20 @@ Pose into_the_ravaged_path(){ return at(RAVAGED_PATH, "entrance", NORTH); } +// Where Oreburgh's welcome scene leaves us, Barry in the Center's door: "a row +// or two below the Center's front, a little west of its door" (01.03's +// save, 2026-10-29); "two tiles west of the door and a row down" (the old +// event). Not Diamond's: its man walks us to the Gym, (23, 23). +Pose after_the_welcome(){ + return Pokemon::tile_pose(OREBURGH, { + TilePos{43, 21}, TilePos{44, 21}, TilePos{45, 21}, TilePos{43, 22}, TilePos{44, 22}, TilePos{45, 22} + }); +} +// Out of Oreburgh's Pokemon Center: on (46, 21), facing south (r217d, r218d). +Pose out_of_oreburgh_center(){ + return at(OREBURGH, "pokemon_center_doorstep", SOUTH); +} + // 01.05, planned on Oreburgh's and the Gate's maps with the Diamond decomp's // zone events (narc_0044, narc_0250) and Platinum's script for the scene // (scripts_oreburgh_city.s, the same trigger and variable). Barry's coord @@ -615,10 +633,68 @@ StoryCheckpoint route_203_to_oreburgh(){ route("west_of_the_welcome", at(OREBURGH, 9, 13, EAST), "east along row 13"), step_into("welcome_trigger", EAST, "the trigger (10, 12..15): the man walks over"), // He walks us to the Pokemon Center between his lines; Barry joins - // there with more lines after each walk. Where it leaves us is - // not known: "a row or two below the Center's front, a little west - // of its door" (01.03's save, 2026-10-29). - scene("oreburgh_welcome", std::nullopt, "the man's lines, the walk to the Center, and Barry's"), + // there with more lines after each walk. + scene("oreburgh_welcome", after_the_welcome(), "the man's lines, the walk to the Center, and Barry's"), + }); +} + +// 01.03, planned on Oreburgh's map and the coal room's (Diamond's zone +// events narc_0044 and narc_0197): Barry in the Center's door says Roark +// went to the mine (Roark ignored A until then, r101 and r104); down the +// door's column to the mine's warp tiles (44..48, 59); straight down the +// entrance hall, which has no map, to its warps (11..13, 21); in the coal +// room down to Roark, whose scene ends with him gone (Platinum's +// scripts_oreburgh_mine_b2f.s: he smashes the rock east of him and walks +// off east); back up and out; and the Center's nurse, so 01.04 starts on +// the doorstep. The Center's door is column 46 on the console: we came out +// of it on (46, 21), 20 taps east of the Gym's door (r217d, r218d), and +// north from below the mine's sign and a tile east went in (2026-10-31). +// +// The hall is walked on counted taps, not held: it is a cave, and a wild +// battle on a hold toward a fade would read as the fade. Its warp tiles +// are only entered on the step after, which meets no wild Pokemon. Down, +// from the city's (46, 59) we land on its (12, 1); up, from the coal +// room's warp we land on its (11, 21) and come out on the city's (45, 59), +// below the mine's sign (the old walk north stopped against it, r103). +StoryCheckpoint oreburgh_mine_to_roark(){ + const Pose below_roark = next_to(MINE, "roark", SOUTH, NORTH); + return row("01.03", "Oreburgh Mine to Roark", after_the_welcome(), { + // The fence south of the Center's east corner (column 49) stops a + // walk east on these rows a tile east of the door, as by hand. + anchored(to_walls("to_the_centers_fence", {{.direction = EAST, .tiles = 6}, {.direction = NORTH, .tiles = 2}}, + "east to the fence below the Center's east corner, north to the Center's front"), at(OREBURGH, 48, 21, NORTH)), + anchored(legs("below_the_centers_door", {{.direction = WEST, .tiles = 2}}, "taps west below the door"), + at(OREBURGH, "pokemon_center_doorstep", NORTH)), + // He stands in the door the first time; walking into him reads as a wall. + talk("barry", "Barry in the Center's door"), + scene("barrys_lines", at(OREBURGH, "pokemon_center_doorstep", NORTH), "Roark has gone to the mine"), + // The Hiker wanders (46..48, 25..27), in the door's column: the + // route goes round him. + route("above_the_mine", at(OREBURGH, 46, 58, SOUTH), "down the door's column to above the mine's warp tiles", "town"), + step_into("into_the_mine", SOUTH, "the warp tile (46, 59)", inside(MINE_HALL)), + // We land on the warp tile we came by, (12, 1): 19 taps end on row + // 20, a step short of the warp row. + legs("down_the_hall", {{.direction = SOUTH, .tiles = 19}}, "down column 12 of the entrance hall"), + step_into("into_the_coal_room", SOUTH, "the hall's warp tiles (11..13, 21)", + Pokemon::tile_pose(MINE, {TilePos{15, 2}, TilePos{15, 1}}, SOUTH)), + // One known tile to plan from: the corridor's column 15 ends at the + // boulder (15, 10), where the old walk south stopped too. + anchored(to_walls("down_to_the_boulder", {{.direction = SOUTH, .tiles = 9}}, "down the corridor to the boulder (15, 10)"), + at(MINE, 15, 9, SOUTH)), + route("below_roark", below_roark, "down the corridor and the room's east side, west along the bottom row"), + // His first line comes after the camera zooms in on us (r98); his + // rocks either side answer A too, but he is the one north of us. + talk("roark", "Roark, north of us between his rocks"), + scene("roark_smashes_the_rock", below_roark, "his lines, Rock Smash on the rock east of him, and he walks off"), + route("below_the_coal_rooms_warp", at(MINE, 15, 2, NORTH), "back up the corridor"), + step_into("up_to_the_hall", NORTH, "the warp tile (15, 1)", inside(MINE_HALL)), + legs("up_the_hall", {{.direction = NORTH, .tiles = 19}}, "up column 11 of the entrance hall"), + step_into("out_of_the_mine", NORTH, "the hall's warp tiles (11..13, 1)", + Pokemon::tile_pose(OREBURGH, {TilePos{45, 59}, TilePos{45, 58}}, NORTH)), + anchored(to_walls("up_to_the_mines_sign", {{.direction = NORTH, .tiles = 3}}, "north against the mine's sign (45, 56)"), + at(OREBURGH, 45, 57, NORTH)), + route("below_the_centers_door_again", at(OREBURGH, "pokemon_center_doorstep", NORTH), "up to below the Center's door", "town"), + heal("heal_oreburgh", "the nurse; out onto the doorstep, where 01.04 starts", out_of_oreburgh_center()), }); } @@ -689,16 +765,13 @@ std::vector to_oreburgh(){ anchored(battle("barry", true, "Barry's battle; a loss goes on, as it did on the console"), on_barrys_trigger()), }), route_203_to_oreburgh(), - row("01.03", "Oreburgh Mine to Roark", std::nullopt, { - event("oreburgh_center_door", "in by the Center's door, talking to Barry in it"), - event("south_into_the_mine", "straight south from the door into the mine"), - event("mine_hall", "through the entrance hall"), - event("roark_in_the_mine", "down the corridor to Roark, his talk, and back out"), - event("oreburgh_center_heal", "the Center's nurse: 01.04 starts at her counter"), - }), - row("01.04", "Oreburgh Gym", std::nullopt, { - event("out_of_the_center", "out from the counter"), - event("gym_until_roark_beaten", "west into the gym, up it to Roark; again from the Center after a loss"), + oreburgh_mine_to_roark(), + // Stays an event: after a loss it walks back from the Center with + // the experience the battles gave, which a reload of this row's + // save would take away (Roark won twice by hand before he lost). + row("01.04", "Oreburgh Gym", out_of_oreburgh_center(), { + event("gym_until_roark_beaten", "west into the gym, up it to Roark; again from the Center after a loss", + next_to(GYM, "roark", SOUTH, NORTH)), }), oreburgh_to_route_203(), }; @@ -743,7 +816,7 @@ std::vector to_floaroma(){ // The map the save screen's card names at each row's start, as the console // read it there (lab-B's logs, 2026-10-31 to 11-02). A building's card -// names its town: 00.04 starts in Rowan's lab, 01.04 at Oreburgh's nurse. +// names its town: 00.04 starts in Rowan's lab. const std::map& start_save_cards(){ static const std::map cards{ {"00.01", "Twinleaf Town"}, diff --git a/devtools/maps/bdsp_maps.py b/devtools/maps/bdsp_maps.py index 6454bb3b47..2f5c1fdf61 100644 --- a/devtools/maps/bdsp_maps.py +++ b/devtools/maps/bdsp_maps.py @@ -411,6 +411,9 @@ def box(x0, y0, x1, y1): 'JubilifeCity': [(0, 22), (0, 23), (40, 40), (21, 42)], # The way up to the entrance hall: stepping on it leaves the room. 'OreburghMine': [(15, 1)], + # The mine's mouth: 01.03 walks down to the row above it and steps on. + # We come out on it too, and walk north off it to a wall first. + 'OreburghCity': [(x, 59) for x in range(44, 49)], } From 72b9552334d89841573a1317bdd1e77f67a77497 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:19:11 -0800 Subject: [PATCH 93/98] BDSP AutoStory: 01.11 on the shared steps 01.11 starts at 01.10's end, (11, 29) facing north, and walks as the console did (r250a, r251a): 8 taps west along row 29 (LEGS), north until the old woman stops us (to_wall, anchored below her at (3, 26)), her talk; a tap west (LEGS, anchored below the old man at (2, 26), facing north, so the talk's turn only turns), his talk. Dropped: the event's tolerance of one uncounted tap west (7 of 8); LEGS wants all 8. Events_Route204.cpp keeps only 01.09's Rock Smash. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events.h | 4 +-- .../PokemonBDSP_AutoStory_Events_Route204.cpp | 36 ++----------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 31 +++++++++++----- 3 files changed, 26 insertions(+), 45 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h index 8a47ffc628..b58b8914f7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -12,8 +12,8 @@ * * They are in files by area: _Events_Lake (00.00 to 00.04), _Sandgem * (00.04), _Jubilife (00.05), _Oreburgh (01.04's Gym) and _Route204 - * (01.09's Rock Smash, 01.11). 01.01 to 01.03 and 01.05 to 01.10 are - * planned walks in the story table. + * (01.09's Rock Smash). 01.01 to 01.03 and 01.05 to 01.11 are planned + * walks in the story table. * */ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp index 157c99cbcc..f7975a6685 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Route204.cpp @@ -1,9 +1,8 @@ -/* BDSP AutoStory - Events: Route 204 to Floaroma (01.09, 01.11) +/* BDSP AutoStory - Events: the Ravaged Path (01.09) * * From: https://github.com/PokemonAutomation/ * - * The Ravaged Path's Rock Smash, and segment 01's walks to the old couple - * in Floaroma's flower field, as they ran on the console (2026-10-31). + * The Ravaged Path's Rock Smash, as it ran on the console (2026-10-31). * */ @@ -72,42 +71,11 @@ void smash_the_rock(VideoStream& stream, ProControllerContext& context){ } } -// 01.11: west 8 along the flower field's bottom row, north until the old woman stops us. -void west_8_north_to_the_old_woman(VideoStream& stream, ProControllerContext& context){ - { - const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, {-1, 0}, 8, "west 8 to the flower field"); - if (west.stop != WalkStop::LIMIT || west.moved_chunks < 7){ - fail_step(stream, "Floaroma: west 8 to the flower field moved " + std::to_string(west.moved_chunks) + " tiles."); - } - // Blocked, or out of taps with the last ones bumping: either way - // she is in front of us (r250a: 3 steps, 2 bumps, "limit"). - const WalkResult north = walk_tiles_dpad(stream, context, run_from_wild_battle, {0, 1}, 5, "north until the old woman"); - if (north.stop != WalkStop::BLOCKED && north.moved_chunks >= 5){ - fail_step(stream, "Floaroma: nothing stopped the walk north to the old woman."); - } - } -} - -// 01.11: one tile west, then a tap north: the old man stands there, so the tap only turns. -void below_the_old_man(VideoStream& stream, ProControllerContext& context){ - const WalkResult west = walk_tiles_dpad(stream, context, run_from_wild_battle, {-1, 0}, 1, "west 1 below the old man"); - if (west.moved_chunks < 1){ - fail_step(stream, "Floaroma: the step west below the old man did not move."); - } - // He stands there, so one tap only turns us to him; the talk after fails - // if nobody answers. No counting: Floaroma's cloud shadows read as moves - // (r84: blocked taps counted, view change 20). - pbf_press_dpad(context, DPAD_UP, 160ms, 500ms); - context.wait_for_all_requests(); -} - } void add_route204_events(StoryEvents& events){ events.emplace("smash_the_rock", console_event(smash_the_rock)); - events.emplace("west_8_north_to_the_old_woman", console_event(west_8_north_to_the_old_woman)); - events.emplace("below_the_old_man", console_event(below_the_old_man)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 4476af2198..33fd1562df 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -777,6 +777,27 @@ std::vector to_oreburgh(){ }; } +// 01.11 on Floaroma's map: the old woman (3, 25) and the old man (2, 25) +// west of her stand still above the flower field's bottom row (r251a). +// The walk to her is the one that ran on the console: 8 taps west along +// row 29, then north until she stops us (r250a: 3 steps, then bumps). +StoryCheckpoint floaromas_old_couple(){ + const Pose below_the_old_woman = next_to(FLOAROMA, "old_woman", SOUTH, NORTH); + const Pose below_the_old_man = next_to(FLOAROMA, "old_man", SOUTH, NORTH); + return row("01.11", "Floaroma's old couple", at(FLOAROMA, "route_204_arrival", NORTH), { + legs("west_along_the_field", {{.direction = WEST, .tiles = 8}}, "8 taps west along row 29"), + anchored(to_walls("up_to_the_old_woman", {{.direction = NORTH, .tiles = 5}}, "north until she stops us"), + below_the_old_woman), + talk("old_woman", "she gives Mew when a Let's Go save is on the console"), + scene("old_woman_talks", below_the_old_woman, "her lines"), + // He stands above that tile, so talk() turning north only turns. + anchored(legs("below_the_old_man", {{.direction = WEST, .tiles = 1}}, "a tile west"), below_the_old_man), + talk("old_man", "he gives Jirachi with a Sword/Shield save"), + scene("old_man_talks", below_the_old_man, "his lines"), + save(), + }); +} + std::vector to_floaroma(){ return { row("01.06", "Route 203 to Jubilife", out_of_oreburgh_gate(), { @@ -802,15 +823,7 @@ std::vector to_floaroma(){ }), through_the_ravaged_path(), route_204_to_floaroma(), - row("01.11", "Floaroma's old couple", std::nullopt, { - event("west_8_north_to_the_old_woman", "west 8, north until she stops us"), - talk("old_woman", "she gives Mew when a Let's Go save is on the console"), - scene("old_woman_talks", std::nullopt, "her lines"), - event("below_the_old_man", "a tile west, a tap north"), - talk("old_man", "he gives Jirachi with a Sword/Shield save"), - scene("old_man_talks", std::nullopt, "his lines"), - save(), - }), + floaromas_old_couple(), }; } From 2aba355d67d5e3d13ceca9b9db7fbe4aa1d6f556 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:20:29 -0800 Subject: [PATCH 94/98] BDSP AutoStory: 00.05's steps on the shared steps 00.05 starts from a save at (48, 62), below Jubilife's steps (45..47, 61) against the wall east of them, where 00.04d's Dawn scene now says it leaves us: the old event went "a tile west, then up the steps" from there, two taps north (2026-10-30). That tile is the event's numbers on the map, not a measured one (#134). up_the_steps is a LEGS step of the same taps, a tile west and two north, ending on (47, 60); the route to the Center plans from that one tile, not the steps' region (46..47, 58..60). Events_Jubilife.cpp keeps only 00.05b's wandering clowns. The Events header lists what stays named and why. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory_Events.h | 17 +++++------ .../PokemonBDSP_AutoStory_Events_Jubilife.cpp | 24 ++-------------- .../PokemonBDSP_AutoStory_StoryTable.cpp | 28 ++++++++++++++----- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h index b58b8914f7..eb34744814 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events.h @@ -4,16 +4,17 @@ * * The story's steps that only BDSP has, by name: a step of the table whose * variant names one of these runs it (StoryExecutor::run()). Some are the - * scripted scenes (the lake's briefcase, the starter, Dawn's catching - * tutorial); most are segment 01's walks, which were measured on the - * console as holds, taps and walks until stopped rather than planned, and - * are kept as they ran there. Each fails by throwing, with a screenshot; - * the executor turns that into a failed step. + * scripted scenes (the Azure Flute, the lake's briefcase, the starter, + * Dawn's catching tutorial and the catch); the rest do what the shared + * steps cannot, as they ran on the console: the Poke Mart lady's Potions + * and the stairs into Jubilife (00.04c, 00.04d), a clown who wanders + * (00.05b), the Gym's tries from the Pokemon Center (01.04) and Rock Smash + * (01.09). Each fails by throwing, with a screenshot; the executor turns + * that into a failed step. * * They are in files by area: _Events_Lake (00.00 to 00.04), _Sandgem - * (00.04), _Jubilife (00.05), _Oreburgh (01.04's Gym) and _Route204 - * (01.09's Rock Smash). 01.01 to 01.03 and 01.05 to 01.11 are planned - * walks in the story table. + * (00.04c, 00.04d), _Jubilife (00.05b), _Oreburgh (01.04) and _Route204 + * (01.09). * */ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp index 9919b71635..af124bdb90 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Jubilife.cpp @@ -1,9 +1,9 @@ -/* BDSP AutoStory - Events: Jubilife City (00.05) +/* BDSP AutoStory - Events: Jubilife City (00.05b) * * From: https://github.com/PokemonAutomation/ * - * The Jubilife beats of 00.05 the story table cannot plan: up the steps, - * and the talk to a wandering clown. + * The Jubilife beat of 00.05b the story table cannot plan: the talk to a + * wandering clown. * */ @@ -31,23 +31,6 @@ using Pokemon::Direction; namespace{ -// 00.05: from the save below Jubilife's steps, against the wall east of -// them: a tile west (the first press only turns), then up the steps. -void up_the_steps(VideoStream& stream, ProControllerContext& context){ - // D-pad taps, a tile each: the stick holds this was (200 ms west, 480 ms - // north) left us between tiles, and every leg after it a column off - // (r57 to r61). By hand: a tap west and taps north from the save land - // on the map's tiles (between the lamps at x 43 and 49 after six). - route_turn(stream, context, Direction::WEST); - walk_tiles_dpad(stream, context, run_from_wild_battle, {-1, 0}, 1, "To the steps"); - route_turn(stream, context, Direction::NORTH); - const WalkResult up = walk_tiles_dpad(stream, context, run_from_wild_battle, {0, 1}, 2, "Up the steps"); - if (up.stop == WalkStop::DIALOG || up.stop == WalkStop::TRANSITION){ - fail_step(stream, std::string("Up the steps: ") + walk_stop_name(up.stop) + "."); - } -} - - // 00.05b: a talk to someone who wanders a tile either side of home along a // row (the clowns by the Mart and the Poketch Company, WANDER_AROUND with // x range 1). Waiting under home alone missed him for a minute (r66: he @@ -103,7 +86,6 @@ Pokemon::Outcome talk_to_wanderer(StoryExecutor& executor, Pokemon::RunState&, c void add_jubilife_events(StoryEvents& events){ - events.emplace("up_the_steps", console_event(up_the_steps)); events.emplace("talk_to_wanderer", talk_to_wanderer); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 33fd1562df..a6c2906d84 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -11,9 +11,9 @@ * on the console (r72 to r261, 2026-10-30); 00.05 to 00.05c, measured there * first, are now planned on Jubilife's map with the people and triggers of * the Diamond decomp's zone events and scripts. Segment 01 ran on the - * console end to end (2026-10-31), its walks as events; 01.05 to 01.10 - * are now planned on the maps the same way (2026-11-02), and have not - * run on a console as plans yet. + * console end to end (2026-10-31), its walks as events; all of it but + * 01.04's Gym is now planned on the maps the same way (2026-11-02), and + * has not run on a console as plans yet. * */ @@ -422,6 +422,16 @@ std::vector to_the_catch(){ }; } +// Where Dawn's scene leaves 00.04d, and 00.05's save: below Jubilife's +// steps (45..47, 61), against the wall east of them, (48, 62). BDSP starts +// her scene as we come in below the steps, not on Diamond's trigger above +// them (45..48, 60): 00.05's old event went "a tile west, then up the +// steps" from its save, two taps north (2026-10-30). Not measured as a +// tile: the event's numbers and the map's (#134). +Pose below_jubilifes_steps(){ + return at(JUBILIFE, 48, 62); +} + std::vector to_jubilife(){ StoryStep potions = make(StoryStepKind::SHOP, "potions", "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp)"); @@ -439,7 +449,7 @@ std::vector to_jubilife(){ row("00.04d", "Route 202's trainers, Dawn in Jubilife", at(ROUTE_202, "save_spot"), { route("north_to_jubilife", on(ROUTE_202, "jubilife_exit"), "north past Route 202's trainers, fighting who sees us"), stairs, - scene("dawn_scene", std::nullopt, "Dawn walks over while we stand still"), + scene("dawn_scene", below_jubilifes_steps(), "Dawn walks over while we stand still"), }), }; } @@ -449,11 +459,15 @@ Pose below_the_school(){ return next_to(JUBILIFE, "trainers_school_door", SOUTH, SOUTH); } -// 00.05: the steps were measured on the console (2026-10-30); the rest is +// 00.05: the steps as measured on the console (2026-10-30); the rest is // planned on the map. StoryCheckpoint the_town_map(){ - return row("00.05", "Jubilife: the Center, the Trainers' School and the Town Map", std::nullopt, { - event("up_the_steps", "a tile west of the save below the steps, then up them", on(JUBILIFE, "up_the_steps")), + return row("00.05", "Jubilife: the Center, the Trainers' School and the Town Map", below_jubilifes_steps(), { + // D-pad taps, a tile each: the stick holds this was (200 ms west, + // 480 ms north) left us between tiles, and every leg after it a + // column off (r57 to r61). + legs("up_the_steps", {{.direction = WEST, .tiles = 1}, {.direction = NORTH, .tiles = 2}}, + "a tile west of the save, then up the steps to (47, 60)"), // Planned on the map: the hand-measured walls (west to the sign, east // across the road, north) leaned on the old turn's nudge and ended // beside the Trainers' School once turns stopped stepping (r57, r58). From 7e7cd4a6cadaaf85fa71f063118c2ea11e0bac63 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:22:54 -0800 Subject: [PATCH 95/98] BDSP AutoStory: 01.03 talks to Barry below the decomp's Center door The Center's door is the decomp's (47, 20), as Sandgem's and Jubilife's doors were where the console came out of them. The map's doorstep (46, 21) only says where 01.04's 20 taps to the Gym were counted from, so out of the Center we may be on either tile. Barry's talk and the heal are now from (47, 21), a tap west of the fence, not two. The heal still lines up on the lit doorway when north finds no door. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StoryTable.cpp | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index a6c2906d84..641f5cadaf 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -579,9 +579,15 @@ Pose after_the_welcome(){ TilePos{43, 21}, TilePos{44, 21}, TilePos{45, 21}, TilePos{43, 22}, TilePos{44, 22}, TilePos{45, 22} }); } -// Out of Oreburgh's Pokemon Center: on (46, 21), facing south (r217d, r218d). +// Below Oreburgh's Pokemon Center's door (47, 20), facing it. +Pose below_oreburghs_center(){ + return at(OREBURGH, 47, 21, NORTH); +} +// Out of Oreburgh's Pokemon Center, facing south: below its door, or on +// the map's doorstep (46, 21), from which 01.04's 20 taps west reach the +// Gym's door column (r217d, r218d). Pose out_of_oreburgh_center(){ - return at(OREBURGH, "pokemon_center_doorstep", SOUTH); + return Pokemon::tile_pose(OREBURGH, {TilePos{47, 21}, map_point(OREBURGH, "pokemon_center_doorstep")}, SOUTH); } // 01.05, planned on Oreburgh's and the Gate's maps with the Diamond decomp's @@ -660,9 +666,11 @@ StoryCheckpoint route_203_to_oreburgh(){ // room down to Roark, whose scene ends with him gone (Platinum's // scripts_oreburgh_mine_b2f.s: he smashes the rock east of him and walks // off east); back up and out; and the Center's nurse, so 01.04 starts on -// the doorstep. The Center's door is column 46 on the console: we came out -// of it on (46, 21), 20 taps east of the Gym's door (r217d, r218d), and -// north from below the mine's sign and a tile east went in (2026-10-31). +// the doorstep. The Center's door is the decomp's (47, 20): out of +// Sandgem's and Jubilife's Centers the console stood in their doors' +// columns. The map's doorstep (46, 21) is the Gym's door column plus the +// 20 taps of 01.04 (r217d, r218d), which do not say where the taps began; +// the heal lines up on the lit doorway if north from (47, 21) misses. // // The hall is walked on counted taps, not held: it is a cave, and a wild // battle on a hold toward a fade would read as the fade. Its warp tiles @@ -677,11 +685,11 @@ StoryCheckpoint oreburgh_mine_to_roark(){ // walk east on these rows a tile east of the door, as by hand. anchored(to_walls("to_the_centers_fence", {{.direction = EAST, .tiles = 6}, {.direction = NORTH, .tiles = 2}}, "east to the fence below the Center's east corner, north to the Center's front"), at(OREBURGH, 48, 21, NORTH)), - anchored(legs("below_the_centers_door", {{.direction = WEST, .tiles = 2}}, "taps west below the door"), - at(OREBURGH, "pokemon_center_doorstep", NORTH)), + anchored(legs("below_the_centers_door", {{.direction = WEST, .tiles = 1}}, "a tap west below the door"), + below_oreburghs_center()), // He stands in the door the first time; walking into him reads as a wall. talk("barry", "Barry in the Center's door"), - scene("barrys_lines", at(OREBURGH, "pokemon_center_doorstep", NORTH), "Roark has gone to the mine"), + scene("barrys_lines", below_oreburghs_center(), "Roark has gone to the mine"), // The Hiker wanders (46..48, 25..27), in the door's column: the // route goes round him. route("above_the_mine", at(OREBURGH, 46, 58, SOUTH), "down the door's column to above the mine's warp tiles", "town"), @@ -707,7 +715,7 @@ StoryCheckpoint oreburgh_mine_to_roark(){ Pokemon::tile_pose(OREBURGH, {TilePos{45, 59}, TilePos{45, 58}}, NORTH)), anchored(to_walls("up_to_the_mines_sign", {{.direction = NORTH, .tiles = 3}}, "north against the mine's sign (45, 56)"), at(OREBURGH, 45, 57, NORTH)), - route("below_the_centers_door_again", at(OREBURGH, "pokemon_center_doorstep", NORTH), "up to below the Center's door", "town"), + route("below_the_centers_door_again", below_oreburghs_center(), "up to below the Center's door", "town"), heal("heal_oreburgh", "the nurse; out onto the doorstep, where 01.04 starts", out_of_oreburgh_center()), }); } From e7282a53dd0a21f3167ce4f0965dc7fdfa56e75a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:41:07 -0800 Subject: [PATCH 96/98] BDSP AutoStory: a map for the mine's entrance hall; Dawn's scene on Jubilife Two fixes for #173's CI (the BDSP simulation tests): - 01.03's taps through the entrance hall had no world to walk on ("no world holds OreburghMineB1F"). The hall gets its map from the Diamond decomp (narc_0313, zone event narc_0196; the Hiker hidden by his flag), with its way up (11..13, 1) as doors, and a room world. Down, we land on (12, 1); up, on (11, 21). - 00.04d's Dawn scene ended on Jubilife while the stairs step before it left the pose on Route 202, so the scene waited for a fade that never comes (the grid sim's "no fade ... map mismatch"; on the console it would have failed after 120 s). The stairs step now ends below Jubilife's steps, (48, 62). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Maps/OreburghMineB1F.json | 43 +++++++++++++++++++ .../PokemonBDSP_AutoStory_StoryPlaces.cpp | 4 +- .../PokemonBDSP_AutoStory_StoryTable.cpp | 15 ++++--- devtools/maps/bdsp_maps.py | 6 +++ 4 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghMineB1F.json diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghMineB1F.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghMineB1F.json new file mode 100644 index 0000000000..1d564db27c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/OreburghMineB1F.json @@ -0,0 +1,43 @@ +{ + "name": "OreburghMineB1F", + "source": {"decomp_file": "pret/pokediamond: files/fielddata/land_data/land_data_release/narc_0313.bin; zone_event narc_0196", "revision": "default branch", "date": "2026-10-31"}, + "tiles": [ + "################################", + "###########DDD##################", + "######...##...##.###############", + "######...##...##.###############", + "####.................###########", + "####.................###########", + "####.................###########", + "####.................###########", + "####.................###########", + "####.................###########", + "######.............#############", + "######.............#############", + "######.............#############", + "######.............#############", + "########.........###############", + "########.........###############", + "########.........###############", + "########.........###############", + "##########.....#################", + "##########.....#################", + "##########.....#################", + "##########.....#################", + "############D###################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################", + "################################" + ], + "objects": [ + {"name": "worker_0", "kind": "npc", "x": 7, "y": 4}, + {"name": "item_1", "kind": "item", "x": 19, "y": 4}, + {"name": "twin_2", "kind": "npc", "x": 6, "y": 3} + ] +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp index 78c7b02584..c5c2e2021b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp @@ -130,12 +130,12 @@ Pokemon::MapData ravaged_path_smashed(){ return map; } -// The interiors (houses, the lab, the Gym, the Gate, the mine's coal room, +// The interiors (houses, the lab, the Gym, the Gate, the mine's two floors, // the Ravaged Path), each its own world, named by its JSON map. const std::map& room_worlds(){ static const std::map rooms = []{ std::map ret; - for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab", "OreburghGym", "OreburghGate", "OreburghMine"}){ + for (const char* room : {"PlayerHouse2F", "PlayerHouse1F", "RivalHouse1F", "RivalHouse2F", "RowansLab", "OreburghGym", "OreburghGate", "OreburghMineB1F", "OreburghMine"}){ ret[room].add(room, story_map(room), {0, 0}); } ret["RavagedPath"].add("RavagedPath", ravaged_path_smashed(), {0, 0}); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 641f5cadaf..b9be1da438 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -59,8 +59,8 @@ const char* const JUBILIFE = "JubilifeCity"; const char* const TRAINERS_SCHOOL = "TrainersSchool"; const char* const OREBURGH = "OreburghCity"; const char* const GYM = "OreburghGym"; -// The mine's entrance hall (Diamond's MAP_OREBURGH_MINE_B1F) has no map: -// a pose there names it. The coal room below it, Roark's, does. +// The mine's entrance hall (Diamond's MAP_OREBURGH_MINE_B1F) and the coal +// room below it, Roark's. const char* const MINE_HALL = "OreburghMineB1F"; const char* const MINE = "OreburghMine"; const char* const GATE = "OreburghGate"; @@ -441,6 +441,9 @@ std::vector to_jubilife(){ StoryStep stairs = step_into("into_jubilife", NORTH, "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); stairs.variant = "jubilife_stairs"; + // On Jubilife from here on: the scene after it plays where we stand, with + // no fade to wait for. + stairs.pose_after = below_jubilifes_steps(); return { row("00.04c", "Potions, back to Route 202's save spot", door_front(SANDGEM, "pokemon_center_door"), { potions, @@ -662,8 +665,8 @@ StoryCheckpoint route_203_to_oreburgh(){ // events narc_0044 and narc_0197): Barry in the Center's door says Roark // went to the mine (Roark ignored A until then, r101 and r104); down the // door's column to the mine's warp tiles (44..48, 59); straight down the -// entrance hall, which has no map, to its warps (11..13, 21); in the coal -// room down to Roark, whose scene ends with him gone (Platinum's +// entrance hall to its warps (11..13, 21); in the coal room down to +// Roark, whose scene ends with him gone (Platinum's // scripts_oreburgh_mine_b2f.s: he smashes the rock east of him and walks // off east); back up and out; and the Center's nurse, so 01.04 starts on // the doorstep. The Center's door is the decomp's (47, 20): out of @@ -693,7 +696,7 @@ StoryCheckpoint oreburgh_mine_to_roark(){ // The Hiker wanders (46..48, 25..27), in the door's column: the // route goes round him. route("above_the_mine", at(OREBURGH, 46, 58, SOUTH), "down the door's column to above the mine's warp tiles", "town"), - step_into("into_the_mine", SOUTH, "the warp tile (46, 59)", inside(MINE_HALL)), + step_into("into_the_mine", SOUTH, "the warp tile (46, 59)", at(MINE_HALL, 12, 1, SOUTH)), // We land on the warp tile we came by, (12, 1): 19 taps end on row // 20, a step short of the warp row. legs("down_the_hall", {{.direction = SOUTH, .tiles = 19}}, "down column 12 of the entrance hall"), @@ -709,7 +712,7 @@ StoryCheckpoint oreburgh_mine_to_roark(){ talk("roark", "Roark, north of us between his rocks"), scene("roark_smashes_the_rock", below_roark, "his lines, Rock Smash on the rock east of him, and he walks off"), route("below_the_coal_rooms_warp", at(MINE, 15, 2, NORTH), "back up the corridor"), - step_into("up_to_the_hall", NORTH, "the warp tile (15, 1)", inside(MINE_HALL)), + step_into("up_to_the_hall", NORTH, "the warp tile (15, 1)", at(MINE_HALL, 11, 21, NORTH)), legs("up_the_hall", {{.direction = NORTH, .tiles = 19}}, "up column 11 of the entrance hall"), step_into("out_of_the_mine", NORTH, "the hall's warp tiles (11..13, 1)", Pokemon::tile_pose(OREBURGH, {TilePos{45, 59}, TilePos{45, 58}}, NORTH)), diff --git a/devtools/maps/bdsp_maps.py b/devtools/maps/bdsp_maps.py index 2f5c1fdf61..1d54c8ac99 100644 --- a/devtools/maps/bdsp_maps.py +++ b/devtools/maps/bdsp_maps.py @@ -125,6 +125,9 @@ # walk north into the hall went up column 14 (the warp is column 15: a # stick hold drifts, or BDSP's stairs are wider). 'OreburghMine': {'header': 'OREBURGH_MINE_B2F', 'rect': (0, 0, 32, 32)}, + # B1F, the entrance hall: 01.03 crosses it on column 12 going down and + # column 11 coming up (the console's walks went straight through). + 'OreburghMineB1F': {'header': 'OREBURGH_MINE_B1F', 'rect': (0, 0, 32, 32)}, # Platinum's matrix 051 (Diamond's matrix is not in the cache), rows # 0..53. Console (2026-10-31, r235b and D-pad probes): in from the south # door onto (19, 50), up the stairs (18..20, 45..47) to the wall at row @@ -414,6 +417,9 @@ def box(x0, y0, x1, y1): # The mine's mouth: 01.03 walks down to the row above it and steps on. # We come out on it too, and walk north off it to a wall first. 'OreburghCity': [(x, 59) for x in range(44, 49)], + # The hall's way up to the city: its other warp row (11..13, 21) has + # cave mouths below it, so a step south from row 20 goes through. + 'OreburghMineB1F': [(11, 1), (12, 1), (13, 1)], } From 8f308aab44e712a0be60d325d8a8324e75f06917 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:57:41 -0800 Subject: [PATCH 97/98] BDSP AutoStory: 00.04d's stairs are an event step, not a STEP The stairs into Jubilife were a STEP whose variant names the event jubilife_stairs. Since the last commit gave it a pose after (below Jubilife's steps, so Dawn's scene waits for no fade), a walk check that holds STEPs to their pose after expected it on Jubilife (the grid sim, which stubs events: "into_jubilife: ended on Route 202"). It is an event like the others now (event(), GAME), run the same way by its variant. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Events_Sandgem.cpp | 2 +- .../AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp index e5b30ec0c0..1473ffd8ec 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Events_Sandgem.cpp @@ -145,7 +145,7 @@ void walk_to_route_202_save_spot( } // North into Jubilife until Dawn's scene starts (00.04d's -// into_jubilife). Jubilife's south edge is a wall with stairs in it, +// jubilife_stairs). Jubilife's south edge is a wall with stairs in it, // west of where the route may leave us (r78 stood at its east end). // Entering starts a scene: Dawn walks over while we stand still. So // walk north; if the wall stops us and no dialogue follows, the stairs diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index b9be1da438..03069d92a0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -438,12 +438,12 @@ std::vector to_jubilife(){ potions.items = {{.item = "potion", .quantity = 10}}; potions.variant = "sandgem_potions"; potions.pose_after = at(ROUTE_202, "save_spot"); - StoryStep stairs = step_into("into_jubilife", NORTH, - "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); - stairs.variant = "jubilife_stairs"; - // On Jubilife from here on: the scene after it plays where we stand, with - // no fade to wait for. - stairs.pose_after = below_jubilifes_steps(); + // An event, not a step: it walks north and west until Dawn's scene + // starts. It leaves us on Jubilife, so the scene after it plays where + // we stand, with no fade to wait for. + const StoryStep stairs = event("jubilife_stairs", + "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)", + below_jubilifes_steps()); return { row("00.04c", "Potions, back to Route 202's save spot", door_front(SANDGEM, "pokemon_center_door"), { potions, From 040693c62486439b4b473732860b0d4c6632dc2a Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 05:27:20 -0800 Subject: [PATCH 98/98] BDSP AutoStory: verify mode, the start checkpoint N runs from its save With the story run options' verify mode on, AutoStory runs the start checkpoint as verify_checkpoint() does. BDSP rows save at their start and never at their end, so the reset from HOME between runs loads the save the first run started from. A new game's intro has no save to reload and is refused. Not verified, the game is closed as after any failure. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory.cpp | 82 ++++++++++++++++--- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index c0cd6c3a82..6bdcdcb259 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include "CommonFramework/Exceptions/OperationFailedException.h" #include "CommonFramework/Notifications/ProgramNotifications.h" @@ -99,6 +100,68 @@ void close_game_after_failure(SingleSwitchProgramEnvironment& env, ProController } } +// A run that stopped short throws, closing the game first unless it +// stopped at the first failure as asked. +void throw_if_stopped(SingleSwitchProgramEnvironment& env, ProControllerContext& context, const Pokemon::SegmentReport& report){ + if (report.completed){ + return; + } + if (!report.stopped_at_first_failure){ + close_game_after_failure(env, context); + } + throw OperationFailedException( + ErrorReportMode::NO_ERROR_REPORT, + std::format( + "AutoStory stopped at {} {}: {}", + report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure) + ) + ); +} + +// Verify mode's checkpoint and runs, and whether the game starts as a new +// game (refused: no save to reload) or stays open after a failure. +struct VerifyRequest{ + std::string id; + Pokemon::VerifyPolicy policy; + bool new_game = false; + bool keep_game = false; +}; + +// Verify mode: the checkpoint run as the request's policy says. Rows save +// at their start, never at their end, so the reset from HOME between runs +// loads the save the first run started from. Throws unless enough runs +// pass, closing the game first as after any failure unless `keep_game`. +void verify_story_checkpoint( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, Pokemon::RunState& state, + Pokemon::Segment& segment, Pokemon::SegmentHooks& hooks, const VerifyRequest& request +){ + if (request.new_game){ + throw UserSetupError(env.console, "Verify mode reloads the start's save between runs: turn off \"Start a new game first\"."); + } + const std::string& id = request.id; + const Pokemon::VerifyPolicy& policy = request.policy; + const bool keep_game = request.keep_game; + Pokemon::VerifyReport report; + try{ + report = Pokemon::verify_checkpoint(state, segment, id, policy, hooks); + }catch (OperationFailedException&){ + if (!keep_game){ + close_game_after_failure(env, context); + } + throw; + } + const std::string summary = std::format( + "AutoStory verify: {} {}/{} passed, {} needed.", id, report.passes, policy.runs, policy.passes_needed + ); + if (!report.verified){ + if (!keep_game){ + close_game_after_failure(env, context); + } + throw OperationFailedException(ErrorReportMode::NO_ERROR_REPORT, summary); + } + env.console.log(summary); +} + } @@ -178,6 +241,12 @@ void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerCo if (!(first == 0 && NEW_GAME_INTRO)){ state.flags.insert("save_loaded"); } + if (const std::optional verify = RUN.VERIFY.policy()){ + const VerifyRequest request{checkpoints[first].id, *verify, first == 0 && NEW_GAME_INTRO, RUN.STOP_AT_FIRST_FAILURE}; + verify_story_checkpoint(env, context, state, segment, hooks, request); + send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); + return; + } Pokemon::SegmentReport report; try{ report = run_segment(state, segment, Pokemon::SegmentRange{checkpoints[first].id, checkpoints[last].id}, RUN.policy(), hooks); @@ -190,18 +259,7 @@ void BDSPAutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerCo Pokemon::AutoStoryStats& stats = env.current_stats(); Pokemon::record_story_stats(stats, report); env.update_stats(); - if (!report.completed){ - if (!report.stopped_at_first_failure){ - close_game_after_failure(env, context); - } - throw OperationFailedException( - ErrorReportMode::NO_ERROR_REPORT, - std::format( - "AutoStory stopped at {} {}: {}", - report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure) - ) - ); - } + throw_if_stopped(env, context, report); env.console.log("AutoStory: done through " + checkpoints[last].id + "."); send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); }