1
0
mirror of https://github.com/TheFunny/ArisuAutoSweeper synced 2026-06-29 15:04:56 +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:
- [x] **Cafe** Claim rewards / Interact / Second cafe
- [x] **Circle** Claim AP
- [x] **Cafe** Claim rewards / Interact / Second floor
- [x] **Club** Claim AP
- [x] **Mailbox** Claim rewards
- [x] **Bounty** Auto sweep
- [x] **Scrimmage** Auto sweep
- [x] **Tactical Challenge** Claim rewards / Auto battle
_Currently only supports JP server._
Supported servers:
- [x] JP
- [x] OVERSEA - Global
## 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)
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] 国际服 - 全球
## 已知问题
若愿意提供其他服务器支持,请开 PR 或 Issue。
- **大小月卡**:未实现自动领取,~~因为没买过~~,可能不影响使用。愿意提供图片的请开 Issue
- **月卡的额外悬赏券和学院交流券**:不太清楚月卡领取额外券的机制,~~因为没买过~~,可能影响相关任务使用券和体力的计算。愿意提供相关信息的请开
Issue
## 相关项目
@@ -36,3 +49,5 @@ _目前仅支持日服。_
感谢 [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
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):
from tasks.tactical_challenge.tactical_challenge import TacticalChallenge
TacticalChallenge(config=self.config, device=self.device).run()
+1 -1
View File
@@ -285,7 +285,7 @@ pre.rich-traceback-code {
*[style*="--header-icon--"] {
margin: .25rem auto .25rem;
border-radius: 1.5rem;
height: 3.5em;
height: 3em;
}
*[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"
}
},
"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": {
"Scheduler": {
"Enable": true,
@@ -84,7 +130,10 @@
"ItemStorage": {
"AP": {},
"Credit": {},
"Pyroxene": {}
"Pyroxene": {},
"BountyTicket": {},
"ScrimmageTicket": {},
"TacticalChallengeTicket": {}
}
}
}
+16 -7
View File
@@ -179,20 +179,29 @@ def iter_assets():
if image.attr != '':
row = deep_get(data, keys=[image.module, image.assets, image.server, image.frame])
row.load_image(image)
# Apply `search` of the first frame to all
# Set `search`
for path, frames in deep_iter(data, depth=3):
print(path, frames)
# If `search` attribute is set in the first frame, apply to all
first = frames[1]
search = first.search if first.search else DataAssets.area_to_search(first.area)
for frame in frames.values():
frame.search = search
if first.search:
for frame in frames.values():
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
def generate_code():
all = iter_assets()
for module, module_data in all.items():
all_assets = iter_assets()
for module, module_data in all_assets.items():
path = os.path.join(AzurLaneConfig.ASSETS_MODULE, module.split('/', maxsplit=1)[0])
output = os.path.join(path, 'assets.py')
if os.path.exists(output):
@@ -204,7 +213,7 @@ def generate_code():
continue
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])
output = os.path.join(path, 'assets')
gen = CodeGenerator()
+100 -20
View File
@@ -74,7 +74,7 @@ class Button(Resource):
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.
@@ -83,18 +83,45 @@ class Button(Resource):
Args:
image: Screenshot.
similarity (float): 0-1.
direct_match: True to ignore `self.search`
Returns:
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)
_, sim, _, point = cv2.minMaxLoc(res)
self._button_offset = np.array(point) + self.search[:2] - self.area[:2]
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
@@ -102,11 +129,12 @@ class Button(Resource):
image: Screenshot.
similarity (float): 0-1.
threshold (int): Default to 10.
direct_match: True to ignore `self.search`
Returns:
bool.
"""
matched = self.match_template(image, similarity=similarity)
matched = self.match_template(image, similarity=similarity, direct_match=direct_match)
if not matched:
return False
@@ -124,10 +152,10 @@ class ButtonWrapper(Resource):
self.name = name
self.data_buttons = kwargs
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):
del_cached_property(self, 'assets')
del_cached_property(self, 'buttons')
self._matched_button = None
def __str__(self):
@@ -144,16 +172,25 @@ class ButtonWrapper(Resource):
def __bool__(self):
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
def buttons(self) -> t.List[Button]:
# for trial in [server.lang, 'share', 'cn']:
for trial in [server.lang, 'share', 'jp']:
assets = self.data_buttons.get(trial, None)
if assets is not None:
for trial in [server.lang, 'share', 'cn']:
try:
assets = self.data_buttons[trial]
if isinstance(assets, Button):
return [assets]
elif isinstance(assets, list):
return assets
except KeyError:
pass
raise ScriptError(f'ButtonWrapper({self}) on server {server.lang} has no fallback button')
@@ -164,16 +201,45 @@ class ButtonWrapper(Resource):
return True
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:
if assets.match_template(image, similarity=similarity):
if assets.match_template(image, similarity=similarity, direct_match=direct_match):
self._matched_button = assets
return True
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:
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
return True
return False
@@ -222,18 +288,32 @@ class ButtonWrapper(Resource):
"""
if isinstance(button, ButtonWrapper):
button = button.matched_button
for b in self.buttons:
for b in self.iter_buttons():
b.load_offset(button)
def clear_offset(self):
for b in self.buttons:
for b in self.iter_buttons():
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:
def __init__(self, button, name='CLICK_BUTTON'):
self.area = button
self.button = button
def __init__(self, area, button=None, name='CLICK_BUTTON'):
self.area = area
if button is None:
self.button = area
else:
self.button = button
self.name = name
def __str__(self):
+17 -12
View File
@@ -1,11 +1,11 @@
import re
import module.config.server as server
from module.base.decorator import cached_property, del_cached_property
from module.base.decorator import cached_property
def get_assets_from_file(file, regex):
def get_assets_from_file(file):
assets = set()
regex = re.compile(r"file='(.*?)'")
with open(file, 'r', encoding='utf-8') as f:
for row in f.readlines():
result = regex.search(row)
@@ -20,11 +20,9 @@ class PreservedAssets:
assets = set()
assets |= get_assets_from_file(
file='./tasks/base/assets/assets_base_page.py',
regex=re.compile(r'^([A-Za-z][A-Za-z0-9_]+) = ')
)
assets |= get_assets_from_file(
file='./tasks/base/assets/assets_base_popup.py',
regex=re.compile(r'^([A-Za-z][A-Za-z0-9_]+) = ')
)
return assets
@@ -44,11 +42,13 @@ class Resource:
@classmethod
def is_loaded(cls, obj):
if hasattr(obj, '_image') and obj._image is None:
return False
elif hasattr(obj, 'image') and obj.image is None:
return False
return True
if hasattr(obj, '_image') and obj._image is not None:
return True
if hasattr(obj, 'image') and obj.image is not None:
return True
if hasattr(obj, 'buttons') and obj.buttons is not None:
return True
return False
@classmethod
def resource_show(cls):
@@ -56,11 +56,16 @@ class Resource:
logger.hr('Show resource')
for key, obj in cls.instances.items():
if cls.is_loaded(obj):
continue
logger.info(f'{obj}: {key}')
logger.info(f'{obj}: {key}')
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
# module.ui has about 80 assets and takes about 3MB
# Alas has about 800 assets, but they are not all loaded.
+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": {
"Scheduler": {
"Enable": {
@@ -348,6 +533,30 @@
"stored": "StoredInt",
"order": 3,
"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
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:
PlayerSelect:
value: 0
@@ -99,3 +141,15 @@ ItemStorage:
stored: StoredInt
order: 3
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:
Scheduler:
Enable: true
Bounty:
Scheduler:
Enable: true
Scrimmage:
Scheduler:
Enable: true
TacticalChallenge:
Scheduler:
Enable: true
+2
View File
@@ -14,6 +14,8 @@
"Cafe",
"Mail",
"Circle",
"Bounty",
"Scrimmage",
"TacticalChallenge",
"DataUpdate"
]
+39
View File
@@ -35,5 +35,44 @@
},
"order": 3,
"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
Circle:
- Scheduler
Bounty:
- Scheduler
- Bounty
- Highway
- DesertRailroad
- Schoolhouse
Scrimmage:
- Scheduler
- Scrimmage
- Trinity
- Gehenna
- Millennium
TacticalChallenge:
- Scheduler
- TacticalChallenge
-13
View File
@@ -499,19 +499,6 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher
self.task_call('DailyQuest')
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
def DEVICE_SCREENSHOT_METHOD(self):
return self.Emulator_ScreenshotMethod
+33
View File
@@ -45,6 +45,36 @@ class GeneratedConfig:
Cafe_AutoAdjust = 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`
TacticalChallenge_PlayerSelect = 0 # 0, 1, 2, 3
@@ -52,3 +82,6 @@ class GeneratedConfig:
ItemStorage_AP = {}
ItemStorage_Credit = {}
ItemStorage_Pyroxene = {}
ItemStorage_BountyTicket = {}
ItemStorage_ScrimmageTicket = {}
ItemStorage_TacticalChallengeTicket = {}
+2 -1
View File
@@ -8,7 +8,8 @@ class ManualConfig:
SCHEDULER_PRIORITY = """
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'])
typ = data['type']
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 = parse_value(value, data=data)
deep_set(new, keys=keys, value=value)
+169 -2
View File
@@ -30,6 +30,14 @@
"name": "Club",
"help": ""
},
"Bounty": {
"name": "Bounty",
"help": ""
},
"Scrimmage": {
"name": "Scrimmage",
"help": ""
},
"TacticalChallenge": {
"name": "Tactical Challenge",
"help": ""
@@ -205,8 +213,155 @@
"help": "Auto adjust cafe interface for better student interaction"
},
"SecondCafe": {
"name": "Second Cafe",
"help": "Enable auto switch to second cafe and perform interaction"
"name": "Second Floor",
"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": {
@@ -239,6 +394,18 @@
"Pyroxene": {
"name": "Pyroxene",
"help": ""
},
"BountyTicket": {
"name": "Bounty",
"help": ""
},
"ScrimmageTicket": {
"name": "Scrimmage",
"help": ""
},
"TacticalChallengeTicket": {
"name": "Tactical Challenge",
"help": ""
}
},
"Gui": {
+168 -1
View File
@@ -30,6 +30,14 @@
"name": "公会",
"help": "社团 / 小组"
},
"Bounty": {
"name": "悬赏通缉",
"help": ""
},
"Scrimmage": {
"name": "学院交流会",
"help": ""
},
"TacticalChallenge": {
"name": "战术对抗赛",
"help": "战术大赛 / 竞技场"
@@ -206,7 +214,154 @@
},
"SecondCafe": {
"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": {
@@ -239,6 +394,18 @@
"Pyroxene": {
"name": "青辉石",
"help": ""
},
"BountyTicket": {
"name": "悬赏通缉",
"help": ""
},
"ScrimmageTicket": {
"name": "学院交流会",
"help": ""
},
"TacticalChallengeTicket": {
"name": "战术对抗赛",
"help": ""
}
},
"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.config.utils import DEFAULT_TIME, deep_get, get_server_last_monday_update, get_server_last_update
# from module.exception import ScriptError
@@ -173,6 +175,20 @@ class StoredCounter(StoredBase):
class StoredAP(StoredCounter):
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):
# FIXED_TOTAL = 500
#
+6 -4
View File
@@ -1,10 +1,9 @@
from module.config.stored.classes import (
StoredAP,
StoredBase,
StoredCounter,
StoredExpiredAt0400,
StoredExpiredAtMonday0400,
StoredBountyTicket,
StoredInt,
StoredScrimmageTicket,
StoredTacticalChallengeTicket,
)
@@ -15,3 +14,6 @@ class StoredGenerated:
AP = StoredAP("DataUpdate.ItemStorage.AP")
Credit = StoredInt("DataUpdate.ItemStorage.Credit")
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
if len(packages) == 0:
logger.critical(f'No Star Rail package found, '
f'please confirm Star Rail has been installed on device "{self.serial}"')
logger.critical(f'No Blue Archive package found, '
f'please confirm Blue Archive has been installed on device "{self.serial}"')
raise RequestHumanTakeover
if len(packages) == 1:
logger.info('Auto package detection found only one package, using it')
@@ -877,6 +877,6 @@ class Connection(ConnectionAttr):
# set_server(self.package)
else:
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')
raise RequestHumanTakeover
+7 -1
View File
@@ -1,6 +1,6 @@
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
DIC_LANG_TO_MODEL = {
@@ -56,6 +56,12 @@ class OcrModel:
except AttributeError:
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
def zhs(self):
return TextSystem('zhs')
+6 -3
View File
@@ -89,12 +89,15 @@ def readable_time(before: str) -> str:
elif diff < 60:
# < 1 min
return t("Gui.Dashboard.JustNow")
elif diff < 3600:
elif diff < 5400:
# < 90 min
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))
elif diff < 1296000:
# < 15 days
return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400))
else:
# > 15 days
# >= 15 days
return t("Gui.Dashboard.LongTimeAgo")
+9 -5
View File
@@ -9,17 +9,17 @@ from queue import Queue
from typing import Callable, Generator, List
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.output import PopupSize, popup, put_html, toast
from pywebio.session import eval_js
from pywebio.session import info as session_info
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 module.config.utils import deep_iter
from module.logger import logger
from module.webui.setting import State
RE_DATETIME = (
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):
if pattern == "datetime":
pattern = RE_DATETIME
try:
datetime.datetime.fromisoformat(string)
return True
except ValueError:
return False
# elif:
return re.fullmatch(pattern=pattern, string=string)
+11 -5
View File
@@ -24,10 +24,10 @@ AP_EXCEED = ButtonWrapper(
name='AP_EXCEED',
jp=Button(
file='./assets/jp/base/popup/AP_EXCEED.png',
area=(610, 124, 669, 153),
search=(590, 104, 689, 173),
color=(139, 153, 168),
button=(535, 494, 746, 565),
area=(611, 147, 669, 177),
search=(591, 127, 689, 197),
color=(143, 156, 170),
button=(539, 470, 743, 533),
),
en=Button(
file='./assets/en/base/popup/AP_EXCEED.png',
@@ -124,7 +124,13 @@ GET_REWARD_SKIP = ButtonWrapper(
)
INSUFFICIENT_INVENTORY = ButtonWrapper(
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(
file='./assets/en/base/popup/INSUFFICIENT_INVENTORY.png',
area=(578, 117, 703, 152),
+6
View File
@@ -94,6 +94,12 @@ class PopupHandler(ModuleBase):
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:
if self.appear_then_click(ITEM_EXPIRED, interval=interval):
return True
+2
View File
@@ -345,6 +345,8 @@ class UI(MainPage):
return True
if self.handle_ap_exceed():
return True
if self.handle_insufficient_inventory():
return True
if self.handle_item_expired():
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.base.timer import Timer
from module.base.button import ClickButton
from module.base.decorator import Config
from module.base.utils.utils import area_offset
from module.ocr.ocr import Digit
from module.ui.switch import Switch
@@ -33,6 +34,8 @@ class CafeStatus(Enum):
class Cafe(UI):
template = CLICKABLE_TEMPLATE
@staticmethod
def merge_points(points, threshold=3):
if len(points) <= 1:
@@ -51,8 +54,7 @@ class Cafe(UI):
@staticmethod
def _extract_clickable_from_image(image):
# convert to hsv for better color matching
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
# set color range
lower_hsv = np.array([18, 200, 220])
upper_hsv = np.array([30, 255, 255])
@@ -62,8 +64,8 @@ class Cafe(UI):
return cv2.bitwise_and(image, image, mask=mask)
def _match_clickable_points(self, image, threshold=0.8):
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
template = cv2.cvtColor(self.btn.matched_button.image, cv2.COLOR_BGR2GRAY)
image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
template = cv2.cvtColor(self.template.matched_button.image, cv2.COLOR_RGB2GRAY)
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
loc = np.where(res >= threshold)
@@ -78,11 +80,11 @@ class Cafe(UI):
points = self.merge_points(points)
if not points:
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 [
ClickButton(
button=area_offset(area, offset=point),
name=self.btn.name
name=self.template.name
)
for point in points
]
@@ -188,15 +190,23 @@ class Cafe(UI):
logger.warning(f'Invalid status: {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):
self.btn = CLICKABLE_TEMPLATE
self.click = 0
self.check = 0
is_reward_on = self.config.Cafe_Reward
is_touch_on = self.config.Cafe_Touch
self.is_adjust_on = self.config.Cafe_AutoAdjust
is_second_cafe_on = self.config.Cafe_SecondCafe
self.ui_ensure(page_cafe)
@@ -240,7 +250,7 @@ class Cafe(UI):
is_reset = True
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):
logger.warning('Cafe switch not found')
continue
@@ -266,11 +276,14 @@ class Cafe(UI):
logger.attr('Status', status)
status = self._handle_cafe(status)
if not is_second_cafe_on:
if not self.is_second_cafe_on:
if status is CafeStatus.FINISHED:
logger.info('Second cafe is not supported or disabled')
logger.info('Cafe finished')
break
else:
if is_second and status is CafeStatus.FINISHED:
logger.info('Cafe finished')
break
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.decorator import Config
from module.exception import GameNotRunningError
from module.logger import logger
from tasks.base.page import page_main
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):
@@ -29,6 +30,16 @@ class Login(UI):
return True
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')
orientation_timer = Timer(5)
startup_timer = Timer(5).start()
@@ -78,6 +89,8 @@ class Login(UI):
# continue
if self.appear_then_click(UPDATE):
continue
if _handle_survey(self):
continue
if self.ui_additional():
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
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.utils import area_pad, area_size, area_offset, random_rectangle_vector_opted
from module.logger import logger
from module.ocr.ocr import Ocr
from tasks.stage.assets.assets_stage_list import *
class StageList:
@@ -15,17 +16,19 @@ class StageList:
def __init__(
self,
name,
area_stage: ButtonWrapper,
area_index: ButtonWrapper,
area_item: ButtonWrapper,
button_enter: ButtonWrapper,
button_list: ButtonWrapper = None,
button_index: ButtonWrapper = None,
button_item: ButtonWrapper = None,
button_enter: ButtonWrapper = None,
button_stars: ButtonWrapper = None,
drag_direction: str = "down"
):
self.name = name
self.stage = area_stage
self.index_ocr = Ocr(area_index, lang='en')
self.stage_item = area_item.button
self.enter = button_enter
self.stage = button_list if button_list else STAGE_LIST
self.index_ocr = Ocr(button_index if button_index else OCR_INDEX, lang='en')
self.stage_item = (button_item if button_item else STAGE_ITEM).button
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.current_index_min = 1
@@ -43,7 +46,8 @@ class StageList:
def __hash__(self):
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))
def load_stage_indexes(self, main: ModuleBase):
@@ -53,7 +57,7 @@ class StageList:
if not self.current_indexes:
logger.warning(f'No valid index in {self.index_ocr.name}')
return
indexes = self._get_indexes()
indexes = self._indexes
self.current_index_min = min(indexes)
self.current_index_max = max(indexes)
@@ -119,7 +123,7 @@ class StageList:
timeout=Timer(1.5, 5)
)
indexes = self._get_indexes()
indexes = self._indexes
if indexes and last_indexes == set(indexes):
logger.warning(f'No more index {index}')
return False
@@ -129,49 +133,66 @@ class StageList:
@staticmethod
def _match_clickable_points(image, template, threshold=0.85):
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
template = cv2.cvtColor(template, cv2.COLOR_RGB2GRAY)
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
loc = np.where(res >= threshold)
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(
self,
index: int,
main: ModuleBase,
index: int,
insight: bool = True,
sweepable: bool = True,
offset: tuple[int, int] = (-20, -15),
skip_first_screenshot: bool = True,
interval: int = 5
interval: int = 1.5
) -> bool:
# insight index, if failed, return False
if insight and not self.insight_index(index, main, skip_first_screenshot):
return False
logger.info(f'Select index: {index}')
click_interval = Timer(interval)
load_index_interval = Timer(1)
timeout = Timer(15, 10).start()
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
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)
# find box of index
index_box = next(filter(lambda x: int(x.ocr_text) == index, self.current_indexes), None)
if index_box is None:
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_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)
if not points:
logger.warning(f'No clickable {self.enter.name}')
return False
continue
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)
@@ -179,3 +200,7 @@ class StageList:
if click_interval.reached_and_reset():
main.device.click(click_button)
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.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.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)
from tasks.tactical_challenge.ui import TacticalChallengeUI
class TCStatus(Enum):
@@ -31,50 +25,12 @@ class TCStatus(Enum):
FINISHED = -1
class TacticalChallenge(UI):
class TacticalChallenge(TacticalChallengeUI):
select_players = (PLAYER_SELECT_FIRST, PLAYER_SELECT_SECOND, PLAYER_SELECT_THIRD)
def _get_ticket(self):
"""
Page:
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
@property
def current_ticket(self):
return self.config.stored.TacticalChallengeTicket.value
def _player_select(self, select):
if select:
@@ -85,15 +41,13 @@ class TacticalChallenge(UI):
def _handle_challenge(self, status):
match status:
case TCStatus.REWARD:
if self._get_reward():
if self.get_reward():
return TCStatus.OCR
case TCStatus.OCR:
is_valid, ticket = self._get_ticket()
if not is_valid:
return status
if ticket == 0:
return TCStatus.FINISHED
return TCStatus.SELECT
if self.get_ticket():
if self.current_ticket == 0:
return TCStatus.FINISHED
return TCStatus.SELECT
case TCStatus.SELECT:
self.appear_then_click(self.select)
if self.appear(PREPARE_CHALLENGE):
@@ -103,7 +57,7 @@ class TacticalChallenge(UI):
if not self.appear(PREPARE_CHALLENGE):
return TCStatus.SKIP
case TCStatus.SKIP:
if not self._set_skip():
if not self.set_skip():
return TCStatus.SKIP
return TCStatus.START
case TCStatus.START:
@@ -118,12 +72,10 @@ class TacticalChallenge(UI):
case TCStatus.WIN | TCStatus.LOSE:
if self.appear_then_click(CHALLENGE_WIN) or self.appear_then_click(CHALLENGE_LOSE):
return status
is_valid, ticket = self._get_ticket()
if not is_valid:
return status
if ticket == 0:
return TCStatus.FINISHED
return TCStatus.FINAL
if self.get_ticket():
if self.current_ticket == 0:
return TCStatus.FINISHED
return TCStatus.FINAL
case TCStatus.FINAL | TCStatus.FINISHED:
return status
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