新增材料图鉴

This commit is contained in:
CMHopeSunshine 2022-09-18 18:11:57 +08:00
parent 7b58799156
commit aacebaca4c
8 changed files with 771 additions and 11 deletions

View File

@ -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])))

View 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"]

View 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}>"

View 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
)

View 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)

View 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"]]

View 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]

View 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)