1
0
mirror of https://github.com/TheFunny/ArisuAutoSweeper synced 2026-01-07 14:05:12 +00:00

Compare commits

...

16 Commits

43 changed files with 680 additions and 36 deletions

View File

@ -12,12 +12,15 @@
The script is still under active development. The following features have been implemented: The script is still under active development. The following features have been implemented:
- [x] **Cafe** Claim rewards / Interact / Second cafe - [x] **Cafe** Claim rewards / Interact / Second floor
- [x] **Circle** Claim AP - [x] **Club** Claim AP
- [x] **Mailbox** Claim rewards - [x] **Mailbox** Claim rewards
- [x] **Tactical Challenge** Claim rewards / Auto battle - [x] **Tactical Challenge** Claim rewards / Auto battle
_Currently only supports JP server._ Supported servers:
- [x] JP
- [x] OVERSEA - Global
## Relative projects ## Relative projects
@ -38,3 +41,5 @@ Thanks to [6bir](https://github.com/6bir) for the icon design.
Thanks to [Alas](https://github.com/LmeSzinc/AzurLaneAutoScript) and [SRC](https://github.com/LmeSzinc/StarRailCopilot) Thanks to [Alas](https://github.com/LmeSzinc/AzurLaneAutoScript) and [SRC](https://github.com/LmeSzinc/StarRailCopilot)
for the development framework. for the development framework.
Thanks to [RedDeadDepresso](https://github.com/RedDeadDepresso) for EN support.

View File

@ -17,7 +17,10 @@
- [x] **邮箱** 领取奖励 - [x] **邮箱** 领取奖励
- [x] **战术对抗赛** 领取奖励 / 自动战斗 - [x] **战术对抗赛** 领取奖励 / 自动战斗
_目前仅支持日服。_ 目前支持的服务器:
- [x] 日服
- [x] 国际服 - 全球
## 相关项目 ## 相关项目
@ -36,3 +39,5 @@ _目前仅支持日服。_
感谢 [Alas](https://github.com/LmeSzinc/AzurLaneAutoScript) 以及 [SRC](https://github.com/LmeSzinc/StarRailCopilot) 感谢 [Alas](https://github.com/LmeSzinc/AzurLaneAutoScript) 以及 [SRC](https://github.com/LmeSzinc/StarRailCopilot)
提供的开发框架。 提供的开发框架。
感谢 [RedDeadDepresso](https://github.com/RedDeadDepresso) 提供英语支持。

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -205,8 +205,8 @@
"help": "Auto adjust cafe interface for better student interaction" "help": "Auto adjust cafe interface for better student interaction"
}, },
"SecondCafe": { "SecondCafe": {
"name": "Second Cafe", "name": "Second Floor",
"help": "Enable auto switch to second cafe and perform interaction" "help": "JP server only\nEnable auto switch to second floor and perform interaction"
} }
}, },
"TacticalChallenge": { "TacticalChallenge": {

View File

@ -206,7 +206,7 @@
}, },
"SecondCafe": { "SecondCafe": {
"name": "第二咖啡厅", "name": "第二咖啡厅",
"help": "自动切换第二咖啡厅进行互动点击" "help": "仅支持日服\n自动切换第二咖啡厅进行互动点击"
} }
}, },
"TacticalChallenge": { "TacticalChallenge": {

View File

@ -863,8 +863,8 @@ class Connection(ConnectionAttr):
# Auto package detection # Auto package detection
if len(packages) == 0: if len(packages) == 0:
logger.critical(f'No Star Rail package found, ' logger.critical(f'No Blue Archive package found, '
f'please confirm Star Rail has been installed on device "{self.serial}"') f'please confirm Blue Archive has been installed on device "{self.serial}"')
raise RequestHumanTakeover raise RequestHumanTakeover
if len(packages) == 1: if len(packages) == 1:
logger.info('Auto package detection found only one package, using it') logger.info('Auto package detection found only one package, using it')
@ -877,6 +877,6 @@ class Connection(ConnectionAttr):
# set_server(self.package) # set_server(self.package)
else: else:
logger.critical( logger.critical(
f'Multiple Star Rail packages found, auto package detection cannot decide which to choose, ' f'Multiple Blue Archive packages found, auto package detection cannot decide which to choose, '
'please copy one of the available devices listed above to Alas.Emulator.PackageName') 'please copy one of the available devices listed above to Alas.Emulator.PackageName')
raise RequestHumanTakeover raise RequestHumanTakeover

View File

@ -0,0 +1,71 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CHECK_DESERT_RAILROAD = ButtonWrapper(
name='CHECK_DESERT_RAILROAD',
jp=Button(
file='./assets/jp/bounty/CHECK_DESERT_RAILROAD.png',
area=(106, 147, 401, 179),
search=(86, 127, 421, 199),
color=(172, 179, 183),
button=(106, 147, 401, 179),
),
en=None,
)
CHECK_HIGHWAY = ButtonWrapper(
name='CHECK_HIGHWAY',
jp=Button(
file='./assets/jp/bounty/CHECK_HIGHWAY.png',
area=(107, 147, 400, 179),
search=(87, 127, 420, 199),
color=(191, 199, 203),
button=(107, 147, 400, 179),
),
en=None,
)
CHECK_SCHOOLHOUSE = ButtonWrapper(
name='CHECK_SCHOOLHOUSE',
jp=Button(
file='./assets/jp/bounty/CHECK_SCHOOLHOUSE.png',
area=(106, 147, 314, 179),
search=(86, 127, 334, 199),
color=(176, 183, 187),
button=(106, 147, 314, 179),
),
en=None,
)
SELECT_DESERT_RAILROAD = ButtonWrapper(
name='SELECT_DESERT_RAILROAD',
jp=Button(
file='./assets/jp/bounty/SELECT_DESERT_RAILROAD.png',
area=(1066, 271, 1224, 311),
search=(1046, 251, 1244, 331),
color=(178, 188, 199),
button=(1066, 271, 1224, 311),
),
en=None,
)
SELECT_HIGHWAY = ButtonWrapper(
name='SELECT_HIGHWAY',
jp=Button(
file='./assets/jp/bounty/SELECT_HIGHWAY.png',
area=(1065, 165, 1223, 203),
search=(1045, 145, 1243, 223),
color=(214, 221, 228),
button=(1065, 165, 1223, 203),
),
en=None,
)
SELECT_SCHOOLHOUSE = ButtonWrapper(
name='SELECT_SCHOOLHOUSE',
jp=Button(
file='./assets/jp/bounty/SELECT_SCHOOLHOUSE.png',
area=(1154, 381, 1223, 417),
search=(1134, 361, 1243, 437),
color=(173, 185, 198),
button=(1154, 381, 1223, 417),
),
en=None,
)

View File

@ -5,6 +5,7 @@ from enum import Enum
from module.logger import logger from module.logger import logger
from module.base.timer import Timer from module.base.timer import Timer
from module.base.button import ClickButton from module.base.button import ClickButton
from module.base.decorator import Config
from module.base.utils.utils import area_offset from module.base.utils.utils import area_offset
from module.ocr.ocr import Digit from module.ocr.ocr import Digit
from module.ui.switch import Switch from module.ui.switch import Switch
@ -33,6 +34,8 @@ class CafeStatus(Enum):
class Cafe(UI): class Cafe(UI):
template = CLICKABLE_TEMPLATE
@staticmethod @staticmethod
def merge_points(points, threshold=3): def merge_points(points, threshold=3):
if len(points) <= 1: if len(points) <= 1:
@ -51,8 +54,7 @@ class Cafe(UI):
@staticmethod @staticmethod
def _extract_clickable_from_image(image): def _extract_clickable_from_image(image):
# convert to hsv for better color matching # convert to hsv for better color matching
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# set color range # set color range
lower_hsv = np.array([18, 200, 220]) lower_hsv = np.array([18, 200, 220])
upper_hsv = np.array([30, 255, 255]) upper_hsv = np.array([30, 255, 255])
@ -62,8 +64,8 @@ class Cafe(UI):
return cv2.bitwise_and(image, image, mask=mask) return cv2.bitwise_and(image, image, mask=mask)
def _match_clickable_points(self, image, threshold=0.8): def _match_clickable_points(self, image, threshold=0.8):
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
template = cv2.cvtColor(self.btn.matched_button.image, cv2.COLOR_BGR2GRAY) template = cv2.cvtColor(self.template.matched_button.image, cv2.COLOR_RGB2GRAY)
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
loc = np.where(res >= threshold) loc = np.where(res >= threshold)
@ -78,11 +80,11 @@ class Cafe(UI):
points = self.merge_points(points) points = self.merge_points(points)
if not points: if not points:
return [] return []
area = area_offset((0, 0, self.btn.width, self.btn.height), offset) area = area_offset((0, 0, self.template.width, self.template.height), offset)
return [ return [
ClickButton( ClickButton(
button=area_offset(area, offset=point), button=area_offset(area, offset=point),
name=self.btn.name name=self.template.name
) )
for point in points for point in points
] ]
@ -188,15 +190,23 @@ class Cafe(UI):
logger.warning(f'Invalid status: {status}') logger.warning(f'Invalid status: {status}')
return status return status
@Config.when(Emulator_GameLanguage='jp')
def is_second_cafe_on(self):
return self.config.Cafe_SecondCafe
@Config.when(Emulator_GameLanguage=None)
def is_second_cafe_on(self):
return False
is_second_cafe_on = property(is_second_cafe_on)
def run(self): def run(self):
self.btn = CLICKABLE_TEMPLATE
self.click = 0 self.click = 0
self.check = 0 self.check = 0
is_reward_on = self.config.Cafe_Reward is_reward_on = self.config.Cafe_Reward
is_touch_on = self.config.Cafe_Touch is_touch_on = self.config.Cafe_Touch
self.is_adjust_on = self.config.Cafe_AutoAdjust self.is_adjust_on = self.config.Cafe_AutoAdjust
is_second_cafe_on = self.config.Cafe_SecondCafe
self.ui_ensure(page_cafe) self.ui_ensure(page_cafe)
@ -240,7 +250,7 @@ class Cafe(UI):
is_reset = True is_reset = True
continue continue
if is_second_cafe_on and not is_second and status == CafeStatus.FINISHED: if self.is_second_cafe_on and not is_second and status == CafeStatus.FINISHED:
if not SWITCH_CAFE.appear(main=self): if not SWITCH_CAFE.appear(main=self):
logger.warning('Cafe switch not found') logger.warning('Cafe switch not found')
continue continue
@ -266,11 +276,14 @@ class Cafe(UI):
logger.attr('Status', status) logger.attr('Status', status)
status = self._handle_cafe(status) status = self._handle_cafe(status)
if not is_second_cafe_on: if not self.is_second_cafe_on:
if status is CafeStatus.FINISHED: if status is CafeStatus.FINISHED:
logger.info('Second cafe is not supported or disabled')
logger.info('Cafe finished')
break break
else: else:
if is_second and status is CafeStatus.FINISHED: if is_second and status is CafeStatus.FINISHED:
logger.info('Cafe finished')
break break
self.config.task_delay(server_update=True, minute=180) self.config.task_delay(server_update=True, minute=180)

View File

@ -0,0 +1,71 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CHECK_GEHENNA = ButtonWrapper(
name='CHECK_GEHENNA',
jp=Button(
file='./assets/jp/school_exchange/CHECK_GEHENNA.png',
area=(109, 149, 195, 177),
search=(89, 129, 215, 197),
color=(201, 205, 206),
button=(109, 149, 195, 177),
),
en=None,
)
CHECK_MILLENNIUM = ButtonWrapper(
name='CHECK_MILLENNIUM',
jp=Button(
file='./assets/jp/school_exchange/CHECK_MILLENNIUM.png',
area=(108, 148, 254, 178),
search=(88, 128, 274, 198),
color=(202, 206, 208),
button=(108, 148, 254, 178),
),
en=None,
)
CHECK_TRINITY = ButtonWrapper(
name='CHECK_TRINITY',
jp=Button(
file='./assets/jp/school_exchange/CHECK_TRINITY.png',
area=(115, 148, 250, 178),
search=(95, 128, 270, 198),
color=(204, 208, 210),
button=(115, 148, 250, 178),
),
en=None,
)
SELECT_GEHENNA = ButtonWrapper(
name='SELECT_GEHENNA',
jp=Button(
file='./assets/jp/school_exchange/SELECT_GEHENNA.png',
area=(1125, 275, 1224, 310),
search=(1105, 255, 1244, 330),
color=(207, 217, 225),
button=(1125, 275, 1224, 310),
),
en=None,
)
SELECT_MILLENNIUM = ButtonWrapper(
name='SELECT_MILLENNIUM',
jp=Button(
file='./assets/jp/school_exchange/SELECT_MILLENNIUM.png',
area=(1069, 381, 1217, 417),
search=(1049, 361, 1237, 437),
color=(206, 214, 222),
button=(1069, 381, 1217, 417),
),
en=None,
)
SELECT_TRINITY = ButtonWrapper(
name='SELECT_TRINITY',
jp=Button(
file='./assets/jp/school_exchange/SELECT_TRINITY.png',
area=(1074, 165, 1221, 204),
search=(1054, 145, 1241, 224),
color=(213, 221, 228),
button=(1074, 165, 1221, 204),
),
en=None,
)

View File

@ -0,0 +1,60 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
OCR_INDEX = ButtonWrapper(
name='OCR_INDEX',
jp=Button(
file='./assets/jp/stage/list/OCR_INDEX.png',
area=(701, 149, 740, 656),
search=(681, 129, 760, 676),
color=(195, 196, 193),
button=(701, 149, 740, 656),
),
en=None,
)
STAGE_ENTER = ButtonWrapper(
name='STAGE_ENTER',
jp=Button(
file='./assets/jp/stage/list/STAGE_ENTER.png',
area=(1093, 173, 1142, 199),
search=(1073, 153, 1162, 219),
color=(106, 171, 200),
button=(1093, 173, 1142, 199),
),
en=None,
)
STAGE_ITEM = ButtonWrapper(
name='STAGE_ITEM',
jp=Button(
file='./assets/jp/stage/list/STAGE_ITEM.png',
area=(687, 148, 1181, 227),
search=(667, 128, 1201, 247),
color=(212, 228, 233),
button=(687, 148, 1181, 227),
),
en=None,
)
STAGE_LIST = ButtonWrapper(
name='STAGE_LIST',
jp=Button(
file='./assets/jp/stage/list/STAGE_LIST.png',
area=(675, 136, 1190, 676),
search=(655, 116, 1210, 696),
color=(194, 204, 209),
button=(675, 136, 1190, 676),
),
en=None,
)
STAGE_STARS = ButtonWrapper(
name='STAGE_STARS',
jp=Button(
file='./assets/jp/stage/list/STAGE_STARS.png',
area=(693, 192, 746, 212),
search=(673, 172, 766, 232),
color=(225, 214, 166),
button=(693, 192, 746, 212),
),
en=None,
)

View File

@ -0,0 +1,148 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CHECK_SWEEP = ButtonWrapper(
name='CHECK_SWEEP',
jp=Button(
file='./assets/jp/stage/sweep/CHECK_SWEEP.png',
area=(638, 188, 722, 212),
search=(618, 168, 742, 232),
color=(174, 184, 197),
button=(638, 188, 722, 212),
),
en=None,
)
ENTER = ButtonWrapper(
name='ENTER',
jp=Button(
file='./assets/jp/stage/sweep/ENTER.png',
area=(791, 514, 1080, 568),
search=(771, 494, 1100, 588),
color=(223, 207, 68),
button=(791, 514, 1080, 568),
),
en=None,
)
EXIT = ButtonWrapper(
name='EXIT',
jp=Button(
file='./assets/jp/stage/sweep/EXIT.png',
area=(1114, 127, 1141, 154),
search=(1094, 107, 1161, 174),
color=(185, 193, 203),
button=(1114, 127, 1141, 154),
),
en=None,
)
MAX = ButtonWrapper(
name='MAX',
jp=Button(
file='./assets/jp/stage/sweep/MAX.png',
area=(1054, 279, 1111, 321),
search=(1034, 259, 1131, 341),
color=(216, 222, 228),
button=(1054, 279, 1111, 321),
),
en=None,
)
MIN = ButtonWrapper(
name='MIN',
jp=Button(
file='./assets/jp/stage/sweep/MIN.png',
area=(760, 278, 816, 322),
search=(740, 258, 836, 342),
color=(194, 194, 194),
button=(760, 278, 816, 322),
),
en=None,
)
MINUS = ButtonWrapper(
name='MINUS',
jp=Button(
file='./assets/jp/stage/sweep/MINUS.png',
area=(838, 279, 876, 320),
search=(818, 259, 896, 340),
color=(221, 222, 222),
button=(838, 279, 876, 320),
),
en=None,
)
OCR_NUM = ButtonWrapper(
name='OCR_NUM',
jp=Button(
file='./assets/jp/stage/sweep/OCR_NUM.png',
area=(896, 281, 975, 323),
search=(876, 261, 995, 343),
color=(81, 94, 113),
button=(896, 281, 975, 323),
),
en=None,
)
PLUS = ButtonWrapper(
name='PLUS',
jp=Button(
file='./assets/jp/stage/sweep/PLUS.png',
area=(995, 278, 1034, 322),
search=(975, 258, 1054, 342),
color=(233, 243, 246),
button=(995, 278, 1034, 322),
),
en=None,
)
SKIP_OK_LOWER = ButtonWrapper(
name='SKIP_OK_LOWER',
jp=Button(
file='./assets/jp/stage/sweep/SKIP_OK_LOWER.png',
area=(541, 551, 740, 616),
search=(521, 531, 760, 636),
color=(112, 212, 247),
button=(541, 551, 740, 616),
),
en=None,
)
SKIP_OK_UPPER = ButtonWrapper(
name='SKIP_OK_UPPER',
jp=Button(
file='./assets/jp/stage/sweep/SKIP_OK_UPPER.png',
area=(542, 474, 738, 545),
search=(522, 454, 758, 565),
color=(112, 212, 248),
button=(542, 474, 738, 545),
),
en=None,
)
SKIP_SKIP = ButtonWrapper(
name='SKIP_SKIP',
jp=Button(
file='./assets/jp/stage/sweep/SKIP_SKIP.png',
area=(545, 475, 736, 540),
search=(525, 455, 756, 560),
color=(110, 207, 243),
button=(545, 475, 736, 540),
),
en=None,
)
SWEEP = ButtonWrapper(
name='SWEEP',
jp=Button(
file='./assets/jp/stage/sweep/SWEEP.png',
area=(796, 385, 1067, 427),
search=(776, 365, 1087, 447),
color=(109, 202, 235),
button=(796, 385, 1067, 427),
),
en=None,
)
SWEEP_CONFIRM = ButtonWrapper(
name='SWEEP_CONFIRM',
jp=Button(
file='./assets/jp/stage/sweep/SWEEP_CONFIRM.png',
area=(611, 147, 669, 177),
search=(591, 127, 689, 197),
color=(143, 156, 170),
button=(664, 470, 871, 534),
),
en=None,
)

View File

@ -2,11 +2,12 @@ import cv2
import numpy as np import numpy as np
from module.base.base import ModuleBase from module.base.base import ModuleBase
from module.base.button import ButtonWrapper, ClickButton from module.base.button import ClickButton, match_template
from module.base.timer import Timer from module.base.timer import Timer
from module.base.utils import area_pad, area_size, area_offset, random_rectangle_vector_opted from module.base.utils import area_pad, area_size, area_offset, random_rectangle_vector_opted
from module.logger import logger from module.logger import logger
from module.ocr.ocr import Ocr from module.ocr.ocr import Ocr
from tasks.stage.assets.assets_stage_list import *
class StageList: class StageList:
@ -15,17 +16,19 @@ class StageList:
def __init__( def __init__(
self, self,
name, name,
area_stage: ButtonWrapper, button_list: ButtonWrapper = None,
area_index: ButtonWrapper, button_index: ButtonWrapper = None,
area_item: ButtonWrapper, button_item: ButtonWrapper = None,
button_enter: ButtonWrapper, button_enter: ButtonWrapper = None,
button_stars: ButtonWrapper = None,
drag_direction: str = "down" drag_direction: str = "down"
): ):
self.name = name self.name = name
self.stage = area_stage self.stage = button_list if button_list else STAGE_LIST
self.index_ocr = Ocr(area_index, lang='en') self.index_ocr = Ocr(button_index if button_index else OCR_INDEX, lang='en')
self.stage_item = area_item.button self.stage_item = (button_item if button_item else STAGE_ITEM).button
self.enter = button_enter self.enter = button_enter if button_enter else STAGE_ENTER
self.sweepable = button_stars if button_stars else STAGE_STARS
self.drag_direction = drag_direction self.drag_direction = drag_direction
self.current_index_min = 1 self.current_index_min = 1
@ -43,7 +46,8 @@ class StageList:
def __hash__(self): def __hash__(self):
return hash(self.name) return hash(self.name)
def _get_indexes(self) -> list[int]: @property
def _indexes(self) -> list[int]:
return list(map(lambda x: int(x.ocr_text), self.current_indexes)) return list(map(lambda x: int(x.ocr_text), self.current_indexes))
def load_stage_indexes(self, main: ModuleBase): def load_stage_indexes(self, main: ModuleBase):
@ -53,7 +57,7 @@ class StageList:
if not self.current_indexes: if not self.current_indexes:
logger.warning(f'No valid index in {self.index_ocr.name}') logger.warning(f'No valid index in {self.index_ocr.name}')
return return
indexes = self._get_indexes() indexes = self._indexes
self.current_index_min = min(indexes) self.current_index_min = min(indexes)
self.current_index_max = max(indexes) self.current_index_max = max(indexes)
@ -119,7 +123,7 @@ class StageList:
timeout=Timer(1.5, 5) timeout=Timer(1.5, 5)
) )
indexes = self._get_indexes() indexes = self._indexes
if indexes and last_indexes == set(indexes): if indexes and last_indexes == set(indexes):
logger.warning(f'No more index {index}') logger.warning(f'No more index {index}')
return False return False
@ -129,20 +133,28 @@ class StageList:
@staticmethod @staticmethod
def _match_clickable_points(image, template, threshold=0.85): def _match_clickable_points(image, template, threshold=0.85):
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) template = cv2.cvtColor(template, cv2.COLOR_RGB2GRAY)
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
loc = np.where(res >= threshold) loc = np.where(res >= threshold)
return [point for point in zip(*loc[::-1])] return [point for point in zip(*loc[::-1])]
def is_sweepable(self, image, main: ModuleBase, skip_first_screenshot=True) -> bool:
if not skip_first_screenshot:
main.device.screenshot()
return match_template(image, self.sweepable.matched_button.image)
def select_index_enter( def select_index_enter(
self, self,
index: int, index: int,
main: ModuleBase, main: ModuleBase,
insight: bool = True, insight: bool = True,
sweepable: bool = True,
offset: tuple[int, int] = (-20, -15),
skip_first_screenshot: bool = True, skip_first_screenshot: bool = True,
interval: int = 5 interval: int = 2
) -> bool: ) -> bool:
if insight and not self.insight_index(index, main, skip_first_screenshot): if insight and not self.insight_index(index, main, skip_first_screenshot):
return False return False
@ -164,9 +176,14 @@ class StageList:
logger.warning(f'No index {index} in {self.index_ocr.name}') logger.warning(f'No index {index} in {self.index_ocr.name}')
return False return False
stage_item_box = area_pad((0, 0, *area_size(self.stage_item))) stage_item_box = area_pad((*offset, *area_size(self.stage_item)))
search_box = area_offset(stage_item_box, index_box.box[:2]) search_box = area_offset(stage_item_box, index_box.box[:2])
search_image = main.image_crop(search_box) search_image = main.image_crop(search_box)
if sweepable and not self.is_sweepable(search_image, main, skip_first_screenshot):
logger.warning(f'Index {index} is not sweepable')
return False
points = self._match_clickable_points(search_image, self.enter.matched_button.image) points = self._match_clickable_points(search_image, self.enter.matched_button.image)
if not points: if not points:

254
tasks/stage/sweep.py Normal file
View File

@ -0,0 +1,254 @@
from module.base.base import ModuleBase
from module.base.timer import Timer
from module.logger import logger
from module.ocr.ocr import Digit
from enum import Enum
from tasks.stage.assets.assets_stage_sweep import *
class SweepStatus(Enum):
SELECT = 1
START = 2
CONFIRM = 3
SKIP = 4
END = 5
FINISH = 6
class StageSweep:
def __init__(
self,
name: str,
sweep_num: int,
max_sweep: int,
):
self.name = name
self.sweep_num = sweep_num
self.check: ButtonWrapper = None
self.num: Digit = None
self.plus: ButtonWrapper = None
self.minus: ButtonWrapper = None
self.max: ButtonWrapper = None
self.min: ButtonWrapper = None
self.sweep: ButtonWrapper = None
self.sweep_confirm: ButtonWrapper = None
self.enter: ButtonWrapper = None
self.exit: ButtonWrapper = None
self.skip_skip: ButtonWrapper = None
self.skip_ok_upper: ButtonWrapper = None
self.skip_ok_lower: ButtonWrapper = None
self.set_button()
self.min_sweep = 1
self.max_sweep = max_sweep
self.current_sweep = 0
self.sweep_method = None
self.set_mode()
def __str__(self):
return f'StageSweep({self.name})'
__repr__ = __str__
def __eq__(self, other):
return str(self) == str(other)
def __hash__(self):
return hash(self.name)
def set_button(
self,
button_check: ButtonWrapper = None,
button_num: ButtonWrapper = None,
button_plus: ButtonWrapper = None,
button_minus: ButtonWrapper = None,
button_max: ButtonWrapper = None,
button_min: ButtonWrapper = None,
button_sweep: ButtonWrapper = None,
button_sweep_confirm: ButtonWrapper = None,
button_enter: ButtonWrapper = None,
button_exit: ButtonWrapper = None,
button_skip_skip: ButtonWrapper = None,
button_skip_ok_upper: ButtonWrapper = None,
button_skip_ok_lower: ButtonWrapper = None,
):
self.check = button_check if button_check else CHECK_SWEEP
self.num = Digit(button_num if button_num else OCR_NUM)
self.plus = button_plus if button_plus else PLUS
self.minus = button_minus if button_minus else MINUS
self.max = button_max if button_max else MAX
self.min = button_min if button_min else MIN
self.sweep = button_sweep if button_sweep else SWEEP
self.sweep_confirm = button_sweep_confirm if button_sweep_confirm else SWEEP_CONFIRM
self.enter = button_enter if button_enter else ENTER
self.exit = button_exit if button_exit else EXIT
self.skip_skip = button_skip_skip if button_skip_skip else SKIP_SKIP
self.skip_ok_upper = button_skip_ok_upper if button_skip_ok_upper else SKIP_OK_UPPER
self.skip_ok_lower = button_skip_ok_lower if button_skip_ok_lower else SKIP_OK_LOWER
def set_mode(self, mode: str = None):
if mode is None:
match self.sweep_num:
case 0:
self.sweep_method = self.set_sweep_min
case -1:
self.sweep_method = self.set_sweep_max
case x if x > 0:
self.sweep_method = self.set_sweep_num
case _:
logger.warning(f'Invalid sweep num: {self.sweep_num}')
return
match mode:
case 'max':
self.sweep_method = self.set_sweep_max
case 'min':
self.sweep_method = self.set_sweep_min
case _:
logger.warning(f'Invalid sweep mode: {mode}')
def check_sweep(self, main: ModuleBase):
return main.appear(self.check)
def check_skip(self, main: ModuleBase):
return main.appear(self.skip_skip) or main.appear(self.skip_ok_upper) or main.appear(self.skip_ok_lower)
def load_sweep_num(self, main: ModuleBase):
while 1:
main.device.screenshot()
ocr_result = self.num.detect_and_ocr(main.device.image)
if not ocr_result:
logger.warning(f'No valid num in {self.num.name}')
continue
if len(ocr_result) == 1:
self.current_sweep = int(ocr_result[0].ocr_text)
return
def set_sweep_num(self, main: ModuleBase, skip_first_screenshot=True) -> bool:
num = self.sweep_num
if num < self.min_sweep or num > self.max_sweep:
logger.warning(f'Invalid sweep num: {num}')
return False
logger.info(f'Set sweep num: {num}')
retry = Timer(1, 2)
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
main.device.screenshot()
self.load_sweep_num(main)
if self.current_sweep == num:
logger.info(f'Sweep num reaches {num}')
return True
elif self.current_sweep == 0:
logger.info(f'Current sweep num is 0')
return False
if retry.reached_and_reset():
diff = num - self.current_sweep
button = self.plus if diff > 0 else self.minus
main.device.multi_click(button, abs(diff), interval=(0.2, 0.3))
def set_sweep_max(self, main: ModuleBase, skip_first_screenshot=True):
logger.info(f'Set sweep max: {self.max_sweep}')
retry = Timer(1, 2)
count = 0
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
main.device.screenshot()
self.load_sweep_num(main)
if self.current_sweep == self.max_sweep:
logger.info(f'Sweep max reaches {self.max_sweep}')
return True
elif count == 1 and self.current_sweep != 1:
logger.info("Set sweep max")
return True
elif self.current_sweep == 0:
logger.info(f'Current sweep num is 0')
return False
if retry.reached_and_reset():
main.click_with_interval(self.max, interval=0)
count += 1
continue
if count > 2:
logger.info("Set sweep max")
return True
def set_sweep_min(self, main: ModuleBase, skip_first_screenshot=True):
logger.info(f'Set sweep min: {self.min_sweep}')
retry = Timer(1, 2)
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
main.device.screenshot()
self.load_sweep_num(main)
if self.current_sweep == self.min_sweep:
logger.info(f'Sweep min reaches {self.min_sweep}')
return True
elif self.current_sweep == 0:
logger.info(f'Current sweep num is 0')
return False
if retry.reached_and_reset():
main.click_with_interval(self.min, interval=0)
def do_sweep(self, main: ModuleBase, skip_first_screenshot=True) -> bool:
timer = Timer(0.5, 1)
timer_stable = Timer(0.5, 1).start()
status = SweepStatus.SELECT
while 1:
if not timer_stable.reached():
continue
if skip_first_screenshot:
skip_first_screenshot = False
else:
main.device.screenshot()
if timer.reached_and_reset():
logger.attr("Status", status)
match status:
case SweepStatus.SELECT:
if self.sweep_method(main, skip_first_screenshot):
status = SweepStatus.START
else:
return False
case SweepStatus.START:
main.appear_then_click(self.sweep, interval=1)
if main.appear(self.sweep_confirm):
status = SweepStatus.CONFIRM
case SweepStatus.CONFIRM:
main.appear_then_click(self.sweep_confirm, interval=1)
if self.check_skip(main):
status = SweepStatus.SKIP
case SweepStatus.SKIP:
main.appear_then_click(self.skip_skip)
main.appear_then_click(self.skip_ok_upper)
main.appear_then_click(self.skip_ok_lower)
if self.check_sweep(main):
status = SweepStatus.END
case SweepStatus.END:
main.appear_then_click(self.exit, interval=1)
if not main.appear(self.check):
status = SweepStatus.FINISH
case SweepStatus.FINISH:
pass
case _:
logger.warning(f'Invalid status: {status}')
return False
if status == SweepStatus.FINISH:
logger.info(f'Sweep finish')
return True