mirror of
https://github.com/xuthus83/LittlePaimon.git
synced 2024-12-16 13:40:53 +08:00
✨ 新增材料图鉴
This commit is contained in:
parent
7b58799156
commit
aacebaca4c
@ -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<day>现在|(今|明|后)(天|日)|周(一|二|
|
||||
'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])))
|
||||
|
17
LittlePaimon/plugins/Paimon_Wiki/genshinmap/__init__.py
Normal file
17
LittlePaimon/plugins/Paimon_Wiki/genshinmap/__init__.py
Normal file
@ -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"]
|
13
LittlePaimon/plugins/Paimon_Wiki/genshinmap/exc.py
Normal file
13
LittlePaimon/plugins/Paimon_Wiki/genshinmap/exc.py
Normal file
@ -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"<StatusError status={self.status}, message={self.message}>"
|
85
LittlePaimon/plugins/Paimon_Wiki/genshinmap/img.py
Normal file
85
LittlePaimon/plugins/Paimon_Wiki/genshinmap/img.py
Normal file
@ -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
|
||||
)
|
187
LittlePaimon/plugins/Paimon_Wiki/genshinmap/models.py
Normal file
187
LittlePaimon/plugins/Paimon_Wiki/genshinmap/models.py
Normal file
@ -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)
|
181
LittlePaimon/plugins/Paimon_Wiki/genshinmap/request.py
Normal file
181
LittlePaimon/plugins/Paimon_Wiki/genshinmap/request.py
Normal file
@ -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"]]
|
122
LittlePaimon/plugins/Paimon_Wiki/genshinmap/utils.py
Normal file
122
LittlePaimon/plugins/Paimon_Wiki/genshinmap/utils.py
Normal file
@ -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]
|
114
LittlePaimon/plugins/Paimon_Wiki/handler.py
Normal file
114
LittlePaimon/plugins/Paimon_Wiki/handler.py
Normal file
@ -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'<m>{map_name[map_id.name]}</m>地图初始化完成')
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user