diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp index 065c20c9a8..27620b11c8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp @@ -22,6 +22,7 @@ #include "Programs/ShinyHunting/PokemonFRLG_PrizeCornerReset.h" #include "Programs/ShinyHunting/PokemonFRLG_ShinyHunt-Fishing.h" #include "Programs/ShinyHunting/PokemonFRLG_ShinyHunt-Overworld.h" +#include "Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.h" #include "Programs/RngManipulation/PokemonFRLG_RngHelper.h" #include "Programs/RngManipulation/PokemonFRLG_SidHelper.h" #include "Programs/RngManipulation/PokemonFRLG_StarterRng.h" @@ -71,6 +72,7 @@ std::vector PanelListFactory::make_panels() const{ ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back(make_SingleSwitchProgram()); + ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back(make_SingleSwitchProgram()); ret.emplace_back("---- RNG Manipulation ----"); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.cpp new file mode 100644 index 0000000000..fd184c529a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.cpp @@ -0,0 +1,237 @@ +/* Wild Pokemon Finder + * + * Search grass encounters until a selected species is found. + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" +#include "Pokemon/Pokemon_Strings.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" +#include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_WildEncounterReader.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_EncountersDatabase.h" +#include "PokemonFRLG_WildPokemonFinder.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +namespace{ + +std::set grass_species_in_database(const EncountersDatabase& database){ + std::set species; + for (const auto& [location, slots] : database.get_throw("grass")){ + for (const AdvEncounterSlot& slot : slots){ + species.insert(slot.species); + } + } + return species; +} + +std::vector all_grass_species(){ + std::set species; + for (const char* path : { + "PokemonFRLG/EncounterSlotsFR.json", + "PokemonFRLG/EncounterSlotsLG.json", + }){ + EncountersDatabase database(path); + const std::set game_species = grass_species_in_database(database); + species.insert(game_species.begin(), game_species.end()); + } + return std::vector(species.begin(), species.end()); +} + +void find_next_battle(ConsoleHandle& console, ProControllerContext& context){ + BlackScreenWatcher battle_entered(COLOR_RED); + int result = 1; + while (result != 0){ + result = run_until( + console, context, + [](ProControllerContext& context){ + ssf_press_left_joystick(context, { -1, 0 }, 0ms, 1000ms); + ssf_mash1_button(context, BUTTON_B, 936ms); + ssf_press_left_joystick(context, { +1, 0 }, 0ms, 1000ms); + ssf_mash1_button(context, BUTTON_B, 936ms); + }, + { battle_entered } + ); + } + + BattleMenuWatcher battle_menu(COLOR_RED); + WallClock deadline = current_time() + 60s; + while (current_time() < deadline){ + result = run_until( + console, context, + [](ProControllerContext& context){ + pbf_mash_button(context, BUTTON_B, 5000ms); + }, + { battle_menu } + ); + if (result == 0){ + return; + } + } + + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "WildPokemonFinder: Battle menu was not detected after entering an encounter.", + console + ); +} + +} + +WildPokemonFinder_Descriptor::WildPokemonFinder_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonFRLG:WildPokemonFinder", + Pokemon::STRING_POKEMON + " FRLG", "Wild Pokemon Finder", + "Programs/PokemonFRLG/WildPokemonFinder.html", + "Search grass encounters for a selected Pokemon. Start in grass where the target appears, and use a lead that can always flee.", + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS + ) +{} + +struct WildPokemonFinder_Descriptor::Stats : public StatsTracker{ + Stats() + : encounters(m_stats["Encounters"]) + , unreadable(m_stats["Unreadable Encounters"]) + { + m_display_order.emplace_back("Encounters"); + m_display_order.emplace_back("Unreadable Encounters", HIDDEN_IF_ZERO); + } + std::atomic& encounters; + std::atomic& unreadable; +}; + +std::unique_ptr WildPokemonFinder_Descriptor::make_stats() const{ + return std::make_unique(); +} + +WildPokemonFinder::WildPokemonFinder() + : GAME_VERSION( + "Game Version:", + { + {GameVersion::firered, "firered", "FireRed"}, + {GameVersion::leafgreen, "leafgreen", "LeafGreen"}, + }, + LockMode::LOCK_WHILE_RUNNING, + GameVersion::firered + ) + , TARGET_POKEMON( + "Target Wild Pokemon:
Only Pokemon available in grass encounters are listed." + " Choose a target available in the selected game version.", + all_grass_species(), + "pikachu" + ) + , LANGUAGE( + "Game Language:", + { + Language::English, + Language::Japanese, + Language::Spanish, + Language::French, + Language::German, + Language::Italian, + }, + LockMode::LOCK_WHILE_RUNNING, + true + ) + , NOTIFICATION_TARGET_FOUND( + "Target Pokemon found", + true, true, ImageAttachmentMode::JPG, + {"Notifs", "Showcase"} + ) + , NOTIFICATION_STATUS_UPDATE("Status Update", true, false, std::chrono::seconds(3600)) + , NOTIFICATIONS({ + &NOTIFICATION_TARGET_FOUND, + &NOTIFICATION_STATUS_UPDATE, + &NOTIFICATION_PROGRAM_FINISH, + }) +{ + PA_ADD_OPTION(GAME_VERSION); + PA_ADD_OPTION(TARGET_POKEMON); + PA_ADD_OPTION(LANGUAGE); + PA_ADD_OPTION(NOTIFICATIONS); +} + +void WildPokemonFinder::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + WildPokemonFinder_Descriptor::Stats& stats = + env.current_stats(); + + const bool firered = GAME_VERSION == GameVersion::firered; + EncountersDatabase encounter_database( + firered ? "PokemonFRLG/EncounterSlotsFR.json" : "PokemonFRLG/EncounterSlotsLG.json" + ); + const std::set available_species = grass_species_in_database(encounter_database); + const std::string target = TARGET_POKEMON.slug(); + if (available_species.find(target) == available_species.end()){ + throw UserSetupError( + env.console, + "The selected Pokemon is not available in grass encounters in " + + std::string(firered ? "FireRed." : "LeafGreen.") + ); + } + + home_black_border_check(env.console, context); + + WildEncounterReader reader(COLOR_RED); + VideoOverlaySet overlays(env.console.overlay()); + reader.make_overlays(overlays); + + env.log("Searching for wild Pokemon: " + target); + while (true){ + find_next_battle(env.console, context); + + VideoSnapshot screen = env.console.video().snapshot(); + PokemonFRLG_WildEncounter encounter = reader.read_encounter( + env.logger(), LANGUAGE, screen, available_species + ); + stats.encounters++; + env.update_stats(); + + if (encounter.name == target){ + const std::string display_name = TARGET_POKEMON.display_name(); + env.log("Target Pokemon found: " + display_name, COLOR_GREEN); + send_program_notification( + env, + NOTIFICATION_TARGET_FOUND, + COLOR_GREEN, + "Target Pokemon found: " + display_name, + {}, "", + screen, + true + ); + send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH); + return; + } + + if (encounter.name.empty()){ + stats.unreadable++; + env.log("Unable to read the wild Pokemon; fleeing and trying again.", COLOR_RED); + env.update_stats(); + }else{ + env.log("Found " + encounter.name + "; target is " + target + "."); + } + + flee_battle(env.console, context); + context.wait_for_all_requests(); + send_program_status_notification(env, NOTIFICATION_STATUS_UPDATE); + } +} + +} +} +} \ No newline at end of file diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.h new file mode 100644 index 0000000000..7fcdde09a3 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.h @@ -0,0 +1,54 @@ +/* Wild Pokemon Finder + * + * Search grass encounters until a selected species is found. + */ + +#ifndef PokemonAutomation_PokemonFRLG_WildPokemonFinder_H +#define PokemonAutomation_PokemonFRLG_WildPokemonFinder_H + +#include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "CommonTools/Options/LanguageOCROption.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" +#include "Pokemon/Options/Pokemon_NameSelectOption.h" +#include "Common/Cpp/Options/EnumDropdownOption.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +class WildPokemonFinder_Descriptor : public SingleSwitchProgramDescriptor{ +public: + WildPokemonFinder_Descriptor(); + struct Stats; + std::unique_ptr make_stats() const override; +}; + +class WildPokemonFinder : public SingleSwitchProgramInstance{ +public: + using Descriptor = WildPokemonFinder_Descriptor; + WildPokemonFinder(); + + void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + + void start_program_border_check(VideoStream&, FeedbackType) override{} + +private: + enum class GameVersion{ + firered, + leafgreen, + }; + + EnumDropdownOption GAME_VERSION; + Pokemon::PokemonNameSelectOption TARGET_POKEMON; + OCR::LanguageOCROption LANGUAGE; + + EventNotificationOption NOTIFICATION_TARGET_FOUND; + EventNotificationOption NOTIFICATION_STATUS_UPDATE; + EventNotificationsOption NOTIFICATIONS; +}; + +} +} +} + +#endif \ No newline at end of file diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index f5e1ea9435..81aec1c5b1 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1702,6 +1702,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_ShinyHunt-Fishing.h Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_ShinyHunt-Overworld.cpp Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_ShinyHunt-Overworld.h + Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.cpp + Source/VideoGames/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_WildPokemonFinder.h Source/VideoGames/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadBattleLevelUp.cpp Source/VideoGames/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadBattleLevelUp.h Source/VideoGames/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadEncounter.cpp