Add: Task assignment
BIN
assets/cn/assignment/claim/CLAIM.BUTTON.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/cn/assignment/claim/CLAIM.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
assets/cn/assignment/claim/CLOSE_REPORT.BUTTON.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/cn/assignment/claim/CLOSE_REPORT.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/cn/assignment/claim/REDISPATCH.BUTTON.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/cn/assignment/claim/REDISPATCH.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/cn/assignment/dispatch/CHARACTER_LIST.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.BUTTON.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/cn/assignment/ui/DISPATCHED.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_1.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_2.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
assets/share/assignment/dispatch/DURATION_12.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/DURATION_20.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/DURATION_4.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
assets/share/assignment/dispatch/DURATION_8.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/share/assignment/dispatch/EMPTY_SLOT.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/share/assignment/ui/CHARACTER_MATERIALS.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/ui/ENTRY_LOADED.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/share/assignment/ui/SYNTHESIS_MATERIALS.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/share/base/page/ASSIGNMENT_CHECK.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/share/base/page/MENU_GOTO_ASSIGNMENT.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
@ -2,6 +2,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import typing as t
|
import typing as t
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
from module.base.code_generator import CodeGenerator
|
from module.base.code_generator import CodeGenerator
|
||||||
from module.config.utils import deep_get, read_file
|
from module.config.utils import deep_get, read_file
|
||||||
@ -12,7 +13,7 @@ UI_LANGUAGES = ['cn', 'cht', 'en', 'jp']
|
|||||||
|
|
||||||
def text_to_variable(text):
|
def text_to_variable(text):
|
||||||
text = re.sub("'s |s' ", '_', text)
|
text = re.sub("'s |s' ", '_', text)
|
||||||
text = re.sub('[ \-—:\']+', '_', text)
|
text = re.sub('[ \-—:\'/]+', '_', text)
|
||||||
text = re.sub(r'[(),#]|</?\w+>', '', text)
|
text = re.sub(r'[(),#]|</?\w+>', '', text)
|
||||||
# text = re.sub(r'[#_]?\d+(_times?)?', '', text)
|
# text = re.sub(r'[#_]?\d+(_times?)?', '', text)
|
||||||
return text
|
return text
|
||||||
@ -154,6 +155,16 @@ class KeywordExtract:
|
|||||||
quest_keywords = [self.text_map[lang].find(quest_hash)[1] for quest_hash in quests_hash]
|
quest_keywords = [self.text_map[lang].find(quest_hash)[1] for quest_hash in quests_hash]
|
||||||
self.load_keywords(quest_keywords, lang)
|
self.load_keywords(quest_keywords, lang)
|
||||||
|
|
||||||
|
def generate_assignment_keywords(self):
|
||||||
|
KeywordFromFile = namedtuple('KeywordFromFile', ('file', 'class_name', 'output_file'))
|
||||||
|
for keyword in (
|
||||||
|
KeywordFromFile('ExpeditionGroup.json', 'AssignmentGroup', './tasks/assignment/keywords/group.py'),
|
||||||
|
KeywordFromFile('ExpeditionData.json', 'AssignmentEntry','./tasks/assignment/keywords/entry.py')
|
||||||
|
):
|
||||||
|
file = os.path.join(TextMap.DATA_FOLDER, 'ExcelOutput', keyword.file)
|
||||||
|
self.load_keywords(deep_get(data, 'Name.Hash') for data in read_file(file).values())
|
||||||
|
self.write_keywords(keyword_class=keyword.class_name, output_file=keyword.output_file)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
self.load_keywords(['模拟宇宙', '拟造花萼(金)', '拟造花萼(赤)', '凝滞虚影', '侵蚀隧洞', '历战余响', '忘却之庭'])
|
self.load_keywords(['模拟宇宙', '拟造花萼(金)', '拟造花萼(赤)', '凝滞虚影', '侵蚀隧洞', '历战余响', '忘却之庭'])
|
||||||
self.write_keywords(keyword_class='DungeonNav', output_file='./tasks/dungeon/keywords/nav.py')
|
self.write_keywords(keyword_class='DungeonNav', output_file='./tasks/dungeon/keywords/nav.py')
|
||||||
@ -170,6 +181,7 @@ class KeywordExtract:
|
|||||||
self.write_keywords(keyword_class='DungeonEntrance', output_file='./tasks/dungeon/keywords/dungeon_entrance.py')
|
self.write_keywords(keyword_class='DungeonEntrance', output_file='./tasks/dungeon/keywords/dungeon_entrance.py')
|
||||||
self.load_keywords(['奖励', '任务'])
|
self.load_keywords(['奖励', '任务'])
|
||||||
self.write_keywords(keyword_class='BattlePassTab', output_file='./tasks/battle_pass/keywords/tab.py')
|
self.write_keywords(keyword_class='BattlePassTab', output_file='./tasks/battle_pass/keywords/tab.py')
|
||||||
|
self.generate_assignment_keywords()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -26,6 +26,9 @@ class Keyword:
|
|||||||
"""
|
"""
|
||||||
Instance attributes and methods
|
Instance attributes and methods
|
||||||
"""
|
"""
|
||||||
|
@cached_property
|
||||||
|
def ch(self) -> str:
|
||||||
|
return self.cn
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cn_parsed(self) -> str:
|
def cn_parsed(self) -> str:
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import re
|
|
||||||
from ppocronnx.predict_system import BoxedResult
|
from ppocronnx.predict_system import BoxedResult
|
||||||
|
|
||||||
import module.config.server as server
|
import module.config.server as server
|
||||||
@ -257,3 +258,44 @@ class DigitCounter(Ocr):
|
|||||||
else:
|
else:
|
||||||
logger.warning(f'No digit counter found in {result}')
|
logger.warning(f'No digit counter found in {result}')
|
||||||
return 0, 0, 0
|
return 0, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
class Duration(Ocr):
|
||||||
|
@cached_property
|
||||||
|
def timedelta_regex(self):
|
||||||
|
hour_regex = {
|
||||||
|
'ch': '小时',
|
||||||
|
'en': 'h\s*'
|
||||||
|
}[self.lang]
|
||||||
|
minute_regex = {
|
||||||
|
'ch': '分钟',
|
||||||
|
'en': 'm\s*'
|
||||||
|
}[self.lang]
|
||||||
|
second_regex = {
|
||||||
|
'ch': '秒',
|
||||||
|
'en': 's'
|
||||||
|
}[self.lang]
|
||||||
|
ret = rf'\D*((?P<hours>\d{{1,2}}){hour_regex})?'
|
||||||
|
ret += rf'((?P<minutes>\d{{1,2}}){minute_regex})?'
|
||||||
|
ret += rf'((?P<seconds>\d{{1,2}}){second_regex})?'
|
||||||
|
return re.compile(ret)
|
||||||
|
|
||||||
|
def format_result(self, result: str) -> timedelta:
|
||||||
|
"""
|
||||||
|
Do OCR on a duration, such as `2h 13m 30s`, `2h`, `13m 30s`, `9s`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
timedelta:
|
||||||
|
"""
|
||||||
|
matched = self.timedelta_regex.match(result)
|
||||||
|
if matched is None:
|
||||||
|
return timedelta()
|
||||||
|
hours = self._sanitize_number(matched.group('hours'))
|
||||||
|
minutes = self._sanitize_number(matched.group('minutes'))
|
||||||
|
seconds = self._sanitize_number(matched.group('seconds'))
|
||||||
|
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
||||||
|
|
||||||
|
def _sanitize_number(self, number) -> int:
|
||||||
|
if number is None:
|
||||||
|
return 0
|
||||||
|
return int(number)
|
||||||
|
@ -28,6 +28,7 @@ class DraggableList:
|
|||||||
keyword_class,
|
keyword_class,
|
||||||
ocr_class,
|
ocr_class,
|
||||||
search_button: ButtonWrapper,
|
search_button: ButtonWrapper,
|
||||||
|
active_color: tuple[int, int, int]
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
@ -42,6 +43,7 @@ class DraggableList:
|
|||||||
keyword_class = keyword_class[0]
|
keyword_class = keyword_class[0]
|
||||||
self.known_rows = list(keyword_class.instances.values())
|
self.known_rows = list(keyword_class.instances.values())
|
||||||
self.search_button = search_button
|
self.search_button = search_button
|
||||||
|
self.active_color = active_color
|
||||||
|
|
||||||
self.row_min = 1
|
self.row_min = 1
|
||||||
self.row_max = len(self.known_rows)
|
self.row_max = len(self.known_rows)
|
||||||
@ -83,12 +85,14 @@ class DraggableList:
|
|||||||
self.cur_buttons = self.ocr_class(self.search_button) \
|
self.cur_buttons = self.ocr_class(self.search_button) \
|
||||||
.matched_ocr(main.device.image, self.keyword_class)
|
.matched_ocr(main.device.image, self.keyword_class)
|
||||||
# Get indexes
|
# Get indexes
|
||||||
indexes = [self.keyword2index(row.matched_keyword) for row in self.cur_buttons]
|
indexes = [self.keyword2index(row.matched_keyword)
|
||||||
|
for row in self.cur_buttons]
|
||||||
indexes = [index for index in indexes if index]
|
indexes = [index for index in indexes if index]
|
||||||
# Check row order
|
# Check row order
|
||||||
if len(indexes) >= 2:
|
if len(indexes) >= 2:
|
||||||
if not np.all(np.diff(indexes) > 0):
|
if not np.all(np.diff(indexes) > 0):
|
||||||
logger.warning(f'Rows given to {self} are not ascending sorted')
|
logger.warning(
|
||||||
|
f'Rows given to {self} are not ascending sorted')
|
||||||
if not indexes:
|
if not indexes:
|
||||||
logger.warning(f'No valid rows loaded into {self}')
|
logger.warning(f'No valid rows loaded into {self}')
|
||||||
return
|
return
|
||||||
@ -157,7 +161,8 @@ class DraggableList:
|
|||||||
elif self.cur_max < row_index:
|
elif self.cur_max < row_index:
|
||||||
self.drag_page('down', main=main)
|
self.drag_page('down', main=main)
|
||||||
# Wait for bottoming out
|
# Wait for bottoming out
|
||||||
main.wait_until_stable(self.search_button, timer=Timer(0, count=0), timeout=Timer(1.5, count=5))
|
main.wait_until_stable(self.search_button, timer=Timer(
|
||||||
|
0, count=0), timeout=Timer(1.5, count=5))
|
||||||
skip_first_screenshot = True
|
skip_first_screenshot = True
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -168,7 +173,7 @@ class DraggableList:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Having gold letters
|
# Having gold letters
|
||||||
if main.image_color_count(button, color=(190, 175, 124), threshold=221, count=50):
|
if main.image_color_count(button, color=self.active_color, threshold=221, count=50):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@ -183,7 +188,8 @@ class DraggableList:
|
|||||||
Returns:
|
Returns:
|
||||||
If success
|
If success
|
||||||
"""
|
"""
|
||||||
result = self.insight_row(row, main=main, skip_first_screenshot=skip_first_screenshot)
|
result = self.insight_row(
|
||||||
|
row, main=main, skip_first_screenshot=skip_first_screenshot)
|
||||||
if not result:
|
if not result:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from module.logger import logger
|
|||||||
|
|
||||||
class Switch:
|
class Switch:
|
||||||
"""
|
"""
|
||||||
A wrapper to handle switches in game, switch among states with reties.
|
A wrapper to handle switches in game, switch among states with retries.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Definitions
|
# Definitions
|
||||||
|
45
tasks/assignment/assets/assets_assignment_claim.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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 ```
|
||||||
|
|
||||||
|
CLAIM = ButtonWrapper(
|
||||||
|
name='CLAIM',
|
||||||
|
cn=Button(
|
||||||
|
file='./assets/cn/assignment/claim/CLAIM.png',
|
||||||
|
area=(1031, 652, 1101, 674),
|
||||||
|
search=(1011, 632, 1121, 694),
|
||||||
|
color=(169, 134, 66),
|
||||||
|
button=(920, 644, 1210, 683),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
CLOSE_REPORT = ButtonWrapper(
|
||||||
|
name='CLOSE_REPORT',
|
||||||
|
cn=Button(
|
||||||
|
file='./assets/cn/assignment/claim/CLOSE_REPORT.png',
|
||||||
|
area=(397, 598, 472, 623),
|
||||||
|
search=(377, 578, 492, 643),
|
||||||
|
color=(159, 157, 153),
|
||||||
|
button=(290, 592, 579, 630),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
OCR_ASSIGNMENT_REPORT_TIME = ButtonWrapper(
|
||||||
|
name='OCR_ASSIGNMENT_REPORT_TIME',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png',
|
||||||
|
area=(894, 191, 1003, 216),
|
||||||
|
search=(874, 171, 1023, 236),
|
||||||
|
color=(62, 63, 63),
|
||||||
|
button=(894, 191, 1003, 216),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
REDISPATCH = ButtonWrapper(
|
||||||
|
name='REDISPATCH',
|
||||||
|
cn=Button(
|
||||||
|
file='./assets/cn/assignment/claim/REDISPATCH.png',
|
||||||
|
area=(784, 598, 901, 622),
|
||||||
|
search=(764, 578, 921, 642),
|
||||||
|
color=(158, 157, 155),
|
||||||
|
button=(700, 592, 987, 629),
|
||||||
|
),
|
||||||
|
)
|
105
tasks/assignment/assets/assets_assignment_dispatch.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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 ```
|
||||||
|
|
||||||
|
ASSIGNMENT_STARTED_CHECK = ButtonWrapper(
|
||||||
|
name='ASSIGNMENT_STARTED_CHECK',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png',
|
||||||
|
area=(1174, 297, 1211, 514),
|
||||||
|
search=(1154, 277, 1231, 534),
|
||||||
|
color=(86, 81, 78),
|
||||||
|
button=(1174, 297, 1211, 514),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
CHARACTER_1 = ButtonWrapper(
|
||||||
|
name='CHARACTER_1',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/CHARACTER_1.png',
|
||||||
|
area=(116, 212, 206, 312),
|
||||||
|
search=(96, 192, 226, 332),
|
||||||
|
color=(149, 134, 123),
|
||||||
|
button=(116, 212, 206, 312),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
CHARACTER_2 = ButtonWrapper(
|
||||||
|
name='CHARACTER_2',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/CHARACTER_2.png',
|
||||||
|
area=(228, 211, 318, 311),
|
||||||
|
search=(208, 191, 338, 331),
|
||||||
|
color=(184, 161, 172),
|
||||||
|
button=(228, 211, 318, 311),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
CHARACTER_LIST = ButtonWrapper(
|
||||||
|
name='CHARACTER_LIST',
|
||||||
|
cn=Button(
|
||||||
|
file='./assets/cn/assignment/dispatch/CHARACTER_LIST.png',
|
||||||
|
area=(90, 165, 170, 186),
|
||||||
|
search=(70, 145, 190, 206),
|
||||||
|
color=(156, 154, 152),
|
||||||
|
button=(90, 165, 170, 186),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
CONFIRM_ASSIGNMENT = ButtonWrapper(
|
||||||
|
name='CONFIRM_ASSIGNMENT',
|
||||||
|
cn=Button(
|
||||||
|
file='./assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png',
|
||||||
|
area=(1024, 653, 1104, 672),
|
||||||
|
search=(1004, 633, 1124, 692),
|
||||||
|
color=(154, 154, 153),
|
||||||
|
button=(920, 645, 1208, 682),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DURATION_12 = ButtonWrapper(
|
||||||
|
name='DURATION_12',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/DURATION_12.png',
|
||||||
|
area=(762, 563, 862, 588),
|
||||||
|
search=(742, 543, 882, 608),
|
||||||
|
color=(63, 58, 50),
|
||||||
|
button=(762, 563, 862, 588),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DURATION_20 = ButtonWrapper(
|
||||||
|
name='DURATION_20',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/DURATION_20.png',
|
||||||
|
area=(882, 564, 982, 589),
|
||||||
|
search=(862, 544, 1002, 609),
|
||||||
|
color=(64, 60, 52),
|
||||||
|
button=(882, 564, 982, 589),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DURATION_4 = ButtonWrapper(
|
||||||
|
name='DURATION_4',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/DURATION_4.png',
|
||||||
|
area=(522, 564, 622, 589),
|
||||||
|
search=(502, 544, 642, 609),
|
||||||
|
color=(164, 142, 109),
|
||||||
|
button=(522, 564, 622, 589),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DURATION_8 = ButtonWrapper(
|
||||||
|
name='DURATION_8',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/DURATION_8.png',
|
||||||
|
area=(640, 564, 740, 589),
|
||||||
|
search=(620, 544, 760, 609),
|
||||||
|
color=(63, 58, 49),
|
||||||
|
button=(640, 564, 740, 589),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
EMPTY_SLOT = ButtonWrapper(
|
||||||
|
name='EMPTY_SLOT',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/dispatch/EMPTY_SLOT.png',
|
||||||
|
area=(1075, 562, 1110, 597),
|
||||||
|
search=(1054, 542, 1220, 616),
|
||||||
|
color=(200, 200, 195),
|
||||||
|
button=(1075, 562, 1110, 597),
|
||||||
|
),
|
||||||
|
)
|
85
tasks/assignment/assets/assets_assignment_ui.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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 ```
|
||||||
|
|
||||||
|
CHARACTER_MATERIALS = ButtonWrapper(
|
||||||
|
name='CHARACTER_MATERIALS',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/ui/CHARACTER_MATERIALS.png',
|
||||||
|
area=(123, 81, 307, 134),
|
||||||
|
search=(103, 61, 327, 154),
|
||||||
|
color=(234, 233, 229),
|
||||||
|
button=(123, 81, 307, 134),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DISPATCHED = ButtonWrapper(
|
||||||
|
name='DISPATCHED',
|
||||||
|
cn=Button(
|
||||||
|
file='./assets/cn/assignment/ui/DISPATCHED.png',
|
||||||
|
area=(1032, 652, 1095, 674),
|
||||||
|
search=(1012, 632, 1115, 694),
|
||||||
|
color=(99, 93, 85),
|
||||||
|
button=(1032, 652, 1095, 674),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ENTRY_LOADED = ButtonWrapper(
|
||||||
|
name='ENTRY_LOADED',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/ui/ENTRY_LOADED.png',
|
||||||
|
area=(446, 164, 466, 615),
|
||||||
|
search=(426, 144, 486, 635),
|
||||||
|
color=(203, 202, 194),
|
||||||
|
button=(446, 164, 466, 615),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
EXP_MATERIALS_CREDITS = ButtonWrapper(
|
||||||
|
name='EXP_MATERIALS_CREDITS',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png',
|
||||||
|
area=(343, 83, 527, 133),
|
||||||
|
search=(323, 63, 547, 153),
|
||||||
|
color=(222, 221, 217),
|
||||||
|
button=(343, 83, 527, 133),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
OCR_ASSIGNMENT_LIMIT = ButtonWrapper(
|
||||||
|
name='OCR_ASSIGNMENT_LIMIT',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png',
|
||||||
|
area=(1095, 86, 1179, 126),
|
||||||
|
search=(1075, 66, 1199, 146),
|
||||||
|
color=(62, 61, 60),
|
||||||
|
button=(1095, 86, 1179, 126),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
OCR_ASSIGNMENT_LIST = ButtonWrapper(
|
||||||
|
name='OCR_ASSIGNMENT_LIST',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png',
|
||||||
|
area=(141, 160, 502, 621),
|
||||||
|
search=(121, 140, 522, 641),
|
||||||
|
color=(202, 200, 194),
|
||||||
|
button=(141, 160, 502, 621),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
OCR_ASSIGNMENT_TIME = ButtonWrapper(
|
||||||
|
name='OCR_ASSIGNMENT_TIME',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png',
|
||||||
|
area=(588, 566, 926, 588),
|
||||||
|
search=(568, 546, 946, 608),
|
||||||
|
color=(128, 111, 89),
|
||||||
|
button=(588, 566, 926, 588),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SYNTHESIS_MATERIALS = ButtonWrapper(
|
||||||
|
name='SYNTHESIS_MATERIALS',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/assignment/ui/SYNTHESIS_MATERIALS.png',
|
||||||
|
area=(558, 85, 748, 135),
|
||||||
|
search=(538, 65, 768, 155),
|
||||||
|
color=(230, 229, 225),
|
||||||
|
button=(558, 85, 748, 135),
|
||||||
|
),
|
||||||
|
)
|
126
tasks/assignment/assignment.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from module.logger import logger
|
||||||
|
from module.ocr.ocr import Duration
|
||||||
|
from tasks.assignment.assets.assets_assignment_claim import CLAIM
|
||||||
|
from tasks.assignment.assets.assets_assignment_dispatch import EMPTY_SLOT
|
||||||
|
from tasks.assignment.assets.assets_assignment_ui import (DISPATCHED,
|
||||||
|
OCR_ASSIGNMENT_TIME)
|
||||||
|
from tasks.assignment.claim import AssignmentClaim
|
||||||
|
from tasks.assignment.keywords import *
|
||||||
|
from tasks.base.page import page_assignment
|
||||||
|
|
||||||
|
|
||||||
|
class Assignment(AssignmentClaim):
|
||||||
|
def run(self, assignments: list[AssignmentEntry] = None, duration: int = None):
|
||||||
|
if assignments is None:
|
||||||
|
assignments = [AssignmentEntry.find(
|
||||||
|
x.strip()) for x in self.config.Assignment_Filter.split('>')]
|
||||||
|
if duration is None:
|
||||||
|
duration = self.config.Assignment_Duration
|
||||||
|
|
||||||
|
self.ui_ensure(page_assignment)
|
||||||
|
# Iterate in user-specified order, return undispatched ones
|
||||||
|
undispatched = list(self._check_inlist(assignments, duration))
|
||||||
|
_, _, total = self._limit_status
|
||||||
|
# There are unchecked assignments
|
||||||
|
if total > len(self.dispatched):
|
||||||
|
self._check_all()
|
||||||
|
_, remain, _ = self._limit_status
|
||||||
|
for assignment in undispatched[:remain]:
|
||||||
|
self.goto_entry(assignment)
|
||||||
|
self.dispatch(assignment, duration, check_limit=False)
|
||||||
|
if remain < len(undispatched):
|
||||||
|
logger.warning(
|
||||||
|
f'The following assignments can not be dispatched due to limit: {", ".join([x.name for x in undispatched])}')
|
||||||
|
self._dispatch_remain(duration, remain - len(undispatched))
|
||||||
|
|
||||||
|
# Scheduler
|
||||||
|
delay = min(self.dispatched.values())
|
||||||
|
logger.info(f'Delay assignment check to {str(delay)}')
|
||||||
|
self.config.task_delay(target=delay)
|
||||||
|
|
||||||
|
def _check_inlist(self, assignments: list[AssignmentEntry], duration: int):
|
||||||
|
"""
|
||||||
|
Dispatch assignments according to user config
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assignments (list[AssignmentEntry]): user specified assignments
|
||||||
|
duration (int): user specified duration
|
||||||
|
"""
|
||||||
|
if not assignments:
|
||||||
|
return
|
||||||
|
logger.hr('Assignment check inlist', level=2)
|
||||||
|
logger.info(
|
||||||
|
f'User specified assignments: {", ".join([x.name for x in assignments])}')
|
||||||
|
for assignment in assignments:
|
||||||
|
self.goto_entry(assignment)
|
||||||
|
if self.appear(CLAIM):
|
||||||
|
self.claim(assignment, duration, should_redispatch=True)
|
||||||
|
continue
|
||||||
|
if self.appear(DISPATCHED):
|
||||||
|
self.dispatched[assignment] = datetime.now() + Duration(
|
||||||
|
OCR_ASSIGNMENT_TIME).ocr_single_line(self.device.image)
|
||||||
|
continue
|
||||||
|
if self.appear(EMPTY_SLOT):
|
||||||
|
dispatched = self.dispatch(assignment, duration)
|
||||||
|
if not dispatched:
|
||||||
|
yield assignment
|
||||||
|
|
||||||
|
def _check_all(self):
|
||||||
|
"""
|
||||||
|
States of assignments from top to bottom are in following order:
|
||||||
|
1. Claimable
|
||||||
|
2. Dispatched
|
||||||
|
3. Dispatchable
|
||||||
|
Break when a dispatchable assignment is encountered
|
||||||
|
"""
|
||||||
|
logger.hr('Assignment check all', level=2)
|
||||||
|
for group in self._iter_groups():
|
||||||
|
self.goto_group(group)
|
||||||
|
entries = self._iter_entries()
|
||||||
|
for _ in range(len(group.entries)):
|
||||||
|
assignment = next(entries)
|
||||||
|
if assignment in self.dispatched:
|
||||||
|
continue
|
||||||
|
self.goto_entry(assignment)
|
||||||
|
if self.appear(CLAIM):
|
||||||
|
self.claim(assignment, None, should_redispatch=False)
|
||||||
|
continue
|
||||||
|
if self.appear(DISPATCHED):
|
||||||
|
self.dispatched[assignment] = datetime.now() + Duration(
|
||||||
|
OCR_ASSIGNMENT_TIME).ocr_single_line(self.device.image)
|
||||||
|
continue
|
||||||
|
if self.appear(EMPTY_SLOT):
|
||||||
|
break
|
||||||
|
|
||||||
|
def _dispatch_remain(self, duration: int, remain: int):
|
||||||
|
"""
|
||||||
|
Dispatch assignments according to preset priority
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration (int): user specified duration
|
||||||
|
remain (int):
|
||||||
|
The number of remaining assignments after
|
||||||
|
processing the ones specified by user
|
||||||
|
"""
|
||||||
|
if remain <= 0:
|
||||||
|
return
|
||||||
|
logger.hr('Assignment dispatch remain', level=2)
|
||||||
|
logger.warning(f'{remain} remain')
|
||||||
|
logger.info(
|
||||||
|
'Dispatch remaining assignments according to preset priority')
|
||||||
|
group_priority = (
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials
|
||||||
|
)
|
||||||
|
for group in group_priority:
|
||||||
|
for assignment in group.entries:
|
||||||
|
if assignment in self.dispatched:
|
||||||
|
continue
|
||||||
|
self.goto_entry(assignment)
|
||||||
|
self.dispatch(assignment, duration, check_limit=False)
|
||||||
|
remain -= 1
|
||||||
|
if remain <= 0:
|
||||||
|
return
|
68
tasks/assignment/claim.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from module.base.timer import Timer
|
||||||
|
from module.ocr.ocr import Duration
|
||||||
|
from tasks.assignment.assets.assets_assignment_claim import *
|
||||||
|
from tasks.assignment.assets.assets_assignment_dispatch import EMPTY_SLOT
|
||||||
|
from tasks.assignment.assets.assets_assignment_ui import DISPATCHED
|
||||||
|
from tasks.assignment.dispatch import AssignmentDispatch
|
||||||
|
from tasks.assignment.keywords import AssignmentEntry
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentClaim(AssignmentDispatch):
|
||||||
|
def claim(self, assignment: AssignmentEntry, duration_expected: int, should_redispatch: bool):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
assignment (AssignmentEntry):
|
||||||
|
duration_expected (int): user specified duration
|
||||||
|
should_redispatch (bool):
|
||||||
|
|
||||||
|
Pages:
|
||||||
|
in: CLAIM
|
||||||
|
out: DISPATCHED(succeed) or EMPTY_SLOT(fail)
|
||||||
|
"""
|
||||||
|
redispatched = False
|
||||||
|
skip_first_screenshot = True
|
||||||
|
counter = Timer(1, count=4).start()
|
||||||
|
while 1:
|
||||||
|
if skip_first_screenshot:
|
||||||
|
skip_first_screenshot = False
|
||||||
|
else:
|
||||||
|
self.device.screenshot()
|
||||||
|
# End
|
||||||
|
if self.appear(EMPTY_SLOT) or self.appear(DISPATCHED):
|
||||||
|
if counter.reached():
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
# Claim reward
|
||||||
|
if self.appear(CLAIM, interval=2):
|
||||||
|
self.device.click(CLAIM)
|
||||||
|
continue
|
||||||
|
if self.appear(REDISPATCH, interval=2):
|
||||||
|
redispatched = should_redispatch and self._is_duration_expected(
|
||||||
|
duration_expected)
|
||||||
|
if redispatched:
|
||||||
|
self._confirm_assignment(REDISPATCH)
|
||||||
|
self.dispatched[assignment] = datetime.now(
|
||||||
|
) + timedelta(hours=duration_expected)
|
||||||
|
else:
|
||||||
|
self.device.click(CLOSE_REPORT)
|
||||||
|
continue
|
||||||
|
# Re-select duration and dispatch
|
||||||
|
if should_redispatch and not redispatched:
|
||||||
|
self.dispatch(assignment, duration_expected, check_limit=False)
|
||||||
|
|
||||||
|
def _is_duration_expected(self, duration: int) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether duration in assignment report page
|
||||||
|
is the same as user specified
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration (int): user specified duration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: If same.
|
||||||
|
"""
|
||||||
|
duration_reported: timedelta = Duration(
|
||||||
|
OCR_ASSIGNMENT_REPORT_TIME).ocr_single_line(self.device.image)
|
||||||
|
return duration_reported.total_seconds() == duration*3600
|
104
tasks/assignment/dispatch.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from module.base.timer import Timer
|
||||||
|
from module.logger import logger
|
||||||
|
from tasks.assignment.assets.assets_assignment_dispatch import *
|
||||||
|
from tasks.assignment.assets.assets_assignment_ui import DISPATCHED
|
||||||
|
from tasks.assignment.keywords import *
|
||||||
|
from tasks.assignment.ui import AssignmentSwitch, AssignmentUI
|
||||||
|
|
||||||
|
ASSIGNMENT_DURATION_SWITCH = AssignmentSwitch(
|
||||||
|
'AssignmentDurationSwitch',
|
||||||
|
(160, 130, 100)
|
||||||
|
)
|
||||||
|
ASSIGNMENT_DURATION_SWITCH.add_state('4', DURATION_4)
|
||||||
|
ASSIGNMENT_DURATION_SWITCH.add_state('8', DURATION_8)
|
||||||
|
ASSIGNMENT_DURATION_SWITCH.add_state('12', DURATION_12)
|
||||||
|
ASSIGNMENT_DURATION_SWITCH.add_state('20', DURATION_20)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentDispatch(AssignmentUI):
|
||||||
|
dispatched: dict[AssignmentEntry, datetime] = dict()
|
||||||
|
|
||||||
|
def dispatch(self, assignment: AssignmentEntry, duration: int, check_limit: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
assignment (AssignmentEntry):
|
||||||
|
duration (int): user specified duration
|
||||||
|
check_limit (bool):
|
||||||
|
|
||||||
|
Pages:
|
||||||
|
in: EMPTY_SLOT
|
||||||
|
out: DISPATCHED(succeed) or EMPTY_SLOT(fail)
|
||||||
|
"""
|
||||||
|
if check_limit and self._limit_status[1] == 0:
|
||||||
|
return False
|
||||||
|
self._select_characters()
|
||||||
|
self._select_duration(duration)
|
||||||
|
self._confirm_assignment(CONFIRM_ASSIGNMENT)
|
||||||
|
self.dispatched[assignment] = datetime.now() + \
|
||||||
|
timedelta(hours=duration)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _select_characters(self):
|
||||||
|
"""
|
||||||
|
Pages:
|
||||||
|
in: EMPTY_SLOT
|
||||||
|
out: CHARACTER_LIST
|
||||||
|
"""
|
||||||
|
skip_first_screenshot = True
|
||||||
|
click_timer = Timer(1, count=3).start()
|
||||||
|
while 1:
|
||||||
|
if skip_first_screenshot:
|
||||||
|
skip_first_screenshot = False
|
||||||
|
else:
|
||||||
|
self.device.screenshot()
|
||||||
|
# End
|
||||||
|
if not self.appear(EMPTY_SLOT):
|
||||||
|
logger.info('Assignment slots are all filled')
|
||||||
|
break
|
||||||
|
# Ensure character list
|
||||||
|
if not self.appear(CHARACTER_LIST):
|
||||||
|
if click_timer.reached_and_reset():
|
||||||
|
self.device.click(EMPTY_SLOT)
|
||||||
|
continue
|
||||||
|
# Select
|
||||||
|
if click_timer.reached_and_reset():
|
||||||
|
self.device.click(CHARACTER_1)
|
||||||
|
self.device.click(CHARACTER_2)
|
||||||
|
|
||||||
|
def _select_duration(self, duration: int):
|
||||||
|
if duration not in {4, 8, 12, 20}:
|
||||||
|
logger.warning(
|
||||||
|
f'Duration {duration} is out of scope, reset it to 20')
|
||||||
|
duration = 20
|
||||||
|
ASSIGNMENT_DURATION_SWITCH.set(str(duration), self)
|
||||||
|
|
||||||
|
def _confirm_assignment(self, dispatch_button: ButtonWrapper) -> bool:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
dispatch_button (ButtonWrapper):
|
||||||
|
Button to be clicked, CONFIRM_ASSIGNMENT or REDISPATCH
|
||||||
|
|
||||||
|
Pages:
|
||||||
|
in: CONFIRM_ASSIGNMENT or REDISPATCH
|
||||||
|
out: DISPATCHED
|
||||||
|
"""
|
||||||
|
skip_first_screenshot = True
|
||||||
|
counter = Timer(1, count=3).start()
|
||||||
|
while 1:
|
||||||
|
if skip_first_screenshot:
|
||||||
|
skip_first_screenshot = False
|
||||||
|
else:
|
||||||
|
self.device.screenshot()
|
||||||
|
# End
|
||||||
|
if self.appear(DISPATCHED) and self.appear(ASSIGNMENT_STARTED_CHECK):
|
||||||
|
if counter.reached():
|
||||||
|
logger.info(f'Assignment started')
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
# Click
|
||||||
|
if self.appear(dispatch_button, interval=2):
|
||||||
|
self.device.click(dispatch_button)
|
||||||
|
counter.reset()
|
||||||
|
continue
|
30
tasks/assignment/keywords/__init__.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import tasks.assignment.keywords.entry as KEYWORDS_ASSIGNMENT_ENTRY
|
||||||
|
import tasks.assignment.keywords.group as KEYWORDS_ASSIGNMENT_GROUP
|
||||||
|
from tasks.assignment.keywords.classes import AssignmentGroup, AssignmentEntry
|
||||||
|
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials.entries = (
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Nine_Billion_Names,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Destruction_of_the_Destroyer,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Winter_Soldiers,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Born_to_Obey,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Root_Out_the_Turpitude,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Fire_Lord_Inflames_Blades_of_War,
|
||||||
|
)
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits.entries = (
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Nameless_Land_Nameless_People,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Akashic_Records,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.The_Invisible_Hand,
|
||||||
|
)
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials.entries = (
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Abandoned_and_Insulted,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.Spring_of_Life,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.The_Land_of_Gold,
|
||||||
|
KEYWORDS_ASSIGNMENT_ENTRY.The_Blossom_in_the_Storm,
|
||||||
|
)
|
||||||
|
for group in (
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials,
|
||||||
|
):
|
||||||
|
for entry in group.entries:
|
||||||
|
entry.group = group
|
20
tasks/assignment/keywords/classes.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from module.ocr.keyword import Keyword
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AssignmentGroup(Keyword):
|
||||||
|
instances: ClassVar = {}
|
||||||
|
entries: tuple[AssignmentEntry] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AssignmentEntry(Keyword):
|
||||||
|
instances: ClassVar = {}
|
||||||
|
group: AssignmentGroup = None
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return super().__hash__()
|
||||||
|
|
109
tasks/assignment/keywords/entry.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from .classes import AssignmentEntry
|
||||||
|
|
||||||
|
# This file was auto-generated, do not modify it manually. To generate:
|
||||||
|
# ``` python -m dev_tools.keyword_extract ```
|
||||||
|
|
||||||
|
Nine_Billion_Names = AssignmentEntry(
|
||||||
|
id=1,
|
||||||
|
name='Nine_Billion_Names',
|
||||||
|
cn='九十亿个名字',
|
||||||
|
cht='九十億個名字',
|
||||||
|
en='Nine Billion Names',
|
||||||
|
jp='九十億の御名',
|
||||||
|
)
|
||||||
|
Destruction_of_the_Destroyer = AssignmentEntry(
|
||||||
|
id=2,
|
||||||
|
name='Destruction_of_the_Destroyer',
|
||||||
|
cn='毁灭者的覆灭',
|
||||||
|
cht='毀滅者的覆滅',
|
||||||
|
en='Destruction of the Destroyer',
|
||||||
|
jp='壊滅者の覆没',
|
||||||
|
)
|
||||||
|
Winter_Soldiers = AssignmentEntry(
|
||||||
|
id=3,
|
||||||
|
name='Winter_Soldiers',
|
||||||
|
cn='寒冬的战士们',
|
||||||
|
cht='寒冬的戰士們',
|
||||||
|
en='Winter Soldiers',
|
||||||
|
jp='寒冬の戦士たち',
|
||||||
|
)
|
||||||
|
Born_to_Obey = AssignmentEntry(
|
||||||
|
id=4,
|
||||||
|
name='Born_to_Obey',
|
||||||
|
cn='生而服从',
|
||||||
|
cht='生而服從',
|
||||||
|
en='Born to Obey',
|
||||||
|
jp='生まれながらに服従する',
|
||||||
|
)
|
||||||
|
Root_Out_the_Turpitude = AssignmentEntry(
|
||||||
|
id=5,
|
||||||
|
name='Root_Out_the_Turpitude',
|
||||||
|
cn='根除恶孽',
|
||||||
|
cht='根除惡孽',
|
||||||
|
en='Root Out the Turpitude',
|
||||||
|
jp='悪孽を根絶やしに',
|
||||||
|
)
|
||||||
|
Fire_Lord_Inflames_Blades_of_War = AssignmentEntry(
|
||||||
|
id=6,
|
||||||
|
name='Fire_Lord_Inflames_Blades_of_War',
|
||||||
|
cn='火帝动炉销剑戟',
|
||||||
|
cht='火帝動爐銷劍戟',
|
||||||
|
en='Fire Lord Inflames Blades of War',
|
||||||
|
jp='剣戟を焼却する火帝炉',
|
||||||
|
)
|
||||||
|
Nameless_Land_Nameless_People = AssignmentEntry(
|
||||||
|
id=7,
|
||||||
|
name='Nameless_Land_Nameless_People',
|
||||||
|
cn='无名之地,无名之人',
|
||||||
|
cht='無名之地,無名之人',
|
||||||
|
en='Nameless Land, Nameless People',
|
||||||
|
jp='無名の地、無名の人',
|
||||||
|
)
|
||||||
|
Akashic_Records = AssignmentEntry(
|
||||||
|
id=8,
|
||||||
|
name='Akashic_Records',
|
||||||
|
cn='阿卡夏记录',
|
||||||
|
cht='阿卡夏紀錄',
|
||||||
|
en='Akashic Records',
|
||||||
|
jp='アーカーシャの記録',
|
||||||
|
)
|
||||||
|
The_Invisible_Hand = AssignmentEntry(
|
||||||
|
id=9,
|
||||||
|
name='The_Invisible_Hand',
|
||||||
|
cn='看不见的手',
|
||||||
|
cht='看不見的手',
|
||||||
|
en='The Invisible Hand',
|
||||||
|
jp='見えざる手',
|
||||||
|
)
|
||||||
|
Abandoned_and_Insulted = AssignmentEntry(
|
||||||
|
id=10,
|
||||||
|
name='Abandoned_and_Insulted',
|
||||||
|
cn='被废弃与损害的',
|
||||||
|
cht='被廢棄與損害的',
|
||||||
|
en='Abandoned and Insulted',
|
||||||
|
jp='捨てられしものと傷つけられしもの',
|
||||||
|
)
|
||||||
|
Spring_of_Life = AssignmentEntry(
|
||||||
|
id=11,
|
||||||
|
name='Spring_of_Life',
|
||||||
|
cn='生命之泉',
|
||||||
|
cht='生命之泉',
|
||||||
|
en='Spring of Life',
|
||||||
|
jp='生命の泉',
|
||||||
|
)
|
||||||
|
The_Land_of_Gold = AssignmentEntry(
|
||||||
|
id=12,
|
||||||
|
name='The_Land_of_Gold',
|
||||||
|
cn='黄金大地',
|
||||||
|
cht='黃金大地',
|
||||||
|
en='The Land of Gold',
|
||||||
|
jp='黄金の大地',
|
||||||
|
)
|
||||||
|
The_Blossom_in_the_Storm = AssignmentEntry(
|
||||||
|
id=13,
|
||||||
|
name='The_Blossom_in_the_Storm',
|
||||||
|
cn='风暴中怒放的花',
|
||||||
|
cht='風暴中怒放的花',
|
||||||
|
en='The Blossom in the Storm',
|
||||||
|
jp='嵐の中で咲き誇る花',
|
||||||
|
)
|
29
tasks/assignment/keywords/group.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from .classes import AssignmentGroup
|
||||||
|
|
||||||
|
# This file was auto-generated, do not modify it manually. To generate:
|
||||||
|
# ``` python -m dev_tools.keyword_extract ```
|
||||||
|
|
||||||
|
Character_Materials = AssignmentGroup(
|
||||||
|
id=1,
|
||||||
|
name='Character_Materials',
|
||||||
|
cn='专属材料',
|
||||||
|
cht='專屬素材',
|
||||||
|
en='Character Materials',
|
||||||
|
jp='専用素材',
|
||||||
|
)
|
||||||
|
EXP_Materials_Credits = AssignmentGroup(
|
||||||
|
id=2,
|
||||||
|
name='EXP_Materials_Credits',
|
||||||
|
cn='经验材料/信用点',
|
||||||
|
cht='經驗素材/信用點',
|
||||||
|
en='EXP Materials/Credits',
|
||||||
|
jp='経験値素材/信用ポイント',
|
||||||
|
)
|
||||||
|
Synthesis_Materials = AssignmentGroup(
|
||||||
|
id=3,
|
||||||
|
name='Synthesis_Materials',
|
||||||
|
cn='合成材料',
|
||||||
|
cht='合成材料',
|
||||||
|
en='Synthesis Materials',
|
||||||
|
jp='合成材料',
|
||||||
|
)
|
162
tasks/assignment/ui.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import re
|
||||||
|
from functools import cached_property
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
from module.base.base import ModuleBase
|
||||||
|
from module.base.timer import Timer
|
||||||
|
from module.logger import logger
|
||||||
|
from module.ocr.ocr import DigitCounter, Ocr
|
||||||
|
from module.ui.draggable_list import DraggableList
|
||||||
|
from module.ui.switch import Switch
|
||||||
|
from tasks.assignment.assets.assets_assignment_ui import *
|
||||||
|
from tasks.assignment.keywords import *
|
||||||
|
from tasks.base.page import page_assignment
|
||||||
|
from tasks.base.ui import UI
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentSwitch(Switch):
|
||||||
|
def __init__(self, name, active_color: tuple[int, int, int], is_selector=True):
|
||||||
|
super().__init__(name, is_selector)
|
||||||
|
self.active_color = active_color
|
||||||
|
|
||||||
|
def get(self, main: ModuleBase):
|
||||||
|
"""
|
||||||
|
Use image_color_count instead to determine whether the button is selected/active
|
||||||
|
|
||||||
|
Args:
|
||||||
|
main (ModuleBase):
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: state name or 'unknown'.
|
||||||
|
"""
|
||||||
|
for data in self.state_list:
|
||||||
|
if main.image_color_count(data['check_button'], self.active_color):
|
||||||
|
return data['state']
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentOcr(Ocr):
|
||||||
|
OCR_REPLACE = {
|
||||||
|
'ch': [
|
||||||
|
(KEYWORDS_ASSIGNMENT_ENTRY.Winter_Soldiers.name, '[黑]冬的战士们'),
|
||||||
|
(KEYWORDS_ASSIGNMENT_ENTRY.Born_to_Obey.name, '[牛]而服从'),
|
||||||
|
(KEYWORDS_ASSIGNMENT_ENTRY.Root_Out_the_Turpitude.name,
|
||||||
|
'根除恶[擎薯尊掌鞋]?'),
|
||||||
|
(KEYWORDS_ASSIGNMENT_ENTRY.Akashic_Records.name, '阿[未][夏复]记录'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def ocr_regex(self) -> re.Pattern | None:
|
||||||
|
rules = AssignmentOcr.OCR_REPLACE.get(self.lang)
|
||||||
|
if rules is None:
|
||||||
|
return None
|
||||||
|
return re.compile('|'.join('(?P<%s>%s)' % pair for pair in rules))
|
||||||
|
|
||||||
|
def after_process(self, result: str):
|
||||||
|
result = super().after_process(result)
|
||||||
|
if self.ocr_regex is None:
|
||||||
|
return result
|
||||||
|
matched = self.ocr_regex.fullmatch(result)
|
||||||
|
if matched is None:
|
||||||
|
return result
|
||||||
|
matched = getattr(KEYWORDS_ASSIGNMENT_ENTRY, matched.lastgroup)
|
||||||
|
matched = getattr(matched, self.lang)
|
||||||
|
logger.attr(name=f'{self.name} after_process',
|
||||||
|
text=f'{result} -> {matched}')
|
||||||
|
return matched
|
||||||
|
|
||||||
|
|
||||||
|
ASSIGNMENT_TOP_SWITCH = AssignmentSwitch(
|
||||||
|
'AssignmentTopSwitch',
|
||||||
|
(240, 240, 240)
|
||||||
|
)
|
||||||
|
ASSIGNMENT_TOP_SWITCH.add_state(
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
|
||||||
|
check_button=CHARACTER_MATERIALS
|
||||||
|
)
|
||||||
|
ASSIGNMENT_TOP_SWITCH.add_state(
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
|
||||||
|
check_button=EXP_MATERIALS_CREDITS
|
||||||
|
)
|
||||||
|
ASSIGNMENT_TOP_SWITCH.add_state(
|
||||||
|
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials,
|
||||||
|
check_button=SYNTHESIS_MATERIALS
|
||||||
|
)
|
||||||
|
|
||||||
|
ASSIGNMENT_ENTRY_LIST = DraggableList(
|
||||||
|
'AssignmentEntryList',
|
||||||
|
keyword_class=AssignmentEntry,
|
||||||
|
ocr_class=AssignmentOcr,
|
||||||
|
search_button=OCR_ASSIGNMENT_LIST,
|
||||||
|
active_color=(40, 40, 40)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentUI(UI):
|
||||||
|
def goto_group(self, group: AssignmentGroup):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
group: AssignmentGroup
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
self = AssignmentUI('src')
|
||||||
|
self.device.screenshot()
|
||||||
|
self.goto_group(KEYWORDS_ASSIGNMENT_GROUP.Character_Materials)
|
||||||
|
"""
|
||||||
|
self.ui_ensure(page_assignment)
|
||||||
|
logger.hr('Assignment group goto', level=3)
|
||||||
|
if ASSIGNMENT_TOP_SWITCH.set(group, main=self):
|
||||||
|
self._wait_until_entry_loaded()
|
||||||
|
|
||||||
|
def goto_entry(self, entry: AssignmentEntry):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
entry: AssignmentEntry
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
self = AssignmentUI('src')
|
||||||
|
self.device.screenshot()
|
||||||
|
self.goto_entry(KEYWORDS_ASSIGNMENT_ENTRY.Nameless_Land_Nameless_People)
|
||||||
|
"""
|
||||||
|
self.goto_group(entry.group)
|
||||||
|
ASSIGNMENT_ENTRY_LIST.select_row(entry, self)
|
||||||
|
|
||||||
|
def _wait_until_entry_loaded(self):
|
||||||
|
skip_first_screenshot = True
|
||||||
|
timeout = Timer(2, count=3).start()
|
||||||
|
while 1:
|
||||||
|
if skip_first_screenshot:
|
||||||
|
skip_first_screenshot = False
|
||||||
|
else:
|
||||||
|
self.device.screenshot()
|
||||||
|
|
||||||
|
if timeout.reached():
|
||||||
|
logger.warning('Wait entry loaded timeout')
|
||||||
|
break
|
||||||
|
# Maybe not reliable
|
||||||
|
if self.image_color_count(ENTRY_LOADED, (35, 35, 35)):
|
||||||
|
logger.info('Entry loaded')
|
||||||
|
break
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _limit_status(self) -> tuple[int, int, int]:
|
||||||
|
self.device.screenshot()
|
||||||
|
return DigitCounter(OCR_ASSIGNMENT_LIMIT).ocr_single_line(self.device.image)
|
||||||
|
|
||||||
|
def _iter_groups(self) -> Iterator[AssignmentGroup]:
|
||||||
|
for state in ASSIGNMENT_TOP_SWITCH.state_list:
|
||||||
|
yield state['state']
|
||||||
|
|
||||||
|
def _iter_entries(self) -> Iterator[AssignmentEntry]:
|
||||||
|
"""
|
||||||
|
Iterate entries from top to bottom
|
||||||
|
"""
|
||||||
|
while 1:
|
||||||
|
ASSIGNMENT_ENTRY_LIST.load_rows(self)
|
||||||
|
for button in ASSIGNMENT_ENTRY_LIST.cur_buttons:
|
||||||
|
yield button.matched_keyword
|
||||||
|
ASSIGNMENT_ENTRY_LIST.drag_page('down', self)
|
||||||
|
self.wait_until_stable(ASSIGNMENT_ENTRY_LIST.search_button, timer=Timer(
|
||||||
|
0, count=0), timeout=Timer(1.5, count=5))
|
@ -3,6 +3,16 @@ from module.base.button import Button, ButtonWrapper
|
|||||||
# This file was auto-generated, do not modify it manually. To generate:
|
# This file was auto-generated, do not modify it manually. To generate:
|
||||||
# ``` python -m dev_tools.button_extract ```
|
# ``` python -m dev_tools.button_extract ```
|
||||||
|
|
||||||
|
ASSIGNMENT_CHECK = ButtonWrapper(
|
||||||
|
name='ASSIGNMENT_CHECK',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/base/page/ASSIGNMENT_CHECK.png',
|
||||||
|
area=(45, 21, 70, 53),
|
||||||
|
search=(25, 1, 90, 73),
|
||||||
|
color=(162, 145, 112),
|
||||||
|
button=(45, 21, 70, 53),
|
||||||
|
),
|
||||||
|
)
|
||||||
BATTLE_PASS_CHECK = ButtonWrapper(
|
BATTLE_PASS_CHECK = ButtonWrapper(
|
||||||
name='BATTLE_PASS_CHECK',
|
name='BATTLE_PASS_CHECK',
|
||||||
share=Button(
|
share=Button(
|
||||||
@ -213,6 +223,16 @@ MENU_CHECK = ButtonWrapper(
|
|||||||
button=(1222, 638, 1252, 669),
|
button=(1222, 638, 1252, 669),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
MENU_GOTO_ASSIGNMENT = ButtonWrapper(
|
||||||
|
name='MENU_GOTO_ASSIGNMENT',
|
||||||
|
share=Button(
|
||||||
|
file='./assets/share/base/page/MENU_GOTO_ASSIGNMENT.png',
|
||||||
|
area=(1090, 269, 1153, 328),
|
||||||
|
search=(1070, 249, 1173, 348),
|
||||||
|
color=(71, 71, 74),
|
||||||
|
button=(1090, 269, 1153, 328),
|
||||||
|
),
|
||||||
|
)
|
||||||
MENU_GOTO_CAMERA = ButtonWrapper(
|
MENU_GOTO_CAMERA = ButtonWrapper(
|
||||||
name='MENU_GOTO_CAMERA',
|
name='MENU_GOTO_CAMERA',
|
||||||
share=Button(
|
share=Button(
|
||||||
|
@ -135,3 +135,8 @@ page_menu.link(MENU_GOTO_CAMERA, destination=page_camera)
|
|||||||
page_synthesize = Page(SYNTHESIZE_CHECK)
|
page_synthesize = Page(SYNTHESIZE_CHECK)
|
||||||
page_synthesize.link(CLOSE, destination=page_menu)
|
page_synthesize.link(CLOSE, destination=page_menu)
|
||||||
page_menu.link(MENU_GOTO_SYNTHESIZE, destination=page_synthesize)
|
page_menu.link(MENU_GOTO_SYNTHESIZE, destination=page_synthesize)
|
||||||
|
|
||||||
|
# Assignment
|
||||||
|
page_assignment = Page(ASSIGNMENT_CHECK)
|
||||||
|
page_assignment.link(CLOSE, destination=page_main)
|
||||||
|
page_menu.link(MENU_GOTO_ASSIGNMENT, destination=page_assignment)
|
||||||
|
@ -92,10 +92,10 @@ class DraggableDungeonList(DraggableList):
|
|||||||
|
|
||||||
|
|
||||||
DUNGEON_NAV_LIST = DraggableList(
|
DUNGEON_NAV_LIST = DraggableList(
|
||||||
'DungeonNavList', keyword_class=DungeonNav, ocr_class=OcrDungeonNav, search_button=OCR_DUNGEON_NAV)
|
'DungeonNavList', keyword_class=DungeonNav, ocr_class=OcrDungeonNav, search_button=OCR_DUNGEON_NAV, active_color=(190, 175, 124))
|
||||||
DUNGEON_LIST = DraggableDungeonList(
|
DUNGEON_LIST = DraggableDungeonList(
|
||||||
'DungeonList', keyword_class=[DungeonList, DungeonEntrance],
|
'DungeonList', keyword_class=[DungeonList, DungeonEntrance],
|
||||||
ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST)
|
ocr_class=OcrDungeonList, search_button=OCR_DUNGEON_LIST, active_color=(190, 175, 124))
|
||||||
|
|
||||||
|
|
||||||
class DungeonUI(UI):
|
class DungeonUI(UI):
|
||||||
|