BIN
assets/cn/assignment/claim/CLAIM.BUTTON.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/cn/assignment/claim/CLAIM.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
assets/cn/assignment/claim/CLOSE_REPORT.BUTTON.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/cn/assignment/claim/CLOSE_REPORT.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/cn/assignment/claim/REDISPATCH.BUTTON.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/cn/assignment/claim/REDISPATCH.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/cn/assignment/dispatch/ASSIGNMENT_START.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/cn/assignment/dispatch/CHARACTER_LIST.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.BUTTON.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/cn/assignment/dispatch/CONFIRM_ASSIGNMENT.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/cn/assignment/ui/DISPATCHED.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/share/assignment/dispatch/ASSIGNMENT_STARTED_CHECK.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_1.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_1_SELECTED.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_2.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
assets/share/assignment/dispatch/CHARACTER_2_SELECTED.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/share/assignment/dispatch/DURATION_12.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/DURATION_20.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/DURATION_4.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
assets/share/assignment/dispatch/DURATION_8.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/dispatch/EMPTY_SLOT.SEARCH.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/share/assignment/dispatch/EMPTY_SLOT.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/share/assignment/ui/CHARACTER_MATERIALS.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
assets/share/assignment/ui/ENTRY_LOADED.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/share/assignment/ui/SYNTHESIS_MATERIALS.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/share/base/page/ASSIGNMENT_CHECK.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/share/base/page/MENU_GOTO_ASSIGNMENT.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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__':
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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, ]
|
@ -13,7 +13,8 @@
|
||||
"tasks": [
|
||||
"Dungeon",
|
||||
"DailyQuest",
|
||||
"BattlePass"
|
||||
"BattlePass",
|
||||
"Assignment"
|
||||
]
|
||||
}
|
||||
}
|
@ -29,3 +29,6 @@ Daily:
|
||||
- Scheduler
|
||||
BattlePass:
|
||||
- Scheduler
|
||||
Assignment:
|
||||
- Scheduler
|
||||
- Assignment
|
||||
|
@ -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
|
||||
|
@ -8,7 +8,7 @@ class ManualConfig:
|
||||
|
||||
SCHEDULER_PRIORITY = """
|
||||
Restart
|
||||
> Dungeon > DailyQuest > BattlePass
|
||||
> Dungeon > Assignment > DailyQuest > BattlePass
|
||||
"""
|
||||
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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": "インストール",
|
||||
|
@ -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": "安装",
|
||||
|
@ -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": "安裝",
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
@ -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')
|
||||
|
45
tasks/assignment/assets/assets_assignment_claim.py
Normal file
@ -0,0 +1,45 @@
|
||||
from module.base.button import Button, ButtonWrapper
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.button_extract ```
|
||||
|
||||
CLAIM = ButtonWrapper(
|
||||
name='CLAIM',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/claim/CLAIM.png',
|
||||
area=(1031, 652, 1101, 674),
|
||||
search=(1011, 632, 1121, 694),
|
||||
color=(169, 134, 66),
|
||||
button=(920, 644, 1210, 683),
|
||||
),
|
||||
)
|
||||
CLOSE_REPORT = ButtonWrapper(
|
||||
name='CLOSE_REPORT',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/claim/CLOSE_REPORT.png',
|
||||
area=(397, 598, 472, 623),
|
||||
search=(377, 578, 492, 643),
|
||||
color=(159, 157, 153),
|
||||
button=(290, 592, 579, 630),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_REPORT_TIME = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_REPORT_TIME',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/claim/OCR_ASSIGNMENT_REPORT_TIME.png',
|
||||
area=(894, 191, 1003, 216),
|
||||
search=(874, 171, 1023, 236),
|
||||
color=(62, 63, 63),
|
||||
button=(894, 191, 1003, 216),
|
||||
),
|
||||
)
|
||||
REDISPATCH = ButtonWrapper(
|
||||
name='REDISPATCH',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/claim/REDISPATCH.png',
|
||||
area=(784, 598, 901, 622),
|
||||
search=(764, 578, 921, 642),
|
||||
color=(158, 157, 155),
|
||||
button=(700, 592, 987, 629),
|
||||
),
|
||||
)
|
135
tasks/assignment/assets/assets_assignment_dispatch.py
Normal 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),
|
||||
),
|
||||
)
|
85
tasks/assignment/assets/assets_assignment_ui.py
Normal file
@ -0,0 +1,85 @@
|
||||
from module.base.button import Button, ButtonWrapper
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.button_extract ```
|
||||
|
||||
CHARACTER_MATERIALS = ButtonWrapper(
|
||||
name='CHARACTER_MATERIALS',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/CHARACTER_MATERIALS.png',
|
||||
area=(123, 81, 307, 134),
|
||||
search=(103, 61, 327, 154),
|
||||
color=(234, 233, 229),
|
||||
button=(123, 81, 307, 134),
|
||||
),
|
||||
)
|
||||
DISPATCHED = ButtonWrapper(
|
||||
name='DISPATCHED',
|
||||
cn=Button(
|
||||
file='./assets/cn/assignment/ui/DISPATCHED.png',
|
||||
area=(1032, 652, 1095, 674),
|
||||
search=(1012, 632, 1115, 694),
|
||||
color=(99, 93, 85),
|
||||
button=(1032, 652, 1095, 674),
|
||||
),
|
||||
)
|
||||
ENTRY_LOADED = ButtonWrapper(
|
||||
name='ENTRY_LOADED',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/ENTRY_LOADED.png',
|
||||
area=(446, 164, 466, 615),
|
||||
search=(426, 144, 486, 635),
|
||||
color=(203, 202, 194),
|
||||
button=(446, 164, 466, 615),
|
||||
),
|
||||
)
|
||||
EXP_MATERIALS_CREDITS = ButtonWrapper(
|
||||
name='EXP_MATERIALS_CREDITS',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/EXP_MATERIALS_CREDITS.png',
|
||||
area=(343, 83, 527, 133),
|
||||
search=(323, 63, 547, 153),
|
||||
color=(222, 221, 217),
|
||||
button=(343, 83, 527, 133),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_LIMIT = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_LIMIT',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIMIT.png',
|
||||
area=(1095, 86, 1179, 126),
|
||||
search=(1075, 66, 1199, 146),
|
||||
color=(62, 61, 60),
|
||||
button=(1095, 86, 1179, 126),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_LIST = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_LIST',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_LIST.png',
|
||||
area=(141, 160, 502, 621),
|
||||
search=(121, 140, 522, 641),
|
||||
color=(202, 200, 194),
|
||||
button=(141, 160, 502, 621),
|
||||
),
|
||||
)
|
||||
OCR_ASSIGNMENT_TIME = ButtonWrapper(
|
||||
name='OCR_ASSIGNMENT_TIME',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/OCR_ASSIGNMENT_TIME.png',
|
||||
area=(588, 566, 926, 588),
|
||||
search=(568, 546, 946, 608),
|
||||
color=(128, 111, 89),
|
||||
button=(588, 566, 926, 588),
|
||||
),
|
||||
)
|
||||
SYNTHESIS_MATERIALS = ButtonWrapper(
|
||||
name='SYNTHESIS_MATERIALS',
|
||||
share=Button(
|
||||
file='./assets/share/assignment/ui/SYNTHESIS_MATERIALS.png',
|
||||
area=(558, 85, 748, 135),
|
||||
search=(538, 65, 768, 155),
|
||||
color=(230, 229, 225),
|
||||
button=(558, 85, 748, 135),
|
||||
),
|
||||
)
|
140
tasks/assignment/assignment.py
Normal 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
@ -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
|
134
tasks/assignment/dispatch.py
Normal 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
|
30
tasks/assignment/keywords/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
import tasks.assignment.keywords.entry as KEYWORDS_ASSIGNMENT_ENTRY
|
||||
import tasks.assignment.keywords.group as KEYWORDS_ASSIGNMENT_GROUP
|
||||
from tasks.assignment.keywords.classes import AssignmentGroup, AssignmentEntry
|
||||
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials.entries = (
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Nine_Billion_Names,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Destruction_of_the_Destroyer,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Winter_Soldiers,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Born_to_Obey,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Root_Out_the_Turpitude,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Fire_Lord_Inflames_Blades_of_War,
|
||||
)
|
||||
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits.entries = (
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Nameless_Land_Nameless_People,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Akashic_Records,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.The_Invisible_Hand,
|
||||
)
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials.entries = (
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Abandoned_and_Insulted,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.Spring_of_Life,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.The_Land_of_Gold,
|
||||
KEYWORDS_ASSIGNMENT_ENTRY.The_Blossom_in_the_Storm,
|
||||
)
|
||||
for group in (
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Character_Materials,
|
||||
KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits,
|
||||
KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials,
|
||||
):
|
||||
for entry in group.entries:
|
||||
entry.group = group
|
20
tasks/assignment/keywords/classes.py
Normal file
@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
from module.ocr.keyword import Keyword
|
||||
|
||||
|
||||
@dataclass(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__()
|
||||
|
109
tasks/assignment/keywords/entry.py
Normal file
@ -0,0 +1,109 @@
|
||||
from .classes import AssignmentEntry
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.keyword_extract ```
|
||||
|
||||
Nine_Billion_Names = AssignmentEntry(
|
||||
id=1,
|
||||
name='Nine_Billion_Names',
|
||||
cn='九十亿个名字',
|
||||
cht='九十億個名字',
|
||||
en='Nine Billion Names',
|
||||
jp='九十億の御名',
|
||||
)
|
||||
Destruction_of_the_Destroyer = AssignmentEntry(
|
||||
id=2,
|
||||
name='Destruction_of_the_Destroyer',
|
||||
cn='毁灭者的覆灭',
|
||||
cht='毀滅者的覆滅',
|
||||
en='Destruction of the Destroyer',
|
||||
jp='壊滅者の覆没',
|
||||
)
|
||||
Winter_Soldiers = AssignmentEntry(
|
||||
id=3,
|
||||
name='Winter_Soldiers',
|
||||
cn='寒冬的战士们',
|
||||
cht='寒冬的戰士們',
|
||||
en='Winter Soldiers',
|
||||
jp='寒冬の戦士たち',
|
||||
)
|
||||
Born_to_Obey = AssignmentEntry(
|
||||
id=4,
|
||||
name='Born_to_Obey',
|
||||
cn='生而服从',
|
||||
cht='生而服從',
|
||||
en='Born to Obey',
|
||||
jp='生まれながらに服従する',
|
||||
)
|
||||
Root_Out_the_Turpitude = AssignmentEntry(
|
||||
id=5,
|
||||
name='Root_Out_the_Turpitude',
|
||||
cn='根除恶孽',
|
||||
cht='根除惡孽',
|
||||
en='Root Out the Turpitude',
|
||||
jp='悪孽を根絶やしに',
|
||||
)
|
||||
Fire_Lord_Inflames_Blades_of_War = AssignmentEntry(
|
||||
id=6,
|
||||
name='Fire_Lord_Inflames_Blades_of_War',
|
||||
cn='火帝动炉销剑戟',
|
||||
cht='火帝動爐銷劍戟',
|
||||
en='Fire Lord Inflames Blades of War',
|
||||
jp='剣戟を焼却する火帝炉',
|
||||
)
|
||||
Nameless_Land_Nameless_People = AssignmentEntry(
|
||||
id=7,
|
||||
name='Nameless_Land_Nameless_People',
|
||||
cn='无名之地,无名之人',
|
||||
cht='無名之地,無名之人',
|
||||
en='Nameless Land, Nameless People',
|
||||
jp='無名の地、無名の人',
|
||||
)
|
||||
Akashic_Records = AssignmentEntry(
|
||||
id=8,
|
||||
name='Akashic_Records',
|
||||
cn='阿卡夏记录',
|
||||
cht='阿卡夏紀錄',
|
||||
en='Akashic Records',
|
||||
jp='アーカーシャの記録',
|
||||
)
|
||||
The_Invisible_Hand = AssignmentEntry(
|
||||
id=9,
|
||||
name='The_Invisible_Hand',
|
||||
cn='看不见的手',
|
||||
cht='看不見的手',
|
||||
en='The Invisible Hand',
|
||||
jp='見えざる手',
|
||||
)
|
||||
Abandoned_and_Insulted = AssignmentEntry(
|
||||
id=10,
|
||||
name='Abandoned_and_Insulted',
|
||||
cn='被废弃与损害的',
|
||||
cht='被廢棄與損害的',
|
||||
en='Abandoned and Insulted',
|
||||
jp='捨てられしものと傷つけられしもの',
|
||||
)
|
||||
Spring_of_Life = AssignmentEntry(
|
||||
id=11,
|
||||
name='Spring_of_Life',
|
||||
cn='生命之泉',
|
||||
cht='生命之泉',
|
||||
en='Spring of Life',
|
||||
jp='生命の泉',
|
||||
)
|
||||
The_Land_of_Gold = AssignmentEntry(
|
||||
id=12,
|
||||
name='The_Land_of_Gold',
|
||||
cn='黄金大地',
|
||||
cht='黃金大地',
|
||||
en='The Land of Gold',
|
||||
jp='黄金の大地',
|
||||
)
|
||||
The_Blossom_in_the_Storm = AssignmentEntry(
|
||||
id=13,
|
||||
name='The_Blossom_in_the_Storm',
|
||||
cn='风暴中怒放的花',
|
||||
cht='風暴中怒放的花',
|
||||
en='The Blossom in the Storm',
|
||||
jp='嵐の中で咲き誇る花',
|
||||
)
|
29
tasks/assignment/keywords/group.py
Normal file
@ -0,0 +1,29 @@
|
||||
from .classes import AssignmentGroup
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.keyword_extract ```
|
||||
|
||||
Character_Materials = AssignmentGroup(
|
||||
id=1,
|
||||
name='Character_Materials',
|
||||
cn='专属材料',
|
||||
cht='專屬素材',
|
||||
en='Character Materials',
|
||||
jp='専用素材',
|
||||
)
|
||||
EXP_Materials_Credits = AssignmentGroup(
|
||||
id=2,
|
||||
name='EXP_Materials_Credits',
|
||||
cn='经验材料/信用点',
|
||||
cht='經驗素材/信用點',
|
||||
en='EXP Materials/Credits',
|
||||
jp='経験値素材/信用ポイント',
|
||||
)
|
||||
Synthesis_Materials = AssignmentGroup(
|
||||
id=3,
|
||||
name='Synthesis_Materials',
|
||||
cn='合成材料',
|
||||
cht='合成材料',
|
||||
en='Synthesis Materials',
|
||||
jp='合成材料',
|
||||
)
|
161
tasks/assignment/ui.py
Normal 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
|
@ -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(
|
||||
|
@ -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)
|
||||
|