From c1b1199517e4c035cae06de0dd53a8f9f2fdfa6f Mon Sep 17 00:00:00 2001 From: magicemptycity <2214463059@qq.com> Date: Mon, 8 Jun 2026 19:05:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(KekkaiActivation):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=8D=A1=E7=89=87=E6=98=9F=E7=BA=A7=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96OCR=E8=AF=86=E5=88=AB=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/i18n/zh-CN.json | 3 +- tasks/KekkaiActivation/config.py | 14 +- tasks/KekkaiActivation/script_task.py | 242 +++++++++++++++++++------- 3 files changed, 196 insertions(+), 63 deletions(-) diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 737c2ae8a..413d1ad62 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -151,5 +151,6 @@ "enable_switch_ap100": "是否切换100体爬塔御魂", "ap100_group_team": "100体组号,队伍号", "enable_switch_ap100_by_name": "是否通过ocr切换100体御魂", - "ap100_group_team_name": "100体组名,队伍名" + "ap100_group_team_name": "100体组名,队伍名", + "card_star": "星级" } \ No newline at end of file diff --git a/tasks/KekkaiActivation/config.py b/tasks/KekkaiActivation/config.py index 953af4561..34b9f4fb1 100644 --- a/tasks/KekkaiActivation/config.py +++ b/tasks/KekkaiActivation/config.py @@ -11,7 +11,17 @@ class CardType(str, Enum): FISH = '斗鱼' TAIKO = '太鼓' - + + +class CardStar(str, Enum): + SIX = '6星' + FIVE = '5星' + #FOUR = '4星' + #THREE = '3星' + # 四五星识图不稳暂时去掉 + def to_int(self) -> int: + return int(self.value[0]) + class ActivationScheduler(Scheduler): priority: int = Field(default=2, description='priority_help') @@ -21,6 +31,8 @@ class ActivationScheduler(Scheduler): class ActivationConfig(BaseModel): card_type: CardType = Field(default=CardType.TAIKO, description='card_rule_help') + card_star: CardStar = Field(default=CardStar.SIX, description='6星:六星及以下、5星:五星及以下') + swipe_retry_limit: int = Field(default=10, description='最多滑动X次后触发未找到符合条件的卡') min_taiko_num: int = Field(default=8, description='挂卡太鼓每小时最少收益,低于则不挂卡') min_fish_num: int = Field(default=16, description='挂卡斗鱼每小时最少收益,低于则不挂卡') exchange_before: bool = Field(default=True, description='exchange_before_help') diff --git a/tasks/KekkaiActivation/script_task.py b/tasks/KekkaiActivation/script_task.py index 81c6a7428..68e78f86e 100644 --- a/tasks/KekkaiActivation/script_task.py +++ b/tasks/KekkaiActivation/script_task.py @@ -1,6 +1,8 @@ # This Python file uses the following encoding: utf-8 # @author runhey # github https://github.com/runhey +from typing import Dict, List, Tuple, Optional, Any, Union +import numpy as np import time import random @@ -21,10 +23,9 @@ from tasks.KekkaiUtilize.utils import CardClass from tasks.KekkaiActivation.assets import KekkaiActivationAssets from tasks.KekkaiActivation.utils import parse_rule -from tasks.KekkaiActivation.config import ActivationConfig +from tasks.KekkaiActivation.config import ActivationConfig, CardType, CardStar from tasks.Utils.config_enum import ShikigamiClass from tasks.GameUi.page import page_main, page_guild -from tasks.KekkaiActivation.config import CardType """ 结界挂卡 """ class ScriptTask(KU, KekkaiActivationAssets): @@ -71,17 +72,121 @@ def dict_card_image(self) -> dict: @cached_property def dict_image_card(self) -> dict: return {v: k for k, v in self.dict_card_image.items()} - + @cached_property - def order_targets(self) -> ImageGrid: - rule = self.config.kekkai_activation.activation_config.card_type - if rule == CardType.TAIKO: - return ImageGrid([self.I_CARDS_KAIKO_6, self.I_CARDS_KAIKO_5]) - elif rule == CardType.FISH: - return ImageGrid([self.I_CARDS_FISH_6, self.I_CARDS_FISH_5]) - else: - logger.error('Unknown utilize rule') - raise ValueError('Unknown utilize rule') + def star_mapping(self) -> dict: + """根据配置的星级返回目标卡和干扰卡的图片列表""" + card_star = self.config.kekkai_activation.activation_config.card_star + target_star = card_star.to_int() # 获取目标星级数值 + + # 定义卡片列表 + cards = { + CardType.TAIKO: [ + (6, self.I_CARDS_KAIKO_6), + (5, self.I_CARDS_KAIKO_5), + (4, self.I_CARDS_KAIKO_4), + (3, self.I_CARDS_KAIKO_3), + ], + CardType.FISH: [ + (6, self.I_CARDS_FISH_6), + (5, self.I_CARDS_FISH_5), + (4, self.I_CARDS_FISH_4), + (3, self.I_CARDS_FISH_3), + ] + } + + result = {'target_images': {}, 'higher_images': {}} + + for card_type, card_list in cards.items(): + # 目标卡:星级 ≤ 目标星级 + result['target_images'][card_type] = [ + card for star, card in card_list if star <= target_star + ] + # 干扰卡:星级 > 目标星级 + result['higher_images'][card_type] = [ + card for star, card in card_list if star > target_star + ] + + return result + + def _select_best_card_from_results( + self, + filtered_results: List[Any], + min_card_num: int, + ocr_count: int + ) -> Optional[RuleClick]: + """ + 从过滤后的OCR结果中选择最佳卡片 + :param filtered_results: 过滤后的OCR结果列表 + :param min_card_num: 最低收益阈值 + :param ocr_count: 当前OCR计数 + :return: 选中的RuleClick对象,或None + """ + logger.info(f"第{ocr_count}次滑动后识别到卡: {[result.ocr_text for result in filtered_results]}") + + # 提取数字并按数字排序 + numeric_results = [] + for result in filtered_results: + numbers = [int(num) for num in re.findall(r'\d+', result.ocr_text)] + if numbers: + if numbers[0] < min_card_num: + logger.debug(f"卡牌收益 {numbers[0]} 低于阈值 {min_card_num},跳过") + continue + numeric_results.append((numbers[0], result)) + + if numeric_results: + # 按数字大到小排序 + sorted_results = [result for _, result in sorted(numeric_results, key=lambda x: x[0], reverse=True)] + max_result = sorted_results[0] + + return self._create_click_target(max_result) + + return None + + + def _create_click_target(self, ocr_result: Any) -> RuleClick: + """ + 创建点击目标 + :param ocr_result: OCR结果对象 + :return: RuleClick对象 + """ + box = ocr_result.box + x_min = self.O_CHECK_CARD_NUMBER.roi[0] + box[0][0] + y_min = self.O_CHECK_CARD_NUMBER.roi[1] + box[0][1] + width = box[1][0] - box[0][0] + height = box[2][1] - box[1][1] + roi = int(x_min), int(y_min), int(width), int(height) + + return RuleClick(roi_front=roi, roi_back=roi, name="tmpclick") + + def _perform_swipe_action(self, current_ocr_count: int, swipe_limit: int, reason: str = "未找到符合条件的卡") -> Tuple[bool, int]: + """ + 执行滑动操作 + + :param current_ocr_count: 当前OCR计数 + :param swipe_limit: 滑动次数限制 + :param reason: 滑动原因,用于日志记录 + :return: (是否成功, 新的OCR计数) + """ + new_ocr_count = current_ocr_count + 1 + + # 检查是否达到滑动上限 + if new_ocr_count >= swipe_limit: + logger.warning(f'已达到滑动次数上限({swipe_limit}),{reason}') + return False, new_ocr_count + + # 执行滑动操作 + logger.info(f"{reason},准备第{new_ocr_count}次滑动") + duration = 2 + safe_pos_x = random.randint(200, 400) + safe_pos_y = random.randint(580, 600) + p1 = (safe_pos_x, safe_pos_y) + p2 = (safe_pos_x, safe_pos_y - 410) + logger.info('Swipe %s -> %s, %sS ' % (point2str(*p1), point2str(*p2), duration)) + self.device.swipe_adb(p1, p2, duration=duration) + time.sleep(1) + self.screenshot() # 确保截图更新 + return True, new_ocr_count def run_activation(self, _config: ActivationConfig) -> bool: """ @@ -236,66 +341,81 @@ def screening_card(self, rule: str): continue def check_card_num(self): - rule = self.config.kekkai_activation.activation_config.card_type + con = self.config.kekkai_activation.activation_config + rule = con.card_type + card_star = con.card_star + swipe_limit = con.swipe_retry_limit + if rule == CardType.TAIKO: - min_card_num = self.config.kekkai_activation.activation_config.min_taiko_num + min_card_num = con.min_taiko_num check_card = "勾玉" elif rule == CardType.FISH: - min_card_num = self.config.kekkai_activation.activation_config.min_fish_num + min_card_num = con.min_fish_num check_card = "体力" else: logger.error('Unknown utilize rule') raise ValueError('Unknown utilize rule') - + + # 获取目标卡和干扰卡图片列表 + star_mapping = self.star_mapping + target_images = star_mapping['target_images'].get(rule, []) + higher_images = star_mapping['higher_images'].get(rule, []) + ocr_count = 0 - while 1: + has_ever_seen_target = False + + while True: self.screenshot() - results = self.O_CHECK_CARD_NUMBER.detect_and_ocr(self.device.image) - ocr_count += 1 - # 第一步:筛选出包含 "体力或者勾玉" 的结果 - filtered_results = [result for result in results if check_card in result.ocr_text] - logger.info(f"识别到卡: {[result.ocr_text for result in filtered_results]}") - - # 第二步:提取数字并按数字排序 - numeric_results = [] - for result in filtered_results: - # 使用正则表达式提取所有数字 - numbers = [int(num) for num in re.findall(r'\d+', result.ocr_text)] - if numbers: # 如果提取到数字 - if numbers[0] < min_card_num: + + # 如果从未见过目标卡,检查当前页面是否有目标卡 + if not has_ever_seen_target: + for target_image in target_images: + if self.appear(target_image): + has_ever_seen_target = True + logger.info(f'首次检测到目标卡: {target_image.name}') + break + + if has_ever_seen_target: + # 检查是否有干扰卡(适用于所有有干扰卡的模式) + has_higher = False + for higher_image in higher_images: + if self.appear(higher_image): + has_higher = True + logger.info(f'检测到干扰卡: {higher_image.name}') + break + + if has_higher: + logger.info(f'{card_star.value}模式下检测到干扰卡,跳过当前页') + success, ocr_count = self._perform_swipe_action( + ocr_count, swipe_limit, "跳过当前页" + ) + if not success: + return None continue - numeric_results.append((numbers[0], result)) # 按第一个数字排序 - - if numeric_results: - # 按数字大到小排序 - sorted_results = [result for _, result in sorted(numeric_results, key=lambda x: x[0], reverse=True)] - max_result = sorted_results[0] # 获取数字最大的结果对象 - - box = max_result.box # 获取边界框坐标 - x_min = self.O_CHECK_CARD_NUMBER.roi[0] + box[0][0] - y_min = self.O_CHECK_CARD_NUMBER.roi[1] + box[0][1] - width = box[1][0] - box[0][0] - height = box[2][1] - box[1][1] - roi = int(x_min), int(y_min), int(width), int(height) - - target = RuleClick(roi_front=roi, roi_back=roi, name="tmpclick") - logger.info(f"选择挂卡: [{max_result.ocr_text}] {roi}") + else: + # 没有找到目标卡,执行常规滑动 + success, ocr_count = self._perform_swipe_action(ocr_count, swipe_limit, "未找到目标卡,滑动") + if not success: + return None + continue - return target - else: - if ocr_count > 3: - logger.error('多次未找到符合条件的结果, 退出') - return None - logger.warning("未找到符合条件的结果, 准备往上滑动") - duration = 2 - safe_pos_x = random.randint(200, 400) - safe_pos_y = random.randint(580, 600) - p1 = (safe_pos_x, safe_pos_y) - p2 = (safe_pos_x, safe_pos_y - 410) - logger.info('Swipe %s -> %s, %sS ' % (point2str(*p1), point2str(*p2), duration)) - self.device.swipe_adb(p1, p2, duration=duration) - time.sleep(1) - continue + # 常规OCR识别逻辑 + results = self.O_CHECK_CARD_NUMBER.detect_and_ocr(self.device.image) + + # 执行常规OCR检查 + filtered_results = [result for result in results if check_card in result.ocr_text] + + # 从结果中选择最佳卡片 + result = self._select_best_card_from_results(filtered_results, min_card_num, ocr_count) + if result: + logger.info(f"选择挂卡: [{result.name}]") + return result + + # 使用滑动函数处理常规滑动 + success, ocr_count = self._perform_swipe_action(ocr_count, swipe_limit) + if not success: + return None + continue def _card_not_found(self): # 获取配置引用 From e740f801ea7f545f4aebd2701f06d8f907277b24 Mon Sep 17 00:00:00 2001 From: magicemptycity <2214463059@qq.com> Date: Wed, 17 Jun 2026 23:55:46 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(TalismanPass):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=94=B6=E5=8F=96=E8=8A=B1=E5=90=88=E6=88=98=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=E5=A5=96=E5=8A=B1=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=BA=A2=E7=82=B9=E8=AF=86=E5=88=AB=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/i18n/zh-CN.json | 4 ++- tasks/TalismanPass/assets.py | 10 +++++--- tasks/TalismanPass/config.py | 1 + tasks/TalismanPass/script_task.py | 10 ++++++-- tasks/TalismanPass/tp/image.json | 25 +++++++++++++------ tasks/TalismanPass/tp/tp_red_point_day.png | Bin 1547 -> 811 bytes tasks/TalismanPass/tp/tp_red_point_level.png | Bin 1216 -> 811 bytes tasks/TalismanPass/tp/tp_red_point_month.png | Bin 0 -> 811 bytes tasks/TalismanPass/tp/tp_red_point_task.png | Bin 1459 -> 609 bytes tasks/TalismanPass/tp/tp_red_point_week.png | Bin 1526 -> 811 bytes 10 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 tasks/TalismanPass/tp/tp_red_point_month.png diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 413d1ad62..29c5d98a2 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -152,5 +152,7 @@ "ap100_group_team": "100体组号,队伍号", "enable_switch_ap100_by_name": "是否通过ocr切换100体御魂", "ap100_group_team_name": "100体组名,队伍名", - "card_star": "星级" + "card_star": "星级", + "get_flower": "启用收取等级奖励" + } \ No newline at end of file diff --git a/tasks/TalismanPass/assets.py b/tasks/TalismanPass/assets.py index abe660255..8f899edcd 100644 --- a/tasks/TalismanPass/assets.py +++ b/tasks/TalismanPass/assets.py @@ -14,13 +14,15 @@ class TalismanPassAssets: # 领取全部 I_TP_GET_ALL = RuleImage(roi_front=(903,599,70,71), roi_back=(903,599,70,71), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_tp_get_all.png") # 任务 的右上方红点 - I_RED_POINT_TASK = RuleImage(roi_front=(1226,312,23,24), roi_back=(1226,312,23,24), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_task.png") + I_RED_POINT_TASK = RuleImage(roi_front=(1218,286,45,45), roi_back=(1218,286,45,45), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_task.png") # 今日 的右上方红点 - I_RED_POINT_DAY = RuleImage(roi_front=(632,157,23,26), roi_back=(632,157,23,26), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_day.png") + I_RED_POINT_DAY = RuleImage(roi_front=(633,153,35,35), roi_back=(633,153,35,35), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_day.png") # 本周 的右上方红点 - I_RED_POINT_WEEK = RuleImage(roi_front=(795,156,24,25), roi_back=(795,156,24,25), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_week.png") + I_RED_POINT_WEEK = RuleImage(roi_front=(795,153,35,35), roi_back=(795,153,35,35), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_week.png") + # 本月 的右上方红点(截点用) + I_RED_POINT_MONTH = RuleImage(roi_front=(966,162,17,17), roi_back=(966,162,17,17), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_month.png") # 等级奖励 - I_RED_POINT_LEVEL = RuleImage(roi_front=(1222,174,21,22), roi_back=(1214,165,40,43), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_level.png") + I_RED_POINT_LEVEL = RuleImage(roi_front=(1215,154,45,45), roi_back=(1215,154,45,45), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_red_point_level.png") # 选择一号奖励 I_TP_LEVEL_1 = RuleImage(roi_front=(203,435,122,59), roi_back=(203,435,122,59), threshold=0.8, method="Template matching", file="./tasks/TalismanPass/tp/tp_tp_level_1.png") # 选择二号奖励 diff --git a/tasks/TalismanPass/config.py b/tasks/TalismanPass/config.py index 6f1ee65c7..5013ef62e 100644 --- a/tasks/TalismanPass/config.py +++ b/tasks/TalismanPass/config.py @@ -18,6 +18,7 @@ class LevelReward(str, Enum): THREE = '体力/樱饼' class TalismanConfig(BaseModel): + get_flower: bool = Field(default=False, description='收取花合战等级奖励') level_reward: LevelReward = Field(default=LevelReward.TWO) harvest_soul: bool = Field(default=False, description='收获1500签御魂') diff --git a/tasks/TalismanPass/script_task.py b/tasks/TalismanPass/script_task.py index c7a57819d..2e63360e2 100644 --- a/tasks/TalismanPass/script_task.py +++ b/tasks/TalismanPass/script_task.py @@ -19,11 +19,12 @@ def run(self): self.goto_page(page_daily) con: TalismanConfig = self.config.talisman_pass.talisman - # 收取全部奖励 + # 收取任务全部奖励 if self.in_task(): self.get_all() # 收取花合战等级奖励 - self.get_flower(con.level_reward) + if con.get_flower: + self.get_flower(con.level_reward) # 收取1500签御魂 if con.harvest_soul: self.goto_page(page_main) @@ -91,6 +92,11 @@ def in_task(self) -> bool: self.screenshot() if self.appear(self.I_TP_GOTO) or self.appear(self.I_TP_EXP): return True + if self.appear(self.I_RED_POINT_TASK): + self.click(self.I_RED_POINT_TASK) + logger.info('Appear task reward') + return True + logger.info('No any task reward') return False def harvest_soul(self): diff --git a/tasks/TalismanPass/tp/image.json b/tasks/TalismanPass/tp/image.json index 112e28d73..543a2557d 100644 --- a/tasks/TalismanPass/tp/image.json +++ b/tasks/TalismanPass/tp/image.json @@ -11,8 +11,8 @@ { "itemName": "red_point_task", "imageName": "tp_red_point_task.png", - "roiFront": "1226,312,23,24", - "roiBack": "1226,312,23,24", + "roiFront": "1218,286,45,45", + "roiBack": "1218,286,45,45", "method": "Template matching", "threshold": 0.8, "description": "任务 的右上方红点" @@ -20,8 +20,8 @@ { "itemName": "red_point_day", "imageName": "tp_red_point_day.png", - "roiFront": "632,157,23,26", - "roiBack": "632,157,23,26", + "roiFront": "633,153,35,35", + "roiBack": "633,153,35,35", "method": "Template matching", "threshold": 0.8, "description": "今日 的右上方红点" @@ -29,8 +29,8 @@ { "itemName": "red_point_week", "imageName": "tp_red_point_week.png", - "roiFront": "795,156,24,25", - "roiBack": "795,156,24,25", + "roiFront": "795,153,35,35", + "roiBack": "795,153,35,35", "method": "Template matching", "threshold": 0.8, "description": "本周 的右上方红点" @@ -38,8 +38,8 @@ { "itemName": "red_point_level", "imageName": "tp_red_point_level.png", - "roiFront": "1222,174,21,22", - "roiBack": "1214,165,40,43", + "roiFront": "1215,154,45,45", + "roiBack": "1215,154,45,45", "method": "Template matching", "threshold": 0.8, "description": "等级奖励" @@ -124,5 +124,14 @@ "method": "Template matching", "threshold": 0.8, "description": "六星御魂标志" + }, + { + "itemName": "red_point_month", + "imageName": "tp_red_point_month.png", + "roiFront": "966,162,17,17", + "roiBack": "966,162,17,17", + "method": "Template matching", + "threshold": 0.8, + "description": "本月 的右上方红点(截点用)" } ] \ No newline at end of file diff --git a/tasks/TalismanPass/tp/tp_red_point_day.png b/tasks/TalismanPass/tp/tp_red_point_day.png index ccbb1586837802da1ba9b741d97393b36dfd3c16..7cee50c5756dcb3989e24ca47b279fc1f8834b56 100644 GIT binary patch literal 811 zcmV+`1JwM9P)1@y zU2Icj90%av?|si%Z9B$jCsbU+X0_mS2{ITuLX?;VZw!mX8)95EUP!$0Gw}laOuXWP z#F(I07!5?6216u9-3ZyH-~@FD*~nn-qg!V7(X~D8dQRW>Z)NE7@R{zn3SL1gb8o6~ zbGmMeu57r2VP7F@0%C#85Hm#Ixl1D>lYZGaZir7k{b~>t+P5|DN!^oP`FmYv!tp%h zedK*{oM26)9IG$4q|OzcZ~HD@`E#sT4A`~*U~^;q*(cgsua4am+x6^$YYgtRp^SY@qhrq&PvNu?lyDiMeTuEWJ8zoY$p`hNBweYfqi(<_--tu-Bo zByJ)8BSOHK3cO;l`{DjAt^DrD;qBl2C|5_7)>H~400JNh5Vx_sf{O8<+}qo9ACJHH zX2LeNZEg^tHd;Qg|7_iQzWw^kkLgnC#2IHP2NB#NNY1e{o`*nOaXZG32Yna{uY zn#jV=riKU4^sXBn4`uVp7#}F(UqafDYiiuN_D3=uy9Op^7qVr(ap=hG)Q!7h&gPg( z43FMDa4Ecytq1}LNE33G)0>u3jU&xlf4Vka@JxL?!K25%oEX0z1b&UlhHTNT_!LQa zxrFZpwUKiDS})mreSGGRYtu%;k!UQ|kl^tz`Z800&CE<{ii1FjuWWfGpD%eOs}-~p ps*O(1<)$X**3`w5P3aAZO&Xd8 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 e4z1xr*!e#zFyewyPf%w700001@y zU2Icj90%av?|si%Z9B$jCsbU+X0_mS2{ITuLX?;VZw!mX8)95EUP!$0Gw}laOuXWP z#F(I07!5?6216u9-3ZyH-~@FD*~nn-qg!V7(X~D8dQRW>Z)NE7@R{zn3SL1gb8o6~ zbGmMeu57r2VP7F@0%C#85Hm#Ixl1D>lYZGaZir7k{b~>t+P5|DN!^oP`FmYv!tp%h zedK*{oM26)9IG$4q|OzcZ~HD@`E#sT4A`~*U~^;q*(cgsua4am+x6^$YYgtRp^SY@qhrq&PvNu?lyDiMeTuEWJ8zoY$p`hNBweYfqi(<_--tu-Bo zByJ)8BSOHK3cO;l`{DjAt^DrD;qBl2C|5_7)>H~400JNh5Vx_sf{O8<+}qo9ACJHH zX2LeNZEg^tHd;Qg|7_iQzWw^kkLgnC#2IHP2NB#NNY1e{o`*nOaXZG32Yna{uY zn#jV=riKU4^sXBn4`uVp7#}F(UqafDYiiuN_D3=uy9Op^7qVr(ap=hG)Q!7h&gPg( z43FMDa4Ecytq1}LNE33G)0>u3jU&xlf4Vka@JxL?!K25%oEX0z1b&UlhHTNT_!LQa zxrFZpwUKiDS})mreSGGRYtu%;k!UQ|kl^tz`Z800&CE<{ii1FjuWWfGpD%eOs}-~p ps*O(1<)$X**3`w5P3aAZp0006gNkl1@y z&r4KM7zW_?J?CBrXC~Cqh5Zn-0Rt z4#u40F;)3x?r`#B@vn4V5I{hhkh@u1or)j%R@3lyc5&M?92mSC1b&f8hiul(`xL44 vGL^m;M8mn#60fRee(}fW*<~Ze;YjQcq__pqBwUPQ00000NkvXXu0mjf=%E=B literal 1459 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