From aacebaca4ca4d9e498b7db0b74d1de551233d217 Mon Sep 17 00:00:00 2001 From: CMHopeSunshine <277073121@qq.com> Date: Sun, 18 Sep 2022 18:11:57 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E`=E6=9D=90?= =?UTF-8?q?=E6=96=99=E5=9B=BE=E9=89=B4`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LittlePaimon/plugins/Paimon_Wiki/__init__.py | 63 ++++-- .../Paimon_Wiki/genshinmap/__init__.py | 17 ++ .../plugins/Paimon_Wiki/genshinmap/exc.py | 13 ++ .../plugins/Paimon_Wiki/genshinmap/img.py | 85 ++++++++ .../plugins/Paimon_Wiki/genshinmap/models.py | 187 ++++++++++++++++++ .../plugins/Paimon_Wiki/genshinmap/request.py | 181 +++++++++++++++++ .../plugins/Paimon_Wiki/genshinmap/utils.py | 122 ++++++++++++ LittlePaimon/plugins/Paimon_Wiki/handler.py | 114 +++++++++++ 8 files changed, 771 insertions(+), 11 deletions(-) create mode 100644 LittlePaimon/plugins/Paimon_Wiki/genshinmap/__init__.py create mode 100644 LittlePaimon/plugins/Paimon_Wiki/genshinmap/exc.py create mode 100644 LittlePaimon/plugins/Paimon_Wiki/genshinmap/img.py create mode 100644 LittlePaimon/plugins/Paimon_Wiki/genshinmap/models.py create mode 100644 LittlePaimon/plugins/Paimon_Wiki/genshinmap/request.py create mode 100644 LittlePaimon/plugins/Paimon_Wiki/genshinmap/utils.py create mode 100644 LittlePaimon/plugins/Paimon_Wiki/handler.py diff --git a/LittlePaimon/plugins/Paimon_Wiki/__init__.py b/LittlePaimon/plugins/Paimon_Wiki/__init__.py index b59f938..b741975 100644 --- a/LittlePaimon/plugins/Paimon_Wiki/__init__.py +++ b/LittlePaimon/plugins/Paimon_Wiki/__init__.py @@ -1,17 +1,20 @@ import time -from nonebot import on_regex +from nonebot import on_regex, on_command from nonebot.adapters.onebot.v11 import MessageEvent, Message, MessageSegment -from nonebot.adapters.onebot.v11.helpers import is_cancellation +from nonebot.adapters.onebot.v11.helpers import HandleCancellation from nonebot.adapters.onebot.v11.exception import ActionFailed -from nonebot.params import RegexDict, ArgPlainText +from nonebot.params import RegexDict, ArgPlainText, CommandArg from nonebot.plugin import PluginMetadata from nonebot.typing import T_State -from LittlePaimon import NICKNAME +from LittlePaimon import NICKNAME, DRIVER from LittlePaimon.utils.alias import get_match_alias from LittlePaimon.utils.message import MessageBuild from LittlePaimon.database.models import PlayerAlias +from LittlePaimon.config import RESOURCE_BASE_PATH +from .handler import init_map, draw_map + # from .abyss_rate_draw import draw_rate_rank, draw_teams_rate __paimon_help__ = { @@ -45,6 +48,14 @@ daily_material = on_regex(r'(?P现在|(今|明|后)(天|日)|周(一|二| 'pm_usage': '<今天|周几>材料', 'pm_priority': 8 }) +material_map = on_command('材料图鉴', priority=11, block=True, state={ + 'pm_name': '材料图鉴', + 'pm_description': '查看某个材料的介绍和采集点。', + 'pm_usage': '材料图鉴<材料名>[地图]', + 'pm_priority': 9 +}) + + # abyss_rate = on_command('syrate', aliases={'深渊登场率', '深境螺旋登场率', '深渊登场率排行', '深渊排行'}, priority=11, block=True, state={ # 'pm_name': '深渊登场率排行', # 'pm_description': '查看本期深渊的角色登场率排行', @@ -88,6 +99,38 @@ async def _(event: MessageEvent, regex_dict: dict = RegexDict()): MessageSegment.image(file='https://static.cherishmoon.fun/LittlePaimon/DailyMaterials/周三周六.jpg')) +@material_map.handle() +async def _(event: MessageEvent, state: T_State, msg: Message = CommandArg()): + if params := msg.extract_plain_text().strip().split(' '): + state['name'] = Message(params[0]) + if len(params) > 1: + if params[1] in {'提瓦特', '层岩巨渊', '渊下宫'}: + state['map'] = params[1] + else: + state['map'] = Message('提瓦特') + + +@material_map.got('map', prompt='地图名称有误,请在【提瓦特、层岩巨渊、渊下宫】中选择') +async def _(event: MessageEvent, state: T_State, map_: str = ArgPlainText('map')): + if map_ not in {'提瓦特', '层岩巨渊', '渊下宫'}: + await material_map.reject('地图名称有误,请在【提瓦特、层岩巨渊、渊下宫】中选择') + else: + state['map'] = Message(map_) + + +@material_map.got('name', prompt='请输入要查询的材料名称,或回答【取消】退出', parameterless=[HandleCancellation(f'好吧,有需要再找{NICKNAME}')]) +async def _(event: MessageEvent, map_: str = ArgPlainText('map'), name: str = ArgPlainText('name')): + if (file_path := RESOURCE_BASE_PATH / 'genshin_map' / 'results' / f'{map_}_{name}.png').exists(): + await material_map.finish(MessageSegment.image(file_path), at_sender=True) + else: + await material_map.send(MessageBuild.Text(f'开始查找{name}的资源点,请稍候...')) + result = await draw_map(name, map_) + await material_map.finish(result, at_sender=True) + + +DRIVER.on_bot_connect(init_map) + + # @abyss_rate.handle() # async def abyss_rate_handler(event: MessageEvent): # abyss_img = await draw_rate_rank() @@ -137,14 +180,14 @@ def create_wiki_matcher(pattern: str, help_fun: str, help_name: str): if name: state['name'] = name - @maps.got('name', prompt=Message.template('请提供要查询的{type}')) + @maps.got('name', prompt=Message.template('请提供要查询的{type}'), + parameterless=[HandleCancellation(f'好吧,有需要再找{NICKNAME}')]) async def _(event: MessageEvent, state: T_State): name = state['name'] if isinstance(name, Message): - if is_cancellation(name): - await maps.finish() name = name.extract_plain_text().strip() - if state['type'] == '角色' and (match_alias := await PlayerAlias.get_or_none(user_id=str(event.user_id), alias=name)): + if state['type'] == '角色' and ( + match_alias := await PlayerAlias.get_or_none(user_id=str(event.user_id), alias=name)): try: await maps.finish(MessageSegment.image(state['img_url'].format(match_alias.character))) except ActionFailed: @@ -169,11 +212,9 @@ def create_wiki_matcher(pattern: str, help_fun: str, help_name: str): else: await maps.finish(MessageBuild.Text(f'没有找到{name}的图鉴')) - @maps.got('choice') + @maps.got('choice', parameterless=[HandleCancellation(f'好吧,有需要再找{NICKNAME}')]) async def _(event: MessageEvent, state: T_State, choice: str = ArgPlainText('choice')): match_alias = state['match_alias'] - if is_cancellation(choice): - await maps.finish() if choice.isdigit() and (1 <= int(choice) <= len(match_alias)): try: await maps.finish(MessageSegment.image(state['img_url'].format(match_alias[int(choice) - 1]))) diff --git a/LittlePaimon/plugins/Paimon_Wiki/genshinmap/__init__.py b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/__init__.py new file mode 100644 index 0000000..a6aa5ae --- /dev/null +++ b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/__init__.py @@ -0,0 +1,17 @@ +from .models import Maps as Maps # noqa: F401 +from .models import Tree as Tree # noqa: F401 +from .models import MapID as MapID # noqa: F401 +from .models import Point as Point # noqa: F401 +from .models import Slice as Slice # noqa: F401 +from .models import MapInfo as MapInfo # noqa: F401 +from .models import XYPoint as XYPoint # noqa: F401 +from .utils import make_map as make_map # noqa: F401 +from .request import get_maps as get_maps # noqa: F401 +from .exc import StatusError as StatusError # noqa: F401 +from .request import get_labels as get_labels # noqa: F401 +from .request import get_points as get_points # noqa: F401 +from .utils import convert_pos as convert_pos # noqa: F401 +from .utils import get_map_by_pos as get_map_by_pos # noqa: F401 +from .utils import get_points_by_id as get_points_by_id # noqa: F401 + +__all__ = ["utils", "request", "exc", "models", "imgs"] diff --git a/LittlePaimon/plugins/Paimon_Wiki/genshinmap/exc.py b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/exc.py new file mode 100644 index 0000000..b28fb60 --- /dev/null +++ b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/exc.py @@ -0,0 +1,13 @@ +class StatusError(ValueError): + """米游社状态异常""" + + def __init__(self, status: int, message: str, *args: object) -> None: + super().__init__(status, message, *args) + self.status = status + self.message = message + + def __str__(self) -> str: + return f"miHoYo API {self.status}: {self.message}" + + def __repr__(self) -> str: + return f"" diff --git a/LittlePaimon/plugins/Paimon_Wiki/genshinmap/img.py b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/img.py new file mode 100644 index 0000000..4facad1 --- /dev/null +++ b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/img.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import List, Tuple + +import numpy as np +from sklearn.cluster import KMeans +from shapely.geometry import Point, Polygon + +from .models import XYPoint + +Pos = Tuple[float, float] +Poses = List[XYPoint] +Points = List[Point] + + +def k_means_points( + points: List[XYPoint], length: int = 500, clusters: int = 3 +) -> List[Tuple[XYPoint, XYPoint, Poses]]: + """ + 通过 K-Means 获取集群坐标列表 + + 参数: + points: `list[XYPoint]` + 坐标列表,建议预先使用 `convert_pos` 进行坐标转换 + + length: `int` (default: 500) + 区域大小,如果太大则可能一个点会在多个集群中 + + clusters: `int` (default: 3) + 集群数量 + + 返回: + `list[tuple[XYPoint, XYPoint, list[XYPoint]]]` + + tuple 中: + 第 1 个元素为集群最左上方的点 + 第 2 个元素为集群最右下方的点 + 第 3 个元素为集群内所有点 + + list 按照集群内点的数量降序排序 + + 提示: + length: + +---------------------+ + │ │ + │ │ + │ │ + |--length--|--length--│ + │ │ + │ │ + │ │ + +---------------------+ + """ + pos_array = np.array(points) + k_means = KMeans(n_clusters=clusters).fit(pos_array) + points_temp: List[Points] = [] + for k_means_pos in k_means.cluster_centers_: + x = ( + k_means_pos[0] - length if k_means_pos[0] > length else 0, + k_means_pos[0] + length, + ) + y = ( + k_means_pos[1] - length if k_means_pos[1] > length else 0, + k_means_pos[1] + length, + ) + path = Polygon( + [(x[0], y[0]), (x[0], y[1]), (x[1], y[1]), (x[1], y[0])] + ) + + points_temp.append( + [Point(i) for i in pos_array if path.contains(Point(i))] + ) + return_list = [] + for i in points_temp: + pos_array_ = np.array([p.xy for p in i]) + return_list.append( + ( + XYPoint(pos_array_[:, 0].min(), pos_array_[:, 1].min()), + XYPoint(pos_array_[:, 0].max(), pos_array_[:, 1].max()), + list(map(lambda p: XYPoint(p.x, p.y), i)), + ) + ) + return sorted( + return_list, key=lambda pos_tuple: len(pos_tuple[2]), reverse=True + ) diff --git a/LittlePaimon/plugins/Paimon_Wiki/genshinmap/models.py b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/models.py new file mode 100644 index 0000000..67d0eb5 --- /dev/null +++ b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/models.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +from enum import IntEnum +from typing import List, Tuple, Optional, NamedTuple + +from pydantic import HttpUrl, BaseModel, validator + + +class MapID(IntEnum): + """地图 ID""" + + teyvat = 2 + """提瓦特""" + enkanomiya = 7 + """渊下宫""" + chasm = 9 + """层岩巨渊·地下矿区""" + golden_apple_archipelago = 12 + """金苹果群岛""" + + +class Label(BaseModel): + id: int + name: str + icon: HttpUrl + parent_id: int + depth: int + node_type: int + jump_type: int + jump_target_id: int + display_priority: int + children: list + activity_page_label: int + area_page_label: List[int] + is_all_area: bool + + +class Tree(BaseModel): + id: int + name: str + icon: str + parent_id: int + depth: int + node_type: int + jump_type: int + jump_target_id: int + display_priority: int + children: List[Label] + activity_page_label: int + area_page_label: List + is_all_area: bool + + +class Point(BaseModel): + id: int + label_id: int + x_pos: float + y_pos: float + author_name: str + ctime: str + display_state: int + + +class Slice(BaseModel): + url: HttpUrl + + +class Maps(BaseModel): + slices: List[HttpUrl] + origin: List[int] + total_size: List[int] + padding: List[int] + + @validator("slices", pre=True) + def slices_to_list(cls, v): + urls: List[str] = [] + for i in v: + urls.extend(j["url"] for j in i) + return urls + + +class MapInfo(BaseModel): + id: int + name: str + parent_id: int + depth: int + detail: Maps + node_type: int + children: list + icon: Optional[HttpUrl] + ch_ext: Optional[str] + + @validator("detail", pre=True) + def detail_str_to_maps(cls, v): + return Maps.parse_raw(v) + + +class XYPoint(NamedTuple): + x: float + y: float + + +class Kind(BaseModel): + id: int + name: str + icon_id: int + icon_url: HttpUrl + is_game: int + + +class SpotKinds(BaseModel): + list: List[Kind] + is_sync: bool + already_share: bool + + +class Spot(BaseModel): + id: int + name: str + content: str + kind_id: int + spot_icon: str + x_pos: float + y_pos: float + nick_name: str + avatar_url: HttpUrl + status: int + + +class SubAnchor(BaseModel): + id: int + name: str + l_x: int + l_y: int + r_x: int + r_y: int + app_sn: str + parent_id: str + map_id: str + sort: int + + +class Anchor(BaseModel): + id: int + name: str + l_x: int + l_y: int + r_x: int + r_y: int + app_sn: str + parent_id: str + map_id: str + children: List[SubAnchor] + sort: int + + def get_children_all_left_point(self) -> List[XYPoint]: + """获取所有子锚点偏左的 `XYPoint` 坐标""" + return [XYPoint(x=i.l_x, y=i.l_y) for i in self.children] + + def get_children_all_right_point(self) -> List[XYPoint]: + """获取所有子锚点偏右的 `XYPoint` 坐标""" + return [XYPoint(x=i.r_x, y=i.r_y) for i in self.children] + + +class PageLabel(BaseModel): + id: int + name: str + type: int + pc_icon_url: str + mobile_icon_url: str + sort: int + pc_icon_url2: str + map_id: int + jump_url: str + jump_type: str + center: Optional[Tuple[float, float]] + zoom: Optional[float] + + @validator("center", pre=True) + def center_str_to_tuple(cls, v: str) -> Optional[Tuple[float, float]]: + if v and (splitted := v.split(",")): + return tuple(map(float, splitted)) + + @validator("zoom", pre=True) + def zoom_str_to_float(cls, v: str): + if v: + return float(v) diff --git a/LittlePaimon/plugins/Paimon_Wiki/genshinmap/request.py b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/request.py new file mode 100644 index 0000000..53bde26 --- /dev/null +++ b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/request.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Tuple + +from httpx import Response, AsyncClient + +from .exc import StatusError +from .models import ( + Spot, + Tree, + MapID, + Point, + Anchor, + MapInfo, + PageLabel, + SpotKinds, +) + +CLIENT = AsyncClient( + base_url="https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/map" +) +API_CLIENT = AsyncClient( + base_url="https://api-takumi.mihoyo.com/common/map_user/ys_obc/v1/map" +) +Spots = Dict[int, List[Spot]] + + +async def _request( + endpoint: str, client: AsyncClient = CLIENT +) -> Dict[str, Any]: + resp = await client.get(endpoint) + resp.raise_for_status() + data: dict[str, Any] = resp.json() + if data["retcode"] != 0: + raise StatusError(data["retcode"], data["message"]) + return data["data"] + + +async def get_labels(map_id: MapID) -> List[Tree]: + """ + 获取米游社资源列表 + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[Tree]` + """ + data = await _request(f"/label/tree?map_id={map_id}&app_sn=ys_obc") + return [Tree.parse_obj(i) for i in data["tree"]] + + +async def get_points(map_id: MapID) -> List[Point]: + """ + 获取米游社坐标列表 + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[Point]` + """ + data = await _request(f"/point/list?map_id={map_id}&app_sn=ys_obc") + return [Point.parse_obj(i) for i in data["point_list"]] + + +async def get_maps(map_id: MapID) -> MapInfo: + """ + 获取米游社地图 + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `MapInfo` + """ + data = await _request(f"/info?map_id={map_id}&app_sn=ys_obc&lang=zh-cn") + return MapInfo.parse_obj(data["info"]) + + +async def get_spot_from_game( + map_id: MapID, cookie: str +) -> Tuple[Spots, SpotKinds]: + """ + 获取游戏内标点 + + 注意:每十分钟只能获取一次,否则会 -2000 错误 + + 参数: + map_id: `MapID` + 地图 ID + + cookie: `str` + 米游社 Cookie + + 返回: + `tuple[Spots, SpotKinds]` + """ + + def _raise_for_retcode(resp: Response) -> Dict[str, Any]: + resp.raise_for_status() + data: dict[str, Any] = resp.json() + if data["retcode"] != 0: + raise StatusError(data["retcode"], data["message"]) + return data["data"] + + # 1. 申请刷新 + resp = await API_CLIENT.post( + "/spot_kind/sync_game_spot", + json={ + "map_id": str(map_id.value), + "app_sn": "ys_obc", + "lang": "zh-cn", + }, + headers={"Cookie": cookie}, + ) + _raise_for_retcode(resp) + + # 2. 获取类别 + resp = await API_CLIENT.get( + "/spot_kind/get_spot_kinds?map_id=2&app_sn=ys_obc&lang=zh-cn", + headers={"Cookie": cookie}, + ) + data = _raise_for_retcode(resp) + spot_kinds_data = SpotKinds.parse_obj(data) + ids = [kind.id for kind in spot_kinds_data.list] + + # 3.获取坐标 + resp = await API_CLIENT.post( + "/spot/get_map_spots_by_kinds", + json={ + "map_id": str(map_id.value), + "app_sn": "ys_obc", + "lang": "zh-cn", + "kind_ids": ids, + }, + ) + data = _raise_for_retcode(resp) + spots: Spots = {} + for k, v in data["spots"].items(): + spots[int(k)] = [Spot.parse_obj(i) for i in v["list"]] + return spots, spot_kinds_data + + +async def get_page_label(map_id: MapID) -> List[PageLabel]: + """ + 获取米游社大地图标签(例如蒙德,龙脊雪山等) + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[PageLabel]` + """ + data = await _request( + f"/get_map_pageLabel?map_id={map_id}&app_sn=ys_obc&lang=zh-cn", + API_CLIENT, + ) + return [PageLabel.parse_obj(i) for i in data["list"]] + + +async def get_anchors(map_id: MapID) -> List[Anchor]: + """ + 获取米游社地图锚点,含子锚点(例如珉林-庆云顶等) + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[Anchor]` + """ + data = await _request( + f"/map_anchor/list?map_id={map_id}&app_sn=ys_obc&lang=zh-cn", + API_CLIENT, + ) + return [Anchor.parse_obj(i) for i in data["list"]] diff --git a/LittlePaimon/plugins/Paimon_Wiki/genshinmap/utils.py b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/utils.py new file mode 100644 index 0000000..c0907d6 --- /dev/null +++ b/LittlePaimon/plugins/Paimon_Wiki/genshinmap/utils.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from io import BytesIO +from typing import List, Union +from asyncio import gather, create_task + +from PIL import Image +from httpx import AsyncClient + +from .models import Maps, Point, XYPoint + +CLIENT = AsyncClient() + + +async def get_img(url: str) -> Image.Image: + resp = await CLIENT.get(url) + resp.raise_for_status() + return Image.open(BytesIO(resp.read())) + + +async def make_map(map: Maps) -> Image.Image: + """ + 获取所有地图并拼接 + + 警告:可能导致内存溢出 + + 在测试中,获取并合成「提瓦特」地图时占用了约 1.4 GiB + + 建议使用 `genshinmap.utils.get_map_by_pos` 获取地图单片 + + 参数: + map: `Maps` + 地图数据,可通过 `get_maps` 获取 + + 返回: + `PIL.Image.Image` 对象 + + 另见: + `get_map_by_pos` + """ + img = Image.new("RGBA", tuple(map.total_size)) + x = 0 + y = 0 + maps: List[Image.Image] = await gather( + *[create_task(get_img(url)) for url in map.slices] + ) + for m in maps: + img.paste(m, (x, y)) + x += 4096 + if x >= map.total_size[0]: + x = 0 + y += 4096 + return img + + +async def get_map_by_pos( + map: Maps, x: Union[int, float], y: Union[int, float] = 0 +) -> Image.Image: + """ + 根据横坐标获取地图单片 + + 参数: + map: `Maps` + 地图数据,可通过 `get_maps` 获取 + + x: `int | float` + 横坐标 + + y: `int | float` (default: 0) + 纵坐标 + + 返回: + `PIL.Image.Image` 对象 + """ + # 4 * (y // 4096) {0,4,8} + # x // 4096 {0,1,2,3} + return await get_img(map.slices[4 * (int(y // 4096)) + int(x // 4096)]) + + +def get_points_by_id(id_: int, points: List[Point]) -> List[XYPoint]: + """ + 根据 Label ID 获取坐标点 + + 参数: + id_: `int` + Label ID + + points: `list[Point]` + 米游社坐标点列表,可通过 `get_points` 获取 + + 返回: + `list[XYPoint]` + """ + return [ + XYPoint(point.x_pos, point.y_pos) + for point in points + if point.label_id == id_ + ] + + +def convert_pos(points: List[XYPoint], origin: List[int]) -> List[XYPoint]: + """ + 将米游社资源坐标转换为以左上角为原点的坐标系的坐标 + + 参数: + points: `list[XYPoint]` + 米游社资源坐标 + + origin: `list[Point]` + 米游社地图 Origin,可通过 `get_maps` 获取 + + 返回: + `list[XYPoint]` + + 示例: + >>> from genshinmap.models import XYPoint + >>> points = [XYPoint(1200, 5000), XYPoint(-4200, 1800)] + >>> origin = [4844,4335] + >>> convert_pos(points, origin) + [XYPoint(x=6044, y=9335), XYPoint(x=644, y=6135)] + """ + return [XYPoint(x + origin[0], y + origin[1]) for x, y in points] diff --git a/LittlePaimon/plugins/Paimon_Wiki/handler.py b/LittlePaimon/plugins/Paimon_Wiki/handler.py new file mode 100644 index 0000000..40f5539 --- /dev/null +++ b/LittlePaimon/plugins/Paimon_Wiki/handler.py @@ -0,0 +1,114 @@ +import math +from LittlePaimon.config import RESOURCE_BASE_PATH +from LittlePaimon.utils import logger, aiorequests +from LittlePaimon.utils.files import load_image +from LittlePaimon.utils.image import PMImage, font_manager as fm +from LittlePaimon.utils.message import MessageBuild + +from .genshinmap import utils, models, request, img + +from PIL import Image, ImageFile, ImageOps +ImageFile.LOAD_TRUNCATED_IMAGES = True +Image.MAX_IMAGE_PIXELS = None + +map_name = { + 'teyvat': '提瓦特', + 'enkanomiya': '渊下宫', + 'chasm': '层岩巨渊' +} +map_name_reverse = { + '提瓦特': 'teyvat', + '渊下宫': 'enkanomiya', + '层岩巨渊': 'chasm' +} + + +async def init_map(refresh: bool = False): + """ + 初始化地图 + :param refresh: 是否刷新 + """ + for map_id in models.MapID: + save_path = RESOURCE_BASE_PATH / 'genshin_map' / 'results' / f'{map_id.name}.png' + save_path.parent.mkdir(parents=True, exist_ok=True) + if map_id.name == 'golden_apple_archipelago' or (save_path.exists() and not refresh): + continue + status_icon = await load_image(RESOURCE_BASE_PATH / 'genshin_map' / 'status_icon.png') + anchor_icon = await load_image(RESOURCE_BASE_PATH / 'genshin_map' / 'anchor_icon.png') + maps = await request.get_maps(map_id) + points = await request.get_points(map_id) + status_points = utils.convert_pos(utils.get_points_by_id(2, points), maps.detail.origin) # 七天神像 + anchor_points = utils.convert_pos(utils.get_points_by_id(3, points), maps.detail.origin) # 传送锚点 + map_img = await utils.make_map(maps.detail) + for point in status_points: + map_img.paste(status_icon, (int(point.x) - 32, int(point.y) - 64), status_icon) + for point in anchor_points: + map_img.paste(anchor_icon, (int(point.x) - 32, int(point.y) - 64), anchor_icon) + map_img.save(save_path) + logger.info('原神地图', f'{map_name[map_id.name]}地图初始化完成') + + +async def draw_map(name: str, map_: str): + """ + 获取地图 + :param name: 材料名 + :param map_: 地图名 + :return: 地图 + """ + map_id = models.MapID[map_name_reverse[map_]] + maps = await request.get_maps(map_id) + labels = await request.get_labels(map_id) + if resources := list(filter(lambda x: x.name == name, [child for label in labels for child in label.children])): + resource = resources[0] + else: + return MessageBuild.Text(f'未查找到材料{name}') + points = await request.get_points(map_id) + if not (points := utils.convert_pos(utils.get_points_by_id(resource.id, points), maps.detail.origin)): + return MessageBuild.Text(f'在{map_}上未查找到材料{name},请尝试其他地图') + point_icon = await load_image(RESOURCE_BASE_PATH / 'genshin_map' / 'point_icon.png') + if len(points) >= 3: + group_point = img.k_means_points(points, 700) + else: + x1_temp = int(points[0].x) - 670 + x2_temp = int(points[0].x) + 670 + y1_temp = int(points[0].y) - 700 + y2_temp = int(points[0].y) + 700 + group_point = [( + models.XYPoint(x1_temp, y1_temp), + models.XYPoint(x2_temp, y2_temp), + points)] + map_img = await load_image(RESOURCE_BASE_PATH / 'genshin_map' / 'results' / f'{map_id.name}.png') + lt_point = group_point[0][0] + rb_point = group_point[0][1] + map_img = map_img.crop((int(lt_point.x), int(lt_point.y), int(rb_point.x), int(rb_point.y))) + for point in group_point[0][2]: + point_trans = (int(point.x) - int(lt_point.x), int(point.y) - int(lt_point.y),) + map_img.paste(point_icon, (point_trans[0] - 16, point_trans[1] - 16), point_icon) + scale_f = map_img.width / map_img.height + if scale_f > 980 / 850: + map_img = map_img.resize((math.ceil(850 * scale_f), 850), Image.ANTIALIAS) + else: + map_img = map_img.resize((980, math.ceil(980 / scale_f)), Image.ANTIALIAS) + map_img = map_img.crop((0, 0, 980, 850)) + map_img = ImageOps.expand(map_img, border=4, fill='#633da3') + total_img = PMImage(await load_image(RESOURCE_BASE_PATH / 'genshin_map' / 'bg.png')) + await total_img.paste(map_img, (48, total_img.height - 60 - map_img.height)) + icon = await aiorequests.get_img(resource.icon, size=(300, 300)) + await total_img.paste(icon, (100, 100)) + await total_img.text(f'「{name}」', 457, 147, fm.get('SourceHanSerifCN-Bold.otf', 72), 'white') + info = await aiorequests.get(f'https://info.minigg.cn/materials?query={name}') + info = info.json() + des = '' + if 'description' in info: + des += info['description'].strip('\n') + if 'source' in info: + des += '\n推荐采集地点:' + ','.join(info['source']) + if des: + await total_img.text_box(des.replace('\n', '^'), (482, 1010), (281, 520), fm.get('SourceHanSansCN-Bold.otf', 30), '#3c3c3c') + await total_img.text('CREATED BY LITTLEPAIMON', (0, total_img.width), total_img.height - 45, fm.get('bahnschrift_bold', 36, 'Bold'), '#3c3c3c', align='center') + total_img.save(RESOURCE_BASE_PATH / 'genshin_map' / 'results' / f'{map_}_{name}.png') + return MessageBuild.Image(total_img, mode='RGB', quality=85) + + + +