StarRailCopilot/module/reward/commission.py
LmeSzinc be00742c3c Add: 适配第一章, 修复大量bug
- 修复处理夜间委托时, 出现递归调用的问题
- 增加红脸出击确认的功能
- 增加了透视识别错误图片保存的开关
- 修复了地图太小时, 透视识别报错的问题
- 修复了相机位于地图外时, 透视识别出错的问题
- 修复了离开退役时, 会连击的问题
- 修复了同时出现低心情和船坞已满弹窗时, 卡住的问题
- 更新了一键退役实装后的安全点击的位置
- 修复了换装滑动失败时, 卡住的问题
- 修复了关闭自动收获后, 出现委托完成的提示是, 进图卡住的问题
- 修复了, 无正在跑的委托时, 报错的问题
2020-04-11 15:26:42 +08:00

466 lines
15 KiB
Python

import re
from datetime import datetime, timedelta
import cv2
import numpy as np
from scipy import signal
from module.base.ocr import Ocr
from module.base.timer import Timer
from module.base.utils import area_offset, get_color
from module.handler.info_bar import InfoBarHandler
from module.logger import logger
from module.reward.assets import *
from module.ui.page import page_reward, page_commission, CAMPAIGN_CHECK
from module.ui.ui import UI
dictionary = {
'major_comm': ['自主训练', '对抗演习', '科研任务', '工具整备', '战术课程', '货物运输'],
'daily_comm': ['日常资源开发', '高阶战术研发'],
'extra_drill': ['航行训练', '防卫巡逻', '海域浮标检查作业'],
'extra_part': ['委托'],
'extra_cube': ['演习'],
'extra_oil': ['油田'],
'extra_book': ['商船护卫'],
'urgent_drill': ['运输部队', '侦查部队', '主力部队', '精锐部队'],
'urgent_part': ['维拉', '', '多伦瓦', '恐班纳'],
'urgent_book': ['土豪尔', '姆波罗', '马拉基', '卡波罗', '马内', '玛丽', '', '特林'],
'urgent_box': ['装备', '物资'],
'urgent_cube': ['解救', '敌袭'],
'urgent_gem': ['要员', '度假', '巡视'],
'urgent_ship': ['观舰']
}
class Commission:
def __init__(self, image, y):
self.y = y
self.stack_y = y
self.area = (188, y - 119, 1199, y)
self.image = image
self.valid = True
# Name
area = area_offset((211, 26, 415, 49), self.area[0:2])
button = Button(area=area, color=(), button=area, name='COMMISSION')
ocr = Ocr(button, lang='cnocr', back=(74, 97, 148), use_binary=False)
self.button = button
self.name = ocr.ocr(image)
self.genre = self.parse_name(self.name)
# Duration time
area = area_offset((290, 74, 390, 92), self.area[0:2])
button = Button(area=area, color=(), button=area, name='DURATION')
ocr = Ocr(button, lang='stage', back=(57, 85, 132))
self.duration = self.parse_time(ocr.ocr(image))
# Expire time
area = area_offset((-49, 68, -45, 84), self.area[0:2])
button = Button(area=area, color=(189, 65, 66), button=area, name='IS_URGENT')
if button.appear_on(image):
area = area_offset((-49, 73, 45, 91), self.area[0:2])
button = Button(area=area, color=(), button=area, name='EXPIRE')
ocr = Ocr(button, lang='stage', back=(189, 65, 66))
self.expire = self.parse_time(ocr.ocr(image))
else:
self.expire = None
# Status
area = area_offset((179, 71, 187, 93), self.area[0:2])
dic = {
0: 'finished',
1: 'running',
2: 'pending'
}
self.status = dic[int(np.argmax(get_color(image, area)))]
def __str__(self):
if self.valid:
if self.expire:
return f'{self.name} (Genre: {self.genre}, Status: {self.status}, Duration: {self.duration}, Expire: {self.expire})'
else:
return f'{self.name} (Genre: {self.genre}, Status: {self.status}, Duration: {self.duration})'
else:
return f'{self.name} (Invalid)'
def __eq__(self, other):
"""
Args:
other (Commission):
Returns:
bool:
"""
threshold = timedelta(seconds=30)
if not self.valid or not other.valid:
return False
if self.genre != other.genre or self.status != other.status:
return False
if (other.duration < self.duration - threshold) or (other.duration > self.duration + threshold):
return False
if (not self.expire and other.expire) or (self.expire and not other.expire):
return False
if self.expire and other.expire:
if (other.expire < self.expire - threshold) or (other.expire > self.expire + threshold):
return False
return True
def parse_time(self, string):
"""
Args:
string (str): Such as 01:00:00, 05:47:10, 17:50:51.
Returns:
timedelta: datetime.timedelta instance.
"""
string = string.replace('D', '0') # Poor OCR
result = re.search('(\d+):(\d+):(\d+)', string)
if not result:
logger.warning(f'Invalid time string: {string}')
self.valid = False
return None
else:
result = [int(s) for s in result.groups()]
return timedelta(hours=result[0], minutes=result[1], seconds=result[2])
def parse_name(self, string):
"""
Args:
string (str): Commission name, such as 'NYB要员护卫'.
Returns:
str: Commission genre, such as 'urgent_gem'.
"""
for key, value in dictionary.items():
for keyword in value:
if keyword in string:
return key
logger.warning(f'Name with unknown genre: {string}')
self.valid = False
return ''
class CommissionGroup:
show = (188, 67, 1199, 692)
height = 210 # About 1.5 commission height
lower = int((show[3] - show[1]) / 2 - height / 2)
template_area = (620, lower, 1154, lower + height)
def __init__(self):
self.template = None
self.swipe = 0
self.commission = []
def __contains__(self, item):
for commission in self.commission:
if commission == item:
return True
return False
def __iter__(self):
return iter(self.commission)
def __bool__(self):
return len(self.commission) > 0
@property
def count(self):
return len(self.commission)
def merge(self, image):
"""Load commissions from image.
If you want to load commissions from multiple image,
make sure that the next one and previous one have something same.
Which means, you merge a image, then swipe a little bit, then merge another image.
Args:
image (PIl.Image.Image):
"""
# Find swipe distance
if self.template is None:
self.template = np.array(image.crop(self.template_area))
self.swipe = 0
res = cv2.matchTemplate(self.template, np.array(image), cv2.TM_CCOEFF_NORMED)
_, similarity, _, position = cv2.minMaxLoc(res)
if similarity < 0.85:
logger.warning(f'Low similarity when finding swipe. Similarity: {similarity}, Position: {position}')
self.swipe -= position[1] - self.template_area[1]
self.template = np.array(image.crop(self.template_area))
# Find commission position
color_height = np.mean(image.crop((597, 0, 619, 720)).convert('L'), axis=1)
parameters = {'height': 200}
peaks, _ = signal.find_peaks(color_height, **parameters)
peaks = [y for y in peaks if y > 67 + 117]
# Add commission to list
for y in peaks:
stack_y = y + self.swipe
diff = np.array([c.stack_y - stack_y for c in self.commission])
if np.any(np.abs(diff) < 3):
continue
commission = Commission(image, y=y)
commission.stack_y = stack_y
logger.info(f'Add commission: {commission}')
self.commission.append(commission)
def commission_choose(daily, urgent, priority, time_limit=None):
"""
Args:
daily (CommissionGroup):
urgent (CommissionGroup):
priority (dict):
time_limit (datetime):
Returns:
CommissionGroup, CommissionGroup: Chosen daily commission, Chosen urgent commission
"""
# Count Commission
commission = daily.commission + urgent.commission
running_count = int(np.sum([1 for c in commission if c.status == 'running']))
logger.attr('Running', running_count)
if running_count >= 4:
return [], []
# Calculate priority
commission = [c for c in commission if c.valid and c.status == 'pending']
comm_priority = []
for comm in commission:
pri = priority[comm.genre]
if comm.duration <= timedelta(hours=2):
pri += priority['duration_shorter_than_2']
if comm.duration >= timedelta(hours=6):
pri += priority['duration_longer_than_6']
if comm.expire:
if comm.expire <= timedelta(hours=2):
pri += priority['expire_shorter_than_2']
if comm.expire >= timedelta(hours=6):
pri += priority['expire_longer_than_6']
comm_priority.append(pri)
# Sort
commission = list(np.array(commission)[np.argsort(comm_priority)])[::-1]
if time_limit:
commission = [comm for comm in commission if datetime.now() + comm.duration <= time_limit]
commission = commission[:4 - running_count]
daily_choose, urgent_choose = CommissionGroup(), CommissionGroup()
for comm in commission:
if comm in daily:
daily_choose.commission.append(comm)
if comm in urgent:
urgent_choose.commission.append(comm)
if daily_choose:
logger.info('Choose daily commission')
for comm in daily_choose:
logger.info(comm)
if urgent_choose:
logger.info('Choose urgent commission')
for comm in urgent_choose:
logger.info(comm)
return daily_choose, urgent_choose
class RewardCommission(UI, InfoBarHandler):
daily: CommissionGroup
urgent: CommissionGroup
daily_choose: CommissionGroup
urgent_choose: CommissionGroup
def _commission_ensure_mode(self, mode):
if self.appear(COMMISSION_DAILY):
current = 'daily'
elif self.appear(COMMISSION_URGENT):
current = 'urgent'
else:
logger.warning('Unknown Commission mode')
return False
if current == mode:
return False
if mode == 'daily':
self.device.click(COMMISSION_DAILY)
if mode == 'urgent':
self.device.click(COMMISSION_URGENT)
self.device.sleep(0.3)
self.device.screenshot()
return True
def _commission_mode_reset(self):
if self.appear(COMMISSION_DAILY):
current, another = COMMISSION_DAILY, COMMISSION_URGENT
elif self.appear(COMMISSION_URGENT):
current, another = COMMISSION_URGENT, COMMISSION_DAILY
else:
logger.warning('Unknown Commission mode')
return False
self.device.click(another)
self.device.screenshot()
self.device.click(current)
self.device.sleep(0.3)
self.device.screenshot()
return True
def _commission_swipe(self, distance=180):
# Distance of two commission is 146px
self.device.swipe((0, -distance), box=(620, 67, 1154, 692), random_range=(-20, -5, 20, 5))
self.device.click(COMMISSION_STOP_SCROLLING)
self.device.sleep(0.3)
self.device.screenshot()
def _commission_scan_list(self):
commission = CommissionGroup()
commission.merge(self.device.image)
if commission.count <= 3:
return commission
prev = commission.count
for _ in range(15):
self._commission_swipe()
commission.merge(self.device.image)
if commission.count - prev <= 0:
break
prev = commission.count
return commission
def _commission_scan_all(self):
logger.hr('Scan daily')
self._commission_ensure_mode('daily')
daily = self._commission_scan_list()
logger.hr('Scan urgent')
self._commission_ensure_mode('urgent')
urgent = self._commission_scan_list()
logger.hr('Showing commission')
logger.info('Daily commission')
for comm in daily:
logger.info(comm)
if urgent.count:
logger.info('Urgent commission')
for comm in urgent:
logger.info(comm)
self.daily = daily
self.urgent = urgent
self.daily_choose, self.urgent_choose = commission_choose(
self.daily,
self.urgent,
priority=self.config.COMMISSION_PRIORITY,
time_limit=self.config.COMMISSION_TIME_LIMIT)
return daily, urgent
def _commission_start_click(self, comm):
"""
Args:
comm (Commission):
"""
logger.info(f'Start commission {comm}')
comm_timer = Timer(3)
while 1:
if comm_timer.reached():
self.device.click(comm.button)
comm_timer.reset()
if self.appear_then_click(COMMISSION_OIL_CONFIRM, offset=True, interval=3):
pass
if self.appear_then_click(COMMISSION_START, interval=3):
pass
if self.appear_then_click(COMMISSION_ADVICE, interval=3):
pass
if self.handle_info_bar():
break
self.device.screenshot()
return True
def _commission_find_and_start(self, comm):
"""
Args:
comm (Commission):
"""
logger.hr(f'Finding commission')
logger.info(f'Finding commission {comm}')
commission = CommissionGroup()
prev = 0
for _ in range(15):
commission.merge(self.device.image)
if commission.count - prev <= 0:
return True
prev = commission.count
if comm in commission:
# Update commission position.
# Because once you start a commission, the commission list changes.
for new_comm in commission:
if comm == new_comm:
comm = new_comm
self._commission_start_click(comm)
return True
self._commission_swipe()
logger.warning(f'Commission not found: {comm}')
return False
def commission_start(self):
"""
Method to scan and run commissions.
Make sure current page is page_commission before calls.
"""
logger.hr('Commission scan', level=2)
self._commission_scan_all()
logger.hr('Commission run', level=2)
if self.daily_choose:
for comm in self.daily_choose:
if not self._commission_ensure_mode('daily'):
self._commission_mode_reset()
self._commission_find_and_start(comm)
if self.urgent_choose:
for comm in self.urgent_choose:
if not self._commission_ensure_mode('urgent'):
self._commission_mode_reset()
self._commission_find_and_start(comm)
if not self.daily_choose and not self.urgent_choose:
logger.info('No commission chose')
def handle_commission_start(self):
"""Make sure current page is page_reward before calls.
Returns:
bool: If runs a commission.
"""
if not self.config.ENABLE_COMMISSION_REWARD:
return False
self.device.screenshot()
if not self.appear(COMMISSION_HAS_PENDING):
logger.info('No commission pending')
return False
self.ui_goto(page_commission)
self.commission_start()
self.ui_goto(page_reward, skip_first_screenshot=True)
def commission_notice_show_at_campaign(self):
"""
Returns:
bool: If any commission finished.
"""
return self.appear(CAMPAIGN_CHECK) and self.appear(COMMISSION_NOTICE_AT_CAMPAIGN)