From 088e9ac315749cdb1bae1ca67d82c0207f26f207 Mon Sep 17 00:00:00 2001 From: xtaodada Date: Thu, 27 Apr 2023 20:25:06 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Support=20starrail=20warp=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/dependence/assets.py | 597 ++++--------- core/dependence/assets.pyi | 167 ---- core/services/wiki/services.py | 15 +- metadata/pool/pool.py | 21 +- metadata/pool/pool_1.py | 1 + metadata/pool/pool_11.py | 9 + metadata/pool/pool_12.py | 9 + metadata/pool/pool_2.py | 1 + metadata/pool/pool_200.py | 1 - metadata/pool/pool_301.py | 324 ------- metadata/pool/pool_302.py | 317 ------- metadata/shortname.py | 801 +++--------------- modules/gacha_log/const.py | 14 +- modules/gacha_log/log.py | 257 ++---- modules/gacha_log/models.py | 53 +- modules/wiki/models/avatar.py | 2 - modules/wiki/models/avatar_config.py | 36 + modules/wiki/models/light_cone_config.py | 20 + modules/wiki/models/monster.py | 1 - modules/wiki/models/wiki.py | 34 + plugins/starrail/wish_log.py | 395 +++++++++ resources/bot/help/help.html | 74 +- resources/starrail/gacha_count/example.html | 47 + .../starrail/gacha_count/gacha_count.css | 214 +++++ .../starrail/gacha_count/gacha_count.html | 55 ++ resources/starrail/gacha_log/example.html | 77 ++ resources/starrail/gacha_log/gacha_log.css | 341 ++++++++ resources/starrail/gacha_log/gacha_log.html | 87 ++ .../starrail/gacha_log/img/提纳里.png | Bin 0 -> 47915 bytes utils/genshin.py | 14 +- 30 files changed, 1729 insertions(+), 2255 deletions(-) delete mode 100644 core/dependence/assets.pyi create mode 100644 metadata/pool/pool_1.py create mode 100644 metadata/pool/pool_11.py create mode 100644 metadata/pool/pool_12.py create mode 100644 metadata/pool/pool_2.py delete mode 100644 metadata/pool/pool_200.py delete mode 100644 metadata/pool/pool_301.py delete mode 100644 metadata/pool/pool_302.py create mode 100644 modules/wiki/models/avatar_config.py create mode 100644 modules/wiki/models/light_cone_config.py create mode 100644 modules/wiki/models/wiki.py create mode 100644 plugins/starrail/wish_log.py create mode 100644 resources/starrail/gacha_count/example.html create mode 100644 resources/starrail/gacha_count/gacha_count.css create mode 100644 resources/starrail/gacha_count/gacha_count.html create mode 100644 resources/starrail/gacha_log/example.html create mode 100644 resources/starrail/gacha_log/gacha_log.css create mode 100644 resources/starrail/gacha_log/gacha_log.html create mode 100644 resources/starrail/gacha_log/img/提纳里.png diff --git a/core/dependence/assets.py b/core/dependence/assets.py index c1c625e..42b0497 100644 --- a/core/dependence/assets.py +++ b/core/dependence/assets.py @@ -1,47 +1,25 @@ -"""用于下载和管理角色、武器、材料等的图标""" -from __future__ import annotations - import asyncio -import re -from abc import ABC, abstractmethod -from functools import cached_property, lru_cache, partial -from multiprocessing import RLock as Lock from pathlib import Path from ssl import SSLZeroReturnError -from typing import AsyncIterator, Awaitable, Callable, ClassVar, Dict, Optional, TYPE_CHECKING, TypeVar, Union +from typing import Optional, List, Dict from aiofiles import open as async_open -from aiofiles.os import remove as async_remove -from enkanetwork import Assets as EnkaAssets -from enkanetwork.model.assets import CharacterAsset as EnkaCharacterAsset -from httpx import AsyncClient, HTTPError, HTTPStatusError, TransportError, URL -from typing_extensions import Self +from httpx import AsyncClient, HTTPError from core.base_service import BaseService -from core.config import config -from metadata.genshin import AVATAR_DATA, HONEY_DATA, MATERIAL_DATA, NAMECARD_DATA, WEAPON_DATA -from metadata.scripts.honey import update_honey_metadata -from metadata.scripts.metadatas import update_metadata_from_ambr, update_metadata_from_github -from metadata.shortname import roleToId, weaponToId -from utils.const import AMBR_HOST, ENKA_HOST, PROJECT_ROOT +from modules.wiki.base import WikiModel +from modules.wiki.models.avatar_config import AvatarIcon +from modules.wiki.models.light_cone_config import LightConeIcon +from utils.const import PROJECT_ROOT from utils.log import logger -from utils.typedefs import StrOrInt, StrOrURL - -if TYPE_CHECKING: - from httpx import Response - from multiprocessing.synchronize import RLock - -__all__ = ("AssetsServiceType", "AssetsService", "AssetsServiceError", "AssetsCouldNotFound", "DEFAULT_EnkaAssets") - -ICON_TYPE = Union[Callable[[bool], Awaitable[Optional[Path]]], Callable[..., Awaitable[Optional[Path]]]] -NAME_MAP_TYPE = Dict[str, StrOrURL] +from utils.typedefs import StrOrURL, StrOrInt ASSETS_PATH = PROJECT_ROOT.joinpath("resources/assets") ASSETS_PATH.mkdir(exist_ok=True, parents=True) -HONEY_HOST = "" -DATA_MAP = {"avatar": AVATAR_DATA, "weapon": WEAPON_DATA, "material": MATERIAL_DATA} - -DEFAULT_EnkaAssets = EnkaAssets(lang="chs") +DATA_MAP = { + "avatar": WikiModel.BASE_URL + "avatar_icons.json", + "light_cone": WikiModel.BASE_URL + "light_cone_icons.json", +} class AssetsServiceError(Exception): @@ -55,84 +33,16 @@ class AssetsCouldNotFound(AssetsServiceError): super().__init__(f"{message}: target={message}") -class _AssetsService(ABC): - _lock: ClassVar["RLock"] = Lock() - _dir: ClassVar[Path] - icon_types: ClassVar[list[str]] - - _client: Optional[AsyncClient] = None - _links: dict[str, str] = {} - - id: int - type: str - - icon: ICON_TYPE - """图标""" - - @abstractmethod - @cached_property - def game_name(self) -> str: - """游戏数据中的名称""" - - @cached_property - def honey_id(self) -> str: - """当前资源在 Honey Impact 所对应的 ID""" - return HONEY_DATA[self.type].get(str(self.id), [""])[0] - - @property - def path(self) -> Path: - """当前资源的文件夹""" - result = self._dir.joinpath(str(self.id)).resolve() - result.mkdir(exist_ok=True, parents=True) - return result - - @property - def client(self) -> AsyncClient: - with self._lock: - if self._client is None or self._client.is_closed: - self._client = AsyncClient() - return self._client +class _AssetsService: + client: Optional[AsyncClient] = None def __init__(self, client: Optional[AsyncClient] = None) -> None: - self._client = client + self.client = client - def __call__(self, target: int) -> Self: - """用于生成与 target 对应的 assets""" - result = self.__class__(self.client) - result.id = target - return result - - def __init_subclass__(cls, **kwargs) -> None: - """初始化一些类变量""" - from itertools import chain - - cls.icon_types = [ # 支持的图标类型 - k - for k, v in chain(cls.__annotations__.items(), *map(lambda x: x.__annotations__.items(), cls.__bases__)) - if v in [ICON_TYPE, "ICON_TYPE"] - ] - cls.type = cls.__name__.lstrip("_").split("Assets")[0].lower() # 当前 assert 的类型 - cls._dir = ASSETS_PATH.joinpath(cls.type) # 图标保存的文件夹 - cls._dir.mkdir(exist_ok=True, parents=True) - - async def _request(self, url: str, interval: float = 0.2) -> "Response": - error = None - for _ in range(5): - try: - return await self.client.get(url, follow_redirects=False) - except (TransportError, SSLZeroReturnError) as e: - error = e - await asyncio.sleep(interval) - continue - if error is not None: - raise error - - async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Path | None: + async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Optional[Path]: """从 url 下载图标至 path""" logger.debug("正在从 %s 下载图标至 %s", url, path) headers = None - if config.enka_network_api_agent is not None and URL(url).host == "enka.network": - headers = {"user-agent": config.enka_network_api_agent} for time in range(retry): try: response = await self.client.get(url, follow_redirects=False, headers=headers) @@ -150,356 +60,144 @@ class _AssetsService(ABC): await file.write(response.content) # 保存图标 return path.resolve() - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: # pylint: disable=W0613,R0201 - """从 ambr.top 上获取目标链接""" - yield None - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: # pylint: disable=W0613,R0201 - """从 enke.network 上获取目标链接""" - yield None - - async def _get_from_honey(self, item: str) -> AsyncIterator[str | None]: - """从 honey 上获取目标链接""" - if (honey_name := self.honey_name_map.get(item, None)) is not None: - yield HONEY_HOST.join(f"img/{honey_name}.png") - yield HONEY_HOST.join(f"img/{honey_name}.webp") - - async def _download_url_generator(self, item: str) -> AsyncIterator[str]: - # 获取当前 `AssetsService` 的所有爬虫 - for func in map(lambda x: getattr(self, x), sorted(filter(lambda x: x.startswith("_get_from_"), dir(self)))): - async for url in func(item): - if url is not None: - try: - response = await self._request(url := str(url)) - response.raise_for_status() - yield url - except HTTPStatusError: - continue - - async def _get_download_url(self, item: str) -> str | None: - """获取图标的下载链接""" - async for url in self._download_url_generator(item): - if url is not None: - return url - - async def _get_img(self, overwrite: bool = False, *, item: str) -> Path | None: - """获取图标""" - path = next(filter(lambda x: x.stem == item, self.path.iterdir()), None) - if not overwrite and path: # 如果需要下载的图标存在且不覆盖( overwrite ) - return path.resolve() - if path is not None and path.exists(): - if overwrite: # 如果覆盖 - await async_remove(path) # 删除已存在的图标 - else: - return path - # 依次从使用当前 assets class 中的爬虫下载图标,顺序为爬虫名的字母顺序 - async for url in self._download_url_generator(item): - if url is not None: - path = self.path.joinpath(f"{item}{Path(url).suffix}") - if (result := await self._download(url, path)) is not None: - return result - - @lru_cache - async def get_link(self, item: str) -> str | None: - """获取相应图标链接""" - return await self._get_download_url(item) - - def __getattr__(self, item: str): - """魔法""" - if item in self.icon_types: - return partial(self._get_img, item=item) - object.__getattribute__(self, item) - return None - - @abstractmethod - @cached_property - def game_name_map(self) -> dict[str, str]: - """游戏中的图标名""" - - @abstractmethod - @cached_property - def honey_name_map(self) -> dict[str, str]: - """来自honey的图标名""" - class _AvatarAssets(_AssetsService): - enka: EnkaCharacterAsset | None + path: Path + data: List[AvatarIcon] + name_map: Dict[str, AvatarIcon] + id_map: Dict[int, AvatarIcon] - side: ICON_TYPE - """侧视图图标""" - - card: ICON_TYPE - """卡片图标""" - - gacha: ICON_TYPE - """抽卡立绘""" - - gacha_card: ICON_TYPE - """抽卡卡片""" - - @cached_property - def game_name(self) -> str: - icon = "UI_AvatarIcon_" - if (avatar := AVATAR_DATA.get(str(self.id), None)) is not None: - icon = avatar["icon"] - else: - for aid, avatar in AVATAR_DATA.items(): - if aid.startswith(str(self.id)): - icon = avatar["icon"] - return re.findall(r"UI_AvatarIcon_(.*)", icon)[0] - - @cached_property - def honey_id(self) -> str: - return HONEY_DATA["avatar"].get(str(self.id), "")[0] - - @cached_property - def enka(self) -> Optional[EnkaCharacterAsset]: - api = getattr(self, "_enka_api", None) - cid = getattr(self, "id", None) - return None if api is None or cid is None else api.character(cid) - - def __init__(self, client: Optional[AsyncClient] = None, enka: Optional[EnkaAssets] = None): + def __init__(self, client: Optional[AsyncClient] = None) -> None: super().__init__(client) - self._enka_api = enka or DEFAULT_EnkaAssets + self.path = ASSETS_PATH.joinpath("avatar") + self.path.mkdir(exist_ok=True, parents=True) - def __call__(self, target: StrOrInt) -> "_AvatarAssets": - temp = target - result = _AvatarAssets(self.client) - if isinstance(target, str): - try: - target = int(target) - except ValueError: - target = roleToId(target) - if isinstance(target, str) or target is None: - raise AssetsCouldNotFound("找不到对应的角色", temp) - result.id = target - result._enka_api = self._enka_api - return result + async def initialize(self): + logger.info("正在初始化角色素材图标") + html = await self.client.get(DATA_MAP["avatar"]) + self.data = [AvatarIcon(**data) for data in html.json()] + self.name_map = {icon.name: icon for icon in self.data} + self.id_map = {icon.id: icon for icon in self.data} + tasks = [] + for icon in self.data: + base_path = self.path / f"{icon.id}" + base_path.mkdir(exist_ok=True, parents=True) + gacha_path = base_path / "gacha.webp" + icon_path = base_path / "icon.webp" + normal_path = base_path / "normal.webp" + if not gacha_path.exists(): + tasks.append(self._download(icon.gacha, gacha_path)) + if not icon_path.exists(): + tasks.append(self._download(icon.icon_, icon_path)) + if not normal_path.exists(): + tasks.append(self._download(icon.normal, normal_path)) + if len(tasks) >= 100: + await asyncio.gather(*tasks) + tasks = [] + if tasks: + await asyncio.gather(*tasks) + logger.info("角色素材图标初始化完成") - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item in {"icon", "side", "gacha"}: - yield str(AMBR_HOST.join(f"assets/UI/{self.game_name_map[item]}.png")) + def get_path(self, icon: AvatarIcon, name: str) -> Path: + path = self.path / f"{icon.id}" + path.mkdir(exist_ok=True, parents=True) + return path / f"{name}.webp" - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if (item_id := self.game_name_map.get(item)) is not None: - yield str(ENKA_HOST.join(f"ui/{item_id}.png")) + def get_by_id(self, id_: int) -> Optional[AvatarIcon]: + return self.id_map.get(id_, None) - @cached_property - def honey_name_map(self) -> dict[str, str]: - return { - "icon": f"{self.honey_id}_icon", - "side": f"{self.honey_id}_side_icon", - "gacha": f"{self.honey_id}_gacha_splash", - "gacha_card": f"{self.honey_id}_gacha_card", - } + def get_by_name(self, name: str) -> Optional[AvatarIcon]: + return self.name_map.get(name, None) - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": f"UI_AvatarIcon_{self.game_name}", - "card": f"UI_AvatarIcon_{self.game_name}_Card", - "side": f"UI_AvatarIcon_Side_{self.game_name}", - "gacha": f"UI_Gacha_AvatarImg_{self.game_name}", - } + def get_target(self, target: StrOrInt) -> Optional[AvatarIcon]: + if isinstance(target, int): + return self.get_by_id(target) + elif isinstance(target, str): + return self.get_by_name(target) + return None + + def gacha(self, target: StrOrInt) -> Path: + icon = self.get_target(target) + if icon is None: + raise AssetsCouldNotFound("角色素材图标不存在", target) + return self.get_path(icon, "gacha") + + def icon(self, target: StrOrInt) -> Path: + icon = self.get_target(target) + if icon is None: + raise AssetsCouldNotFound("角色素材图标不存在", target) + return self.get_path(icon, "icon") + + def normal(self, target: StrOrInt) -> Path: + icon = self.get_target(target) + if icon is None: + raise AssetsCouldNotFound("角色素材图标不存在", target) + return self.get_path(icon, "normal") -class _WeaponAssets(_AssetsService): - awaken: ICON_TYPE - """突破后图标""" +class _LightConeAssets(_AssetsService): + path: Path + data: List[LightConeIcon] + name_map: Dict[str, LightConeIcon] + id_map: Dict[int, LightConeIcon] - gacha: ICON_TYPE - """抽卡立绘""" + def __init__(self, client: Optional[AsyncClient] = None) -> None: + super().__init__(client) + self.path = ASSETS_PATH.joinpath("light_cone") + self.path.mkdir(exist_ok=True, parents=True) - @cached_property - def game_name(self) -> str: - return re.findall(r"UI_EquipIcon_(.*)", WEAPON_DATA[str(self.id)]["icon"])[0] + async def initialize(self): + logger.info("正在初始化光锥素材图标") + html = await self.client.get(DATA_MAP["light_cone"]) + self.data = [LightConeIcon(**data) for data in html.json()] + self.name_map = {icon.name: icon for icon in self.data} + self.id_map = {icon.id: icon for icon in self.data} + tasks = [] + for icon in self.data: + base_path = self.path / f"{icon.id}" + base_path.mkdir(exist_ok=True, parents=True) + gacha_path = base_path / "gacha.webp" + icon_path = base_path / "icon.webp" + if not gacha_path.exists(): + tasks.append(self._download(icon.gacha, gacha_path)) + if not icon_path.exists(): + tasks.append(self._download(icon.icon_, icon_path)) + if len(tasks) >= 100: + await asyncio.gather(*tasks) + tasks = [] + if tasks: + await asyncio.gather(*tasks) + logger.info("光锥素材图标初始化完成") - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": f"UI_EquipIcon_{self.game_name}", - "awaken": f"UI_EquipIcon_{self.game_name}_Awaken", - "gacha": f"UI_Gacha_EquipIcon_{self.game_name}", - } + def get_path(self, icon: LightConeIcon, name: str) -> Path: + path = self.path / f"{icon.id}" + path.mkdir(exist_ok=True, parents=True) + return path / f"{name}.webp" - @cached_property - def honey_id(self) -> str: - return f"i_n{self.id}" + def get_by_id(self, id_: int) -> Optional[LightConeIcon]: + return self.id_map.get(id_, None) - def __call__(self, target: StrOrInt) -> Self: - temp = target - result = _WeaponAssets(self.client) - if isinstance(target, str): - target = int(target) if target.isnumeric() else weaponToId(target) - if isinstance(target, str) or target is None: - raise AssetsCouldNotFound("找不到对应的武器", temp) - result.id = target - return result + def get_by_name(self, name: str) -> Optional[LightConeIcon]: + return self.name_map.get(name, None) - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item == "icon": - yield str(AMBR_HOST.join(f"assets/UI/{self.game_name_map.get(item)}.png")) + def get_target(self, target: StrOrInt) -> Optional[LightConeIcon]: + if isinstance(target, int): + return self.get_by_id(target) + elif isinstance(target, str): + return self.get_by_name(target) + return None - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if item in self.game_name_map: - yield str(ENKA_HOST.join(f"ui/{self.game_name_map.get(item)}.png")) + def gacha(self, target: StrOrInt) -> Path: + icon = self.get_target(target) + if icon is None: + raise AssetsCouldNotFound("光锥素材图标不存在", target) + return self.get_path(icon, "gacha") - @cached_property - def honey_name_map(self) -> dict[str, str]: - return { - "icon": f"{self.honey_id}", - "awaken": f"{self.honey_id}_awaken_icon", - "gacha": f"{self.honey_id}_gacha_icon", - } - - -class _MaterialAssets(_AssetsService): - @cached_property - def game_name(self) -> str: - return str(self.id) - - @cached_property - def game_name_map(self) -> dict[str, str]: - return {"icon": f"UI_ItemIcon_{self.game_name}"} - - @cached_property - def honey_name_map(self) -> dict[str, str]: - return {"icon": self.honey_id} - - def __call__(self, target: StrOrInt) -> Self: - temp = target - result = _MaterialAssets(self.client) - if isinstance(target, str): - if target.isnumeric(): - target = int(target) - else: - target = {v["name"]: int(k) for k, v in MATERIAL_DATA.items()}.get(target) - if isinstance(target, str) or target is None: - raise AssetsCouldNotFound("找不到对应的素材", temp) - result.id = target - return result - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item == "icon": - yield str(AMBR_HOST.join(f"assets/UI/{self.game_name_map.get(item)}.png")) - - async def _get_from_honey(self, item: str) -> AsyncIterator[str | None]: - yield HONEY_HOST.join(f"/img/{self.honey_name_map.get(item)}.png") - yield HONEY_HOST.join(f"/img/{self.honey_name_map.get(item)}.webp") - - -class _ArtifactAssets(_AssetsService): - flower: ICON_TYPE - """生之花""" - - plume: ICON_TYPE - """死之羽""" - - sands: ICON_TYPE - """时之沙""" - - goblet: ICON_TYPE - """空之杯""" - - circlet: ICON_TYPE - """理之冠""" - - @cached_property - def honey_id(self) -> str: - return HONEY_DATA["artifact"][str(self.id)][0] - - @cached_property - def game_name(self) -> str: - return f"UI_RelicIcon_{self.id}" - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if item in self.game_name_map: - yield str(ENKA_HOST.join(f"ui/{self.game_name_map.get(item)}.png")) - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item in self.game_name_map: - yield str(AMBR_HOST.join(f"assets/UI/reliquary/{self.game_name_map[item]}.png")) - - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": f"UI_RelicIcon_{self.id}_4", - "flower": f"UI_RelicIcon_{self.id}_4", - "plume": f"UI_RelicIcon_{self.id}_2", - "sands": f"UI_RelicIcon_{self.id}_5", - "goblet": f"UI_RelicIcon_{self.id}_1", - "circlet": f"UI_RelicIcon_{self.id}_3", - } - - @cached_property - def honey_name_map(self) -> dict[str, str]: - first_id = int(re.findall(r"\d+", HONEY_DATA["artifact"][str(self.id)][-1])[0]) - return { - "icon": f"i_n{first_id + 30}", - "flower": f"i_n{first_id + 30}", - "plume": f"i_n{first_id + 10}", - "sands": f"i_n{first_id + 40}", - "goblet": f"i_n{first_id}", - "circlet": f"i_n{first_id + 20}", - } - - -class _NamecardAssets(_AssetsService): - enka: EnkaCharacterAsset | None - - navbar: ICON_TYPE - """好友名片背景""" - - profile: ICON_TYPE - """个人资料名片背景""" - - @cached_property - def honey_id(self) -> str: - return HONEY_DATA["namecard"][str(self.id)][0] - - @cached_property - def game_name(self) -> str: - return NAMECARD_DATA[str(self.id)]["icon"] - - @lru_cache - def _get_id_from_avatar_id(self, avatar_id: Union[int, str]) -> int: - avatar_icon_name = AVATAR_DATA[str(avatar_id)]["icon"].replace("AvatarIcon", "NameCardIcon") - for namecard_id, namecard_data in NAMECARD_DATA.items(): - if namecard_data["icon"] == avatar_icon_name: - return int(namecard_id) - raise ValueError(avatar_id) - - def __call__(self, target: int) -> "_NamecardAssets": - result = _NamecardAssets(self.client) - if target > 10000000: - target = self._get_id_from_avatar_id(target) - result.id = target - result.enka = DEFAULT_EnkaAssets.namecards(target) - return result - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item == "profile": - yield AMBR_HOST.join(f"assets/UI/namecard/{self.game_name_map[item]}.png.png") - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if (url := getattr(self.enka, {"profile": "banner"}.get(item, item), None)) is not None: - yield url.url - - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": self.game_name, - "navbar": NAMECARD_DATA[str(self.id)]["navbar"], - "profile": NAMECARD_DATA[str(self.id)]["profile"], - } - - @cached_property - def honey_name_map(self) -> dict[str, str]: - return { - "icon": self.honey_id, - "navbar": f"{self.honey_id}_back", - "profile": f"{self.honey_id}_profile", - } + def icon(self, target: StrOrInt) -> Path: + icon = self.get_target(target) + if icon is None: + raise AssetsCouldNotFound("光锥素材图标不存在", target) + return self.get_path(icon, "icon") class AssetsService(BaseService.Dependence): @@ -510,26 +208,19 @@ class AssetsService(BaseService.Dependence): 若本地不存在,则从网络上下载;若存在,则返回其路径 """ + client: Optional[AsyncClient] = None + avatar: _AvatarAssets """角色""" - weapon: _WeaponAssets - """武器""" - - material: _MaterialAssets - """素材""" - - artifact: _ArtifactAssets - """圣遗物""" - - namecard: _NamecardAssets - """名片""" + light_cone: _LightConeAssets + """光锥""" def __init__(self): - for attr, assets_type_name in filter( - lambda x: (not x[0].startswith("_")) and x[1].endswith("Assets"), self.__annotations__.items() - ): - setattr(self, attr, globals()[assets_type_name]()) + self.client = AsyncClient(timeout=60.0) + self.avatar = _AvatarAssets(self.client) + self.light_cone = _LightConeAssets(self.client) - -AssetsServiceType = TypeVar("AssetsServiceType", bound=_AssetsService) + async def initialize(self): # pylint: disable=W0221 + await self.avatar.initialize() + await self.light_cone.initialize() diff --git a/core/dependence/assets.pyi b/core/dependence/assets.pyi deleted file mode 100644 index ba1b250..0000000 --- a/core/dependence/assets.pyi +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from functools import partial -from pathlib import Path -from typing import Awaitable, Callable, ClassVar, TypeVar - -from enkanetwork import Assets as EnkaAssets -from enkanetwork.model.assets import CharacterAsset as EnkaCharacterAsset -from httpx import AsyncClient -from typing_extensions import Self - -from core.base_service import BaseService -from utils.typedefs import StrOrInt - -__all__ = ("AssetsServiceType", "AssetsService", "AssetsServiceError", "AssetsCouldNotFound", "DEFAULT_EnkaAssets") - -ICON_TYPE = Callable[[bool], Awaitable[Path | None]] | Callable[..., Awaitable[Path | None]] -DEFAULT_EnkaAssets: EnkaAssets -_GET_TYPE = partial | list[str] | int | str | ICON_TYPE | Path | AsyncClient | None | Self | dict[str, str] - -class AssetsServiceError(Exception): ... - -class AssetsCouldNotFound(AssetsServiceError): - message: str - target: str - def __init__(self, message: str, target: str): ... - -class _AssetsService(ABC): - icon_types: ClassVar[list[str]] - id: int - type: str - - icon: ICON_TYPE - """图标""" - - @abstractmethod - @property - def game_name(self) -> str: - """游戏数据中的名称""" - @property - def honey_id(self) -> str: - """当前资源在 Honey Impact 所对应的 ID""" - @property - def path(self) -> Path: - """当前资源的文件夹""" - @property - def client(self) -> AsyncClient: - """当前的 http client""" - def __init__(self, client: AsyncClient | None = None) -> None: ... - def __call__(self, target: int) -> Self: - """用于生成与 target 对应的 assets""" - def __getattr__(self, item: str) -> _GET_TYPE: - """魔法""" - async def get_link(self, item: str) -> str | None: - """获取相应图标链接""" - @abstractmethod - @property - def game_name_map(self) -> dict[str, str]: - """游戏中的图标名""" - @abstractmethod - @property - def honey_name_map(self) -> dict[str, str]: - """来自honey的图标名""" - -class _AvatarAssets(_AssetsService): - enka: EnkaCharacterAsset | None - - side: ICON_TYPE - """侧视图图标""" - - card: ICON_TYPE - """卡片图标""" - - gacha: ICON_TYPE - """抽卡立绘""" - - gacha_card: ICON_TYPE - """抽卡卡片""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - @property - def enka(self) -> EnkaCharacterAsset | None: ... - def __init__(self, client: AsyncClient | None = None, enka: EnkaAssets | None = None) -> None: ... - def __call__(self, target: StrOrInt) -> Self: ... - def __getitem__(self, item: str) -> _GET_TYPE | EnkaCharacterAsset: ... - def game_name(self) -> str: ... - -class _WeaponAssets(_AssetsService): - awaken: ICON_TYPE - """突破后图标""" - - gacha: ICON_TYPE - """抽卡立绘""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def __call__(self, target: StrOrInt) -> Self: ... - def game_name(self) -> str: ... - -class _MaterialAssets(_AssetsService): - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def __call__(self, target: StrOrInt) -> Self: ... - def game_name(self) -> str: ... - -class _ArtifactAssets(_AssetsService): - flower: ICON_TYPE - """生之花""" - - plume: ICON_TYPE - """死之羽""" - - sands: ICON_TYPE - """时之沙""" - - goblet: ICON_TYPE - """空之杯""" - - circlet: ICON_TYPE - """理之冠""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def game_name(self) -> str: ... - -class _NamecardAssets(_AssetsService): - enka: EnkaCharacterAsset | None - - navbar: ICON_TYPE - """好友名片背景""" - - profile: ICON_TYPE - """个人资料名片背景""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def game_name(self) -> str: ... - -class AssetsService(BaseService.Dependence): - avatar: _AvatarAssets - """角色""" - - weapon: _WeaponAssets - """武器""" - - material: _MaterialAssets - """素材""" - - artifact: _ArtifactAssets - """圣遗物""" - - namecard: _NamecardAssets - """名片""" - -AssetsServiceType = TypeVar("AssetsServiceType", bound=_AssetsService) diff --git a/core/services/wiki/services.py b/core/services/wiki/services.py index be4f0e4..c623763 100644 --- a/core/services/wiki/services.py +++ b/core/services/wiki/services.py @@ -23,12 +23,15 @@ class WikiService(BaseService): async def initialize(self) -> None: logger.info("正在加载 Wiki 数据") - await self.character.read() - await self.material.read() - await self.monster.read() - await self.relic.read() - await self.light_cone.read() - await self.raider.read() + try: + await self.character.read() + await self.material.read() + await self.monster.read() + await self.relic.read() + await self.light_cone.read() + await self.raider.read() + except Exception as e: + logger.error("加载 Wiki 数据失败", exc_info=e) logger.info("加载 Wiki 数据完成") async def refresh_wiki(self) -> NoReturn: diff --git a/metadata/pool/pool.py b/metadata/pool/pool.py index 8b09eea..f64423e 100644 --- a/metadata/pool/pool.py +++ b/metadata/pool/pool.py @@ -1,13 +1,16 @@ -from metadata.pool.pool_200 import POOL_200 -from metadata.pool.pool_301 import POOL_301 -from metadata.pool.pool_302 import POOL_302 +from metadata.pool.pool_1 import POOL_1 +from metadata.pool.pool_2 import POOL_2 +from metadata.pool.pool_11 import POOL_11 +from metadata.pool.pool_12 import POOL_12 def get_pool_by_id(pool_type): - if pool_type == 200: - return POOL_200 - if pool_type == 301: - return POOL_301 - if pool_type == 302: - return POOL_302 + if pool_type == 1: + return POOL_1 + elif pool_type == 2: + return POOL_2 + if pool_type == 11: + return POOL_11 + if pool_type == 12: + return POOL_12 return None diff --git a/metadata/pool/pool_1.py b/metadata/pool/pool_1.py new file mode 100644 index 0000000..0595a15 --- /dev/null +++ b/metadata/pool/pool_1.py @@ -0,0 +1 @@ +POOL_1 = [{"five": ["常驻池"], "four": [], "from": "2023-04-26 06:00:00", "name": "常驻池", "to": "2050-09-15 17:59:59"}] diff --git a/metadata/pool/pool_11.py b/metadata/pool/pool_11.py new file mode 100644 index 0000000..fcab44d --- /dev/null +++ b/metadata/pool/pool_11.py @@ -0,0 +1,9 @@ +POOL_11 = [ + { + "five": ["希尔"], + "four": ["娜塔莎", "佩拉", "虎克"], + "from": "2023-04-26 06:00:00", + "name": "蝶立锋锷", + "to": "2023-05-17 17:59:59", + }, +] diff --git a/metadata/pool/pool_12.py b/metadata/pool/pool_12.py new file mode 100644 index 0000000..28b4f42 --- /dev/null +++ b/metadata/pool/pool_12.py @@ -0,0 +1,9 @@ +POOL_12 = [ + { + "five": ["于夜色中"], + "four": ["一场术后对话", "晚安与睡颜", "鼹鼠党欢迎你"], + "from": "2023-04-26 06:00:00", + "name": "流光定影", + "to": "2023-05-17 17:59:59", + }, +] diff --git a/metadata/pool/pool_2.py b/metadata/pool/pool_2.py new file mode 100644 index 0000000..3d959aa --- /dev/null +++ b/metadata/pool/pool_2.py @@ -0,0 +1 @@ +POOL_2 = [{"five": ["新手池"], "four": [], "from": "2023-04-26 06:00:00", "name": "新手池", "to": "2050-09-15 17:59:59"}] diff --git a/metadata/pool/pool_200.py b/metadata/pool/pool_200.py deleted file mode 100644 index 3bc8339..0000000 --- a/metadata/pool/pool_200.py +++ /dev/null @@ -1 +0,0 @@ -POOL_200 = [{"five": ["常驻池"], "four": [], "from": "2020-09-15 06:00:00", "name": "常驻池", "to": "2050-09-15 17:59:59"}] diff --git a/metadata/pool/pool_301.py b/metadata/pool/pool_301.py deleted file mode 100644 index c5e1a9f..0000000 --- a/metadata/pool/pool_301.py +++ /dev/null @@ -1,324 +0,0 @@ -POOL_301 = [ - { - "five": ["纳西妲", "妮露"], - "four": ["久岐忍", "多莉", "莱依拉"], - "from": "2023-04-12 06:00:00", - "name": "月草的赐慧|翩舞歈莲", - "to": "2023-05-02 17:59:59", - }, - { - "five": ["申鹤", "神里绫华"], - "four": ["米卡", "砂糖", "迪奥娜"], - "from": "2023-03-21 18:00:00", - "name": "孤辰茕怀|白鹭霜华", - "to": "2023-04-11 14:59:59", - }, - { - "five": ["迪希雅", "赛诺"], - "four": ["班尼特", "芭芭拉", "柯莱"], - "from": "2023-03-01 06:00:00", - "name": "烈阳烁金|雳裁冥昭", - "to": "2023-03-21 17:59:59", - }, - { - "five": ["胡桃", "夜兰"], - "four": ["行秋", "凝光", "北斗"], - "from": "2023-02-07 18:00:00", - "name": "赤团开时|素霓伣天", - "to": "2023-02-28 14:59:59", - }, - { - "five": ["艾尔海森", "魈"], - "four": ["瑶瑶", "云堇", "辛焱"], - "from": "2023-01-18 06:00:00", - "name": "敕诫枢谋|烟火之邀", - "to": "2023-02-07 17:59:59", - }, - { - "five": ["雷电将军", "神里绫人"], - "four": ["罗莎莉亚", "早柚", "九条裟罗"], - "from": "2022-12-27 18:00:00", - "name": "影寂天下人|苍流踏花", - "to": "2023-01-17 14:59:59", - }, - { - "five": ["流浪者", "荒泷一斗"], - "four": ["珐露珊", "五郎", "烟绯"], - "from": "2022-12-07 06:00:00", - "name": "余火变相|鬼门斗宴", - "to": "2022-12-27 17:59:59", - }, - { - "five": ["八重神子", "达达利亚"], - "four": ["莱依拉", "托马", "鹿野院平藏"], - "from": "2022-11-18 18:00:00", - "name": "华紫樱绯|暂别冬都", - "to": "2022-12-06 14:59:59", - }, - { - "five": ["纳西妲", "宵宫"], - "four": ["雷泽", "诺艾尔", "班尼特"], - "from": "2022-11-2 06:00:00", - "name": "月草的赐慧|焰色天河", - "to": "2022-11-18 17:59:59", - }, - { - "five": ["妮露", "阿贝多"], - "four": ["北斗", "芭芭拉", "香菱"], - "from": "2022-10-14 18:00:00", - "name": "翩舞歈莲|深秘之息", - "to": "2022-11-01 14:59:59", - }, - { - "five": ["赛诺", "温迪"], - "four": ["久岐忍", "早柚", "坎蒂丝"], - "from": "2022-09-28 06:00:00", - "name": "雳裁冥昭|杯装之诗", - "to": "2022-10-14 17:59:59", - }, - { - "five": ["甘雨", "心海"], - "four": ["行秋", "砂糖", "多莉"], - "from": "2022-09-09 18:00:00", - "name": "浮生孰来|浮岳虹珠", - "to": "2022-09-27 14:59:59", - }, - { - "five": ["提纳里", "钟离"], - "four": ["云堇", "辛焱", "班尼特"], - "from": "2022-08-24 06:00:00", - "name": "巡御蘙荟|陵薮市朝", - "to": "2022-09-09 17:59:59", - }, - { - "five": ["宵宫"], - "four": ["云堇", "辛焱", "班尼特"], - "from": "2022-08-02 18:00:00", - "name": "焰色天河", - "to": "2022-08-23 14:59:59", - }, - { - "five": ["枫原万叶", "可莉"], - "four": ["凝光", "鹿野院平藏", "托马"], - "from": "2022-07-13 06:00:00", - "name": "红叶逐荒波", - "to": "2022-08-02 17:59:59", - }, - { - "five": ["荒泷一斗"], - "four": ["烟绯", "芭芭拉", "诺艾尔"], - "from": "2022-06-21 18:00:00", - "name": "鬼门斗宴", - "to": "2022-07-12 14:59:59", - }, - { - "five": ["夜兰", "魈"], - "four": ["烟绯", "芭芭拉", "诺艾尔"], - "from": "2022-05-31 06:00:00", - "name": "素霓伣天|烟火之邀", - "to": "2022-06-21 17:59:59", - }, - { - "five": ["神里绫华"], - "four": ["罗莎莉亚", "早柚", "雷泽"], - "from": "2022-04-19 17:59:59", - "name": "白鹭之庭", - "to": "2022-05-31 05:59:59", - }, - { - "five": ["神里绫人", "温迪"], - "four": ["香菱", "砂糖", "云堇"], - "from": "2022-03-30 06:00:00", - "name": "苍流踏花|杯装之诗", - "to": "2022-04-19 17:59:59", - }, - { - "five": ["雷电将军", "珊瑚宫心海"], - "four": ["辛焱", "九条裟罗", "班尼特"], - "from": "2022-03-08 18:00:00", - "name": "影寂天下人|浮岳虹珠", - "to": "2022-03-29 14:59:59", - }, - { - "five": ["八重神子"], - "four": ["菲谢尔", "迪奥娜", "托马"], - "from": "2022-02-16 06:00:00", - "name": "华紫樱绯", - "to": "2022-03-08 17:59:59", - }, - { - "five": ["甘雨", "钟离"], - "four": ["行秋", "北斗", "烟绯"], - "from": "2022-01-25 18:00:00", - "name": "浮生孰来|陵薮市朝", - "to": "2022-02-15 14:59:59", - }, - { - "five": ["申鹤", "魈"], - "four": ["云堇", "凝光", "重云"], - "from": "2022-01-05 06:00:00", - "name": "出尘入世|烟火之邀", - "to": "2022-01-25 17:59:59", - }, - { - "five": ["荒泷一斗"], - "four": ["五郎", "芭芭拉", "香菱"], - "from": "2021-12-14 18:00:00", - "name": "鬼门斗宴", - "to": "2022-01-04 14:59:59", - }, - { - "five": ["阿贝多", "优菈"], - "four": ["班尼特", "诺艾尔", "罗莎莉亚"], - "from": "2021-11-24 06:00:00", - "name": "深秘之息|浪涌之瞬", - "to": "2021-12-14 17:59:59", - }, - { - "five": ["胡桃"], - "four": ["托马", "迪奥娜", "早柚"], - "from": "2021-11-02 18:00:00", - "name": "赤团开时", - "to": "2021-11-23 14:59:59", - }, - { - "five": ["达达利亚"], - "four": ["凝光", "重云", "烟绯"], - "from": "2021-10-13 06:00:00", - "name": "暂别冬都", - "to": "2021-11-02 17:59:59", - }, - { - "five": ["珊瑚宫心海"], - "four": ["罗莎莉亚", "北斗", "行秋"], - "from": "2021-09-21 18:00:00", - "name": "浮岳虹珠", - "to": "2021-10-12 14:59:59", - }, - { - "five": ["雷电将军"], - "four": ["九条裟罗", "香菱", "砂糖"], - "from": "2021-09-01 06:00:00", - "name": "影寂天下人", - "to": "2021-09-21 17:59:59", - }, - { - "five": ["宵宫"], - "four": ["早柚", "迪奥娜", "辛焱"], - "from": "2021-08-10 18:00:00", - "name": "焰色天河", - "to": "2021-08-31 14:59:59", - }, - { - "five": ["神里绫华"], - "four": ["凝光", "重云", "烟绯"], - "from": "2021-07-21 06:00:00", - "name": "白鹭之庭", - "to": "2021-08-10 17:59:59", - }, - { - "five": ["枫原万叶"], - "four": ["罗莎莉亚", "班尼特", "雷泽"], - "from": "2021-06-29 18:00:00", - "name": "红叶逐荒波", - "to": "2021-07-20 14:59:59", - }, - { - "five": ["可莉"], - "four": ["芭芭拉", "砂糖", "菲谢尔"], - "from": "2021-06-09 06:00:00", - "name": "逃跑的太阳", - "to": "2021-06-29 17:59:59", - }, - { - "five": ["优菈"], - "four": ["辛焱", "行秋", "北斗"], - "from": "2021-05-18 18:00:00", - "name": "浪沫的旋舞", - "to": "2021-06-08 14:59:59", - }, - { - "five": ["钟离"], - "four": ["烟绯", "诺艾尔", "迪奥娜"], - "from": "2021-04-28 06:00:00", - "name": "陵薮市朝", - "to": "2021-05-18 17:59:59", - }, - { - "five": ["达达利亚"], - "four": ["罗莎莉亚", "芭芭拉", "菲谢尔"], - "from": "2021-04-06 18:00:00", - "name": "暂别冬都", - "to": "2021-04-27 14:59:59", - }, - { - "five": ["温迪"], - "four": ["砂糖", "雷泽", "诺艾尔"], - "from": "2021-03-17 06:00:00", - "name": "杯装之诗", - "to": "2021-04-06 15:59:59", - }, - { - "five": ["胡桃"], - "four": ["行秋", "香菱", "重云"], - "from": "2021-03-02 18:00:00", - "name": "赤团开时", - "to": "2021-03-16 14:59:59", - }, - { - "five": ["刻晴"], - "four": ["凝光", "班尼特", "芭芭拉"], - "from": "2021-02-17 18:00:00", - "name": "鱼龙灯昼", - "to": "2021-03-02 15:59:59", - }, - { - "five": ["魈"], - "four": ["迪奥娜", "北斗", "辛焱"], - "from": "2021-02-03 06:00:00", - "name": "烟火之邀", - "to": "2021-02-17 15:59:59", - }, - { - "five": ["甘雨"], - "four": ["香菱", "行秋", "诺艾尔"], - "from": "2021-01-12 18:00:00", - "name": "浮生孰来", - "to": "2021-02-02 14:59:59", - }, - { - "five": ["阿贝多"], - "four": ["菲谢尔", "砂糖", "班尼特"], - "from": "2020-12-23 06:00:00", - "name": "深秘之息", - "to": "2021-01-12 15:59:59", - }, - { - "five": ["钟离"], - "four": ["辛焱", "雷泽", "重云"], - "from": "2020-12-01 18:00:00", - "name": "陵薮市朝", - "to": "2020-12-22 14:59:59", - }, - { - "five": ["达达利亚"], - "four": ["迪奥娜", "北斗", "凝光"], - "from": "2020-11-11 06:00:00", - "name": "暂别冬都", - "to": "2020-12-01 15:59:59", - }, - { - "five": ["可莉"], - "four": ["行秋", "诺艾尔", "砂糖"], - "from": "2020-10-20 18:00:00", - "name": "闪焰的驻足", - "to": "2020-11-10 14:59:59", - }, - { - "five": ["温迪"], - "four": ["芭芭拉", "菲谢尔", "香菱"], - "from": "2020-9-28 06:00:00", - "name": "杯装之诗", - "to": "2020-10-18 17:59:59", - }, -] diff --git a/metadata/pool/pool_302.py b/metadata/pool/pool_302.py deleted file mode 100644 index 04a7ef2..0000000 --- a/metadata/pool/pool_302.py +++ /dev/null @@ -1,317 +0,0 @@ -POOL_302 = [ - { - "five": ["千夜浮梦", "圣显之钥"], - "four": ["西福斯的月光", "西风大剑", "匣里灭辰", "祭礼残章", "绝弦"], - "from": "2023-04-12 06:00:00", - "name": "神铸赋形", - "to": "2023-05-02 17:59:59", - }, - { - "five": ["息灾", "雾切之回光"], - "four": ["暗巷的酒与诗", "祭礼剑", "钟剑", "西风长枪", "西风猎弓"], - "from": "2023-03-21 18:00:00", - "name": "神铸赋形", - "to": "2023-04-11 14:59:59", - }, - { - "five": ["苇海信标", "赤沙之杖"], - "four": ["暗巷闪光", "暗巷猎手", "祭礼大剑", "匣里灭辰", "昭心"], - "from": "2023-03-01 06:00:00", - "name": "神铸赋形", - "to": "2023-03-21 17:59:59", - }, - { - "five": ["护摩之杖", "若水"], - "four": ["千岩古剑", "西风剑", "匣里灭辰", "西风秘典", "弓藏"], - "from": "2023-02-07 18:00:00", - "name": "神铸赋形", - "to": "2023-02-28 14:59:59", - }, - { - "five": ["裁叶萃光", "和璞鸢"], - "four": ["千岩长枪", "笛剑", "雨裁", "流浪乐章", "祭礼弓"], - "from": "2023-01-18 06:00:00", - "name": "神铸赋形", - "to": "2023-02-07 17:59:59", - }, - { - "five": ["薙草之稻光", "波乱月白经津"], - "four": ["恶王丸", "曚云之月", "匣里龙吟", "西风长枪", "祭礼残章"], - "from": "2022-12-27 18:00:00", - "name": "神铸赋形", - "to": "2023-01-17 14:59:59", - }, - { - "five": ["图莱杜拉的回忆", "赤角石溃杵"], - "four": ["祭礼剑", "西风大剑", "断浪长鳍", "昭心", "西风猎弓"], - "from": "2022-12-07 06:00:00", - "name": "神铸赋形", - "to": "2022-12-27 17:59:59", - }, - { - "five": ["神乐之真意", "冬极白星"], - "four": ["西风剑", "钟剑", "匣里灭辰", "西风秘典", "绝弦"], - "from": "2022-11-18 18:00:00", - "name": "神铸赋形", - "to": "2022-12-06 14:59:59", - }, - { - "five": ["千夜浮梦", "飞雷之弦振"], - "four": ["笛剑", "祭礼大剑", "西风长枪", "流浪乐章", "弓藏"], - "from": "2022-11-2 06:00:00", - "name": "神铸赋形", - "to": "2022-11-18 17:59:59", - }, - { - "five": ["圣显之钥", "磐岩结绿"], - "four": ["西福斯的月光", "雨裁", "匣里灭辰", "流浪的晚星", "祭礼弓"], - "from": "2022-10-14 18:00:00", - "name": "神铸赋形", - "to": "2022-11-01 14:59:59", - }, - { - "five": ["赤沙之杖", "终末嗟叹之诗"], - "four": ["匣里龙吟", "玛海菈的水色", "西风长枪", "祭礼残章", "西风猎弓"], - "from": "2022-09-28 06:00:00", - "name": "神铸赋形", - "to": "2022-10-14 17:59:59", - }, - { - "five": ["阿莫斯之弓", "不灭月华"], - "four": ["祭礼剑", "西风大剑", "匣里灭辰", "昭心", "弓藏"], - "from": "2022-09-09 18:00:00", - "name": "神铸赋形", - "to": "2022-09-27 14:59:59", - }, - { - "five": ["猎人之径", "贯虹之槊"], - "four": ["西风剑", "钟剑", "西风长枪", "西风秘典", "绝弦"], - "from": "2022-08-24 06:00:00", - "name": "神铸赋形", - "to": "2022-09-09 17:59:59", - }, - { - "five": ["飞雷之弦振", "斫峰之刃"], - "four": ["暗巷的酒与诗", "暗巷猎手", "笛剑", "祭礼大剑", "匣里灭辰"], - "from": "2022-08-02 18:00:00", - "name": "神铸赋形", - "to": "2022-08-23 14:59:59", - }, - { - "five": ["苍古自由之誓", "四风原典"], - "four": ["千岩古剑", "匣里龙吟", "匣里灭辰", "祭礼残章", "绝弦"], - "from": "2022-07-13 06:00:00", - "name": "神铸赋形", - "to": "2022-08-02 17:59:59", - }, - { - "five": ["赤角石溃杵", "尘世之锁"], - "four": ["千岩古剑", "匣里龙吟", "匣里灭辰", "祭礼残章", "绝弦"], - "from": "2022-06-21 18:00:00", - "name": "神铸赋形", - "to": "2022-07-12 14:59:59", - }, - { - "five": ["若水", "和璞鸢"], - "four": ["千岩长枪", "祭礼剑", "西风大剑", "昭心", "祭礼弓"], - "from": "2022-05-31 06:00:00", - "name": "神铸赋形", - "to": "2022-06-21 17:59:59", - }, - { - "five": ["雾切之回光", "无工之剑"], - "four": ["西风剑", "钟剑", "西风长枪", "西风秘典", "西风猎弓"], - "from": "2022-04-19 17:59:59", - "name": "神铸赋形", - "to": "2022-05-31 05:59:59", - }, - { - "five": ["波乱月白经津", "终末嗟叹之诗"], - "four": ["弓藏", "笛剑", "流浪乐章", "匣里灭辰", "祭礼大剑"], - "from": "2022-03-30 06:00:00", - "name": "神铸赋形", - "to": "2022-04-19 17:59:59", - }, - { - "five": ["薙草之稻光", "不灭月华"], - "four": ["恶王丸", "曚云之月", "匣里龙吟", "西风长枪", "祭礼残章"], - "from": "2022-03-08 18:00:00", - "name": "神铸赋形", - "to": "2022-03-29 14:59:59", - }, - { - "five": ["神乐之真意", "磐岩结绿"], - "four": ["祭礼剑", "雨裁", "断浪长鳍", "昭心", "绝弦"], - "from": "2022-02-16 06:00:00", - "name": "神铸赋形", - "to": "2022-03-08 17:59:59", - }, - { - "five": ["贯虹之槊", "阿莫斯之弓"], - "four": ["西风剑", "千岩古剑", "匣里灭辰", "西风秘典", "祭礼弓"], - "from": "2022-01-25 18:00:00", - "name": "神铸赋形", - "to": "2022-02-15 14:59:59", - }, - { - "five": ["息灾", "和璞鸢"], - "four": ["笛剑", "西风大剑", "千岩长枪", "流浪乐章", "西风猎弓"], - "from": "2022-01-05 06:00:00", - "name": "神铸赋形", - "to": "2022-01-25 17:59:59", - }, - { - "five": ["赤角石溃杵", "天空之翼"], - "four": ["暗巷闪光", "钟剑", "西风长枪", "祭礼残章", "幽夜华尔兹"], - "from": "2021-12-14 18:00:00", - "name": "神铸赋形", - "to": "2022-01-04 14:59:59", - }, - { - "five": ["苍古自由之誓", "松籁响起之时"], - "four": ["匣里龙吟", "祭礼大剑", "匣里灭辰", "暗巷的酒与诗", "暗巷猎手"], - "from": "2021-11-24 06:00:00", - "name": "神铸赋形", - "to": "2021-12-14 17:59:59", - }, - { - "five": ["护摩之杖", "终末嗟叹之诗"], - "four": ["祭礼剑", "雨裁", "断浪长鳍", "流浪乐章", "曚云之月"], - "from": "2021-11-02 18:00:00", - "name": "神铸赋形", - "to": "2021-11-23 14:59:59", - }, - { - "five": ["冬极白星", "尘世之锁"], - "four": ["西风剑", "恶王丸", "西风长枪", "昭心", "弓藏"], - "from": "2021-10-13 06:00:00", - "name": "神铸赋形", - "to": "2021-11-02 17:59:59", - }, - { - "five": ["不灭月华", "磐岩结绿"], - "four": ["笛剑", "西风大剑", "匣里灭辰", "西风秘典", "绝弦"], - "from": "2021-09-21 18:00:00", - "name": "神铸赋形", - "to": "2021-10-12 14:59:59", - }, - { - "five": ["薙草之稻光", "无工之剑"], - "four": ["匣里龙吟", "钟剑", "西风长枪", "流浪乐章", "祭礼弓"], - "from": "2021-09-01 06:00:00", - "name": "神铸赋形", - "to": "2021-09-21 17:59:59", - }, - { - "five": ["飞雷之弦振", "天空之刃"], - "four": ["祭礼剑", "雨裁", "匣里灭辰", "祭礼残章", "西风猎弓"], - "from": "2021-08-10 18:00:00", - "name": "神铸赋形", - "to": "2021-08-31 14:59:59", - }, - { - "five": ["雾切之回光", "天空之脊"], - "four": ["西风剑", "祭礼大剑", "西风长枪", "西风秘典", "绝弦"], - "from": "2021-07-21 06:00:00", - "name": "神铸赋形", - "to": "2021-08-10 17:59:59", - }, - { - "five": ["苍古自由之誓", "天空之卷"], - "four": ["暗巷闪光", "西风大剑", "匣里灭辰", "暗巷的酒与诗", "暗巷猎手"], - "from": "2021-06-29 18:00:00", - "name": "神铸赋形", - "to": "2021-07-20 14:59:59", - }, - { - "five": ["天空之傲", "四风原典"], - "four": ["匣里龙吟", "钟剑", "西风长枪", "流浪乐章", "幽夜华尔兹"], - "from": "2021-06-09 06:00:00", - "name": "神铸赋形", - "to": "2021-06-29 17:59:59", - }, - { - "five": ["松籁响起之时", "风鹰剑"], - "four": ["祭礼剑", "雨裁", "匣里灭辰", "祭礼残章", "弓藏"], - "from": "2021-05-18 18:00:00", - "name": "神铸赋形", - "to": "2021-06-08 14:59:59", - }, - { - "five": ["斫峰之刃", "尘世之锁"], - "four": ["笛剑", "千岩古剑", "祭礼弓", "昭心", "千岩长枪"], - "from": "2021-04-28 06:00:00", - "name": "神铸赋形", - "to": "2021-05-18 17:59:59", - }, - { - "five": ["天空之翼", "四风原典"], - "four": ["西风剑", "祭礼大剑", "暗巷猎手", "西风秘典", "西风长枪"], - "from": "2021-04-06 18:00:00", - "name": "神铸赋形", - "to": "2021-04-27 14:59:59", - }, - { - "five": ["终末嗟叹之诗", "天空之刃"], - "four": ["暗巷闪光", "西风大剑", "西风猎弓", "暗巷的酒与诗", "匣里灭辰"], - "from": "2021-03-17 06:00:00", - "name": "神铸赋形", - "to": "2021-04-06 15:59:59", - }, - { - "five": ["护摩之杖", "狼的末路"], - "four": ["匣里龙吟", "千岩古剑", "祭礼弓", "流浪乐章", "千岩长枪"], - "from": "2021-02-23 18:00:00", - "name": "神铸赋形", - "to": "2021-03-16 14:59:59", - }, - { - "five": ["磐岩结绿", "和璞鸢"], - "four": ["笛剑", "祭礼大剑", "弓藏", "昭心", "西风长枪"], - "from": "2021-02-03 06:00:00", - "name": "神铸赋形", - "to": "2021-02-23 15:59:59", - }, - { - "five": ["阿莫斯之弓", "天空之傲"], - "four": ["祭礼剑", "钟剑", "匣里灭辰", "昭心", "西风猎弓"], - "from": "2021-01-12 18:00:00", - "name": "神铸赋形", - "to": "2021-02-02 14:59:59", - }, - { - "five": ["斫峰之刃", "天空之卷"], - "four": ["西风剑", "西风大剑", "西风长枪", "祭礼残章", "绝弦"], - "from": "2020-12-23 06:00:00", - "name": "神铸赋形", - "to": "2021-01-12 15:59:59", - }, - { - "five": ["贯虹之槊", "无工之剑"], - "four": ["匣里龙吟", "钟剑", "西风秘典", "西风猎弓", "匣里灭辰"], - "from": "2020-12-01 18:00:00", - "name": "神铸赋形", - "to": "2020-12-22 14:59:59", - }, - { - "five": ["天空之翼", "尘世之锁"], - "four": ["笛剑", "雨裁", "昭心", "弓藏", "西风长枪"], - "from": "2020-11-11 06:00:00", - "name": "神铸赋形", - "to": "2020-12-01 15:59:59", - }, - { - "five": ["四风原典", "狼的末路"], - "four": ["祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓", "匣里灭辰"], - "from": "2020-10-20 18:00:00", - "name": "神铸赋形", - "to": "2020-11-10 14:59:59", - }, - { - "five": ["风鹰剑", "阿莫斯之弓"], - "four": ["祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓", "匣里灭辰"], - "from": "2020-09-28 06:00:00", - "name": "神铸赋形", - "to": "2020-10-18 17:59:59", - }, -] diff --git a/metadata/shortname.py b/metadata/shortname.py index 2c76e98..76bd163 100644 --- a/metadata/shortname.py +++ b/metadata/shortname.py @@ -5,700 +5,107 @@ from typing import List from metadata.genshin import WEAPON_DATA -__all__ = ["roles", "weapons", "roleToId", "roleToName", "weaponToName", "weaponToId", "not_real_roles", "roleToTag"] +__all__ = ["roles", "light_cones", "roleToId", "roleToName", "lightConeToName", "lightConeToId", "not_real_roles", "roleToTag"] # noinspection SpellCheckingInspection roles = { - 20000000: [ - "旅行者", - "主角", - "卑鄙的外乡人", - "荣誉骑士", - "爷", - "履刑者", - "人之子", - "命定之人", - "荣誉骑士", - "小可爱", # 丽莎 - "小家伙", # 八重神子 - "金发异乡人", - "大黄金钓鱼手", # 派蒙 - "黄毛阿姨", - "黄毛叔叔", - "大黄倭瓜那菈", - ], - 10000002: ["神里绫华", "ayaka", "kamisato ayaka", "神里", "绫华", "神里凌华", "凌华", "白鹭公主", "神里大小姐", "冰骗骗花", "龟龟"], - 10000003: ["琴", "jean", "团长", "代理团长", "琴团长", "蒲公英骑士", "蒙德砍王", "骑士团的魂"], - 10000005: ["空", "aether", "男主", "男主角", "龙哥", "空哥", "王子"], - 10000006: ["丽莎", "lisa", "图书管理员", "图书馆管理员", "蔷薇魔女"], - 10000007: ["荧", "lumine", "女主", "女主角", "莹", "萤", "黄毛阿姨", "荧妹", "公主殿下"], - 10000014: ["芭芭拉", "barbara", "巴巴拉", "拉粑粑", "拉巴巴", "内鬼", "加湿器", "闪耀偶像", "偶像", "蒙德辣王"], - 10000015: ["凯亚", "kaeya", "盖亚", "凯子哥", "凯鸭", "矿工", "矿工头子", "骑兵队长", "凯子", "凝冰渡海真君", "花脸猫"], - 10000016: [ - "迪卢克", - "diluc", - "卢姥爷", - "姥爷", - "卢老爷", - "卢锅巴", - "正义人", - "正e人", - "正E人", - "卢本伟", - "暗夜英雄", - "卢卢伯爵", - "落魄了", - "落魄了家人们", - "哦哦哦", - "前夫哥", - "在此烧鸟真君", - "E键三连真君", - ], - 10000020: [ - "雷泽", - "razor", - "狼少年", - "狼崽子", - "狼崽", - "卢皮卡", - "小狼", - "小狼狼", - "小狼狗", - "小赛诺", - "替身使者", - "须佐狼乎", - "蒙德砍王", - "炸矿之星", - ], - 10000021: [ - "安柏", - "amber", - "安伯", - "兔兔伯爵", - "飞行冠军", - "侦查骑士", - "侦察骑士", - "点火姬", - "点火机", - "打火机", - "打火姬", - "燃炬焚棘真君", - "初代目提瓦特第一火弓", - ], - 10000022: [ - "温迪", - "venti", - "barbatos", - "温蒂", - "风神", - "卖唱的", - "巴巴托斯", - "巴巴脱丝", - "芭芭托斯", - "芭芭脱丝", - "干点正事", - "不干正事", - "吟游诗人", - "诶嘿", - "唉嘿", - "摸鱼", - "最弱最丢人的七神", - "卖唱的大哥哥", - "巴巴托斯大人", - "欸嘿聚怪真君", - "荻花洲的吹笛人", - "直升机", - ], - 10000023: [ - "香菱", - "xiangling", - "香玲", - "锅巴", - "厨师", - "万民堂厨师", - "香师傅", - "哪吒", - "锅巴发射器", - "无敌风火轮真君", - "舌尖上的璃月", - "提瓦特枪王", - ], - 10000024: ["北斗", "beidou", "大姐头", "大姐", "无冕的龙王", "稻妻人形继电石"], - 10000025: ["行秋", "xingqiu", "秋秋人", "秋妹妹", "书呆子", "水神", "飞云商会二少爷", "秋秋人", "6星水神", "枕玉老师"], - 10000026: [ - "魈", - "xiao", - "杏仁豆腐", - "打桩机", - "插秧", - "三眼五显仙人", - "三眼五显真人", - "降魔大圣", - "护法夜叉", - "快乐风男", - "无聊", - "靖妖傩舞", - "矮子仙人", - "三点五尺仙人", - "跳跳虎", - "护法夜叉大将", - "金鹏大将", - "这里无能真君", - "抬头不见低头见真君", - "跳跳虎", - "随叫随到真君", - "成天冷着脸的帅气小哥", - ], - 10000027: ["凝光", "ningguang", "富婆", "天权", "天权星", "寻山见矿真君"], - 10000029: [ - "可莉", - "klee", - "嘟嘟可", - "火花骑士", - "蹦蹦炸弹", - "炸鱼", - "放火烧山", - "放火烧山真君", - "蒙德最强战力", - "逃跑的太阳", - "啦啦啦", - "哒哒哒", - "炸弹人", - "禁闭室", - "艾莉丝的女儿", - "阿贝多的义妹", - "火化骑士", - "炸鱼禁闭真君", - "蒙德小坦克", - "骑士团团宠", - ], - 10000030: [ - "钟离", - "zhongli", - "morax", - "摩拉克斯", - "岩王爷", - "岩神", - "钟师傅", - "天动万象", - "岩王帝君", - "未来可期", - "帝君", - "契约之神", - "社会废人", - "未来可期真君", - "废人养成器", - "听书人", - ], - 10000031: ["菲谢尔", "fischl", "皇女", "小艾米", "小艾咪", "奥兹", "断罪皇女", "中二病", "中二少女", "中二皇女", "奥兹发射器"], - 10000032: ["班尼特", "bennett", "点赞哥", "点赞", "倒霉少年", "倒霉蛋", "霹雳闪雷真君", "班神", "班爷", "倒霉", "火神", "六星真神"], - 10000033: [ - "达达利亚", - "tartaglia", - "childe", - "ajax", - "达达鸭", - "达达利鸭", - "公子", - "玩具销售员", - "玩具推销员", - "钱包", - "鸭鸭", - "愚人众末席", - "至冬国驻璃月港玩具推销员主管", - "钟离的钱包", - "近战弓兵", - "在蒙德认识的冒险家", - "永别冬都", - "汤达人", - "大貉妖处理专家", - ], - 10000034: ["诺艾尔", "noelle", "女仆", "高达", "岩王帝姬", "山吹", "冰萤术士", "岩王帝姬"], - 10000035: ["七七", "qiqi", "僵尸", "肚饿真君", "度厄真君", "77", "起死回骸童子", "救苦度厄真君", "椰羊创始人", "不卜庐砍王", "不卜庐剑圣"], - 10000036: ["重云", "chongyun", "纯阳之体", "冰棍", "驱邪世家", "大外甥"], - 10000037: ["甘雨", "ganyu", "椰羊", "椰奶", "鸡腿猎人", "咕噜咕噜滚下山真君", "肝雨", "走路上山真君"], - 10000038: [ - "阿贝多", - "albedo", - "可莉哥哥", - "升降机", - "升降台", - "电梯", - "白垩之子", - "贝爷", - "白垩", - "阿贝少", - "花呗多", - "阿贝夕", - "abd", - "阿师傅", - "小王子", - "调查小队队长", - "西风骑士团首席炼金术师", - "白垩老师", - "电梯人", - "蒙德岩神", - "平平无奇", - "蒙德NPC", - ], - 10000039: ["迪奥娜", "diona", "迪欧娜", "dio", "dio娜", "冰猫", "猫猫", "猫娘", "喵喵", "调酒师"], - 10000041: [ - "莫娜", - "mona", - "穷鬼", - "穷光蛋", - "穷", - "莫纳", - "占星术士", - "占星师", - "讨龙真君", - "半部讨龙真君", - "阿斯托洛吉斯·莫娜·梅姬斯图斯", - "astrologist mona megistus", - "梅姬斯图斯", - "梅姬斯图斯卿", - "梅姬", - "半部讨龙真君", - ], - 10000042: [ - "刻晴", - "keqing", - "刻情", - "氪晴", - "刻师傅", - "刻师父", - "牛杂", - "牛杂师傅", - "斩尽牛杂", - "免疫", - "免疫免疫", - "屁斜剑法", - "玉衡星", - "阿晴", - "啊晴", - "得不到的女人", - "金丝虾球真君", - "璃月雷神", - "刻猫猫", - ], - 10000043: ["砂糖", "sucrose", "雷莹术士", "雷萤术士", "雷荧术士"], - 10000044: ["辛焱", "xinyan", "辛炎", "黑妹", "摇滚"], - 10000045: [ - "罗莎莉亚", - "rosaria", - "罗莎莉娅", - "白色史莱姆", - "白史莱姆", - "修女", - "罗莎利亚", - "罗莎利娅", - "罗沙莉亚", - "罗沙莉娅", - "罗沙利亚", - "罗沙利娅", - "萝莎莉亚", - "萝莎莉娅", - "萝莎利亚", - "萝莎利娅", - "萝沙莉亚", - "萝沙莉娅", - "萝沙利亚", - "萝沙利娅", - ], - 10000046: [ - "胡桃", - "hutao", - "hu tao", - "胡淘", - "往生堂堂主", - "火化", - "抬棺的", - "蝴蝶", - "核桃", - "堂主", - "胡堂主", - "雪霁梅香", - "赤团开时", - "黑无常", - "嘘嘘鬼王", - "琪亚娜", - "薪炎之律者", - ], - 10000047: ["枫原万叶", "kazuha", "kaedehara kazuha", "万叶", "叶天帝", "天帝", "人型气象观测台", "浪人武士"], - 10000048: ["烟绯", "yanfei", "烟老师", "律师", "罗翔", "璃月港的知名律法咨询师", "璃月罗翔", "铁人三项真君"], - 10000049: [ - "宵宫", - "yoimiya", - "霄宫", - "烟花", - "肖宫", - "肖工", - "绷带女孩", - "夏祭的女王", - "地对鸽导弹", - "打火姬二代目", - "长野原加特林", - "花见坂军火商", - ], - 10000050: ["托马", "thoma", "家政官", "太郎丸", "地头蛇", "男仆", "男妈妈"], - 10000051: ["优菈", "eula", "优拉", "尤拉", "尤菈", "浪花骑士", "记仇", "喷嚏记仇真君"], - 10000052: [ - "雷电将军", - "shougun", - "raiden shogun", - "raiden", - "ei", - "raiden ei", - "baal", - "雷神", - "将军", - "雷军", - "巴尔", - "阿影", - "影", - "巴尔泽布", - "煮饭婆", - "奶香一刀", - "无想一刀", - "宅女", - "大御所大人", - "鸣神", - "永恒之神", - "姐控", - "不会做饭真君", - "宅女程序员", - "奶香一刀真君", - "雷电芽衣", - "又哭又闹真君", - "御建鸣神主尊大御所大人", - ], - 10000053: ["早柚", "sayu", "小狸猫", "狸猫", "咕噜咕噜赶路真君", "柚岩龙蜥", "善于潜行的矮子", "专业人士"], - 10000054: [ - "珊瑚宫心海", - "kokomi", - "sangonomiya kokomi", - "心海", - "军师", - "珊瑚宫", - "书记", - "观赏鱼", - "水母", - "鱼", - "现人神巫女", - "宅家派节能军师", - "藤原千花", - "能量管理大师", - "五星观赏鱼", - "海天后", - "深海舌鲆鱼小姐", - ], - 10000055: ["五郎", "gorou", "柴犬", "土狗", "希娜", "希娜小姐", "海祇岛的小狗大将", "修勾", "五郎大将的朋友", "小狗勾"], - 10000056: [ - "九条裟罗", - "sara", - "kujou sara", - "九条", - "九条沙罗", - "裟罗", - "条家的养子", - "雷系班尼特", - "雷神单推头子", - "珊瑚宫心海的冤家", - "荒泷一斗的冤家", - "外置暴伤", - "维密天使", - ], - 10000057: [ - "荒泷一斗", - "itto", - "arataki itto", - "荒龙一斗", - "荒泷天下第一斗", - "一斗", - "一抖", - "荒泷", - "1斗", - "牛牛", - "斗子哥", - "牛子哥", - "牛子", - "孩子王", - "斗虫", - "巧乐兹", - "放牛的", - "岩丘丘萨满", - "伐伐伐伐伐木工", - "希娜小姐的榜一大哥", - ], - 10000058: [ - "八重神子", - "miko", - "yae miko", - "八重", - "神子", - "狐狸", - "想得美哦", - "巫女", - "屑狐狸", - "骚狐狸", - "八重宫司", - "婶子", - "小八", - "白辰血脉的后裔", - "兼具智慧和美貌的八重神子大人", - "稻妻老八", - "雷丘丘萨满", - "八重樱", - "嗑瓜子", - "小奥兹", - "玲珑油豆腐小姐", - ], - 10000059: [ - "鹿野院平藏", - "heizou", - "shikanoin heizou", - "heizo", - "鹿野苑", - "鹿野院", - "平藏", - "鹿野苑平藏", - "鹿野", - "小鹿", - "天领奉行侦探", - "鹿野奈奈的表弟", - "风拳前锋军", - "拳师", - "名侦探柯南", - "捕快展昭", - ], - 10000060: ["夜兰", "yelan", "夜阑", "叶澜", "腋兰", "夜天后", "自称就职于总务司的神秘人士", "岩上茶室老板", "夜上海", "胸怀大痣"], - 10000062: ["埃洛伊", "aloy", "异界的救世主"], - 10000063: ["申鹤", "shenhe", "神鹤", "小姨", "小姨子", "审鹤", "仙家弟子", "驱邪世家旁", "药材杀手"], - 10000064: ["云堇", "yunjin", "yun jin", "云瑾", "云先生", "云锦", "神女劈观", "岩北斗", "五更琉璃"], - 10000065: [ - "久岐忍", - "kuki", - "kuki shinobu", - "shinobu", - "97忍", - "小忍", - "久歧忍", - "97", - "茄忍", - "阿忍", - "忍姐", - "鬼之副手", - "不是忍者的忍者", - "医疗忍者", - "考证专家", - ], - 10000066: [ - "神里绫人", - "ayato", - "kamisato ayato", - "绫人", - "神里凌人", - "凌人", - "0人", - "神人", - "零人", - "大舅哥", - "神里绫华的兄长", - "荒泷一斗的虫友", - "奥托", - "奥托·阿波卡利斯", - "奥托主教", - "藏镜仕男", - "袖藏奶茶真君", - "真正的甘雨", - "可莉的爷爷", - ], - 10000067: [ - "柯莱", - "collei", - "柯来", - "科莱", - "科来", - "小天使", - "须弥安柏", - "须弥飞行冠军", - "见习巡林员", - "克莱", - "草安伯", - "道成林见习巡林员", - "提纳里的学徒", - "安柏的挚友", - "兰那罗奶奶", - ], - 10000068: ["多莉", "dori", "多利", "多力", "多丽", "奸商", "须弥百货商人", "歌玛哈巴依老爷", "艾尔卡萨扎莱宫之主"], - 10000069: [ - "提纳里", - "tighnari", - "小提", - "提那里", - "缇娜里", - "提哪里", - "驴", - "柯莱老师", - "柯莱师傅", - "巡林官", - "提那里", - "耳朵很好摸", - "道成林巡林官", - "柯莱的师父", - ], - 10000070: ["妮露", "nilou", "尼露", "祖拜尔剧场之星", "红牛"], - 10000071: ["赛诺", "cyno", "赛洛", "大风纪官", "大风机关", "胡狼头大人", "夹击妹抖", "游戏王", "冷笑话爱好者", "牌佬", "沙漠死神", "胡狼"], - 10000072: ["坎蒂丝", "candace", "坎迪斯", "水北斗", "赤王后裔", "阿如村守护者"], - 10000073: [ - "纳西妲", - "nahida", - "buer", - "草王", - "草神", - "小吉祥草王", - "草萝莉", - "艹萝莉", - "羽毛球", - "布耶尔", - "纳西坦", - "摩诃善法大吉祥智慧主", - "智慧之神", - "草木之主", - "草神大人", - ], - 10000074: ["莱依拉", "layla", "拉一拉", "莱伊拉", "莫娜的同行", "西琳", "黑塔"], - 10000075: [ - "流浪者", - "wanderer", - "散兵", - "伞兵", - "伞兵一号", - "雷电国崩", - "国崩", - "卢本伟", - "雷电大炮", - "雷大炮", - "大炮", - "sb", - "斯卡拉姆齐", - "倾奇者", - "黑主", - "崩崩小圆帽", - "七叶寂照秘密主", - "七彩阳光秘密主", - "正机之神", - "伪神", - ], - 10000076: ["珐露珊", "faruzan", "法露珊", "珐妹", "初音", "初音未来", "miku", "发露姗", "发姐", "法姐", "百岁珊", "百岁山", "童姥", "知论派名宿"], - 10000077: ["瑶瑶", "yaoyao", "遥遥", "遥遥无期", "香菱师妹", "萝卜", "四星草奶"], - 10000078: ["艾尔海森", "alhaitham", "爱尔海森", "艾尔海参", "艾尔", "海森", "海参", "海神", "埃尔海森", "草刻晴", "书记官", "代理大贤者"], - 10000079: ["迪希雅", "dehya", "狮女", "狮子", "腕豪", "女拳"], - 10000080: ["米卡", "mika", "镜音连", "咪卡", "小米"], - 10000081: ["白术", "baizhuer"], - 10000082: ["卡维", "kaveh"], + 1001: ['三月七'], + 1002: ['丹恒'], + 1003: ['姬子'], + 1004: ['瓦尔特'], + 1005: ['卡芙卡'], + 1006: ['银狼'], + 1008: ['阿兰'], + 1009: ['艾丝妲'], + 1013: ['黑塔'], + 1101: ['布洛妮娅'], + 1102: ['希儿'], + 1103: ['希露瓦'], + 1104: ['杰帕德'], + 1105: ['娜塔莎'], + 1106: ['佩拉'], + 1107: ['克拉拉'], + 1108: ['桑博'], + 1109: ['虎克'], + 1201: ['青雀'], + 1202: ['停云'], + 1203: ['罗刹'], + 1204: ['景元'], + 1206: ['素裳'], + 1209: ['彦卿'], + 1211: ['白露'], + 8004: ['开拓者'], } -not_real_roles = [10000081, 10000082] -weapons = { - # 1.x - "决斗之枪": ["决斗枪", "决斗", "月卡枪"], - "螭骨剑": ["螭骨", "丈育剑", "离骨剑", "月卡大剑"], - "黑剑": ["月卡剑"], - "苍翠猎弓": ["绿弓", "月卡弓"], - "匣里日月": ["日月"], - "匣里灭辰": ["灭辰"], - "匣里龙吟": ["龙吟"], - "流月针": ["针"], - "流浪乐章": ["赌狗书", "赌狗乐章", "赌狗"], - "昭心": ["糟心"], - "讨龙英杰谭": ["讨龙"], - "神射手之誓": ["脚气弓", "神射手"], - "黑缨枪": ["史莱姆枪"], - "黑岩刺枪": ["黑岩枪"], - "黑岩战弓": ["黑岩弓"], - "天空之刃": ["天空剑"], - "天空之傲": ["天空大剑"], - "天空之脊": ["天空枪", "薄荷枪", "薄荷"], - "天空之卷": ["天空书", "厕纸"], - "天空之翼": ["天空弓"], - "四风原典": ["四风", "可莉专武"], - "阿莫斯之弓": ["阿莫斯", "ams", "痛苦弓", "甘雨专武"], - "狼的末路": ["狼末"], - "和璞鸢": ["鸟枪", "绿枪", "魈专武"], - "风鹰剑": ["风鹰"], - "试作斩岩": ["斩岩"], - "试作星镰": ["星镰"], - "试作金珀": ["金珀"], - "试作古华": ["古华"], - "试作澹月": ["澹月"], - "万国诸海图谱": ["万国", "万国诸海"], - "尘世之锁": ["尘世锁", "尘世", "盾书", "锁"], - "无工之剑": ["蜈蚣", "蜈蚣大剑", "无工大剑", "盾大剑", "无工"], - "贯虹之槊": ["贯虹", "岩枪", "盾枪", "钟离专武"], - "斫峰之刃": ["斫峰", "盾剑"], - "腐殖之剑": ["腐殖", "腐殖剑"], - "雪葬的星银": ["雪葬", "星银", "雪葬星银", "雪山大剑"], - "磐岩结绿": ["绿箭", "绿剑"], - "护摩之杖": ["护摩", "护摩枪", "护膜", "胡桃专武"], - "千岩长枪": ["千岩枪"], - "千岩古剑": ["千岩剑", "千岩大剑"], - "西风长枪": ["西风枪"], - "西风猎弓": ["西风弓"], - "西风秘典": ["西风书"], - "暗巷闪光": ["暗巷剑", "暗巷小剑", "暗巷"], - "暗巷猎手": ["暗巷弓"], - "暗巷的酒与诗": ["暗巷法器", "暗巷书"], - "风花之颂": ["风花弓"], - "终末嗟叹之诗": ["终末", "终末弓", "叹气弓", "乐团弓", "温迪专武"], - "松籁响起之时": ["松籁", "乐团大剑", "松剑", "优菈专武"], - "苍古自由之誓": ["苍古", "乐团剑", "枫原万叶专武"], - "幽夜华尔兹": ["幽夜", "幽夜弓", "华尔兹", "皇女弓"], - "嘟嘟可故事集": ["嘟嘟可"], - # 2.x - "天目影打刀": ["天目刀", "天目"], - "桂木斩长正": ["桂木", "斩长正"], - "喜多院十文字": ["喜多院", "十文字"], - "破魔之弓": ["破魔弓", "破魔"], - "白辰之环": ["白辰", "白辰环"], - "雾切之回光": ["雾切", "神里绫华专武"], - "飞雷之弦振": ["飞雷", "飞雷弓", "宵宫专武"], - "薙草之稻光": ["薙草", "稻光", "薙草稻光", "马尾枪", "马尾", "薙刀", "雷电将军专武"], - "不灭月华": ["月华", "珊瑚宫心海专武"], - "「渔获」": ["鱼叉", "渔叉", "渔获"], - "衔珠海皇": ["海皇", "咸鱼剑", "咸鱼大剑"], - "冬极白星": ["冬极", "达达利亚专武"], - "曚云之月": ["曚云弓", "曚云"], - "恶王丸": ["断浪大剑"], - "断浪长鳍": ["断浪", "断浪长枪", "断浪枪"], - "辰砂之纺锤": ["辰砂", "辰砂纺锤", "纺锤", "阿贝多专武"], - "赤角石溃杵": ["赤角", "石溃杵", "荒泷一斗专武", "巧乐兹"], - "息灾": ["申鹤专武"], - "神乐之真意": ["神乐", "真意", "八重神子专武"], - "证誓之明瞳": ["证誓", "明瞳", "证誓明瞳"], - "波乱月白经津": ["波乱", "月白", "波乱月白", "经津", "波波津", "神里绫人专武", "钵钵鸡"], - "若水": ["麒麟弓", "夜兰专武"], - "笼钓瓶一心": ["万叶刀", "一心传名刀", "妖刀"], - # 3.x - "猎人之径": ["草弓", "提纳里专武"], - "竭泽": ["鱼弓"], - "原木刀": ["须弥锻造单手剑"], - "森林王器": ["须弥锻造大剑", "原木大剑"], - "贯月矢": ["须弥锻造长枪", "原木枪"], - "盈满之实": ["须弥锻造法器"], - "王下近侍": ["须弥锻造弓", "原木弓"], - "赤沙之杖": ["赤沙", "赛诺专武", "船桨"], - "圣显之钥": ["圣显之钥", "圣显", "不灭剑华", "妮露专武", "板砖"], - "风信之锋": ["风信", "风信锋"], - "西福斯的月光": ["西福斯", "月光", "月光小剑", "月光剑"], - "玛海菈的水色": ["玛海菈", "水色"], - "流浪的晚星": ["晚星"], - "千夜浮梦": ["千夜", "神灯", "茶壶", "夜壶"], - "图莱杜拉的回忆": ["图莱杜拉", "铃铛", "流浪者专武"], - "东花坊时雨": ["东花坊", "时雨", "伞"], - "裁叶萃光": ["萃光", "韭菜刀", "裁叶", "菜叶"], - "饰铁之花": ["饰铁", "铁花"], - "苇海信标": ["苇海", "信标"], - "碧落之珑": ["碧落", "白术专武"], +not_real_roles = [] +light_cones = { + 20000: ['锋镝'], + 20001: ['物穰'], + 20002: ['天倾'], + 20003: ['琥珀'], + 20004: ['幽邃'], + 20005: ['齐颂'], + 20006: ['智库'], + 20007: ['离弦'], + 20008: ['嘉果'], + 20009: ['乐圮'], + 20010: ['戍御'], + 20011: ['渊环'], + 20012: ['轮契'], + 20013: ['灵钥'], + 20014: ['相抗'], + 20015: ['蕃息'], + 20016: ['俱殁'], + 20017: ['开疆'], + 20018: ['匿影'], + 20019: ['调和'], + 20020: ['睿见'], + 21000: ['一场术后对话'], + 21001: ['晚安与睡颜'], + 21002: ['余生的第一天'], + 21003: ['唯有沉默'], + 21004: ['记忆中的模样'], + 21005: ['鼹鼠党欢迎你'], + 21006: ['「我」的诞生'], + 21007: ['同一种心情'], + 21008: ['猎物的视线'], + 21009: ['朗道的选择'], + 21010: ['论剑'], + 21011: ['与行星相会'], + 21012: ['秘密誓心'], + 21013: ['别让世界静下来'], + 21014: ['此时恰好'], + 21015: ['决心如汗珠般闪耀'], + 21016: ['宇宙市场趋势'], + 21017: ['点个关注吧!'], + 21018: ['舞!舞!舞!'], + 21019: ['在蓝天下'], + 21020: ['天才们的休憩'], + 21021: ['等价交换'], + 21022: ['延长记号'], + 21023: ['我们是地火'], + 21024: ['春水初生'], + 21025: ['过往未来'], + 21026: ['汪!散步时间!'], + 21027: ['早餐的仪式感'], + 21028: ['暖夜不会漫长'], + 21029: ['后会有期'], + 21030: ['这就是我啦!'], + 21031: ['重返幽冥'], + 21032: ['镂月裁云之意'], + 21033: ['无处可逃'], + 21034: ['今日亦是和平的一日'], + 23000: ['银河铁道之夜'], + 23001: ['于夜色中'], + 23002: ['无可取代的东西'], + 23003: ['但战斗还未结束'], + 23004: ['以世界之名'], + 23005: ['制胜的瞬间'], + 23010: ['拂晓之前'], + 23012: ['如泥酣眠'], + 23013: ['时节不居'], + 24000: ['记一位星神的陨落'], + 24001: ['星海巡航'], + 24002: ['记忆的质料'] } @@ -720,16 +127,18 @@ def roleToId(name: str) -> int | None: # noinspection PyPep8Naming @functools.lru_cache() -def weaponToName(shortname: str) -> str: - """将武器昵称转为正式名""" - return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname) +def lightConeToName(shortname: str) -> str: + """将光锥昵称转为正式名""" + shortname = str.casefold(shortname) # 忽略大小写 + return next((value[0] for value in light_cones.values() for name in value if name == shortname), shortname) # noinspection PyPep8Naming @functools.lru_cache() -def weaponToId(name: str) -> int | None: - """获取武器ID""" - return next((int(key) for key, value in WEAPON_DATA.items() if weaponToName(name) in value["name"]), None) +def lightConeToId(name: str) -> int | None: + """获取光锥ID""" + name = str.casefold(name) + return next((key for key, value in light_cones.items() for n in value if n == name), None) # noinspection PyPep8Naming diff --git a/modules/gacha_log/const.py b/modules/gacha_log/const.py index 6783b8c..31ca98b 100644 --- a/modules/gacha_log/const.py +++ b/modules/gacha_log/const.py @@ -1,13 +1,11 @@ -from genshin.models import BannerType +from genshin.models import StarRailBannerType -PAIMONMOE_VERSION = 3 -UIGF_VERSION = "v2.2" +UIWF_VERSION = "v1.0" GACHA_TYPE_LIST = { - BannerType.NOVICE: "新手祈愿", - BannerType.PERMANENT: "常驻祈愿", - BannerType.WEAPON: "武器祈愿", - BannerType.CHARACTER1: "角色祈愿", - BannerType.CHARACTER2: "角色祈愿", + StarRailBannerType.NOVICE: "新手跃迁", + StarRailBannerType.PERMANENT: "常驻跃迁", + StarRailBannerType.CHARACTER: "角色跃迁", + StarRailBannerType.WEAPON: "光锥跃迁", } diff --git a/modules/gacha_log/log.py b/modules/gacha_log/log.py index 2bf7d93..b6bad2a 100644 --- a/modules/gacha_log/log.py +++ b/modules/gacha_log/log.py @@ -3,19 +3,16 @@ import contextlib import datetime import json from concurrent.futures import ThreadPoolExecutor -from os import PathLike from pathlib import Path -from typing import Dict, IO, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple import aiofiles from genshin import AuthkeyTimeout, Client, InvalidAuthkey -from genshin.models import BannerType -from openpyxl import load_workbook +from genshin.models import StarRailBannerType from core.dependence.assets import AssetsService from metadata.pool.pool import get_pool_by_id -from metadata.shortname import roleToId, weaponToId -from modules.gacha_log.const import GACHA_TYPE_LIST, PAIMONMOE_VERSION +from modules.gacha_log.const import GACHA_TYPE_LIST from modules.gacha_log.error import ( GachaLogAccountNotFound, GachaLogAuthkeyTimeout, @@ -24,7 +21,6 @@ from modules.gacha_log.error import ( GachaLogInvalidAuthkey, GachaLogMixedProvider, GachaLogNotFound, - PaimonMoeGachaLogFileError, ) from modules.gacha_log.models import ( FiveStarItem, @@ -32,16 +28,14 @@ from modules.gacha_log.models import ( GachaItem, GachaLogInfo, ImportType, - ItemType, Pool, - UIGFGachaType, - UIGFInfo, - UIGFItem, - UIGFModel, + UIWFInfo, + UIWFItem, + UIWFModel, ) from utils.const import PROJECT_ROOT -GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "gacha_log") +GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "warp_log") GACHA_LOG_PATH.mkdir(parents=True, exist_ok=True) @@ -64,11 +58,11 @@ class GachaLog: async def load_history_info( self, user_id: str, uid: str, only_status: bool = False ) -> Tuple[Optional[GachaLogInfo], bool]: - """读取历史抽卡记录数据 + """读取历史跃迁记录数据 :param user_id: 用户id :param uid: 原神uid :param only_status: 是否只读取状态 - :return: 抽卡记录数据 + :return: 跃迁记录数据 """ file_path = self.gacha_log_path / f"{user_id}-{uid}.json" if only_status: @@ -81,7 +75,7 @@ class GachaLog: return GachaLogInfo(user_id=user_id, uid=uid, update_time=datetime.datetime.now()), False async def remove_history_info(self, user_id: str, uid: str) -> bool: - """删除历史抽卡记录数据 + """删除历史跃迁记录数据 :param user_id: 用户id :param uid: 原神uid :return: 是否删除成功 @@ -102,10 +96,10 @@ class GachaLog: return False async def save_gacha_log_info(self, user_id: str, uid: str, info: GachaLogInfo): - """保存抽卡记录数据 + """保存跃迁记录数据 :param user_id: 用户id :param uid: 原神uid - :param info: 抽卡记录数据 + :param info: 跃迁记录数据 """ save_path = self.gacha_log_path / f"{user_id}-{uid}.json" save_path_bak = self.gacha_log_path / f"{user_id}-{uid}.json.bak" @@ -118,21 +112,21 @@ class GachaLog: # 写入数据 await self.save_json(save_path, info.json()) - async def gacha_log_to_uigf(self, user_id: str, uid: str) -> Optional[Path]: - """抽卡日记转换为 UIGF 格式 + async def gacha_log_to_uiwf(self, user_id: str, uid: str) -> Optional[Path]: + """跃迁日记转换为 UIWF 格式 :param user_id: 用户ID :param uid: 游戏UID - :return: 转换是否成功、转换信息、UIGF文件目录 + :return: 转换是否成功、转换信息、UIWF文件目录 """ data, state = await self.load_history_info(user_id, uid) if not state: raise GachaLogNotFound - save_path = self.gacha_log_path / f"{user_id}-{uid}-uigf.json" - info = UIGFModel(info=UIGFInfo(uid=uid, export_app=ImportType.PaiGram.value, export_app_version="v3"), list=[]) + save_path = self.gacha_log_path / f"{user_id}-{uid}-uiwf.json" + info = UIWFModel(info=UIWFInfo(uid=uid, export_app=ImportType.PaiGram.value, export_app_version="v3"), list=[]) for items in data.item_list.values(): for item in items: info.list.append( - UIGFItem( + UIWFItem( id=item.id, name=item.name, gacha_type=item.gacha_type, @@ -153,9 +147,9 @@ class GachaLog: four_star = len([i for i in data if i.rank_type == "4"]) if total > 50: if total <= five_star * 15: - raise GachaLogFileError("检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。") + raise GachaLogFileError("检测到您将要导入的跃迁记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。") if four_star < five_star: - raise GachaLogFileError("检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。") + raise GachaLogFileError("检测到您将要导入的跃迁记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。") return True except Exception as exc: # pylint: disable=W0703 raise GachaLogFileError from exc @@ -164,7 +158,7 @@ class GachaLog: def import_data_backend(all_items: List[GachaItem], gacha_log: GachaLogInfo, temp_id_data: Dict) -> int: new_num = 0 for item_info in all_items: - pool_name = GACHA_TYPE_LIST[BannerType(int(item_info.gacha_type))] + pool_name = GACHA_TYPE_LIST[StarRailBannerType(int(item_info.gacha_type))] if item_info.id not in temp_id_data[pool_name]: gacha_log.item_list[pool_name].append(item_info) temp_id_data[pool_name].append(item_info.id) @@ -187,11 +181,6 @@ class GachaLog: all_items = [GachaItem(**i) for i in data["list"]] await self.verify_data(all_items) gacha_log, status = await self.load_history_info(str(user_id), uid) - if import_type == ImportType.PAIMONMOE: - if status and gacha_log.get_import_type != ImportType.PAIMONMOE: - raise GachaLogMixedProvider - elif status and gacha_log.get_import_type == ImportType.PAIMONMOE: - raise GachaLogMixedProvider # 将唯一 id 放入临时数据中,加快查找速度 temp_id_data = { pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items() @@ -216,10 +205,11 @@ class GachaLog: except GachaLogMixedProvider as e: raise GachaLogMixedProvider from e except Exception as exc: + breakpoint() raise GachaLogException from exc async def get_gacha_log_data(self, user_id: int, client: Client, authkey: str) -> int: - """使用authkey获取抽卡记录数据,并合并旧数据 + """使用authkey获取跃迁记录数据,并合并旧数据 :param user_id: 用户id :param client: genshin client :param authkey: authkey @@ -227,8 +217,6 @@ class GachaLog: """ new_num = 0 gacha_log, _ = await self.load_history_info(str(user_id), str(client.uid)) - if gacha_log.get_import_type == ImportType.PAIMONMOE: - raise GachaLogMixedProvider # 将唯一 id 放入临时数据中,加快查找速度 temp_id_data = {pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items()} try: @@ -261,30 +249,20 @@ class GachaLog: for i in gacha_log.item_list.values(): i.sort(key=lambda x: (x.time, x.id)) gacha_log.update_time = datetime.datetime.now() - gacha_log.import_type = ImportType.UIGF.value + gacha_log.import_type = ImportType.UIWF.value await self.save_gacha_log_info(str(user_id), str(client.uid), gacha_log) return new_num @staticmethod def check_avatar_up(name: str, gacha_time: datetime.datetime) -> bool: - if name in {"莫娜", "七七", "迪卢克", "琴"}: + if name in {"姬子", "瓦尔特", "布洛妮娅", "杰帕德", "克拉拉", "彦卿", "白露"}: return False - if name == "刻晴": - start_time = datetime.datetime.strptime("2021-02-17 18:00:00", "%Y-%m-%d %H:%M:%S") - end_time = datetime.datetime.strptime("2021-03-02 15:59:59", "%Y-%m-%d %H:%M:%S") - if not start_time < gacha_time < end_time: - return False - elif name == "提纳里": - start_time = datetime.datetime.strptime("2022-08-24 06:00:00", "%Y-%m-%d %H:%M:%S") - end_time = datetime.datetime.strptime("2022-09-09 17:59:59", "%Y-%m-%d %H:%M:%S") - if not start_time < gacha_time < end_time: - return False return True - async def get_all_5_star_items(self, data: List[GachaItem], assets: AssetsService, pool_name: str = "角色祈愿"): + async def get_all_5_star_items(self, data: List[GachaItem], assets: AssetsService, pool_name: str = "角色跃迁"): """ 获取所有5星角色 - :param data: 抽卡记录 + :param data: 跃迁记录 :param assets: 资源服务 :param pool_name: 池子名称 :return: 5星角色列表 @@ -294,21 +272,27 @@ class GachaLog: for item in data: count += 1 if item.rank_type == "5": - if item.item_type == "角色" and pool_name in {"角色祈愿", "常驻祈愿"}: + if item.item_type == "角色" and pool_name in {"角色跃迁", "常驻跃迁", "新手跃迁"}: + if pool_name == "新手跃迁": + isUp, isBig = True, False + elif pool_name == "角色跃迁": + isUp, isBig = self.check_avatar_up(item.name, item.time), (not result[-1].isUp) if result else False + else: + isUp, isBig = False, False data = { "name": item.name, - "icon": (await assets.avatar(roleToId(item.name)).icon()).as_uri(), + "icon": assets.avatar.icon(item.name).as_uri(), "count": count, "type": "角色", - "isUp": self.check_avatar_up(item.name, item.time) if pool_name == "角色祈愿" else False, - "isBig": (not result[-1].isUp) if result and pool_name == "角色祈愿" else False, + "isUp": isUp, + "isBig": isBig, "time": item.time, } result.append(FiveStarItem.construct(**data)) - elif item.item_type == "武器" and pool_name in {"武器祈愿", "常驻祈愿"}: + elif item.item_type == "光锥" and pool_name in {"光锥跃迁", "常驻跃迁"}: data = { "name": item.name, - "icon": (await assets.weapon(weaponToId(item.name)).icon()).as_uri(), + "icon": assets.light_cone.icon(item.name).as_uri(), "count": count, "type": "武器", "isUp": False, @@ -324,7 +308,7 @@ class GachaLog: async def get_all_4_star_items(data: List[GachaItem], assets: AssetsService): """ 获取 no_fout_star - :param data: 抽卡记录 + :param data: 跃迁记录 :param assets: 资源服务 :return: no_fout_star """ @@ -336,18 +320,18 @@ class GachaLog: if item.item_type == "角色": data = { "name": item.name, - "icon": (await assets.avatar(roleToId(item.name)).icon()).as_uri(), + "icon": assets.avatar.icon(item.name).as_uri(), "count": count, "type": "角色", "time": item.time, } result.append(FourStarItem.construct(**data)) - elif item.item_type == "武器": + elif item.item_type == "光锥": data = { "name": item.name, - "icon": (await assets.weapon(weaponToId(item.name)).icon()).as_uri(), + "icon": assets.light_cone.icon(item.name).as_uri(), "count": count, - "type": "武器", + "type": "光锥", "time": item.time, } result.append(FourStarItem.construct(**data)) @@ -387,7 +371,7 @@ class GachaLog: {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, {"num": five_star_const, "unit": "个", "lable": "五星常驻"}, {"num": up_avg, "unit": "抽", "lable": "UP平均"}, - {"num": up_cost, "unit": "", "lable": "UP花费原石"}, + {"num": up_cost, "unit": "", "lable": "UP花费星琼"}, ], ] @@ -400,7 +384,7 @@ class GachaLog: # 五星平均 five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0 # 五星武器 - five_star_weapon = len([i for i in all_five if i.type == "武器"]) + five_star_weapon = len([i for i in all_five if i.type == "光锥"]) # 总共四星 four_star = len(all_four) # 四星平均 @@ -414,7 +398,7 @@ class GachaLog: {"num": no_five_star, "unit": "抽", "lable": "未出五星"}, {"num": five_star, "unit": "个", "lable": "五星"}, {"num": five_star_avg, "unit": "抽", "lable": "五星平均"}, - {"num": five_star_weapon, "unit": "个", "lable": "五星武器"}, + {"num": five_star_weapon, "unit": "个", "lable": "五星光锥"}, ], [ {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, @@ -433,7 +417,7 @@ class GachaLog: # 五星平均 five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0 # 四星武器 - four_star_weapon = len([i for i in all_four if i.type == "武器"]) + four_star_weapon = len([i for i in all_four if i.type == "光锥"]) # 总共四星 four_star = len(all_four) # 四星平均 @@ -447,7 +431,7 @@ class GachaLog: {"num": no_five_star, "unit": "抽", "lable": "未出五星"}, {"num": five_star, "unit": "个", "lable": "五星"}, {"num": five_star_avg, "unit": "抽", "lable": "五星平均"}, - {"num": four_star_weapon, "unit": "个", "lable": "四星武器"}, + {"num": four_star_weapon, "unit": "个", "lable": "四星光锥"}, ], [ {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, @@ -460,7 +444,7 @@ class GachaLog: @staticmethod def count_fortune(pool_name: str, summon_data, weapon: bool = False): """ - 角色 武器 + 角色 光锥 欧 50以下 45以下 吉 50-60 45-55 中 60-70 55-65 @@ -482,9 +466,9 @@ class GachaLog: return f"{pool_name} · 非" return pool_name - async def get_analysis(self, user_id: int, client: Client, pool: BannerType, assets: AssetsService): + async def get_analysis(self, user_id: int, client: Client, pool: StarRailBannerType, assets: AssetsService): """ - 获取抽卡记录分析数据 + 获取跃迁记录分析数据 :param user_id: 用户id :param client: genshin client :param pool: 池子类型 @@ -502,13 +486,13 @@ class GachaLog: all_five, no_five_star = await self.get_all_5_star_items(data, assets, pool_name) all_four, no_four_star = await self.get_all_4_star_items(data, assets) summon_data = None - if pool == BannerType.CHARACTER1: + if pool in [StarRailBannerType.CHARACTER, StarRailBannerType.NOVICE]: summon_data = self.get_301_pool_data(total, all_five, no_five_star, no_four_star) pool_name = self.count_fortune(pool_name, summon_data) - elif pool == BannerType.WEAPON: + elif pool == StarRailBannerType.WEAPON: summon_data = self.get_302_pool_data(total, all_five, all_four, no_five_star, no_four_star) pool_name = self.count_fortune(pool_name, summon_data, True) - elif pool == BannerType.PERMANENT: + elif pool == StarRailBannerType.PERMANENT: summon_data = self.get_200_pool_data(total, all_five, all_four, no_five_star, no_four_star) pool_name = self.count_fortune(pool_name, summon_data) last_time = data[0].time.strftime("%Y-%m-%d %H:%M") @@ -526,9 +510,9 @@ class GachaLog: } async def get_pool_analysis( - self, user_id: int, client: Client, pool: BannerType, assets: AssetsService, group: bool + self, user_id: int, client: Client, pool: StarRailBannerType, assets: AssetsService, group: bool ) -> dict: - """获取抽卡记录分析数据 + """获取跃迁记录分析数据 :param user_id: 用户id :param client: genshin client :param pool: 池子类型 @@ -573,7 +557,7 @@ class GachaLog: } async def get_all_five_analysis(self, user_id: int, client: Client, assets: AssetsService) -> dict: - """获取五星抽卡记录分析数据 + """获取五星跃迁记录分析数据 :param user_id: 用户id :param client: genshin client :param assets: 资源服务 @@ -612,128 +596,3 @@ class GachaLog: "pool": pool_data, "hasMore": False, } - - @staticmethod - def convert_xlsx_to_uigf(file: Union[str, PathLike, IO[bytes]], zh_dict: Dict) -> Dict: - """转换 paimone.moe 或 非小酋 导出 xlsx 数据为 UIGF 格式 - :param file: 导出的 xlsx 文件 - :param zh_dict: - :return: UIGF 格式数据 - """ - - def from_paimon_moe( - uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int - ) -> UIGFItem: - item_type = ItemType.CHARACTER if item_type == "Character" else ItemType.WEAPON - return UIGFItem( - id=str(_id), - name=zh_dict[name], - gacha_type=uigf_gacha_type, - item_type=item_type, - rank_type=str(rank_type), - time=date_string, - uigf_gacha_type=uigf_gacha_type, - ) - - def from_uigf( - uigf_gacha_type: str, - gacha__type: str, - item_type: str, - name: str, - date_string: str, - rank_type: str, - _id: str, - ) -> UIGFItem: - return UIGFItem( - id=_id, - name=name, - gacha_type=gacha__type, - item_type=item_type, - rank_type=rank_type, - time=date_string, - uigf_gacha_type=uigf_gacha_type, - ) - - def from_fxq( - uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int - ) -> UIGFItem: - item_type = ItemType.CHARACTER if item_type == "角色" else ItemType.WEAPON - return UIGFItem( - id=str(_id), - name=name, - gacha_type=uigf_gacha_type, - item_type=item_type, - rank_type=str(rank_type), - time=date_string, - uigf_gacha_type=uigf_gacha_type, - ) - - wb = load_workbook(file) - wb_len = len(wb.worksheets) - - if wb_len == 6: - import_type = ImportType.PAIMONMOE - elif wb_len == 5: - import_type = ImportType.UIGF - elif wb_len == 4: - import_type = ImportType.FXQ - else: - raise GachaLogFileError("xlsx 格式错误") - - paimonmoe_sheets = { - UIGFGachaType.BEGINNER: "Beginners' Wish", - UIGFGachaType.STANDARD: "Standard", - UIGFGachaType.CHARACTER: "Character Event", - UIGFGachaType.WEAPON: "Weapon Event", - } - fxq_sheets = { - UIGFGachaType.BEGINNER: "新手祈愿", - UIGFGachaType.STANDARD: "常驻祈愿", - UIGFGachaType.CHARACTER: "角色活动祈愿", - UIGFGachaType.WEAPON: "武器活动祈愿", - } - data = UIGFModel(info=UIGFInfo(export_app=import_type.value), list=[]) - if import_type == ImportType.PAIMONMOE: - ws = wb["Information"] - if ws["B2"].value != PAIMONMOE_VERSION: - raise PaimonMoeGachaLogFileError(file_version=ws["B2"].value, support_version=PAIMONMOE_VERSION) - count = 1 - for gacha_type in paimonmoe_sheets: - ws = wb[paimonmoe_sheets[gacha_type]] - for row in ws.iter_rows(min_row=2, values_only=True): - if row[0] is None: - break - data.list.append(from_paimon_moe(gacha_type, row[0], row[1], row[2], row[3], count)) - count += 1 - elif import_type == ImportType.UIGF: - ws = wb["原始数据"] - type_map = {} - count = 0 - for row in ws["1"]: - if row.value is None: - break - type_map[row.value] = count - count += 1 - for row in ws.iter_rows(min_row=2, values_only=True): - if row[0] is None: - break - data.list.append( - from_uigf( - row[type_map["uigf_gacha_type"]], - row[type_map["gacha_type"]], - row[type_map["item_type"]], - row[type_map["name"]], - row[type_map["time"]], - row[type_map["rank_type"]], - row[type_map["id"]], - ) - ) - else: - for gacha_type in fxq_sheets: - ws = wb[fxq_sheets[gacha_type]] - for row in ws.iter_rows(min_row=2, values_only=True): - if row[0] is None: - break - data.list.append(from_fxq(gacha_type, row[2], row[1], row[0], row[3], row[6])) - - return json.loads(data.json()) diff --git a/modules/gacha_log/models.py b/modules/gacha_log/models.py index 1bf6dae..9582a5d 100644 --- a/modules/gacha_log/models.py +++ b/modules/gacha_log/models.py @@ -4,15 +4,13 @@ from typing import Any, Dict, List, Union from pydantic import BaseModel, validator -from metadata.shortname import not_real_roles, roleToId, weaponToId -from modules.gacha_log.const import UIGF_VERSION +from metadata.shortname import not_real_roles, roleToId, lightConeToId +from modules.gacha_log.const import UIWF_VERSION class ImportType(Enum): PaiGram = "PaiGram" - PAIMONMOE = "PAIMONMOE" - FXQ = "FXQ" - UIGF = "UIGF" + UIWF = "UIWF" UNKNOWN = "UNKNOWN" @@ -44,20 +42,20 @@ class GachaItem(BaseModel): @validator("name") def name_validator(cls, v): - if item_id := (roleToId(v) or weaponToId(v)): + if item_id := (roleToId(v) or lightConeToId(v)): if item_id not in not_real_roles: return v raise ValueError("Invalid name") @validator("gacha_type") def check_gacha_type(cls, v): - if v not in {"100", "200", "301", "302", "400"}: - raise ValueError("gacha_type must be 200, 301, 302 or 400") + if v not in {"1", "2", "11", "12"}: + raise ValueError("gacha_type must be 1, 2, 11 or 12") return v @validator("item_type") def check_item_type(cls, item): - if item not in {"角色", "武器"}: + if item not in {"角色", "光锥"}: raise ValueError("error item type") return item @@ -74,10 +72,10 @@ class GachaLogInfo(BaseModel): update_time: datetime.datetime import_type: str = "" item_list: Dict[str, List[GachaItem]] = { - "角色祈愿": [], - "武器祈愿": [], - "常驻祈愿": [], - "新手祈愿": [], + "角色跃迁": [], + "光锥跃迁": [], + "常驻跃迁": [], + "新手跃迁": [], } @property @@ -131,37 +129,36 @@ class Pool: class ItemType(Enum): CHARACTER = "角色" - WEAPON = "武器" + LIGHTCONE = "光锥" -class UIGFGachaType(Enum): - BEGINNER = "100" - STANDARD = "200" - CHARACTER = "301" - WEAPON = "302" - CHARACTER2 = "400" +class UIWFGachaType(Enum): + BEGINNER = "2" + STANDARD = "1" + CHARACTER = "11" + LIGHTCONE = "12" -class UIGFItem(BaseModel): +class UIWFItem(BaseModel): id: str name: str count: str = "1" - gacha_type: UIGFGachaType + gacha_type: UIWFGachaType item_id: str = "" item_type: ItemType rank_type: str time: str - uigf_gacha_type: UIGFGachaType + uigf_gacha_type: UIWFGachaType -class UIGFInfo(BaseModel): +class UIWFInfo(BaseModel): uid: str = "0" lang: str = "zh-cn" export_time: str = "" export_timestamp: int = 0 export_app: str = "" export_app_version: str = "" - uigf_version: str = UIGF_VERSION + uigf_version: str = UIWF_VERSION def __init__(self, **data: Any): super().__init__(**data) @@ -170,6 +167,6 @@ class UIGFInfo(BaseModel): self.export_timestamp = int(datetime.datetime.now().timestamp()) -class UIGFModel(BaseModel): - info: UIGFInfo - list: List[UIGFItem] +class UIWFModel(BaseModel): + info: UIWFInfo + list: List[UIWFItem] diff --git a/modules/wiki/models/avatar.py b/modules/wiki/models/avatar.py index 51759fa..a46942c 100644 --- a/modules/wiki/models/avatar.py +++ b/modules/wiki/models/avatar.py @@ -44,8 +44,6 @@ class Avatar(BaseModel): """角色ID""" name: str """名称""" - icon: str - """图标""" quality: Quality """品质""" destiny: Destiny diff --git a/modules/wiki/models/avatar_config.py b/modules/wiki/models/avatar_config.py new file mode 100644 index 0000000..77852b1 --- /dev/null +++ b/modules/wiki/models/avatar_config.py @@ -0,0 +1,36 @@ +from typing import List + +from pydantic import BaseModel + + +class AvatarName(BaseModel): + Hash: int + + +class AvatarConfig(BaseModel): + name: str = "" + AvatarID: int + AvatarName: AvatarName + AvatarVOTag: str + Release: bool + + +class AvatarIcon(BaseModel): + id: int + """角色ID""" + name: str + """名称""" + icon: List[str] + """图标(从小到大)""" + + @property + def gacha(self) -> str: + return self.icon[2] + + @property + def icon_(self) -> str: + return self.icon[0] + + @property + def normal(self) -> str: + return self.icon[1] diff --git a/modules/wiki/models/light_cone_config.py b/modules/wiki/models/light_cone_config.py new file mode 100644 index 0000000..88fb96f --- /dev/null +++ b/modules/wiki/models/light_cone_config.py @@ -0,0 +1,20 @@ +from typing import List + +from pydantic import BaseModel + + +class LightConeIcon(BaseModel): + id: int + """光锥ID""" + name: str + """名称""" + icon: List[str] + """图标(从小到大)""" + + @property + def gacha(self) -> str: + return self.icon[1] + + @property + def icon_(self) -> str: + return self.icon[0] diff --git a/modules/wiki/models/monster.py b/modules/wiki/models/monster.py index ccc20d6..474f3c7 100644 --- a/modules/wiki/models/monster.py +++ b/modules/wiki/models/monster.py @@ -23,4 +23,3 @@ class Monster(BaseModel): """抗性""" find_area: str """发现地点""" - diff --git a/modules/wiki/models/wiki.py b/modules/wiki/models/wiki.py new file mode 100644 index 0000000..d096596 --- /dev/null +++ b/modules/wiki/models/wiki.py @@ -0,0 +1,34 @@ +from typing import List, Dict + +import ujson +from pydantic import BaseModel + + +class Content(BaseModel): + content_id: int + """内容ID""" + ext: str + """扩展信息""" + icon: str + """图标""" + summary: str + """摘要""" + title: str + """标题""" + article_user_name: str = "" + """作者""" + bbs_url: str = "" + """BBS对应地址""" + + @property + def data(self) -> Dict: + return ujson.loads(self.ext) + + +class Children(BaseModel): + id: int + """分类ID""" + name: str + """分类名称""" + list: List[Content] + """内容列表""" diff --git a/plugins/starrail/wish_log.py b/plugins/starrail/wish_log.py new file mode 100644 index 0000000..fec9211 --- /dev/null +++ b/plugins/starrail/wish_log.py @@ -0,0 +1,395 @@ +from io import BytesIO + +from genshin.models import StarRailBannerType +from telegram import Document, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User +from telegram.constants import ChatAction +from telegram.ext import CallbackContext, CommandHandler, ConversationHandler, MessageHandler, filters +from telegram.helpers import create_deep_linked_url + +from core.dependence.assets import AssetsService +from core.plugin import Plugin, conversation, handler +from core.services.cookies import CookiesService +from core.services.players import PlayersService +from core.services.template.models import FileType +from core.services.template.services import TemplateService +from modules.gacha_log.error import ( + GachaLogAccountNotFound, + GachaLogAuthkeyTimeout, + GachaLogFileError, + GachaLogInvalidAuthkey, + GachaLogMixedProvider, + GachaLogNotFound, +) +from modules.gacha_log.helpers import from_url_get_authkey +from modules.gacha_log.log import GachaLog +from plugins.tools.genshin import PlayerNotFoundError, GenshinHelper +from utils.log import logger + +try: + import ujson as jsonlib + +except ImportError: + import json as jsonlib + +INPUT_URL, INPUT_FILE, CONFIRM_DELETE = range(10100, 10103) + + +class WishLogPlugin(Plugin.Conversation): + """跃迁记录导入/导出/分析""" + + def __init__( + self, + template_service: TemplateService, + players_service: PlayersService, + assets: AssetsService, + cookie_service: CookiesService, + helper: GenshinHelper, + ): + self.template_service = template_service + self.players_service = players_service + self.assets_service = assets + self.cookie_service = cookie_service + self.gacha_log = GachaLog() + self.helper = helper + + async def _refresh_user_data( + self, user: User, data: dict = None, authkey: str = None, verify_uid: bool = True + ) -> str: + """刷新用户数据 + :param user: 用户 + :param data: 数据 + :param authkey: 认证密钥 + :return: 返回信息 + """ + try: + logger.debug("尝试获取已绑定的星穹铁道账号") + client = await self.helper.get_genshin_client(user.id, need_cookie=False) + if authkey: + new_num = await self.gacha_log.get_gacha_log_data(user.id, client, authkey) + return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条跃迁记录" + if data: + new_num = await self.gacha_log.import_gacha_log_data(user.id, client, data, verify_uid) + return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条跃迁记录" + except GachaLogNotFound: + return "彦卿没有找到你的跃迁记录,快来私聊彦卿导入吧~" + except GachaLogAccountNotFound: + return "导入失败,可能文件包含的跃迁记录所属 uid 与你当前绑定的 uid 不同" + except GachaLogFileError: + return "导入失败,数据格式错误" + except GachaLogInvalidAuthkey: + return "更新数据失败,authkey 无效" + except GachaLogAuthkeyTimeout: + return "更新数据失败,authkey 已经过期" + except GachaLogMixedProvider: + return "导入失败,你已经通过其他方式导入过跃迁记录了,本次无法导入" + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + return "彦卿没有找到您所绑定的账号信息,请先私聊彦卿绑定账号" + + async def import_from_file(self, user: User, message: Message, document: Document = None) -> None: + if not document: + document = message.document + # TODO: 使用 mimetype 判断文件类型 + if document.file_name.endswith(".json"): + file_type = "json" + else: + await message.reply_text("文件格式错误,请发送符合 UIWF 标准的跃迁记录文件") + return + if document.file_size > 5 * 1024 * 1024: + await message.reply_text("文件过大,请发送小于 5 MB 的文件") + return + try: + out = BytesIO() + await (await document.get_file()).download_to_memory(out=out) + if file_type == "json": + # bytesio to json + data = jsonlib.loads(out.getvalue().decode("utf-8")) + else: + await message.reply_text("文件解析失败,请检查文件") + return + except GachaLogFileError: + await message.reply_text("文件解析失败,请检查文件是否符合 UIWF 标准") + return + except (KeyError, IndexError, ValueError): + await message.reply_text("文件解析失败,请检查文件编码是否正确或符合 UIWF 标准") + return + except Exception as exc: + logger.error("文件解析失败 %s", repr(exc)) + await message.reply_text("文件解析失败,请检查文件是否符合 UIWF 标准") + return + await message.reply_chat_action(ChatAction.TYPING) + reply = await message.reply_text("文件解析成功,正在导入数据") + await message.reply_chat_action(ChatAction.TYPING) + try: + text = await self._refresh_user_data(user, data=data, verify_uid=file_type == "json") + except Exception as exc: # pylint: disable=W0703 + logger.error("文件解析失败 %s", repr(exc)) + text = "文件解析失败,请检查文件是否符合 UIWF 标准" + await reply.edit_text(text) + + @conversation.entry_point + @handler.command(command="warp_log_import", filters=filters.ChatType.PRIVATE, block=False) + @handler.message(filters=filters.Regex("^导入跃迁记录(.*)") & filters.ChatType.PRIVATE, block=False) + async def command_start(self, update: Update, context: CallbackContext) -> int: + message = update.effective_message + user = update.effective_user + args = self.get_args(context) + logger.info("用户 %s[%s] 导入跃迁记录命令请求", user.full_name, user.id) + authkey = from_url_get_authkey(args[0] if args else "") + # if not args: + # player_info = await self.players_service.get_player(user.id, region=RegionEnum.HYPERION) + # if player_info is not None: + # cookies = await self.cookie_service.get(user.id, account_id=player_info.account_id) + # if cookies is not None and cookies.data and "stoken" in cookies.data: + # if stuid := next( + # (value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None + # ): + # cookies.data["stuid"] = stuid + # client = genshin.Client( + # cookies=cookies.data, + # game=genshin.types.Game.STARRAIL, + # region=genshin.Region.CHINESE, + # lang="zh-cn", + # uid=player_info.player_id, + # ) + # authkey = await get_authkey_by_stoken(client) + if not authkey: + await message.reply_text( + "开始导入跃迁历史记录:请通过 https://starrailstation.com/cn/warp#import 获取跃迁记录链接后发送给我" + "(非 starrailstation.com 导出的文件数据)\n\n" + "> 你还可以向彦卿发送从其他工具导出的 UIWF 标准的记录文件\n" + # "> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n" + "注意:导入的数据将会与旧数据进行合并。", + parse_mode="html", + ) + return INPUT_URL + text = "小彦卿正在从服务器获取数据,请稍后" + if not args: + text += "\n\n> 由于你绑定的 Cookie 中存在 stoken ,本次通过 stoken 自动刷新数据" + reply = await message.reply_text(text) + await message.reply_chat_action(ChatAction.TYPING) + data = await self._refresh_user_data(user, authkey=authkey) + await reply.edit_text(data) + return ConversationHandler.END + + @conversation.state(state=INPUT_URL) + @handler.message(filters=~filters.COMMAND, block=False) + async def import_data_from_message(self, update: Update, _: CallbackContext) -> int: + message = update.effective_message + user = update.effective_user + if message.document: + await self.import_from_file(user, message) + return ConversationHandler.END + if not message.text: + await message.reply_text("请发送文件或链接") + return INPUT_URL + authkey = from_url_get_authkey(message.text) + reply = await message.reply_text("小彦卿正在从服务器获取数据,请稍后") + await message.reply_chat_action(ChatAction.TYPING) + text = await self._refresh_user_data(user, authkey=authkey) + await reply.edit_text(text) + return ConversationHandler.END + + @conversation.entry_point + @handler.command(command="warp_log_delete", filters=filters.ChatType.PRIVATE, block=False) + @handler.message(filters=filters.Regex("^删除跃迁记录(.*)") & filters.ChatType.PRIVATE, block=False) + async def command_start_delete(self, update: Update, context: CallbackContext) -> int: + message = update.effective_message + user = update.effective_user + logger.info("用户 %s[%s] 删除跃迁记录命令请求", user.full_name, user.id) + try: + client = await self.helper.get_genshin_client(user.id, need_cookie=False) + context.chat_data["uid"] = client.uid + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号") + return ConversationHandler.END + _, status = await self.gacha_log.load_history_info(str(user.id), str(client.uid), only_status=True) + if not status: + await message.reply_text("你还没有导入跃迁记录哦~") + return ConversationHandler.END + await message.reply_text("你确定要删除跃迁记录吗?(此项操作无法恢复),如果确定请发送 ”确定“,发送其他内容取消") + return CONFIRM_DELETE + + @conversation.state(state=CONFIRM_DELETE) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) + async def command_confirm_delete(self, update: Update, context: CallbackContext) -> int: + message = update.effective_message + user = update.effective_user + if message.text == "确定": + status = await self.gacha_log.remove_history_info(str(user.id), str(context.chat_data["uid"])) + await message.reply_text("跃迁记录已删除" if status else "跃迁记录删除失败") + return ConversationHandler.END + await message.reply_text("已取消") + return ConversationHandler.END + + @handler(CommandHandler, command="warp_log_force_delete", block=False, admin=True) + async def command_warp_log_force_delete(self, update: Update, context: CallbackContext): + message = update.effective_message + args = self.get_args(context) + if not args: + await message.reply_text("请指定用户ID") + return + try: + cid = int(args[0]) + if cid < 0: + raise ValueError("Invalid cid") + client = await self.helper.get_genshin_client(cid, need_cookie=False) + _, status = await self.gacha_log.load_history_info(str(cid), str(client.uid), only_status=True) + if not status: + await message.reply_text("该用户还没有导入跃迁记录") + return + status = await self.gacha_log.remove_history_info(str(cid), str(client.uid)) + await message.reply_text("跃迁记录已强制删除" if status else "跃迁记录删除失败") + except GachaLogNotFound: + await message.reply_text("该用户还没有导入跃迁记录") + except PlayerNotFoundError: + await message.reply_text("该用户暂未绑定账号") + except (ValueError, IndexError): + await message.reply_text("用户ID 不合法") + + @handler(CommandHandler, command="warp_log_export", filters=filters.ChatType.PRIVATE, block=False) + @handler(MessageHandler, filters=filters.Regex("^导出跃迁记录(.*)") & filters.ChatType.PRIVATE, block=False) + async def command_start_export(self, update: Update, context: CallbackContext) -> None: + message = update.effective_message + user = update.effective_user + logger.info("用户 %s[%s] 导出跃迁记录命令请求", user.full_name, user.id) + try: + client = await self.helper.get_genshin_client(user.id, need_cookie=False) + await message.reply_chat_action(ChatAction.TYPING) + path = await self.gacha_log.gacha_log_to_uiwf(str(user.id), str(client.uid)) + await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT) + await message.reply_document(document=open(path, "rb+"), caption="跃迁记录导出文件 - UIWF V1.0") + except GachaLogNotFound: + logger.info("未找到用户 %s[%s] 的跃迁记录", user.full_name, user.id) + buttons = [ + [InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "warp_log_import"))] + ] + await message.reply_text("彦卿没有找到你的跃迁记录,快来私聊彦卿导入吧~", reply_markup=InlineKeyboardMarkup(buttons)) + except GachaLogAccountNotFound: + await message.reply_text("导入失败,可能文件包含的跃迁记录所属 uid 与你当前绑定的 uid 不同") + except GachaLogFileError: + await message.reply_text("导入失败,数据格式错误") + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号") + + @handler(CommandHandler, command="warp_log", block=False) + @handler(MessageHandler, filters=filters.Regex("^跃迁记录?(光锥|角色|常驻|新手)$"), block=False) + async def command_start_analysis(self, update: Update, context: CallbackContext) -> None: + message = update.effective_message + user = update.effective_user + pool_type = StarRailBannerType.CHARACTER + if args := self.get_args(context): + if "光锥" in args: + pool_type = StarRailBannerType.WEAPON + elif "常驻" in args: + pool_type = StarRailBannerType.STANDARD + elif "新手" in args: + pool_type = StarRailBannerType.NOVICE + logger.info("用户 %s[%s] 跃迁记录命令请求 || 参数 %s", user.full_name, user.id, pool_type.name) + try: + client = await self.helper.get_genshin_client(user.id, need_cookie=False) + await message.reply_chat_action(ChatAction.TYPING) + data = await self.gacha_log.get_analysis(user.id, client, pool_type, self.assets_service) + if isinstance(data, str): + reply_message = await message.reply_text(data) + if filters.ChatType.GROUPS.filter(message): + self.add_delete_message_job(reply_message, delay=300) + self.add_delete_message_job(message, delay=300) + else: + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + png_data = await self.template_service.render( + "starrail/gacha_log/gacha_log.html", + data, + full_page=True, + file_type=FileType.DOCUMENT if len(data.get("fiveLog")) > 36 else FileType.PHOTO, + query_selector=".body_box", + ) + if png_data.file_type == FileType.DOCUMENT: + await png_data.reply_document(message, filename="跃迁记录.png") + else: + await png_data.reply_photo(message) + except GachaLogNotFound: + logger.info("未找到用户 %s[%s] 的跃迁记录", user.full_name, user.id) + buttons = [ + [InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "warp_log_import"))] + ] + await message.reply_text("彦卿没有找到你此卡池的跃迁记录,快来点击按钮私聊彦卿导入吧~", reply_markup=InlineKeyboardMarkup(buttons)) + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]] + if filters.ChatType.GROUPS.filter(message): + reply_message = await message.reply_text( + "未查询到您所绑定的账号信息,请先私聊彦卿绑定账号", reply_markup=InlineKeyboardMarkup(buttons) + ) + self.add_delete_message_job(reply_message, delay=30) + self.add_delete_message_job(message, delay=30) + else: + await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) + + @handler(CommandHandler, command="warp_count", block=False) + @handler(MessageHandler, filters=filters.Regex("^跃迁统计?(光锥|角色|常驻|新手)$"), block=False) + async def command_start_count(self, update: Update, context: CallbackContext) -> None: + message = update.effective_message + user = update.effective_user + pool_type = StarRailBannerType.CHARACTER + all_five = False + if args := self.get_args(context): + if "光锥" in args: + pool_type = StarRailBannerType.WEAPON + elif "常驻" in args: + pool_type = StarRailBannerType.STANDARD + elif "新手" in args: + pool_type = StarRailBannerType.NOVICE + if "仅五星" in args: + all_five = True + logger.info("用户 %s[%s] 跃迁统计命令请求 || 参数 %s || 仅五星 %s", user.full_name, user.id, pool_type.name, all_five) + try: + client = await self.helper.get_genshin_client(user.id, need_cookie=False) + group = filters.ChatType.GROUPS.filter(message) + await message.reply_chat_action(ChatAction.TYPING) + if all_five: + data = await self.gacha_log.get_all_five_analysis(user.id, client, self.assets_service) + else: + data = await self.gacha_log.get_pool_analysis(user.id, client, pool_type, self.assets_service, group) + if isinstance(data, str): + reply_message = await message.reply_text(data) + if filters.ChatType.GROUPS.filter(message): + self.add_delete_message_job(reply_message) + self.add_delete_message_job(message) + else: + document = False + if data["hasMore"] and not group: + document = True + data["hasMore"] = False + await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if document else ChatAction.UPLOAD_PHOTO) + png_data = await self.template_service.render( + "starrail/gacha_count/gacha_count.html", + data, + full_page=True, + query_selector=".body_box", + file_type=FileType.DOCUMENT if document else FileType.PHOTO, + ) + if document: + await png_data.reply_document(message, filename="跃迁统计.png") + else: + await png_data.reply_photo(message) + except GachaLogNotFound: + logger.info("未找到用户 %s[%s] 的跃迁记录", user.full_name, user.id) + buttons = [ + [InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "warp_log_import"))] + ] + await message.reply_text("彦卿没有找到你此卡池的跃迁记录,快来私聊彦卿导入吧~", reply_markup=InlineKeyboardMarkup(buttons)) + except PlayerNotFoundError: + logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) + buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]] + if filters.ChatType.GROUPS.filter(message): + reply_message = await message.reply_text( + "未查询到您所绑定的账号信息,请先私聊彦卿绑定账号", reply_markup=InlineKeyboardMarkup(buttons) + ) + self.add_delete_message_job(reply_message, delay=30) + + self.add_delete_message_job(message, delay=30) + else: + await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) diff --git a/resources/bot/help/help.html b/resources/bot/help/help.html index 4d80d6c..db55edf 100644 --- a/resources/bot/help/help.html +++ b/resources/bot/help/help.html @@ -60,14 +60,14 @@ - - - - - - - - + +
+
+ /dailynote + +
+
查询实时便笺
+
/ledger @@ -103,21 +103,21 @@ - - - - - - - - - - - - - - - + +
+
+ /warp_log + +
+
跃迁记录
+
+
+
+ /warp_count + +
+
跃迁统计
+
@@ -181,19 +181,19 @@ - - - - - - - - - - - - - + +
+
/warp_log_import
+
导入跃迁记录
+
+
+
/warp_log_export
+
导出跃迁记录
+
+
+
/warp_log_delete
+
删除跃迁记录
+
@@ -207,7 +207,7 @@ - + diff --git a/resources/starrail/gacha_count/example.html b/resources/starrail/gacha_count/example.html new file mode 100644 index 0000000..9fddf2c --- /dev/null +++ b/resources/starrail/gacha_count/example.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + +
+
+
ID: 10001
+

抽卡统计-角色祈愿

+ +
+
+
+
+

「枫原万叶、可莉」

+ 98抽 +
+ 2022-08-02 - 2022-08-02 +
+
+
+
+ 20 + +
+
+
+
*完整数据请私聊查看
+ +
+ + diff --git a/resources/starrail/gacha_count/gacha_count.css b/resources/starrail/gacha_count/gacha_count.css new file mode 100644 index 0000000..74cf8f2 --- /dev/null +++ b/resources/starrail/gacha_count/gacha_count.css @@ -0,0 +1,214 @@ +@font-face { + font-family: "tttgbnumber"; + src: url("./../../fonts/tttgbnumber.ttf"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "HYWenHei-55W"; + src: url("../../fonts/HYWenHei-85W.ttf"); + font-weight: normal; + font-style: normal; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + user-select: none; +} + +body { + font-size: 16px; + width: 530px; + color: #1E1F20; + transform: scale(1.5); + transform-origin: 0 0; +} + +.container { + width: 530px; + padding: 20px 15px 10px 15px; + background-color: #F5F6FB; +} + +.head_box { + border-radius: 15px; + font-family: tttgbnumber, serif; + padding: 10px 20px; + position: relative; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + +} + +.head_box .id_text { + font-size: 24px; +} + +.head_box .day_text { + font-size: 20px; +} + +.head_box .genshin_logo { + position: absolute; + top: 1px; + right: 15px; + width: 97px; +} + +.base_info { + position: relative; + padding-left: 10px; +} + +.uid { + font-family: tttgbnumber, serif; +} + +.pool_box { + font-family: HYWenHei-55W, serif; + border-radius: 12px; + margin-top: 20px; + margin-bottom: 20px; + padding: 10px 5px 5px 5px; + background: #FFF; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + position: relative; +} + +.title_box { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.title { + white-space: nowrap; + max-width: 210px; + overflow: hidden; +} + +.name_box { + display: flex; + align-items: center; + flex: 1; +} + +.title_box .date { + margin-right: 10px; +} + +.list_box { + display: flex; + flex-wrap: wrap; +} + +.item { + margin: 0 0 10px 10px; + border-radius: 7px; + overflow: hidden; + box-shadow: 0 2px 6px 0 rgb(132 93 90 / 30%); + height: 70px; + width: 70px; + background: #E9E5DC; + position: relative; +} + +.item .role_img { + width: 100%; + overflow: hidden; + background-size: 100%; + background-repeat: no-repeat; + position: absolute; + top: 0; + /* filter: contrast(95%); */ +} + +.item .num { + position: absolute; + top: 0; + right: 0; + z-index: 9; + font-size: 18px; + text-align: center; + color: #FFF; + border-radius: 3px; + padding: 1px 5px; + background: rgb(0 0 0 / 50%); + font-family: "tttgbnumber", serif; +} + +.label_301 { + background-color: rgb(235 106 75); +} + +.label_302 { + background-color: #E69449; +} + +.label_200 { + background-color: #757CC8; +} + +.label { + color: #FFF; + border-radius: 10px; + font-size: 16px; + padding: 2px 7px; + vertical-align: 2px; +} + +.bg5 { + background-image: url(./../../genshin/abyss/background/roleStarBg5.png); + width: 100%; + height: 70px; + /* filter: brightness(1.1); */ + background-size: 100%; + background-repeat: no-repeat; +} + +.bg4 { + width: 100%; + height: 70px; + background-image: url(./../../genshin/abyss/background/roleStarBg4.png); + background-size: 100%; + background-repeat: no-repeat; +} + +.list_box .item .life1 { + background-color: #62A8EA; +} + +.list_box .item .life2 { + background-color: #62A8EA; +} + +.list_box .item .life3 { + background-color: #45B97C; +} + +.list_box .item .life4 { + background-color: #45B97C; +} + +.list_box .item .life5 { + background-color: #FF5722; +} + +.list_box .item .life6 { + background-color: #FF5722; +} + +.logo { + font-size: 14px; + font-family: "tttgbnumber", serif; + text-align: center; + color: #7994A7; +} + +.hasMore { + font-size: 12px; + margin: -6px 0 10px 6px; + color: #7F858A; +} diff --git a/resources/starrail/gacha_count/gacha_count.html b/resources/starrail/gacha_count/gacha_count.html new file mode 100644 index 0000000..7dac67b --- /dev/null +++ b/resources/starrail/gacha_count/gacha_count.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + +
+
+
ID: {{ uid }}
+

跃迁统计-{{ typeName }}

+ +
+ {% for val in pool %} +
+
+
+

「{{ val.name }}」

+ {{ val.count }}抽 +
+ {% if typeName != "常驻跃迁" %} + {{ val.start }} - {{ val.end }} + {% endif %} +
+
+ {% for v in val.list %} +
+
+ {{ v.count }} + +
+ {% endfor %} +
+
+ {% endfor %} + {% if hasMore %} +
*完整数据请私聊查看
+ {% endif %} + +
+ + diff --git a/resources/starrail/gacha_log/example.html b/resources/starrail/gacha_log/example.html new file mode 100644 index 0000000..a353278 --- /dev/null +++ b/resources/starrail/gacha_log/example.html @@ -0,0 +1,77 @@ + + + + + + + + + + + + + +
+
+ +
+
+ ID: 10001 +
+

+ 81抽 + 角色祈愿池 · 欧 +

+ +
+ +
+
数据总览
+
+
+
1
+
未出五星
+
+
+ +
+ + 五星历史 2022-10-07 01:10 ~ 2022-10-07 23:10 + +
+ + +
+
+ UP + + +
80
+
+
+ +
+ + 四星最近历史 + +
+ +
+
+ + +
10
+
+
+
+ +
+
+ + \ No newline at end of file diff --git a/resources/starrail/gacha_log/gacha_log.css b/resources/starrail/gacha_log/gacha_log.css new file mode 100644 index 0000000..412ac47 --- /dev/null +++ b/resources/starrail/gacha_log/gacha_log.css @@ -0,0 +1,341 @@ +@font-face { + font-family: "tttgbnumber"; + src: url("./../../fonts/tttgbnumber.ttf"); + font-weight: normal; + font-style: normal; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + user-select: none; +} + +body { + font-size: 18px; + color: #1e1f20; + font-family: PingFangSC-Medium, PingFang SC, sans-serif; + transform: scale(1.5); + transform-origin: 0 0; + width: 510px; +} + +.container { + width: 510px; + padding: 20px 15px 10px 15px; + background-color: #f5f6fb; +} + +.head_box { + border-radius: 9999px; + font-family: tttgbnumber, sans-serif; + padding: 10px 20px; + position: relative; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); +} + +.head_box .id_text { + font-size: 24px; +} + +.head_box .day_text { + font-size: 20px; +} + +.head_box .genshin_logo { + position: absolute; + top: 1px; + right: 15px; + width: 97px; +} + +.logo { + font-size: 12px; + font-family: "tttgbnumber", serif; + text-align: center; + color: #7994a7; + position: relative; + padding-left: 10px; +} + +.data_box { + border-radius: 15px; + margin-top: 20px; + margin-bottom: 10px; + padding: 20px 0 5px 10px; + background: #fff; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + position: relative; +} + +.tab_lable { + position: absolute; + top: -10px; + left: -8px; + background: #d4b98c; + color: #fff; + font-size: 14px; + padding: 3px 10px; + border-radius: 15px 0 15px 15px; + z-index: 20; +} + +.data_line { + display: flex; + justify-content: space-around; + margin-bottom: 14px; + padding-right: 10px; +} + +.data_line_item { + width: 100px; + text-align: center; + + /* margin: 0 20px; */ +} + +.num { + font-family: tttgbnumber, serif; + font-size: 24px; +} + +.num .unit { + font-size: 12px; +} + +.data_box .lable { + font-size: 14px; + color: #7f858a; + line-height: 1; + margin-top: 3px; +} + +.info_box_border { + border-radius: 15px; + + /* margin-top: 20px; */ + margin-bottom: 20px; + padding: 6px 0 5px 10px; + background: #fff; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + position: relative; +} + +.card_list { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} + +.card_list .item { + margin: 0 8px 10px 0; + border-radius: 7px; + box-shadow: 0 2px 6px 0 rgb(132 93 90 / 30%); + height: 90px; + position: relative; + overflow: hidden; + background: #e7e5d9; +} + +.card_list .item img { + width: 70px; + height: 70px; + border-radius: 7px 7px 20px 0; +} + +.card_list .item.star5 img { + background-image: url(./../../genshin/abyss/background/roleStarBg5.png); + width: 100%; + height: 70px; + /* filter: brightness(1.1); */ + background-size: 100%; + background-repeat: no-repeat; +} + +.card_list .item.star4 img { + width: 100%; + height: 70px; + background-image: url(./../../genshin/abyss/background/roleStarBg4.png); + background-size: 100%; + background-repeat: no-repeat; +} + +.card_list .item .num { + position: absolute; + top: 0; + right: 0; + z-index: 9; + font-size: 18px; + text-align: center; + color: #fff; + border-radius: 3px; + padding: 1px 5px; + background: rgb(0 0 0 / 50%); + font-family: "tttgbnumber", serif; +} + +.card_list .item .name, +.card_list .item .num_name { + position: absolute; + top: 71px; + left: 0; + z-index: 9; + font-size: 12px; + text-align: center; + width: 100%; + height: 16px; + line-height: 18px; +} + +.card_list .item .num_name { + font-family: "tttgbnumber", serif; + font-size: 16px; +} + +.base_info { + position: relative; + padding-left: 10px; + margin: 5px 10px; +} + +.uid::before { + content: " "; + position: absolute; + width: 5px; + height: 24px; + border-radius: 1px; + left: 0; + top: 0; + background: #d3bc8d; +} + +.label_301 { + background-color: rgb(235 106 75); +} + +.label_302 { + background-color: #e69449; +} + +.label_200 { + background-color: #757cc8; +} + +.label { + color: #fff; + border-radius: 10px; + font-size: 12px; + padding: 2px 7px; + vertical-align: 2px; +} + +.ritem { + display: flex; + font-size: 12px; + margin-bottom: 5px; +} + +.info_role { + display: flex; + flex-wrap: wrap; + padding: 0 0 5px 9px; +} + +.ritem .role { + width: 20px; + height: 20px; + background-color: #ffb285; + border-radius: 100%; +} + +.ritem .weapon_box { + overflow: hidden; + width: 20px; + height: 20px; + border-radius: 100%; +} + +.ritem .weapon { + width: 20px; + height: 20px; + background-color: #ffb285; + border-radius: 100%; + transform: scale(1.5); + -webkit-transform: scale(1.5); + +} + +.ritem .role_text { + margin: 2px 3px 0 2px; + display: flex; + align-items: baseline; +} + +.ritem .role_name { + width: 24px; + white-space: nowrap; + overflow: hidden; +} + +.ritem .role_num { + width: 24px; +} + +.line_box { + height: 32px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #7d7d7d; + padding-bottom: 5px; +} + +.line_box .line { + height: 2px; + flex-grow: 1; + background-color: #ebebeb; + margin: 0 10px; +} + +.red { + color: #f21000; +} + +.orange { + color: #ff8d00; +} + +.green { + color: #12d88c; +} + +.blue { + color: #4169e1; +} + +.purple { + color: #7500ff; +} + +.minimum { + position: absolute; + top: 0; + right: 0; + z-index: 9; + font-size: 12px; + text-align: center; + color: #fff; + border-radius: 3px; + padding: 1px 3px; + background-color: rgb(0 0 0 / 80%); + font-family: "tttgbnumber", serif; +} + +.hasMore { + font-size: 12px; + margin: 6px 0; + color: #7f858a; +} diff --git a/resources/starrail/gacha_log/gacha_log.html b/resources/starrail/gacha_log/gacha_log.html new file mode 100644 index 0000000..dfc02bc --- /dev/null +++ b/resources/starrail/gacha_log/gacha_log.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + +
+
+ +
+
+ ID: {{ uid }} +
+

+ {{ allNum }}抽 + {{ typeName }} +

+
+ +
+
数据总览
+ {% for val in line %} +
+ {% for item in val %} +
+
{{item.num}}{{item.unit}}
+
{{item.lable}}
+
+ {% endfor %} +
+ {% endfor %} + +
+ + 五星历史 {{firstTime}} ~ {{lastTime}} + +
+ + +
+ {% for val in fiveLog %} +
+ {% if val.isUp %} + UP + {% endif %} + + +
{{ val.count }}
+
+ {% endfor %} +
+ +
+ + 四星最近历史 + +
+ +
+ {% for val in fourLog %} +
+ + +
{{ val.count }}
+
+ {% endfor %} +
+
+ +
+
+ + \ No newline at end of file diff --git a/resources/starrail/gacha_log/img/提纳里.png b/resources/starrail/gacha_log/img/提纳里.png new file mode 100644 index 0000000000000000000000000000000000000000..fb923acba960da288c7370e3f7c4a1e3db807e1b GIT binary patch literal 47915 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 str: - return "hk4e_cn" if game_uid < 600000000 else "hk4e_global" +def recognize_starrail_game_biz(game_uid: int) -> str: + return "hkrpg_cn" if game_uid < 600000000 else "hkrpg_global" async def get_authkey_by_stoken(client: Client) -> Optional[str]: @@ -32,9 +32,9 @@ async def get_authkey_by_stoken(client: Client) -> Optional[str]: headers = GACHA_HEADERS.copy() json = { "auth_appid": "webview_gacha", - "game_biz": recognize_genshin_game_biz(client.uid), + "game_biz": recognize_starrail_game_biz(client.uid), "game_uid": client.uid, - "region": recognize_genshin_server(client.uid), + "region": recognize_starrail_server(client.uid), } device_id = hex_digest(str(client.uid)) headers["x-rpc-device_id"] = device_id @@ -56,9 +56,9 @@ async def fetch_hk4e_token_by_cookie(client: Client) -> None: "Content-Type": "application/json;charset=UTF-8", } json = { - "game_biz": recognize_genshin_game_biz(client.uid), + "game_biz": recognize_starrail_game_biz(client.uid), "lang": "zh-cn", "uid": str(client.uid), - "region": recognize_genshin_server(client.uid), + "region": recognize_starrail_server(client.uid), } await client.cookie_manager.request(url, method="POST", json=json, headers=headers)