Merge branch 'LmeSzinc:master' into master

This commit is contained in:
zhaoqiman 2024-05-17 19:26:08 +08:00 committed by GitHub
commit 92c6433ae9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
150 changed files with 4163 additions and 1634 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/character/Robin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -441,10 +441,13 @@ pre.rich-traceback-code {
[id^="pywebio-scope-dashboard-value-"] {
display: flex;
align-items: flex-end;
align-items: baseline;
height: 1.5rem;
}
[id^="pywebio-scope-arg_stored-stored-value-"] p {
margin-bottom: 0;
}
#pywebio-scope-log {
line-height: 1.2;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

View File

@ -199,7 +199,9 @@
"DomainStrategy": "combat",
"UseImmersifier": true,
"DoubleEvent": true,
"UseStamina": false
"WeeklyFarming": false,
"UseStamina": false,
"SimulatedUniverseFarm": {}
},
"RogueBlessing": {
"PresetBlessingFilter": "preset",

View File

@ -1,6 +1,7 @@
import copy
import os
import subprocess
import sys
from typing import Optional, Union
from deploy.Windows.logger import logger
@ -80,12 +81,6 @@ class DeployConfig(ConfigModel):
self.config_template = {}
self.read()
# Bypass webui.config.DeployConfig.__setattr__()
# Don't write these into deploy.yaml
super().__setattr__('GitOverCdn', self.Repository in ['cn'])
if self.Repository in ['global', 'cn']:
super().__setattr__('Repository', 'https://github.com/LmeSzinc/StarRailCopilot')
self.write()
self.show_config()
@ -109,9 +104,21 @@ class DeployConfig(ConfigModel):
if hasattr(self, key):
super().__setattr__(key, value)
self.config_redirect()
def write(self):
poor_yaml_write(self.config, self.file)
def config_redirect(self):
"""
Redirect deploy config, must be called after each `read()`
"""
# Bypass webui.config.DeployConfig.__setattr__()
# Don't write these into deploy.yaml
super().__setattr__('GitOverCdn', self.Repository in ['cn'])
if self.Repository in ['global', 'cn']:
super().__setattr__('Repository', 'https://github.com/LmeSzinc/StarRailCopilot')
def filepath(self, path):
"""
Args:
@ -143,7 +150,7 @@ class DeployConfig(ConfigModel):
if os.path.exists(exe):
return exe
logger.warning(f'AdbExecutable: {exe} does not exists, use `adb` instead')
logger.warning(f'AdbExecutable: {exe} does not exist, use `adb` instead')
return 'adb'
@cached_property
@ -152,12 +159,18 @@ class DeployConfig(ConfigModel):
if os.path.exists(exe):
return exe
logger.warning(f'GitExecutable: {exe} does not exists, use `git` instead')
logger.warning(f'GitExecutable: {exe} does not exist, use `git` instead')
return 'git'
@cached_property
def python(self) -> str:
return self.filepath(self.PythonExecutable)
exe = self.filepath(self.PythonExecutable)
if os.path.exists(exe):
return exe
current = sys.executable.replace("\\", "/")
logger.warning(f'PythonExecutable: {exe} does not exist, use current python instead: {current}')
return current
@cached_property
def requirements_file(self) -> str:

View File

@ -5,6 +5,22 @@ from dev_tools.keywords.base import UI_LANGUAGES, GenerateKeyword
from module.config.utils import deep_get
def resort(dic: dict):
# Poor assigment sort for 2.2
order = [
1008, 1007, 1006, 1005, 1004, 1003, 1002, 1001,
3001, 2001, 4001,
5008, 5006, 5005, 5003, 5002, 5007, 5004, 5001,
]
out = {}
for index in order:
value = dic.pop(index)
out[index] = value
for k, v, in dic.items():
out[k] = v
return out
@cache
def get_assignment_entry_data():
"""
@ -16,6 +32,9 @@ def get_assignment_entry_data():
deep_get(expedition, 'Name.Hash'): deep_get(expedition, 'ExpeditionID')
for expedition in GenerateKeyword.read_file('./ExcelOutput/ExpeditionData.json').values()
}
rev = {v: k for k, v in expedition_namehash_to_id.items()}
rev = resort(rev)
expedition_namehash_to_id = {v: k for k, v in rev.items()}
expedition_id_to_reward_id = {
deep_get(expedition, '4.2.ExpeditionID'): deep_get(expedition, '4.2.RewardID')
for expedition in GenerateKeyword.read_file('./ExcelOutput/ExpeditionReward.json').values()
@ -119,5 +138,6 @@ class GenerateAssignmentEventEntry(GenerateKeyword):
if __name__ == "__main__":
from dev_tools.keywords.base import TextMap
TextMap.DATA_FOLDER = '../StarRailData'
GenerateAssignment()()

View File

@ -63,7 +63,8 @@ class TextMap:
def text_to_variable(text):
text = re.sub("'s |s' ", '_', text)
text = re.sub(r'[ \-—:\'/•.]+', '_', text)
text = re.sub(r'[ \-—–:\'/•.™]+', '_', text)
text = re.sub(r'[█]+', '', text)
text = re.sub(r'[(),#"?!&%*]|</?\w+>', '', text)
# text = re.sub(r'[#_]?\d+(_times?)?', '', text)
text = re.sub(r'<color=#?\w+>', '', text)

View File

@ -16,6 +16,8 @@ def dungeon_name(name: str) -> str:
name = f'Echo_of_War_{name}'
if name in ['The_Swarm_Disaster', 'Gold_and_Gears']:
name = f'Simulated_Universe_{name}'
name = name.replace('Stagnant_Shadow_Stagnant_Shadow', 'Stagnant_Shadow')
name = name.replace('Cavern_of_Corrosion_Cavern_of_Corrosion', 'Cavern_of_Corrosion')
return name
@ -70,7 +72,10 @@ class GenerateDungeonList(GenerateKeyword):
if text.startswith('Calyx_Crimson'):
plane = MapPlane.find_plane_id(keyword['plane_id'])
text = f'{text}_{plane.name}'
if plane is not None:
text = f'{text}_{plane.name}'
else:
text = f'{text}_unknown_plane'
return text
def convert_keyword(self, text: str, lang: str) -> str:

View File

@ -68,3 +68,7 @@ class GenerateMapPlane(GenerateKeyword):
return f'Special_{text}'
else:
return f'{world.short_name}_{text}'
def convert_keyword(self, text: str, lang: str) -> str:
text = text.replace('', '')
return super().convert_keyword(text, lang=lang)

View File

@ -1,5 +1,3 @@
from concurrent.futures import ThreadPoolExecutor
import module.config.server as server_
from module.base.button import Button, ButtonWrapper, ClickButton, match_template
from module.base.timer import Timer
@ -50,11 +48,22 @@ class ModuleBase:
self.interval_timer = {}
@cached_class_property
def worker(self) -> ThreadPoolExecutor:
def worker(self):
"""
A thread pool to run things at background
Examples:
```
def func(image):
logger.info('Update thread start')
with self.config.multi_set():
self.dungeon_get_simuni_point(image)
self.dungeon_update_stamina(image)
ModuleBase.worker.submit(func, self.device.image)
```
"""
logger.hr('Creating worker')
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(1)
return pool

View File

@ -34,12 +34,34 @@ class Button(Resource):
def clear_offset(self):
self._button_offset = (0, 0)
def is_offset_in(self, x=0, y=0):
"""
Args:
x:
y:
Returns:
bool: If _button_offset is in (-x, -y, x, y)
"""
if x:
if self._button_offset[0] < -x or self._button_offset[0] > x:
return False
if y:
if self._button_offset[1] < -y or self._button_offset[1] > y:
return False
return True
@cached_property
def image(self):
return load_image(self.file, self.area)
@cached_property
def image_binary(self):
return rgb2gray(self.image)
def resource_release(self):
del_cached_property(self, 'image')
del_cached_property(self, 'image_binary')
self.clear_offset()
def __str__(self):
@ -96,6 +118,29 @@ class Button(Resource):
self._button_offset = np.array(point) + self.search[:2] - self.area[:2]
return sim > similarity
def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool:
"""
Detects assets by template matching.
To Some buttons, its location may not be static, `_button_offset` will be set.
Args:
image: Screenshot.
similarity (float): 0-1.
direct_match: True to ignore `self.search`
Returns:
bool.
"""
if not direct_match:
image = crop(image, self.search, copy=False)
image = rgb2gray(image)
res = cv2.matchTemplate(self.image_binary, image, cv2.TM_CCOEFF_NORMED)
_, sim, _, point = cv2.minMaxLoc(res)
self._button_offset = np.array(point) + self.search[:2] - self.area[:2]
return sim > similarity
def match_multi_template(self, image, similarity=0.85, direct_match=False):
"""
Detects assets by template matching, return multiple reults
@ -208,6 +253,13 @@ class ButtonWrapper(Resource):
return True
return False
def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool:
for assets in self.buttons:
if assets.match_template_binary(image, similarity=similarity, direct_match=direct_match):
self._matched_button = assets
return True
return False
def match_multi_template(self, image, similarity=0.85, threshold=5, direct_match=False):
"""
Detects assets by template matching, return multiple results
@ -295,6 +347,17 @@ class ButtonWrapper(Resource):
for b in self.iter_buttons():
b.clear_offset()
def is_offset_in(self, x=0, y=0):
"""
Args:
x:
y:
Returns:
bool: If _button_offset is in (-x, -y, x, y)
"""
return self.matched_button.is_offset_in(x=x, y=y)
def load_search(self, area):
"""
Set `search` attribute.

View File

@ -625,17 +625,29 @@ def image_paste(image, background, origin):
def rgb2gray(image):
"""
gray = ( MAX(r, g, b) + MIN(r, g, b)) / 2
Args:
image (np.ndarray): Shape (height, width, channel)
Returns:
np.ndarray: Shape (height, width)
"""
# r, g, b = cv2.split(image)
# return cv2.add(
# cv2.multiply(cv2.max(cv2.max(r, g), b), 0.5),
# cv2.multiply(cv2.min(cv2.min(r, g), b), 0.5)
# )
r, g, b = cv2.split(image)
return cv2.add(
cv2.multiply(cv2.max(cv2.max(r, g), b), 0.5),
cv2.multiply(cv2.min(cv2.min(r, g), b), 0.5)
)
maximum = cv2.max(r, g)
cv2.max(maximum, b, dst=maximum)
cv2.convertScaleAbs(maximum, alpha=0.5, dst=maximum)
cv2.min(r, g, dst=r)
cv2.min(r, b, dst=r)
cv2.convertScaleAbs(r, alpha=0.5, dst=r)
# minimum = r
cv2.add(maximum, r, dst=maximum)
return maximum
def rgb2hsv(image):
@ -791,11 +803,24 @@ def color_similarity_2d(image, color):
Returns:
np.ndarray: uint8
"""
r, g, b = cv2.split(cv2.subtract(image, (*color, 0)))
positive = cv2.max(cv2.max(r, g), b)
r, g, b = cv2.split(cv2.subtract((*color, 0), image))
negative = cv2.max(cv2.max(r, g), b)
return cv2.subtract(255, cv2.add(positive, negative))
# r, g, b = cv2.split(cv2.subtract(image, (*color, 0)))
# positive = cv2.max(cv2.max(r, g), b)
# r, g, b = cv2.split(cv2.subtract((*color, 0), image))
# negative = cv2.max(cv2.max(r, g), b)
# return cv2.subtract(255, cv2.add(positive, negative))
diff = cv2.subtract(image, (*color, 0))
r, g, b = cv2.split(diff)
cv2.max(r, g, dst=r)
cv2.max(r, b, dst=r)
positive = r
cv2.subtract((*color, 0), image, dst=diff)
r, g, b = cv2.split(diff)
cv2.max(r, g, dst=r)
cv2.max(r, b, dst=r)
negative = r
cv2.add(positive, negative, dst=positive)
cv2.subtract(255, positive, dst=positive)
return positive
def extract_letters(image, letter=(255, 255, 255), threshold=128):
@ -809,11 +834,24 @@ def extract_letters(image, letter=(255, 255, 255), threshold=128):
Returns:
np.ndarray: Shape (height, width)
"""
r, g, b = cv2.split(cv2.subtract(image, (*letter, 0)))
positive = cv2.max(cv2.max(r, g), b)
r, g, b = cv2.split(cv2.subtract((*letter, 0), image))
negative = cv2.max(cv2.max(r, g), b)
return cv2.multiply(cv2.add(positive, negative), 255.0 / threshold)
# r, g, b = cv2.split(cv2.subtract(image, (*letter, 0)))
# positive = cv2.max(cv2.max(r, g), b)
# r, g, b = cv2.split(cv2.subtract((*letter, 0), image))
# negative = cv2.max(cv2.max(r, g), b)
# return cv2.multiply(cv2.add(positive, negative), 255.0 / threshold)
diff = cv2.subtract(image, (*letter, 0))
r, g, b = cv2.split(diff)
cv2.max(r, g, dst=r)
cv2.max(r, b, dst=r)
positive = r
cv2.subtract((*letter, 0), image, dst=diff)
r, g, b = cv2.split(diff)
cv2.max(r, g, dst=r)
cv2.max(r, b, dst=r)
negative = r
cv2.add(positive, negative, dst=positive)
cv2.convertScaleAbs(positive, alpha=255.0 / threshold, dst=positive)
return positive
def extract_white_letters(image, threshold=128):
@ -827,10 +865,21 @@ def extract_white_letters(image, threshold=128):
Returns:
np.ndarray: Shape (height, width)
"""
# minimum = cv2.min(cv2.min(r, g), b)
# maximum = cv2.max(cv2.max(r, g), b)
# return cv2.multiply(cv2.add(maximum, cv2.subtract(maximum, minimum)), 255.0 / threshold)
r, g, b = cv2.split(cv2.subtract((255, 255, 255, 0), image))
minimum = cv2.min(cv2.min(r, g), b)
maximum = cv2.max(cv2.max(r, g), b)
return cv2.multiply(cv2.add(maximum, cv2.subtract(maximum, minimum)), 255.0 / threshold)
maximum = cv2.max(r, g)
cv2.max(maximum, b, dst=maximum)
cv2.convertScaleAbs(maximum, alpha=0.5, dst=maximum)
cv2.min(r, g, dst=r)
cv2.min(r, b, dst=r)
cv2.convertScaleAbs(r, alpha=0.5, dst=r)
minimum = r
cv2.subtract(maximum, minimum, dst=minimum)
cv2.add(maximum, minimum, dst=maximum)
cv2.convertScaleAbs(maximum, alpha=255.0 / threshold, dst=maximum)
return maximum
def color_mapping(image, max_multiply=2):
@ -849,7 +898,9 @@ def color_mapping(image, max_multiply=2):
low, high = np.min(image), np.max(image)
multiply = min(255 / (high - low), max_multiply)
add = (255 - multiply * (low + high)) / 2
image = cv2.add(cv2.multiply(image, multiply), add)
# image = cv2.add(cv2.multiply(image, multiply), add)
cv2.multiply(image, multiply, dst=image)
cv2.add(image, add, dst=image)
image[image > 255] = 255
image[image < 0] = 0
return image.astype(np.uint8)
@ -909,7 +960,7 @@ def color_bar_percentage(image, area, prev_color, reverse=False, starter=0, thre
Returns:
float: 0 to 1.
"""
image = crop(image, area)
image = crop(image, area, copy=False)
image = image[:, ::-1, :] if reverse else image
length = image.shape[1]
prev_index = starter

View File

@ -48,7 +48,8 @@
"aScreenCap_nc",
"DroidCast",
"DroidCast_raw",
"scrcpy"
"scrcpy",
"nemu_ipc"
],
"display": "hide"
},
@ -237,6 +238,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden",
"Calyx_Crimson_Erudition_Jarilo_RivetTown",
@ -246,6 +248,7 @@
"Calyx_Crimson_Nihility_Luofu_AlchemyCommission",
"Stagnant_Shadow_Spike",
"Stagnant_Shadow_Perdition",
"Stagnant_Shadow_Duty",
"Stagnant_Shadow_Blaze",
"Stagnant_Shadow_Scorch",
"Stagnant_Shadow_Ire",
@ -290,6 +293,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden",
"Calyx_Crimson_Erudition_Jarilo_RivetTown",
@ -357,6 +361,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden",
"Calyx_Crimson_Erudition_Jarilo_RivetTown",
@ -373,6 +378,7 @@
"do_not_achieve",
"Stagnant_Shadow_Spike",
"Stagnant_Shadow_Perdition",
"Stagnant_Shadow_Duty",
"Stagnant_Shadow_Blaze",
"Stagnant_Shadow_Scorch",
"Stagnant_Shadow_Ire",
@ -426,6 +432,7 @@
"Argenti",
"Arlan",
"Asta",
"Aventurine",
"Bailu",
"BlackSwan",
"Blade",
@ -454,6 +461,7 @@
"Natasha",
"Pela",
"Qingque",
"Robin",
"RuanMei",
"Sampo",
"Seele",
@ -1068,100 +1076,100 @@
"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",
"A_Startling_Night_Terror",
"Tranquility_of_Vimala_bhumi",
"Nameless_Land_Nameless_People",
"A_Startling_Night_Terror",
"Fire_Lord_Inflames_Blades_of_War",
"Root_Out_the_Turpitude",
"Born_to_Obey",
"Winter_Soldiers",
"Destruction_of_the_Destroyer",
"Nine_Billion_Names",
"Akashic_Records",
"Nameless_Land_Nameless_People",
"The_Invisible_Hand",
"Abandoned_and_Insulted",
"Spring_of_Life",
"The_Land_of_Gold",
"The_Blossom_in_the_Storm",
"Legend_of_the_Puppet_Master",
"Scalpel_and_Screwdriver",
"The_Wages_of_Humanity",
"Legend_of_the_Puppet_Master",
"The_Land_of_Gold",
"Spring_of_Life",
"Fragments_of_Illusory_Dreams",
"Scalpel_and_Screwdriver"
"The_Blossom_in_the_Storm",
"Abandoned_and_Insulted"
]
},
"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",
"A_Startling_Night_Terror",
"Tranquility_of_Vimala_bhumi",
"Nameless_Land_Nameless_People",
"A_Startling_Night_Terror",
"Fire_Lord_Inflames_Blades_of_War",
"Root_Out_the_Turpitude",
"Born_to_Obey",
"Winter_Soldiers",
"Destruction_of_the_Destroyer",
"Nine_Billion_Names",
"Akashic_Records",
"Nameless_Land_Nameless_People",
"The_Invisible_Hand",
"Abandoned_and_Insulted",
"Spring_of_Life",
"The_Land_of_Gold",
"The_Blossom_in_the_Storm",
"Legend_of_the_Puppet_Master",
"Scalpel_and_Screwdriver",
"The_Wages_of_Humanity",
"Legend_of_the_Puppet_Master",
"The_Land_of_Gold",
"Spring_of_Life",
"Fragments_of_Illusory_Dreams",
"Scalpel_and_Screwdriver"
"The_Blossom_in_the_Storm",
"Abandoned_and_Insulted"
]
},
"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",
"A_Startling_Night_Terror",
"Tranquility_of_Vimala_bhumi",
"Nameless_Land_Nameless_People",
"A_Startling_Night_Terror",
"Fire_Lord_Inflames_Blades_of_War",
"Root_Out_the_Turpitude",
"Born_to_Obey",
"Winter_Soldiers",
"Destruction_of_the_Destroyer",
"Nine_Billion_Names",
"Akashic_Records",
"Nameless_Land_Nameless_People",
"The_Invisible_Hand",
"Abandoned_and_Insulted",
"Spring_of_Life",
"The_Land_of_Gold",
"The_Blossom_in_the_Storm",
"Legend_of_the_Puppet_Master",
"Scalpel_and_Screwdriver",
"The_Wages_of_Humanity",
"Legend_of_the_Puppet_Master",
"The_Land_of_Gold",
"Spring_of_Life",
"Fragments_of_Illusory_Dreams",
"Scalpel_and_Screwdriver"
"The_Blossom_in_the_Storm",
"Abandoned_and_Insulted"
]
},
"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",
"A_Startling_Night_Terror",
"Tranquility_of_Vimala_bhumi",
"Nameless_Land_Nameless_People",
"A_Startling_Night_Terror",
"Fire_Lord_Inflames_Blades_of_War",
"Root_Out_the_Turpitude",
"Born_to_Obey",
"Winter_Soldiers",
"Destruction_of_the_Destroyer",
"Nine_Billion_Names",
"Akashic_Records",
"Nameless_Land_Nameless_People",
"The_Invisible_Hand",
"Abandoned_and_Insulted",
"Spring_of_Life",
"The_Land_of_Gold",
"The_Blossom_in_the_Storm",
"Legend_of_the_Puppet_Master",
"Scalpel_and_Screwdriver",
"The_Wages_of_Humanity",
"Legend_of_the_Puppet_Master",
"The_Land_of_Gold",
"Spring_of_Life",
"Fragments_of_Illusory_Dreams",
"Scalpel_and_Screwdriver"
"The_Blossom_in_the_Storm",
"Abandoned_and_Insulted"
]
},
"Duration": {
@ -1303,7 +1311,8 @@
"Echo_of_War_Destruction_Beginning",
"Echo_of_War_End_of_the_Eternal_Freeze",
"Echo_of_War_Divine_Seed",
"Echo_of_War_Borehole_Planet_Old_Crater"
"Echo_of_War_Borehole_Planet_Old_Crater",
"Echo_of_War_Salutations_of_Ashen_Dreams"
]
},
"Team": {
@ -1340,6 +1349,7 @@
"Argenti",
"Arlan",
"Asta",
"Aventurine",
"Bailu",
"BlackSwan",
"Blade",
@ -1368,6 +1378,7 @@
"Natasha",
"Pela",
"Qingque",
"Robin",
"RuanMei",
"Sampo",
"Seele",
@ -1466,9 +1477,19 @@
"type": "checkbox",
"value": true
},
"WeeklyFarming": {
"type": "checkbox",
"value": false
},
"UseStamina": {
"type": "checkbox",
"value": false
},
"SimulatedUniverseFarm": {
"type": "stored",
"value": {},
"display": "disabled",
"stored": "StoredSimulatedUniverseElite"
}
},
"RogueBlessing": {

View File

@ -29,7 +29,18 @@ Emulator:
option: [ auto, cn, en ]
ScreenshotMethod:
value: auto
option: [ auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy ]
option: [
auto,
ADB,
ADB_nc,
uiautomator2,
aScreenCap,
aScreenCap_nc,
DroidCast,
DroidCast_raw,
scrcpy,
nemu_ipc,
]
ControlMethod:
value: MaaTouch
option: [ minitouch, MaaTouch ]
@ -246,7 +257,11 @@ RogueWorld:
option: [ combat, occurrence ]
UseImmersifier: true
DoubleEvent: true
WeeklyFarming: false
UseStamina: false
SimulatedUniverseFarm:
stored: StoredSimulatedUniverseElite
display: disabled
RogueBlessing:
PresetBlessingFilter:

View File

@ -293,5 +293,18 @@
},
"order": 0,
"color": "#777777"
},
"SimulatedUniverseFarm": {
"name": "SimulatedUniverseFarm",
"path": "Rogue.RogueWorld.SimulatedUniverseFarm",
"i18n": "RogueWorld.SimulatedUniverseFarm.name",
"stored": "StoredSimulatedUniverseElite",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 100,
"value": 0
},
"order": 0,
"color": "#777777"
}
}

View File

@ -176,6 +176,10 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher
self.data, keys="Alas.Optimization.CloseGameDuringWait", default=False
)
@property
def is_actual_task(self):
return self.task.command.lower() not in ['alas', 'template']
@property
def is_cloud_game(self):
return deep_get(

View File

@ -20,7 +20,7 @@ class GeneratedConfig:
Emulator_GameClient = 'android' # android, cloud_android
Emulator_PackageName = 'auto' # auto, CN-Official, CN-Bilibili, OVERSEA-America, OVERSEA-Asia, OVERSEA-Europe, OVERSEA-TWHKMO
Emulator_GameLanguage = 'auto' # auto, cn, en
Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy
Emulator_ScreenshotMethod = 'auto' # auto, ADB, ADB_nc, uiautomator2, aScreenCap, aScreenCap_nc, DroidCast, DroidCast_raw, scrcpy, nemu_ipc
Emulator_ControlMethod = 'MaaTouch' # minitouch, MaaTouch
Emulator_AdbRestart = False
@ -46,20 +46,20 @@ class GeneratedConfig:
CloudStorage_CloudRemainFree = {}
# Group `Dungeon`
Dungeon_Name = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission, Stagnant_Shadow_Spike, Stagnant_Shadow_Perdition, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Ire, Stagnant_Shadow_Rime, Stagnant_Shadow_Icicle, Stagnant_Shadow_Nectar, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Doom, Stagnant_Shadow_Gust, Stagnant_Shadow_Celestial, Stagnant_Shadow_Quanta, Stagnant_Shadow_Abomination, Stagnant_Shadow_Roast, Stagnant_Shadow_Mirage, Stagnant_Shadow_Puppetry, 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, Cavern_of_Corrosion_Path_of_Elixir_Seekers, Cavern_of_Corrosion_Path_of_Darkness, Cavern_of_Corrosion_Path_of_Dreamdive
Dungeon_NameAtDoubleCalyx = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission
Dungeon_Name = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission, Stagnant_Shadow_Spike, Stagnant_Shadow_Perdition, Stagnant_Shadow_Duty, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Ire, Stagnant_Shadow_Rime, Stagnant_Shadow_Icicle, Stagnant_Shadow_Nectar, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Doom, Stagnant_Shadow_Gust, Stagnant_Shadow_Celestial, Stagnant_Shadow_Quanta, Stagnant_Shadow_Abomination, Stagnant_Shadow_Roast, Stagnant_Shadow_Mirage, Stagnant_Shadow_Puppetry, 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, Cavern_of_Corrosion_Path_of_Elixir_Seekers, Cavern_of_Corrosion_Path_of_Darkness, Cavern_of_Corrosion_Path_of_Dreamdive
Dungeon_NameAtDoubleCalyx = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission
Dungeon_NameAtDoubleRelic = 'Cavern_of_Corrosion_Path_of_Providence' # 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, Cavern_of_Corrosion_Path_of_Elixir_Seekers, Cavern_of_Corrosion_Path_of_Darkness, Cavern_of_Corrosion_Path_of_Dreamdive
Dungeon_Team = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
# Group `DungeonDaily`
DungeonDaily_CalyxGolden = 'Calyx_Golden_Treasures_Jarilo_VI' # do_not_achieve, Calyx_Golden_Memories_Jarilo_VI, Calyx_Golden_Memories_The_Xianzhou_Luofu, Calyx_Golden_Memories_Penacony, Calyx_Golden_Aether_Jarilo_VI, Calyx_Golden_Aether_The_Xianzhou_Luofu, Calyx_Golden_Aether_Penacony, Calyx_Golden_Treasures_Jarilo_VI, Calyx_Golden_Treasures_The_Xianzhou_Luofu, Calyx_Golden_Treasures_Penacony
DungeonDaily_CalyxCrimson = 'Calyx_Crimson_Destruction_Herta_StorageZone' # do_not_achieve, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission
DungeonDaily_StagnantShadow = 'Stagnant_Shadow_Quanta' # do_not_achieve, Stagnant_Shadow_Spike, Stagnant_Shadow_Perdition, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Ire, Stagnant_Shadow_Rime, Stagnant_Shadow_Icicle, Stagnant_Shadow_Nectar, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Doom, Stagnant_Shadow_Gust, Stagnant_Shadow_Celestial, Stagnant_Shadow_Quanta, Stagnant_Shadow_Abomination, Stagnant_Shadow_Roast, Stagnant_Shadow_Mirage, Stagnant_Shadow_Puppetry
DungeonDaily_CalyxCrimson = 'Calyx_Crimson_Destruction_Herta_StorageZone' # do_not_achieve, Calyx_Crimson_Destruction_Herta_StorageZone, Calyx_Crimson_Destruction_Luofu_ScalegorgeWaterscape, Calyx_Crimson_Preservation_Herta_SupplyZone, Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark, Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains, Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue, Calyx_Crimson_Abundance_Jarilo_BackwaterPass, Calyx_Crimson_Abundance_Luofu_FyxestrollGarden, Calyx_Crimson_Erudition_Jarilo_RivetTown, Calyx_Crimson_Harmony_Jarilo_RobotSettlement, Calyx_Crimson_Harmony_Penacony_TheReverieDreamscape, Calyx_Crimson_Nihility_Jarilo_GreatMine, Calyx_Crimson_Nihility_Luofu_AlchemyCommission
DungeonDaily_StagnantShadow = 'Stagnant_Shadow_Quanta' # do_not_achieve, Stagnant_Shadow_Spike, Stagnant_Shadow_Perdition, Stagnant_Shadow_Duty, Stagnant_Shadow_Blaze, Stagnant_Shadow_Scorch, Stagnant_Shadow_Ire, Stagnant_Shadow_Rime, Stagnant_Shadow_Icicle, Stagnant_Shadow_Nectar, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Doom, Stagnant_Shadow_Gust, Stagnant_Shadow_Celestial, Stagnant_Shadow_Quanta, Stagnant_Shadow_Abomination, Stagnant_Shadow_Roast, Stagnant_Shadow_Mirage, Stagnant_Shadow_Puppetry
DungeonDaily_CavernOfCorrosion = 'Cavern_of_Corrosion_Path_of_Providence' # do_not_achieve, 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, Cavern_of_Corrosion_Path_of_Elixir_Seekers, Cavern_of_Corrosion_Path_of_Darkness, Cavern_of_Corrosion_Path_of_Dreamdive
# Group `DungeonSupport`
DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use
DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Acheron, Argenti, Arlan, Asta, Bailu, BlackSwan, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, DrRatio, FuXuan, Gallagher, Gepard, Guinaifen, Hanya, Herta, Himeko, Hook, Huohuo, JingYuan, Jingliu, Kafka, Luka, Luocha, Lynx, March7th, Misha, Natasha, Pela, Qingque, RuanMei, Sampo, Seele, Serval, SilverWolf, Sparkle, Sushang, Tingyun, TopazNumby, TrailblazerDestruction, TrailblazerPreservation, Welt, Xueyi, Yanqing, Yukong
DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Acheron, Argenti, Arlan, Asta, Aventurine, Bailu, BlackSwan, Blade, Bronya, Clara, DanHeng, DanHengImbibitorLunae, DrRatio, FuXuan, Gallagher, Gepard, Guinaifen, Hanya, Herta, Himeko, Hook, Huohuo, JingYuan, Jingliu, Kafka, Luka, Luocha, Lynx, March7th, Misha, Natasha, Pela, Qingque, Robin, RuanMei, Sampo, Seele, Serval, SilverWolf, Sparkle, Sushang, Tingyun, TopazNumby, TrailblazerDestruction, TrailblazerPreservation, Welt, Xueyi, Yanqing, Yukong
# Group `DungeonStorage`
DungeonStorage_TrailblazePower = {}
@ -72,7 +72,7 @@ class GeneratedConfig:
SupportReward_Collect = True
# Group `Weekly`
Weekly_Name = 'Echo_of_War_Divine_Seed' # Echo_of_War_Destruction_Beginning, Echo_of_War_End_of_the_Eternal_Freeze, Echo_of_War_Divine_Seed, Echo_of_War_Borehole_Planet_Old_Crater
Weekly_Name = 'Echo_of_War_Divine_Seed' # Echo_of_War_Destruction_Beginning, Echo_of_War_End_of_the_Eternal_Freeze, Echo_of_War_Divine_Seed, Echo_of_War_Borehole_Planet_Old_Crater, Echo_of_War_Salutations_of_Ashen_Dreams
Weekly_Team = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
# Group `AchievableQuest`
@ -119,10 +119,10 @@ class GeneratedConfig:
BattlePassStorage_BattlePassQuestTrailblazePower = {}
# Group `Assignment`
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, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver
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, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver
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, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver
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, A_Startling_Night_Terror, Tranquility_of_Vimala_bhumi, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity, Fragments_of_Illusory_Dreams, Scalpel_and_Screwdriver
Assignment_Name_1 = 'Nameless_Land_Nameless_People' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted
Assignment_Name_2 = 'Akashic_Records' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted
Assignment_Name_3 = 'The_Invisible_Hand' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted
Assignment_Name_4 = 'Nine_Billion_Names' # Tranquility_of_Vimala_bhumi, A_Startling_Night_Terror, Fire_Lord_Inflames_Blades_of_War, Root_Out_the_Turpitude, Born_to_Obey, Winter_Soldiers, Destruction_of_the_Destroyer, Nine_Billion_Names, Akashic_Records, Nameless_Land_Nameless_People, The_Invisible_Hand, Scalpel_and_Screwdriver, The_Wages_of_Humanity, Legend_of_the_Puppet_Master, The_Land_of_Gold, Spring_of_Life, Fragments_of_Illusory_Dreams, The_Blossom_in_the_Storm, Abandoned_and_Insulted
Assignment_Duration = 20 # 4, 8, 12, 20
Assignment_Event = True
Assignment_Assignment = {}
@ -138,7 +138,9 @@ class GeneratedConfig:
RogueWorld_DomainStrategy = 'combat' # combat, occurrence
RogueWorld_UseImmersifier = True
RogueWorld_DoubleEvent = True
RogueWorld_WeeklyFarming = False
RogueWorld_UseStamina = False
RogueWorld_SimulatedUniverseFarm = {}
# Group `RogueBlessing`
RogueBlessing_PresetBlessingFilter = 'preset' # preset, custom

View File

@ -100,7 +100,7 @@ class ConfigGenerator:
options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Echo_of_War])
# Insert characters
from tasks.character.keywords import CharacterList
unsupported_characters = ['Aventurine']
unsupported_characters = ["Boothill", "TrailblazerHarmony"]
characters = [character.name for character in CharacterList.instances.values()
if character.name not in unsupported_characters]
option_add(keys='DungeonSupport.Character.option', options=characters)
@ -430,7 +430,7 @@ class ConfigGenerator:
value=i18n_crimson[ingame_lang].format(path=path, plane=plane))
if dungeon.is_Cavern_of_Corrosion:
value = deep_get(new, keys=['Dungeon', 'Name', dungeon.name], default='')
suffix = i18n_relic[ingame_lang].format(dungeon=dungeon_name)
suffix = i18n_relic[ingame_lang].format(dungeon=dungeon_name).replace('Cavern of Corrosion: ', '')
if not value.endswith(suffix):
deep_set(new, keys=['Dungeon', 'Name', dungeon.name], value=f'{value}{suffix}')
@ -489,7 +489,7 @@ class ConfigGenerator:
for dungeon in dungeons:
world = dungeon.plane.world
world_name = world.__getattribute__(ingame_lang)
dungeon_name = dungeon.__getattribute__(ingame_lang)
dungeon_name = dungeon.__getattribute__(ingame_lang).replace('Echo of War: ', '')
value = f'{dungeon_name} ({world_name})'
deep_set(new, keys=['Weekly', 'Name', dungeon.name], value=value)
# Rogue worlds
@ -653,6 +653,7 @@ class ConfigUpdater:
('Dungeon.Dungeon.NameAtDoubleCalyx', 'Dungeon.Dungeon.NameAtDoubleCalyx', convert_20_dungeon),
('Dungeon.DungeonDaily.CalyxGolden', 'Dungeon.DungeonDaily.CalyxGolden', convert_20_dungeon),
('Dungeon.DungeonDaily.CalyxCrimson', 'Dungeon.DungeonDaily.CalyxCrimson', convert_20_dungeon),
('Rogue.RogueWorld.SimulatedUniverseElite', 'Rogue.RogueWorld.SimulatedUniverseFarm', convert_rogue_farm),
]
@cached_property
@ -863,6 +864,8 @@ class ConfigUpdater:
yield 'Rogue.RogueBlessing.CustomResonanceFilter'
if deep_get(data, 'Rogue.RogueBlessing.PresetCurioFilter') != 'custom':
yield 'Rogue.RogueBlessing.CustomCurioFilter'
if deep_get(data, 'Rogue.RogueWorld.WeeklyFarming', default=False) is False:
yield 'Rogue.RogueWorld.SimulatedUniverseFarm'
def get_hidden_args(self, data) -> t.Set[str]:
"""

View File

@ -30,3 +30,10 @@ def convert_20_dungeon(value):
return 'Calyx_Crimson_Abundance_Jarilo_BackwaterPass'
return value
def convert_rogue_farm(value):
if isinstance(value, dict) and 'value' in value.keys():
value['value'] = 100 - value['value']
value['total'] = 100
return value

View File

@ -131,7 +131,8 @@
"aScreenCap_nc": "aScreenCap_nc",
"DroidCast": "DroidCast",
"DroidCast_raw": "DroidCast_raw",
"scrcpy": "scrcpy"
"scrcpy": "scrcpy",
"nemu_ipc": "nemu_ipc"
},
"ControlMethod": {
"name": "Control Method",
@ -261,6 +262,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "Trace: Preservation (Supply Zone)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Trace: Preservation (Clock Studios Theme Park)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Trace: The Hunt (Outlying Snow Plains)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Trace: The Hunt (SoulGlad Scorchsand Audition Venue)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Trace: Abundance (Backwater Pass)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Trace: Abundance (Fyxestroll Garden)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "Trace: Erudition (Rivet Town)",
@ -270,6 +272,7 @@
"Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "Trace: Nihility (Alchemy Commission)",
"Stagnant_Shadow_Spike": "Ascension: Physical (Natasha / Clara / Luka / Sushang)",
"Stagnant_Shadow_Perdition": "Ascension: Physical (Hanya / Argenti)",
"Stagnant_Shadow_Duty": "Ascension: Physical (Boothill / Robin)",
"Stagnant_Shadow_Blaze": "Ascension: Fire (Himeko / Asta / Hook)",
"Stagnant_Shadow_Scorch": "Ascension: Fire (Guinaifen / Topaz & Numby)",
"Stagnant_Shadow_Ire": "Ascension: Fire (Gallagher)",
@ -312,6 +315,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "Trace: Preservation (Supply Zone)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Trace: Preservation (Clock Studios Theme Park)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Trace: The Hunt (Outlying Snow Plains)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Trace: The Hunt (SoulGlad Scorchsand Audition Venue)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Trace: Abundance (Backwater Pass)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Trace: Abundance (Fyxestroll Garden)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "Trace: Erudition (Rivet Town)",
@ -375,6 +379,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "Trace: Preservation (Supply Zone)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Trace: Preservation (Clock Studios Theme Park)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Trace: The Hunt (Outlying Snow Plains)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Trace: The Hunt (SoulGlad Scorchsand Audition Venue)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Trace: Abundance (Backwater Pass)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Trace: Abundance (Fyxestroll Garden)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "Trace: Erudition (Rivet Town)",
@ -389,6 +394,7 @@
"do_not_achieve": "Don't Do This Quest",
"Stagnant_Shadow_Spike": "Ascension: Physical (Natasha / Clara / Luka / Sushang)",
"Stagnant_Shadow_Perdition": "Ascension: Physical (Hanya / Argenti)",
"Stagnant_Shadow_Duty": "Ascension: Physical (Boothill / Robin)",
"Stagnant_Shadow_Blaze": "Ascension: Fire (Himeko / Asta / Hook)",
"Stagnant_Shadow_Scorch": "Ascension: Fire (Guinaifen / Topaz & Numby)",
"Stagnant_Shadow_Ire": "Ascension: Fire (Gallagher)",
@ -440,6 +446,7 @@
"Argenti": "Argenti",
"Arlan": "Arlan",
"Asta": "Asta",
"Aventurine": "Aventurine",
"Bailu": "Bailu",
"BlackSwan": "Black Swan",
"Blade": "Blade",
@ -468,6 +475,7 @@
"Natasha": "Natasha",
"Pela": "Pela",
"Qingque": "Qingque",
"Robin": "Robin",
"RuanMei": "Ruan Mei",
"Sampo": "Sampo",
"Seele": "Seele",
@ -532,7 +540,8 @@
"Echo_of_War_Destruction_Beginning": "Destruction's Beginning (Herta Space Station)",
"Echo_of_War_End_of_the_Eternal_Freeze": "End of the Eternal Freeze (Jarilo-VI)",
"Echo_of_War_Divine_Seed": "Divine Seed (The Xianzhou Luofu)",
"Echo_of_War_Borehole_Planet_Old_Crater": "Borehole Planet's Old Crater (Herta Space Station)"
"Echo_of_War_Borehole_Planet_Old_Crater": "Borehole Planet's Old Crater (Herta Space Station)",
"Echo_of_War_Salutations_of_Ashen_Dreams": "Salutations of Ashen Dreams (Penacony)"
},
"Team": {
"name": "Dungeon Team",
@ -800,94 +809,94 @@
"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)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)",
"Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)",
"Born_to_Obey": "Ancient Part (Born to Obey)",
"Winter_Soldiers": "Silvermane Badge (Winter Soldiers)",
"Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)",
"Nine_Billion_Names": "Extinguished Core (Nine Billion Names)",
"Akashic_Records": "Light Cone EXP Material (Akashic Records)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"The_Invisible_Hand": "Credit (The Invisible Hand)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)",
"The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)"
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)"
},
"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)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)",
"Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)",
"Born_to_Obey": "Ancient Part (Born to Obey)",
"Winter_Soldiers": "Silvermane Badge (Winter Soldiers)",
"Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)",
"Nine_Billion_Names": "Extinguished Core (Nine Billion Names)",
"Akashic_Records": "Light Cone EXP Material (Akashic Records)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"The_Invisible_Hand": "Credit (The Invisible Hand)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)",
"The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)"
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)"
},
"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)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)",
"Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)",
"Born_to_Obey": "Ancient Part (Born to Obey)",
"Winter_Soldiers": "Silvermane Badge (Winter Soldiers)",
"Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)",
"Nine_Billion_Names": "Extinguished Core (Nine Billion Names)",
"Akashic_Records": "Light Cone EXP Material (Akashic Records)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"The_Invisible_Hand": "Credit (The Invisible Hand)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)",
"The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)"
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)"
},
"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)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Tranquility_of_Vimala_bhumi": "Tatters of Thought (Tranquility of Vimala-bhumi)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"A_Startling_Night_Terror": "Dream Collection Component (A Startling Night Terror)",
"Fire_Lord_Inflames_Blades_of_War": "Artifex's Module (Fire Lord Inflames Blades of War)",
"Root_Out_the_Turpitude": "Immortal Scionette (Root Out the Turpitude)",
"Born_to_Obey": "Ancient Part (Born to Obey)",
"Winter_Soldiers": "Silvermane Badge (Winter Soldiers)",
"Destruction_of_the_Destroyer": "Thief's Instinct (Destruction of the Destroyer)",
"Nine_Billion_Names": "Extinguished Core (Nine Billion Names)",
"Akashic_Records": "Light Cone EXP Material (Akashic Records)",
"Nameless_Land_Nameless_People": "Character EXP Material (Nameless Land, Nameless People)",
"The_Invisible_Hand": "Credit (The Invisible Hand)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)",
"The_Wages_of_Humanity": "Human-Height Auspicious Crops & Extract of Medicinal Herbs (The Wages of Humanity)",
"Legend_of_the_Puppet_Master": "Discarded Ingenium Parts & Jade Abacus Unit (Legend of the Puppet Master)",
"The_Land_of_Gold": "Basic Ingredients & Protein Rice (The Land of Gold)",
"Spring_of_Life": "Solid Water & Virtual Particle (Spring of Life)",
"Fragments_of_Illusory_Dreams": "Tranquility & Broken Dreams (Fragments of Illusory Dreams)",
"Scalpel_and_Screwdriver": "Rusty Gear & Old Molar (Scalpel and Screwdriver)"
"The_Blossom_in_the_Storm": "Gaseous Liquid & Seed (The Blossom in the Storm)",
"Abandoned_and_Insulted": "Phlogiston & Metal (Abandoned and Insulted)"
},
"Duration": {
"name": "Dispatch Duration",
@ -969,9 +978,17 @@
"name": "Participate in Double Planer Event",
"help": ""
},
"WeeklyFarming": {
"name": "Farm 100 Elites Weekly",
"help": ""
},
"UseStamina": {
"name": "Farm Planers Using Trailblase Power",
"help": "Task \"Dungeon\" will no longer run, and all trailblaze power will be used first to claim immersion rewards, except for double events."
},
"SimulatedUniverseFarm": {
"name": "Progress of elite boss farmed",
"help": ""
}
},
"RogueBlessing": {

View File

@ -131,7 +131,8 @@
"aScreenCap_nc": "aScreenCap_nc",
"DroidCast": "DroidCast",
"DroidCast_raw": "DroidCast_raw",
"scrcpy": "scrcpy"
"scrcpy": "scrcpy",
"nemu_ipc": "nemu_ipc"
},
"ControlMethod": {
"name": "Método de control",
@ -261,6 +262,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "Rastros: Conservación (Zona de suministros)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Rastros: Conservación (Parque temático de los Estudios Reloj)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Rastros: Cacería (Llanuras nevadas de las afueras)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Rastros: Cacería (Recinto de las Audiciones FelizAlma en la Arena Ardiente)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Rastros: Abundancia (Paso del Remanso)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Rastros: Abundancia (Jardín del Sosiego)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "Rastros: Erudición (Villarremache)",
@ -270,6 +272,7 @@
"Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "Rastros: Nihilidad (Comisión de Alquimia)",
"Stagnant_Shadow_Spike": "Ascension: Físico (Natasha / Clara / Luka / Sushang)",
"Stagnant_Shadow_Perdition": "Ascension: Físico (Hanya / Argenti)",
"Stagnant_Shadow_Duty": "Ascension: Físico (Boothill / Robin)",
"Stagnant_Shadow_Blaze": "Ascension: Fuego (Himeko / Asta / Hook)",
"Stagnant_Shadow_Scorch": "Ascension: Fuego (Guinaifen / Topaz y Conti)",
"Stagnant_Shadow_Ire": "Ascension: Fuego (Gallagher)",
@ -312,6 +315,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "Rastros: Conservación (Zona de suministros)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Rastros: Conservación (Parque temático de los Estudios Reloj)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Rastros: Cacería (Llanuras nevadas de las afueras)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Rastros: Cacería (Recinto de las Audiciones FelizAlma en la Arena Ardiente)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Rastros: Abundancia (Paso del Remanso)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Rastros: Abundancia (Jardín del Sosiego)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "Rastros: Erudición (Villarremache)",
@ -375,6 +379,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "Rastros: Conservación (Zona de suministros)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "Rastros: Conservación (Parque temático de los Estudios Reloj)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "Rastros: Cacería (Llanuras nevadas de las afueras)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "Rastros: Cacería (Recinto de las Audiciones FelizAlma en la Arena Ardiente)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "Rastros: Abundancia (Paso del Remanso)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "Rastros: Abundancia (Jardín del Sosiego)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "Rastros: Erudición (Villarremache)",
@ -389,6 +394,7 @@
"do_not_achieve": "No hacer esta misión",
"Stagnant_Shadow_Spike": "Ascension: Físico (Natasha / Clara / Luka / Sushang)",
"Stagnant_Shadow_Perdition": "Ascension: Físico (Hanya / Argenti)",
"Stagnant_Shadow_Duty": "Ascension: Físico (Boothill / Robin)",
"Stagnant_Shadow_Blaze": "Ascension: Fuego (Himeko / Asta / Hook)",
"Stagnant_Shadow_Scorch": "Ascension: Fuego (Guinaifen / Topaz y Conti)",
"Stagnant_Shadow_Ire": "Ascension: Fuego (Gallagher)",
@ -440,6 +446,7 @@
"Argenti": "Argenti",
"Arlan": "Arlan",
"Asta": "Asta",
"Aventurine": "Aventurino",
"Bailu": "Bailu",
"BlackSwan": "Cisne Negro",
"Blade": "Blade",
@ -468,6 +475,7 @@
"Natasha": "Natasha",
"Pela": "Pela",
"Qingque": "Qingque",
"Robin": "Robin",
"RuanMei": "Ruan Mei",
"Sampo": "Sampo",
"Seele": "Seele",
@ -532,7 +540,8 @@
"Echo_of_War_Destruction_Beginning": "El principio de la Destrucción (Estación Espacial Herta)",
"Echo_of_War_End_of_the_Eternal_Freeze": "El fin del Hielo Eterno (Jarilo-VI)",
"Echo_of_War_Divine_Seed": "Semilla divina (El Luofu de Xianzhou)",
"Echo_of_War_Borehole_Planet_Old_Crater": "Cráter del planeta devorado (Estación Espacial Herta)"
"Echo_of_War_Borehole_Planet_Old_Crater": "Cráter del planeta devorado (Estación Espacial Herta)",
"Echo_of_War_Salutations_of_Ashen_Dreams": "Ecos de la guerra: Tributo del sueño ceniciento (Colonipenal)"
},
"Team": {
"name": "Equipo de mazmorra",
@ -800,94 +809,94 @@
"Name_1": {
"name": "Preferencia de Encargo 1",
"help": "",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"The_Invisible_Hand": "Crédito (La mano invisible)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)",
"The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)"
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)"
},
"Name_2": {
"name": "Preferencia de Encargo 2",
"help": "",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"The_Invisible_Hand": "Crédito (La mano invisible)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)",
"The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)"
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)"
},
"Name_3": {
"name": "Preferencia de Encargo 3",
"help": "",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"The_Invisible_Hand": "Crédito (La mano invisible)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)",
"The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)"
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)"
},
"Name_4": {
"name": "Preferencia de Encargo 4",
"help": "",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Tranquility_of_Vimala_bhumi": "Jirones de pensamientos (Limpieza y purificación)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"A_Startling_Night_Terror": "Componente del acumulador de sueños (Pesadilla aterradora)",
"Fire_Lord_Inflames_Blades_of_War": "Componente artificial mecánico (Prendan los fuelles, fundan las armas)",
"Root_Out_the_Turpitude": "Brote verde inmortal (La raíz del mal)",
"Born_to_Obey": "Componente antiguo (Creados para obedecer)",
"Winter_Soldiers": "Pin del guardia (Los guerreros del invierno)",
"Destruction_of_the_Destroyer": "Instinto del ladrón (La destrucción del destructor)",
"Nine_Billion_Names": "Núcleo apagado (Nueve mil millones de nombres)",
"Akashic_Records": "Material de EXP de conos de luz (Los Registros de Akasha)",
"Nameless_Land_Nameless_People": "Material de EXP de personaje (Lugar anónimo, personas anónimas)",
"The_Invisible_Hand": "Crédito (La mano invisible)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)",
"The_Wages_of_Humanity": "Cosecha tan alta como una persona & Extracto de hierbas medicinales (La paga de la humanidad)",
"Legend_of_the_Puppet_Master": "Componentes mecánicos abandonados & Unidad de ábaco de jade (La leyenda del titiritero)",
"The_Land_of_Gold": "Ingredientes básicos & Arroz proteico (Tierra de oportunidades)",
"Spring_of_Life": "Agua sólida & Partícula virtual (La fuente de la vida)",
"Fragments_of_Illusory_Dreams": "Tranquilidad & Sueños rotos (Fragmentos de sueños ilusorios)",
"Scalpel_and_Screwdriver": "Engranaje oxidado & Muela vieja (Bisturí y destornillador)"
"The_Blossom_in_the_Storm": "Líquido gaseoso & Semilla (Flores en la tormenta)",
"Abandoned_and_Insulted": "Flogisto & Metal (Abandonado e insultado)"
},
"Duration": {
"name": "Duración del encargo",
@ -969,9 +978,17 @@
"name": "Participa en doble planer evento",
"help": ""
},
"WeeklyFarming": {
"name": "Granja 100 élites semanalmente",
"help": ""
},
"UseStamina": {
"name": "Reclamar de planers mediante poder trazacaminos",
"help": "La tarea de mazmorra ya no se ejecutará y todo el poder trazacaminos se usará primero para reclamar recompensas de inmersión, excepto para eventos dobles"
},
"SimulatedUniverseFarm": {
"name": "Progreso de élites derrotadas",
"help": ""
}
},
"RogueBlessing": {

View File

@ -131,7 +131,8 @@
"aScreenCap_nc": "aScreenCap_nc",
"DroidCast": "DroidCast",
"DroidCast_raw": "DroidCast_raw",
"scrcpy": "scrcpy"
"scrcpy": "scrcpy",
"nemu_ipc": "nemu_ipc"
},
"ControlMethod": {
"name": "Emulator.ControlMethod.name",
@ -261,6 +262,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "軌跡素材:存護(サポート部分)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "軌跡素材:存護(クラークフィルムランド)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "軌跡素材:巡狩(郊外雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "軌跡素材:巡狩(スラーダ熱砂オーディション会場)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "軌跡素材:豊穣(外縁通路)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "軌跡素材:豊穣(綏園)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "軌跡素材:知恵(リベットタウン)",
@ -270,6 +272,7 @@
"Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "軌跡素材:虚無(丹鼎司)",
"Stagnant_Shadow_Spike": "キャラクター昇格素材:物理(ナターシャ / クラーラ / ルカ / 素裳)",
"Stagnant_Shadow_Perdition": "キャラクター昇格素材:物理(寒鴉 / アルジェンティ)",
"Stagnant_Shadow_Duty": "キャラクター昇格素材:物理(ブートヒル / ロビン)",
"Stagnant_Shadow_Blaze": "キャラクター昇格素材:炎(姫子 / アスター / フック)",
"Stagnant_Shadow_Scorch": "キャラクター昇格素材:炎(桂乃芬 / トパーズ&カブ)",
"Stagnant_Shadow_Ire": "キャラクター昇格素材:炎(ギャラガー)",
@ -312,6 +315,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "軌跡素材:存護(サポート部分)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "軌跡素材:存護(クラークフィルムランド)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "軌跡素材:巡狩(郊外雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "軌跡素材:巡狩(スラーダ熱砂オーディション会場)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "軌跡素材:豊穣(外縁通路)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "軌跡素材:豊穣(綏園)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "軌跡素材:知恵(リベットタウン)",
@ -375,6 +379,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "軌跡素材:存護(サポート部分)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "軌跡素材:存護(クラークフィルムランド)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "軌跡素材:巡狩(郊外雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "軌跡素材:巡狩(スラーダ熱砂オーディション会場)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "軌跡素材:豊穣(外縁通路)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "軌跡素材:豊穣(綏園)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "軌跡素材:知恵(リベットタウン)",
@ -389,6 +394,7 @@
"do_not_achieve": "do_not_achieve",
"Stagnant_Shadow_Spike": "キャラクター昇格素材:物理(ナターシャ / クラーラ / ルカ / 素裳)",
"Stagnant_Shadow_Perdition": "キャラクター昇格素材:物理(寒鴉 / アルジェンティ)",
"Stagnant_Shadow_Duty": "キャラクター昇格素材:物理(ブートヒル / ロビン)",
"Stagnant_Shadow_Blaze": "キャラクター昇格素材:炎(姫子 / アスター / フック)",
"Stagnant_Shadow_Scorch": "キャラクター昇格素材:炎(桂乃芬 / トパーズ&カブ)",
"Stagnant_Shadow_Ire": "キャラクター昇格素材:炎(ギャラガー)",
@ -440,6 +446,7 @@
"Argenti": "アルジェンティ",
"Arlan": "アーラン",
"Asta": "アスター",
"Aventurine": "アベンチュリン",
"Bailu": "白露",
"BlackSwan": "ブラックスワン",
"Blade": "刃",
@ -468,6 +475,7 @@
"Natasha": "ナターシャ",
"Pela": "ペラ",
"Qingque": "青雀",
"Robin": "ロビン",
"RuanMei": "ルアン・メェイ",
"Sampo": "サンポ",
"Seele": "ゼーレ",
@ -532,7 +540,8 @@
"Echo_of_War_Destruction_Beginning": "歴戦余韻・壊滅の始まり (宇宙ステーション「ヘルタ」)",
"Echo_of_War_End_of_the_Eternal_Freeze": "歴戦余韻・寒波の幕切れ (ヤリーロ-VI)",
"Echo_of_War_Divine_Seed": "歴戦余韻・不死の神実 (仙舟「羅浮」)",
"Echo_of_War_Borehole_Planet_Old_Crater": "歴戦余韻・星を蝕む往日の面影 (宇宙ステーション「ヘルタ」)"
"Echo_of_War_Borehole_Planet_Old_Crater": "歴戦余韻・星を蝕む往日の面影 (宇宙ステーション「ヘルタ」)",
"Echo_of_War_Salutations_of_Ashen_Dreams": "歴戦余韻・現世の夢の礼賛 (ピノコニー)"
},
"Team": {
"name": "Weekly.Team.name",
@ -800,94 +809,94 @@
"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": "工造機関(剣戟を焼却する火帝炉)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)",
"Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)",
"Born_to_Obey": "古代パーツ(生まれながらに服従する)",
"Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)",
"Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)",
"Nine_Billion_Names": "消滅した原核(九十億の御名)",
"Akashic_Records": "光円錐経験値素材(アーカーシャの記録)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"The_Invisible_Hand": "信用ポイント(見えざる手)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)",
"The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)"
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)"
},
"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": "工造機関(剣戟を焼却する火帝炉)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)",
"Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)",
"Born_to_Obey": "古代パーツ(生まれながらに服従する)",
"Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)",
"Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)",
"Nine_Billion_Names": "消滅した原核(九十億の御名)",
"Akashic_Records": "光円錐経験値素材(アーカーシャの記録)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"The_Invisible_Hand": "信用ポイント(見えざる手)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)",
"The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)"
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)"
},
"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": "工造機関(剣戟を焼却する火帝炉)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)",
"Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)",
"Born_to_Obey": "古代パーツ(生まれながらに服従する)",
"Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)",
"Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)",
"Nine_Billion_Names": "消滅した原核(九十億の御名)",
"Akashic_Records": "光円錐経験値素材(アーカーシャの記録)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"The_Invisible_Hand": "信用ポイント(見えざる手)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)",
"The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)"
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)"
},
"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": "工造機関(剣戟を焼却する火帝炉)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Tranquility_of_Vimala_bhumi": "思考の粉末(離垢清浄)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"A_Startling_Night_Terror": "ドリームコレクションパーツ(魂震える悪夢)",
"Fire_Lord_Inflames_Blades_of_War": "工造機関(剣戟を焼却する火帝炉)",
"Root_Out_the_Turpitude": "永寿の萌芽(悪孽を根絶やしに)",
"Born_to_Obey": "古代パーツ(生まれながらに服従する)",
"Winter_Soldiers": "シルバーメインの釦(寒冬の戦士たち)",
"Destruction_of_the_Destroyer": "略奪の本能(壊滅者の覆没)",
"Nine_Billion_Names": "消滅した原核(九十億の御名)",
"Akashic_Records": "光円錐経験値素材(アーカーシャの記録)",
"Nameless_Land_Nameless_People": "キャラクター経験値素材(無名の地、無名の人)",
"The_Invisible_Hand": "信用ポイント(見えざる手)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)",
"The_Wages_of_Humanity": "一人稲 & 薬草抽出物(人類扶養)",
"Legend_of_the_Puppet_Master": "廃棄された機巧部品 & 玉兆単元(傀儡師伝説)",
"The_Land_of_Gold": "基本食材 & タンパク米(黄金の大地)",
"Spring_of_Life": "固形純水 & 仮想粒子(生命の泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 砕けた夢(幻夢の残片)",
"Scalpel_and_Screwdriver": "錆びた歯車 & 古びた大臼歯(メスとスクリュードライバー)"
"The_Blossom_in_the_Storm": "気態流体 & 種子(嵐の中で咲き誇る花)",
"Abandoned_and_Insulted": "燃素 & 金属(捨てられしものと傷つけられしもの)"
},
"Duration": {
"name": "派遣時間",
@ -969,9 +978,17 @@
"name": "RogueWorld.DoubleEvent.name",
"help": "RogueWorld.DoubleEvent.help"
},
"WeeklyFarming": {
"name": "RogueWorld.WeeklyFarming.name",
"help": "RogueWorld.WeeklyFarming.help"
},
"UseStamina": {
"name": "RogueWorld.UseStamina.name",
"help": "RogueWorld.UseStamina.help"
},
"SimulatedUniverseFarm": {
"name": "RogueWorld.SimulatedUniverseFarm.name",
"help": "RogueWorld.SimulatedUniverseFarm.help"
}
},
"RogueBlessing": {

View File

@ -131,7 +131,8 @@
"aScreenCap_nc": "aScreenCap_nc",
"DroidCast": "DroidCast",
"DroidCast_raw": "DroidCast_raw",
"scrcpy": "scrcpy"
"scrcpy": "scrcpy",
"nemu_ipc": "nemu_ipc"
},
"ControlMethod": {
"name": "模拟器控制方案",
@ -261,6 +262,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "行迹材料:存护(支援舱段)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行迹材料:存护(克劳克影视乐园)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行迹材料:巡猎(城郊雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行迹材料:巡猎(苏乐达热砂海选会场)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行迹材料:丰饶(边缘通路)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行迹材料:丰饶(绥园)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "行迹材料:智识(铆钉镇)",
@ -270,6 +272,7 @@
"Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "行迹材料:虚无(丹鼎司)",
"Stagnant_Shadow_Spike": "角色晋阶材料:物理(娜塔莎 / 克拉拉 / 卢卡 / 素裳)",
"Stagnant_Shadow_Perdition": "角色晋阶材料:物理(寒鸦 / 银枝)",
"Stagnant_Shadow_Duty": "角色晋阶材料:物理(波提欧 / 知更鸟)",
"Stagnant_Shadow_Blaze": "角色晋阶材料:火(姬子 / 艾丝妲 / 虎克)",
"Stagnant_Shadow_Scorch": "角色晋阶材料:火(桂乃芬 / 托帕&账账)",
"Stagnant_Shadow_Ire": "角色晋阶材料:火(加拉赫)",
@ -312,6 +315,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "行迹材料:存护(支援舱段)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行迹材料:存护(克劳克影视乐园)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行迹材料:巡猎(城郊雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行迹材料:巡猎(苏乐达热砂海选会场)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行迹材料:丰饶(边缘通路)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行迹材料:丰饶(绥园)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "行迹材料:智识(铆钉镇)",
@ -375,6 +379,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "行迹材料:存护(支援舱段)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行迹材料:存护(克劳克影视乐园)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行迹材料:巡猎(城郊雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行迹材料:巡猎(苏乐达热砂海选会场)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行迹材料:丰饶(边缘通路)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行迹材料:丰饶(绥园)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "行迹材料:智识(铆钉镇)",
@ -389,6 +394,7 @@
"do_not_achieve": "不完成这个任务",
"Stagnant_Shadow_Spike": "角色晋阶材料:物理(娜塔莎 / 克拉拉 / 卢卡 / 素裳)",
"Stagnant_Shadow_Perdition": "角色晋阶材料:物理(寒鸦 / 银枝)",
"Stagnant_Shadow_Duty": "角色晋阶材料:物理(波提欧 / 知更鸟)",
"Stagnant_Shadow_Blaze": "角色晋阶材料:火(姬子 / 艾丝妲 / 虎克)",
"Stagnant_Shadow_Scorch": "角色晋阶材料:火(桂乃芬 / 托帕&账账)",
"Stagnant_Shadow_Ire": "角色晋阶材料:火(加拉赫)",
@ -440,6 +446,7 @@
"Argenti": "银枝",
"Arlan": "阿兰",
"Asta": "艾丝妲",
"Aventurine": "砂金",
"Bailu": "白露",
"BlackSwan": "黑天鹅",
"Blade": "刃",
@ -468,6 +475,7 @@
"Natasha": "娜塔莎",
"Pela": "佩拉",
"Qingque": "青雀",
"Robin": "知更鸟",
"RuanMei": "阮•梅",
"Sampo": "桑博",
"Seele": "希儿",
@ -532,7 +540,8 @@
"Echo_of_War_Destruction_Beginning": "毁灭的开端•历战余响 (空间站「黑塔」)",
"Echo_of_War_End_of_the_Eternal_Freeze": "寒潮的落幕•历战余响 (雅利洛-Ⅵ)",
"Echo_of_War_Divine_Seed": "不死的神实•历战余响 (仙舟「罗浮」)",
"Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的旧靥•历战余响 (空间站「黑塔」)"
"Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的旧靥•历战余响 (空间站「黑塔」)",
"Echo_of_War_Salutations_of_Ashen_Dreams": "尘梦的赞礼•历战余响 (匹诺康尼)"
},
"Team": {
"name": "打本队伍",
@ -800,94 +809,94 @@
"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": "工造机杼(火帝动炉销剑戟)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)",
"Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)",
"Born_to_Obey": "古代零件(生而服从)",
"Winter_Soldiers": "铁卫扣饰(寒冬的战士们)",
"Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)",
"Nine_Billion_Names": "熄灭原核(九十亿个名字)",
"Akashic_Records": "光锥经验材料(阿卡夏记录)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"The_Invisible_Hand": "信用点(看不见的手)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)",
"The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)"
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)"
},
"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": "工造机杼(火帝动炉销剑戟)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)",
"Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)",
"Born_to_Obey": "古代零件(生而服从)",
"Winter_Soldiers": "铁卫扣饰(寒冬的战士们)",
"Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)",
"Nine_Billion_Names": "熄灭原核(九十亿个名字)",
"Akashic_Records": "光锥经验材料(阿卡夏记录)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"The_Invisible_Hand": "信用点(看不见的手)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)",
"The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)"
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)"
},
"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": "工造机杼(火帝动炉销剑戟)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)",
"Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)",
"Born_to_Obey": "古代零件(生而服从)",
"Winter_Soldiers": "铁卫扣饰(寒冬的战士们)",
"Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)",
"Nine_Billion_Names": "熄灭原核(九十亿个名字)",
"Akashic_Records": "光锥经验材料(阿卡夏记录)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"The_Invisible_Hand": "信用点(看不见的手)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)",
"The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)"
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)"
},
"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": "工造机杼(火帝动炉销剑戟)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Tranquility_of_Vimala_bhumi": "思绪末屑(离垢清净)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"A_Startling_Night_Terror": "蓄梦元件(劫梦惊魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造机杼(火帝动炉销剑戟)",
"Root_Out_the_Turpitude": "永寿幼芽(根除恶孽)",
"Born_to_Obey": "古代零件(生而服从)",
"Winter_Soldiers": "铁卫扣饰(寒冬的战士们)",
"Destruction_of_the_Destroyer": "掠夺的本能(毁灭者的覆灭)",
"Nine_Billion_Names": "熄灭原核(九十亿个名字)",
"Akashic_Records": "光锥经验材料(阿卡夏记录)",
"Nameless_Land_Nameless_People": "角色经验材料(无名之地,无名之人)",
"The_Invisible_Hand": "信用点(看不见的手)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)",
"The_Wages_of_Humanity": "一人嘉禾 & 药草提取物(赡养人类)",
"Legend_of_the_Puppet_Master": "废弃机巧零件 & 玉兆单元(偃师传说)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黄金大地)",
"Spring_of_Life": "固态净水 & 虚粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎梦(幻梦的残片)",
"Scalpel_and_Screwdriver": "锈迹齿轮 & 老旧臼齿(手术刀与螺丝刀)"
"The_Blossom_in_the_Storm": "气态流体 & 种子(风暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金属(被废弃与损害的)"
},
"Duration": {
"name": "派遣时长",
@ -969,9 +978,17 @@
"name": "参与双倍内圈仪器活动",
"help": ""
},
"WeeklyFarming": {
"name": "每周刷100精英怪",
"help": ""
},
"UseStamina": {
"name": "使用开拓力刷内圈遗器",
"help": "每日副本任务将不再打本,所有开拓力将优先被用于领取浸器奖励,双倍活动时除外"
},
"SimulatedUniverseFarm": {
"name": "刷精英怪进度",
"help": ""
}
},
"RogueBlessing": {

View File

@ -131,7 +131,8 @@
"aScreenCap_nc": "aScreenCap_nc",
"DroidCast": "DroidCast",
"DroidCast_raw": "DroidCast_raw",
"scrcpy": "scrcpy"
"scrcpy": "scrcpy",
"nemu_ipc": "nemu_ipc"
},
"ControlMethod": {
"name": "模擬器控制方案",
@ -261,6 +262,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "行跡材料:存護(支援艙段)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行跡材料:存護(克勞克影視樂園)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行跡材料:巡獵(城郊雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行跡材料:巡獵(蘇樂達熱砂海選會場)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行跡材料:豐饒(邊緣通道)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行跡材料:豐饒(綏園)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "行跡材料:智識(鉚釘鎮)",
@ -270,6 +272,7 @@
"Calyx_Crimson_Nihility_Luofu_AlchemyCommission": "行跡材料:虛無(丹鼎司)",
"Stagnant_Shadow_Spike": "角色晉階材料:物理(娜塔莎 / 克拉拉 / 盧卡 / 素裳)",
"Stagnant_Shadow_Perdition": "角色晉階材料:物理(寒鴉 / 銀枝)",
"Stagnant_Shadow_Duty": "角色晉階材料:物理(波提歐 / 知更鳥)",
"Stagnant_Shadow_Blaze": "角色晉階材料:火(姬子 / 艾絲妲 / 虎克)",
"Stagnant_Shadow_Scorch": "角色晉階材料:火(桂乃芬 / 托帕&帳帳)",
"Stagnant_Shadow_Ire": "角色晉階材料:火(加拉赫)",
@ -312,6 +315,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "行跡材料:存護(支援艙段)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行跡材料:存護(克勞克影視樂園)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行跡材料:巡獵(城郊雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行跡材料:巡獵(蘇樂達熱砂海選會場)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行跡材料:豐饒(邊緣通道)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行跡材料:豐饒(綏園)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "行跡材料:智識(鉚釘鎮)",
@ -375,6 +379,7 @@
"Calyx_Crimson_Preservation_Herta_SupplyZone": "行跡材料:存護(支援艙段)",
"Calyx_Crimson_Preservation_Penacony_ClockStudiosThemePark": "行跡材料:存護(克勞克影視樂園)",
"Calyx_Crimson_The_Hunt_Jarilo_OutlyingSnowPlains": "行跡材料:巡獵(城郊雪原)",
"Calyx_Crimson_The_Hunt_Penacony_SoulGladScorchsandAuditionVenue": "行跡材料:巡獵(蘇樂達熱砂海選會場)",
"Calyx_Crimson_Abundance_Jarilo_BackwaterPass": "行跡材料:豐饒(邊緣通道)",
"Calyx_Crimson_Abundance_Luofu_FyxestrollGarden": "行跡材料:豐饒(綏園)",
"Calyx_Crimson_Erudition_Jarilo_RivetTown": "行跡材料:智識(鉚釘鎮)",
@ -389,6 +394,7 @@
"do_not_achieve": "不完成這個任務",
"Stagnant_Shadow_Spike": "角色晉階材料:物理(娜塔莎 / 克拉拉 / 盧卡 / 素裳)",
"Stagnant_Shadow_Perdition": "角色晉階材料:物理(寒鴉 / 銀枝)",
"Stagnant_Shadow_Duty": "角色晉階材料:物理(波提歐 / 知更鳥)",
"Stagnant_Shadow_Blaze": "角色晉階材料:火(姬子 / 艾絲妲 / 虎克)",
"Stagnant_Shadow_Scorch": "角色晉階材料:火(桂乃芬 / 托帕&帳帳)",
"Stagnant_Shadow_Ire": "角色晉階材料:火(加拉赫)",
@ -440,6 +446,7 @@
"Argenti": "銀枝",
"Arlan": "阿蘭",
"Asta": "艾絲妲",
"Aventurine": "砂金",
"Bailu": "白露",
"BlackSwan": "黑天鵝",
"Blade": "刃",
@ -468,6 +475,7 @@
"Natasha": "娜塔莎",
"Pela": "佩拉",
"Qingque": "青雀",
"Robin": "知更鳥",
"RuanMei": "阮•梅",
"Sampo": "桑博",
"Seele": "希兒",
@ -532,7 +540,8 @@
"Echo_of_War_Destruction_Beginning": "毀滅的開端•歷戰餘響 (太空站「黑塔」)",
"Echo_of_War_End_of_the_Eternal_Freeze": "寒潮的落幕•歷戰餘響 (雅利洛-Ⅵ)",
"Echo_of_War_Divine_Seed": "不死的神實•歷戰餘響 (仙舟「羅浮」)",
"Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的舊靨•歷戰餘響 (太空站「黑塔」)"
"Echo_of_War_Borehole_Planet_Old_Crater": "蛀星的舊靨•歷戰餘響 (太空站「黑塔」)",
"Echo_of_War_Salutations_of_Ashen_Dreams": "塵夢的讚禮•歷戰餘響 (匹諾康尼)"
},
"Team": {
"name": "打本隊伍",
@ -800,94 +809,94 @@
"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": "工造機杼(火帝動爐銷劍戟)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)",
"Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)",
"Born_to_Obey": "古代零件(生而服從)",
"Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)",
"Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)",
"Nine_Billion_Names": "熄滅原核(九十億個名字)",
"Akashic_Records": "光錐經驗素材(阿卡夏紀錄)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"The_Invisible_Hand": "信用點(看不見的手)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)",
"The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)"
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)"
},
"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": "工造機杼(火帝動爐銷劍戟)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)",
"Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)",
"Born_to_Obey": "古代零件(生而服從)",
"Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)",
"Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)",
"Nine_Billion_Names": "熄滅原核(九十億個名字)",
"Akashic_Records": "光錐經驗素材(阿卡夏紀錄)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"The_Invisible_Hand": "信用點(看不見的手)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)",
"The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)"
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)"
},
"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": "工造機杼(火帝動爐銷劍戟)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)",
"Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)",
"Born_to_Obey": "古代零件(生而服從)",
"Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)",
"Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)",
"Nine_Billion_Names": "熄滅原核(九十億個名字)",
"Akashic_Records": "光錐經驗素材(阿卡夏紀錄)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"The_Invisible_Hand": "信用點(看不見的手)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)",
"The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)"
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)"
},
"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": "工造機杼(火帝動爐銷劍戟)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Tranquility_of_Vimala_bhumi": "思緒末屑(離垢清淨)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"A_Startling_Night_Terror": "蓄夢元件(劫夢驚魂)",
"Fire_Lord_Inflames_Blades_of_War": "工造機杼(火帝動爐銷劍戟)",
"Root_Out_the_Turpitude": "永壽幼芽(根除惡孽)",
"Born_to_Obey": "古代零件(生而服從)",
"Winter_Soldiers": "鐵衛扣飾(寒冬的戰士們)",
"Destruction_of_the_Destroyer": "掠奪的本能(毀滅者的覆滅)",
"Nine_Billion_Names": "熄滅原核(九十億個名字)",
"Akashic_Records": "光錐經驗素材(阿卡夏紀錄)",
"Nameless_Land_Nameless_People": "角色經驗素材(無名之地,無名之人)",
"The_Invisible_Hand": "信用點(看不見的手)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)",
"The_Wages_of_Humanity": "一人嘉禾 & 藥草萃取物(贍養人類)",
"Legend_of_the_Puppet_Master": "廢棄機巧零件 & 玉兆單元(偃師傳說)",
"The_Land_of_Gold": "基本食材 & 蛋白米(黃金大地)",
"Spring_of_Life": "固態淨水 & 虛粒子(生命之泉)",
"Fragments_of_Illusory_Dreams": "安逸 & 碎夢(幻夢的殘片)",
"Scalpel_and_Screwdriver": "鏽跡齒輪 & 老舊臼齒(手術刀與螺絲起子)"
"The_Blossom_in_the_Storm": "氣態流體 & 種子(風暴中怒放的花)",
"Abandoned_and_Insulted": "燃素 & 金屬(被廢棄與損害的)"
},
"Duration": {
"name": "派遣時間",
@ -969,9 +978,17 @@
"name": "參與雙倍內圈儀器活動",
"help": ""
},
"WeeklyFarming": {
"name": "每週農100精英怪",
"help": ""
},
"UseStamina": {
"name": "用開拓力農遺器",
"help": "每日副本任務將不再打本,所有開拓力將優先被用於領取浸器獎勵,雙倍活動時除外"
},
"SimulatedUniverseFarm": {
"name": "農精英怪進度",
"help": ""
}
},
"RogueBlessing": {

View File

@ -18,7 +18,7 @@ VALID_PACKAGE = set(list(VALID_SERVER.values()))
VALID_CLOUD_SERVER = {
'CN-Official': 'com.miHoYo.cloudgames.hkrpg',
}
VALID_CLOUD_PACKAGE = set(list(VALID_SERVER.values()))
VALID_CLOUD_PACKAGE = set(list(VALID_CLOUD_SERVER.values()))
def set_lang(lang_: str):

View File

@ -208,6 +208,15 @@ class StoredSimulatedUniverse(StoredCounter, StoredExpiredAtMonday0400):
pass
class StoredSimulatedUniverseElite(StoredCounter, StoredExpiredAtMonday0400):
# These variables are used in Rogue Farming feature.
# FIXED_TOTAL --- Times of boss drop chance per week. In current version of StarRail, this value is 100.
FIXED_TOTAL = 100
# value --- Times left to farm. Resets to 100 every Monday 04:00, and decreases each time the elite boss is cleared.
class StoredAssignment(StoredCounter):
pass

View File

@ -20,6 +20,7 @@ from module.config.stored.classes import (
StoredImmersifier,
StoredInt,
StoredSimulatedUniverse,
StoredSimulatedUniverseElite,
StoredTrailblazePower,
)
@ -50,3 +51,4 @@ class StoredGenerated:
Assignment = StoredAssignment("Assignment.Assignment.Assignment")
Credit = StoredInt("DataUpdate.ItemStorage.Credit")
StallerJade = StoredInt("DataUpdate.ItemStorage.StallerJade")
SimulatedUniverseFarm = StoredSimulatedUniverseElite("Rogue.RogueWorld.SimulatedUniverseFarm")

View File

@ -1,9 +1,9 @@
import ipaddress
import logging
import platform
import re
import socket
import subprocess
import sys
import time
from functools import wraps
@ -12,7 +12,8 @@ from adbutils import AdbClient, AdbDevice, AdbTimeout, ForwardItem, ReverseItem
from adbutils.errors import AdbError
import module.config.server as server_
from module.base.decorator import Config, cached_property, del_cached_property
import platform
from module.base.decorator import Config, cached_property, del_cached_property, run_once
from module.base.utils import SelectedGrids, ensure_time
from module.device.connection_attr import ConnectionAttr
from module.device.method.utils import (
@ -84,10 +85,17 @@ class AdbDeviceWithStatus(AdbDevice):
def __bool__(self):
return True
@cached_property
def port(self) -> int:
try:
return int(self.serial.split(':')[1])
except (IndexError, ValueError):
return 0
@cached_property
def may_mumu12_family(self):
# 127.0.0.1:16XXX
return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16')
return 16384 <= self.port <= 17408
class Connection(ConnectionAttr):
@ -276,6 +284,7 @@ class Connection(ConnectionAttr):
@cached_property
def nemud_app_keep_alive(self) -> str:
res = self.adb_getprop('nemud.app_keep_alive')
logger.attr('nemud.app_keep_alive', res)
return res
@retry
@ -284,7 +293,6 @@ class Connection(ConnectionAttr):
return False
res = self.nemud_app_keep_alive
logger.attr('nemud.app_keep_alive', res)
if res == '':
# Empty property, probably MuMu6 or MuMu12 version < 3.5.6
return True
@ -299,6 +307,15 @@ class Connection(ConnectionAttr):
logger.warning(f'Invalid nemud.app_keep_alive value: {res}')
return False
@cached_property
def is_mumu_over_version_356(self) -> bool:
"""
Returns:
bool: If MuMu12 version >= 3.5.6,
which has nemud.app_keep_alive and always be a vertical device
"""
return self.nemud_app_keep_alive != ''
@cached_property
def _nc_server_host_port(self):
"""
@ -496,30 +513,51 @@ class Connection(ConnectionAttr):
def adb_forward_remove(self, local):
"""
Equivalent to `adb -s <serial> forward --remove <local>`
No error raised when removing a non-existent forward
More about the commands send to ADB server, see:
https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/SERVICES.TXT
Args:
local (str): Such as 'tcp:2437'
"""
with self.adb_client._connect() as c:
list_cmd = f"host-serial:{self.serial}:killforward:{local}"
c.send_command(list_cmd)
c.check_okay()
try:
with self.adb_client._connect() as c:
list_cmd = f"host-serial:{self.serial}:killforward:{local}"
c.send_command(list_cmd)
c.check_okay()
except AdbError as e:
# No error raised when removing a non-existed forward
# adbutils.errors.AdbError: listener 'tcp:8888' not found
msg = str(e)
if re.search(r'listener .*? not found', msg):
logger.warning(f'{type(e).__name__}: {msg}')
else:
raise
def adb_reverse_remove(self, local):
"""
Equivalent to `adb -s <serial> reverse --remove <local>`
No error raised when removing a non-existent reverse
Args:
local (str): Such as 'tcp:2437'
"""
with self.adb_client._connect() as c:
c.send_command(f"host:transport:{self.serial}")
c.check_okay()
list_cmd = f"reverse:killforward:{local}"
c.send_command(list_cmd)
c.check_okay()
try:
with self.adb_client._connect() as c:
c.send_command(f"host:transport:{self.serial}")
c.check_okay()
list_cmd = f"reverse:killforward:{local}"
c.send_command(list_cmd)
c.check_okay()
except AdbError as e:
# No error raised when removing a non-existed forward
# adbutils.errors.AdbError: listener 'tcp:8888' not found
msg = str(e)
if re.search(r'listener .*? not found', msg):
logger.warning(f'{type(e).__name__}: {msg}')
else:
raise
def adb_push(self, local, remote):
"""
@ -549,14 +587,14 @@ class Connection(ConnectionAttr):
# Disconnect offline device before connecting
for device in self.list_device():
if device.status == 'offline':
logger.warning(f'Device {serial} is offline, disconnect it before connecting')
self.adb_disconnect(serial)
logger.warning(f'Device {device.serial} is offline, disconnect it before connecting')
self.adb_disconnect(device.serial)
elif device.status == 'unauthorized':
logger.error(f'Device {serial} is unauthorized, please accept ADB debugging on your device')
logger.error(f'Device {device.serial} is unauthorized, please accept ADB debugging on your device')
elif device.status == 'device':
pass
else:
logger.warning(f'Device {serial} is is having a unknown status: {device.status}')
logger.warning(f'Device {device.serial} is is having a unknown status: {device.status}')
# Skip for emulator-5554
if 'emulator-' in serial:
@ -764,23 +802,45 @@ class Connection(ConnectionAttr):
If serial=='auto' and only 1 device detected, use it
"""
logger.hr('Detect device')
logger.info('Here are the available devices, '
'copy to Alas.Emulator.Serial to use it or set Alas.Emulator.Serial="auto"')
devices = self.list_device()
available = SelectedGrids([])
devices = SelectedGrids([])
# Show available devices
available = devices.select(status='device')
for device in available:
logger.info(device.serial)
if not len(available):
logger.info('No available devices')
@run_once
def brute_force_connect():
logger.info('Brute force connect')
from deploy.Windows.emulator import EmulatorManager
manager = EmulatorManager()
manager.brute_force_connect()
# Show unavailable devices if having any
unavailable = devices.delete(available)
if len(unavailable):
logger.info('Here are the devices detected but unavailable')
for device in unavailable:
logger.info(f'{device.serial} ({device.status})')
for _ in range(2):
logger.info('Here are the available devices, '
'copy to Alas.Emulator.Serial to use it or set Alas.Emulator.Serial="auto"')
devices = self.list_device()
# Show available devices
available = devices.select(status='device')
for device in available:
logger.info(device.serial)
if not len(available):
logger.info('No available devices')
# Show unavailable devices if having any
unavailable = devices.delete(available)
if len(unavailable):
logger.info('Here are the devices detected but unavailable')
for device in unavailable:
logger.info(f'{device.serial} ({device.status})')
# brute_force_connect
if self.config.Emulator_Serial == 'auto' and available.count == 0:
logger.warning(f'No available device found')
if sys.platform == 'win32':
brute_force_connect()
continue
else:
break
else:
break
# Auto device detection
if self.config.Emulator_Serial == 'auto':
@ -790,7 +850,7 @@ class Connection(ConnectionAttr):
raise RequestHumanTakeover
elif available.count == 1:
logger.info(f'Auto device detection found only one device, using it')
self.serial = available[0].serial
self.config.Emulator_Serial = self.serial = available[0].serial
del_cached_property(self, 'adb')
elif available.count == 2 \
and available.select(serial='127.0.0.1:7555') \
@ -799,7 +859,7 @@ class Connection(ConnectionAttr):
# For MuMu12 serials like 127.0.0.1:7555 and 127.0.0.1:16384
# ignore 7555 use 16384
remain = available.select(may_mumu12_family=True).first_or_none()
self.serial = remain.serial
self.config.Emulator_Serial = self.serial = remain.serial
del_cached_property(self, 'adb')
else:
logger.critical('Multiple devices found, auto device detection cannot decide which to choose, '
@ -808,6 +868,7 @@ class Connection(ConnectionAttr):
# Handle LDPlayer
# LDPlayer serial jumps between `127.0.0.1:5555+{X}` and `emulator-5554+{X}`
# No config write since it's dynamic
port_serial, emu_serial = get_serial_pair(self.serial)
if port_serial and emu_serial:
# Might be LDPlayer, check connected devices
@ -834,6 +895,57 @@ class Connection(ConnectionAttr):
f'Using serial: {emu_serial}')
self.serial = emu_serial
# Redirect MuMu12 from 127.0.0.1:7555 to 127.0.0.1:16xxx
if self.serial == '127.0.0.1:7555':
for _ in range(2):
mumu12 = available.select(may_mumu12_family=True)
if mumu12.count == 1:
emu_serial = mumu12.first_or_none().serial
logger.warning(f'Redirect MuMu12 {self.serial} to {emu_serial}')
self.config.Emulator_Serial = self.serial = emu_serial
break
elif mumu12.count >= 2:
logger.warning(f'Multiple MuMu12 serial found, cannot redirect')
break
else:
# Only 127.0.0.1:7555
if self.is_mumu_over_version_356:
# is_mumu_over_version_356 and nemud_app_keep_alive was cached
# Acceptable since it's the same device
logger.warning(f'Device {self.serial} is MuMu12 but corresponding port not found')
brute_force_connect()
devices = self.list_device()
# Show available devices
available = devices.select(status='device')
for device in available:
logger.info(device.serial)
if not len(available):
logger.info('No available devices')
continue
else:
# MuMu6
break
# MuMu12 uses 127.0.0.1:16385 if port 16384 is occupied, auto redirect
# No config write since it's dynamic
if self.is_mumu12_family:
matched = False
for device in available.select(may_mumu12_family=True):
if device.port == self.port:
# Exact match
matched = True
break
if not matched:
for device in available.select(may_mumu12_family=True):
if -2 <= device.port - self.port <= 2:
# Port switched
logger.info(f'MuMu12 port switches from {self.serial} to {device.serial}')
del_cached_property(self, 'port')
del_cached_property(self, 'is_mumu12_family')
del_cached_property(self, 'is_mumu_family')
self.serial = device.serial
break
@retry
def list_package(self, show_log=True):
"""
@ -864,7 +976,7 @@ class Connection(ConnectionAttr):
list[str]: List of package names
"""
packages = self.list_package(show_log=show_log)
packages = [p for p in packages if p in server_.VALID_PACKAGE]
packages = [p for p in packages if p in server_.VALID_PACKAGE or p in server_.VALID_CLOUD_PACKAGE]
return packages
def detect_package(self, set_config=True):

View File

@ -7,7 +7,6 @@ from adbutils import AdbClient, AdbDevice
from module.base.decorator import cached_property
from module.config.config import AzurLaneConfig
from module.config.utils import deep_iter
from module.exception import RequestHumanTakeover
from module.logger import logger
@ -49,7 +48,6 @@ class ConnectionAttr:
self.serial_check()
self.config.DEVICE_OVER_HTTP = self.is_over_http
@staticmethod
def revise_serial(serial):
serial = serial.replace(' ', '')
@ -123,6 +121,18 @@ class ConnectionAttr:
def is_wsa(self):
return bool(re.match(r'^wsa', self.serial))
@cached_property
def port(self) -> int:
try:
return int(self.serial.split(':')[1])
except (IndexError, ValueError):
return 0
@cached_property
def is_mumu12_family(self):
# 127.0.0.1:16XXX
return 16384 <= self.port <= 17408
@cached_property
def is_mumu_family(self):
# 127.0.0.1:7555
@ -130,9 +140,8 @@ class ConnectionAttr:
return self.serial == '127.0.0.1:7555' or self.is_mumu12_family
@cached_property
def is_mumu12_family(self):
# 127.0.0.1:16384 + 32*n
return len(self.serial) == 15 and self.serial.startswith('127.0.0.1:16')
def is_nox_family(self):
return 62001 <= self.port <= 63025
@cached_property
def is_emulator(self):
@ -178,7 +187,8 @@ class ConnectionAttr:
rf"SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config") as key:
port = QueryValueEx(key, "BstAdbPort")[0]
except FileNotFoundError:
logger.error(rf'Unable to find registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config')
logger.error(
rf'Unable to find registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config')
logger.error('Please confirm that your are using BlueStack 4 hyper-v and not regular BlueStacks 4')
logger.error(r'Please check if there is any other emulator instances under '
r'registry HKEY_LOCAL_MACHINE\SOFTWARE\BlueStacks_bgp64_hyperv\Guests')

View File

@ -5,11 +5,12 @@ from module.base.utils import *
from module.device.method.hermit import Hermit
from module.device.method.maatouch import MaaTouch
from module.device.method.minitouch import Minitouch
from module.device.method.nemu_ipc import NemuIpc
from module.device.method.scrcpy import Scrcpy
from module.logger import logger
class Control(Hermit, Minitouch, Scrcpy, MaaTouch):
class Control(Hermit, Minitouch, Scrcpy, MaaTouch, NemuIpc):
def handle_control_check(self, button):
# Will be overridden in Device
pass
@ -22,6 +23,7 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch):
'minitouch': self.click_minitouch,
'Hermit': self.click_hermit,
'MaaTouch': self.click_maatouch,
'nemu_ipc': self.click_nemu_ipc,
}
def click(self, button, control_check=True):
@ -78,6 +80,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch):
self.long_click_scrcpy(x, y, duration)
elif method == 'MaaTouch':
self.long_click_maatouch(x, y, duration)
elif method == 'nemu_ipc':
self.long_click_nemu_ipc(x, y, duration)
else:
self.swipe_adb((x, y), (x, y), duration)
@ -86,13 +90,9 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch):
p1, p2 = ensure_int(p1, p2)
duration = ensure_time(duration)
method = self.config.Emulator_ControlMethod
if method == 'minitouch':
logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2)))
elif method == 'uiautomator2':
if method == 'uiautomator2':
logger.info('Swipe %s -> %s, %s' % (point2str(*p1), point2str(*p2), duration))
elif method == 'scrcpy':
logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2)))
elif method == 'MaaTouch':
elif method in ['minitouch', 'MaaTouch', 'scrcpy', 'nemu_ipc']:
logger.info('Swipe %s -> %s' % (point2str(*p1), point2str(*p2)))
else:
# ADB needs to be slow, or swipe doesn't work
@ -114,6 +114,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch):
self.swipe_scrcpy(p1, p2)
elif method == 'MaaTouch':
self.swipe_maatouch(p1, p2)
elif method == 'nemu_ipc':
self.swipe_nemu_ipc(p1, p2)
else:
self.swipe_adb(p1, p2, duration=duration)
@ -163,6 +165,8 @@ class Control(Hermit, Minitouch, Scrcpy, MaaTouch):
self.drag_scrcpy(p1, p2, point_random=point_random)
elif method == 'MaaTouch':
self.drag_maatouch(p1, p2, point_random=point_random)
elif method == 'nemu_ipc':
self.drag_nemu_ipc(p1, p2, point_random=point_random)
else:
logger.warning(f'Control method {method} does not support drag well, '
f'falling back to ADB swipe may cause unexpected behaviour')

View File

@ -1,10 +1,15 @@
import collections
import itertools
# Patch pkg_resources before importing adbutils and uiautomator2
from module.device.pkg_resources import get_distribution
# Just avoid being removed by import optimization
_ = get_distribution
from module.base.timer import Timer
from module.device.app_control import AppControl
from module.device.control import Control
from module.device.platform import Platform
from module.device.screenshot import Screenshot
from module.exception import (
EmulatorNotRunningError,
@ -56,7 +61,7 @@ def show_function_call():
logger.info('Function calls:' + ''.join(func_list))
class Device(Screenshot, Control, AppControl, Platform):
class Device(Screenshot, Control, AppControl):
_screen_size_checked = False
detect_record = set()
click_record = collections.deque(maxlen=30)
@ -82,12 +87,26 @@ class Device(Screenshot, Control, AppControl, Platform):
if self.config.EmulatorInfo_Emulator == 'auto':
_ = self.emulator_instance
# SRC only, use nemu_ipc if available
available = self.nemu_ipc_available()
logger.attr('nemu_ipc_available', available)
if available:
self.config.override(Emulator_ScreenshotMethod='nemu_ipc')
self.screenshot_interval_set()
self.method_check()
# Auto-select the fastest screenshot method
if not self.config.is_template_config and self.config.Emulator_ScreenshotMethod == 'auto':
self.run_simple_screenshot_benchmark()
# Early init
if self.config.is_actual_task:
if self.config.Emulator_ControlMethod == 'MaaTouch':
self.early_maatouch_init()
if self.config.Emulator_ControlMethod == 'minitouch':
self.early_minitouch_init()
def run_simple_screenshot_benchmark(self):
"""
Perform a screenshot method benchmark, test 3 times on each method.
@ -101,7 +120,23 @@ class Device(Screenshot, Control, AppControl, Platform):
bench = Benchmark(config=self.config, device=self)
method = bench.run_simple_screenshot_benchmark()
# Set
self.config.Emulator_ScreenshotMethod = method
with self.config.multi_set():
self.config.Emulator_ScreenshotMethod = method
# if method == 'nemu_ipc':
# self.config.Emulator_ControlMethod = 'nemu_ipc'
def method_check(self):
"""
Check combinations of screenshot method and control methods
"""
# nemu_ipc should be together
# if self.config.Emulator_ScreenshotMethod == 'nemu_ipc' and self.config.Emulator_ControlMethod != 'nemu_ipc':
# logger.warning('When using nemu_ipc, both screenshot and control should use nemu_ipc')
# self.config.Emulator_ControlMethod = 'nemu_ipc'
# if self.config.Emulator_ScreenshotMethod != 'nemu_ipc' and self.config.Emulator_ControlMethod == 'nemu_ipc':
# logger.warning('When not using nemu_ipc, both screenshot and control should not use nemu_ipc')
# self.config.Emulator_ControlMethod = 'minitouch'
pass
def screenshot(self):
"""
@ -127,6 +162,18 @@ class Device(Screenshot, Control, AppControl, Platform):
# stop it during wait
if self.config.Emulator_ScreenshotMethod == 'scrcpy':
self._scrcpy_server_stop()
if self.config.Emulator_ScreenshotMethod == 'nemu_ipc':
self.nemu_ipc_release()
def get_orientation(self):
"""
Callbacks when orientation changed.
"""
o = super().get_orientation()
self.on_orientation_change_maatouch()
return o
def stuck_record_add(self, button):
self.detect_record.add(str(button))

View File

@ -128,7 +128,7 @@ class Adb(Connection):
if image is None:
raise ImageTruncated('Empty image after cv2.imdecode')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image)
cv2.cvtColor(image, cv2.COLOR_BGR2RGB, dst=image)
if image is None:
raise ImageTruncated('Empty image after cv2.cvtColor')

View File

@ -95,6 +95,8 @@ class DroidCast(Uiautomator2):
"""
_droidcast_port: int = 0
droidcast_width: int = 0
droidcast_height: int = 0
@cached_property
def droidcast_session(self):
@ -112,15 +114,37 @@ class DroidCast(Uiautomator2):
- /preview
To get PNG screenshots.
"""
def droidcast_url(self, url='/preview'):
if self.is_mumu_over_version_356:
w, h = self.droidcast_width, self.droidcast_height
if self.orientation == 0:
return f'http://127.0.0.1:{self._droidcast_port}{url}?width={w}&height={h}'
elif self.orientation == 1:
return f'http://127.0.0.1:{self._droidcast_port}{url}?width={h}&height={w}'
else:
# logger.warning('DroidCast receives invalid device orientation')
pass
return f'http://127.0.0.1:{self._droidcast_port}{url}'
def droidcast_raw_url(self, url='/screenshot'):
if self.is_mumu_over_version_356:
w, h = self.droidcast_width, self.droidcast_height
if self.orientation == 0:
return f'http://127.0.0.1:{self._droidcast_port}{url}?width={w}&height={h}'
elif self.orientation == 1:
return f'http://127.0.0.1:{self._droidcast_port}{url}?width={h}&height={w}'
else:
# logger.warning('DroidCast receives invalid device orientation')
pass
return f'http://127.0.0.1:{self._droidcast_port}{url}'
def droidcast_init(self):
logger.hr('DroidCast init')
self.droidcast_stop()
self._droidcast_update_resolution()
logger.info('Pushing DroidCast apk')
self.adb_push(self.config.DROIDCAST_FILEPATH_LOCAL, self.config.DROIDCAST_FILEPATH_REMOTE)
@ -150,10 +174,25 @@ class DroidCast(Uiautomator2):
else:
logger.error(f'Unknown DROIDCAST_VERSION: {self.config.DROIDCAST_VERSION}')
def _droidcast_update_resolution(self):
if self.is_mumu_over_version_356:
logger.info('Update droidcast resolution')
w, h = self.resolution_uiautomator2(cal_rotation=False)
self.get_orientation()
# 720, 1280
# mumu12 > 3.5.6 is always a vertical device
self.droidcast_width, self.droidcast_height = w, h
logger.info(f'Droicast resolution: {(w, h)}')
@retry
def screenshot_droidcast(self):
self.config.DROIDCAST_VERSION = 'DroidCast'
if self.is_mumu_over_version_356:
if not self.droidcast_width or not self.droidcast_height:
self._droidcast_update_resolution()
resp = self.droidcast_session.get(self.droidcast_url(), timeout=3)
if resp.status_code == 404:
raise DroidCastVersionIncompatible('DroidCast server does not have /preview')
image = resp.content
@ -173,16 +212,37 @@ class DroidCast(Uiautomator2):
if image is None:
raise ImageTruncated('Empty image after cv2.cvtColor')
if self.is_mumu_over_version_356:
if self.orientation == 1:
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
return image
@retry
def screenshot_droidcast_raw(self):
self.config.DROIDCAST_VERSION = 'DroidCast_raw'
shape = (720, 1280)
if self.is_mumu_over_version_356:
if not self.droidcast_width or not self.droidcast_height:
self._droidcast_update_resolution()
if self.droidcast_height and self.droidcast_width:
shape = (self.droidcast_height, self.droidcast_width)
rotate = self.is_mumu_over_version_356 and self.orientation == 1
image = self.droidcast_session.get(self.droidcast_raw_url(), timeout=3).content
# DroidCast_raw returns a RGB565 bitmap
try:
arr = np.frombuffer(image, dtype=np.uint16).reshape((720, 1280))
arr = np.frombuffer(image, dtype=np.uint16)
if rotate:
arr = arr.reshape(shape)
# arr = cv2.rotate(arr, cv2.ROTATE_90_CLOCKWISE)
# A little bit faster?
arr = cv2.transpose(arr)
cv2.flip(arr, 1, dst=arr)
else:
arr = arr.reshape(shape)
except ValueError as e:
if len(image) < 500:
logger.warning(f'Unexpected screenshot: {image}')
@ -210,26 +270,26 @@ class DroidCast(Uiautomator2):
# b = b.astype(np.uint8)
# image = cv2.merge([r, g, b])
# The same as the code above but costs about 5ms instead of 10ms.
# The same as the code above but costs about 3~4ms instead of 10ms.
# Note that cv2.convertScaleAbs is 5x fast as cv2.multiply, cv2.add is 8x fast as cv2.convertScaleAbs
# Note that cv2.convertScaleAbs includes rounding
r = cv2.bitwise_and(arr, 0b1111100000000000)
cv2.multiply(r, 0.00390625, dst=r)
r = np.uint8(r)
m = cv2.multiply(r, 0.03125)
r = cv2.convertScaleAbs(r, alpha=0.00390625)
m = cv2.convertScaleAbs(r, alpha=0.03125)
cv2.add(r, m, dst=r)
g = cv2.bitwise_and(arr, 0b0000011111100000)
cv2.multiply(g, 0.125, dst=g)
g = np.uint8(g)
m = cv2.multiply(g, 0.015625)
g = cv2.convertScaleAbs(g, alpha=0.125)
m = cv2.convertScaleAbs(g, alpha=0.015625, dst=m)
cv2.add(g, m, dst=g)
b = cv2.bitwise_and(arr, 0b0000000000011111)
cv2.multiply(b, 8, dst=b)
b = np.uint8(b)
m = cv2.multiply(b, 0.03125)
b = cv2.convertScaleAbs(b, alpha=8)
m = cv2.convertScaleAbs(b, alpha=0.03125, dst=m)
cv2.add(b, m, dst=b)
image = cv2.merge([r, g, b])
return image
def droidcast_wait_startup(self):

View File

@ -1,14 +1,15 @@
import socket
import threading
from functools import wraps
from adbutils.errors import AdbError
from module.base.decorator import cached_property, del_cached_property
from module.base.decorator import cached_property, del_cached_property, has_cached_property
from module.base.timer import Timer
from module.base.utils import *
from module.device.connection import Connection
from module.device.method.minitouch import CommandBuilder, insert_swipe
from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error
from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep
from module.exception import RequestHumanTakeover
from module.logger import logger
@ -36,20 +37,20 @@ def retry(func):
def init():
self.adb_reconnect()
del_cached_property(self, 'maatouch_builder')
del_cached_property(self, '_maatouch_builder')
# Emulator closed
except ConnectionAbortedError as e:
logger.error(e)
def init():
self.adb_reconnect()
del_cached_property(self, 'maatouch_builder')
del_cached_property(self, '_maatouch_builder')
# AdbError
except AdbError as e:
if handle_adb_error(e):
def init():
self.adb_reconnect()
del_cached_property(self, 'maatouch_builder')
del_cached_property(self, '_maatouch_builder')
else:
break
# MaaTouchNotInstalledError: Received "Aborted" from MaaTouch
@ -58,12 +59,12 @@ def retry(func):
def init():
self.maatouch_install()
del_cached_property(self, 'maatouch_builder')
del_cached_property(self, '_maatouch_builder')
except BrokenPipeError as e:
logger.error(e)
def init():
del_cached_property(self, 'maatouch_builder')
del_cached_property(self, '_maatouch_builder')
# Unknown, probably a trucked image
except Exception as e:
logger.exception(e)
@ -101,20 +102,76 @@ class MaaTouch(Connection):
"""
max_x: int
max_y: int
_maatouch_stream = socket.socket
_maatouch_stream: socket.socket = None
_maatouch_stream_storage = None
_maatouch_init_thread = None
_maatouch_orientation: int = None
@cached_property
def maatouch_builder(self):
@retry
def _maatouch_builder(self):
self.maatouch_init()
return MaatouchBuilder(self)
@property
def maatouch_builder(self):
# Wait init thread
if self._maatouch_init_thread is not None:
self._maatouch_init_thread.join()
del self._maatouch_init_thread
self._maatouch_init_thread = None
return self._maatouch_builder
def early_maatouch_init(self):
"""
Start a thread to init maatouch connection while the Alas instance just starting to take screenshots
This would speed up the first click 0.2 ~ 0.4s.
"""
if has_cached_property(self, '_maatouch_builder'):
return
def early_maatouch_init_func():
_ = self._maatouch_builder
thread = threading.Thread(target=early_maatouch_init_func, daemon=True)
self._maatouch_init_thread = thread
thread.start()
def on_orientation_change_maatouch(self):
"""
MaaTouch caches devices orientation at its startup
A restart is required when orientation changed
"""
if self._maatouch_orientation is None:
return
if self.orientation == self._maatouch_orientation:
return
logger.info(f'Orientation changed {self._maatouch_orientation} => {self.orientation}, re-init MaaTouch')
del_cached_property(self, '_maatouch_builder')
self.early_maatouch_init()
def maatouch_init(self):
logger.hr('MaaTouch init')
max_x, max_y = 1280, 720
max_contacts = 2
max_pressure = 50
# Try to close existing stream
if self._maatouch_stream is not None:
try:
self._maatouch_stream.close()
except Exception as e:
logger.error(e)
del self._maatouch_stream
if self._maatouch_stream_storage is not None:
del self._maatouch_stream_storage
# MaaTouch caches devices orientation at its startup
super(MaaTouch, self).get_orientation()
self._maatouch_orientation = self.orientation
# CLASSPATH=/data/local/tmp/maatouch app_process / com.shxyke.MaaTouch.App
stream = self.adb_shell(
['CLASSPATH=/data/local/tmp/maatouch', 'app_process', '/', 'com.shxyke.MaaTouch.App'],
@ -245,3 +302,8 @@ class MaaTouch(Connection):
builder.up().commit()
builder.send()
if __name__ == '__main__':
self = MaaTouch('src')
self.maatouch_uninstall()

View File

@ -1,7 +1,7 @@
import asyncio
import json
import re
import socket
import threading
import time
from functools import wraps
from typing import List
@ -10,11 +10,11 @@ import websockets
from adbutils.errors import AdbError
from uiautomator2 import _Service
from module.base.decorator import Config, cached_property, del_cached_property
from module.base.decorator import Config, cached_property, del_cached_property, has_cached_property
from module.base.timer import Timer
from module.base.utils import *
from module.device.connection import Connection
from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error
from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep
from module.exception import RequestHumanTakeover, ScriptError
from module.logger import logger
@ -86,6 +86,8 @@ def insert_swipe(p0, p3, speed=15, min_distance=10):
distance = np.linalg.norm(np.subtract(points[1:], points[0]), axis=1)
mask = np.append(True, distance > min_distance)
points = np.array(points)[mask].tolist()
if len(points) <= 1:
points = [p0, p3]
else:
points = [p0, p3]
@ -314,12 +316,18 @@ def retry(func):
def init():
self.adb_reconnect()
if self._minitouch_port:
self.adb_forward_remove(f'tcp:{self._minitouch_port}')
del_cached_property(self, '_minitouch_builder')
# Emulator closed
except ConnectionAbortedError as e:
logger.error(e)
def init():
self.adb_reconnect()
if self._minitouch_port:
self.adb_forward_remove(f'tcp:{self._minitouch_port}')
del_cached_property(self, '_minitouch_builder')
# MinitouchNotInstalledError: Received empty data from minitouch
except MinitouchNotInstalledError as e:
logger.error(e)
@ -328,7 +336,7 @@ def retry(func):
self.install_uiautomator2()
if self._minitouch_port:
self.adb_forward_remove(f'tcp:{self._minitouch_port}')
del_cached_property(self, 'minitouch_builder')
del_cached_property(self, '_minitouch_builder')
# MinitouchOccupiedError: Timeout when connecting to minitouch
except MinitouchOccupiedError as e:
logger.error(e)
@ -337,19 +345,22 @@ def retry(func):
self.restart_atx()
if self._minitouch_port:
self.adb_forward_remove(f'tcp:{self._minitouch_port}')
del_cached_property(self, 'minitouch_builder')
del_cached_property(self, '_minitouch_builder')
# AdbError
except AdbError as e:
if handle_adb_error(e):
def init():
self.adb_reconnect()
if self._minitouch_port:
self.adb_forward_remove(f'tcp:{self._minitouch_port}')
del_cached_property(self, '_minitouch_builder')
else:
break
except BrokenPipeError as e:
logger.error(e)
def init():
del_cached_property(self, 'minitouch_builder')
del_cached_property(self, '_minitouch_builder')
# Unknown, probably a trucked image
except Exception as e:
logger.exception(e)
@ -365,23 +376,59 @@ def retry(func):
class Minitouch(Connection):
_minitouch_port: int = 0
_minitouch_client: socket.socket
_minitouch_client: socket.socket = None
_minitouch_pid: int
_minitouch_ws: websockets.WebSocketClientProtocol
max_x: int
max_y: int
_minitouch_init_thread = None
@cached_property
def minitouch_builder(self):
@retry
def _minitouch_builder(self):
self.minitouch_init()
return CommandBuilder(self)
@property
def minitouch_builder(self):
# Wait init thread
if self._minitouch_init_thread is not None:
self._minitouch_init_thread.join()
del self._minitouch_init_thread
self._minitouch_init_thread = None
return self._minitouch_builder
def early_minitouch_init(self):
"""
Start a thread to init minitouch connection while the Alas instance just starting to take screenshots
This would speed up the first click 0.05s.
"""
if has_cached_property(self, '_minitouch_builder'):
return
def early_minitouch_init_func():
_ = self._minitouch_builder
thread = threading.Thread(target=early_minitouch_init_func, daemon=True)
self._minitouch_init_thread = thread
thread.start()
@Config.when(DEVICE_OVER_HTTP=False)
def minitouch_init(self):
logger.hr('MiniTouch init')
max_x, max_y = 1280, 720
max_contacts = 2
max_pressure = 50
# Try to close existing stream
if self._minitouch_client is not None:
try:
self._minitouch_client.close()
except Exception as e:
logger.error(e)
del self._minitouch_client
self.get_orientation()
self._minitouch_port = self.adb_forward("localabstract:minitouch")

View File

@ -0,0 +1,541 @@
import asyncio
import ctypes
import os
import sys
from functools import partial, wraps
import cv2
import numpy as np
from module.base.decorator import cached_property, del_cached_property, has_cached_property
from module.base.utils import ensure_time
from module.device.method.minitouch import insert_swipe, random_rectangle_point
from module.device.method.utils import RETRY_TRIES, retry_sleep
from module.device.platform import Platform
from module.exception import RequestHumanTakeover
from module.logger import logger
class NemuIpcIncompatible(Exception):
pass
class NemuIpcError(Exception):
pass
class CaptureStd:
"""
Capture stdout and stderr from both python and C library
https://stackoverflow.com/questions/5081657/how-do-i-prevent-a-c-shared-library-to-print-on-stdout-in-python/17954769
```
with CaptureStd() as capture:
# String wasn't printed
print('whatever')
# But captured in ``capture.stdout``
print(f'Got stdout: "{capture.stdout}"')
print(f'Got stderr: "{capture.stderr}"')
```
"""
def __init__(self):
self.stdout = b''
self.stderr = b''
def _redirect_stdout(self, to):
sys.stdout.close()
os.dup2(to, self.fdout)
sys.stdout = os.fdopen(self.fdout, 'w')
def _redirect_stderr(self, to):
sys.stderr.close()
os.dup2(to, self.fderr)
sys.stderr = os.fdopen(self.fderr, 'w')
def __enter__(self):
self.fdout = sys.stdout.fileno()
self.fderr = sys.stderr.fileno()
self.reader_out, self.writer_out = os.pipe()
self.reader_err, self.writer_err = os.pipe()
self.old_stdout = os.dup(self.fdout)
self.old_stderr = os.dup(self.fderr)
file_out = os.fdopen(self.writer_out, 'w')
file_err = os.fdopen(self.writer_err, 'w')
self._redirect_stdout(to=file_out.fileno())
self._redirect_stderr(to=file_err.fileno())
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._redirect_stdout(to=self.old_stdout)
self._redirect_stderr(to=self.old_stderr)
os.close(self.old_stdout)
os.close(self.old_stderr)
self.stdout = self.recvall(self.reader_out)
self.stderr = self.recvall(self.reader_err)
os.close(self.reader_out)
os.close(self.reader_err)
@staticmethod
def recvall(reader, length=1024) -> bytes:
fragments = []
while 1:
chunk = os.read(reader, length)
if chunk:
fragments.append(chunk)
else:
break
output = b''.join(fragments)
return output
class CaptureNemuIpc(CaptureStd):
instance = None
def is_capturing(self):
"""
Only capture at the topmost wrapper to avoid nested capturing
If a capture is ongoing, this instance does nothing
"""
cls = self.__class__
return isinstance(cls.instance, cls) and cls.instance != self
def __enter__(self):
if self.is_capturing():
return self
super().__enter__()
CaptureNemuIpc.instance = self
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.is_capturing():
return
CaptureNemuIpc.instance = None
super().__exit__(exc_type, exc_val, exc_tb)
self.check_stdout()
self.check_stderr()
def check_stdout(self):
if not self.stdout:
return
logger.info(f'NemuIpc stdout: {self.stdout}')
def check_stderr(self):
if not self.stderr:
return
logger.error(f'NemuIpc stderr: {self.stderr}')
# Calling an old MuMu12 player
# Tested on 3.4.0
# b'nemu_capture_display rpc error: 1783\r\n'
# Tested on 3.7.3
# b'nemu_capture_display rpc error: 1745\r\n'
if b'error: 1783' in self.stderr or b'error: 1745' in self.stderr:
raise NemuIpcIncompatible(
f'NemuIpc requires MuMu12 version >= 3.8.13, please check your version')
# contact_id incorrect
# b'nemu_capture_display cannot find rpc connection\r\n'
if b'cannot find rpc connection' in self.stderr:
raise NemuIpcError(self.stderr)
# Emulator died
# b'nemu_capture_display rpc error: 1722\r\n'
# MuMuVMMSVC.exe died
# b'nemu_capture_display rpc error: 1726\r\n'
# No idea how to handle yet
if b'error: 1722' in self.stderr or b'error: 1726' in self.stderr:
raise NemuIpcError('Emulator instance is probably dead')
def retry(func):
@wraps(func)
def retry_wrapper(self, *args, **kwargs):
"""
Args:
self (NemuIpcImpl):
"""
init = None
for _ in range(RETRY_TRIES):
try:
if callable(init):
retry_sleep(_)
init()
return func(self, *args, **kwargs)
# Can't handle
except RequestHumanTakeover:
break
# Can't handle
except NemuIpcIncompatible as e:
logger.error(e)
break
# Function call timeout
except asyncio.TimeoutError:
logger.warning(f'Func {func.__name__}() call timeout, retrying: {_}')
def init():
self.reconnect()
# NemuIpcError
except NemuIpcError as e:
logger.error(e)
def init():
self.reconnect()
# Unknown, probably a trucked image
except Exception as e:
logger.exception(e)
def init():
pass
logger.critical(f'Retry {func.__name__}() failed')
raise RequestHumanTakeover
return retry_wrapper
class NemuIpcImpl:
def __init__(self, nemu_folder: str, instance_id: int, display_id: int = 0):
"""
Args:
nemu_folder: Installation path of MuMu12, e.g. E:/ProgramFiles/MuMuPlayer-12.0
instance_id: Emulator instance ID, starting from 0
display_id: Always 0 if keep app alive was disabled
"""
self.nemu_folder: str = nemu_folder
self.instance_id: int = instance_id
self.display_id: int = display_id
ipc_dll = os.path.abspath(os.path.join(nemu_folder, './shell/sdk/external_renderer_ipc.dll'))
logger.info(
f'NemuIpcImpl init, '
f'nemu_folder={nemu_folder}, '
f'ipc_dll={ipc_dll}, '
f'instance_id={instance_id}, '
f'display_id={display_id}'
)
try:
self.lib = ctypes.CDLL(ipc_dll)
except OSError as e:
logger.error(e)
# OSError: [WinError 126] 找不到指定的模块。
if not os.path.exists(ipc_dll):
raise NemuIpcIncompatible(
f'ipc_dll={ipc_dll} does not exist, '
f'NemuIpc requires MuMu12 version >= 3.8.13, please check your version')
else:
raise NemuIpcIncompatible(
f'ipc_dll={ipc_dll} exists, but cannot be loaded')
self.connect_id: int = 0
self.width = 0
self.height = 0
def connect(self):
if self.connect_id > 0:
return
connect_id = self.ev_run_sync(
self.lib.nemu_connect,
self.nemu_folder, self.instance_id
)
if connect_id == 0:
raise NemuIpcError(
'Connection failed, please check if nemu_folder is correct and emulator is running'
)
self.connect_id = connect_id
# logger.info(f'NemuIpc connected: {self.connect_id}')
def disconnect(self):
if self.connect_id == 0:
return
self.ev_run_sync(
self.lib.nemu_disconnect,
self.connect_id
)
# logger.info(f'NemuIpc disconnected: {self.connect_id}')
self.connect_id = 0
def reconnect(self):
self.disconnect()
self.connect()
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
@cached_property
def _ev(self):
return asyncio.new_event_loop()
async def ev_run_async(self, func, *args, **kwargs):
"""
Args:
func: Sync function to call
*args:
**kwargs:
Raises:
asyncio.TimeoutError: If function call timeout
"""
func_wrapped = partial(func, *args, **kwargs)
# Increased timeout for slow PCs
# Default screenshot interval is 0.2s, so a 0.15s timeout would have a fast retry without extra time costs
result = await asyncio.wait_for(self._ev.run_in_executor(None, func_wrapped), timeout=0.15)
return result
def ev_run_sync(self, func, *args, **kwargs):
"""
Args:
func: Sync function to call
*args:
**kwargs:
Raises:
asyncio.TimeoutError: If function call timeout
NemuIpcIncompatible:
NemuIpcError
"""
result = self._ev.run_until_complete(self.ev_run_async(func, *args, **kwargs))
err = False
if func.__name__ == 'nemu_connect':
if result == 0:
err = True
else:
if result > 0:
err = True
# Get to actual error message printed in std
if err:
logger.warning(f'Failed to call {func.__name__}, result={result}')
with CaptureNemuIpc():
result = self._ev.run_until_complete(self.ev_run_async(func, *args, **kwargs))
return result
def get_resolution(self):
"""
Get emulator resolution, `self.width` and `self.height` will be set
"""
if self.connect_id == 0:
self.connect()
width_ptr = ctypes.pointer(ctypes.c_int(0))
height_ptr = ctypes.pointer(ctypes.c_int(0))
nullptr = ctypes.POINTER(ctypes.c_int)()
ret = self.ev_run_sync(
self.lib.nemu_capture_display,
self.connect_id, self.display_id, 0, width_ptr, height_ptr, nullptr
)
if ret > 0:
raise NemuIpcError('nemu_capture_display failed during get_resolution()')
self.width = width_ptr.contents.value
self.height = height_ptr.contents.value
@retry
def screenshot(self):
"""
Returns:
np.ndarray: Image array in RGBA color space
Note that image is upside down
"""
if self.connect_id == 0:
self.connect()
self.get_resolution()
width_ptr = ctypes.pointer(ctypes.c_int(self.width))
height_ptr = ctypes.pointer(ctypes.c_int(self.height))
length = self.width * self.height * 4
pixels_pointer = ctypes.pointer((ctypes.c_ubyte * length)())
ret = self.ev_run_sync(
self.lib.nemu_capture_display,
self.connect_id, self.display_id, length, width_ptr, height_ptr, pixels_pointer
)
if ret > 0:
raise NemuIpcError('nemu_capture_display failed during screenshot()')
# image = np.ctypeslib.as_array(pixels_pointer, shape=(self.height, self.width, 4))
image = np.ctypeslib.as_array(pixels_pointer.contents).reshape((self.height, self.width, 4))
return image
def convert_xy(self, x, y):
"""
Convert classic ADB coordinates to Nemu's
`self.height` must be updated before calling this method
Returns:
int, int
"""
x, y = int(x), int(y)
x, y = self.height - y, x
return x, y
@retry
def down(self, x, y):
"""
Contact down, continuous contact down will be considered as swipe
"""
if self.connect_id == 0:
self.connect()
if self.height == 0:
self.get_resolution()
x, y = self.convert_xy(x, y)
ret = self.ev_run_sync(
self.lib.nemu_input_event_touch_down,
self.connect_id, self.display_id, x, y
)
if ret > 0:
raise NemuIpcError('nemu_input_event_touch_down failed')
@retry
def up(self):
"""
Contact up
"""
if self.connect_id == 0:
self.connect()
ret = self.ev_run_sync(
self.lib.nemu_input_event_touch_up,
self.connect_id, self.display_id
)
if ret > 0:
raise NemuIpcError('nemu_input_event_touch_up failed')
def serial_to_id(serial: str):
"""
Predict instance ID from serial
E.g.
"127.0.0.1:16384" -> 0
"127.0.0.1:16416" -> 1
Returns:
int: instance_id, or None if failed to predict
"""
try:
port = int(serial.split(':')[1])
except (IndexError, ValueError):
return None
index, offset = divmod(port - 16384, 32)
if 0 <= index < 32 and offset in [0, 1, 2]:
return index
else:
return None
class NemuIpc(Platform):
@cached_property
def nemu_ipc(self) -> NemuIpcImpl:
"""
Initialize a nemu ipc implementation
"""
# Try existing settings first
if self.config.EmulatorInfo_path:
folder = os.path.abspath(os.path.join(self.config.EmulatorInfo_path, '../../'))
index = serial_to_id(self.serial)
if index is not None:
try:
return NemuIpcImpl(
nemu_folder=folder,
instance_id=index,
display_id=0
).__enter__()
except (NemuIpcIncompatible, NemuIpcError) as e:
logger.error(e)
logger.error('Emulator info incorrect')
# Search emulator instance
# with E:\ProgramFiles\MuMuPlayer-12.0\shell\MuMuPlayer.exe
# installation path is E:\ProgramFiles\MuMuPlayer-12.0
if self.emulator_instance is None:
logger.error('Unable to use NemuIpc because emulator instance not found')
raise RequestHumanTakeover
try:
return NemuIpcImpl(
nemu_folder=self.emulator_instance.emulator.abspath('../'),
instance_id=self.emulator_instance.MuMuPlayer12_id,
display_id=0
).__enter__()
except (NemuIpcIncompatible, NemuIpcError) as e:
logger.error(e)
logger.error('Unable to initialize NemuIpc')
raise RequestHumanTakeover
def nemu_ipc_available(self) -> bool:
if not self.is_mumu_family:
return False
if self.nemud_app_keep_alive == '':
return False
try:
_ = self.nemu_ipc
except RequestHumanTakeover:
return False
return True
def nemu_ipc_release(self):
if has_cached_property(self, 'nemu_ipc'):
self.nemu_ipc.disconnect()
del_cached_property(self, 'nemu_ipc')
logger.info('nemu_ipc released')
def screenshot_nemu_ipc(self):
image = self.nemu_ipc.screenshot()
image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)
cv2.flip(image, 0, dst=image)
return image
def click_nemu_ipc(self, x, y):
down = ensure_time((0.010, 0.020))
self.nemu_ipc.down(x, y)
self.sleep(down)
self.nemu_ipc.up()
self.sleep(0.050 - down)
def long_click_nemu_ipc(self, x, y, duration=1.0):
self.nemu_ipc.down(x, y)
self.sleep(duration)
self.nemu_ipc.up()
self.sleep(0.050)
def swipe_nemu_ipc(self, p1, p2):
points = insert_swipe(p0=p1, p3=p2)
for point in points:
self.nemu_ipc.down(*point)
self.sleep(0.010)
self.nemu_ipc.up()
self.sleep(0.050)
def drag_nemu_ipc(self, p1, p2, point_random=(-10, -10, 10, 10)):
p1 = np.array(p1) - random_rectangle_point(point_random)
p2 = np.array(p2) - random_rectangle_point(point_random)
points = insert_swipe(p0=p1, p3=p2, speed=20)
for point in points:
self.nemu_ipc.down(*point)
self.sleep(0.010)
self.nemu_ipc.down(*p2)
self.sleep(0.140)
self.nemu_ipc.down(*p2)
self.sleep(0.140)
self.nemu_ipc.up()
self.sleep(0.050)

View File

@ -65,6 +65,8 @@ class ScrcpyCore(Connection):
Raises:
ScrcpyError:
adbutils.AdbTimeout:
socket.timeout:
"""
logger.hr('Scrcpy server start')
commands = ScrcpyOptions.command_v120(jar_path=self.config.SCRCPY_FILEPATH_REMOTE)
@ -72,6 +74,7 @@ class ScrcpyCore(Connection):
commands,
stream=True,
)
self._scrcpy_server_stream.conn.settimeout(3)
logger.info('Create server stream')
ret = self._scrcpy_server_stream.read(10)
@ -104,6 +107,7 @@ class ScrcpyCore(Connection):
self._scrcpy_video_socket = self.adb.create_connection(
Network.LOCAL_ABSTRACT, "scrcpy"
)
self._scrcpy_video_socket.settimeout(3)
break
except AdbError:
sleep(0.1)
@ -115,6 +119,7 @@ class ScrcpyCore(Connection):
self._scrcpy_control_socket = self.adb.create_connection(
Network.LOCAL_ABSTRACT, "scrcpy"
)
self._scrcpy_control_socket.settimeout(3)
logger.info('Fetch device info')
device_name = self._scrcpy_video_socket.recv(64).decode("utf-8").rstrip("\x00")
@ -151,23 +156,35 @@ class ScrcpyCore(Connection):
# logger.error(err)
self._scrcpy_alive = False
if self._scrcpy_server_stream is not None:
try:
self._scrcpy_server_stream.close()
except Exception:
pass
if self._scrcpy_stream_loop_thread is not None:
self._scrcpy_stream_loop_thread.join(1)
del self._scrcpy_stream_loop_thread
self._scrcpy_stream_loop_thread = None
if self._scrcpy_control_socket is not None:
try:
self._scrcpy_control_socket.close()
except Exception:
pass
except Exception as e:
logger.error(e)
del self._scrcpy_control_socket
self._scrcpy_control_socket = None
if self._scrcpy_video_socket is not None:
try:
self._scrcpy_video_socket.close()
except Exception:
pass
except Exception as e:
logger.error(e)
del self._scrcpy_video_socket
self._scrcpy_video_socket = None
if self._scrcpy_server_stream is not None:
try:
self._scrcpy_server_stream.close()
except Exception as e:
logger.error(e)
del self._scrcpy_server_stream
self._scrcpy_server_stream = None
logger.info('Scrcpy server stopped')
@ -195,7 +212,8 @@ class ScrcpyCore(Connection):
try:
raw_h264 = self._scrcpy_video_socket.recv(0x10000)
if raw_h264 == b"":
raise ScrcpyError("Video stream is disconnected")
if self._scrcpy_alive:
raise ScrcpyError("_scrcpy_stream_loop_thread: Video stream disconnected")
packets = codec.parse(raw_h264)
for packet in packets:
frames = codec.decode(packet)
@ -212,5 +230,8 @@ class ScrcpyCore(Connection):
if self._scrcpy_alive:
logger.error(f'_scrcpy_stream_loop_thread: {repr(e)}')
raise
except Exception as e:
logger.error(f'_scrcpy_stream_loop_thread exception: {repr(e)}')
raise
raise ScrcpyError('_scrcpy_stream_loop stopped')

View File

@ -1,15 +1,16 @@
import socket
import time
from functools import wraps
import numpy as np
from adbutils.errors import AdbError
from adbutils.errors import AdbError, AdbTimeout
import module.device.method.scrcpy.const as const
from module.base.utils import random_rectangle_point
from module.device.method.minitouch import insert_swipe
from module.device.method.scrcpy.core import ScrcpyCore, ScrcpyError
from module.device.method.uiautomator_2 import Uiautomator2
from module.device.method.utils import RETRY_TRIES, retry_sleep, handle_adb_error
from module.device.method.utils import RETRY_TRIES, handle_adb_error, retry_sleep
from module.exception import RequestHumanTakeover
from module.logger import logger
@ -19,7 +20,7 @@ def retry(func):
def retry_wrapper(self, *args, **kwargs):
"""
Args:
self (Minitouch):
self (ScrcpyCore):
"""
init = None
for _ in range(RETRY_TRIES):
@ -47,6 +48,13 @@ def retry(func):
except ScrcpyError as e:
logger.error(e)
def init():
self.scrcpy_init()
# AdbTimeout
# socket.timeout
except (AdbTimeout, socket.timeout) as e:
logger.error(e)
def init():
self.scrcpy_init()
# AdbError
@ -85,7 +93,8 @@ class Scrcpy(ScrcpyCore, Uiautomator2):
now = time.time()
while 1:
time.sleep(0.001)
if self._scrcpy_stream_loop_thread is None or not self._scrcpy_stream_loop_thread.is_alive():
thread = self._scrcpy_stream_loop_thread
if thread is None or not thread.is_alive():
raise ScrcpyError('_scrcpy_stream_loop_thread died')
if self._scrcpy_last_frame_time > now:
screenshot = self._scrcpy_last_frame.copy()

View File

@ -242,8 +242,19 @@ class Uiautomator2(Connection):
hierarchy = etree.fromstring(content.encode('utf-8'))
return hierarchy
def uninstall_uiautomator2(self):
logger.info('Removing uiautomator2')
for file in [
'app-uiautomator.apk',
'app-uiautomator-test.apk',
'minitouch',
'minitouch.so',
'atx-agent',
]:
self.adb_shell(["rm", f"/data/local/tmp/{file}"])
@retry
def resolution_uiautomator2(self) -> t.Tuple[int, int]:
def resolution_uiautomator2(self, cal_rotation=True) -> t.Tuple[int, int]:
"""
Faster u2.window_size(), cause that calls `dumpsys display` twice.
@ -252,9 +263,10 @@ class Uiautomator2(Connection):
"""
info = self.u2.http.get('/info').json()
w, h = info['display']['width'], info['display']['height']
rotation = self.get_orientation()
if (w > h) != (rotation % 2 == 1):
w, h = h, w
if cal_rotation:
rotation = self.get_orientation()
if (w > h) != (rotation % 2 == 1):
w, h = h, w
return w, h
def resolution_check_uiautomator2(self):

View File

@ -1,3 +1,4 @@
import os
import random
import re
import socket
@ -5,6 +6,7 @@ import time
import typing as t
import uiautomator2 as u2
import uiautomator2cache
from adbutils import AdbTimeout
from lxml import etree
@ -51,6 +53,25 @@ from module.logger import logger
RETRY_TRIES = 5
RETRY_DELAY = 3
# Patch uiautomator2 appdir
u2.init.appdir = os.path.dirname(uiautomator2cache.__file__)
# Patch uiautomator2 logger
u2_logger = u2.logger
u2_logger.debug = logger.info
u2_logger.info = logger.info
u2_logger.warning = logger.warning
u2_logger.error = logger.error
u2_logger.critical = logger.critical
def setup_logger(*args, **kwargs):
return u2_logger
u2.setup_logger = setup_logger
u2.init.setup_logger = setup_logger
def is_port_using(port_num):
""" if port is using by others, return True. else return False """
@ -253,7 +274,7 @@ def remove_suffix(s, suffix):
Returns:
str, bytes:
"""
return s[:len(suffix)] if s.endswith(suffix) else s
return s[:-len(suffix)] if s.endswith(suffix) else s
def remove_shell_warning(s):

View File

@ -0,0 +1,109 @@
import os
import re
import sys
from module.base.decorator import cached_property
from module.logger import logger
"""
Importing pkg_resources is so slow, like 0.4 ~ 1.0s, just google it you will find it indeed really slow.
Since it was some kind of standard library there is no way to modify it or speed it up.
So here's a poor but fast implementation of pkg_resources returning the things in need.
To patch:
```
# Patch pkg_resources before importing adbutils and uiautomator2
from module.device.pkg_resources import get_distribution
# Just avoid being removed by import optimization
_ = get_distribution
```
"""
# Inject sys.modules, pretend we have pkg_resources imported
try:
sys.modules['pkg_resources'] = sys.modules['module.device.pkg_resources']
except KeyError:
logger.error('Patch pkg_resources failed, patch module does not exists')
def remove_suffix(s, suffix):
"""
Remove suffix of a string or bytes like `string.removesuffix(suffix)`, which is on Python3.9+
Args:
s (str, bytes):
suffix (str, bytes):
Returns:
str, bytes:
"""
return s[:-len(suffix)] if s.endswith(suffix) else s
class FakeDistributionObject:
def __init__(self, dist, version):
self.dist = dist
self.version = version
def __str__(self):
return f'{self.__class__.__name__}({self.dist}={self.version})'
__repr__ = __str__
class PackageCache:
@cached_property
def site_packages(self):
# Just whatever library to locate the `site-packages` directory
import requests
path = os.path.abspath(os.path.join(requests.__file__, '../../'))
return path
@cached_property
def dict_installed_packages(self):
"""
Returns:
dict: Key: str, package name
Value: FakeDistributionObject
"""
dic = {}
for file in os.listdir(self.site_packages):
# mxnet_cu101-1.6.0.dist-info
# adbutils-0.11.0-py3.7.egg-info
res = re.match(r'^([a-zA-Z0-9._]+)-([a-zA-Z0-9._]+)-', file)
if res:
version = remove_suffix(res.group(2), '.dist')
# version = res.group(2)
obj = FakeDistributionObject(
dist=res.group(1),
version=version,
)
dic[obj.dist] = obj
return dic
PACKAGE_CACHE = PackageCache()
def resource_filename(*args):
if args == ("adbutils", "binaries"):
path = os.path.abspath(os.path.join(PACKAGE_CACHE.site_packages, *args))
return path
def get_distribution(dist):
"""Return a current distribution object for a Requirement or string"""
if dist == 'adbutils':
return PACKAGE_CACHE.dict_installed_packages.get(
'adbutils',
FakeDistributionObject('adbutils', '0.11.0'),
)
if dist == 'uiautomator2':
return PACKAGE_CACHE.dict_installed_packages.get(
'uiautomator2',
FakeDistributionObject('uiautomator2', '2.16.17'),
)
class DistributionNotFound(Exception):
pass

View File

@ -36,6 +36,21 @@ def get_serial_pair(serial):
return None, None
def remove_duplicated_path(paths):
"""
Args:
paths (list[str]):
Returns:
list[str]:
"""
paths = sorted(set(paths))
dic = {}
for path in paths:
dic.setdefault(path.lower(), path)
return list(dic.values())
@dataclass
class EmulatorInstanceBase:
# Serial for adb connection
@ -90,7 +105,7 @@ class EmulatorInstanceBase:
Returns:
int: Instance ID, or None if this is not a MuMu 12 instance
"""
res = re.search(r'MuMuPlayer-12.0-(\d+)', self.name)
res = re.search(r'MuMuPlayer(?:Global)?-12.0-(\d+)', self.name)
if res:
return int(res.group(1))
res = re.search(r'YXArkNights-12.0-(\d+)', self.name)
@ -205,6 +220,14 @@ class EmulatorBase:
class EmulatorManagerBase:
@staticmethod
def iter_running_emulator():
"""
Yields:
str: Path to emulator executables, may contains duplicate values
"""
return
@cached_property
def all_emulators(self) -> t.List[EmulatorBase]:
"""

View File

@ -8,7 +8,8 @@ from dataclasses import dataclass
# module/device/platform/emulator_base.py
# module/device/platform/emulator_windows.py
# Will be used in Alas Easy Install, they shouldn't import any Alas modules.
from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase
from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase, \
remove_duplicated_path
from module.device.platform.utils import cached_property, iter_folder
@ -70,7 +71,7 @@ class Emulator(EmulatorBase):
def path_to_type(cls, path: str) -> str:
"""
Args:
path: Path to .exe file
path: Path to .exe file, case insensitive
Returns:
str: Emulator type, such as Emulator.NoxPlayer
@ -78,46 +79,49 @@ class Emulator(EmulatorBase):
folder, exe = os.path.split(path)
folder, dir1 = os.path.split(folder)
folder, dir2 = os.path.split(folder)
if exe == 'Nox.exe':
if dir2 == 'Nox':
exe = exe.lower()
dir1 = dir1.lower()
dir2 = dir2.lower()
if exe == 'nox.exe':
if dir2 == 'nox':
return cls.NoxPlayer
elif dir2 == 'Nox64':
elif dir2 == 'nox64':
return cls.NoxPlayer64
else:
return cls.NoxPlayer
if exe == 'Bluestacks.exe':
if dir1 in ['BlueStacks', 'BlueStacks_cn']:
if exe == 'bluestacks.exe':
if dir1 in ['bluestacks', 'bluestacks_cn']:
return cls.BlueStacks4
elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']:
elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']:
return cls.BlueStacks5
else:
return cls.BlueStacks4
if exe == 'HD-Player.exe':
if dir1 in ['BlueStacks', 'BlueStacks_cn']:
if exe == 'hd-player.exe':
if dir1 in ['bluestacks', 'bluestacks_cn']:
return cls.BlueStacks4
elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']:
elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']:
return cls.BlueStacks5
else:
return cls.BlueStacks5
if exe == 'dnplayer.exe':
if dir1 == 'LDPlayer':
if dir1 == 'ldplayer':
return cls.LDPlayer3
elif dir1 == 'LDPlayer4':
elif dir1 == 'ldplayer4':
return cls.LDPlayer4
elif dir1 == 'LDPlayer9':
elif dir1 == 'ldplayer9':
return cls.LDPlayer9
else:
return cls.LDPlayer3
if exe == 'NemuPlayer.exe':
if exe == 'nemuplayer.exe':
if dir2 == 'nemu':
return cls.MuMuPlayer
elif dir2 == 'nemu9':
return cls.MuMuPlayerX
else:
return cls.MuMuPlayer
if exe == 'MuMuPlayer.exe':
if exe == 'mumuplayer.exe':
return cls.MuMuPlayer12
if exe == 'MEmu.exe':
if exe == 'memu.exe':
return cls.MEmuPlayer
return ''
@ -143,7 +147,9 @@ class Emulator(EmulatorBase):
elif 'NemuMultiPlayer.exe' in exe:
yield exe.replace('NemuMultiPlayer.exe', 'NemuPlayer.exe')
elif 'MuMuMultiPlayer.exe' in exe:
yield exe.replace('MuMuMultiPlayer.exe', 'MuMuManager.exe')
yield exe.replace('MuMuMultiPlayer.exe', 'MuMuPlayer.exe')
elif 'MuMuManager.exe' in exe:
yield exe.replace('MuMuManager.exe', 'MuMuPlayer.exe')
elif 'MEmuConsole.exe' in exe:
yield exe.replace('MEmuConsole.exe', 'MEmu.exe')
else:
@ -316,7 +322,7 @@ class EmulatorManager(EmulatorManagerBase):
Get recently executed programs in UserAssist
https://github.com/forensicmatt/MonitorUserAssist
Returns:
Yields:
str: Path to emulator executables, may contains duplicate values
"""
path = r'Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist'
@ -447,6 +453,31 @@ class EmulatorManager(EmulatorManagerBase):
uninstall = res.group(1) if res else uninstall
yield uninstall
@staticmethod
def iter_running_emulator():
"""
Yields:
str: Path to emulator executables, may contains duplicate values
"""
try:
import psutil
except ModuleNotFoundError:
return
# Since this is a one-time-usage, we access psutil._psplatform.Process directly
# to bypass the call of psutil.Process.is_running().
# This only costs about 0.017s.
for pid in psutil.pids():
proc = psutil._psplatform.Process(pid)
try:
exe = proc.cmdline()
exe = exe[0].replace(r'\\', '/').replace('\\', '/')
except (psutil.AccessDenied, IndexError):
# psutil.AccessDenied
continue
if Emulator.is_emulator(exe):
yield exe
@cached_property
def all_emulators(self) -> t.List[Emulator]:
"""
@ -474,7 +505,7 @@ class EmulatorManager(EmulatorManagerBase):
exe.add(ld)
# Uninstall registry
for uninstall in self.iter_uninstall_registry():
for uninstall in EmulatorManager.iter_uninstall_registry():
# Find emulator executable from uninstaller
for file in iter_folder(abspath(os.path.dirname(uninstall)), ext='.exe'):
if Emulator.is_emulator(file) and os.path.exists(file):
@ -488,9 +519,14 @@ class EmulatorManager(EmulatorManagerBase):
if Emulator.is_emulator(file) and os.path.exists(file):
exe.add(file)
# Running
for file in EmulatorManager.iter_running_emulator():
if os.path.exists(file):
exe.add(file)
# De-redundancy
exe = [Emulator(path).path for path in exe if Emulator.is_emulator(path)]
exe = sorted(set(exe))
exe = [Emulator(path) for path in exe]
exe = [Emulator(path) for path in remove_duplicated_path(exe)]
return exe
@cached_property

View File

@ -6,7 +6,8 @@ from pydantic import BaseModel
from module.base.decorator import cached_property, del_cached_property
from module.base.utils import SelectedGrids
from module.device.connection import Connection
from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase
from module.device.method.utils import get_serial_pair
from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase, remove_duplicated_path
from module.logger import logger
@ -47,8 +48,20 @@ class PlatformBase(Connection, EmulatorManagerBase):
@cached_property
def emulator_info(self) -> EmulatorInfo:
emulator = self.config.EmulatorInfo_Emulator
name = str(self.config.EmulatorInfo_name).strip().replace('\n', '')
path = str(self.config.EmulatorInfo_path).strip().replace('\n', '')
if emulator == 'auto':
emulator = ''
def parse_info(value):
if isinstance(value, str):
value = value.strip().replace('\n', '')
if value in ['None', 'False', 'True']:
value = ''
return value
else:
return ''
name = parse_info(self.config.EmulatorInfo_name)
path = parse_info(self.config.EmulatorInfo_path)
return EmulatorInfo(
emulator=emulator,
@ -68,8 +81,14 @@ class PlatformBase(Connection, EmulatorManagerBase):
path=data.path,
name=data.name,
)
# Redirect emulator-5554 to 127.0.0.1:5555
serial = self.serial
port_serial, _ = get_serial_pair(self.serial)
if port_serial is not None:
serial = port_serial
instance = self.find_emulator_instance(
serial=str(self.config.Emulator_Serial).strip(),
serial=serial,
name=data.name,
path=data.path,
emulator=data.emulator,
@ -117,7 +136,7 @@ class PlatformBase(Connection, EmulatorManagerBase):
# Search by serial
select = instances.select(**search_args)
if select.count == 0:
logger.warning(f'No emulator instance with {search_args}')
logger.warning(f'No emulator instance with {search_args}, serial invalid')
return None
if select.count == 1:
instance = select[0]
@ -130,9 +149,9 @@ class PlatformBase(Connection, EmulatorManagerBase):
search_args['name'] = name
select = instances.select(**search_args)
if select.count == 0:
logger.warning(f'No emulator instances with {search_args}')
return None
if select.count == 1:
logger.warning(f'No emulator instances with {search_args}, name invalid')
search_args.pop('name')
elif select.count == 1:
instance = select[0]
logger.hr('Emulator instance', level=2)
logger.info(f'Found emulator instance: {instance}')
@ -143,9 +162,9 @@ class PlatformBase(Connection, EmulatorManagerBase):
search_args['path'] = path
select = instances.select(**search_args)
if select.count == 0:
logger.warning(f'No emulator instances with {search_args}')
return None
if select.count == 1:
logger.warning(f'No emulator instances with {search_args}, path invalid')
search_args.pop('path')
elif select.count == 1:
instance = select[0]
logger.hr('Emulator instance', level=2)
logger.info(f'Found emulator instance: {instance}')
@ -156,9 +175,28 @@ class PlatformBase(Connection, EmulatorManagerBase):
search_args['type'] = emulator
select = instances.select(**search_args)
if select.count == 0:
logger.warning(f'No emulator instances with {search_args}')
return None
if select.count == 1:
logger.warning(f'No emulator instances with {search_args}, type invalid')
search_args.pop('type')
elif select.count == 1:
instance = select[0]
logger.hr('Emulator instance', level=2)
logger.info(f'Found emulator instance: {instance}')
return instance
# Still too many instances, search from running emulators
running = remove_duplicated_path(list(self.iter_running_emulator()))
logger.info('Running emulators')
for exe in running:
logger.info(exe)
if len(running) == 1:
logger.info('Only one running emulator')
# Same as searching path
search_args['path'] = running[0]
select = instances.select(**search_args)
if select.count == 0:
logger.warning(f'No emulator instances with {search_args}, path invalid')
search_args.pop('path')
elif select.count == 1:
instance = select[0]
logger.hr('Emulator instance', level=2)
logger.info(f'Found emulator instance: {instance}')
@ -167,9 +205,3 @@ class PlatformBase(Connection, EmulatorManagerBase):
# Still too many instances
logger.warning(f'Found multiple emulator instances with {search_args}')
return None
if __name__ == '__main__':
self = PlatformBase('alas')
d = self.emulator_instance
print(d)

View File

@ -13,13 +13,14 @@ from module.base.utils import get_color, image_size, limit_in, save_image
from module.device.method.adb import Adb
from module.device.method.ascreencap import AScreenCap
from module.device.method.droidcast import DroidCast
from module.device.method.nemu_ipc import NemuIpc
from module.device.method.scrcpy import Scrcpy
from module.device.method.wsa import WSA
from module.exception import RequestHumanTakeover, ScriptError
from module.logger import logger
class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy):
class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy, NemuIpc):
_screen_size_checked = False
_screen_black_checked = False
_minicap_uninstalled = False
@ -38,6 +39,7 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy):
'DroidCast': self.screenshot_droidcast,
'DroidCast_raw': self.screenshot_droidcast_raw,
'scrcpy': self.screenshot_scrcpy,
'nemu_ipc': self.screenshot_nemu_ipc,
}
def screenshot(self):
@ -70,6 +72,10 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy):
return self.image
@property
def has_cached_image(self):
return hasattr(self, 'image') and self.image is not None
def _handle_orientated_image(self, image):
"""
Args:
@ -159,6 +165,9 @@ class Screenshot(Adb, WSA, DroidCast, AScreenCap, Scrcpy):
if interval != origin:
logger.warning(f'Optimization.ScreenshotInterval {origin} is revised to {interval}')
self.config.Optimization_ScreenshotInterval = interval
# Allow nemu_ipc to have a lower default
if self.config.Emulator_ScreenshotMethod == 'nemu_ipc':
interval = limit_in(origin, 0.1, 0.2)
elif interval == 'combat':
origin = self.config.Optimization_CombatScreenshotInterval
interval = limit_in(origin, 0.3, 1.0)

View File

@ -1,4 +0,0 @@
from module.logger import logger
def handle_notify(*args, **kwargs):
logger.error('Error notify is not supported yet')

View File

@ -0,0 +1,4 @@
def handle_notify(*args, **kwargs):
# Lazy import onepush
from module.notify.notify import handle_notify
return handle_notify(*args, **kwargs)

75
module/notify/notify.py Normal file
View File

@ -0,0 +1,75 @@
import onepush.core
import yaml
from onepush import get_notifier
from onepush.core import Provider
from onepush.exceptions import OnePushException
from onepush.providers.custom import Custom
from requests import Response
from module.logger import logger
onepush.core.log = logger
def handle_notify(_config: str, **kwargs) -> bool:
try:
config = {}
for item in yaml.safe_load_all(_config):
config.update(item)
except Exception:
logger.error("Fail to load onepush config, skip sending")
return False
try:
provider_name: str = config.pop("provider", None)
if provider_name is None:
logger.info("No provider specified, skip sending")
return False
notifier: Provider = get_notifier(provider_name)
required: list[str] = notifier.params["required"]
config.update(kwargs)
# pre check
for key in required:
if key not in config:
logger.warning(
f"Notifier {notifier.name} require param '{key}' but not provided"
)
if isinstance(notifier, Custom):
if "method" not in config or config["method"] == "post":
config["datatype"] = "json"
if not ("data" in config or isinstance(config["data"], dict)):
config["data"] = {}
if "title" in kwargs:
config["data"]["title"] = kwargs["title"]
if "content" in kwargs:
config["data"]["content"] = kwargs["content"]
if provider_name.lower() == "gocqhttp":
access_token = config.get("access_token")
if access_token:
config["token"] = access_token
resp = notifier.notify(**config)
if isinstance(resp, Response):
if resp.status_code != 200:
logger.warning("Push notify failed!")
logger.warning(f"HTTP Code:{resp.status_code}")
return False
else:
if provider_name.lower() == "gocqhttp":
return_data: dict = resp.json()
if return_data["status"] == "failed":
logger.warning("Push notify failed!")
logger.warning(
f"Return message:{return_data['wording']}")
return False
except OnePushException:
logger.exception("Push notify failed")
return False
except Exception as e:
logger.exception(e)
return False
logger.info("Push notify success")
return True

View File

@ -7,7 +7,7 @@ import module.config.server as server
from module.exception import ScriptError
# ord('') = 65294
REGEX_PUNCTUATION = re.compile(r'[ ,.\'"“”,。…:;!??·・•●〇°*※\-—/\\\n\t()\[\]()「」『』【】《》[]]')
REGEX_PUNCTUATION = re.compile(r'[ ,.\'"“”,。…:;!??·・•●〇°*※\-—/\\|丨\n\t()\[\]()「」『』【】《》[]]')
def parse_name(n):

View File

@ -422,9 +422,12 @@ class Duration(Ocr):
class OcrWhiteLetterOnComplexBackground(Ocr):
white_preprocess = True
def pre_process(self, image):
image = extract_white_letters(image, threshold=255)
image = cv2.merge([image, image, image])
if self.white_preprocess:
image = extract_white_letters(image, threshold=255)
image = cv2.merge([image, image, image])
return image
def detect_and_ocr(self, *args, **kwargs):

View File

@ -160,6 +160,7 @@ class DraggableList:
logger.info(f'Insight row: {row}, index={row_index}')
last_buttons: set[OcrResultButton] = None
bottom_check = Timer(3, count=5).start()
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
@ -183,8 +184,11 @@ class DraggableList:
0, count=0), timeout=Timer(1.5, count=5))
skip_first_screenshot = True
if self.cur_buttons and last_buttons == set(self.cur_buttons):
logger.warning(f'No more rows in {self}')
return False
if bottom_check.reached():
logger.warning(f'No more rows in {self}')
return False
else:
bottom_check.reset()
last_buttons = set(self.cur_buttons)
return True

View File

@ -37,6 +37,8 @@ class DeployConfig(_DeployConfig):
if hasattr(self, key):
super().__setattr__(key, value)
self.config_redirect()
def write(self):
"""
Write `self.config` into deploy config.

View File

@ -325,11 +325,17 @@ def put_arg_input(kwargs: T_Output_Kwargs) -> Output:
)
def product_stored_row(kwargs: T_Output_Kwargs, key, value):
kwargs = copy.copy(kwargs)
kwargs["name"] += f'_{key}'
kwargs["value"] = value
return put_input(**kwargs).style("--input--")
def product_stored_row(key, value):
if key[-1].isdigit():
# quest1, quest2, quest3
return [put_text(value).style("--dashboard-time--")]
else:
# calyx, relic
# 3 (relic)
return [
put_text(value).style("--dashboard-value--"),
put_text(f" ({key})").style("--dashboard-time--"),
]
def put_arg_stored(kwargs: T_Output_Kwargs) -> Output:
@ -337,11 +343,32 @@ def put_arg_stored(kwargs: T_Output_Kwargs) -> Output:
kwargs["disabled"] = True
values = kwargs.pop("value", {})
value = values.pop("value", "")
total = values.pop("total", "")
time_ = values.pop("time", "")
rows = [product_stored_row(kwargs, key, value) for key, value in values.items() if value]
if value != "" and total != "":
rows = [put_scope(f"dashboard-value-{name}", [
put_text(value).style("--dashboard-value--"),
put_text(f" / {total}").style("--dashboard-time--"),
])]
elif value != "":
rows = [put_scope(f"dashboard-value-{name}", [
put_text(value).style("--dashboard-value--")
])]
else:
rows = []
if values:
rows += [
put_scope(f"dashboard-value-{name}-{key}", product_stored_row(key, value))
for key, value in values.items() if value != ""
]
if time_:
rows += [product_stored_row(kwargs, "time", time_)]
rows.append(
put_text(time_).style("--dashboard-time--")
)
return put_scope(
f"arg_container-stored-{name}",
[

View File

@ -21,6 +21,7 @@ pyyaml
inflection
prettytable==2.2.1
pydantic>=2.4
onepush==1.3.0
# OCR
pponnxcr==2.0

View File

@ -37,6 +37,7 @@ markdown-it-py==2.2.0 # via rich
mdurl==0.1.2 # via markdown-it-py
mpmath==1.3.0 # via sympy
numpy==1.24.3 # via -r requirements-in.txt, onnxruntime, opencv-python, pponnxcr, scipy, shapely
onepush==1.3.0 # via -r requirements-in.txt
onnxruntime==1.14.1 # via pponnxcr
opencv-python==4.7.0.72 # via -r requirements-in.txt, pponnxcr
packaging==20.9 # via deprecation, onnxruntime, uiautomator2
@ -48,6 +49,7 @@ protobuf==4.23.0 # via onnxruntime
psutil==5.9.3 # via -r requirements-in.txt
py==1.11.0 # via retry
pyclipper==1.3.0.post4 # via pponnxcr
pycryptodome==3.20.0 # via onepush
pydantic==2.4.2 # via -r requirements-in.txt
pydantic-core==2.10.1 # via pydantic
pyelftools==0.29 # via apkutils2
@ -58,7 +60,7 @@ pyreadline3==3.4.1 # via humanfriendly
python-dotenv==1.0.0 # via uvicorn
pywebio==1.8.3 # via -r requirements-in.txt
pyyaml==6.0 # via -r requirements-in.txt, uvicorn
requests==2.30.0 # via adbutils, uiautomator2
requests==2.30.0 # via adbutils, onepush, uiautomator2
retry==0.9.2 # via adbutils, uiautomator2
rich==13.3.5 # via -r requirements-in.txt
scipy==1.10.1 # via -r requirements-in.txt

View File

@ -16,7 +16,13 @@ class Route(RouteBase, Combat, CharacterTrial):
def wait_next_skill(self, expected_end=None, skip_first_screenshot=True):
# Ended at START_TRIAL
def combat_end():
return self.match_template_color(START_TRIAL)
if self.match_template_color(START_TRIAL):
logger.info('Trial ended at START_TRIAL')
return True
if self.is_in_main():
logger.warning('Trial ended at is_in_main()')
return True
return False
return super().wait_next_skill(expected_end=combat_end, skip_first_screenshot=skip_first_screenshot)

View File

@ -15,6 +15,7 @@ class Route(RouteBase):
| enemy1 | Waypoint((46.2, 328.2)), | 12.6 | 8 |
| item2 | Waypoint((42.4, 299.0)), | 352.8 | 348 |
| door2 | Waypoint((46.4, 284.5)), | 4.2 | 1 |
| door2end | Waypoint((47.2, 274.8)), | 11.1 | 4 |
| enemy2left | Waypoint((31.2, 248.8)), | 183.8 | 84 |
| enemy2right | Waypoint((55.2, 247.2)), | 96.7 | 91 |
| item3 | Waypoint((68.5, 226.5)), | 30.2 | 29 |
@ -32,6 +33,7 @@ class Route(RouteBase):
enemy1 = Waypoint((46.2, 328.2))
item2 = Waypoint((42.4, 299.0))
door2 = Waypoint((46.4, 284.5))
door2end = Waypoint((47.2, 274.8))
enemy2left = Waypoint((31.2, 248.8))
enemy2right = Waypoint((55.2, 247.2))
item3 = Waypoint((68.5, 226.5))
@ -47,6 +49,7 @@ class Route(RouteBase):
# self.clear_item(item2)
self.clear_enemy(
door2.set_threshold(3),
door2end.set_threshold(3),
# Go through door
enemy2left,
enemy2right.straight_run(),
@ -58,6 +61,7 @@ class Route(RouteBase):
self.clear_enemy(
enemy3.straight_run(),
)
# ('Combat_Herta_SupplyZone_F2_X45Y369', 0.243, (57.2, 351.6))
def Herta_SupplyZone_F2_X397Y233(self):
"""

View File

@ -63,7 +63,7 @@ class Route(RouteBase):
)
# 2
self.clear_enemy(
node2,
node2.set_threshold(3),
enemy2.straight_run(),
)
# 3
@ -116,6 +116,43 @@ class Route(RouteBase):
self.clear_enemy(enemy2left.straight_run())
self.clear_enemy(enemy3.straight_run())
def Jarilo_BackwaterPass_F1_X503Y736(self):
"""
| Waypoint | Position | Direction | Rotation |
| ---------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((507.2, 733.7)), | 6.7 | 4 |
| enemy1 | Waypoint((507.0, 644.0)), | 12.6 | 6 |
| enemy2left | Waypoint((536.0, 630.5)), | 48.1 | 43 |
| enemy3 | Waypoint((557.0, 585.2)), | 114.1 | 6 |
| exit_ | Waypoint((557.0, 585.2)), | 114.1 | 6 |
| exit1 | Waypoint((549.5, 575.4)), | 356.2 | 354 |
| exit2 | Waypoint((565.4, 575.6)), | 4.1 | 359 |
"""
self.map_init(plane=Jarilo_BackwaterPass, floor="F1", position=(503.2, 736.9))
self.register_domain_exit(
Waypoint((557.0, 585.2)), end_rotation=6,
left_door=Waypoint((549.5, 575.4)), right_door=Waypoint((565.4, 575.6)))
enemy1 = Waypoint((507.0, 644.0))
enemy2left = Waypoint((536.0, 630.5))
enemy3 = Waypoint((557.0, 585.2))
# ===== End of generated waypoints =====
self.clear_enemy(enemy1)
self.clear_enemy(enemy2left.straight_run())
self.clear_enemy(enemy3.straight_run())
"""
Notes
Herta_SupplyZone_F2_X397Y239 is the same as Herta_SupplyZone_F2_X397Y233
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Combat_Jarilo_BackwaterPass_F1_X507Y733', 0.26, (503.2, 736.9)),
# ('Combat_Luofu_ArtisanshipCommission_F1_X41Y640', 0.18, (50.7, 644.4)),
# ('Combat_Luofu_DivinationCommission_F1_X737Y372', 0.174, (717.2, 355.4)),
# ('Combat_Herta_SupplyZone_F2_X45Y369', 0.168, (46.5, 370.0))
# ]
def Jarilo_BackwaterPass_F1_X555Y643(self):
"""
| Waypoint | Position | Direction | Rotation |

View File

@ -1,3 +1,4 @@
from module.logger import logger
from tasks.map.control.waypoint import Waypoint
from tasks.map.keywords.plane import Jarilo_CorridorofFadingEchoes
from tasks.rogue.route.base import RouteBase
@ -30,6 +31,7 @@ class Route(RouteBase):
| ----------- | -------------------------- | --------- | -------- |
| spawn | Waypoint((201.2, 1071.4)), | 6.7 | 4 |
| enemy1right | Waypoint((200.3, 1032.4)), | 342.0 | 343 |
| node1 | Waypoint((194.6, 1023.4)), | 109.3 | 294 |
| enemy1left | Waypoint((168.6, 1022.3)), | 279.8 | 89 |
| node2 | Waypoint((118.4, 1019.0)), | 282.9 | 285 |
| enemy2left | Waypoint((105.2, 1012.0)), | 317.9 | 315 |
@ -45,6 +47,7 @@ class Route(RouteBase):
Waypoint((103.4, 919.2)), end_rotation=4,
left_door=Waypoint((98.8, 908.9)), right_door=Waypoint((111.4, 909.8)))
enemy1right = Waypoint((200.3, 1032.4))
node1 = Waypoint((194.6, 1023.4))
enemy1left = Waypoint((168.6, 1022.3))
node2 = Waypoint((118.4, 1019.0))
enemy2left = Waypoint((105.2, 1012.0))
@ -56,20 +59,22 @@ class Route(RouteBase):
# 1
self.rotation_set(315)
self.clear_enemy(
enemy1right.set_threshold(5),
enemy1left.set_threshold(5),
enemy1right.set_threshold(3),
node1.set_threshold(3),
enemy1left.set_threshold(3),
)
# 2
self.clear_enemy(
enemy1left.set_threshold(5),
enemy1left.set_threshold(3),
node2.set_threshold(5),
enemy2left,
enemy2right,
)
# 3
self.rotation_set(0)
self.clear_enemy(
node3.set_threshold(5),
enemy3.straight_run(),
node3.set_threshold(3),
enemy3,
)
def Jarilo_CorridorofFadingEchoes_F1_X266Y457(self):
@ -167,12 +172,23 @@ class Route(RouteBase):
enemy2right.straight_run(),
enemy2left.straight_run().set_threshold(5),
)
self.clear_enemy(
enemy2left.set_threshold(5),
node3.straight_run(),
node4.set_threshold(3).straight_run(),
enemy4.straight_run(),
)
if self.minimap.is_position_near(enemy2left.position, threshold=30):
logger.info('Near enemy2right')
self.clear_enemy(
enemy2left.set_threshold(5),
node3.straight_run(),
node4.set_threshold(3).straight_run(),
enemy4.straight_run(),
)
else:
logger.info('Not near enemy2right')
self.clear_enemy(
enemy2right.set_threshold(5),
enemy2left.set_threshold(5),
node3.straight_run(),
node4.set_threshold(3).straight_run(),
enemy4.straight_run(),
)
def Jarilo_CorridorofFadingEchoes_F1_X437Y122(self):
"""

View File

@ -105,6 +105,51 @@ class Route(RouteBase):
if self.minimap.position_diff(enemy3.position) > 25:
self.clear_enemy(enemy3)
def Luofu_ArtisanshipCommission_F1_X481Y920(self):
"""
| Waypoint | Position | Direction | Rotation |
| ----------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((473.5, 920.9)), | 4.5 | 4 |
| enemy1left | Waypoint((475.0, 848.4)), | 4.4 | 4 |
| enemy2right | Waypoint((493.5, 807.4)), | 157.1 | 48 |
| enemy3 | Waypoint((528.9, 782.9)), | 198.5 | 91 |
| exit_ | Waypoint((528.9, 782.9)), | 198.5 | 91 |
| exit1 | Waypoint((537.0, 773.2)), | 99.0 | 89 |
| exit2 | Waypoint((537.5, 790.6)), | 101.1 | 91 |
"""
self.map_init(plane=Luofu_ArtisanshipCommission, floor="F1", position=(481.5, 920.9))
self.register_domain_exit(
Waypoint((528.9, 782.9)), end_rotation=91,
left_door=Waypoint((537.0, 773.2)), right_door=Waypoint((537.5, 790.6)))
enemy1left = Waypoint((475.0, 848.4))
enemy2right = Waypoint((493.5, 807.4))
enemy3 = Waypoint((528.9, 782.9))
# ===== End of generated waypoints =====
self.rotation_set(30)
self.clear_enemy(
enemy1left,
enemy2right,
)
self.clear_enemy(enemy3)
if self.minimap.position_diff(enemy3.position) > 25:
self.clear_enemy(enemy3)
"""
Notes
Luofu_ArtisanshipCommission_F1_X481Y920 is the same as Luofu_ArtisanshipCommission_F1_X473Y920
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Combat_Luofu_ArtisanshipCommission_F1_X473Y920', 0.168, (481.0, 920.9)),
# ('Combat_Luofu_ArtisanshipCommission_F1_X41Y640', 0.163, (40.8, 664.0)),
# ('Combat_Luofu_ArtisanshipCommission_F1_X667Y189', 0.14, (705.0, 193.3))
# ]
# Best 3 nearby predictions: [
# ('Combat_Herta_SupplyZone_F2_X45Y369', 0.128, (47.6, 369.4)),
# ('Combat_Herta_SupplyZone_F2Rogue_X219Y112', 0.093, (219.6, 108.7))
# ]
def Luofu_ArtisanshipCommission_F1_X543Y269(self):
"""
| Waypoint | Position | Direction | Rotation |

View File

@ -256,3 +256,58 @@ class Route(RouteBase):
)
self.clear_item(item4)
self.clear_enemy(enemy4)
def Luofu_Cloudford_F1_X432Y685(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((435.4, 669.2)), | 6.7 | 4 |
| item1 | Waypoint((432.2, 628.3)), | 2.7 | 357 |
| enemy1 | Waypoint((428.6, 598.8)), | 8.0 | 177 |
| node2 | Waypoint((421.2, 590.8)), | 44.2 | 285 |
| node3 | Waypoint((366.6, 588.2)), | 274.2 | 274 |
| enemy3 | Waypoint((344.9, 590.4)), | 191.8 | 357 |
| item4 | Waypoint((309.6, 580.2)), | 290.1 | 281 |
| enemy4 | Waypoint((271.3, 585.5)), | 285.0 | 274 |
| exit_ | Waypoint((271.3, 585.5)), | 285.0 | 274 |
| exit1 | Waypoint((267.9, 592.3)), | 275.9 | 274 |
| exit2 | Waypoint((267.8, 580.0)), | 275.8 | 274 |
"""
self.map_init(plane=Luofu_Cloudford, floor="F1", position=(432.8, 685.1))
self.register_domain_exit(
Waypoint((271.3, 585.5)), end_rotation=274,
left_door=Waypoint((267.9, 592.3)), right_door=Waypoint((267.8, 580.0)))
item1 = Waypoint((432.2, 628.3))
enemy1 = Waypoint((428.6, 598.8))
node2 = Waypoint((421.2, 590.8))
node3 = Waypoint((366.6, 588.2))
enemy3 = Waypoint((344.9, 590.4))
item4 = Waypoint((309.6, 580.2))
enemy4 = Waypoint((271.3, 585.5))
# ===== End of generated waypoints =====
self.clear_item(item1)
self.clear_enemy(enemy1)
# Go through bridges
self.rotation_set(270)
self.minimap.lock_rotation(270)
self.clear_enemy(
node2.set_threshold(3),
node3.set_threshold(3),
enemy3,
)
self.clear_item(item4)
self.clear_enemy(enemy4)
"""
Notes
Luofu_Cloudford_F1_X435Y685 is the same as Luofu_Cloudford_F1_X435Y669
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Combat_Luofu_Cloudford_F1_X433Y617', 0.195, (432.8, 668.4)),
# ('Combat_Herta_SupplyZone_F2_X45Y369', 0.18, (24.2, 372.2)),
# ('Combat_Luofu_Cloudford_F1_X435Y669', 0.18, (432.8, 685.1))
# ]
# (432.9, 684.9)
# ('Combat_Luofu_Cloudford_F1_X435Y669', 0.172, (432.8, 685.0))

View File

@ -105,3 +105,62 @@ class Route(RouteBase):
Luofu_Cloudford_F1Rogue_X49Y405 is the same as Luofu_Cloudford_F1Rogue_X59Y405
but for wrong spawn point detected
"""
def Luofu_Cloudford_F1Rogue_X44Y405(self):
"""
| Waypoint | Position | Direction | Rotation |
| ------------ | ------------------------- | --------- | -------- |
| spawn | Waypoint((59.3, 405.6)), | 96.7 | 91 |
| item1 | Waypoint((96.9, 393.0)), | 87.7 | 84 |
| enemy1 | Waypoint((126.2, 402.5)), | 96.8 | 101 |
| node2 | Waypoint((142.9, 413.0)), | 96.8 | 101 |
| enemy2top | Waypoint((214.6, 432.8)), | 94.1 | 87 |
| enemy2bottom | Waypoint((211.4, 483.3)), | 191.8 | 174 |
| enemy3 | Waypoint((288.0, 452.2)), | 87.7 | 260 |
| exit_ | Waypoint((291.8, 454.4)), | 5.7 | 91 |
| exit1 | Waypoint((295.0, 451.4)), | 96.7 | 89 |
| exit2 | Waypoint((296.0, 460.2)), | 96.9 | 89 |
"""
self.map_init(plane=Luofu_Cloudford, floor="F1Rogue", position=(43.8, 405.0))
self.register_domain_exit(
Waypoint((291.8, 454.4)), end_rotation=91,
left_door=Waypoint((295.0, 451.4)), right_door=Waypoint((296.0, 460.2)))
item1 = Waypoint((96.9, 393.0))
enemy1 = Waypoint((126.2, 402.5))
node2 = Waypoint((142.9, 413.0))
enemy2top = Waypoint((214.6, 432.8))
enemy2bottom = Waypoint((211.4, 483.3))
enemy3 = Waypoint((288.0, 452.2))
# ===== End of generated waypoints =====
self.rotation_set(120)
self.minimap.lock_rotation(120)
# 1, ignore item1, which position may cause detection error
# self.clear_item(item1)
self.clear_enemy(enemy1)
# 2 moving enemy
# Ignore enemy2, it might be a pig, you can never catch it.
# self.clear_enemy(
# enemy2top,
# enemy2bottom.straight_run(),
# )
# 3
self.clear_enemy(
node2.set_threshold(3),
enemy3,
)
if self.minimap.position_diff(enemy3.position) > 60:
logger.info('Cleared an enemy but have not reached enemy3')
self.clear_enemy(enemy3)
"""
Notes
Luofu_Cloudford_F1Rogue_X44Y405 is the same as Luofu_Cloudford_F1Rogue_X59Y405
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Combat_Luofu_Cloudford_F1Rogue_X59Y405', 0.349, (43.8, 405.0)),
# ('Combat_Luofu_Cloudford_F1Rogue_X49Y405', 0.349, (43.8, 405.0)),
# ('Combat_Herta_SupplyZone_F2_X594Y247', 0.262, (641.3, 250.5))
# ]
# Best 3 nearby predictions: [('Combat_Jarilo_RivetTown_F1_X181Y439', 0.109, (178.5, 435.4))]

View File

@ -70,6 +70,7 @@ class Route(RouteBase):
# 1, enemy first
self.clear_enemy(enemy1)
self.clear_item(item1)
# item1 is cleared on the way to enemy2, or will get stuck at corner
# self.clear_item(item1)
# 2, ignore item2, bad way
self.clear_enemy(enemy2)

View File

@ -30,6 +30,36 @@ class Route(RouteBase):
but for wrong spawn point detected
"""
def Jarilo_CorridorofFadingEchoes_F1_X415Y953(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((415.5, 947.9)), | 96.7 | 91 |
| enemy | Waypoint((464.0, 953.0)), | 96.8 | 94 |
| reward | Waypoint((472.7, 958.5)), | 214.6 | 114 |
| exit_ | Waypoint((480.0, 944.0)), | 92.7 | 84 |
"""
self.map_init(plane=Jarilo_CorridorofFadingEchoes, floor="F1", position=(415.4, 953.3))
enemy = Waypoint((464.0, 953.0))
reward = Waypoint((472.7, 958.5))
exit_ = Waypoint((480.0, 944.0))
self.clear_elite(enemy)
self.domain_reward(reward)
self.domain_single_exit(exit_)
# ===== End of generated waypoints =====
"""
Notes
Jarilo_CorridorofFadingEchoes_F1_X415Y953 is the same as Jarilo_CorridorofFadingEchoes_F1_X415Y947
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y933', 0.169, (415.4, 953.3)),
# ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y947', 0.169, (415.4, 953.3)),
# ('Elite_Herta_SupplyZone_F2_X680Y247', 0.162, (738.4, 252.2))
# ]
def Jarilo_CorridorofFadingEchoes_F1_X415Y947(self):
"""
| Waypoint | Position | Direction | Rotation |

View File

@ -24,6 +24,36 @@ class Route(RouteBase):
self.domain_single_exit(exit_)
# ===== End of generated waypoints =====
def Luofu_ArtisanshipCommission_F1_X391Y493(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((385.2, 494.6)), | 94.2 | 91 |
| enemy | Waypoint((444.2, 490.5)), | 94.2 | 91 |
| reward | Waypoint((448.6, 497.2)), | 149.7 | 91 |
| exit_ | Waypoint((458.0, 483.7)), | 94.2 | 91 |
"""
self.map_init(plane=Luofu_ArtisanshipCommission, floor="F1", position=(391.1, 493.2))
enemy = Waypoint((444.2, 490.5))
reward = Waypoint((448.6, 497.2))
exit_ = Waypoint((458.0, 483.7))
self.clear_elite(enemy)
self.domain_reward(reward)
self.domain_single_exit(exit_)
# ===== End of generated waypoints =====
"""
Notes
Luofu_ArtisanshipCommission_F1_X391Y493 is the same as Luofu_ArtisanshipCommission_F1_X385Y494
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Elite_Luofu_ArtisanshipCommission_F1_X385Y494', 0.182, (391.1, 493.2)),
# ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y933', 0.157, (364.0, 951.0)),
# ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y947', 0.157, (364.0, 951.0))
# ]
def Luofu_ArtisanshipCommission_F1_X504Y493(self):
"""
| Waypoint | Position | Direction | Rotation |

View File

@ -25,3 +25,34 @@ class Route(RouteBase):
self.domain_reward(reward)
self.domain_single_exit(exit_)
# ===== End of generated waypoints =====
@locked_rotation(0)
def Luofu_Cloudford_F1_X342Y1003(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | -------------------------- | --------- | -------- |
| spawn | Waypoint((337.3, 1003.4)), | 6.7 | 4 |
| enemy | Waypoint((336.2, 962.2)), | 6.7 | 4 |
| reward | Waypoint((342.9, 950.8)), | 44.2 | 31 |
| exit_ | Waypoint((328.8, 942.8)), | 316.1 | 331 |
"""
self.map_init(plane=Luofu_Cloudford, floor="F1", position=(342.3, 1003.4))
enemy = Waypoint((336.2, 962.2))
reward = Waypoint((342.9, 950.8))
exit_ = Waypoint((328.8, 942.8))
self.clear_elite(enemy)
self.domain_reward(reward)
self.domain_single_exit(exit_)
# ===== End of generated waypoints =====
"""
Notes
Luofu_Cloudford_F1_X342Y1003 is the same as Luofu_Cloudford_F1_X337Y1003
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Elite_Luofu_Cloudford_F1_X337Y1003', 0.169, (342.3, 1002.7)),
# ('Elite_Luofu_ArtisanshipCommission_F1_X504Y493', 0.106, (519.3, 452.7)),
# ('Elite_Jarilo_CorridorofFadingEchoes_F1_X415Y933', 0.104, (433.8, 982.0))
# ]

View File

@ -25,3 +25,34 @@ class Route(RouteBase):
self.domain_reward(reward)
self.domain_single_exit(exit_)
# ===== End of generated waypoints =====
@locked_rotation(90)
def Luofu_StargazerNavalia_F1_X617Y511(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((617.5, 511.5)), | 96.7 | 91 |
| enemy | Waypoint((664.6, 512.6)), | 96.8 | 94 |
| reward | Waypoint((677.1, 521.2)), | 212.8 | 108 |
| exit_ | Waypoint((684.6, 505.0)), | 91.3 | 82 |
"""
self.map_init(plane=Luofu_StargazerNavalia, floor="F1", position=(617.5, 511.5))
enemy = Waypoint((664.6, 512.6))
reward = Waypoint((677.1, 521.2))
exit_ = Waypoint((684.6, 505.0))
self.clear_elite(enemy)
self.domain_reward(reward)
self.domain_single_exit(exit_)
# ===== End of generated waypoints =====
"""
Notes
Herta_SupplyZone_F2_X397Y239 is the same as Herta_SupplyZone_F2_X397Y233
but for wrong spawn point detected
"""
# Best 3 predictions: [
# ('Elite_Luofu_StargazerNavalia_F1_X617Y511', 0.338, (621.0, 507.0)),
# ('Elite_Luofu_ArtisanshipCommission_F1_X385Y494', 0.203, (329.2, 492.8)),
# ('Elite_Jarilo_SilvermaneGuardRestrictedZone_F1_X225Y425', 0.181, (224.8, 423.2))
# ]

View File

@ -67,3 +67,35 @@ class Route(RouteBase):
self.clear_item(item)
self.clear_event(event)
# ===== End of generated waypoints =====
def Jarilo_BackwaterPass_F1_X611Y761(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((613.3, 755.7)), | 319.8 | 318 |
| item | Waypoint((603.0, 734.6)), | 342.6 | 343 |
| event | Waypoint((586.8, 724.7)), | 318.0 | 315 |
| exit_ | Waypoint((576.9, 728.6)), | 126.2 | 304 |
| exit1 | Waypoint((567.0, 732.7)), | 311.8 | 306 |
| exit2 | Waypoint((576.2, 722.0)), | 308.1 | 306 |
"""
self.map_init(plane=Jarilo_BackwaterPass, floor="F1", position=(611.4, 761.2))
self.register_domain_exit(
Waypoint((576.9, 728.6)), end_rotation=304,
left_door=Waypoint((567.0, 732.7)), right_door=Waypoint((576.2, 722.0)))
item = Waypoint((603.0, 734.6))
event = Waypoint((586.8, 724.7))
self.clear_item(item)
self.clear_event(event)
# ===== End of generated waypoints =====
# Best 3 predictions: [
# ('Occurrence_Jarilo_BackwaterPass_F1_X613Y755', 0.203, (611.4, 761.2)),
# ('Occurrence_Herta_SupplyZone_F2Rogue_X397Y223', 0.148, (381.4, 207.5)),
# ('Occurrence_Herta_SupplyZone_F2Rogue_X397Y227', 0.148, (381.4, 207.5))
# ]
"""
Notes
Jarilo_BackwaterPass_F1_X611Y761 is the same as Jarilo_BackwaterPass_F1_X613Y755
but for wrong spawn point detected
"""

View File

@ -49,6 +49,38 @@ class Route(RouteBase):
self.clear_event(event)
# ===== End of generated waypoints =====
def Jarilo_SilvermaneGuardRestrictedZone_F1_X435Y233(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((439.3, 237.1)), | 354.1 | 348 |
| item | Waypoint((440.8, 215.2)), | 15.6 | 11 |
| event | Waypoint((434.8, 192.4)), | 355.9 | 359 |
| exit_ | Waypoint((428.6, 190.4)), | 76.4 | 338 |
| exit1 | Waypoint((416.8, 184.4)), | 337.5 | 334 |
| exit2 | Waypoint((428.8, 180.4)), | 339.1 | 336 |
"""
self.map_init(plane=Jarilo_SilvermaneGuardRestrictedZone, floor="F1", position=(435.5, 233.5))
self.register_domain_exit(
Waypoint((428.6, 190.4)), end_rotation=338,
left_door=Waypoint((416.8, 184.4)), right_door=Waypoint((428.8, 180.4)))
item = Waypoint((440.8, 215.2))
event = Waypoint((434.8, 192.4))
self.clear_item(item)
self.clear_event(event)
# ===== End of generated waypoints =====
# Best 3 predictions: [
# ('Occurrence_Jarilo_SilvermaneGuardRestrictedZone_F1_X439Y237', 0.194, (435.5, 233.5)),
# ('Occurrence_Luofu_ScalegorgeWaterscape_F1_X619Y387', 0.118, (593.5, 412.0)),
# ('Occurrence_Luofu_Cloudford_F1_X244Y951', 0.098, (193.8, 931.7))
# ]
"""
Notes
Jarilo_SilvermaneGuardRestrictedZone_F1_X435Y233 is the same as Jarilo_SilvermaneGuardRestrictedZone_F1_X439Y237
but for wrong spawn point detected
"""
def Jarilo_SilvermaneGuardRestrictedZone_F1_X509Y541(self):
"""
| Waypoint | Position | Direction | Rotation |

View File

@ -27,6 +27,32 @@ class Route(RouteBase):
self.clear_event(event)
# ===== End of generated waypoints =====
@locked_rotation(270)
def Luofu_Cloudford_F1_X244Y951(self):
"""
| Waypoint | Position | Direction | Rotation |
| -------- | ------------------------- | --------- | -------- |
| spawn | Waypoint((241.4, 947.5)), | 274.2 | 274 |
| event | Waypoint((199.0, 940.8)), | 300.1 | 294 |
| exit_ | Waypoint((193.1, 947.2)), | 12.8 | 274 |
| exit1 | Waypoint((179.0, 956.4)), | 279.8 | 278 |
| exit2 | Waypoint((184.1, 940.2)), | 282.9 | 278 |
"""
self.map_init(plane=Luofu_Cloudford, floor="F1", position=(244, 951))
self.register_domain_exit(
Waypoint((193.1, 947.2)), end_rotation=274,
left_door=Waypoint((179.0, 956.4)), right_door=Waypoint((184.1, 940.2)))
event = Waypoint((199.0, 940.8))
self.clear_event(event)
# ===== End of generated waypoints =====
"""
Notes
Luofu_Cloudford_F1_X244Y951 is the same as Luofu_Cloudford_F1_X241Y947
but for wrong spawn point detected
"""
@locked_position
@locked_rotation(0)
def Luofu_Cloudford_F1_X281Y873(self):

View File

@ -28,3 +28,8 @@ class Route(RouteBase):
self.clear_item(item_X504Y610)
self.clear_event(event_X510Y626)
# ===== End of generated waypoints =====
def clear_event(self, *waypoints):
# Too many clicks on A_BUTTON, so no items enroute in Luofu_StargazerNavalia_F1_X521Y595
self.enroute_add_item = False
return super().clear_event(*waypoints)

Some files were not shown because too many files have changed in this diff Show More