Merge pull request #23 from Zebartin/assignment

Assignment
This commit is contained in:
LmeSzinc 2023-06-26 18:32:51 +08:00 committed by GitHub
commit a27c6015b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1588 additions and 13 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

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

View File

@ -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'[(),#]|</?\w+>', '', 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__':

View File

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

View File

@ -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"
]
}
}
}
}

View File

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

View File

@ -13,7 +13,8 @@
"tasks": [
"Dungeon",
"DailyQuest",
"BattlePass"
"BattlePass",
"Assignment"
]
}
}

View File

@ -29,3 +29,6 @@ Daily:
- Scheduler
BattlePass:
- Scheduler
Assignment:
- Scheduler
- Assignment

View File

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

View File

@ -8,7 +8,7 @@ class ManualConfig:
SCHEDULER_PRIORITY = """
Restart
> Dungeon > DailyQuest > BattlePass
> Dungeon > Assignment > DailyQuest > BattlePass
"""
"""

View File

@ -356,6 +356,13 @@ class ConfigGenerator:
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')
option += list(VALID_PACKAGE.keys())
@ -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)

View File

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

View File

@ -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": "インストール",

View File

@ -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": "安装",

View File

@ -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": "安裝",

View File

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

View File

@ -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<hours>\d{1,2})小时)?((?P<minutes>\d{1,2})分钟)?((?P<seconds>\d{1,2})秒})?',
'en': r'\D*((?P<hours>\d{1,2})h\s*)?((?P<minutes>\d{1,2})m\s*)?((?P<seconds>\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)

View File

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

View File

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

4
src.py
View File

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

View File

@ -0,0 +1,45 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CLAIM = ButtonWrapper(
name='CLAIM',
cn=Button(
file='./assets/cn/assignment/claim/CLAIM.png',
area=(1031, 652, 1101, 674),
search=(1011, 632, 1121, 694),
color=(169, 134, 66),
button=(920, 644, 1210, 683),
),
)
CLOSE_REPORT = ButtonWrapper(
name='CLOSE_REPORT',
cn=Button(
file='./assets/cn/assignment/claim/CLOSE_REPORT.png',
area=(397, 598, 472, 623),
search=(377, 578, 492, 643),
color=(159, 157, 153),
button=(290, 592, 579, 630),
),
)
OCR_ASSIGNMENT_REPORT_TIME = ButtonWrapper(
name='OCR_ASSIGNMENT_REPORT_TIME',
share=Button(
file='./assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png',
area=(894, 191, 1003, 216),
search=(874, 171, 1023, 236),
color=(62, 63, 63),
button=(894, 191, 1003, 216),
),
)
REDISPATCH = ButtonWrapper(
name='REDISPATCH',
cn=Button(
file='./assets/cn/assignment/claim/REDISPATCH.png',
area=(784, 598, 901, 622),
search=(764, 578, 921, 642),
color=(158, 157, 155),
button=(700, 592, 987, 629),
),
)

View File

@ -0,0 +1,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),
),
)

View File

@ -0,0 +1,85 @@
from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.button_extract ```
CHARACTER_MATERIALS = ButtonWrapper(
name='CHARACTER_MATERIALS',
share=Button(
file='./assets/share/assignment/ui/CHARACTER_MATERIALS.png',
area=(123, 81, 307, 134),
search=(103, 61, 327, 154),
color=(234, 233, 229),
button=(123, 81, 307, 134),
),
)
DISPATCHED = ButtonWrapper(
name='DISPATCHED',
cn=Button(
file='./assets/cn/assignment/ui/DISPATCHED.png',
area=(1032, 652, 1095, 674),
search=(1012, 632, 1115, 694),
color=(99, 93, 85),
button=(1032, 652, 1095, 674),
),
)
ENTRY_LOADED = ButtonWrapper(
name='ENTRY_LOADED',
share=Button(
file='./assets/share/assignment/ui/ENTRY_LOADED.png',
area=(446, 164, 466, 615),
search=(426, 144, 486, 635),
color=(203, 202, 194),
button=(446, 164, 466, 615),
),
)
EXP_MATERIALS_CREDITS = ButtonWrapper(
name='EXP_MATERIALS_CREDITS',
share=Button(
file='./assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png',
area=(343, 83, 527, 133),
search=(323, 63, 547, 153),
color=(222, 221, 217),
button=(343, 83, 527, 133),
),
)
OCR_ASSIGNMENT_LIMIT = ButtonWrapper(
name='OCR_ASSIGNMENT_LIMIT',
share=Button(
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png',
area=(1095, 86, 1179, 126),
search=(1075, 66, 1199, 146),
color=(62, 61, 60),
button=(1095, 86, 1179, 126),
),
)
OCR_ASSIGNMENT_LIST = ButtonWrapper(
name='OCR_ASSIGNMENT_LIST',
share=Button(
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png',
area=(141, 160, 502, 621),
search=(121, 140, 522, 641),
color=(202, 200, 194),
button=(141, 160, 502, 621),
),
)
OCR_ASSIGNMENT_TIME = ButtonWrapper(
name='OCR_ASSIGNMENT_TIME',
share=Button(
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png',
area=(588, 566, 926, 588),
search=(568, 546, 946, 608),
color=(128, 111, 89),
button=(588, 566, 926, 588),
),
)
SYNTHESIS_MATERIALS = ButtonWrapper(
name='SYNTHESIS_MATERIALS',
share=Button(
file='./assets/share/assignment/ui/SYNTHESIS_MATERIALS.png',
area=(558, 85, 748, 135),
search=(538, 65, 768, 155),
color=(230, 229, 225),
button=(558, 85, 748, 135),
),
)

View File

@ -0,0 +1,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

97
tasks/assignment/claim.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,30 @@
import tasks.assignment.keywords.entry as KEYWORDS_ASSIGNMENT_ENTRY
import tasks.assignment.keywords.group as KEYWORDS_ASSIGNMENT_GROUP
from tasks.assignment.keywords.classes import AssignmentGroup, AssignmentEntry
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials.entries = (
KEYWORDS_ASSIGNMENT_ENTRY.Nine_Billion_Names,
KEYWORDS_ASSIGNMENT_ENTRY.Destruction_of_the_Destroyer,
KEYWORDS_ASSIGNMENT_ENTRY.Winter_Soldiers,
KEYWORDS_ASSIGNMENT_ENTRY.Born_to_Obey,
KEYWORDS_ASSIGNMENT_ENTRY.Root_Out_the_Turpitude,
KEYWORDS_ASSIGNMENT_ENTRY.Fire_Lord_Inflames_Blades_of_War,
)
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits.entries = (
KEYWORDS_ASSIGNMENT_ENTRY.Nameless_Land_Nameless_People,
KEYWORDS_ASSIGNMENT_ENTRY.Akashic_Records,
KEYWORDS_ASSIGNMENT_ENTRY.The_Invisible_Hand,
)
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials.entries = (
KEYWORDS_ASSIGNMENT_ENTRY.Abandoned_and_Insulted,
KEYWORDS_ASSIGNMENT_ENTRY.Spring_of_Life,
KEYWORDS_ASSIGNMENT_ENTRY.The_Land_of_Gold,
KEYWORDS_ASSIGNMENT_ENTRY.The_Blossom_in_the_Storm,
)
for group in (
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials,
):
for entry in group.entries:
entry.group = group

View File

@ -0,0 +1,20 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import ClassVar
from module.ocr.keyword import Keyword
@dataclass(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__()

View File

@ -0,0 +1,109 @@
from .classes import AssignmentEntry
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.keyword_extract ```
Nine_Billion_Names = AssignmentEntry(
id=1,
name='Nine_Billion_Names',
cn='九十亿个名字',
cht='九十億個名字',
en='Nine Billion Names',
jp='九十億の御名',
)
Destruction_of_the_Destroyer = AssignmentEntry(
id=2,
name='Destruction_of_the_Destroyer',
cn='毁灭者的覆灭',
cht='毀滅者的覆滅',
en='Destruction of the Destroyer',
jp='壊滅者の覆没',
)
Winter_Soldiers = AssignmentEntry(
id=3,
name='Winter_Soldiers',
cn='寒冬的战士们',
cht='寒冬的戰士們',
en='Winter Soldiers',
jp='寒冬の戦士たち',
)
Born_to_Obey = AssignmentEntry(
id=4,
name='Born_to_Obey',
cn='生而服从',
cht='生而服從',
en='Born to Obey',
jp='生まれながらに服従する',
)
Root_Out_the_Turpitude = AssignmentEntry(
id=5,
name='Root_Out_the_Turpitude',
cn='根除恶孽',
cht='根除惡孽',
en='Root Out the Turpitude',
jp='悪孽を根絶やしに',
)
Fire_Lord_Inflames_Blades_of_War = AssignmentEntry(
id=6,
name='Fire_Lord_Inflames_Blades_of_War',
cn='火帝动炉销剑戟',
cht='火帝動爐銷劍戟',
en='Fire Lord Inflames Blades of War',
jp='剣戟を焼却する火帝炉',
)
Nameless_Land_Nameless_People = AssignmentEntry(
id=7,
name='Nameless_Land_Nameless_People',
cn='无名之地,无名之人',
cht='無名之地,無名之人',
en='Nameless Land, Nameless People',
jp='無名の地、無名の人',
)
Akashic_Records = AssignmentEntry(
id=8,
name='Akashic_Records',
cn='阿卡夏记录',
cht='阿卡夏紀錄',
en='Akashic Records',
jp='アーカーシャの記録',
)
The_Invisible_Hand = AssignmentEntry(
id=9,
name='The_Invisible_Hand',
cn='看不见的手',
cht='看不見的手',
en='The Invisible Hand',
jp='見えざる手',
)
Abandoned_and_Insulted = AssignmentEntry(
id=10,
name='Abandoned_and_Insulted',
cn='被废弃与损害的',
cht='被廢棄與損害的',
en='Abandoned and Insulted',
jp='捨てられしものと傷つけられしもの',
)
Spring_of_Life = AssignmentEntry(
id=11,
name='Spring_of_Life',
cn='生命之泉',
cht='生命之泉',
en='Spring of Life',
jp='生命の泉',
)
The_Land_of_Gold = AssignmentEntry(
id=12,
name='The_Land_of_Gold',
cn='黄金大地',
cht='黃金大地',
en='The Land of Gold',
jp='黄金の大地',
)
The_Blossom_in_the_Storm = AssignmentEntry(
id=13,
name='The_Blossom_in_the_Storm',
cn='风暴中怒放的花',
cht='風暴中怒放的花',
en='The Blossom in the Storm',
jp='嵐の中で咲き誇る花',
)

View File

@ -0,0 +1,29 @@
from .classes import AssignmentGroup
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.keyword_extract ```
Character_Materials = AssignmentGroup(
id=1,
name='Character_Materials',
cn='专属材料',
cht='專屬素材',
en='Character Materials',
jp='専用素材',
)
EXP_Materials_Credits = AssignmentGroup(
id=2,
name='EXP_Materials_Credits',
cn='经验材料/信用点',
cht='經驗素材/信用點',
en='EXP Materials/Credits',
jp='経験値素材/信用ポイント',
)
Synthesis_Materials = AssignmentGroup(
id=3,
name='Synthesis_Materials',
cn='合成材料',
cht='合成材料',
en='Synthesis Materials',
jp='合成材料',
)

161
tasks/assignment/ui.py Normal file
View File

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

View File

@ -3,6 +3,16 @@ from module.base.button import Button, ButtonWrapper
# This file was auto-generated, do not modify it manually. To generate:
# ``` 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(

View File

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