diff --git a/assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png b/assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png index 519439bbd..9a3d8b60c 100644 Binary files a/assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png and b/assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png differ diff --git a/assets/jp/combat/GET_ITEMS_1.png b/assets/jp/combat/GET_ITEMS_1.png index b321db895..1c68ab63c 100644 Binary files a/assets/jp/combat/GET_ITEMS_1.png and b/assets/jp/combat/GET_ITEMS_1.png differ diff --git a/assets/jp/combat/GET_ITEMS_2.png b/assets/jp/combat/GET_ITEMS_2.png index d69a8e86f..91d02538b 100644 Binary files a/assets/jp/combat/GET_ITEMS_2.png and b/assets/jp/combat/GET_ITEMS_2.png differ diff --git a/assets/jp/combat/GET_ITEMS_3.png b/assets/jp/combat/GET_ITEMS_3.png index d69a8e86f..8748a7e5b 100644 Binary files a/assets/jp/combat/GET_ITEMS_3.png and b/assets/jp/combat/GET_ITEMS_3.png differ diff --git a/assets/tw/handler/AIR_STRIKE_CANCEL.png b/assets/tw/handler/AIR_STRIKE_CANCEL.png new file mode 100644 index 000000000..7ffa8c40c Binary files /dev/null and b/assets/tw/handler/AIR_STRIKE_CANCEL.png differ diff --git a/assets/tw/handler/AIR_STRIKE_CONFIRM.png b/assets/tw/handler/AIR_STRIKE_CONFIRM.png new file mode 100644 index 000000000..6baca294e Binary files /dev/null and b/assets/tw/handler/AIR_STRIKE_CONFIRM.png differ diff --git a/campaign/Readme.md b/campaign/Readme.md index f3bc38dd7..587837aad 100644 --- a/campaign/Readme.md +++ b/campaign/Readme.md @@ -301,4 +301,4 @@ To add a new event, add a new row in here, and run `python -m module.config.conf | 20260528 | event 20220818 cn | Operation Convergence | - | - | - | 復刻遠匯點作戰 | | 20260605 | event 20260520 cn | Alliance Before the Hagiobull | - | - | - | 聖印前的同盟 | | 20260618 | event 20240521 cn | Light of the Martyrium Rerun | 复刻绽放于辉光之城 | Light of the Martyrium Rerun | 赫輝のマルティリウム(復刻) | - | -| 20260625 | event 20260625 cn | - | 美梦巡演:奇妙夜 | - | - | - | +| 20260625 | event 20260625 cn | Miracle by Midnight | 美梦巡演:奇妙夜 | - | 幻夢のカヴァルカード | - | diff --git a/campaign/event_20260625_cn/sp.py b/campaign/event_20260625_cn/sp.py index fc756161c..64b1e5647 100644 --- a/campaign/event_20260625_cn/sp.py +++ b/campaign/event_20260625_cn/sp.py @@ -66,7 +66,7 @@ class Config: MAP_HAS_MYSTERY = False MAP_CHAPTER_SWITCH_20260326 = True STAGE_ENTRANCE = ['half', '20240725'] - MAP_HAS_MODE_SWITCH = True + MAP_HAS_MODE_SWITCH = False STAGE_INCREASE_AB = True MAP_WALK_USE_CURRENT_FLEET = True STAR_REQUIRE_1 = 0 diff --git a/module/combat/assets.py b/module/combat/assets.py index 740c71d07..b6eb2d1e1 100644 --- a/module/combat/assets.py +++ b/module/combat/assets.py @@ -29,10 +29,10 @@ EXP_INFO_C = Button(area={'cn': (332, 56, 345, 107), 'en': (332, 56, 345, 107), 'jp': (332, 56, 345, 107), 'tw': (332, 56, 345, 107)}, color={'cn': (198, 208, 198), 'en': (198, 208, 198), 'jp': (198, 208, 198), 'tw': (198, 208, 198)}, button={'cn': (1133, 634, 1262, 650), 'en': (1133, 634, 1262, 650), 'jp': (1133, 634, 1262, 650), 'tw': (1133, 634, 1262, 650)}, file={'cn': './assets/cn/combat/EXP_INFO_C.png', 'en': './assets/en/combat/EXP_INFO_C.png', 'jp': './assets/jp/combat/EXP_INFO_C.png', 'tw': './assets/tw/combat/EXP_INFO_C.png'}) EXP_INFO_D = Button(area={'cn': (328, 45, 341, 119), 'en': (328, 45, 341, 119), 'jp': (328, 45, 341, 119), 'tw': (328, 45, 341, 119)}, color={'cn': (199, 208, 199), 'en': (199, 208, 199), 'jp': (199, 208, 199), 'tw': (199, 208, 199)}, button={'cn': (1133, 634, 1262, 650), 'en': (1133, 634, 1262, 650), 'jp': (1133, 634, 1262, 650), 'tw': (1133, 634, 1262, 650)}, file={'cn': './assets/cn/combat/EXP_INFO_D.png', 'en': './assets/en/combat/EXP_INFO_D.png', 'jp': './assets/jp/combat/EXP_INFO_D.png', 'tw': './assets/tw/combat/EXP_INFO_D.png'}) EXP_INFO_S = Button(area={'cn': (342, 107, 389, 119), 'en': (342, 107, 389, 119), 'jp': (342, 107, 389, 119), 'tw': (342, 107, 389, 119)}, color={'cn': (233, 242, 127), 'en': (233, 242, 127), 'jp': (233, 242, 127), 'tw': (233, 242, 127)}, button={'cn': (1133, 634, 1262, 650), 'en': (1133, 634, 1262, 650), 'jp': (1133, 634, 1262, 650), 'tw': (1133, 634, 1262, 650)}, file={'cn': './assets/cn/combat/EXP_INFO_S.png', 'en': './assets/en/combat/EXP_INFO_S.png', 'jp': './assets/jp/combat/EXP_INFO_S.png', 'tw': './assets/tw/combat/EXP_INFO_S.png'}) -GET_ITEMS_1 = Button(area={'cn': (538, 217, 741, 253), 'en': (551, 223, 736, 250), 'jp': (548, 217, 741, 253), 'tw': (539, 217, 742, 253)}, color={'cn': (160, 192, 248), 'en': (166, 194, 235), 'jp': (144, 183, 250), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_1.png', 'en': './assets/en/combat/GET_ITEMS_1.png', 'jp': './assets/jp/combat/GET_ITEMS_1.png', 'tw': './assets/tw/combat/GET_ITEMS_1.png'}) +GET_ITEMS_1 = Button(area={'cn': (538, 217, 741, 253), 'en': (551, 223, 736, 250), 'jp': (539, 220, 741, 252), 'tw': (539, 217, 742, 253)}, color={'cn': (160, 192, 248), 'en': (166, 194, 235), 'jp': (146, 184, 249), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_1.png', 'en': './assets/en/combat/GET_ITEMS_1.png', 'jp': './assets/jp/combat/GET_ITEMS_1.png', 'tw': './assets/tw/combat/GET_ITEMS_1.png'}) GET_ITEMS_1_RYZA = Button(area={'cn': (564, 217, 721, 245), 'en': (577, 211, 704, 239), 'jp': (566, 217, 719, 244), 'tw': (564, 218, 723, 246)}, color={'cn': (176, 199, 243), 'en': (172, 199, 246), 'jp': (179, 201, 243), 'tw': (173, 197, 242)}, button={'cn': (1000, 631, 1055, 689), 'en': (1000, 631, 1055, 689), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_1_RYZA.png', 'en': './assets/en/combat/GET_ITEMS_1_RYZA.png', 'jp': './assets/jp/combat/GET_ITEMS_1_RYZA.png', 'tw': './assets/tw/combat/GET_ITEMS_1_RYZA.png'}) -GET_ITEMS_2 = Button(area={'cn': (538, 146, 742, 182), 'en': (551, 149, 735, 175), 'jp': (547, 143, 742, 179), 'tw': (538, 148, 741, 182)}, color={'cn': (160, 192, 248), 'en': (167, 195, 235), 'jp': (145, 183, 250), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_2.png', 'en': './assets/en/combat/GET_ITEMS_2.png', 'jp': './assets/jp/combat/GET_ITEMS_2.png', 'tw': './assets/tw/combat/GET_ITEMS_2.png'}) -GET_ITEMS_3 = Button(area={'cn': (539, 143, 742, 179), 'en': (548, 136, 740, 172), 'jp': (547, 143, 742, 179), 'tw': (546, 145, 742, 178)}, color={'cn': (161, 193, 248), 'en': (152, 185, 237), 'jp': (145, 183, 250), 'tw': (156, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_3.png', 'en': './assets/en/combat/GET_ITEMS_3.png', 'jp': './assets/jp/combat/GET_ITEMS_3.png', 'tw': './assets/tw/combat/GET_ITEMS_3.png'}) +GET_ITEMS_2 = Button(area={'cn': (538, 146, 742, 182), 'en': (551, 149, 735, 175), 'jp': (536, 146, 741, 182), 'tw': (538, 148, 741, 182)}, color={'cn': (160, 192, 248), 'en': (167, 195, 235), 'jp': (145, 182, 249), 'tw': (155, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_2.png', 'en': './assets/en/combat/GET_ITEMS_2.png', 'jp': './assets/jp/combat/GET_ITEMS_2.png', 'tw': './assets/tw/combat/GET_ITEMS_2.png'}) +GET_ITEMS_3 = Button(area={'cn': (539, 143, 742, 179), 'en': (548, 136, 740, 172), 'jp': (540, 143, 742, 179), 'tw': (546, 145, 742, 178)}, color={'cn': (161, 193, 248), 'en': (152, 185, 237), 'jp': (145, 182, 248), 'tw': (156, 190, 248)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_ITEMS_3.png', 'en': './assets/en/combat/GET_ITEMS_3.png', 'jp': './assets/jp/combat/GET_ITEMS_3.png', 'tw': './assets/tw/combat/GET_ITEMS_3.png'}) GET_ITEMS_3_CHECK = Button(area={'cn': (335, 184, 947, 203), 'en': (335, 184, 947, 203), 'jp': (335, 184, 947, 203), 'tw': (335, 184, 947, 203)}, color={'cn': (84, 95, 109), 'en': (84, 95, 109), 'jp': (84, 95, 109), 'tw': (84, 95, 109)}, button={'cn': (335, 184, 947, 203), 'en': (335, 184, 947, 203), 'jp': (335, 184, 947, 203), 'tw': (335, 184, 947, 203)}, file={'cn': './assets/cn/combat/GET_ITEMS_3_CHECK.png', 'en': './assets/en/combat/GET_ITEMS_3_CHECK.png', 'jp': './assets/jp/combat/GET_ITEMS_3_CHECK.png', 'tw': './assets/tw/combat/GET_ITEMS_3_CHECK.png'}) GET_SHIP = Button(area={'cn': (1104, 610, 1110, 630), 'en': (1104, 610, 1110, 630), 'jp': (1104, 610, 1110, 630), 'tw': (1104, 610, 1110, 630)}, color={'cn': (255, 255, 255), 'en': (255, 255, 255), 'jp': (255, 255, 255), 'tw': (255, 255, 255)}, button={'cn': (1000, 631, 1055, 689), 'en': (999, 630, 1047, 691), 'jp': (1000, 631, 1055, 689), 'tw': (1000, 631, 1055, 689)}, file={'cn': './assets/cn/combat/GET_SHIP.png', 'en': './assets/en/combat/GET_SHIP.png', 'jp': './assets/jp/combat/GET_SHIP.png', 'tw': './assets/tw/combat/GET_SHIP.png'}) LOADING_BAR = Button(area={'cn': (33, 676, 1247, 680), 'en': (33, 676, 1247, 680), 'jp': (33, 676, 1247, 680), 'tw': (33, 676, 1247, 680)}, color={'cn': (172, 205, 232), 'en': (172, 205, 232), 'jp': (172, 205, 232), 'tw': (172, 205, 232)}, button={'cn': (33, 676, 1247, 680), 'en': (33, 676, 1247, 680), 'jp': (33, 676, 1247, 680), 'tw': (33, 676, 1247, 680)}, file={'cn': './assets/cn/combat/LOADING_BAR.png', 'en': './assets/en/combat/LOADING_BAR.png', 'jp': './assets/jp/combat/LOADING_BAR.png', 'tw': './assets/tw/combat/LOADING_BAR.png'}) diff --git a/module/commission/project.py b/module/commission/project.py index efa27bab7..2b3888260 100644 --- a/module/commission/project.py +++ b/module/commission/project.py @@ -1,6 +1,5 @@ from datetime import datetime, timedelta -import module.config.server as server from module.base.decorator import Config from module.base.filter import Filter from module.base.utils import * @@ -23,36 +22,56 @@ ) -class SuffixOcr(Ocr): - """后缀 OCR 识别器,用于识别委托名称末尾的罗马数字后缀。 +def crop_suffix_image(image, area): + """裁剪委托名称右侧的罗马数字后缀图像。 - 预处理时裁剪掉图像右侧空白区域,只保留后缀部分以提高识别准确率。 - """ - - def pre_process(self, image): - """预处理图像,裁剪右侧空白区域以聚焦后缀字符。 - - 通过检测每列像素最小值定位文本右边界,再向左回退若干像素 - 以确保完整保留后缀字符。日服字符较宽,需要更大的回退量。 - - Args: - image: 输入的灰度图像。 - - Returns: - 裁剪后的图像。 - """ - image = super().pre_process(image) + Args: + image: 游戏截图。 + area: 委托名称区域。 - left = np.where(np.min(image[5:-5, :], axis=0) < 85)[0] - # 日服字符较宽,需要回退更多像素 - if server.server in ['jp']: - look_back = 21 - else: - look_back = 18 - if len(left): - image = image[:, left[-1] - look_back:] + Returns: + 后缀裁剪图,黑字白底;未检测到文字时返回 None。 + """ + name_image = crop(image, area) + name_image = extract_letters(name_image, letter=(255, 255, 255), threshold=128).astype(np.uint8) + + line = cv2.reduce(name_image[5:-5, :], 0, cv2.REDUCE_AVG).flatten() + columns = np.where(line < 250)[0] + if not len(columns): + return None + + # 从最右侧文字向左回看,尽量完整包含罗马数字后缀。 + threshold = 250 + look_back = 10 + for i in range(columns[-1], 0, -1): + if line[i] > threshold: + if columns[-1] - i > look_back: + look_back = columns[-1] - i + break + + left = columns[-1] - look_back + right = columns[-1] + 1 + x1, y1 = area[0:2] + suffix_area = area_offset((left - 3, -3, right + 3, name_image.shape[0] + 3), (x1, y1)) + image = crop(image, suffix_area) + image = extract_letters(image, letter=(255, 255, 255), threshold=128).astype(np.uint8) + return image + + +def image_hash(image): + """计算图像哈希,用于日志输出。 + + Args: + image: 输入图像。 + + Returns: + 图像 MD5;图像为空时返回空字符串。 + """ + if image is None: + return '' - return image + import hashlib + return hashlib.md5(image.tobytes()).hexdigest() class Commission: @@ -68,9 +87,10 @@ class Commission: name: str # 委托名称是否解析成功 valid: bool - # 罗马数字后缀,委托无后缀时可能识别错误 - # 值: ⅠⅡⅢⅤⅣⅥ - suffix: str + # 裁剪出的后缀图像,黑字白底;无后缀时为 None + suffix_image: np.ndarray + # 后缀图像哈希,仅用于日志;无后缀时为空字符串 + suffix_hash: str # 委托类型名称,定义在 project_data.py 中 # 值: major_comm, daily_resource, urgent_cube, ... genre: str @@ -146,9 +166,9 @@ def commission_parse(self): self.name = result self.genre = self.commission_name_parse(self.name) - # 后缀识别 - ocr = SuffixOcr(button, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='IV') - self.suffix = self.beautify_name(ocr.ocr(self.image)) + # 后缀图像识别 + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # 执行时长 area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -198,9 +218,9 @@ def commission_parse(self): self.name = result self.genre = self.commission_name_parse(self.name) - # 后缀识别 - ocr = SuffixOcr(button, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='IV') - self.suffix = self.beautify_name(ocr.ocr(self.image)) + # 后缀图像识别 + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # 执行时长 area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -254,9 +274,9 @@ def commission_parse(self): self.name = result self.genre = self.commission_name_parse(self.name) - # 后缀识别 - ocr = SuffixOcr(button, lang='azur_lane', letter=(255, 255, 255), threshold=128, alphabet='IV') - self.suffix = self.beautify_name(ocr.ocr(self.image)) + # 后缀图像识别 + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # 执行时长 area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -292,7 +312,7 @@ def commission_parse(self): def commission_parse(self): """解析委托信息(CN 服务器,默认回退)。 - CN 服后缀直接从名称末尾提取罗马数字,不使用独立 OCR。 + CN 服同样裁剪名称右侧后缀图像,用于后续相似度匹配。 解析内容:名称、后缀、时长、过期时间、状态。 """ # 名称识别 @@ -306,8 +326,9 @@ def commission_parse(self): self.name = result self.genre = self.commission_name_parse(self.name) - # 后缀——直接从名称末尾提取罗马数字 - self.suffix = self.beautify_name(''.join(c for c in result[-4:] if c in 'IV')) + # 后缀图像识别 + self.suffix_image = crop_suffix_image(self.image, self.button.area) + self.suffix_hash = image_hash(self.suffix_image) # 执行时长 area = area_offset((290, 68, 390, 95), self.area[0:2]) @@ -341,7 +362,7 @@ def commission_parse(self): def __str__(self): """返回委托的可读字符串表示,包含名称、类型、状态和时长。""" - name = f'{self.name} | {self.suffix}' + name = f'{self.name} | {self.suffix_hash}' if self.suffix_hash else self.name if not self.valid: return f'{name} (Invalid)' info = {'Genre': self.genre, 'Status': self.status, 'Duration': self.duration} @@ -372,7 +393,7 @@ def __eq__(self, other): if self.genre != other.genre or self.status != other.status: return False if self.category_str == 'daily': - if self.suffix != other.suffix: + if not self.suffix_match(other): return False if self.genre == 'urgent_box': for tag in ['NYB', 'BIW']: @@ -389,7 +410,7 @@ def __eq__(self, other): return False if self.repeat_count != other.repeat_count: return False - if self.genre in ['extra_oil', 'night_oil'] and self.suffix != other.suffix: + if self.genre in ['extra_oil', 'night_oil'] and not self.suffix_match(other): return False return True @@ -398,6 +419,36 @@ def __hash__(self): """返回委托的哈希值,基于类型和名称。""" return hash(f'{self.genre}_{self.name}') + def suffix_match(self, other, similarity=0.75): + """判断两个委托的后缀图像是否匹配。 + + Args: + other: 要比较的委托对象。 + similarity: 相似度阈值,范围 0-1。 + + Returns: + 后缀是否匹配。 + """ + if self.suffix_image is None and other.suffix_image is None: + return True + if self.suffix_image is None or other.suffix_image is None: + return False + + def match(image, template): + template = crop(template, (3, 3, template.shape[1] - 3, template.shape[0] - 3), copy=False) + if image.shape[0] < template.shape[0] or image.shape[1] < template.shape[1]: + return 0.0 + + res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) + _, sim, _, _ = cv2.minMaxLoc(res) + return sim + + sim = max( + match(self.suffix_image, other.suffix_image), + match(other.suffix_image, self.suffix_image) + ) + return sim >= similarity + def parse_time(self, string): """解析时间字符串为 timedelta 对象。 diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 8e3011fa5..69e9daae7 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -2291,7 +2291,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -2800,7 +2800,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -3738,7 +3738,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -4179,7 +4179,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -4620,7 +4620,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -7163,7 +7163,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -7621,7 +7621,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -8079,7 +8079,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -8537,7 +8537,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" @@ -8985,7 +8985,7 @@ "event_20240521_cn" ], "option_jp": [ - "event_20240521_cn" + "event_20260625_cn" ], "option_tw": [ "event_20260520_cn" diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 883383f61..8a4278267 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -1072,7 +1072,7 @@ "event_20260326_cn": "ワンダラー募集計画", "event_20260417_cn": "バケーションレーン・きらめく砂浜", "event_20260520_cn": "聖印前の同盟", - "event_20260625_cn": "美梦巡演:奇妙夜", + "event_20260625_cn": "幻夢のカヴァルカード", "raid_20200624": "特別演習超空強襲波(復刻)", "raid_20210708": "交錯する新たな波 (復刻)", "raid_20220127": "秘密事件調査", diff --git a/module/handler/assets.py b/module/handler/assets.py index 62138d0bd..53a5ff186 100644 --- a/module/handler/assets.py +++ b/module/handler/assets.py @@ -6,8 +6,8 @@ AIR_ATTACK_CONFIRM = Button(area={'cn': (1161, 645, 1222, 675), 'en': (1161, 645, 1222, 675), 'jp': (1161, 645, 1222, 675), 'tw': (1161, 645, 1222, 675)}, color={'cn': (130, 166, 212), 'en': (130, 166, 212), 'jp': (130, 166, 212), 'tw': (130, 166, 212)}, button={'cn': (1161, 645, 1222, 675), 'en': (1161, 645, 1222, 675), 'jp': (1161, 645, 1222, 675), 'tw': (1161, 645, 1222, 675)}, file={'cn': './assets/cn/handler/AIR_ATTACK_CONFIRM.png', 'en': './assets/cn/handler/AIR_ATTACK_CONFIRM.png', 'jp': './assets/cn/handler/AIR_ATTACK_CONFIRM.png', 'tw': './assets/cn/handler/AIR_ATTACK_CONFIRM.png'}) AIR_ATTACK_ENTER = Button(area={'cn': (1194, 456, 1249, 530), 'en': (1194, 456, 1249, 530), 'jp': (1194, 456, 1249, 530), 'tw': (1194, 456, 1249, 530)}, color={'cn': (123, 124, 131), 'en': (123, 124, 131), 'jp': (123, 124, 131), 'tw': (123, 124, 131)}, button={'cn': (1194, 456, 1249, 530), 'en': (1194, 456, 1249, 530), 'jp': (1194, 456, 1249, 530), 'tw': (1194, 456, 1249, 530)}, file={'cn': './assets/cn/handler/AIR_ATTACK_ENTER.png', 'en': './assets/cn/handler/AIR_ATTACK_ENTER.png', 'jp': './assets/cn/handler/AIR_ATTACK_ENTER.png', 'tw': './assets/cn/handler/AIR_ATTACK_ENTER.png'}) -AIR_STRIKE_CANCEL = Button(area={'cn': (948, 644, 1009, 676), 'en': (948, 644, 1009, 676), 'jp': (948, 644, 1009, 676), 'tw': (948, 644, 1009, 676)}, color={'cn': (194, 124, 117), 'en': (194, 124, 117), 'jp': (194, 124, 117), 'tw': (194, 124, 117)}, button={'cn': (948, 644, 1009, 676), 'en': (948, 644, 1009, 676), 'jp': (948, 644, 1009, 676), 'tw': (948, 644, 1009, 676)}, file={'cn': './assets/cn/handler/AIR_STRIKE_CANCEL.png', 'en': './assets/cn/handler/AIR_STRIKE_CANCEL.png', 'jp': './assets/jp/handler/AIR_STRIKE_CANCEL.png', 'tw': './assets/cn/handler/AIR_STRIKE_CANCEL.png'}) -AIR_STRIKE_CONFIRM = Button(area={'cn': (1161, 644, 1222, 675), 'en': (1161, 644, 1222, 675), 'jp': (1161, 644, 1222, 675), 'tw': (1161, 644, 1222, 675)}, color={'cn': (137, 170, 211), 'en': (137, 170, 211), 'jp': (137, 170, 211), 'tw': (137, 170, 211)}, button={'cn': (1161, 644, 1222, 675), 'en': (1161, 644, 1222, 675), 'jp': (1161, 644, 1222, 675), 'tw': (1161, 644, 1222, 675)}, file={'cn': './assets/cn/handler/AIR_STRIKE_CONFIRM.png', 'en': './assets/cn/handler/AIR_STRIKE_CONFIRM.png', 'jp': './assets/jp/handler/AIR_STRIKE_CONFIRM.png', 'tw': './assets/cn/handler/AIR_STRIKE_CONFIRM.png'}) +AIR_STRIKE_CANCEL = Button(area={'cn': (948, 644, 1009, 676), 'en': (948, 644, 1009, 676), 'jp': (948, 644, 1009, 676), 'tw': (889, 646, 946, 674)}, color={'cn': (194, 124, 117), 'en': (194, 124, 117), 'jp': (194, 124, 117), 'tw': (207, 162, 159)}, button={'cn': (948, 644, 1009, 676), 'en': (948, 644, 1009, 676), 'jp': (948, 644, 1009, 676), 'tw': (889, 646, 946, 674)}, file={'cn': './assets/cn/handler/AIR_STRIKE_CANCEL.png', 'en': './assets/cn/handler/AIR_STRIKE_CANCEL.png', 'jp': './assets/jp/handler/AIR_STRIKE_CANCEL.png', 'tw': './assets/tw/handler/AIR_STRIKE_CANCEL.png'}) +AIR_STRIKE_CONFIRM = Button(area={'cn': (1161, 644, 1222, 675), 'en': (1161, 644, 1222, 675), 'jp': (1161, 644, 1222, 675), 'tw': (1102, 646, 1161, 674)}, color={'cn': (137, 170, 211), 'en': (137, 170, 211), 'jp': (137, 170, 211), 'tw': (155, 181, 216)}, button={'cn': (1161, 644, 1222, 675), 'en': (1161, 644, 1222, 675), 'jp': (1161, 644, 1222, 675), 'tw': (1102, 646, 1161, 674)}, file={'cn': './assets/cn/handler/AIR_STRIKE_CONFIRM.png', 'en': './assets/cn/handler/AIR_STRIKE_CONFIRM.png', 'jp': './assets/jp/handler/AIR_STRIKE_CONFIRM.png', 'tw': './assets/tw/handler/AIR_STRIKE_CONFIRM.png'}) AIR_STRIKE_ENTER = Button(area={'cn': (1194, 456, 1249, 530), 'en': (1194, 456, 1249, 530), 'jp': (1194, 456, 1249, 530), 'tw': (1194, 456, 1249, 530)}, color={'cn': (123, 124, 131), 'en': (123, 124, 131), 'jp': (123, 124, 131), 'tw': (123, 124, 131)}, button={'cn': (1194, 456, 1249, 530), 'en': (1194, 456, 1249, 530), 'jp': (1194, 456, 1249, 530), 'tw': (1194, 456, 1249, 530)}, file={'cn': './assets/cn/handler/AIR_STRIKE_ENTER.png', 'en': './assets/cn/handler/AIR_STRIKE_ENTER.png', 'jp': './assets/cn/handler/AIR_STRIKE_ENTER.png', 'tw': './assets/cn/handler/AIR_STRIKE_ENTER.png'}) ANDROID_NO_RESPOND = Button(area={'cn': (341, 433, 391, 472), 'en': (341, 433, 391, 472), 'jp': (341, 433, 391, 472), 'tw': (341, 433, 391, 472)}, color={'cn': (217, 237, 235), 'en': (217, 237, 235), 'jp': (217, 237, 235), 'tw': (217, 237, 235)}, button={'cn': (341, 433, 391, 472), 'en': (341, 433, 391, 472), 'jp': (341, 433, 391, 472), 'tw': (341, 433, 391, 472)}, file={'cn': './assets/cn/handler/ANDROID_NO_RESPOND.png', 'en': './assets/en/handler/ANDROID_NO_RESPOND.png', 'jp': './assets/jp/handler/ANDROID_NO_RESPOND.png', 'tw': './assets/tw/handler/ANDROID_NO_RESPOND.png'}) AUTO_SEARCH_MAP_OPTION_OFF = Button(area={'cn': (1205, 549, 1275, 566), 'en': (1203, 552, 1277, 564), 'jp': (1204, 547, 1276, 568), 'tw': (1205, 546, 1275, 567)}, color={'cn': (196, 169, 169), 'en': (151, 132, 138), 'jp': (179, 153, 156), 'tw': (153, 132, 137)}, button={'cn': (1205, 549, 1275, 566), 'en': (1203, 552, 1277, 564), 'jp': (1204, 547, 1276, 568), 'tw': (1205, 546, 1275, 567)}, file={'cn': './assets/cn/handler/AUTO_SEARCH_MAP_OPTION_OFF.png', 'en': './assets/en/handler/AUTO_SEARCH_MAP_OPTION_OFF.png', 'jp': './assets/jp/handler/AUTO_SEARCH_MAP_OPTION_OFF.png', 'tw': './assets/tw/handler/AUTO_SEARCH_MAP_OPTION_OFF.png'}) diff --git a/module/island_daily_interact/assets.py b/module/island_daily_interact/assets.py index cb1e2bf83..df76c922e 100644 --- a/module/island_daily_interact/assets.py +++ b/module/island_daily_interact/assets.py @@ -23,7 +23,7 @@ ROUTE_BUSINESS_NURSERY_COMPLETE = Button(area={'cn': (801, 312, 1033, 344), 'en': (801, 312, 1033, 344), 'jp': (801, 312, 1033, 344), 'tw': (801, 312, 1033, 344)}, color={'cn': (233, 235, 233), 'en': (233, 235, 233), 'jp': (233, 235, 233), 'tw': (233, 235, 233)}, button={'cn': (801, 312, 1033, 344), 'en': (801, 312, 1033, 344), 'jp': (801, 312, 1033, 344), 'tw': (801, 312, 1033, 344)}, file={'cn': './assets/cn/island_daily_interact/ROUTE_BUSINESS_NURSERY_COMPLETE.png', 'en': './assets/cn/island_daily_interact/ROUTE_BUSINESS_NURSERY_COMPLETE.png', 'jp': './assets/cn/island_daily_interact/ROUTE_BUSINESS_NURSERY_COMPLETE.png', 'tw': './assets/cn/island_daily_interact/ROUTE_BUSINESS_NURSERY_COMPLETE.png'}) ROUTE_JUU_NURSERY_COMPLETE = Button(area={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, color={'cn': (232, 233, 233), 'en': (232, 233, 233), 'jp': (232, 233, 233), 'tw': (232, 233, 233)}, button={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, file={'cn': './assets/cn/island_daily_interact/ROUTE_JUU_NURSERY_COMPLETE.png', 'en': './assets/cn/island_daily_interact/ROUTE_JUU_NURSERY_COMPLETE.png', 'jp': './assets/cn/island_daily_interact/ROUTE_JUU_NURSERY_COMPLETE.png', 'tw': './assets/cn/island_daily_interact/ROUTE_JUU_NURSERY_COMPLETE.png'}) ROUTE_PLAIN_COMPLETE = Button(area={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, color={'cn': (232, 233, 233), 'en': (232, 233, 233), 'jp': (232, 233, 233), 'tw': (232, 233, 233)}, button={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, file={'cn': './assets/cn/island_daily_interact/ROUTE_PLAIN_COMPLETE.png', 'en': './assets/cn/island_daily_interact/ROUTE_PLAIN_COMPLETE.png', 'jp': './assets/cn/island_daily_interact/ROUTE_PLAIN_COMPLETE.png', 'tw': './assets/cn/island_daily_interact/ROUTE_PLAIN_COMPLETE.png'}) -ROUTE_PORT_BUSINESS_COMPLETE = Button(area={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, color={'cn': (232, 233, 233), 'en': (232, 233, 233), 'jp': (232, 233, 233), 'tw': (232, 233, 233)}, button={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, file={'cn': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png', 'en': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png', 'jp': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png', 'tw': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png'}) +ROUTE_PORT_BUSINESS_COMPLETE = Button(area={'cn': (801, 311, 1033, 340), 'en': (801, 311, 1033, 340), 'jp': (801, 311, 1033, 340), 'tw': (801, 311, 1033, 340)}, color={'cn': (236, 237, 237), 'en': (236, 237, 237), 'jp': (236, 237, 237), 'tw': (236, 237, 237)}, button={'cn': (801, 311, 1033, 340), 'en': (801, 311, 1033, 340), 'jp': (801, 311, 1033, 340), 'tw': (801, 311, 1033, 340)}, file={'cn': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png', 'en': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png', 'jp': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png', 'tw': './assets/cn/island_daily_interact/ROUTE_PORT_BUSINESS_COMPLETE.png'}) ROUTE_PORT_COMPLETE = Button(area={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, color={'cn': (232, 233, 233), 'en': (232, 233, 233), 'jp': (232, 233, 233), 'tw': (232, 233, 233)}, button={'cn': (801, 344, 1033, 373), 'en': (801, 344, 1033, 373), 'jp': (801, 344, 1033, 373), 'tw': (801, 344, 1033, 373)}, file={'cn': './assets/cn/island_daily_interact/ROUTE_PORT_COMPLETE.png', 'en': './assets/cn/island_daily_interact/ROUTE_PORT_COMPLETE.png', 'jp': './assets/cn/island_daily_interact/ROUTE_PORT_COMPLETE.png', 'tw': './assets/cn/island_daily_interact/ROUTE_PORT_COMPLETE.png'}) TEMPLATE_BUSINESS_DELIVERY_TASK_ICON = Template(file={'cn': './assets/cn/island_daily_interact/TEMPLATE_BUSINESS_DELIVERY_TASK_ICON.png', 'en': './assets/cn/island_daily_interact/TEMPLATE_BUSINESS_DELIVERY_TASK_ICON.png', 'jp': './assets/cn/island_daily_interact/TEMPLATE_BUSINESS_DELIVERY_TASK_ICON.png', 'tw': './assets/cn/island_daily_interact/TEMPLATE_BUSINESS_DELIVERY_TASK_ICON.png'}) TEMPLATE_JUU_EXPRESS_TASK_ICON = Template(file={'cn': './assets/cn/island_daily_interact/TEMPLATE_JUU_EXPRESS_TASK_ICON.png', 'en': './assets/cn/island_daily_interact/TEMPLATE_JUU_EXPRESS_TASK_ICON.png', 'jp': './assets/cn/island_daily_interact/TEMPLATE_JUU_EXPRESS_TASK_ICON.png', 'tw': './assets/cn/island_daily_interact/TEMPLATE_JUU_EXPRESS_TASK_ICON.png'}) diff --git a/module/webui/api.py b/module/webui/api.py index f479e76d6..f830d0c93 100644 --- a/module/webui/api.py +++ b/module/webui/api.py @@ -320,8 +320,10 @@ async def _collect_ws_scrcpy_preroll(session, max_wait=3.0): data = await asyncio.wait_for(session.recv(), timeout=timeout) except asyncio.TimeoutError: continue - if not data: + if data is None: break + if data == b"": + continue parsed = _ws_scrcpy_parse_initial(data) if parsed: display = (parsed.get("displays") or [{}])[0] @@ -359,6 +361,7 @@ def _live_preview_error_message(error): WS_SCRCPY_FILEPATH_REMOTE = "/data/local/tmp/ws-scrcpy-server-v1.19-ws7.jar" WS_SCRCPY_MAGIC_INITIAL = b"scrcpy_initial" WS_SCRCPY_DEVICE_NAME_LENGTH = 64 +WS_SCRCPY_PID_FILE_REMOTE = "/data/local/tmp/ws_scrcpy.pid" def _ws_scrcpy_int(data, offset): @@ -523,14 +526,19 @@ def _server_command(self): ] return f"CLASSPATH={WS_SCRCPY_FILEPATH_REMOTE} nohup app_process {' '.join(args)} >/dev/null 2>&1 &" - def _local_port_open(self): - if not self.local_port: + def _server_running(self): + try: + pid = self.connection.adb_shell(f"test -f {WS_SCRCPY_PID_FILE_REMOTE} && cat {WS_SCRCPY_PID_FILE_REMOTE}", timeout=2) + pid = str(pid or "").strip().split()[0] + except Exception: + pid = "" + if not pid: return False try: - with socket.create_connection(("127.0.0.1", int(self.local_port)), timeout=0.3): - return True - except OSError: + cmdline = self.connection.adb_shell(f"cat /proc/{int(pid)}/cmdline", timeout=2, rstrip=False) + except Exception: return False + return WS_SCRCPY_PACKAGE in str(cmdline) and WS_SCRCPY_VERSION in str(cmdline) def start_server(self): if not os.path.exists(WS_SCRCPY_FILEPATH_LOCAL): @@ -539,7 +547,7 @@ def start_server(self): logger.hr("实时 ws-scrcpy 预览启动") self.connection.adb_push(WS_SCRCPY_FILEPATH_LOCAL, WS_SCRCPY_FILEPATH_REMOTE) self.local_port = self.connection.adb_forward(f"tcp:{WS_SCRCPY_PORT}") - if self._local_port_open(): + if self._server_running(): logger.info("ws-scrcpy server 已在运行,复用设备端服务") return output = self.connection.adb_shell(self._server_command(), timeout=2) @@ -558,7 +566,7 @@ async def connect(self): last_error = None for _index in range(20): try: - self.remote_ws = await connect(url, max_size=None) + self.remote_ws = await connect(url, max_size=None, ping_interval=None, close_timeout=1) self.alive = True return except Exception as e: @@ -1062,8 +1070,10 @@ async def _ws_live_ws_scrcpy(websocket, instance, fps, target_width, bitrate_sca while session.alive: data = await session.recv() - if not data: + if data is None: break + if data == b"": + continue parsed = _ws_scrcpy_parse_initial(data) if parsed: display = (parsed.get("displays") or [{}])[0]