新增抽卡记录分析和导出功能
@ -18,8 +18,6 @@ sv=hoshino.Service('原神模拟抽卡')
|
||||
|
||||
@sv.on_rex(r'^抽((?P<num>\d+)|(?:.*))十连(?P<pool>.*?)$')
|
||||
async def gacha(bot, ev):
|
||||
if ev.message_type == 'guild' and ev.channel_id != '1983536' and ev.channel_id != '1916789':
|
||||
return
|
||||
gid = ev.group_id
|
||||
uid = ev.user_id
|
||||
init_user_info(uid)
|
||||
@ -61,10 +59,8 @@ async def gacha(bot, ev):
|
||||
save_user_info()
|
||||
await bot.send(ev, MessageSegment.image(img), at_sender=True)
|
||||
|
||||
@sv.on_prefix('查看抽卡记录')
|
||||
@sv.on_prefix('模拟抽卡记录')
|
||||
async def gacharecord(bot, ev):
|
||||
if ev.channel_id != '1983536' and ev.channel_id != '1916789':
|
||||
return
|
||||
uid = ev.user_id
|
||||
init_user_info(uid)
|
||||
if user_info[uid]['gacha_list']['wish_total'] == 0:
|
||||
@ -127,10 +123,8 @@ async def getrwrecord(msg,uid):
|
||||
res = res.replace(',',' ')
|
||||
return res
|
||||
|
||||
@sv.on_fullmatch('删除抽卡记录')
|
||||
@sv.on_fullmatch('删除模拟抽卡记录')
|
||||
async def deleterecord(bot,ev):
|
||||
if ev.channel_id != '1983536' and ev.channel_id !='1916789':
|
||||
return
|
||||
uid = ev.user_id
|
||||
init_user_info(uid)
|
||||
try:
|
||||
@ -142,8 +136,6 @@ async def deleterecord(bot,ev):
|
||||
|
||||
@sv.on_prefix('选择定轨')
|
||||
async def choosedg(bot,ev):
|
||||
if ev.channel_id != '1983536' and ev.channel_id !='1916789':
|
||||
return
|
||||
uid = ev.user_id
|
||||
init_user_info(uid)
|
||||
dg_weapon = ev.message.extract_plain_text().strip()
|
||||
@ -161,8 +153,6 @@ async def choosedg(bot,ev):
|
||||
|
||||
@sv.on_fullmatch('删除定轨')
|
||||
async def deletedg(bot,ev):
|
||||
if ev.channel_id != '1983536' and ev.channel_id !='1916789':
|
||||
return
|
||||
uid = ev.user_id
|
||||
init_user_info(uid)
|
||||
if user_info[uid]['gacha_list']['dg_name'] == '':
|
||||
@ -175,8 +165,6 @@ async def deletedg(bot,ev):
|
||||
|
||||
@sv.on_fullmatch('查看定轨')
|
||||
async def deletedg(bot,ev):
|
||||
if ev.channel_id != '1983536' and ev.channel_id !='1916789':
|
||||
return
|
||||
uid = ev.user_id
|
||||
init_user_info(uid)
|
||||
weapon_up_list = await getdg_weapon()
|
||||
|
115
hoshino/modules/Genshin_Paimon/gacha_log_export/UIGF_and_XLSX.py
Normal file
@ -0,0 +1,115 @@
|
||||
from .meta_data import *
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import xlsxwriter
|
||||
|
||||
data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),'user_data', 'gacha_log_data')
|
||||
|
||||
def id_generator():
|
||||
id = 1000000000000000000
|
||||
while True:
|
||||
id = id + 1
|
||||
yield str(id)
|
||||
|
||||
def convertUIGF(gachaLog, uid):
|
||||
UIGF_data = {}
|
||||
UIGF_data["info"] = {}
|
||||
UIGF_data["info"]["uid"] = uid
|
||||
UIGF_data["info"]["lang"] = "zh-cn"
|
||||
UIGF_data["info"]["export_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
UIGF_data["info"]["export_app"] = "genshin-gacha-export"
|
||||
UIGF_data["info"]["export_app_version"] = 'v2.5.0.02221942'
|
||||
UIGF_data["info"]["uigf_version"] = "v2.2"
|
||||
UIGF_data["info"]["export_timestamp"] = int(time.time())
|
||||
all_gachaDictList = []
|
||||
|
||||
for gacha_type in gachaQueryTypeIds:
|
||||
gacha_log = gachaLog.get(gacha_type, [])
|
||||
gacha_log = sorted(gacha_log, key=lambda gacha: gacha["time"], reverse=True)
|
||||
gacha_log.reverse()
|
||||
for gacha in gacha_log:
|
||||
gacha["uigf_gacha_type"] = gacha_type
|
||||
all_gachaDictList.extend(gacha_log)
|
||||
all_gachaDictList = sorted(all_gachaDictList, key=lambda gacha: gacha["time"])
|
||||
|
||||
id = id_generator()
|
||||
for gacha in all_gachaDictList:
|
||||
if gacha.get("id", "") == "":
|
||||
gacha["id"] = next(id)
|
||||
all_gachaDictList = sorted(all_gachaDictList, key=lambda gacha: gacha["id"])
|
||||
UIGF_data["list"] = all_gachaDictList
|
||||
return UIGF_data
|
||||
|
||||
def writeXLSX(uid, gachaLog, gachaTypeIds):
|
||||
t = time.strftime("%Y%m%d%H%M%S", time.localtime())
|
||||
workbook = xlsxwriter.Workbook(os.path.join(data_path, f"gachaExport-{uid}.xlsx"))
|
||||
for id in gachaTypeIds:
|
||||
gachaDictList = gachaLog[id]
|
||||
gachaTypeName = gachaQueryTypeDict[id]
|
||||
gachaDictList.reverse()
|
||||
worksheet = workbook.add_worksheet(gachaTypeName)
|
||||
content_css = workbook.add_format({"align": "left", "font_name": "微软雅黑", "border_color": "#c4c2bf", "bg_color": "#ebebeb", "border": 1})
|
||||
title_css = workbook.add_format({"align": "left", "font_name": "微软雅黑", "color": "#757575", "bg_color": "#dbd7d3", "border_color": "#c4c2bf", "border": 1, "bold": True})
|
||||
excel_header = ["时间", "名称", "类别", "星级", "祈愿类型", "总次数", "保底内"]
|
||||
worksheet.set_column("A:A", 22)
|
||||
worksheet.set_column("B:B", 14)
|
||||
worksheet.set_column("E:E", 14)
|
||||
worksheet.write_row(0, 0, excel_header, title_css)
|
||||
worksheet.freeze_panes(1, 0)
|
||||
counter = 0
|
||||
pity_counter = 0
|
||||
for gacha in gachaDictList:
|
||||
time_str = gacha["time"]
|
||||
name = gacha["name"]
|
||||
item_type = gacha["item_type"]
|
||||
rank_type = gacha["rank_type"]
|
||||
gacha_type = gacha["gacha_type"]
|
||||
uid = gacha["uid"]
|
||||
gacha_type_name = gacha_type_dict.get(gacha_type, "")
|
||||
counter = counter + 1
|
||||
pity_counter = pity_counter + 1
|
||||
excel_data = [time_str, name, item_type, rank_type, gacha_type_name, counter, pity_counter]
|
||||
excel_data[3] = int(excel_data[3])
|
||||
worksheet.write_row(counter, 0, excel_data, content_css)
|
||||
if excel_data[3] == 5:
|
||||
pity_counter = 0
|
||||
|
||||
star_5 = workbook.add_format({"color": "#bd6932", "bold": True})
|
||||
star_4 = workbook.add_format({"color": "#a256e1", "bold": True})
|
||||
star_3 = workbook.add_format({"color": "#8e8e8e"})
|
||||
first_row = 1 # 不包含表头第一行 (zero indexed)
|
||||
first_col = 0 # 第一列
|
||||
last_row = len(gachaDictList) # 最后一行
|
||||
last_col = len(excel_header) - 1 # 最后一列,zero indexed 所以要减 1
|
||||
worksheet.conditional_format(first_row, first_col, last_row, last_col, {"type": "formula", "criteria": "=$D2=5", "format": star_5})
|
||||
worksheet.conditional_format(first_row, first_col, last_row, last_col, {"type": "formula", "criteria": "=$D2=4", "format": star_4})
|
||||
worksheet.conditional_format(first_row, first_col, last_row, last_col, {"type": "formula", "criteria": "=$D2=3", "format": star_3})
|
||||
|
||||
worksheet = workbook.add_worksheet("原始数据")
|
||||
raw_data_header = ["count", "gacha_type", "id", "item_id", "item_type", "lang", "name", "rank_type", "time", "uid", "uigf_gacha_type"]
|
||||
worksheet.write_row(0, 0, raw_data_header)
|
||||
|
||||
UIGF_data = convertUIGF(gachaLog, uid)
|
||||
all_gachaDictList = UIGF_data["list"]
|
||||
all_counter = 0
|
||||
|
||||
for gacha in all_gachaDictList:
|
||||
count = gacha.get("count", "")
|
||||
gacha_type = gacha.get("gacha_type", "")
|
||||
id = gacha.get("id", "")
|
||||
item_id = gacha.get("item_id", "")
|
||||
item_type = gacha.get("item_type", "")
|
||||
lang = gacha.get("lang", "")
|
||||
name = gacha.get("name", "")
|
||||
rank_type = gacha.get("rank_type", "")
|
||||
time_str = gacha.get("time", "")
|
||||
uid = gacha.get("uid", "")
|
||||
uigf_gacha_type = gacha.get("uigf_gacha_type", "")
|
||||
|
||||
excel_data = [count, gacha_type, id, item_id, item_type, lang, name, rank_type, time_str, uid, uigf_gacha_type]
|
||||
worksheet.write_row(all_counter + 1, 0, excel_data)
|
||||
all_counter += 1
|
||||
|
||||
workbook.close()
|
153
hoshino/modules/Genshin_Paimon/gacha_log_export/__init__.py
Normal file
@ -0,0 +1,153 @@
|
||||
import json,os,re
|
||||
from hoshino import R,MessageSegment,logger, Service
|
||||
from hoshino.typing import CQEvent, Message
|
||||
from ..util import get_uid_by_qq, update_last_query_to_qq
|
||||
from .gacha_logs import get_data
|
||||
from .get_img import get_gacha_log_img
|
||||
from .api import toApi, checkApi
|
||||
|
||||
sv = Service('原神抽卡记录导出')
|
||||
|
||||
data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),'user_data', 'gacha_log_data')
|
||||
if not os.path.exists(data_path):
|
||||
os.makedirs(data_path)
|
||||
if not os.path.exists(os.path.join(data_path, 'user_gacha_log.json')):
|
||||
with open(os.path.join(data_path, 'user_gacha_log.json'),'w',encoding='UTF-8') as f:
|
||||
json.dump({},f,ensure_ascii=False)
|
||||
|
||||
@sv.on_prefix('导出抽卡记录', 'dcckjl', 'exportgachalog')
|
||||
async def ckjl(bot,ev):
|
||||
if ev.message_type != 'group':
|
||||
await bot.send(ev,'在群聊中才能导出抽卡记录文件哦!')
|
||||
return
|
||||
msg = ev.message.extract_plain_text().strip().split(' ')
|
||||
uid = ''
|
||||
if len(msg[0]) == 9 and msg[0].isdigit():
|
||||
uid = msg[0]
|
||||
if len(msg) >= 2:
|
||||
filetype = msg[1]
|
||||
else:
|
||||
filetype = 'xlsx'
|
||||
else:
|
||||
filetype = msg[0]
|
||||
if not filetype or filetype not in ['xlsx', 'json']:
|
||||
filetype = 'xlsx'
|
||||
qq = str(ev.user_id)
|
||||
if ev.message_type == 'guild':
|
||||
rm = str(ev.message)
|
||||
else:
|
||||
rm = str(ev.raw_message)
|
||||
match = re.search(r"\[CQ:at,qq=(.*)\]", rm)
|
||||
if match:
|
||||
uid = ''
|
||||
qq = str(match.group(1))
|
||||
if not uid:
|
||||
uid = get_uid_by_qq(qq)
|
||||
if not uid:
|
||||
await bot.send(ev,'请把uid给派蒙哦,比如抽卡记录导出100000001 xlsx',at_sender=True)
|
||||
return
|
||||
update_last_query_to_qq(qq, uid)
|
||||
if filetype == 'xlsx':
|
||||
filetype = f'gachaExport-{uid}.xlsx'
|
||||
else:
|
||||
filetype = f'UIGF_gachaData-{uid}.json'
|
||||
local_data = os.path.join(data_path, filetype)
|
||||
if not os.path.exists(local_data):
|
||||
await bot.send(ev, '你在派蒙这里还没有抽卡记录哦,使用 更新抽卡记录 吧!', at_sender=True)
|
||||
return
|
||||
await bot.upload_group_file(group_id=ev.group_id, file=local_data, name=filetype)
|
||||
|
||||
@sv.on_prefix(('更新抽卡记录', '获取抽卡记录', 'updategachalog', 'gxckjl'))
|
||||
async def update_ckjl(bot,ev):
|
||||
msg = ev.message.extract_plain_text().strip().split(' ')
|
||||
uid = ''
|
||||
if len(msg[0]) == 9 and msg[0].isdigit():
|
||||
uid = msg[0]
|
||||
if len(msg) >= 2:
|
||||
url = msg[1]
|
||||
else:
|
||||
url = ''
|
||||
else:
|
||||
url = msg[0]
|
||||
qq = str(ev.user_id)
|
||||
if not uid:
|
||||
uid = get_uid_by_qq(qq)
|
||||
if not uid:
|
||||
await bot.send(ev,'请把uid给派蒙哦,比如获取抽卡记录100000001 链接',at_sender=True)
|
||||
return
|
||||
if url:
|
||||
match = re.search(r'(https://webstatic.mihoyo.com/.*#/log)', url)
|
||||
if match:
|
||||
url = str(match.group(1))
|
||||
else:
|
||||
await bot.send(ev,'你这个抽卡链接不对哦,应该是以https://webstatic.mihoyo开头的!', at_sender=True)
|
||||
return
|
||||
else:
|
||||
with open(os.path.join(data_path, 'user_gacha_log.json'), 'r', encoding="utf-8") as f:
|
||||
user_data = json.load(f)
|
||||
if qq in user_data and uid in user_data[qq]:
|
||||
url = user_data[qq][uid]
|
||||
await bot.send(ev,'发现历史抽卡记录链接,尝试使用...')
|
||||
else:
|
||||
await bot.send(ev, '拿到游戏抽卡记录链接后,对派蒙说[获取抽卡记录 uid 链接]就可以啦\n获取抽卡记录链接的方式和vx小程序的是一样的,还请旅行者自己搜方法', at_sender=True)
|
||||
return
|
||||
with open(os.path.join(data_path, 'user_gacha_log.json'), 'r', encoding="utf-8") as f:
|
||||
user_data = json.load(f)
|
||||
if qq not in user_data:
|
||||
user_data[qq] = {}
|
||||
user_data[qq][uid] = url
|
||||
with open(os.path.join(data_path, 'user_gacha_log.json'), 'w', encoding="utf-8") as f:
|
||||
json.dump(user_data, f, ensure_ascii=False, sort_keys=False, indent=4)
|
||||
|
||||
url = toApi(url)
|
||||
apiRes = await checkApi(url)
|
||||
if apiRes != 'OK':
|
||||
await bot.send(ev,apiRes,at_sender=True)
|
||||
return
|
||||
await bot.send(ev, '抽卡记录开始获取,请给派蒙一点时间...')
|
||||
await get_data(url)
|
||||
|
||||
local_data = os.path.join(data_path, f'gachaData-{uid}.json')
|
||||
with open(local_data, 'r', encoding="utf-8") as f:
|
||||
gacha_data = json.load(f)
|
||||
gacha_img = await get_gacha_log_img(gacha_data, 'all')
|
||||
await bot.send(ev, gacha_img, at_sender=True)
|
||||
|
||||
@sv.on_prefix('抽卡记录', 'ckjl', 'gachalog')
|
||||
async def get_ckjl(bot,ev):
|
||||
msg = ev.message.extract_plain_text().strip().split(' ')
|
||||
uid = ''
|
||||
if len(msg[0]) == 9 and msg[0].isdigit():
|
||||
uid = msg[0]
|
||||
if len(msg) >= 2:
|
||||
pool = msg[1]
|
||||
else:
|
||||
pool = 'all'
|
||||
else:
|
||||
pool = msg[0]
|
||||
if not pool or pool not in ['all', '角色', '武器', '常驻', '新手']:
|
||||
pool = 'all'
|
||||
qq = str(ev.user_id)
|
||||
if ev.message_type == 'guild':
|
||||
rm = str(ev.message)
|
||||
else:
|
||||
rm = str(ev.raw_message)
|
||||
match = re.search(r"\[CQ:at,qq=(.*)\]", rm)
|
||||
if match:
|
||||
uid = ''
|
||||
qq = str(match.group(1))
|
||||
if not uid:
|
||||
uid = get_uid_by_qq(qq)
|
||||
if not uid:
|
||||
await bot.send(ev,'请把uid给派蒙哦,比如抽卡记录100000001 all',at_sender=True)
|
||||
return
|
||||
update_last_query_to_qq(qq, uid)
|
||||
|
||||
local_data = os.path.join(data_path, f'gachaData-{uid}.json')
|
||||
if not os.path.exists(local_data):
|
||||
await bot.send(ev, '你在派蒙这里还没有抽卡记录哦,对派蒙说 获取抽卡记录 吧!', at_sender=True)
|
||||
return
|
||||
with open(local_data, 'r', encoding="utf-8") as f:
|
||||
gacha_data = json.load(f)
|
||||
gacha_img = await get_gacha_log_img(gacha_data, pool)
|
||||
await bot.send(ev, gacha_img, at_sender=True)
|
65
hoshino/modules/Genshin_Paimon/gacha_log_export/api.py
Normal file
@ -0,0 +1,65 @@
|
||||
import json
|
||||
from urllib import parse
|
||||
from hoshino import aiorequests
|
||||
|
||||
def toApi(url):
|
||||
spliturl = str(url).split("?")
|
||||
if "webstatic-sea" in spliturl[0] or "hk4e-api-os" in spliturl[0]:
|
||||
spliturl[0] = "https://hk4e-api-os.mihoyo.com/event/gacha_info/api/getGachaLog"
|
||||
else:
|
||||
spliturl[0] = "https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog"
|
||||
url = "?".join(spliturl)
|
||||
return url
|
||||
|
||||
|
||||
def getApi(url, gachaType, size, page, end_id=""):
|
||||
parsed = parse.urlparse(url)
|
||||
querys = parse.parse_qsl(str(parsed.query))
|
||||
param_dict = dict(querys)
|
||||
param_dict["size"] = size
|
||||
param_dict["gacha_type"] = gachaType
|
||||
param_dict["page"] = page
|
||||
param_dict["lang"] = "zh-cn"
|
||||
param_dict["end_id"] = end_id
|
||||
param = parse.urlencode(param_dict)
|
||||
path = str(url).split("?")[0]
|
||||
api = path + "?" + param
|
||||
return api
|
||||
|
||||
|
||||
async def checkApi(url):
|
||||
try:
|
||||
r = await aiorequests.get(url)
|
||||
s = (await r.content).decode("utf-8")
|
||||
j = json.loads(s)
|
||||
except Exception as e:
|
||||
# print("API请求解析出错:\n", traceback.format_exc())
|
||||
return f'API请求解析出错:{e}'
|
||||
|
||||
if not j["data"]:
|
||||
if j["message"] == "authkey valid error":
|
||||
#print("authkey错误")
|
||||
return "authkey错误,请重新获取链接给派蒙!"
|
||||
elif j["message"] == "authkey timeout":
|
||||
return "authkey已过期,请重新获取链接给派蒙!"
|
||||
else:
|
||||
# print("数据为空,错误代码:" + j["message"])
|
||||
return f'数据为空,错误代码:{j["message"]}'
|
||||
return 'OK'
|
||||
|
||||
def getQueryVariable(variable):
|
||||
query = str(url).split("?")[1]
|
||||
vars = query.split("&")
|
||||
for v in vars:
|
||||
if v.split("=")[0] == variable:
|
||||
return v.split("=")[1]
|
||||
return ""
|
||||
|
||||
def getGachaInfo():
|
||||
region = getQueryVariable("region")
|
||||
lang = getQueryVariable("lang")
|
||||
gachaInfoUrl = "https://webstatic.mihoyo.com/hk4e/gacha_info/{}/items/{}.json".format(region, lang)
|
||||
r = requests.get(gachaInfoUrl)
|
||||
s = r.content.decode("utf-8")
|
||||
gachaInfo = json.loads(s)
|
||||
return gachaInfo
|
@ -0,0 +1,95 @@
|
||||
import os
|
||||
import json
|
||||
from asyncio import sleep
|
||||
from hoshino import aiorequests
|
||||
from .api import toApi, getApi, checkApi
|
||||
from .meta_data import gachaQueryTypeIds, gachaQueryTypeNames, gachaQueryTypeDict
|
||||
from .UIGF_and_XLSX import convertUIGF, writeXLSX
|
||||
|
||||
data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),'user_data', 'gacha_log_data')
|
||||
|
||||
async def getGachaLogs(url, gachaTypeId):
|
||||
size = "20"
|
||||
# api限制一页最大20
|
||||
gachaList = []
|
||||
end_id = "0"
|
||||
for page in range(1, 9999):
|
||||
api = getApi(url, gachaTypeId, size, page, end_id)
|
||||
r = await aiorequests.get(api)
|
||||
s = (await r.content).decode("utf-8")
|
||||
j = json.loads(s)
|
||||
gacha = j["data"]["list"]
|
||||
if not len(gacha):
|
||||
break
|
||||
for i in gacha:
|
||||
gachaList.append(i)
|
||||
end_id = j["data"]["list"][-1]["id"]
|
||||
await sleep(0.5)
|
||||
|
||||
return gachaList
|
||||
|
||||
def mergeDataFunc(localData, gachaData):
|
||||
|
||||
for banner in gachaQueryTypeDict:
|
||||
bannerLocal = localData["gachaLog"][banner]
|
||||
bannerGet = gachaData["gachaLog"][banner]
|
||||
if bannerGet == bannerLocal:
|
||||
pass
|
||||
else:
|
||||
# print("合并", gachaQueryTypeDict[banner], end=": ", flush=True)
|
||||
flaglist = [1] * len(bannerGet)
|
||||
loc = [[i["time"], i["name"]] for i in bannerLocal]
|
||||
for i in range(len(bannerGet)):
|
||||
gachaGet = bannerGet[i]
|
||||
get = [gachaGet["time"], gachaGet["name"]]
|
||||
if get in loc:
|
||||
pass
|
||||
else:
|
||||
flaglist[i] = 0
|
||||
|
||||
tempData = []
|
||||
for i in range(len(bannerGet)):
|
||||
if flaglist[i] == 0:
|
||||
gachaGet = bannerGet[i]
|
||||
tempData.insert(0, gachaGet)
|
||||
# print("追加", len(tempData), "条记录", flush=True)
|
||||
for i in tempData:
|
||||
localData["gachaLog"][banner].insert(0, i)
|
||||
|
||||
return localData
|
||||
|
||||
|
||||
async def get_data(url):
|
||||
gachaData = {}
|
||||
gachaData["gachaLog"] = {}
|
||||
for gachaTypeId in gachaQueryTypeIds:
|
||||
gachaLog = await getGachaLogs(url, gachaTypeId)
|
||||
gachaData["gachaLog"][gachaTypeId] = gachaLog
|
||||
|
||||
uid_flag = 1
|
||||
for gachaType in gachaData["gachaLog"]:
|
||||
for log in gachaData["gachaLog"][gachaType]:
|
||||
if uid_flag and log["uid"]:
|
||||
gachaData["uid"] = log["uid"]
|
||||
uid_flag = 0
|
||||
|
||||
uid = gachaData["uid"]
|
||||
localDataFilePath = os.path.join(data_path, f"gachaData-{uid}.json")
|
||||
|
||||
if os.path.isfile(localDataFilePath):
|
||||
with open(localDataFilePath, "r", encoding="utf-8") as f:
|
||||
localData = json.load(f)
|
||||
mergeData = mergeDataFunc(localData, gachaData)
|
||||
else:
|
||||
mergeData = gachaData
|
||||
mergeData["gachaType"] = gachaQueryTypeDict
|
||||
# 写入json
|
||||
with open(localDataFilePath, "w", encoding="utf-8") as f:
|
||||
json.dump(mergeData, f, ensure_ascii=False, sort_keys=False, indent=4)
|
||||
# 写入UIFG json
|
||||
with open(os.path.join(data_path, f"UIGF_gachaData-{uid}.json"), "w", encoding="utf-8") as f:
|
||||
UIGF_data = convertUIGF(mergeData['gachaLog'], uid)
|
||||
json.dump(UIGF_data, f, ensure_ascii=False, sort_keys=False, indent=4)
|
||||
# 写入xlsx
|
||||
writeXLSX(uid, mergeData['gachaLog'], gachaQueryTypeIds)
|
||||
|
135
hoshino/modules/Genshin_Paimon/gacha_log_export/get_img.py
Normal file
@ -0,0 +1,135 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
from ..util import pil2b64
|
||||
from hoshino.typing import MessageSegment
|
||||
|
||||
res_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'res')
|
||||
|
||||
def get_font(size):
|
||||
return ImageFont.truetype(os.path.join(res_path, 'msyh.ttc'), size)
|
||||
|
||||
async def get_circle_avatar(avatar, size):
|
||||
avatar = Image.open(os.path.join(res_path, 'role_avatar', f'{avatar}.png'))
|
||||
w, h = avatar.size
|
||||
bg = Image.new('RGBA', (w, h), (213, 153, 77, 255))
|
||||
bg.alpha_composite(avatar, (0, 0))
|
||||
bg = bg.resize((size, size))
|
||||
scale = 5
|
||||
mask = Image.new('L', (size * scale, size * scale), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.ellipse((0, 0, size * scale, size * scale), fill=255)
|
||||
mask = mask.resize((size, size), Image.ANTIALIAS)
|
||||
ret_img = bg.copy()
|
||||
ret_img.putalpha(mask)
|
||||
return ret_img
|
||||
|
||||
async def sort_data(gacha_data):
|
||||
sprog_data = {'type': '新手', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
|
||||
permanent_data = {'type': '常驻', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
|
||||
role_data = {'type': '角色', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
|
||||
weapon_data = {'type': '武器', 'total_num': 0, '5_star': [], '4_star': [], '5_gacha': 0, '4_gacha': 0}
|
||||
new_gacha_data = [sprog_data, permanent_data, role_data, weapon_data]
|
||||
i = 0
|
||||
for pool in gacha_data['gachaLog'].values():
|
||||
pool.reverse()
|
||||
new_gacha_data[i]['total_num'] = len(pool)
|
||||
for p in pool:
|
||||
if p['rank_type'] == "5":
|
||||
new_gacha_data[i]['5_star'].append((p['name'], new_gacha_data[i]['5_gacha'] + 1))
|
||||
new_gacha_data[i]['5_gacha'] = 0
|
||||
new_gacha_data[i]['4_gacha'] = 0
|
||||
elif p['rank_type'] == "4":
|
||||
new_gacha_data[i]['4_star'].append((p['name'], new_gacha_data[i]['4_gacha'] + 1))
|
||||
new_gacha_data[i]['5_gacha'] += 1
|
||||
new_gacha_data[i]['4_gacha'] = 0
|
||||
else:
|
||||
new_gacha_data[i]['5_gacha'] += 1
|
||||
new_gacha_data[i]['4_gacha'] += 1
|
||||
i += 1
|
||||
return new_gacha_data
|
||||
|
||||
async def draw_gacha_log(data):
|
||||
if data['total_num'] == 0:
|
||||
return None
|
||||
top = Image.open(os.path.join(res_path, 'player_card', 'gacha_log_top.png'))
|
||||
mid = Image.open(os.path.join(res_path, 'player_card', '卡片身体.png')).resize((768, 80))
|
||||
bottom = Image.open(os.path.join(res_path, 'player_card', '卡片底部.png')).resize((768, 51))
|
||||
five_star = data['5_star']
|
||||
col = int(len(five_star) / 6)
|
||||
if not len(five_star) % 6 == 0:
|
||||
col += 1
|
||||
top_draw = ImageDraw.Draw(top)
|
||||
top_draw.text((348, 30), f'{data["type"]}池', font=get_font(24), fill='#F8F5F1')
|
||||
top_draw.text((146 - 6 * len(str(data["total_num"])), 88), f'{data["total_num"]}', font=get_font(24), fill='black')
|
||||
five_ave = round(sum([x[1] for x in five_star]) / len(five_star), 1) if five_star else ' '
|
||||
top_draw.text((321 - 10 * len(str(five_ave)), 88), f'{five_ave}', font=get_font(24), fill='black' if five_ave != ' ' and five_ave > 60 else 'red')
|
||||
five_per = round(len(five_star) / (data['total_num'] - data['5_gacha']), 4) if five_star else -1
|
||||
five_per_str = str(five_per * 100) + '%' if five_per > -1 else ' '
|
||||
top_draw.text((427, 88), f'{five_per_str}', font=get_font(24), fill='black' if five_per < 0.016 else 'red')
|
||||
five_up = round(len([x[0] for x in five_star if not x[0] in ['刻晴', '迪卢克', '七七', '莫娜', '琴']]) / len(five_star),
|
||||
4) if five_star else -1
|
||||
five_up_str = str(five_up * 100) + '%' if five_per > -1 else ' '
|
||||
top_draw.text((578 if len(five_up_str) != 6 else 569, 88), f'{five_up_str}', font=get_font(24), fill='black' if five_up < 0.75 else 'red')
|
||||
most_five = sorted(five_star, key=lambda x: x[1], reverse=False)[0][0] if five_star else ' '
|
||||
top_draw.text((152 - 14 * len(most_five), 163), f'{most_five}', font=get_font(24), fill='red')
|
||||
four_ave = round(sum([x[1] for x in data['4_star']]) / len(data['4_star']), 1) if data['4_star'] else ' '
|
||||
top_draw.text((316 - 10 * len(str(four_ave)), 163), f'{four_ave}', font=get_font(24), fill='black' if four_ave != ' ' and four_ave > 7 else 'red')
|
||||
top_draw.text((461 - 6 * len(str(data['5_gacha'])), 163), f'{data["5_gacha"]}', font=get_font(24), fill='black')
|
||||
top_draw.text((604, 163), f'{data["4_gacha"]}', font=get_font(24), fill='black')
|
||||
bg_img = Image.new('RGBA', (768, 288 + col * 80 - (20 if col > 0 else 0) + 51), (0, 0, 0, 0))
|
||||
bg_img.paste(top, (0, 0))
|
||||
for i in range(0, col):
|
||||
bg_img.paste(mid, (0, 288 + i * 80))
|
||||
bg_img.paste(bottom, (0, 288 + col * 80 - (20 if col > 0 else 0)))
|
||||
bg_draw = ImageDraw.Draw(bg_img)
|
||||
n = 0
|
||||
for c in five_star:
|
||||
avatar = await get_circle_avatar(c[0], 45)
|
||||
if c[1] <= 20:
|
||||
color = 'red'
|
||||
elif 20 < c[1] <= 50:
|
||||
color = 'orangered'
|
||||
elif 50 < c[1] < 70:
|
||||
color = 'darkorange'
|
||||
else:
|
||||
color = 'black'
|
||||
bg_img.alpha_composite(avatar, (30 + 120 * (n % 6), 298 + 80 * int(n / 6)))
|
||||
bg_draw.text((111 + 120 * (n % 6) - 8 * len(c[0]), 298 + 80 * int(n / 6)), f'{c[0]}', font=get_font(16), fill=color)
|
||||
bg_draw.text((107 - 5 * len(str(c[1])) + 120 * (n % 6), 317 + 80 * int(n / 6)), f'[{c[1]}]', font=get_font(16), fill=color)
|
||||
n += 1
|
||||
return bg_img
|
||||
|
||||
async def get_gacha_log_img(gacha_data, pool):
|
||||
all_gacha_data = await sort_data(gacha_data)
|
||||
if pool != 'all':
|
||||
for pd in all_gacha_data:
|
||||
if pd['type'] == pool:
|
||||
img = await draw_gacha_log(pd)
|
||||
break
|
||||
if not img:
|
||||
return '这个池子没有抽卡记录哦'
|
||||
total_height = (img.size)[1]
|
||||
else:
|
||||
img_list = []
|
||||
total_height = 0
|
||||
now_height = 0
|
||||
for pd in all_gacha_data:
|
||||
p_img = await draw_gacha_log(pd)
|
||||
if p_img:
|
||||
img_list.append(p_img)
|
||||
total_height += (p_img.size)[1]
|
||||
if not img_list:
|
||||
return '没有找到任何抽卡记录诶!'
|
||||
img = Image.new('RGBA', (768, total_height), (0, 0, 0, 255))
|
||||
for i in img_list:
|
||||
img.paste(i, (0, now_height))
|
||||
now_height += (i.size)[1]
|
||||
img_draw = ImageDraw.Draw(img)
|
||||
img_draw.text((595, 44), f'UID:{gacha_data["uid"]}', font=get_font(16), fill='black')
|
||||
img_draw.text((530, total_height - 45), 'Created by 惜月の小派蒙', font=get_font(16), fill='black')
|
||||
|
||||
img = pil2b64(img, 95)
|
||||
img = MessageSegment.image(img)
|
||||
return img
|
||||
|
||||
|
10
hoshino/modules/Genshin_Paimon/gacha_log_export/meta_data.py
Normal file
@ -0,0 +1,10 @@
|
||||
gachaQueryTypeIds = ["100", "200", "301", "302"]
|
||||
gachaQueryTypeNames = ["新手祈愿", "常驻祈愿", "角色活动祈愿", "武器活动祈愿"]
|
||||
gachaQueryTypeDict = dict(zip(gachaQueryTypeIds, gachaQueryTypeNames))
|
||||
gacha_type_dict = {
|
||||
"100": "新手祈愿",
|
||||
"200": "常驻祈愿",
|
||||
"301": "角色活动祈愿",
|
||||
"302": "武器活动祈愿",
|
||||
"400": "角色活动祈愿-2",
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
import json
|
||||
import urllib
|
||||
import requests
|
||||
from asyncio import sleep
|
||||
import time
|
||||
import os
|
||||
import hoshino,os
|
||||
from hoshino import R,MessageSegment,aiorequests,logger,Service
|
||||
|
||||
sv = Service('原神抽卡记录导出')
|
||||
|
||||
def checkApi(url) -> bool:
|
||||
if not url:
|
||||
#print("url为空")
|
||||
return "url为空"
|
||||
if "getGachaLog" not in url:
|
||||
#print("错误的url,检查是否包含getGachaLog")
|
||||
return "错误的url,检查是否包含getGachaLog"
|
||||
try:
|
||||
r = requests.get(url)
|
||||
s = r.content.decode("utf-8")
|
||||
j = json.loads(s)
|
||||
except Exception as e:
|
||||
#print("API请求解析出错:" + str(e))
|
||||
return "API请求解析出错:" + str(e)
|
||||
|
||||
if not j["data"]:
|
||||
if j["message"] == "authkey valid error":
|
||||
return "authkey错误"
|
||||
else:
|
||||
return "数据为空,错误代码:" + j["message"]
|
||||
return "OK"
|
||||
|
||||
def getApi(url: str, gachaType: str, size: str, page: int, end_id="") -> str:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
querys = urllib.parse.parse_qsl(parsed.query)
|
||||
param_dict = dict(querys)
|
||||
param_dict["size"] = size
|
||||
param_dict["gacha_type"] = gachaType
|
||||
param_dict["page"] = page
|
||||
param_dict["lang"] = "zh-cn"
|
||||
param_dict["end_id"] = end_id
|
||||
param = urllib.parse.urlencode(param_dict)
|
||||
path = url.split("?")[0]
|
||||
api = path + "?" + param
|
||||
return api
|
||||
|
||||
|
||||
async def getGachaLogs(url: str, gachaTypeId: str, gachaTypeDict: dict) -> list:
|
||||
size = "20"
|
||||
# api限制一页最大20
|
||||
gachaList = []
|
||||
gachaList_temp = []
|
||||
end_id = "0"
|
||||
c = 0
|
||||
for page in range(1, 9999):
|
||||
#print(f"正在获取 {gachaTypeDict[gachaTypeId]} 第 {page} 页", flush=True)
|
||||
api = getApi(url, gachaTypeId, size, page, end_id)
|
||||
r = requests.get(api)
|
||||
s = r.content.decode("utf-8")
|
||||
j = json.loads(s)
|
||||
gacha = j["data"]["list"]
|
||||
if not len(gacha):
|
||||
break
|
||||
for i in gacha:
|
||||
gachaList_temp.append(i)
|
||||
end_id = j["data"]["list"][-1]["id"]
|
||||
await sleep(0.3)
|
||||
if gachaList_temp:
|
||||
gachaList.append({'uid':gachaList_temp[0]['uid']})
|
||||
gachaList_temp.reverse()
|
||||
for i in gachaList_temp:
|
||||
c += 1
|
||||
if i['rank_type'] == "5":
|
||||
fivechar = {}
|
||||
fivechar['name'] = i['name']
|
||||
fivechar['item_type'] = i['item_type']
|
||||
fivechar['count'] = c
|
||||
c = 0
|
||||
gachaList.append(fivechar)
|
||||
gachaList.append({'未出货':c})
|
||||
return gachaList
|
||||
|
||||
# def getGachaTypes(url: str) -> list:
|
||||
# tmp_url = url.replace("getGachaLog", "getConfigList")
|
||||
# parsed = urllib.parse.urlparse(tmp_url)
|
||||
# querys = urllib.parse.parse_qsl(parsed.query)
|
||||
# param_dict = dict(querys)
|
||||
# param_dict["lang"] = "zh-cn"
|
||||
# param = urllib.parse.urlencode(param_dict)
|
||||
# path = tmp_url.split("?")[0]
|
||||
# tmp_url = path + "?" + param
|
||||
# r = requests.get(tmp_url)
|
||||
# s = r.content.decode("utf-8")
|
||||
# configList = json.loads(s)
|
||||
# return configList["data"]["gacha_type_list"]
|
||||
|
||||
# def mergeDataFunc(localData: dict, gachaData: dict) -> dict:
|
||||
# gachaTypes = gachaData["gachaType"]
|
||||
# gachaTypeIds = [banner["key"] for banner in gachaTypes]
|
||||
# gachaTypeNames = [banner["name"] for banner in gachaTypes]
|
||||
# gachaTypeDict = dict(zip(gachaTypeIds, gachaTypeNames))
|
||||
|
||||
# for banner in gachaTypeDict:
|
||||
# bannerLocal = localData["gachaLog"][banner]
|
||||
# bannerGet = gachaData["gachaLog"][banner]
|
||||
# if bannerGet == bannerLocal:
|
||||
# pass
|
||||
# else:
|
||||
# #print("合并", gachaTypeDict[banner])
|
||||
# flaglist = [1] * len(bannerGet)
|
||||
# loc = [[i["time"], i["name"]] for i in bannerLocal]
|
||||
# for i in range(len(bannerGet)):
|
||||
# gachaGet = bannerGet[i]
|
||||
# get = [gachaGet["time"], gachaGet["name"]]
|
||||
# if get in loc:
|
||||
# pass
|
||||
# else:
|
||||
# flaglist[i] = 0
|
||||
|
||||
# print("获取到", len(flaglist), "条记录")
|
||||
# tempData = []
|
||||
# for i in range(len(bannerGet)):
|
||||
# if flaglist[i] == 0:
|
||||
# gachaGet = bannerGet[i]
|
||||
# tempData.insert(0, gachaGet)
|
||||
# print("追加", len(tempData), "条记录")
|
||||
# for i in tempData:
|
||||
# localData["gachaLog"][banner].insert(0, i)
|
||||
|
||||
# return localData
|
||||
|
||||
@sv.on_prefix('原神抽卡记录导出')
|
||||
async def get_gacha_record(bot,ev):
|
||||
url = ev.message.extract_plain_text().strip()
|
||||
spliturl = url.split("?")
|
||||
spliturl[0] = "https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog"
|
||||
url = "?".join(spliturl)
|
||||
checkApimsg = checkApi(url)
|
||||
if checkApimsg == 'OK':
|
||||
await bot.send(ev,'获取抽卡记录中,请稍候...',at_sender=True)
|
||||
gachaTypes = {'200': '常驻祈愿', '301': '角色活动祈愿', '302': '武器活动祈愿'}
|
||||
gachaData = {}
|
||||
gachaData["gachaLog"] = {}
|
||||
|
||||
for gachaType in gachaTypes.items():
|
||||
gachaLog = await getGachaLogs(url, gachaType[0], gachaTypes)
|
||||
gachaData["gachaLog"][gachaType[1]] = gachaLog
|
||||
|
||||
uid_flag = 1
|
||||
for gachaType in gachaData["gachaLog"]:
|
||||
for log in gachaData["gachaLog"][gachaType]:
|
||||
if uid_flag and log["uid"]:
|
||||
gachaData["uid"] = log["uid"]
|
||||
uid_flag = 0
|
||||
|
||||
|
||||
#gen_path = os.path.dirname(os.path.realpath(sys.argv[0]))
|
||||
uid = gachaData["uid"]
|
||||
mergeData = gachaData
|
||||
msg = f'你的抽卡记录如下\nuid: {uid}\n'
|
||||
for gachaType in gachaTypes.values():
|
||||
if gachaType in mergeData['gachaLog']:
|
||||
msg += gachaType +': '
|
||||
if len(mergeData['gachaLog'][gachaType]) <= 2:
|
||||
msg += '没有五星记录'
|
||||
else:
|
||||
for role in mergeData['gachaLog'][gachaType]:
|
||||
if 'name' in role:
|
||||
msg += f"{role['name']}({role['count']}) "
|
||||
msg += '\n'
|
||||
await bot.send(ev,msg,at_sender=True)
|
||||
else:
|
||||
await bot.send(ev,checkApimsg,at_sender=True)
|
BIN
hoshino/modules/Genshin_Paimon/res/player_card/gacha_log_top.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/74544.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/89514.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/89543.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/92544.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/95452.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者100555.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者100556.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者101.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者102.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者103.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者104.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者105.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者106.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者10602.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/旅行者10605.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔10341.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔10342.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔10343.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔341.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔342.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔343.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔344.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔345.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/reliquaries/诺艾尔346.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/七七.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/丽莎.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/九条裟罗.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/云堇.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/五郎.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/优菈.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/八重神子.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/凝光.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/凯亚.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/刻晴.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/北斗.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/可莉.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/埃洛伊.png
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/安柏.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/宵宫.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/托马.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/早柚.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/枫原万叶.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/温迪.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/烟绯.png
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/珊瑚宫心海.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/班尼特.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/琴.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/甘雨.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/申鹤.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/砂糖.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/神里绫人.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/神里绫华.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/空.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/罗莎莉亚.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/胡桃.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/芭芭拉.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/荒泷一斗.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/荧.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/莫娜.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/菲谢尔.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/行秋.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/诺艾尔.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/辛焱.png
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/达达利亚.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/迪卢克.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/迪奥娜.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/重云.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/钟离.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/阿贝多.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/雷泽.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/雷电将军.png
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/香菱.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
hoshino/modules/Genshin_Paimon/res/role_avatar/魈.png
Normal file
After Width: | Height: | Size: 90 KiB |
@ -18,3 +18,5 @@ uuid>=1.30
|
||||
filetype
|
||||
imageio
|
||||
loguru
|
||||
xlsxwriter>=1.3.7
|
||||
urllib3>=1.26.8
|