Esempio n. 1
0
class ArknightsHelper(object):
    def __init__(
            self,
            current_strength=None,  # 当前理智
            adb_host=None,  # 当前绑定到的设备
            out_put=True,  # 是否有命令行输出
            call_by_gui=False):  # 是否为从 GUI 程序调用
        ensure_adb_alive()
        self.adb = ADBConnector(adb_serial=adb_host)
        self.__is_game_active = False
        self.__call_by_gui = call_by_gui
        self.is_called_by_gui = call_by_gui
        self.viewport = self.adb.screenshot().size
        self.operation_time = []
        self.delay_impl = sleep
        if DEBUG_LEVEL >= 1:
            self.__print_info()
        self.refill_with_item = config.get('behavior/refill_ap_with_item',
                                           False)
        self.refill_with_originium = config.get(
            'behavior/refill_ap_with_originium', False)
        self.use_refill = self.refill_with_item or self.refill_with_originium
        self.loots = {}
        self.use_penguin_report = config.get('reporting/enabled', False)
        if self.use_penguin_report:
            self.penguin_reporter = penguin_stats.reporter.PenguinStatsReporter(
            )
        self.refill_count = 0
        self.max_refill_count = None
        logger.debug("成功初始化模块")

    def __print_info(self):
        logger.info('当前系统信息:')
        logger.info('ADB 服务器:\t%s:%d', *config.ADB_SERVER)
        logger.info('分辨率:\t%dx%d', *self.viewport)
        # logger.info('OCR 引擎:\t%s', ocr.engine.info)
        logger.info('截图路径:\t%s', config.SCREEN_SHOOT_SAVE_PATH)

        if config.enable_baidu_api:
            logger.info(
                '%s', """百度API配置信息:
        APP_ID\t{app_id}
        API_KEY\t{api_key}
        SECRET_KEY\t{secret_key}
                        """.format(app_id=config.APP_ID,
                                   api_key=config.API_KEY,
                                   secret_key=config.SECRET_KEY))

    def __del(self):
        self.adb.run_device_cmd("am force-stop {}".format(
            config.ArkNights_PACKAGE_NAME))

    def destroy(self):
        self.__del()

    def check_game_active(self):  # 启动游戏 需要手动调用
        logger.debug("helper.check_game_active")
        current = self.adb.run_device_cmd(
            'dumpsys window windows | grep mCurrentFocus').decode(
                errors='ignore')
        logger.debug("正在尝试启动游戏")
        logger.debug(current)
        if config.ArkNights_PACKAGE_NAME in current:
            self.__is_game_active = True
            logger.debug("游戏已启动")
        else:
            self.adb.run_device_cmd("am start -n {}/{}".format(
                config.ArkNights_PACKAGE_NAME, config.ArkNights_ACTIVITY_NAME))
            logger.debug("成功启动游戏")
            self.__is_game_active = True

    def __wait(
            self,
            n=10,  # 等待时间中值
            MANLIKE_FLAG=True):  # 是否在此基础上设偏移量
        if MANLIKE_FLAG:
            m = uniform(0, 0.3)
            n = uniform(n - m * 0.5 * n, n + m * n)
        self.delay_impl(n)

    def mouse_click(
            self,  # 点击一个按钮
            XY):  # 待点击的按钮的左上和右下坐标
        assert (self.viewport == (1280, 720))
        logger.debug("helper.mouse_click")
        xx = randint(XY[0][0], XY[1][0])
        yy = randint(XY[0][1], XY[1][1])
        logger.info("接收到点击坐标并传递xx:{}和yy:{}".format(xx, yy))
        self.adb.touch_tap((xx, yy))
        self.__wait(TINY_WAIT, MANLIKE_FLAG=True)

    def tap_rect(self, rc):
        hwidth = (rc[2] - rc[0]) / 2
        hheight = (rc[3] - rc[1]) / 2
        midx = rc[0] + hwidth
        midy = rc[1] + hheight
        xdiff = max(-1, min(1, gauss(0, 0.2)))
        ydiff = max(-1, min(1, gauss(0, 0.2)))
        tapx = int(midx + xdiff * hwidth)
        tapy = int(midy + ydiff * hheight)
        self.adb.touch_tap((tapx, tapy))
        self.__wait(TINY_WAIT, MANLIKE_FLAG=True)

    def tap_quadrilateral(self, pts):
        pts = np.asarray(pts)
        acdiff = max(0, min(2, gauss(1, 0.2)))
        bddiff = max(0, min(2, gauss(1, 0.2)))
        halfac = (pts[2] - pts[0]) / 2
        m = pts[0] + halfac * acdiff
        pt2 = pts[1] if bddiff > 1 else pts[3]
        halfvec = (pt2 - m) / 2
        finalpt = m + halfvec * bddiff
        self.adb.touch_tap(tuple(int(x) for x in finalpt))
        self.__wait(TINY_WAIT, MANLIKE_FLAG=True)

    def wait_for_still_image(self,
                             threshold=16,
                             crop=None,
                             timeout=60,
                             raise_for_timeout=True):
        if crop is None:
            shooter = lambda: self.adb.screenshot()
        else:
            shooter = lambda: self.adb.screenshot().crop(crop)
        screenshot = shooter()
        t0 = time.monotonic()
        ts = t0 + timeout
        n = 0
        minerr = 65025
        while time.monotonic() < ts:
            self.__wait(1)
            screenshot2 = shooter()
            mse = imgreco.imgops.compare_mse(screenshot, screenshot2)
            if mse <= threshold:
                return screenshot2
            screenshot = screenshot2
            if mse < minerr:
                minerr = mse
            n += 1
            if n == 9:
                logger.info("等待画面静止")
        if raise_for_timeout:
            raise RuntimeError("%d 秒内画面未静止,最小误差=%d,阈值=%d" %
                               (timeout, minerr, threshold))
        return None

    def module_login(self):
        logger.debug("helper.module_login")
        logger.info("发送坐标LOGIN_QUICK_LOGIN: {}".format(
            CLICK_LOCATION['LOGIN_QUICK_LOGIN']))
        self.mouse_click(CLICK_LOCATION['LOGIN_QUICK_LOGIN'])
        self.__wait(BIG_WAIT)
        logger.info("发送坐标LOGIN_START_WAKEUP: {}".format(
            CLICK_LOCATION['LOGIN_START_WAKEUP']))
        self.mouse_click(CLICK_LOCATION['LOGIN_START_WAKEUP'])
        self.__wait(BIG_WAIT)

    def module_battle_slim(
            self,
            c_id=None,  # 待战斗的关卡编号
            set_count=1000,  # 战斗次数
            check_ai=True,  # 是否检查代理指挥
            **kwargs):  # 扩展参数:
        '''
        :param sub 是否为子程序 (是否为module_battle所调用)
        :param auto_close 是否自动关闭, 默认为 false
        :param self_fix 是否尝试自动修复, 默认为 false
        :param MAX_TIME 最大检查轮数, 默认在 config 中设置,
            每隔一段时间进行一轮检查判断作战是否结束
            建议自定义该数值以便在出现一定失误,
            超出最大判断次数后有一定的自我修复能力
        :return:
            True 完成指定次数的作战
            False 理智不足, 退出作战
        '''
        logger.debug("helper.module_battle_slim")
        sub = kwargs["sub"] \
            if "sub" in kwargs else False
        auto_close = kwargs["auto_close"] \
            if "auto_close" in kwargs else False
        if not sub:
            logger.info("战斗-选择{}...启动".format(c_id))
        if set_count == 0:
            return True
        self.operation_time = []
        count = 0
        try:
            for count in range(set_count):
                # logger.info("开始第 %d 次战斗", count + 1)
                self.operation_once_statemachine(c_id, )
                logger.info("第 %d 次作战完成", count + 1)
                if count != set_count - 1:
                    # 2019.10.06 更新逻辑后,提前点击后等待时间包括企鹅物流
                    if config.reporter:
                        self.__wait(SMALL_WAIT, MANLIKE_FLAG=True)
                    else:
                        self.__wait(BIG_WAIT, MANLIKE_FLAG=True)
        except StopIteration:
            logger.error('未能进行第 %d 次作战', count + 1)
            remain = set_count - count - 1
            if remain > 0:
                logger.error('已忽略余下的 %d 次战斗', remain)

        if not sub:
            if auto_close:
                logger.info("简略模块{}结束,系统准备退出".format(c_id))
                self.__wait(120, False)
                self.__del()
            else:
                logger.info("简略模块{}结束".format(c_id))
                return True
        else:
            logger.info("当前任务{}结束,准备进行下一项任务".format(c_id))
            return True

    def can_perform_refill(self):
        if not self.use_refill:
            return False
        if self.max_refill_count is not None:
            return self.refill_count < self.max_refill_count
        else:
            return True

    @dataclass
    class operation_once_state:
        state: Callable = None
        stop: bool = False
        operation_start: float = 0
        first_wait: bool = True
        mistaken_delegation: bool = False
        prepare_reco: dict = None

    def operation_once_statemachine(self, c_id):
        smobj = ArknightsHelper.operation_once_state()

        def on_prepare(smobj):
            count_times = 0
            while True:
                screenshot = self.adb.screenshot()
                recoresult = imgreco.before_operation.recognize(screenshot)
                if recoresult is not None:
                    logger.debug('当前画面关卡:%s', recoresult['operation'])
                    if c_id is not None:
                        # 如果传入了关卡 ID,检查识别结果
                        if recoresult['operation'] != c_id:
                            logger.error('不在关卡界面')
                            raise StopIteration()
                    break
                else:
                    count_times += 1
                    self.__wait(1, False)
                    if count_times <= 7:
                        logger.warning('不在关卡界面')
                        self.__wait(TINY_WAIT, False)
                        continue
                    else:
                        logger.error('{}次检测后都不再关卡界面,退出进程'.format(count_times))
                        raise StopIteration()

            self.CURRENT_STRENGTH = int(recoresult['AP'].split('/')[0])
            ap_text = '理智' if recoresult['consume_ap'] else '门票'
            logger.info('当前%s %d, 关卡消耗 %d', ap_text, self.CURRENT_STRENGTH,
                        recoresult['consume'])
            if self.CURRENT_STRENGTH < int(recoresult['consume']):
                logger.error(ap_text + '不足 无法继续')
                if recoresult['consume_ap'] and self.can_perform_refill():
                    logger.info('尝试回复理智')
                    self.tap_rect(
                        imgreco.before_operation.get_start_operation_rect(
                            self.viewport))
                    self.__wait(SMALL_WAIT)
                    screenshot = self.adb.screenshot()
                    refill_type = imgreco.before_operation.check_ap_refill_type(
                        screenshot)
                    confirm_refill = False
                    if refill_type == 'item' and self.refill_with_item:
                        logger.info('使用道具回复理智')
                        confirm_refill = True
                    if refill_type == 'originium' and self.refill_with_originium:
                        logger.info('碎石回复理智')
                        confirm_refill = True
                    # FIXME: 道具回复量不足时也会尝试使用
                    if confirm_refill:
                        self.tap_rect(
                            imgreco.before_operation.
                            get_ap_refill_confirm_rect(self.viewport))
                        self.refill_count += 1
                        self.__wait(MEDIUM_WAIT)
                        return  # to on_prepare state
                    logger.error('未能回复理智')
                    self.tap_rect(
                        imgreco.before_operation.get_ap_refill_cancel_rect(
                            self.viewport))
                raise StopIteration()

            if not recoresult['delegated']:
                logger.info('设置代理指挥')
                self.tap_rect(
                    imgreco.before_operation.get_delegate_rect(self.viewport))
                return  # to on_prepare state

            logger.info("理智充足 开始行动")
            self.tap_rect(
                imgreco.before_operation.get_start_operation_rect(
                    self.viewport))
            smobj.prepare_reco = recoresult
            smobj.state = on_troop

        def on_troop(smobj):
            count_times = 0
            while True:
                self.__wait(TINY_WAIT, False)
                screenshot = self.adb.screenshot()
                recoresult = imgreco.before_operation.check_confirm_troop_rect(
                    screenshot)
                if recoresult:
                    logger.info('确认编队')
                    break
                else:
                    count_times += 1
                    if count_times <= 7:
                        logger.warning('等待确认编队')
                        continue
                    else:
                        logger.error('{} 次检测后不再确认编队界面'.format(count_times))
                        raise StopIteration()
            self.tap_rect(
                imgreco.before_operation.get_confirm_troop_rect(self.viewport))
            smobj.operation_start = monotonic()
            smobj.state = on_operation

        def on_operation(smobj):
            if smobj.first_wait:
                if len(self.operation_time) == 0:
                    wait_time = BATTLE_NONE_DETECT_TIME
                else:
                    wait_time = sum(self.operation_time) / len(
                        self.operation_time) - 7
                logger.info('等待 %d s' % wait_time)
                self.__wait(wait_time, MANLIKE_FLAG=False)
                smobj.first_wait = False
            t = monotonic() - smobj.operation_start

            logger.info('已进行 %.1f s,判断是否结束', t)

            screenshot = self.adb.screenshot()
            if imgreco.end_operation.check_level_up_popup(screenshot):
                logger.info("等级提升")
                self.operation_time.append(t)
                smobj.state = on_level_up_popup
                return

            if smobj.prepare_reco['consume_ap']:
                detector = imgreco.end_operation.check_end_operation
            else:
                detector = imgreco.end_operation.check_end_operation_alt
            if detector(screenshot):
                logger.info('战斗结束')
                self.operation_time.append(t)
                crop = imgreco.end_operation.get_still_check_rect(
                    self.viewport)
                if self.wait_for_still_image(crop=crop,
                                             timeout=15,
                                             raise_for_timeout=True):
                    smobj.state = on_end_operation
                return
            dlgtype, ocrresult = imgreco.common.recognize_dialog(screenshot)
            if dlgtype is not None:
                if dlgtype == 'yesno' and '代理指挥' in ocrresult:
                    logger.warning('代理指挥出现失误')
                    smobj.mistaken_delegation = True
                    if config.get('behavior/mistaken_delegation/settle',
                                  False):
                        logger.info('以 2 星结算关卡')
                        self.tap_rect(
                            imgreco.common.get_dialog_right_button_rect(
                                screenshot))
                        self.__wait(2)
                        return
                    else:
                        logger.info('放弃关卡')
                        self.tap_rect(
                            imgreco.common.get_dialog_left_button_rect(
                                screenshot))
                        # 关闭失败提示
                        self.wait_for_still_image()
                        self.tap_rect(
                            imgreco.common.get_reward_popup_dismiss_rect(
                                screenshot))
                        self.__wait(1)
                        return
                elif dlgtype == 'yesno' and '将会恢复' in ocrresult:
                    logger.info('发现放弃行动提示,关闭')
                    self.tap_rect(
                        imgreco.common.get_dialog_left_button_rect(screenshot))
                else:
                    logger.error('未处理的对话框:[%s] %s', dlgtype, ocrresult)
                    raise RuntimeError('unhandled dialog')

            logger.info('战斗未结束')
            self.__wait(BATTLE_FINISH_DETECT)

        def on_level_up_popup(smobj):
            self.__wait(SMALL_WAIT, MANLIKE_FLAG=True)
            logger.info('关闭升级提示')
            self.tap_rect(
                imgreco.end_operation.get_dismiss_level_up_popup_rect(
                    self.viewport))
            self.wait_for_still_image()
            smobj.state = on_end_operation

        def on_end_operation(smobj):
            screenshot = self.adb.screenshot()
            logger.info('离开结算画面')
            self.tap_rect(
                imgreco.end_operation.get_dismiss_end_operation_rect(
                    self.viewport))
            reportresult = penguin_stats.reporter.ReportResult.NotReported
            try:
                # 掉落识别
                drops = imgreco.end_operation.recognize(screenshot)
                logger.debug('%s', repr(drops))
                logger.info('掉落识别结果:%s', format_recoresult(drops))
                log_total = len(self.loots)
                for _, group in drops['items']:
                    for name, qty in group:
                        if name is not None and qty is not None:
                            self.loots[name] = self.loots.get(name, 0) + qty
                if log_total:
                    self.log_total_loots()
                if self.use_penguin_report:
                    reportresult = self.penguin_reporter.report(drops)
                    if isinstance(reportresult,
                                  penguin_stats.reporter.ReportResult.Ok):
                        logger.debug('report hash = %s',
                                     reportresult.report_hash)
            except Exception as e:
                logger.error('', exc_info=True)
            if self.use_penguin_report and reportresult is penguin_stats.reporter.ReportResult.NotReported:
                filename = os.path.join(config.SCREEN_SHOOT_SAVE_PATH,
                                        '未上报掉落-%d.png' % time.time())
                with open(filename, 'wb') as f:
                    screenshot.save(f, format='PNG')
                logger.error('未上报掉落截图已保存到 %s', filename)
            smobj.stop = True

        smobj.state = on_prepare
        smobj.stop = False
        smobj.operation_start = 0

        while not smobj.stop:
            oldstate = smobj.state
            smobj.state(smobj)
            if smobj.state != oldstate:
                logger.debug('state changed to %s', smobj.state.__name__)

        if smobj.mistaken_delegation and config.get(
                'behavior/mistaken_delegation/skip', True):
            raise StopIteration()

    def back_to_main(self):  # 回到主页
        logger.info("正在返回主页")
        while True:
            screenshot = self.adb.screenshot()

            if imgreco.main.check_main(screenshot):
                break

            # 检查是否有返回按钮
            if imgreco.common.check_nav_button(screenshot):
                logger.info('发现返回按钮,点击返回')
                self.tap_rect(
                    imgreco.common.get_nav_button_back_rect(self.viewport))
                self.__wait(SMALL_WAIT)
                # 点击返回按钮之后重新检查
                continue

            if imgreco.common.check_get_item_popup(screenshot):
                logger.info('当前为获得物资画面,关闭')
                self.tap_rect(
                    imgreco.common.get_reward_popup_dismiss_rect(
                        self.viewport))
                self.__wait(SMALL_WAIT)
                continue

            # 检查是否在设置画面
            if imgreco.common.check_setting_scene(screenshot):
                logger.info("当前为设置/邮件画面,返回")
                self.tap_rect(
                    imgreco.common.get_setting_back_rect(self.viewport))
                self.__wait(SMALL_WAIT)
                continue

            # 检测是否有关闭按钮
            rect, confidence = imgreco.common.find_close_button(screenshot)
            if confidence > 0.9:
                logger.info("发现关闭按钮")
                self.tap_rect(rect)
                self.__wait(SMALL_WAIT)
                continue

            dlgtype, ocr = imgreco.common.recognize_dialog(screenshot)
            if dlgtype == 'yesno':
                if '基建' in ocr or '停止招募' in ocr:
                    self.tap_rect(
                        imgreco.common.get_dialog_right_button_rect(
                            screenshot))
                    self.__wait(3)
                    continue
                elif '招募干员' in ocr or '加急' in ocr:
                    self.tap_rect(
                        imgreco.common.get_dialog_left_button_rect(screenshot))
                    self.__wait(3)
                    continue
                else:
                    raise RuntimeError('未适配的对话框')
            elif dlgtype == 'ok':
                self.tap_rect(
                    imgreco.common.get_dialog_ok_button_rect(screenshot))
                self.__wait(1)
                continue

            raise RuntimeError('未知画面')
        logger.info("已回到主页")

    def module_battle(
            self,  # 完整的战斗模块
            c_id,  # 选择的关卡
            set_count=1000):  # 作战次数
        logger.debug("helper.module_battle")
        self.goto_stage(c_id)
        self.module_battle_slim(c_id,
                                set_count=set_count,
                                check_ai=True,
                                sub=True)
        return True

    def main_handler(self, task_list, clear_tasks=False, auto_close=True):

        logger.info("装载模块...")
        logger.info("战斗模块...启动")
        flag = False
        if len(task_list) == 0:
            logger.fatal("任务清单为空!")

        for c_id, count in task_list:
            if not stage_path.is_stage_supported(c_id):
                raise ValueError(c_id)
            logger.info("开始 %s", c_id)
            flag = self.module_battle(c_id, count)

        if flag:
            if self.__call_by_gui or auto_close is False:
                logger.info("所有模块执行完毕")
            else:
                if clear_tasks:
                    self.clear_daily_task()
                logger.info("所有模块执行完毕... 60s后退出")
                self.__wait(60)
                self.__del()
        else:
            if self.__call_by_gui or auto_close is False:
                logger.error("发生未知错误... 进程已结束")
            else:
                logger.error("发生未知错误... 60s后退出")
                self.__wait(60)
                self.__del()

    def clear_daily_task(self):
        logger.debug("helper.clear_daily_task")
        logger.info("领取每日任务")
        self.back_to_main()
        screenshot = self.adb.screenshot()
        logger.info('进入任务界面')
        self.tap_quadrilateral(imgreco.main.get_task_corners(screenshot))
        self.__wait(SMALL_WAIT)
        screenshot = self.adb.screenshot()

        hasbeginner = imgreco.task.check_beginners_task(screenshot)
        if hasbeginner:
            logger.info('发现见习任务,切换到每日任务')
            self.tap_rect(
                imgreco.task.get_daily_task_rect(screenshot, hasbeginner))
            self.__wait(TINY_WAIT)
            screenshot = self.adb.screenshot()

        while imgreco.task.check_collectable_reward(screenshot):
            logger.info('完成任务')
            self.tap_rect(
                imgreco.task.get_collect_reward_button_rect(self.viewport))
            self.__wait(SMALL_WAIT)
            while True:
                screenshot = self.adb.screenshot()
                if imgreco.common.check_get_item_popup(screenshot):
                    logger.info('领取奖励')
                    self.tap_rect(
                        imgreco.common.get_reward_popup_dismiss_rect(
                            self.viewport))
                    self.__wait(SMALL_WAIT)
                else:
                    break
            screenshot = self.adb.screenshot()
        logger.info("奖励已领取完毕")

    def recruit(self):
        from . import recruit_calc
        logger.info('识别招募标签')
        tags = imgreco.recruit.get_recruit_tags(self.adb.screenshot())
        logger.info('可选标签:%s', ' '.join(tags))
        result = recruit_calc.calculate(tags)
        logger.debug('计算结果:%s', repr(result))
        return result

    def find_and_tap(self, partition, target):
        lastpos = None
        while True:
            screenshot = self.adb.screenshot()
            recoresult = imgreco.map.recognize_map(screenshot, partition)
            if recoresult is None:
                # TODO: retry
                logger.error('未能定位关卡地图')
                raise RuntimeError('recognition failed')
            if target in recoresult:
                pos = recoresult[target]
                logger.info('目标 %s 坐标: %s', target, pos)
                if lastpos is not None and tuple(pos) == tuple(lastpos):
                    logger.error('拖动后坐标未改变')
                    raise RuntimeError('拖动后坐标未改变')
                if 0 < pos[0] < self.viewport[0]:
                    logger.info('目标在可视区域内,点击')
                    self.adb.touch_tap(pos, offsets=(5, 5))
                    self.__wait(3)
                    break
                else:
                    lastpos = pos
                    originX = self.viewport[0] // 2 + randint(-100, 100)
                    originY = self.viewport[1] // 2 + randint(-100, 100)
                    if pos[0] < 0:  # target in left of viewport
                        logger.info('目标在可视区域左侧,向右拖动')
                        # swipe right
                        diff = -pos[0]
                        if abs(diff) < 100:
                            diff = 120
                        diff = min(diff, self.viewport[0] - originX)
                    elif pos[0] > self.viewport[
                            0]:  # target in right of viewport
                        logger.info('目标在可视区域右侧,向左拖动')
                        # swipe left
                        diff = self.viewport[0] - pos[0]
                        if abs(diff) < 100:
                            diff = -120
                        diff = max(diff, -originX)
                    self.adb.touch_swipe2((originX, originY),
                                          (diff * 0.7 * uniform(0.8, 1.2), 0),
                                          max(250, diff / 2))
                    self.__wait(5)
                    continue

            else:
                raise KeyError((target, partition))

    def find_and_tap_daily(self, partition, target, *, recursion=0):
        screenshot = self.adb.screenshot()
        recoresult = imgreco.map.recognize_daily_menu(screenshot, partition)
        if target in recoresult:
            pos, conf = recoresult[target]
            logger.info('目标 %s 坐标=%s 差异=%f', target, pos, conf)
            offset = self.viewport[1] * 0.12  ## 24vh * 24vh range
            self.tap_rect((*(pos - offset), *(pos + offset)))
        else:
            if recursion == 0:
                logger.info('目标可能在可视区域右侧,向左拖动')
                originX = self.viewport[0] // 2 + randint(-100, 100)
                originY = self.viewport[1] // 2 + randint(-100, 100)
                self.adb.touch_swipe2((originX, originY),
                                      (-self.viewport[0] * 0.2, 0), 400)
                self.__wait(2)
                self.find_and_tap_daily(partition,
                                        target,
                                        recursion=recursion + 1)
            else:
                logger.error('未找到目标,是否未开放关卡?')

    def goto_stage(self, stage):
        if not stage_path.is_stage_supported(stage):
            logger.error('不支持的关卡:%s', stage)
            raise ValueError(stage)
        path = stage_path.get_stage_path(stage)
        self.back_to_main()
        logger.info('进入作战')
        self.tap_quadrilateral(
            imgreco.main.get_ballte_corners(self.adb.screenshot()))
        self.__wait(3)
        if path[0] == 'main':
            self.find_and_tap('episodes', path[1])
            self.find_and_tap(path[1], path[2])
        elif path[0] == 'material' or path[0] == 'soc':
            logger.info('选择类别')
            self.tap_rect(
                imgreco.map.get_daily_menu_entry(self.viewport, path[0]))
            self.find_and_tap_daily(path[0], path[1])
            self.find_and_tap(path[1], path[2])
        else:
            raise NotImplementedError()

    def get_credit(self):
        logger.debug("helper.get_credit")
        logger.info("领取信赖")
        self.back_to_main()
        screenshot = self.adb.screenshot()
        logger.info('进入好友列表')
        self.tap_quadrilateral(imgreco.main.get_friend_corners(screenshot))
        self.__wait(SMALL_WAIT)
        self.tap_quadrilateral(imgreco.main.get_friend_list(screenshot))
        self.__wait(SMALL_WAIT)
        logger.info('访问好友基建')
        self.tap_quadrilateral(imgreco.main.get_friend_build(screenshot))
        self.__wait(MEDIUM_WAIT)
        building_count = 0
        while building_count <= 11:
            screenshot = self.adb.screenshot()
            self.tap_quadrilateral(
                imgreco.main.get_next_friend_build(screenshot))
            self.__wait(MEDIUM_WAIT)
            building_count = building_count + 1
            logger.info('访问第 %s 位好友', building_count)
        logger.info('信赖领取完毕')

    def get_building(self):
        logger.debug("helper.get_building")
        logger.info("清空基建")
        self.back_to_main()
        screenshot = self.adb.screenshot()
        logger.info('进入我的基建')
        self.tap_quadrilateral(imgreco.main.get_back_my_build(screenshot))
        self.__wait(MEDIUM_WAIT + 3)
        self.tap_quadrilateral(imgreco.main.get_my_build_task(screenshot))
        self.__wait(SMALL_WAIT)
        logger.info('收取制造产物')
        self.tap_quadrilateral(
            imgreco.main.get_my_build_task_clear(screenshot))
        self.__wait(SMALL_WAIT)
        logger.info('清理贸易订单')
        self.tap_quadrilateral(imgreco.main.get_my_sell_task_1(screenshot))
        self.__wait(SMALL_WAIT + 1)
        self.tap_quadrilateral(imgreco.main.get_my_sell_tasklist(screenshot))
        self.__wait(SMALL_WAIT - 1)
        sell_count = 0
        while sell_count <= 6:
            screenshot = self.adb.screenshot()
            self.tap_quadrilateral(
                imgreco.main.get_my_sell_task_main(screenshot))
            self.__wait(TINY_WAIT)
            sell_count = sell_count + 1
        self.tap_quadrilateral(imgreco.main.get_my_sell_task_2(screenshot))
        self.__wait(SMALL_WAIT - 1)
        sell_count = 0
        while sell_count <= 6:
            screenshot = self.adb.screenshot()
            self.tap_quadrilateral(
                imgreco.main.get_my_sell_task_main(screenshot))
            self.__wait(TINY_WAIT)
            sell_count = sell_count + 1
        self.back_to_main()
        logger.info("基建领取完毕")

    def log_total_loots(self):
        logger.info('目前已获得:%s',
                    ', '.join('%sx%d' % tup for tup in self.loots.items()))
Esempio n. 2
0
class ArknightsHelper(object):
    def __init__(self, adb_host=None, device_connector=None, frontend=None):  # 当前绑定到的设备
        self.adb = None
        if adb_host is not None or device_connector is not None:
            self.connect_device(device_connector, adb_serial=adb_host)
        if frontend is None:
            frontend = DummyFrontend()
            if self.adb is None:
                self.connect_device(auto_connect())
        self.frontend = frontend
        self.frontend.attach(self)
        self.operation_time = []
        if DEBUG_LEVEL >= 1:
            self.__print_info()
        self.refill_with_item = config.get('behavior/refill_ap_with_item', False)
        self.refill_with_originium = config.get('behavior/refill_ap_with_originium', False)
        self.use_refill = self.refill_with_item or self.refill_with_originium
        self.loots = {}
        self.use_penguin_report = config.get('reporting/enabled', False)
        if self.use_penguin_report:
            self.penguin_reporter = penguin_stats.reporter.PenguinStatsReporter()
        self.refill_count = 0
        self.max_refill_count = None

        logger.debug("成功初始化模块")

    def ensure_device_connection(self):
        if self.adb is None:
            raise RuntimeError('not connected to device')

    def connect_device(self, connector=None, *, adb_serial=None):
        if connector is not None:
            self.adb = connector
        elif adb_serial is not None:
            self.adb = ADBConnector(adb_serial)
        else:
            self.adb = None
            return
        self.viewport = self.adb.screen_size
        if self.adb.screenshot_rotate %180:
            self.viewport = (self.viewport[1], self.viewport[0])
        if self.viewport[1] < 720 or Fraction(self.viewport[0], self.viewport[1]) < Fraction(16, 9):
            title = '设备当前分辨率(%dx%d)不符合要求' % (self.viewport[0], self.viewport[1])
            body = '需要宽高比等于或大于 16∶9,且渲染高度不小于 720。'
            details = None
            if Fraction(self.viewport[1], self.viewport[0]) >= Fraction(16, 9):
                body = '屏幕截图可能需要旋转,请尝试在 device-config 中指定旋转角度。'
                img = self.adb.screenshot()
                imgfile = os.path.join(config.SCREEN_SHOOT_SAVE_PATH, 'orientation-diagnose-%s.png' % time.strftime("%Y%m%d-%H%M%S"))
                img.save(imgfile)
                import json
                details = '参考 %s 以更正 device-config.json[%s]["screenshot_rotate"]' % (imgfile, json.dumps(self.adb.config_key))
            for msg in [title, body, details]:
                if msg is not None:
                    logger.warn(msg)
            frontend.alert(title, body, 'warn', details)

    def __print_info(self):
        logger.info('当前系统信息:')
        logger.info('分辨率:\t%dx%d', *self.viewport)
        # logger.info('OCR 引擎:\t%s', ocr.engine.info)
        logger.info('截图路径:\t%s', config.SCREEN_SHOOT_SAVE_PATH)

        if config.enable_baidu_api:
            logger.info('%s',
                        """百度API配置信息:
        APP_ID\t{app_id}
        API_KEY\t{api_key}
        SECRET_KEY\t{secret_key}
                        """.format(
                            app_id=config.APP_ID, api_key=config.API_KEY, secret_key=config.SECRET_KEY
                        )
                        )

    def __del(self):
        self.adb.run_device_cmd("am force-stop {}".format(config.ArkNights_PACKAGE_NAME))

    def destroy(self):
        self.__del()

    def check_game_active(self):  # 启动游戏 需要手动调用
        logger.debug("helper.check_game_active")
        current = self.adb.run_device_cmd('dumpsys window windows | grep mCurrentFocus').decode(errors='ignore')
        logger.debug("正在尝试启动游戏")
        logger.debug(current)
        if config.ArkNights_PACKAGE_NAME in current:
            logger.debug("游戏已启动")
        else:
            self.adb.run_device_cmd(
                "am start -n {}/{}".format(config.ArkNights_PACKAGE_NAME, config.ArkNights_ACTIVITY_NAME))
            logger.debug("成功启动游戏")

    def __wait(self, n=10,  # 等待时间中值
               MANLIKE_FLAG=True, allow_skip=False):  # 是否在此基础上设偏移量
        if MANLIKE_FLAG:
            m = uniform(0, 0.3)
            n = uniform(n - m * 0.5 * n, n + m * n)
        self.frontend.delay(n, allow_skip)

    def mouse_click(self,  # 点击一个按钮
                    XY):  # 待点击的按钮的左上和右下坐标
        assert (self.viewport == (1280, 720))
        logger.debug("helper.mouse_click")
        xx = randint(XY[0][0], XY[1][0])
        yy = randint(XY[0][1], XY[1][1])
        logger.info("接收到点击坐标并传递xx:{}和yy:{}".format(xx, yy))
        self.adb.touch_tap((xx, yy))
        self.__wait(TINY_WAIT, MANLIKE_FLAG=True)

    def tap_rect(self, rc):
        hwidth = (rc[2] - rc[0]) / 2
        hheight = (rc[3] - rc[1]) / 2
        midx = rc[0] + hwidth
        midy = rc[1] + hheight
        xdiff = max(-1, min(1, gauss(0, 0.2)))
        ydiff = max(-1, min(1, gauss(0, 0.2)))
        tapx = int(midx + xdiff * hwidth)
        tapy = int(midy + ydiff * hheight)
        self.adb.touch_tap((tapx, tapy))
        self.__wait(TINY_WAIT, MANLIKE_FLAG=True)

    def tap_quadrilateral(self, pts):
        pts = np.asarray(pts)
        acdiff = max(0, min(2, gauss(1, 0.2)))
        bddiff = max(0, min(2, gauss(1, 0.2)))
        halfac = (pts[2] - pts[0]) / 2
        m = pts[0] + halfac * acdiff
        pt2 = pts[1] if bddiff > 1 else pts[3]
        halfvec = (pt2 - m) / 2
        finalpt = m + halfvec * bddiff
        self.adb.touch_tap(tuple(int(x) for x in finalpt))
        self.__wait(TINY_WAIT, MANLIKE_FLAG=True)

    def wait_for_still_image(self, threshold=16, crop=None, timeout=60, raise_for_timeout=True, check_delay=1):
        if crop is None:
            shooter = lambda: self.adb.screenshot(False)
        else:
            shooter = lambda: self.adb.screenshot(False).crop(crop)
        screenshot = shooter()
        t0 = time.monotonic()
        ts = t0 + timeout
        n = 0
        minerr = 65025
        message_shown = False
        while (t1 := time.monotonic()) < ts:
            if check_delay > 0:
                self.__wait(check_delay, False, True)
            screenshot2 = shooter()
            mse = imgreco.imgops.compare_mse(screenshot, screenshot2)
            if mse <= threshold:
                return screenshot2
            screenshot = screenshot2
            if mse < minerr:
                minerr = mse
            if not message_shown and t1-t0 > 10:
                logger.info("等待画面静止")
        if raise_for_timeout:
            raise RuntimeError("%d 秒内画面未静止,最小误差=%d,阈值=%d" % (timeout, minerr, threshold))
        return None