Skip to content

Commit 0cddc48

Browse files
committed
fix: improve stage detection, battle timeout, and PyInstaller packaging
- Add PyInstaller --onedir support with _internal path handling - Fix battle timeout detection with proper 2-second wait intervals - Enhance AFK stage screen detection with multiple search regions - Add p_challenge button support for new UI - Update sun-and-stars detection region for better accuracy - Improve error logging and screenshot capture on failures
1 parent 2f114e2 commit 0cddc48

6 files changed

Lines changed: 202 additions & 89 deletions

File tree

automation/afkj_automation.py

Lines changed: 167 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,50 +1129,76 @@ def formation_handler(
11291129
formation_number (int): The formation number to load. Defaults to 1.
11301130
already_open (bool): Whether the formations menu is already open. Defaults to False.
11311131
"""
1132-
if self.metadata.load_formations is False:
1133-
self.logger.info("Formation loading disabled")
1134-
return
1132+
try:
1133+
if self.metadata.load_formations is False:
1134+
self.logger.info("Formation loading disabled")
1135+
return
1136+
1137+
if self.metadata.formation > 7:
1138+
self.logger.info(
1139+
"Formation selected higher than 7, starting from 1 again.."
1140+
)
1141+
self.metadata.formation = 1
11351142

1136-
if self.metadata.formation > 7:
11371143
self.logger.info(
1138-
"Formation selected higher than 7, starting from 1 again.."
1144+
"Loading formation #" + str(math.trunc(self.metadata.formation))
11391145
)
1140-
self.metadata.formation = 1
1141-
1142-
self.logger.info(
1143-
"Loading formation #" + str(math.trunc(self.metadata.formation))
1144-
)
1145-
counter = 1
1146-
unowned_counter = 0
1147-
self.wait()
1148-
if already_open is False: # Sometimes we're already in the formations menu
1149-
self.click("buttons/records", seconds=3)
1150-
while counter != formation_number:
1151-
self.click_xy(1000, 1025)
1152-
counter += 1
1153-
1154-
self.click("buttons/copy", seconds=2)
1155-
# Handle 'Hero not owned' popup
1156-
if self.is_visible("labels/not_owned"):
1157-
while self.is_visible(
1158-
"labels/not_owned"
1159-
): # Try next formation and check again
1160-
self.logger.info("Hero/Artifact not owned, trying next formation..")
1161-
self.click_xy(360, 1250)
1146+
counter = 1
1147+
unowned_counter = 0
1148+
self.wait()
1149+
1150+
if already_open is False: # Sometimes we're already in the formations menu
1151+
# Try multiple regions to find records button
1152+
records_regions = [
1153+
self.metadata.regions["bottom_buttons"],
1154+
(0, 1600, 1080, 320),
1155+
]
1156+
records_clicked = False
1157+
for region in records_regions:
1158+
if self.is_visible("buttons/records", region=region, seconds=0, retry=5, confidence=0.5, click=True):
1159+
records_clicked = True
1160+
break
1161+
if not records_clicked:
1162+
self.click("buttons/records", seconds=3)
1163+
self.wait(3)
1164+
1165+
while counter != formation_number:
11621166
self.click_xy(1000, 1025)
1163-
self.click("buttons/copy")
1164-
self.metadata.formation += 1
1165-
unowned_counter += 1
1166-
if unowned_counter > 7:
1167-
self.logger.info("All formations contained an unowned hero!")
1168-
self.click_location(
1169-
"neutral"
1170-
) # Close windows back to battle screen
1171-
self.click_location(
1172-
"neutral"
1173-
) # Close windows back to battle screen
1174-
break
1175-
self.click("buttons/confirm", suppress=True, seconds=0)
1167+
counter += 1
1168+
self.wait(0.5) # Small wait between clicks
1169+
1170+
if not self.is_visible("buttons/copy", seconds=0, retry=5, click=True):
1171+
self.click("buttons/copy", seconds=2)
1172+
1173+
self.wait(1) # Wait for any popups
1174+
1175+
# Handle 'Hero not owned' popup
1176+
if self.is_visible("labels/not_owned", seconds=0, retry=1):
1177+
while self.is_visible(
1178+
"labels/not_owned",
1179+
seconds=0,
1180+
retry=1,
1181+
): # Try next formation and check again
1182+
self.logger.info("Hero/Artifact not owned, trying next formation..")
1183+
self.click_xy(360, 1250)
1184+
self.click_xy(1000, 1025)
1185+
self.click("buttons/copy")
1186+
self.metadata.formation += 1
1187+
unowned_counter += 1
1188+
if unowned_counter > 7:
1189+
self.logger.info("All formations contained an unowned hero!")
1190+
self.click_location("neutral")
1191+
self.click_location("neutral")
1192+
break
1193+
1194+
# Note: In updated UI, there's no confirm button after copy - formation is loaded directly
1195+
self.wait(1)
1196+
self.click_location("neutral")
1197+
self.wait(0.5)
1198+
except Exception as e:
1199+
self.logger.error(f"Error in formation_handler: {e}", exc_info=True)
1200+
self.save_screenshot("formation_handler_error")
1201+
raise
11761202

11771203
def blind_push(
11781204
self,
@@ -1510,12 +1536,21 @@ def blind_push(
15101536
# For pushing afk stages
15111537
if mode == "afkstages":
15121538
timeout = 0
1513-
if self.is_visible(
1514-
"buttons/records",
1515-
region=self.metadata.regions["bottom_buttons"],
1516-
seconds=0,
1517-
retry=20,
1518-
):
1539+
1540+
# Try multiple regions to detect AFK Stages screen
1541+
records_regions = [
1542+
(self.metadata.regions["bottom_buttons"], 20),
1543+
((0, 1600, 1080, 320), 10),
1544+
((600, 600, 480, 400), 10),
1545+
((0, 0, 1080, 1920), 5),
1546+
]
1547+
records_found = False
1548+
for region, retries in records_regions:
1549+
if self.is_visible("buttons/records", region=region, seconds=0, retry=retries, confidence=0.5):
1550+
records_found = True
1551+
break
1552+
1553+
if records_found:
15191554

15201555
# Change formation if we we beat the 2nd round or have defeat >10 times in a row
15211556
if (
@@ -1547,8 +1582,6 @@ def blind_push(
15471582
elif load_formation is True:
15481583
self.formation_handler(self.metadata.formation)
15491584

1550-
# Season 3 single stage code
1551-
15521585
# Start Battle
15531586
self.click(
15541587
"buttons/battle",
@@ -1562,39 +1595,67 @@ def blind_push(
15621595
) # Long wait to stop false positives from the back button on the battle selection screen
15631596

15641597
# Wait til we see the back button in the post battle screen before running next checks
1598+
timeout = 0
15651599
while not self.is_visible(
15661600
"buttons/back",
15671601
region=self.metadata.regions["bottom_buttons"],
1568-
seconds=2,
1602+
seconds=0,
1603+
retry=1,
15691604
):
1605+
self.wait(2) # Wait 2 seconds between each check
15701606
timeout += 1
1571-
if (
1572-
timeout > 30
1573-
): # If nothing at 30 seconds start clicking in case battery saver mode is active
1574-
self.click_location("neutral")
1575-
if (
1576-
timeout > 60
1577-
): # Still nothing at 60 seconds? Quit as somethings gone wrong
1578-
self.logger.info("Battle timeout error!")
1607+
if timeout > 45: # Battle is 90 seconds, if still not done, something went wrong
1608+
self.logger.error("Battle timeout error! Could not detect battle completion after 90 seconds.")
1609+
self.save_screenshot("battle_timeout_error")
15791610
break
15801611

15811612
# Post battle screen detection
15821613
result = ""
1583-
while result == "":
1584-
# Loop the different scenarios until we get an image match ('retry' is defeat, 'battle' is normal stage victory, 'talent_trials' is talent stage victory)
1585-
images = [
1586-
"buttons/retry",
1587-
"buttons/battle",
1588-
"buttons/talent_trials",
1589-
]
1590-
result = self.is_visible_array(
1591-
images,
1592-
confidence=0.9,
1593-
seconds=0,
1594-
retry=1,
1595-
click=True,
1596-
region=self.metadata.regions["bottom_buttons"],
1597-
)
1614+
result_timeout = 0
1615+
max_result_timeout = 20 # Maximum 20 attempts (40 seconds)
1616+
1617+
images = ["buttons/retry", "buttons/battle", "buttons/talent_trials", "buttons/p_challenge"]
1618+
search_regions = [
1619+
(0, 1500, 1080, 420),
1620+
self.metadata.regions["bottom_buttons"],
1621+
(0, 0, 1080, 1920),
1622+
]
1623+
1624+
while result == "" and result_timeout < max_result_timeout:
1625+
for region in search_regions:
1626+
result = self.is_visible_array(
1627+
images,
1628+
seconds=0,
1629+
retry=1,
1630+
click=True,
1631+
region=region,
1632+
)
1633+
1634+
if result != "" and result != "not_found":
1635+
break
1636+
1637+
if result == "not_found" or result == "":
1638+
result = "" # Reset to continue loop
1639+
result_timeout += 1
1640+
self.wait(2)
1641+
else:
1642+
break
1643+
1644+
if result == "" or result == "not_found":
1645+
self.logger.error("Could not detect battle result! Trying individual button detection...")
1646+
self.save_screenshot("battle_result_detection_error")
1647+
1648+
# Try individual button detection
1649+
buttons_to_check = ["buttons/retry", "buttons/battle", "buttons/talent_trials", "buttons/p_challenge"]
1650+
for btn in buttons_to_check:
1651+
if self.is_visible(btn, seconds=0, retry=1, region=(0, 0, 1080, 1920)):
1652+
result = btn
1653+
break
1654+
1655+
if result == "" or result == "not_found":
1656+
self.logger.error("Could not detect any battle result button. Exiting...")
1657+
self.logger.error("Please check the screenshot: battle_result_detection_error.png")
1658+
return
15981659

15991660
# Retry button indicates defeat, we run the defeat logic
16001661
if result == "buttons/retry":
@@ -1604,17 +1665,40 @@ def blind_push(
16041665
)
16051666
self.blind_push("afkstages", load_formation=False)
16061667

1607-
# The other two mean we have a victory
1608-
elif result == "buttons/battle" or result == "buttons/talent_trials":
1668+
# Victory buttons: battle, talent_trials, or p_challenge
1669+
elif result == "buttons/battle" or result == "buttons/talent_trials" or result == "buttons/p_challenge":
16091670
self.metadata.stage_defeats = 0 # Reset defeats
16101671
self.metadata.formation = 1 # Reset formation
16111672
self.logger.info("Victory! Stage passed\n")
16121673
self.metadata.first_stage_won = False
16131674
self.blind_push("afkstages", load_formation=True)
1614-
else:
1615-
self.logger.info("Something went wrong opening AFK Stages!")
1675+
if not records_found:
1676+
self.logger.error(
1677+
"Failed to detect AFK Stages screen! "
1678+
"Expected 'buttons/records' button not found after multiple search attempts. "
1679+
"This usually means the stage selection screen did not open correctly."
1680+
)
1681+
self.logger.debug(
1682+
"Searched for 'buttons/records' in: "
1683+
f"1. bottom_buttons region: {self.metadata.regions['bottom_buttons']}, "
1684+
f"2. extended region: (0, 1500, 1080, 420), "
1685+
f"3. full screen: (0, 0, 1080, 1920)"
1686+
)
1687+
self.logger.info("Checking if we're on a different screen...")
1688+
1689+
# Try to detect what screen we're actually on
1690+
if self.is_visible("labels/sunandstars", region=self.metadata.regions["sunandstars"], seconds=0, retry=1):
1691+
self.logger.warning("Detected main screen instead of AFK Stages screen. Stage selection may have failed.")
1692+
elif self.is_visible("buttons/back", region=self.metadata.regions["bottom_buttons"], seconds=0, retry=1):
1693+
self.logger.warning("Detected back button but not records button. May be on wrong screen.")
1694+
16161695
self.save_screenshot("afk_stage_error")
1617-
self.recover()
1696+
self.logger.info("Attempting to recover to main screen...")
1697+
recovery_result = self.recover()
1698+
if recovery_result:
1699+
self.logger.info("Recovery successful, returned to main screen")
1700+
else:
1701+
self.logger.error("Recovery failed, could not return to main screen")
16181702

16191703
def open_afk_stages(self, afkstages: bool = True) -> None:
16201704
"""Opens the AFK or Talent Stages based on the provided flag.
@@ -1627,6 +1711,8 @@ def open_afk_stages(self, afkstages: bool = True) -> None:
16271711
afkstages (bool): If True, opens the standard AFK Stages. If False, opens the
16281712
Talent Stages.
16291713
"""
1714+
stage_type = "AFK Stages" if afkstages else "Talent Stages"
1715+
16301716
# Open afk stage screen without prompting loot if it's >1h uncollected
16311717
self.click_xy(450, 1825, seconds=3)
16321718
self.click(
@@ -1650,8 +1736,11 @@ def open_afk_stages(self, afkstages: bool = True) -> None:
16501736
+ str(self.config.getint("PUSHING", "defeat_limit"))
16511737
+ " defeats\n"
16521738
)
1653-
self.click_xy(370, 1600, seconds=2) # AFK Stage button
1739+
self.click_xy(370, 1600, seconds=2) # Talent Stage button
16541740
self.click("buttons/confirm", suppress=True)
1741+
1742+
# Give the screen time to load after clicking
1743+
self.wait(2)
16551744

16561745
def afk_stage_chain_proxy(self) -> None:
16571746
"""Starts an AFK Stage chain by attempting to start the stage and then

client/emulator_client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,16 @@ def __init__(self, config: ConfigParser, logger: Logger) -> None:
2626
self.device: Union[Any, None] = None
2727
self.config: ConfigParser = config
2828
self.adb: Client = Client(host="127.0.0.1", port=5037)
29-
self.cwd: str = os.path.dirname(os.path.abspath(sys.argv[0]))
29+
30+
# Get the correct base path for both development and PyInstaller frozen executable
31+
if getattr(sys, 'frozen', False):
32+
# Running as compiled executable (PyInstaller)
33+
# In --onedir mode, resources are in _internal subdirectory
34+
self.cwd: str = os.path.join(os.path.dirname(sys.executable), '_internal')
35+
else:
36+
# Running in development mode
37+
self.cwd: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38+
3039
self.logger: Logger = logger
3140

3241
self.wait = partial(

img/buttons/p_challenge.png

38.1 KB
Loading

img/buttons/records.png

5.8 KB
Loading

interaction/emulator_interaction.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -555,18 +555,25 @@ def recover(self, count: int = 3) -> bool:
555555
"""
556556
timer: int = 0
557557

558-
if self.is_visible("labels/sunandstars", region=(770, 40, 100, 100)):
558+
if self.is_visible("labels/sunandstars", region=(950, 220, 150, 120)):
559559
return True
560560

561+
self.logger.info(f"Attempting to recover to main screen (max {count} attempts)")
561562
while timer < count:
562563
self.click("buttons/back", suppress=True)
563564
self.click("buttons/back2", suppress=True)
564565
self.click_location("neutral")
565566
timer += 1
566-
if self.is_visible("labels/sunandstars", region=(770, 40, 100, 100)):
567+
if self.is_visible("labels/sunandstars", region=(950, 220, 150, 120)):
568+
self.logger.info(f"Recovery successful after {timer} attempt(s)")
567569
return True
568570

569571
timestamp: str = datetime.now().strftime("%d-%m-%y_%H-%M-%S")
572+
self.logger.error(
573+
f"Recovery failed after {count} attempts. "
574+
f"Could not detect main screen (looking for 'labels/sunandstars'). "
575+
f"Saving screenshot: recovery_timeout_{timestamp}"
576+
)
570577
self.save_screenshot("recovery_timeout_" + timestamp)
571578
return False
572579

@@ -591,17 +598,25 @@ def safe_open_and_close(self, name: str, state: str) -> Union[None, Literal[True
591598
"""
592599
# We call this at the start and end of every activity to make sure we are back at the main map screen, if not we are lost and exit
593600
if state == "open":
594-
self.logger.debug("opening task " + name)
595-
if self.recover() is True:
596-
self.logger.debug(name + " opened successfully!")
601+
self.logger.debug("Opening task: " + name)
602+
recovery_result = self.recover()
603+
if recovery_result is True:
604+
self.logger.debug(name + " opened successfully - confirmed on main screen")
597605
else:
598-
self.logger.info("Issue opening " + name)
606+
self.logger.warning(
607+
f"Issue opening {name}: Could not confirm we are on main screen. "
608+
"Recovery process failed. Task may not start correctly."
609+
)
599610

600611
if state == "close":
601-
if self.recover() is True:
602-
self.logger.debug(name + " completed successfully!")
612+
recovery_result = self.recover()
613+
if recovery_result is True:
614+
self.logger.debug(name + " completed successfully - confirmed on main screen")
603615
return True
604616
else:
605617
timestamp: str = datetime.now().strftime("%d-%m-%y_%H-%M-%S")
606618
self.save_screenshot(name + "_close_error_" + timestamp)
607-
self.logger.info("Issue closing " + name + ".")
619+
self.logger.warning(
620+
f"Issue closing {name}: Could not confirm we are on main screen. "
621+
f"Recovery process failed. Screenshot saved: {name}_close_error_{timestamp}"
622+
)

0 commit comments

Comments
 (0)