mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-11-22 16:40:28 +00:00
356 lines
12 KiB
Python
356 lines
12 KiB
Python
import re
|
|
|
|
import cv2
|
|
import numpy as np
|
|
from scipy import signal
|
|
|
|
import module.config.server as server
|
|
from module.base.timer import Timer
|
|
from module.base.utils import area_center, crop, rgb2luma
|
|
from module.config.server import VALID_LANG
|
|
from module.exception import RequestHumanTakeover, ScriptError
|
|
from module.logger import logger
|
|
from module.ocr.ocr import OcrResultButton, OcrWhiteLetterOnComplexBackground
|
|
from tasks.base.assets.assets_base_main_page import OCR_CHARACTERS, OCR_MAP_NAME, ROGUE_LEAVE_FOR_NOW
|
|
from tasks.base.assets.assets_base_page import CLOSE, MAP_EXIT
|
|
from tasks.base.page import Page, page_gacha, page_main
|
|
from tasks.base.popup import PopupHandler
|
|
from tasks.character.keywords import CharacterList
|
|
from tasks.daily.assets.assets_daily_trial import START_TRIAL
|
|
from tasks.map.keywords import KEYWORDS_MAP_PLANE, MapPlane
|
|
|
|
|
|
class OcrPlaneName(OcrWhiteLetterOnComplexBackground):
|
|
def after_process(self, result):
|
|
# RobotSettlement1
|
|
result = re.sub(r'-[Ii1]$', '', result)
|
|
result = re.sub(r'I$', '', result)
|
|
result = re.sub(r'\d+$', '', result)
|
|
# Herta's OfficeY/
|
|
result = re.sub(r'Y/?$', '', result)
|
|
# Stargazer Navatia -> Stargazer Navalia
|
|
result = result.replace('avatia', 'avalia')
|
|
# DomainiRespite
|
|
result = result.replace('omaini', 'omain')
|
|
# Domain=Combat
|
|
result = result.replace('=', '')
|
|
# Domain--Occunrence
|
|
# Domain'--Occurence
|
|
# Domain-Qccurrence
|
|
result = result.replace('cunr', 'cur').replace('uren', 'urren').replace('Qcc', 'Occ')
|
|
# Domain-Elit
|
|
# Domain--Etite
|
|
result = re.sub(r'[Ee]lit$', 'Elite', result)
|
|
result = result.replace('tite', 'lite')
|
|
|
|
# 区域-战
|
|
result = re.sub(r'区域.*战$', '区域战斗', result)
|
|
# 区域-事伴, 区域-事祥
|
|
result = re.sub(r'事[伴祥]', '事件', result)
|
|
# 医域-战斗
|
|
result = result.replace('医域', '区域')
|
|
# 区域-战半, 区域-战头, 区域-战头书
|
|
result = re.sub(r'战[半头]', '战斗', result)
|
|
# 区域一战斗
|
|
result = re.sub(r'区域[\-—-一=]', '区域-', result)
|
|
# 累塔的办公室
|
|
result = result.replace('累塔', '黑塔')
|
|
if '星港' in result:
|
|
result = '迴星港'
|
|
|
|
result = result.replace(' ', '')
|
|
|
|
return super().after_process(result)
|
|
|
|
|
|
class OcrCharacterName(OcrWhiteLetterOnComplexBackground):
|
|
merge_thres_x = 20
|
|
merge_thres_y = 20
|
|
|
|
def after_process(self, result):
|
|
result = result.replace('蛆', '妲')
|
|
|
|
return super().after_process(result)
|
|
|
|
|
|
class MainPage(PopupHandler):
|
|
# Same as BigmapPlane class
|
|
# Current plane
|
|
plane: MapPlane = KEYWORDS_MAP_PLANE.Herta_ParlorCar
|
|
character_buttons: list[OcrResultButton] = []
|
|
character_current: CharacterList | None = None
|
|
|
|
_lang_checked = False
|
|
|
|
@property
|
|
def characters(self) -> list[CharacterList]:
|
|
characters = [button.matched_keyword for button in self.character_buttons]
|
|
return characters
|
|
|
|
def update_plane(self, lang=None) -> MapPlane | None:
|
|
"""
|
|
Pages:
|
|
in: page_main
|
|
"""
|
|
if lang is None:
|
|
lang = server.lang
|
|
ocr = OcrPlaneName(OCR_MAP_NAME, lang=lang)
|
|
result = ocr.ocr_single_line(self.device.image)
|
|
# Try to match
|
|
keyword = ocr._match_result(result, keyword_classes=MapPlane, lang=lang)
|
|
if keyword is not None:
|
|
self.plane = keyword
|
|
logger.attr('CurrentPlane', keyword)
|
|
return keyword
|
|
# Try to remove suffix
|
|
for suffix in range(1, 5):
|
|
keyword = ocr._match_result(result[:-suffix], keyword_classes=MapPlane, lang=lang)
|
|
if keyword is not None:
|
|
self.plane = keyword
|
|
logger.attr('CurrentPlane', keyword)
|
|
return keyword
|
|
|
|
return None
|
|
|
|
def check_lang_from_map_plane(self) -> str | None:
|
|
logger.info('check_lang_from_map_plane')
|
|
lang_unknown = self.config.Emulator_GameLanguage == 'auto'
|
|
|
|
if lang_unknown:
|
|
lang_list = VALID_LANG
|
|
else:
|
|
# Try current lang first
|
|
lang_list = [server.lang] + [lang for lang in VALID_LANG if lang != server.lang]
|
|
|
|
for lang in lang_list:
|
|
logger.info(f'Try ocr in lang {lang}')
|
|
keyword = self.update_plane(lang)
|
|
if keyword is not None:
|
|
logger.info(f'check_lang_from_map_plane matched lang: {lang}')
|
|
if lang_unknown or lang != server.lang:
|
|
self.config.Emulator_GameLanguage = lang
|
|
server.set_lang(lang)
|
|
return lang
|
|
|
|
if lang_unknown:
|
|
logger.critical('Cannot detect in-game text language, please set it to 简体中文 or English')
|
|
raise RequestHumanTakeover
|
|
else:
|
|
logger.warning(f'Cannot detect in-game text language, assume current lang={server.lang} is correct')
|
|
return server.lang
|
|
|
|
def handle_lang_check(self, page: Page):
|
|
"""
|
|
Args:
|
|
page:
|
|
|
|
Returns:
|
|
bool: If checked
|
|
"""
|
|
if MainPage._lang_checked:
|
|
return False
|
|
if page != page_main:
|
|
return False
|
|
|
|
self.check_lang_from_map_plane()
|
|
MainPage._lang_checked = True
|
|
return True
|
|
|
|
def acquire_lang_checked(self):
|
|
"""
|
|
Returns:
|
|
bool: If checked
|
|
"""
|
|
if MainPage._lang_checked:
|
|
return False
|
|
|
|
logger.info('acquire_lang_checked')
|
|
try:
|
|
self.ui_goto(page_main)
|
|
except AttributeError:
|
|
logger.critical('Method ui_goto() not found, class MainPage must be inherited by class UI')
|
|
raise ScriptError
|
|
|
|
self.handle_lang_check(page=page_main)
|
|
return True
|
|
|
|
def update_characters(self) -> list[CharacterList]:
|
|
ocr = OcrCharacterName(OCR_CHARACTERS)
|
|
self.character_buttons = ocr.matched_ocr(self.device.image, keyword_classes=CharacterList)
|
|
characters = self.characters
|
|
logger.attr('Characters', characters)
|
|
self.character_current = self._convert_selected_to_character(self._update_current_character())
|
|
return characters
|
|
|
|
def _update_current_character(self) -> list[int]:
|
|
"""
|
|
Returns:
|
|
list[int]: Selected index, 1 to 4.
|
|
"""
|
|
# 50px-width area starting from the right edge of HP bars
|
|
area = (1101, 151, 1151, 459)
|
|
# Y coordinates where the color peaks should be when character is selected
|
|
expected_peaks = np.array([201, 279, 357, 435])
|
|
expected_peaks_in_area = expected_peaks - area[1]
|
|
# Use Luminance to fit H264 video stream
|
|
image = rgb2luma(crop(self.device.image, area))
|
|
# Remove character names
|
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
|
|
image = cv2.erode(image, kernel)
|
|
# To find peaks along Y
|
|
line = cv2.reduce(image, 1, cv2.REDUCE_AVG).flatten().astype(int)
|
|
|
|
# Find color peaks
|
|
parameters = {
|
|
'height': (60, 255),
|
|
'prominence': 30,
|
|
'distance': 5,
|
|
}
|
|
peaks, _ = signal.find_peaks(line, **parameters)
|
|
# Remove smooth peaks
|
|
parameters = {
|
|
'height': (5, 255),
|
|
'prominence': 5,
|
|
'distance': 5,
|
|
}
|
|
diff = -np.diff(line)
|
|
diff_peaks, _ = signal.find_peaks(diff, **parameters)
|
|
|
|
def is_steep_peak(y, threshold=5):
|
|
return np.abs(diff_peaks - y).min() <= threshold
|
|
|
|
def peak_to_selected(y, threshold=5):
|
|
distance = np.abs(expected_peaks_in_area - y)
|
|
return np.argmin(distance) + 1 if distance.min() < threshold else 0
|
|
|
|
selected = [peak_to_selected(peak) for peak in peaks if peak_to_selected(peak) and is_steep_peak(peak)]
|
|
logger.attr('CharacterSelected', selected)
|
|
return selected
|
|
|
|
def _convert_selected_to_character(self, selected: list[int]) -> CharacterList | None:
|
|
expected_peaks = np.array([201, 279, 357, 435])
|
|
if not selected:
|
|
logger.warning(f'No current character')
|
|
logger.attr('CurrentCharacter', None)
|
|
return None
|
|
elif len(selected) == 1:
|
|
selected = selected[0]
|
|
else:
|
|
logger.warning(f'Too many current characters: {selected}, using first')
|
|
selected = selected[0]
|
|
|
|
expected_y = expected_peaks[selected - 1]
|
|
for button in self.character_buttons:
|
|
y = area_center(button.area)[1]
|
|
if expected_y - 78 < y < expected_y:
|
|
logger.attr('CurrentCharacter', button.matched_keyword)
|
|
return button.matched_keyword
|
|
|
|
logger.warning(f'Current character: {selected} does not belong to any detected character')
|
|
logger.attr('CurrentCharacter', None)
|
|
return None
|
|
|
|
def character_switch(self, character: CharacterList | str | int, skip_first_screenshot=True) -> bool:
|
|
"""
|
|
Args:
|
|
character: CharacterList object, or character name, or select index from 1 to 4.
|
|
skip_first_screenshot:
|
|
|
|
Returns:
|
|
bool: If chose
|
|
"""
|
|
logger.info(f'Character choose: {character}')
|
|
characters = self.characters
|
|
if isinstance(character, int):
|
|
character = self._convert_selected_to_character([character])
|
|
if character is None:
|
|
return False
|
|
try:
|
|
index = characters.index(character) + 1
|
|
except IndexError:
|
|
logger.warning(f'Cannot choose character {character} as it was not detected')
|
|
return False
|
|
else:
|
|
if isinstance(character, str):
|
|
character = CharacterList.find(character)
|
|
try:
|
|
index = characters.index(character) + 1
|
|
except IndexError:
|
|
logger.warning(f'Cannot choose character {character} as it was not detected')
|
|
return False
|
|
|
|
button = self.character_buttons[index - 1]
|
|
interval = Timer(1, count=3)
|
|
count = 0
|
|
while 1:
|
|
if skip_first_screenshot:
|
|
skip_first_screenshot = False
|
|
else:
|
|
self.device.screenshot()
|
|
|
|
# End
|
|
selected = self._update_current_character()
|
|
if index in selected:
|
|
logger.info('Character chose')
|
|
return True
|
|
if count > 3:
|
|
logger.warning('Failed to choose character, assume chose')
|
|
return False
|
|
|
|
try:
|
|
is_in_main = self.is_in_main
|
|
except AttributeError:
|
|
logger.critical('Method ui_goto() not found, class MainPage must be inherited by class UI')
|
|
raise ScriptError
|
|
|
|
if interval.reached() and is_in_main():
|
|
self.device.click(button)
|
|
interval.reset()
|
|
count += 1
|
|
|
|
def ui_leave_special(self):
|
|
"""
|
|
Leave from:
|
|
- Rogue domains
|
|
- Character trials
|
|
|
|
Returns:
|
|
bool: If left a special plane
|
|
|
|
Pages:
|
|
in: Any
|
|
out: page_main
|
|
"""
|
|
if not self.appear(MAP_EXIT):
|
|
return False
|
|
|
|
logger.info('UI leave special')
|
|
skip_first_screenshot = True
|
|
clicked = False
|
|
while 1:
|
|
if skip_first_screenshot:
|
|
skip_first_screenshot = False
|
|
else:
|
|
self.device.screenshot()
|
|
|
|
# End
|
|
if clicked:
|
|
if self.appear(page_main.check_button):
|
|
logger.info(f'Leave to {page_main}')
|
|
break
|
|
|
|
if self.appear_then_click(MAP_EXIT, interval=2):
|
|
continue
|
|
if self.handle_popup_confirm():
|
|
continue
|
|
if self.match_template_color(START_TRIAL, interval=2):
|
|
logger.info(f'{START_TRIAL} -> {CLOSE}')
|
|
self.device.click(CLOSE)
|
|
clicked = True
|
|
continue
|
|
if self.handle_ui_close(page_gacha.check_button, interval=2):
|
|
continue
|
|
if self.appear_then_click(ROGUE_LEAVE_FOR_NOW, interval=2):
|
|
clicked = True
|
|
continue
|