1
0
mirror of https://github.com/TheFunny/ArisuAutoSweeper synced 2026-06-29 08:34:51 +00:00

56 Commits

Author SHA1 Message Date
YoursFunny baac90ecf0 fix(resource): remove non-existing assets loading 2023-11-21 20:42:24 +08:00
YoursFunny 53ec298fed fix(alas): validate datetime instead of using regex 2023-11-21 20:29:26 +08:00
YoursFunny b4f18f78ff perf: use more friendly record time on dashboard 2023-11-21 20:27:19 +08:00
YoursFunny eb9af42f38 fix: ignore value in state type args 2023-11-21 20:16:40 +08:00
YoursFunny c29d972c6c feat: support direct_match and match_multi_template 2023-11-21 20:07:45 +08:00
YoursFunny 92b34d4760 perf: release resource when free 2023-11-21 19:59:32 +08:00
YoursFunny 04853b6c31 feat: support load search for buttons 2023-11-21 19:53:02 +08:00
YoursFunny 9604e8962a fix: accept area attr in ClickButton 2023-11-21 19:20:04 +08:00
YoursFunny 03380b2d71 fix: use distinctive search attr for each button frame 2023-11-21 19:17:06 +08:00
YoursFunny c27bd74050 fix: adjust icon css 2023-11-21 19:00:14 +08:00
YoursFunny c3e9945b15 lang: use shorter description for items 2023-11-21 18:43:04 +08:00
YoursFunny 1dd100ac04 lang: fix typo 2023-11-21 15:28:08 +08:00
YoursFunny 77dca70af1 doc: update readme 2023-11-21 15:25:41 +08:00
YoursFunny b8ecd0c9d6 feat: support scrimmage 2023-11-21 15:18:07 +08:00
YoursFunny ceb24283f3 feat: add scrimmage gui option 2023-11-21 15:17:49 +08:00
YoursFunny d0c591af3a fix(bounty): add error handler when enter sweep failed 2023-11-21 14:21:22 +08:00
YoursFunny 589b0b08ec perf(sweep): improve sweep list stability 2023-11-21 14:20:02 +08:00
YoursFunny 299bd6c687 refactor(sweep): change parameter order 2023-11-21 13:54:59 +08:00
YoursFunny e61afaf43b fix(sweep): filter non-digit ocr text 2023-11-21 13:14:30 +08:00
YoursFunny 30e8c8b21b fix(tc): add reward handler 2023-11-21 12:55:16 +08:00
YoursFunny f7b165f589 perf(tc): improve get reward method 2023-11-20 22:34:01 +08:00
YoursFunny 08959e5f1c feat(tc): update storage of tc ticket 2023-11-20 22:26:24 +08:00
YoursFunny e929a1efb1 refactor(tc): separate ui operation 2023-11-20 22:10:40 +08:00
YoursFunny 8e29d7d2c0 feat: add scrimmage config 2023-11-20 21:43:11 +08:00
YoursFunny d3a1a77d6a feat: add scrimmage assets 2023-11-20 21:28:34 +08:00
YoursFunny 930c741de6 doc: update readme 2023-11-20 20:08:26 +08:00
YoursFunny 2c19afdf26 feat(bounty): add error handler choice 2023-11-20 17:28:55 +08:00
YoursFunny bbf3bf7c36 perf(bounty): improve ending logic 2023-11-20 17:06:08 +08:00
YoursFunny 5e4abc147e feat(bounty): detect ticket and end when zero 2023-11-20 16:50:18 +08:00
YoursFunny eb8048ccd6 doc: update readme 2023-11-20 16:31:49 +08:00
YoursFunny 8bec814b8d feat: support bounty 2023-11-20 16:25:06 +08:00
YoursFunny 5fb2810bdc feat: add bounty assets 2023-11-20 16:23:27 +08:00
YoursFunny 8ea95dc340 feat: add bounty gui option 2023-11-20 16:21:40 +08:00
YoursFunny 9d3e581321 feat: add ticket storage in config and gui 2023-11-20 16:20:02 +08:00
YoursFunny d47e463365 perf(sweep): improve sweep flexibility 2023-11-19 23:59:01 +08:00
YoursFunny 0a697e9398 fix: add required argument 2023-11-19 19:41:43 +08:00
YoursFunny 143f519adb fix: remove useless code 2023-11-19 17:05:06 +08:00
YoursFunny af71e797db fix(login): change survey handle for default server 2023-11-19 17:04:40 +08:00
YoursFunny 6ef18ed8c0 feat(login): add survey page handle 2023-11-18 19:16:34 +08:00
YoursFunny 8781e7830c fix(popup): make correction to ap exceed and add insufficient inventory for JP 2023-11-17 14:09:34 +08:00
YoursFunny 84f78230d2 doc: update readme 2023-11-17 13:45:43 +08:00
YoursFunny f506616ba9 feat: add assets for bounty 2023-11-17 13:17:08 +08:00
YoursFunny c1d9ac4f64 feat: add assets for scrimmage (school exchange) 2023-11-17 13:17:08 +08:00
YoursFunny fb28fe297a feat(stage): add sweepable button 2023-11-17 13:17:08 +08:00
YoursFunny 9b17f1948a perf(stage): adjust click interval 2023-11-17 13:17:08 +08:00
YoursFunny e82c4f875c feat(stage): add offset to stage item box 2023-11-17 13:17:07 +08:00
YoursFunny 51ecdf4908 feat(stage): support sweepable detection 2023-11-17 13:17:07 +08:00
YoursFunny 8d2882e752 fix: use correct color space for template matching 2023-11-17 13:17:07 +08:00
YoursFunny 1a66e767f3 refactor(stage): reuse assets for all stages 2023-11-17 13:17:07 +08:00
YoursFunny 04fab819b4 feat(stage): add sweep support 2023-11-17 13:17:07 +08:00
YoursFunny 023972682d refactor: move stage list to its own folder 2023-11-17 13:17:06 +08:00
YoursFunny 2cefb26759 refactor: set indexes as property 2023-11-17 13:17:06 +08:00
YoursFunny 7ed7cabbaa lang: adjust text of second floor 2023-11-17 13:17:06 +08:00
YoursFunny b62e301c55 feat(cafe): detect server before attempt on second floor 2023-11-17 13:17:06 +08:00
YoursFunny 93e8fd1f0c refactor(cafe): rename template button 2023-11-17 13:17:06 +08:00
YoursFunny b9e0cc3026 fix: adapt for BA 2023-11-17 13:17:05 +08:00
82 changed files with 2149 additions and 180 deletions
+10 -3
View File
@@ -12,12 +12,17 @@
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] **Bounty** Auto sweep
- [x] **Scrimmage** Auto sweep
- [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 +43,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.
+16 -1
View File
@@ -15,9 +15,22 @@
- [x] **咖啡厅** 领取奖励 / 互动 / 第二咖啡厅 - [x] **咖啡厅** 领取奖励 / 互动 / 第二咖啡厅
- [x] **公会** 领取体力 - [x] **公会** 领取体力
- [x] **邮箱** 领取奖励 - [x] **邮箱** 领取奖励
- [x] **悬赏通缉** 自动扫荡
- [x] **学院交流会** 自动扫荡
- [x] **战术对抗赛** 领取奖励 / 自动战斗 - [x] **战术对抗赛** 领取奖励 / 自动战斗
_目前支持日服。_ 目前支持的服务器:
- [x] 日服
- [x] 国际服 - 全球
## 已知问题
若愿意提供其他服务器支持,请开 PR 或 Issue。
- **大小月卡**:未实现自动领取,~~因为没买过~~,可能不影响使用。愿意提供图片的请开 Issue
- **月卡的额外悬赏券和学院交流券**:不太清楚月卡领取额外券的机制,~~因为没买过~~,可能影响相关任务使用券和体力的计算。愿意提供相关信息的请开
Issue
## 相关项目 ## 相关项目
@@ -36,3 +49,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) 提供英语支持。
+8
View File
@@ -34,6 +34,14 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
from tasks.mail.mail import Mail from tasks.mail.mail import Mail
Mail(config=self.config, device=self.device).run() Mail(config=self.config, device=self.device).run()
def bounty(self):
from tasks.bounty.bounty import Bounty
Bounty(config=self.config, device=self.device).run()
def scrimmage(self):
from tasks.scrimmage.scrimmage import Scrimmage
Scrimmage(config=self.config, device=self.device).run()
def tactical_challenge(self): def tactical_challenge(self):
from tasks.tactical_challenge.tactical_challenge import TacticalChallenge from tasks.tactical_challenge.tactical_challenge import TacticalChallenge
TacticalChallenge(config=self.config, device=self.device).run() TacticalChallenge(config=self.config, device=self.device).run()
+1 -1
View File
@@ -285,7 +285,7 @@ pre.rich-traceback-code {
*[style*="--header-icon--"] { *[style*="--header-icon--"] {
margin: .25rem auto .25rem; margin: .25rem auto .25rem;
border-radius: 1.5rem; border-radius: 1.5rem;
height: 3.5em; height: 3em;
} }
*[style*="--header-text--"] { *[style*="--header-text--"] {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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: 6.0 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: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 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

+50 -1
View File
@@ -63,6 +63,52 @@
"ServerUpdate": "04:00" "ServerUpdate": "04:00"
} }
}, },
"Bounty": {
"Scheduler": {
"Enable": true,
"NextRun": "2020-01-01 00:00:00",
"Command": "Bounty",
"ServerUpdate": "04:00"
},
"Bounty": {
"OnError": "skip"
},
"Highway": {
"Stage": 1,
"Count": 2
},
"DesertRailroad": {
"Stage": 1,
"Count": 2
},
"Schoolhouse": {
"Stage": 1,
"Count": 2
}
},
"Scrimmage": {
"Scheduler": {
"Enable": true,
"NextRun": "2020-01-01 00:00:00",
"Command": "Scrimmage",
"ServerUpdate": "04:00"
},
"Scrimmage": {
"OnError": "skip"
},
"Trinity": {
"Stage": 1,
"Count": 2
},
"Gehenna": {
"Stage": 1,
"Count": 2
},
"Millennium": {
"Stage": 1,
"Count": 2
}
},
"TacticalChallenge": { "TacticalChallenge": {
"Scheduler": { "Scheduler": {
"Enable": true, "Enable": true,
@@ -84,7 +130,10 @@
"ItemStorage": { "ItemStorage": {
"AP": {}, "AP": {},
"Credit": {}, "Credit": {},
"Pyroxene": {} "Pyroxene": {},
"BountyTicket": {},
"ScrimmageTicket": {},
"TacticalChallengeTicket": {}
} }
} }
} }
+16 -7
View File
@@ -179,20 +179,29 @@ def iter_assets():
if image.attr != '': if image.attr != '':
row = deep_get(data, keys=[image.module, image.assets, image.server, image.frame]) row = deep_get(data, keys=[image.module, image.assets, image.server, image.frame])
row.load_image(image) row.load_image(image)
# Apply `search` of the first frame to all # Set `search`
for path, frames in deep_iter(data, depth=3): for path, frames in deep_iter(data, depth=3):
print(path, frames) print(path, frames)
# If `search` attribute is set in the first frame, apply to all
first = frames[1] first = frames[1]
search = first.search if first.search else DataAssets.area_to_search(first.area) if first.search:
for frame in frames.values(): for frame in frames.values():
frame.search = search frame.search = first.search
else:
for frame in frames.values():
if frame.search:
# Follow frame specific `search`
pass
else:
# Generate `search` from `area`
frame.search = DataAssets.area_to_search(frame.area)
return data return data
def generate_code(): def generate_code():
all = iter_assets() all_assets = iter_assets()
for module, module_data in all.items(): for module, module_data in all_assets.items():
path = os.path.join(AzurLaneConfig.ASSETS_MODULE, module.split('/', maxsplit=1)[0]) path = os.path.join(AzurLaneConfig.ASSETS_MODULE, module.split('/', maxsplit=1)[0])
output = os.path.join(path, 'assets.py') output = os.path.join(path, 'assets.py')
if os.path.exists(output): if os.path.exists(output):
@@ -204,7 +213,7 @@ def generate_code():
continue continue
os.remove(prev) os.remove(prev)
for module, module_data in all.items(): for module, module_data in all_assets.items():
path = os.path.join(AzurLaneConfig.ASSETS_MODULE, module.split('/', maxsplit=1)[0]) path = os.path.join(AzurLaneConfig.ASSETS_MODULE, module.split('/', maxsplit=1)[0])
output = os.path.join(path, 'assets') output = os.path.join(path, 'assets')
gen = CodeGenerator() gen = CodeGenerator()
+101 -21
View File
@@ -74,7 +74,7 @@ class Button(Resource):
threshold=threshold threshold=threshold
) )
def match_template(self, image, similarity=0.85) -> bool: def match_template(self, image, similarity=0.85, direct_match=False) -> bool:
""" """
Detects assets by template matching. Detects assets by template matching.
@@ -83,18 +83,45 @@ class Button(Resource):
Args: Args:
image: Screenshot. image: Screenshot.
similarity (float): 0-1. similarity (float): 0-1.
direct_match: True to ignore `self.search`
Returns: Returns:
bool. bool.
""" """
image = crop(image, self.search, copy=False) if not direct_match:
image = crop(image, self.search, copy=False)
res = cv2.matchTemplate(self.image, image, cv2.TM_CCOEFF_NORMED) res = cv2.matchTemplate(self.image, image, cv2.TM_CCOEFF_NORMED)
_, sim, _, point = cv2.minMaxLoc(res) _, sim, _, point = cv2.minMaxLoc(res)
self._button_offset = np.array(point) + self.search[:2] - self.area[:2] self._button_offset = np.array(point) + self.search[:2] - self.area[:2]
return sim > similarity return sim > similarity
def match_template_color(self, image, similarity=0.85, threshold=30) -> bool: def match_multi_template(self, image, similarity=0.85, direct_match=False):
"""
Detects assets by template matching, return multiple reults
Args:
image: Screenshot.
similarity (float): 0-1.
direct_match: True to ignore `self.search`
Returns:
list:
"""
if not direct_match:
image = crop(image, self.search, copy=False)
res = cv2.matchTemplate(self.image, image, cv2.TM_CCOEFF_NORMED)
res = cv2.inRange(res, similarity, 1.)
try:
points = np.array(cv2.findNonZero(res))[:, 0, :]
points += self.search[:2]
return points.tolist()
except IndexError:
# Empty result
# IndexError: too many indices for array: array is 0-dimensional, but 3 were indexed
return []
def match_template_color(self, image, similarity=0.85, threshold=30, direct_match=False) -> bool:
""" """
Template match first, color match then Template match first, color match then
@@ -102,11 +129,12 @@ class Button(Resource):
image: Screenshot. image: Screenshot.
similarity (float): 0-1. similarity (float): 0-1.
threshold (int): Default to 10. threshold (int): Default to 10.
direct_match: True to ignore `self.search`
Returns: Returns:
bool.
""" """
matched = self.match_template(image, similarity=similarity) matched = self.match_template(image, similarity=similarity, direct_match=direct_match)
if not matched: if not matched:
return False return False
@@ -124,10 +152,10 @@ class ButtonWrapper(Resource):
self.name = name self.name = name
self.data_buttons = kwargs self.data_buttons = kwargs
self._matched_button: t.Optional[Button] = None self._matched_button: t.Optional[Button] = None
self.resource_add(self.name) self.resource_add(f'{name}:{next(self.iter_buttons(), None)}')
def resource_release(self): def resource_release(self):
del_cached_property(self, 'assets') del_cached_property(self, 'buttons')
self._matched_button = None self._matched_button = None
def __str__(self): def __str__(self):
@@ -144,16 +172,25 @@ class ButtonWrapper(Resource):
def __bool__(self): def __bool__(self):
return True return True
def iter_buttons(self) -> t.Iterator[Button]:
for _, assets in self.data_buttons.items():
if isinstance(assets, Button):
yield assets
elif isinstance(assets, list):
for asset in assets:
yield asset
@cached_property @cached_property
def buttons(self) -> t.List[Button]: def buttons(self) -> t.List[Button]:
# for trial in [server.lang, 'share', 'cn']: for trial in [server.lang, 'share', 'cn']:
for trial in [server.lang, 'share', 'jp']: try:
assets = self.data_buttons.get(trial, None) assets = self.data_buttons[trial]
if assets is not None:
if isinstance(assets, Button): if isinstance(assets, Button):
return [assets] return [assets]
elif isinstance(assets, list): elif isinstance(assets, list):
return assets return assets
except KeyError:
pass
raise ScriptError(f'ButtonWrapper({self}) on server {server.lang} has no fallback button') raise ScriptError(f'ButtonWrapper({self}) on server {server.lang} has no fallback button')
@@ -164,16 +201,45 @@ class ButtonWrapper(Resource):
return True return True
return False return False
def match_template(self, image, similarity=0.85) -> bool: def match_template(self, image, similarity=0.85, direct_match=False) -> bool:
for assets in self.buttons: for assets in self.buttons:
if assets.match_template(image, similarity=similarity): if assets.match_template(image, similarity=similarity, direct_match=direct_match):
self._matched_button = assets self._matched_button = assets
return True return True
return False return False
def match_template_color(self, image, similarity=0.85, threshold=30) -> bool: def match_multi_template(self, image, similarity=0.85, threshold=5, direct_match=False):
"""
Detects assets by template matching, return multiple results
Args:
image: Screenshot.
similarity (float): 0-1.
threshold:
direct_match: True to ignore `self.search`
Returns:
list[ClickButton]:
"""
ps = []
for assets in self.buttons: for assets in self.buttons:
if assets.match_template_color(image, similarity=similarity, threshold=threshold): ps += assets.match_multi_template(image, similarity=similarity, direct_match=direct_match)
if not ps:
return []
from module.base.utils.points import Points
ps = Points(ps).group(threshold=threshold)
area_list = [area_offset(self.area, p - self.area[:2]) for p in ps]
button_list = [area_offset(self.button, p - self.area[:2]) for p in ps]
return [
ClickButton(area=info[0], button=info[1], name=f'{self.name}_result{i}')
for i, info in enumerate(zip(area_list, button_list))
]
def match_template_color(self, image, similarity=0.85, threshold=30, direct_match=False) -> bool:
for assets in self.buttons:
if assets.match_template_color(
image, similarity=similarity, threshold=threshold, direct_match=direct_match):
self._matched_button = assets self._matched_button = assets
return True return True
return False return False
@@ -222,18 +288,32 @@ class ButtonWrapper(Resource):
""" """
if isinstance(button, ButtonWrapper): if isinstance(button, ButtonWrapper):
button = button.matched_button button = button.matched_button
for b in self.buttons: for b in self.iter_buttons():
b.load_offset(button) b.load_offset(button)
def clear_offset(self): def clear_offset(self):
for b in self.buttons: for b in self.iter_buttons():
b.clear_offset() b.clear_offset()
def load_search(self, area):
"""
Set `search` attribute.
Note that this method is irreversible.
Args:
area:
"""
for b in self.iter_buttons():
b.search = area
class ClickButton: class ClickButton:
def __init__(self, button, name='CLICK_BUTTON'): def __init__(self, area, button=None, name='CLICK_BUTTON'):
self.area = button self.area = area
self.button = button if button is None:
self.button = area
else:
self.button = button
self.name = name self.name = name
def __str__(self): def __str__(self):
@@ -265,4 +345,4 @@ def match_template(image, template, similarity=0.85):
""" """
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
_, sim, _, point = cv2.minMaxLoc(res) _, sim, _, point = cv2.minMaxLoc(res)
return sim > similarity return sim > similarity
+18 -13
View File
@@ -1,11 +1,11 @@
import re import re
import module.config.server as server from module.base.decorator import cached_property
from module.base.decorator import cached_property, del_cached_property
def get_assets_from_file(file, regex): def get_assets_from_file(file):
assets = set() assets = set()
regex = re.compile(r"file='(.*?)'")
with open(file, 'r', encoding='utf-8') as f: with open(file, 'r', encoding='utf-8') as f:
for row in f.readlines(): for row in f.readlines():
result = regex.search(row) result = regex.search(row)
@@ -20,11 +20,9 @@ class PreservedAssets:
assets = set() assets = set()
assets |= get_assets_from_file( assets |= get_assets_from_file(
file='./tasks/base/assets/assets_base_page.py', file='./tasks/base/assets/assets_base_page.py',
regex=re.compile(r'^([A-Za-z][A-Za-z0-9_]+) = ')
) )
assets |= get_assets_from_file( assets |= get_assets_from_file(
file='./tasks/base/assets/assets_base_popup.py', file='./tasks/base/assets/assets_base_popup.py',
regex=re.compile(r'^([A-Za-z][A-Za-z0-9_]+) = ')
) )
return assets return assets
@@ -44,11 +42,13 @@ class Resource:
@classmethod @classmethod
def is_loaded(cls, obj): def is_loaded(cls, obj):
if hasattr(obj, '_image') and obj._image is None: if hasattr(obj, '_image') and obj._image is not None:
return False return True
elif hasattr(obj, 'image') and obj.image is None: if hasattr(obj, 'image') and obj.image is not None:
return False return True
return True if hasattr(obj, 'buttons') and obj.buttons is not None:
return True
return False
@classmethod @classmethod
def resource_show(cls): def resource_show(cls):
@@ -56,11 +56,16 @@ class Resource:
logger.hr('Show resource') logger.hr('Show resource')
for key, obj in cls.instances.items(): for key, obj in cls.instances.items():
if cls.is_loaded(obj): if cls.is_loaded(obj):
continue logger.info(f'{obj}: {key}')
logger.info(f'{obj}: {key}')
def release_resources(next_task=''): def release_resources(next_task=''):
# Release all OCR models
# det models take 400MB
if not next_task:
from module.ocr.models import OCR_MODEL
OCR_MODEL.resource_release()
# Release assets cache # Release assets cache
# module.ui has about 80 assets and takes about 3MB # module.ui has about 80 assets and takes about 3MB
# Alas has about 800 assets, but they are not all loaded. # Alas has about 800 assets, but they are not all loaded.
@@ -74,4 +79,4 @@ def release_resources(next_task=''):
obj.resource_release() obj.resource_release()
# Useless in most cases, but just call it # Useless in most cases, but just call it
# gc.collect() # gc.collect()
+209
View File
@@ -257,6 +257,191 @@
} }
} }
}, },
"Bounty": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"value": true,
"option": [
true,
false
]
},
"NextRun": {
"type": "datetime",
"value": "2020-01-01 00:00:00",
"validate": "datetime"
},
"Command": {
"type": "input",
"value": "Bounty",
"display": "hide"
},
"ServerUpdate": {
"type": "input",
"value": "04:00",
"display": "hide"
}
},
"Bounty": {
"OnError": {
"type": "select",
"value": "skip",
"option": [
"stop",
"skip"
]
}
},
"Highway": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Count": {
"type": "input",
"value": 2
}
},
"DesertRailroad": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Count": {
"type": "input",
"value": 2
}
},
"Schoolhouse": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Count": {
"type": "input",
"value": 2
}
}
},
"Scrimmage": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"value": true,
"option": [
true,
false
]
},
"NextRun": {
"type": "datetime",
"value": "2020-01-01 00:00:00",
"validate": "datetime"
},
"Command": {
"type": "input",
"value": "Scrimmage",
"display": "hide"
},
"ServerUpdate": {
"type": "input",
"value": "04:00",
"display": "hide"
}
},
"Scrimmage": {
"OnError": {
"type": "select",
"value": "skip",
"option": [
"stop",
"skip"
]
}
},
"Trinity": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4
]
},
"Count": {
"type": "input",
"value": 2
}
},
"Gehenna": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4
]
},
"Count": {
"type": "input",
"value": 2
}
},
"Millennium": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4
]
},
"Count": {
"type": "input",
"value": 2
}
}
},
"TacticalChallenge": { "TacticalChallenge": {
"Scheduler": { "Scheduler": {
"Enable": { "Enable": {
@@ -348,6 +533,30 @@
"stored": "StoredInt", "stored": "StoredInt",
"order": 3, "order": 3,
"color": "#21befc" "color": "#21befc"
},
"BountyTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBountyTicket",
"order": 4,
"color": "#94cb44"
},
"ScrimmageTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredScrimmageTicket",
"order": 5,
"color": "#f86c6a"
},
"TacticalChallengeTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredTacticalChallengeTicket",
"order": 6,
"color": "#7ac8e5"
} }
} }
} }
+54
View File
@@ -81,6 +81,48 @@ Cafe:
AutoAdjust: true AutoAdjust: true
SecondCafe: true SecondCafe: true
Bounty:
OnError:
value: skip
option: [ stop, skip ]
Highway:
Stage:
value: 1
option: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Count: 2
DesertRailroad:
Stage:
value: 1
option: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Count: 2
Schoolhouse:
Stage:
value: 1
option: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Count: 2
Scrimmage:
OnError:
value: skip
option: [ stop, skip ]
Trinity:
Stage:
value: 1
option: [ 1, 2, 3, 4 ]
Count: 2
Gehenna:
Stage:
value: 1
option: [ 1, 2, 3, 4 ]
Count: 2
Millennium:
Stage:
value: 1
option: [ 1, 2, 3, 4 ]
Count: 2
TacticalChallenge: TacticalChallenge:
PlayerSelect: PlayerSelect:
value: 0 value: 0
@@ -99,3 +141,15 @@ ItemStorage:
stored: StoredInt stored: StoredInt
order: 3 order: 3
color: "#21befc" color: "#21befc"
BountyTicket:
stored: StoredBountyTicket
order: 4
color: "#94cb44"
ScrimmageTicket:
stored: StoredScrimmageTicket
order: 5
color: "#f86c6a"
TacticalChallengeTicket:
stored: StoredTacticalChallengeTicket
order: 6
color: "#7ac8e5"
+6
View File
@@ -13,6 +13,12 @@ Mail:
Circle: Circle:
Scheduler: Scheduler:
Enable: true Enable: true
Bounty:
Scheduler:
Enable: true
Scrimmage:
Scheduler:
Enable: true
TacticalChallenge: TacticalChallenge:
Scheduler: Scheduler:
Enable: true Enable: true
+2
View File
@@ -14,6 +14,8 @@
"Cafe", "Cafe",
"Mail", "Mail",
"Circle", "Circle",
"Bounty",
"Scrimmage",
"TacticalChallenge", "TacticalChallenge",
"DataUpdate" "DataUpdate"
] ]
+39
View File
@@ -35,5 +35,44 @@
}, },
"order": 3, "order": 3,
"color": "#21befc" "color": "#21befc"
},
"BountyTicket": {
"name": "BountyTicket",
"path": "DataUpdate.ItemStorage.BountyTicket",
"i18n": "ItemStorage.BountyTicket.name",
"stored": "StoredBountyTicket",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 6,
"value": 0
},
"order": 4,
"color": "#94cb44"
},
"ScrimmageTicket": {
"name": "ScrimmageTicket",
"path": "DataUpdate.ItemStorage.ScrimmageTicket",
"i18n": "ItemStorage.ScrimmageTicket.name",
"stored": "StoredScrimmageTicket",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 6,
"value": 0
},
"order": 5,
"color": "#f86c6a"
},
"TacticalChallengeTicket": {
"name": "TacticalChallengeTicket",
"path": "DataUpdate.ItemStorage.TacticalChallengeTicket",
"i18n": "ItemStorage.TacticalChallengeTicket.name",
"stored": "StoredTacticalChallengeTicket",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 5,
"value": 0
},
"order": 6,
"color": "#7ac8e5"
} }
} }
+12
View File
@@ -29,6 +29,18 @@ Daily:
- Scheduler - Scheduler
Circle: Circle:
- Scheduler - Scheduler
Bounty:
- Scheduler
- Bounty
- Highway
- DesertRailroad
- Schoolhouse
Scrimmage:
- Scheduler
- Scrimmage
- Trinity
- Gehenna
- Millennium
TacticalChallenge: TacticalChallenge:
- Scheduler - Scheduler
- TacticalChallenge - TacticalChallenge
-13
View File
@@ -499,19 +499,6 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher
self.task_call('DailyQuest') self.task_call('DailyQuest')
self.task_stop() self.task_stop()
def update_battle_pass_quests(self):
"""
Raises:
TaskEnd: Call task `BattlePass` and stop current task
"""
if self.stored.BattlePassTodayQuest.is_expired():
if self.stored.BattlePassLevel.is_full():
logger.info('BattlePassLevel full, no updates')
else:
logger.info('BattlePassTodayQuest expired, call task to update')
self.task_call('BattlePass')
self.task_stop()
@property @property
def DEVICE_SCREENSHOT_METHOD(self): def DEVICE_SCREENSHOT_METHOD(self):
return self.Emulator_ScreenshotMethod return self.Emulator_ScreenshotMethod
+33
View File
@@ -45,6 +45,36 @@ class GeneratedConfig:
Cafe_AutoAdjust = True Cafe_AutoAdjust = True
Cafe_SecondCafe = True Cafe_SecondCafe = True
# Group `Bounty`
Bounty_OnError = 'skip' # stop, skip
# Group `Highway`
Highway_Stage = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
Highway_Count = 2
# Group `DesertRailroad`
DesertRailroad_Stage = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
DesertRailroad_Count = 2
# Group `Schoolhouse`
Schoolhouse_Stage = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
Schoolhouse_Count = 2
# Group `Scrimmage`
Scrimmage_OnError = 'skip' # stop, skip
# Group `Trinity`
Trinity_Stage = 1 # 1, 2, 3, 4
Trinity_Count = 2
# Group `Gehenna`
Gehenna_Stage = 1 # 1, 2, 3, 4
Gehenna_Count = 2
# Group `Millennium`
Millennium_Stage = 1 # 1, 2, 3, 4
Millennium_Count = 2
# Group `TacticalChallenge` # Group `TacticalChallenge`
TacticalChallenge_PlayerSelect = 0 # 0, 1, 2, 3 TacticalChallenge_PlayerSelect = 0 # 0, 1, 2, 3
@@ -52,3 +82,6 @@ class GeneratedConfig:
ItemStorage_AP = {} ItemStorage_AP = {}
ItemStorage_Credit = {} ItemStorage_Credit = {}
ItemStorage_Pyroxene = {} ItemStorage_Pyroxene = {}
ItemStorage_BountyTicket = {}
ItemStorage_ScrimmageTicket = {}
ItemStorage_TacticalChallengeTicket = {}
+2 -1
View File
@@ -8,7 +8,8 @@ class ManualConfig:
SCHEDULER_PRIORITY = """ SCHEDULER_PRIORITY = """
Restart Restart
> Cafe > Circle > Mail > DataUpdate > TacticalChallenge > Cafe > Circle > Mail > DataUpdate > Bounty
> Scrimmage > TacticalChallenge
""" """
""" """
+2 -1
View File
@@ -458,7 +458,8 @@ class ConfigUpdater:
value = deep_get(old, keys=keys, default=data['value']) value = deep_get(old, keys=keys, default=data['value'])
typ = data['type'] typ = data['type']
display = data.get('display') display = data.get('display')
if is_template or value is None or value == '' or typ == 'lock' or (display == 'hide' and typ != 'stored'): if (is_template or value is None or value == ''
or typ in ['lock', 'state'] or (display == 'hide' and typ != 'stored')):
value = data['value'] value = data['value']
value = parse_value(value, data=data) value = parse_value(value, data=data)
deep_set(new, keys=keys, value=value) deep_set(new, keys=keys, value=value)
+169 -2
View File
@@ -30,6 +30,14 @@
"name": "Club", "name": "Club",
"help": "" "help": ""
}, },
"Bounty": {
"name": "Bounty",
"help": ""
},
"Scrimmage": {
"name": "Scrimmage",
"help": ""
},
"TacticalChallenge": { "TacticalChallenge": {
"name": "Tactical Challenge", "name": "Tactical Challenge",
"help": "" "help": ""
@@ -205,8 +213,155 @@
"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"
}
},
"Bounty": {
"_info": {
"name": "Bounty Settings",
"help": ""
},
"OnError": {
"name": "Error Handling",
"help": "Perform the selected action when an error occurs (ticket not enough or any invalid setting)",
"stop": "Stop script",
"skip": "Skip current task"
}
},
"Highway": {
"_info": {
"name": "Overpass",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Overpass A",
"2": "02 - Overpass B",
"3": "03 - Overpass C",
"4": "04 - Overpass D",
"5": "05 - Overpass E",
"6": "06 - Overpass F",
"7": "07 - Overpass G",
"8": "08 - Overpass H",
"9": "09 - Overpass I"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"DesertRailroad": {
"_info": {
"name": "Desert Railroad",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Abandoned Train A",
"2": "02 - Abandoned Train B",
"3": "03 - Abandoned Train C",
"4": "04 - Abandoned Train D",
"5": "05 - Abandoned Train E",
"6": "06 - Abandoned Train F",
"7": "07 - Abandoned Train G",
"8": "08 - Abandoned Train H",
"9": "09 - Abandoned Train I"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"Schoolhouse": {
"_info": {
"name": "Classroom",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Besieged Classroom A",
"2": "02 - Besieged Classroom B",
"3": "03 - Besieged Classroom C",
"4": "04 - Besieged Classroom D",
"5": "05 - Besieged Classroom E",
"6": "06 - Besieged Classroom F",
"7": "07 - Besieged Classroom G",
"8": "08 - Besieged Classroom H",
"9": "09 - Besieged Classroom I"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"Scrimmage": {
"_info": {
"name": "Scrimmage Settings",
"help": ""
},
"OnError": {
"name": "Error Handling",
"help": "Perform the selected action when an error occurs (ticket not enough or any invalid setting)",
"stop": "Stop script",
"skip": "Skip current task"
}
},
"Trinity": {
"_info": {
"name": "Trinity",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Trinity A",
"2": "02 - Trinity B",
"3": "03 - Trinity C",
"4": "04 - Trinity D"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"Gehenna": {
"_info": {
"name": "Gehenna",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Gehenna A",
"2": "02 - Gehenna B",
"3": "03 - Gehenna C",
"4": "04 - Gehenna D"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"Millennium": {
"_info": {
"name": "Millennium",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Millennium A",
"2": "02 - Millennium B",
"3": "03 - Millennium C",
"4": "04 - Millennium D"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
} }
}, },
"TacticalChallenge": { "TacticalChallenge": {
@@ -239,6 +394,18 @@
"Pyroxene": { "Pyroxene": {
"name": "Pyroxene", "name": "Pyroxene",
"help": "" "help": ""
},
"BountyTicket": {
"name": "Bounty",
"help": ""
},
"ScrimmageTicket": {
"name": "Scrimmage",
"help": ""
},
"TacticalChallengeTicket": {
"name": "Tactical Challenge",
"help": ""
} }
}, },
"Gui": { "Gui": {
+168 -1
View File
@@ -30,6 +30,14 @@
"name": "公会", "name": "公会",
"help": "社团 / 小组" "help": "社团 / 小组"
}, },
"Bounty": {
"name": "悬赏通缉",
"help": ""
},
"Scrimmage": {
"name": "学院交流会",
"help": ""
},
"TacticalChallenge": { "TacticalChallenge": {
"name": "战术对抗赛", "name": "战术对抗赛",
"help": "战术大赛 / 竞技场" "help": "战术大赛 / 竞技场"
@@ -206,7 +214,154 @@
}, },
"SecondCafe": { "SecondCafe": {
"name": "第二咖啡厅", "name": "第二咖啡厅",
"help": "自动切换第二咖啡厅进行互动点击" "help": "仅支持日服\n自动切换第二咖啡厅进行互动点击"
}
},
"Bounty": {
"_info": {
"name": "悬赏通缉设置",
"help": ""
},
"OnError": {
"name": "错误处理",
"help": "当出现票券不足或设置有误等错误时,执行该操作",
"stop": "停止脚本运行",
"skip": "跳过当前任务"
}
},
"Highway": {
"_info": {
"name": "高架公路",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 高架公路 A",
"2": "02 - 高架公路 B",
"3": "03 - 高架公路 C",
"4": "04 - 高架公路 D",
"5": "05 - 高架公路 E",
"6": "06 - 高架公路 F",
"7": "07 - 高架公路 G",
"8": "08 - 高架公路 H",
"9": "09 - 高架公路 I"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"DesertRailroad": {
"_info": {
"name": "沙漠铁路",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 被遗弃的列车 A",
"2": "02 - 被遗弃的列车 B",
"3": "03 - 被遗弃的列车 C",
"4": "04 - 被遗弃的列车 D",
"5": "05 - 被遗弃的列车 E",
"6": "06 - 被遗弃的列车 F",
"7": "07 - 被遗弃的列车 G",
"8": "08 - 被遗弃的列车 H",
"9": "09 - 被遗弃的列车 I"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"Schoolhouse": {
"_info": {
"name": "教室",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 被袭击的教室 A",
"2": "02 - 被袭击的教室 B",
"3": "03 - 被袭击的教室 C",
"4": "04 - 被袭击的教室 D",
"5": "05 - 被袭击的教室 E",
"6": "06 - 被袭击的教室 F",
"7": "07 - 被袭击的教室 G",
"8": "08 - 被袭击的教室 H",
"9": "09 - 被袭击的教室 I"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"Scrimmage": {
"_info": {
"name": "学院交流会设置",
"help": ""
},
"OnError": {
"name": "错误处理",
"help": "当出现票券不足或设置有误等错误时,执行该操作",
"stop": "停止脚本运行",
"skip": "跳过当前任务"
}
},
"Trinity": {
"_info": {
"name": "三一",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 三一 A",
"2": "02 - 三一 B",
"3": "03 - 三一 C",
"4": "04 - 三一 D"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"Gehenna": {
"_info": {
"name": "格黑娜",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 格黑娜 A",
"2": "02 - 格黑娜 B",
"3": "03 - 格黑娜 C",
"4": "04 - 格黑娜 D"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"Millennium": {
"_info": {
"name": "千年",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 千年 A",
"2": "02 - 千年 B",
"3": "03 - 千年 C",
"4": "04 - 千年 D"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
} }
}, },
"TacticalChallenge": { "TacticalChallenge": {
@@ -239,6 +394,18 @@
"Pyroxene": { "Pyroxene": {
"name": "青辉石", "name": "青辉石",
"help": "" "help": ""
},
"BountyTicket": {
"name": "悬赏通缉",
"help": ""
},
"ScrimmageTicket": {
"name": "学院交流会",
"help": ""
},
"TacticalChallengeTicket": {
"name": "战术对抗赛",
"help": ""
} }
}, },
"Gui": { "Gui": {
+16
View File
@@ -3,6 +3,8 @@ from functools import cached_property as functools_cached_property
from module.base.decorator import cached_property from module.base.decorator import cached_property
from module.config.utils import DEFAULT_TIME, deep_get, get_server_last_monday_update, get_server_last_update from module.config.utils import DEFAULT_TIME, deep_get, get_server_last_monday_update, get_server_last_update
# from module.exception import ScriptError # from module.exception import ScriptError
@@ -173,6 +175,20 @@ class StoredCounter(StoredBase):
class StoredAP(StoredCounter): class StoredAP(StoredCounter):
pass pass
class StoredBountyTicket(StoredCounter, StoredExpiredAt0400):
FIXED_TOTAL = 6
pass
class StoredScrimmageTicket(StoredCounter, StoredExpiredAt0400):
FIXED_TOTAL = 6
class StoredTacticalChallengeTicket(StoredCounter, StoredExpiredAt0400):
FIXED_TOTAL = 5
# class StoredDailyActivity(StoredCounter, StoredExpiredAt0400): # class StoredDailyActivity(StoredCounter, StoredExpiredAt0400):
# FIXED_TOTAL = 500 # FIXED_TOTAL = 500
# #
+6 -4
View File
@@ -1,10 +1,9 @@
from module.config.stored.classes import ( from module.config.stored.classes import (
StoredAP, StoredAP,
StoredBase, StoredBountyTicket,
StoredCounter,
StoredExpiredAt0400,
StoredExpiredAtMonday0400,
StoredInt, StoredInt,
StoredScrimmageTicket,
StoredTacticalChallengeTicket,
) )
@@ -15,3 +14,6 @@ class StoredGenerated:
AP = StoredAP("DataUpdate.ItemStorage.AP") AP = StoredAP("DataUpdate.ItemStorage.AP")
Credit = StoredInt("DataUpdate.ItemStorage.Credit") Credit = StoredInt("DataUpdate.ItemStorage.Credit")
Pyroxene = StoredInt("DataUpdate.ItemStorage.Pyroxene") Pyroxene = StoredInt("DataUpdate.ItemStorage.Pyroxene")
BountyTicket = StoredBountyTicket("DataUpdate.ItemStorage.BountyTicket")
ScrimmageTicket = StoredScrimmageTicket("DataUpdate.ItemStorage.ScrimmageTicket")
TacticalChallengeTicket = StoredTacticalChallengeTicket("DataUpdate.ItemStorage.TacticalChallengeTicket")
+3 -3
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
+7 -1
View File
@@ -1,6 +1,6 @@
from pponnxcr import TextSystem as TextSystem_ from pponnxcr import TextSystem as TextSystem_
from module.base.decorator import cached_property from module.base.decorator import cached_property, del_cached_property
from module.exception import ScriptError from module.exception import ScriptError
DIC_LANG_TO_MODEL = { DIC_LANG_TO_MODEL = {
@@ -56,6 +56,12 @@ class OcrModel:
except AttributeError: except AttributeError:
raise ScriptError(f'OCR model under lang "{lang}" does not exists') raise ScriptError(f'OCR model under lang "{lang}" does not exists')
def resource_release(self):
del_cached_property(self, 'zhs')
del_cached_property(self, 'en')
del_cached_property(self, 'ja')
del_cached_property(self, 'zht')
@cached_property @cached_property
def zhs(self): def zhs(self):
return TextSystem('zhs') return TextSystem('zhs')
+6 -3
View File
@@ -89,12 +89,15 @@ def readable_time(before: str) -> str:
elif diff < 60: elif diff < 60:
# < 1 min # < 1 min
return t("Gui.Dashboard.JustNow") return t("Gui.Dashboard.JustNow")
elif diff < 3600: elif diff < 5400:
# < 90 min
return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60)) return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60))
elif diff < 86400: elif diff < 129600:
# < 36 hours
return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600)) return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600))
elif diff < 1296000: elif diff < 1296000:
# < 15 days
return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400)) return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400))
else: else:
# > 15 days # >= 15 days
return t("Gui.Dashboard.LongTimeAgo") return t("Gui.Dashboard.LongTimeAgo")
+9 -5
View File
@@ -9,17 +9,17 @@ from queue import Queue
from typing import Callable, Generator, List from typing import Callable, Generator, List
import pywebio import pywebio
from module.config.utils import deep_iter
from module.logger import logger
from module.webui.setting import State
from pywebio.input import PASSWORD, input from pywebio.input import PASSWORD, input
from pywebio.output import PopupSize, popup, put_html, toast from pywebio.output import PopupSize, popup, put_html, toast
from pywebio.session import eval_js from pywebio.session import eval_js
from pywebio.session import info as session_info from pywebio.session import info as session_info
from pywebio.session import register_thread, run_js from pywebio.session import register_thread, run_js
from rich.console import Console, ConsoleOptions from rich.console import Console
from rich.terminal_theme import TerminalTheme from rich.terminal_theme import TerminalTheme
from module.config.utils import deep_iter
from module.logger import logger
from module.webui.setting import State
RE_DATETIME = ( RE_DATETIME = (
r"\d{4}\-(0\d|1[0-2])\-([0-2]\d|[3][0-1]) " r"\d{4}\-(0\d|1[0-2])\-([0-2]\d|[3][0-1]) "
@@ -455,7 +455,11 @@ def get_localstorage(key):
def re_fullmatch(pattern, string): def re_fullmatch(pattern, string):
if pattern == "datetime": if pattern == "datetime":
pattern = RE_DATETIME try:
datetime.datetime.fromisoformat(string)
return True
except ValueError:
return False
# elif: # elif:
return re.fullmatch(pattern=pattern, string=string) return re.fullmatch(pattern=pattern, string=string)
+11 -5
View File
@@ -24,10 +24,10 @@ AP_EXCEED = ButtonWrapper(
name='AP_EXCEED', name='AP_EXCEED',
jp=Button( jp=Button(
file='./assets/jp/base/popup/AP_EXCEED.png', file='./assets/jp/base/popup/AP_EXCEED.png',
area=(610, 124, 669, 153), area=(611, 147, 669, 177),
search=(590, 104, 689, 173), search=(591, 127, 689, 197),
color=(139, 153, 168), color=(143, 156, 170),
button=(535, 494, 746, 565), button=(539, 470, 743, 533),
), ),
en=Button( en=Button(
file='./assets/en/base/popup/AP_EXCEED.png', file='./assets/en/base/popup/AP_EXCEED.png',
@@ -124,7 +124,13 @@ GET_REWARD_SKIP = ButtonWrapper(
) )
INSUFFICIENT_INVENTORY = ButtonWrapper( INSUFFICIENT_INVENTORY = ButtonWrapper(
name='INSUFFICIENT_INVENTORY', name='INSUFFICIENT_INVENTORY',
jp=None, jp=Button(
file='./assets/jp/base/popup/INSUFFICIENT_INVENTORY.png',
area=(610, 124, 669, 153),
search=(590, 104, 689, 173),
color=(139, 153, 168),
button=(535, 494, 746, 565),
),
en=Button( en=Button(
file='./assets/en/base/popup/INSUFFICIENT_INVENTORY.png', file='./assets/en/base/popup/INSUFFICIENT_INVENTORY.png',
area=(578, 117, 703, 152), area=(578, 117, 703, 152),
+6
View File
@@ -94,6 +94,12 @@ class PopupHandler(ModuleBase):
return False return False
def handle_insufficient_inventory(self, interval=5) -> bool:
if self.appear_then_click(INSUFFICIENT_INVENTORY, interval=interval):
return True
return False
def handle_item_expired(self, interval=5) -> bool: def handle_item_expired(self, interval=5) -> bool:
if self.appear_then_click(ITEM_EXPIRED, interval=interval): if self.appear_then_click(ITEM_EXPIRED, interval=interval):
return True return True
+2
View File
@@ -345,6 +345,8 @@ class UI(MainPage):
return True return True
if self.handle_ap_exceed(): if self.handle_ap_exceed():
return True return True
if self.handle_insufficient_inventory():
return True
if self.handle_item_expired(): if self.handle_item_expired():
return True return True
+93
View File
@@ -0,0 +1,93 @@
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_BOUNTY = ButtonWrapper(
name='CHECK_BOUNTY',
jp=Button(
file='./assets/jp/bounty/CHECK_BOUNTY.png',
area=(654, 87, 889, 129),
search=(634, 67, 909, 149),
color=(88, 113, 139),
button=(654, 87, 889, 129),
),
en=None,
)
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,
)
OCR_TICKET = ButtonWrapper(
name='OCR_TICKET',
jp=Button(
file='./assets/jp/bounty/OCR_TICKET.png',
area=(195, 85, 237, 113),
search=(175, 65, 257, 133),
color=(197, 206, 213),
button=(195, 85, 237, 113),
),
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,
)
+119
View File
@@ -0,0 +1,119 @@
from enum import Flag
from module.base.timer import Timer
from module.exception import RequestHumanTakeover
from module.logger import logger
from tasks.base.assets.assets_base_page import BACK
from tasks.base.page import page_bounty
from tasks.bounty.assets.assets_bounty import *
from tasks.bounty.ui import BountyUI
class BountyStatus(Flag):
OCR = 0
SELECT = 1
ENTER = 2
SWEEP = 3
END = 4
FINISH = 5
class Bounty(BountyUI):
@property
def bounty_info(self):
bounty = (SELECT_HIGHWAY, SELECT_DESERT_RAILROAD, SELECT_SCHOOLHOUSE)
check = (CHECK_HIGHWAY, CHECK_DESERT_RAILROAD, CHECK_SCHOOLHOUSE)
stage = (self.config.Highway_Stage, self.config.DesertRailroad_Stage, self.config.Schoolhouse_Stage)
count = (self.config.Highway_Count, self.config.DesertRailroad_Count, self.config.Schoolhouse_Count)
info = zip(bounty, check, stage, count)
return filter(lambda x: x[3] > 0, info)
@property
def valid_task(self) -> list:
task = list(self.bounty_info)
if not task:
logger.warning('Bounty enabled but no task set')
self.error_handler()
return task
def error_handler(self):
action = self.config.Bounty_OnError
if action == 'stop':
raise RequestHumanTakeover
elif action == 'skip':
self.config.task_delay(server_update=True)
self.config.task_stop()
@property
def is_ticket_enough(self) -> bool:
return self.current_ticket >= self.current_count
@property
def current_bounty(self):
return self.task[0][:2]
@property
def current_stage(self):
return self.task[0][2]
@property
def current_count(self):
return self.task[0][3]
@property
def current_ticket(self):
return self.config.stored.BountyTicket.value
def handle_bounty(self, status):
match status:
case BountyStatus.OCR:
if self.get_ticket():
if self.current_ticket == 0 or not self.task:
return BountyStatus.FINISH
return BountyStatus.SELECT
case BountyStatus.SELECT:
if not self.is_ticket_enough:
logger.warning('Bounty ticket not enough')
self.error_handler()
if self.select_bounty(*self.current_bounty):
return BountyStatus.ENTER
case BountyStatus.ENTER:
if self.enter_stage(self.current_stage):
return BountyStatus.SWEEP
else:
self.error_handler()
case BountyStatus.SWEEP:
if self.do_sweep(self.current_count):
self.task.pop(0)
return BountyStatus.END
return BountyStatus.ENTER
case BountyStatus.END:
if self.appear(CHECK_BOUNTY):
return BountyStatus.OCR
self.click_with_interval(BACK, interval=2)
case BountyStatus.FINISH:
return status
case _:
logger.warning(f'Invalid status: {status}')
return status
def run(self):
self.ui_ensure(page_bounty)
self.task = self.valid_task
action_timer = Timer(0.5, 1)
status = BountyStatus.OCR
while 1:
self.device.screenshot()
if self.ui_additional():
continue
if action_timer.reached_and_reset():
logger.attr('Status', status)
status = self.handle_bounty(status)
if status == BountyStatus.FINISH:
break
self.config.task_delay(server_update=True)
+48
View File
@@ -0,0 +1,48 @@
from module.base.timer import Timer
from module.logger import logger
from module.ocr.ocr import DigitCounter
from tasks.base.ui import UI
from tasks.bounty.assets.assets_bounty import *
from tasks.stage.list import StageList
from tasks.stage.sweep import StageSweep
BOUNTY_LIST = StageList('BountyList')
BOUNTY_SWEEP = StageSweep('BountySweep', 6)
class BountyUI(UI):
def select_bounty(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper):
timer = Timer(5, 10).start()
while 1:
self.device.screenshot()
self.appear_then_click(dest_enter, interval=1)
if self.appear(dest_check):
return True
if timer.reached():
return False
def enter_stage(self, index: int) -> bool:
if BOUNTY_LIST.select_index_enter(self, index):
return True
return False
def do_sweep(self, num: int) -> bool:
if BOUNTY_SWEEP.do_sweep(self, num=num):
return True
return False
def get_ticket(self):
"""
Page:
in: page_bounty
"""
if not self.appear(CHECK_BOUNTY):
logger.warning('OCR failed due to invalid page')
return False
ticket, _, total = DigitCounter(OCR_TICKET).ocr_single_line(self.device.image)
if total == 0:
logger.warning('Invalid ticket')
return False
logger.attr('BountyTicket', ticket)
self.config.stored.BountyTicket.set(ticket)
return True
+23 -10
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)
+14 -1
View File
@@ -1,9 +1,10 @@
from module.base.timer import Timer from module.base.timer import Timer
from module.base.decorator import Config
from module.exception import GameNotRunningError from module.exception import GameNotRunningError
from module.logger import logger from module.logger import logger
from tasks.base.page import page_main from tasks.base.page import page_main
from tasks.base.ui import UI from tasks.base.ui import UI
from tasks.login.assets.assets_login import LOGIN_CONFIRM, LOGIN_LOADING, UPDATE from tasks.login.assets.assets_login import LOGIN_CONFIRM, LOGIN_LOADING, UPDATE, SURVEY
class Login(UI): class Login(UI):
@@ -29,6 +30,16 @@ class Login(UI):
return True return True
return False return False
@Config.when(Emulator_GameLanguage='en')
def _handle_survey(self):
if self.appear_then_click(SURVEY):
return True
return False
@Config.when(Emulator_GameLanguage=None)
def _handle_survey(self):
pass
logger.hr('App login') logger.hr('App login')
orientation_timer = Timer(5) orientation_timer = Timer(5)
startup_timer = Timer(5).start() startup_timer = Timer(5).start()
@@ -78,6 +89,8 @@ class Login(UI):
# continue # continue
if self.appear_then_click(UPDATE): if self.appear_then_click(UPDATE):
continue continue
if _handle_survey(self):
continue
if self.ui_additional(): if self.ui_additional():
continue continue
@@ -0,0 +1,93 @@
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/scrimmage/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/scrimmage/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_SCRIMMAGE = ButtonWrapper(
name='CHECK_SCRIMMAGE',
jp=Button(
file='./assets/jp/scrimmage/CHECK_SCRIMMAGE.png',
area=(651, 90, 889, 130),
search=(631, 70, 909, 150),
color=(96, 119, 144),
button=(651, 90, 889, 130),
),
en=None,
)
CHECK_TRINITY = ButtonWrapper(
name='CHECK_TRINITY',
jp=Button(
file='./assets/jp/scrimmage/CHECK_TRINITY.png',
area=(115, 148, 250, 178),
search=(95, 128, 270, 198),
color=(204, 208, 210),
button=(115, 148, 250, 178),
),
en=None,
)
OCR_TICKET = ButtonWrapper(
name='OCR_TICKET',
jp=Button(
file='./assets/jp/scrimmage/OCR_TICKET.png',
area=(195, 85, 235, 113),
search=(175, 65, 255, 133),
color=(206, 211, 215),
button=(195, 85, 235, 113),
),
en=None,
)
SELECT_GEHENNA = ButtonWrapper(
name='SELECT_GEHENNA',
jp=Button(
file='./assets/jp/scrimmage/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/scrimmage/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/scrimmage/SELECT_TRINITY.png',
area=(1074, 165, 1221, 204),
search=(1054, 145, 1241, 224),
color=(213, 221, 228),
button=(1074, 165, 1221, 204),
),
en=None,
)
+144
View File
@@ -0,0 +1,144 @@
from enum import Enum
from module.base.timer import Timer
from module.exception import RequestHumanTakeover
from module.logger import logger
from tasks.base.assets.assets_base_page import BACK
from tasks.base.page import page_school_exchange
from tasks.scrimmage.assets.assets_scrimmage import *
from tasks.scrimmage.ui import ScrimmageUI
class ScrimmageStatus(Enum):
OCR = 0
SELECT = 1
ENTER = 2
SWEEP = 3
END = 4
FINISH = 5
class Scrimmage(ScrimmageUI):
@property
def scrimmage_info(self):
bounty = (SELECT_TRINITY, SELECT_GEHENNA, SELECT_MILLENNIUM)
check = (CHECK_TRINITY, CHECK_GEHENNA, CHECK_MILLENNIUM)
stage = (self.config.Trinity_Stage, self.config.Gehenna_Stage, self.config.Millennium_Stage)
count = (self.config.Trinity_Count, self.config.Gehenna_Count, self.config.Millennium_Count)
ap = (10 if stage == 1 else 15 for stage in stage)
info = zip(bounty, check, stage, count, ap)
return filter(lambda x: x[3] > 0, info)
@property
def valid_task(self) -> list:
task = list(self.scrimmage_info)
if not task:
logger.warning('Scrimmage enabled but no task set')
self.error_handler()
return task
def error_handler(self):
action = self.config.Bounty_OnError
if action == 'stop':
raise RequestHumanTakeover
elif action == 'skip':
self.config.task_delay(server_update=True)
self.config.task_stop()
@property
def is_ticket_enough(self) -> bool:
return self.current_ticket >= self.current_count
@property
def is_ap_enough(self) -> bool:
return self.current_ap >= self.current_task_ap
@property
def current_scrimmage(self):
return self.task[0][:2]
@property
def current_stage(self):
return self.task[0][2]
@property
def current_count(self):
return self.task[0][3]
@property
def current_task_ap(self):
return self.task[0][4]
@property
def current_ticket(self):
return self.config.stored.ScrimmageTicket.value
@property
def current_ap(self):
return self.config.stored.AP.value
def update_ap(self):
ap = self.config.stored.AP
ap_old = ap.value
ap_new = ap_old - self.current_task_ap
ap.set(ap_new, ap.total)
logger.info(f'Set AP: {ap_old} -> {ap_new}')
def handle_scrimmage(self, status):
match status:
case ScrimmageStatus.OCR:
if self.get_ticket():
if self.current_ticket == 0 or not self.task:
return ScrimmageStatus.FINISH
return ScrimmageStatus.SELECT
case ScrimmageStatus.SELECT:
if not self.is_ticket_enough:
logger.warning('Scrimmage ticket not enough')
self.error_handler()
if not self.is_ap_enough:
logger.warning('AP not enough')
self.error_handler()
if self.select_scrimmage(*self.current_scrimmage):
return ScrimmageStatus.ENTER
case ScrimmageStatus.ENTER:
if self.enter_stage(self.current_stage):
return ScrimmageStatus.SWEEP
else:
self.error_handler()
case ScrimmageStatus.SWEEP:
if self.do_sweep(self.current_count):
self.update_ap()
self.task.pop(0)
return ScrimmageStatus.END
return ScrimmageStatus.ENTER
case ScrimmageStatus.END:
if self.appear(CHECK_SCRIMMAGE):
return ScrimmageStatus.OCR
self.click_with_interval(BACK, interval=2)
case ScrimmageStatus.FINISH:
return status
case _:
logger.warning(f'Invalid status: {status}')
return status
def run(self):
self.ui_ensure(page_school_exchange)
self.task = self.valid_task
action_timer = Timer(0.5, 1)
status = ScrimmageStatus.OCR
while 1:
self.device.screenshot()
if self.ui_additional():
continue
if action_timer.reached_and_reset():
logger.attr('Status', status)
status = self.handle_scrimmage(status)
if status == ScrimmageStatus.FINISH:
break
self.config.task_delay(server_update=True)
self.config.task_call('DataUpdate')
+48
View File
@@ -0,0 +1,48 @@
from module.base.timer import Timer
from module.logger import logger
from module.ocr.ocr import DigitCounter
from tasks.base.ui import UI
from tasks.scrimmage.assets.assets_scrimmage import *
from tasks.stage.list import StageList
from tasks.stage.sweep import StageSweep
SCRIMMAGE_LIST = StageList('ScrimmageList')
SCRIMMAGE_SWEEP = StageSweep('ScrimmageSweep', 6)
class ScrimmageUI(UI):
def select_scrimmage(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper):
timer = Timer(5, 10).start()
while 1:
self.device.screenshot()
self.appear_then_click(dest_enter, interval=1)
if self.appear(dest_check):
return True
if timer.reached():
return False
def enter_stage(self, index: int) -> bool:
if SCRIMMAGE_LIST.select_index_enter(self, index, insight=False):
return True
return False
def do_sweep(self, num: int) -> bool:
if SCRIMMAGE_SWEEP.do_sweep(self, num=num):
return True
return False
def get_ticket(self):
"""
Page:
in: page_bounty
"""
if not self.appear(CHECK_SCRIMMAGE):
logger.warning('OCR failed due to invalid page')
return False
ticket, _, total = DigitCounter(OCR_TICKET).ocr_single_line(self.device.image)
if total == 0:
logger.warning('Invalid ticket')
return False
logger.attr('ScrimmageTicket', ticket)
self.config.stored.ScrimmageTicket.set(ticket)
return True
+60
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,
)
+148
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,
)
+45 -20
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,49 +133,66 @@ 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,
main: ModuleBase, main: ModuleBase,
index: int,
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 = 1.5
) -> bool: ) -> bool:
# insight index, if failed, return False
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
logger.info(f'Select index: {index}') logger.info(f'Select index: {index}')
click_interval = Timer(interval) click_interval = Timer(interval)
load_index_interval = Timer(1) load_index_interval = Timer(1)
timeout = Timer(15, 10).start()
while 1: while 1:
if skip_first_screenshot: if skip_first_screenshot:
skip_first_screenshot = False skip_first_screenshot = False
else: else:
main.device.screenshot() main.device.screenshot()
if load_index_interval.reached_and_reset(): # load index if not insight
if load_index_interval.reached_and_reset() and not insight:
self.load_stage_indexes(main=main) self.load_stage_indexes(main=main)
# find box of index
index_box = next(filter(lambda x: int(x.ocr_text) == index, self.current_indexes), None) index_box = next(filter(lambda x: int(x.ocr_text) == index, self.current_indexes), None)
if index_box is None: if index_box is None:
logger.warning(f'No index {index} in {self.index_ocr.name}') logger.warning(f'No index {index} in {self.index_ocr.name}')
return False continue
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:
logger.warning(f'No clickable {self.enter.name}') logger.warning(f'No clickable {self.enter.name}')
return False continue
point = area_offset((0, 0, *area_size(self.enter.button)), points[0]) point = area_offset((0, 0, *area_size(self.enter.button)), points[0])
click_button = ClickButton(area_offset(point, search_box[:2]), name=self.enter.name) click_button = ClickButton(area_offset(point, search_box[:2]), name=self.enter.name)
@@ -179,3 +200,7 @@ class StageList:
if click_interval.reached_and_reset(): if click_interval.reached_and_reset():
main.device.click(click_button) main.device.click(click_button)
return True return True
if timeout.reached():
logger.warning(f'{self.enter.name} failed')
return False
+263
View File
@@ -0,0 +1,263 @@
from enum import Enum
from module.base.base import ModuleBase
from module.base.timer import Timer
from module.logger import logger
from module.ocr.ocr import Digit
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,
max_sweep: int,
):
self.name = name
self.sweep_num = None
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
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, num: int = None) -> bool:
if num is not None:
self.sweep_num = num
match 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: {num}')
return True
if mode is not None:
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}')
return True
logger.warning(f'Invalid sweep setting')
return False
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):
timer = Timer(0.5, 2).start()
while 1:
main.device.screenshot()
if not timer.reached_and_reset():
continue
ocr_result = list(filter(lambda x: x.ocr_text.isdigit(), 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, mode: str = None, num: int = None, skip_first_screenshot=True) -> bool:
if not self.set_mode(mode, num):
return False
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
+15 -63
View File
@@ -3,15 +3,9 @@ from enum import Enum
from module.base.timer import Timer from module.base.timer import Timer
from module.logger import logger from module.logger import logger
from module.ocr.ocr import DigitCounter
from module.ui.switch import Switch
from tasks.base.page import page_tactical_challenge from tasks.base.page import page_tactical_challenge
from tasks.base.ui import UI
from tasks.tactical_challenge.assets.assets_tactical_challenge import * from tasks.tactical_challenge.assets.assets_tactical_challenge import *
from tasks.tactical_challenge.ui import TacticalChallengeUI
SWITCH_SKIP = Switch('Skip_switch')
SWITCH_SKIP.add_state('on', SKIP_ON)
SWITCH_SKIP.add_state('off', SKIP_OFF)
class TCStatus(Enum): class TCStatus(Enum):
@@ -31,50 +25,12 @@ class TCStatus(Enum):
FINISHED = -1 FINISHED = -1
class TacticalChallenge(UI): class TacticalChallenge(TacticalChallengeUI):
select_players = (PLAYER_SELECT_FIRST, PLAYER_SELECT_SECOND, PLAYER_SELECT_THIRD) select_players = (PLAYER_SELECT_FIRST, PLAYER_SELECT_SECOND, PLAYER_SELECT_THIRD)
def _get_ticket(self): @property
""" def current_ticket(self):
Page: return self.config.stored.TacticalChallengeTicket.value
in: page_tactical_challenge
"""
ocr = DigitCounter(OCR_TICKET).ocr_single_line(self.device.image)
# number of tickets remaining
ticket, _, total = ocr
if total == 0:
logger.warning('Invalid ticket')
return False, 5
logger.attr('Ticket', ticket)
return True, ticket
def _get_reward(self):
if self.match_color(GET_REWARD_DAILY):
self.device.click(GET_REWARD_DAILY)
logger.info('Get tc daily reward')
return True
if self.match_color(GET_REWARD_CREDIT):
self.device.click(GET_REWARD_CREDIT)
logger.info('Get tc credit reward')
return True
if self.match_color(GOT_REWARD_DAILY) and self.match_color(GOT_REWARD_CREDIT):
logger.info('Both tc reward got')
return True
return False
def _set_skip(self):
"""
Set skip switch to on
:returns: True if switch is set, False if switch not found
"""
if not SWITCH_SKIP.appear(main=self):
logger.info('Skip switch not found')
return False
SWITCH_SKIP.set('on', main=self)
return True
def _player_select(self, select): def _player_select(self, select):
if select: if select:
@@ -85,15 +41,13 @@ class TacticalChallenge(UI):
def _handle_challenge(self, status): def _handle_challenge(self, status):
match status: match status:
case TCStatus.REWARD: case TCStatus.REWARD:
if self._get_reward(): if self.get_reward():
return TCStatus.OCR return TCStatus.OCR
case TCStatus.OCR: case TCStatus.OCR:
is_valid, ticket = self._get_ticket() if self.get_ticket():
if not is_valid: if self.current_ticket == 0:
return status return TCStatus.FINISHED
if ticket == 0: return TCStatus.SELECT
return TCStatus.FINISHED
return TCStatus.SELECT
case TCStatus.SELECT: case TCStatus.SELECT:
self.appear_then_click(self.select) self.appear_then_click(self.select)
if self.appear(PREPARE_CHALLENGE): if self.appear(PREPARE_CHALLENGE):
@@ -103,7 +57,7 @@ class TacticalChallenge(UI):
if not self.appear(PREPARE_CHALLENGE): if not self.appear(PREPARE_CHALLENGE):
return TCStatus.SKIP return TCStatus.SKIP
case TCStatus.SKIP: case TCStatus.SKIP:
if not self._set_skip(): if not self.set_skip():
return TCStatus.SKIP return TCStatus.SKIP
return TCStatus.START return TCStatus.START
case TCStatus.START: case TCStatus.START:
@@ -118,12 +72,10 @@ class TacticalChallenge(UI):
case TCStatus.WIN | TCStatus.LOSE: case TCStatus.WIN | TCStatus.LOSE:
if self.appear_then_click(CHALLENGE_WIN) or self.appear_then_click(CHALLENGE_LOSE): if self.appear_then_click(CHALLENGE_WIN) or self.appear_then_click(CHALLENGE_LOSE):
return status return status
is_valid, ticket = self._get_ticket() if self.get_ticket():
if not is_valid: if self.current_ticket == 0:
return status return TCStatus.FINISHED
if ticket == 0: return TCStatus.FINAL
return TCStatus.FINISHED
return TCStatus.FINAL
case TCStatus.FINAL | TCStatus.FINISHED: case TCStatus.FINAL | TCStatus.FINISHED:
return status return status
case _: case _:
+54
View File
@@ -0,0 +1,54 @@
from module.base.timer import Timer
from module.logger import logger
from module.ocr.ocr import DigitCounter
from module.ui.switch import Switch
from tasks.base.ui import UI
from tasks.tactical_challenge.assets.assets_tactical_challenge import *
SWITCH_SKIP = Switch('Skip_switch')
SWITCH_SKIP.add_state('on', SKIP_ON)
SWITCH_SKIP.add_state('off', SKIP_OFF)
class TacticalChallengeUI(UI):
def get_ticket(self):
"""
Page:
in: page_tactical_challenge
"""
ticket, _, total = DigitCounter(OCR_TICKET).ocr_single_line(self.device.image)
if total == 0:
logger.warning('Invalid ticket')
return False
logger.attr('TacticalChallengeTicket', ticket)
self.config.stored.TacticalChallengeTicket.set(ticket)
return True
def get_reward(self):
timer = Timer(10, 10).start()
while 1:
self.device.screenshot()
self.ui_additional()
if self.match_color(GOT_REWARD_DAILY) and self.match_color(GOT_REWARD_CREDIT):
return True
if self.match_color(GET_REWARD_DAILY):
self.device.click(GET_REWARD_DAILY)
logger.info('Get daily reward')
if self.match_color(GET_REWARD_CREDIT):
self.device.click(GET_REWARD_CREDIT)
logger.info('Get credit reward')
if timer.reached():
return False
def set_skip(self):
"""
Set skip switch to on
Returns:
True if switch is set, False if switch not found
"""
if not SWITCH_SKIP.appear(main=self):
logger.info('Skip switch not found')
return False
SWITCH_SKIP.set('on', main=self)
return True