diff --git a/README.md b/README.md index 60f224ef..15012046 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,14 @@ 自动清日常 bug反馈/意见/交流群: 885228564 -请先运行一次`_download_web.py`下载前端资源。 +请先运行一次`python3 _download_web.py`下载前端资源。 + +如果网络不好,可自行[下载压缩包](https://github.com/Lanly109/AutoPCR_Web/releases/latest),然后`python3 _download_web.py /path/to/zip`安装。 ## HTTP 服务器模式 ```bash -py -3.8 httpserver_test.py +python3 _httpserver_test.py ``` 访问`/daily/login` diff --git a/_download_web.py b/_download_web.py index cae931d9..5011bcd9 100644 --- a/_download_web.py +++ b/_download_web.py @@ -1,22 +1,22 @@ import asyncio import aiohttp -from requests import get import os import shutil async def get_latest_release_info(owner, repo): url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" - response = get(url) - if response.ok: - release_info = response.json() - tag_name = release_info['tag_name'] - assets = release_info['assets'] - asset_download_urls = [asset['browser_download_url'] for asset in assets] - return tag_name, asset_download_urls - else: - print(response.status_code) - print(response.json()) - return None, None + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.ok: + release_info = await response.json() + tag_name = release_info['tag_name'] + assets = release_info['assets'] + asset_download_urls = [asset['browser_download_url'] for asset in assets] + return tag_name, asset_download_urls + else: + print(response.status) + print(await response.json()) + return None, None path = os.path.dirname(os.path.abspath(__file__)) @@ -37,37 +37,42 @@ async def save_version(version): f.write(version) async def download_assets(asset_download_urls): - web_path = os.path.join(path, "autopcr", "http_server", "ClientApp") - if not os.path.exists(web_path): - os.makedirs(web_path) + ret = [] async with aiohttp.ClientSession() as session: for url in asset_download_urls: async with session.get(url) as response: if response.status == 200: filename = url.split('/')[-1] - print(f"Downloading {filename}") filepath = os.path.join(path, filename) + print(f"Downloading {filename} -> {filepath}") with open(filepath, 'wb') as f: while True: chunk = await response.content.read(1024) if not chunk: break f.write(chunk) - print(f"Downloaded {filename}") - - shutil.rmtree(web_path) - print(f"Removed old web file") - - import zipfile - with zipfile.ZipFile(filepath, 'r') as zip_ref: - zip_ref.extractall(web_path) - print(f"Unzipped {filename}") - - os.remove(filepath) - print(f"Delete {filename}") + ret.append(filepath) else: print(f"Failed to download {url}") raise Exception(f"Failed to download {url}") + return ret + +async def extract_web(filepaths): + web_path = os.path.join(path, "autopcr", "http_server", "ClientApp") + if not os.path.exists(web_path): + os.makedirs(web_path) + + print(f"Removed old web file") + shutil.rmtree(web_path) + + for filepath in filepaths: + print(f"Unzipped {filepath}") + import zipfile + with zipfile.ZipFile(filepath, 'r') as zip_ref: + zip_ref.extractall(web_path) + + print(f"Delete {filepath}") + os.remove(filepath) async def do_download(): owner = 'Lanly109' @@ -76,7 +81,8 @@ async def do_download(): if latest_release_tag and asset_download_urls: print(f"Latest release tag: {latest_release_tag}") if await check_version(latest_release_tag): - await download_assets(asset_download_urls) + filepaths = await download_assets(asset_download_urls) + await extract_web(filepaths) await save_version(latest_release_tag) else: print("Already up to date") @@ -84,6 +90,15 @@ async def do_download(): print("Failed to fetch latest release info") raise Exception("Failed to fetch latest release info") +async def main(zips): + if zips: + await extract_web(zips) + else: + await do_download() + + if __name__ == "__main__": + import sys + zips = sys.argv[1:] loop = asyncio.get_event_loop() - loop.run_until_complete(do_download()) + loop.run_until_complete(main(zips)) diff --git a/autopcr/core/datamgr.py b/autopcr/core/datamgr.py index 00ca2d98..1440fde4 100644 --- a/autopcr/core/datamgr.py +++ b/autopcr/core/datamgr.py @@ -162,28 +162,18 @@ def clear_inventory(self): self.hatsune_quest_dict.clear() def get_unique_equip_material_demand(self, equip_slot:int, unit_id: int, token: ItemType) -> int: - if unit_id not in db.unit_unique_equip[equip_slot]: - return 0 - equip_id = db.unit_unique_equip[equip_slot][unit_id].equip_id - rank = self.unit[unit_id].unique_equip_slot[0].rank if unit_id in self.unit and self.unit[unit_id].unique_equip_slot else -1 - return ( - flow(db.unique_equip_required[equip_id].items()) - .where(lambda x: x[0] >= rank) - .select(lambda x: x[1][token]) - .sum() - ) + start_rank = self.unit[unit_id].unique_equip_slot[0].rank if unit_id in self.unit and self.unit[unit_id].unique_equip_slot else 0 + demand = db.get_unique_equip_material_demand(unit_id, equip_slot, start_rank, db.unique_equipment_max_rank[equip_slot]) + return demand.get(token, 0) def get_unit_eqiup_demand(self, unit_id: int) -> typing.Counter[ItemType]: unit = self.unit[unit_id] + unit_id = unit.id rank = unit.promotion_level - - return db.craft_equip( - flow(db.unit_promotion_equip_count[unit_id].items()) - .where(lambda x: x[0] >= rank) - .select(lambda x: x[1]) - .sum(seed=Counter()) - - Counter((eInventoryType(eInventoryType.Equip), equip.id) for equip in unit.equip_slot if equip.is_slot) - )[0] + equip_slot = [equip.is_slot for equip in unit.equip_slot] + equips = db.get_rank_promote_equip_demand(unit_id, rank, equip_slot, db.equip_max_rank, db.equip_max_rank_equip_slot) + equip_demand, mana = db.craft_equip(equips) + return equip_demand def get_exceed_level_unit_demand(self, unit_id: int, token: ItemType) -> int: if unit_id in self.unit and self.unit[unit_id].exceed_stage: @@ -220,7 +210,7 @@ def get_library_unit_data(self) -> List: "r": str(unit.unit_rarity), "u": hex(unit.id // 100)[2:], "t": f"{db.equip_max_rank}.{db.equip_max_rank_equip_num}", - "q": str(db.unique_equip_rank[unit.unique_equip_slot[0].rank].enhance_level) if unit.unique_equip_slot and unit.unique_equip_slot[0].rank > 0 else "0", + "q": str(db.unique_equip_rank[1][unit.unique_equip_slot[0].rank].enhance_level) if unit.unique_equip_slot and unit.unique_equip_slot[0].rank > 0 else "0", "b": "true" if unit.exceed_stage else "false", "f": False }) @@ -415,6 +405,9 @@ def recover_max_time(self, quest: int) -> int: else: # hatsune, shiori 0 return self.settings.hatsune_recover_challenge_count.recovery_max_count + def filter_inventory(self, filter: Callable) -> List[ItemType]: + return [item for item in self._inventory if filter(item) and self._inventory[item] > 0] + def get_inventory(self, item: ItemType) -> int: return self._inventory.get(item, 0) @@ -455,6 +448,10 @@ def is_mission_finished(self, system_id: int): db.daily_mission_data[x.mission_id].system_id == system_id) )) != 0 + def get_not_enough_item(self, demand: typing.Counter[ItemType]) -> List[Tuple[ItemType, int]]: + bad = [(item, cnt - self.get_inventory(item)) for item, cnt in demand.items() if cnt > self.get_inventory(item)] + return bad + async def request(self, request: Request[TResponse], next: RequestHandler) -> TResponse: resp = await next.request(request) if resp: await resp.update(self, request) diff --git a/autopcr/core/pcrclient.py b/autopcr/core/pcrclient.py index 5295ffe2..36917642 100644 --- a/autopcr/core/pcrclient.py +++ b/autopcr/core/pcrclient.py @@ -50,16 +50,40 @@ async def season_ticket_new_accept(self, season_id: int, mission_id: int): req.mission_id = mission_id return await self.request(req) - async def deck_update(self, deck_number: int, units: List[int], sorted: bool = False): + async def deck_update(self, deck_number: int, units: List[int]): req = DeckUpdateRequest() req.deck_number = deck_number cnt = len(units) - if not sorted: - units = db.deck_sort_unit(units) + units = db.deck_sort_unit(units) for i in range(1, 6): setattr(req, f"unit_id_{i}",units[i - 1] if i <= cnt else 0) return await self.request(req) + async def set_growth_item_unique(self, unit_id: int, item_id: int): + req = UnitSetGrowthItemUniqueRequest() + req.unit_id = unit_id + req.item_id = item_id + return await self.request(req) + + async def set_my_party_tab(self, tab_number: int, tab_name: str): + req = SetMyPartyTabRequest() + req.tab_number = tab_number + req.tab_name = tab_name + return await self.request(req) + + async def set_my_party(self, tab_number: int, party_number: int, party_label_type: int, party_name: str, units: List[int], change_rarity_unit_list: List[ChangeRarityUnit]): + req = SetMyPartyRequest() + req.tab_number = tab_number + req.party_number = party_number + req.party_label_type = party_label_type + req.party_name = party_name + cnt = len(units) + units = db.deck_sort_unit(units) + for i in range(1, 6): + setattr(req, f"unit_id_{i}", units[i - 1] if i <= cnt else 0) + req.change_rarity_unit_list = change_rarity_unit_list + return await self.request(req) + async def deck_update_list(self, deck_list: List): req = DeckUpdateListRequest() req.deck_list = deck_list @@ -126,6 +150,55 @@ async def equipment_enhance(self, unit_id: int, equip_slot_num: int, current_enh req.item_list = [InventoryInfoPost(id=item[1], type=eInventoryType.Item, count=count) for item, count in items.items()] return await self.request(req) + async def unique_equip_free_enhance(self, unit_id: int, equip_slot_num: int, current_enhancement_pt: int, after_enhancement_pt: int): + req = EquipmentFreeMultiEnhanceUniqueRequest() + req.unit_id = unit_id + req.equip_slot_num = equip_slot_num + req.current_enhancement_pt = current_enhancement_pt + req.after_enhancement_pt = after_enhancement_pt + return await self.request(req) + + async def equipment_rankup_unique(self, unit_id: int, equip_slot_num: int, equip_recipe_dict: typing.Counter[ItemType], item_recipe_dict: typing.Counter[ItemType], current_rank: int): + req = UniqueEquipRankupRequest() + req.unit_id = unit_id + req.equip_slot_num = equip_slot_num + req.equip_recipe_list = [UserEquipParameterIdCount(id=item[1], count=count) for item, count in equip_recipe_dict.items()] + req.item_recipe_list = [UserEquipParameterIdCount(id=item[1], count=count) for item, count in item_recipe_dict.items()] + req.current_rank = current_rank + return await self.request(req) + + async def equipment_craft_unique(self, equip_id: int, equip_recipe_dict: typing.Counter[ItemType], item_recipe_dict: typing.Counter[ItemType], current_equip_num: int): + req = UniqueEquipCraftRequest() + req.equip_id = equip_id + req.equip_recipe_list = [UserEquipParameterIdCount(id=item[1], count=count) for item, count in equip_recipe_dict.items()] + req.item_recipe_list = [UserEquipParameterIdCount(id=item[1], count=count) for item, count in item_recipe_dict.items()] + req.current_equip_num = current_equip_num + await self.request(req) + + async def equipment_multi_enhance_unique(self, unit_id: int, equip_slot_num: int, current_gold_num: int, craft_equip_recipe_list: List[EnhanceRecipe], craft_item_recipe_list: List[EnhanceRecipe], rank_up_equip_recipe_list: List[EnhanceRecipe], rank_up_item_recipe_list: List[EnhanceRecipe], rank_up_exp_potion_list: List[EnhanceRecipe], current_rank: int, after_rank: int, enhancement_item_list: List[EnhanceRecipe], current_enhancement_pt: int): # 仅用于equipment_craft_unique紧接着调用来装备 + req = UniqueEquipMultiEnhanceRequest() + req.unit_id = unit_id + req.equip_slot_num = equip_slot_num + req.current_gold_num = current_gold_num + req.craft_equip_recipe_list = craft_equip_recipe_list + req.craft_item_recipe_list = craft_item_recipe_list + req.rank_up_equip_recipe_list = rank_up_equip_recipe_list + req.rank_up_item_recipe_list = rank_up_item_recipe_list + req.rank_up_exp_potion_list = rank_up_exp_potion_list + req.current_rank = current_rank + req.after_rank = after_rank + req.enhancement_item_list = enhancement_item_list + req.current_enhancement_pt = current_enhancement_pt + return await self.request(req) + + async def equipment_enhance_unique(self, unit_id: int, equip_slot_num: int, items: typing.Counter[ItemType], current_enhancement_pt: int): + req = UniqueEquipEnhanceRequest() + req.unit_id = unit_id + req.equip_slot_num = equip_slot_num + req.item_list = [InventoryInfoPost(id=item[1], type=eInventoryType.Item, count=count) for item, count in items.items()] + req.current_enhancement_pt = current_enhancement_pt + return await self.request(req) + async def equipment_free_enhance(self, unit_id: int, equip_slot_num: int, after_equip_level: int): req = EquipmentFreeEnhanceRequest() req.unit_id = unit_id @@ -215,7 +288,7 @@ async def prepare_mana(self, mana: int): if self.data.get_mana() >= mana: return True elif self.data.get_mana(include_bank = True) >= mana: - await self.draw_from_bank(mana, mana - self.data.get_mana()) + await self.draw_from_bank(self.data.user_gold_bank_info.bank_gold, mana - self.data.get_mana()) return True else: return False diff --git a/autopcr/core/sdkclient.py b/autopcr/core/sdkclient.py index 7f8a4ee0..febf650a 100644 --- a/autopcr/core/sdkclient.py +++ b/autopcr/core/sdkclient.py @@ -2,7 +2,6 @@ from typing import Tuple from abc import abstractmethod from ..sdk.validator import Validator -from ..sdk.bsgamesdk import captch from copy import deepcopy from ..constants import DEFAULT_HEADERS, IOS_HEADERS @@ -12,10 +11,12 @@ class platform(Enum): class account: type: platform + qq: str username: str password: str - def __init__(self, usr: str, pwd: str, type: platform): + def __init__(self, qid: str, usr: str, pwd: str, type: platform): + self.qq = qid self.username = usr self.password = pwd self.type = type @@ -42,8 +43,7 @@ def __init__(self, info: account, captchaVerifier=Validator, errlogger=_defaultL async def login(self) -> Tuple[str, str]: ... async def do_captcha(self): - cap = await captch() - return await self.captchaVerifier(self.account, cap['gt'], cap['challenge'], cap['gt_user_id']) + return await self.captchaVerifier(self.qq) def header(self): if self._account.type == platform.Android: @@ -75,6 +75,10 @@ def channel(self): @property def account(self): return self._account.username + + @property + def qq(self): + return self._account.qq @property @abstractmethod diff --git a/autopcr/core/sessionmgr.py b/autopcr/core/sessionmgr.py index 8fdbca14..6de46902 100644 --- a/autopcr/core/sessionmgr.py +++ b/autopcr/core/sessionmgr.py @@ -73,7 +73,7 @@ async def _ensure_token(self, next: RequestHandler): raise finally: from ..sdk.validator import validate_dict, ValidateInfo - validate_dict[self._account] = ValidateInfo(status="ok") + validate_dict[self.sdk.qq].append(ValidateInfo(status="ok")) async def _login(self, next: RequestHandler): if os.path.exists(self.cacheFile): diff --git a/autopcr/db/database.py b/autopcr/db/database.py index 5a6a6fe5..cf5c2da3 100644 --- a/autopcr/db/database.py +++ b/autopcr/db/database.py @@ -83,10 +83,12 @@ def update(self, dbmgr: dbmgr): ) ) - self.unique_equip_rank: Dict[int, UniqueEquipmentEnhanceDatum] = ( # 第二维是int? + self.unique_equip_rank: Dict[int, Dict[int, UniqueEquipmentEnhanceDatum]] = ( UniqueEquipmentEnhanceDatum.query(db) + .group_by(lambda x: x.equip_slot) + .to_dict(lambda x: x.key, lambda x: x .group_by(lambda x: x.rank) - .to_dict(lambda x: x.key, lambda x: x.max(lambda y: y.enhance_level)) + .to_dict(lambda x: x.key, lambda x: x.max(lambda y: y.enhance_level))) ) self.equip_craft: Dict[ItemType, List[Tuple[ItemType, int]]] = ( @@ -134,6 +136,16 @@ def update(self, dbmgr: dbmgr): len(x.get(self.equip_max_rank, {})) for x in self.unit_promotion_equip_count.values() ) + self.equip_max_rank_equip_slot: List[bool] = [ # 简洁 + [False, True, False, True, False, True], + [False, True, False, True, True, True], + [False, True, True, True, True, True], + ][self.equip_max_rank_equip_num - 3] + + self.unique_equipment_max_rank: Dict[int, int] = { + equip_slot: max(self.unique_equip_rank[equip_slot].keys()) for equip_slot in self.unique_equip_rank + } + self.hatsune_schedule: Dict[int, HatsuneSchedule] = ( HatsuneSchedule.query(db) .to_dict(lambda x: x.event_id, lambda x: x) @@ -197,6 +209,13 @@ def update(self, dbmgr: dbmgr): x.to_dict(lambda x: x.enhance_level, lambda x: x)) ) + self.unique_equipment_rank_up: Dict[int, Dict[int, UniqueEquipmentRankup]] = ( + UniqueEquipmentRankup.query(db) + .group_by(lambda x: x.equip_id) + .to_dict(lambda x: x.key, lambda x: x + .to_dict(lambda x: x.unique_equip_rank, lambda x: x)) + ) + self.unique_equipment_max_level: Dict[int, int] = { equip_slot: max(self.unique_equipment_enhance_data[equip_slot].keys()) for equip_slot in self.unique_equipment_enhance_data } @@ -481,6 +500,11 @@ def update(self, dbmgr: dbmgr): .to_dict(lambda x: x.growth_id, lambda x: x) ) + self.growth_parameter_unique: Dict[int, GrowthParameterUnique] = ( + GrowthParameterUnique.query(db) + .to_dict(lambda x: x.growth_id, lambda x: x) + ) + self.unit_data: Dict[int, UnitDatum] = ( UnitDatum.query(db) .to_dict(lambda x: x.unit_id, lambda x: x) @@ -695,6 +719,12 @@ def is_equip_raw_ore(self, item: ItemType) -> bool: def is_equip_craftable(self, item: ItemType) -> bool: return item in self.equip_craft + def is_equip_glow_ball(self, item: ItemType) -> bool: + return item[0] == eInventoryType.Item and item[1] >= 21900 and item[1] < 21950 + + def is_unique_equip_glow_ball(self, item: ItemType) -> bool: + return item[0] == eInventoryType.Item and item[1] >= 21950 and item[1] < 22000 + def is_room_item_level_upable(self, team_level: int, item: RoomUserItem) -> bool: return (item.room_item_level < self.room_item[item.room_item_id].max_level and item.room_item_level in self.room_item_detail[item.room_item_id] and @@ -935,6 +965,17 @@ def is_today(self, time: datetime.datetime) -> bool: def get_today_start_time(self) -> datetime.datetime: return self.get_start_time(datetime.datetime.now()) + def get_unique_equip_material_demand(self, unit_id: int, slot_id:int, start_rank:int, target_rank: int) -> typing.Counter[ItemType]: + if unit_id not in db.unit_unique_equip[slot_id]: + return Counter() + equip_id = db.unit_unique_equip[slot_id][unit_id].equip_id + return ( + flow(db.unique_equip_required[equip_id].items()) + .where(lambda x: x[0] >= start_rank and x[0] < target_rank) + .select(lambda x: x[1]) + .sum(seed = Counter()) + ) + def get_rank_promote_equip_demand(self, unit_id: int, start_rank: int, start_rank_equip_slot: List[bool], target_rank, target_rank_equip_slot: List[bool]) -> typing.Counter[ItemType]: # 都是整装 ret = ( (flow(self.unit_promotion_equip_count[unit_id].items()) @@ -1000,8 +1041,25 @@ def get_unique_equip_level_from_pt(self, equip_slot: int, enhancement_pt: int): level = max([1] + histort_level) return level + def get_unique_equip_max_level_from_rank(self, equip_slot: int, rank: int): + return self.unique_equip_rank[equip_slot][rank].enhance_level + + def get_unique_equip_rank_from_level(self, equip_slot: int, level: int): + rank = self.unique_equipment_enhance_data[equip_slot][level].rank if level in self.unique_equipment_enhance_data[equip_slot] else 1 + return rank + + def get_unique_equip_rank_required_level(self, slot_id: int, unit_id: int, rank: int): + rank -= 1 # db是从当前rank升下一级的花费限制,因此升到rank的限制来自于rank-1 + equip_id = db.unit_unique_equip[slot_id][unit_id].equip_id + level = self.unique_equipment_rank_up[equip_id][rank].unit_level if rank > 0 else 1 + return level + + def get_unique_equip_pt_from_level(self, equip_slot: int, level: int): + pt = self.unique_equipment_enhance_data[equip_slot][level].total_point if level in self.unique_equipment_enhance_data[equip_slot] else 0 + return pt + def deck_sort_unit(self, units: List[int]): - return sorted(units, key=lambda x: self.unit_data[x].search_area_width) + return sorted(units, key=lambda x: self.unit_data[x].search_area_width if x in self.unit_data else 9999) def is_stamina_type(self, type_id: int) -> bool: return type_id in [eInventoryType.Stamina, eInventoryType.SeasonPassStamina] @@ -1031,6 +1089,9 @@ def unit_rank_candidate(self): def unit_level_candidate(self): return list(range(1, self.team_max_level + 1 + 10)) + def unit_unique_equip_level_candidate(self, equip_slot: int): + return list(range(0, self.unique_equipment_max_level[equip_slot] + 1)) + def last_normal_quest_candidate(self): last_start_time = flow(self.normal_quest_data.values()) \ .where(lambda x: db.parse_time(x.start_time) <= datetime.datetime.now()) \ diff --git a/autopcr/http_server/httpserver.py b/autopcr/http_server/httpserver.py index 286126c5..cb32139e 100644 --- a/autopcr/http_server/httpserver.py +++ b/autopcr/http_server/httpserver.py @@ -1,7 +1,7 @@ from copy import deepcopy from datetime import timedelta import traceback -import quart +import quart, asyncio from quart import request, Blueprint, send_file, send_from_directory from quart_rate_limiter import RateLimiter, rate_limit, RateLimitExceeded from quart_auth import AuthUser, QuartAuth, Unauthorized, current_user, login_required, login_user, logout_user @@ -13,6 +13,8 @@ from ..constants import CACHE_DIR from ..util.draw import instance as drawer +APP_VERSION = "1.1.0" + CACHE_HTTP_DIR = os.path.join(CACHE_DIR, 'http_server') PATH = os.path.dirname(os.path.abspath(__file__)) @@ -38,6 +40,7 @@ def __init__(self, host = '0.0.0.0', port = 2, qq_mod = False): self.host = host self.port = port + self.validate_server = {} self.configure_routes() self.qq_mod = qq_mod @@ -52,7 +55,7 @@ async def inner(accountmgr: AccountManager, acc: str, *args, **kwargs): except AccountException as e: return str(e), 400 except Exception as e: - print(e) + traceback.print_exc() return "服务器发生错误", 500 else: return "Please specify an account", 400 @@ -73,6 +76,15 @@ async def inner(*args, **kwargs): def configure_routes(self): + @self.api.before_request + async def check_app_version(): + return None + version = request.headers.get('X-App-Version', None) + if version != APP_VERSION: + return f"后端期望前端版本为{APP_VERSION},请更新", 400 + else: + return None + @self.api.errorhandler(RateLimitExceeded) async def handle_rate_limit_exceeded_error(error): return "您冲得太快了,休息一下吧", 429 @@ -109,7 +121,54 @@ async def create_account(accountmgr: AccountManager): except AccountException as e: return str(e), 400 except Exception as e: - print(e) + traceback.print_exc() + return "服务器发生错误", 500 + + @self.api.route('/account/import', methods = ["POST"]) + @login_required + @HttpServer.wrapaccountmgr() + async def create_accounts(accountmgr: AccountManager): + try: + file = await request.files + if 'file' not in file: + return "请选择文件", 400 + file = file['file'] + if file.filename.split('.')[-1] != 'tsv': + return "文件格式错误", 400 + data = file.read().decode() + ok, msg = await accountmgr.create_accounts_from_tsv(data) + return msg, 200 if ok else 400 + except ValueError as e: + return str(e), 400 + except Exception as e: + traceback.print_exc() + return "服务器发生错误", 500 + + @self.api.route('/', methods = ["DELETE"]) + @login_required + @HttpServer.wrapaccountmgr() + async def delete_qq(accountmgr: AccountManager): + try: + accountmgr.delete_mgr() + logout_user() + return "删除QQ成功", 200 + except AccountException as e: + return str(e), 400 + except Exception as e: + traceback.print_exc() + return "服务器发生错误", 500 + + @self.api.route('/account', methods = ["DELETE"]) + @login_required + @HttpServer.wrapaccountmgr() + async def delete_account(accountmgr: AccountManager): + try: + accountmgr.delete_all_accounts() + return "删除账号成功", 200 + except AccountException as e: + return str(e), 400 + except Exception as e: + traceback.print_exc() return "服务器发生错误", 500 @self.api.route('/account/sync', methods = ["POST"]) @@ -130,7 +189,7 @@ async def sync_account_config(accountmgr: AccountManager): except AccountException as e: return str(e), 400 except Exception as e: - print(e) + traceback.print_exc() return "服务器发生错误", 500 @self.api.route('/account/', methods = ['GET']) @@ -153,6 +212,8 @@ async def update_account(account: Account): account.data.password = data['password'] if 'channel' in data: account.data.channel = data['channel'] + if 'batch_accounts' in data: + account.data.batch_accounts = data['batch_accounts'] return "保存账户信息成功", 200 elif request.method == "DELETE": account.delete() @@ -160,25 +221,12 @@ async def update_account(account: Account): else: return "", 404 - @self.api.route('/account//daily', methods = ['GET']) + @self.api.route('/account//', methods = ['GET']) @login_required @HttpServer.wrapaccountmgr(readonly = True) @HttpServer.wrapaccount(readonly= True) - async def get_daily_config(mgr: Account): - if request.method == 'GET': - return mgr.generate_daily_info() - else: - return "", 404 - - @self.api.route('/account//tools', methods = ['GET']) - @login_required - @HttpServer.wrapaccountmgr(readonly = True) - @HttpServer.wrapaccount(readonly= True) - async def get_tools_config(mgr: Account): - if request.method == 'GET': - return mgr.generate_tools_info() - else: - return "", 404 + async def get_modules_config(mgr: Account, modules_key: str): + return mgr.generate_modules_info(modules_key) @self.api.route('/account//config', methods = ['PUT']) @login_required @@ -195,7 +243,7 @@ async def put_config(mgr: Account): @HttpServer.wrapaccount() async def do_daily(mgr: Account): try: - await mgr.do_daily() + await mgr.do_daily(mgr._parent.secret.clan) return mgr.generate_result_info(), 200 except ValueError as e: return str(e), 400 @@ -203,18 +251,37 @@ async def do_daily(mgr: Account): traceback.print_exc() return "服务器发生错误", 500 - @self.api.route('/account//daily_result/', methods = ['GET']) + @self.api.route('/account//daily_result', methods = ['GET']) + @login_required + @HttpServer.wrapaccountmgr(readonly = True) + @HttpServer.wrapaccount(readonly= True) + async def daily_result_list(mgr: Account): + try: + resp = mgr.get_daily_result_list() + resp = [r.response('/daily/api/account/{}' + '/daily_result/' + str(r.key)) for r in resp] + return resp, 200 + except ValueError as e: + return str(e), 400 + except Exception as e: + traceback.print_exc() + return "服务器发生错误", 500 + + @self.api.route('/account//daily_result/', methods = ['GET']) @login_required @HttpServer.wrapaccountmgr(readonly = True) @HttpServer.wrapaccount(readonly= True) - async def daily_result(mgr: Account, safe_time: str): + async def daily_result(mgr: Account, key: str): try: - resp = await mgr.get_daily_result_from_time(safe_time) + resp_text = request.args.get('text', 'false').lower() + resp = await mgr.get_daily_result_from_key(key) if not resp: return "无结果", 404 - img = await drawer.draw_tasks_result(resp) - bytesio = await drawer.img2bytesio(img) - return await send_file(bytesio, mimetype='image/jpg') + if resp_text == 'false': + img = await drawer.draw_tasks_result(resp) + bytesio = await drawer.img2bytesio(img) + return await send_file(bytesio, mimetype='image/jpg') + else: + return resp.to_json(), 200 except ValueError as e: return str(e), 400 except Exception as e: @@ -228,16 +295,13 @@ async def daily_result(mgr: Account, safe_time: str): async def do_single(mgr: Account): data = await request.get_json() order = data.get("order", "") - resp_text = request.args.get('text', 'false').lower() try: - resp, _ = await mgr.do_from_key(deepcopy(mgr.client.keys), order) - if resp_text == 'false': - img = await drawer.draw_task_result(resp) - bytesio = await drawer.img2bytesio(img) - return await send_file(bytesio, mimetype='image/jpg') - else: - return resp.log, 200 + await mgr.do_from_key(deepcopy(mgr.client.keys), order, mgr._parent.secret.clan) + resp = mgr.get_single_result_list(order) + resp = [r.response('/daily/api/account/{}' + f'/single_result/{order}/{r.key}') for r in resp] + return resp, 200 except ValueError as e: + traceback.print_exc() return str(e), 400 except Exception as e: traceback.print_exc() @@ -247,10 +311,25 @@ async def do_single(mgr: Account): @login_required @HttpServer.wrapaccountmgr(readonly = True) @HttpServer.wrapaccount(readonly= True) - async def single_result(mgr: Account, order: str): + async def single_result_list(mgr: Account, order: str): + try: + resp = mgr.get_single_result_list(order) + resp = [r.response('/daily/api/account/{}' + f'/single_result/{order}/{r.key}') for r in resp] + return resp, 200 + except ValueError as e: + return str(e), 400 + except Exception as e: + traceback.print_exc() + return "服务器发生错误", 500 + + @self.api.route('/account//single_result//', methods = ['GET']) + @login_required + @HttpServer.wrapaccountmgr(readonly = True) + @HttpServer.wrapaccount(readonly= True) + async def single_result(mgr: Account, order: str, key: str): resp_text = request.args.get('text', 'false').lower() try: - resp = await mgr.get_single_result(order) + resp = await mgr.get_single_result_from_key(order, key) if not resp: return "无结果", 404 @@ -259,25 +338,47 @@ async def single_result(mgr: Account, order: str): bytesio = await drawer.img2bytesio(img) return await send_file(bytesio, mimetype='image/jpg') else: - return resp.log, 200 + return resp.to_json(), 200 except ValueError as e: return str(e), 400 except Exception as e: traceback.print_exc() return "服务器发生错误", 500 - @self.api.route('/account//query_validate', methods = ['GET']) + @self.api.route('/query_validate', methods = ['GET']) @login_required @HttpServer.wrapaccountmgr(readonly = True) - @HttpServer.wrapaccount(readonly = True) - async def query_validate(mgr: Account): - from ..sdk.validator import validate_dict, ValidateInfo - if mgr.data.username not in validate_dict: - return ValidateInfo(status="empty").to_dict(), 200 - else: - ret = validate_dict[mgr.data.username].to_dict() - del validate_dict[mgr.data.username] - return ret, 200 + async def query_validate(accountmgr: AccountManager): + from ..sdk.validator import validate_dict + if "text/event-stream" not in request.accept_mimetypes: + return "", 400 + + server_id = secrets.token_urlsafe(8) + self.validate_server[accountmgr.qid] = server_id + + async def send_events(qid, server_id): + for _ in range(30): + if self.validate_server[qid] != server_id: + break + if qid in validate_dict and validate_dict[qid]: + ret = validate_dict[qid].pop().to_json() + id = secrets.token_urlsafe(8) + yield f'''id: {id} +retry: 1000 +data: {ret}\n\n''' + else: + await asyncio.sleep(1) + + response = await quart.make_response( + send_events(accountmgr.qid, server_id), + { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Transfer-Encoding': 'chunked', + }, + ) + response.timeout = None + return response @self.api.route('/validate', methods = ['POST']) async def validate(): # TODO think to check login or not @@ -326,7 +427,7 @@ async def register(): except UserException as e: return str(e), 400 except Exception as e: - print(e) + traceback.print_exc() return "服务器发生错误", 500 @self.api.route('/logout', methods = ['POST']) diff --git a/autopcr/model/handlers.py b/autopcr/model/handlers.py index cc41a2e2..69e044d6 100644 --- a/autopcr/model/handlers.py +++ b/autopcr/model/handlers.py @@ -605,6 +605,82 @@ async def update(self, mgr: datamgr, request): if self.user_gold: mgr.gold = self.user_gold +@handles +class UniqueEquipEnhanceResponse(responses.UniqueEquipEnhanceResponse): + async def update(self, mgr: datamgr, request): + if self.item_list: + for item in self.item_list: + mgr.update_inventory(item) + mgr.unit[self.unit_data.id] = self.unit_data + if self.user_gold: + mgr.gold = self.user_gold + +@handles +class UniqueEquipRankupResponse(responses.UniqueEquipRankupResponse): + async def update(self, mgr: datamgr, request): + if self.item_list: + for item in self.item_list: + mgr.update_inventory(item) + mgr.unit[self.unit_data.id] = self.unit_data + if self.user_gold: + mgr.gold = self.user_gold + if self.equip_list: + for equip in self.equip_list: + mgr.update_inventory(equip) + +@handles +class EquipmentFreeMultiEnhanceUniqueResponse(responses.EquipmentFreeMultiEnhanceUniqueResponse): + async def update(self, mgr: datamgr, request): + mgr.unit[self.unit_data.id] = self.unit_data + if self.equip_list: + for equip in self.equip_list: + mgr.update_inventory(equip) + +@handles +class UniqueEquipCraftResponse(responses.UniqueEquipCraftResponse): + async def update(self, mgr: datamgr, request): + if self.item_list: + for item in self.item_list: + mgr.update_inventory(item) + if self.equip_list: + for equip in self.equip_list: + mgr.update_inventory(equip) + if self.user_gold: + mgr.gold = self.user_gold + +@handles +class UniqueEquipMultiEnhanceResponse(responses.UniqueEquipMultiEnhanceResponse): + async def update(self, mgr: datamgr, request): + mgr.unit[self.unit_data.id] = self.unit_data + if self.item_list: + for item in self.item_list: + mgr.update_inventory(item) + if self.equip_list: + for equip in self.equip_list: + mgr.update_inventory(equip) + if self.user_gold: + mgr.gold = self.user_gold + +@handles +class UnitSetGrowthItemUniqueResponse(responses.UnitSetGrowthItemUniqueResponse): + async def update(self, mgr: datamgr, request): + if self.unit_data: + mgr.unit[self.unit_data.id] = self.unit_data + if self.item_data: + for item in self.item_data: + mgr.update_inventory(item) + if self.growth_parameter_list: + mgr.growth_unit.update( + {request.unit_id: + GrowthInfo(unit_id=request.unit_id, growth_parameter_list=self.growth_parameter_list) + }) + +@handles +class ChangeRarityResponse(responses.ChangeRarityResponse): + async def update(self, mgr: datamgr, request): + if self.unit_data_list: + for unit in self.unit_data_list: + mgr.unit[unit.id] = unit # 菜 就别玩 HatsuneTopResponse.__annotations__['event_status'] = HatsuneEventStatus diff --git a/autopcr/module/accountmgr.py b/autopcr/module/accountmgr.py index 03f6790a..ff4eff35 100644 --- a/autopcr/module/accountmgr.py +++ b/autopcr/module/accountmgr.py @@ -1,13 +1,15 @@ # 名字需要斟酌一下 +import asyncio from dataclasses import dataclass, field from dataclasses_json import dataclass_json + from ..core.pcrclient import pcrclient from ..core.sdkclient import account, platform -from .modulemgr import ModuleManager, TaskResult, ModuleResult +from .modulemgr import ModuleManager, TaskResult, ModuleResult, eResultStatus from ..sdk.sdkclients import create import os, re, shutil -from typing import Any, Dict, Iterator, List, Union +from typing import Any, Dict, Iterator, List, Tuple, Union from ..constants import CONFIG_PATH, OLD_CONFIG_PATH, RESULT_DIR, BSDK, CHANNEL_OPTION from asyncio import Lock import json @@ -24,14 +26,42 @@ class UserException(Exception): @dataclass_json @dataclass -class DailyResult: +class ResultInfo: + alias: str = "" + key: str = "" path: str = "" - time: str = "无" - time_safe: str = "无" - status: str = "skip" + time: str = "" + url: str = "" + _type: str = "" + status: eResultStatus = eResultStatus.SKIP - def safe_info(self) -> "DailyResult": - return DailyResult(path = "", time = self.time, time_safe = self.time_safe, status = self.status) + def save_result(self, result): + with open(self.path, 'w') as f: + f.write(result.to_json()) + def delete_result(self): + if os.path.exists(self.path): + os.remove(self.path) + def get_result(self): + raise NotImplementedError + def response(self, url_format: str): + url = url_format.format(self.alias) + return ResultInfo(alias = self.alias, key = self.key, time = self.time, url = url, status = self.status) + +@dataclass_json +@dataclass +class TaskResultInfo(ResultInfo): + _type: str = "daily_result" + def get_result(self) -> TaskResult: + with open(self.path, 'r') as f: + return TaskResult.from_json(f.read()) + +@dataclass_json +@dataclass +class ModuleResultInfo(ResultInfo): + _type: str = "single_result" + def get_result(self) -> ModuleResult: + with open(self.path, 'r') as f: + return ModuleResult.from_json(f.read()) @dataclass_json @dataclass @@ -40,13 +70,18 @@ class AccountData: password: str = "" channel: str = BSDK config: Dict[str, Any] = field(default_factory=dict) - daily_result: List[DailyResult] = field(default_factory=list) + daily_result: List[TaskResultInfo] = field(default_factory=list) + single_result: Dict[str, List[ModuleResultInfo]] = field(default_factory=dict) + batch_accounts: List[str] = field(default_factory=list) @dataclass_json @dataclass class UserData: password: str = "" default_account: str = "" + clan: bool = False + +BATCHINFO = "BATCH_RUNNER" class Account(ModuleManager): def __init__(self, parent: 'AccountManager', qid: str, account: str, readonly: bool = False): @@ -59,7 +94,11 @@ def __init__(self, parent: 'AccountManager', qid: str, account: str, readonly: b self.id = hashlib.md5(account.encode('utf-8')).hexdigest() if not os.path.exists(self._filename): - raise AccountException("账号不存在") + if account == BATCHINFO: + with open(self._filename, 'w') as f: + f.write(AccountData().to_json()) + else: + raise AccountException("账号不存在") with open(self._filename, 'r') as f: self.data: AccountData = AccountData.from_json(f.read()) @@ -85,64 +124,82 @@ async def save_data(self): with open(self._filename, 'w') as f: f.write(self.data.to_json()) - async def save_daily_result(self, result: TaskResult, status: str): + async def push_result(self, result_list: List[Any], result: ResultInfo) -> List[Any]: + while len(result_list) >= 4: + result_list.pop().delete_result() + return [result] + result_list + + async def save_daily_result(self, result: TaskResult, status: eResultStatus) -> TaskResultInfo: now = datetime.datetime.now() time_safe = db.format_time_safe(now) file = os.path.join(RESULT_DIR, f"{self.token}_daily_{time_safe}.json") - with open(file, 'w') as f: - f.write(result.to_json()) - old_list = self.data.daily_result - while len(old_list) >= 4: - if os.path.exists(old_list[-1].path): - os.remove(old_list[-1].path) - old_list.pop() - item = DailyResult(path = file, time = db.format_time(now), time_safe=time_safe, status = status) - self.data.daily_result = [item] + old_list - - async def save_single_result(self, module: str, result: ModuleResult): - file = os.path.join(RESULT_DIR, f"{self.token}_{module}.json") - with open(file, 'w') as f: - f.write(result.to_json()) + item = TaskResultInfo(alias = self.alias, key=time_safe, path = file, time = db.format_time(now), status = status) + item.save_result(result) + self.data.daily_result = await self.push_result(self.data.daily_result, item) + return item + + async def save_single_result(self, module: str, result: ModuleResult) -> ModuleResultInfo: + now = datetime.datetime.now() + time_safe = db.format_time_safe(now) + file = os.path.join(RESULT_DIR, f"{self.token}_{module}_{time_safe}.json") + + item = ModuleResultInfo(alias = self.alias, key = time_safe, path = file, time = db.format_time(now), status = result.status) + item.save_result(result) + self.data.single_result[module] = await self.push_result(self.data.single_result.get(module, []), item) + return item async def get_daily_result_from_id(self, id: int = 0) -> Union[None, TaskResult]: - if len(self.data.daily_result) > id: - try: - with open(self.data.daily_result[id].path, 'r') as f: - return TaskResult.from_json(f.read()) - except Exception as e: - traceback.print_exc() - return None - else: + try: + return self.data.daily_result[id].get_result() + except Exception as e: + traceback.print_exc() return None - async def get_daily_result_from_time(self, safe_time: str) -> Union[None, TaskResult]: - ret = [daily_result.path for daily_result in self.data.daily_result if safe_time == daily_result.time_safe] - if ret: - with open(ret[0], 'r') as f: - return TaskResult.from_json(f.read()) - else: + async def get_single_result_from_id(self, module, id: int = 0) -> Union[None, ModuleResult]: + try: + return self.data.single_result[module][id].get_result() + except Exception as e: + traceback.print_exc() return None - async def get_single_result(self, module) -> Union[None, ModuleResult]: - file = os.path.join(RESULT_DIR, f"{self.token}_{module}.json") - if os.path.exists(file): - with open(file, 'r') as f: - return ModuleResult.from_json(f.read()) - else: + async def get_daily_result_from_key(self, key: str) -> Union[None, TaskResult]: + try: # 为何不用dict?因为先前就用的list,不好更改,以及结果数不多,直接遍历也无所谓 + result = next(filter(lambda x: x.key == key, self.data.daily_result)) + return result.get_result() + except Exception as e: + traceback.print_exc() return None - def get_last_daily_clean(self) -> DailyResult: - if self.data.daily_result: - return self.data.daily_result[0].safe_info() + async def get_single_result_from_key(self, module, key: str) -> Union[None, ModuleResult]: + try: + result = next(filter(lambda x: x.key == key, self.data.single_result[module])) + return result.get_result() + except Exception as e: + traceback.print_exc() + return None + + def get_last_daily_clean(self) -> TaskResultInfo: + daily_result = self.get_daily_result_list() + if daily_result: + return daily_result[0] else: - return DailyResult() + return TaskResultInfo() + + def get_daily_result_list(self) -> List[TaskResultInfo]: + ret = self.data.daily_result + return ret + + def get_single_result_list(self, module: str) -> List[ModuleResultInfo]: + ret = self.data.single_result.get(module, []) + return ret def get_client(self) -> pcrclient: return self.get_android_client() def get_ios_client(self) -> pcrclient: # Header TODO client = pcrclient(create(self.data.channel, account( + self.qq, self.data.username, self.data.password, platform.IOS @@ -151,6 +208,7 @@ def get_ios_client(self) -> pcrclient: # Header TODO def get_android_client(self) -> pcrclient: client = pcrclient(create(self.data.channel, account( + self.qq, self.data.username, self.data.password, platform.Android @@ -171,23 +229,18 @@ def _mask_str(mask_str: str) -> str: 'password': 8 * "*" if self.data.password else "", 'channel': self.data.channel, 'channel_option': CHANNEL_OPTION, - 'area': [{"key": 'daily', "name":"日常"}, {"key": 'tools', "name":"工具"}] + 'area': super().generate_tab(clan = self._parent.secret.clan) } def generate_result_info(self): ret = { 'name': self.alias, 'daily_clean_time': self.get_last_daily_clean().to_dict(), - 'daily_clean_time_list': [daily_result.safe_info().to_dict() for daily_result in self.data.daily_result], } return ret - def generate_daily_info(self): - info = super().generate_daily_config() - return info - - def generate_tools_info(self): - info = super().generate_tools_config() + def generate_modules_info(self, key: str): + info = super().generate_modules_info(key) return info def delete(self): @@ -197,6 +250,44 @@ def is_clan_battle_forbidden(self): username = self.data.username.lower() return self._parent._parent.is_clan_battle_forbidden(username) +class AccountBatch(Account): + def __init__(self, parent: 'AccountManager', qid: str, accounts: str = BATCHINFO, readonly: bool = False): + super().__init__(parent, qid, accounts, readonly) + # self.enable_account = set([x for x in self.data.batch_accounts]) & set(self._parent.accounts()) + self.enable_account = set(self._parent.accounts()) + + def generate_info(self): + accounts = list(self.enable_account) + all_accounts = sorted(list(self._parent.accounts())) + return { + 'alias': self.alias, + 'batch_accounts': accounts, + 'all_accounts': all_accounts, + 'area': super().generate_tab(clan = self._parent.secret.clan, batch = True) + } + + async def do_from_key(self, config: dict, key: str, isAdminCall: bool = False) -> List[ModuleResultInfo]: + async def do_from_key_pre(alias: str): + async with self._parent.load(alias) as acc: + res = await acc.do_from_key(config, key, isAdminCall) + return res + + loop = asyncio.get_event_loop() + alias = [] + task = [] + for acc in self.enable_account: + alias.append(acc) + task.append(loop.create_task(do_from_key_pre(acc))) + + resps = await asyncio.gather(*task, return_exceptions=True) + self.data.single_result[key] = resps + return resps + + async def do_daily(self): + raise NotImplementedError + async def do_task(self): + raise NotImplementedError + class AccountManager: pathsyntax = re.compile(r'[^\\\|?*/#]{1,32}') @@ -213,6 +304,8 @@ def __init__(self, parent: 'UserManager', qid: str, readonly: bool = False): self.secret: UserData = UserData.from_json(f.read()) self.old_secret = deepcopy(self.secret) + self.secret.clan |= self.qid.startswith('g') + async def __aenter__(self): if not self.readonly: await self._lck.acquire() @@ -254,6 +347,8 @@ def validate_password(self, password: str) -> bool: def load(self, account: str = "", readonly = False) -> Account: if not AccountManager.pathsyntax.fullmatch(account): raise AccountException(f'非法账户名{account}') + if account == BATCHINFO: + return AccountBatch(self, self.qid, account, readonly = readonly) if not account: account = self.secret.default_account if not account and len(list(self.accounts())) == 1: @@ -270,6 +365,13 @@ def delete(self, account: str): raise AccountException(f'非法账户名{account}') os.remove(self.path(account)) + def delete_all_accounts(self): + for account in self.accounts(): + self.delete(account) + + def delete_mgr(self): + self._parent.delete(self.qid) + @property def default_account(self) -> str: return self.secret.default_account @@ -277,9 +379,40 @@ def default_account(self) -> str: def accounts(self) -> Iterator[str]: account_files = sorted(os.listdir(self.root)) for fn in account_files: - if fn.endswith('.json'): + if fn.endswith('.json') and not fn.startswith(BATCHINFO): yield fn[:-5] + async def create_accounts_from_tsv(self, tsv: str) -> Tuple[bool, str]: + acc = [] + ok = True + exist_accounts = set(self.accounts()) + msg = [] + for line in tsv.splitlines(): + if not line: + continue + alias, username, password, *channel = line.split('\t') + + if not AccountManager.pathsyntax.fullmatch(alias): + ok = False + msg.append(f'非法昵称{alias}') + if alias in exist_accounts: + ok = False + msg.append(f'昵称重复{alias}') + channel = channel[0] if channel else BSDK + if channel not in CHANNEL_OPTION: + ok = False + msg.append(f'未知服务器{channel}') + + exist_accounts.add(alias) + acc.append((alias, username, password, channel)) + if not ok: + return False, '\n'.join(msg) + + for alias, username, password, channel in acc: + with open(self.path(alias), 'w') as f: + f.write(AccountData(username = username, password = password, channel = channel).to_json()) + return True, f'成功导入{len(acc)}个账号' + async def generate_info(self): accounts = [] for account in self.accounts(): @@ -288,11 +421,12 @@ async def generate_info(self): return { 'qq': self.qid, 'default_account': self.default_account, - 'accounts': accounts + 'accounts': accounts, + 'clan': self.secret.clan } class UserManager: - pathsyntax = re.compile(r'\d{5,12}') + pathsyntax = re.compile(r'g?\d{5,12}') def __init__(self, root: str): self.root = root @@ -318,7 +452,7 @@ def validate_password(self, qid: str, password: str) -> bool: return False return self.load(qid).validate_password(password) except Exception as e: - print(e) + traceback.print_exc() return False def qid_path(self, qid: str) -> str: @@ -361,7 +495,7 @@ def delete(self, qid: str, account: str = ""): def qids(self) -> Iterator[str]: for fn in os.listdir(self.root): - if fn.isdigit() and os.path.isdir(os.path.join(self.root, fn)): + if os.path.isdir(os.path.join(self.root, fn)): yield fn instance = UserManager(os.path.join(CONFIG_PATH)) diff --git a/autopcr/module/config.py b/autopcr/module/config.py index ec62d52e..715353be 100644 --- a/autopcr/module/config.py +++ b/autopcr/module/config.py @@ -109,6 +109,11 @@ def decorator(cls): return config_option(key=key, desc=desc, default=default, config_type='time')(cls) return decorator +def texttype(key:str, desc: str, default): + def decorator(cls): + return config_option(key=key, desc=desc, default=default, config_type='text')(cls) + return decorator + def conditional_execution1(key: str, default, desc: str = "执行条件", check: bool = True): # need login async def do_check(self, client: pcrclient) -> Tuple[bool, str]: run_time = self.get_value() diff --git a/autopcr/module/crons.py b/autopcr/module/crons.py index f791f5d6..2896ceae 100644 --- a/autopcr/module/crons.py +++ b/autopcr/module/crons.py @@ -46,7 +46,8 @@ async def task(qid, account, cur): try: if await mgr.is_cron_run(cur.hour, cur.minute): write_cron_log(eCronOperation.START, cur, qid, account, eResultStatus.SUCCESS) - _, status = await mgr.do_daily() + res = await mgr.do_daily() + status = res.status cur = datetime.datetime.now() write_cron_log(eCronOperation.FINISH, cur, qid, account, status) except Exception as e: diff --git a/autopcr/module/modulebase.py b/autopcr/module/modulebase.py index 7ec891ba..f143fccb 100644 --- a/autopcr/module/modulebase.py +++ b/autopcr/module/modulebase.py @@ -59,13 +59,26 @@ async def new_do_check(client: pcrclient) -> Tuple[bool, str]: def text_result(cls): return _wrap_init(cls, lambda self: setattr(self, 'text_result', True)) -class eResultStatus(Enum): +class eResultStatus(str, Enum): SUCCESS = "成功" SKIP = "跳过" WARNING = "警告" ABORT = "中止" ERROR = "错误" PANIC = "致命" + @classmethod + def _missing_(cls, value): + old = { + 'success': cls.SUCCESS, + 'skip': cls.SKIP, + 'warning': cls.WARNING, + 'abort': cls.ABORT, + 'error': cls.ERROR, + 'panic': cls.PANIC + } + if value in old: + return old[value] + return ValueError(f"{value} not found in eResultStatus") @dataclass_json @dataclass @@ -174,7 +187,9 @@ async def do_from(self, client: pcrclient) -> ModuleResult: result.log = str(e) result.status = eResultStatus.ERROR finally: - result.log = ('\n'.join(self.warn + self.log) + "\n" + result.log).strip() or "ok" + result.log = ('\n'.join(self.warn + + (['----'] if self.warn and self.log else []) + + self.log) + "\n" + result.log).strip() or "ok" return result diff --git a/autopcr/module/modulelistmgr.py b/autopcr/module/modulelistmgr.py new file mode 100644 index 00000000..16894978 --- /dev/null +++ b/autopcr/module/modulelistmgr.py @@ -0,0 +1,50 @@ +from typing import Dict, List, Callable, Any +from .modules import cron_modules, daily_modules, clan_modules, danger_modules, tool_modules, ModuleList, Module, CronModule +from .modulemgr import ModuleManager + +class ModuleListManager: + + def __init__(self, modulemgr: ModuleManager): + self.modulemgr = modulemgr + self.modules: Dict[str, ModuleList] = { + cron_modules.key: cron_modules, + daily_modules.key: daily_modules, + clan_modules.key: clan_modules, + danger_modules.key: danger_modules, + tool_modules.key: tool_modules, + } + self.name_to_modules: Dict[str, Callable] = {m.__name__: m for ml in self.modules.values() for m in ml.modules} + + @property + def daily_modules(self) -> List[Module]: + return self.get_modules_list('daily') + + @property + def cron_modules(self) -> List[CronModule]: + return self.get_modules_list('cron') + + def get_module_from_key(self, key: str) -> Module: + if key not in self.name_to_modules: + raise ValueError(f"模块{key}未找到") + return self.name_to_modules[key](self.modulemgr) + + def get_modules_list(self, key: str) -> List[Any]: + modules = self.modules.get(key, ModuleList()).modules + return [m(self.modulemgr) for m in modules] + + def generate_info(self, key: str): + modules = self.get_modules_list(key) + return { + 'config': {**{key: m.get_config(key) for m in modules for key in m.config}, **{m.key: m.get_config(m.key) for m in modules}}, + 'order': [m.key for m in modules], + 'info': {m.key: m.generate_info() for m in modules}, + } + + def generate_tab(self, clan: bool = False, batch: bool = False): + if clan: + modules = [daily_modules, tool_modules, clan_modules] + elif batch: + modules = [daily_modules, tool_modules, danger_modules] + else: + modules = [cron_modules, daily_modules, tool_modules, danger_modules] + return [{'key': m.key, 'name': m.name} for m in modules] diff --git a/autopcr/module/modulemgr.py b/autopcr/module/modulemgr.py index f1dbb0c5..6f51e91c 100644 --- a/autopcr/module/modulemgr.py +++ b/autopcr/module/modulemgr.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from dataclasses_json import dataclass_json -from typing import List, Dict, Tuple +from typing import List, Dict from ..model.error import * from ..model.enums import * @@ -16,44 +16,34 @@ class TaskResult: order: List[str] result: Dict[str, ModuleResult] + def get_last_result(self) -> ModuleResult: + if self.order: + return self.result[self.order[-1]] + else: + return ModuleResult() + class ModuleManager: _modules: List[type] = [] def __init__(self, config, parent): - from .modules import daily_modules, tool_modules, cron_modules, hidden_modules - from .modules.cron import CronModule + from .modulelistmgr import ModuleListManager from .accountmgr import Account self.parent: Account = parent - self.cron_modules: List[CronModule] = [m(self) for m in cron_modules] - self.daily_modules: List[Module] = [m(self) for m in daily_modules] - self.tool_modules: List[Module] = [m(self) for m in tool_modules] - self.hidden_modules: List[Module] = [m(self) for m in hidden_modules] - self.name_to_modules: Dict[str, Module] = {m.key: m for m in (self.daily_modules + self.tool_modules + self.hidden_modules)} + self.modules_list: ModuleListManager = ModuleListManager(self) self.client = self.parent.get_client() - self._crons = [] self._load_config(config) def _load_config(self, config): try: - self._crons.clear() for key, value in config.items(): self.client.keys[key] = value - - for key in [key for key in config if key.startswith("cron")]: - enable = config[key] - if enable: - time = config.get("time_" + key, "25:00") - if time: # in some case time is None - hour, minute = time.split(":")[0:2] - is_clan_battle_run = config.get("clanbattle_run_" + key, False) - self._crons.append((int(hour), int(minute), is_clan_battle_run)) except: traceback.print_exc() raise async def is_cron_run(self, hour: int, minute: int) -> bool: - for cron in self.cron_modules: + for cron in self.modules_list.cron_modules: if await cron.is_cron_run(hour, minute): await cron.update_client(self.client) return True @@ -63,39 +53,31 @@ async def is_cron_run(self, hour: int, minute: int) -> bool: def get_config(self, name, default): return self.client.keys.get(name, default) - def generate_config(self, modules: List[Module]): - return { - 'config': {**{key: m.get_config(key) for m in modules for key in m.config}, **{m.key: m.get_config(m.key) for m in modules}}, - 'order': [m.key for m in modules], - 'info': {m.key: m.generate_info() for m in modules}, - } - - def generate_daily_config(self): - return self.generate_config(self.cron_modules + self.daily_modules) + def generate_modules_info(self, key: str): + return self.modules_list.generate_info(key) - def generate_tools_config(self): - return self.generate_config(self.tool_modules) + def generate_tab(self, clan: bool = False, batch: bool = False): + return self.modules_list.generate_tab(clan, batch) - async def do_daily(self, isAdminCall: bool = False) -> Tuple[TaskResult, eResultStatus]: - resp = await self.do_task(self.client.keys, self.daily_modules, isAdminCall) + async def do_daily(self, isAdminCall: bool = False) -> "TaskResultInfo": + resp = await self.do_task(self.client.keys, self.modules_list.daily_modules, isAdminCall) status = eResultStatus.SUCCESS if any(m.status == eResultStatus.WARNING or m.status == eResultStatus.ABORT for m in resp.result.values()): status = eResultStatus.WARNING if any(m.status == eResultStatus.PANIC or m.status == eResultStatus.ERROR for m in resp.result.values()): status = eResultStatus.ERROR - await self.parent.save_daily_result(resp, status.value) - return resp, status + res = await self.parent.save_daily_result(resp, status) + return res - async def do_from_key(self, config: dict, key: str, isAdminCall: bool = False) -> Tuple[ModuleResult, str]: + async def do_from_key(self, config: dict, key: str, isAdminCall: bool = False) -> "ModuleResultInfo": config.update({ key: True, - "stamina_relative_not_run": False }) - modules = [self.name_to_modules[key]] + modules = [self.modules_list.get_module_from_key(key)] raw_resp = await self.do_task(config, modules, isAdminCall) resp = raw_resp.result[key] - await self.parent.save_single_result(key, resp) - return resp, resp.status.value + res = await self.parent.save_single_result(key, resp) + return res async def do_task(self, config: dict, modules: List[Module], isAdminCall: bool = False) -> TaskResult: if db.is_clan_battle_time() and self.parent.is_clan_battle_forbidden() and not isAdminCall: diff --git a/autopcr/module/modules/__init__.py b/autopcr/module/modules/__init__.py index dfc1806b..4d54b897 100644 --- a/autopcr/module/modules/__init__.py +++ b/autopcr/module/modules/__init__.py @@ -1,3 +1,5 @@ +from dataclasses import field +from typing import Any from .autosweep import * from .clan import * from .cron import * @@ -12,100 +14,134 @@ from .tools import * from .unit import * -cron_modules = [ - cron1, - cron2, - cron3, - cron4, - cron5, - cron6, -] +@dataclass +class ModuleList: + name: str = "" + key: str = "" + modules: List[Any] = field(default_factory=list) -daily_modules = [ - global_config, - chara_fortune, - mission_receive_first, - clan_like, - room_like_back, - free_gacha, - normal_gacha, - monthly_gacha, - room_accept_all, - explore_exp, - explore_mana, - underground_skip, - special_underground_skip, - tower_cloister_sweep, - smart_very_hard_sweep, - jjc_reward, - xinsui5_sweep, - xinsui4_sweep, - xinsui3_sweep, - xinsui2_sweep, - xinsui1_sweep, - starcup2_sweep, - starcup1_sweep, - hatsune_h_sweep, - hatsune_dear_reading, - present_receive, - smart_sweep, - mirai_very_hard_sweep, - smart_hard_sweep, - smart_shiori_sweep, - last_normal_quest_sweep, - smart_normal_sweep, +cron_modules = ModuleList( + '定时', + 'cron', + [ + cron1, + cron2, + cron3, + cron4, + cron5, + cron6, + ] +) - all_in_hatsune, +daily_modules = ModuleList( + '日常', + 'daily', + [ + global_config, + chara_fortune, + mission_receive_first, + clan_like, + room_like_back, + free_gacha, + normal_gacha, + monthly_gacha, + room_accept_all, + explore_exp, + explore_mana, + underground_skip, + special_underground_skip, + tower_cloister_sweep, + smart_very_hard_sweep, + jjc_reward, + xinsui5_sweep, + xinsui4_sweep, + xinsui3_sweep, + xinsui2_sweep, + xinsui1_sweep, + starcup2_sweep, + starcup1_sweep, + hatsune_h_sweep, + hatsune_dear_reading, + present_receive, + smart_sweep, + mirai_very_hard_sweep, + smart_hard_sweep, + smart_shiori_sweep, + last_normal_quest_sweep, + smart_normal_sweep, - hatsune_hboss_sweep, - hatsune_mission_accept1, - hatsune_gacha_exchange, - hatsune_mission_accept2, + all_in_hatsune, - jjc_daily, - pjjc_daily, - unit_equip_enhance_up, - unit_skill_level_up, + hatsune_hboss_sweep, + hatsune_mission_accept1, + hatsune_gacha_exchange, + hatsune_mission_accept2, - mission_receive_last, - seasonpass_accept, - seasonpass_reward, + jjc_daily, + pjjc_daily, + unit_equip_enhance_up, + unit_skill_level_up, - normal_shop, - limit_shop, - underground_shop, - jjc_shop, - pjjc_shop, - clanbattle_shop, - - clan_equip_request, - love_up, - main_story_reading, - tower_story_reading, - hatsune_story_reading, - hatsune_sub_story_reading, - guild_story_reading, - unit_story_reading, - room_upper_all, - user_info, -] + mission_receive_last, + seasonpass_accept, + seasonpass_reward, -hidden_modules = [ -] + normal_shop, + limit_shop, + underground_shop, + jjc_shop, + pjjc_shop, + clanbattle_shop, + + clan_equip_request, + love_up, + main_story_reading, + tower_story_reading, + hatsune_story_reading, + hatsune_sub_story_reading, + guild_story_reading, + unit_story_reading, + room_upper_all, + user_info, + ] +) -tool_modules = [ - # cook_pudding, - unit_promote, - missing_unit, - get_need_equip, - get_normal_quest_recommand, - get_need_memory, - get_need_xinsui, - get_clan_support_unit, - get_library_import_data, - jjc_back, - pjjc_back, - jjc_info, - pjjc_info, - gacha_start, -] +clan_modules = ModuleList( + '公会', + 'clan', + [ + unit_promote_batch, + set_my_party, + ] +) + +danger_modules = ModuleList( + '危险', + 'danger', + [ + gacha_start, + ] +) + +tool_modules = ModuleList( + '工具', + 'tool', + [ + # cook_pudding, + unit_promote, + unit_set_unique_equip_growth, + missing_unit, + get_need_equip, + get_normal_quest_recommand, + get_need_memory, + get_need_pure_memory, + get_need_xinsui, + get_clan_support_unit, + get_library_import_data, + jjc_back, + pjjc_back, + jjc_info, + pjjc_info, + pjjc_shuffle_team, + ] +) diff --git a/autopcr/module/modules/autosweep.py b/autopcr/module/modules/autosweep.py index 0165d6a1..5e7e72ad 100644 --- a/autopcr/module/modules/autosweep.py +++ b/autopcr/module/modules/autosweep.py @@ -255,7 +255,7 @@ def get_max_times(self, client: pcrclient, quest_id: int) -> int: class mirai_very_hard_sweep(simple_demand_sweep_base): async def get_need_list(self, client: pcrclient) -> List[Tuple[ItemType, int]]: need_list = client.data.get_pure_memory_demand_gap() - need_list += Counter({(eInventoryType.Item, pure_memory_id): 150 * cnt for pure_memory_id, cnt in unique_equip_2_pure_memory_id}) + need_list.update(Counter({(eInventoryType.Item, pure_memory_id): 150 * cnt for pure_memory_id, cnt in unique_equip_2_pure_memory_id})) need_list = [(token, need) for token, need in need_list.items() if need > 0] if not need_list: raise SkipError("所有纯净碎片均已盈余") diff --git a/autopcr/module/modules/story.py b/autopcr/module/modules/story.py index 1bee5847..b4cb9e76 100644 --- a/autopcr/module/modules/story.py +++ b/autopcr/module/modules/story.py @@ -164,7 +164,7 @@ async def do_task(self, client: pcrclient): else: self._log(f"共{len(self.log)}篇") -@description('仅阅读当期活动') +@description('阅读当期活动和已通关外传') @name('阅读活动信赖度') @default(True) class hatsune_dear_reading(Module): @@ -188,6 +188,12 @@ async def do_task(self, client: pcrclient): continue resp = await client.get_shiori_event_top(event_id) + if not resp.unchoiced_dear_story_id_list and \ + all(story_id not in client.data.unlock_story_ids for story_id in db.dear_story_detail[db.dear_story_data[event_id].story_group_id]): + event_name = db.event_name[event_id] + self._log(f"活动{event_name}信赖度未解锁") + continue + resp = await client.get_shiori_dear_top(event_id) for story in resp.unlock_dear_story_info_list: if not story.is_choiced: diff --git a/autopcr/module/modules/sweep.py b/autopcr/module/modules/sweep.py index 98f11af5..0e29d670 100644 --- a/autopcr/module/modules/sweep.py +++ b/autopcr/module/modules/sweep.py @@ -149,9 +149,15 @@ async def do_task(self, client: pcrclient): raise SkipError("当前无特别地下城") infos = await client.get_dungeon_info() + special_dungeon_area = db.get_open_secret_dungeon_area() - special_info = await client.get_special_dungeon_info(special_dungeon_area) + _special_info = None secret_dungeon_retreat = self.get_config("secret_dungeon_retreat") + async def special_dungeon_info(refresh: bool = False): + nonlocal _special_info + if refresh or not _special_info: + _special_info = await client.get_special_dungeon_info(special_dungeon_area) + return _special_info def dungeon_name(id: int): if id in db.dungeon_area: @@ -169,7 +175,8 @@ async def do_enter(id): if db.secret_dungeon_area[id].open_area_id not in infos.dungeon_cleared_area_id_list: raise AbortError(f"【{dungeon_name(id)}】未讨伐,无法进入特别地下城") - await client.deck_update(ePartyType.DUNGEON, [0, 0, 0, 0, 0], sorted=True) + await special_dungeon_info(refresh=True) + await client.deck_update(ePartyType.DUNGEON, [0, 0, 0, 0, 0]) req = await client.enter_special_dungeon(id) reward_list = req.skip_result_list if req.skip_result_list else [] @@ -181,6 +188,7 @@ async def do_enter(id): self._log(f"进入了【{dungeon_name(id)}】,获得了:\n{result}") rest = infos.rest_challenge_count[0].count + if infos.enter_area_id != 0: if not db.is_secret_dungeon_id(infos.enter_area_id): raise AbortError("当前位于普通地下城,不支持扫荡") @@ -188,7 +196,7 @@ async def do_enter(id): self._log(f"当前位于【{dungeon_name(infos.enter_area_id)}】") if rest: if secret_dungeon_retreat: - if special_info.clear_num == 0: + if (await special_dungeon_info()).clear_num == 0: raise AbortError("特别地下城未通关,将不撤退") await do_retreat(infos.enter_area_id) else: @@ -198,7 +206,7 @@ async def do_enter(id): if rest: await do_enter(special_dungeon_area) - if special_info.clear_num == 0: + if (await special_dungeon_info()).clear_num == 0: raise AbortError("特别地下城尚未首通,请记得通关") if not rest: diff --git a/autopcr/module/modules/tools.py b/autopcr/module/modules/tools.py index 298529c2..225fc990 100644 --- a/autopcr/module/modules/tools.py +++ b/autopcr/module/modules/tools.py @@ -12,6 +12,8 @@ from ...model.enums import * from ...util.arena import instance as ArenaQuery import datetime +import random +from collections import Counter @name('【活动限时】一键做布丁') @default(True) @@ -521,6 +523,28 @@ def use_cache(self) -> bool: return self.get_config("pjjc_info_cache") async def get_rank_info(self, client: pcrclient, num: int, page: int) -> List[GrandArenaSearchOpponent]: return (await client.grand_arena_rank(num, page)).ranking +@description('将pjjc防守阵容随机错排') +@name('pjjc换防') +class pjjc_shuffle_team(Module): + async def do_task(self, client: pcrclient): + ids = random.choice([ [1, 2, 0], [2, 0, 1] ]) + deck_list: List[DeckListData] = [] + cnt = 3 + for i in range(cnt): + deck_number = getattr(ePartyType, f"GRAND_ARENA_DEF_{i + 1}") + units = client.data.deck_list[deck_number] + units_id = [getattr(units, f"unit_id_{i + 1}") for i in range(5)] + + deck = DeckListData() + deck_number = getattr(ePartyType, f"GRAND_ARENA_DEF_{ids[i] + 1}") + deck.deck_number = deck_number + deck.unit_list = units_id + deck_list.append(deck) + + deck_list.sort(key=lambda x: x.deck_number) + self._log('\n'.join([f"{i} -> {j}" for i, j in enumerate(ids)])) + await client.deck_update_list(deck_list) + @description('获得可导入到兰德索尔图书馆的账号数据') @name('兰德索尔图书馆导入数据') @@ -556,6 +580,20 @@ async def do_task(self, client: pcrclient): msg = '\n'.join([f'{db.get_inventory_name_san(item[0])}: {"缺少" if item[1] > 0 else "盈余"}{abs(item[1])}片{("(" + msg[item[0]] + ")") if item[0] in msg else ""}' for item in demand]) self._log(msg) +@description('根据每个角色升六星(国服当前)、满二专(日服当前)所需的纯净碎片减去库存的结果') +@name('获取纯净碎片缺口') +@default(True) +class get_need_pure_memory(Module): + async def do_task(self, client: pcrclient): + from .autosweep import unique_equip_2_pure_memory_id + need_list = client.data.get_pure_memory_demand_gap() + need_list.update(Counter({(eInventoryType.Item, pure_memory_id): 150 * cnt for pure_memory_id, cnt in unique_equip_2_pure_memory_id})) + demand = list(need_list.items()) + demand = sorted(demand, key=lambda x: x[1], reverse=True) + msg = {} + msg = '\n'.join([f'{db.get_inventory_name_san(item[0])}: {"缺少" if item[1] > 0 else "盈余"}{abs(item[1])}片' for item in demand]) + self._log(msg) + @description('根据每个角色开专、升级至当前最高专所需的心碎减去库存的结果,大心转换成10心碎') @name('获取心碎缺口') @default(True) @@ -624,3 +662,55 @@ async def do_task(self, client: pcrclient): msg = '\n--------\n'.join(tot) self._log(msg) + +@description('从指定面板的指定队开始设置。6行重复,标题+5行角色ID 角色名字 角色等级 角色星级') +@texttype("set_my_party_text", "队伍阵容", "") +@inttype("party_start_num", "初始队伍", 1, [i for i in range(1, 11)]) +@inttype("tab_start_num", "初始面板", 1, [i for i in range(1, 7)]) +@name('设置编队') +class set_my_party(Module): + async def do_task(self, client: pcrclient): + set_my_party_text: str = self.get_config('set_my_party_text') + tab_number: int = self.get_config('tab_start_num') + party_number: int = self.get_config('party_start_num') - 1 + party = set_my_party_text.splitlines() + for i in range(0, len(party), 6): + + party_number += 1 + if party_number == 11: + tab_number += 1 + party_number = 1 + if tab_number >= 6: + raise AbortError("队伍数量超过上限") + + title = party[i] + "记得借人" + unit_list = [u.split('\t') for u in party[i + 1 : i + 1 + 5]] + + own_unit = [u for u in unit_list if int(u[0]) in client.data.unit] + not_own_unit = [u for u in unit_list if int(u[0]) not in client.data.unit] + if not_own_unit: + self._warn(f"{title}未持有:{', '.join([u[1] for u in not_own_unit])}") + + change_rarity_list = [] + unit_list = [] + for unit in own_unit: + id = int(unit[0]) + star = int(unit[3]) + unit_data = client.data.unit[id] + can_change_star = unit_data.unit_rarity == 5 + now_star = unit_data.battle_rarity if unit_data.battle_rarity else unit_data.unit_rarity + if can_change_star and star != now_star: + if star >= 3 and star <= 5 and now_star >= 3 and now_star <= 5: + change_rarity = ChangeRarityUnit(unit_id=id, battle_rarity=star) + change_rarity_list.append(change_rarity) + else: + self._warn(f"{title}:{unit[1]}星级无法{now_star} -> {star}") + unit_list.append(id) + + if change_rarity_list: + await client.unit_change_rarity(change_rarity_list) + if not unit_list: + self._warn(f"{title}没有可用的角色") + else: + await client.set_my_party(tab_number, party_number, 4, title, unit_list, change_rarity_list) + self._log(f"设置了{title}") diff --git a/autopcr/module/modules/unit.py b/autopcr/module/modules/unit.py index 2dbb8fe9..dff3912d 100644 --- a/autopcr/module/modules/unit.py +++ b/autopcr/module/modules/unit.py @@ -6,7 +6,7 @@ from ...core.pcrclient import pcrclient from ...model.error import * from ...db.database import db -from ...db.models import GrowthParameter +from ...db.models import GrowthParameter, GrowthParameterUnique from ...model.enums import * from ...model.common import SkillLevelInfo, SkillLevelUpDetail, UnitData @@ -15,6 +15,7 @@ class UnitController(Module): unit_id: int client: pcrclient auto_level_up : bool = True + auto_rank_up : bool = True skill_name = { eSkillLocationCategory.UNION_BURST_SKILL: "UB", @@ -27,22 +28,54 @@ class UnitController(Module): def unit(self) -> UnitData: return self.client.data.unit[self.unit_id] + @property + def unit_name(self) -> str: + return db.get_unit_name(self.unit_id) + + async def set_unique_growth_unit(self): + ball = self.client.data.filter_inventory(db.is_unique_equip_glow_ball) + if not ball: + raise AbortError(f"没有专武球") + if not self.unit.unique_equip_slot: + raise AbortError(f"{self.unit_name}专武未实装") + if self.unit.unique_equip_slot[0].is_slot: + raise AbortError(f"{self.unit_name}专武已装备") + if await self.is_unique_growth_unit(): + raise AbortError(f"{self.unit_name}已装备专武球") + ball = ball[0] + self._log(f"{self.unit_name}装备专武球") + await self.client.set_growth_item_unique(self.unit_id, ball[1]) + async def is_growth_unit(self) -> Union[GrowthParameter, None]: - unit_name = db.get_unit_name(self.unit_id) if self.unit_id not in self.client.data.unit: - raise AbortError(f"未解锁角色{unit_name}") + raise AbortError(f"未解锁角色{self.unit_name}") if self.unit_id not in self.client.data.growth_unit: return None - growth_id = list(set(self.client.data.growth_unit[self.unit_id].growth_parameter_list.growth_id_list) & set(db.growth_parameter.keys())) # 专武球后应该需要修改 - if len(growth_id) != 1: - raise ValueError("未知的光辉球类型" + ','.join(map(str, growth_id))) + growth_id = list(set(self.client.data.growth_unit[self.unit_id].growth_parameter_list.growth_id_list) & set(db.growth_parameter.keys())) + if not growth_id: + return None + if len(growth_id) > 1: + raise ValueError(f"{self.unit_name}怎么装了多个辉光球?" + ','.join(map(str, growth_id))) growth_limit = db.growth_parameter[growth_id[0]] return growth_limit + async def is_unique_growth_unit(self) -> Union[GrowthParameterUnique, None]: + if self.unit_id not in self.client.data.unit: + raise AbortError(f"未解锁角色{self.unit_name}") + if self.unit_id not in self.client.data.growth_unit: + return None + growth_id = list(set(self.client.data.growth_unit[self.unit_id].growth_parameter_list.growth_id_list) & set(db.growth_parameter_unique.keys())) + if not growth_id: + return None + if len(growth_id) > 1: + raise ValueError(f"{self.unit_name}怎么装了多个专武辉光球?" + ','.join(map(str, growth_id))) + growth_limit = db.growth_parameter_unique[growth_id[0]] + return growth_limit + async def unit_skill_up(self, location: eSkillLocationCategory, target_level: int, current_level: int, free: bool): step = target_level - current_level if free: - self._log(f"{db.get_unit_name(self.unit.id)}技能{self.skill_name[location]}升至{current_level + step}级") + self._log(f"{self.unit_name}技能{self.skill_name[location]}升至{current_level + step}级") skill_level_up_detail = SkillLevelUpDetail() skill_level_up_detail.location = location skill_level_up_detail.step = step @@ -51,21 +84,20 @@ async def unit_skill_up(self, location: eSkillLocationCategory, target_level: in else: mana = db.get_skill_up_cost(current_level, target_level) if not (await self.client.prepare_mana(mana)): - raise AbortError(f"{db.get_unit_name(self.unit.id)}技能{self.skill_name[location]}升至{target_level}级需要{mana}玛娜,当前玛娜不足") + raise AbortError(f"{self.unit_name}技能{self.skill_name[location]}升至{target_level}级需要{mana}玛娜,当前玛娜不足") skill_levelup_list = [SkillLevelUpDetail(location=location, step=target_level-current_level, current_level=current_level)] - self._log(f"{db.get_unit_name(self.unit.id)}技能{self.skill_name[location]}升至{current_level + step}级") + self._log(f"{self.unit_name}技能{self.skill_name[location]}升至{current_level + step}级") await self.client.skill_level_up(self.unit.id, skill_levelup_list) # md python 没有引用 async def unit_skill_up_aware(self, location: eSkillLocationCategory, skill: Callable[[], SkillLevelInfo], target_skill_level: int, limit: Union[None, GrowthParameter] = None): - if limit and limit.skill_level < target_skill_level: - raise AbortError(f"{db.get_unit_name(self.unit.id)}技能{self.skill_name[location]}超过了免费可提升等级{limit.skill_level},请自行完成免费可提升的所有内容") - self._log(f"{db.get_unit_name(self.unit.id)}技能{self.skill_name[location]}超过了免费可提升等级{limit.skill_level},先提升至等级{limit.skill_level}") + if limit and limit.skill_level < target_skill_level and skill().skill_level < limit.skill_level: + self._log(f"{self.unit_name}技能{self.skill_name[location]}超过了免费可提升等级{limit.skill_level},先提升至等级{limit.skill_level}") await self.unit_skill_up_aware(location, skill, limit.skill_level, limit) if target_skill_level > self.unit.unit_level: - self._log(f"{db.get_unit_name(self.unit.id)}技能{self.skill_name[location]}升至{target_skill_level}级需要角色等级升至{target_skill_level}级") + self._log(f"{self.unit_name}技能{self.skill_name[location]}升至{target_skill_level}级需要角色等级升至{target_skill_level}级") if not self.auto_level_up: raise AbortError(f"当前设置不自动拉角色等级,无法升级") await self.unit_level_up_aware(target_skill_level, limit) @@ -74,19 +106,19 @@ async def unit_skill_up_aware(self, location: eSkillLocationCategory, skill: Cal async def unit_promotion_up(self, target_promotion_level: int, free: bool): if free: - self._log(f"{db.get_unit_name(self.unit.id)}升至品级{target_promotion_level}") + self._log(f"{self.unit_name}升至品级{target_promotion_level}") await self.client.unit_free_promotion(self.unit.id, target_promotion_level) else: now_equip_slot = [equip.is_slot for equip in self.unit.equip_slot] equips = db.get_rank_promote_equip_demand(self.unit.id, self.unit.promotion_level, now_equip_slot, target_promotion_level, [False] * 6) cost_equip, mana = db.craft_equip(equips) if not (await self.client.prepare_mana(mana)): - raise AbortError(f"{db.get_unit_name(self.unit.id)}升至品级{target_promotion_level}需要{mana}玛娜,当前玛娜不足") + raise AbortError(f"{self.unit_name}升至品级{target_promotion_level}需要{mana}玛娜,当前玛娜不足") - bad = [(item, cnt - self.client.data.get_inventory(item)) for item, cnt in cost_equip.items() if cnt > self.client.data.get_inventory(item)] + bad = self.client.data.get_not_enough_item(cost_equip) if bad: bad_list = '\n'.join([f"{db.get_inventory_name_san(item)}缺少{cnt}片" for item, cnt in bad]) - raise AbortError(f"{db.get_unit_name(self.unit.id)}升至品级{target_promotion_level}所需材料不足:\n{bad_list}") + raise AbortError(f"{self.unit_name}升至品级{target_promotion_level}所需材料不足:\n{bad_list}") equip_recipe_list = [db.craft_equip( db.unit_promotion_equip_count[self.unit.id][self.unit.promotion_level] - @@ -95,18 +127,17 @@ async def unit_promotion_up(self, target_promotion_level: int, free: bool): ] + [db.craft_equip( db.unit_promotion_equip_count[self.unit.id][rank] )[0] for rank in range(self.unit.promotion_level + 1, target_promotion_level)] - self._log(f"{db.get_unit_name(self.unit.id)}升至品级{target_promotion_level}") + self._log(f"{self.unit_name}升至品级{target_promotion_level}") await self.client.multi_promotion(self.unit.id, target_promotion_level, equip_recipe_list) async def unit_promotion_up_aware(self, target_promotion_level: int, limit: Union[None, GrowthParameter]): - if limit and target_promotion_level > limit.promotion_level: - raise AbortError(f"目标品级{target_promotion_level}超过了免费可提升品级{limit.promotion_level},请自行完成免费可提升的所有内容") + if limit and target_promotion_level > limit.promotion_level and self.unit.promotion_level < limit.promotion_level: self._log(f"目标品级{target_promotion_level}超过了免费可提升品级{limit.promotion_level},先提升至品级{limit.promotion_level}") await self.unit_promotion_up_aware(limit.promotion_level, limit) demand_level = db.get_promotion_demand_level(self.unit.id, target_promotion_level) if self.unit.unit_level < demand_level: - self._log(f"{db.get_unit_name(self.unit.id)}升至品级{target_promotion_level}需要升至{demand_level}级") + self._log(f"{self.unit_name}升至品级{target_promotion_level}需要升至{demand_level}级") await self.unit_level_up_aware(demand_level, limit) await self.unit_promotion_up(target_promotion_level, @@ -115,64 +146,62 @@ async def unit_promotion_up_aware(self, target_promotion_level: int, limit: Unio async def unit_level_up(self, target_level: int, free: bool): if free: - self._log(f"{db.get_unit_name(self.unit.id)}升至{target_level}级") + self._log(f"{self.unit_name}升至{target_level}级") await self.client.unit_free_level_up(self.unit.id, target_level) else: exp_demand = db.get_level_up_total_exp(target_level) - self.unit.unit_exp exp_demand_limit = db.get_level_up_total_exp(target_level + 1) - self.unit.unit_exp cost_potion = self.client.data.get_level_up_exp_potion_demand(exp_demand, exp_demand_limit) if not cost_potion: - raise AbortError(f"{db.get_unit_name(self.unit.id)}升至{target_level}级所需经验药水不足,或无法达到其要求") + raise AbortError(f"{self.unit_name}升至{target_level}级所需经验药水不足,或无法达到其要求") - self._log(f"{db.get_unit_name(self.unit.id)}升至{target_level}级") + self._log(f"{self.unit_name}升至{target_level}级") await self.client.unit_level_up(self.unit.id, cost_potion) async def unit_level_up_aware(self, target_level: int, limit: Union[GrowthParameter, None] = None): - if limit and target_level > limit.unit_level: - raise AbortError(f"目标等级{target_level}超过了免费可提升等级{limit.unit_level},请自行完成免费可提升的所有内容") - self._log(f"目标等级{target_level}超过了免费可提升等级{limit.unit_level},先提升至等级{limit.unit_level}") + if limit and target_level > limit.unit_level and self.unit.unit_level < limit.unit_level: + self._log(f"{self.unit_name}目标等级{target_level}超过了免费可提升等级{limit.unit_level},先提升至等级{limit.unit_level}") await self.unit_level_up_aware(limit.unit_level, limit) level_limit = self.client.data.team_level + (10 if self.unit.exceed_stage else 0) if target_level > level_limit: - raise AbortError(f"{db.get_unit_name(self.unit.id)}的目标等级{target_level}超过了可提升的最高等级{level_limit},无法升级") + raise AbortError(f"{self.unit_name}目标等级{target_level}超过了可提升的最高等级{level_limit},无法升级") await self.unit_level_up(target_level, limit is not None and target_level <= limit.unit_level) async def unit_equip_slot(self, equip_id: int, slot_num: int, free: bool): if free: - self._log(f"{db.get_unit_name(self.unit.id)}装备{db.get_equip_name(equip_id)}") + self._log(f"{self.unit_name}装备{db.get_equip_name(equip_id)}") await self.client.unit_free_equip(self.unit_id, [slot_num]) else: item: ItemType = (eInventoryType.Equip, equip_id) cost_equip, mana = db.craft_equip(Counter({item: 1})) if (not await self.client.prepare_mana(mana)): - raise AbortError(f"{db.get_unit_name(self.unit_id)}装备{db.get_equip_name(equip_id)}需要{mana}玛娜,当前玛娜不足") + raise AbortError(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}需要{mana}玛娜,当前玛娜不足") - bad = [(item, cnt - self.client.data.get_inventory(item)) for item, cnt in cost_equip.items() if cnt > self.client.data.get_inventory(item)] + bad = self.client.data.get_not_enough_item(cost_equip) if bad: bad_list = '\n'.join([f"{db.get_inventory_name_san(item)}缺少{cnt}片" for item, cnt in bad]) - raise AbortError(f"{db.get_unit_name(self.unit.id)}装备{db.get_equip_name(equip_id)}所需材料不足:\n{bad_list}") + raise AbortError(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}所需材料不足:\n{bad_list}") - self._log(f"{db.get_unit_name(self.unit.id)}装备{db.get_equip_name(equip_id)}") + self._log(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}") await self.client.unit_craft_equip(self.unit.id, slot_num, cost_equip) async def unit_equip_slot_aware(self, equip_id: int, slot_num: int, limit: Union[None, GrowthParameter] = None): if db.equip_data[equip_id].require_level > self.unit.unit_level: - self._log(f"{db.get_unit_name(self.unit.id)}装备{db.get_equip_name(equip_id)}需要角色升至{db.equip_data[equip_id].require_level}级") + self._log(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}需要角色升至{db.equip_data[equip_id].require_level}级") if not self.auto_level_up: raise AbortError(f"当前设置不自动拉角色等级,无法装备") await self.unit_level_up_aware(db.equip_data[equip_id].require_level, limit) - if limit and limit.promotion_level <= self.unit.promotion_level and getattr(limit, f"equipment_{slot_num}") == -1: # 不知何用 - raise AbortError(f"{db.get_unit_name(self.unit.id)}品级{self.unit.promotion_level}的{slot_num}位装备{db.get_equip_name(equip_id)}不支持免费装备") - - await self.unit_equip_slot(equip_id, slot_num, limit is not None) + await self.unit_equip_slot(equip_id, slot_num, limit is not None and ( + limit.promotion_level > self.unit.promotion_level or \ + limit.promotion_level == self.unit.promotion_level and getattr(limit, f"equipment_{slot_num}") != -1)) async def unit_equip_enhance(self, equip_id: int, slot_num: int, enhancement_level: int, free: bool): if free: - self._log(f"{db.get_unit_name(self.unit.id)}{slot_num}位装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级") + self._log(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级") await self.client.equipment_free_enhance(self.unit.id, slot_num, enhancement_level) else: current_enhancement_pt = self.unit.equip_slot[slot_num - 1].enhancement_pt @@ -180,15 +209,125 @@ async def unit_equip_enhance(self, equip_id: int, slot_num: int, enhancement_lev pt_limit = -1 if enhancement_level == 5 else db.get_equip_star_pt(equip_id, enhancement_level + 1) - current_enhancement_pt cost_stone = self.client.data.get_equip_enhance_stone_demand(pt, pt_limit) if not cost_stone: - raise AbortError(f"{db.get_unit_name(self.unit.id)}{slot_num}位上装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级所需强化石不足,或无法达到其要求") + raise AbortError(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级所需强化石不足,或无法达到其要求") mana = pt * 200 # guess if not (await self.client.prepare_mana(mana)): - raise AbortError(f"{db.get_unit_name(self.unit.id)}{slot_num}位上装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级需要{mana}玛娜,当前玛娜不足") + raise AbortError(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级需要{mana}玛娜,当前玛娜不足") - self._log(f"{db.get_unit_name(self.unit.id)}{slot_num}位上装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级") + self._log(f"{self.unit_name}{slot_num}位装备{db.get_equip_name(equip_id)}强化至{enhancement_level}星级") await self.client.equipment_enhance(self.unit.id, slot_num, current_enhancement_pt, cost_stone) + async def unit_unique_slot_aware(self, limit: Union[GrowthParameterUnique, None] = None): + if self.unit.promotion_level < 7: + self._log(f"{self.unit_name}专武需要角色升至品级7") + if not self.auto_rank_up: + raise AbortError(f"当前设置不自动拉品级,无法装备") + await self.unit_promotion_up_aware(7, await self.is_growth_unit()) + + if limit: + pass + else: + await self.unit_unique_slot(False) + + async def unit_unique_slot(self, free: bool): + if free: # impossible + pass + else: + equip_id = db.unit_unique_equip[1][self.unit_id].equip_id + if self.client.data.get_inventory((eInventoryType.Equip, equip_id)) == 0: + demand = db.get_unique_equip_material_demand(self.unit_id, 1, 0, 1) + bad = self.client.data.get_not_enough_item(demand) + if bad: + bad_list = '\n'.join([f"{db.get_inventory_name_san(item)}缺少{cnt}片" for item, cnt in bad]) + raise AbortError(f"制作{self.unit_name}专武所需材料不足:\n{bad_list}") + self._log(f"{self.unit_name}制作专武") + equip_recipe_dict = Counter({item: cnt for item, cnt in demand.items() if db.is_equip(item) or item == db.xinsui}) + item_recipe_dict = demand - equip_recipe_dict + await self.client.equipment_craft_unique(equip_id, equip_recipe_dict, item_recipe_dict, 0) + mana = self.client.data.get_mana() + self._log(f"{self.unit_name}装备专武") + await self.client.equipment_multi_enhance_unique(self.unit_id, 1, mana, [], [], [], [], [], 0, 1, [], 0) + + async def unit_unique_equip_rank_up_aware(self, target_rank: int, limit: Union[GrowthParameterUnique, None] = None): + require_level = db.get_unique_equip_rank_required_level(1, self.unit_id, target_rank) + if require_level > self.unit.unit_level: + self._log(f"{self.unit_name}专武突破至{target_rank}品级需要角色升至{require_level}级") + if not self.auto_level_up: + raise AbortError(f"当前设置不自动拉角色等级,无法升级") + await self.unit_level_up_aware(require_level, await self.is_growth_unit()) + + # 先判断整个突破过程是否可行 + start_rank = self.unit.unique_equip_slot[0].rank if not limit else max(self.unit.unique_equip_slot[0].rank, limit.unique_equip_rank_1) + demand = db.get_unique_equip_material_demand(self.unit_id, 1, start_rank, target_rank) + bad = self.client.data.get_not_enough_item(demand) + if bad: + bad_list = '\n'.join([f"{db.get_inventory_name_san(item)}缺少{cnt}片" for item, cnt in bad]) + raise AbortError(f"{self.unit_name}专武突破至{target_rank}品级所需材料不足:\n{bad_list}") + + if not self.unit.unique_equip_slot: + raise AbortError(f"{self.unit_name}专武未实装") + + if self.unit.unique_equip_slot[0].is_slot == 0: + self._log(f"{self.unit_name}专武突破至{target_rank}品级需要先装备专武") + await self.unit_unique_slot_aware(limit) + + if limit and self.unit.unique_equip_slot[0].enhancement_pt < limit.unique_equip_strength_point_1: + pass + else: + await self.unit_unique_equip_rank_up(target_rank, False) + + async def unit_unique_equip_rank_up(self, target_rank: int, free: bool): + if free: # impossible + pass + else: + current_rank = self.unit.unique_equip_slot[0].rank + demand = db.get_unique_equip_material_demand(self.unit_id, 1, current_rank, target_rank) + bad = self.client.data.get_not_enough_item(demand) + if bad: + bad_list = '\n'.join([f"{db.get_inventory_name_san(item)}缺少{cnt}片" for item, cnt in bad]) + raise AbortError(f"{self.unit_name}专武突破至{target_rank}品级所需材料不足:\n{bad_list}") + for cur_rank in range(current_rank, target_rank): + self._log(f"{self.unit_name}专武突破至{cur_rank + 1}品级") + demand = db.get_unique_equip_material_demand(self.unit_id, 1, cur_rank, cur_rank + 1) + equip_recipe_dict = Counter({item: cnt for item, cnt in demand.items() if db.is_equip(item) or item == db.xinsui}) + item_recipe_dict = demand - equip_recipe_dict + await self.client.equipment_rankup_unique(self.unit.id, 1, equip_recipe_dict, item_recipe_dict, cur_rank) + + async def unit_unique_equip_enhance_aware(self, target_level: int, limit: Union[GrowthParameterUnique, None] = None): + target_pt = db.get_unique_equip_pt_from_level(1, target_level) + if limit and target_pt > limit.unique_equip_strength_point_1 and self.unit.unique_equip_slot[0].enhancement_pt < limit.unique_equip_strength_point_1: + level = db.get_unique_equip_level_from_pt(1, limit.unique_equip_strength_point_1) + self._log(f"目标专武等级{target_level}超过了免费可提升等级{level},先提升至等级{level}") + await self.unit_unique_equip_enhance_aware(level, limit) + + target_rank = db.get_unique_equip_rank_from_level(1, target_level) + + if target_rank > self.unit.unique_equip_slot[0].rank: + self._log(f"{self.unit_name}专武升至{target_level}级需要先突破至{target_rank}品级") + await self.unit_unique_equip_rank_up_aware(target_rank, limit) + + await self.unit_unique_equip_enhance(target_level, limit is not None and target_level <= db.get_unique_equip_level_from_pt(1, limit.unique_equip_strength_point_1)) + + async def unit_unique_equip_enhance(self, target_level: int, free: bool): + if free: + self._log(f"{self.unit_name}专武升至{target_level}级") + start_pt = self.unit.unique_equip_slot[0].enhancement_pt + target_pt = db.get_unique_equip_pt_from_level(1, target_level) + await self.client.unique_equip_free_enhance(self.unit.id, 1, start_pt, target_pt) + else: + current_enhancement_pt: int = self.unit.unique_equip_slot[0].enhancement_pt + target_pt = db.get_unique_equip_pt_from_level(1, target_level) + pt = target_pt - current_enhancement_pt + + pt_limit = -1 if target_level == db.get_unique_equip_max_level_from_rank(1, self.unit.unique_equip_slot[0].rank) else db.get_unique_equip_pt_from_level(1, target_level + 1) - current_enhancement_pt + cost_stone = self.client.data.get_equip_enhance_stone_demand(pt, pt_limit) + if not cost_stone: + raise AbortError(f"{self.unit_name}专武升至{target_level}级所需强化石不足,或无法达到其要求") + self._log(f"{self.unit_name}专武升至{target_level}级") + await self.client.equipment_enhance_unique(self.unit.id, 1, cost_stone, current_enhancement_pt) + + @description('支持全部角色') @name('完成装备强化任务') @singlechoice("equip_enhance_up_unit", "强化角色", "100101:日和莉", [f"{unit}:{db.unit_data[unit].unit_name}" for unit in db.unlock_unit_condition]) @@ -255,16 +394,29 @@ async def do_task(self, client: pcrclient): stop = True break except AbortError as e: - print(e) + traceback.print_exc() continue else: self._log(f"所有技能均等于角色等级{self.unit.unit_level}级,需提升角色等级") await self.unit_level_up_aware(self.unit.unit_level + 1, growth_limit) +@description('仅支持设置未装备专武的角色,优先使用低专武球') +@name('设置专武球') +@singlechoice("unit_set_unique_equip_growth_id", "装备角色", "100101:日和莉", [f"{unit}:{db.unit_data[unit].unit_name}" for unit in db.unlock_unit_condition]) +@default(False) +class unit_set_unique_equip_growth(UnitController): + + async def do_task(self, client: pcrclient): + self.client = client + unit_id, unit_name = self.get_config('unit_set_unique_equip_growth_id').split(':') + self.unit_id = int(unit_id) + await self.set_unique_growth_unit() @description('支持全部角色,装备星级-1表示不穿装备,自动拉等级指当前等级不足以穿装备或提升技能等级,将会提升角色等级') @name('拉角色练度') +@booltype("unit_promote_rank_when_fail_to_unique_equip", "自动拉品级", False) @booltype("unit_promote_level_when_fail_to_equip_or_skill", "自动拉等级", False) +@singlechoice("unit_promote_unique_equip_level", "专武等级", 0, lambda : db.unit_unique_equip_level_candidate(1)) @singlechoice("unit_promote_equip_5", "右下装备星级", -1, [-1,0,1,2,3,4,5]) @singlechoice("unit_promote_equip_4", "左下装备星级", -1, [-1,0,1,2,3,4,5]) @singlechoice("unit_promote_equip_3", "右中装备星级", -1, [-1,0,1,2,3,4,5]) @@ -289,6 +441,7 @@ async def do_task(self, client: pcrclient): growth_limit = await self.is_growth_unit() self.auto_level_up = bool(self.get_config('unit_promote_level_when_fail_to_equip_or_skill')) + self.auto_rank_up = bool(self.get_config('unit_promote_rank_when_fail_to_unique_equip')) # 拉等级 target_level = int(self.get_config('unit_promote_level')) @@ -316,8 +469,10 @@ async def do_task(self, client: pcrclient): self._log(f"{i+1}号位装备预设{star}星级超过了最大星级{promotion_limit},提升至最大星级{promotion_limit}") star = promotion_limit - if equip.enhancement_level < star: - await self.unit_equip_enhance(equip.id, i + 1, star, growth_limit is not None) + if equip.enhancement_level < star: # 暂时没有星级强化限制 + await self.unit_equip_enhance(equip.id, i + 1, star, growth_limit is not None and ( + self.unit.promotion_level < growth_limit.promotion_level or \ + self.unit.promotion_level == growth_limit.promotion_level and getattr(growth_limit, f"equipment_{i}") > star)) # 拉技能 target_skill_ub_level = int(self.get_config('unit_promote_skill_ub')) @@ -335,3 +490,105 @@ async def do_task(self, client: pcrclient): target_skill_ex_level = int(self.get_config('unit_promote_skill_ex')) if self.unit.ex_skill and self.unit.ex_skill[0].skill_level < target_skill_ex_level: await self.unit_skill_up_aware(eSkillLocationCategory.EX_SKILL_1, lambda: self.unit.ex_skill[0], target_skill_ex_level, growth_limit) + + + growth_limit_unique = await self.is_unique_growth_unit() + target_unique_level = int(self.get_config('unit_promote_unique_equip_level')) + if self.unit.unique_equip_slot and self.unit.unique_equip_slot[0].enhancement_level < target_unique_level: + await self.unit_unique_equip_enhance_aware(target_unique_level, growth_limit_unique) + +@description(''' +角色ID 角色名字 角色等级 角色星级 角色好感度 角色Rank 装备等级(左上) 装备等级(右上) 装备等级(左中) 装备等级(右中) 装备等级(左下) 装备等级(右下) UB技能等级 技能1等级 技能2等级 EX技能等级 专武等级 高级设置 | 不会使用专武球,专武升级请认真考虑!不考虑星级、好感度、高级设置''') +@name('批量拉角色练度') +@texttype("unit_promote_text", "目标练度", "") +@default(False) +class unit_promote_batch(UnitController): + + async def do_task(self, client: pcrclient): + + self.client = client + unit_promote_texts = self.get_config('unit_promote_text').strip().split('\n') + + for unit_text in unit_promote_texts: + '''角色ID 角色名字 角色等级 角色星级 角色好感度 角色Rank 装备等级(左上) 装备等级(右上) 装备等级(左中) 装备等级(右中) 装备等级(左下) 装备等级(右下) UB技能等级 技能1等级 技能2等级 EX技能等级 专武等级 高级设置''' + try: + + unit_id, unit_name, target_level, target_star, target_dear, target_rank, \ + equip_0, equip_1, equip_2, equip_3, equip_4, equip_5, \ + ub, s1, s2, ex, unique_level, dear_info = unit_text.split() + + equip_star = [equip_0, equip_1, equip_2, equip_3, equip_4, equip_5] + + self.unit_id = int(unit_id) + + growth_limit = await self.is_growth_unit() + self.auto_level_up = False + self.auto_rank_up = False + + # 拉等级 + target_level = int(target_level) + if self.unit.unit_level < target_level: + await self.unit_level_up_aware(target_level, growth_limit) + + # 拉品级 + target_promotion_rank = int(target_rank) + if self.unit.promotion_level < target_promotion_rank: + await self.unit_promotion_up_aware(target_promotion_rank, growth_limit) + + # 拉装备 + for i in range(6): + equip = self.unit.equip_slot[i] + if equip.id == 999999: + continue + + try: + star = int(equip_star[i]) + except: + star = -1 + + if star != -1: + if not equip.is_slot: + await self.unit_equip_slot_aware(equip.id, i + 1, growth_limit) + + promotion_limit = db.get_equip_max_star(equip.id) + if star > promotion_limit: + self._log(f"{i+1}号位装备预设{star}星级超过了最大星级{promotion_limit},提升至最大星级{promotion_limit}") + star = promotion_limit + + if equip.enhancement_level < star: + await self.unit_equip_enhance(equip.id, i + 1, star, growth_limit is not None and ( + self.unit.promotion_level < growth_limit.promotion_level or \ + self.unit.promotion_level == growth_limit.promotion_level and getattr(growth_limit, f"equipment_{i}") > star)) + + # 拉技能 + target_skill_ub_level = int(ub) + if self.unit.union_burst and self.unit.union_burst[0].skill_level < target_skill_ub_level: + await self.unit_skill_up_aware(eSkillLocationCategory.UNION_BURST_SKILL, lambda: self.unit.union_burst[0], target_skill_ub_level, growth_limit) + + target_skill_s1_level = int(s1) + if self.unit.main_skill and self.unit.main_skill[0].skill_level < target_skill_s1_level: + await self.unit_skill_up_aware(eSkillLocationCategory.MAIN_SKILL_1, lambda: self.unit.main_skill[0], target_skill_s1_level, growth_limit) + + target_skill_s2_level = int(s2) + if len(self.unit.main_skill) > 1 and self.unit.main_skill[1].skill_level < target_skill_s2_level: + await self.unit_skill_up_aware(eSkillLocationCategory.MAIN_SKILL_2, lambda: self.unit.main_skill[1], target_skill_s2_level, growth_limit) + + target_skill_ex_level = int(ex) + if self.unit.ex_skill and self.unit.ex_skill[0].skill_level < target_skill_ex_level: + await self.unit_skill_up_aware(eSkillLocationCategory.EX_SKILL_1, lambda: self.unit.ex_skill[0], target_skill_ex_level, growth_limit) + + # 拉专武 + growth_limit_unique = await self.is_unique_growth_unit() + try: + target_unique_level = int(unique_level) + except: + target_unique_level = 0 + if self.unit.unique_equip_slot and self.unit.unique_equip_slot[0].enhancement_level < target_unique_level: + await self.unit_unique_equip_enhance_aware(target_unique_level, growth_limit_unique) + + + except Exception as e: + self._warn(str(e)) + self._log(str(e)) + traceback.print_exc() + continue diff --git a/autopcr/sdk/bsgamesdk.py b/autopcr/sdk/bsgamesdk.py index 6f338605..60a70bc3 100644 --- a/autopcr/sdk/bsgamesdk.py +++ b/autopcr/sdk/bsgamesdk.py @@ -86,12 +86,12 @@ def make_captch(gt,challenge,gt_user): res = requests.get(url="http://api.ydaaa.com/start_handle",params=data) return res ''' -async def login(bili_account,bili_pwd, make_captch): +async def login(qq, bili_account,bili_pwd, make_captch): print(f'logging in with acc={bili_account}, pwd = {bili_pwd}') login_sta= await login1(bili_account,bili_pwd) # if "access_key" not in login_sta: if login_sta['code'] == 200000: - captch_done=await make_captch(bili_account) + captch_done=await make_captch(qq) login_sta=await login2(bili_account,bili_pwd,captch_done["challenge"],captch_done['gt_user_id'],captch_done['validate']) return login_sta else: diff --git a/autopcr/sdk/sdkclients.py b/autopcr/sdk/sdkclients.py index 89927efb..8f7c5488 100644 --- a/autopcr/sdk/sdkclients.py +++ b/autopcr/sdk/sdkclients.py @@ -7,6 +7,7 @@ class bsdkclient(sdkclient): async def login(self): while True: resp = await login( + self._account.qq, self._account.username, self._account.password, self.captchaVerifier diff --git a/autopcr/sdk/validator.py b/autopcr/sdk/validator.py index a7a3b33a..da2f242f 100644 --- a/autopcr/sdk/validator.py +++ b/autopcr/sdk/validator.py @@ -1,10 +1,11 @@ -from typing import Dict +from typing import Dict, List from ..util import aiorequests, questutils from ..model.error import PanicError from json import loads import asyncio, time from dataclasses import dataclass from dataclasses_json import dataclass_json +from collections import defaultdict @dataclass_json @dataclass @@ -17,14 +18,14 @@ class ValidateInfo: status: str = "" validate: str = "" -validate_dict: Dict[str, ValidateInfo] = {} +validate_dict: Dict[str, List[ValidateInfo]] = defaultdict(list) validate_ok_dict: Dict[str, ValidateInfo] = {} -async def Validator(account): +async def Validator(qq): info = None for validator in [remoteValidator, localValidator, manualValidator]: try: - info = await validator(account) + info = await validator(qq) if info: break except Exception as e: @@ -35,7 +36,7 @@ async def Validator(account): raise PanicError("验证码验证超时") return info -async def manualValidator(account): +async def manualValidator(qq): print('use manual validator') from .bsgamesdk import captch @@ -46,14 +47,14 @@ async def manualValidator(account): id = questutils.create_quest_token() url = f"/daily/validate?id={id}&captcha_type=1&challenge={challenge}>={gt}&userid={userid}&gs=1" - validate_dict[account] = ValidateInfo( + validate_dict[qq].append(ValidateInfo( id=id, challenge=challenge, gt=gt, userid=userid, url=url, status="need validate" - ) + )) info = None for _ in range(120): if id not in validate_ok_dict: @@ -69,7 +70,7 @@ async def manualValidator(account): return info -async def localValidator(account): +async def localValidator(qq): print('use local validator') from .bsgamesdk import captch @@ -105,7 +106,7 @@ async def localValidator(account): } return info -async def remoteValidator(account): +async def remoteValidator(qq): print('use remote validator') url = f"https://pcrd.tencentbot.top/geetest_renew" diff --git a/autopcr/util/draw_table.py b/autopcr/util/draw_table.py index f7339f3d..d9783367 100644 --- a/autopcr/util/draw_table.py +++ b/autopcr/util/draw_table.py @@ -100,7 +100,7 @@ def draw_table(table, header=[], font=ImageFont.load_default(), cell_pad=(20, 10 _left += col_max_wid[j] - draw.textsize(table[i][j], font)[0] _top = top _top += (row_max_hei[i] - draw.textsize(table[i][j], font)[1]) // 2 # always vertical center - draw.text((_left, _top), table[i][j], font=font, fill=color) + draw.text((_left, _top), table[i][j].replace('\t', ' '), font=font, fill=color) left += col_max_wid[j] + cell_pad[0] * 2 top += row_max_hei[i] + cell_pad[1] * 2 diff --git a/server.py b/server.py index 01d358ab..babc3d16 100644 --- a/server.py +++ b/server.py @@ -1,7 +1,7 @@ from collections import Counter from typing import Any, Callable, Coroutine, Dict, List, Tuple -from .autopcr.module.modulemgr import TaskResult +from .autopcr.module.accountmgr import BATCHINFO, TaskResultInfo from .autopcr.module.modulebase import eResultStatus from .autopcr.util.draw_table import outp_b64 from .autopcr.http_server.httpserver import HttpServer @@ -45,6 +45,7 @@ - {prefix}定时日志 查看定时运行状态 - {prefix}查缺角色 查看缺少的限定常驻角色 - {prefix}查心碎 查询缺口心碎 +- {prefix}查纯净碎片 查询缺口纯净碎片,国服六星+日服二专需求 - {prefix}查记忆碎片 [可刷取|大师币] 查询缺口记忆碎片,可按地图可刷取或大师币商店过滤 - {prefix}查装备 [] [fav] 查询缺口装备,rank为数字,只查询>=rank的角色缺口装备,fav表示只查询favorite的角色 - {prefix}刷图推荐 [] [fav] 查询缺口装备的刷图推荐,格式同上 @@ -100,6 +101,7 @@ async def target_qq(self) -> str: ... async def send_qq(self) -> str: ... async def message(self) -> List[str]: ... async def is_admin(self) -> bool: ... + async def is_super_admin(self) -> bool: ... async def get_group_member_list(self) -> List: ... class HoshinoEvent(BotEvent): @@ -145,29 +147,33 @@ async def finish(self, msg: str): async def is_admin(self) -> bool: return priv.check_priv(self.ev, priv.ADMIN) + async def is_super_admin(self) -> bool: + return priv.check_priv(self.ev, priv.SU) + def wrap_hoshino_event(func): async def wrapper(bot: HoshinoBot, ev: CQEvent, *args, **kwargs): await func(HoshinoEvent(bot, ev), *args, **kwargs) wrapper.__name__ = func.__name__ return wrapper -async def check_validate(botev: BotEvent, acc: Account): +async def check_validate(botev: BotEvent, qq: str, cnt: int = 1): from .autopcr.sdk.validator import validate_dict for _ in range(360): - if acc.data.username in validate_dict: - status = validate_dict[acc.data.username].status + if qq in validate_dict and validate_dict[qq]: + validate = validate_dict[qq].pop() + status = validate.status if status == "ok": - del validate_dict[acc.data.username] - break + del validate_dict[qq] + cnt -= 1 + if not cnt: break + continue - url = validate_dict[acc.data.username].url + url = validate.url url = address + url.lstrip("/daily/") msg=f"pcr账号登录需要验证码,请点击以下链接在120秒内完成认证:\n{url}" await botev.send(msg) - del validate_dict[acc.data.username] - else: await asyncio.sleep(1) @@ -175,16 +181,20 @@ async def is_valid_qq(qq: str): qq = str(qq) groups = (await sv.get_enable_groups()).keys() bot = nonebot.get_bot() - for group in groups: - try: - async for member in await bot.get_group_member_list(group_id=group): - if qq == str(member['user_id']): - return True - except: - for member in await bot.get_group_member_list(group_id=group): - if qq == str(member['user_id']): - return True - return False + if qq.startswith("g"): + gid = qq.lstrip('g') + return gid.isdigit() and int(gid) in groups + else: + for group in groups: + try: + async for member in await bot.get_group_member_list(group_id=group): + if qq == str(member['user_id']): + return True + except: + for member in await bot.get_group_member_list(group_id=group): + if qq == str(member['user_id']): + return True + return False def check_final_args_be_empty(func): async def wrapper(botev: BotEvent, *args, **kwargs): @@ -236,15 +246,19 @@ async def wrapper(botev: BotEvent, accmgr: AccountManager, *args, **kwargs): alias = msg[0] if msg else "" - if alias not in accmgr.accounts(): + if alias == '所有': + pass + # alias = BATCHINFO + # del msg[0] + elif alias not in accmgr.accounts(): alias = accmgr.default_account else: del msg[0] - if len(list(accmgr.accounts())) == 1: + if alias != BATCHINFO and len(list(accmgr.accounts())) == 1: alias = list(accmgr.accounts())[0] - if alias not in accmgr.accounts(): + if alias != BATCHINFO and alias not in accmgr.accounts(): if alias: await botev.finish(f"未找到昵称为【{alias}】的账号") else: @@ -280,6 +294,16 @@ async def wrapper(botev: BotEvent, tool: ToolInfo, *args, **kwargs): wrapper.__name__ = func.__name__ return wrapper +def require_super_admin(func): + async def wrapper(botev: BotEvent, *args, **kwargs): + if await botev.target_qq() != await botev.send_qq() and not await botev.is_super_admin(): + await botev.finish("仅超级管理员调用他人") + else: + return await func(botev = botev, *args, **kwargs) + + wrapper.__name__ = func.__name__ + return wrapper + @sv.on_fullmatch(["帮助自动清日常", f"{prefix}帮助"]) @wrap_hoshino_event async def bangzhu_text(botev: BotEvent): @@ -296,7 +320,7 @@ async def clean_daily_all(botev: BotEvent, accmgr: AccountManager): is_admin_call = await botev.is_admin() async def clean_daily_pre(alias: str): async with accmgr.load(alias) as acc: - return await clean_daily(botev, acc, is_admin_call) + return await acc.do_daily(is_admin_call) for acc in accmgr.accounts(): alias.append(escape(acc)) @@ -308,19 +332,20 @@ async def clean_daily_pre(alias: str): except Exception as e: print(e) - resps = await asyncio.gather(*task, return_exceptions=True) + loop = asyncio.get_event_loop() + loop.create_task(check_validate(botev, accmgr.qid, len(alias))) + + resps: List[TaskResultInfo] = await asyncio.gather(*task, return_exceptions=True) header = ["昵称", "清日常结果", "状态"] - content = [[alias[i], daily_result[0].result[daily_result[0].order[-1]].log, "#" + daily_result[1]] for i, daily_result in enumerate(resps) if not isinstance(daily_result, Exception)] + content = [ + [alias[i], daily_result.get_result().get_last_result().log, "#" + daily_result.status] + for i, daily_result in enumerate(resps) + ] img = await drawer.draw(header, content) - err = [(alias[i], resp) for i, resp in enumerate(resps) if isinstance(resp, Exception)] msg = outp_b64(img) await botev.send(msg) - if err: - msg = "\n".join([f"{a}: {m}" for a, m in err]) - await botev.send(msg) - @sv.on_fullmatch(f"{prefix}查禁用") @wrap_hoshino_event async def query_clan_battle_forbidden(botev: BotEvent): @@ -398,7 +423,12 @@ async def clean_daily_from(botev: BotEvent, acc: Account): try: is_admin_call = await botev.is_admin() - resp, _ = await clean_daily(botev = botev, acc = acc, is_admin_call = is_admin_call) + + loop = asyncio.get_event_loop() + loop.create_task(check_validate(botev, acc.qq)) + + res = await acc.do_daily(is_admin_call) + resp = res.get_result() img = await drawer.draw_tasks_result(resp) msg = f"{alias}" msg += outp_b64(img) @@ -406,12 +436,6 @@ async def clean_daily_from(botev: BotEvent, acc: Account): except Exception as e: await botev.send(f'{alias}: {e}') -async def clean_daily(botev: BotEvent, acc: Account, is_admin_call = False) -> Tuple[TaskResult, str]: - loop = asyncio.get_event_loop() - loop.create_task(check_validate(botev, acc)) - resp, status = await acc.do_daily(is_admin_call) - return resp, status.value - @sv.on_prefix(f"{prefix}日常报告") @wrap_hoshino_event @wrap_accountmgr @@ -437,7 +461,7 @@ async def clean_daily_time(botev: BotEvent, accmgr: AccountManager): content = [] for alias in accmgr.accounts(): async with accmgr.load(alias, readonly=True) as acc: - content += [[acc.alias, daily_result.time, "#" + daily_result.status] for daily_result in acc.data.daily_result] + content += [[acc.alias, daily_result.time, "#" + daily_result.status.value] for daily_result in acc.get_daily_result_list()] if not content: await botev.finish("暂无日常记录") @@ -538,10 +562,11 @@ async def tool_used(botev: BotEvent, tool: ToolInfo, config: Dict[str, str], acc alias = escape(acc.alias) try: loop = asyncio.get_event_loop() - loop.create_task(check_validate(botev, acc)) + loop.create_task(check_validate(botev, acc.qq)) is_admin_call = await botev.is_admin() - resp, _ = await acc.do_from_key(config, tool.key, is_admin_call) + resp = await acc.do_from_key(config, tool.key, is_admin_call) + resp = resp.get_result() img = await drawer.draw_task_result(resp) msg = f"{alias}" msg += outp_b64(img) @@ -653,8 +678,12 @@ async def find_memory(botev: BotEvent): } return config +@register_tool("查纯净碎片", "get_need_pure_memory") +async def find_pure_memory(botev: BotEvent): + return {} @register_tool(f"来发十连", "gacha_start") +@require_super_admin async def shilian(botev: BotEvent): cc_until_get = False pool_id = "" @@ -749,6 +778,11 @@ async def quest_recommand(botev: BotEvent): } return config + +@register_tool("pjjc换防", "pjjc_shuffle_team") +async def pjjc_shuffle_team(botev: BotEvent): + return {} + @register_tool("查缺角色", "missing_unit") async def find_missing_unit(botev: BotEvent): return {}