diff --git a/assets/cn/assignment/dispatch/ASSIGNMENT_START.SEARCH.png b/assets/cn/assignment/dispatch/ASSIGNMENT_START.SEARCH.png new file mode 100644 index 000000000..cfe8dcbba Binary files /dev/null and b/assets/cn/assignment/dispatch/ASSIGNMENT_START.SEARCH.png differ diff --git a/assets/cn/assignment/dispatch/CHARACTER_SUPPORT_LIST.png b/assets/cn/assignment/dispatch/CHARACTER_SUPPORT_LIST.png new file mode 100644 index 000000000..a60e2cca0 Binary files /dev/null and b/assets/cn/assignment/dispatch/CHARACTER_SUPPORT_LIST.png differ diff --git a/assets/en/assignment/dispatch/ASSIGNMENT_START.SEARCH.png b/assets/en/assignment/dispatch/ASSIGNMENT_START.SEARCH.png new file mode 100644 index 000000000..8104994df Binary files /dev/null and b/assets/en/assignment/dispatch/ASSIGNMENT_START.SEARCH.png differ diff --git a/assets/en/assignment/dispatch/CHARACTER_SUPPORT_LIST.png b/assets/en/assignment/dispatch/CHARACTER_SUPPORT_LIST.png new file mode 100644 index 000000000..c242d5c98 Binary files /dev/null and b/assets/en/assignment/dispatch/CHARACTER_SUPPORT_LIST.png differ diff --git a/assets/share/assignment/claim/REPORT.png b/assets/share/assignment/claim/REPORT.png new file mode 100644 index 000000000..8351c9a92 Binary files /dev/null and b/assets/share/assignment/claim/REPORT.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_SUPPORT.png b/assets/share/assignment/dispatch/CHARACTER_SUPPORT.png new file mode 100644 index 000000000..d28c642f3 Binary files /dev/null and b/assets/share/assignment/dispatch/CHARACTER_SUPPORT.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_SUPPORT_SELECTED.png b/assets/share/assignment/dispatch/CHARACTER_SUPPORT_SELECTED.png new file mode 100644 index 000000000..bffd93c18 Binary files /dev/null and b/assets/share/assignment/dispatch/CHARACTER_SUPPORT_SELECTED.png differ diff --git a/assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png b/assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png index 4f96e9044..3935824f7 100644 Binary files a/assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png and b/assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png differ diff --git a/assets/share/assignment/dispatch/EMPTY_SLOT_SUPPORT.png b/assets/share/assignment/dispatch/EMPTY_SLOT_SUPPORT.png new file mode 100644 index 000000000..20a119000 Binary files /dev/null and b/assets/share/assignment/dispatch/EMPTY_SLOT_SUPPORT.png differ diff --git a/assets/share/assignment/ui/CHARACTER_MATERIALS.png b/assets/share/assignment/ui/CHARACTER_MATERIALS.png deleted file mode 100644 index f851dabde..000000000 Binary files a/assets/share/assignment/ui/CHARACTER_MATERIALS.png and /dev/null differ diff --git a/assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png b/assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png deleted file mode 100644 index 5745314cc..000000000 Binary files a/assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png and /dev/null differ diff --git a/assets/share/assignment/ui/LOCKED.png b/assets/share/assignment/ui/LOCKED.png new file mode 100644 index 000000000..47a2fbb0f Binary files /dev/null and b/assets/share/assignment/ui/LOCKED.png differ diff --git a/assets/share/assignment/ui/OCR_ASSIGNMENT_GROUP_LIST.png b/assets/share/assignment/ui/OCR_ASSIGNMENT_GROUP_LIST.png index 604c8bb02..7948b5001 100644 Binary files a/assets/share/assignment/ui/OCR_ASSIGNMENT_GROUP_LIST.png and b/assets/share/assignment/ui/OCR_ASSIGNMENT_GROUP_LIST.png differ diff --git a/assets/share/assignment/ui/SYNTHESIS_MATERIALS.png b/assets/share/assignment/ui/SYNTHESIS_MATERIALS.png deleted file mode 100644 index a86c08e9c..000000000 Binary files a/assets/share/assignment/ui/SYNTHESIS_MATERIALS.png and /dev/null differ diff --git a/dev_tools/keyword_extract.py b/dev_tools/keyword_extract.py index a8b85aabf..2b6727974 100644 --- a/dev_tools/keyword_extract.py +++ b/dev_tools/keyword_extract.py @@ -1,7 +1,6 @@ import os import re import typing as t -from collections import namedtuple from functools import cached_property from module.base.code_generator import CodeGenerator @@ -254,14 +253,19 @@ class KeywordExtract: self.clear_keywords() 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') + self.load_keywords(['空间站特派']) + self.write_keywords( + keyword_class='AssignmentEventGroup', + output_file='./tasks/assignment/keywords/event_group.py' + ) + for file_name, class_name, output_file in ( + ('ExpeditionGroup.json', 'AssignmentGroup', './tasks/assignment/keywords/group.py'), + ('ExpeditionData.json', 'AssignmentEntry', './tasks/assignment/keywords/entry.py'), + ('ActivityExpedition.json', 'AssignmentEventEntry', './tasks/assignment/keywords/event_entry.py'), ): - file = os.path.join(TextMap.DATA_FOLDER, 'ExcelOutput', keyword.file) + file = os.path.join(TextMap.DATA_FOLDER, 'ExcelOutput', file_name) 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) + self.write_keywords(keyword_class=class_name, output_file=output_file) def generate_map_planes(self): planes = { diff --git a/module/ocr/ocr.py b/module/ocr/ocr.py index 81506bfa7..1a79ed80b 100644 --- a/module/ocr/ocr.py +++ b/module/ocr/ocr.py @@ -375,17 +375,17 @@ class Duration(Ocr): def timedelta_regex(cls, lang): regex_str = { 'cn': r'^(?P.*?)' - r'((?P\d{1,2})天)?' - r'((?P\d{1,2})小时)?' - r'((?P\d{1,2})分钟)?' - r'((?P\d{1,2})秒)?' - r'$', + r'((?P\d{1,2})\s*天\s*)?' + r'((?P\d{1,2})\s*小时\s*)?' + r'((?P\d{1,2})\s*分钟\s*)?' + r'((?P\d{1,2})\s*秒)?' + r'(?P[^天时钟秒]*?)$', 'en': r'^(?P.*?)' r'((?P\d{1,2})\s*d\s*)?' r'((?P\d{1,2})\s*h\s*)?' r'((?P\d{1,2})\s*m\s*)?' r'((?P\d{1,2})\s*s)?' - r'$' + r'(?P[^dhms]*?)$' }[lang] return re.compile(regex_str) diff --git a/module/ui/draggable_list.py b/module/ui/draggable_list.py index 7f8f46c92..806ff35b0 100644 --- a/module/ui/draggable_list.py +++ b/module/ui/draggable_list.py @@ -42,9 +42,11 @@ class DraggableList: self.name = name self.keyword_class = keyword_class self.ocr_class = ocr_class - if isinstance(keyword_class, list): - keyword_class = keyword_class[0] - self.known_rows = list(keyword_class.instances.values()) + if not isinstance(keyword_class, list): + keyword_class = [keyword_class] + self.known_rows = [ + kw for kc in keyword_class for kw in kc.instances.values() + ] self.search_button = search_button self.check_row_order = check_row_order self.active_color = active_color diff --git a/tasks/assignment/assets/assets_assignment_claim.py b/tasks/assignment/assets/assets_assignment_claim.py index 8a7599770..fc395f5d1 100644 --- a/tasks/assignment/assets/assets_assignment_claim.py +++ b/tasks/assignment/assets/assets_assignment_claim.py @@ -43,3 +43,13 @@ REDISPATCH = ButtonWrapper( button=(779, 592, 905, 629), ), ) +REPORT = ButtonWrapper( + name='REPORT', + share=Button( + file='./assets/share/assignment/claim/REPORT.png', + area=(0, 154, 266, 542), + search=(0, 134, 286, 562), + color=(33, 34, 37), + button=(0, 154, 266, 542), + ), +) diff --git a/tasks/assignment/assets/assets_assignment_dispatch.py b/tasks/assignment/assets/assets_assignment_dispatch.py index 3607b873e..9a1fe40ab 100644 --- a/tasks/assignment/assets/assets_assignment_dispatch.py +++ b/tasks/assignment/assets/assets_assignment_dispatch.py @@ -8,14 +8,14 @@ ASSIGNMENT_START = ButtonWrapper( cn=Button( file='./assets/cn/assignment/dispatch/ASSIGNMENT_START.png', area=(581, 321, 699, 349), - search=(561, 301, 719, 369), + search=(573, 299, 707, 412), color=(93, 84, 66), button=(581, 321, 699, 349), ), en=Button( file='./assets/en/assignment/dispatch/ASSIGNMENT_START.png', area=(679, 323, 784, 347), - search=(659, 303, 804, 367), + search=(669, 297, 794, 416), color=(93, 83, 65), button=(679, 323, 784, 347), ), @@ -87,6 +87,43 @@ CHARACTER_LIST = ButtonWrapper( button=(91, 163, 136, 180), ), ) +CHARACTER_SUPPORT = ButtonWrapper( + name='CHARACTER_SUPPORT', + share=Button( + file='./assets/share/assignment/dispatch/CHARACTER_SUPPORT.png', + area=(103, 212, 435, 302), + search=(83, 192, 455, 322), + color=(62, 60, 63), + button=(103, 212, 435, 302), + ), +) +CHARACTER_SUPPORT_LIST = ButtonWrapper( + name='CHARACTER_SUPPORT_LIST', + cn=Button( + file='./assets/cn/assignment/dispatch/CHARACTER_SUPPORT_LIST.png', + area=(91, 166, 171, 186), + search=(71, 146, 191, 206), + color=(147, 146, 143), + button=(91, 166, 171, 186), + ), + en=Button( + file='./assets/en/assignment/dispatch/CHARACTER_SUPPORT_LIST.png', + area=(90, 167, 267, 189), + search=(70, 147, 287, 209), + color=(169, 168, 165), + button=(90, 167, 267, 189), + ), +) +CHARACTER_SUPPORT_SELECTED = ButtonWrapper( + name='CHARACTER_SUPPORT_SELECTED', + share=Button( + file='./assets/share/assignment/dispatch/CHARACTER_SUPPORT_SELECTED.png', + area=(190, 270, 266, 295), + search=(170, 250, 286, 315), + color=(39, 39, 39), + button=(190, 270, 266, 295), + ), +) CONFIRM_ASSIGNMENT = ButtonWrapper( name='CONFIRM_ASSIGNMENT', cn=Button( @@ -149,8 +186,18 @@ EMPTY_SLOT = ButtonWrapper( share=Button( file='./assets/share/assignment/dispatch/EMPTY_SLOT.png', area=(1075, 562, 1110, 597), - search=(1054, 542, 1220, 616), + search=(873, 543, 1099, 609), color=(200, 200, 195), button=(1075, 562, 1110, 597), ), ) +EMPTY_SLOT_SUPPORT = ButtonWrapper( + name='EMPTY_SLOT_SUPPORT', + share=Button( + file='./assets/share/assignment/dispatch/EMPTY_SLOT_SUPPORT.png', + area=(1152, 561, 1187, 592), + search=(1132, 541, 1207, 612), + color=(203, 202, 198), + button=(1152, 561, 1187, 592), + ), +) diff --git a/tasks/assignment/assets/assets_assignment_ui.py b/tasks/assignment/assets/assets_assignment_ui.py index bfc216277..be4d6683b 100644 --- a/tasks/assignment/assets/assets_assignment_ui.py +++ b/tasks/assignment/assets/assets_assignment_ui.py @@ -3,16 +3,6 @@ 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=(146, 91, 255, 124), - search=(126, 71, 275, 144), - color=(213, 213, 208), - button=(146, 91, 255, 124), - ), -) DISPATCHED = ButtonWrapper( name='DISPATCHED', cn=Button( @@ -40,14 +30,14 @@ ENTRY_LOADED = ButtonWrapper( button=(467, 235, 498, 619), ), ) -EXP_MATERIALS_CREDITS = ButtonWrapper( - name='EXP_MATERIALS_CREDITS', +LOCKED = ButtonWrapper( + name='LOCKED', share=Button( - file='./assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png', - area=(310, 85, 447, 134), - search=(290, 65, 467, 154), - color=(214, 214, 210), - button=(310, 85, 447, 134), + file='./assets/share/assignment/ui/LOCKED.png', + area=(1051, 480, 1237, 630), + search=(1031, 460, 1257, 650), + color=(53, 48, 40), + button=(1051, 480, 1237, 630), ), ) OCR_ASSIGNMENT_ENTRY_LIST = ButtonWrapper( @@ -64,10 +54,10 @@ OCR_ASSIGNMENT_GROUP_LIST = ButtonWrapper( name='OCR_ASSIGNMENT_GROUP_LIST', share=Button( file='./assets/share/assignment/ui/OCR_ASSIGNMENT_GROUP_LIST.png', - area=(106, 70, 848, 135), - search=(86, 50, 868, 155), - color=(73, 72, 70), - button=(106, 70, 848, 135), + area=(116, 81, 827, 131), + search=(96, 61, 847, 151), + color=(80, 79, 77), + button=(116, 81, 827, 131), ), ) OCR_ASSIGNMENT_LIMIT = ButtonWrapper( @@ -90,13 +80,3 @@ OCR_ASSIGNMENT_TIME = ButtonWrapper( button=(605, 564, 886, 589), ), ) -SYNTHESIS_MATERIALS = ButtonWrapper( - name='SYNTHESIS_MATERIALS', - share=Button( - file='./assets/share/assignment/ui/SYNTHESIS_MATERIALS.png', - area=(521, 91, 603, 128), - search=(501, 71, 623, 148), - color=(208, 208, 203), - button=(521, 91, 603, 128), - ), -) diff --git a/tasks/assignment/assignment.py b/tasks/assignment/assignment.py index 589c0a27f..445b4d9b1 100644 --- a/tasks/assignment/assignment.py +++ b/tasks/assignment/assignment.py @@ -1,17 +1,12 @@ 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_ui import ( - DISPATCHED, - OCR_ASSIGNMENT_TIME, -) +from tasks.assignment.assets.assets_assignment_dispatch import EMPTY_SLOT +from tasks.assignment.assets.assets_assignment_ui import DISPATCHED, LOCKED from tasks.assignment.claim import AssignmentClaim -from tasks.assignment.keywords import ( - AssignmentEntry, - KEYWORDS_ASSIGNMENT_GROUP, -) +from tasks.assignment.keywords import (KEYWORDS_ASSIGNMENT_GROUP, + AssignmentEntry, AssignmentEventGroup) from tasks.base.page import page_assignment, page_menu from tasks.battle_pass.keywords import KEYWORD_BATTLE_PASS_QUEST from tasks.daily.keywords import KEYWORDS_DAILY_QUEST @@ -19,7 +14,7 @@ from tasks.daily.synthesize import SynthesizeUI class Assignment(AssignmentClaim, SynthesizeUI): - def run(self, assignments: list[AssignmentEntry] = None, duration: int = None): + def run(self, assignments: list[AssignmentEntry] = None, duration: int = None, event_first: bool = None): self.config.update_battle_pass_quests() self.config.update_daily_quests() @@ -35,14 +30,25 @@ class Assignment(AssignmentClaim, SynthesizeUI): 'There are duplicate assignments in config, check it out') if duration is None: duration = self.config.Assignment_Duration + if event_first is None: + event_first = self.config.Assignment_WhenEventAssignmentsArePresent == 'event_first' self.dispatched = dict() self.has_new_dispatch = False self.ensure_scroll_top(page_menu) self.ui_ensure(page_assignment) - # Iterate in user-specified order, return undispatched ones - undispatched = list(self._check_inlist(assignments, duration)) - remain = self._check_all() + event_ongoing = next(( + g for g in self._iter_groups() + if isinstance(g, AssignmentEventGroup) + ), None) + if event_first and event_ongoing is not None: + undispatched = assignments + remain = self._check_all() + remain = self._dispatch_event(remain) + else: + # Iterate in user-specified order, return undispatched ones + undispatched = list(self._check_inlist(assignments, duration)) + remain = self._check_all() # There are unchecked assignments if remain > 0: for assignment in undispatched[:remain]: @@ -61,7 +67,8 @@ class Assignment(AssignmentClaim, SynthesizeUI): quests = self.config.stored.BattlePassTodayQuest.load_quests() if self.has_new_dispatch: if KEYWORD_BATTLE_PASS_QUEST.Dispatch_1_assignments in quests: - logger.info('Achieved battle pass quest Dispatch_1_assignments') + logger.info( + 'Achieved battle pass quest Dispatch_1_assignments') self.config.task_call('BattlePass') # Check daily quests = self.config.stored.DailyQuest.load_quests() @@ -93,6 +100,8 @@ class Assignment(AssignmentClaim, SynthesizeUI): f'User specified assignments: {", ".join([x.name for x in assignments])}') _, remain, _ = self._limit_status for assignment in assignments: + if assignment in self.dispatched: + continue logger.hr('Assignment inlist', level=2) logger.info(f'Check assignment inlist: {assignment}') self.goto_entry(assignment) @@ -100,8 +109,8 @@ class Assignment(AssignmentClaim, SynthesizeUI): 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) + self.dispatched[assignment] = datetime.now() + \ + self._get_assignment_time() continue if remain > 0: self.dispatch(assignment, duration) @@ -123,9 +132,7 @@ class Assignment(AssignmentClaim, SynthesizeUI): return remain for group in self._iter_groups(): self.goto_group(group) - entries = self._iter_entries() - for _ in range(len(group.entries)): - assignment = next(entries) + for assignment in self._iter_entries(): if assignment in self.dispatched: continue logger.hr('Assignment all', level=2) @@ -136,8 +143,8 @@ class Assignment(AssignmentClaim, SynthesizeUI): remain += 1 continue if self.appear(DISPATCHED): - self.dispatched[assignment] = datetime.now() + Duration( - OCR_ASSIGNMENT_TIME).ocr_single_line(self.device.image) + self.dispatched[assignment] = datetime.now() + \ + self._get_assignment_time() if total == len(self.dispatched): return remain continue @@ -174,3 +181,27 @@ class Assignment(AssignmentClaim, SynthesizeUI): remain -= 1 if remain <= 0: return + + def _dispatch_event(self, remain: int): + if remain <= 0: + return remain + logger.hr('Assignment dispatch event', level=1) + for group in self._iter_groups(): + if not isinstance(group, AssignmentEventGroup): + continue + self.goto_group(group) + for assignment in self._iter_entries(): + if assignment in self.dispatched: + continue + logger.hr('Assignment event', level=2) + logger.info(f'Check assignment event: {assignment}') + self.goto_entry(assignment) + if self.appear(LOCKED): + logger.info('Assignment is locked') + break + if self.appear(EMPTY_SLOT): + self.dispatch(assignment, None) + remain -= 1 + if remain <= 0: + return remain + return remain diff --git a/tasks/assignment/claim.py b/tasks/assignment/claim.py index d5277f14f..6763653f9 100644 --- a/tasks/assignment/claim.py +++ b/tasks/assignment/claim.py @@ -41,7 +41,7 @@ class AssignmentClaim(AssignmentDispatch): """ Pages: in: CLAIM - out: REDISPATCH + out: REPORT """ skip_first_screenshot = True while 1: @@ -50,7 +50,9 @@ class AssignmentClaim(AssignmentDispatch): else: self.device.screenshot() # End - if self.appear(REDISPATCH): + # Neither CLOSE_REPORT nor REDISPATCH is shown + # If it is an EVENT assignment + if self.appear(REPORT): logger.info('Assignment report appears') break # Claim rewards @@ -63,7 +65,7 @@ class AssignmentClaim(AssignmentDispatch): should_redispatch (bool): determined by user config and duration in report Pages: - in: CLOSE_REPORT and REDISPATCH + in: REPORT out: page_assignment """ click_button = REDISPATCH if should_redispatch else CLOSE_REPORT @@ -80,6 +82,9 @@ class AssignmentClaim(AssignmentDispatch): # Close report if self.appear_then_click(click_button, interval=2): continue + # Only for EVENT assignments + if self.appear_then_click(REPORT, interval=2): + continue def _is_duration_expected(self, duration: int) -> bool: """ diff --git a/tasks/assignment/dispatch.py b/tasks/assignment/dispatch.py index 2396b88ab..e2d0d6f07 100644 --- a/tasks/assignment/dispatch.py +++ b/tasks/assignment/dispatch.py @@ -47,21 +47,25 @@ class AssignmentDispatch(AssignmentUI): dispatched: dict[AssignmentEntry, datetime] = dict() has_new_dispatch: bool = False - def dispatch(self, assignment: AssignmentEntry, duration: int): + def dispatch(self, assignment: AssignmentEntry, duration: int | None): """ Dispatch assignment. Should be called only when limit is checked Args: assignment (AssignmentEntry): - duration (int): user specified duration + duration (int | None): user specified duration, None for event assignments Pages: in: EMPTY_SLOT out: DISPATCHED """ self._select_characters() - self._select_duration(duration) + if isinstance(assignment, AssignmentEventEntry): + self._select_support() + duration = self._get_assignment_time().total_seconds() / 3600 + else: + self._select_duration(duration) self._confirm_assignment() self._wait_until_assignment_started() future = now() + timedelta(hours=duration) @@ -103,6 +107,30 @@ class AssignmentDispatch(AssignmentUI): if not self.image_color_count(CHARACTER_2_SELECTED, (240, 240, 240)): self.device.click(CHARACTER_2) + def _select_support(self): + skip_first_screenshot = True + self.interval_clear( + (CHARACTER_SUPPORT_LIST, CHARACTER_SUPPORT_SELECTED), interval=2) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + # End + if self.match_color(CHARACTER_SUPPORT_SELECTED): + logger.info('Support character is selected') + break + # Ensure support list + if not self.appear(CHARACTER_SUPPORT_LIST): + if self.interval_is_reached(CHARACTER_SUPPORT_LIST, interval=2): + self.interval_reset(CHARACTER_SUPPORT_LIST, interval=2) + self.device.click(EMPTY_SLOT_SUPPORT) + continue + # Select + if self.interval_is_reached(CHARACTER_SUPPORT_SELECTED, interval=2): + self.interval_reset(CHARACTER_SUPPORT_SELECTED, interval=2) + self.device.click(CHARACTER_SUPPORT) + def _select_duration(self, duration: int): if duration not in {4, 8, 12, 20}: logger.warning( diff --git a/tasks/assignment/keywords/__init__.py b/tasks/assignment/keywords/__init__.py index a2d030258..9112ad297 100644 --- a/tasks/assignment/keywords/__init__.py +++ b/tasks/assignment/keywords/__init__.py @@ -1,6 +1,8 @@ import tasks.assignment.keywords.entry as KEYWORDS_ASSIGNMENT_ENTRY import tasks.assignment.keywords.group as KEYWORDS_ASSIGNMENT_GROUP -from tasks.assignment.keywords.classes import AssignmentEntry, AssignmentGroup +import tasks.assignment.keywords.event_entry as KEYWORDS_ASSIGNMENT_EVENT_ENTRY +import tasks.assignment.keywords.event_group as KEYWORDS_ASSIGNMENT_EVENT_GROUP +from tasks.assignment.keywords.classes import * KEYWORDS_ASSIGNMENT_GROUP.Character_Materials.entries = ( KEYWORDS_ASSIGNMENT_ENTRY.Nine_Billion_Names, @@ -23,10 +25,37 @@ KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials.entries = ( KEYWORDS_ASSIGNMENT_ENTRY.Legend_of_the_Puppet_Master, KEYWORDS_ASSIGNMENT_ENTRY.The_Wages_of_Humanity, ) +KEYWORDS_ASSIGNMENT_EVENT_GROUP.Space_Station_Task_Force.entries = ( + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Repulsion_Bridge_Errors, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Meal_Delivery_Robot_Check_Up, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Noise_Complaint, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Interior_Temperature_Modulator, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Researcher_Health_Reports, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Confidential_Investigation, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Borrowed_Equipment, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Booking_System, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Non_Digital_Documents, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Drip_Feed_Errors, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Pet_Movement_Route_Planning, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Food_Improvement_Plan, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Curio_Distribution, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Super_Urgent_Waiting_Online, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Ventilation_Problem, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Unstable_Connection, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Chronology_Checks, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Supply_Chain_Management, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Malicious_Occupation_of_Public_Space, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Uniform_Material, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Virus_Re_creation_Report, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Abnormal_Signal, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Flexible_Working_Approval, + KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Lighting_Issue, +) for group in ( KEYWORDS_ASSIGNMENT_GROUP.Character_Materials, KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits, KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials, + KEYWORDS_ASSIGNMENT_EVENT_GROUP.Space_Station_Task_Force, ): for entry in group.entries: assert entry.group is None diff --git a/tasks/assignment/keywords/classes.py b/tasks/assignment/keywords/classes.py index 25e11167c..e5a7c774d 100644 --- a/tasks/assignment/keywords/classes.py +++ b/tasks/assignment/keywords/classes.py @@ -15,6 +15,19 @@ class AssignmentGroup(Keyword): class AssignmentEntry(Keyword): instances: ClassVar = {} group: AssignmentGroup = None + def __hash__(self) -> int: return super().__hash__() + +@dataclass(repr=False) +class AssignmentEventGroup(AssignmentGroup): + instances: ClassVar = {} + + +@dataclass(repr=False) +class AssignmentEventEntry(AssignmentEntry): + instances: ClassVar = {} + + def __hash__(self) -> int: + return super().__hash__() diff --git a/tasks/assignment/keywords/event_entry.py b/tasks/assignment/keywords/event_entry.py new file mode 100644 index 000000000..1bebdc86b --- /dev/null +++ b/tasks/assignment/keywords/event_entry.py @@ -0,0 +1,197 @@ +from .classes import AssignmentEventEntry + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.keyword_extract ``` + +Repulsion_Bridge_Errors = AssignmentEventEntry( + id=1, + name='Repulsion_Bridge_Errors', + cn='斥力桥报错', + cht='斥力橋錯誤', + en='Repulsion Bridge Errors', + jp='斥力ブリッジエラー', +) +Meal_Delivery_Robot_Check_Up = AssignmentEventEntry( + id=2, + name='Meal_Delivery_Robot_Check_Up', + cn='送餐机器人检修', + cht='送餐機器人檢修', + en='Meal-Delivery Robot Check-Up', + jp='配膳ロボット点検修理', +) +Noise_Complaint = AssignmentEventEntry( + id=3, + name='Noise_Complaint', + cn='噪音投诉问题', + cht='噪音投訴問題', + en='Noise Complaint', + jp='騒音苦情問題', +) +Interior_Temperature_Modulator = AssignmentEventEntry( + id=4, + name='Interior_Temperature_Modulator', + cn='室内温度调节器', + cht='室內溫度調節器', + en='Interior Temperature Modulator', + jp='室内温度調節器', +) +Researcher_Health_Reports = AssignmentEventEntry( + id=5, + name='Researcher_Health_Reports', + cn='科员的体检报告', + cht='組員的體檢報告', + en="Researchers' Health Reports", + jp='スタッフの健康診断報告', +) +Confidential_Investigation = AssignmentEventEntry( + id=6, + name='Confidential_Investigation', + cn='秘密调查行动', + cht='秘密調查行動', + en='Confidential Investigation', + jp='秘密裏の調査', +) +Borrowed_Equipment = AssignmentEventEntry( + id=7, + name='Borrowed_Equipment', + cn='实验器械借用', + cht='實驗器械借用', + en='Borrowed Equipment', + jp='実験機器借用', +) +Booking_System = AssignmentEventEntry( + id=8, + name='Booking_System', + cn='会议室预约系统', + cht='會議室預約系統', + en='Booking System', + jp='会議室予約システム', +) +Non_Digital_Documents = AssignmentEventEntry( + id=9, + name='Non_Digital_Documents', + cn='非电子版文件', + cht='非電子版文件', + en='Non-Digital Documents', + jp='非デジタル版ファイル', +) +Drip_Feed_Errors = AssignmentEventEntry( + id=10, + name='Drip_Feed_Errors', + cn='液滴系统报错', + cht='液滴系統錯誤', + en='Drip-Feed Errors', + jp='水やりシステムエラー', +) +Pet_Movement_Route_Planning = AssignmentEventEntry( + id=11, + name='Pet_Movement_Route_Planning', + cn='宠物行动路线规划', + cht='寵物行動路線規劃', + en='Pet Movement Route Planning', + jp='ペットの行動ルート規制', +) +Food_Improvement_Plan = AssignmentEventEntry( + id=12, + name='Food_Improvement_Plan', + cn='餐饮优化方案', + cht='餐飲改良方案', + en='Food Improvement Plan', + jp='飲食優良化法案', +) +Curio_Distribution = AssignmentEventEntry( + id=13, + name='Curio_Distribution', + cn='奇物借用问题', + cht='奇物借用問題', + en='Curio Distribution', + jp='奇物借用問題', +) +Super_Urgent_Waiting_Online = AssignmentEventEntry( + id=14, + name='Super_Urgent_Waiting_Online', + cn='来活人很急在线等', + cht='急,線上等', + en='Super Urgent, Waiting Online', + jp='緊急助っ人求むオンラインにて待つ', +) +Ventilation_Problem = AssignmentEventEntry( + id=15, + name='Ventilation_Problem', + cn='空气流通问题', + cht='空氣流通問題', + en='Ventilation Problem', + jp='換気問題', +) +Unstable_Connection = AssignmentEventEntry( + id=16, + name='Unstable_Connection', + cn='连接不稳定问题', + cht='連線不穩定問題', + en='Unstable Connection', + jp='接続不安定問題', +) +Chronology_Checks = AssignmentEventEntry( + id=17, + name='Chronology_Checks', + cn='编年史校对', + cht='編年史校對', + en='Chronology Checks', + jp='編年史校正', +) +Supply_Chain_Management = AssignmentEventEntry( + id=18, + name='Supply_Chain_Management', + cn='物流供应链管理', + cht='物流供應鏈管理', + en='Supply Chain Management', + jp='物流供給路線管理', +) +Malicious_Occupation_of_Public_Space = AssignmentEventEntry( + id=19, + name='Malicious_Occupation_of_Public_Space', + cn='公共区域被恶意侵占', + cht='公共區域被惡意侵佔', + en='Malicious Occupation of Public Space', + jp='公共区域の悪意による独占', +) +Uniform_Material = AssignmentEventEntry( + id=20, + name='Uniform_Material', + cn='科室服装面料', + cht='科室服裝材質', + en='Uniform Material', + jp='スタッフ制服の素材', +) +Virus_Re_creation_Report = AssignmentEventEntry( + id=21, + name='Virus_Re_creation_Report', + cn='病毒溯源报告', + cht='病毒溯源報告', + en='Virus Re-creation Report', + jp='ウイルス根源報告', +) +Abnormal_Signal = AssignmentEventEntry( + id=22, + name='Abnormal_Signal', + cn='舱段信号异常', + cht='艙段訊號異常', + en='Abnormal Signal', + jp='部分の信号異常', +) +Flexible_Working_Approval = AssignmentEventEntry( + id=23, + name='Flexible_Working_Approval', + cn='轮休审批流程', + cht='輪休審批流程', + en='Flexible Working Approval', + jp='交代休み審査フロー', +) +Lighting_Issue = AssignmentEventEntry( + id=24, + name='Lighting_Issue', + cn='灯光照明问题', + cht='燈光照明問題', + en='Lighting Issue', + jp='照明の色問題', +) diff --git a/tasks/assignment/keywords/event_group.py b/tasks/assignment/keywords/event_group.py new file mode 100644 index 000000000..71a92873d --- /dev/null +++ b/tasks/assignment/keywords/event_group.py @@ -0,0 +1,13 @@ +from .classes import AssignmentEventGroup + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.keyword_extract ``` + +Space_Station_Task_Force = AssignmentEventGroup( + id=1, + name='Space_Station_Task_Force', + cn='空间站特派', + cht='太空站特派', + en='Space Station Task Force', + jp='ステーション特派', +) diff --git a/tasks/assignment/ui.py b/tasks/assignment/ui.py index a825026ed..7fd7e2f0d 100644 --- a/tasks/assignment/ui.py +++ b/tasks/assignment/ui.py @@ -1,6 +1,7 @@ import re +from collections.abc import Iterator +from datetime import timedelta from functools import cached_property -from typing import Iterator from module.base.timer import Timer from module.exception import ScriptError @@ -26,6 +27,14 @@ class AssignmentOcr(Ocr): (KEYWORDS_ASSIGNMENT_ENTRY.Akashic_Records.name, '阿[未][夏复]记录'), (KEYWORDS_ASSIGNMENT_ENTRY.Legend_of_the_Puppet_Master.name, '^师传说'), (KEYWORDS_ASSIGNMENT_ENTRY.The_Wages_of_Humanity.name, '[赠]养人类'), + (KEYWORDS_ASSIGNMENT_EVENT_GROUP.Space_Station_Task_Force.name, + '[新0]空间站特派[新]'), + ], + 'en': [ + (KEYWORDS_ASSIGNMENT_EVENT_ENTRY.Food_Improvement_Plan.name, + 'Food\s*[I]{0}mprovement Plan'), + (KEYWORDS_ASSIGNMENT_EVENT_GROUP.Space_Station_Task_Force.name, + '^(S[np]ace Station Ta[^sk]{0,3})?[F-]orce') ] } @@ -39,7 +48,15 @@ class AssignmentOcr(Ocr): def filter_detected(self, result) -> bool: # Drop duration rows res = Duration.timedelta_regex(self.lang).search(result.ocr_text) - return not bool(res.group('seconds')) + if res.group('hours') or res.group('seconds'): + return False + # Locked event assignments + locked_pattern = { + 'cn': '解锁$', + 'en': 'Locked$', + }[self.lang] + res = re.search(locked_pattern, result.ocr_text) + return not res def after_process(self, result: str): result = super().after_process(result) @@ -50,7 +67,17 @@ class AssignmentOcr(Ocr): if matched is None: return result keyword_lang = self.lang - matched = getattr(KEYWORDS_ASSIGNMENT_ENTRY, matched.lastgroup) + for keyword_class in ( + KEYWORDS_ASSIGNMENT_ENTRY, KEYWORDS_ASSIGNMENT_EVENT_ENTRY, + KEYWORDS_ASSIGNMENT_GROUP, KEYWORDS_ASSIGNMENT_EVENT_GROUP, + ): + try: + matched = getattr(keyword_class, matched.lastgroup) + break + except AttributeError: + continue + else: + raise ScriptError(f'No keyword found for {matched.lastgroup}') matched = getattr(matched, keyword_lang) logger.attr(name=f'{self.name} after_process', text=f'{result} -> {matched}') @@ -59,15 +86,16 @@ class AssignmentOcr(Ocr): ASSIGNMENT_GROUP_LIST = DraggableList( 'AssignmentGroupList', - keyword_class=AssignmentGroup, - ocr_class=Ocr, + keyword_class=[AssignmentGroup, AssignmentEventGroup], + ocr_class=AssignmentOcr, search_button=OCR_ASSIGNMENT_GROUP_LIST, + check_row_order=False, active_color=(240, 240, 240), drag_direction='right' ) ASSIGNMENT_ENTRY_LIST = DraggableList( 'AssignmentEntryList', - keyword_class=AssignmentEntry, + keyword_class=[AssignmentEntry, AssignmentEventEntry], ocr_class=AssignmentOcr, search_button=OCR_ASSIGNMENT_ENTRY_LIST, check_row_order=False, @@ -86,7 +114,11 @@ class AssignmentUI(UI): self.device.screenshot() self.goto_group(KEYWORDS_ASSIGNMENT_GROUP.Character_Materials) """ + selected = ASSIGNMENT_GROUP_LIST.get_selected_row(self) + if selected and selected.matched_keyword == group: + return logger.hr('Assignment group goto', level=3) + self._wait_until_group_loaded() if ASSIGNMENT_GROUP_LIST.select_row(group, self): self._wait_until_entry_loaded() @@ -112,6 +144,23 @@ class AssignmentUI(UI): self.goto_group(entry.group) ASSIGNMENT_ENTRY_LIST.select_row(entry, self) + def _wait_until_group_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 group loaded timeout') + break + if self.image_color_count(OCR_ASSIGNMENT_GROUP_LIST, (40, 40, 40), count=20000) and \ + self.image_color_count(OCR_ASSIGNMENT_GROUP_LIST, (240, 240, 240), count=7000): + logger.info('Group loaded') + break + def _wait_until_entry_loaded(self): skip_first_screenshot = True timeout = Timer(2, count=3).start() @@ -142,8 +191,13 @@ class AssignmentUI(UI): self.config.stored.Assignment.set(0, 0) return current, remain, total + def _get_assignment_time(self) -> timedelta: + return Duration(OCR_ASSIGNMENT_TIME).ocr_single_line(self.device.image) + def _iter_groups(self) -> Iterator[AssignmentGroup]: - ASSIGNMENT_GROUP_LIST.load_rows(main=self) + self._wait_until_group_loaded() + ASSIGNMENT_GROUP_LIST.insight_row( + KEYWORDS_ASSIGNMENT_GROUP.Character_Materials, self) for button in ASSIGNMENT_GROUP_LIST.cur_buttons: yield button.matched_keyword @@ -152,5 +206,8 @@ class AssignmentUI(UI): Iterate entries from top to bottom """ ASSIGNMENT_ENTRY_LIST.load_rows(main=self) - for button in ASSIGNMENT_ENTRY_LIST.cur_buttons: - yield button.matched_keyword + # Freeze ocr results here + yield from [ + button.matched_keyword + for button in ASSIGNMENT_ENTRY_LIST.cur_buttons + ]