diff --git a/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.cpp b/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.cpp
index 1f9a04ba2c..1878b25cba 100644
--- a/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.cpp
+++ b/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.cpp
@@ -18,6 +18,7 @@
#include "Pokemon/Pokemon_Notification.h"
#include "Pokemon/Pokemon_Strings.h"
#include "PokemonSwSh/Commands/PokemonSwSh_Commands_DateSpam.h"
+#include "PokemonSwSh/Inference/PokemonSwSh_BoxEmptySlotDetector.h"
#include "PokemonSwSh/Inference/PokemonSwSh_BoxGenderDetector.h"
#include "PokemonSwSh/Inference/PokemonSwSh_BoxShinySymbolDetector.h"
#include "PokemonSwSh/Inference/PokemonSwSh_DialogBoxDetector.h"
@@ -104,19 +105,6 @@ EggAutonomous::EggAutonomous()
LockMode::LOCK_WHILE_RUNNING,
1, 1
)
- , NUM_EGGS_IN_COLUMN(
- "Num Eggs in Column:
How many eggs already deposited in the first column in Box 1.",
- {
- {0, "0", "0"},
- {1, "1", "1"},
- {2, "2", "2"},
- {3, "3", "3"},
- {4, "4", "4"},
- {5, "5", "5"},
- },
- LockMode::LOCK_WHILE_RUNNING,
- 0
- )
, AUTO_SAVING(
"Auto-Saving:
Automatically save the game to recover from crashes and allow eggs to be unhatched.
"
"(Unhatching eggs can be useful for obtaining breeding parents by rehatching a perfect egg in a game with a different language.)
"
@@ -179,7 +167,6 @@ EggAutonomous::EggAutonomous()
PA_ADD_OPTION(LANGUAGE);
PA_ADD_OPTION(MAX_KEEPERS);
PA_ADD_OPTION(LOOPS_PER_FETCH);
- PA_ADD_OPTION(NUM_EGGS_IN_COLUMN);
PA_ADD_OPTION(AUTO_SAVING);
PA_ADD_OPTION(FILTERS0);
PA_ADD_OPTION(NOTIFICATIONS);
@@ -213,12 +200,52 @@ void EggAutonomous::program(SingleSwitchProgramEnvironment& env, ProControllerCo
return;
}
+ menus_to_boxsystem(env.console, context);
+ check_box(env.console, context);
+ context.wait_for_all_requests();
+ context.wait_for(500ms);
+ auto screen = env.console.video().snapshot();
+ size_t num_eggs_in_column_0 = count_eggs_in_first_box_column(env.console, screen);
+ size_t num_empty_slots_in_column_0 = count_empty_slots_in_first_box_column(env.console, screen);
+ size_t num_eggs_in_party = count_eggs_in_party(env.console, screen);
+ size_t num_empty_slots_in_party = count_empty_slots_in_party(env.console, screen);
+ if (num_eggs_in_column_0 + num_empty_slots_in_column_0 != 5){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "Total number of eggs and empty slots in the first box column don't add up to 5. "
+ "During setup, ensure there are no non-egg Pokemon in the first box column.",
+ env.console
+ );
+ }
+ if (num_eggs_in_party + num_empty_slots_in_party != 5){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "Total number of eggs and empty slots in the party don't add up to 5. "
+ "During setup, ensure there is only one Pokemon in the lead slot, and no other Pokemon.",
+ env.console
+ );
+ }
+
+ env.log("Starting with " + std::to_string(num_eggs_in_party) + " eggs in the party and " +
+ std::to_string(num_eggs_in_column_0) + " eggs in the first box column.");
+
+ // select top Pokemon in party
+ box_scroll(context, DPAD_LEFT);
+ // Change box view to judge or stats
+ Language language = LANGUAGE;
+ if (language == Language::None){
+ change_view_to_stats_or_judge(env.console, context);
+ }else{
+ change_view_to_judge(env.console, context, language);
+ }
if (AUTO_SAVING == AutoSave::AfterStartAndKeep){
- save_game(env, context);
- m_num_eggs_in_storage_when_game_saved = static_cast(NUM_EGGS_IN_COLUMN.current_value());
+ save_game(env.console, context);
+ m_num_eggs_in_storage_when_game_saved = num_eggs_in_column_0;
+ m_num_eggs_in_party_when_game_saved = num_eggs_in_party;
}
- m_num_eggs_retrieved = static_cast(NUM_EGGS_IN_COLUMN.current_value());
+ num_eggs_in_column_0_at_batch_start = num_eggs_in_column_0;
+ m_num_eggs_in_party_at_batch_start = num_eggs_in_party;
m_num_pokemon_kept = 0;
@@ -232,7 +259,7 @@ void EggAutonomous::program(SingleSwitchProgramEnvironment& env, ProControllerCo
if (TOUCH_DATE_INTERVAL.ok_to_touch_now()){
env.log("Touching date to prevent rollover.");
env.console.overlay().add_log("Touching date", COLOR_WHITE);
- pbf_press_button(context, BUTTON_HOME, 160ms, GameSettings::instance().GAME_TO_HOME_DELAY_SAFE0);
+ go_home(env.console, context);
touch_date_from_home(env.console, context, ConsoleSettings::instance().SETTINGS_TO_HOME_DELAY0);
resume_game_no_interact(env.console, context, ConsoleSettings::instance().TOLERATE_SYSTEM_UPDATE_MENU_FAST);
}
@@ -269,7 +296,7 @@ void EggAutonomous::program(SingleSwitchProgramEnvironment& env, ProControllerCo
env.console
);
}
- ssf_press_button(context, BUTTON_HOME, GameSettings::instance().GAME_TO_HOME_DELAY_SAFE0, 160ms);
+ go_home(env.console, context);
env.console.overlay().add_log("Reset game", COLOR_WHITE);
reset_game_from_home_with_inference(
env.console, context,
@@ -277,7 +304,8 @@ void EggAutonomous::program(SingleSwitchProgramEnvironment& env, ProControllerCo
);
m_player_at_loop_start = false;
- m_num_eggs_retrieved = m_num_eggs_in_storage_when_game_saved;
+ num_eggs_in_column_0_at_batch_start = m_num_eggs_in_storage_when_game_saved;
+ m_num_eggs_in_party_at_batch_start = m_num_eggs_in_party_when_game_saved;
}
}
@@ -299,119 +327,108 @@ bool EggAutonomous::run_batch(
env.update_stats();
send_program_status_notification(env, NOTIFICATION_STATUS_UPDATE);
+ size_t total_eggs_to_fetch = 10 - num_eggs_in_column_0_at_batch_start - m_num_eggs_in_party_at_batch_start;
+ size_t num_eggs_retrieved = 0;
+
+ EggAutoPhase phase = EggAutoPhase::BIKE_LOOP;
if (m_player_at_loop_start == false){ // reset position
- const bool fly_from_overworld = true; // fly from menu
- call_flying_taxi(env, context, fly_from_overworld);
+ phase = EggAutoPhase::FLY_RESET;
}
- size_t bike_loop_count = 0;
+ size_t total_bike_loop_count = 0;
const size_t MAX_BIKE_LOOP_COUNT = 100;
+ size_t num_loops_since_last_fetch_attempt = 0;
+
size_t num_eggs_hatched = 0;
m_player_at_loop_start = false;
-
+
// Each iteration in the while-loop is made by:
// - bike loops of LOOPS_PER_FETCH times. Bike loops begin at lady or nursery front door, end at lady.
// - if not enough eggs fetched, talk to lady to try fetching an egg.
- while (num_eggs_hatched < 5 || m_num_eggs_retrieved < 5){
- // Detect when Y-Comm icon disappears. This is the time an egg is hatching
- const bool y_comm_visible_when_egg_hatching = false;
- YCommIconWatcher egg_hatching_detector(COLOR_RED, y_comm_visible_when_egg_hatching);
-
- bool restart_bike_loop = false;
- for (size_t i_bike_loop = 0; i_bike_loop < this->LOOPS_PER_FETCH && bike_loop_count < MAX_BIKE_LOOP_COUNT;){
- context.wait_for_all_requests();
- // +1 here because video overlay is for general users. General users start counts at 1, while us programmers start count at 0.
- if (restart_bike_loop){
- env.console.overlay().add_log("Restart loop " + std::to_string(bike_loop_count+1), COLOR_WHITE);
- restart_bike_loop = false;
- }else{
- env.console.overlay().add_log("Loop " + std::to_string(bike_loop_count+1), COLOR_WHITE);
+ while (num_eggs_hatched < 5 || num_eggs_retrieved < total_eggs_to_fetch){
+ env.log("Eggs hatched: " + std::to_string(num_eggs_hatched) +
+ "/5. Eggs fetched: " + std::to_string(num_eggs_retrieved) + "/" + std::to_string(total_eggs_to_fetch));
+
+ // NOTE: the egg hatching detector cannot be constantly running,
+ // since it only detects the black dialog box, and speaking to the lady will also produce a black dialog box.
+ // therefore, each phase has its own egg hatching detector, so it can be more easily turned on and off.
+
+ switch(phase){
+ case EggAutoPhase::BIKE_LOOP:{
+ env.console.overlay().add_log("Loop " + std::to_string(total_bike_loop_count+1), COLOR_WHITE);
+ env.console.log("Bike Loop " + std::to_string(total_bike_loop_count+1));
+ bool hatch_detected = run_bike_loop(env, context);
+ if (hatch_detected){
+ phase = EggAutoPhase::HATCHING;
+ continue;
}
- int ret = run_until(
- env.console, context,
- [](ProControllerContext& context){
- travel_to_spin_location(context);
- travel_back_to_lady(context);
- },
- {{egg_hatching_detector}}
- );
- if (ret < 0){ // we are at nursery lady; no egg hatching detected
- ++i_bike_loop;
- ++bike_loop_count;
- continue;
+ // done Bike loop
+ ++num_loops_since_last_fetch_attempt;
+ ++total_bike_loop_count;
+ if (total_bike_loop_count >= MAX_BIKE_LOOP_COUNT){
+ // throw exception
+ exceed_bike_loop_limit(env, context, MAX_BIKE_LOOP_COUNT);
}
- // Egg hatching
- do{
- ++num_eggs_hatched;
- stats.m_hatched++;
- env.update_stats();
- wait_for_egg_hatched(env, context, stats, num_eggs_hatched);
- if (num_eggs_hatched == 5){
- // We hatched all five eggs. No more eggs can hatch. Go to next loop
- break;
+ if (num_loops_since_last_fetch_attempt < LOOPS_PER_FETCH){
+ phase = EggAutoPhase::BIKE_LOOP; // repeat bike loop
+ }else{
+ if (num_eggs_retrieved < total_eggs_to_fetch){
+ phase = EggAutoPhase::FETCH_EGG;
+ }else{
+ env.log("Done retrieving eggs. Resume bike loop to hatch eggs.");
+ // done retrieving eggs
+ // resume hatching eggs if needed
+ phase = EggAutoPhase::BIKE_LOOP;
}
- // Now we see if we can hatch one more egg.
- ret = run_until(
- env.console, context,
- [](ProControllerContext& context){
- // Try move a little to hatch more:
- // We move toward lower-left so that it wont hit the lady or enter the Nursory.
- // Add 1 second of settle time to stop moving.
- // We need to not be moving before trying to fly or an egg will hatch during
- // that sequence when we cannot handle it.
- pbf_move_left_joystick(context, {-1, -1}, 800ms, 1000ms);
- },
- {{egg_hatching_detector}}
- );
- }while (ret == 0);
- // now no more hatching in this bike loop
- // We either cannot find a consecutive hatch any more or we already hatch five of them
-
- if (num_eggs_hatched == 5 && m_num_eggs_retrieved == 5){
- m_player_at_loop_start = false;
- break;
}
-
- // Now we either cannot find a consecutive hatch any more, or we already hatch five for them, but
- // we still need to fetch more eggs
-
- // Use fly to reset the location because now we don't know where the player character is.
+ continue;
+ }
+ case EggAutoPhase::HATCHING:{
+ env.console.log("Hatching egg.");
+ num_eggs_hatched = hatch_routine(env, context, stats, num_eggs_hatched);
+ // env.log("Hatched eggs " + std::to_string(num_eggs_hatched) + "/5");
+ env.console.overlay().add_log("Found egg " + std::to_string(num_eggs_retrieved) + "/" + std::to_string(total_eggs_to_fetch), COLOR_WHITE);
+ phase = EggAutoPhase::FLY_RESET;
+ continue;
+ }
+ case EggAutoPhase::FLY_RESET:{
+ env.console.log("Call flying taxi to reset position.");
const bool fly_from_overworld = true;
- call_flying_taxi(env, context, fly_from_overworld);
- restart_bike_loop = true;
- // We don't update i_bike_loop here because we haven't finished one full bike loop due to egg hatching
- } // end one bike loop
-
- if (bike_loop_count >= MAX_BIKE_LOOP_COUNT){
- env.log("Reached max number of bike loops " + std::to_string(MAX_BIKE_LOOP_COUNT));
- env.console.overlay().add_log("Error: max loops " + std::to_string(MAX_BIKE_LOOP_COUNT), COLOR_WHITE);
- env.log("Take a screenshot of party to debug.");
- // Now take a photo at the player's party for dumping debug info:
- // Enter Rotom Phone menu
- pbf_press_button(context, BUTTON_X, 160ms, GameSettings::instance().OVERWORLD_TO_MENU_DELAY0);
- // Select Pokemon App
- navigate_to_menu_app(env.console, context, POKEMON_APP_INDEX);
- // From menu enter Pokemon App
- ssf_press_button(context, BUTTON_A, GameSettings::instance().MENU_TO_POKEMON_DELAY0, EGG_BUTTON_HOLD_DELAY);
- context.wait_for_all_requests();
-
- OperationFailedException::fire(
- ErrorReport::SEND_ERROR_REPORT,
- "Max number of loops reached. Not enough eggs in party?",
- env.console
- );
+ bool hatch_detected = call_flying_taxi(env, context, fly_from_overworld);
+ if (hatch_detected){
+ phase = EggAutoPhase::HATCHING;
+ }else{
+ phase = EggAutoPhase::BIKE_LOOP;
+ }
+ continue;
}
-
- context.wait_for_all_requests();
- if (m_num_eggs_retrieved < 5){
- // Update num_eggs_retrieved
- m_num_eggs_retrieved = talk_to_lady_to_fetch_egg(env, context, stats, m_num_eggs_retrieved);
- if (num_eggs_hatched == 5 && m_num_eggs_retrieved == 5){
- m_player_at_loop_start = true;
- break;
+ case EggAutoPhase::FETCH_EGG:{
+ env.console.log("Talk to lady to fetch egg.");
+ EggFetchResult fetch_result = talk_to_lady_to_fetch_egg(env, context, stats);
+ if (fetch_result.hatch_detected){
+ phase = EggAutoPhase::HATCHING;
+ }else{
+ if(fetch_result.found_egg){
+ num_eggs_retrieved++;
+ // env.log("Found egg " + std::to_string(num_eggs_retrieved) + "/" + std::to_string(total_eggs_to_fetch));
+ env.console.overlay().add_log("Found egg " + std::to_string(num_eggs_retrieved) + "/" + std::to_string(total_eggs_to_fetch), COLOR_WHITE);
+ stats.m_fetch_success++;
+ env.update_stats();
+ }
+
+ if (!fetch_result.spoke_to_lady){ // never spoke to lady
+ phase = EggAutoPhase::FLY_RESET;
+ }else{
+ // if spoke to lady, back to the bike loop regardless of the success of the egg fetch
+ num_loops_since_last_fetch_attempt = 0;
+ phase = EggAutoPhase::BIKE_LOOP;
+ }
+
}
+ continue;
+ }
}
}
@@ -425,7 +442,8 @@ bool EggAutonomous::run_batch(
// While checking hatched pokemon, we find that We need to stop the program:
return true;
}
- m_num_eggs_retrieved = 0;
+ num_eggs_in_column_0_at_batch_start = 0;
+ m_num_eggs_in_party_at_batch_start = 5;
// after process_hatched_pokemon(), the player location is at the start of bike loop
m_player_at_loop_start = true;
@@ -444,43 +462,161 @@ bool EggAutonomous::run_batch(
}
if (save){
- save_game(env, context);
+ save_game(env.console, context);
m_num_eggs_in_storage_when_game_saved = 0;
+ m_num_eggs_in_party_when_game_saved = 5;
}
context.wait_for_all_requests();
return false;
}
-void EggAutonomous::save_game(SingleSwitchProgramEnvironment& env, ProControllerContext& context){
- context.wait_for_all_requests();
- env.log("Save game.");
- env.console.overlay().add_log("Save game", COLOR_WHITE);
- pbf_press_button(context, BUTTON_X, 80ms, GameSettings::instance().OVERWORLD_TO_MENU_DELAY0);
- pbf_press_button(context, BUTTON_R, 80ms, 2000ms);
- pbf_mash_button(context, BUTTON_A, 500ms);
- mash_B_until_y_comm_icon(env, context, "Cannot detect end of saving game.");
+bool EggAutonomous::run_bike_loop(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context
+){
+ YCommIconWatcher no_overworld(COLOR_RED, false);
+ int ret = run_until(
+ env.console, context,
+ [](ProControllerContext& context){
+ travel_to_spin_location2(context);
+ travel_back_to_lady2(context);
+ },
+ {{no_overworld}}
+ );
+
+ if (ret == 0){ // no overworld detected. Check if we find egg hatching.
+ BlackDialogBoxWatcher2 egg_hatching_detector;
+ int ret2 = wait_until(
+ env.console, context,
+ std::chrono::seconds(10),
+ {
+ egg_hatching_detector,
+ }
+ );
+ if (ret2 == 0){
+ env.console.log("Hatching detected during bike loop.");
+ return true;
+ }else{
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "run_bike_loop: No recognized state after 10 seconds.",
+ env.console
+ );
+ }
+ }
+
+ return false;
}
-void EggAutonomous::call_flying_taxi(
+void EggAutonomous::exceed_bike_loop_limit(
SingleSwitchProgramEnvironment& env,
ProControllerContext& context,
- bool fly_from_overworld
+ size_t max_bike_loop_count
){
+ env.console.log("Reached max number of bike loops " + std::to_string(max_bike_loop_count));
+ env.console.overlay().add_log("Error: max loops " + std::to_string(max_bike_loop_count), COLOR_WHITE);
+ env.console.log("Take a screenshot of party to debug.");
+ // Now take a photo at the player's party for dumping debug info:
+ // Enter Rotom Phone menu
+ menus_to_mainmenu(env.console, context);
+ // Select Pokemon App
+ navigate_to_menu_app(env.console, context, POKEMON_APP_INDEX);
+ // From menu enter Pokemon App
+ ssf_press_button(context, BUTTON_A, GameSettings::instance().MENU_TO_POKEMON_DELAY0, EGG_BUTTON_HOLD_DELAY);
context.wait_for_all_requests();
- env.console.overlay().add_log("Call Flying Taxi", COLOR_WHITE);
- if (fly_from_overworld){
- // Open menu
- env.log("Fly from overworld to reset position");
- ssf_press_button(context, BUTTON_X, GameSettings::instance().OVERWORLD_TO_MENU_DELAY0, 160ms);
- }else{
- env.log("Fly from menu to reset position");
+
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "Max number of loops reached. Not enough eggs in party?",
+ env.console
+ );
+
+}
+
+size_t EggAutonomous::hatch_routine(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context,
+ EggAutonomous_Descriptor::Stats& stats,
+ size_t num_eggs_hatched
+){
+ // confirm we are starting with the hatching screen
+ BlackDialogBoxWatcher2 egg_hatching_detector;
+ int ret0 = wait_until(
+ env.console, context,
+ std::chrono::seconds(5),
+ {
+ egg_hatching_detector,
+ }
+ );
+ if (ret0 < 0){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "hatch_routine: We expected to see a hatching egg, but no hatching detected.",
+ env.console
+ );
}
- navigate_to_menu_app(env.console, context, TOWN_MAP_APP_INDEX);
+ int ret = -1;
+ do{
+ ++num_eggs_hatched;
+ stats.m_hatched++;
+ env.update_stats();
+ wait_for_egg_hatched(env, context, stats, num_eggs_hatched);
- fly_home(context, false);
- mash_B_until_y_comm_icon(env, context, "Cannot detect end of flying taxi animation.");
+ // Now we see if we can hatch one more egg.
+ ret = run_until(
+ env.console, context,
+ [](ProControllerContext& context){
+ // Try move a little to hatch more:
+ // We move toward lower-left so that it wont hit the lady or enter the Nursory.
+ // Add 1 second of settle time to stop moving.
+ // We need to not be moving before trying to fly or an egg will hatch during
+ // that sequence when we cannot handle it.
+ pbf_move_left_joystick(context, {-1, -1}, 800ms, 1000ms);
+ },
+ {{egg_hatching_detector}}
+ );
+ }while (ret == 0);
+
+ return num_eggs_hatched;
+}
+
+
+bool EggAutonomous::call_flying_taxi(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context,
+ bool fly_from_overworld
+){
+
+ BlackDialogBoxWatcher2 egg_hatching_detector;
+ int ret = run_until(
+ env.console, context,
+ [&](ProControllerContext& context){
+ context.wait_for_all_requests();
+ env.console.overlay().add_log("Call Flying Taxi", COLOR_WHITE);
+ if (fly_from_overworld){
+ // Open menu
+ env.log("Fly from overworld to reset position");
+ menus_to_mainmenu(env.console, context);
+ }else{
+ env.log("Fly from menu to reset position");
+ }
+
+ navigate_to_menu_app(env.console, context, TOWN_MAP_APP_INDEX);
+ },
+ {{egg_hatching_detector}}
+ );
+
+ bool hatch_detected = ret == 0;
+ if (hatch_detected){
+ env.console.log("Hatching detected while trying to call flying taxi.");
+ }else {
+ fly_home(context, false);
+ mash_B_until_y_comm_icon(env.console, context, "Cannot detect end of flying taxi animation.");
+ }
+
+ return hatch_detected;
}
void EggAutonomous::wait_for_egg_hatched(
@@ -508,80 +644,111 @@ void EggAutonomous::wait_for_egg_hatched(
}
}
-size_t EggAutonomous::talk_to_lady_to_fetch_egg(
+EggFetchResult EggAutonomous::talk_to_lady_to_fetch_egg(
SingleSwitchProgramEnvironment& env,
ProControllerContext& context,
- EggAutonomous_Descriptor::Stats& stats,
- size_t num_eggs_retrieved
+ EggAutonomous_Descriptor::Stats& stats
){
env.log("Fetching egg");
stats.m_fetch_attempts++;
env.update_stats();
- // collect_egg(context);
+
+ YCommIconWatcher overworld;
RetrieveEggArrowFinder egg_arrow_detector(env.console);
CheckNurseryArrowFinder no_egg_arrow_detector(env.console);
+ WhiteDialogBoxWatcher white_dialog;
+ BlackDialogBoxWatcher2 black_dialog;
- int ret = run_until(
- env.console, context,
- [](ProControllerContext& context){
- for (size_t i_hatched = 0; i_hatched < 2; i_hatched++){
- pbf_press_button(context, BUTTON_A, 160ms, 1200ms);
- }
- pbf_wait(context, 1600ms);
- },
- {
- egg_arrow_detector,
- no_egg_arrow_detector,
- }
- );
-
- const bool y_comm_visible_at_end_of_dialog = true;
- YCommIconWatcher dialog_over_detector(COLOR_RED, y_comm_visible_at_end_of_dialog);
- switch (ret){
- case 0:
- ++num_eggs_retrieved;
- env.log("Found egg");
- env.console.overlay().add_log("Found egg " + std::to_string(num_eggs_retrieved) + "/5", COLOR_WHITE);
- stats.m_fetch_success++;
- env.update_stats();
- // Press A to get the egg
- ssf_press_button(context, BUTTON_A, 320ms, 160ms);
+ size_t seen_overworld = 0;
+ bool egg_status_known = false;
+ bool found_egg = false;
- ret = run_until(
+ WallClock deadline = current_time() + std::chrono::minutes(2);
+ while(!egg_status_known && current_time() < deadline){
+ context.wait_for_all_requests();
+ int ret = wait_until(
env.console, context,
- [](ProControllerContext& context){
- pbf_mash_button(context, BUTTON_B, 30s);
- },
- {{dialog_over_detector}}
+ std::chrono::seconds(30),
+ {
+ overworld,
+ egg_arrow_detector,
+ black_dialog,
+ white_dialog,
+ no_egg_arrow_detector,
+ }
);
- break;
+ switch (ret){
+ case 0: // overworld
+ env.log("Detected Overworld...", COLOR_BLUE);
+ seen_overworld++;
+ if (seen_overworld > 10){
+ // No NPC found
+ env.log("Stuck in Overworld. Daycare lady not found.", COLOR_BLUE);
+ return EggFetchResult{
+ .found_egg = false,
+ .hatch_detected = false,
+ .spoke_to_lady = false
+ };
+ }
+ pbf_press_button(context, BUTTON_A, 160ms, 100ms);
+ continue;
+ case 1: // egg_arrow_detector
+ env.log("Found egg");
+ found_egg = true;
+ // Press A to get the egg
+ ssf_press_button(context, BUTTON_A, 320ms, 160ms);
+ continue;
+ case 2: // black_dialog
+ if (found_egg){
+ env.log("Received egg");
+ egg_status_known = true; // break the loop. then mash B
+
+ }else{ // this black dialog might actually be a hatching egg.
+ env.log("Hatching detected while trying to talk to lady to fetch egg.");
+ return EggFetchResult{
+ .found_egg = false,
+ .hatch_detected = true,
+ .spoke_to_lady = true
+ };
+ }
+ continue;
+ case 3: // white_dialog
+ env.log("Detected dialog box...", COLOR_BLUE);
+ pbf_press_button(context, BUTTON_A, 160ms, 40ms);
+ continue;
+ case 4: // no_egg_arrow_detector
+ env.log("No egg");
+ env.console.overlay().add_log("No egg", COLOR_WHITE);
+ found_egg = false;
+ egg_status_known = true; // break the loop. then mash B
+ continue;
+ default:
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "talk_to_lady_to_fetch_egg(): No recognized state after 30 seconds.",
+ env.console
+ );
+ }
+ }
- case 1:
- env.log("No egg");
- env.console.overlay().add_log("No egg", COLOR_WHITE);
- run_until(
- env.console, context,
- [](ProControllerContext& context){
- pbf_mash_button(context, BUTTON_B, 5s);
- },
- {{dialog_over_detector}}
+ if (!egg_status_known){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "talk_to_lady_to_fetch_egg(): Caught in loop. Unable to speak to lady after 2 minutes.",
+ env.console
);
- return num_eggs_retrieved;
-// break;
+ }
- default:
- env.log("Daycare lady not found.");
- env.console.overlay().add_log("No daycare lady", COLOR_WHITE);
- return num_eggs_retrieved;
-// OperationFailedException::fire(
-// ErrorReport::SEND_ERROR_REPORT,
-// "Cannot detect dialog selection arrow when talking to Nursery lady.",
-// env.console
-// );
- }
-
- // If dialog over is not detected:
- if (ret < 0){
+ // we know if the egg was found or not
+ // mash b to return to overworld
+ int ret2 = run_until(
+ env.console, context,
+ [](ProControllerContext& context){
+ pbf_mash_button(context, BUTTON_B, 30s);
+ },
+ {{overworld}}
+ );
+ if (ret2 < 0){ // If dialog over is not detected:
OperationFailedException::fire(
ErrorReport::SEND_ERROR_REPORT,
"Cannot detect end of Nursery lady dialog. No Y-Comm mark found.",
@@ -589,7 +756,11 @@ size_t EggAutonomous::talk_to_lady_to_fetch_egg(
);
}
- return num_eggs_retrieved;
+ return EggFetchResult{
+ .found_egg = found_egg,
+ .hatch_detected = false,
+ .spoke_to_lady = true
+ };
}
// After all five eggs hatched and another five eggs deposit into the first column of the box,
@@ -610,10 +781,37 @@ bool EggAutonomous::process_hatched_pokemon(
menus_to_boxsystem(env.console, context);
+ // Before processing the box:
+ // Confirm that Box Column 0 has 5 eggs, and the party has no eggs
+ context.wait_for_all_requests();
+ context.wait_for(500ms);
+ auto screen0 = env.console.video().snapshot();
+ size_t num_eggs_in_column_0_before = count_eggs_in_first_box_column(env.console, screen0);
+ size_t num_empty_slots_in_column_0_before = count_empty_slots_in_first_box_column(env.console, screen0);
+ size_t num_eggs_in_party_before = count_eggs_in_party(env.console, screen0);
+ size_t num_empty_slots_in_party_before = count_empty_slots_in_party(env.console, screen0);
+ if (num_eggs_in_column_0_before != 5 || num_empty_slots_in_column_0_before != 0){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "process_hatched_pokemon: Before processing, we expected 5 eggs in the first box column.",
+ env.console
+ );
+ }
+ if (num_eggs_in_party_before != 0 || num_empty_slots_in_party_before != 0){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "process_hatched_pokemon: Before processing, we expected a party without eggs (and no empty slots), since they should all be hatched.",
+ env.console
+ );
+ }
+
+
const Milliseconds BOX_CHANGE_DELAY = GameSettings::instance().BOX_CHANGE_DELAY0;
const Milliseconds BOX_PICKUP_DROP_DELAY = GameSettings::instance().BOX_PICKUP_DROP_DELAY0;
+ // select top Pokemon in party
box_scroll(context, DPAD_LEFT);
+ // select the first egg
box_scroll(context, DPAD_DOWN);
context.wait_for_all_requests();
@@ -825,6 +1023,32 @@ bool EggAutonomous::process_hatched_pokemon(
// Press A to finish dropping the egg column
ssf_press_button_ptv(context, BUTTON_A, BOX_PICKUP_DROP_DELAY, EGG_BUTTON_HOLD_DELAY);
+ // After processing the box:
+ // Confirm that Box Column 0 is empty, and the party is full of eggs
+ check_box(env.console, context);
+ context.wait_for_all_requests();
+ context.wait_for(500ms);
+ auto screen = env.console.video().snapshot();
+ size_t num_eggs_in_column_0_after = count_eggs_in_first_box_column(env.console, screen);
+ size_t num_empty_slots_in_column_0_after = count_empty_slots_in_first_box_column(env.console, screen);
+ size_t num_eggs_in_party_after = count_eggs_in_party(env.console, screen);
+ size_t num_empty_slots_in_party_after = count_empty_slots_in_party(env.console, screen);
+ if (num_eggs_in_column_0_after != 0 || num_empty_slots_in_column_0_after != 5){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "process_hatched_pokemon: After processing, we expected an empty first box column.",
+ env.console
+ );
+ }
+
+ if (num_eggs_in_party_after != 5 || num_empty_slots_in_party_after != 0){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "process_hatched_pokemon: After processing, we expected a party full of 5 eggs.",
+ env.console
+ );
+ }
+
// Back out to menu.
menus_to_mainmenu(env.console, context);
@@ -854,30 +1078,105 @@ bool EggAutonomous::process_hatched_pokemon(
return false;
}
-void EggAutonomous::mash_B_until_y_comm_icon(
- SingleSwitchProgramEnvironment& env,
- ProControllerContext& context,
- const std::string& error_msg
-){
+
+
+void EggAutonomous::check_box(VideoStream& stream, ProControllerContext& context){
context.wait_for_all_requests();
- const bool y_comm_visible = true;
- YCommIconWatcher y_comm_detector(COLOR_RED, y_comm_visible);
- int ret = run_until(
- env.console, context,
- [](ProControllerContext& context){
- pbf_mash_button(context, BUTTON_B, 10s);
- },
- {y_comm_detector}
- );
- if (ret != 0){
+ context.wait_for(500ms);
+
+ auto screen = stream.video().snapshot();
+ check_box_filled(stream, screen);
+ check_non_egg_lead(stream, screen);
+}
+
+
+void EggAutonomous::check_box_filled(VideoStream& stream, const ImageViewRGB32& screen){
+ for (uint8_t row = 0; row < 5; row++){
+ for (uint8_t column = 1; column < 6; column++){
+ BoxEmptySlotDetector slot(SlotLocation::BOX, row, column);
+ bool is_empty = slot.detect(screen);
+ // stream.log("row " + std::to_string(row) + " col " + std::to_string(column) + (is_empty ? " is_empty" : " not empty"));
+ if (is_empty){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "check_box_filled: Box is not filled.",
+ stream
+ );
+ }
+
+ }
+ }
+}
+
+void EggAutonomous::check_non_egg_lead(VideoStream& stream, const ImageViewRGB32& screen){
+ BoxEmptySlotDetector slot(SlotLocation::PARTY, 0, 0);
+ bool is_empty = slot.detect(screen);
+ if (is_empty){
OperationFailedException::fire(
ErrorReport::SEND_ERROR_REPORT,
- error_msg + " No Y-Comm mark found.",
- env.console
+ "check_non_egg_lead: Detected an empty lead slot in party. This shouldn't be possible.",
+ stream
+ );
+ }
+
+ BoxEggDetector egg(SlotLocation::PARTY, 0);
+ bool is_egg = egg.detect(screen);
+ if (is_egg){
+ OperationFailedException::fire(
+ ErrorReport::SEND_ERROR_REPORT,
+ "check_non_egg_lead: Detected an egg in lead slot of party.",
+ stream
);
}
}
+size_t EggAutonomous::count_eggs_in_party(VideoStream& stream, const ImageViewRGB32& screen){
+ size_t num_eggs = 0;
+ for (uint8_t row = 0; row < 6; row++){
+ BoxEggDetector egg(SlotLocation::PARTY, row);
+ bool is_egg = egg.detect(screen);
+ if (is_egg) { num_eggs++; }
+ }
+
+ return num_eggs;
+}
+
+size_t EggAutonomous::count_empty_slots_in_party(VideoStream& stream, const ImageViewRGB32& screen){
+ size_t num_empty = 0;
+ for (uint8_t row = 0; row < 6; row++){
+ BoxEmptySlotDetector slot(SlotLocation::PARTY, row, 0);
+ bool is_empty = slot.detect(screen);
+ if (is_empty) { num_empty++; }
+ }
+
+ return num_empty;
+}
+
+size_t EggAutonomous::count_eggs_in_first_box_column(VideoStream& stream, const ImageViewRGB32& screen){
+
+ size_t num_eggs = 0;
+ for (uint8_t row = 0; row < 5; row++){
+ BoxEggDetector egg(SlotLocation::BOX, row);
+ bool is_egg = egg.detect(screen);
+ if (is_egg) { num_eggs++; }
+ }
+
+ return num_eggs;
+}
+
+size_t EggAutonomous::count_empty_slots_in_first_box_column(VideoStream& stream, const ImageViewRGB32& screen){
+
+ size_t num_empty = 0;
+ for (uint8_t row = 0; row < 5; row++){
+ BoxEmptySlotDetector slot(SlotLocation::BOX, row, 0);
+ bool is_empty = slot.detect(screen);
+ if (is_empty) { num_empty++; }
+ }
+
+ return num_empty;
+}
+
+
diff --git a/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.h b/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.h
index a4790bafac..3938963cce 100644
--- a/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.h
+++ b/SerialPrograms/Source/PokemonSwSh/Programs/EggPrograms/PokemonSwSh_EggAutonomous.h
@@ -7,6 +7,7 @@
#ifndef PokemonAutomation_PokemonSwSh_EggAutonomous_H
#define PokemonAutomation_PokemonSwSh_EggAutonomous_H
+#include
#include "Common/Cpp/Options/BooleanCheckBoxOption.h"
#include "Common/Cpp/Options/SimpleIntegerOption.h"
#include "CommonFramework/Notifications/EventNotificationsTable.h"
@@ -22,6 +23,21 @@ namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonSwSh{
+enum class EggAutoPhase{
+ BIKE_LOOP,
+ HATCHING,
+ FLY_RESET,
+ FETCH_EGG,
+
+};
+
+struct EggFetchResult{
+ bool found_egg;
+ bool hatch_detected;
+ bool spoke_to_lady;
+};
+
+
class EggAutonomous_Descriptor : public SingleSwitchProgramDescriptor{
public:
@@ -50,12 +66,32 @@ class EggAutonomous : public SingleSwitchProgramInstance{
EggAutonomous_Descriptor::Stats& stats
);
- void save_game(SingleSwitchProgramEnvironment& env, ProControllerContext& context);
+
+ // return true if egg hatching detected during the bike loop
+ bool run_bike_loop(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context
+ );
+
+ void exceed_bike_loop_limit(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context,
+ size_t max_bike_loop_count
+ );
+
+ // Return updated `num_eggs_hatched` to reflect change in hatched eggs.
+ size_t hatch_routine(
+ SingleSwitchProgramEnvironment& env,
+ ProControllerContext& context,
+ EggAutonomous_Descriptor::Stats& stats,
+ size_t num_eggs_hatched
+ );
// Call flying taxi to reset player character position to Nursery front door.
// fly_from_overworld: if true, the game is in the overworld while calling this function. If false, the game is in the menu.
// Note: the cursor in the menu must already be at Town Map.
- void call_flying_taxi(
+ // return true if egg hatching detected while trying to open Rotom phone menu
+ bool call_flying_taxi(
SingleSwitchProgramEnvironment& env,
ProControllerContext& context,
bool fly_from_overworld
@@ -71,12 +107,14 @@ class EggAutonomous : public SingleSwitchProgramInstance{
);
// Call this function when standing in front of the lady to fetch one egg.
- // Return updated `num_eggs_retrieved` to reflect change in fetched eggs.
- size_t talk_to_lady_to_fetch_egg(
+ // return EggFetchResult, which is a struct of the following:
+ // - found_egg: boolean that is true if an egg was retrieved from the lady
+ // - hatch_detected: boolean that is true if hatch was detected.
+ // - spoke_to_lady: boolean that is true if we spoke to the lady
+ EggFetchResult talk_to_lady_to_fetch_egg(
SingleSwitchProgramEnvironment& env,
ProControllerContext& context,
- EggAutonomous_Descriptor::Stats& stats,
- size_t num_eggs_retrieved
+ EggAutonomous_Descriptor::Stats& stats
);
// After all five eggs hatched and another five eggs deposit into the first column of the box,
@@ -93,13 +131,26 @@ class EggAutonomous : public SingleSwitchProgramInstance{
bool need_taxi
);
- // Used to wait until Y-Comm icon shows up.
- // Throw error if it does not find it after 10 sec.
- void mash_B_until_y_comm_icon(
- SingleSwitchProgramEnvironment& env,
- ProControllerContext& context,
- const std::string& error_msg
- );
+ // Starting within the box, confirm that the lead is not an egg, and that the box is full except for the first column
+ // this is run before each batch
+ void check_box(VideoStream& stream, ProControllerContext& context);
+
+ // ensure that all rows/columns are filled except the first column
+ // ASSUMES: the cursor should NOT be on the box, with the exception that it can be at the top row.
+ // This is because it pop-up text otherwise covers up the box slot, preventing proper detection
+ void check_box_filled(VideoStream& stream, const ImageViewRGB32& screen);
+
+ // ensure that the lead pokemon in the party is not an egg
+ void check_non_egg_lead(VideoStream& stream, const ImageViewRGB32& screen);
+
+ size_t count_eggs_in_party(VideoStream& stream, const ImageViewRGB32& screen);
+ size_t count_empty_slots_in_party(VideoStream& stream, const ImageViewRGB32& screen);
+
+ size_t count_eggs_in_first_box_column(VideoStream& stream, const ImageViewRGB32& screen);
+ size_t count_empty_slots_in_first_box_column(VideoStream& stream, const ImageViewRGB32& screen);
+
+
+
StartInGripOrGameOption START_LOCATION;
TouchDateIntervalOption TOUCH_DATE_INTERVAL;
@@ -111,7 +162,6 @@ class EggAutonomous : public SingleSwitchProgramInstance{
SimpleIntegerOption MAX_KEEPERS;
SimpleIntegerOption LOOPS_PER_FETCH;
- IntegerEnumDropdownOption NUM_EGGS_IN_COLUMN;
enum class AutoSave{
NoAutoSave,
@@ -137,13 +187,22 @@ class EggAutonomous : public SingleSwitchProgramInstance{
// How many eggs have been placed behind a game save.
// This is used so that if we recover from an error, we know how many eggs are in storage.
size_t m_num_eggs_in_storage_when_game_saved = 0;
- // How many eggs are already deposited to storage so far.
- size_t m_num_eggs_retrieved = 0;
+
+ // This is used so that if we recover from an error, we know how many eggs are in the party
+ size_t m_num_eggs_in_party_when_game_saved = 0;
+
+ // number of eggs in box column 0 at the start of run_batch()
+ size_t num_eggs_in_column_0_at_batch_start = 0;
+
+ // number of eggs in party at the start of run_batch()
+ size_t m_num_eggs_in_party_at_batch_start = 0;
// Is player's location at the bike loop start
bool m_player_at_loop_start = false;
};
+
+
}
}