diff --git a/assets/cn/assignment/claim/CLAIM.BUTTON.png b/assets/cn/assignment/claim/CLAIM.BUTTON.png new file mode 100644 index 000000000..4e822ac8f Binary files /dev/null and b/assets/cn/assignment/claim/CLAIM.BUTTON.png differ diff --git a/assets/cn/assignment/claim/CLAIM.png b/assets/cn/assignment/claim/CLAIM.png new file mode 100644 index 000000000..193a9af36 Binary files /dev/null and b/assets/cn/assignment/claim/CLAIM.png differ diff --git a/assets/cn/assignment/claim/CLOSE_REPORT.BUTTON.png b/assets/cn/assignment/claim/CLOSE_REPORT.BUTTON.png new file mode 100644 index 000000000..959bec95e Binary files /dev/null and b/assets/cn/assignment/claim/CLOSE_REPORT.BUTTON.png differ diff --git a/assets/cn/assignment/claim/CLOSE_REPORT.png b/assets/cn/assignment/claim/CLOSE_REPORT.png new file mode 100644 index 000000000..5e9cc57db Binary files /dev/null and b/assets/cn/assignment/claim/CLOSE_REPORT.png differ diff --git a/assets/cn/assignment/claim/REDISPATCH.BUTTON.png b/assets/cn/assignment/claim/REDISPATCH.BUTTON.png new file mode 100644 index 000000000..d8c0b9062 Binary files /dev/null and b/assets/cn/assignment/claim/REDISPATCH.BUTTON.png differ diff --git a/assets/cn/assignment/claim/REDISPATCH.png b/assets/cn/assignment/claim/REDISPATCH.png new file mode 100644 index 000000000..6c8373c57 Binary files /dev/null and b/assets/cn/assignment/claim/REDISPATCH.png differ diff --git a/assets/cn/assignment/dispatch/ASSIGNMENT_START.png b/assets/cn/assignment/dispatch/ASSIGNMENT_START.png new file mode 100644 index 000000000..efd15ed00 Binary files /dev/null and b/assets/cn/assignment/dispatch/ASSIGNMENT_START.png differ diff --git a/assets/cn/assignment/dispatch/CHARACTER_LIST.png b/assets/cn/assignment/dispatch/CHARACTER_LIST.png new file mode 100644 index 000000000..50adb0d1c Binary files /dev/null and b/assets/cn/assignment/dispatch/CHARACTER_LIST.png differ diff --git a/assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.BUTTON.png b/assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.BUTTON.png new file mode 100644 index 000000000..761818dbd Binary files /dev/null and b/assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.BUTTON.png differ diff --git a/assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png b/assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png new file mode 100644 index 000000000..f01d59f9c Binary files /dev/null and b/assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png differ diff --git a/assets/cn/assignment/ui/DISPATCHED.png b/assets/cn/assignment/ui/DISPATCHED.png new file mode 100644 index 000000000..3b69f9216 Binary files /dev/null and b/assets/cn/assignment/ui/DISPATCHED.png differ diff --git a/assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png b/assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png new file mode 100644 index 000000000..287dd7e54 Binary files /dev/null and b/assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png differ diff --git a/assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png b/assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png new file mode 100644 index 000000000..bc3588333 Binary files /dev/null and b/assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_1.png b/assets/share/assignment/dispatch/CHARACTER_1.png new file mode 100644 index 000000000..ed0fa0469 Binary files /dev/null and b/assets/share/assignment/dispatch/CHARACTER_1.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png b/assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png new file mode 100644 index 000000000..c8ed4441b Binary files /dev/null and b/assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_2.png b/assets/share/assignment/dispatch/CHARACTER_2.png new file mode 100644 index 000000000..cb5cbbbe4 Binary files /dev/null and b/assets/share/assignment/dispatch/CHARACTER_2.png differ diff --git a/assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png b/assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png new file mode 100644 index 000000000..2c784891e Binary files /dev/null and b/assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png differ diff --git a/assets/share/assignment/dispatch/DURATION_12.png b/assets/share/assignment/dispatch/DURATION_12.png new file mode 100644 index 000000000..ed98a0866 Binary files /dev/null and b/assets/share/assignment/dispatch/DURATION_12.png differ diff --git a/assets/share/assignment/dispatch/DURATION_20.png b/assets/share/assignment/dispatch/DURATION_20.png new file mode 100644 index 000000000..c18556dca Binary files /dev/null and b/assets/share/assignment/dispatch/DURATION_20.png differ diff --git a/assets/share/assignment/dispatch/DURATION_4.png b/assets/share/assignment/dispatch/DURATION_4.png new file mode 100644 index 000000000..b4ffb2b2f Binary files /dev/null and b/assets/share/assignment/dispatch/DURATION_4.png differ diff --git a/assets/share/assignment/dispatch/DURATION_8.png b/assets/share/assignment/dispatch/DURATION_8.png new file mode 100644 index 000000000..57f5b8ff8 Binary files /dev/null and b/assets/share/assignment/dispatch/DURATION_8.png differ diff --git a/assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png b/assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png new file mode 100644 index 000000000..4f96e9044 Binary files /dev/null and b/assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png differ diff --git a/assets/share/assignment/dispatch/EMPTY_SLOT.png b/assets/share/assignment/dispatch/EMPTY_SLOT.png new file mode 100644 index 000000000..06cd6cf48 Binary files /dev/null and b/assets/share/assignment/dispatch/EMPTY_SLOT.png differ diff --git a/assets/share/assignment/ui/CHARACTER_MATERIALS.png b/assets/share/assignment/ui/CHARACTER_MATERIALS.png new file mode 100644 index 000000000..a92a59204 Binary files /dev/null and b/assets/share/assignment/ui/CHARACTER_MATERIALS.png differ diff --git a/assets/share/assignment/ui/ENTRY_LOADED.png b/assets/share/assignment/ui/ENTRY_LOADED.png new file mode 100644 index 000000000..cef60fc94 Binary files /dev/null and b/assets/share/assignment/ui/ENTRY_LOADED.png differ diff --git a/assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png b/assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png new file mode 100644 index 000000000..ab4f7717d Binary files /dev/null and b/assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png differ diff --git a/assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png b/assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png new file mode 100644 index 000000000..d1da78934 Binary files /dev/null and b/assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png differ diff --git a/assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png b/assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png new file mode 100644 index 000000000..6c5a382ff Binary files /dev/null and b/assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png differ diff --git a/assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png b/assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png new file mode 100644 index 000000000..e7e9e168d Binary files /dev/null and b/assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png differ diff --git a/assets/share/assignment/ui/SYNTHESIS_MATERIALS.png b/assets/share/assignment/ui/SYNTHESIS_MATERIALS.png new file mode 100644 index 000000000..a07bbdfbd Binary files /dev/null and b/assets/share/assignment/ui/SYNTHESIS_MATERIALS.png differ diff --git a/assets/share/base/page/ASSIGNMENT_CHECK.png b/assets/share/base/page/ASSIGNMENT_CHECK.png new file mode 100644 index 000000000..c9869cf7c Binary files /dev/null and b/assets/share/base/page/ASSIGNMENT_CHECK.png differ diff --git a/assets/share/base/page/MENU_GOTO_ASSIGNMENT.png b/assets/share/base/page/MENU_GOTO_ASSIGNMENT.png new file mode 100644 index 000000000..1dcdd1844 Binary files /dev/null and b/assets/share/base/page/MENU_GOTO_ASSIGNMENT.png differ diff --git a/config/template.json b/config/template.json index 7811b8717..85f345438 100644 --- a/config/template.json +++ b/config/template.json @@ -59,5 +59,20 @@ "Command": "BattlePass", "ServerUpdate": "04:00" } + }, + "Assignment": { + "Scheduler": { + "Enable": false, + "NextRun": "2020-01-01 00:00:00", + "Command": "Assignment", + "ServerUpdate": "04:00" + }, + "Assignment": { + "Duration": 20, + "Name_1": "Nameless_Land_Nameless_People", + "Name_2": "Akashic_Records", + "Name_3": "The_Invisible_Hand", + "Name_4": "Nine_Billion_Names" + } } } \ No newline at end of file diff --git a/dev_tools/keyword_extract.py b/dev_tools/keyword_extract.py index 10a0dd701..c46e5a336 100644 --- a/dev_tools/keyword_extract.py +++ b/dev_tools/keyword_extract.py @@ -2,6 +2,7 @@ import os import re import typing as t from functools import cached_property +from collections import namedtuple from module.base.code_generator import CodeGenerator from module.config.utils import deep_get, read_file @@ -12,7 +13,7 @@ UI_LANGUAGES = ['cn', 'cht', 'en', 'jp'] def text_to_variable(text): text = re.sub("'s |s' ", '_', text) - text = re.sub('[ \-—:\']+', '_', text) + text = re.sub('[ \-—:\'/]+', '_', text) text = re.sub(r'[(),#]|', '', text) # text = re.sub(r'[#_]?\d+(_times?)?', '', 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] 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): self.load_keywords(['模拟宇宙', '拟造花萼(金)', '拟造花萼(赤)', '凝滞虚影', '侵蚀隧洞', '历战余响', '忘却之庭']) 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.load_keywords(['奖励', '任务']) self.write_keywords(keyword_class='BattlePassTab', output_file='./tasks/battle_pass/keywords/tab.py') + self.generate_assignment_keywords() if __name__ == '__main__': diff --git a/module/base/base.py b/module/base/base.py index c44c758c5..a091c3b75 100644 --- a/module/base/base.py +++ b/module/base/base.py @@ -209,7 +209,7 @@ class ModuleBase: def interval_reset(self, button, interval=5): if isinstance(button, (list, tuple)): for b in button: - self.interval_reset(b) + self.interval_reset(b, interval) return if button.name in self.interval_timer: @@ -220,7 +220,7 @@ class ModuleBase: def interval_clear(self, button, interval=5): if isinstance(button, (list, tuple)): for b in button: - self.interval_clear(b) + self.interval_clear(b, interval) return if button.name in self.interval_timer: diff --git a/module/config/argument/args.json b/module/config/argument/args.json index b4b93b751..6f4f95fc0 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -258,5 +258,116 @@ "display": "hide" } } + }, + "Assignment": { + "Scheduler": { + "Enable": { + "type": "checkbox", + "value": false + }, + "NextRun": { + "type": "datetime", + "value": "2020-01-01 00:00:00", + "validate": "datetime" + }, + "Command": { + "type": "input", + "value": "Assignment", + "display": "hide" + }, + "ServerUpdate": { + "type": "input", + "value": "04:00", + "display": "hide" + } + }, + "Assignment": { + "Duration": { + "type": "select", + "value": 20, + "option": [ + 4, + 8, + 12, + 20 + ] + }, + "Name_1": { + "type": "select", + "value": "Nameless_Land_Nameless_People", + "option": [ + "Nine_Billion_Names", + "Destruction_of_the_Destroyer", + "Winter_Soldiers", + "Born_to_Obey", + "Root_Out_the_Turpitude", + "Fire_Lord_Inflames_Blades_of_War", + "Nameless_Land_Nameless_People", + "Akashic_Records", + "The_Invisible_Hand", + "Abandoned_and_Insulted", + "Spring_of_Life", + "The_Land_of_Gold", + "The_Blossom_in_the_Storm" + ] + }, + "Name_2": { + "type": "select", + "value": "Akashic_Records", + "option": [ + "Nine_Billion_Names", + "Destruction_of_the_Destroyer", + "Winter_Soldiers", + "Born_to_Obey", + "Root_Out_the_Turpitude", + "Fire_Lord_Inflames_Blades_of_War", + "Nameless_Land_Nameless_People", + "Akashic_Records", + "The_Invisible_Hand", + "Abandoned_and_Insulted", + "Spring_of_Life", + "The_Land_of_Gold", + "The_Blossom_in_the_Storm" + ] + }, + "Name_3": { + "type": "select", + "value": "The_Invisible_Hand", + "option": [ + "Nine_Billion_Names", + "Destruction_of_the_Destroyer", + "Winter_Soldiers", + "Born_to_Obey", + "Root_Out_the_Turpitude", + "Fire_Lord_Inflames_Blades_of_War", + "Nameless_Land_Nameless_People", + "Akashic_Records", + "The_Invisible_Hand", + "Abandoned_and_Insulted", + "Spring_of_Life", + "The_Land_of_Gold", + "The_Blossom_in_the_Storm" + ] + }, + "Name_4": { + "type": "select", + "value": "Nine_Billion_Names", + "option": [ + "Nine_Billion_Names", + "Destruction_of_the_Destroyer", + "Winter_Soldiers", + "Born_to_Obey", + "Root_Out_the_Turpitude", + "Fire_Lord_Inflames_Blades_of_War", + "Nameless_Land_Nameless_People", + "Akashic_Records", + "The_Invisible_Hand", + "Abandoned_and_Insulted", + "Spring_of_Life", + "The_Land_of_Gold", + "The_Blossom_in_the_Storm" + ] + } + } } } \ No newline at end of file diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index e1a940f3b..f597cf5a9 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -77,3 +77,21 @@ Dungeon: Team: value: 1 option: [ 1, 2, 3, 4, 5, 6 ] + +Assignment: + Duration: + value: 20 + option: [4, 8, 12, 20] + # Options in Name_x will be injected in config updater + Name_1: + value: Nameless_Land_Nameless_People + option: [Nameless_Land_Nameless_People, ] + Name_2: + value: Akashic_Records + option: [Nameless_Land_Nameless_People, ] + Name_3: + value: The_Invisible_Hand + option: [Nameless_Land_Nameless_People, ] + Name_4: + value: Nine_Billion_Names + option: [Nameless_Land_Nameless_People, ] \ No newline at end of file diff --git a/module/config/argument/menu.json b/module/config/argument/menu.json index 4be8fdcd5..e164e6947 100644 --- a/module/config/argument/menu.json +++ b/module/config/argument/menu.json @@ -13,7 +13,8 @@ "tasks": [ "Dungeon", "DailyQuest", - "BattlePass" + "BattlePass", + "Assignment" ] } } \ No newline at end of file diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index df48b67ff..6edcdb991 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -29,3 +29,6 @@ Daily: - Scheduler BattlePass: - Scheduler + Assignment: + - Scheduler + - Assignment diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 6693be31f..8a6b3d014 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -41,3 +41,10 @@ class GeneratedConfig: # Group `Dungeon` Dungeon_Name = 'Calyx_Golden_Memories' # Calyx_Golden_Memories, Calyx_Golden_Aether, Calyx_Golden_Treasures, Calyx_Crimson_Destruction, Calyx_Crimson_Preservation, Calyx_Crimson_Calyx_Crimson_Hunt, Calyx_Crimson_Abundance, Calyx_Crimson_Erudition, Calyx_Crimson_Harmony, Calyx_Crimson_Nihility, Stagnant_Shadow_Quanta, Stagnant_Shadow_Gust, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Blaze, Stagnant_Shadow_Spike, Stagnant_Shadow_Rime, Stagnant_Shadow_Mirage, Stagnant_Shadow_Icicle, Stagnant_Shadow_Doom, Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration Dungeon_Team = 1 # 1, 2, 3, 4, 5, 6 + + # Group `Assignment` + Assignment_Duration = 20 # 4, 8, 12, 20 + Assignment_Name_1 = 'Nameless_Land_Nameless_People' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm + Assignment_Name_2 = 'Akashic_Records' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm + Assignment_Name_3 = 'The_Invisible_Hand' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm + Assignment_Name_4 = 'Nine_Billion_Names' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm diff --git a/module/config/config_manual.py b/module/config/config_manual.py index 695b688ad..659b42392 100644 --- a/module/config/config_manual.py +++ b/module/config/config_manual.py @@ -8,7 +8,7 @@ class ManualConfig: SCHEDULER_PRIORITY = """ Restart - > Dungeon > DailyQuest > BattlePass + > Dungeon > Assignment > DailyQuest > BattlePass """ """ diff --git a/module/config/config_updater.py b/module/config/config_updater.py index 3aed912ad..a863d94a9 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -355,6 +355,13 @@ class ConfigGenerator: dungeons = [dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_daily_dungeon] deep_set(self.argument, keys='Dungeon.Name.option', value=dungeons) deep_set(self.args, keys='Dungeon.Dungeon.Name.option', value=dungeons) + + def insert_assignment(self): + from tasks.assignment.keywords import AssignmentEntry + assignments = [entry.name for entry in AssignmentEntry.instances.values()] + for i in range(4): + deep_set(self.argument, keys=f'Assignment.Name_{i+1}.option', value=assignments) + deep_set(self.args, keys=f'Assignment.Assignment.Name_{i+1}.option', value=assignments) def insert_package(self): option = deep_get(self.argument, keys='Emulator.PackageName.option') @@ -369,6 +376,7 @@ class ConfigGenerator: _ = self.menu # _ = self.event self.insert_dungeon() + self.insert_assignment() self.insert_package() # self.insert_server() write_file(filepath_args(), self.args) diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 65f9764e1..fdf7934b5 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -29,6 +29,10 @@ "BattlePass": { "name": "Nameless Honor", "help": "" + }, + "Assignment": { + "name": "Assignment", + "help": "" } }, "Scheduler": { @@ -216,6 +220,88 @@ "6": "6" } }, + "Assignment": { + "_info": { + "name": "Assignment Settings", + "help": "Claim rewards and dispatch, handling specified assignments first\nIf the assignment limit is not reached after that, others will be dispatched according to preset priority (EXP Materials/Credits → Character Materials → Synthesis Materials)" + }, + "Duration": { + "name": "Dispatch Duration", + "help": "", + "4": "4", + "8": "8", + "12": "12", + "20": "20" + }, + "Name_1": { + "name": "Assignment 1 Preference", + "help": "", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "The_Invisible_Hand": "Credit (The Invisible Hand)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", + "Spring_of_Life": "Virtual Particle & Solid Water (Spring of Life)", + "The_Land_of_Gold": "Protein Rice & Basic Ingredients (The Land of Gold)", + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)" + }, + "Name_2": { + "name": "Assignment 2 Preference", + "help": "", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "The_Invisible_Hand": "Credit (The Invisible Hand)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", + "Spring_of_Life": "Virtual Particle & Solid Water (Spring of Life)", + "The_Land_of_Gold": "Protein Rice & Basic Ingredients (The Land of Gold)", + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)" + }, + "Name_3": { + "name": "Assignment 3 Preference", + "help": "", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "The_Invisible_Hand": "Credit (The Invisible Hand)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", + "Spring_of_Life": "Virtual Particle & Solid Water (Spring of Life)", + "The_Land_of_Gold": "Protein Rice & Basic Ingredients (The Land of Gold)", + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)" + }, + "Name_4": { + "name": "Assignment 4 Preference", + "help": "", + "Nine_Billion_Names": "Extinguished Core (Nine Billion Names)", + "Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)", + "Winter_Soldiers": "Silvermane Badge (Winter Soldiers)", + "Born_to_Obey": "Ancient Part (Born to Obey)", + "Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)", + "Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)", + "Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)", + "Akashic_Records": "Light Cone EXP Material (Akashic Records)", + "The_Invisible_Hand": "Credit (The Invisible Hand)", + "Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)", + "Spring_of_Life": "Virtual Particle & Solid Water (Spring of Life)", + "The_Land_of_Gold": "Protein Rice & Basic Ingredients (The Land of Gold)", + "The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)" + } + }, "Gui": { "Aside": { "Install": "Install", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 53e8ae4c1..7abd6f0b7 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -29,6 +29,10 @@ "BattlePass": { "name": "ナナシの勲功", "help": "" + }, + "Assignment": { + "name": "依頼設定", + "help": "" } }, "Scheduler": { @@ -216,6 +220,88 @@ "6": "6" } }, + "Assignment": { + "_info": { + "name": "依頼設定", + "help": "" + }, + "Duration": { + "name": "派遣時間", + "help": "", + "4": "4", + "8": "8", + "12": "12", + "20": "20" + }, + "Name_1": { + "name": "依頼 1", + "help": "", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Nameless_Land_Nameless_People": "キャラクターの経験値素材(無名の地、無名の人)", + "Akashic_Records": "光円錐強化素材(アーカーシャの記録)", + "The_Invisible_Hand": "信用ポイント(見えざる手)", + "Abandoned_and_Insulted": "燃素と金属(捨てられしものと傷つけられしもの)", + "Spring_of_Life": "仮想粒子と固形純水(生命の泉)", + "The_Land_of_Gold": "タンパク米と基本食材(黄金の大地)", + "The_Blossom_in_the_Storm": "気態流体と種子(嵐の中で咲き誇る花)" + }, + "Name_2": { + "name": "依頼 2", + "help": "", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Nameless_Land_Nameless_People": "キャラクターの経験値素材(無名の地、無名の人)", + "Akashic_Records": "光円錐強化素材(アーカーシャの記録)", + "The_Invisible_Hand": "信用ポイント(見えざる手)", + "Abandoned_and_Insulted": "燃素と金属(捨てられしものと傷つけられしもの)", + "Spring_of_Life": "仮想粒子と固形純水(生命の泉)", + "The_Land_of_Gold": "タンパク米と基本食材(黄金の大地)", + "The_Blossom_in_the_Storm": "気態流体と種子(嵐の中で咲き誇る花)" + }, + "Name_3": { + "name": "依頼 3", + "help": "", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Nameless_Land_Nameless_People": "キャラクターの経験値素材(無名の地、無名の人)", + "Akashic_Records": "光円錐強化素材(アーカーシャの記録)", + "The_Invisible_Hand": "信用ポイント(見えざる手)", + "Abandoned_and_Insulted": "燃素と金属(捨てられしものと傷つけられしもの)", + "Spring_of_Life": "仮想粒子と固形純水(生命の泉)", + "The_Land_of_Gold": "タンパク米と基本食材(黄金の大地)", + "The_Blossom_in_the_Storm": "気態流体と種子(嵐の中で咲き誇る花)" + }, + "Name_4": { + "name": "依頼 4", + "help": "", + "Nine_Billion_Names": "消滅した原核(九十億の御名)", + "Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)", + "Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)", + "Born_to_Obey": "古代パーツ(生まれながらに服従する)", + "Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)", + "Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)", + "Nameless_Land_Nameless_People": "キャラクターの経験値素材(無名の地、無名の人)", + "Akashic_Records": "光円錐強化素材(アーカーシャの記録)", + "The_Invisible_Hand": "信用ポイント(見えざる手)", + "Abandoned_and_Insulted": "燃素と金属(捨てられしものと傷つけられしもの)", + "Spring_of_Life": "仮想粒子と固形純水(生命の泉)", + "The_Land_of_Gold": "タンパク米と基本食材(黄金の大地)", + "The_Blossom_in_the_Storm": "気態流体と種子(嵐の中で咲き誇る花)" + } + }, "Gui": { "Aside": { "Install": "インストール", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 92c0655c2..4801577da 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -29,6 +29,10 @@ "BattlePass": { "name": "无名勋礼", "help": "" + }, + "Assignment": { + "name": "委托设置", + "help": "" } }, "Scheduler": { @@ -216,6 +220,88 @@ "6": "6" } }, + "Assignment": { + "_info": { + "name": "委托设置", + "help": "领取奖励并派遣,优先处理指定委托\n若处理指定委托之后未达到上限,则按经验材料 → 角色专属素材 → 合成材料的顺序来派遣委托" + }, + "Duration": { + "name": "派遣时长", + "help": "", + "4": "4", + "8": "8", + "12": "12", + "20": "20" + }, + "Name_1": { + "name": "第1个委托选择", + "help": "", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Born_to_Obey": "古代零件(生而服从)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "Akashic_Records": "光锥强化材料(阿卡夏记录)", + "The_Invisible_Hand": "信用点(看不见的手)", + "Abandoned_and_Insulted": "燃素&金属(被废弃与损害的)", + "Spring_of_Life": "虚粒子&固态净水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黄金大地)", + "The_Blossom_in_the_Storm": "气态流体&种子(风暴中怒放的花)" + }, + "Name_2": { + "name": "第2个委托选择", + "help": "", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Born_to_Obey": "古代零件(生而服从)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "Akashic_Records": "光锥强化材料(阿卡夏记录)", + "The_Invisible_Hand": "信用点(看不见的手)", + "Abandoned_and_Insulted": "燃素&金属(被废弃与损害的)", + "Spring_of_Life": "虚粒子&固态净水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黄金大地)", + "The_Blossom_in_the_Storm": "气态流体&种子(风暴中怒放的花)" + }, + "Name_3": { + "name": "第3个委托选择", + "help": "", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Born_to_Obey": "古代零件(生而服从)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "Akashic_Records": "光锥强化材料(阿卡夏记录)", + "The_Invisible_Hand": "信用点(看不见的手)", + "Abandoned_and_Insulted": "燃素&金属(被废弃与损害的)", + "Spring_of_Life": "虚粒子&固态净水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黄金大地)", + "The_Blossom_in_the_Storm": "气态流体&种子(风暴中怒放的花)" + }, + "Name_4": { + "name": "第4个委托选择", + "help": "", + "Nine_Billion_Names": "熄灭原核(九十亿个名字)", + "Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)", + "Winter_Soldiers": "铁卫扣饰(寒冬的战士们)", + "Born_to_Obey": "古代零件(生而服从)", + "Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)", + "Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)", + "Akashic_Records": "光锥强化材料(阿卡夏记录)", + "The_Invisible_Hand": "信用点(看不见的手)", + "Abandoned_and_Insulted": "燃素&金属(被废弃与损害的)", + "Spring_of_Life": "虚粒子&固态净水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黄金大地)", + "The_Blossom_in_the_Storm": "气态流体&种子(风暴中怒放的花)" + } + }, "Gui": { "Aside": { "Install": "安装", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 896196f48..da98b1d65 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -29,6 +29,10 @@ "BattlePass": { "name": "無名勳禮", "help": "" + }, + "Assignment": { + "name": "委託設置", + "help": "" } }, "Scheduler": { @@ -216,6 +220,88 @@ "6": "6" } }, + "Assignment": { + "_info": { + "name": "委託設置", + "help": "領取獎勵並派遣,優先處理指定委託\n若處理指定委託之後未達到上限,則按經驗材料 → 角色專屬素材 → 合成材料的順序來派遣委託" + }, + "Duration": { + "name": "派遣時間", + "help": "", + "4": "4", + "8": "8", + "12": "12", + "20": "20" + }, + "Name_1": { + "name": "第1個委託選擇", + "help": "", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Born_to_Obey": "古代零件(生而服從)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Nameless_Land_Nameless_People": "角色經驗材料(無名之地,無名之人)", + "Akashic_Records": "光錐強化材料(阿卡夏記錄)", + "The_Invisible_Hand": "信用點(看不見的手)", + "Abandoned_and_Insulted": "燃素&金屬(被廢棄與損害的)", + "Spring_of_Life": "虛粒子&固態淨水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黃金大地)", + "The_Blossom_in_the_Storm": "氣態流體&種子(風暴中怒放的花)" + }, + "Name_2": { + "name": "第2個委託選擇", + "help": "", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Born_to_Obey": "古代零件(生而服從)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Nameless_Land_Nameless_People": "角色經驗材料(無名之地,無名之人)", + "Akashic_Records": "光錐強化材料(阿卡夏記錄)", + "The_Invisible_Hand": "信用點(看不見的手)", + "Abandoned_and_Insulted": "燃素&金屬(被廢棄與損害的)", + "Spring_of_Life": "虛粒子&固態淨水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黃金大地)", + "The_Blossom_in_the_Storm": "氣態流體&種子(風暴中怒放的花)" + }, + "Name_3": { + "name": "第3個委託選擇", + "help": "", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Born_to_Obey": "古代零件(生而服從)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Nameless_Land_Nameless_People": "角色經驗材料(無名之地,無名之人)", + "Akashic_Records": "光錐強化材料(阿卡夏記錄)", + "The_Invisible_Hand": "信用點(看不見的手)", + "Abandoned_and_Insulted": "燃素&金屬(被廢棄與損害的)", + "Spring_of_Life": "虛粒子&固態淨水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黃金大地)", + "The_Blossom_in_the_Storm": "氣態流體&種子(風暴中怒放的花)" + }, + "Name_4": { + "name": "第4個委託選擇", + "help": "", + "Nine_Billion_Names": "熄滅原核(九十億個名字)", + "Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)", + "Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)", + "Born_to_Obey": "古代零件(生而服從)", + "Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)", + "Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)", + "Nameless_Land_Nameless_People": "角色經驗材料(無名之地,無名之人)", + "Akashic_Records": "光錐強化材料(阿卡夏記錄)", + "The_Invisible_Hand": "信用點(看不見的手)", + "Abandoned_and_Insulted": "燃素&金屬(被廢棄與損害的)", + "Spring_of_Life": "虛粒子&固態淨水(生命之泉)", + "The_Land_of_Gold": "蛋白米&基本食材(黃金大地)", + "The_Blossom_in_the_Storm": "氣態流體&種子(風暴中怒放的花)" + } + }, "Gui": { "Aside": { "Install": "安裝", diff --git a/module/ocr/keyword.py b/module/ocr/keyword.py index 12ad779a7..1781ca6c1 100644 --- a/module/ocr/keyword.py +++ b/module/ocr/keyword.py @@ -26,6 +26,9 @@ class Keyword: """ Instance attributes and methods """ + @cached_property + def ch(self) -> str: + return self.cn @cached_property def cn_parsed(self) -> str: diff --git a/module/ocr/ocr.py b/module/ocr/ocr.py index a0964e1a8..f4446f2b6 100644 --- a/module/ocr/ocr.py +++ b/module/ocr/ocr.py @@ -1,7 +1,8 @@ +import re import time +from datetime import timedelta import cv2 -import re from ppocronnx.predict_system import BoxedResult import module.config.server as server @@ -257,3 +258,33 @@ class DigitCounter(Ocr): else: logger.warning(f'No digit counter found in {result}') return 0, 0, 0 + + +class Duration(Ocr): + @cached_property + def timedelta_regex(self): + regex_str = { + 'ch': r'\D*((?P\d{1,2})小时)?((?P\d{1,2})分钟)?((?P\d{1,2})秒})?', + 'en': r'\D*((?P\d{1,2})h\s*)?((?P\d{1,2})m\s*)?((?P\d{1,2})s)?' + }[self.lang] + return re.compile(regex_str) + + 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) diff --git a/module/ui/draggable_list.py b/module/ui/draggable_list.py index c45b8d54a..acb32d399 100644 --- a/module/ui/draggable_list.py +++ b/module/ui/draggable_list.py @@ -28,6 +28,8 @@ class DraggableList: keyword_class, ocr_class, search_button: ButtonWrapper, + check_row_order: bool = True, + active_color: tuple[int, int, int] = (190, 175, 124) ): """ Args: @@ -42,6 +44,8 @@ class DraggableList: keyword_class = keyword_class[0] self.known_rows = list(keyword_class.instances.values()) self.search_button = search_button + self.check_row_order = check_row_order + self.active_color = active_color self.row_min = 1 self.row_max = len(self.known_rows) @@ -83,12 +87,14 @@ class DraggableList: self.cur_buttons = self.ocr_class(self.search_button) \ .matched_ocr(main.device.image, self.keyword_class) # 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] # Check row order - if len(indexes) >= 2: + if self.check_row_order and len(indexes) >= 2: 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: logger.warning(f'No valid rows loaded into {self}') return @@ -157,7 +163,8 @@ class DraggableList: elif self.cur_max < row_index: self.drag_page('down', main=main) # 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 return True @@ -168,7 +175,7 @@ class DraggableList: return False # 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 False @@ -183,7 +190,8 @@ class DraggableList: Returns: 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: return False diff --git a/module/ui/switch.py b/module/ui/switch.py index f4373cfb4..eddc9472b 100644 --- a/module/ui/switch.py +++ b/module/ui/switch.py @@ -7,7 +7,7 @@ from module.logger import logger 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: # Definitions diff --git a/src.py b/src.py index d4c3ff97d..e39464425 100644 --- a/src.py +++ b/src.py @@ -34,6 +34,10 @@ class StarRailCopilot(AzurLaneAutoScript): from tasks.battle_pass.battle_pass import BattlePassUI BattlePassUI(config=self.config, device=self.device).run() + def assignment(self): + from tasks.assignment.assignment import Assignment + Assignment(config=self.config, device=self.device).run() + if __name__ == '__main__': src = StarRailCopilot('src') diff --git a/tasks/assignment/assets/assets_assignment_claim.py b/tasks/assignment/assets/assets_assignment_claim.py new file mode 100644 index 000000000..947c4704d --- /dev/null +++ b/tasks/assignment/assets/assets_assignment_claim.py @@ -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), + ), +) diff --git a/tasks/assignment/assets/assets_assignment_dispatch.py b/tasks/assignment/assets/assets_assignment_dispatch.py new file mode 100644 index 000000000..02d560cd1 --- /dev/null +++ b/tasks/assignment/assets/assets_assignment_dispatch.py @@ -0,0 +1,135 @@ +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_START = ButtonWrapper( + name='ASSIGNMENT_START', + cn=Button( + file='./assets/cn/assignment/dispatch/ASSIGNMENT_START.png', + area=(581, 321, 699, 349), + search=(561, 301, 719, 369), + color=(93, 84, 66), + button=(581, 321, 699, 349), + ), +) +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_1_SELECTED = ButtonWrapper( + name='CHARACTER_1_SELECTED', + share=Button( + file='./assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png', + area=(114, 207, 134, 225), + search=(94, 187, 154, 245), + color=(192, 204, 193), + button=(114, 207, 134, 225), + ), +) +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_2_SELECTED = ButtonWrapper( + name='CHARACTER_2_SELECTED', + share=Button( + file='./assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png', + area=(226, 207, 245, 225), + search=(206, 187, 265, 245), + color=(179, 194, 187), + button=(226, 207, 245, 225), + ), +) +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), + ), +) diff --git a/tasks/assignment/assets/assets_assignment_ui.py b/tasks/assignment/assets/assets_assignment_ui.py new file mode 100644 index 000000000..f21944631 --- /dev/null +++ b/tasks/assignment/assets/assets_assignment_ui.py @@ -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), + ), +) diff --git a/tasks/assignment/assignment.py b/tasks/assignment/assignment.py new file mode 100644 index 000000000..804641f14 --- /dev/null +++ b/tasks/assignment/assignment.py @@ -0,0 +1,140 @@ +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, page_menu +from tasks.daily.synthesize import SynthesizeUI + + +class Assignment(AssignmentClaim, SynthesizeUI): + def run(self, assignments: list[AssignmentEntry] = None, duration: int = None): + if assignments is None: + assignments = ( + getattr(self.config, f'Assignment_Name_{i+1}', None) for i in range(4)) + # remove duplicate while keeping order + assignments = list(dict.fromkeys( + x for x in assignments if x is not None)) + assignments = [AssignmentEntry.find(x) for x in assignments] + if len(assignments) < 4: + logger.warning( + 'There are duplicate assignments in config, check it out') + if duration is None: + duration = self.config.Assignment_Duration + + 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() + # There are unchecked assignments + if remain > 0: + for assignment in undispatched[:remain]: + self.goto_entry(assignment) + self.dispatch(assignment, duration) + if remain < len(undispatched): + logger.warning('The following assignments can not be dispatched due to limit: ' + f'{", ".join([x.name for x in undispatched[remain:]])}') + elif remain > len(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])}') + _, remain, _ = self._limit_status + 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 remain > 0: + self.dispatch(assignment, duration) + remain -= 1 + else: + 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) + _, remain, total = self._limit_status + if total == len(self.dispatched): + 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) + if assignment in self.dispatched: + continue + self.goto_entry(assignment) + if self.appear(CLAIM): + self.claim(assignment, None, should_redispatch=False) + remain += 1 + continue + if self.appear(DISPATCHED): + self.dispatched[assignment] = datetime.now() + Duration( + OCR_ASSIGNMENT_TIME).ocr_single_line(self.device.image) + continue + break + return remain + + 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) + remain -= 1 + if remain <= 0: + return diff --git a/tasks/assignment/claim.py b/tasks/assignment/claim.py new file mode 100644 index 000000000..7d3c30cd5 --- /dev/null +++ b/tasks/assignment/claim.py @@ -0,0 +1,97 @@ +from datetime import datetime, timedelta + +from module.base.timer import Timer +from module.logger import logger +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 or EMPTY_SLOT + """ + redispatched = False + self._wait_for_report() + if should_redispatch: + redispatched = self._is_duration_expected(duration_expected) + self._exit_report(redispatched) + if redispatched: + self._wait_until_assignment_started() + self.dispatched[assignment] = datetime.now( + ) + timedelta(hours=duration_expected) + elif should_redispatch: + # Re-select duration and dispatch + self.dispatch(assignment, duration_expected) + + def _wait_for_report(self): + """ + Pages: + in: CLAIM + out: REDISPATCH + """ + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + # End + if self.appear(REDISPATCH): + logger.info('Assignment report appears') + break + # Claim rewards + if self.appear_then_click(CLAIM, interval=2): + continue + + def _exit_report(self, should_redispatch: bool): + """ + Args: + should_redispatch (bool): determined by user config and duration in report + + Pages: + in: CLOSE_REPORT and REDISPATCH + out: EMPTY_SLOT or DISPATCHED + """ + click_button, check_button = CLOSE_REPORT, EMPTY_SLOT + if should_redispatch: + click_button, check_button = REDISPATCH, DISPATCHED + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + # End + if self.appear(check_button): + logger.info('Assignment report is closed') + break + # Close report + if self.appear_then_click(click_button, interval=2): + continue + + 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 diff --git a/tasks/assignment/dispatch.py b/tasks/assignment/dispatch.py new file mode 100644 index 000000000..04405f1f9 --- /dev/null +++ b/tasks/assignment/dispatch.py @@ -0,0 +1,134 @@ +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): + """ + Dispatch assignment. + Should be called only when limit is checked + + Args: + assignment (AssignmentEntry): + duration (int): user specified duration + + Pages: + in: EMPTY_SLOT + out: DISPATCHED + """ + self._select_characters() + self._select_duration(duration) + self._confirm_assignment() + self._wait_until_assignment_started() + self.dispatched[assignment] = datetime.now() + \ + timedelta(hours=duration) + + def _select_characters(self): + """ + Pages: + in: EMPTY_SLOT + out: CHARACTER_LIST + """ + skip_first_screenshot = True + self.interval_clear( + (CHARACTER_LIST, CHARACTER_1_SELECTED, CHARACTER_2_SELECTED), interval=2) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + # End + if self.match_template_color(CONFIRM_ASSIGNMENT): + logger.info('Characters are all selected') + break + # Ensure character list + if not self.appear(CHARACTER_LIST): + if self.interval_is_reached(CHARACTER_LIST, interval=2): + self.interval_reset(CHARACTER_LIST, interval=2) + self.device.click(EMPTY_SLOT) + continue + # Select + if self.interval_is_reached(CHARACTER_1_SELECTED, interval=2): + self.interval_reset(CHARACTER_1_SELECTED, interval=2) + if not self.image_color_count(CHARACTER_1_SELECTED, (240, 240, 240)): + self.device.click(CHARACTER_1) + if self.interval_is_reached(CHARACTER_2_SELECTED, interval=2): + self.interval_reset(CHARACTER_2_SELECTED, interval=2) + if not self.image_color_count(CHARACTER_2_SELECTED, (240, 240, 240)): + 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): + """ + Pages: + in: CONFIRM_ASSIGNMENT + out: DISPATCHED + """ + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + # End + if self.appear(DISPATCHED): + logger.info(f'Assignment dispatched') + break + # Click + if self.appear_then_click(CONFIRM_ASSIGNMENT, interval=2): + continue + + def _wait_until_assignment_started(self): + """ + Pages: + in: DISPATCHED + out: ASSIGNMENT_STARTED_CHECK + """ + skip_first_screenshot = True + timeout = Timer(2, count=4).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + # End + if self.appear(ASSIGNMENT_START): + logger.info('Assignment start') + break + # Timeout + if timeout.reached(): + logger.warning('Wait for assignment start timeout') + break + skip_first_screenshot = True + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + # End + if self.appear(ASSIGNMENT_STARTED_CHECK): + logger.info('Assignment started') + break diff --git a/tasks/assignment/keywords/__init__.py b/tasks/assignment/keywords/__init__.py new file mode 100644 index 000000000..3ecbdeebf --- /dev/null +++ b/tasks/assignment/keywords/__init__.py @@ -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 \ No newline at end of file diff --git a/tasks/assignment/keywords/classes.py b/tasks/assignment/keywords/classes.py new file mode 100644 index 000000000..25e11167c --- /dev/null +++ b/tasks/assignment/keywords/classes.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import ClassVar + +from module.ocr.keyword import Keyword + + +@dataclass(repr=False) +class AssignmentGroup(Keyword): + instances: ClassVar = {} + entries: tuple[AssignmentEntry] = () + + +@dataclass(repr=False) +class AssignmentEntry(Keyword): + instances: ClassVar = {} + group: AssignmentGroup = None + def __hash__(self) -> int: + return super().__hash__() + diff --git a/tasks/assignment/keywords/entry.py b/tasks/assignment/keywords/entry.py new file mode 100644 index 000000000..70f6b06b7 --- /dev/null +++ b/tasks/assignment/keywords/entry.py @@ -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='嵐の中で咲き誇る花', +) diff --git a/tasks/assignment/keywords/group.py b/tasks/assignment/keywords/group.py new file mode 100644 index 000000000..9f12793ea --- /dev/null +++ b/tasks/assignment/keywords/group.py @@ -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='合成材料', +) diff --git a/tasks/assignment/ui.py b/tasks/assignment/ui.py new file mode 100644 index 000000000..adabd863e --- /dev/null +++ b/tasks/assignment/ui.py @@ -0,0 +1,161 @@ +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 + keyword_lang = self.lang + if self.lang == 'ch': + keyword_lang = 'cn' + matched = getattr(KEYWORDS_ASSIGNMENT_ENTRY, matched.lastgroup) + matched = getattr(matched, keyword_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, + check_row_order=False, + 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) + """ + 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 + """ + ASSIGNMENT_ENTRY_LIST.load_rows(main=self) + for button in ASSIGNMENT_ENTRY_LIST.cur_buttons: + yield button.matched_keyword diff --git a/tasks/base/assets/assets_base_page.py b/tasks/base/assets/assets_base_page.py index 65f5a6ba4..6e71523a8 100644 --- a/tasks/base/assets/assets_base_page.py +++ b/tasks/base/assets/assets_base_page.py @@ -3,6 +3,16 @@ 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_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( name='BATTLE_PASS_CHECK', share=Button( @@ -213,6 +223,16 @@ MENU_CHECK = ButtonWrapper( 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( name='MENU_GOTO_CAMERA', share=Button( diff --git a/tasks/base/page.py b/tasks/base/page.py index 9e329636b..d4a4678e0 100644 --- a/tasks/base/page.py +++ b/tasks/base/page.py @@ -135,3 +135,8 @@ page_menu.link(MENU_GOTO_CAMERA, destination=page_camera) page_synthesize = Page(SYNTHESIZE_CHECK) page_synthesize.link(CLOSE, destination=page_menu) 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)