diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options.h b/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options.h index 6b4e8335c..fc3696686 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options.h +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options.h @@ -59,6 +59,12 @@ class EndBattleDecider{ const PathStats& path_stats, bool any_shiny, bool boss_is_shiny ) const = 0; + + // For BossFinder: whether the boss is in the "save on the go" list. + virtual bool is_in_save_list(const std::string& boss_slug) const { return false; } + + // For Standard/StrongBoss: whether to keep a followed path when prompted. + //virtual bool should_keep_followed_path() const { return false; } }; diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.cpp b/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.cpp index 61d467e4c..e25168c13 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.cpp +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.cpp @@ -4,10 +4,14 @@ * */ +#include +#include //#include "Common/Compiler.h" //#include "Common/Cpp/Json/JsonValue.h" //#include "Common/Cpp/Json/JsonArray.h" //#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/ConfigOption.h" //#include "CommonFramework/Globals.h" #include "Pokemon/Pokemon_Strings.h" #include "Pokemon/Resources/Pokemon_PokemonNames.h" @@ -16,6 +20,7 @@ #include "PokemonSwSh/Resources/PokemonSwSh_MaxLairDatabase.h" #include "PokemonSwSh_MaxLair_Options_BossAction.h" + //#include //using std::cout; //using std::endl; @@ -49,30 +54,101 @@ BossActionRow::BossActionRow(std::string slug, const std::string& name_slug, con BossAction::CATCH_AND_STOP_IF_SHINY ) , ball(LockMode::UNLOCK_WHILE_RUNNING, "poke-ball") + , save_on_the_go(LockMode::UNLOCK_WHILE_RUNNING, false) { PA_ADD_STATIC(pokemon); add_option(action, "Action"); add_option(ball, "Ball"); + add_option(save_on_the_go, "Save Path"); + + save_on_the_go.set_visibility( + action == BossAction::CATCH_AND_STOP_IF_SHINY ? ConfigOptionState::ENABLED : ConfigOptionState::DISABLED + ); + + action.add_listener(*this); +} + +void BossActionRow::on_config_value_changed(void* object) { + if (action != BossAction::CATCH_AND_STOP_IF_SHINY) { + save_on_the_go = false; + } } BossActionTable::BossActionTable() : StaticTableOption("Boss Actions:", LockMode::UNLOCK_WHILE_RUNNING) + , m_reverting(false) { for (const auto& item : all_bosses_by_dex()){ // cout << item.second << endl; const MaxLairSlugs& slugs = get_maxlair_slugs(item.second); const std::string& sprite_slug = *slugs.sprite_slugs.begin(); const std::string& name_slug = slugs.name_slug; - add_row(std::make_unique(item.second, name_slug, sprite_slug)); + + auto row = std::make_unique(item.second, name_slug, sprite_slug); + m_rows.push_back(row.get()); + add_row(std::move(row)); } finish_construction(); + + for (auto* row : m_rows) { + row->save_on_the_go.add_listener(*this); + row->action.add_listener(*this); + } + + update_checkbox_states(); } + +BossActionTable::~BossActionTable(){ + for (auto* row : m_rows) { + row->save_on_the_go.remove_listener(*this); + row->action.remove_listener(*this); + } +} + +void BossActionTable::update_checkbox_states() { + size_t checked = 0; + for (auto* row : m_rows) { + if (row->save_on_the_go) checked++; + } + for (auto* row : m_rows) { + bool action_ok = (row->action == BossAction::CATCH_AND_STOP_IF_SHINY); + bool disable_by_max = (checked >= 3 && !row->save_on_the_go); + ConfigOptionState state = (action_ok && !disable_by_max) ? ConfigOptionState::ENABLED : ConfigOptionState::DISABLED; + row->save_on_the_go.set_visibility(state); + } +} + +void BossActionTable::on_config_value_changed(void* object) { + if (m_reverting) return; + + // Counting how many checkboxes are currently checked + size_t checked = 0; + for (auto* row : m_rows) { + if (row->save_on_the_go) checked++; + } + + // If we exceed the 3 boxes ticked, we revert the change for the last box ticked + if (checked > 3) { + for (auto* row : m_rows) { + if (object == &row->save_on_the_go && row->save_on_the_go) { + m_reverting = true; + row->save_on_the_go = false; + m_reverting = false; + + return; + } + } + } + update_checkbox_states(); +} + std::vector BossActionTable::make_header() const{ std::vector ret{ STRING_POKEMON, "Action", - STRING_POKEBALL + STRING_POKEBALL, + "Save Path (Max 3)" }; return ret; } diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.h b/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.h index 3e228c508..8b01a2b25 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.h +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/Options/PokemonSwSh_MaxLair_Options_BossAction.h @@ -11,6 +11,10 @@ #include "Common/Cpp/Options/StaticTableOption.h" #include "CommonFramework/Options/LabelCellOption.h" #include "PokemonSwSh/Options/PokemonSwSh_BallSelectOption.h" +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/ConfigOption.h" +#include +#include namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -23,20 +27,36 @@ enum class BossAction{ CATCH_AND_STOP_IF_SHINY, }; +const EnumDropdownDatabase& BossAction_Database(); -class BossActionRow : public StaticTableRow{ +class BossActionRow : public StaticTableRow, +private ConfigOption::Listener +{ public: BossActionRow(std::string slug, const std::string& name_slug, const std::string& sprite_slug); + virtual void on_config_value_changed(void* object) override; + LabelCellOption pokemon; EnumDropdownCell action; PokemonBallSelectCell ball; + BooleanCheckBoxCell save_on_the_go; }; -class BossActionTable : public StaticTableOption{ +class BossActionTable : public StaticTableOption, +private ConfigOption::Listener +{ public: BossActionTable(); - virtual std::vector make_header() const; + ~BossActionTable(); + + virtual void on_config_value_changed(void* object) override; + virtual std::vector make_header() const override; + +private: + std::vector m_rows; + bool m_reverting; + void update_checkbox_states(); }; diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/PokemonSwSh_MaxLair_BossFinder.cpp b/SerialPrograms/Source/PokemonSwSh/MaxLair/PokemonSwSh_MaxLair_BossFinder.cpp index 1b503f243..6a80a6de1 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/PokemonSwSh_MaxLair_BossFinder.cpp +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/PokemonSwSh_MaxLair_BossFinder.cpp @@ -151,6 +151,9 @@ class EndBattleDecider_BossFinder : public EndBattleDecider{ } throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid enum."); } + virtual bool is_in_save_list(const std::string& boss_slug) const override { + return get_filter(boss_slug).save_on_the_go; + } private: diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.cpp b/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.cpp index a86a977f6..20b302072 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.cpp +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.cpp @@ -8,23 +8,82 @@ #include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "CommonTools/Images/SolidColorTest.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Inference/Pokemon_NameReader.h" +#include "PokemonSwSh/MaxLair/Inference/PokemonSwSh_MaxLair_Detect_PokemonReader.h" #include "PokemonSwSh_MaxLair_Run_Entrance.h" +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonSwSh{ namespace MaxLairInternal{ +using namespace Pokemon; + +namespace { +// Boxes for the corresponding pokemon names in case the list is full and the player battled a boss that isn't in the list +const ImageFloatBox NAME_BOXES[3] = { + {0.685000, 0.531000, 0.130000, 0.061000}, + {0.685000, 0.586000, 0.130000, 0.061000}, + {0.685000, 0.645000, 0.130000, 0.053000} +}; + +// Read the three saved paths names if the user has already saved 3 paths +std::vector read_saved_paths( + VideoStream& stream, + Language language, + const ImageViewRGB32& screen + ) { + + std::vector slugs; + + for (int i = 0; i < 3; ++i) { + auto cropped = extract_box_reference(screen, NAME_BOXES[i]); + OCR::StringMatchResult result = PokemonNameReader::instance().read_substring( + stream.logger(), language, cropped, OCR::BLACK_OR_WHITE_TEXT_FILTERS(), + 0.01, 0.50, 2.0 + ); + if (result.results.empty()) { + slugs.emplace_back(); + } else { + slugs.push_back(result.results.begin()->second.token); + } + } + return slugs; +} + +// Read the three currently saved paths (if any) from the entrance screen and return the index of the first slot that is NOT protected, returns -1 otherwise +int find_unprotected_slot( + const std::vector& current_slugs, + const EndBattleDecider& actions, + Logger& logger + ) { + for (int i = 0; i < 3; ++i) { + if (current_slugs[i].empty()) { + // Empty slot, override and add error + logger.log("Failed to read slot " + std::to_string(i) + ", replacing it.", COLOR_RED); + return i; + } + if (!actions.is_in_save_list(current_slugs[i])) { + logger.log("Slot " + std::to_string(i) + " contains unprotected boss, will replace it.", COLOR_BLUE); + return i; + } + } + logger.log("All slots already saved and protected", COLOR_RED); + return -1; +} +} void run_entrance( - AdventureRuntime& runtime, - ProgramEnvironment& env, size_t console_index, - VideoStream& stream, ProControllerContext& context, - bool save_path, - GlobalStateTracker& state_tracker -){ + AdventureRuntime& runtime, + ProgramEnvironment& env, size_t console_index, + VideoStream& stream, ProControllerContext& context, + bool followed_path, + GlobalStateTracker& state_tracker + ){ GlobalState& state = state_tracker[console_index]; - + if (!state.adventure_started){ stream.log("Failed to start raid.", COLOR_RED); runtime.session_stats.add_error(); @@ -35,29 +94,142 @@ void run_entrance( runtime.path_stats.clear(); } } - - - OverlayBoxScope box(stream.overlay(), {0.782, 0.850, 0.030, 0.050}); - - pbf_wait(context, 2000ms); - while (true){ - if (save_path){ - pbf_press_button(context, BUTTON_A, 160ms, 1000ms); - }else{ - pbf_press_button(context, BUTTON_B, 160ms, 1000ms); + context.wait_for(1000ms); + + // Get the boss slug + std::string boss_slug; + if (runtime.host_index < runtime.console_settings.active_consoles()) { + boss_slug = state_tracker.infer_actual_state(runtime.host_index).boss; + }; + + bool save_path = false; + + if (!followed_path) { + // Check if the user checked the box to save the path when running the BossFinder program + + if (!boss_slug.empty()) { + save_path = runtime.actions.is_in_save_list(boss_slug); + stream.log("Boss: " + boss_slug + ", should save: " + (save_path ? "Yes" : "No"), COLOR_BLUE); + }; + } else { + save_path = followed_path; + } + + Language language = runtime.console_settings[console_index].language; + + // Overlay box to detect when a dialogue box, a Yes/No option to save a path or erasing a path if our list is full is present + OverlayBoxScope dialog_box(stream.overlay(), {0.78, 0.85, 0.03, 0.05}); + OverlayBoxScope yes_no_box(stream.overlay(), {0.68, 0.75, 0.135, 0.02}); + OverlayBoxScope paths_box(stream.overlay(), {0.685, 0.515, 0.13, 0.013}); + + // Timeout: 5 minutes + auto start_time = std::chrono::steady_clock::now(); + const auto timeout = std::chrono::minutes(5); + + while(true) { + auto now = std::chrono::steady_clock::now(); + if (now - start_time > timeout) { + stream.log("Entrance dialogue timed out after 5 minutes.", COLOR_RED); + throw OperationFailedException(ErrorReport::SEND_ERROR_REPORT, "Entrance dialogue timed out.", stream); } + context.wait_for_all_requests(); - + context.wait_for(2000ms); VideoSnapshot screen = stream.video().snapshot(); - ImageStats stats = image_stats(extract_box_reference(screen, box)); - if (!is_grey(stats, 400, 1000)){ - break; - } + if (!screen) continue; + + ImageStats dialog_box_stats = image_stats(extract_box_reference(screen, dialog_box)); + + ImageStats yes_no_box_stats = image_stats(extract_box_reference(screen, yes_no_box)); + + ImageStats paths_box_stats = image_stats(extract_box_reference(screen, paths_box)); + + bool dialog_box_present = is_grey(dialog_box_stats, 400, 1000); + + bool yes_no_box_present = is_white(yes_no_box_stats, 400, 10); + + bool paths_box_present = is_white(paths_box_stats, 400, 10); + + if (paths_box_present && yes_no_box_present) { + + if (save_path) { + // Bring the cursor to the bottom position to correctly identify boss names + pbf_press_dpad(context, DPAD_UP, 160ms, 80ms); + + context.wait_for_all_requests(); + + VideoSnapshot clean_screen = stream.video().snapshot(); + + if (!clean_screen) continue; + + size_t attempts = 0; + const size_t MAX_ATTEMPTS = 10; + bool done = false; + while (!done && attempts < MAX_ATTEMPTS) { + attempts++; + + std::vector names = read_saved_paths(stream, language, clean_screen); + int non_empty = 0; + for (const auto& n : names) { + if (!n.empty()) ++non_empty; + } + bool in_list = (non_empty >= 2); // If there are at least 2 readable names, then we can read the list + + if (in_list) { + context.wait_for(1000ms); + + int slot = find_unprotected_slot(names, runtime.actions, stream.logger()); + if (slot == -1) { + stream.log("Unable to save new boss or all bosses from list already saved, cancelling", COLOR_ORANGE); + runtime.session_stats.add_error(); + pbf_press_button(context, BUTTON_B, 160ms, 1000ms); + } else { + // Bring cursor back to the top position + pbf_press_dpad(context, DPAD_DOWN, 160ms, 500ms); + context.wait_for(1000ms); + // Then move down to target slot + for (int i = 0; i < slot; ++i) { + pbf_press_dpad(context, DPAD_DOWN, 160ms, 500ms); + }; + context.wait_for_all_requests(); + stream.log("Erasing old path and saving new path"); + pbf_press_button(context, BUTTON_A, 160ms, 1000ms); + + send_program_notification(env, runtime.notification_status, COLOR_BLUE, "Path Saved", {{"Boss: ", boss_slug}}, ""); + + } + context.wait_for_all_requests(); + done = true; + } + if (attempts >= MAX_ATTEMPTS) { + stream.log("New-path save dialogue timed out.", COLOR_RED); + }; + } + } else { + stream.log("Not saving path"); + pbf_press_button(context, BUTTON_B, 160ms, 1000ms); + }; + + } else if (!paths_box_present && yes_no_box_present) { + + if (save_path) { + stream.log("Saving new path or keeping old path"); + pbf_press_button(context, BUTTON_A, 160ms, 1000ms); + send_program_notification(env, runtime.notification_status, COLOR_BLUE, "Path Saved", {{"Boss: ", boss_slug}}, ""); + } else { + stream.log("Not saving new path"); + pbf_press_button(context, BUTTON_B, 160ms, 1000ms); + } + } else if (!dialog_box_present) { + return; + }; + + pbf_press_button(context, BUTTON_A, 160ms, 1000ms); } + + + } - - - } } } diff --git a/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.h b/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.h index 14c0d8a90..cd9ae7c3e 100644 --- a/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.h +++ b/SerialPrograms/Source/PokemonSwSh/MaxLair/Program/PokemonSwSh_MaxLair_Run_Entrance.h @@ -22,7 +22,7 @@ void run_entrance( AdventureRuntime& runtime, ProgramEnvironment& env, size_t console_index, VideoStream& stream, ProControllerContext& context, - bool save_path, + bool followed_path, GlobalStateTracker& state_tracker );