commit 9e60caaeebb7b85d32f4cccfe040b4eb7332cf31 Author: sanse <2931589710@qq.com> Date: Tue Oct 28 09:30:54 2025 +0800 首次提交 diff --git a/.env b/.env new file mode 100644 index 0000000..a96545d --- /dev/null +++ b/.env @@ -0,0 +1,38 @@ +DEBUG=true +HOST=0.0.0.0 # 配置 NoneBot 监听的 IP / 主机名 +PORT=43001 # 配置 NoneBot 监听的端口 +COMMAND_START=["","/"] # 配置命令起始字符 +COMMAND_SEP=["."] # 配置命令分割字符 +DRIVER=~fastapi+~httpx +LOG_LEVEL=DEBUG + + +SUPERUSERS=[] # 超级管理员 + +NICKNAME=[] # 机器人昵称 + +# QQ官方机器人配置文件 +# QQ_IS_SANDBOX=true +QQ_BOTS='[{ + "id": "102084781", + "token": "TqIcQANJ9YcK4TnZqikOtPI7QAzmZumQ", + "secret": "eTI7wlbRH7xndULC3ulcUME6yqiaTMF8", + "intent": { + "c2c_group_at_messages": true + }, + "use_websocket": false + }]' + +APSCHEDULER_CONFIG={"apscheduler.timezone": "Asia/Shanghai"} + + +# .env.prod +savedata = data/ram_data # 保存路径,相对路径,此处为保存至运行目录下的 "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 + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..9de98fd --- /dev/null +++ b/bot.py @@ -0,0 +1,22 @@ +import nonebot +from nonebot.adapters.qq import Adapter as QQ +from nonebot.log import logger + +# 初始化 NoneBot 以及 数据库 +nonebot.init() +app = nonebot.get_asgi() + +# 注册适配器 +driver = nonebot.get_driver() +driver.register_adapter(QQ) + + +# 加载自定义插件 +nonebot.load_plugins("src/plugins") # 加载bot自定义插件 + +# 加载配置文件中的插件 +nonebot.load_from_toml("pyproject.toml") + +if __name__ == "__main__": + logger.warning("Bot启动") + nonebot.run() diff --git a/dpces.txt b/dpces.txt new file mode 100644 index 0000000..1a0a739 --- /dev/null +++ b/dpces.txt @@ -0,0 +1,15 @@ +pip install nonebot-plugin-rauthman + +pip install nonebot-plugin-user + +pip install nonebot_plugin_userinfo + +pip install nonebot_plugin_session + +pip install nonebot-plugin-send-anything-anywhere + +pip install nonebot-plugin-apscheduler + +pip install nonebot-adapter-qq + +pip install nonebot2[fastapi] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..efd6c8a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[tool.poetry] +authors = ["sansenhoshi "] +description = "基于 NoneBot2 的 BF查询机器人" +license = "MIT" +name = "BF_BOT" +readme = "README.md" +repository = "" +version = "0.0.1" + +[tool.poetry.dependencies] +python = "^3.13" +eorzeaenv = "^2.2.8" +matplotlib = "^3.7.1" +expiringdict = "^1.2.2" + +nonebot2 = { extras = ["httpx", "fastapi", "websockets"], version = "^2.1.0" } +nb-cli = "^1.2.3" +nonebot-adapter-onebot = "2.2.4" + +nonebot-plugin-user = "^0.0.1" + +nonebot-plugin-apscheduler = "^0.3.0" +nonebot-plugin-send-anything-anywhere = "^0.3.1" +nonebot-plugin-alconna = "^0.24.0" +nonebot-plugin-session = "^0.1.0" +nonebot-plugin-userinfo = "^0.1.0" + +[tool.poetry.group.dev.dependencies] +nonebug = "^0.3.3" +pytest-cov = "^4.0.0" +pytest-mock = "^3.6.1" +pytest-xdist = "^3.0.2" +pytest-asyncio = "^0.21.0" +respx = "^0.20.1" +freezegun = "^1.2.2" +nonebug-saa = { git = "https://github.com/MountainDash/nonebug-saa.git" } + +[tool.nonebot] +adapters = [ + { name = "QQ", module_name = "nonebot.adapter.qq" } +] + +plugins = [ + "nonebot_plugin_apscheduler", + "nonebot_plugin_saa", + "nonebot_plugin_session", + "nonebot_plugin_userinfo", + "nonebot_plugin_user", + "nonebot_plugin_rauthman", +] + +[tool.black] +line-length = 88 + +[tool.isort] +profile = "black" +line_length = 88 +skip_gitignore = true + +[tool.pytest.ini_options] +asyncio_mode = "auto" + +[tool.pyright] +typeCheckingMode = "basic" + +[tool.ruff] +select = ["E", "W", "F", "UP", "C", "T", "PYI", "Q"] +ignore = ["E402", "E501", "E711", "C901", "UP037"] + + +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=1.0.0"] diff --git a/src/plugins/bf_bot/__init__.py b/src/plugins/bf_bot/__init__.py new file mode 100644 index 0000000..3b5ba42 --- /dev/null +++ b/src/plugins/bf_bot/__init__.py @@ -0,0 +1,115 @@ +# import time + +from nonebot import on_command, require +from nonebot.adapters import Message +from nonebot.matcher import Matcher +from nonebot.params import CommandArg +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.log import logger +import json + +require("nonebot_plugin_alconna") +from nonebot_plugin_alconna import UniMessage +from .data import * +from .data_utils import * +from .image_builder import * +from .text_utils import * + +__plugin_meta__ = PluginMetadata( + name="BF查询", + description="战地3,4,1,5,2042,6", + usage="", + extra={ + + }, +) + +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) + +bf_dict = { + "bf3": "战地3", + "bf4": "战地4", + "bf1": "战地1", + "bfv": "战地5", + "bf2042": "战地2042", + "bf6": "战地6", +} + + +@query.handle() +async def handle_function(matcher: Matcher, msg: Message = CommandArg()): + cmd = matcher.state["_prefix"]["command"][0] + game = cmd + content = msg.extract_plain_text() + play_stat = "" + if cmd == "bf3": + play_stat = await get_data_bf3(content, "pc") + elif cmd == "bf4": + play_stat = await get_data_bf4(content, "pc") + elif cmd == "bf1": + play_stat = await get_data_bf1(content, "pc") + elif cmd == "bfv": + play_stat = await get_data_bfv(content, "pc") + elif cmd == "bf2042": + play_stat = await get_data_bfv(content, "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: + await UniMessage.text("指令异常").finish() + if "errors" in play_stat: + logger.warning(play_stat['errors'][0]) + msg = play_stat['errors'][0] + else: + if cmd == "bf6": + img = build_bf6_simple_card(play_stat) + else: + weapon, vehicle = await get_best_weapon_and_best_vehicle(play_stat) + player = play_stat['userName'] + pid = play_stat['userId'] + kd = play_stat['killDeath'] + kpm = play_stat['killsPerMinute'] + spm = play_stat['scorePerMinute'] + acc = play_stat['accuracy'] + # 战地3 特化 + if cmd == 'bf3': + head_shots = play_stat['headShots'] + else: + head_shots = play_stat['headshots'] + + rank = play_stat['rank'] + time_play = convert_to_hours(play_stat['timePlayed']) + kills = int(play_stat['kills']) + kill_assists = int(play_stat['killAssists']) + revives = int(play_stat['revives']) + wins = int(play_stat['wins']) + loses = int(play_stat['loses']) + best_weapon = weapon + best_vehicle = vehicle + best_class = play_stat['bestClass'] + longest_head_shot = play_stat['longestHeadShot'] + highest_ill_streak = play_stat['highestKillStreak'] + + # await stats_calculator(play_stat) + + 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, + kill_assists, revives, wins, loses, destroyed, best_weapon, best_vehicle, + best_class, + longest_head_shot, highest_ill_streak, stat_data, level_designation) + await UniMessage.image(raw=img.getvalue()).finish() + await UniMessage.text(f"\n玩家【{content}】的【{bf_dict[cmd]}】数据\n{msg}").send() + + +@bind.handle() +async def bind_user(matcher: Matcher, msg: Message = CommandArg()): + matcher.state.get() diff --git a/src/plugins/bf_bot/avatar_cache/0.png b/src/plugins/bf_bot/avatar_cache/0.png new file mode 100644 index 0000000..f59ae5b Binary files /dev/null and b/src/plugins/bf_bot/avatar_cache/0.png differ diff --git a/src/plugins/bf_bot/bf6_data.py b/src/plugins/bf_bot/bf6_data.py new file mode 100644 index 0000000..440e40e --- /dev/null +++ b/src/plugins/bf_bot/bf6_data.py @@ -0,0 +1,380 @@ +import asyncio +import os +from pathlib import Path +from typing import List, Dict, Optional +from nonebot import logger +from curl_cffi import AsyncSession, CurlError + +# ---------- 配置 ---------- +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}" +file_path = os.path.dirname(__file__).replace("\\", "/") +exported_cookie_path = Path(f"{file_path}/cookies/tracker.txt") # 你导出的 cookies.txt +CUSTOM_UA = """Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36""" + + +# 你从抓包复制下来的完整请求头(不需要改格式) +RAW_BROWSER_HEADERS = """ +accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 +accept-encoding: gzip, deflate, br, zstd +accept-language: zh-CN,zh;q=0.9 +cache-control: max-age=0 +referer: https://account.tracker.gg/ +sec-ch-ua: "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141" +sec-ch-ua-mobile: ?0 +sec-ch-ua-platform: "Windows" +sec-fetch-dest: document +sec-fetch-mode: navigate +sec-fetch-site: same-origin +sec-fetch-user: ?1 +upgrade-insecure-requests: 1 +user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 +""" # ← 这里可以直接粘贴你浏览器的 headers(不用转义) + +# ============================================================= + + +def parse_raw_headers(raw: str) -> Dict[str, str]: + """ + 将抓包的多行 headers 文本解析为 Python dict + 自动去除 :authority、:method、:path、:scheme 等伪头 + """ + headers = {} + skip_prefix = (":authority", ":method", ":path", ":scheme") + for line in raw.strip().splitlines(): + if not line.strip(): + continue + if any(line.lower().startswith(p) for p in skip_prefix): + continue + if ":" not in line: + continue + key, value = line.split(":", 1) + headers[key.strip()] = value.strip() + return headers + + +def load_cookies_from_txt(path: Path) -> List[Dict[str, str]]: + """ + 从 cookies.txt 文件读取 cookies(支持 Netscape 格式 / name=value 格式) + """ + cookies = [] + if not path.exists(): + return cookies + with path.open("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: + cookies.append({"name": parts[5], "value": parts[6]}) + elif "=" in line: + k, v = line.split("=", 1) + cookies.append({"name": k.strip(), "value": v.strip()}) + return cookies + + +def build_cookie_header(cookies: List[Dict[str, str]]) -> str: + return "; ".join(f"{c['name']}={c['value']}" for c in cookies) + + +def build_headers( + raw_headers: str, + cookies: Optional[List[Dict[str, str]]] = None, + ua_override: Optional[str] = None, +) -> Dict[str, str]: + """ + 综合生成请求头: + 1. 从 RAW_BROWSER_HEADERS 解析 headers + 2. 若存在 cookies,则替换 cookie 字段 + 3. 若存在 ua_override,则覆盖 UA 字段 + """ + headers = parse_raw_headers(raw_headers) + + if cookies: + headers["cookie"] = build_cookie_header(cookies) + + if ua_override: + headers["user-agent"] = ua_override + + return headers + + +async def fetch_with_cookies(url: str, headers: dict, impersonate: str = "chrome110", timeout: int = 15): + async with AsyncSession() as session: + try: + response = await session.get(url, headers=headers, impersonate=impersonate, timeout=timeout) + return response + except CurlError as e: + return {"error": f"cURL 错误: {e}"} + except Exception as e: + return {"error": f"未知错误: {e}"} + + +def is_challenge_response(resp) -> bool: + if isinstance(resp, dict) and "error" in resp: + return True + try: + status = getattr(resp, "status_code", None) + text = getattr(resp, "text", "") or "" + if status in (429, 403): + return True + low = text.lower() + if any(k in low for k in ["cloudflare", "captcha", "cf-challenge", "just a moment"]): + return True + except Exception: + return True + return False + + +async def search_user_with_fallback(url: str): + cookies = load_cookies_from_txt(exported_cookie_path) + headers = build_headers(RAW_BROWSER_HEADERS, cookies=cookies, ua_override=CUSTOM_UA) + + logger.info(f"请求 URL: {url}") + logger.info(f"使用 headers 字段数: {len(headers)}") + + resp = await fetch_with_cookies(url, headers) + if is_challenge_response(resp): + logger.warning("⚠️ Cloudflare 拦截或 cookies 失效。") + if isinstance(resp, dict): + return resp + return {"status": resp.status_code, "preview": resp.text[:200]} + else: + logger.info("✅ 请求成功。") + return getattr(resp, "json", lambda: resp)() if hasattr(resp, "json") else resp + + +async def format_data(response, search_type): + status = "" + + platform_info = response['data']['platformInfo'] + segments = response['data']['segments'] + + overview = "" + weapons = [] + vehicles = [] + gamemodes = [] + gadgets = [] + kits = [] + maps = [] + + for segment in segments: + if segment['type'] == 'overview': + overview = segment + elif segment['type'] == 'weapon': + weapons.append(segment) + elif segment['type'] == 'vehicle': + vehicles.append(segment) + elif segment['type'] == 'gamemode': + gamemodes.append(segment) + elif segment['type'] == 'gadget': + gadgets.append(segment) + elif segment['type'] == 'kit': + kits.append(segment) + elif segment['type'] == 'level': + maps.append(segment) + if search_type == 0: + status = await get_overview(platform_info, overview, weapons, vehicles, gamemodes, gadgets, kits, maps) + if search_type == 1: + status = await get_weapons(platform_info, overview, weapons, vehicles, gamemodes, gadgets, kits, maps) + + return status + + +async def get_user_id_2_data(title_id, search_type): + url = url_overview.format(param=title_id) + info = await search_user_with_fallback(url) + info = await format_data(info, search_type) + return info + + +# 调用此方法获取格式化后的数据(调用此方法即可) +async def get_info(player, search_type): + # 先使用橘子搜索 + url = url_search.format(platform="orign", name=player) + result = await search_user_with_fallback(url) + # 无结果再使用steam搜索 + if "errors" in result: + url = url_search.format(platform="steam", name=player) + result = await search_user_with_fallback(url) + title_id_list = [] + logger.warning(result) + if "errors" in result: + return 3, '查询异常' + for res in result['data']: + if res['status'] is not None: + name = res['platformUserHandle'] + uid = res['titleUserId'] + status = res['status'].strip().split('•', 1)[0].replace("Rank", "") + user = { + 'name': name, + 'rank': status, + 'uid': uid, + } + title_id_list.append(user) + if len(title_id_list) > 1: + user_list = sorted(title_id_list, key=lambda k: k['rank'], reverse=True) + msg = '\n'.join(f'用户名:{info["name"]}-等级:{info["rank"]}-UID:{info["uid"]}\n' for info in user_list) + logger.info(f"检测到多个同名用户{msg}") + return 0, title_id_list + elif len(title_id_list) == 1: + logger.info(f"单用户{title_id_list}") + info = await get_user_id_2_data(title_id_list[0]['uid'], search_type) + return 1, info + else: + logger.info(f"未查询到用户") + return 2, '未查询到用户' + + +async def get_overview(platform_info, overview, weapons, vehicles, gamemodes, gadgets, kits, maps): + top_kill_weapon = sorted(weapons, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_weapon = { + "名称": top_kill_weapon['metadata']['name'], + "击杀": top_kill_weapon['stats']['kills']['value'], + "KPM": top_kill_weapon['stats']['killsPerMinute']['value'], + "时长": top_kill_weapon['stats']['timePlayed']['displayValue'], + "爆头率": top_kill_weapon['stats']['headshotPercentage']['displayValue'], + "命中率": top_kill_weapon['stats']['shotsAccuracy']['displayValue'] + } + top_kill_vehicle = sorted(vehicles, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_vehicle = { + "名称": top_kill_vehicle['metadata']['name'], + "击杀": top_kill_vehicle['stats']['kills']['value'], + "KPM": top_kill_vehicle['stats']['killsPerMinute']['value'], + "时长": top_kill_vehicle['stats']['timePlayed']['displayValue'], + "碾压": top_kill_vehicle['stats']['roadKills']['value'], + "摧毁": top_kill_vehicle['stats']['destroyedWith']['value'], + + } + shot_count = overview['stats']['shotsFired']['value'] + hit_count = overview['stats']['shotsHit']['value'] + logger.info(f"开火数:{shot_count}") + logger.info(f"命中数:{hit_count}") + acc = round((hit_count / shot_count) * 100, 2) + logger.info(f"命中率:{acc}%") + player_info = { + '玩家名称': platform_info['platformUserHandle'], + '游玩平台': platform_info['platformSlug'], + '游戏等级': overview['stats']['careerPlayerRank']['displayValue'], + '游玩时长': overview['stats']['timePlayed']['displayValue'], + '游玩场次': overview['stats']['matchesPlayed']['displayValue'], + '对局胜率': overview['stats']['wlPercentage']['displayValue'], + '对局得分': overview['stats']['score']['displayValue'], + '玩家K/D': overview['stats']['kdRatio']['value'], + '玩家KPM': overview['stats']['killsPerMinute']['value'], + '玩家SPM': overview['stats']['scorePerMinute']['value'], + '真人击杀': overview['stats']['playerKills']['displayValue'], + '击杀总计': overview['stats']['kills']['displayValue'], + '目标占领': overview['stats']['objectivesCaptured']['value'], + '游戏助攻': overview['stats']['assists']['displayValue'], + '救援数量': overview['stats']['revives']['displayValue'], + '最高连杀': overview['stats']['multiKills']['value'], + '命中率': f"{acc}%", + '爆头率': overview['stats']['headshotPercentage']['displayValue'], + '最佳武器': best_weapon, + '最佳载具': best_vehicle, + } + return player_info + + +async def get_weapons(platform_info, overview, weapons, vehicles, gamemodes, gadgets, kits, maps): + top_kill_weapon = sorted(weapons, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_weapon = { + "名称": top_kill_weapon['metadata']['name'], + "击杀": top_kill_weapon['stats']['kills']['value'], + "KPM": top_kill_weapon['stats']['killsPerMinute']['value'], + "时长": top_kill_weapon['stats']['timePlayed']['displayValue'], + "爆头率": top_kill_weapon['stats']['headshotPercentage']['displayValue'], + "命中率": top_kill_weapon['stats']['shotsAccuracy']['displayValue'] + } + top_kill_vehicle = sorted(vehicles, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_vehicle = { + "名称": top_kill_vehicle['metadata']['name'], + "击杀": top_kill_vehicle['stats']['kills']['value'], + "KPM": top_kill_vehicle['stats']['killsPerMinute']['value'], + "时长": top_kill_vehicle['stats']['timePlayed']['displayValue'], + "碾压": top_kill_vehicle['stats']['roadKills']['value'], + "摧毁": top_kill_vehicle['stats']['destroyedWith']['value'], + + } + shot_count = overview['stats']['shotsFired']['value'] + hit_count = overview['stats']['shotsHit']['value'] + logger.info(f"开火数:{shot_count}") + logger.info(f"命中数:{hit_count}") + acc = round((hit_count / shot_count) * 100, 2) + logger.info(f"命中率:{acc}%") + player_info = { + '玩家名称': platform_info['platformUserHandle'], + '游玩平台': platform_info['platformSlug'], + '游戏等级': overview['stats']['careerPlayerRank']['displayValue'], + '游玩时长': overview['stats']['timePlayed']['displayValue'], + '游玩场次': overview['stats']['matchesPlayed']['displayValue'], + '对局胜率': overview['stats']['wlPercentage']['displayValue'], + '对局得分': overview['stats']['score']['displayValue'], + '玩家K/D': overview['stats']['kdRatio']['value'], + '玩家KPM': overview['stats']['killsPerMinute']['value'], + '玩家SPM': overview['stats']['scorePerMinute']['value'], + '真人击杀': overview['stats']['playerKills']['displayValue'], + '击杀总计': overview['stats']['kills']['displayValue'], + '目标占领': overview['stats']['objectivesCaptured']['value'], + '游戏助攻': overview['stats']['assists']['displayValue'], + '救援数量': overview['stats']['revives']['displayValue'], + '最高连杀': overview['stats']['multiKills']['value'], + '命中率': f"{acc}%", + '爆头率': overview['stats']['headshotPercentage']['displayValue'], + '最佳武器': best_weapon, + '最佳载具': best_vehicle, + } + return player_info + + +async def get_vehicles(platform_info, overview, weapons, vehicles, gamemodes, gadgets, kits, maps): + top_kill_weapon = sorted(weapons, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_weapon = { + "名称": top_kill_weapon['metadata']['name'], + "击杀": top_kill_weapon['stats']['kills']['value'], + "KPM": top_kill_weapon['stats']['killsPerMinute']['value'], + "时长": top_kill_weapon['stats']['timePlayed']['displayValue'], + "爆头率": top_kill_weapon['stats']['headshotPercentage']['displayValue'], + "命中率": top_kill_weapon['stats']['shotsAccuracy']['displayValue'] + } + top_kill_vehicle = sorted(vehicles, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_vehicle = { + "名称": top_kill_vehicle['metadata']['name'], + "击杀": top_kill_vehicle['stats']['kills']['value'], + "KPM": top_kill_vehicle['stats']['killsPerMinute']['value'], + "时长": top_kill_vehicle['stats']['timePlayed']['displayValue'], + "碾压": top_kill_vehicle['stats']['roadKills']['value'], + "摧毁": top_kill_vehicle['stats']['destroyedWith']['value'], + + } + shot_count = overview['stats']['shotsFired']['value'] + hit_count = overview['stats']['shotsHit']['value'] + logger.info(f"开火数:{shot_count}") + logger.info(f"命中数:{hit_count}") + acc = round((hit_count / shot_count) * 100, 2) + logger.info(f"命中率:{acc}%") + player_info = { + '玩家名称': platform_info['platformUserHandle'], + '游玩平台': platform_info['platformSlug'], + '游戏等级': overview['stats']['careerPlayerRank']['displayValue'], + '游玩时长': overview['stats']['timePlayed']['displayValue'], + '游玩场次': overview['stats']['matchesPlayed']['displayValue'], + '对局胜率': overview['stats']['wlPercentage']['displayValue'], + '对局得分': overview['stats']['score']['displayValue'], + '玩家K/D': overview['stats']['kdRatio']['value'], + '玩家KPM': overview['stats']['killsPerMinute']['value'], + '玩家SPM': overview['stats']['scorePerMinute']['value'], + '真人击杀': overview['stats']['playerKills']['displayValue'], + '击杀总计': overview['stats']['kills']['displayValue'], + '目标占领': overview['stats']['objectivesCaptured']['value'], + '游戏助攻': overview['stats']['assists']['displayValue'], + '救援数量': overview['stats']['revives']['displayValue'], + '最高连杀': overview['stats']['multiKills']['value'], + '命中率': f"{acc}%", + '爆头率': overview['stats']['headshotPercentage']['displayValue'], + '最佳武器': best_weapon, + '最佳载具': best_vehicle, + } + return player_info diff --git a/src/plugins/bf_bot/bf6_state_data.py b/src/plugins/bf_bot/bf6_state_data.py new file mode 100644 index 0000000..325b08f --- /dev/null +++ b/src/plugins/bf_bot/bf6_state_data.py @@ -0,0 +1,173 @@ +import json +import asyncio +import os +from curl_cffi import requests, AsyncSession, CurlError +from nonebot import logger + +url_search = "https://api.tracker.gg/api/v2/bf6/standard/search?platform=origin&query={name}&autocomplete=true" +url_overview = "https://api.tracker.gg/api/v2/bf6/standard/profile/ign/{user_id}?" + +filepath = os.path.dirname(__file__).replace("\\", "/") + + +def load_cookies_from_txt(path: str): + cookies = [] + with open(path, "r", encoding="utf-8") as f: + for line in f: + if line.startswith("#") or not line.strip(): + continue + parts = line.strip().split("\t") + if len(parts) >= 7: + cookies.append({"name": parts[5], "value": parts[6]}) + return cookies + + +def build_cookie_header(cookies): + return "; ".join([f"{c['name']}={c['value']}" for c in cookies]) + + +async def fetch_url(url: str, headers: dict): + async with AsyncSession() as session: + try: + response = await session.get(url, headers=headers, impersonate="chrome110") + return response + except CurlError as e: + logger.warning("cURL 错误:", e) + return None + + +async def search_user(name: str): + cookies = load_cookies_from_txt(f"{filepath}/cookies/tracker.txt") + cookie_header = build_cookie_header(cookies) + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", + "user-agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/139.0.0.0 Safari/537.36" + ), + "cookie": cookie_header, + "referer": "https://tracker.gg/", + } + + search_url = url_search.format(name=name) + response = await fetch_url(search_url, headers=headers) + if not response: + logger.warning("请求失败") + return + + if response.status_code == 200: + logger.info("搜索成功") + json_data = response.json() + title_id_list = [] + for res in json_data['data']: + title_id_list.append(res['titleUserId']) + for title_id in title_id_list: + flag, info = await get_data(title_id) + if flag: + return info + else: + continue + else: + logger.warning(f"请求失败,状态码: {response.status_code}") + logger.warning(response.text) + + +async def get_data(user_id: str): + cookies = load_cookies_from_txt(f"{filepath}/cookies/tracker.txt") + cookie_header = build_cookie_header(cookies) + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", + "user-agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/139.0.0.0 Safari/537.36" + ), + "cookie": cookie_header, + "referer": "https://tracker.gg/", + } + + get_url = url_overview.format(user_id=user_id) + response = await fetch_url(get_url, headers=headers) + if not response: + logger.warning("获取失败") + return False + + if response.status_code == 200: + logger.info("获取成功") + platform_info = response.json()['data']['platformInfo'] + segments = response.json()['data']['segments'] + + overview = "" + weapons = [] + vehicles = [] + gamemodes = [] + gadgets = [] + kits = [] + maps = [] + + for segment in segments: + if segment['type'] == 'overview': + overview = segment + elif segment['type'] == 'weapon': + weapons.append(segment) + elif segment['type'] == 'vehicle': + vehicles.append(segment) + elif segment['type'] == 'gamemode': + gamemodes.append(segment) + elif segment['type'] == 'gadget': + gadgets.append(segment) + elif segment['type'] == 'kit': + kits.append(segment) + elif segment['type'] == 'level': + maps.append(segment) + + top_kill_weapon = sorted(weapons, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_weapon = { + "名称": top_kill_weapon['metadata']['name'], + "击杀": top_kill_weapon['stats']['kills']['value'], + "KPM": top_kill_weapon['stats']['killsPerMinute']['value'], + "时长": top_kill_weapon['stats']['timePlayed']['displayValue'], + "爆头率": top_kill_weapon['stats']['headshotPercentage']['displayValue'], + "命中率": top_kill_weapon['stats']['shotsAccuracy']['displayValue'] + } + top_kill_vehicle = sorted(vehicles, key=lambda k: k['stats']['kills']['value'], reverse=True)[0] + best_vehicle = { + "名称": top_kill_vehicle['metadata']['name'], + "击杀": top_kill_vehicle['stats']['kills']['value'], + "KPM": top_kill_vehicle['stats']['killsPerMinute']['value'], + "时长": top_kill_vehicle['stats']['timePlayed']['displayValue'], + "碾压": top_kill_vehicle['stats']['roadKills']['value'], + + } + + player_info = { + '玩家': platform_info['platformUserHandle'], + '平台': platform_info['platformSlug'], + '等级': overview['stats']['careerPlayerRank']['displayValue'], + '时长': overview['stats']['timePlayed']['displayValue'], + '场次': overview['stats']['matchesPlayed']['displayValue'], + '胜率': overview['stats']['wlPercentage']['displayValue'], + '总得分': overview['stats']['score']['displayValue'], + 'K/D': overview['stats']['kdRatio']['value'], + 'KPM': overview['stats']['killsPerMinute']['value'], + 'SPM': overview['stats']['scorePerMinute']['value'], + '击杀': overview['stats']['kills']['displayValue'], + '助攻': overview['stats']['assists']['displayValue'], + '救援': overview['stats']['revives']['displayValue'], + '命中率': f"{round(overview['stats']['shotsFired']['value'] / overview['stats']['shotsHit']['value'])}%", + '爆头率': overview['stats']['headshotPercentage']['displayValue'], + '最佳武器': best_weapon, + '最佳载具': best_vehicle, + } + return True, player_info + else: + logger.warning(f"请求失败,状态码: {response.status_code}") + logger.warning(response.text) + + +if __name__ == "__main__": + name = "HEIZI-HARUSAME" + asyncio.run(search_user(name)) diff --git a/src/plugins/bf_bot/bf_state_data.py b/src/plugins/bf_bot/bf_state_data.py new file mode 100644 index 0000000..15df206 --- /dev/null +++ b/src/plugins/bf_bot/bf_state_data.py @@ -0,0 +1,233 @@ +import json +from nonebot import logger +import os + +from cffi.model import voidp_type +from jinja2 import Environment, FileSystemLoader +from playwright.async_api import async_playwright, ViewportSize +import asyncio + +from Core.Utils import get_root_path +from Core.BrowserCore import BrowserTab +from curl_cffi import requests, AsyncSession, CurlError +import base64 + +b = BrowserTab("https://battlefieldtracker.com/bf2042/profile/origin/Sansorano_Yume/overview") + +url_zy = "https://battlefieldtracker.com/bf2042/profile/origin/{name}/overview" +url_over = "https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/{name}" +url_weapon = "https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/{name}/segments/weapon" +url_vehicle = "https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/{name}/segments/vehicle" +url_soldier = "https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/{name}/segments/soldier" +url_history = "https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/{name}/history" +url_ban = "https://api.gametools.network/bfban/checkban/?names={name}" + +url_dict = { + "overview": url_over, + "weapons": url_weapon, + "vehicle": url_vehicle, + "soldier": url_soldier, + "ban": url_ban, +} +headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", + "priority": "u=1, i", + "sec-ch-ua": '"Not;A=Brand";v="99", "Microsoft Edge";v="139", "Chromium";v="139"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0" +} +classesList = { + "Mackay": " 麦凯", + "Angel": " 天使", + "Falck": " 法尔克", + "Paik": " 白智秀", + "Sundance": " 日舞", + "Dozer": " 推土机", + "Rao": " 拉奥", + "Lis": " 莉丝", + "Irish": "爱尔兰佬", + "Crawford": "克劳福德", + "Boris": " 鲍里斯", + "Zain": " 扎因", + "Casper": " 卡斯帕", + "Blasco": "布拉斯科", + "BF3 Recon": "BF3 侦察", + "BF3 Support": "BF3 支援", + "BF3 Assault": "BF3 突击", + "BF3 Engineer": "BF3 工程", + "BC2 Recon": "BC2 侦察", + "BC2 Medic": "BC2 医疗", + "BC2 Assault": "BC2 突击", + "BC2 Engineer": "BC2 工程", + "1942 Anti-tank": "1942 反坦克", + "1942 Assault": "1942 突击", + "1942 Medic": "1942 医疗", + "1942 Engineer": "1942 工程", + "1942 Scout": "1942 侦察", +} + + +def get_base64(img_path: str, deafult: str = None): + path = f"{get_root_path()}/{img_path}" + if not os.path.exists(path): + if deafult is None: + return False, None + return get_base64(deafult) + with open(path, "rb") as f: + return True, base64.b64encode(f.read()).decode() + + +async def fetch_url(url: str, name: str, browser: str = "chrome110"): + async with AsyncSession() as session: + try: + response = await session.get(url, impersonate=browser) + return { + "name": name, + "status": response.status_code, + "content": response.json(), + } + except TimeoutError: + return {"name": name, "status": 0, "error": "请求超时"} + except ConnectionError: + return {"name": name, "status": 0, "error": "连接失败"} + except CurlError as e: + return {"name": name, "status": 0, "error": f"底层 cURL 错误: {str(e)}"} + except Exception as e: + return {"name": name, "status": 0, "error": f"未知错误: {str(e)}"} + + +async def bf2042_state(cmd): + name = cmd + raw = {} + logger.info("开始get数据") + task = [fetch_url(v.format(name=name), k) for k, v in url_dict.items()] + res = await asyncio.gather(*task) + logger.info("开始解析数据") + if raw is None or isinstance(raw, str): + await event.send("没有找到该玩家或者服务器请求失败,请等待") + return + flag_cf = False + has_player = False + for i in res: + if i["status"] == 404: + try: + json.loads(i["content"]) + has_player = True + except json.JSONDecodeError: + flag_cf = True + return + if i["status"] == 0 or i["status"] == 400: + has_player = True + if flag_cf: + await event.send("当前数据获取失败,正在过盾,请等待响应") + await b.cf_bypasser.bypass_turnstile() + b.page.get_screenshot("img/hc.png") + if b.cf_bypasser.is_bypassed() or b.cf_bypasser.is_turnstile(): + await event.send("已获取认证,请重新查询") + else: + await event.send(Message(TextSegment("获取失败已返回结果图")).append(ImgSegment("img/hc.png"))) + return + if has_player: + await event.send("没有找到该玩家,,该文件的名字可能出错或者该玩家并未开启隐私设置") + return + logging.info(1) + overview = res[0]["content"]["data"]["segments"][0]["stats"] + logging.info(2) + res[1]["content"]["data"].sort(key=lambda x: x["stats"]["kills"]["value"], reverse=True) + weapons = res[1]["content"]["data"][:6] + # logging.info(weapons) + logging.info(3) + res[2]["content"]["data"].sort(key=lambda x: x["stats"]["kills"]["value"], reverse=True) + veh = res[2]["content"]["data"][:6] + logging.info(veh) + logging.info(4) + res[3]["content"]["data"].sort(key=lambda x: x["stats"]["kills"]["value"], reverse=True) + class_data = res[3]["content"]["data"][0] + # logging.info(class_data) + logging.info(5) + cheat_hack = res[4]["content"]["names"][name.lower()]["hacker"] + logging.info(6) + + percentile_kills = overview["kills"]["percentile"] + kills_t = f"Top {round(100 - percentile_kills, 2)}" if percentile > 50 else f"Bottom {percentile}" + kills_t = f"{kills_t}%" + + percentile_deaths = overview["deaths"]["percentile"] + death_t = f"Top {round(100 - percentile_deaths, 2)}" if percentile > 50 else f"Bottom {percentile}" + death_t = f"{death_t}%" + + percentile_assists = overview["assists"]["percentile"] + zg_t = f"Top {round(100 - percentile_assists, 2)}" if percentile > 50 else f"Bottom {percentile}" + zg_t = f"{zg_t}%" + + percentile_wins = overview["wins"]["percentile"] + slcs_t = f"Top {round(100 - percentile_wins, 2)}" if percentile > 50 else f"Bottom {percentile}" + slcs_t = f"{slcs_t}%" + + percentile_losses = overview["losses"]["percentile"] + sbcs_t = f"Top {round(100 - percentile_losses, 2)}" if percentile > 50 else f"Bottom {percentile}" + sbcs_t = f"{sbcs_t}%" + + percentile_revives = overview["revives"]["percentile"] + fhcs_t = f"Top {round(100 - percentile_revives, 2)}" if percentile > 50 else f"Bottom {percentile}" + fhcs_t = f"{fhcs_t}%" + + data = { + "playerName": name, + "level": overview["level"]["displayValue"], + "time": overview["timePlayed"]["displayValue"], + "kd": overview["kdRatio"]["displayValue"], + "kd_real": overview["humanKdRatio"]["displayValue"], + "kpm": overview["killsPerMinute"]["displayValue"], + "win_acc": overview["wlPercentage"]["displayValue"], + "kills": overview["kills"]["displayValue"], + "kills_p": overview["kills"]["percentile"], + "kills_t": kills_t, + "death": overview["deaths"]["displayValue"], + "death_p": overview["deaths"]["percentile"], + "death_t": death_t, + "ai_kills": overview["aiKills"]["displayValue"], + "zr_kills": overview["humanKills"]["displayValue"], + "zk_kills": overview["vehicleKills"]["displayValue"], + "dc_kills": overview["multiKills"]["displayValue"], + "jz_kills": overview["meleeKills"]["displayValue"], + "zj_kills": overview["roadKills"]["displayValue"], + "zg": overview["assists"]["displayValue"], + "zg_p": overview["assists"]["percentile"], + "zg_t": zg_t, + "slcs": overview["wins"]["displayValue"], + "slcs_p": overview["wins"]["percentile"], + "slcs_t": slcs_t, + "sbcs": overview["losses"]["displayValue"], + "sbcs_p": overview["losses"]["percentile"], + "sbcs_t": sbcs_t, + "fhcs": overview["revives"]["displayValue"], + "fhcs_p": overview["revives"]["percentile"], + "fhcs_t": fhcs_t, + "zsh": overview["damageDealt"]["displayValue"], + "mfzsh": round(overview["damageDealt"]["value"] / (overview["timePlayed"]["value"] / 60), 2), + "chzj": overview["vehiclesDestroyed"]["displayValue"], + "kjjs": overview["scopedKills"]["displayValue"], + "mbzlsj": overview["objectiveTime"]["displayValue"], + "zlmb": overview["objectivesCaptured"]["displayValue"], + "fsmb": overview["defendedObjectives"]["displayValue"], + "a_c_c": f"{overview['objectivesArmed']['displayValue']}/{overview['objectivesDisarmed']['displayValue']}/{overview['objectivesDestroyed']['displayValue']}", + "fsqy": overview["defendedSectors"]["displayValue"], + "sqqb": overview["intelPickedUp"]["displayValue"], + "tqqb": overview["intelExtracted"]["displayValue"], + "btl": overview["headshotPercentage"]["displayValue"], + "bts": overview["headshotKills"]["displayValue"], + "w_data": weapons, + "v_data": veh, + "zjmz": classesList[class_data["metadata"]["name"].strip()] if class_data["metadata"][ + "name"].strip() in classesList else + class_data["metadata"]["name"], + "zj_kd": class_data["stats"]["kdRatio"]["displayValue"], + "zj_kpm": class_data["stats"]["killsPerMinute"]["displayValue"], + "zjia_kills": class_data["stats"]["kills"]["displayValue"], + "zj_time": class_data["stats"]["timePlayed"]["displayValue"], + } + logger.info("解析数据完成") + return data diff --git a/src/plugins/bf_bot/cookies/tracker.txt b/src/plugins/bf_bot/cookies/tracker.txt new file mode 100644 index 0000000..77c2ff2 --- /dev/null +++ b/src/plugins/bf_bot/cookies/tracker.txt @@ -0,0 +1,25 @@ +# Netscape HTTP Cookie File +# https://curl.haxx.se/rfc/cookie_spec.html +# This is a generated file! Do not edit. + +tracker.gg FALSE / FALSE 1763712553 _lr_env_src_ats false +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 / TRUE 1792657486 __stripe_mid f3195526-098b-451a-a803-afe861f3546981d4b7 +.tracker.gg TRUE / FALSE 1795681481 _ga GA1.1.1090555935.1761016835 +.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 1766305486 pbjs-unifiedid_cst YiwPLDosoA%3D%3D +.tracker.gg TRUE / FALSE 1784448555 _cc_id d7583525ab694e187a68c7c9adac9679 +tracker.gg FALSE / FALSE 1761621671 _lr_sampling_rate 100 +.tracker.gg TRUE / FALSE 1794757153 cto_bidid ZYK0w18xUFhlUWZoQ2ttd2Vlc0lnVjV1azlmSXI5ZlBVRUZ3QjVFazJUOHAlMkZnYWlFc0l2endrZGVDb1Z6dXdJdXA4V3Y1OVlEVGI4VUlNV1QxejgwWmxwJTJGRjByeGJFbWhFRTBQQnExU1luNTlvd1klM0Q +.tracker.gg TRUE / TRUE 1761122345 __cf_bm 0U2HKncz5gYebQMBxL_qtaeuqoAdy.5lnue9xCP_Lls-1761120544-1.0.1.1-vFt.KkK9agrVBKJiVR6KstByxEN86BEvyVVQxg5VhtRT40Oq6Bmaan1yzLbW9V0ixAlOLPpYcflX6CqpCjl0D9Lg.73D54Jnm3KLQ5tGxnEZhnr0ORp6W5HYsfpsNiRc +tracker.gg FALSE / FALSE 1761124153 _lr_retry_request true +.tracker.gg TRUE / FALSE 1761206955 panoramaId_expiry 1761206955341 +.tracker.gg TRUE / TRUE 1761123286 __stripe_sid 99b864d5-840b-4cce-9c31-b4699e594ec16de245 +.tracker.gg TRUE / FALSE 1795681481 _ga_HWSV72GK8X GS2.1.s1761120545$o13$g1$t1761121480$j60$l0$h0 +tracker.gg FALSE / FALSE 1766305486 pbjs-unifiedid_last Wed%2C%2022%20Oct%202025%2008%3A24%3A46%20GMT +.tracker.gg TRUE / FALSE 1794817546 cto_bundle dv27kl9RN0JGSHVyZE9za1E5a1ZNZEo4R21rQVkxVUFtYSUyRjQxd0JaWDJtWGhOVmZnV2hxUEVzemZ2TE9XSVB0UXNMVkZySUtXaW0lMkZ4WTRnN3FhSmNFOE4lMkI2WXg4cW42UFpKd2I3QmViY1JuTkZYMG9FVUZ1STJESHc2NDUzMHBEMyUyQm92bUtEMzJlajF0bXMyZ3M1Z0YwJTJGRkR3JTNEJTNE +.tracker.gg TRUE / TRUE 1792657481 cf_clearance betnOuJnsRXlDgs9rKx_2GJXze5bdRFK3eVFFB9k610-1761121481-1.2.1.1-RDnj4R4HqOjhE1Sn5Dp5wKysllw6cVSYRPbbL4y_STOAnBlQEVRv_7xC1h9TleDg9Ecyy2neJCW1Xk6BNd51K4htSHptKFlQF8tv_JqMlDyFg9DxShAveOMXu4o9vkivWZmFvJRKv9ERqm33dfsYAQD5lkyYFtsurraCOPzu3TILLbsnjoi2v_kCILeFLPdEbeiABRHQUrsZdi1jhwKM9cDJ_XHs0bmPZrDOfVaf444 diff --git a/src/plugins/bf_bot/data.py b/src/plugins/bf_bot/data.py new file mode 100644 index 0000000..9242237 --- /dev/null +++ b/src/plugins/bf_bot/data.py @@ -0,0 +1,57 @@ +import json + +import requests + +from .bf6_data import * + + +async def get_data_bf3(user_name, platform): + url = f"https://api.gametools.network/bf3/all/?format_values=true&name={user_name}&platform={platform}" + + payload = {} + headers = { + 'accept': 'application/json' + } + response = requests.request("GET", url, headers=headers, data=payload) + data_json = json.loads(response.text) + return data_json + + +async def get_data_bf4(user_name, platform): + url = f"https://api.gametools.network/bf4/all/?format_values=true&name={user_name}&platform={platform}" + payload = {} + headers = { + 'accept': 'application/json' + } + response = requests.request("GET", url, headers=headers, data=payload) + data_json = json.loads(response.text) + return data_json + + +async def get_data_bf1(user_name, platform): + url = f"https://api.gametools.network/bf1/all/?format_values=true&name={user_name}&platform={platform}&skip_battlelog=false&lang=en-us" + + payload = {} + headers = { + 'accept': 'application/json' + } + response = requests.request("GET", url, headers=headers, data=payload) + data_json = json.loads(response.text) + return data_json + + +async def get_data_bfv(user_name, platform): + url = f"https://api.gametools.network/bfv/all/?format_values=true&name={user_name}&platform={platform}&skip_battlelog=false&lang=en-us" + + payload = {} + headers = { + 'accept': 'application/json' + } + response = requests.request("GET", url, headers=headers, data=payload) + data_json = json.loads(response.text) + return data_json + + +async def get_data_bf6(user_name, search_type): + flag, data_json = await get_info(user_name, search_type) + return flag, data_json diff --git a/src/plugins/bf_bot/data_utils.py b/src/plugins/bf_bot/data_utils.py new file mode 100644 index 0000000..cc4ec83 --- /dev/null +++ b/src/plugins/bf_bot/data_utils.py @@ -0,0 +1,141 @@ +import os +from PIL import Image, ImageDraw, ImageFont +from nonebot import logger +from functools import reduce + +from .img_utils import png_resize +from .param import round_data, interval_table + +filepath = os.path.dirname(__file__).replace("\\", "/") + + +# async def avatar_cache(avatar_url, player_id): +# if avatar_url: +# try: +# # 添加10s超时判断,如果超时直接使用默认头像 +# res = BytesIO(requests.get(avatar_url, timeout=10).content) +# avatar = Image.open(res).convert('RGBA') +# avatar.save(f'{filepath}/avatar_cache/{player_id}.png', 'PNG') +# return avatar +# except requests.exceptions.RequestException as e: +# logger.warning(f"请求异常:{e}") +# else: +# return None +# +# +# async def get_avatar(avatar_url, player_id): +# img = Image.open(filepath + "/avatar_cache/0.png").convert('RGBA') +# path = filepath + "/avatar_cache/" +# try: +# avatar_list = os.listdir(path) +# if player_id in str(avatar_list): +# logger.info(f"本地已存在{player_id}头像") +# img = Image.open(f"{path}{player_id}.png").convert('RGBA') +# else: +# logger.info(f"未检测到{player_id}头像,缓存至本地") +# out_put = filepath + f"/avatar_cache/{player_id}.png" +# img = await avatar_cache(avatar_url, out_put) +# except Exception as err: +# logger.error(f"获取头像失败:{str(err)}") +# return img + + +async def get_best_weapon_and_best_vehicle(player_stat): + weapon = sorted(player_stat["weapons"], key=lambda k: k['kills'], reverse=True)[0] + vehicle = sorted(player_stat["vehicles"], key=lambda k: k['kills'], reverse=True)[0] + return weapon, vehicle + + +async def get_vehicle_destroyed(player_stat): + vehicle = reduce(lambda acc, x: acc + x['destroyed'], player_stat, 0) + return vehicle + + +async def stats_calculator(player_stat, game): + per500_data = round_data[game] + # 场次(无直接数值反馈,采用胜利数+失败数) + match_played = player_stat["wins"] + player_stat["loses"] + + kd = player_stat["killDeath"] + kpm = player_stat["killsPerMinute"] + kill_assists = player_stat["killAssists"] + acc = float(player_stat["accuracy"].replace("%", "")) + if game == "bf3": + headshots = float(0) + else: + headshots = float(player_stat["headshots"].replace("%", "")) + spm = player_stat["scorePerMinute"] + revives = player_stat["revives"] + + # second_play = player_stat["secondsPlayed"] + + apr = kill_assists / match_played + rpr = revives / match_played + + # SPM*秒数/60/场数=场均得分 + # dpr = score_perMinute * (second_play / 60) / match_played + + # 计算比值 + # kd + ratio_kd = round(kd / per500_data["kd"], 2) + # kpm + ratio_kpm = round(kpm / per500_data["kpm"], 2) + # 爆头率 + ratio_hs = round(headshots / 100 / per500_data["hs"], 2) + # 场均救援 + ratio_rpr = round(rpr / per500_data["rpr"], 2) + # 场均助攻 + ratio_apr = round(apr / per500_data["apr"], 2) + # 分钟得分 + ratio_spm = round(spm / per500_data["spm"], 2) + logger.info(f"雷达图数据") + logger.info(f"生涯KD比值:{ratio_kd}") + logger.info(f"生涯Kpm比值:{ratio_kpm}") + logger.info(f"生涯准度比值:{ratio_hs}") + logger.info(f"场均救援比值:{ratio_rpr}") + logger.info(f"场均助攻比值:{ratio_apr}") + logger.info(f"分钟得分比值:{ratio_spm}") + + stat_data = [ratio_kd, ratio_apr, ratio_hs, ratio_spm, ratio_kpm, ratio_rpr] + + # 血腥度 kd*0.5 + kpm *0.4 +hs*0.1 + player_blood = kd * 0.5 + kpm * 0.4 + headshots * 0.1 + ratio_blood = player_blood / per500_data["blood"] + + # 贡献度 场均助攻*0.4 + 场均救援*0.4 + spm*0.2 + player_dedication = rpr * 0.4 + apr * 0.4 + spm * 0.2 + ratio_dedication = player_dedication / per500_data["dedication"] + + # 全能度 血腥度*0.5 + 贡献度*0.5 + player_all_round = player_blood * 0.5 + player_dedication * 0.5 + ratio_all_round = player_all_round / per500_data["all_round"] + + logger.info(f"称号评级数据") + logger.info(f"血腥度:{player_blood} 比值:{ratio_blood}") + logger.info(f"贡献度:{player_dedication} 比值:{ratio_dedication}") + logger.info(f"全能度:{player_all_round} 比值:{ratio_all_round}") + level_designation = get_level_designation(ratio_blood, ratio_dedication, ratio_all_round) + return stat_data, level_designation + + +def get_level_designation(blood, dedication, all_round): + feature_type = "" + final_max = max([blood, dedication, all_round]) + if final_max == blood: + feature_type = "blood" + elif final_max == dedication: + feature_type = "dedication" + elif final_max == all_round: + feature_type = "all_round" + logger.info(f"最高能力:{feature_type}") + logger.info(f"最高能力值:{final_max}") + final_info = get_feature(final_max, feature_type) + return final_info + + +def get_feature(value, feature_type): + """根据输入值和数值类别,返回对应特征值""" + for (low, high), features in interval_table.items(): + if low <= value < high: + return features[feature_type] + raise ValueError(f"数值 {value} 不在任何区间内") diff --git a/src/plugins/bf_bot/font/MYingHeiPRC-W5.ttf b/src/plugins/bf_bot/font/MYingHeiPRC-W5.ttf new file mode 100644 index 0000000..b7d5a5e Binary files /dev/null and b/src/plugins/bf_bot/font/MYingHeiPRC-W5.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-Bold.ttf b/src/plugins/bf_bot/font/Purista-Bold.ttf new file mode 100644 index 0000000..3a69ef8 Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-Bold.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-BoldItalic.ttf b/src/plugins/bf_bot/font/Purista-BoldItalic.ttf new file mode 100644 index 0000000..5d8f781 Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-BoldItalic.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-Light.ttf b/src/plugins/bf_bot/font/Purista-Light.ttf new file mode 100644 index 0000000..5166300 Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-Light.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-LightItalic.ttf b/src/plugins/bf_bot/font/Purista-LightItalic.ttf new file mode 100644 index 0000000..2ca2c3e Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-LightItalic.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-Medium.ttf b/src/plugins/bf_bot/font/Purista-Medium.ttf new file mode 100644 index 0000000..bd863bb Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-Medium.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-MediumItalic.ttf b/src/plugins/bf_bot/font/Purista-MediumItalic.ttf new file mode 100644 index 0000000..20e52df Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-MediumItalic.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-SemiBold.ttf b/src/plugins/bf_bot/font/Purista-SemiBold.ttf new file mode 100644 index 0000000..5a5fa2e Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-SemiBold.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-SemiBoldItalic.ttf b/src/plugins/bf_bot/font/Purista-SemiBoldItalic.ttf new file mode 100644 index 0000000..23fbbbf Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-SemiBoldItalic.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-Thin.ttf b/src/plugins/bf_bot/font/Purista-Thin.ttf new file mode 100644 index 0000000..077aee0 Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-Thin.ttf differ diff --git a/src/plugins/bf_bot/font/Purista-ThinItalic.ttf b/src/plugins/bf_bot/font/Purista-ThinItalic.ttf new file mode 100644 index 0000000..844bd51 Binary files /dev/null and b/src/plugins/bf_bot/font/Purista-ThinItalic.ttf differ diff --git a/src/plugins/bf_bot/font/SourceHanSansCN-VF.ttf b/src/plugins/bf_bot/font/SourceHanSansCN-VF.ttf new file mode 100644 index 0000000..8c6bccb Binary files /dev/null and b/src/plugins/bf_bot/font/SourceHanSansCN-VF.ttf differ diff --git a/src/plugins/bf_bot/font/bf-sub-headline-bold 等宽数字.ttf b/src/plugins/bf_bot/font/bf-sub-headline-bold 等宽数字.ttf new file mode 100644 index 0000000..4975599 Binary files /dev/null and b/src/plugins/bf_bot/font/bf-sub-headline-bold 等宽数字.ttf differ diff --git a/src/plugins/bf_bot/font/bf-sub-headline-bold.ttf b/src/plugins/bf_bot/font/bf-sub-headline-bold.ttf new file mode 100644 index 0000000..f3d51ee Binary files /dev/null and b/src/plugins/bf_bot/font/bf-sub-headline-bold.ttf differ diff --git a/src/plugins/bf_bot/font/演示创黑.ttf b/src/plugins/bf_bot/font/演示创黑.ttf new file mode 100644 index 0000000..8cacfc4 Binary files /dev/null and b/src/plugins/bf_bot/font/演示创黑.ttf differ diff --git a/src/plugins/bf_bot/font/演示创黑FLY.ttf b/src/plugins/bf_bot/font/演示创黑FLY.ttf new file mode 100644 index 0000000..9be851a Binary files /dev/null and b/src/plugins/bf_bot/font/演示创黑FLY.ttf differ diff --git a/src/plugins/bf_bot/image_builder.py b/src/plugins/bf_bot/image_builder.py new file mode 100644 index 0000000..33d5851 --- /dev/null +++ b/src/plugins/bf_bot/image_builder.py @@ -0,0 +1,320 @@ +from PIL import Image, ImageDraw, ImageFont +import math +import os +from io import BytesIO +import io +import textwrap +import cv2 +import time +import datetime +import numpy as np +from .img_utils import * +from .data_utils import * +from .param import * + +filepath = os.path.dirname(__file__).replace("\\", "/") + +# 字体 +font_XXL = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 72) +font_XL = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 64) +font_L = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 48) +font_M = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 36) +font_MS = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 32) +font_S = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28) +font_XS = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 24) +font_XXS = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18) +font_XXXS = 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, + head_shots=10, rank=100, time_play=100, kills=100, kill_assists=150, revives=114, wins=10, + loses=10, destroyed=514, best_weapon="M1907 SF", best_vehicle="坦克", best_class="Support", + longest_head_shot=1200, highest_ill_streak=100, stat_data=None, level_designation=None): + if stat_data is None: + stat_data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + # 获取当前事件并且格式化 + 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, (200, 200), interpolation=cv2.INTER_AREA) + + # 创建圆形蒙版 + mask = np.zeros((200, 200), dtype=np.uint8) + cv2.circle(mask, (100, 100), 100, 255, -1) # 绘制半径100的实心圆 + + # 处理透明通道 + if avatar.shape[2] == 4: + # 合并圆形蒙版与原有alpha通道 + alpha = cv2.bitwise_and(avatar[:, :, 3], mask) + avatar[:, :, 3] = alpha + else: + # 无透明通道时创建BGRA图像 + avatar = cv2.cvtColor(avatar, cv2.COLOR_BGR2BGRA) + avatar[:, :, 3] = mask + + # 粘贴头像(保持原有坐标) + y, x = 55, 426 + h, w = avatar.shape[:2] + alpha = avatar[:, :, 3] / 255.0 + for c in range(3): + img[y:y + h, x:x + w, c] = (alpha * avatar[:, :, c] + + (1 - alpha) * img[y:y + h, x:x + w, c]) + + # 公告栏 + img = notice_paste(img) + + # ---------------- PIL 部分 ---------------- + pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + draw = ImageDraw.Draw(pil_img) + + # 玩家信息 + draw.text((665, 65), f"{player}", fill="white", font=font_L) + draw.text((745, 133), f"{pid}", fill="white", font=font_M) + + # 等级游玩时长 + draw.text((740, 212), f"{rank}", fill="white", font=font_M) + draw.text((985, 212), f"{time_play}H", fill="white", font=font_M) + # 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.text((730, 195), f"{rank}", fill="white", font=font_M) + # draw.text((1000, 195), f"{time_play}", fill="white", font=font_M) + + # 生涯总览 + + # 场次 + draw_centered_text(draw=draw, text=(wins + loses), x_left=45, x_right=265, y=350, + font=font_L, + fill="white") + + # 命中率 + draw_centered_text(draw=draw, text=round(float(acc.replace("%", "")), 1), x_left=415, x_right=495, y=343, + font=font_M, + 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, + fill="white") + # kd + draw_centered_text(draw=draw, text=kd, x_left=585, x_right=785, y=340, font=font_XL, fill="white") + # kpm + draw_centered_text(draw=draw, text=round(kpm, 1), x_left=825, x_right=1025, y=340, font=font_XL, fill="white") + # 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=kills, x_left=870, x_right=1190, y=595, font=font_L, fill="white") + + # 助攻 + draw_centered_text(draw=draw, text=kill_assists, x_left=820, x_right=1145, y=770, 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_centered_text(draw=draw, text=destroyed, x_left=720, x_right=1040, y=1140, font=font_L, fill="white") + + # 底部栏 + + # 评级 + 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, 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) + + # 生成时间 + draw.text((1870, 1420), f"{formatted_time}", fill=(154, 132, 149), font=font_XXXS) + + # ---------------- 转回 OpenCV ---------------- + 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) + if success: + b_io = io.BytesIO(buffer) + return b_io # 就可以直接 return 二进制流 + else: + return None + + +def build_best(draw: ImageDraw, img: Image, weapons, vehicle, game: str): + # 最佳武器 + weapon_name = weapons["weaponName"] + w_kills = weapons["kills"] + w_kpm = weapons["killsPerMinute"] + acc = weapons["accuracy"].replace("%", "") + headshots = weapons["headshots"].replace("%", "") + + # 最佳载具 + 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_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_kpm, x_left=1510, x_right=1635, y=1260, font=font_L, 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=headshots, x_left=1695, x_right=1760, y=1200, 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_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_kpm, x_left=2295, x_right=2465, y=1055, font=font_XL, fill="white") + draw_centered_text(draw=draw, text=destroyed, x_left=2265, x_right=2440, y=1260, 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) + + img = image_paste_center(wp_icon, img, (1165, 1110)) + img = image_paste_center(vc_icon, img, (1920, 1110)) + return img + + +def normalize_for_radar(data, buffer=0.1, scale_min=0.0, scale_max=1.0): + """ + 将数据动态归一化到指定区间 [scale_min, scale_max], + 并给最大值预留 buffer,让图形更饱满。 + + Args: + data (list/ndarray): 原始数据 + buffer (float): 缓冲比例,例如 0.1 表示最大值按 1.1 倍处理 + scale_min (float): 映射区间下限 + scale_max (float): 映射区间上限 + + Returns: + list: 归一化后的数据 + """ + data = np.array(data, dtype=float) + d_min, d_max = data.min(), data.max() + + # 给最大值留 buffer + d_max = d_max * (1 + buffer) + + if d_max == d_min: # 避免除零 + norm = np.ones_like(data) * (scale_min + scale_max) / 2 + else: + norm = (data - d_min) / (d_max - d_min) + norm = scale_min + norm * (scale_max - scale_min) + + return norm.tolist() + + +# ---------------- 雷达图示例 ---------------- +def draw_radar(img, stat_data, center=(311, 796), r=185, + buffer=0.1, scale_min=0.1, scale_max=1.05): + # 动态归一化 + scaled_data = normalize_for_radar(stat_data, buffer, scale_min, scale_max) + + points = [] + for i, v in enumerate(scaled_data): + angle = 2 * np.pi / len(scaled_data) * i - np.pi / 2 + px = int(center[0] + r * v * np.cos(angle)) + py = int(center[1] + r * v * np.sin(angle)) + points.append([px, py]) + points = np.array(points, np.int32) + + # 绘制填充 + overlay = img.copy() + cv2.fillPoly(overlay, [points], color=(255, 255, 255, 76)) + img = cv2.addWeighted(overlay, 0.3, img, 0.7, 0) + # 绘制边框 + cv2.polylines(img, [points], isClosed=True, color=(255, 255, 255), thickness=2) + return img + + +def build_bf6_simple_card(player_info): + # 1.创建黑色板块 1920*1080 + new_img = Image.new('RGBA', (1080, 1920), (255, 255, 255, 1000)) + + draw = ImageDraw.Draw(new_img) + + draw.text((50, 60), f"玩家名称:{player_info['玩家名称']}", fill='black', font=font_M) + draw.text((550, 60), f"游玩平台:{player_info['游玩平台']}", fill='black', font=font_M) + + draw.text((50, 160), f"游戏等级:{player_info['游戏等级']}", fill='black', font=font_M) + draw.text((550, 160), f"游玩时长:{player_info['游玩时长']}", fill='black', font=font_M) + + draw.text((50, 260), f"游玩场次:{player_info['游玩场次']}", fill='black', font=font_M) + draw.text((550, 260), f"对局胜率:{player_info['对局胜率']}", fill='black', font=font_M) + + draw.text((50, 360), f"对局得分:{player_info['对局得分']}", fill='black', font=font_M) + draw.text((550, 360), f"玩家SPM:{player_info['玩家SPM']}", fill='black', font=font_M) + + draw.text((50, 460), f"玩家K/D:{player_info['玩家K/D']}", fill='black', font=font_M) + draw.text((550, 460), f"玩家KPM:{player_info['玩家KPM']}", 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((50,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((550, 760), f"最高连杀:{player_info['最高连杀']}", fill='black', font=font_M) + + draw.text((50, 860), f"命中率:{player_info['命中率']}", fill='black', font=font_M) + draw.text((550, 860), f"爆头率:{player_info['爆头率']}", fill='black', font=font_M) + + draw.text((150, 1060), f"最佳武器", fill='black', font=font_M) + draw.text((650, 1060), f"最佳载具", fill='black', font=font_M) + + draw.text((50, 1160), f"名称:{player_info['最佳武器']['名称']}", fill='black', font=font_M) + draw.text((50, 1260), f"击杀:{player_info['最佳武器']['击杀']}", fill='black', font=font_M) + draw.text((50, 1360), f"KPM:{player_info['最佳武器']['KPM']}", fill='black', font=font_M) + draw.text((50, 1460), f"时长:{player_info['最佳武器']['时长']}", fill='black', font=font_M) + draw.text((50, 1560), f"命中率:{player_info['最佳武器']['命中率']}", fill='black', font=font_M) + draw.text((50, 1660), f"爆头率:{player_info['最佳武器']['爆头率']}", fill='black', font=font_M) + + draw.text((550, 1160), f"名称:{player_info['最佳载具']['名称']}", fill='black', font=font_M) + draw.text((550, 1260), f"击杀:{player_info['最佳载具']['击杀']}", fill='black', font=font_M) + draw.text((550, 1360), f"KPM:{player_info['最佳载具']['KPM']}", fill='black', font=font_M) + draw.text((550, 1460), f"时长:{player_info['最佳载具']['时长']}", fill='black', font=font_M) + draw.text((550, 1560), f"碾压:{player_info['最佳载具']['碾压']}", fill='black', font=font_M) + draw.text((550, 1660), f"摧毁:{player_info['最佳载具']['摧毁']}", fill='black', font=font_M) + + b_io = BytesIO() + new_img.save(b_io, format="PNG") + return b_io # 就可以直接 return 二进制流 + + +def build_bf6_weapon_card(weapon_info): + # 1.创建黑色板块 1920*1080 + new_img = Image.new('RGBA', (1080, 1920), (255, 255, 255, 1000)) diff --git a/src/plugins/bf_bot/img/bf1/vehicles/Artillery truck.png b/src/plugins/bf_bot/img/bf1/vehicles/Artillery truck.png new file mode 100644 index 0000000..fa57d39 Binary files /dev/null and b/src/plugins/bf_bot/img/bf1/vehicles/Artillery truck.png differ diff --git a/src/plugins/bf_bot/img/bf1/vehicles/Heavy machine gun.png b/src/plugins/bf_bot/img/bf1/vehicles/Heavy machine gun.png new file mode 100644 index 0000000..361cb8e Binary files /dev/null and b/src/plugins/bf_bot/img/bf1/vehicles/Heavy machine gun.png differ diff --git a/src/plugins/bf_bot/img/bf1/vehicles/Mark v landship.png b/src/plugins/bf_bot/img/bf1/vehicles/Mark v landship.png new file mode 100644 index 0000000..45f8202 Binary files /dev/null and b/src/plugins/bf_bot/img/bf1/vehicles/Mark v landship.png differ diff --git a/src/plugins/bf_bot/img/bf1/weapon/Annihilator Trench.png b/src/plugins/bf_bot/img/bf1/weapon/Annihilator Trench.png new file mode 100644 index 0000000..958034b Binary files /dev/null and b/src/plugins/bf_bot/img/bf1/weapon/Annihilator Trench.png differ diff --git a/src/plugins/bf_bot/img/bf1/weapon/Automatico M1918 Factory.png b/src/plugins/bf_bot/img/bf1/weapon/Automatico M1918 Factory.png new file mode 100644 index 0000000..843e375 Binary files /dev/null and b/src/plugins/bf_bot/img/bf1/weapon/Automatico M1918 Factory.png differ diff --git a/src/plugins/bf_bot/img/bf1/weapon/M1907 SL Sweeper.png b/src/plugins/bf_bot/img/bf1/weapon/M1907 SL Sweeper.png new file mode 100644 index 0000000..eac29ad Binary files /dev/null and b/src/plugins/bf_bot/img/bf1/weapon/M1907 SL Sweeper.png differ diff --git a/src/plugins/bf_bot/img/bf3/vehicles/VEHICLE AIR JET FIGHTER F18.png b/src/plugins/bf_bot/img/bf3/vehicles/VEHICLE AIR JET FIGHTER F18.png new file mode 100644 index 0000000..f8f2bad Binary files /dev/null and b/src/plugins/bf_bot/img/bf3/vehicles/VEHICLE AIR JET FIGHTER F18.png differ diff --git a/src/plugins/bf_bot/img/bf3/weapon/M16A4.png b/src/plugins/bf_bot/img/bf3/weapon/M16A4.png new file mode 100644 index 0000000..5ffea79 Binary files /dev/null and b/src/plugins/bf_bot/img/bf3/weapon/M16A4.png differ diff --git a/src/plugins/bf_bot/img/bf4/vehicles/RHIB-BOAT.png b/src/plugins/bf_bot/img/bf4/vehicles/RHIB-BOAT.png new file mode 100644 index 0000000..c677256 Binary files /dev/null and b/src/plugins/bf_bot/img/bf4/vehicles/RHIB-BOAT.png differ diff --git a/src/plugins/bf_bot/img/bf4/weapon/scar-h.png b/src/plugins/bf_bot/img/bf4/weapon/scar-h.png new file mode 100644 index 0000000..8cbec20 Binary files /dev/null and b/src/plugins/bf_bot/img/bf4/weapon/scar-h.png differ diff --git a/src/plugins/bf_bot/img/bfv/vehicles/Lvt.png b/src/plugins/bf_bot/img/bfv/vehicles/Lvt.png new file mode 100644 index 0000000..2260f06 Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/vehicles/Lvt.png differ diff --git a/src/plugins/bf_bot/img/bfv/vehicles/Panzer iv.png b/src/plugins/bf_bot/img/bfv/vehicles/Panzer iv.png new file mode 100644 index 0000000..0e761f2 Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/vehicles/Panzer iv.png differ diff --git a/src/plugins/bf_bot/img/bfv/vehicles/Sherman.png b/src/plugins/bf_bot/img/bfv/vehicles/Sherman.png new file mode 100644 index 0000000..ac4aa6d Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/vehicles/Sherman.png differ diff --git a/src/plugins/bf_bot/img/bfv/vehicles/Stationary mg34.png b/src/plugins/bf_bot/img/bfv/vehicles/Stationary mg34.png new file mode 100644 index 0000000..906dd6a Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/vehicles/Stationary mg34.png differ diff --git a/src/plugins/bf_bot/img/bfv/vehicles/Tiger i.png b/src/plugins/bf_bot/img/bfv/vehicles/Tiger i.png new file mode 100644 index 0000000..9bcdd68 Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/vehicles/Tiger i.png differ diff --git a/src/plugins/bf_bot/img/bfv/vehicles/Type 97.png b/src/plugins/bf_bot/img/bfv/vehicles/Type 97.png new file mode 100644 index 0000000..b30dd05 Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/vehicles/Type 97.png differ diff --git a/src/plugins/bf_bot/img/bfv/weapon/Gewehr M9530.png b/src/plugins/bf_bot/img/bfv/weapon/Gewehr M9530.png new file mode 100644 index 0000000..f651cc7 Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/weapon/Gewehr M9530.png differ diff --git a/src/plugins/bf_bot/img/bfv/weapon/M1907 SF.png b/src/plugins/bf_bot/img/bfv/weapon/M1907 SF.png new file mode 100644 index 0000000..4fbe06c Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/weapon/M1907 SF.png differ diff --git a/src/plugins/bf_bot/img/bfv/weapon/StG 44.png b/src/plugins/bf_bot/img/bfv/weapon/StG 44.png new file mode 100644 index 0000000..c278be5 Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/weapon/StG 44.png differ diff --git a/src/plugins/bf_bot/img/bfv/weapon/Suomi KP-31.png b/src/plugins/bf_bot/img/bfv/weapon/Suomi KP-31.png new file mode 100644 index 0000000..2fae98c Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/weapon/Suomi KP-31.png differ diff --git a/src/plugins/bf_bot/img/bfv/weapon/Type 99 Arisaka.png b/src/plugins/bf_bot/img/bfv/weapon/Type 99 Arisaka.png new file mode 100644 index 0000000..e38ee03 Binary files /dev/null and b/src/plugins/bf_bot/img/bfv/weapon/Type 99 Arisaka.png differ diff --git a/src/plugins/bf_bot/img/vc.png b/src/plugins/bf_bot/img/vc.png new file mode 100644 index 0000000..ed904e0 Binary files /dev/null and b/src/plugins/bf_bot/img/vc.png differ diff --git a/src/plugins/bf_bot/img/wp.png b/src/plugins/bf_bot/img/wp.png new file mode 100644 index 0000000..ffc9b88 Binary files /dev/null and b/src/plugins/bf_bot/img/wp.png differ diff --git a/src/plugins/bf_bot/img_utils.py b/src/plugins/bf_bot/img_utils.py new file mode 100644 index 0000000..83a4a3c --- /dev/null +++ b/src/plugins/bf_bot/img_utils.py @@ -0,0 +1,367 @@ +import hashlib +import json +import os +import random +import time +from io import BytesIO +from typing import List, Tuple + +import cv2 +import numpy as np +from nonebot import logger + +import aiohttp +import requests +import requests.exceptions +from PIL import Image, ImageDraw, ImageFont + +filepath = os.path.dirname(__file__).replace("\\", "/") + + +# 圆角遮罩处理 +def draw_rect(img, pos, radius, **kwargs): + trans = Image.new('RGBA', img.size, (0, 0, 0, 0)) + alpha_draw = ImageDraw.Draw(trans, "RGBA") + alpha_draw.rounded_rectangle(pos, radius, **kwargs) + img.paste(Image.alpha_composite(img, trans)) + return img + + +# 圆角处理 +def circle_corner(img, radii): + """ + 半透明圆角处理 + :param img: 要修改的文件 + :param radii: 圆角弧度 + :return: 返回修改过的文件 + """ + circle = Image.new('L', (radii * 2, radii * 2), 0) # 创建黑色方形 + draw = ImageDraw.Draw(circle) + draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 + + img = img.convert("RGBA") + w, h = img.size + + # 创建一个alpha层,存放四个圆角,使用透明度切除圆角外的图片 + alpha = Image.new('L', img.size, 255) + alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角 + alpha.paste(circle.crop((radii, 0, radii * 2, radii)), + (w - radii, 0)) # 右上角 + alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), + (w - radii, h - radii)) # 右下角 + alpha.paste(circle.crop((0, radii, radii, radii * 2)), + (0, h - radii)) # 左下角 + img.putalpha(alpha) # 白色区域透明可见,黑色区域不可见 + + # 添加圆角边框 + draw = ImageDraw.Draw(img) + draw.rounded_rectangle(img.getbbox(), outline="white", width=3, radius=radii) + return img + + +# PNG重绘大小 +def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=''): + """ + PNG缩放透明度处理 + :param source_file: 源文件(Image.open()) + :param new_width: 设置的宽度 + :param new_height: 设置的高度 + :param resample: 抗锯齿 + :param ref_file: 参考文件 + :return: + """ + img = source_file + img = img.convert("RGBA") + width, height = img.size + + if ref_file != '': + imgRef = Image.open(ref_file) + new_width, new_height = imgRef.size + else: + if new_height == 0: + new_height = new_width * width / height + + bands = img.split() + resample_map = { + "NEAREST": Image.NEAREST, + "BILINEAR": Image.BILINEAR, + "BICUBIC": Image.BICUBIC, + "LANCZOS": Image.LANCZOS + } + resample_method = resample_map.get(resample, Image.LANCZOS) # 默认使用 LANCZOS + + bands = [b.resize((new_width, new_height), resample=resample_method) for b in bands] + resized_file = Image.merge('RGBA', bands) + + return resized_file + + +# 图片粘贴 +def image_paste(paste_image, under_image, pos): + """ + + :param paste_image: 需要粘贴的图片 + :param under_image: 底图 + :param pos: 位置(x,y)坐标 + :return: 返回图片 + """ + # 获取需要贴入图片的透明通道 + r, g, b, alpha = paste_image.split() + # 粘贴时将alpha值传递至mask属性 + under_image.paste(paste_image, pos, alpha) + return under_image + + +def image_paste_center(paste_image: Image, under_image: Image, pos): + """ + 将一张图片粘贴到另一张图片上,并使粘贴图片的Y轴中心对齐到指定y坐标 + + :param paste_image: 需要粘贴的图片 (RGBA) + :param under_image: 底图 (RGBA) + :param pos: 粘贴位置 (x, y),其中y表示目标对齐的中心位置 + :return: 返回合成后的图片 + """ + x, y = pos + # 计算上下居中偏移 + paste_w, paste_h = paste_image.size + y_top = y - paste_h // 2 # 上边位置 = 中心点y - 半高 + + # 获取透明通道 + r, g, b, alpha = paste_image.split() + under_image.paste(paste_image, (x, y_top), alpha) + return under_image + + +def download_icon(url): + data = download_url(url) + img = BytesIO(data) + return img + + +# 下载QQ头像 +def download_avatar(user_id: str) -> bytes: + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640" + data = download_url(url) + if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e": + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100" + data = download_url(url) + return data + + +# 根据URL下载文件 +def download_url(url: str) -> bytes: + for i in range(3): + try: + resp = requests.get(url) + if resp.status_code != 200: + continue + return resp.content + except Exception as e: + print(f"Error downloading {url}, retry {i}/3: {str(e)}") + + +# 图片裁剪 +def cut_image(pic_data: bytes, target_ratio: float): + try: + pic_data = Image.open(BytesIO(pic_data)) + w, h = pic_data.size + pic_ratio = w / h + if pic_ratio > target_ratio: + # 宽高比大于目标比例,按高度缩放。保持宽度不变 + new_h = w / target_ratio + h_delta = (h - new_h) / 2 + w_delta = 0 + else: + # 宽高比小于目标比例,按宽度缩放。保持高度不变 + new_w = h * target_ratio + w_delta = (w - new_w) / 2 + h_delta = 0 + cropped = pic_data.crop((w_delta, h_delta, w - w_delta, h - h_delta)) + return cropped + except Exception as e: + raise Exception(f"图片剪裁失败{str(e)}") + + +def draw_centered_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) # 获取文本像素宽度 + area_width = x_right - x_left + x_center = x_left + (area_width - text_width) / 2 + draw.text((x_center, y), text, font=font, fill=fill) + + +def get_save_icon(game, name, icon_type, url): + name = name.replace("/", "") + icon = get_icon_from_cache(name, game, icon_type) + if not icon: + if game == "bf1": + icon = get_icon_from_url(url) + if icon_type == "weapon": + icon = png_resize(icon, 500, 125) + else: + icon = png_resize(icon, 315, 79) + elif game == "bf3": + icon = get_icon_from_url(url) + if icon_type == "weapon": + icon = png_resize(icon, 500, 300) + else: + icon = png_resize(icon, 315, 200) + elif game == "bf4": + icon = get_icon_from_url(url) + if icon_type == "weapon": + icon = png_resize(icon, 500, 165) + else: + icon = png_resize(icon, 315, 80) + elif game == "bfv": + icon = get_icon_from_url(url) + if icon_type == "weapon": + icon = png_resize(icon, 500, 166) + else: + icon = png_resize(icon, 315, 315) + icon.save(f"{filepath}/img/{game}/{icon_type}/{name}.png") + logger.info(f"文件已经缓存至:{filepath}/img/{game}/{icon_type}/{name}.png") + + # 上述步骤处理完再执行一次icon判空 + if icon: + return icon + else: + # 针对空图片处理 + if icon_type == "weapon": + icon = Image.open(filepath + "/img/wp.png").convert('RGBA') + else: + icon = Image.open(filepath + "/img/vc.png").convert('RGBA') + return icon + + +# 获取icon +def get_icon_from_url(url): + if url: + try: + # 添加10s超时判断,如果超时返回空 + res = BytesIO(requests.get(url, timeout=10).content) + icon = Image.open(res).convert('RGBA') + return icon + except requests.exceptions.RequestException as e: + logger.error(f"请求异常:{e}") + + +def get_icon_from_cache(icon_name, game, icon_type): + path = f"{filepath}/img/{game}/{icon_type}" + try: + icon_list = os.listdir(path) + if icon_name in str(icon_list): + logger.info(f"本地存在{icon_name}物品") + img = Image.open(f"{path}/{icon_name}.png").convert('RGBA') + return img + except Exception as err: + logger.error(f"获取图标失败:{str(err)}") + return None + + +def cutout_region(img, mask, alpha=0): + """ + 在图像指定区域设置透明度 + :param img: 输入图像 (BGR 或 BGRA) + :param mask: 区域定义 + - 矩形: (x1, y1, x2, y2) + - 圆形: (cx, cy, r) + - 任意 mask: numpy.ndarray (0/255) + :param alpha: 设置的透明度 (0=透明, 255=不透明, 0-255之间半透明) + :return: 处理后的图像 (BGRA) + """ + + # 转为 BGRA (确保有 alpha 通道) + if img.shape[2] == 3: + b, g, r = cv2.split(img) + a = np.ones(b.shape, dtype=np.uint8) * 255 + img = cv2.merge((b, g, r, a)) + + h, w = img.shape[:2] + region_mask = np.zeros((h, w), dtype=np.uint8) + + # 根据传入参数类型生成区域 mask + if isinstance(mask, tuple): + if len(mask) == 4: # 矩形 + x1, y1, x2, y2 = mask + region_mask[y1:y2, x1:x2] = 255 + elif len(mask) == 3: # 圆形 + cx, cy, r = mask + cv2.circle(region_mask, (cx, cy), r, 255, -1) + else: + raise ValueError("不支持的 mask tuple 格式") + elif isinstance(mask, np.ndarray): # 自定义 mask + if mask.shape != (h, w): + raise ValueError("mask 尺寸必须与图像一致") + region_mask = mask + else: + raise TypeError("mask 必须是 tuple 或 numpy.ndarray") + + # 修改 alpha 通道 + img[region_mask > 0, 3] = alpha + + return img + + +def paste_image_cv2(fg, bg, pos): + """ + 将前景图粘贴到背景图上,支持透明度 (Alpha 混合) + :param fg: 前景图 (必须是 BGRA) + :param bg: 背景图 (BGR 或 BGRA) + :param pos: 粘贴位置 (x, y) 左上角坐标 + :return: 合成后的图像 (BGRA) + """ + + x, y = pos + fh, fw = fg.shape[:2] + + # 确保背景有 alpha 通道 + if bg.shape[2] == 3: + b, g, r = cv2.split(bg) + a = np.ones(b.shape, dtype=np.uint8) * 255 + bg = cv2.merge((b, g, r, a)) + + # 截取 ROI 区域 + roi = bg[y:y + fh, x:x + fw] + + # 防止越界 + if roi.shape[0] != fh or roi.shape[1] != fw: + raise ValueError("前景图超出背景范围") + + # 提取 alpha 通道 + alpha_fg = fg[:, :, 3] / 255.0 + alpha_bg = 1.0 - alpha_fg + + # 混合 RGB + for c in range(3): + roi[:, :, c] = (alpha_fg * fg[:, :, c] + + alpha_bg * roi[:, :, c]) + + # 更新 alpha 通道(取最大值,保证透明度正确) + roi[:, :, 3] = np.clip(fg[:, :, 3] + roi[:, :, 3], 0, 255) + + # 放回背景 + bg[y:y + fh, x:x + fw] = roi + return bg + + +def notice_paste(cv2_bg): + pos = (1176, 318) + # 获取公告 + notice_path = f"{filepath}/notice/notice.png" + front_img = cv2.imread(notice_path) + mask = np.zeros((631, 1355), dtype=np.uint8) + pts = np.array([[0, 0], [167, 0], [0, 631]], np.int32) + cv2.fillPoly(mask, [pts], 255) + front_img = cutout_region(front_img, mask, alpha=0) + mix_bg = paste_image_cv2(front_img, cv2_bg, pos) + return mix_bg diff --git a/src/plugins/bf_bot/notice/notice.png b/src/plugins/bf_bot/notice/notice.png new file mode 100644 index 0000000..0219ff8 Binary files /dev/null and b/src/plugins/bf_bot/notice/notice.png differ diff --git a/src/plugins/bf_bot/param.py b/src/plugins/bf_bot/param.py new file mode 100644 index 0000000..1c57dda --- /dev/null +++ b/src/plugins/bf_bot/param.py @@ -0,0 +1,1003 @@ +import re + +round_data = { + "bf1": { + "kd": 4.93, + "kpm": 2.69, + "hs": 0.26, + "rpr": 4.16, + "apr": 2.04, + "spm": 2739.54, + "blood": 3.5670, + "dedication": 7.5020, + "all_round": 5.5345 + }, + "bf3": { + "kd": 11.51, + "kpm": 4.59, + "hs": 0.26, + "rpr": 7.84, + "apr": 25.51, + "spm": 27983.00, + "blood": 7.6170, + "dedication": 846.7660, + "all_round": 427.1915 + }, + "bfv": { + "kd": 4.22, + "kpm": 1.78, + "hs": 0.26, + "rpr": 4.23, + "apr": 5.23, + "spm": 579.82, + "blood": 2.8480, + "dedication": 119.7840, + "all_round": 61.2980 + }, + "bf4": { + "kd": 6.0, + "kpm": 3.0, + "hs": 0.26, + "rpr": 10.00, + "apr": 10.00, + "spm": 5000.00, + "blood": 4.0, + "dedication": 2000.00, + "all_round": 1000.00 + }, +} + +classes = { + "Assault": "突击兵", + "Engineer": "工程兵", + "Support": "支援兵", + "Recon": "侦察兵", + "Commander": "指挥官", + "Medic": "医疗兵", + "Scout": "侦察兵", + "Pilot": "驾驶员", + "Cavalry": "骑兵", + "Tanker": "坦克" +} + +bf_item = { + "bf1": { + "Wex": "韦克斯火焰喷射器", + "MG 08/15": "MG08/15轻机枪", + "Tankgewehr M1918": "M1918反坦克步枪", + "Villar Perosa": "维拉尔·佩罗萨双管机枪", + "Martini-Henry Grenade Launcher": "马蒂尼-亨利榴弹发射器", + "Mortar — HE": "迫击炮—高爆弹", + "Bandage Pouch": "绷带包", + "Crossbow Launcher — HE": "十字弩发射器—高爆弹", + "Trench Periscope": "战壕潜望镜", + "Tripwire Bomb — GAS": "绊雷—毒气", + "K Bullets": "K型子弹", + "Anti-Tank Grenade": "反坦克手榴弹", + "Flare Gun — Flash": "信号枪—闪光", + "Anti-Tank Mine": "反坦克地雷", + "Medical Syringe": "医疗注射器", + "Tripwire Bomb — HE": "绊雷—高爆", + "Tripwire Bomb — INC": "绊雷—燃烧", + "Flare Gun — Spot": "信号枪—标记", + "Crossbow Launcher — FRG": "十字弩发射器—破片", + "Ammo Pouch": "弹药包", + "Sniper Decoy": "狙击手诱饵", + "Medical Crate": "医疗箱", + "Sniper Shield": "狙击手盾牌", + "Ammo Crate": "弹药箱", + "AT Rocket Gun": "反坦克火箭枪", + "Mortar — AIR": "迫击炮—空爆弹", + "Dynamite": "炸药", + "Limpet Charge": "吸附式炸弹", + "Repair Tool": "维修工具", + "AA Rocket Gun": "防空火箭枪", + "Livens Projector": "利文斯投射器", + "Mondragón Storm": "蒙德拉贡风暴型", + "Mondragón Optical": "蒙德拉贡光学型", + "Cei-Rigotti Factory": "切-里戈蒂工厂型", + "M1907 SL Sweeper": "M1907 SL扫荡型", + "Autoloading 8 .25 Extended": "自动装填8型.25加长型", + "Selbstlader 1906 Factory": "1906半自动工厂型", + "M1907 SL Trench": "M1907 SL堑壕型", + "Selbstlader M1916 Factory": "M1916半自动工厂型", + "Selbstlader M1916 Optical": "M1916半自动光学型", + "Mondragón Sniper": "蒙德拉贡狙击型", + "Cei-Rigotti Optical": "切-里戈蒂光学型", + "Cei-Rigotti Trench": "切-里戈蒂堑壕型", + "Selbstlader M1916 Marksman": "M1916半自动神射手型", + "M1907 SL Factory": "M1907 SL工厂型", + "Autoloading 8 .35 Factory": "自动装填8型.35工厂型", + "Autoloading 8 .35 Marksman": "自动装填8型.35神射手型", + "RSC 1917 Factory": "RSC 1917工厂型", + "RSC 1917 Optical": "RSC 1917光学型", + "Selbstlader 1906 Sniper": "1906半自动狙击型", + "General Liu Rifle Storm": "刘将军步枪风暴型", + "General Liu Rifle Factory": "刘将军步枪工厂型", + "Fedorov Avtomat Trench": "费德洛夫自动步枪堑壕型", + "Fedorov Avtomat Optical": "费德洛夫自动步枪光学型", + "Farquhar-Hill Optical": "法夸尔-希尔光学型", + "Farquhar-Hill Storm": "法夸尔-希尔风暴型", + "Howell Automatic Factory": "豪厄尔自动步枪工厂型", + "Howell Automatic Sniper": "豪厄尔自动步枪狙击型", + "M1917 Patrol Carbine": "M1917巡逻卡宾枪", + "Carcano M91 Patrol Carbine": "卡尔卡诺M91巡逻卡宾枪", + "Hellfighter Trench Shotgun": "地狱战士堑壕霰弹枪", + "12g Automatic Hunter": "12号自动猎兵型", + "Model 10-A Factory": "10-A型工厂型", + "Model 10-A Hunter": "10-A型猎兵型", + "12g Automatic Backbored": "12号自动扩膛型", + "M97 Trench Gun Hunter": "M97堑壕枪猎兵型", + "M97 Trench Gun Backbored": "M97堑壕枪扩膛型", + "Model 10-A Slug": "10-A型独头弹型", + "12g Automatic Extended": "12号自动加长型", + "M97 Trench Gun Sweeper": "M97堑壕枪扫荡型", + "Sjögren Inertial Factory": "肖格伦惯性工厂型", + "Model 1900 Factory": "1900型工厂型", + "Model 1900 Slug": "1900型独头弹型", + "Sjögren Inertial Slug": "肖格伦惯性独头弹型", + "Mars Automatic": "火星自动手枪", + "Auto Revolver": "自动左轮手枪", + "Red Baron's P08": "红男爵的P08", + "C96": "C96手枪", + "C93": "C93手枪", + "Bodeo 1889": "博迪奥1889", + "Bull Dog Revolver": "斗牛犬左轮", + "Repetierpistole M1912": "M1912连发手枪", + "1903 Hammerless": "1903无击锤", + "Hellfighter M1911": "地狱战士M1911", + "Modello 1915": "1915型手枪", + "Gasser M1870": "加瑟M1870", + "Frommer Stop": "弗罗默停止式", + "No. 3 Revolver": "3号左轮", + "M1911": "M1911手枪", + "Kolibri": "蜂鸟手枪", + "Mle 1903": "1903型手枪", + "P08 Pistol": "P08手枪", + "Taschenpistole M1914": "M1914袖珍手枪", + "Obrez Pistol": "奥布雷兹手枪", + "Nagant Revolver": "纳甘左轮", + "Revolver Mk VI": "Mk VI左轮", + "Type 38 Arisaka Patrol": "三八式有坂巡逻卡宾枪", + "Madsen MG Trench": "麦德森机枪堑壕型", + "M1909 Benét–Mercié Storm": "M1909贝内特-梅西耶风暴型", + "Huot Automatic Low Weight": "胡特自动轻量型", + "Lewis Gun Low Weight": "刘易斯机枪轻量型", + "M1909 Benét–Mercié Telescopic": "M1909贝内特-梅西耶望远式", + "BAR M1918 Telescopic": "BAR M1918望远式", + "BAR M1918 Storm": "BAR M1918风暴型", + "Lewis Gun Suppressive": "刘易斯机枪压制型", + "Madsen MG Storm": "麦德森机枪风暴型", + "Madsen MG Low Weight": "麦德森机枪轻量型", + "BAR M1918 Trench": "BAR M1918堑壕型", + "Lewis Gun Optical": "刘易斯机枪光学型", + "MG15 n.A. Suppressive": "MG15 n.A.压制型", + "MG15 n.A. Storm": "MG15 n.A.风暴型", + "M1909 Benét–Mercié Optical": "M1909贝内特-梅西耶光学型", + "MG15 n.A. Low Weight": "MG15 n.A.轻量型", + "Chauchat Telescopic": "绍沙望远式", + "Chauchat Low Weight": "绍沙轻量型", + "Huot Automatic Optical": "胡特自动光学型", + "Perino Model 1908 Defensive": "佩里诺1908型防御型", + "Perino Model 1908 Low Weight": "佩里诺1908型轻量型", + "Parabellum MG14/17 Low Weight": "帕拉贝鲁姆MG14/17轻量型", + "Parabellum MG14/17 Suppressive": "帕拉贝鲁姆MG14/17压制型", + "M1917 MG Low Weight": "M1917机枪轻量型", + "M1917 MG Telescopic": "M1917机枪望远式", + "lMG 08/18 Low Weight": "lMG 08/18轻量型", + "lMG 08/18 Suppressive": "lMG 08/18压制型", + "Shovel": "工兵铲", + "Trench Mace": "战壕钉头锤", + "Sawtooth Knife": "锯齿刀", + "Club": "棍棒", + "Compact Trench Knife": "紧凑型战壕刀", + "Trench Knife": "战壕刀", + "Hellfighter Bolo Knife": "地狱战士波洛刀", + "US Trench Knife": "美军战壕刀", + "Bedouin Dagger": "贝都因匕首", + "Jambiya Knife": "詹比亚弯刀", + "Spiked Club": "尖刺棍棒", + "Bayonet Charge": "刺刀冲锋", + "Hatchet": "手斧", + "Bartek Bludgeon": "巴特克重棍", + "Cavalry Sword": "骑兵军刀", + "Combat Knife": "战斗刀", + "Survival Knife": "生存刀", + "Pickaxe": "镐", + "Nail Knife": "钉刀", + "Saber": "马刀", + "Trench Fleur": "战壕百合", + "Raider Club": "掠夺者棍棒", + "Billhook": "钩镰", + "Cogwheel Club": "齿轮棍", + "Kukri": "廓尔喀弯刀", + "Arditi Dagger": "敢死队匕首", + "Cavalry Lance": "骑兵长矛", + "Cossack Dagger": "哥萨克匕首", + "Dud Club": "哑弹棍", + "Totokia": "托托基亚权杖", + "French Butcher Knife": "法式屠刀", + "Russian Award Knife": "俄军奖赏刀", + "Coupe Coupe": "库普刀", + "Russian Axe": "俄式斧", + "Naval Cutlass": "海军弯刀", + "Grappling Hook": "抓钩", + "Ottoman Flanged Mace": "奥斯曼凸缘钉头锤", + "Ottoman Kilij": "奥斯曼基利杰弯刀", + "Yatagan Sword": "亚特坎剑", + "Meat Cleaver": "切肉刀", + "Prybar": "撬棍", + "Wine Bottle": "酒瓶", + "Sickle": "镰刀", + "Welsh Blade": "威尔士刀", + "M1903 Marksman": "M1903神射手型", + "Russian 1895 Sniper": "1895俄式狙击型", + "Lawrence of Arabia's SMLE": "阿拉伯的劳伦斯SMLE", + "Gewehr 98 Marksman": "Gewehr 98神射手型", + "Gewehr M.95 Infantry": "Gewehr M.95步兵型", + "SMLE MKIII Marksman": "SMLE MKIII神射手型", + "Martini-Henry Infantry": "马蒂尼-亨利步兵型", + "SMLE MKIII Infantry": "SMLE MKIII步兵型", + "Gewehr 98 Infantry": "Gewehr 98步兵型", + "Gewehr M.95 Carbine": "Gewehr M.95卡宾型", + "Gewehr M.95 Marksman": "Gewehr M.95神射手型", + "Russian 1895 Trench": "1895俄式堑壕型", + "Russian 1895 Infantry": "1895俄式步兵型", + "M1903 Sniper": "M1903狙击型", + "M1903 Experimental": "M1903实验型", + "SMLE MKIII Carbine": "SMLE MKIII卡宾型", + "Gewehr 98 Sniper": "Gewehr 98狙击型", + "Lebel Model 1886 Infantry": "勒贝尔1886型步兵型", + "Lebel Model 1886": "勒贝尔1886型", + "Lebel Model 1886 Sniper": "勒贝尔1886型狙击型", + "Martini-Henry Sniper": "马蒂尼-亨利狙击型", + "Vetterli-Vitali M1870/87 Carbine": "维特利-维塔利M1870/87卡宾型", + "Vetterli-Vitali M1870/87 Infantry": "维特利-维塔利M1870/87步兵型", + "Mosin-Nagant M91 Marksman": "莫辛-纳甘M91神射手型", + "Mosin-Nagant M91 Infantry": "莫辛-纳甘M91步兵型", + "Carcano M91 Carbine": "卡尔卡诺M91卡宾型", + "Type 38 Arisaka Infantry": "三八式有坂步兵型", + "Ross MkIII Marksman": "罗斯MkIII神射手型", + "M1917 Enfield Infantry": "M1917恩菲尔德步兵型", + "Ross MkIII Infantry": "罗斯MkIII步兵型", + "M1917 Enfield Silenced": "M1917恩菲尔德消音型", + "Incendiary Grenade": "燃烧手榴弹", + "Rifle Grenade — FRG": "步枪榴弹—破片", + "Impact Grenade": "冲击手榴弹", + "Frag Grenade": "破片手榴弹", + "Rifle Grenade — SMK": "步枪榴弹—烟雾", + "Mini Grenade": "迷你手榴弹", + "Smoke Grenade": "烟雾弹", + "Gas Grenade": "毒气弹", + "Light Anti-Tank Grenade": "轻型反坦克手榴弹", + "Rifle Grenade — HE": "步枪榴弹—高爆", + "Russian Standard Grenade": "俄军标准手榴弹", + "Improvised Grenade": "简易手榴弹", + "Gewehr M.95": "Gewehr M.95步枪", + "M1903": "M1903步枪", + "Gewehr 98": "Gewehr 98步枪", + "SMLE MKIII": "SMLE MKIII步枪", + "Russian 1895": "1895俄式步枪", + "Mosin-Nagant M91": "莫辛-纳甘M91步枪", + "Sawed Off Shotgun": "锯短型霰弹枪", + "Pieper M1893": "皮珀M1893", + "C96 Carbine": "C96卡宾枪", + "Frommer Stop Auto": "弗罗默停止式自动型", + "P08 Artillerie": "P08炮兵型", + "M1911 Extended": "M1911加长型", + "Mle 1903 Extended": "1903型加长型", + "C93 Carbine": "C93卡宾枪", + "MP 18 Trench": "MP18堑壕型", + "MP 18 Experimental": "MP18实验型", + "Automatico M1918 Factory": "M1918自动步枪工厂型", + "MP 18 Optical": "MP18光学型", + "Automatico M1918 Storm": "M1918自动步枪风暴型", + "Automatico M1918 Trench": "M1918自动步枪堑壕型", + "Hellriegel 1915 Factory": "赫尔里格尔1915工厂型", + "Ribeyrolles 1918 Factory": "里贝罗勒1918工厂型", + "Hellriegel 1915 Defensive": "赫尔里格尔1915防御型", + "SMG 08/18 Optical": "SMG08/18光学型", + "SMG 08/18 Factory": "SMG08/18工厂型", + "M1917 Trench Carbine": "M1917堑壕卡宾枪", + "Maschinenpistole M1912/P.16 Storm": "M1912/P.16冲锋枪风暴型", + "RSC SMG Factory": "RSC冲锋枪工厂型", + "RSC SMG Optical": "RSC冲锋枪光学型", + "Annihilator Trench": "歼灭者堑壕型", + "Maschinenpistole M1912/P.16 Experimental": "M1912/P.16冲锋枪实验型", + "Ribeyrolles 1918 Optical": "里贝罗勒1918光学型", + "A7v heavy tank": "A7V重型坦克", + "Mark v landship": "马克V型陆地舰", + "Ft-17 light tank": "FT-17轻型坦克", + "Artillery truck": "炮兵卡车", + "St chamond": "圣沙蒙坦克", + "Putilov garford": "普蒂洛夫加福德装甲车", + "Halberstadt cl. ii attack plane": "哈尔伯施塔特CL.II攻击机", + "Bristol f2.b attack plane": "布里斯托F2.B攻击机", + "A.e.f 2-a2 attack plane": "AEF 2-A2攻击机", + "Rumpler c.i attack plane": "鲁姆普勒C.I攻击机", + "Gotha g.iv bomber": "哥达G.IV轰炸机", + "Caproni ca.5 bomber": "卡普罗尼Ca.5轰炸机", + "Airco dh.10": "空客DH.10轰炸机", + "Hansa-brandenburg g.i": "汉莎-勃兰登堡G.I轰炸机", + "Spad s xiii fighter": "斯帕德S.XIII战斗机", + "Sopwith camel fighter": "索普威斯骆驼战斗机", + "Dr.1 fighter": "福克Dr.I战斗机", + "Albatros diii fighter": "阿尔巴特罗斯D.III战斗机", + "Ilya muromets": "伊利亚穆罗梅茨轰炸机", + "C-class airship": "C级飞艇", + "M30 scout": "M30侦察车", + "37/95 scout": "37/95侦察车", + "Kft scout": "KFT侦察车", + "Mc 18j sidecar": "MC 18J边车", + "Mc 3.5hp sidecar": "MC 3.5HP边车", + "Rnas armored car": "RNAS装甲车", + "Romfell armored car": "罗姆菲尔装甲车", + "Ev4 armored car": "EV4装甲车", + "F.t. armored car": "FT装甲车", + "M.a.s. torpedo boat": "MAS鱼雷艇", + "Y lighter landing craft": "Y型登陆艇", + "L-class destroyer": "L级驱逐舰", + "Fk 96 field gun": "FK 96野战炮", + "Fortress gun": "要塞炮", + "Qf 1 aa": "QF 1防空炮", + "Heavy machine gun": "重机枪", + "He auto-cannon": "高爆自动炮", + "Bl 9.2 siege gun": "BL 9.2英寸攻城炮", + "305/52 o coastal gun": "305/52 O海岸炮", + "Airship l30": "L30飞艇", + "Armored train": "装甲列车", + "Dreadnought": "无畏舰", + "Char 2c": "夏尔2C超重型坦克", + "Horse": "战马" + }, + "bfv": { + "MG 42": "MG42机枪", + "MG 34": "MG34机枪", + "VGO": "VGO机枪", + "M1922 MG": "M1922机枪", + "S2-200": "S2-200机枪", + "M1919A6": "M1919A6机枪", + "Boys AT Rifle": "博伊斯反坦克步枪", + "Panzerbüchse 39": "39型反坦克步枪", + "Frag Grenade Rifle": "步枪榴弹(高爆)", + "Sticky Dynamite": "粘性炸药", + "Panzerfaust": "铁拳", + "Ammo Crate": "弹药箱", + "Bandages": "绷带", + "AT Mine": "反坦克地雷", + "AP Mine": "人员杀伤地雷", + "Medical Crate": "医疗箱", + "Spotting Scope": "瞄准镜", + "Flare Gun": "信号枪", + "Spawn Beacon": "信标", + "Smoke Grenade": "烟雾弹", + "Frag Grenade": "破片手榴弹", + "Incendiary Grenade": "燃烧弹", + "PIAT": "PIAT反坦克发射器", + "AT Grenade Pistol": "反坦克榴弹手枪", + "Smoke Grenade Rifle": "步枪烟雾弹", + "Anti-Tank Bundle Grenade": "反坦克集束手榴弹", + "Throwing Blade": "飞刀", + "Impact Grenade": "冲击手榴弹", + "Medical Syringe": "医疗针", + "Fliegerfaust": "飞拳", + "Shaped Charge": "锥形炸药", + "Lunge Mine": "刺雷", + "M1A1 Bazooka": "M1A1巴祖卡", + "M2 Flamethrower": "M2火焰喷射器", + "Katana": "武士刀", + "Kunai": "苦无", + "Doppel-Schuss": "双管步枪", + "Type 99 Mine": "九九式地雷", + "Firecrackers": "鞭炮", + "RMN50 Rifle Frag": "RMN50步枪榴弹", + "Demolition Grenade": "爆破手榴弹", + "Pistol Flamethrower": "手枪式火焰喷射器", + "Kampfpistole": "战斗手枪", + "Trench Carbine": "战壕卡宾枪", + "P08 Carbine": "P08卡宾枪", + "Liberator": "解放者手枪", + "Repetierpistole M1912": "M1912连发手枪", + "Mk VI Revolver": "Mk VI左轮手枪", + "M1911": "M1911手枪", + "P38 Pistol": "P38手枪", + "Ruby": "鲁比手枪", + "P08 Pistol": "P08手枪", + "Type 94": "九四式手枪", + "Model 27": "27型左轮手枪", + "Welrod": "威尔罗德手枪", + "PPK": "PPK手枪", + "PPKS": "PPK/S手枪", + "M1911 Suppressed": "M1911消音手枪", + "StG 44": "StG44突击步枪", + "Sturmgewehr 1-5": "StG1-5突击步枪", + "M1907 SF": "M1907半自动步枪", + "Ribeyrolles 1918": "里贝罗勒1918", + "Breda M1935 PG": "贝雷塔M1935", + "M2 Carbine": "M2卡宾枪", + "Commando Carbine": "突击队卡宾枪", + "M28 con Tromboncino": "M28枪榴弹步枪", + "Jungle Carbine": "丛林卡宾枪", + "Gewehr 43": "Gewehr 43步枪", + "M1A1 Carbine": "M1A1卡宾枪", + "Turner SMLE": "特纳SMLE步枪", + "Selbstlader 1916": "1916型半自动步枪", + "Gewehr 1-5": "Gewehr 1-5步枪", + "Ag m/42": "Ag m/42步枪", + "MAS 44": "MAS 44步枪", + "Karabin 1938M": "1938M卡宾枪", + "M1 Garand": "M1加兰德步枪", + "M3 Infrared": "M3红外线步枪", + "M1941 Johnson": "M1941约翰逊步枪", + "Scout Knife M1916": "M1916侦察刀", + "Hatchet": "手斧", + "Club": "棍棒", + "Shovel": "工兵铲", + "Pickaxe": "镐", + "Kukri": "廓尔喀弯刀", + "British Army Jack Knife": "英军小刀", + "Lever Pipe": "撬棍", + "K98 Bayonet": "K98刺刀", + "Poignard": "匕首", + "German Naval Dagger": "德国海军匕首", + "Broken Bottle": "破瓶", + "Hachiwari": "破刀", + "Escape Axe": "逃生斧", + "EGW Survival Knife": "EGW生存刀", + "Combat Knife": "战斗刀", + "Commando Machete": "突击队砍刀", + "Sai": "十手", + "Bolo-Guna": "波洛刀", + "Ilse's Pickaxe": "伊尔丝的镐", + "Control Stick": "操纵杆", + "Lion Head Sword": "狮头剑", + "Golden Eagle": "金鹰", + "Lewis Gun": "刘易斯机枪", + "KE7": "KE7轻机枪", + "Bren Gun": "布伦轻机枪", + "FG-42": "FG-42伞兵步枪", + "LS/26": "LS/26轻机枪", + "Madsen MG": "麦德森机枪", + "Chauchat": "绍沙轻机枪", + "BAR M1918A2": "BAR M1918A2轻机枪", + "Type 97 MG": "九七式机枪", + "Type 11 LMG": "十一年式轻机枪", + "12g Automatic": "12号自动霰弹枪", + "M30 Drilling": "M30三管猎枪", + "M1897": "M1897霰弹枪", + "Model 37": "37型霰弹枪", + "Sjögren Shotgun": "肖格伦霰弹枪", + "M1928A1": "M1928A1冲锋枪", + "MP34": "MP34冲锋枪", + "MP28": "MP28冲锋枪", + "STEN": "斯登冲锋枪", + "Suomi KP/-31": "索米KP/-31冲锋枪", + "EMP": "EMP冲锋枪", + "MP40": "MP40冲锋枪", + "ZK-383": "ZK-383冲锋枪", + "MAB 38": "MAB 38冲锋枪", + "Type 100": "百式冲锋枪", + "M3 Grease Gun": "M3黄油枪", + "Type 2A": "二式冲锋枪", + "Welgun": "韦尔冈冲锋枪", + "Krag–Jørgensen": "克拉格-约根森步枪", + "Gewehr M95/30": "M95/30步枪", + "Lee-Enfield No.4 Mk I": "李-恩菲尔德No.4 Mk I步枪", + "Kar98k": "Kar98k步枪", + "Ross Rifle Mk III": "罗斯步枪Mk III", + "Type 99 Arisaka": "九九式步枪", + "K31/43": "K31/43步枪", + "RSC": "RSC步枪", + "Selbstlader 1906": "1906型半自动步枪", + "Model 8": "8型半自动步枪", + "ZH-29": "ZH-29步枪", + "Stuka b-1": "斯图卡B-1俯冲轰炸机", + "Stuka b-2": "斯图卡B-2俯冲轰炸机", + "Mosquito mkii": "蚊式MkII战斗机", + "Mosquito fb mkvi": "蚊式FB MkVI战斗轰炸机", + "Bf 109 g-2": "BF109 G-2战斗机", + "Bf 109 g-6": "BF109 G-6战斗机", + "Spitfire mk va": "喷火MkVA战斗机", + "Spitfire mk vb": "喷火MkVB战斗机", + "Blenheim mki": "布伦海姆MKI轰炸机", + "Blenheim mk if": "布伦海姆MK IF战斗机", + "Ju-88 a": "容克88A轰炸机", + "Ju-88 c": "容克88C重型战斗机", + "Corsair f4u-1a": "海盗F4U-1A战斗机", + "Corsair f4u-1c": "海盗F4U-1C战斗机", + "Zero a6m2": "零式A6M2战斗机", + "Zero a6m5": "零式A6M5战斗机", + "P51d fighter": "P51D野马战斗机", + "P51k fighter": "P51K野马战斗机", + "A-20 bomber": "A-20浩劫轰炸机", + "P-70 night fighter": "P-70夜间战斗机", + "Kübelwagen": "水桶车", + "M3": "M3半履带车", + "T48 gmc": "T48运兵车", + "Kettenkrad": "履带摩托车", + "Sd. kfz 251 halftrack": "Sd.Kfz 251半履带车", + "Sd. kfz. 251 pakwagen": "Sd.Kfz 251反坦克车", + "Universal carrier": "布伦通用载具", + "Tractor": "拖拉机", + "Type 95 car": "九五式小型乘用车", + "Gpw": "威利斯吉普", + "Dinghy": "橡皮艇", + "Lcvp": "登陆艇", + "Churchill mk vii": "丘吉尔Mk VII坦克", + "Churchill gun carrier": "丘吉尔运载坦克", + "Churchill crocodile": "丘吉尔鳄式喷火坦克", + "Tiger i": "虎式坦克", + "Sturmtiger": "突击虎式坦克", + "Panzer 38t": "38(t)坦克", + "Staghound t17e1": "猎鹿犬T17E1装甲车", + "Panzer iv": "四号坦克", + "Flakpanzer iv": "四号防空坦克", + "Sturmgeschutz iv": "四号突击炮", + "Valentine mk viii": "瓦伦丁Mk VIII坦克", + "Valentine archer": "瓦伦丁弓箭手坦克", + "Valentine aa mk i": "瓦伦丁防空坦克", + "Sherman": "谢尔曼坦克", + "T34 calliope": "T34管风琴火箭坦克", + "Hachi": "哈奇轻型坦克", + "Type 97": "九七式中型坦克", + "Lvt": "两栖登陆车", + "Ka-mi": "卡米两栖坦克", + "M8 greyhound": "M8灰狗装甲车", + "Sdkfz 234 puma": "Sd.Kfz 234美洲狮装甲车", + "6 pounder": "6磅反坦克炮", + "Flak 38": "Flak 38防空炮", + "Pak 40": "Pak 40反坦克炮", + "Vickers": "维克斯重机枪", + "Stationary mg34": "固定式MG34机枪", + "M2 hmg": "M2重机枪", + "Type 93 hmg": "九三式重机枪", + "40mm aa": "40mm防空炮", + "Type 10": "十年式120mm高射炮" + + }, + "bf3": { + "RPK": "RPK", + "SKS": "SKS", + "M39": "M39", + "Pecheneg": "Pecheneg", + "M416": "M416", + "Saiga": "Saiga", + "G36C": "G36C", + "M412 Rex": "M412 Rex", + "SV98": "SV98", + "AKS74U": "AKS74U", + "RPG-7": "RPG-7", + "Crossbow kobra": "Crossbow kobra", + "Glock 17": "Glock 17", + "AN94": "AN94", + "M60": "M60", + "M1911 LIT": "M1911 LIT", + "SCAR": "SCAR", + "MK11": "MK11", + "Glock 18 Silenced": "Glock 18 Silenced", + "UMP": "UMP", + "PDR": "PDR", + "M98B": "M98B", + "M9 Silenced": "M9 Silenced", + "XP2 L86A1": "XP2 L86A1", + "M40A5": "M40A5", + "USAS": "USAS", + "XP2 LSAT": "XP2 LSAT", + "XP1 QBB95": "XP1 QBB95", + "XP1 HK53": "XP1 HK53", + "M1911": "M1911", + "XP1 QBU88": "XP1 QBU88", + "M16A4": "M16A4", + "XP2 AUG": "XP2 AUG", + "M1014": "M1014", + "AEK971": "AEK971", + "G3A4": "G3A4", + "Taurus 44": "Taurus 44", + "Glock 17 Silenced": "Glock 17 Silenced", + "Underslung Launcher": "Underslung Launcher", + "Glock 18": "Glock 18", + "FGM-148 JAVELIN": "FGM-148 JAVELIN", + "F2000": "F2000", + "M1911 SILENCED": "M1911 SILENCED", + "Knife": "Knife", + "MP443 LIT": "MP443 LIT", + "Taurus 44 scoped": "Taurus 44 scoped", + "XP1 L96": "XP1 L96", + "XP1 FAMAS": "XP1 FAMAS", + "AK74M": "AK74M", + "M249": "M249", + "A91": "A91", + "AS-VAL": "AS-VAL", + "P90": "P90", + "M93R": "M93R", + "XP1 PP19": "XP1 PP19", + "870": "870", + "XP2 HK417": "XP2 HK417", + "MP443 Silenced": "MP443 Silenced", + "M9 Flashlight": "M9 Flashlight", + "PP2000": "PP2000", + "XP2 SPAS12": "XP2 SPAS12", + "MP7": "MP7", + "Underslung Shotgun": "Underslung Shotgun", + "MP 443": "MP 443", + "Crossbow Scoped": "Crossbow Scoped", + "XP1 L85A2": "XP1 L85A2", + "DAO": "DAO", + "M27": "M27", + "SG553": "SG553", + "Type88": "Type88", + "SA-18 IGLA AA": "SA-18 IGLA AA", + "M240": "M240", + "XP2 MTAR-21": "XP2 MTAR-21", + "SVD": "SVD", + "KH2002": "KH2002", + "XP1 Jackhammer": "XP1 Jackhammer", + "XP2 SCARL": "XP2 SCARL", + "M1911 Tactical": "M1911 Tactical", + "XP2 MP5K": "XP2 MP5K", + "FIM-92 STINGER AA": "FIM-92 STINGER AA", + "M9": "M9", + "XP2 ACR": "XP2 ACR", + "M4A1": "M4A1", + "XP2 JNG90": "XP2 JNG90", + "XP1 QBZ95B": "XP1 QBZ95B", + "XP1 MG36": "XP1 MG36", + "Mk153 SMAW": "Mk153 SMAW", + "VEHICLE IFV BTR 90": "VEHICLE IFV BTR 90", + "VEHICLE AIR HELICOPTER ATTACK MI28": "VEHICLE AIR HELICOPTER ATTACK MI28", + "STATIONARY AA CENTURION C-RAM": "STATIONARY AA CENTURION C-RAM", + "VEHICLE AIR JET ATTACK A10": "VEHICLE AIR JET ATTACK A10", + "VEHICLE AIR HELICOPTER SCOUT Z11": "VEHICLE AIR HELICOPTER SCOUT Z11", + "VEHICLE MBT T90": "VEHICLE MBT T90", + "VEHICLE AA LAV-AD": "VEHICLE AA LAV-AD", + "VEHICLE JEEP GROWLER ITV": "VEHICLE JEEP GROWLER ITV", + "VEHICLE JEEP VAN MODIFIED": "VEHICLE JEEP VAN MODIFIED", + "VEHICLE JEEP HUMVEE XP5": "VEHICLE JEEP HUMVEE XP5", + "VEHICLE TRANSPORT RIB BOAT": "VEHICLE TRANSPORT RIB BOAT", + "VEHICLE QUAD BIKE": "VEHICLE QUAD BIKE", + "VEHICLE AIR GUNSHIP": "VEHICLE AIR GUNSHIP", + "STATIONARY AA PANTSIR-S1": "STATIONARY AA PANTSIR-S1", + "VEHICLE AIR JET FIGHTER SU35": "VEHICLE AIR JET FIGHTER SU35", + "VEHICLE AIR HELICOPTER ATTACK AH1Z": "VEHICLE AIR HELICOPTER ATTACK AH1Z", + "VEHICLE IFV BMP": "VEHICLE IFV BMP", + "VEHICLE JEEP VODNIK": "VEHICLE JEEP VODNIK", + "STATIONARY AT KORNET": "STATIONARY AT KORNET", + "VEHICLE IFV LAV-25": "VEHICLE IFV LAV-25", + "VEHICLE JEEP DPV": "VEHICLE JEEP DPV", + "VEHICLE AIR JET ATTACK SU25": "VEHICLE AIR JET ATTACK SU25", + "VEHICLE AIR JET FIGHTER F18": "VEHICLE AIR JET FIGHTER F18", + "VEHICLE TD M1128": "VEHICLE TD M1128", + "VEHICLE AIR JET FIGHTER F-35 JSF": "VEHICLE AIR JET FIGHTER F-35 JSF", + "VEHICLE MBT M1 ABRAMS": "VEHICLE MBT M1 ABRAMS", + "VEHICLE JEEP VODNIK MODIFIED": "VEHICLE JEEP VODNIK MODIFIED", + "VEHICLE TRANSPORT KA-60": "VEHICLE TRANSPORT KA-60", + "VEHICLE TD SPRUT-SD": "VEHICLE TD SPRUT-SD", + "VEHICLE JEEP VODNIK XP5": "VEHICLE JEEP VODNIK XP5", + "VEHICLE AIR DROPSHIP": "VEHICLE AIR DROPSHIP", + "VEHICLE MART BM-23": "VEHICLE MART BM-23", + "STATIONARY AT TOW": "STATIONARY AT TOW", + "VEHICLE MART HIMARS": "VEHICLE MART HIMARS", + "VEHICLE AA 9K22 TUNGUSKA": "VEHICLE AA 9K22 TUNGUSKA", + "VEHICLE JEEP VDV": "VEHICLE JEEP VDV", + "VEHICLE TRANSPORT UH-1Y VENOM": "VEHICLE TRANSPORT UH-1Y VENOM", + "VEHICLE JEEP HUMVEE": "VEHICLE JEEP HUMVEE", + "VEHICLE TRANSPORT AAV-7A1": "VEHICLE TRANSPORT AAV-7A1", + "VEHICLE AIR HELICOPTER SCOUT AH6": "VEHICLE AIR HELICOPTER SCOUT AH6", + "VEHICLE SKIDLOADER": "VEHICLE SKIDLOADER", + "VEHICLE BIKE DIRTBIKE": "VEHICLE BIKE DIRTBIKE", + "VEHICLE JEEP HUMVEE MODIFIED": "VEHICLE JEEP HUMVEE MODIFIED" + }, + "bf4": { + "ak-12": "ak-12", + "ace-53-sv": "ace-53-sv", + "m224-mortar": "m224-mortar", + "mtar-21": "mtar-21", + "boot": "boot", + "ucav": "ucav", + "rpk": "rpk", + "m82a3-mid": "m82a3-mid", + "rorsch-mk-1": "rorsch-mk-1", + "m39-emr": "m39-emr", + "as-val": "as-val", + "338-recon": "338-recon", + "m67-frag": "m67-frag", + "pkp-pecheneg": "pkp-pecheneg", + "m416": "m416", + "qbu-88": "qbu-88", + "m32-mgl": "m32-mgl", + "aug-a3": "aug-a3", + "g36c": "g36c", + "acb-90": "acb-90", + "compact-45": "compact-45", + "mbt-law": "mbt-law", + "mare-s-leg": "mare-s-leg", + "repair-tool": "repair-tool", + "jng-90": "jng-90", + "m412-rex": "m412-rex", + "xm25-smoke": "xm25-smoke", + "hvm-ii": "hvm-ii", + "precision": "precision", + "mg4": "mg4", + "u-100-mk5": "u-100-mk5", + "acw-r": "acw-r", + "saiga-12k": "saiga-12k", + "m60-e4": "m60-e4", + "lsat": "lsat", + "m82a3-cqb": "m82a3-cqb", + "cbj-ms": "cbj-ms", + "unica-6": "unica-6", + "mpx": "mpx", + "scar-h": "scar-h", + "mk11-mod-0": "mk11-mod-0", + "dbv-12": "dbv-12", + "ump-45": "ump-45", + "scout": "scout", + "eod-bot": "eod-bot", + "deagle-44": "deagle-44", + "v40-mini": "v40-mini", + "m98b": "m98b", + "aku-12": "aku-12", + "scar-h-sv": "scar-h-sv", + "f2000": "f2000", + "cs-lr4": "cs-lr4", + "gol-magnum": "gol-magnum", + "qbz-95-1": "qbz-95-1", + "m40a5": "m40a5", + "usas-12": "usas-12", + "sks": "sks", + "tanto": "tanto", + "seal": "seal", + "carbon-fiber": "carbon-fiber", + "fy-js": "fy-js", + "weaver": "weaver", + "m1911": "m1911", + "rfb": "rfb", + "type-95b-1": "type-95b-1", + "xm25-airburst": "xm25-airburst", + "id-p-xp6-iname-m60ult": "id-p-xp6-iname-m60ult", + "m320-smk": "m320-smk", + "m16a4": "m16a4", + "l115": "l115", + "c4-explosive": "c4-explosive", + "trench": "trench", + "c100": "c100", + "neck": "neck", + "aek-971": "aek-971", + "44-magnum": "44-magnum", + "cz-3a1": "cz-3a1", + "l86a2": "l86a2", + "usas-12-flir": "usas-12-flir", + "m2-slam": "m2-slam", + "cz-805": "cz-805", + "amr-2": "amr-2", + "m320-he": "m320-he", + "bj-2": "bj-2", + "scout-elite": "scout-elite", + "shorty-12g": "shorty-12g", + "m320-dart": "m320-dart", + "sv-98": "sv-98", + "rpk-12": "rpk-12", + "defibrillator": "defibrillator", + "p226": "p226", + "p90": "p90", + "bulldog": "bulldog", + "spas-12": "spas-12", + "m249": "m249", + "qbs-09": "qbs-09", + "mx4": "mx4", + "a-91": "a-91", + "m82a3": "m82a3", + "an-94": "an-94", + "ace-21-cqb": "ace-21-cqb", + "groza-1": "groza-1", + "93r": "93r", + "machete": "machete", + "sw40": "sw40", + "xm25-dart": "xm25-dart", + "mp7": "mp7", + "m320-3gl": "m320-3gl", + "tactical": "tactical", + "dive": "dive", + "aa-mine": "aa-mine", + "m320-lvg": "m320-lvg", + "870-mcs": "870-mcs", + "rgo-impact": "rgo-impact", + "fgm-148-javelin": "fgm-148-javelin", + "m15-at-mine": "m15-at-mine", + "amr-2-cqb": "amr-2-cqb", + "groza-4": "groza-4", + "m26-frag": "m26-frag", + "sr338": "sr338", + "uts-15": "uts-15", + "m34-incendiary": "m34-incendiary", + "ace-52-cqb": "ace-52-cqb", + "hawk-12g": "hawk-12g", + "sar-21": "sar-21", + "pp-2000": "pp-2000", + "famas": "famas", + "aws": "aws", + "m26-mass": "m26-mass", + "qbb-95-1": "qbb-95-1", + "mp443": "mp443", + "g18": "g18", + "svd-12": "svd-12", + "bowie": "bowie", + "cs5": "cs5", + "sg553": "sg553", + "m18-smoke": "m18-smoke", + "type-88-lmg": "type-88-lmg", + "shank": "shank", + "sa-18-igla": "sa-18-igla", + "bayonet": "bayonet", + "m26-slug": "m26-slug", + "survival": "survival", + "m240b": "m240b", + "qsz-92": "qsz-92", + "pdw-r": "pdw-r", + "amr-2-mid": "amr-2-mid", + "rpg-7v2": "rpg-7v2", + "ar160": "ar160", + "fgm-172-sraw": "fgm-172-sraw", + "m1014": "m1014", + "l85a2": "l85a2", + "ump-9": "ump-9", + "srr-61": "srr-61", + "m26-dart": "m26-dart", + "fim-92-stinger": "fim-92-stinger", + "m84-flashbang": "m84-flashbang", + "m18-claymore": "m18-claymore", + "sr-2": "sr-2", + "m9": "m9", + "ak-5c": "ak-5c", + "ace-23": "ace-23", + "m320-fb": "m320-fb", + "m136-cs": "m136-cs", + "improvised": "improvised", + "cz-75": "cz-75", + "m4": "m4", + "fn57": "fn57", + "js2": "js2", + "ballistic-shield": "ballistic-shield", + "hand-flare": "hand-flare", + "dao-12": "dao-12", + "mk153-smaw": "mk153-smaw", + "BTR-90": "BTR-90", + "MI-28-HAVOC": "MI-28-HAVOC", + "PANTSIR-S1": "PANTSIR-S1", + "ZBD-09": "ZBD-09", + "A10-WARTHOG": "A10-WARTHOG", + "Z-11W": "Z-11W", + "T-90A": "T-90A", + "DV-15": "DV-15", + "ZFB-05": "ZFB-05", + "M1161-ITV": "M1161-ITV", + "UH-1Y-VENOM": "UH-1Y-VENOM", + "RAWR": "RAWR", + "KA-60-KASATKA": "KA-60-KASATKA", + "50-CAL": "50-CAL", + "Z-10W": "Z-10W", + "LAUNCH-POD": "LAUNCH-POD", + "M1-ABRAMS1": "M1-ABRAMS1", + "J-20": "J-20", + "UCAV1": "UCAV1", + "SPM-3": "SPM-3", + "SU-25TM-FROGFOOT": "SU-25TM-FROGFOOT", + "HJ-8-LAUNCHER": "HJ-8-LAUNCHER", + "PWC": "PWC", + "AH-1Z-VIPER": "AH-1Z-VIPER", + "MAV": "MAV", + "AH-6J-LITTLE-BIRD": "AH-6J-LITTLE-BIRD", + "Z-9-HAITUN": "Z-9-HAITUN", + "ACV": "ACV", + "Q-5-FANTAN": "Q-5-FANTAN", + "DV-151": "DV-151", + "9M133-KORNET-LAUNCHER": "9M133-KORNET-LAUNCHER", + "50-CAL1": "50-CAL1", + "DPV": "DPV", + "SUAV": "SUAV", + "XD-1-ACCIPITER": "XD-1-ACCIPITER", + "9K22-TUNGUSKA-M": "9K22-TUNGUSKA-M", + "HJ-8-LAUNCHER1": "HJ-8-LAUNCHER1", + "BOMBER": "BOMBER", + "LAV-25": "LAV-25", + "LD-2000-AA": "LD-2000-AA", + "UH-1Y-VENOM1": "UH-1Y-VENOM1", + "F35": "F35", + "LAV-251": "LAV-251", + "OLD-CANNON": "OLD-CANNON", + "TYPE-99-MBT": "TYPE-99-MBT", + "KA-60-KASATKA1": "KA-60-KASATKA1", + "LAV-AD": "LAV-AD", + "M224-MORTAR1": "M224-MORTAR1", + "Z-11W1": "Z-11W1", + "SPM-31": "SPM-31", + "M220-TOW-LAUNCHER": "M220-TOW-LAUNCHER", + "M1421": "M1421", + "UCAV2": "UCAV2", + "DIRTBIKE": "DIRTBIKE", + "9K22-TUNGUSKA-M1": "9K22-TUNGUSKA-M1", + "SNOWMOBILE": "SNOWMOBILE", + "SCHIPUNOV-42": "SCHIPUNOV-42", + "RHIB-BOAT": "RHIB-BOAT", + "UH-1Y-VENOM2": "UH-1Y-VENOM2", + "BOMBER1": "BOMBER1", + "BOMBER2": "BOMBER2", + "BTR-901": "BTR-901", + "SKID-LOADER": "SKID-LOADER", + "AC-130-GUNSHIP": "AC-130-GUNSHIP", + "VDV-BUGGY": "VDV-BUGGY", + "RCB": "RCB", + "EOD-BOT1": "EOD-BOT1", + "QUAD-BIKE": "QUAD-BIKE", + "AAV-7A1-AMTRAC": "AAV-7A1-AMTRAC", + "M142": "M142", + "MRAP": "MRAP", + "LAV-AD1": "LAV-AD1", + "LYT2021": "LYT2021", + "50-CAL2": "50-CAL2", + "AA-MINE1": "AA-MINE1", + "Z-11W2": "Z-11W2", + "M1-ABRAMS": "M1-ABRAMS", + "SU-50": "SU-50", + "TYPE-95-AA": "TYPE-95-AA", + "AH-6J-LITTLE-BIRD1": "AH-6J-LITTLE-BIRD1", + "T-90A1": "T-90A1", + "HT-95-LEVKOV": "HT-95-LEVKOV" + } +} + +color_select = { + "SS": (220, 20, 60), # 赤紅色 + "S": (255, 140, 0), # 橙色 + "A": (255, 225, 0), # 黃色 + "B": (30, 144, 255), # 藍色 + "C": (0, 204, 102) # 綠色 +} + +interval_table = { + (0, 0.25): {"blood": {"designation": "炮灰新兵", + "level": "C", + "color": color_select["C"], }, + "dedication": {"designation": "后勤新兵", + "level": "C", + "color": color_select["C"]}, + "all_round": {"designation": "战场新兵", + "level": "C", + "color": color_select["C"]}}, + (0.25, 0.50): {"blood": {"designation": "冷血先锋", + "level": "B", + "color": color_select["B"]}, + "dedication": {"designation": "战地医师", + "level": "B", + "color": color_select["B"]}, + "all_round": {"designation": "略知一二", + "level": "B", + "color": color_select["B"]}}, + (0.50, 0.75): {"blood": {"designation": "火线狂徒", + "level": "A", + "color": color_select["A"]}, + "dedication": {"designation": "风雨使者", + "level": "A", + "color": color_select["A"]}, + "all_round": {"designation": "博学多才", + "level": "A", + "color": color_select["A"]}}, + (0.75, 1.0): {"blood": {"designation": "绞肉战神", + "level": "S", + "color": color_select["S"]}, + "dedication": {"designation": "广厦之荫", + "level": "S", + "color": color_select["S"]}, + "all_round": {"designation": "六艺俱精", + "level": "S", + "color": color_select["S"]}}, + (1.0, float("inf")): {"blood": {"designation": "死神之眼", + "level": "SS", + "color": color_select["SS"]}, + "dedication": {"designation": "救世圣手", + "level": "SS", + "color": color_select["SS"]}, + "all_round": {"designation": "成为传奇", + "level": "SS", + "color": color_select["SS"]}} +} diff --git a/src/plugins/bf_bot/template/bf1.png b/src/plugins/bf_bot/template/bf1.png new file mode 100644 index 0000000..ecb5b91 Binary files /dev/null and b/src/plugins/bf_bot/template/bf1.png differ diff --git a/src/plugins/bf_bot/template/bf3.png b/src/plugins/bf_bot/template/bf3.png new file mode 100644 index 0000000..cb88a27 Binary files /dev/null and b/src/plugins/bf_bot/template/bf3.png differ diff --git a/src/plugins/bf_bot/template/bf4.png b/src/plugins/bf_bot/template/bf4.png new file mode 100644 index 0000000..b66c256 Binary files /dev/null and b/src/plugins/bf_bot/template/bf4.png differ diff --git a/src/plugins/bf_bot/template/bfv.png b/src/plugins/bf_bot/template/bfv.png new file mode 100644 index 0000000..cf037b4 Binary files /dev/null and b/src/plugins/bf_bot/template/bfv.png differ diff --git a/src/plugins/bf_bot/test.py b/src/plugins/bf_bot/test.py new file mode 100644 index 0000000..5dc35e5 --- /dev/null +++ b/src/plugins/bf_bot/test.py @@ -0,0 +1,5 @@ +from bf6_data import * + +if __name__ == "__main__": + name = "A.R.O.N.A" + asyncio.run(get_info(name, 0)) diff --git a/src/plugins/bf_bot/text_utils.py b/src/plugins/bf_bot/text_utils.py new file mode 100644 index 0000000..84511e8 --- /dev/null +++ b/src/plugins/bf_bot/text_utils.py @@ -0,0 +1,33 @@ +import re + + +def convert_to_hours(time_str): + """ + 将各种时间格式字符串转换为小时数 + 支持格式示例: + - "1 day, 1:49:15" (包含天数) + - "1:56:55" (小时:分钟:秒) + - "0:06:41" (分钟:秒) + - "14:11:36" (小时:分钟:秒) + """ + total_hours = 0.0 + + # 处理包含天数的部分 + if "day" in time_str: + days_part, time_part = time_str.split(", ") + days = float(re.search(r"\d+", days_part).group()) + total_hours += days * 24 + time_str = time_part + + # 分割时间部分 + parts = list(map(float, time_str.split(":"))) + + # 根据时间部分长度计算 + if len(parts) == 3: # 时:分:秒 + total_hours += parts[0] + parts[1] / 60 + parts[2] / 3600 + elif len(parts) == 2: # 分:秒 + total_hours += parts[0] / 60 + parts[1] / 3600 + elif len(parts) == 1: # 仅秒 + total_hours += parts[0] / 3600 + + return round(total_hours, 2) diff --git a/src/plugins/bf_bot/user_data/data_utils.py b/src/plugins/bf_bot/user_data/data_utils.py new file mode 100644 index 0000000..ea62bb7 --- /dev/null +++ b/src/plugins/bf_bot/user_data/data_utils.py @@ -0,0 +1,92 @@ +import sqlite3 +from typing import List, Dict, Any, Optional + + +class TableManager: + def __init__(self, db_path: str = 'user_data.db'): + self.db_path = db_path + + def _execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor: + """执行SQL语句的通用方法""" + with sqlite3.connect(self.db_path) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(sql, params) + conn.commit() + return cursor + + +class UserManager(TableManager): + def add_user(self, qq_id: str, ea_id: str, dog_tag_list: str) -> int: + """添加用户记录""" + cursor = self._execute( + "INSERT INTO users (qq_id, ea_id, dog_tag_list) VALUES (?, ?, ?)", + (qq_id, ea_id, dog_tag_list) + ) + return cursor.lastrowid + + def get_user_by_qq(self, qq_id: str) -> Optional[Dict[str, Any]]: + """通过QQ号查询用户""" + cursor = self._execute( + "SELECT * FROM users WHERE qq_id = ?", + (qq_id,) + ) + return dict(cursor.fetchone()) if cursor.fetchone() else None + + def update_dog_tags(self, user_id: int, new_tags: str) -> bool: + """更新用户的狗牌列表""" + self._execute( + "UPDATE users SET dog_tag_list = ? WHERE id = ?", + (new_tags, user_id) + ) + return True + + +class DogTagManager(TableManager): + def create_tag(self, name: str) -> int: + """创建新狗牌""" + cursor = self._execute( + "INSERT INTO dog_tag (name) VALUES (?)", + (name,) + ) + return cursor.lastrowid + + def get_all_tags(self) -> List[Dict[str, Any]]: + """获取所有狗牌""" + cursor = self._execute("SELECT * FROM dog_tag") + return [dict(row) for row in cursor.fetchall()] + + +class QueryRecordManager(TableManager): + def log_query(self, user_id: str, target_id: str, status: str) -> int: + """记录查询操作""" + cursor = self._execute( + """INSERT INTO query_record + (user_id, target_id, status) + VALUES (?, ?, ?)""", + (user_id, target_id, status) + ) + return cursor.lastrowid + + def get_user_history(self, user_id: str) -> List[Dict[str, Any]]: + """获取用户查询历史""" + cursor = self._execute( + "SELECT * FROM query_record WHERE user_id = ?", + (user_id,) + ) + return [dict(row) for row in cursor.fetchall()] + + +if __name__ == "__main__": + # 使用示例 + user_db = UserManager() + user_id = user_db.add_user("123456", "EA_001", "tag1,tag2") + print(f"Created user with ID: {user_id}") + + tag_db = DogTagManager() + 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}") diff --git a/src/plugins/bf_bot/user_data/init_database.py b/src/plugins/bf_bot/user_data/init_database.py new file mode 100644 index 0000000..eb36628 --- /dev/null +++ b/src/plugins/bf_bot/user_data/init_database.py @@ -0,0 +1,114 @@ +import os +import sqlite3 +from nonebot import logger +from typing import Optional, List, Tuple + + +class DatabaseManager: + def __init__(self, db_path: str = 'user_data.db'): + self.db_path = db_path + self.expected_tables = { + 'users': [ + ('id', 'INTEGER PRIMARY KEY AUTOINCREMENT'), + ('qq_id', 'TEXT NOT NULL'), + ('ea_id', 'TEXT UNIQUE'), + ('dog_tag_list', 'TEXT NOT NULL'), + ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') + ], + 'dog_tag': [ + ('id', 'INTEGER PRIMARY KEY'), + ('name', 'TEXT NOT NULL'), + ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') + ] + , + 'query_record': [ + ('id', 'INTEGER PRIMARY KEY'), + ('user_id', 'TEXT NOT NULL'), + ('target_id', 'TEXT NOT NULL'), + ('status', 'TEXT NOT NULL'), + ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') + ] + } + + def initialize_database(self) -> bool: + """初始化数据库并创建表结构""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # 创建所有表 + for table_name, columns in self.expected_tables.items(): + columns_sql = ', '.join(f'{col} {typ}' for col, typ in columns) + cursor.execute(f''' + CREATE TABLE IF NOT EXISTS {table_name} ( + {columns_sql} + ) + ''') + + conn.commit() + return True + + except Exception as e: + logger.error(f"Database initialization failed: {str(e)}") + if os.path.exists(self.db_path): + conn.close() # 确保连接关闭 + for _ in range(3): # 重试机制 + try: + os.remove(self.db_path) + break + except PermissionError: + time.sleep(0.5) + return False + + def _verify_database_integrity(self, conn: sqlite3.Connection) -> bool: + """验证数据库完整性""" + cursor = conn.cursor() + + # 检查表是否存在 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + existing_tables = {row[0] for row in cursor.fetchall()} + + if existing_tables != set(self.expected_tables.keys()): + logger.warning(f"Missing tables: {set(self.expected_tables.keys()) - existing_tables}") + return False + + # 检查每个表的结构 + for table_name, expected_columns in self.expected_tables.items(): + cursor.execute(f"PRAGMA table_info({table_name})") + actual_columns = [(row[1], row[2]) for row in cursor.fetchall()] + + if not self._compare_columns(actual_columns, expected_columns): + logger.warning(f"Table {table_name} structure mismatch") + return False + + # 执行完整性检查 + cursor.execute("PRAGMA integrity_check") + integrity_result = cursor.fetchone() + if integrity_result[0] != 'ok': + logger.warning(f"Integrity check failed: {integrity_result}") + return False + + return True + + def _compare_columns(self, actual: List[Tuple], expected: List[Tuple]) -> bool: + """比较实际列与预期列是否匹配""" + if len(actual) != len(expected): + return False + + for (act_col, act_type), (exp_col, exp_type) in zip(actual, expected): + if act_col.lower() != exp_col.lower(): + return False + if not act_type.upper().startswith(exp_type.upper()): + return False + + return True + + +# 初始化 +if __name__ == '__main__': + db_manager = DatabaseManager() + if db_manager.initialize_database(): + print("Database initialized successfully") + print(f"Database file created at: {os.path.abspath(db_manager.db_path)}") + else: + print("Database initialization failed") diff --git a/启动机器人.bat b/启动机器人.bat new file mode 100644 index 0000000..2c7e4e7 --- /dev/null +++ b/启动机器人.bat @@ -0,0 +1,8 @@ +@echo off +chcp 65001 > nul +echo 启动 Bot +call .venv\Scripts\activate.bat +python bot.py +deactivate +pause +exit \ No newline at end of file