Merge branch 'LmeSzinc:master' into master
BIN
assets/character/Aventurine.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
assets/character/Robin.png
Normal file
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.1 KiB |
@ -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;
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 7.6 KiB |
BIN
assets/share/rogue/path/PATH_LOADED_CHECK.png
Normal file
After Width: | Height: | Size: 13 KiB |
@ -199,7 +199,9 @@
|
||||
"DomainStrategy": "combat",
|
||||
"UseImmersifier": true,
|
||||
"DoubleEvent": true,
|
||||
"UseStamina": false
|
||||
"WeeklyFarming": false,
|
||||
"UseStamina": false,
|
||||
"SimulatedUniverseFarm": {}
|
||||
},
|
||||
"RogueBlessing": {
|
||||
"PresetBlessingFilter": "preset",
|
||||
|
@ -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:
|
||||
|
@ -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()()
|
||||
|
@ -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)
|
||||
|
@ -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'])
|
||||
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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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]:
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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'
|
||||
"""
|
||||
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'
|
||||
"""
|
||||
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,6 +802,17 @@ class Connection(ConnectionAttr):
|
||||
If serial=='auto' and only 1 device detected, use it
|
||||
"""
|
||||
logger.hr('Detect device')
|
||||
available = SelectedGrids([])
|
||||
devices = SelectedGrids([])
|
||||
|
||||
@run_once
|
||||
def brute_force_connect():
|
||||
logger.info('Brute force connect')
|
||||
from deploy.Windows.emulator import EmulatorManager
|
||||
manager = EmulatorManager()
|
||||
manager.brute_force_connect()
|
||||
|
||||
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()
|
||||
@ -782,6 +831,17 @@ class Connection(ConnectionAttr):
|
||||
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':
|
||||
if available.count == 0:
|
||||
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
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))
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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()
|
@ -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")
|
||||
|
541
module/device/method/nemu_ipc.py
Normal 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)
|
@ -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')
|
||||
|
@ -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()
|
||||
|
@ -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,6 +263,7 @@ class Uiautomator2(Connection):
|
||||
"""
|
||||
info = self.u2.http.get('/info').json()
|
||||
w, h = info['display']['width'], info['display']['height']
|
||||
if cal_rotation:
|
||||
rotation = self.get_orientation()
|
||||
if (w > h) != (rotation % 2 == 1):
|
||||
w, h = h, w
|
||||
|
@ -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):
|
||||
|
109
module/device/pkg_resources/__init__.py
Normal 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
|
@ -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]:
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -1,4 +0,0 @@
|
||||
from module.logger import logger
|
||||
|
||||
def handle_notify(*args, **kwargs):
|
||||
logger.error('Error notify is not supported yet')
|
4
module/notify/__init__.py
Normal 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
@ -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
|
@ -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):
|
||||
|
@ -422,7 +422,10 @@ class Duration(Ocr):
|
||||
|
||||
|
||||
class OcrWhiteLetterOnComplexBackground(Ocr):
|
||||
white_preprocess = True
|
||||
|
||||
def pre_process(self, image):
|
||||
if self.white_preprocess:
|
||||
image = extract_white_letters(image, threshold=255)
|
||||
image = cv2.merge([image, image, image])
|
||||
return image
|
||||
|
@ -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):
|
||||
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
|
||||
|
@ -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.
|
||||
|
@ -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}",
|
||||
[
|
||||
|
@ -21,6 +21,7 @@ pyyaml
|
||||
inflection
|
||||
prettytable==2.2.1
|
||||
pydantic>=2.4
|
||||
onepush==1.3.0
|
||||
|
||||
# OCR
|
||||
pponnxcr==2.0
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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 |
|
||||
|
@ -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),
|
||||
)
|
||||
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):
|
||||
"""
|
||||
|
@ -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 |
|
||||
|
@ -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))
|
||||
|
@ -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))]
|
||||
|
@ -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)
|
||||
|
@ -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 |
|
||||
|
@ -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 |
|
||||
|
@ -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))
|
||||
# ]
|
||||
|
@ -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))
|
||||
# ]
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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 |
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|