diff --git a/SerialPrograms/Source/ComputerPrograms/UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/UnitTestRunner.cpp index 500b366a95..4f3b0ca5dd 100644 --- a/SerialPrograms/Source/ComputerPrograms/UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/UnitTestRunner.cpp @@ -19,6 +19,7 @@ #include "CommonTools/OCR/OCR_Tests.h" #include "Kernels/Kernels_Tests.h" #include "PokemonFRLG/PokemonFRLG_Tests.h" +#include "PokemonRSE/PokemonRSE_Tests.h" #include "PokemonHome/PokemonHome_Tests.h" #include "PokemonSwSh/PokemonSwSh_Tests.h" #include "PokemonLA/PokemonLA_Tests.h" @@ -43,6 +44,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ NintendoSwitch::add_tests_FailedToConnectDetector(ret); NintendoSwitch::add_tests_UpdatePopupDetector(ret); NintendoSwitch::PokemonFRLG::add_tests(ret); + NintendoSwitch::PokemonRSE::add_tests(ret); NintendoSwitch::PokemonHome::add_tests(ret); NintendoSwitch::PokemonSwSh::add_tests(ret); NintendoSwitch::PokemonLA::add_tests(ret); diff --git a/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.cpp b/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.cpp index 66f53f1770..c0ab925c84 100644 --- a/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.cpp +++ b/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.cpp @@ -6,6 +6,7 @@ #include "CommonTools/Images/SolidColorTest.h" #include "CommonTools/Images/ImageFilter.h" +#include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/ImageTools/ImageStats.h" @@ -201,6 +202,175 @@ bool SelectionDialogDetector::detect(const ImageViewRGB32& screen){ } +static std::vector confirm_candidates(ConfirmSlot slot, ConfirmPosition position) { + double offset = SELECTION_SLOT_OFFSETS[(int)slot]; + + switch (position) { + case ConfirmPosition::STANDARD: + return { + { 0.69, 0.45 + offset, SelectionIndicator::ARROW }, + { 0.685, 0.458 + offset, SelectionIndicator::BORDER } + }; + case ConfirmPosition::SPECIAL: + return { + { 0.79, 0.45 + offset, SelectionIndicator::ARROW }, + { 0.785, 0.458 + offset, SelectionIndicator::BORDER } + }; + case ConfirmPosition::STARTER: + return { + { 0.79, 0.45 + offset, SelectionIndicator::ARROW }, + { 0.725, 0.397 + offset, SelectionIndicator::ARROW }, // jpn rs + { 0.72, 0.405 + offset, SelectionIndicator::BORDER } + }; + case ConfirmPosition::MART: + return { + { 0.69, 0.45 + offset, SelectionIndicator::ARROW }, + { 0.255, 0.45 + offset, SelectionIndicator::ARROW }, // jpn rs + { 0.25, 0.458 + offset, SelectionIndicator::BORDER } + }; + case ConfirmPosition::TOP_LEFT: + return { + { 0.094, 0.115 + offset, SelectionIndicator::ARROW }, + { 0.089, 0.123 + offset, SelectionIndicator::BORDER } + }; + default: + return { + { 0.69, 0.45 + offset, SelectionIndicator::ARROW }, + { 0.685, 0.458 + offset, SelectionIndicator::BORDER } + }; + } +} + +ConfirmSlotDetector::ConfirmSlotDetector(Color color, ConfirmSlot slot, ConfirmPosition position) + : SelectionSlotDetector(color + , confirm_candidates(slot, position)) +{} + + + +class Test_SelectionDialogDetector : public UnitTest { +public: + + Test_SelectionDialogDetector( + const std::string& image, + bool expected + ) + : UnitTest("PokemonRSE::SelectionDialogDetector - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + , m_expected(expected) + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override { + SelectionDialogDetector detector(COLOR_RED); + ImageRGB32 image(m_image); + return detector.detect(image) == m_expected; + }; + +private: + std::string m_image; + bool m_expected; +}; + +void add_tests_SelectionDialogDetector(UnitTestDatabase& database) { + // Switch 1/2 + + /* + // Original GBA resolution; {0.0, 0.0, 1.0, 1.0} + database.add("PokemonRSE/SelectionDialogDetector/GBA-Eng-Ruby_true.png", true); + database.add("PokemonRSE/SelectionDialogDetector/GBA-Ger-Emerald_true.png", true); + + // Game Boy Player; {0.064217, 0.107667, 0.86413, 0.787928} + database.add("PokemonRSE/SelectionDialogDetector/GBP-Ger-Emerald_true.png", true); + database.add("PokemonRSE/SelectionDialogDetector/GBP-Ger-Ruby_true.png", true); + */ +} + + +class Test_ConfirmSlotDetector : public UnitTest { +public: + + Test_ConfirmSlotDetector( + const std::string& image, + ConfirmSlot slot, + ConfirmPosition position, + bool expected + ) + : UnitTest("PokemonRSE::ConfirmSlotDetector - " + std::to_string((int)slot) + " - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + , m_slot(slot) + , m_position(position) + , m_expected(expected) + { + } + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override { + ConfirmSlotDetector detector(COLOR_RED, m_slot, m_position); + ImageRGB32 image(m_image); + return detector.detect(image) == m_expected; + }; + +private: + std::string m_image; + ConfirmSlot m_slot; + ConfirmPosition m_position; + bool m_expected; +}; + +void add_tests_ConfirmSlotDetector(UnitTestDatabase& database) { + // Switch 1/2 + + /* + // Original GBA resolution {0.0, 0.0, 1.0, 1.0} + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Eng-Ruby_yes.png", ConfirmSlot::YES, ConfirmPosition::STANDARD, true); + //database.add("PokemonRSE/ConfirmSlotDetector/GBA-Eng-Ruby_yes.png", ConfirmSlot::NO, ConfirmPosition::STANDARD, false); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Emerald_yes.png", ConfirmSlot::YES, ConfirmPosition::STANDARD, true); + //database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Emerald_yes.png", ConfirmSlot::NO, ConfirmPosition::STANDARD, false); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Ruby_yes.png", ConfirmSlot::YES, ConfirmPosition::STANDARD, true); + //database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Ruby_yes.png", ConfirmSlot::NO, ConfirmPosition::STANDARD, false); + + // GBA SPECIAL + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Eng-Emerald-Special_no.png", ConfirmSlot::NO, ConfirmPosition::SPECIAL, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Eng-Ruby-Special_yes.png", ConfirmSlot::YES, ConfirmPosition::SPECIAL, true); + + // GBA TOP_LEFT + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Eng-Emerald-Top_no.png", ConfirmSlot::NO, ConfirmPosition::TOP_LEFT, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Eng-Ruby-Top_no.png", ConfirmSlot::NO, ConfirmPosition::TOP_LEFT, true); + + // GBA MART + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Emerald-Mart_yes.png", ConfirmSlot::YES, ConfirmPosition::MART, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Ruby-Mart_no.png", ConfirmSlot::NO, ConfirmPosition::MART, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Jpn-Ruby-Mart_yes.png", ConfirmSlot::YES, ConfirmPosition::MART, true); + + // GBA STARTER + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Emerald-Starter_yes.png", ConfirmSlot::YES, ConfirmPosition::STARTER, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Ger-Ruby-Starter_no.png", ConfirmSlot::NO, ConfirmPosition::STARTER, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBA-Jpn-Ruby-Starter_yes.png", ConfirmSlot::YES, ConfirmPosition::STARTER, true); + + // Game Boy Player {0.064217, 0.107667, 0.86413, 0.787928} + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Emerald_no.png", ConfirmSlot::NO, ConfirmPosition::STANDARD, true); + //database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Emerald_no.png", ConfirmSlot::YES, ConfirmPosition::STANDARD, false); + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Emerald_yes.png", ConfirmSlot::YES, ConfirmPosition::STANDARD, true); + //database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Emerald_yes.png", ConfirmSlot::NO, ConfirmPosition::STANDARD, false); + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Ruby_no.png", ConfirmSlot::NO, ConfirmPosition::STANDARD, true); + //database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Ruby_no.png", ConfirmSlot::YES, ConfirmPosition::STANDARD, false); + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Ruby_yes.png", ConfirmSlot::YES, ConfirmPosition::STANDARD, true); + //database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Ruby_yes.png", ConfirmSlot::NO, ConfirmPosition::STANDARD, false); + + // GBP SPECIAL + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Emerald-Special_yes.png", ConfirmSlot::YES, ConfirmPosition::SPECIAL, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Ruby-Special_no.png", ConfirmSlot::NO, ConfirmPosition::SPECIAL, true); + + // GBP MART + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Emerald-Mart_yes.png", ConfirmSlot::YES, ConfirmPosition::MART, true); + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Ruby-Mart_yes.png", ConfirmSlot::YES, ConfirmPosition::MART, true); + + // GBP STARTER + database.add("PokemonRSE/ConfirmSlotDetector/GBP-Ger-Emerald-Starter_yes.png", ConfirmSlot::YES, ConfirmPosition::STARTER, true); + */ +} + + + } } diff --git a/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.h b/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.h index 64d5bcbcbb..7d29576fe5 100644 --- a/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.h +++ b/SerialPrograms/Source/PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.h @@ -9,12 +9,14 @@ #include #include -#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "Common/Cpp/Color.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "CommonTools/VisualDetector.h" #include "CommonTools/VisualDetectors/BlackScreenDetector.h" #include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "PokemonRSE/Inference/PokemonRSE_SelectionDetector.h" #include "PokemonRSE/PokemonRSE_Settings.h" namespace PokemonAutomation{ @@ -161,6 +163,35 @@ class SelectionDialogWatcher : public DetectorToFinder{ }; +enum class ConfirmSlot { + YES, + NO +}; + +enum class ConfirmPosition { + STANDARD, // e.g. save menu + SPECIAL, // e.g. clock in your room, replace move; not starter selection: ruby/sapphire is different + STARTER, // starter selection + MART, // confirm purchase + TOP_LEFT // e.g. player name +}; + +class ConfirmSlotDetector : public SelectionSlotDetector { +public: + ConfirmSlotDetector(Color color, ConfirmSlot slot, ConfirmPosition position = ConfirmPosition::STANDARD); +}; +class ConfirmSlotWatcher : public DetectorToFinder { +public: + ConfirmSlotWatcher(Color color, ConfirmSlot slot, ConfirmPosition position = ConfirmPosition::STANDARD, Milliseconds hold_duration = Milliseconds(250)) + : DetectorToFinder("ConfirmSlotWatcher", hold_duration , color, slot, position) + {} +}; + + + +void add_tests_SelectionDialogDetector(UnitTestDatabase& database); +void add_tests_ConfirmSlotDetector(UnitTestDatabase& database); + } } } diff --git a/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.cpp b/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.cpp index b78281e285..23ef5d6c92 100644 --- a/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.cpp +++ b/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.cpp @@ -5,7 +5,10 @@ */ #include "Common/Cpp/Color.h" +#include "Common/Cpp/Exceptions.h" #include "CommonTools/Images/SolidColorTest.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "PokemonRSE/PokemonRSE_Settings.h" @@ -15,9 +18,26 @@ namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonRSE{ -StartMenuDetector::StartMenuDetector(Color color) + +static double menu_bottom_box_y(StartMenuState state){ + switch (state) { + case StartMenuState::FULL: + return 0.94; + case StartMenuState::PRE_POKENAV: + case StartMenuState::SAFARI: + return 0.84; + case StartMenuState::PRE_POKEDEX: + return 0.74; + case StartMenuState::PRE_POKEMON: + return 0.64; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid StartMenuState"); + } +} + +StartMenuDetector::StartMenuDetector(Color color, StartMenuState state) : m_menu_top_box(0.766756, 0.046978, 0.197701, 0.01334) - , m_menu_bottom_box(0.766756, 0.94, 0.197701, 0.01334) + , m_menu_bottom_box(0.766756, menu_bottom_box_y(state), 0.197701, 0.01334) {} void StartMenuDetector::make_overlays(VideoOverlaySet& items) const{ const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; @@ -36,6 +56,141 @@ bool StartMenuDetector::detect(const ImageViewRGB32& screen){ } +// Row of each option from the top of the menu, per state; -1 = not shown +// column = (int)StartMenuSlot, row = (int)StartMenuState. +// DEX MON BAG NAV TRN SAV OPT EXT RET +constexpr int8_t START_MENU_ROWS[5][9] = { + /* PRE_POKEMON */ { -1, -1, 0, -1, 1, 2, 3, 4, -1 }, + /* PRE_POKEDEX */ { -1, 0, 1, -1, 2, 3, 4, 5, -1 }, + /* PRE_POKENAV */ { 0, 1, 2, -1, 3, 4, 5, 6, -1 }, + /* FULL */ { 0, 1, 2, 3, 4, 5, 6, 7, -1 }, + /* SAFARI */ { 1, 2, 3, -1, 4, -1, 5, 6, 0 }, +}; + +static std::vector start_menu_slot_candidates(StartMenuSlot slot, StartMenuState state) { + int8_t row = START_MENU_ROWS[(int)state][(int)slot]; + if (row == -1) { + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid StartMenuSlot for StartMenuState"); + } + double offset = SELECTION_SLOT_OFFSETS[row]; + return { + { 0.725, 0.098 + offset, SelectionIndicator::ARROW }, + { 0.759, 0.098 + offset, SelectionIndicator::ARROW }, // jpn rse + { 0.754, 0.106 + offset, SelectionIndicator::BORDER } + }; +} + + +StartMenuSlotDetector::StartMenuSlotDetector(Color color, StartMenuSlot slot, StartMenuState state) + : SelectionSlotDetector(color, start_menu_slot_candidates(slot, state)) +{} + + + +class Test_StartMenuDetector : public UnitTest { +public: + + Test_StartMenuDetector( + const std::string& image, + StartMenuState state, + bool expected + ) + : UnitTest("PokemonRSE::StartMenuDetector - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + , m_state(state) + , m_expected(expected) + { + } + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override { + StartMenuDetector detector(COLOR_RED, m_state); + ImageRGB32 image(m_image); + return detector.detect(image) == m_expected; + }; + +private: + std::string m_image; + StartMenuState m_state; + bool m_expected; +}; + +void add_tests_StartMenuDetector(UnitTestDatabase & database) { + // Switch 1/2 + + /* + // Original GBA resolution; {0.0, 0.0, 1.0, 1.0} + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-Full-Pokedex_true.png", StartMenuState::FULL, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-PrePokenav-Exit_true.png", StartMenuState::PRE_POKENAV, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-PrePokedex-Option_true.png", StartMenuState::PRE_POKEDEX, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-PrePokemon-Bag_true.png", StartMenuState::PRE_POKEMON, true); + + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-Full-Exit_true.png", StartMenuState::FULL, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-PrePokenav-Bag_true.png", StartMenuState::PRE_POKENAV, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-PrePokedex-Exit_true.png", StartMenuState::PRE_POKEDEX, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-PrePokemon-Bag_true.png", StartMenuState::PRE_POKEMON, true); + + // Game Boy Player; {0.064217, 0.107667, 0.86413, 0.787928} + database.add("PokemonRSE/StartMenuDetector/GBP-Ger-Emerald-Full-Pokemon_true.png", StartMenuState::FULL, true); + database.add("PokemonRSE/StartMenuDetector/GBP-Ger-Ruby-Full-Pokemon_true.png", StartMenuState::FULL, true); + */ +} + + +class Test_StartMenuSlotDetector : public UnitTest { +public: + + Test_StartMenuSlotDetector( + const std::string& image, + StartMenuSlot slot, + StartMenuState state, + bool expected + ) + : UnitTest("PokemonRSE::StartMenuSlotDetector - " + std::to_string((int)slot) + " - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + , m_slot(slot) + , m_state(state) + , m_expected(expected) + { + } + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override { + StartMenuSlotDetector detector(COLOR_RED, m_slot, m_state); + ImageRGB32 image(m_image); + return detector.detect(image) == m_expected; + }; + +private: + std::string m_image; + StartMenuSlot m_slot; + StartMenuState m_state; + bool m_expected; +}; + +void add_tests_StartMenuSlotDetector(UnitTestDatabase & database) { + // Switch 1/2 + + /* + // Original GBA resolution; {0.0, 0.0, 1.0, 1.0} + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-Full-Pokedex_true.png", StartMenuSlot::POKEDEX, StartMenuState::FULL, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-PrePokenav-Exit_true.png", StartMenuSlot::EXIT, StartMenuState::PRE_POKENAV, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-PrePokedex-Option_true.png", StartMenuSlot::OPTION, StartMenuState::PRE_POKEDEX, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Emerald-PrePokemon-Bag_true.png", StartMenuSlot::BAG, StartMenuState::PRE_POKEMON, true); + + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-Full-Exit_true.png", StartMenuSlot::EXIT, StartMenuState::FULL, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-PrePokenav-Bag_true.png", StartMenuSlot::BAG, StartMenuState::PRE_POKENAV, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-PrePokedex-Exit_true.png", StartMenuSlot::EXIT, StartMenuState::PRE_POKEDEX, true); + database.add("PokemonRSE/StartMenuDetector/GBA-Ger-Ruby-PrePokemon-Bag_true.png", StartMenuSlot::BAG, StartMenuState::PRE_POKEMON, true); + + // Game Boy Player; {0.064217, 0.107667, 0.86413, 0.787928} + database.add("PokemonRSE/StartMenuDetector/GBP-Ger-Emerald-Full-Pokemon_true.png", StartMenuSlot::POKEMON, StartMenuState::FULL, true); + database.add("PokemonRSE/StartMenuDetector/GBP-Ger-Ruby-Full-Pokemon_true.png", StartMenuSlot::POKEMON, StartMenuState::FULL, true); + */ +} + + + + + } } } diff --git a/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.h b/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.h index 4dee431bc4..0a17dc6858 100644 --- a/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.h +++ b/SerialPrograms/Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.h @@ -9,8 +9,10 @@ #include #include "Common/Cpp/Color.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "CommonTools/VisualDetector.h" +#include "PokemonRSE/Inference/PokemonRSE_SelectionDetector.h" namespace PokemonAutomation{ class CancellableScope; @@ -18,13 +20,33 @@ namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonRSE{ +enum class StartMenuState { + PRE_POKEMON = 0, + PRE_POKEDEX = 1, + PRE_POKENAV = 2, + FULL = 3, + SAFARI = 4, +}; + +enum class StartMenuSlot { + POKEDEX = 0, + POKEMON = 1, + BAG = 2, + POKENAV = 3, + TRAINER = 4, + SAVE = 5, + OPTION = 6, + EXIT = 7, + RETIRE = 8, +}; + // Detect the full start menu by looking for empty white sections on the top and bottom -// Works for all RSE, all languages. Will not work for early game (before pokedex or party). +// Works for all RSE, all languages. // No arrow detection, RS japan and all emerald languages have an arrow, // but RS for all non-japan languages use a red box for selection. class StartMenuDetector : public StaticScreenDetector{ public: - StartMenuDetector(Color color); + StartMenuDetector(Color color, StartMenuState state = StartMenuState::FULL); virtual void make_overlays(VideoOverlaySet& items) const override; virtual bool detect(const ImageViewRGB32& screen) override; @@ -35,11 +57,26 @@ class StartMenuDetector : public StaticScreenDetector{ }; class StartMenuWatcher : public DetectorToFinder{ public: - StartMenuWatcher(Color color = COLOR_RED) - : DetectorToFinder("StartMenuWatcher", std::chrono::milliseconds(250), color) + StartMenuWatcher(Color color = COLOR_RED, StartMenuState state = StartMenuState::FULL) + : DetectorToFinder("StartMenuWatcher", std::chrono::milliseconds(250), color, state) {} }; +// Detect whether a specified slot is selected +class StartMenuSlotDetector : public SelectionSlotDetector { +public: + StartMenuSlotDetector(Color color, StartMenuSlot slot, StartMenuState state = StartMenuState::FULL); +}; +class StartMenuSlotWatcher : public DetectorToFinder { +public: + StartMenuSlotWatcher(Color color, StartMenuSlot slot, StartMenuState state = StartMenuState::FULL) + : DetectorToFinder("StartMenuSlotWatcher", std::chrono::milliseconds(250), color, slot, state) + {} +}; + + +void add_tests_StartMenuDetector(UnitTestDatabase& database); +void add_tests_StartMenuSlotDetector(UnitTestDatabase& database); } } diff --git a/SerialPrograms/Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.cpp b/SerialPrograms/Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.cpp new file mode 100644 index 0000000000..947871df5b --- /dev/null +++ b/SerialPrograms/Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.cpp @@ -0,0 +1,175 @@ +/* Selection Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonTools/ImageMatch/WaterfillTemplateMatcher.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/Images/WaterfillUtilities.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "Kernels/Waterfill/Kernels_Waterfill_Types.h" +#include "PokemonRSE/PokemonRSE_Settings.h" +#include "PokemonRSE_SelectionDetector.h" + +#include + +//using std::cout; +//using std::endl; + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonRSE{ + + +class SelectionArrowMatcher : public ImageMatch::WaterfillTemplateMatcher { +public: + SelectionArrowMatcher(const char* path) + : WaterfillTemplateMatcher( + path, + Color(50, 50, 50), Color(115, 115, 115), 24 + ) + { + m_aspect_ratio_lower = 0.8; + m_aspect_ratio_upper = 1.2; + m_area_ratio_lower = 0.8; + m_area_ratio_upper = 1.2; + } + + + static const SelectionArrowMatcher& emerald_matcher() { + static SelectionArrowMatcher matcher("PokemonRSE/SelectionArrowEmerald.png"); + return matcher; + } + static const SelectionArrowMatcher& ruby_sapphire_jpn_matcher() { + static SelectionArrowMatcher matcher("PokemonRSE/SelectionArrowRubySapphireJpn.png"); + return matcher; + } + static const SelectionArrowMatcher& emerald_jpn_matcher() { + static SelectionArrowMatcher matcher("PokemonRSE/SelectionArrowEmeraldJpn.png"); + return matcher; + } +}; + +SelectionArrowDetector::SelectionArrowDetector(Color color, const ImageFloatBox& box) + : m_color(color) + , m_box(box) +{} +void SelectionArrowDetector::make_overlays(VideoOverlaySet& items) const { + const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; + items.add(m_color, GAME_BOX.inner_to_outer(m_box)); +} +bool SelectionArrowDetector::detect(const ImageViewRGB32& screen) { + const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; + ImageViewRGB32 game_screen = extract_box_reference(screen, GAME_BOX); + ImageViewRGB32 image = extract_box_reference(game_screen, m_box); + + double screen_rel_height = game_screen.height() / 160.0; + double screen_rel_width = game_screen.width() / 240.0; + double screen_rel_size = screen_rel_height * screen_rel_width; + + const double MIN_AREA_240 = 10; + const double RMSD_THRESHOLD = 80; + size_t min_area = size_t(screen_rel_size * MIN_AREA_240); + + const std::vector> FILTERS = { + {0xff4f4f4f, 0xff777777}, // Emerald + {0xff2f2f2f, 0xff5c5c5c}, // Emerald + {0xff363636, 0xff5e5e5e} // Ruby/Sapphire + }; + + auto try_match = [&](const SelectionArrowMatcher& matcher) -> bool { + return match_template_by_waterfill( + game_screen.size(), + image, + matcher, + FILTERS, + { min_area, SIZE_MAX }, + RMSD_THRESHOLD, + [&](Kernels::Waterfill::WaterfillObject& object) -> bool { + //std::cout << "area: " << object.area << std::endl; + return true; + } + ); + }; + + return try_match(SelectionArrowMatcher::emerald_matcher()) + || try_match(SelectionArrowMatcher::emerald_jpn_matcher()) + || try_match(SelectionArrowMatcher::ruby_sapphire_jpn_matcher()); +} + + +SelectionBorderDetector::SelectionBorderDetector(Color color, const ImageFloatBox& box) + : m_color(color) + , m_box(box) +{} +void SelectionBorderDetector::make_overlays(VideoOverlaySet& items) const { + const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; + items.add(m_color, GAME_BOX.inner_to_outer(m_box)); +} +bool SelectionBorderDetector::detect(const ImageViewRGB32& screen) { + const BoxOption& GAME_BOX = GameSettings::instance().GAME_BOX; + ImageViewRGB32 game_screen = extract_box_reference(screen, GAME_BOX); + ImageViewRGB32 image = extract_box_reference(game_screen, m_box); + + double screen_rel_height = game_screen.height() / 160.0; + double screen_rel_width = game_screen.width() / 240.0; + double screen_rel_size = screen_rel_height * screen_rel_width; + + const double MIN_AREA_240 = 5; // the left/right side of the border is 12-13 pixels high + size_t min_area = size_t(screen_rel_size * MIN_AREA_240); + + size_t pixels_in_range; + filter_rgb32_range( + pixels_in_range, + image, + 0xffc82c23, + 0xffff9a91, + Color(0), + false + ); + + return pixels_in_range > min_area; +} + + +SelectionSlotDetector::SelectionSlotDetector(Color color, const std::vector& candidates) + : m_color(color) +{ + for (const auto& candidate : candidates) { + double slot_y = candidate.y; + double width = (candidate.indicator == SelectionIndicator::ARROW) ? SELECTION_ARROW_BOX_WIDTH : SELECTION_BORDER_BOX_WIDTH; + double height = (candidate.indicator == SelectionIndicator::BORDER) ? SELECTION_BORDER_BOX_HEIGHT : SELECTION_ARROW_BOX_HEIGHT; + ImageFloatBox box(candidate.x, slot_y, width, height); + + if (candidate.indicator == SelectionIndicator::ARROW) { + m_detectors.emplace_back(std::make_unique(color, box)); + } else if (candidate.indicator == SelectionIndicator::BORDER) { + m_detectors.emplace_back(std::make_unique(color, box)); + } + } +} +void SelectionSlotDetector::make_overlays(VideoOverlaySet& items) const { + for (const auto& detector : m_detectors) { + detector->make_overlays(items); + } +} +bool SelectionSlotDetector::detect(const ImageViewRGB32& screen) { + for (auto& detector : m_detectors) { + if (detector->detect(screen)) { + return true; + } + } + return false; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.h b/SerialPrograms/Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.h new file mode 100644 index 0000000000..f1f794585d --- /dev/null +++ b/SerialPrograms/Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.h @@ -0,0 +1,135 @@ +/* Selection Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonRSE_SelectionDetector_H +#define PokemonAutomation_PokemonRSE_SelectionDetector_H + +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/VisualDetector.h" + +#include + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonRSE{ + + +constexpr double SELECTION_ARROW_BOX_WIDTH = 0.039; +constexpr double SELECTION_ARROW_BOX_HEIGHT = 0.10; +constexpr double SELECTION_BORDER_BOX_WIDTH = 0.02; +constexpr double SELECTION_BORDER_BOX_HEIGHT = 0.084; + +constexpr double SELECTION_SLOT_OFFSETS[] = { + 0.0, // slot 0 + 0.10, // slot 1 + 0.20, // slot 2 + 0.30, // slot 3 + 0.40, // slot 4 + 0.50, // slot 5 + 0.60, // slot 6 + 0.70, // slot 7 +}; + +enum class SelectionIndicator { + ARROW, + BORDER, +}; + +struct SlotCandidate { + double x; + double y; + SelectionIndicator indicator; +}; + + +// international Emerald, japanese Ruby/Sapphire/Emerald +class SelectionArrowDetector : public StaticScreenDetector{ +public: + SelectionArrowDetector( + Color color, + const ImageFloatBox& box + ); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + const Color m_color; + const ImageFloatBox m_box; +}; +class SelectionArrowWatcher : public DetectorToFinder{ +public: + SelectionArrowWatcher( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("SelectionArrowWatcher", hold_duration, color, box) + {} +}; + + +// international Ruby/Sapphire +class SelectionBorderDetector : public StaticScreenDetector { +public: + SelectionBorderDetector( + Color color, + const ImageFloatBox& box + ); + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + const ImageFloatBox m_box; +}; +class SelectionBorderWatcher : public DetectorToFinder { +public: + SelectionBorderWatcher( + Color color, + const ImageFloatBox& box, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("SelectionBorderWatcher", hold_duration, color, box) + {} +}; + + +// checks for one selection slot +// In most cases uses at least one arrow and one border detector +class SelectionSlotDetector : public StaticScreenDetector { +public: + SelectionSlotDetector( + Color color, + const std::vector& candidates + ); + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + std::vector> m_detectors; +}; +class SelectionWatcher : public DetectorToFinder { +public: + SelectionWatcher( + Color color, + VideoOverlay* overlay, + const std::vector& candidates, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("SelectionWatcher", hold_duration, color, candidates) + { + } +}; + + +} +} +} + +#endif \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.cpp b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.cpp index ded02f10c3..74ad32216d 100644 --- a/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.cpp +++ b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.cpp @@ -38,7 +38,8 @@ GameSettings::GameSettings() {Device::switch_1_2, "switch_1_2", "Nintendo Switch 1 and 2"}, {Device::rg35xx, "rg35xx", "RG35XX"}, {Device::tablet, "tablet", "tablet"}, - //{Device::custom, "custom", "Custom"}, + {Device::gameboyplayer, "gameboyplayer", "GameBoyPlayer"}, + {Device::custom, "custom", "Custom"}, }, LockMode::LOCK_WHILE_RUNNING, Device::switch_1_2 @@ -115,6 +116,13 @@ void GameSettings::on_config_value_changed(void* object){ GAME_BOX.HEIGHT.set(0.643026); GAME_BOX.set_visibility(ConfigOptionState::DISABLED); break; + case Device::gameboyplayer: + GAME_BOX.X.set(0.064217); + GAME_BOX.Y.set(0.107667); + GAME_BOX.WIDTH.set(0.86413); + GAME_BOX.HEIGHT.set(0.787928); + GAME_BOX.set_visibility(ConfigOptionState::DISABLED); + break; case Device::custom: GAME_BOX.set_visibility(ConfigOptionState::ENABLED); break; diff --git a/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.h b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.h index 54704ba1a1..fc6086e0e7 100644 --- a/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.h +++ b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Settings.h @@ -29,6 +29,7 @@ class GameSettings : public BatchOption, private ConfigOption::Listener{ switch_1_2, rg35xx, tablet, + gameboyplayer, custom, }; diff --git a/SerialPrograms/Source/PokemonRSE/PokemonRSE_Tests.cpp b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Tests.cpp new file mode 100644 index 0000000000..e34b1af8de --- /dev/null +++ b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Tests.cpp @@ -0,0 +1,27 @@ +/* Pokemon RSE Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "PokemonRSE/Inference/Dialogs/PokemonRSE_DialogDetector.h" +#include "PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.h" +#include "PokemonRSE_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonRSE{ + + +void add_tests(UnitTestDatabase& database){ + add_tests_SelectionDialogDetector(database); + add_tests_ConfirmSlotDetector(database); + add_tests_StartMenuDetector(database); + add_tests_StartMenuSlotDetector(database); +} + + +} +} +} \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonRSE/PokemonRSE_Tests.h b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Tests.h new file mode 100644 index 0000000000..8873ff84d7 --- /dev/null +++ b/SerialPrograms/Source/PokemonRSE/PokemonRSE_Tests.h @@ -0,0 +1,25 @@ +/* Pokemon RSE Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonRSE_Tests_H +#define PokemonAutomation_PokemonRSE_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonRSE{ + + + +void add_tests(UnitTestDatabase& database); + + + +} +} +} +#endif \ No newline at end of file diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 00d52f1fc0..ea4541a76d 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -2156,6 +2156,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonRSE/Inference/Menus/PokemonRSE_StartMenuDetector.h Source/PokemonRSE/Inference/Menus/PokemonRSE_SummaryDetector.cpp Source/PokemonRSE/Inference/Menus/PokemonRSE_SummaryDetector.h + Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.cpp + Source/PokemonRSE/Inference/PokemonRSE_SelectionDetector.h Source/PokemonRSE/Inference/PokemonRSE_ShinyNumberDetector.cpp Source/PokemonRSE/Inference/PokemonRSE_ShinyNumberDetector.h Source/PokemonRSE/Inference/Sounds/PokemonRSE_ShinySoundDetector.cpp @@ -2166,6 +2168,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonRSE/PokemonRSE_Panels.h Source/PokemonRSE/PokemonRSE_Settings.cpp Source/PokemonRSE/PokemonRSE_Settings.h + Source/PokemonRSE/PokemonRSE_Tests.cpp + Source/PokemonRSE/PokemonRSE_Tests.h Source/PokemonRSE/Programs/ShinyHunting/PokemonRSE_AudioStarterReset.cpp Source/PokemonRSE/Programs/ShinyHunting/PokemonRSE_AudioStarterReset.h Source/PokemonRSE/Programs/ShinyHunting/PokemonRSE_GiftReset.cpp