diff --git a/Honkai_Star_Rail.py b/Honkai_Star_Rail.py index 22268fa2..fcde46d7 100644 --- a/Honkai_Star_Rail.py +++ b/Honkai_Star_Rail.py @@ -293,6 +293,11 @@ def main(self, option:str=_('大世界'),start: str=None,role_list: str=None) -> if option in self.option_list: (start, role_list) = self.choose_map(option) if not start else (start, role_list) if start: + if option == _("遗器模块"): # 遗器模块此时不需要将游戏窗口激活 + log.info(_("遗器模块初始化中...")) + relic = Relic(game_title) + relic.relic_entrance() + return True log.info(_("脚本将自动切换至游戏窗口,请保持游戏窗口激活")) calculated(game_title, start=False).switch_window() time.sleep(0.5) @@ -310,10 +315,6 @@ def main(self, option:str=_('大世界'),start: str=None,role_list: str=None) -> elif option == _("派遣委托"): commission = Commission(4, game_title) commission.start() # 读取配置 - elif option == _("遗器模块"): - relic = Relic(game_title) - relic.relic_entrance() - return True else: raise Exception(role_list) else: diff --git a/model/cnocr_for_relic/2.2/densenet_lite_114-fc/cnocr-v2.2-densenet_lite_114-fc-epoch=037-complete_match_epoch=0.7859-model.onnx b/model/cnocr_for_relic/2.2/densenet_lite_114-fc/cnocr-v2.2-densenet_lite_114-fc-epoch=037-complete_match_epoch=0.7859-model.onnx new file mode 100644 index 00000000..03db25eb Binary files /dev/null and b/model/cnocr_for_relic/2.2/densenet_lite_114-fc/cnocr-v2.2-densenet_lite_114-fc-epoch=037-complete_match_epoch=0.7859-model.onnx differ diff --git a/model/cnocr_for_relic/2.2/ppocr/en_number_mobile_v2.0_rec_infer.onnx b/model/cnocr_for_relic/2.2/ppocr/en_number_mobile_v2.0_rec_infer.onnx new file mode 100644 index 00000000..a469d084 Binary files /dev/null and b/model/cnocr_for_relic/2.2/ppocr/en_number_mobile_v2.0_rec_infer.onnx differ diff --git a/requirements.txt b/requirements.txt index 8d0d8996..9922074b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,5 +29,4 @@ cryptography pluggy httpcore pydantic -pprint jsonschema diff --git a/utils/calculated.py b/utils/calculated.py index c9656456..f85510f3 100644 --- a/utils/calculated.py +++ b/utils/calculated.py @@ -32,7 +32,7 @@ class calculated(CV_Tools): - def __init__(self, title=_("崩坏:星穹铁道"), det_model_name="ch_PP-OCRv3_det", rec_model_name= "densenet_lite_114-fc", number=False, start=True): + def __init__(self, title=_("崩坏:星穹铁道"), det_model_name="ch_PP-OCRv3_det", rec_model_name= "densenet_lite_114-fc", det_root="model/cnstd", rec_root="model/cnocr", number=False, start=True): """ 参数: :param det_model_name: 文字定位模型 @@ -56,9 +56,9 @@ def __init__(self, title=_("崩坏:星穹铁道"), det_model_name="ch_PP-OCRv3 dir = sys._MEIPASS else: dir = Path() - det_root, rec_root = os.path.join(dir, "model/cnstd"), os.path.join(dir, "model/cnocr") + det_root, rec_root = os.path.join(dir, det_root), os.path.join(dir, rec_root) self.ocr = CnOcr(det_model_name=det_model_name, rec_model_name=rec_model_name, rec_vocab_fp="model/cnocr/label_cn.txt", det_root=det_root, rec_root=rec_root) if not number else CnOcr(det_model_name=det_model_name, rec_model_name=rec_model_name,det_root="./model/cnstd", rec_root="./model/cnocr", cand_alphabet='0123456789') - self.number_ocr = CnOcr(det_model_name=det_model_name, rec_model_name=rec_model_name, det_root=det_root, rec_root=rec_root, cand_alphabet='0123456789.+%') + self.number_ocr = CnOcr(det_model_name=det_model_name, rec_model_name="en_number_mobile_v2.0", det_root=det_root, rec_root=rec_root, cand_alphabet='0123456789.+%') #self.ocr = CnOcr(det_model_name='db_resnet34', rec_model_name='densenet_lite_114-fc') self.check_list = lambda x,y: re.match(x, str(y)) != None self.compare_lists = lambda a, b: all(x <= y for x, y in zip(a, b)) @@ -109,14 +109,14 @@ def click(self, points = None, click_time=0.5): time.sleep(click_time) self.mouse.release(mouse.Button.left) - def appoint_click(self, points, appoint_points, hsv = [18, 18, 18]): + def appoint_click(self, points, appoint_points, rgb = [18, 18, 18]): """ 说明: 点击坐标直到指定指定点位变成指定颜色 参数: :param points: 坐标 :param appoint_points: 指定坐标 - :param hsv: 三色值 + :param rgb: 三色值 """ start_time = time.time() while True: @@ -126,9 +126,9 @@ def appoint_click(self, points, appoint_points, hsv = [18, 18, 18]): self.mouse.press(mouse.Button.left) time.sleep(0.5) self.mouse.release(mouse.Button.left) - result = self.get_pix_r(appoint_points) + result = self.get_pix_rgb(appoint_points) log.debug(result) - if result == hsv: + if result == rgb: break if time.time() - start_time > 5: log.info(_(_("识别超时"))) @@ -441,7 +441,7 @@ def fighting(self): time.sleep(0.1) if self.has_red((4, 7, 10, 19)): while True: - result = self.get_pix_rgb(pos=(40, 62)) + result = self.get_pix_hsv(game_pos=(40, 62)) log.debug(f"进入战斗取色: {result}") if self.compare_lists([0, 0, 222], result) and self.compare_lists(result, [0, 0, 255]): self.click() @@ -454,7 +454,7 @@ def fighting(self): self.wait_fight_end() return True time.sleep(0.2) - result = self.get_pix_rgb(pos=(40, 62)) + result = self.get_pix_hsv(game_pos=(40, 62)) log.debug(f"进入战斗取色: {result}") if not (self.compare_lists([0, 0, 225], result) and self.compare_lists(result, [0, 0, 255])): self.wait_fight_end() # 无论是否识别到敌人都判断是否结束战斗,反正怪物袭击 @@ -463,10 +463,10 @@ def fighting(self): def check_fighting(self): while True: if ( - self.compare_lists([0, 0, 222], self.get_pix_rgb(pos=(1435, 58))) and - self.compare_lists(self.get_pix_rgb(pos=(1435, 58)), [0, 0, 240]) and - self.compare_lists([20, 90, 80], self.get_pix_rgb(pos=(88, 979))) and - self.compare_lists(self.get_pix_rgb(pos=(88, 979)), [25, 100, 90]) + self.compare_lists([0, 0, 222], self.get_pix_hsv(game_pos=(1435, 58))) and + self.compare_lists(self.get_pix_hsv(game_pos=(1435, 58)), [0, 0, 240]) and + self.compare_lists([20, 90, 80], self.get_pix_hsv(game_pos=(88, 979))) and + self.compare_lists(self.get_pix_hsv(game_pos=(88, 979)), [25, 100, 90]) ): log.info(_("未在战斗状态")) break @@ -502,10 +502,10 @@ def wait_fight_end(self, type=0): while True: if type == 0: if ( - self.compare_lists([0, 0, 222], self.get_pix_rgb(pos=(1435, 58))) and - self.compare_lists(self.get_pix_rgb(pos=(1435, 58)), [0, 0, 240]) and - self.compare_lists([20, 90, 80], self.get_pix_rgb(pos=(88, 979))) and - self.compare_lists(self.get_pix_rgb(pos=(88, 979)), [25, 100, 90]) + self.compare_lists([0, 0, 222], self.get_pix_hsv(game_pos=(1435, 58))) and + self.compare_lists(self.get_pix_hsv(game_pos=(1435, 58)), [0, 0, 240]) and + self.compare_lists([20, 90, 80], self.get_pix_hsv(game_pos=(88, 979))) and + self.compare_lists(self.get_pix_hsv(game_pos=(88, 979)), [25, 100, 90]) ): log.info(_("完成自动战斗")) break @@ -574,7 +574,7 @@ def move_com(self, com, sleep_time=1): self.keyboard.press(com) start_time = time.perf_counter() if sra_config_obj.sprint: - result = self.get_pix_r(pos=(1712, 958)) + result = self.get_pix_rgb(game_pos=(1712, 958)) if (self.compare_lists(result, [130, 160, 180]) or self.compare_lists([200, 200, 200], result)): time.sleep(0.05) log.info("疾跑") @@ -625,7 +625,7 @@ def ocr_pos(self, characters:str = None, points = (0,0,0,0)): pos = data[characters] if characters in data else None return characters, pos - def ocr_pos_for_singleLine(self, characters_list:list[str] = None, points = (0,0,0,0), number = False, debug = False, img_pk:tuple = None) -> Union[int, str]: + def ocr_pos_for_single_line(self, characters_list:list[str] = None, points = (0,0,0,0), number = False, debug = False, img_pk:tuple = None) -> Union[int, str]: """ 说明: 获取指定坐标的单行文字 @@ -699,18 +699,40 @@ def part_ocr(self, points = (0,0,0,0), debug=False, left=False, number = False, log.info(data) # show_img(img_fp) timestamp_str = str(int(datetime.timestamp(datetime.now()))) - cv.imwrite(f"log/image/relic_{str(points)}_{timestamp_str}.png", img_fp) + cv.imwrite(f"logs/image/relic_{str(points)}_{timestamp_str}.png", img_fp) else: log.debug(data) return data + + def get_relative_pix_rgb(self, game_pos: Union[tuple, None]=None, points: tuple=(0, 0, 0, 0)): + """ + 说明: + 获取相对坐标的BGR颜色 + 参数: + :param game_pos: 游戏图片的相对坐标 + 返回: + :return rgb: 颜色 + """ + return self.get_pix_rgb(game_pos=self.rp2ap(game_pos), points=points) + + def get_relative_pix_hsv(self, game_pos: Union[tuple, None]=None, points: tuple=(0, 0, 0, 0)): + """ + 说明: + 获取相对坐标的HSV颜色 + 参数: + :param game_pos: 游戏图片的相对坐标 + 返回: + :return hsv: 颜色 + """ + return self.get_pix_hsv(game_pos=self.rp2ap(game_pos), points=points) - def get_pix_r(self, desktop_pos: Union[tuple, None]=None, pos: Union[tuple, None]=None, points: tuple=(0, 0, 0, 0)): + def get_pix_rgb(self, desktop_pos: Union[tuple, None]=None, game_pos: Union[tuple, None]=None, points: tuple=(0, 0, 0, 0)): """ 说明: - 获取指定坐标的颜色 + 获取绝对坐标的BGR颜色 参数: :param desktop_pos: 包含桌面的坐标 - :param pos: 图片的坐标 + :param game_pos: 游戏图片的绝对坐标 返回: :return rgb: 颜色 """ @@ -719,38 +741,38 @@ def get_pix_r(self, desktop_pos: Union[tuple, None]=None, pos: Union[tuple, None if desktop_pos: x = int(desktop_pos[0])-int(left) y = int(desktop_pos[1])-int(top) - elif pos: - x = int(pos[0]) - y = int(pos[1]) + elif game_pos: + x = int(game_pos[0]) + y = int(game_pos[1]) rgb = img[y, x] blue = img[y, x, 0] green = img[y, x, 1] red = img[y, x, 2] return [blue,green,red] - def get_pix_rgb(self, desktop_pos: Union[tuple, None]=None, pos: Union[tuple, None]=None, points: tuple=(0, 0, 0, 0)): + def get_pix_hsv(self, desktop_pos: Union[tuple, None]=None, game_pos: Union[tuple, None]=None, points: tuple=(0, 0, 0, 0)): """ 说明: - 获取指定坐标的颜色 + 获取绝对坐标的HSV颜色 参数: :param desktop_pos: 包含桌面的坐标 - :param pos: 图片的坐标 + :param game_pos: 游戏图片的绝对坐标 返回: - :return rgb: 颜色 + :return hsv: 颜色 """ img, left, top, __, __, __, __ = self.take_screenshot(points) HSV=cv.cvtColor(img,cv.COLOR_BGR2HSV) if desktop_pos: x = int(desktop_pos[0])-int(left) y = int(desktop_pos[1])-int(top) - elif pos: - x = int(pos[0]) - y = int(pos[1]) - rgb = HSV[y, x] - blue = HSV[y, x, 0] - green = HSV[y, x, 1] - red = HSV[y, x, 2] - return [blue,green,red] + elif game_pos: + x = int(game_pos[0]) + y = int(game_pos[1]) + hsv = HSV[y, x] + hue = HSV[y, x, 0] # 色相 + satu = HSV[y, x, 1] # 饱和度 + value = HSV[y, x, 2] # 明度 + return [hue,satu,value] def hsv2pos(self, img, color, tolerance = 0): """ @@ -811,7 +833,7 @@ def wait_join(self): join_time = sra_config_obj.join_time while True: ''' - result = self.get_pix_r(pos=(960, 86)) + result = self.get_pix_rgb(pos=(960, 86)) log.info(result) endtime = time.time() - start_time if self.compare_lists([222, 222, 116], result): @@ -828,7 +850,7 @@ def wait_join(self): return endtime ''' endtime = time.time() - start_time - result = self.get_pix_rgb(pos=(40, 62)) + result = self.get_pix_hsv(game_pos=(40, 62)) log.debug(result) if self.compare_lists([0, 0, 222], result): log.info(_("已进入地图")) @@ -855,6 +877,12 @@ def switch_window(self, dt=0.1): log.info(_('没找到窗口{title}').format(title=self.title)) time.sleep(dt) + def switch_cmd(self, dt=0.1): + time.sleep(dt) + log.debug(self.cmd.title) + self.cmd.activate() + time.sleep(dt) + def open_map(self, open_key): while True: self.keyboard.press(open_key) @@ -879,10 +907,10 @@ def teleport(self, key, value, threshold=0.95): time.sleep(0.3) # 缓冲 while True: if ( - self.compare_lists([0, 0, 222], self.get_pix_rgb(pos=(1435, 58))) and - self.compare_lists(self.get_pix_rgb(pos=(1435, 58)), [0, 0, 240]) and - self.compare_lists([20, 90, 80], self.get_pix_rgb(pos=(88, 979))) and - self.compare_lists(self.get_pix_rgb(pos=(88, 979)), [25, 100, 90]) + self.compare_lists([0, 0, 222], self.get_pix_hsv(game_pos=(1435, 58))) and + self.compare_lists(self.get_pix_hsv(game_pos=(1435, 58)), [0, 0, 240]) and + self.compare_lists([20, 90, 80], self.get_pix_hsv(game_pos=(88, 979))) and + self.compare_lists(self.get_pix_hsv(game_pos=(88, 979)), [25, 100, 90]) ): log.info(_("完成入画")) break @@ -961,12 +989,58 @@ def change_team(self): else: return False - def get_data_hash(self, data) -> str: + +class Array2dict: + + def __init__(self, arr:np.ndarray, key_index:int = -1, value_index:int = None): """ 说明: - 求任意类型数据 (包括list和dict) 的哈希值 - 首先将数据规范化输出为str,再计算md5转16进制 + 将np数组转化为字典暂住内存,用于对数组短时间内的频繁查找 + 参数: + :param arr: 二维数组 + :param key_index: 待查找的关键字所在的数组列标 + :param value_index: 待查找的数值所在的数组列标 (为空时表示查找关键字的行标) """ - # pprint默认sort_dicts=True,对键值进行排序,以确保字典类型数据的唯一性 - return hashlib.md5(pprint.pformat(data).encode('utf-8')).hexdigest() + if arr.ndim != 2: + raise ValueError("输入的数组必须为二维数组") + # 将np数组转化为字典 + if value_index is None: # 默认将key的行标作为value,以取代np.where + self.data_dict = {row[key_index]: idx for idx, row in enumerate(arr)} + else: + self.data_dict = {row[key_index]: row[value_index] for row in arr} + log.debug(self.data_dict) + + def __getitem__(self, key): + return self.data_dict[key] + +def get_data_hash(data:Any, key_filter:list[str] = None) -> str: + """ + 说明: + 求任意类型数据 (包括list和dict) 的哈希值 + 首先将数据规范化输出为str,再计算md5转16进制 + 参数: + :param data: 任意类型数据 + :param key_filter: 键值过滤器 + """ + if not key_filter: + tmp_data = data + elif type(data) is dict: + tmp_data = dict(data).copy() # 注意dict'='为引用传递,此处需拷贝副本 + [tmp_data.pop(key) if key in tmp_data else None for key in key_filter] + else: + log.eror(_("不支持dict以外类型的类型使用键值过滤器")) + return None + # pprint默认sort_dicts=True,对键值进行排序,以确保字典类型的唯一性 + return hashlib.md5(pprint.pformat(tmp_data).encode('utf-8')).hexdigest() + +def str_just(text:str, width:int, left = True): + """ + 说明: + 封装str.rjust()&str.ljust(),以适应中文字符的实际宽度 + """ + ch_cnt = (len(text.encode('utf-8')) - len(text)) // 2 # 中文字符的个数 + if left: + return text.ljust(width-ch_cnt) + else: + return text.rjust(width-ch_cnt) \ No newline at end of file diff --git a/utils/config.py b/utils/config.py index 1ea19602..105acfd7 100644 --- a/utils/config.py +++ b/utils/config.py @@ -17,6 +17,7 @@ LOADOUT_FILE_NAME = "relics_loadout.json" TEAM_FILE_NAME = "relics_team.json" + def normalize_file_path(filename): # 尝试在当前目录下读取文件 current_dir = os.getcwd() @@ -74,34 +75,46 @@ def init_json_file(filename: str): 参数: :param filename: 文件名称 """ - with open(filename, "wb+") as f: - log.info(_(f"{filename} 文件初始化")) - f.write( - orjson.dumps( - {},option = orjson.OPT_PASSTHROUGH_DATETIME | orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_INDENT_2 - ) - ) + with open(filename, "wb") as f: + log.info(_(f"{filename} 文件初始化")) + f.write(orjson.dumps({}, option = orjson.OPT_PASSTHROUGH_DATETIME | orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_INDENT_2)) -def modify_json_file(filename: str, key: str, value) -> dict: +def modify_json_file(filename:str, key:str, value:Any) -> dict: """ 说明: - 写入文件,并返回文件字典 + 将键值对写入json文件,并返回写入后的字典 参数: :param filename: 文件名称 :param key: key :param value: value + 返回: + data: 修改后的json字典 """ # 先读,再写 data, file_path = read_json_file(filename, path=True) data[key] = value + return rewrite_json_file(file_path, data) + + +def rewrite_json_file(filename:str, data:dict) -> dict: + """ + 说明: + 重写整个json文件 + 参数: + :param filename: 文件名称 + :param data: json的完整字典 + 返回: + data: 修改后的json字典 + """ + file_path = normalize_file_path(filename) try: with open(file_path, "wb") as f: f.write(orjson.dumps(data, option=orjson.OPT_PASSTHROUGH_DATETIME | orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_INDENT_2)) except PermissionError as e: import time time.sleep(1) - return modify_json_file(filename, key, value) + return rewrite_json_file(filename, data) return data @@ -320,6 +333,13 @@ class SRAData(metaclass=SRADataMeta): """切换队伍的队伍编号""" stop: bool = False """是否停止""" + fuzzy_match_for_relic: bool = True + """是否在遗器搜索时开启模糊匹配""" + check_stats_for_relic: bool = True + """是否在遗器OCR时开启对副词条的数据验证""" + detail_for_relic: bool = True + """是否在打印遗器信息时显示拓展信息""" + def __init__(self) -> None: ... diff --git a/utils/cv_tools.py b/utils/cv_tools.py index 1a3a8c2c..06791034 100644 --- a/utils/cv_tools.py +++ b/utils/cv_tools.py @@ -36,11 +36,12 @@ def show_imgs(imgs, title='Image'): class CV_Tools: def __init__(self, title=_("崩坏:星穹铁道")): + self.cmd = pwc.getActiveWindow() self.window = pwc.getWindowsWithTitle(title) if not self.window: raise Exception(_("你游戏没开,我真服了")) self.window = self.window[0] - self.window.activate() # 将游戏调至前台 + # self.window.activate() # 将游戏调至前台 self.hwnd = self.window._hWnd def take_screenshot(self,points=(0,0,0,0),sleep = 3): diff --git a/utils/map.py b/utils/map.py index 625bac59..82c034d3 100644 --- a/utils/map.py +++ b/utils/map.py @@ -1,9 +1,11 @@ -import threading +import os import time - +import threading +import cv2 as cv from pynput import keyboard +from datetime import datetime -from .calculated import * +from .calculated import calculated from .config import (CONFIG_FILE_NAME, _, get_file, insert_key, read_json_file, read_maps, sra_config_obj) from .log import fight_log, log, set_log diff --git a/utils/relic.py b/utils/relic.py index 5b2432ac..f61a13f4 100644 --- a/utils/relic.py +++ b/utils/relic.py @@ -1,173 +1,217 @@ +import re +import time import math import pprint import questionary import numpy as np -from .calculated import * -from .config import read_json_file, modify_json_file, RELIC_FILE_NAME, LOADOUT_FILE_NAME, TEAM_FILE_NAME, _ +from collections import Counter + +from .calculated import calculated, Array2dict, get_data_hash, str_just +from .config import (read_json_file, modify_json_file, rewrite_json_file, + RELIC_FILE_NAME, LOADOUT_FILE_NAME, TEAM_FILE_NAME, _, sra_config_obj) from .exceptions import Exception, RelicOCRException from .log import log pp = pprint.PrettyPrinter(indent=1, width=40, sort_dicts=False) IS_PC = True # paltform flag (同时保存了模拟器与PC的识别位点) class Relic: - def __init__(self, title=_("崩坏:星穹铁道")): - if sra_config_obj.language != "zh_CN": - raise Exception(_("暂不支持简体中文之外的语言")) - self.calculated = calculated(title) + """ + <<<遗器模块>>> + 已完成功能: + 1.识别遗器数据 (单次用时约0.5s) + a.[新增]支持所有稀有度遗器 (识别指定点位色相[黄,紫,蓝,绿]) + 2.保存人物配装 + 3.读取人物配装并装备 (遗器将强制替换,支持精确匹配与模糊匹配) + 4.[新增]兼容四星遗器: + a.兼容校验函数 (增加四星遗器副词条档位数据) + 5.[新增]模糊匹配成功后自动更新相关数据库 (同时会在新旧遗器间建立后继关系) + 6.[新增]在配装选择界面,打印配装的简要信息 + 待解决问题: + 1.[已解决]OCR准确率低: + 对于中文识别更换为项目早期的OCR模型;对于数字识别更换为仅包含英文数字的轻量模型 + 待开发功能: + 1.保存队伍配装 + 2.读取队伍配装并装备 + 3.遗器管理 + a.在模块入口成功识别当前遗器后,可选择进行数据录入、查询可能的遗器历史数据 (基于模糊匹配) + b.美化遗器打印 + 4.配装管理 + a.可选择对配装重命名 + ... + 相关说明: + 1.[新增]本模块的所有识别位点均采用百分比相对坐标,以兼容不同平台支持不同分辨率 + 2.[新增]本模块首先会基于安卓模拟器进行测试,再基于PC端测试 + """ + # 静态参数 + equip_set_name = [_("头部"), _("手部"), _("躯干"), _("脚部"), _("位面球"), _("连结绳")] + """遗器部位名称,已经按游戏界面顺序排序""" + equip_set_abbr = [_("头"), _("手"), _("衣"), _("鞋"), _("球"), _("绳")] + """遗器部位简称""" + relic_set_name = np.array([ # 注:因为数据有时要行取有时要列取,故采用数组存储 + [_("过客"), _("过客"), _("治疗"), _("云无留迹的过客")], + [_("枪手"), _("枪手"), _("快枪手"), _("野穗伴行的快枪手")], + [_("圣骑"), _("圣骑"), _("防御"), _("净庭教宗的圣骑士")], + [_("雪猎"), _("猎人"), _("冰套"), _("密林卧雪的猎人")], + [_("拳王"), _("拳王"), _("物理"), _("街头出身的拳王")], + [_("铁卫"), _("铁卫"), _("减伤"), _("戍卫风雪的铁卫")], + [_("火匠"), _("火匠"), _("火套"), _("熔岩锻铸的火匠")], + [_("天才"), _("天才"), _("量子"), _("繁星璀璨的天才")], + [_("乐队"), _("雷电"), _("雷套"), _("激奏雷电的乐队")], + [_("翔"), _("翔"), _("风套"), _("晨昏交界的翔鹰")], + [_("怪盗"), _("怪盗"), _("怪盗"), _("流星追迹的怪盗")], + [_("废"), _("废"), _("虚数"), _("盗匪荒漠的废土客")], + [_("者"), _("长存"), _("莳者"), _("宝命长存的莳者")], + [_("信使"), _("信使"), _("速度"), _("骇域漫游的信使")], + [_("黑塔"), _("太空"), _("空间站"), _("太空封印站")], + [_("仙"), _("仙"), _("仙舟"), _("不老者的仙舟")], + [_("公司"), _("公司"), _("命中"), _("泛银河商业公司")], + [_("贝洛"), _("贝洛"), _("防御"), _("筑城者的贝洛伯格")], + [_("螺丝"), _("差分"), _("差分"), _("星体差分机")], + [_("萨尔"), _("停转"), _("停转"), _("停转的萨尔索图")], + [_("利亚"), _("盗贼"), _("击破"), _("盗贼公国塔利亚")], + [_("瓦克"), _("瓦克"), _("翁瓦克"), _("生命的翁瓦克")], + [_("泰科"), _("繁星"), _("繁星"), _("繁星竞技场")], + [_("伊须"), _("龙骨"), _("龙骨"), _("折断的龙骨")] + ], dtype=np.str_) + """遗器套装名称:0-套装散件名的共有词(ocr-必须),1-套装名的特异词(ocr-可选,为了增强鲁棒性),2-玩家惯用简称(print),3-套装全称(json),已按[1.4游戏]遗器筛选界面排序""" + stats_name = np.array([ + [_("命值"), _("生"), _("生命值")], [_("击力"), _("攻"), _("攻击力")], [_("防御"), _("防"), _("防御力")], + [_("命值"), _("生"), _("生命值%")], [_("击力"), _("攻"), _("攻击力%")], [_("防御"), _("防"), _("防御力%")], + [_("度"), _("速"), _("速度")], [_("击率"), _("暴击"), _("暴击率")], [_("击伤"), _("爆伤"), _("暴击伤害")], [_("命中"), _("命中"), _("效果命中")], [_("治疗"), _("治疗"), _("治疗量加成")], + [_("理"), _("伤害"), _("物理属性伤害")], [_("火"), _("火伤"), _("火属性伤害")], [_("冰"), _("冰伤"), _("冰属性伤害")], [_("雷"), _("雷伤"), _("雷属性伤害")], [_("风"), _("风伤"), _("风属性伤害")], + [_("量"), _("量子"), _("量子属性伤害")], [_("数"), _("虚数"), _("虚数属性伤害")], + [_("抵抗"), _("效果抵抗"), _("效果抵抗")], [_("破"), _("击破"), _("击破特攻")], [_("恢复"), _("能"), _("能量恢复效率")] + ], dtype=np.str_) + """遗器属性名称:0-属性名的特异词(ocr-不区分大小词条),1-玩家惯用简称(print),2-属性全称(json-区分大小词条)""" + not_pre_stats = [_("生命值"), _("攻击力"), _("防御力"), _("速度")] + """遗器的整数属性名称""" + base_stats_name = np.concatenate((stats_name[:2],stats_name[3:-3],stats_name[-2:]), axis=0) + """遗器主属性名称""" + base_stats_name4equip = [base_stats_name[0:1], + base_stats_name[1:2], + np.vstack((base_stats_name[2:5],base_stats_name[6:10])), + base_stats_name[2:6], + np.vstack((base_stats_name[2:5],base_stats_name[10:17])), + np.vstack((base_stats_name[2:5],base_stats_name[-2:]))] + """遗器各部位主属性名称""" + subs_stats_name = np.vstack((stats_name[:10],stats_name[-3:-1])) + """遗器副属性名称,已按副词条顺序排序""" + subs_stats_tier = [ + [(27.096, 3.3870 ), (13.548 , 1.6935 ), (13.548 , 1.6935 ), (2.7648, 0.3456), (2.7648, 0.3456), (3.456, 0.4320), # 四星遗器数值 + (1.60, 0.20), (2.0736, 0.2592), (4.1472, 0.5184), (2.7648, 0.3456), (2.7648, 0.3456), (4.1472, 0.5184)], + [(33.870, 4.233755), (16.935 , 2.116877), (16.935 , 2.116877), (3.4560, 0.4320), (3.4560, 0.4320), (4.320, 0.5400), # 五星遗器数值 + (2.00, 0.30), (2.5920, 0.3240), (5.1840, 0.6480), (3.4560, 0.4320), (3.4560, 0.4320), (5.1840, 0.6480)]] + """副属性词条档位:t0-基础值,t1-每提升一档的数值;l1-四星遗器数值,l2-五星遗器数值 <<数据来源:米游社@666bj>>""" - # 部位,已经按页面顺序排序 - self.equip_set_name = [_("头部"), _("手部"), _("躯干"), _("脚部"), _("位面球"), _("连结绳") - ] - # 套装:np,0-套装散件的共有词(ocr-必须),1-套装简称(ocr-可选,为了增强鲁棒性),2-套装全称(json),已按遗器筛选页面排序 - self.relic_set_name = np.array([ # 注:因为数据有时要行取有时要列取,故采用数组存储 - [_("过客"), _("过客"), _("云无留迹的过客")], - [_("枪手"), _("枪手"), _("野穗伴行的快枪手")], - [_("圣骑"), _("圣骑"), _("净庭教宗的圣骑士")], - [_("雪猎"), _("猎人"), _("密林卧雪的猎人")], - [_("拳王"), _("拳王"), _("街头出身的拳王")], - [_("铁卫"), _("铁卫"), _("戍卫风雪的铁卫")], - [_("火匠"), _("火匠"), _("熔岩锻铸的火匠")], - [_("天才"), _("天才"), _("繁星璀璨的天才")], - [_("乐队"), _("雷电"), _("激奏雷电的乐队")], - [_("翔"), _("翔"), _("晨昏交界的翔鹰")], - [_("怪盗"), _("怪盗"), _("流星追迹的怪盗")], - [_("废"), _("废"), _("盗匪荒漠的废土客")], - [_("黑塔"), _("太空"), _("太空封印站")], - [_("仙"), _("仙"), _("不老者的仙舟")], - [_("公司"), _("公司"), _("泛银河商业公司")], - [_("贝洛"), _("贝洛"), _("筑城者的贝洛伯格")], - [_("螺丝"), _("差分"), _("星体差分机")], - [_("萨尔"), _("停转"), _("停转的萨尔索图")], - [_("利亚"), _("盗贼"), _("盗贼公国塔利亚")], - [_("瓦克"), _("瓦克"), _("生命的翁瓦克")], - [_("泰科"), _("繁星"), _("繁星竞技场")], - [_("伊须"), _("龙骨"), _("折断的龙骨")], - [_("者"), _("长存"), _("宝命长存的莳者")], - [_("信使"), _("信使"), _("骇域漫游的信使")] - ], dtype=np.str_) - # 属性:np,0-属性简称(ocr-不区分大小词条),1-属性全称(json-区分大小词条) - self.stats_name = np.array([ - [_("命值"), _("生命值")], [_("击力"), _("攻击力")], [_("防御"), _("防御力")], - [_("命值"), _("生命值%")], [_("击力"), _("攻击力%")], [_("防御"), _("防御力%")], - [_("度"), _("速度")], [_("击率"), _("暴击率")], [_("击伤"), _("暴击伤害")], [_("命中"), _("效果命中")], [_("治疗"), _("治疗量加成")], - [_("理"), _("物理属性伤害")], [_("火"), _("火属性伤害")], [_("水"), _("冰属性伤害")], [_("雷"), _("雷属性伤害")], [_("风"), _("风属性伤害")], - [_("量"), _("量子属性伤害")], [_("数"), _("虚数属性伤害")], - [_("抵抗"), _("效果抵抗")], [_("破"), _("击破特攻")], [_("恢复"), _("能量恢复效率")] - ], dtype=np.str_) - # 整数属性 - self.not_pre_stats = [_("生命值"), _("攻击力"), _("防御力"), _("速度")] - # 主属性 - self.base_stats_name = np.concatenate((self.stats_name[:2],self.stats_name[3:-3],self.stats_name[-2:]), axis=0) - # 各部位主属性:list[np] - self.base_stats_name4equip = [self.base_stats_name[0:1], - self.base_stats_name[1:2], - np.vstack((self.base_stats_name[2:5],self.base_stats_name[6:10])), - self.base_stats_name[2:6], - np.vstack((self.base_stats_name[2:5],self.base_stats_name[10:17])), - np.vstack((self.base_stats_name[2:5],self.base_stats_name[-2:]))] - # 副属性,已按副词条顺序排序 - self.subs_stats_name = np.vstack((self.stats_name[:10],self.stats_name[-3:-1])) - # 副属性词条挡位: 0-基础值,1-每提升一档的数值 - self.subs_stats_tier = [(33.870, 4.234), (16.935, 2.117), (16.935, 2.117), (3.456, 0.432), (3.456, 0.432), (4.320, 0.540), - (2.0, 0.3), (2.592, 0.324), (5.184, 0.648), (3.456, 0.432), (3.456, 0.432), (5.184, 0.648)] - # json数据格式规范 - self.relics_schema = { + # json数据格式规范 + relics_schema = { # 遗器数据集 + "type": "object", + "additionalProperties": { # [主键]遗器哈希值 (由其键值遗器数据自动生成) "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "equip_set": { - "type": "string", - "enum": self.equip_set_name - }, - "relic_set": { - "type": "string", - "enum": self.relic_set_name[:, -1].tolist() - }, - "rarity": {"type": "integer"}, - "level": {"type": "integer"}, - "base_stats": { - "type": "object", - "minProperties": 1, - "maxProperties": 1, - "properties": { - key: {"type": "number"} for key in self.base_stats_name[:, -1]}, - "additionalProperties": False - }, - "subs_stats": { - "type": "object", - "minProperties": 2, - "maxProperties": 4, - "properties": { - key: {"type": "number"} for key in self.subs_stats_name[:, -1]}, - "additionalProperties": False - } - }, - "required": ["relic_set", "equip_set", "level", "base_stats", "subs_stats"], - "additionalProperties": False - }} - self.loadout_schema = { + "properties": { + "equip_set": { # 遗器部位 + "type": "string", + "enum": equip_set_name }, + "relic_set": { # 遗器套装 + "type": "string", + "enum": relic_set_name[:, -1].tolist() }, + "rarity": { # 遗器稀有度 (2-5星) + "type": "integer", + "minimum": 2, + "maximum": 5 }, + "level": { # 遗器等级 (0-15级) + "type": "integer", + "minimum": 0, + "maximum": 15 }, + "base_stats": { # 遗器主属性 (词条数为 1) + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "properties": { + key: {"type": "number"} for key in base_stats_name[:, -1]}, + "additionalProperties": False }, + "subs_stats": { # 遗器副属性 (词条数为 1-4) + "type": "object", + "minProperties": 1, + "maxProperties": 4, + "properties": { + key: {"type": "number"} for key in subs_stats_name[:, -1]}, + "additionalProperties": False }, + "pre_ver_hash": {"type": "string"} # [外键]本次升级前的版本 + }, + "required": ["relic_set", "equip_set", "rarity", "level", "base_stats", "subs_stats"], # 需包含遗器的全部固有属性 + "additionalProperties": False + }} + loadout_schema = { # 人物遗器配装数据集 + "type": "object", + "additionalProperties": { # [主键]人物名称 (以OCR结果为准) "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "array", - "minItems": 6, - "maxItems": 6, - "items": {"type": "string"} - }}} - self.team_schema = { + "additionalProperties": { # [次主键]配装名称 (自定义) + "type": "array", # 配装组成 (6件遗器,按部位排序) + "minItems": 6, + "maxItems": 6, + "items": {"type": "string"} # [外键]遗器哈希值 + }}} + team_schema = { # 队伍遗器配装数据集 + "type": "object", + "additionalProperties": { # [主键]队伍名称 (自定义) "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "minProperties": 1, - "maxProperties": 4 - }} + "additionalProperties": { # [外键]队伍成员名称 (以OCR结果为准) + "type": "string" # [外键]各队伍成员的配装名称 + }, + "minProperties": 1, + "maxProperties": 4 + }} + relic_data_filter = ["pre_ver_hash"] + """遗器数据过滤器""" + + def __init__(self, title=_("崩坏:星穹铁道")): + """ + 说明: + 初始化,载入遗器数据并校验 + """ + if sra_config_obj.language != "zh_CN": + raise Exception(_("暂不支持简体中文之外的语言")) + self.calculated = calculated(title, rec_root="model/cnocr_for_relic") + + self.is_fuzzy_match = sra_config_obj.fuzzy_match_for_relic + """是否在遗器搜索时开启模糊匹配""" + self.is_check_stats = sra_config_obj.check_stats_for_relic + """是否在遗器OCR时开启对副词条的数据验证 (关闭后,会将is_detail强制关闭)""" + self.is_detail = sra_config_obj.detail_for_relic and self.is_check_stats + """是否在打印遗器信息时显示详细信息 (如各副词条的强化次数、档位积分,以及提高原数据的小数精度)""" + # 读取json文件,仅初始化时检查格式规范 self.relics_data = read_json_file(RELIC_FILE_NAME, schema=self.relics_schema) self.loadout_data = read_json_file(LOADOUT_FILE_NAME, schema=self.loadout_schema) self.team_data = read_json_file(TEAM_FILE_NAME, schema=self.team_schema) log.info(_("遗器数据载入完成")) + log.info(_(f"共载入 {len(list(self.relics_data.keys()))} 件遗器数据")) - self.is_fuzzy_match = True # 在搜索时开启遗器数据模糊匹配 - self.is_check = True # 是否对副词条数据进行校验 (关闭后,可临时使程序能够识别五星以下遗器,同时会将数据增强强制关闭) - self.is_detail = True # 在打印遗器信息时进行数据增强,显示拓展信息 - self.is_detail = self.is_detail if self.is_check else False + # 校验遗器哈希值 + if not self.check_relic_data_hash(): + option = questionary.select(_("是否依据当前遗器数据更新哈希值:"), [_("是"), _("否")]).ask() + if option == _("是"): + self.check_relic_data_hash(updata=True) def relic_entrance(self): """ 说明: 遗器模块入口 - 已完成功能: - 1.识别遗器数据 (可打印增强信息,目前仅支持五星遗器) - 2.保存人物配装 - 3.读取人物配装并装备 (遗器将强制替换,支持模糊匹配) - 待解决问题: - 1.OCR准确率过低 (对模型进行重训练) - (碎碎念:测试结果看PC端比模拟器的OCR准确率还低,但明明PC端的截图分辨率更高...不知道是否为本人的测试环境问题) - 待开发功能: - 1.保存队伍配装 - 2.读取队伍配装并装备 - 3.遗器管理与配装管理 - 4.兼容四星遗器: - a. 兼容校验函数 (增加四星遗器副词条挡位数据) - b. 对遗器稀有度的识别 (识别指定点位色相[黄,紫]) - 5.模糊匹配成功后更新相关数据库 - ... """ title = _("遗器模块:") options = [_("保存当前人物的配装"), _("读取当前人物的配装"), _("识别当前遗器的数据"), _("返回主菜单")] - option = options[0] # 保存上一次的选择 + option = None # 保存上一次的选择 while True: + self.calculated.switch_cmd() option = questionary.select(title, options, default=option).ask() if option == _("保存当前人物的配装"): + self.calculated.switch_window() self.save_loadout_for_char() elif option == _("读取当前人物的配装"): self.equip_loadout_for_char() elif option == _("识别当前遗器的数据"): + self.calculated.switch_window() data = self.try_ocr_relic() self.print_relic(data) elif option == _("返回主菜单"): @@ -193,12 +237,14 @@ def equip_loadout_for_char(self): log.info(_("当前人物配装记录为空")) return title = _("请选择将要进行装备的配装:") - options = list(character_data.keys()) + options_map = {str_just(loadout_name, 12) + self.get_loadout_brief(hash_list): hash_list for loadout_name, hash_list in character_data.items()} + options = list(options_map.keys()) options.append(_("返回上一级")) option = questionary.select(title, options).ask() if option == _("返回上一级"): return - relic_hash = character_data[option] + relic_hash = options_map[option] + self.calculated.switch_window() # 进行配装 self.calculated.relative_click((12,40) if IS_PC else (16,48)) # 点击遗器,进入[人物]-[遗器]界面 time.sleep(0.5) @@ -219,40 +265,29 @@ def equip_loadout(self, relics_hash:list[str]): :param relics_hash: 遗器配装哈希值列表 """ equip_pos_list = [(4,13),(9,13),(13,13),(18,13),(23,13),(27,13)] if IS_PC else [(5,14),(11,14),(17,14),(23,14),(28,14),(34,14)] - pre_relic_set_index = -1 + relic_filter = self.Relic_filter(self.calculated) # 遗器筛选器初始化 + relic_set_name_dict = Array2dict(self.relic_set_name) for equip_indx, equip_pos in enumerate(equip_pos_list): # 遗器部位循环 # 选择部位 log.info(_(f"选择部位:{self.equip_set_name[equip_indx]}")) self.calculated.relative_click(equip_pos) time.sleep(0.5) - # 筛选遗器 (加快遗器搜索) + # 获取遗器数据 tmp_hash = relics_hash[equip_indx] tmp_data = self.relics_data[tmp_hash] log.debug(tmp_hash) - relic_set_index = np.where(self.relic_set_name[:, -1] == tmp_data["relic_set"])[0][0] - if pre_relic_set_index != relic_set_index: # 判断与先前套装发生变化 - log.info(_("筛选遗器套装")) - self.calculated.relative_click((3,92) if IS_PC else (4,92)) # 点击筛选图标 - time.sleep(0.5) - ... # 筛选稀有度 - self.calculated.relative_click((93,20) if IS_PC else (92,23)) # 点击套装选择 - time.sleep(0.5) - self.calculated.relative_click((40,70) if IS_PC else (37,76)) # 清除之前的筛选项 - # 搜索遗器套装名,并点击 - self.search_relic_set_for_filter(relic_set_index) - time.sleep(0.2) - self.calculated.relative_click((62,70) if IS_PC else (64,76)) # 点击确认 - time.sleep(0.5) - self.calculated.relative_click((3,92) if IS_PC else (4,92)) # 筛选框外任意点击退出筛选 - pre_relic_set_index = relic_set_index + relic_set_index = relic_set_name_dict[tmp_data["relic_set"]] + rarity = tmp_data["rarity"] + # 筛选遗器 (加快遗器搜索) + relic_filter.do(relic_set_index, rarity) # 搜索遗器 - pos = self.search_relic(equip_indx, key_hash=tmp_hash) # , key_data=tmp_data) + pos = self.search_relic(equip_indx, key_hash=tmp_hash, key_data=tmp_data) if pos is None: log.error(_(f"遗器搜索失败: {tmp_hash}")) continue # 点击装备 self.calculated.relative_click(pos) - button = self.calculated.ocr_pos_for_singleLine([_("装备"), _("替换"), _("卸下")], points=(80,90,85,94) if IS_PC else (75,90,82,95)) # 需识别[装备,替换,卸下] + button = self.calculated.ocr_pos_for_single_line([_("装备"), _("替换"), _("卸下")], points=(80,90,85,94) if IS_PC else (75,90,82,95)) # 需识别[装备,替换,卸下] if button in [0,1]: log.info(_("点击装备")) self.calculated.relative_click((82,92) if IS_PC else (78,92)) @@ -296,14 +331,14 @@ def save_loadout(self, character_name:str=None, max_retries=3): """ character_name = character_name if character_name else self.ocr_character_name() character_data = self.loadout_data[character_name] - equip_pos = [(4,13),(9,13),(13,13),(18,13),(23,13),(27,13)] if IS_PC else [(5,14), (11,14), (17,14), (23,14), (28,14), (34,14)] + equip_pos_list = [(4,13),(9,13),(13,13),(18,13),(23,13),(27,13)] if IS_PC else [(5,14), (11,14), (17,14), (23,14), (28,14), (34,14)] relics_hash = [] - for equip_indx in range(6): # 遗器部位循环 + for equip_indx, equip_pos in enumerate(equip_pos_list): # 遗器部位循环 log.info(_(f"选择部位:{self.equip_set_name[equip_indx]}")) - self.calculated.relative_click(equip_pos[equip_indx]) + self.calculated.relative_click(equip_pos) time.sleep(1) tmp_data = self.try_ocr_relic(equip_indx, max_retries) - tmp_hash = self.calculated.get_data_hash(tmp_data) + tmp_hash = get_data_hash(tmp_data, self.relic_data_filter) log.debug("\n"+pp.pformat(tmp_data)) self.print_relic(tmp_data) if tmp_hash in self.relics_data: @@ -313,40 +348,98 @@ def save_loadout(self, character_name:str=None, max_retries=3): self.add_relic_data(tmp_data, tmp_hash) relics_hash.append(tmp_hash) log.info(_("配装识别完毕")) - loadout_name = input(_("自定义配装名称: ")) # 需作为字典key值,确保唯一性 (但不同的人物可以有同一配装名称) + self.calculated.switch_cmd() + loadout_name = input(_(">>>>命名配装名称: ")) # 需作为字典key值,确保唯一性 (但不同的人物可以有同一配装名称) while loadout_name in character_data: - loadout_name = input(_("名称冲突,请重定义: ")) + loadout_name = input(_(">>>>命名冲突,请重命名: ")) character_data[loadout_name] = relics_hash self.loadout_data = modify_json_file(LOADOUT_FILE_NAME, character_name, character_data) log.info(_("配装录入成功")) + self.calculated.switch_window() - def search_relic_set_for_filter(self, relic_set_index:int): + class Relic_filter: """ 说明: - 在当前滑动[人物]-[遗器]-[遗器详情]-[遗器筛选]界面内,搜索遗器套装名,并点击。 - 综合OCR识别与方位计算 - 参数: - :param equip_set_index: 遗器套装索引 - """ - is_left = relic_set_index % 2 == 0 # 计算左右栏 - page_num = 0 if relic_set_index < 8 else (1 if relic_set_index < 16 else 2) # 计算页数 (将第2页的末尾两件放至第3页来处理) - last_page = 1 - # 滑动翻页 - for i in range(page_num): - time.sleep(0.2) - self.calculated.relative_swipe((30,60) if IS_PC else (30,62), (30,31) if IS_PC else (30,27)) # 整页翻动 (此界面的动态延迟较大) - if i != last_page: # 非末页,将翻页的动态延迟暂停 (末页会有个短暂反弹动画后自动停止) - self.calculated.relative_click((35,35) if IS_PC else (35,32), 0.5) # 长按选中 - self.calculated.relative_click((35,35) if IS_PC else (35,32)) # 取消选中 - points = ((28,33,42,63) if is_left else (53,33,67,63)) if IS_PC else ((22,29,41,65) if is_left else (53,29,72,65)) - self.calculated.ocr_click(self.relic_set_name[relic_set_index, 1], points=points) + 遗器筛选器。封装了在[人物]-[遗器]-[遗器详情]-[遗器筛选]界面内的遗器筛选方法, + 目前可以对遗器套装与稀有度进行筛选,并记录先前的筛选状态 + (注意在未退出[遗器详情]界面时切换遗器,会保留上一次的筛选状态) + """ + rarity_pos_list = [(77,38),(89,38),(77,42),(89,42)] if IS_PC else [(71,45),(86,45),(71,52),(86,52)] + """稀有度筛选项的点击位点 (分别为2,3,4,5星稀有度)""" + + def __init__(self, calculated:calculated): + self.calculated = calculated + # 记录上一次的筛选状态 + self.pre_relic_set_index = -1 + """过去遗器套装索引""" + self.pre_rarity = -1 + """过去稀有度""" + + def do(self, relic_set_index:int, rairty:int): + """ + 说明: + 在当前[人物]-[遗器]-[遗器详情]内进行遗器筛选 + 参数: + :param relic_set_index: 遗器套装索引 + :param rairty: 稀有度 + """ + if self.pre_relic_set_index == relic_set_index and self.pre_rarity == rairty: # 筛选条件未改变 + return + # 若筛选条件之一发生改变,未改变的不会进行重复动作 + log.info(_(f"进行遗器筛选,筛选条件: set={relic_set_index}, rairty={rairty}")) + self.calculated.relative_click((3,92) if IS_PC else (4,92)) # 点击筛选图标进入[遗器筛选]界面 + time.sleep(0.5) + # 筛选遗器套装 + if self.pre_relic_set_index != relic_set_index: + self.calculated.relative_click((93,20) if IS_PC else (92,23)) # 点击套装选择进入[遗器套装筛选]界面 + time.sleep(0.5) + self.calculated.relative_click((40,70) if IS_PC else (37,76)) # 清除之前的筛选项 + time.sleep(0.2) + self.search_relic_set_for_filter(relic_set_index) # 搜索遗器套装名,并点击 + time.sleep(0.2) + self.calculated.relative_click((62,70) if IS_PC else (64,76)) # 点击确认退出[遗器套装筛选]界面 + time.sleep(0.5) + self.pre_relic_set_index = relic_set_index + # 筛选遗器稀有度 (注意稀有度筛选要在遗器筛选之后,不然识别位点会改变) + if self.pre_rarity != rairty: + if self.pre_rarity != -1: # 非初始清除之前的筛选项 + self.calculated.relative_click(self.rarity_pos_list[self.pre_rarity-2]) + time.sleep(0.5) + self.calculated.relative_click(self.rarity_pos_list[rairty-2]) # 点击目标稀有度 + time.sleep(0.5) + self.pre_rarity = rairty + ... # 其他筛选条件 + self.calculated.relative_click((3,92) if IS_PC else (4,92)) # 任意点击筛选框外退出[遗器筛选]界面 + + def search_relic_set_for_filter(self, relic_set_index:int): + """ + 说明: + 在当前滑动[人物]-[遗器]-[遗器详情]-[遗器筛选]-[遗器套装筛选]界面内,搜索遗器套装名,并点击。 + 综合OCR识别与方位计算 + 参数: + :param equip_set_index: 遗器套装索引 + """ + is_left = relic_set_index % 2 == 0 # 计算左右栏 + page_num = 0 if relic_set_index < 8 else (1 if relic_set_index < 16 else 2) # 计算页数 (将第2页的末尾两件放至第3页来处理) + last_page = 1 + # 滑动翻页 + for i in range(page_num): + time.sleep(0.2) + self.calculated.relative_swipe((30,60) if IS_PC else (30,62), (30,31) if IS_PC else (30,27)) # 整页翻动 (此界面的动态延迟较大) + if i != last_page: # 非末页,将翻页的动态延迟暂停 (末页会有个短暂反弹动画后自动停止) + self.calculated.relative_click((35,35) if IS_PC else (35,32), 0.5) # 长按选中 + time.sleep(0.5) + self.calculated.relative_click((35,35) if IS_PC else (35,32)) # 取消选中 + points = ((28,33,42,63) if is_left else (53,33,67,63)) if IS_PC else ((22,29,41,65) if is_left else (53,29,72,65)) + self.calculated.ocr_click(Relic.relic_set_name[relic_set_index, 1], points=points) + def search_relic(self, equip_indx:int, key_hash:str=None, key_data:dict=None, overtime=180, max_retries=3) -> tuple[int, int]: """ 说明: 在当前滑动[人物]-[遗器]-[遗器详情]界面内,搜索匹配的遗器。 key_hash非空: 激活精确匹配 (假设数据保存期间遗器未再次升级); - key_data非空: 激活模糊匹配 (假设数据保存期间遗器再次升级); + key_data非空: 激活模糊匹配 (假设数据保存期间遗器再次升级,匹配成功后自动更新遗器数据); key_hash & key_data均空: 遍历当前页面内的遗器 参数: :param equip_indx: 遗器部位索引 @@ -356,44 +449,45 @@ def search_relic(self, equip_indx:int, key_hash:str=None, key_data:dict=None, ov 返回: :return pos: 坐标 """ - pos_start = (5,24) if IS_PC else (7, 28) + pos_start = (5,24) if IS_PC else (7,28) d_x, d_y, k_x, k_y = (7, 14, 4, 5) if IS_PC else (8, 17, 4, 4) + r_x = range(pos_start[0], pos_start[0]+d_x*k_x, d_x) + r_y = range(pos_start[1], pos_start[1]+d_y*k_y, d_y) pre_pos = [""] start_time = time.time() while True: - x, y = pos_start # 翻页复位 - for i in range(0, k_y): # 行 - x = pos_start[0] # 首列复位 - for j in range(0, k_x): # 列 - self.calculated.relative_click((x, y)) # 点击遗器,同时将翻页的动态延迟暂停 - time.sleep(0.2) - log.info(f"({i+1},{j+1},{len(pre_pos)})") # 显示当前所识别遗器的方位与序列号 - tmp_data = self.try_ocr_relic(equip_indx, max_retries) - # log.info("\n"+pp.pformat(tmp_data)) - tmp_hash = self.calculated.get_data_hash(tmp_data) - if key_hash and key_hash == tmp_hash: # 精确匹配 - return (x, y) - if key_data and self.is_fuzzy_match and self.compare_relics(key_data, tmp_data): # 模糊匹配 - log.info(_("模糊匹配成功!")) - print(_("旧遗器:")) - self.print_relic(key_data) - print(_("新遗器:")) - self.print_relic(tmp_data) - ... # 更新数据库 (将旧有遗器数据替换,并更新遗器配装数据的哈希值) - return (x, y) - # 判断是否遍历完毕 - if pre_pos[-1] == tmp_hash: - log.info(_("遗器数据未发生变化,怀疑点击到空白区域搜索至最后")) - return None # 判断点击到空白,遗器数据未发生变化,结束搜索 - if j == 0: # 首列遗器 - if tmp_hash in pre_pos: - if i == k_y-1: - log.info(_("已搜索至最后")) - return None # 判断已滑动至末页,结束搜索 - break # 本行已搜索过,跳出本行 - pre_pos.append(tmp_hash) # 记录 - x += d_x - y += d_y + for index in range(0, k_y*k_x): + i = index // k_x # 行 + j = index % k_x # 列 + x, y = r_x[j], r_y[i] # 坐标查表 + self.calculated.relative_click((x, y)) # 点击遗器,同时将翻页的动态延迟暂停 + time.sleep(0.2) + log.info(f"({i+1},{j+1},{len(pre_pos)})") # 显示当前所识别遗器的方位与序列号 + tmp_data = self.try_ocr_relic(equip_indx, max_retries) + # log.info("\n"+pp.pformat(tmp_data)) + tmp_hash = get_data_hash(tmp_data) + if key_hash and key_hash == tmp_hash: # 精确匹配 + return (x, y) + if key_data and self.is_fuzzy_match and self.compare_relics(key_data, tmp_data): # 模糊匹配 + print(_("<<<<旧遗器>>>>")) + self.print_relic(key_data) + print(_("<<<<新遗器>>>>")) + self.print_relic(tmp_data) + log.info(_("模糊匹配成功!自动更新遗器数据")) + # 更新数据库 (录入新遗器数据,并将配装数据中的旧有哈希值替换) + tmp_data["pre_ver_hash"] = key_hash # 建立后继关系 + self.updata_relic_data(key_hash, tmp_hash, equip_indx, tmp_data) + return (x, y) + # 判断是否遍历完毕 + if pre_pos[-1] == tmp_hash: + log.info(_("遗器数据未发生变化,怀疑点击到空白区域搜索至最后")) + return None # 判断点击到空白,遗器数据未发生变化,结束搜索 + if j == 0 and tmp_hash in pre_pos: # 判断当前行的首列遗器是否已被搜索 + if i == k_y-1: + log.info(_("已搜索至最后")) + return None # 判断已滑动至末页,结束搜索 + break # 本行已搜索过,跳出本行 + pre_pos.append(tmp_hash) # 记录 # 滑动翻页 (从末尾位置滑动至顶部,即刚好滑动一整页) log.info(_("滑动翻页")) self.calculated.relative_swipe((pos_start[0], pos_start[1]+(k_y-1)*d_y), (pos_start[0], pos_start[1]-d_y)) @@ -419,6 +513,60 @@ def compare_relics(self, old_data:dict, new_data:dict) -> bool: if old_data["subs_stats"][key] > new_data["subs_stats"][key]: return False return True + + def check_relic_data_hash(self, updata=False): + """ + 说明: + 检查遗器数据是否发生手动修改 (应对json数据格式变动或手动矫正仪器数值), + 若发生修改,可选择更新仪器哈希值,并替换配装数据中相应的数值 + """ + equip_set_dict = {key: value for value, key in enumerate(self.equip_set_name)} + relics_data_copy = self.relics_data.copy() # 字典迭代过程中不允许修改key + cnt = 0 + for old_hash, data in relics_data_copy.items(): + new_hash = get_data_hash(data, self.relic_data_filter) + if old_hash != new_hash: + equip_indx = equip_set_dict[data["equip_set"]] + log.debug(f"(old={old_hash}, new={new_hash})") + if updata: + self.updata_relic_data(old_hash, new_hash, equip_indx) + cnt += 1 + if not cnt: + log.info(_(f"遗器哈希值校验成功")) + return True + if updata: + log.info(_(f"已更新 {cnt} 件遗器的哈希值")) + return True + else: + log.error(_(f"发现 {cnt} 件遗器的哈希值校验失败")) + return False + + def updata_relic_data(self, old_hash:str, new_hash:str, equip_indx:int, new_data:dict=None, delete_old_data=False): + """ + 说明: + 更改仪器数据,先后修改遗器与配装文件 + 参数: + :param old_hash: 遗器旧哈希值 + :param new_hash: 遗器新哈希值 + :parma equip_indx: 遗器部位索引 (减轻一点遍历压力) + :parma new_data: 新的遗器数据 + :parma delete_old_data: 是否删除旧的数据 + """ + # 修改遗器文件 + if new_data is None: + self.relics_data[new_hash] = self.relics_data.pop(old_hash) + else: + if delete_old_data: + self.relics_data.pop(old_hash) + self.relics_data[new_hash] = new_data + rewrite_json_file(RELIC_FILE_NAME, self.relics_data) + # 修改配装文件 + for char_name, loadouts in self.loadout_data.items(): + for loadout_name, hash_list in loadouts.items(): + if hash_list[equip_indx] == old_hash: + self.loadout_data[char_name][loadout_name][equip_indx] = new_hash + rewrite_json_file(LOADOUT_FILE_NAME, self.loadout_data) + # 队伍配装文件无需修改 def add_relic_data(self, data:dict, data_hash:str=None) -> bool: """ @@ -426,7 +574,7 @@ def add_relic_data(self, data:dict, data_hash:str=None) -> bool: 录入仪器数据 """ if not data_hash: - data_hash = self.calculated.get_data_hash(data) + data_hash = get_data_hash(data, self.relic_data_filter) if data_hash not in self.relics_data: self.relics_data = modify_json_file(RELIC_FILE_NAME, data_hash, data) # 返回更新后的字典 return True @@ -441,8 +589,8 @@ def ocr_character_name(self) -> str: 返回: :return character_name: 人物名称 """ - str = self.calculated.ocr_pos_for_singleLine(points=(10,6,18,9) if IS_PC else (13,4,22,9)) # 识别人物名称 (主角名称为玩家自定义,无法适用预选列表) - character_name = re.sub(r"[.’,,。、·'\"/\\]", '', str) # 删除由于背景光点造成的误判 + str = self.calculated.ocr_pos_for_single_line(points=(10.4,6,18,9) if IS_PC else (13,4,22,9)) # 识别人物名称 (主角名称为玩家自定义,无法适用预选列表) + character_name = re.sub(r"[.’,,。、·'-_——\"/\\]", '', str) # 删除由于背景光点造成的误判 log.info(_(f"识别人物: {character_name}")) if character_name not in self.loadout_data: self.loadout_data = modify_json_file(LOADOUT_FILE_NAME, character_name, {}) @@ -473,7 +621,7 @@ def try_ocr_relic(self, equip_set_index:int = None, max_retries = 3) -> dict: def ocr_relic(self, equip_set_index:int = None) -> dict: """ 说明: - OCR当前静态[人物]-[遗器]-[遗器详情]界面内的遗器数据,用时1-2s。 + OCR当前静态[人物]-[遗器]-[遗器详情]界面内的遗器数据,单次用时约0.5s。 更改为ocr_for_single_line()后,相较ocr()已缩短一半用时,且提高了部分识别的准确性, 若更改为ocr_for_single_lines()后的性能变化【待测】(代码重构较大) 参数: @@ -485,70 +633,76 @@ def ocr_relic(self, equip_set_index:int = None) -> dict: img_pc = self.calculated.take_screenshot() # 仅截取一次图片 # [1]部位识别 if equip_set_index is None: - equip_set_index = self.calculated.ocr_pos_for_singleLine(self.equip_set_name, points=(77,19,83,23) if IS_PC else (71,22,78,26), img_pk=img_pc) + equip_set_index = self.calculated.ocr_pos_for_single_line(self.equip_set_name, points=(77,19,83,23) if IS_PC else (71,22,78,26), img_pk=img_pc) if equip_set_index < 0: raise RelicOCRException(_("遗器套装OCR错误")) equip_set_name = self.equip_set_name[equip_set_index] # [2]套装识别 name_list = self.relic_set_name[:, 0].tolist() - relic_set_index = self.calculated.ocr_pos_for_singleLine(name_list, points=(77,15,92,19) if IS_PC else (71,17,88,21), img_pk=img_pc) + relic_set_index = self.calculated.ocr_pos_for_single_line(name_list, points=(77,15,92,19) if IS_PC else (71,17,88,21), img_pk=img_pc) if relic_set_index < 0: raise RelicOCRException(_("遗器部位OCR错误")) relic_set_name = self.relic_set_name[relic_set_index, -1] # [3] 稀有度识别 - ... - rarity = 5 # 目前只支持5星遗器,默认5星 - ... + hue, __, __ = self.calculated.get_relative_pix_hsv((43,55) if IS_PC else (41,55)) # 识别指定位点色相 + log.debug(f"hue = {hue}") + if hue < 40: # [黄]模拟器测试结果的均值为 25 + rarity = 5 + elif hue < 80: # [绿]未有测试样本 + rarity = 2 + elif hue < 120: # [蓝]模拟器测试结果的均值为 105 + rarity = 3 + elif hue < 160: # [紫]模拟器测试结果的均值为 140 + rarity = 4 + else: + raise RelicOCRException(_("遗器稀有度识别错误")) # [4]等级识别 - level = self.calculated.ocr_pos_for_singleLine(points=(95,19,98,23) if IS_PC else (94,22,98,26), number=True, img_pk=img_pc) + level = self.calculated.ocr_pos_for_single_line(points=(95,19,98,23) if IS_PC else (94,22,98,26), number=True, img_pk=img_pc) level = int(level.split('+')[-1]) # 消除开头可能的'+'号 if level > 15: raise RelicOCRException(_("遗器等级OCR错误")) # [5]主属性识别 name_list = self.base_stats_name4equip[equip_set_index][:, 0].tolist() - base_stats_index = self.calculated.ocr_pos_for_singleLine(name_list, points=(79,25,92,29) if IS_PC else (74,29,89,34), img_pk=img_pc) - base_stats_value = self.calculated.ocr_pos_for_singleLine(points=(93,25,98,29) if IS_PC else (91,29,98,34), number=True, img_pk=img_pc) + base_stats_index = self.calculated.ocr_pos_for_single_line(name_list, points=(79.5,25,92,29) if IS_PC else (74,29,89,34), img_pk=img_pc) + base_stats_value = self.calculated.ocr_pos_for_single_line(points=(93,25,98,29) if IS_PC else (91,29,98,34), number=True, img_pk=img_pc) if base_stats_index < 0: raise RelicOCRException(_("遗器主词条OCR错误")) if base_stats_value is None: raise RelicOCRException(_("遗器主词条数值OCR错误")) - else: - base_stats_value = str(base_stats_value).replace('.', '') # 删除所有真假小数点 - if '%' in base_stats_value: - s = base_stats_value.split('%')[0] # 修正'48%1'如此的错误识别 - base_stats_value = s[:-1] + '.' + s[-1:] # 添加未识别的小数点 - base_stats_value = float(base_stats_value) + base_stats_value = str(base_stats_value).replace('.', '') # 删除所有真假小数点 + if '%' in base_stats_value: + s = base_stats_value.split('%')[0] # 修正'48%1'如此的错误识别 + base_stats_value = s[:-1] + '.' + s[-1:] # 添加未识别的小数点 + base_stats_value = float(base_stats_value) base_stats_name = str(self.base_stats_name4equip[equip_set_index][base_stats_index, -1]) # [6]副属性识别 (词条数量 2-4) - subs_stats_name_points = [(79,29,85,33),(79,33,85,36.5),(79,36.5,85,40),(79,40,85,44)] if IS_PC else [(74,35,81,38),(74,39,81,43),(74,44,81,47),(74,48,81,52)] + subs_stats_name_points = [(79.5,29,85,33),(79.5,33,85,36.5),(79.5,36.5,85,40),(79.5,40,85,44)] if IS_PC else [(74,35,81,38),(74,39,81,43),(74,44,81,47),(74,48,81,52)] subs_stats_value_points = [(93,29,98,33),(93,33,98,36.5),(93,36.5,98,40),(93,40,98,44)] if IS_PC else [(92,35,98,38),(92,39,98,43),(92,44,98,47),(92,48,98,52)] name_list = self.subs_stats_name[:, 0].tolist() subs_stats_dict = {} total_level = 0 for name_point, value_point in zip(subs_stats_name_points, subs_stats_value_points): - tmp_index = self.calculated.ocr_pos_for_singleLine(name_list, points=name_point, img_pk=img_pc) + tmp_index = self.calculated.ocr_pos_for_single_line(name_list, points=name_point, img_pk=img_pc) if tmp_index is None: break # 所识别data为空,即词条为空,正常退出循环 if tmp_index < 0: raise RelicOCRException(_("遗器副词条OCR错误")) - tmp_value = self.calculated.ocr_pos_for_singleLine(points=value_point, number=True, img_pk=img_pc) + tmp_value = self.calculated.ocr_pos_for_single_line(points=value_point, number=True, img_pk=img_pc) if tmp_value is None: raise RelicOCRException(_("遗器副词条数值OCR错误")) - else: - tmp_value = str(tmp_value).replace('.', '') # 删除所有真假小数点 - if '%' in tmp_value: - s = tmp_value.split('%')[0] # 修正'48%1'如此的错误识别 - tmp_value = s[:-1] + '.' + s[-1:] # 添加未识别的小数点 - if tmp_index >= 0 and tmp_index < 3: - tmp_index += 3 # 小词条转为大词条 - tmp_value = float(tmp_value) + tmp_value = str(tmp_value).replace('.', '') # 删除所有真假小数点 + if '%' in tmp_value: + s = tmp_value.split('%')[0] # 修正'48%1'如此的错误识别 + tmp_value = s[:-1] + '.' + s[-1:] # 添加未识别的小数点 + if tmp_index >= 0 and tmp_index < 3: + tmp_index += 3 # 小词条转为大词条 + tmp_value = float(tmp_value) tmp_name = str(self.subs_stats_name[tmp_index, -1]) - check = self.get_subs_stats_detail((tmp_name, tmp_value), tmp_index) + check = self.get_subs_stats_detail((tmp_name, tmp_value), rarity, tmp_index) if check is None: raise RelicOCRException(_("遗器副词条数值OCR错误")) - else: - total_level += check[0] + total_level += check[0] subs_stats_dict[tmp_name] = tmp_value - if self.is_check and total_level > level // 3 + 4: + if self.is_check_stats and rarity in [4,5] and total_level > level // 3 + 4: log.error(f"total_level: {total_level}") raise RelicOCRException(_("遗器副词条某一数值OCR错误")) # [7]生成结果数据包 @@ -571,62 +725,100 @@ def print_relic(self, data:dict): """ 说明: 打印遗器信息, - 可通过is_detail设置打印普通信息与增强信息 + 可通过is_detail设置打印普通信息与拓展信息 """ print(_("部位: {equip_set}").format(equip_set=data["equip_set"])) print(_("套装: {relic_set}").format(relic_set=data["relic_set"])) - print(_("星级: {rarity}").format(rarity=data["rarity"])) - print(_("等级: {level}").format(level=data["level"])) + print(_("星级: {star}").format(star='★'*data["rarity"])) + print(_("等级: +{level}").format(level=data["level"])) print(_("主词条:")) name, value = list(data["base_stats"].items())[0] pre = " " if name in self.not_pre_stats else "%" print(_(" {name:<4}\t{value:>5}{pre}").format(name=name, value=value, pre=pre)) print(_("副词条:")) + subs_stats_dict = Array2dict(self.subs_stats_name) for name, value in data["subs_stats"].items(): pre = " " if name in self.not_pre_stats else "%" - if self.is_detail: - ret = self.get_subs_stats_detail((name, value)) # 增强信息并校验数据 - if ret: - level, score, result = ret - tag = '>'*(level-1) # 强化次数的标识 - print(_(" {name:<4}\t{tag:<7}{value:>6.3f}{pre} [{score}]").format(name=name, tag=tag, value=result, score=score, pre=pre)) - else: # 数据校验失败 - print(_(" {name:<4}\t{value:>5}{pre} [ERROR ]").format(name=name, value=value, pre=pre)) - else: + if not self.is_detail or data["rarity"] not in [4,5]: # 不满足校验条件 print(_(" {name:<4}\t{value:>5}{pre}").format(name=name, value=value, pre=pre)) + continue + stats_index = subs_stats_dict[name] + # 增强信息并校验数据 + ret = self.get_subs_stats_detail((name, value), data["rarity"], stats_index) + if ret: # 数据校验成功 + level, score, result = ret + tag = '>'*(level-1) # 强化次数的标识 + print(_(" {name:<4}\t{tag:<7}{value:>6.3f}{pre} [{score}]").format(name=name, tag=tag, value=result, score=score, pre=pre)) + else: # 数据校验失败 + print(_(" {name:<4}\t{value:>5}{pre} [ERROR]").format(name=name, value=value, pre=pre)) + print('-'*50) + + def get_loadout_brief(self, relics_hash:list[str]) -> str: + """ + 说明: + 获取配装的简要信息 (包含内外圈套装信息与主词条信息) + """ + set_abbr_dict = Array2dict(self.relic_set_name, -1, 2) + stats_abbr_dict = Array2dict(self.base_stats_name, -1, 1) + outer_set_list, inner_set_list, base_stats_list = [], [], [] + # 获取遗器数据 + for equip_indx in range(len((relics_hash))): + tmp_data = self.relics_data[relics_hash[equip_indx]] + tmp_set = set_abbr_dict[tmp_data["relic_set"]] + tmp_base_stats = stats_abbr_dict[list(tmp_data["base_stats"].keys())[0]] + base_stats_list.append(tmp_base_stats) + if equip_indx < 4: + outer_set_list.append(tmp_set) # 外圈 + else: + inner_set_list.append(tmp_set) # 内圈 + outer_set_cnt = Counter(outer_set_list) + inner_set_cnt = Counter(inner_set_list) + # 生成信息 + msg = "外:" + '+'.join([str(cnt) + name for name, cnt in outer_set_cnt.items()]) + " " + msg += "内:" + '+'.join([str(cnt) + name for name, cnt in inner_set_cnt.items()]) + " " + # msg += " ".join([self.equip_set_abbr[idx]+":"+name for idx, name in enumerate(base_stats_list) if idx > 1]) + msg += ".".join([name for idx, name in enumerate(base_stats_list) if idx > 1]) # 排除头部与手部 + return msg - def get_subs_stats_detail(self, data:tuple[str, float], index:int=None) -> tuple[int, int, float]: + def get_subs_stats_detail(self, data:tuple[str, float], rarity:int, stats_index:int = None) -> tuple[int, int, float]: """ 说明: - 计算副词条的详细信息 (强化次数、挡位总积分与修正后的数值) + 计算副词条的详细信息 (如强化次数、档位积分,以及提高原数据的小数精度) 对于速度属性只能做保守估计,其他属性可做准确计算。 可以作为副词条校验函数 (可以检测出大部分的OCR错误) + 支持五星遗器与四星遗器 + 参数: + :param data: 遗器副词条键值对 + :param stats_index: 遗器副词条索引 + :param rarity: 遗器稀有度 返回: :return level: 强化次数: 0次强化记为1,最高5次强化为6 - :return score: 挡位总积分: 1挡记0分, 2挡记1分, 3挡记2分 + :return score: 档位总积分: 1档记0分, 2档记1分, 3档记2分 :return result: 修正后数值 (提高了原数值精度) """ - if not self.is_check: + if not self.is_check_stats or rarity not in [4,5]: # 仅支持五星遗器与四星遗器 return (0,0,0) name, value = data - index = np.where(self.subs_stats_name[:, -1] == name)[0][0] if index is None else index - a, d = self.subs_stats_tier[index] + stats_index = np.where(self.subs_stats_name[:, -1] == name)[0][0] if stats_index is None else stats_index + rarity_index = rarity - 4 # 0-四星,1-五星 + a, d = self.subs_stats_tier[rarity_index][stats_index] if name in self.not_pre_stats: a_ = int(a) # 从个分位截断小数 else: a_ = int(a * 10)/10 # 从十分位截断小数 - level = int(value / a_) # 向下取整 - score = (math.ceil((value - a*level) / d - 1.e-6)) # 向上取整 (考虑浮点数运算的数值损失) + level = int(value / a_) # 向下取整 + a_ = a_ if name == _("速度") else a # 给四星速度打补丁 + score = (math.ceil((value - a_*level) / d - 1.e-6)) # 向上取整 (考虑浮点数运算的数值损失) if score < 0: # 总分小于零打补丁 (由于真实总分过大导致) level -= 1 - score = math.ceil((value - a*level) / d - 1.e-6) - result = round(a*level + d*score, 3) # 四舍五入 (考虑浮点数运算的数值损失) + score = math.ceil((value - a_*level) / d - 1.e-6) + result = round(a*level + d*score, 3) # 四舍五入 (考虑浮点数运算的数值损失) # 校验数据 check = result - value log.debug(f"[{a}, {d}], l={level}, s={score}, r={result}") if check < 0 or \ - name in self.not_pre_stats and check > 1 or \ - name not in self.not_pre_stats and check > 0.1 or \ + name in self.not_pre_stats and check >= 1 or \ + name not in self.not_pre_stats and check >= 0.1 or \ level > 6 or level < 1 or \ score > level*2 or score < 0: log.error(_(f"校验失败,原数据或计算方法有误: {data}"))