mirror of
https://github.com/TheFunny/ArisuAutoSweeper
synced 2026-06-28 21:45:07 +00:00
Compare commits
20 Commits
f7b165f589
...
sync_src
| Author | SHA1 | Date | |
|---|---|---|---|
|
baac90ecf0
|
|||
|
53ec298fed
|
|||
|
b4f18f78ff
|
|||
|
eb9af42f38
|
|||
|
c29d972c6c
|
|||
|
92b34d4760
|
|||
|
04853b6c31
|
|||
|
9604e8962a
|
|||
|
03380b2d71
|
|||
|
c27bd74050
|
|||
|
c3e9945b15
|
|||
|
1dd100ac04
|
|||
|
77dca70af1
|
|||
|
b8ecd0c9d6
|
|||
|
ceb24283f3
|
|||
|
d0c591af3a
|
|||
|
589b0b08ec
|
|||
|
299bd6c687
|
|||
|
e61afaf43b
|
|||
|
30e8c8b21b
|
@@ -16,6 +16,7 @@ The script is still under active development. The following features have been i
|
|||||||
- [x] **Club** Claim AP
|
- [x] **Club** Claim AP
|
||||||
- [x] **Mailbox** Claim rewards
|
- [x] **Mailbox** Claim rewards
|
||||||
- [x] **Bounty** Auto sweep
|
- [x] **Bounty** Auto sweep
|
||||||
|
- [x] **Scrimmage** Auto sweep
|
||||||
- [x] **Tactical Challenge** Claim rewards / Auto battle
|
- [x] **Tactical Challenge** Claim rewards / Auto battle
|
||||||
|
|
||||||
Supported servers:
|
Supported servers:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
- [x] **公会** 领取体力
|
- [x] **公会** 领取体力
|
||||||
- [x] **邮箱** 领取奖励
|
- [x] **邮箱** 领取奖励
|
||||||
- [x] **悬赏通缉** 自动扫荡
|
- [x] **悬赏通缉** 自动扫荡
|
||||||
|
- [x] **学院交流会** 自动扫荡
|
||||||
- [x] **战术对抗赛** 领取奖励 / 自动战斗
|
- [x] **战术对抗赛** 领取奖励 / 自动战斗
|
||||||
|
|
||||||
目前支持的服务器:
|
目前支持的服务器:
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
|
|||||||
from tasks.bounty.bounty import Bounty
|
from tasks.bounty.bounty import Bounty
|
||||||
Bounty(config=self.config, device=self.device).run()
|
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()
|
||||||
|
|||||||
@@ -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--"] {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()
|
||||||
@@ -8,7 +8,8 @@ class ManualConfig:
|
|||||||
|
|
||||||
SCHEDULER_PRIORITY = """
|
SCHEDULER_PRIORITY = """
|
||||||
Restart
|
Restart
|
||||||
> Cafe > Circle > Mail > DataUpdate > Bounty > TacticalChallenge
|
> Cafe > Circle > Mail > DataUpdate > Bounty
|
||||||
|
> Scrimmage > TacticalChallenge
|
||||||
"""
|
"""
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -396,15 +396,15 @@
|
|||||||
"help": ""
|
"help": ""
|
||||||
},
|
},
|
||||||
"BountyTicket": {
|
"BountyTicket": {
|
||||||
"name": "Bounty Ticket",
|
"name": "Bounty",
|
||||||
"help": ""
|
"help": ""
|
||||||
},
|
},
|
||||||
"ScrimmageTicket": {
|
"ScrimmageTicket": {
|
||||||
"name": "Scrimmage Ticket",
|
"name": "Scrimmage",
|
||||||
"help": ""
|
"help": ""
|
||||||
},
|
},
|
||||||
"TacticalChallengeTicket": {
|
"TacticalChallengeTicket": {
|
||||||
"name": "Tactical Challenge Ticket",
|
"name": "Tactical Challenge",
|
||||||
"help": ""
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"help": "社团 / 小组"
|
"help": "社团 / 小组"
|
||||||
},
|
},
|
||||||
"Bounty": {
|
"Bounty": {
|
||||||
"name": "通缉悬赏",
|
"name": "悬赏通缉",
|
||||||
"help": ""
|
"help": ""
|
||||||
},
|
},
|
||||||
"Scrimmage": {
|
"Scrimmage": {
|
||||||
@@ -396,15 +396,15 @@
|
|||||||
"help": ""
|
"help": ""
|
||||||
},
|
},
|
||||||
"BountyTicket": {
|
"BountyTicket": {
|
||||||
"name": "悬赏通缉票券",
|
"name": "悬赏通缉",
|
||||||
"help": ""
|
"help": ""
|
||||||
},
|
},
|
||||||
"ScrimmageTicket": {
|
"ScrimmageTicket": {
|
||||||
"name": "学院交流会票券",
|
"name": "学院交流会",
|
||||||
"help": ""
|
"help": ""
|
||||||
},
|
},
|
||||||
"TacticalChallengeTicket": {
|
"TacticalChallengeTicket": {
|
||||||
"name": "战术对抗赛票券",
|
"name": "战术对抗赛",
|
||||||
"help": ""
|
"help": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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,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)
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ class Bounty(BountyUI):
|
|||||||
case BountyStatus.ENTER:
|
case BountyStatus.ENTER:
|
||||||
if self.enter_stage(self.current_stage):
|
if self.enter_stage(self.current_stage):
|
||||||
return BountyStatus.SWEEP
|
return BountyStatus.SWEEP
|
||||||
|
else:
|
||||||
|
self.error_handler()
|
||||||
case BountyStatus.SWEEP:
|
case BountyStatus.SWEEP:
|
||||||
if self.do_sweep(self.current_count):
|
if self.do_sweep(self.current_count):
|
||||||
self.task.pop(0)
|
self.task.pop(0)
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ class BountyUI(UI):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def enter_stage(self, index: int) -> bool:
|
def enter_stage(self, index: int) -> bool:
|
||||||
if BOUNTY_LIST.select_index_enter(index, self):
|
if BOUNTY_LIST.select_index_enter(self, index):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -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
|
||||||
+13
-5
@@ -148,33 +148,37 @@ class StageList:
|
|||||||
|
|
||||||
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,
|
sweepable: bool = True,
|
||||||
offset: tuple[int, int] = (-20, -15),
|
offset: tuple[int, int] = (-20, -15),
|
||||||
skip_first_screenshot: bool = True,
|
skip_first_screenshot: bool = True,
|
||||||
interval: int = 2
|
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((*offset, *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])
|
||||||
@@ -188,7 +192,7 @@ class StageList:
|
|||||||
|
|
||||||
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)
|
||||||
@@ -196,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
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
from module.base.base import ModuleBase
|
from module.base.base import ModuleBase
|
||||||
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 Digit
|
from module.ocr.ocr import Digit
|
||||||
from enum import Enum
|
|
||||||
from tasks.stage.assets.assets_stage_sweep import *
|
from tasks.stage.assets.assets_stage_sweep import *
|
||||||
|
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ class StageSweep:
|
|||||||
main.device.screenshot()
|
main.device.screenshot()
|
||||||
if not timer.reached_and_reset():
|
if not timer.reached_and_reset():
|
||||||
continue
|
continue
|
||||||
ocr_result = self.num.detect_and_ocr(main.device.image)
|
ocr_result = list(filter(lambda x: x.ocr_text.isdigit(), self.num.detect_and_ocr(main.device.image)))
|
||||||
if not ocr_result:
|
if not ocr_result:
|
||||||
logger.warning(f'No valid num in {self.num.name}')
|
logger.warning(f'No valid num in {self.num.name}')
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TacticalChallengeUI(UI):
|
|||||||
timer = Timer(10, 10).start()
|
timer = Timer(10, 10).start()
|
||||||
while 1:
|
while 1:
|
||||||
self.device.screenshot()
|
self.device.screenshot()
|
||||||
|
self.ui_additional()
|
||||||
if self.match_color(GOT_REWARD_DAILY) and self.match_color(GOT_REWARD_CREDIT):
|
if self.match_color(GOT_REWARD_DAILY) and self.match_color(GOT_REWARD_CREDIT):
|
||||||
return True
|
return True
|
||||||
if self.match_color(GET_REWARD_DAILY):
|
if self.match_color(GET_REWARD_DAILY):
|
||||||
|
|||||||
Reference in New Issue
Block a user