增加战地六战绩全套构建

This commit is contained in:
sansenhoshi 2025-12-24 11:30:48 +08:00
parent 92f6a8bfb4
commit 9ee37493fe
18 changed files with 4612 additions and 259 deletions

43
.env
View File

@ -1,15 +1,14 @@
DEBUG=true DEBUG=true
HOST=0.0.0.0 # 配置 NoneBot 监听的 IP / 主机名 HOST=0.0.0.0
PORT=43001 # 配置 NoneBot 监听的端口 PORT=43001
COMMAND_START=["","/"] # 配置命令起始字符 COMMAND_START=["","/"]
COMMAND_SEP=["."] # 配置命令分割字符 COMMAND_SEP=["."]
DRIVER=~fastapi+~httpx DRIVER=~fastapi+~httpx
LOG_LEVEL=DEBUG LOG_LEVEL=DEBUG
SUPERUSERS=[]
SUPERUSERS=[] # 超级管理员 NICKNAME=[]
NICKNAME=[] # 机器人昵称
# QQ官方机器人配置文件 # QQ官方机器人配置文件
# QQ_IS_SANDBOX=true # QQ_IS_SANDBOX=true
@ -21,18 +20,30 @@ QQ_BOTS='[{
"c2c_group_at_messages": true "c2c_group_at_messages": true
}, },
"use_websocket": false "use_websocket": false
}]' }
]'
APSCHEDULER_CONFIG={"apscheduler.timezone": "Asia/Shanghai"} APSCHEDULER_CONFIG={"apscheduler.timezone": "Asia/Shanghai"}
# .env.prod # .env.prod
savedata = data/ram_data # 保存路径,相对路径,此处为保存至运行目录下的 "Yuni/savedata/" 下,默认为 "" # 保存路径,相对路径,此处为保存至运行目录下的 "Yuni/savedata/" 下,默认为 ""
ram_policy = 0 # 授权策略 0 为根据可用功能 1 为根据服务级别,默认为 0
ram_cmd = ram # 指令名,或者叫触发词,默认为 ram
ram_add = -a # 启用功能(根据可用功能),默认为 -a
ram_rm = -r # 禁用功能(根据可用功能),默认为 -r
ram_show = -s # 展示群功能状态(根据可用功能),默认为 -s
ram_available = -v # 展示全局可用功能(根据可用功能),默认为 -v
savedata=data/ram_data
# 授权策略 0 为根据可用功能 1 为根据服务级别,默认为 0
ram_policy=0
# 指令名,或者叫触发词,默认为 ram
ram_cmd=ram
# 启用功能(根据可用功能),默认为 -a
ram_add=-a
# 禁用功能(根据可用功能),默认为 -r
ram_rm=-r
# 展示群功能状态(根据可用功能),默认为 -s
ram_show=-s
# 展示全局可用功能(根据可用功能),默认为 -v
ram_available=-v

3
bot.py
View File

@ -1,5 +1,6 @@
import nonebot import nonebot
from nonebot.adapters.qq import Adapter as QQ from nonebot.adapters.qq import Adapter as QQ
from nonebot.adapters.onebot.v11 import Adapter as Onebot
from nonebot.log import logger from nonebot.log import logger
# 初始化 NoneBot 以及 数据库 # 初始化 NoneBot 以及 数据库
@ -9,7 +10,7 @@ app = nonebot.get_asgi()
# 注册适配器 # 注册适配器
driver = nonebot.get_driver() driver = nonebot.get_driver()
driver.register_adapter(QQ) driver.register_adapter(QQ)
driver.register_adapter(Onebot)
# 加载自定义插件 # 加载自定义插件
nonebot.load_plugins("src/plugins") # 加载bot自定义插件 nonebot.load_plugins("src/plugins") # 加载bot自定义插件

View File

@ -37,7 +37,9 @@ nonebug-saa = { git = "https://github.com/MountainDash/nonebug-saa.git" }
[tool.nonebot] [tool.nonebot]
adapters = [ adapters = [
{ name = "QQ", module_name = "nonebot.adapter.qq" } { name = "QQ", module_name = "nonebot.adapter.qq" },
{ name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" },
{ name = "OneBot V12", module_name = "nonebot.adapters.onebot.v12" },
] ]
plugins = [ plugins = [

View File

@ -1,7 +1,7 @@
import time import time
from nonebot import on_command, require from nonebot import on_command, require
from nonebot.adapters import Message from nonebot.adapters import Message, Event, Bot
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.params import CommandArg from nonebot.params import CommandArg
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
@ -9,6 +9,8 @@ from nonebot.rule import to_me
from nonebot.log import logger from nonebot.log import logger
import json import json
from .user_data.data_utils import UserManager
require("nonebot_plugin_alconna") require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
from .data import * from .data import *
@ -19,14 +21,26 @@ from .text_utils import *
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="BF查询", name="BF查询",
description="战地341520426", description="战地341520426",
usage="", usage="""
bf3: /bf3 EAID (查询BF3数据)
bf4: /bf4 EAID (查询BF4数据)
bf1: /bf1 EAID (查询BF5数据)
bfv: /bfv EAID (查询BF6数据)
bf2042: /bf2042 EAID (查询BF2042数据)
bf6: /bf6 EAID (查询BF6数据)
绑定: /绑定 EAID (绑定你的QQ与EAID)
解绑: /解绑 (解除你的QQ与当前绑定的EAID)
修改绑定: /修改绑定 EAID (修改你的QQ与当前绑定的EAID)
""".strip(),
extra={ extra={
}, },
) )
query = on_command("bft", rule=to_me(), aliases={"bf3", "bf4", "bfv", "bf1", "bf2042", "bf6"}, block=True) query = on_command("bft", rule=to_me(), aliases={"bf3", "bf4", "bfv", "bf1", "bf2042", "bf6"}, block=True)
bind = on_command("bind", rule=to_me(), aliases={"绑定"}, block=True) bind = on_command("bfbind", rule=to_me(), aliases={"绑定"}, block=True)
unbind = on_command("unbind", rule=to_me(), aliases={"解绑"}, block=True)
update_bind = on_command("update_bind", rule=to_me(), aliases={"修改绑定"}, block=True)
bf_dict = { bf_dict = {
"bf3": "战地3", "bf3": "战地3",
@ -39,54 +53,73 @@ bf_dict = {
@query.handle() @query.handle()
async def handle_function(matcher: Matcher, msg: Message = CommandArg()): async def handle_function(event: Event, matcher: Matcher, msg: Message = CommandArg()):
start_time = time.time()
usermanager = UserManager()
cmd = matcher.state["_prefix"]["command"][0] cmd = matcher.state["_prefix"]["command"][0]
game = cmd game = cmd
content = msg.extract_plain_text() input_text = msg.extract_plain_text().strip()
play_stat = "" logger.info(f"{type(input_text)}, {repr(input_text)}")
if cmd == "bf3": if input_text is None or input_text == "":
play_stat = await get_data_bf3(content, "pc") content = usermanager.get_user_by_qq(event.get_user_id())
elif cmd == "bf4": if not content:
play_stat = await get_data_bf4(content, "pc") await UniMessage.at(user_id=event.get_user_id()).text(
elif cmd == "bf1": "未检测到id也未检测到绑定记录请使用 绑定 EAID 进行绑定操作").send()
play_stat = await get_data_bf1(content, "pc") return
elif cmd == "bfv": player_id = content['ea_player_id']
play_stat = await get_data_bfv(content, "pc") user_id = content['ea_user_id']
elif cmd == "bf2042": player = content['ea_player_name']
play_stat = await get_data_bfv(content, "pc") platform = "pc"
elif cmd == "bf6":
flag, play_stat = await get_data_bf6(content, 0)
if flag == 0:
msg = '检测到多个同名用户\n' + '\n'.join(
f'用户名:{info["name"]}-等级:{info["rank"]}-UID:{info["uid"]}' for info in play_stat)
await UniMessage.text(msg).finish()
elif flag == 2:
msg = "未找到该玩家名"
await UniMessage.text(msg).finish()
else: else:
await UniMessage.text("指令异常").finish() content = await get_player_info_by_name(input_text, "pc")
if 'errors' in content:
await UniMessage.at(user_id=event.get_user_id()).text("未找到玩家,请检查是否拼写正确").send()
return
player_id = content['personaId']
user_id = content['userId']
player = content['personaName'] if 'personaName' in content else content['name']
platform = "pc"
await UniMessage.text(f"正在查询 {player}{bf_dict[cmd]} 数据,请耐心等待").send()
play_stat = ""
try:
if cmd == "bf3":
play_stat = await get_data_bf3(player_id, user_id, platform)
elif cmd == "bf4":
play_stat = await get_data_bf4(player_id, user_id, platform)
elif cmd == "bf1":
play_stat = await get_data_bf1(player_id, user_id, platform)
elif cmd == "bfv":
play_stat = await get_data_bfv(player_id, user_id, platform)
elif cmd == "bf2042":
await UniMessage.at(event.get_user_id()).text("正在开发中,敬请期待!").send()
# play_stat = await get_data_bfv(player_id, user_id, platform)
elif cmd == "bf6":
play_stat = await get_data_bf6(player_id, user_id, platform)
else:
await UniMessage.at(event.get_user_id()).text("指令异常").send()
return
# logger.info(f"{json.dumps(play_stat, ensure_ascii=False, indent=2)}")
if "errors" in play_stat: if "errors" in play_stat:
logger.warning(play_stat['errors'][0]) logger.warning(play_stat['errors'][0])
msg = play_stat['errors'][0] msg = play_stat['errors'][0]
else: else:
if cmd == "bf6": # logger.info(f"结果{play_stat}")
img = build_bf6_simple_card(play_stat) # player = play_stat['userName']
else:
weapon, vehicle = await get_best_weapon_and_best_vehicle(play_stat) weapon, vehicle = await get_best_weapon_and_best_vehicle(play_stat)
player = play_stat['userName']
pid = play_stat['userId'] pid = play_stat['userId']
kd = play_stat['killDeath'] kd = play_stat['killDeath']
kpm = play_stat['killsPerMinute'] kpm = play_stat['killsPerMinute']
spm = play_stat['scorePerMinute']
acc = play_stat['accuracy'] acc = play_stat['accuracy']
# 战地3 特化 # 战地3 特化
if cmd == 'bf3': if cmd == 'bf3':
head_shots = play_stat['headShots'] head_shots = play_stat['headShots']
else: else:
head_shots = play_stat['headshots'] head_shots = play_stat['headshots']
rank = play_stat['rank']
time_play = convert_to_hours(play_stat['timePlayed']) time_play = convert_to_hours(play_stat['timePlayed'])
logger.info(f"游玩时长监测:{time_play}")
if time_play == 0:
await UniMessage.at(event.get_user_id()).text("查询接口异常,请等待一段时间后查询").send()
return
kills = int(play_stat['kills']) kills = int(play_stat['kills'])
kill_assists = int(play_stat['killAssists']) kill_assists = int(play_stat['killAssists'])
revives = int(play_stat['revives']) revives = int(play_stat['revives'])
@ -95,21 +128,141 @@ async def handle_function(matcher: Matcher, msg: Message = CommandArg()):
best_weapon = weapon best_weapon = weapon
best_vehicle = vehicle best_vehicle = vehicle
best_class = play_stat['bestClass'] best_class = play_stat['bestClass']
destroyed = await get_vehicle_destroyed(play_stat["vehicles"])
if cmd == 'bf6':
rank = await get_bf6_rank(player)
logger.info(f"等级:{rank}")
captured = play_stat['objective']['captured']
score = sum(item.get("score", 0) for item in play_stat['classes'])
# 计算4兵种游玩时长
seconds = sum(item.get("secondsPlayed", 0) for item in play_stat['classes'])
kpm = round(kills / (int(seconds) / 60), 1)
logger.info(f"击杀数:{kills},游玩分钟数:{seconds / 60}计算kpm值{kpm}")
spm = int(score / (int(seconds) / 60))
logger.info(f"计算spm值{spm}")
repairs = play_stat['repairs']
img = await build_bf6_stats_card(game='bf6',
qq_id=event.get_user_id(),
player=player,
pid=pid,
rank=rank,
kd=kd,
kpm=kpm,
spm=spm,
acc=acc,
head_shots=head_shots,
time_play=time_play,
kills=kills,
kill_assists=kill_assists,
revives=revives,
repairs=repairs,
captured=captured,
score=score,
wins=wins,
loses=loses,
destroyed=destroyed,
best_weapon=best_weapon,
best_vehicle=best_vehicle,
best_class=best_class)
else:
spm = play_stat['scorePerMinute']
rank = play_stat['rank']
longest_head_shot = play_stat['longestHeadShot'] longest_head_shot = play_stat['longestHeadShot']
highest_ill_streak = play_stat['highestKillStreak'] highest_ill_streak = play_stat['highestKillStreak']
# await stats_calculator(play_stat)
stat_data, level_designation = await stats_calculator(play_stat, cmd) stat_data, level_designation = await stats_calculator(play_stat, cmd)
destroyed = await get_vehicle_destroyed(play_stat["vehicles"])
img = await build_stats_card(game, player, pid, kd, kpm, spm, acc, head_shots, rank, time_play, kills, img = await build_stats_card(game, player, pid, kd, kpm, spm, acc, head_shots, rank, time_play, kills,
kill_assists, revives, wins, loses, destroyed, best_weapon, best_vehicle, kill_assists, revives, wins, loses, destroyed, best_weapon, best_vehicle,
best_class, best_class,
longest_head_shot, highest_ill_streak, stat_data, level_designation) longest_head_shot, highest_ill_streak, stat_data, level_designation)
await UniMessage.image(raw=img.getvalue()).finish() end_time = time.time()
await UniMessage.text(f"\n玩家【{content}】的【{bf_dict[cmd]}】数据\n{msg}").send() duration = round(end_time - start_time, 3)
await UniMessage.image(raw=img.getvalue()).at(user_id=event.get_user_id()).text(
f"本次查询耗时:{duration}s").send()
except Exception as e:
logger.exception(f"异常:{e}")
@bind.handle() @bind.handle()
async def bind_user(matcher: Matcher, msg: Message = CommandArg()): async def bind_user(event: Event, matcher: Matcher, msg: Message = CommandArg()):
matcher.state.get() user_manager = UserManager()
player_name = msg.extract_plain_text()
qq_id = event.get_user_id()
# 执行已有数据查询操作
user_record = user_manager.get_user_by_qq(qq_id)
if user_record:
user_info = await get_player_info_by_ea_id(user_record['ea_player_id'], user_record['ea_user_id'], 'pc')
await (UniMessage.at(user_id=event.get_user_id())
.text(f"您已经将账号绑定到: {user_info['personaName']},如果你需要更改绑定请发送 修改绑定 EAID ").send())
return
else:
user_info_json = await get_player_info_by_name(player_name, "pc")
if 'errors' in user_info_json:
await UniMessage.at(user_id=event.get_user_id()).text("未找到玩家,请检查是否拼写正确").send()
player_id = user_info_json['personaId']
user_id = user_info_json['userId']
player_name = user_info_json['personaName'] if 'personaName' in user_info_json else user_info_json['name']
avatar_url = user_info_json['avatar']
logger.info(f"QQ号{qq_id}EA双ID{player_id}-{user_id}")
# 检测ea账号 是否被绑定至其他qq
bind_record = user_manager.get_user_by_ea_id(user_id)
if bind_record:
await (UniMessage.at(user_id=event.get_user_id())
.text(
f"您的EA账号 {bind_record['ea_player_name']} 已经绑定到: {bind_record['qq_id']}请您确认绑定的EAID").send())
return
else:
# 执行绑定操作
record = user_manager.add_user(qq_id, player_name, player_id, user_id, "[]", "[]")
logger.info(f"插入数据返回结果:{record}")
if record > 0:
await UniMessage.at(user_id=event.get_user_id()).text(f"您已经成功绑定至: {player_name}").send()
@unbind.handle()
async def bind_user(event: Event, matcher: Matcher, msg: Message = CommandArg()):
user_manager = UserManager()
qq_id = event.get_user_id()
# 执行已有数据查询操作
user_record = user_manager.get_user_by_qq(qq_id)
if user_record:
user_info = await get_player_info_by_ea_id(user_record['ea_player_id'], user_record['ea_user_id'], 'pc')
rows = user_manager.delete_user_by_qq(qq_id)
if rows > 0:
await (UniMessage.at(user_id=event.get_user_id())
.text(
f"您已经与: {user_info['personaName']} 成功解除绑定,如果你需要重新绑定请发送 修改绑定 EAID ").send())
return
else:
await UniMessage.at(user_id=event.get_user_id()).text(f"未查询到绑定记录").send()
@update_bind.handle()
async def bind_user(event: Event, matcher: Matcher, msg: Message = CommandArg()):
user_manager = UserManager()
player_name = msg.extract_plain_text()
qq_id = event.get_user_id()
# 执行已有数据查询操作
user_record = user_manager.get_user_by_qq(qq_id)
if user_record:
user_info_json = await get_player_info_by_name(player_name, "pc")
if 'errors' in user_info_json:
await UniMessage.at(user_id=event.get_user_id()).text("未找到玩家,请检查是否拼写正确").send()
player_id = user_info_json['personaId']
user_id = user_info_json['userId']
player_name = user_info_json['personaName'] if 'personaName' in user_info_json else user_info_json['name']
avatar_url = user_info_json['avatar']
logger.info(f"QQ号{qq_id}EA双ID{player_id}-{user_id}")
# 执行修改绑定操作
record = user_manager.update_user(qq_id, player_name, player_id, user_id, "[]", "[]")
logger.info(f"修改数据返回结果:{record}")
if record > 0 and record == 1:
await UniMessage.at(user_id=event.get_user_id()).text(
f"您已经成功从:{user_record['ea_player_name']}修改绑定至: {player_name}").send()
else:
await UniMessage.at(user_id=event.get_user_id()).text(
f"未查询到绑定记录: {player_name},如果需要绑定,请@机器人并发送 绑定 {player_name} ").send()

View File

@ -6,11 +6,6 @@ from nonebot import logger
from curl_cffi import AsyncSession, CurlError from curl_cffi import AsyncSession, CurlError
import random import random
try:
import browser_cookie3
except ImportError:
browser_cookie3 = None # 可选,如果没安装则 fallback 到 cookies.txt
# ---------- 配置 ---------- # ---------- 配置 ----------
file_path = os.path.dirname(__file__).replace("\\", "/") file_path = os.path.dirname(__file__).replace("\\", "/")
exported_cookie_path = Path(f"{file_path}/cookies/tracker.txt") # 你导出的 cookies.txt exported_cookie_path = Path(f"{file_path}/cookies/tracker.txt") # 你导出的 cookies.txt
@ -59,15 +54,6 @@ def load_cookies_from_txt(path: Path) -> List[Dict[str, str]]:
return cookies return cookies
def load_browser_cookies(domain="tracker.gg") -> List[Dict[str, str]]:
if not browser_cookie3:
return []
try:
return [{"name": c.name, "value": c.value} for c in browser_cookie3.chrome(domain_name=domain)]
except Exception:
return []
def build_cookie_header(cookies: List[Dict[str, str]]) -> str: def build_cookie_header(cookies: List[Dict[str, str]]) -> str:
return "; ".join(f"{c['name']}={c['value']}" for c in cookies) return "; ".join(f"{c['name']}={c['value']}" for c in cookies)
@ -130,10 +116,7 @@ def is_challenge_response(resp) -> bool:
async def search_user_with_fallback(url: str): async def search_user_with_fallback(url: str):
# 1⃣ 优先尝试浏览器 cookie # cookies.txt
cookies = load_browser_cookies()
# 2⃣ 如果浏览器 cookie 不存在,再 fallback 到 cookies.txt
if not cookies:
cookies = load_cookies_from_txt(exported_cookie_path) cookies = load_cookies_from_txt(exported_cookie_path)
headers = build_headers(RAW_BROWSER_HEADERS, cookies=cookies, ua_override=CUSTOM_UA) headers = build_headers(RAW_BROWSER_HEADERS, cookies=cookies, ua_override=CUSTOM_UA)
@ -142,12 +125,12 @@ async def search_user_with_fallback(url: str):
resp = await fetch_with_cookies(url, headers) resp = await fetch_with_cookies(url, headers)
if is_challenge_response(resp): if is_challenge_response(resp):
logger.warning("⚠️ Cloudflare 拦截或 cookies 失效。") logger.warning("Cloudflare 拦截或 cookies 失效。")
if isinstance(resp, dict): if isinstance(resp, dict):
return resp return resp
return {"status": getattr(resp, "status_code", None), "preview": getattr(resp, "text", "")[:200]} return {"status": getattr(resp, "status_code", None), "preview": getattr(resp, "text", "")[:200]}
else: else:
logger.info("请求成功。") logger.info("请求成功。")
return getattr(resp, "json", lambda: resp)() if hasattr(resp, "json") else resp return getattr(resp, "json", lambda: resp)() if hasattr(resp, "json") else resp

View File

@ -2,24 +2,30 @@
# https://curl.haxx.se/rfc/cookie_spec.html # https://curl.haxx.se/rfc/cookie_spec.html
# This is a generated file! Do not edit. # This is a generated file! Do not edit.
tracker.gg FALSE / FALSE 1763712553 _lr_env_src_ats false .tracker.gg TRUE / TRUE 1795483904 _scor_uid d3fe4e72704a49548e3d944283183d16
tracker.gg FALSE / FALSE 1766200813 nitro-uid %7B%22TDID%22%3A%2238381d61-f87b-42c2-9a52-7764b7e111ac%22%2C%22TDID_LOOKUP%22%3A%22TRUE%22%2C%22TDID_CREATED_AT%22%3A%222025-09-21T03%3A20%3A13%22%7D
tracker.gg FALSE / FALSE 1766200813 nitro-uid_cst znv0HA%3D%3D
.tracker.gg TRUE / TRUE 1794816553 _scor_uid d3fe4e72704a49548e3d944283183d16
.tracker.gg TRUE / FALSE 1792552814 ncmp.domain tracker.gg .tracker.gg TRUE / FALSE 1792552814 ncmp.domain tracker.gg
.tracker.gg TRUE / TRUE 1792657486 __stripe_mid f3195526-098b-451a-a803-afe861f3546981d4b7 .tracker.gg TRUE / TRUE 1797905122 __stripe_mid f3195526-098b-451a-a803-afe861f3546981d4b7
.tracker.gg TRUE / FALSE 1795681481 _ga GA1.1.1090555935.1761016835 .tracker.gg TRUE / FALSE 1800929047 _ga GA1.1.1090555935.1761016835
.tracker.gg TRUE / FALSE 1795576862 _ga_4115T4MP2X GS2.1.s1761016839$o1$g1$t1761016862$j37$l0$h0 .tracker.gg TRUE / FALSE 1795576862 _ga_4115T4MP2X GS2.1.s1761016839$o1$g1$t1761016862$j37$l0$h0
tracker.gg FALSE / FALSE 1766305486 pbjs-unifiedid %7B%22TDID%22%3A%2238381d61-f87b-42c2-9a52-7764b7e111ac%22%2C%22TDID_LOOKUP%22%3A%22TRUE%22%2C%22TDID_CREATED_AT%22%3A%222025-09-21T03%3A21%3A09%22%7D tracker.gg FALSE / FALSE 1771553052 pbjs-unifiedid_cst YiwPLDosoA%3D%3D
tracker.gg FALSE / FALSE 1766305486 pbjs-unifiedid_cst YiwPLDosoA%3D%3D .tracker.gg TRUE / FALSE 1789697050 _cc_id d7583525ab694e187a68c7c9adac9679
.tracker.gg TRUE / FALSE 1784448555 _cc_id d7583525ab694e187a68c7c9adac9679 .tracker.gg TRUE / FALSE 1792826487 _sharedID bed2ec05800bb7f9e68b74ba7bdcd4d2
tracker.gg FALSE / FALSE 1761621671 _lr_sampling_rate 100 .tracker.gg TRUE / FALSE 1797905048 _pubcid a08276dc-b9f1-4665-af99-d3e92418fa0d
.tracker.gg TRUE / FALSE 1794757153 cto_bidid ZYK0w18xUFhlUWZoQ2ttd2Vlc0lnVjV1azlmSXI5ZlBVRUZ3QjVFazJUOHAlMkZnYWlFc0l2endrZGVDb1Z6dXdJdXA4V3Y1OVlEVGI4VUlNV1QxejgwWmxwJTJGRjByeGJFbWhFRTBQQnExU1luNTlvd1klM0Q tracker.gg FALSE / FALSE 1771553049 nitro-uid_cst V0fMHQ%3D%3D
.tracker.gg TRUE / TRUE 1761122345 __cf_bm 0U2HKncz5gYebQMBxL_qtaeuqoAdy.5lnue9xCP_Lls-1761120544-1.0.1.1-vFt.KkK9agrVBKJiVR6KstByxEN86BEvyVVQxg5VhtRT40Oq6Bmaan1yzLbW9V0ixAlOLPpYcflX6CqpCjl0D9Lg.73D54Jnm3KLQ5tGxnEZhnr0ORp6W5HYsfpsNiRc .tracker.gg TRUE / FALSE 1797905048 _pubcid_cst znv0HA%3D%3D
tracker.gg FALSE / FALSE 1761124153 _lr_retry_request true tracker.gg FALSE / FALSE 1768961048 _lr_env_src_ats false
.tracker.gg TRUE / FALSE 1761206955 panoramaId_expiry 1761206955341 .tracker.gg TRUE / FALSE 1797905049 _nitroID eac08417a7dae49a9a17a3077a566d71
.tracker.gg TRUE / TRUE 1761123286 __stripe_sid 99b864d5-840b-4cce-9c31-b4699e594ec16de245 tracker.gg FALSE / FALSE 1771553049 nitro-uid %7B%22TDID%22%3A%2238381d61-f87b-42c2-9a52-7764b7e111ac%22%2C%22TDID_LOOKUP%22%3A%22TRUE%22%2C%22TDID_CREATED_AT%22%3A%222025-11-22T02%3A04%3A10%22%7D
.tracker.gg TRUE / FALSE 1795681481 _ga_HWSV72GK8X GS2.1.s1761120545$o13$g1$t1761121480$j60$l0$h0 .tracker.gg TRUE / FALSE 1800093848 cto_bidid 1MhyDl8xUFhlUWZoQ2ttd2Vlc0lnVjV1azlmSXI5ZlBVRUZ3QjVFazJUOHAlMkZnYWlFc0l2endrZGVDb1Z6dXdJdXA4V3Y1OVlEVGI4VUlNV1QxejgwWmxwJTJGRiUyQjlkcXVKbW14OFo2dDZoUWFHMjcwMCUzRA
tracker.gg FALSE / FALSE 1766305486 pbjs-unifiedid_last Wed%2C%2022%20Oct%202025%2008%3A24%3A46%20GMT .tracker.gg TRUE / FALSE 1766455452 panoramaId_expiry 1766455451737
.tracker.gg TRUE / FALSE 1794817546 cto_bundle dv27kl9RN0JGSHVyZE9za1E5a1ZNZEo4R21rQVkxVUFtYSUyRjQxd0JaWDJtWGhOVmZnV2hxUEVzemZ2TE9XSVB0UXNMVkZySUtXaW0lMkZ4WTRnN3FhSmNFOE4lMkI2WXg4cW42UFpKd2I3QmViY1JuTkZYMG9FVUZ1STJESHc2NDUzMHBEMyUyQm92bUtEMzJlajF0bXMyZ3M1Z0YwJTJGRkR3JTNEJTNE .tracker.gg TRUE / FALSE 1766455452 panoramaId 06fa4e910365dd224dafc8b188a2a9fb927a5781cea050ef85eb9d2d9bd1d20a
.tracker.gg TRUE / TRUE 1792657481 cf_clearance betnOuJnsRXlDgs9rKx_2GJXze5bdRFK3eVFFB9k610-1761121481-1.2.1.1-RDnj4R4HqOjhE1Sn5Dp5wKysllw6cVSYRPbbL4y_STOAnBlQEVRv_7xC1h9TleDg9Ecyy2neJCW1Xk6BNd51K4htSHptKFlQF8tv_JqMlDyFg9DxShAveOMXu4o9vkivWZmFvJRKv9ERqm33dfsYAQD5lkyYFtsurraCOPzu3TILLbsnjoi2v_kCILeFLPdEbeiABRHQUrsZdi1jhwKM9cDJ_XHs0bmPZrDOfVaf444 .tracker.gg TRUE / FALSE 1766455452 panoramaIdType panoDevice
tracker.gg FALSE / FALSE 1771553052 pbjs-unifiedid %7B%22TDID%22%3A%2238381d61-f87b-42c2-9a52-7764b7e111ac%22%2C%22TDID_LOOKUP%22%3A%22TRUE%22%2C%22TDID_CREATED_AT%22%3A%222025-11-22T02%3A04%3A13%22%7D
tracker.gg FALSE / FALSE 1771553052 pbjs-unifiedid_last Mon%2C%2022%20Dec%202025%2002%3A04%3A12%20GMT
.tracker.gg TRUE / FALSE 1800065052 cto_bundle Lj73nF81bUZFQXRhbjgxQmE5SG1uZWMyaEcxdGZBYUsxY09Fb05TT2hpSVo2ckptaXA0clZkeDBIQnklMkJrcG56U21MWnRvYlZJaVJUdmolMkI4YnBMQkgxWjRqaDZpTmhJVnRkVWIlMkZGVzBYR0Q1dnJ0WWhEeVNwQVpCTXZHc2tHUnVKSHRTeSUyRnpnQWIlMkJOV3BwSGdYTU1mcktaY2t3JTNEJTNE
tracker.gg FALSE / FALSE 1766973852 _lr_sampling_rate 100
.tracker.gg TRUE / FALSE 1800947800 _ga_HWSV72GK8X GS2.1.s1766387800$o31$g0$t1766387800$j60$l0$h0
.tracker.gg TRUE / TRUE 1800065052 __gads ID=ac9c2c8840aa7cc9:T=1766369052:RT=1766388112:S=ALNI_Mb6I0wyc1mq0xflsJiO_mg_p7NYyg
.tracker.gg TRUE / TRUE 1800065052 __gpi UID=000011d0cd089cb2:T=1766369052:RT=1766388112:S=ALNI_MZYW_bpm3kBzbSJVap3HmFbUvCzkQ
.tracker.gg TRUE / TRUE 1781921052 __eoi ID=161dfd629cbfa555:T=1766369052:RT=1766388112:S=AA-Afjb7PNr8G6MBMl83ajT_75cS
.tracker.gg TRUE / TRUE 1797905047 cf_clearance yXkCKjvvWaL14EKripPAXHHkJsg_U80n6RCKMxweSPg-1766369048-1.2.1.1-KaeYOmcV609jWIn.Rrw.fhT5HXMcy4R1H.s67uhk5ewh5P8yuXU6VZGYdMtGF8zUBAu5dVsg9dyp0C1jMqMPKgZ3syEcCryrW5_sPsLS8EUlLb1fXq2ehVCRtEHZhfP_zZuQSGxil2vTq1Z9feznMkY1cMb1hIaSZ3JUajyF7w.YryqLr4n5lOS3dlQeaXYS__X_gbzxyJNOzMXDuYQtfqc50JQt5ymEACbbPIGsK20

View File

@ -3,55 +3,123 @@ import json
import requests import requests
from .get_bf6_data import * from .get_bf6_data import *
from .tracker_data import *
async def get_data_bf3(user_name, platform): async def get_data_bf3(player_id, user_id, platform):
url = f"https://api.gametools.network/bf3/all/?format_values=true&name={user_name}&platform={platform}" url = f"https://api.gametools.network/bf3/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us"
payload = {} payload = {}
headers = { headers = {
'accept': 'application/json' 'accept': 'application/json'
} }
logger.info(f"请求URL{url}")
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
data_json = json.loads(response.text) data_json = json.loads(response.text)
return data_json return data_json
async def get_data_bf4(user_name, platform): async def get_data_bf4(player_id, user_id, platform):
url = f"https://api.gametools.network/bf4/all/?format_values=true&name={user_name}&platform={platform}" url = f"https://api.gametools.network/bf4/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us"
payload = {} payload = {}
headers = { headers = {
'accept': 'application/json' 'accept': 'application/json'
} }
logger.info(f"请求URL{url}")
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
data_json = json.loads(response.text) data_json = json.loads(response.text)
return data_json return data_json
async def get_data_bf1(user_name, platform): async def get_data_bf1(player_id, user_id, platform):
url = f"https://api.gametools.network/bf1/all/?format_values=true&name={user_name}&platform={platform}&skip_battlelog=false&lang=en-us" url = f"https://api.gametools.network/bf1/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us"
payload = {}
headers = {
'accept': 'application/json'
}
logger.info(f"请求URL{url}")
response = requests.request("GET", url, headers=headers, data=payload)
data_json = json.loads(response.text)
return data_json
async def get_data_bfv(player_id, user_id, platform):
url = f"https://api.gametools.network/bfv/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us"
payload = {} payload = {}
headers = { headers = {
'accept': 'application/json' 'accept': 'application/json'
} }
logger.info(f"请求URL{url}")
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
data_json = json.loads(response.text) data_json = json.loads(response.text)
return data_json return data_json
async def get_data_bfv(user_name, platform): async def get_data_bf6(player_id, user_id, platform):
url = f"https://api.gametools.network/bfv/all/?format_values=true&name={user_name}&platform={platform}&skip_battlelog=false&lang=en-us" url = f"https://api.gametools.network/bf6/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us"
payload = {} payload = {}
headers = { headers = {
'accept': 'application/json' 'accept': 'application/json'
} }
logger.info(f"请求URL{url}")
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
data_json = json.loads(response.text) data_json = json.loads(response.text)
return data_json return data_json
async def get_data_bf6(user_name, search_type): async def get_player_info_by_name(user_name, platform):
flag, data_json = await get_info(user_name, search_type) user_info_url = f"https://api.gametools.network/bfglobal/player/?name={user_name}&platform={platform}&skip_battlelog=false"
return flag, data_json
payload = {}
headers = {
'accept': 'application/json'
}
logger.info(f"查询用户请求url:{user_info_url}")
response = requests.request("GET", user_info_url, headers=headers, data=payload)
user_info_json = json.loads(response.text)
return user_info_json
async def get_player_info_by_ea_id(player_id, user_id, platform):
url = f"https://api.gametools.network/bfglobal/player/?playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false"
payload = {}
headers = {
'accept': 'application/json'
}
response = requests.request("GET", url, headers=headers, data=payload)
user_info_json = json.loads(response.text)
return user_info_json
async def get_bf6_rank(player):
url = f"https://api.tracker.gg/api/v2/bf6/standard/search?platform=origin&query={player}&autocomplete=true"
result = await fetch_tracker_search(url, exported_cookie_path)
# 无结果再使用steam搜索
if "errors" in result:
return 0
user_info = {}
if "status" in result or "errors" in result:
return 0
for res in result['data']:
if res['status'] is not None and res['platformSlug'] == 'origin':
name = res['platformUserHandle']
uid = res['titleUserId']
status = res['status'].strip().split('', 1)[0].replace("Rank", "")
user = {
'name': name,
'rank': status,
'uid': uid,
}
user_info = user
logger.info(f"单用户{user_info}")
logger.info(f"查询结果: {json.dumps(user_info, ensure_ascii=False, indent=2)}")
if 'rank' not in user_info:
return 0
return user_info['rank']

View File

@ -3,6 +3,8 @@ from PIL import Image, ImageDraw, ImageFont
from nonebot import logger from nonebot import logger
from functools import reduce from functools import reduce
# from img_utils import png_resize
# from param import round_data, interval_table
from .img_utils import png_resize from .img_utils import png_resize
from .param import round_data, interval_table from .param import round_data, interval_table

View File

@ -0,0 +1,4 @@
def bind_user(user_info):
user_info['']

View File

@ -0,0 +1,60 @@
import json
import requests
async def get_data_bf6(player_id, user_id, platform):
url = "https://api.gametools.network/bf6/multiple/?raw=false&format_values=true"
payload = json.dumps([
{
"player_id": player_id,
"user_id": user_id,
"platform": platform
}
])
headers = {
'accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
info = json.loads(response.text)
return info
async def get_data_user(user_name, platform):
url = f"https://api.gametools.network/bfglobal/player/?name={user_name}&platform={platform}&skip_battlelog=false"
payload = {}
headers = {
'accept': 'application/json'
}
response = requests.request("GET", url, headers=headers, data=payload)
data_json = json.loads(response.text)
player_id = data_json["personaId"]
user_id = data_json["userId"]
platform = data_json["platform"]
user_name = data_json["personaName"]
if "errors" in data_json:
return False, data_json
user_info = {
"player_id": player_id,
"user_id": user_id,
"platform": platform,
"user_name": user_name
}
return True, user_info
async def get_player_game_info(user_name, platform):
flag, user_info = await get_data_user(user_name, platform)
info = await get_data_bf6(user_info['player_id'], user_info['user_id'], user_info['platform'])
info['userName'] = user_name
info_format = json.dumps(info, ensure_ascii=False, indent=2)
print(info_format)

View File

@ -5,9 +5,10 @@ from typing import List, Dict, Optional
from nonebot import logger from nonebot import logger
from curl_cffi import AsyncSession, CurlError from curl_cffi import AsyncSession, CurlError
from .bf6_data import * from .bf6_data import *
# from bf6_data import *
import requests
import json import json
url_search = "https://api.tracker.gg/api/v2/bf6/standard/search?platform={platform}&query={name}&autocomplete=true"
url_search = "https://api.tracker.gg/api/v2/bf6/standard/search?platform={platform}&query={name}"
url_overview = "https://api.tracker.gg/api/v2/bf6/standard/profile/ign/{param}" url_overview = "https://api.tracker.gg/api/v2/bf6/standard/profile/ign/{param}"
@ -65,8 +66,8 @@ async def get_info(player, search_type):
url = url_search.format(platform="steam", name=player) url = url_search.format(platform="steam", name=player)
result = await search_user_with_fallback(url) result = await search_user_with_fallback(url)
title_id_list = [] title_id_list = []
logger.info(f"查询结果: {json.dumps(result, ensure_ascii=False, indent=2)}") logger.info(f"查询结果: \n{json.dumps(result, ensure_ascii=False, indent=2)}")
if "errors" in result: if "status" in result or "errors" in result:
return 3, '查询异常' return 3, '查询异常'
for res in result['data']: for res in result['data']:
if res['status'] is not None: if res['status'] is not None:
@ -245,3 +246,26 @@ async def get_vehicles(platform_info, overview, weapons, vehicles, gamemodes, ga
'最佳载具': best_vehicle, '最佳载具': best_vehicle,
} }
return player_info return player_info
async def get_bf6_data(player_id, user_id, platform):
url = "https://api.gametools.network/bf6/multiple/?raw=false&format_values=true"
payload = json.dumps([
{
"player_id": player_id,
"user_id": user_id,
"platform": platform
}
])
headers = {
'accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
player_stat_json = json.loads(response.text)
return player_stat_json

View File

@ -12,18 +12,39 @@ from .img_utils import *
from .data_utils import * from .data_utils import *
from .param import * from .param import *
# from img_utils import *
# from data_utils import *
# from param import *
filepath = os.path.dirname(__file__).replace("\\", "/") filepath = os.path.dirname(__file__).replace("\\", "/")
# 字体 # 字体
font_XXL = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 72) font_XXL = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 72)
font_XL = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 64) font_XL = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 64)
font_L = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 48) font_L = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 48)
font_M = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 36) font_ML = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 42)
font_MS = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 32) font_M = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 36)
font_S = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28) font_MS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 32)
font_XS = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 24) font_S = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 28)
font_XXS = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18) font_XS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 24)
font_XXXS = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 16) font_XXS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 18)
font_XXXS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 16)
font_XXL_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 72)
font_XL_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 64)
font_L_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 48)
font_ML_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 42)
font_M_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 36)
font_MS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 32)
font_S_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28)
font_XS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 24)
font_XXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18)
font_XXXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 16)
font_S_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28)
font_XS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 20)
font_XXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18)
font_XXXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 16)
async def build_stats_card(game="bfv", player="Player001", pid=1145141919, kd=1.25, kpm=2.3, spm=500, acc=25.6, async def build_stats_card(game="bfv", player="Player001", pid=1145141919, kd=1.25, kpm=2.3, spm=500, acc=25.6,
@ -75,12 +96,12 @@ async def build_stats_card(game="bfv", player="Player001", pid=1145141919, kd=1.
draw = ImageDraw.Draw(pil_img) draw = ImageDraw.Draw(pil_img)
# 玩家信息 # 玩家信息
draw.text((665, 65), f"{player}", fill="white", font=font_L) draw.text((665, 65), f"{player}", fill="white", font=font_L_CH)
draw.text((745, 133), f"{pid}", fill="white", font=font_M) draw.text((745, 133), f"{pid}", fill="white", font=font_M_CH)
# 等级游玩时长 # 等级游玩时长
draw.text((740, 212), f"{rank}", fill="white", font=font_M) draw.text((740, 212), f"{rank}", fill="white", font=font_M_CH)
draw.text((985, 212), f"{time_play}H", fill="white", font=font_M) draw.text((985, 212), f"{time_play}H", fill="white", font=font_M_CH)
# draw_centered_text(draw=draw, text=rank, x_left=730, x_right=905, y=210, font=font_M, fill="white", ) # draw_centered_text(draw=draw, text=rank, x_left=730, x_right=905, y=210, font=font_M, fill="white", )
# draw_centered_text(draw=draw, text=f"{time_play}H", x_left=965, x_right=1120, y=210, font=font_M, fill="white", ) # draw_centered_text(draw=draw, text=f"{time_play}H", x_left=965, x_right=1120, y=210, font=font_M, fill="white", )
# draw.text((730, 195), f"{rank}", fill="white", font=font_M) # draw.text((730, 195), f"{rank}", fill="white", font=font_M)
@ -90,11 +111,172 @@ async def build_stats_card(game="bfv", player="Player001", pid=1145141919, kd=1.
# 场次 # 场次
draw_centered_text(draw=draw, text=(wins + loses), x_left=45, x_right=265, y=350, draw_centered_text(draw=draw, text=(wins + loses), x_left=45, x_right=265, y=350,
font=font_L, font=font_L_CH,
fill="white") fill="white")
# 命中率 # 命中率
draw_centered_text(draw=draw, text=round(float(acc.replace("%", "")), 1), x_left=415, x_right=495, y=343, draw_centered_text(draw=draw, text=round(float(acc.replace("%", "")), 1), x_left=415, x_right=495, y=343,
font=font_M_CH,
fill="white")
# 爆头率
if head_shots:
head_shots = head_shots.replace("%", "")
else:
head_shots = 0
draw_centered_text(draw=draw, text=round(float(head_shots), 1), x_left=395, x_right=470, y=420, font=font_M_CH,
fill="white")
# kd
draw_centered_text(draw=draw, text=kd, x_left=585, x_right=785, y=340, font=font_XL_CH, fill="white")
# kpm
draw_centered_text(draw=draw, text=round(kpm, 1), x_left=825, x_right=1025, y=340, font=font_XL_CH, fill="white")
# spm
draw_centered_text(draw=draw, text=round(spm, 1), x_left=1065, x_right=1265, y=340, font=font_XL_CH, fill="white")
# 击杀
draw_centered_text(draw=draw, text=kills, x_left=870, x_right=1190, y=595, font=font_L_CH, fill="white")
# 助攻
draw_centered_text(draw=draw, text=kill_assists, x_left=820, x_right=1145, y=770, font=font_L_CH, fill="white")
# 急救
draw_centered_text(draw=draw, text=revives, x_left=770, x_right=1090, y=960, font=font_L_CH, fill="white")
# 摧毁
draw_centered_text(draw=draw, text=destroyed, x_left=720, x_right=1040, y=1140, font=font_L_CH, fill="white")
# 底部栏
# 评级
if game == 'bf6':
logger.info("bf6不计算称号")
else:
level = level_designation['level']
des = level_designation['designation']
color = level_designation['color']
draw_centered_text(draw=draw, text=level, x_left=70, x_right=200, y=1120, font=font_XL_CH, fill=color)
draw_centered_text(draw=draw, text=des, x_left=200, x_right=550, y=1120, font=font_XL_CH, fill="white")
# 最佳兵种
draw_centered_text(draw=draw, text=classes[best_class], x_left=140, x_right=310, y=1290, font=font_L_CH, fill="white")
# 最远击杀
draw_centered_text(draw=draw, text=f"{longest_head_shot}m", x_left=455, x_right=625, y=1290, font=font_L_CH,
fill="white")
# 最高连杀
draw_centered_text(draw=draw, text=highest_ill_streak, x_left=765, x_right=935, y=1290, font=font_L_CH, fill="white")
# 最佳⭐
pil_img = build_best(draw, pil_img, best_weapon, best_vehicle, game)
# 生成时间
draw.text((1870, 1420), f"{formatted_time}", fill=(154, 132, 149), font=font_XXXS_CH)
# ---------------- 转回 OpenCV ----------------
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
# ---------------- 雷达图 ----------------
if game != "bf6":
center = (311, 796)
r = 185
scale_min = 0.25 if min(stat_data) < 0.25 else min(stat_data)
img = draw_radar(img, stat_data, center=center, r=r,
buffer=0.1, scale_min=scale_min, scale_max=1.10)
# 保存
success, buffer = cv2.imencode(".png", img)
if success:
b_io = io.BytesIO(buffer)
return b_io # 就可以直接 return 二进制流
else:
return None
async def build_bf6_stats_card(game="bfv",
qq_id=2931,
player="Player001",
pid=1145141919,
rank=114,
kd=1.25,
kpm=2.3,
spm=500,
acc=25.6,
head_shots=10,
time_play=100,
kills=100,
kill_assists=150,
revives=114,
repairs=1000,
captured=300,
score=145674,
wins=10,
loses=10,
destroyed=514,
best_weapon="M1907 SF",
best_vehicle="坦克",
best_class="Support", ):
# 获取当前事件并且格式化
now = datetime.datetime.now()
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S")
# ---------------- OpenCV 部分 ----------------
# 载入模板
img = cv2.imread(f"{filepath}/template/{game}.png", cv2.IMREAD_COLOR)
# 载入头像并缩放
avatar = cv2.imread(f"{filepath}/avatar_cache/0.png", cv2.IMREAD_UNCHANGED)
avatar = cv2.resize(avatar, (134, 134), interpolation=cv2.INTER_AREA)
# 粘贴头像(保持原有坐标)
y, x = 65, 105
h, w = avatar.shape[:2]
img[y:y + h, x:x + w] = avatar
# 公告栏
img = notice_paste_bf6(img)
img = designation_paste(player, img,
user_id=qq_id,
start_x=104,
start_y=215,
target_height=30,
spacing=10)
# ---------------- PIL 部分 ----------------
pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_img)
# 玩家信息 105+160
draw.text((265, 80), f"{player}", fill="white", font=font_XL)
draw.text((265, 140), f"{pid}", fill="#CCCCCC", font=font_L)
# 等级
pil_img = paste_rank_icon(int(rank), pil_img)
draw_centered_text(draw=draw, text=f"{rank}", x_left=950, x_right=1075, y=205,
font=font_M,
fill="white")
# draw.text((740, 212), f"{rank}", fill="white", font=font_M)
# 游玩时长
draw.text((2300, 350), f"{time_play}H", fill="white", font=font_L)
# 场次
draw.text((2300, 450), f"{wins + loses}", fill="white", font=font_L)
# draw_centered_text(draw=draw, text=(wins + loses), x_left=45, x_right=265, y=350,
# font=font_L,
# fill="white")
# 生涯总览
# 胜率
win_rate = round((wins / (wins + loses)) * 100, 1)
logger.info(f"胜场{wins},负场{loses},胜率{win_rate}")
draw_centered_text(draw=draw, text=f"{win_rate}%", x_left=2090, x_right=2200, y=375,
font=font_XL,
fill="white")
# 命中率
acc_format = round(float(acc.replace("%", "")), 1)
draw_centered_text(draw=draw, text=f"{acc_format}%", x_left=1210, x_right=1303, y=363,
font=font_M, font=font_M,
fill="white") fill="white")
# 爆头率 # 爆头率
@ -102,62 +284,83 @@ async def build_stats_card(game="bfv", player="Player001", pid=1145141919, kd=1.
head_shots = head_shots.replace("%", "") head_shots = head_shots.replace("%", "")
else: else:
head_shots = 0 head_shots = 0
draw_centered_text(draw=draw, text=round(float(head_shots), 1), x_left=395, x_right=470, y=420, font=font_M, draw_centered_text(draw=draw, text=f"{round(float(head_shots), 1)}%", x_left=1210, x_right=1303, y=458, font=font_M,
fill="white") fill="white")
# kd # kd
draw_centered_text(draw=draw, text=kd, x_left=585, x_right=785, y=340, font=font_XL, fill="white") # draw.text(text=kd, xy=(1350, 355), font=font_XL, fill="white")
draw_centered_text(draw=draw, text=kd, x_left=1350, x_right=1500, y=355, font=font_XL, fill="white")
# kpm # kpm
draw_centered_text(draw=draw, text=round(kpm, 1), x_left=825, x_right=1025, y=340, font=font_XL, fill="white") draw_centered_text(draw=draw, text=round(kpm, 1), x_left=1610, x_right=1760, y=355, font=font_XL, fill="white")
# spm # spm
draw_centered_text(draw=draw, text=round(spm, 1), x_left=1065, x_right=1265, y=340, font=font_XL, fill="white") draw_centered_text(draw=draw, text=round(spm, 1), x_left=1870, x_right=2010, y=355, font=font_XL, fill="white")
# 击杀 # 击杀
draw_centered_text(draw=draw, text=kills, x_left=870, x_right=1190, y=595, font=font_L, fill="white") draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1165, x_right=1455, y=805,
font=font_L,
# 助攻 fill="#333333")
draw_centered_text(draw=draw, text=kill_assists, x_left=820, x_right=1145, y=770, font=font_L, fill="white") draw_right_aligned_text(draw=draw, text=add_commas(str(kills)), x_left=1165, x_right=1455, y=805,
font=font_L, fill="white")
# 急救 # 急救
draw_centered_text(draw=draw, text=revives, x_left=770, x_right=1090, y=960, font=font_L, fill="white") draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1165, x_right=1455, y=1080,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(revives)), x_left=1165, x_right=1455, y=1080,
font=font_L, fill="white")
# 摧毁 # 摧毁
draw_centered_text(draw=draw, text=destroyed, x_left=720, x_right=1040, y=1140, font=font_L, fill="white") draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1165, x_right=1455, y=1350,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(destroyed)), x_left=1165, x_right=1455, y=1350,
font=font_L, fill="white")
# 右侧
# 占领
draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=745,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(captured)), x_left=1485, x_right=1780, y=745,
font=font_L, fill="white")
# 得分
draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=955,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(score)), x_left=1485, x_right=1780, y=955,
font=font_L, fill="white")
# 助攻
draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=1160,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(kill_assists)), x_left=1485, x_right=1780, y=1160,
font=font_L, fill="white")
# 修理
draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=1370,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(repairs)), x_left=1485, x_right=1780, y=1370,
font=font_L, fill="white")
# 底部栏 # 底部栏
# 评级 # # 最佳兵种
level = level_designation['level'] # logger.info(f"最佳兵种:{best_class}")
des = level_designation['designation'] # draw_centered_text(draw=draw, text=classes[best_class], x_left=140, x_right=310, y=1290, font=font_L, fill="white")
color = level_designation['color']
draw_centered_text(draw=draw, text=level, x_left=70, x_right=200, y=1120, font=font_XL, fill=color)
draw_centered_text(draw=draw, text=des, x_left=200, x_right=550, y=1120, font=font_XL, fill="white")
# 最佳兵种
draw_centered_text(draw=draw, text=classes[best_class], x_left=140, x_right=310, y=1290, font=font_L, fill="white")
# 最远击杀
draw_centered_text(draw=draw, text=f"{longest_head_shot}m", x_left=455, x_right=625, y=1290, font=font_L,
fill="white")
# 最高连杀
draw_centered_text(draw=draw, text=highest_ill_streak, x_left=765, x_right=935, y=1290, font=font_L, fill="white")
# 最佳⭐ # 最佳⭐
pil_img = build_best(draw, pil_img, best_weapon, best_vehicle, game) pil_img = build_best_bf6(draw, pil_img, best_weapon, best_vehicle, best_class, game)
# 生成时间 # 生成时间
draw.text((1870, 1420), f"{formatted_time}", fill=(154, 132, 149), font=font_XXXS) draw.text((1750, 10), f"{formatted_time}/战地中文社区/维护 SANSENHOSHI", fill=(154, 132, 149), font=font_XS_CH)
# ---------------- 转回 OpenCV ---------------- # ---------------- 转回 OpenCV ----------------
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
# ---------------- 雷达图 ---------------- # ---------------- 雷达图 ----------------
center = (311, 796)
r = 185
scale_min = 0.25 if min(stat_data) < 0.25 else min(stat_data)
img = draw_radar(img, stat_data, center=center, r=r,
buffer=0.1, scale_min=scale_min, scale_max=1.10)
# 保存 # 保存
success, buffer = cv2.imencode(".png", img) success, buffer = cv2.imencode(".png", img)
if success: if success:
@ -184,18 +387,18 @@ def build_best(draw: ImageDraw, img: Image, weapons, vehicle, game: str):
# 绘制最佳⭐ # 绘制最佳⭐
# 武器 # 武器
draw_centered_text(draw=draw, text=bf_item[game][weapon_name].upper(), x_left=1240, x_right=1750, y=1355, draw_centered_text(draw=draw, text=bf_item[game][weapon_name].upper(), x_left=1240, x_right=1750, y=1355,
font=font_MS, fill="white") font=font_MS_CH, fill="white")
draw_centered_text(draw=draw, text=w_kills, x_left=1190, x_right=1390, y=1260, font=font_L, fill="white") draw_centered_text(draw=draw, text=w_kills, x_left=1190, x_right=1390, y=1260, font=font_L_CH, fill="white")
draw_centered_text(draw=draw, text=w_kpm, x_left=1510, x_right=1635, y=1260, font=font_L, fill="white") draw_centered_text(draw=draw, text=w_kpm, x_left=1510, x_right=1635, y=1260, font=font_L_CH, fill="white")
draw_centered_text(draw=draw, text=acc, x_left=1735, x_right=1803, y=1030, font=font_M, fill="white") draw_centered_text(draw=draw, text=acc, x_left=1735, x_right=1803, y=1030, font=font_M_CH, fill="white")
draw_centered_text(draw=draw, text=headshots, x_left=1695, x_right=1760, y=1200, font=font_M, fill="white") draw_centered_text(draw=draw, text=headshots, x_left=1695, x_right=1760, y=1200, font=font_M_CH, fill="white")
# 载具 # 载具
draw_centered_text(draw=draw, text=bf_item[game][vehicle_name].upper(), x_left=1990, x_right=2440, y=1355, draw_centered_text(draw=draw, text=bf_item[game][vehicle_name].upper(), x_left=1990, x_right=2440, y=1355,
font=font_MS, fill="white") font=font_MS_CH, fill="white")
draw_centered_text(draw=draw, text=v_kills, x_left=1940, x_right=2150, y=1260, font=font_L, fill="white") draw_centered_text(draw=draw, text=v_kills, x_left=1940, x_right=2150, y=1260, font=font_L_CH, fill="white")
draw_centered_text(draw=draw, text=v_kpm, x_left=2295, x_right=2465, y=1055, font=font_XL, fill="white") draw_centered_text(draw=draw, text=v_kpm, x_left=2295, x_right=2465, y=1055, font=font_XL_CH, fill="white")
draw_centered_text(draw=draw, text=destroyed, x_left=2265, x_right=2440, y=1260, font=font_L, fill="white") draw_centered_text(draw=draw, text=destroyed, x_left=2265, x_right=2440, y=1260, font=font_L_CH, fill="white")
# 图片 # 图片
weapon_url = weapons["image"] weapon_url = weapons["image"]
@ -208,6 +411,64 @@ def build_best(draw: ImageDraw, img: Image, weapons, vehicle, game: str):
return img return img
def build_best_bf6(draw: ImageDraw, img: Image, weapons, vehicle, classes, game: str):
# 最佳武器
weapon_name = weapons["weaponName"]
w_kills = weapons["kills"]
w_kpm = weapons["killsPerMinute"]
acc = round(float(weapons["accuracy"].replace("%", "")), 1)
headshots = round(float(weapons["headshots"].replace("%", "")), 1)
# 最佳载具
vehicle_name = vehicle["vehicleName"]
v_kills = vehicle["kills"]
v_kpm = vehicle["killsPerMinute"]
destroyed = vehicle["destroyed"]
# 绘制最佳⭐
# 武器
# draw_centered_text(draw=draw, text=bf_item[game][weapon_name].upper(), x_left=1240, x_right=1750, y=1355,
# font=font_MS, fill="white")
draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 7), x_left=95, x_right=313, y=665, font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(w_kills)), x_left=95, x_right=313, y=665,
font=font_L, fill="white")
draw_centered_text(draw=draw, text=w_kpm, x_left=335, x_right=447, y=665, font=font_L, fill="white")
draw_centered_text(draw=draw, text=f"{acc}%", x_left=470, x_right=580, y=670, font=font_M, fill="white")
draw_centered_text(draw=draw, text=f"{headshots}%", x_left=610, x_right=717, y=670, font=font_M, fill="white")
# 载具
# draw_centered_text(draw=draw, text=bf_item[game][vehicle_name].upper(), x_left=1990, x_right=2440, y=1355,
# font=font_MS, fill="white")
draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 7), x_left=95, x_right=313, y=1087,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=add_commas(str(v_kills)), x_left=95, x_right=313, y=1087,
font=font_L, fill="white")
draw_centered_text(draw=draw, text=v_kpm, x_left=351, x_right=463, y=1087, font=font_L, fill="white")
draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 7), x_left=477, x_right=688, y=1087,
font=font_L,
fill="#333333")
draw_right_aligned_text(draw=draw, text=destroyed, x_left=477, x_right=688, y=1087, font=font_L, fill="white")
# 图片
weapon_url = weapons["image"]
vehicle_url = vehicle["image"]
wp_icon = get_save_icon(game, weapon_name, "weapon", weapon_url)
vc_icon = get_save_icon(game, vehicle_name, "vehicles", vehicle_url)
classes_icon = get_icon_from_cache(classes, 'bf6', 'classes')
wp_icon_new = png_resize(wp_icon, new_width=725 - 88)
vc_icon_new = png_resize(vc_icon, new_width=725 - 88)
classes_icon_new = png_resize(classes_icon, new_height=1072 - 325)
img = image_paste(wp_icon_new, img, (88, 325))
img = image_paste(vc_icon_new, img, (88, 750))
img = image_paste(classes_icon_new, img, (753, 325))
return img
def normalize_for_radar(data, buffer=0.1, scale_min=0.0, scale_max=1.0): def normalize_for_radar(data, buffer=0.1, scale_min=0.0, scale_max=1.0):
""" """
将数据动态归一化到指定区间 [scale_min, scale_max] 将数据动态归一化到指定区间 [scale_min, scale_max]
@ -237,6 +498,24 @@ def normalize_for_radar(data, buffer=0.1, scale_min=0.0, scale_max=1.0):
return norm.tolist() return norm.tolist()
def add_commas(text):
return re.sub(r'(?<!^)(?=(\d{3})+$)', ',', text)
def add_commas_with_padding(value, total_digits):
"""
固定总位数补零后再添加千分位分隔符
:param value: 输入数字int / str
:param total_digits: 固定总位数 7
:return: 格式化后的字符串 '1,234,567'
"""
# 转字符串并补零
text = str(value).zfill(total_digits)
# 添加千分位
return re.sub(r'(?<!^)(?=(\d{3})+$)', ',', text)
# ---------------- 雷达图示例 ---------------- # ---------------- 雷达图示例 ----------------
def draw_radar(img, stat_data, center=(311, 796), r=185, def draw_radar(img, stat_data, center=(311, 796), r=185,
buffer=0.1, scale_min=0.1, scale_max=1.05): buffer=0.1, scale_min=0.1, scale_max=1.05):
@ -284,7 +563,7 @@ def build_bf6_simple_card(player_info):
draw.text((50, 560), f"击杀总计:{player_info['击杀总计']}", fill='black', font=font_M) draw.text((50, 560), f"击杀总计:{player_info['击杀总计']}", fill='black', font=font_M)
draw.text((550, 560), f"真人击杀:{player_info['真人击杀']}", fill='black', font=font_M) draw.text((550, 560), f"真人击杀:{player_info['真人击杀']}", fill='black', font=font_M)
draw.text((50,660), f"游戏助攻:{player_info['游戏助攻']}", fill='black', font=font_M) draw.text((50, 660), f"游戏助攻:{player_info['游戏助攻']}", fill='black', font=font_M)
draw.text((550, 660), f"目标占领:{player_info['目标占领']}", fill='black', font=font_M) draw.text((550, 660), f"目标占领:{player_info['目标占领']}", fill='black', font=font_M)
draw.text((50, 760), f"救援数量:{player_info['救援数量']}", fill='black', font=font_M) draw.text((50, 760), f"救援数量:{player_info['救援数量']}", fill='black', font=font_M)

View File

@ -5,6 +5,7 @@ import random
import time import time
from io import BytesIO from io import BytesIO
from typing import List, Tuple from typing import List, Tuple
from .param import rank_pic
import cv2 import cv2
import numpy as np import numpy as np
@ -15,6 +16,8 @@ import requests
import requests.exceptions import requests.exceptions
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from src.plugins.bf_bot.user_data.data_utils import UserManager, DesignationManager
filepath = os.path.dirname(__file__).replace("\\", "/") filepath = os.path.dirname(__file__).replace("\\", "/")
@ -59,41 +62,72 @@ def circle_corner(img, radii):
return img return img
# PNG重绘大小 def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=""):
def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=''):
""" """
PNG缩放透明度处理 PNG 等比缩放保持 Alpha 透明度
:param source_file: 源文件Image.open() :param source_file: Image.open() 得到的 Image 对象
:param new_width: 设置的宽度 :param new_width: 目标宽度可选
:param new_height: 设置的高度 :param new_height: 目标高度可选
:param resample: 抗锯齿 :param resample: NEAREST / BILINEAR / BICUBIC / LANCZOS
:param ref_file: 参考文件 :param ref_file: 参考图片路径等比适配到参考尺寸
:return: :return: PIL.Image (RGBA)
""" """
img = source_file
img = img.convert("RGBA")
width, height = img.size
if ref_file != '': img = source_file.convert("RGBA")
imgRef = Image.open(ref_file) src_w, src_h = img.size
new_width, new_height = imgRef.size
# -------------------------
# 计算目标尺寸(等比)
# -------------------------
if ref_file:
ref_img = Image.open(ref_file)
ref_w, ref_h = ref_img.size
scale = min(ref_w / src_w, ref_h / src_h)
target_w = int(src_w * scale)
target_h = int(src_h * scale)
else: else:
if new_height == 0: if new_width > 0 and new_height > 0:
new_height = new_width * width / height scale = min(new_width / src_w, new_height / src_h)
target_w = int(src_w * scale)
target_h = int(src_h * scale)
bands = img.split() elif new_width > 0:
scale = new_width / src_w
target_w = new_width
target_h = int(src_h * scale)
elif new_height > 0:
scale = new_height / src_h
target_h = new_height
target_w = int(src_w * scale)
else:
# 未指定任何尺寸,直接返回
return img
# -------------------------
# 重采样方式
# -------------------------
resample_map = { resample_map = {
"NEAREST": Image.NEAREST, "NEAREST": Image.NEAREST,
"BILINEAR": Image.BILINEAR, "BILINEAR": Image.BILINEAR,
"BICUBIC": Image.BICUBIC, "BICUBIC": Image.BICUBIC,
"LANCZOS": Image.LANCZOS "LANCZOS": Image.LANCZOS
} }
resample_method = resample_map.get(resample, Image.LANCZOS) # 默认使用 LANCZOS resample_method = resample_map.get(resample, Image.LANCZOS)
bands = [b.resize((new_width, new_height), resample=resample_method) for b in bands] # -------------------------
resized_file = Image.merge('RGBA', bands) # Alpha 安全 resize逐通道
# -------------------------
bands = img.split()
resized_bands = [
band.resize((target_w, target_h), resample=resample_method)
for band in bands
]
return resized_file return Image.merge("RGBA", resized_bands)
# 图片粘贴 # 图片粘贴
@ -200,8 +234,36 @@ def draw_centered_text(draw, text, x_left, x_right, y, font, fill):
draw.text((x_center, y), text, font=font, fill=fill) draw.text((x_center, y), text, font=font, fill=fill)
def draw_right_aligned_text(draw, text, x_left, x_right, y, font, fill):
"""
在指定区域内从右往左绘制文本右对齐
:param draw: ImageDraw 对象
:param text: 要绘制的文本
:param x_left: 区域左边界 x
:param x_right: 区域右边界 x文本右侧贴齐这里
:param y: 文本基线 y 坐标
:param font: ImageFont 对象
:param fill: 文本颜色
"""
text = str(text)
text_width = font.getlength(text)
# 右对齐:文本右边贴到 x_right
x_start = x_right - text_width
# 可选:防止越界(超出左边界时截断)
if x_start < x_left:
x_start = x_left
draw.text((x_start, y), text, font=font, fill=fill)
def get_save_icon(game, name, icon_type, url): def get_save_icon(game, name, icon_type, url):
if game == 'bf6':
name = name.replace("/", "").upper()
else:
name = name.replace("/", "") name = name.replace("/", "")
logger.info(f"查询的物品: {name},图标类型:{icon_type},游戏:{game}")
icon = get_icon_from_cache(name, game, icon_type) icon = get_icon_from_cache(name, game, icon_type)
if not icon: if not icon:
if game == "bf1": if game == "bf1":
@ -257,8 +319,10 @@ def get_icon_from_url(url):
def get_icon_from_cache(icon_name, game, icon_type): def get_icon_from_cache(icon_name, game, icon_type):
path = f"{filepath}/img/{game}/{icon_type}" path = f"{filepath}/img/{game}/{icon_type}"
logger.info(f"查询物品:{icon_name}物品路径:{path}")
try: try:
icon_list = os.listdir(path) icon_list = os.listdir(path)
# logger.info(f"所有物品:{icon_list}")
if icon_name in str(icon_list): if icon_name in str(icon_list):
logger.info(f"本地存在{icon_name}物品") logger.info(f"本地存在{icon_name}物品")
img = Image.open(f"{path}/{icon_name}.png").convert('RGBA') img = Image.open(f"{path}/{icon_name}.png").convert('RGBA')
@ -365,3 +429,129 @@ def notice_paste(cv2_bg):
front_img = cutout_region(front_img, mask, alpha=0) front_img = cutout_region(front_img, mask, alpha=0)
mix_bg = paste_image_cv2(front_img, cv2_bg, pos) mix_bg = paste_image_cv2(front_img, cv2_bg, pos)
return mix_bg return mix_bg
def notice_paste_bf6(cv2_bg):
x, y = 0, 0
notice_path = f"{filepath}/notice/notice_bf6.png"
front_img = cv2.imread(notice_path, cv2.IMREAD_UNCHANGED)
if front_img is None:
raise RuntimeError(f"Failed to load image: {notice_path}")
h, w = front_img.shape[:2]
# ROI 边界裁剪
bg_h, bg_w = cv2_bg.shape[:2]
w = min(w, bg_w - x)
h = min(h, bg_h - y)
roi = cv2_bg[y:y + h, x:x + w]
# 有 Alpha 通道 → Alpha 混合
if front_img.shape[2] == 4:
alpha = front_img[:h, :w, 3] / 255.0
alpha = alpha[:, :, None]
cv2_bg[y:y + h, x:x + w] = (
alpha * front_img[:h, :w, :3] +
(1 - alpha) * roi
).astype(np.uint8)
# 无 Alpha → 直接覆盖
else:
cv2_bg[y:y + h, x:x + w] = front_img[:h, :w]
return cv2_bg
def designation_paste(player,
cv2_bg,
user_id,
start_x=10,
start_y=10,
target_height=40,
spacing=6
):
designation_list_path = get_designation_list_by_user_id(user_id, player)
if not designation_list_path:
return cv2_bg
icons = []
total_width = 0
for path in designation_list_path:
icon = cv2.imread(path, cv2.IMREAD_UNCHANGED)
if icon is None:
continue
h, w = icon.shape[:2]
scale = target_height / h
new_w = int(w * scale)
icon = cv2.resize(icon, (new_w, target_height))
icons.append(icon)
total_width += new_w
total_width += spacing * (len(icons) - 1)
x = start_x
y = start_y # 关键变化点:直接使用 top-left
bg_h, bg_w = cv2_bg.shape[:2]
for icon in icons:
ih, iw = icon.shape[:2]
if x + iw > bg_w or y + ih > bg_h:
break
roi = cv2_bg[y:y + ih, x:x + iw]
if icon.shape[2] == 4:
alpha = icon[:, :, 3] / 255.0
for c in range(3):
roi[:, :, c] = roi[:, :, c] * (1 - alpha) + icon[:, :, c] * alpha
else:
roi[:] = icon
cv2_bg[y:y + ih, x:x + iw] = roi
x += iw + spacing
return cv2_bg
def get_designation_list_by_user_id(user_id, player):
usermanager = UserManager()
designationmanager = DesignationManager()
user_info = usermanager.get_user_by_qq(user_id)
icon_no_list = []
if user_info is not None and user_info['ea_player_name'].upper() == player.upper():
logger.info(f"玩家标签列表:{user_info}")
icon_no_list = user_info['designation']
icon_list = designationmanager.get_designation_by_id_list(icon_no_list)
icon_path_list = []
for icon in icon_list:
icon_file_name = icon['icon_path']
icon_path_list.append(f"{filepath}/img/icon/designation/{icon_file_name}")
return icon_path_list
def paste_rank_icon(rank, img):
rank_pic_path = get_rank_pic(rank)
rank_icon_path = f"{filepath}/img/icon/rank/{rank_pic_path}"
rank_icon = Image.open(rank_icon_path)
rank_icon = png_resize(rank_icon, new_width=132, new_height=192)
fm_img = image_paste(rank_icon, img, (946, 61))
return fm_img
def get_rank_pic(rank: int) -> str:
"""
根据 rank 返回对应的 rank_pic 路径
"""
for (start, end), info in rank_pic.items():
if start <= rank <= end:
return info["path"]
return "t_ui_rankswatch_1-9 1.png"

View File

@ -60,6 +60,28 @@ classes = {
"Tanker": "坦克" "Tanker": "坦克"
} }
mods = {
}
rank_pic = {
(0, 9): {"path": "t_ui_rankswatch_1-9 1.png"},
(10, 24): {"path": "t_ui_rankswatch_10-24 1.png"},
(25, 44): {"path": "t_ui_rankswatch_25-44 1.png"},
(45, 49): {"path": "t_ui_rankswatch_45-49 1.png"},
(50, 99): {"path": "t_ui_rankswatch_50-99 1.png"},
(100, 149): {"path": "t_ui_rankswatch_100-149 1.png"},
(150, 199): {"path": "t_ui_rankswatch_150-190 1.png"},
(200, 249): {"path": "t_ui_rankswatch_200-240 1.png"},
(250, 299): {"path": "t_ui_rankswatch_250-290 1.png"},
(300, 349): {"path": "t_ui_rankswatch_300-340 1.png"},
(350, 399): {"path": "t_ui_rankswatch_350-390 1.png"},
(400, 449): {"path": "t_ui_rankswatch_400-450 1.png"},
(450, 499): {"path": "t_ui_rankswatch_450-490 1.png"},
(500, 2999): {"path": "t_ui_rankswatch_500-3000 1.png"},
(3000, 5000): {"path": "t_ui_rankswatch_3000-5000 1.png"},
}
bf_item = { bf_item = {
"bf1": { "bf1": {
"Wex": "韦克斯火焰喷射器", "Wex": "韦克斯火焰喷射器",
@ -943,7 +965,19 @@ bf_item = {
"AH-6J-LITTLE-BIRD1": "AH-6J-LITTLE-BIRD1", "AH-6J-LITTLE-BIRD1": "AH-6J-LITTLE-BIRD1",
"T-90A1": "T-90A1", "T-90A1": "T-90A1",
"HT-95-LEVKOV": "HT-95-LEVKOV" "HT-95-LEVKOV": "HT-95-LEVKOV"
} },
"bf6": {'L110': 'L110', 'PW5A3': 'PW5A3', 'M433': 'M433', 'RPKM': 'RPKM', 'M87A1': 'M87A1', 'P18': 'P18',
'M277': 'M277', 'B36A4': 'B36A4', 'L85A3': 'L85A3', 'M2010 ESR': 'M2010 ESR', 'M1014': 'M1014',
'AK-205': 'AK-205', 'SVK-8.6': 'SVK-8.6', 'SGX': 'SGX', 'LMR27': 'LMR27', 'QBZ-192': 'QBZ-192',
'M417 A2': 'M417 A2', 'DRS-IAR': 'DRS-IAR', 'KORD 6P67': 'KORD 6P67', 'USG-90': 'USG-90', 'M4A1': 'M4A1',
'KTS100 MK8': 'KTS100 MK8', 'M45A1': 'M45A1', 'KV9': 'KV9', 'SOR-556 Mk2': 'SOR-556 MK2', 'PW7A2': 'PW7A2',
'M123K': 'M123K', 'M44': 'M44', 'M250': 'M250', 'UMG-40': 'UMG-40', 'TR-7': 'TR-7', 'SL9': 'SL9',
'M240L': 'M240L', 'SCW-10': 'SCW-10', 'NVO-228E': 'NVO-228E', 'M/60': 'M/60', 'PSR': 'PSR', 'SVDM': 'SVDM',
'AK4D': 'AK4D', 'SOR-300SC': 'SOR-300SC', 'SG 553R': 'SG 553R', 'GRT-BC': 'GRT-BC', '18.5KS-K': '18.5KS-K',
'ES 5.7': 'ES 5.7', 'M39 EMR': 'M39 EMR', 'SV-98': 'SV-98', 'Panthera KHT': 'PANTHERA KHT',
'M77E Falchion': 'M77E FALCHION', 'Leo A4': 'LEO A4', 'Strf 09 A4': 'STRF 09 A4',
'M1A2 SEPv3': 'M1A2 SEPV3', 'Cheetah 1A2': 'CHEETAH 1A2', 'Glider 96': 'GLIDER 96',
'M3A3 Bradley': 'M3A3 BRADLEY', 'Su-57': 'SU-57', 'F-61V': 'F-61V', 'F-39E': 'F-39E', 'VECTOR': 'VECTOR'}
} }
color_select = { color_select = {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,110 @@
import asyncio
import os
from pathlib import Path
from typing import List, Dict, Optional
from nonebot import logger
from playwright.async_api import async_playwright, Response
async def fetch_tracker_search(
search_url: str,
cookies_txt: str,
wait_seconds: int = 5,
) -> Optional[Dict]:
"""
使用 Playwright + cookies 访问 tracker.gg API 并截获响应数据
"""
cookies = parse_netscape_cookies(cookies_txt)
if not cookies:
logger.error("cookies.txt 解析失败或为空")
return None
result: Optional[Dict] = None
async with async_playwright() as p:
browser = await p.chromium.launch(
executable_path="C:/Program Files/Google/Chrome/Application/chrome.exe",
headless=True, # False 可以看到浏览器操作
args=[
"--autoplay-policy=no-user-gesture-required",
"--disable-features=AutoplayDisableSuppression",
"--use-fake-ui-for-media-stream",
]
)
context = await browser.new_context(
viewport={"width": 640, "height": 320},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/143.0.0.0 Safari/537.36"
),
)
await context.add_cookies(cookies)
page = await context.new_page()
# 响应监听
async def handle_response(response: Response):
nonlocal result
try:
if search_url in response.url:
# 尝试解析 JSON
data = await response.json()
if data and "data" in data:
result = data
logger.info(f"成功截获响应数据: {data}")
except Exception as e:
logger.warning(f"解析响应失败: {e}")
page.on("response", handle_response)
try:
# 直接访问 API 链接
await page.goto(search_url, wait_until="domcontentloaded", timeout=60000)
# 等待响应截获
await page.wait_for_timeout(wait_seconds * 1000)
except Exception as e:
logger.error(f"访问页面出错: {e}")
finally:
await browser.close()
return result
def parse_netscape_cookies(cookies_txt_path: str) -> List[Dict]:
"""
解析 Netscape HTTP Cookie File -> Playwright cookies
"""
cookies: List[Dict] = []
if not os.path.exists(cookies_txt_path):
return cookies
with open(cookies_txt_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) != 7:
continue
domain, include_sub, path, secure, expiry, name, value = parts
cookie = {
"name": name,
"value": value,
"domain": domain,
"path": path,
"secure": secure.upper() == "TRUE",
"httpOnly": False,
}
if expiry.isdigit() and int(expiry) > 0:
cookie["expires"] = int(expiry)
cookies.append(cookie)
return cookies

View File

@ -1,10 +1,12 @@
import sqlite3 import sqlite3
import os
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
class TableManager: class TableManager:
def __init__(self, db_path: str = 'user_data.db'): def __init__(self, db_path: str = 'user_data.db'):
self.db_path = db_path base_dir = os.path.dirname(os.path.abspath(__file__))
self.db_path = os.path.join(base_dir, 'user_data.db')
def _execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor: def _execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor:
"""执行SQL语句的通用方法""" """执行SQL语句的通用方法"""
@ -17,76 +19,115 @@ class TableManager:
class UserManager(TableManager): class UserManager(TableManager):
def add_user(self, qq_id: str, ea_id: str, dog_tag_list: str) -> int: def add_user(self, qq_id: str, ea_player_name: str, ea_player_id: str, ea_user_id: str, designation: str,
"""添加用户记录""" medals: str) -> int:
"""添加用户"""
cursor = self._execute( cursor = self._execute(
"INSERT INTO users (qq_id, ea_id, dog_tag_list) VALUES (?, ?, ?)", "INSERT INTO users (qq_id, ea_player_name,ea_player_id,ea_user_id, designation,medals) VALUES (?, ?, ?, ?, ?,?)",
(qq_id, ea_id, dog_tag_list) (qq_id, ea_player_name, ea_player_id, ea_user_id, designation, medals)
) )
return cursor.lastrowid return cursor.lastrowid
def update_user(self, qq_id: str, ea_player_name: str, ea_player_id: str, ea_user_id: str, designation: str,
medals: str) -> int:
"""修改用户"""
cursor = self._execute(
"UPDATE users SET ea_player_name = ?, ea_player_id = ?,ea_user_id = ? WHERE qq_id = ?",
(ea_player_name, ea_player_id, ea_user_id, qq_id)
)
return cursor.rowcount
def get_user_by_qq(self, qq_id: str) -> Optional[Dict[str, Any]]: def get_user_by_qq(self, qq_id: str) -> Optional[Dict[str, Any]]:
"""通过QQ号查询用户"""
cursor = self._execute( cursor = self._execute(
"SELECT * FROM users WHERE qq_id = ?", "SELECT * FROM users WHERE qq_id = ?",
(qq_id,) (qq_id,)
) )
return dict(cursor.fetchone()) if cursor.fetchone() else None row = cursor.fetchone()
return dict(row) if row else None
def update_dog_tags(self, user_id: int, new_tags: str) -> bool: def get_user_by_ea_id(self, ea_user_id: str) -> Optional[Dict[str, Any]]:
cursor = self._execute(
"SELECT * FROM users WHERE ea_user_id = ?",
(ea_user_id,)
)
row = cursor.fetchone()
return dict(row) if row else None
def delete_user_by_qq(self, qq_id: str) -> Optional[Dict[str, Any]]:
cursor = self._execute(
"DELETE FROM users WHERE qq_id = ?",
(qq_id,)
)
return cursor.rowcount
def update_dog_tags(self, qq_id: int, medals: str) -> bool:
"""更新用户的狗牌列表""" """更新用户的狗牌列表"""
self._execute( self._execute(
"UPDATE users SET dog_tag_list = ? WHERE id = ?", "UPDATE users SET medals = ? WHERE id = ?",
(new_tags, user_id) (medals, qq_id)
) )
return True return True
class DogTagManager(TableManager): class DesignationManager(TableManager):
def create_tag(self, name: str) -> int: def create_designation(self, name: str, icon_path: str) -> int:
"""创建新狗牌""" """创建新标签"""
cursor = self._execute( cursor = self._execute(
"INSERT INTO dog_tag (name) VALUES (?)", "INSERT INTO designation (name,icon_path) VALUES (?,?)",
(name,) (name, icon_path)
) )
return cursor.lastrowid return cursor.lastrowid
def get_all_tags(self) -> List[Dict[str, Any]]: def get_all_designation(self) -> List[Dict[str, Any]]:
"""获取所有狗牌""" """获取所有标签"""
cursor = self._execute("SELECT * FROM dog_tag") cursor = self._execute("SELECT * FROM designation")
return [dict(row) for row in cursor.fetchall()]
def get_designation_by_id_list(self, ids: List[int]) -> List[Dict[str, Any]]:
"""根据 ID 列表获取标签"""
if not ids:
return []
placeholders = ",".join(["?"] * len(ids))
sql = f"""
SELECT *
FROM designation
WHERE id IN ({placeholders})
"""
cursor = self._execute(sql, ids)
return [dict(row) for row in cursor.fetchall()] return [dict(row) for row in cursor.fetchall()]
class QueryRecordManager(TableManager): # class QueryRecordManager(TableManager):
def log_query(self, user_id: str, target_id: str, status: str) -> int: # def log_query(self, user_id: str, target_id: str, status: str) -> int:
"""记录查询操作""" # """记录查询操作"""
cursor = self._execute( # cursor = self._execute(
"""INSERT INTO query_record # """INSERT INTO query_record
(user_id, target_id, status) # (user_id, target_id, status)
VALUES (?, ?, ?)""", # VALUES (?, ?, ?)""",
(user_id, target_id, status) # (user_id, target_id, status)
) # )
return cursor.lastrowid # return cursor.lastrowid
#
def get_user_history(self, user_id: str) -> List[Dict[str, Any]]: # def get_user_history(self, user_id: str) -> List[Dict[str, Any]]:
"""获取用户查询历史""" # """获取用户查询历史"""
cursor = self._execute( # cursor = self._execute(
"SELECT * FROM query_record WHERE user_id = ?", # "SELECT * FROM query_record WHERE user_id = ?",
(user_id,) # (user_id,)
) # )
return [dict(row) for row in cursor.fetchall()] # return [dict(row) for row in cursor.fetchall()]
if __name__ == "__main__": if __name__ == "__main__":
# 使用示例 tag_db = DesignationManager()
user_db = UserManager() # tag_id = tag_db.create_designation("DEV","DEV.png")
user_id = user_db.add_user("123456", "EA_001", "tag1,tag2") # tag_id = tag_db.create_designation("DICE", "DICE.png")
print(f"Created user with ID: {user_id}") # tag_id = tag_db.create_designation("IC", "IC.png")
# tag_id = tag_db.create_designation("OWNER", "OWNER.png")
# print(f"Created tag with ID: {tag_id}")
tag_db = DogTagManager() record_id = tag_db.get_designation_by_id_list([1, 2, 3])
tag_id = tag_db.create_tag("新狗牌")
print(f"Created tag with ID: {tag_id}")
record_db = QueryRecordManager()
record_id = record_db.log_query("123", "456", "success")
print(f"Logged query with ID: {record_id}") print(f"Logged query with ID: {record_id}")

View File

@ -11,20 +11,32 @@ class DatabaseManager:
'users': [ 'users': [
('id', 'INTEGER PRIMARY KEY AUTOINCREMENT'), ('id', 'INTEGER PRIMARY KEY AUTOINCREMENT'),
('qq_id', 'TEXT NOT NULL'), ('qq_id', 'TEXT NOT NULL'),
('ea_id', 'TEXT UNIQUE'), ('ea_player_name', 'TEXT UNIQUE'),
('dog_tag_list', 'TEXT NOT NULL'), ('ea_player_id', 'TEXT UNIQUE'),
('ea_user_id', 'TEXT UNIQUE'),
('designation', 'TEXT NOT NULL'),
('medals', 'TEXT NOT NULL'),
('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP')
], ],
'dog_tag': [ 'designation': [
('id', 'INTEGER PRIMARY KEY'), ('id', 'INTEGER PRIMARY KEY'),
('name', 'TEXT NOT NULL'), ('name', 'TEXT NOT NULL'),
('icon_path', 'TEXT NOT NULL'),
('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP')
],
'medals': [
('id', 'INTEGER PRIMARY KEY'),
('name', 'TEXT NOT NULL'),
('icon_path', 'TEXT NOT NULL'),
('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP')
] ]
, ,
'query_record': [ 'query_record': [
('id', 'INTEGER PRIMARY KEY'), ('id', 'INTEGER PRIMARY KEY'),
('user_id', 'TEXT NOT NULL'), ('user_id', 'TEXT NOT NULL'),
('target_id', 'TEXT NOT NULL'), ('ea_player_name', 'TEXT NOT NULL'),
('ea_player_id', 'TEXT NOT NULL'),
('ea_user_id', 'TEXT NOT NULL'),
('status', 'TEXT NOT NULL'), ('status', 'TEXT NOT NULL'),
('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP')
] ]