Add: Task assignment

This commit is contained in:
Zebartin 2023-06-19 08:39:41 +08:00
parent 55f82245b3
commit 346e7a1578
48 changed files with 981 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 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: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -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__':

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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

View 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),
),
)

View 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),
),
)

View 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),
),
)

View 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
View 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

View 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

View 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

View 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__()

View 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='嵐の中で咲き誇る花',
)

View 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
View 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))

View File

@ -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(

View File

@ -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)

View File

@ -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):