diff --git a/hexi/plugins/nonebot_plugin_bf_bot/__init__.py b/hexi/plugins/nonebot_plugin_bf_bot/__init__.py index 7e0ded5..cc33b69 100644 --- a/hexi/plugins/nonebot_plugin_bf_bot/__init__.py +++ b/hexi/plugins/nonebot_plugin_bf_bot/__init__.py @@ -1,274 +1,62 @@ -from nonebot import on_command, require -from nonebot.adapters import Message, Event, Bot +"""BF查询 —— 入口(MTSS:handlers → services → repository + utils)。 + +只做五件事:元数据 / require / 导入子模块(命令 + 配置 + Web)/ 建表 / 扩展桥启停钩子。 +业务命令在 handlers/,编排在 services/,数据读写走 repository.py。 +""" + +from nonebot import get_driver, require from nonebot.log import logger -from nonebot.matcher import Matcher -from nonebot.params import CommandArg from nonebot.plugin import PluginMetadata -from nonebot.rule import to_me -import time -from .user_data.data_utils import UserManager 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=""" - bf3: /bf3 EAID (查询BF3数据) - bf4: /bf4 EAID (查询BF4数据) - bf1: /bf1 EAID (查询BF5数据) - bfv: /bfv EAID (查询BF6数据) - bf2042: /bf2042 EAID (查询BF2042数据) - bf6: /bf6 EAID (查询BF6数据) + bf3: /bf3 [EAID] (查询BF3数据) + bf4: /bf4 [EAID] (查询BF4数据) + bf1: /bf1 [EAID] (查询BF1数据) + bfv: /bfv [EAID] (查询BFV数据) + bf2042: /bf2042 [EAID] (查询BF2042生涯数据) + bf2042: /bf2042 武器|载具|装备 [EAID] (BF2042 榜单) + bf2042: /bf2042 模式 [EAID] (BF2042 各模式明细) + bf6: /bf6 [EAID] (查询BF6数据) 绑定: /绑定 EAID (绑定你的QQ与EAID) 解绑: /解绑 (解除你的QQ与当前绑定的EAID) 修改绑定: /修改绑定 EAID (修改你的QQ与当前绑定的EAID) + 以上查询命令省略 EAID 时使用已绑定账号;命令需要 @机器人。 """.strip(), type="application", - extra={ - - }, ) -query = on_command("bft", rule=to_me(), aliases={"bf3", "bf4", "bfv", "bf1", "bf2042", "bf6"}, block=True) -bind = on_command("bfbind", rule=to_me(), aliases={"绑定"}, block=True) -unbind = on_command("unbind", rule=to_me(), aliases={"解绑"}, block=True) -update_bind = on_command("update_bind", rule=to_me(), aliases={"修改绑定"}, block=True) +# 导入子模块:handlers 注册命令,config 注册配置,web 提供 /api/bf_bot +from . import config as _config # noqa: E402 +from . import handlers as _handlers # noqa: E402,F401 +from . import web as _web # noqa: E402,F401 +from .repository import init_database # noqa: E402 -bf_dict = { - "bf3": "战地3", - "bf4": "战地4", - "bf1": "战地1", - "bfv": "战地5", - "bf2042": "战地2042", - "bf6": "战地6", -} +# 建表(幂等):库文件不随仓库分发,新环境/热重载都要在这里补齐,否则查询报 no such table +if not init_database(): + logger.error("用户数据库初始化失败,绑定/标签功能将不可用") +_config.register_config() -@query.handle() -async def handle_function(event: Event, matcher: Matcher, msg: Message = CommandArg()): - start_time = time.time() - usermanager = UserManager() - cmd = matcher.state["_prefix"]["command"][0] - game = cmd - input_text = msg.extract_plain_text().strip() - logger.info(f"{type(input_text)}, {repr(input_text)}") - if input_text is None or input_text == "": - content = usermanager.get_user_by_qq(event.get_user_id()) - if not content: - await UniMessage.at(user_id=event.get_user_id()).text( - "未检测到id,也未检测到绑定记录,请使用 绑定 EAID 进行绑定操作").send() - return - player_id = content['ea_player_id'] - user_id = content['ea_user_id'] - player = content['ea_player_name'] - platform = "pc" - else: - content = await get_player_info_by_name(input_text, "pc") - if 'errors' in content: - await UniMessage.at(user_id=event.get_user_id()).text("未找到玩家,请检查是否拼写正确").send() - return - player_id = content['personaId'] - user_id = content['userId'] - player = content.get('personaName') or content.get('displayName') or content.get('username', 'error') - if 'errors' in player: - await UniMessage.text(f"请检查id是否正确!").send() - return - platform = "pc" - await UniMessage.text(f"正在查询 {player} 的 {bf_dict[cmd]} 数据,请耐心等待").send() - play_stat = "" - try: - if cmd == "bf3": - play_stat = await get_data_bf3(player, player_id, user_id, platform) - elif cmd == "bf4": - play_stat = await get_data_bf4(player, player_id, user_id, platform) - elif cmd == "bf1": - play_stat = await get_data_bf1(player, player_id, user_id, platform) - elif cmd == "bfv": - play_stat = await get_data_bfv(player, player_id, user_id, platform) - elif cmd == "bf2042": - await UniMessage.at(event.get_user_id()).text("正在开发中,敬请期待!").send() - elif cmd == "bf6": - play_stat, rank = await asyncio.gather( - get_data_bf6(player, player_id, user_id, platform), - get_bf6_rank(player) - ) - logger.info(f"等级:{rank}") - else: - await UniMessage.at(event.get_user_id()).text("指令异常").send() - return - if "errors" in play_stat: - logger.warning(play_stat['errors'][0]) - msg = play_stat['errors'][0] - await UniMessage.text(f"{msg}").send() - return - else: - # logger.info(f"结果{play_stat}") - # player = play_stat['userName'] - weapon, vehicle = await get_best_weapon_and_best_vehicle(play_stat) - pid = play_stat['userId'] - kd = play_stat['killDeath'] - kpm = play_stat['killsPerMinute'] - acc = play_stat['accuracy'] - # 战地3 特化 - if cmd == 'bf3': - head_shots = play_stat['headShots'] - else: - head_shots = play_stat['headshots'] - time_play = convert_to_hours(play_stat['timePlayed']) - logger.info(f"游玩时长监测:{time_play}") - if time_play == 0: - await UniMessage.at(event.get_user_id()).text("查询接口异常,请等待一段时间后查询").send() - return - kills = int(play_stat['kills']) - 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'] - destroyed = await get_vehicle_destroyed(play_stat["vehicles"]) - if cmd == 'bf6': - best_class = await get_best_classes(play_stat) - captured = play_stat['objective']['captured'] - score = sum(item.get("score", 0) for item in play_stat['classes']) - # 计算4兵种游玩时长 - seconds = sum(item.get("secondsPlayed", 0) for item in play_stat['classes']) - kpm = round(kills / (int(seconds) / 60), 2) - logger.info(f"击杀数:{kills},游玩分钟数:{seconds / 60},计算kpm值:{kpm}") - spm = int(score / (int(seconds) / 60)) - logger.info(f"计算spm值:{spm}") - repairs = play_stat['repairs'] - img = await build_bf6_stats_card(game='bf6', - qq_id=event.get_user_id(), - player=player, - pid=pid, - rank=rank, - kd=kd, - kpm=kpm, - spm=spm, - acc=acc, - head_shots=head_shots, - time_play=time_play, - kills=kills, - kill_assists=kill_assists, - revives=revives, - repairs=repairs, - captured=captured, - score=score, - wins=wins, - loses=loses, - destroyed=destroyed, - best_weapon=best_weapon, - best_vehicle=best_vehicle, - best_class=best_class) - else: - spm = play_stat['scorePerMinute'] - rank = play_stat['rank'] - longest_head_shot = play_stat['longestHeadShot'] - highest_ill_streak = play_stat['highestKillStreak'] - stat_data, level_designation = await stats_calculator(play_stat, cmd) - 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) - end_time = time.time() - duration = round(end_time - start_time, 3) - await UniMessage.image(raw=img.getvalue()).at(user_id=event.get_user_id()).text( - f"本次查询耗时:{duration}s").send() - except Exception as e: - logger.exception(f"异常:{e}") +# BF2042 浏览器扩展桥:WS 服务端随 bot 启停(127.0.0.1;见 services/fetchers/tracker_bridge.py) +from .services.fetchers import tracker_bridge as _tracker_bridge # noqa: E402 +_driver = get_driver() +_driver.on_startup(_tracker_bridge.start_bridge_server) +_driver.on_shutdown(_tracker_bridge.stop_bridge_server) -@bind.handle() -async def bind_user(event: Event, matcher: Matcher, msg: Message = CommandArg()): - user_manager = UserManager() - player_name = msg.extract_plain_text() - qq_id = event.get_user_id() +# 注册到统一 Web 管理台(/hub):hub 启动时挂载 /api/bf_bot +# 前端页面 hexi/web/src/plugins/bf_bot/(绑定列表 + 删除) +from hexi.web_hub.web_plugin_registry import register_web_plugin # noqa: E402 - # 执行已有数据查询操作 - user_record = user_manager.get_user_by_qq(qq_id) - if user_record: - user_info = await get_player_info_by_ea_id(user_record['ea_player_id'], user_record['ea_user_id'], 'pc') - await (UniMessage.at(user_id=event.get_user_id()) - .text(f"您已经将账号绑定到: {user_info['personaName']},如果你需要更改绑定请发送 修改绑定 EAID ").send()) - return - else: - user_info_json = await get_player_info_by_name(player_name, "pc") - if 'errors' in user_info_json: - await UniMessage.at(user_id=event.get_user_id()).text("未找到玩家,请检查是否拼写正确").send() - player_id = user_info_json['personaId'] - user_id = user_info_json['userId'] - player_name = user_info_json.get('personaName') or user_info_json.get('displayName') or user_info_json.get( - 'username', 'error') - avatar_url = user_info_json['avatar'] - logger.info(f"QQ号:{qq_id},EA双ID:{player_id}-{user_id}") - - # 检测ea账号 是否被绑定至其他qq - bind_record = user_manager.get_user_by_ea_id(user_id) - if bind_record: - await (UniMessage.at(user_id=event.get_user_id()) - .text( - f"您的EA账号 {bind_record['ea_player_name']} 已经绑定到: {bind_record['qq_id']},请您确认绑定的EAID").send()) - return - else: - # 执行绑定操作 - record = user_manager.add_user(qq_id, player_name, player_id, user_id, "[]", "[]") - logger.info(f"插入数据返回结果:{record}") - if record > 0: - await UniMessage.at(user_id=event.get_user_id()).text(f"您已经成功绑定至: {player_name}").send() - - -@unbind.handle() -async def bind_user(event: Event, matcher: Matcher, msg: Message = CommandArg()): - user_manager = UserManager() - qq_id = event.get_user_id() - - # 执行已有数据查询操作 - user_record = user_manager.get_user_by_qq(qq_id) - if user_record: - user_info = await get_player_info_by_ea_id(user_record['ea_player_id'], user_record['ea_user_id'], 'pc') - rows = user_manager.delete_user_by_qq(qq_id) - if rows > 0: - await (UniMessage.at(user_id=event.get_user_id()) - .text( - f"您已经与: {user_info['personaName']} 成功解除绑定,如果你需要重新绑定请发送 修改绑定 EAID ").send()) - return - else: - await UniMessage.at(user_id=event.get_user_id()).text(f"未查询到绑定记录").send() - - -@update_bind.handle() -async def bind_user(event: Event, matcher: Matcher, msg: Message = CommandArg()): - user_manager = UserManager() - player_name = msg.extract_plain_text() - qq_id = event.get_user_id() - - # 执行已有数据查询操作 - user_record = user_manager.get_user_by_qq(qq_id) - if user_record: - user_info_json = await get_player_info_by_name(player_name, "pc") - if 'errors' in user_info_json: - await UniMessage.at(user_id=event.get_user_id()).text("未找到玩家,请检查是否拼写正确").send() - player_id = user_info_json['personaId'] - user_id = user_info_json['userId'] - player_name = user_info_json.get('personaName') or user_info_json.get('displayName') or user_info_json.get( - 'username', 'error') - avatar_url = user_info_json['avatar'] - logger.info(f"QQ号:{qq_id},EA双ID:{player_id}-{user_id}") - - # 执行修改绑定操作 - record = user_manager.update_user(qq_id, player_name, player_id, user_id, "[]", "[]") - logger.info(f"修改数据返回结果:{record}") - if record > 0 and record == 1: - await UniMessage.at(user_id=event.get_user_id()).text( - f"您已经成功从:{user_record['ea_player_name']}修改绑定至: {player_name}").send() - else: - await UniMessage.at(user_id=event.get_user_id()).text( - f"未查询到绑定记录: {player_name},如果需要绑定,请@机器人并发送 绑定 {player_name} ").send() +register_web_plugin( + "bf_bot", + "BF查询", + "gamepad", + lambda: _web.build_admin_app(), + module_name=__name__, +) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/bf6_data.py b/hexi/plugins/nonebot_plugin_bf_bot/bf6_data.py deleted file mode 100644 index a0b94d7..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/bf6_data.py +++ /dev/null @@ -1,137 +0,0 @@ -import asyncio -import os -import random -from pathlib import Path -from typing import List, Dict, Optional - -from curl_cffi import AsyncSession, CurlError -from nonebot import logger - -# ---------- 配置 ---------- -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 = """ -sec-ch-ua-platform: "Windows" -sec-ch-ua: "Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141" -sec-ch-ua-mobile: ?0 -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 -""" - -# ====================== - - -def parse_raw_headers(raw: str) -> Dict[str, str]: - 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 = [] - 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]: - headers = parse_raw_headers(raw_headers) - - # 强制改成 XHR 请求风格 - headers.update({ - "accept": "application/json, text/plain, */*", - "origin": "https://tracker.gg", - "referer": "https://tracker.gg/", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-site", - }) - - 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: - await asyncio.sleep(random.uniform(0.5, 1.2)) # 模拟浏览器请求间隔 - response = await session.get(url, headers=headers, impersonate=impersonate, timeout=timeout) - - # 重试一次,防止偶尔 CF 拦截 - if getattr(response, "status_code", 0) in (403, 429): - await asyncio.sleep(2) - 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.txt - 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": getattr(resp, "status_code", None), "preview": getattr(resp, "text", "")[:200]} - else: - logger.info("请求成功。") - return getattr(resp, "json", lambda: resp)() if hasattr(resp, "json") else resp - - diff --git a/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/README.md b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/README.md new file mode 100644 index 0000000..2105863 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/README.md @@ -0,0 +1,80 @@ +# HeXi BF2042 捕获桥(浏览器扩展) + +被动捕获 tracker.gg 的 BF2042 API 响应,经 WebSocket 转发给本机 bot。 +服务端有两个等价实现:bot 插件内的 `services/fetchers/tracker_bridge.py`(正式, +随 bot 启停,端口/token 是 /hub 插件配置页里的 `bf_ext_ws_port`/`bf_ext_ws_token`), +以及 `debug/bf2042_bridge_server.py`(独立联调用,不启 bot)。两侧默认 +`ws://127.0.0.1:39272/?token=hexi-bf2042`。 + +## 原理 + +历史通道要走 `--remote-debugging-port` + Playwright `connect_over_cdp`。本扩展把捕获 +搬进浏览器内部,参照 FetchV / 猫抓一类捕获扩展的通行模式: + +- `inject.js`(主世界,document_start)用 Proxy 包住 `fetch` / `XMLHttpRequest`, + 被动观察**页面自己发出**的请求(不改写、不重放,只读 clone 的响应体); +- `content.js`(隔离世界)把捕获经 `chrome.runtime` 转给 SW + (内容脚本连 `ws://127.0.0.1` 会被页面 CSP 的 connect-src 拦掉,所以 WS 必须放 SW); +- `background.js`(SW)持有到 bot 的 WebSocket,转发捕获、执行取数任务: + 找/开 tracker.gg 标签页 → 回首页落稳 → 站内跳转进 profile 页 → 等页面发 XHR。 +- **失败重试**:单轮 22s 内关键响应没齐(多半撞了 CF 挑战/403,页面 XHR 拿不到 + 数据),自动重走"首页→站内跳转"刷新,最多 3 轮;每轮结束做页面健康检查 + (挑战页/封禁页/登录过期)并打日志。 + +**不需要任何调试端口**,浏览器就是个普通 Edge 实例,可以常驻不关。 + +## 安装(一次性) + +1. 用专用配置开 Edge(登录态 + CF 信任都在里面): + ``` + msedge.exe --user-data-dir="\hexi\data\nonebot_plugin_bf_bot\edge_profile" + ``` +2. 地址栏开 `edge://extensions` → 打开"开发人员模式" → "加载解压缩的扩展" → 选本目录。 +3. 若有"开发者模式扩展"启动提示,保留即可(纯提醒,不影响)。 + +代码改动后在 `edge://extensions` 点该扩展的"刷新"按钮生效;SW 日志在扩展详情页点 +"Service Worker" 检查。 + +## 联调(不启 bot) + +``` +.venv\Scripts\python.exe debug\bf2042_bridge_server.py # 手动浏览观察 +.venv\Scripts\python.exe debug\bf2042_bridge_server.py --job EAID # 自动取数全流程 +``` + +预期输出:先 "扩展已连接";手动打开任意 BF2042 profile 页会打印被动捕获; +`--job` 时扩展自动完成首页→站内跳转,打印 4 个响应,最后 "任务完成 keys=..."。 + +注意:bot 在跑时 39272 已被 bot 自己的扩展桥占用——要跑独立联调服务端得先停 +bot(或临时改扩展 `background.js` 的端口常量)。bot 在跑时直接看 bot 日志里 +"BF2042 扩展桥" 相关行即可,不用另起服务端。 + +## WebSocket 协议(v0) + +服务端:`ws://127.0.0.1:39272/?token=hexi-bf2042`(仅回环 + 共享 token; +常量在 `background.js` 顶部,bot 侧对应配置项为 `bf_ext_ws_port`/`bf_ext_ws_token`, +两边保持一致)。 + +| 方向 | 消息 | +|---|---| +| ext→bot | `{"type":"hello","name":"hexi-bf2042-bridge","version":"0.1.0"}` | +| bot→ext | `{"type":"fetch","id":"...","player":"X","platform":"origin","profileUrl":"https://tracker.gg/bf2042/profile/origin/X/overview","apiBase":"https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/X"}` | +| ext→bot | `{"type":"capture","id":null 或任务id,"key":"profile\|weapon\|vehicle\|soldier","url":"...","status":200,"body":{...}}` | +| ext→bot | `{"type":"job_done","id":"...","keys":["profile","weapon","vehicle"]}` | +| ext→bot | `{"type":"job_error","id":"...","keys":[...],"detail":"..."}` | +| 双向 | `{"type":"ping"}` | + +- 无任务时的捕获以 `id: null` 被动转发。 +- 任务的 URL 匹配"两遍 decode 再比",消除 python `quote()` 与 JS + `encodeURIComponent` 的转义差异。 +- 任务期间只认自己驱动的标签页(`tabId` 过滤)。 + +## 注意 + +- 导航定律保留:首页 bootstrap → 站内跳转(`location.href`);深链直开会被挑战。 +- MV3 SW 保活:活跃 WebSocket(Chromium 116+)+ `chrome.alarms`(30s)双保险, + bot 重启后扩展自动重连。 +- 若手动浏览时页面有数据、扩展却捕获不到 → 该请求可能不经过页面 `fetch`/`XHR` + (如 Worker 发起)。届时把捕获后端换成 `chrome.debugger`(只开 Network 域), + 管道其余部分不用动。 +- 本目录不要放 `.py`(它是插件目录子目录的一部分,保持纯前端文件)。 diff --git a/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/background.js b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/background.js new file mode 100644 index 0000000..b6957cf --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/background.js @@ -0,0 +1,283 @@ +// HeXi BF2042 捕获桥 - Service Worker +// +// 两个职责: +// 1) 持有到本机 bot 的 WebSocket(自动重连 + 心跳),转发页面捕获的响应; +// 2) 执行 bot 下发的取数任务:找/开 tracker.gg 标签页 → 首页 bootstrap → +// 站内跳转进 profile 页 → 等页面自己发出 XHR → 上报 job_done。 +// +// 导航定律(来自插件文档,勿改):地址栏深链会被 Cloudflare 挑战,必须先落首页、 +// 再在站内跳转。全程不用任何调试端口,浏览器就是个普通实例。 +// +// 重试机制:页面撞 CF 挑战/403 时,页面自己的 XHR 不会产出数据,本轮 +// ATTEMPT_WAIT_MS 内关键响应收不齐 → 自动重走"首页→站内跳转"(刷新等价), +// 最多 MAX_ATTEMPTS 轮;提前收齐即收工。 + +const WS_URL = "ws://127.0.0.1:39272/"; +const WS_TOKEN = "hexi-bf2042"; // 仅本机回环 + 共享 token;修改时与 bot 侧配置同步 +const HOMEPAGE = "https://tracker.gg/"; +const CAPTURE_PREFIX = + "https://api.tracker.gg/api/v2/bf2042/standard/profile/"; +const JOB_KEYS = ["profile", "weapon", "vehicle"]; // 齐了即收工(soldier 可选) +const SETTLE_MS = 3000; // 首页落稳再跳,镜像 python 通道的 sleep(3) +const JOB_TIMEOUT_MS = 150000; +// 重试:单轮 X 秒内关键数据没齐 = 多半撞了 CF 挑战/403,重走"首页→站内跳转"刷新 +const ATTEMPT_WAIT_MS = 22000; +const MAX_ATTEMPTS = 3; + +let ws = null; +let retryMs = 1000; +let job = null; // { id, base, tabId, keys:Set, attempts, timer } + +const log = (...a) => console.log("[bf2042-bridge]", ...a); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +// ------------------------------------------------------------------ WS +function connect() { + if (ws) return; // CONNECTING / OPEN 状态都不重复建 + try { + ws = new WebSocket(`${WS_URL}?token=${encodeURIComponent(WS_TOKEN)}`); + } catch (_) { + scheduleReconnect(); + return; + } + ws.onopen = () => { + retryMs = 1000; + log("已连接 bot"); + send({ type: "hello", name: "hexi-bf2042-bridge", version: "0.1.0" }); + }; + ws.onmessage = (ev) => { + try { + handleMessage(JSON.parse(ev.data)); + } catch (_) {} + }; + ws.onclose = () => scheduleReconnect(); + ws.onerror = () => { + try { + ws.close(); + } catch (_) {} + }; +} + +function scheduleReconnect() { + ws = null; + setTimeout(connect, retryMs); + retryMs = Math.min(retryMs * 2, 30000); // 1s → 2s → ... → 上限 30s +} + +function send(obj) { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(obj)); + return true; + } + return false; +} + +// 心跳:兼作 MV3 SW 保活(Chromium 116+ 活跃 WS 本就会保活,alarms 双保险, +// 覆盖"bot 重启导致断线、SW 又在重连定时器前被回收"的窗口)。 +chrome.alarms.create("keepalive", { periodInMinutes: 0.5 }); +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name !== "keepalive") return; + if (!ws) connect(); + else send({ type: "ping" }); +}); + +connect(); + +// ------------------------------------------------------------------ 工具 +function norm(url) { + return String(url || "").split("?")[0].replace(/\/+$/, ""); +} +// 两边都 decode 再比,消除 python quote() 与 JS encodeURIComponent 的转义差异 +function canon(url) { + try { + return decodeURIComponent(norm(url)); + } catch (_) { + return norm(url); + } +} +function isTracker(url) { + return /^https:\/\/(www\.)?tracker\.gg\//.test(String(url || "")); +} +function keyOf(url) { + const u = norm(url); + for (const k of ["weapon", "vehicle", "soldier", "gadget"]) { + if (u.endsWith(`/segments/${k}`)) return k; + } + return "profile"; // profile 基础响应(gamemode 等段都在它里面) +} + +// ------------------------------------------------------------------ 任务 +function handleMessage(msg) { + if (!msg || typeof msg !== "object") return; + if (msg.type === "fetch") startJob(msg).catch(() => {}); +} + +async function startJob(msg) { + if (job) return send({ type: "job_error", id: msg.id, detail: "busy" }); + if (!msg.profileUrl || !msg.apiBase) { + return send({ type: "job_error", id: msg.id, detail: "bad_request" }); + } + job = { + id: msg.id, + base: canon(msg.apiBase), + tabId: null, + keys: new Set(), + attempts: 0, + timer: setTimeout(() => finishJob("timeout"), JOB_TIMEOUT_MS), + }; + try { + const tab = await ensureTab(); + job.tabId = tab.id; + await chrome.tabs.update(tab.id, { active: true }); + + for (let attempt = 1; attempt <= MAX_ATTEMPTS && job; attempt++) { + job.attempts = attempt; + // 严格镜像已验证路径:每轮都先落首页,停顿,再站内跳转 + try { + await chrome.tabs.update(tab.id, { url: HOMEPAGE }); + await waitComplete(tab.id); + } catch (_) { + break; // 标签页没了 + } + await sleep(SETTLE_MS); + log(`第 ${attempt}/${MAX_ATTEMPTS} 轮,站内跳转:`, msg.profileUrl); + // setTimeout 0:让注入脚本先结束再触发跳转,避免 "frame detached" 噪声 + chrome.scripting + .executeScript({ + target: { tabId: tab.id }, + world: "MAIN", + func: (u) => { + setTimeout(() => { + window.location.href = u; + }, 0); + }, + args: [msg.profileUrl], + }) + .catch(() => {}); + + const ok = await waitForKeys(tab.id, ATTEMPT_WAIT_MS); + if (!job) return; // 期间已收工/超时 + if (ok) return finishJob("done"); + + const health = await pageHealth(tab.id); + log(`第 ${attempt} 轮未收齐(${health}),${attempt < MAX_ATTEMPTS ? "刷新重试" : "放弃"}`); + } + if (job) finishJob("no_data", "多轮重试后仍未捕获到数据"); + } catch (e) { + finishJob("navigate_failed", String(e)); + } +} + +// 等待关键响应齐(或任务被外部收工);返回是否收齐 +function waitForKeys(tabId, timeoutMs) { + return new Promise((resolve) => { + const t0 = Date.now(); + const timer = setInterval(() => { + if (!job) { + clearInterval(timer); + resolve(false); + return; + } + if (JOB_KEYS.every((k) => job.keys.has(k))) { + clearInterval(timer); + resolve(true); + return; + } + if (Date.now() - t0 >= timeoutMs) { + clearInterval(timer); + resolve(false); + } + }, 500); + }); +} + +// 页面健康检查:区分"CF 挑战/封禁"与"页面正常但没数据"(仅供日志) +async function pageHealth(tabId) { + try { + const [res] = await chrome.scripting.executeScript({ + target: { tabId }, + func: () => { + const t = (document.title || "") + " " + (document.body?.innerText || "").slice(0, 300); + return t.slice(0, 200); + }, + }); + const t = String(res?.result || ""); + if (/just a moment|安全验证|正在进行|verify you are human/i.test(t)) return "CF挑战页"; + if (/you'?ve been blocked|been blocked/i.test(t)) return "TRN封禁页"; + if (/username sign in|sign in with your tracker/i.test(t)) return "登录过期"; + return "页面无挑战"; + } catch (_) { + return "健康检查失败(标签页可能已关闭)"; + } +} + +async function ensureTab() { + const tabs = await chrome.tabs.query({ + url: ["https://tracker.gg/*", "https://*.tracker.gg/*"], + }); + if (tabs.length) return tabs[0]; + const tab = await chrome.tabs.create({ url: HOMEPAGE, active: true }); + await waitComplete(tab.id); + return tab; +} + +function waitComplete(tabId, timeoutMs = 30000) { + return new Promise((resolve) => { + const done = () => { + clearTimeout(timer); + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + }; + const timer = setTimeout(done, timeoutMs); + const listener = (id, info) => { + if (id === tabId && info.status === "complete") done(); + }; + chrome.tabs.onUpdated.addListener(listener); + chrome.tabs + .get(tabId) + .then((t) => { + if (t.status === "complete") done(); + }) + .catch(done); + }); +} + +function finishJob(reason, detail) { + if (!job) return; + clearTimeout(job.timer); + const { id, keys } = job; + job = null; + if (reason === "done") { + send({ type: "job_done", id, keys: [...keys] }); + } else { + send({ type: "job_error", id, keys: [...keys], detail: detail || reason }); + } +} + +// ------------------------------------------------------------------ 捕获回传 +chrome.runtime.onMessage.addListener((msg, sender) => { + if (!msg || msg.type !== "captured") return; + const url = String(msg.url || ""); + if (!url.startsWith(CAPTURE_PREFIX)) return; + const key = keyOf(url); + + if (job) { + // 只认我们驱动的那个标签页,避免把用户同时手动浏览的流量掺进来 + if (job.tabId != null && sender.tab && sender.tab.id !== job.tabId) return; + if (!canon(url).startsWith(job.base)) return; // 不是这次任务那位玩家的 + job.keys.add(key); + send({ + type: "capture", + id: job.id, + key, + url, + status: msg.status, + body: msg.body, + }); + if (JOB_KEYS.every((k) => job.keys.has(k))) finishJob("done"); + return; + } + + // 无任务:被动转发(id 为 null),人工浏览也顺手喂数据/便于联调 + send({ type: "capture", id: null, key, url, status: msg.status, body: msg.body }); +}); diff --git a/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/content.js b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/content.js new file mode 100644 index 0000000..575f370 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/content.js @@ -0,0 +1,25 @@ +// 隔离世界桥(document_start):把主世界钩子 postMessage 上来的捕获转发给 SW。 +// +// WS 由 SW 持有 —— 内容脚本直接连 ws://127.0.0.1 会被页面 CSP 的 connect-src 拦掉。 + +(() => { + "use strict"; + + const MARKER = "__hexi_bf2042_capture__"; + + window.addEventListener("message", (event) => { + if (event.source !== window) return; + const data = event.data; + if (!data || data[MARKER] !== true) return; + try { + chrome.runtime.sendMessage({ + type: "captured", + url: String(data.url || ""), + status: data.status, + body: data.body, + }); + } catch (_) { + // SW 正在重启等场景,丢弃即可(数据可重查,不值得重试) + } + }); +})(); diff --git a/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/inject.js b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/inject.js new file mode 100644 index 0000000..ccb730b --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/inject.js @@ -0,0 +1,102 @@ +// 主世界钩子(document_start):被动观察页面自己发出的请求,把命中的响应体副本 +// postMessage 给 content.js。参照 FetchV / 猫抓一类捕获扩展的通行模式。 +// +// 铁律:只观察,不改写请求与响应;任何异常都必须静默,绝不能影响页面本身。 + +(() => { + "use strict"; + + const MARKER = "__hexi_bf2042_capture__"; + const API_PREFIX = + "https://api.tracker.gg/api/v2/bf2042/standard/profile/"; + + function match(url) { + return typeof url === "string" && url.startsWith(API_PREFIX); + } + + function report(url, status, body) { + try { + window.postMessage({ [MARKER]: true, url, status, body }, "*"); + } catch (_) { + /* 忽略:上报失败不能影响页面 */ + } + } + + // ---- fetch ---- + // 用 Proxy 包一层:Function.prototype.toString.call() 显示为 native + // (Proxy 的 target 是原生函数),比"改写后补 toString"更难被识破。 + if (typeof window.fetch === "function") { + const orig = window.fetch; + window.fetch = new Proxy(orig, { + apply(target, thisArg, args) { + const input = args[0]; + const url = + typeof input === "string" ? input : (input && input.url) || ""; + const result = Reflect.apply(target, thisArg, args); + if (match(url)) { + result + .then((resp) => { + try { + const ctype = resp.headers.get("content-type") || ""; + if (!ctype.includes("json")) return; + // 立刻 clone,免得页面随后消费了原响应体 + resp + .clone() + .json() + .then((body) => report(url, resp.status, body)) + .catch(() => {}); + } catch (_) {} + }) + .catch(() => {}); + } + return result; + }, + }); + } + + // ---- XMLHttpRequest ---- + const XHR = window.XMLHttpRequest; + if (XHR && XHR.prototype) { + const origOpen = XHR.prototype.open; + const origSend = XHR.prototype.send; + const urls = new WeakMap(); // 挂在实例上会留可见的自定义属性,用 WeakMap 更干净 + + XHR.prototype.open = new Proxy(origOpen, { + apply(target, thisArg, args) { + try { + urls.set(thisArg, String(args[1] ?? "")); + } catch (_) {} + return Reflect.apply(target, thisArg, args); + }, + }); + + XHR.prototype.send = new Proxy(origSend, { + apply(target, thisArg, args) { + try { + const url = urls.get(thisArg); + if (match(url)) { + thisArg.addEventListener( + "loadend", + () => { + try { + const rt = thisArg.responseType; + let body = null; + if (rt === "" || rt === "text") { + body = JSON.parse(thisArg.responseText); + } else if (rt === "json") { + body = thisArg.response; + } else { + return; // arraybuffer / blob 等类型不处理 + } + if (body != null) report(url, thisArg.status, body); + } catch (_) {} + }, + { once: true } + ); + } + } catch (_) {} + return Reflect.apply(target, thisArg, args); + }, + }); + } +})(); diff --git a/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/manifest.json b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/manifest.json new file mode 100644 index 0000000..fad378f --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/browser_ext/manifest.json @@ -0,0 +1,31 @@ +{ + "manifest_version": 3, + "name": "HeXi BF2042 捕获桥", + "version": "0.1.0", + "description": "被动捕获 tracker.gg 的 BF2042 响应并经 WebSocket 转发给本机禾汐 bot。浏览器无需开调试端口。", + "minimum_chrome_version": "116", + "permissions": ["tabs", "scripting", "alarms"], + "host_permissions": [ + "https://tracker.gg/*", + "https://*.tracker.gg/*", + "http://127.0.0.1/*" + ], + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": ["https://tracker.gg/*", "https://*.tracker.gg/*"], + "js": ["inject.js"], + "run_at": "document_start", + "world": "MAIN", + "all_frames": true + }, + { + "matches": ["https://tracker.gg/*", "https://*.tracker.gg/*"], + "js": ["content.js"], + "run_at": "document_start", + "all_frames": true + } + ] +} diff --git a/hexi/plugins/nonebot_plugin_bf_bot/config.py b/hexi/plugins/nonebot_plugin_bf_bot/config.py new file mode 100644 index 0000000..060cb6c --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/config.py @@ -0,0 +1,123 @@ +"""配置:按《插件配置文件标准》注册到统一 Web 管理台。 + +3 个配置项都与 BF2042 取数通道有关:浏览器扩展桥的端口 / token +(``services/fetchers/tracker_bridge.py``),以及取数浏览器专用配置目录 +(登录态所在,诊断提示用)。来源是模块运行态,Web 保存后由 setter 热更新、 +立即生效;重启后依次按 plugin_config.json 值、`HEXI_BF_*` 环境变量、默认值回退。 + +读取一律走本模块的 helper(内部 `get_effective_value`),不要在别处缓存值。 +""" + +from __future__ import annotations + +import os + +from hexi.web_hub.config_standard import register_config_items +from hexi.web_hub.web_config import get_effective_value + +from .paths import DEFAULT_COOKIES_PATH, EDGE_PROFILE_DIR + +_PLUGIN_ID = __package__ or __name__.rsplit(".", 1)[0] + +_KEY_EDGE_PROFILE = "bf_edge_profile_dir" +_KEY_EXT_WS_PORT = "bf_ext_ws_port" +_KEY_EXT_WS_TOKEN = "bf_ext_ws_token" +# 旧 cookie 通道的配置键:仅保留读取 helper 兼容旧调用,不再注册 +_KEY_COOKIES = "bf_tracker_cookies_path" + + +# 运行态现值:启动时用环境变量兜底初始化(ENV 名与 Web 写回的 .env 名一致) +_state: dict[str, object] = { + _KEY_EDGE_PROFILE: os.environ.get("HEXI_BF_EDGE_PROFILE") or str(EDGE_PROFILE_DIR), + _KEY_EXT_WS_PORT: os.environ.get("HEXI_BF_EXT_WS_PORT") or "39272", + _KEY_EXT_WS_TOKEN: os.environ.get("HEXI_BF_EXT_WS_TOKEN") or "hexi-bf2042", + _KEY_COOKIES: os.environ.get("HEXI_BF_TRACKER_COOKIES") + or str(DEFAULT_COOKIES_PATH), +} + + +def edge_profile_dir() -> str: + """BF2042 取数浏览器专用配置目录(内含登录态、CF 信任与捕获桥扩展)。""" + value = get_effective_value(_PLUGIN_ID, _KEY_EDGE_PROFILE, None) + if value: + return str(value) + return str(_state[_KEY_EDGE_PROFILE] or EDGE_PROFILE_DIR) + + +def ext_ws_port() -> int: + """扩展桥 WS 服务端端口(仅本机回环)。""" + value = get_effective_value(_PLUGIN_ID, _KEY_EXT_WS_PORT, None) + if value is None: + value = _state[_KEY_EXT_WS_PORT] + try: + return int(str(value)) + except (TypeError, ValueError): + return 39272 + + +def ext_ws_token() -> str: + """扩展桥共享 token(扩展 background.js 顶部常量需与之保持一致)。""" + value = get_effective_value(_PLUGIN_ID, _KEY_EXT_WS_TOKEN, None) + if value: + return str(value) + return str(_state[_KEY_EXT_WS_TOKEN] or "hexi-bf2042") + + +def tracker_cookies_path() -> str: + """[旧通道兼容] Netscape 格式 cookie 文件路径。""" + value = get_effective_value(_PLUGIN_ID, _KEY_COOKIES, None) + if value: + return str(value) + return str(_state[_KEY_COOKIES] or DEFAULT_COOKIES_PATH) + + +def _set(key: str, value: object) -> None: + _state[key] = value + + +def register_config() -> None: + """注册 3 个取数通道配置项(Web 可读可改,保存即生效)。""" + register_config_items( + _PLUGIN_ID, + [ + { + "key": _KEY_EXT_WS_PORT, + "label": "扩展桥 WS 端口", + "type": "string", + "default": "39272", + "env": "HEXI_BF_EXT_WS_PORT", + "description": ( + "扩展桥在本机 127.0.0.1 的监听端口(仅回环)。" + "改后需重启 bot,且扩展 background.js 顶部常量要同步。" + ), + "getter": lambda: _state[_KEY_EXT_WS_PORT], + "setter": lambda v: _set(_KEY_EXT_WS_PORT, v), + }, + { + "key": _KEY_EXT_WS_TOKEN, + "label": "扩展桥 token", + "type": "password", + "default": "hexi-bf2042", + "env": "HEXI_BF_EXT_WS_TOKEN", + "description": ( + "扩展连接时的共享密钥(防本机其它程序误连)。" + "与扩展 background.js 顶部 WS_TOKEN 保持一致;保存即生效。" + ), + "getter": lambda: _state[_KEY_EXT_WS_TOKEN], + "setter": lambda v: _set(_KEY_EXT_WS_TOKEN, v), + }, + { + "key": _KEY_EDGE_PROFILE, + "label": "取数浏览器配置目录", + "type": "path", + "default": str(EDGE_PROFILE_DIR), + "env": "HEXI_BF_EDGE_PROFILE", + "description": ( + "取数浏览器的专用用户数据目录(登录态 + CF 信任 + 捕获桥扩展都在这)。" + "会话过期时用它打开 Edge 手动登录一次即可。" + ), + "getter": lambda: _state[_KEY_EDGE_PROFILE], + "setter": lambda v: _set(_KEY_EDGE_PROFILE, v), + }, + ], + ) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/data.py b/hexi/plugins/nonebot_plugin_bf_bot/data.py deleted file mode 100644 index 6d8a9e7..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/data.py +++ /dev/null @@ -1,284 +0,0 @@ -import httpx -import re -from typing import Any, Dict, Optional -from .get_bf6_data import * -from .tracker_data import * - - -async def get_data_bf3(player, player_id, user_id, platform): - url = f"https://api.gametools.network/bf3/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us" - url = f"https://api.gametools.network/bf3/all/?format_values=true&name={player}&platform={platform}&skip_battlelog=false&lang=en-us" - async with httpx.AsyncClient(timeout=10.0) as client: - # 创建两个并发任务 - task_id = asyncio.create_task(client.get(url_by_id)) - task_name = asyncio.create_task(client.get(url_by_name)) - - # 等待第一个完成的任务 - done, pending = await asyncio.wait( - [task_id, task_name], - return_when=asyncio.FIRST_COMPLETED - ) - # 取消未完成的任务 - for task in pending: - task.cancel() - # 检查已完成的任务是否成功且无 errors - for task in done: - try: - response = await task - if response.status_code == 200: - data = response.json() - if 'errors' not in data: - logger.info(f"成功获取数据({'ID' if task is task_id else 'Name'}方式)") - return data - except (httpx.RequestError, ValueError) as e: - logger.warning(f"请求或解析失败: {e}") - if pending: - pass - # 如果所有尝试都失败,返回错误 - return {"errors": ["未找到玩家"]} - - -async def get_data_bf4(player, player_id, user_id, platform): - url_by_id = f"https://api.gametools.network/bf4/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us" - url_by_name = f"https://api.gametools.network/bf4/all/?format_values=true&name={player}&platform={platform}&skip_battlelog=false&lang=en-us" - async with httpx.AsyncClient(timeout=10.0) as client: - # 创建两个并发任务 - task_id = asyncio.create_task(client.get(url_by_id)) - task_name = asyncio.create_task(client.get(url_by_name)) - - # 等待第一个完成的任务 - done, pending = await asyncio.wait( - [task_id, task_name], - return_when=asyncio.FIRST_COMPLETED - ) - # 取消未完成的任务 - for task in pending: - task.cancel() - # 检查已完成的任务是否成功且无 errors - for task in done: - try: - response = await task - if response.status_code == 200: - data = response.json() - if 'errors' not in data: - logger.info(f"成功获取数据({'ID' if task is task_id else 'Name'}方式)") - return data - except (httpx.RequestError, ValueError) as e: - logger.warning(f"请求或解析失败: {e}") - if pending: - pass - # 如果所有尝试都失败,返回错误 - return {"errors": ["未找到玩家"]} - - -async def get_data_bf1(player, player_id, user_id, platform): - url_by_id = f"https://api.gametools.network/bf1/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us" - url_by_name = f"https://api.gametools.network/bf1/all/?format_values=true&name={player}&platform={platform}&skip_battlelog=false&lang=en-us" - async with httpx.AsyncClient(timeout=10.0) as client: - # 创建两个并发任务 - task_id = asyncio.create_task(client.get(url_by_id)) - task_name = asyncio.create_task(client.get(url_by_name)) - - # 等待第一个完成的任务 - done, pending = await asyncio.wait( - [task_id, task_name], - return_when=asyncio.FIRST_COMPLETED - ) - # 取消未完成的任务 - for task in pending: - task.cancel() - # 检查已完成的任务是否成功且无 errors - for task in done: - try: - response = await task - if response.status_code == 200: - data = response.json() - if 'errors' not in data: - logger.info(f"成功获取数据({'ID' if task is task_id else 'Name'}方式)") - return data - except (httpx.RequestError, ValueError) as e: - logger.warning(f"请求或解析失败: {e}") - if pending: - pass - # 如果所有尝试都失败,返回错误 - return {"errors": ["未找到玩家"]} - - -async def get_data_bfv(player, player_id, user_id, platform): - url_by_id = f"https://api.gametools.network/bfv/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us" - url_by_name = f"https://api.gametools.network/bfv/all/?format_values=true&name={player}&platform={platform}&skip_battlelog=false&lang=en-us" - - async with httpx.AsyncClient(timeout=10.0) as client: - # 创建两个并发任务 - task_id = asyncio.create_task(client.get(url_by_id)) - task_name = asyncio.create_task(client.get(url_by_name)) - - # 等待第一个完成的任务 - done, pending = await asyncio.wait( - [task_id, task_name], - return_when=asyncio.FIRST_COMPLETED - ) - # 取消未完成的任务 - for task in pending: - task.cancel() - # 检查已完成的任务是否成功且无 errors - for task in done: - try: - response = await task - if response.status_code == 200: - data = response.json() - if 'errors' not in data: - logger.info(f"成功获取数据({'ID' if task is task_id else 'Name'}方式)") - return data - except (httpx.RequestError, ValueError) as e: - logger.warning(f"请求或解析失败: {e}") - if pending: - pass - # 如果所有尝试都失败,返回错误 - return {"errors": ["未找到玩家"]} - - -async def get_data_bf6(player_name, player_id, user_id, platform): - # 构造两个 URL - url_by_id = f"https://api.gametools.network/bf6/all/?format_values=true&playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false&lang=en-us" - url_by_name = f"https://api.gametools.network/bf6/all/?format_values=true&name={player_name}&platform={platform}&skip_battlelog=false&lang=en-us" - - async with httpx.AsyncClient(timeout=10.0) as client: - # 创建两个并发任务 - task_id = asyncio.create_task(client.get(url_by_id)) - task_name = asyncio.create_task(client.get(url_by_name)) - - # 等待第一个完成的任务 - done, pending = await asyncio.wait( - [task_id, task_name], - return_when=asyncio.FIRST_COMPLETED - ) - # 取消未完成的任务 - for task in pending: - task.cancel() - # 检查已完成的任务是否成功且无 errors - for task in done: - try: - response = await task - if response.status_code == 200: - data = response.json() - if 'errors' not in data: - logger.info(f"成功获取数据({'ID' if task is task_id else 'Name'}方式)") - return data - except (httpx.RequestError, ValueError) as e: - logger.warning(f"请求或解析失败: {e}") - if pending: - pass - # 如果所有尝试都失败,返回错误 - return {"errors": ["未找到玩家"]} - - -async def get_player_info_by_name(user_name, platform): - user_info_url = f"https://api.gametools.network/bfglobal/player/?name={user_name}&platform={platform}&skip_battlelog=false" - - payload = {} - headers = { - 'accept': 'application/json' - } - logger.info(f"查询用户请求url:{user_info_url}") - response = requests.request("GET", user_info_url, headers=headers, data=payload) - - user_info_json = json.loads(response.text) - return user_info_json - - -async def get_player_info_by_ea_id(player_id, user_id, platform): - url = f"https://api.gametools.network/bfglobal/player/?playerid={player_id}&oid={user_id}&platform={platform}&skip_battlelog=false" - - payload = {} - headers = { - 'accept': 'application/json' - } - - response = requests.request("GET", url, headers=headers, data=payload) - - user_info_json = json.loads(response.text) - return user_info_json - - -def normalize_name(name: str) -> str: - """统一用户名格式(大小写 + 空格)""" - return (name or "").strip().lower() - - -def extract_rank(status: str) -> int: - """从 status 中提取 Rank 数值""" - if not status: - return 0 - match = re.search(r'Rank\s+(\d+)', status) - return int(match.group(1)) if match else 0 - -async def get_bf6_rank(player,player_id, user_id, platform): - url_by_name = f"https://api.gametools.network/bf6/profile/?name={player}&playerid=1234&nucleus_id=1234&platform=ea&skip_battlelog=true&lang=en-us" - url_by_id = f"https://api.gametools.network/bf6/profile/?name={player}&playerid=1234&nucleus_id=1234&platform=ea&skip_battlelog=true&lang=en-us" - payload = {} - headers = { - 'accept': 'application/json' - } - - response = requests.request("GET", url, headers=headers, data=payload) - result: Dict[str, Any] = response.json().get("playerProfiles") - logger.info(f"Tracker用户信息:{result}") - - if not result or "errors" in result: - logger.warning(f"查询失败或返回错误: {player}") - return 0 - - data = result.get("data") or [] - if not data: - logger.info(f"未搜索到用户: {player}") - return 0 - - norm_player = normalize_name(player) - - # 评分函数(核心策略) - def score(r: Dict[str, Any]) -> int: - if not r or not r.get("status"): - return -1 - - name = normalize_name(r.get("platformUserHandle")) - platform = r.get("platformSlug") - - s = 0 - - # 用户名匹配权重(最高优先级) - if name == norm_player: - s += 100 - elif norm_player in name: - s += 50 - - # 平台权重(次优先级) - if platform == "origin": - s += 20 - - return s - - # 选出最优 candidate - target: Optional[Dict[str, Any]] = max(data, key=score, default=None) - - if not target or score(target) <= 0: - logger.info(f"未匹配到有效用户: {player}") - return 0 - - # 提取信息 - name = target.get("platformUserHandle") - uid = target.get("titleUserId") - status = target.get("status", "") - - rank = extract_rank(status) - - user_info = { - "name": name, - "uid": uid, - "rank": rank, - "platform": target.get("platformSlug"), - } - - logger.info(f"最终匹配用户: {user_info}") - - return rank diff --git a/hexi/plugins/nonebot_plugin_bf_bot/data_utils.py b/hexi/plugins/nonebot_plugin_bf_bot/data_utils.py deleted file mode 100644 index 0be661a..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/data_utils.py +++ /dev/null @@ -1,151 +0,0 @@ -import os -from functools import reduce - -from PIL import Image, ImageDraw, ImageFont -from nonebot import logger - -# from img_utils import png_resize -# from param import round_data, interval_table -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_best_classes(player_stat): - classes_data = player_stat['classes'] - data = [obj for obj in classes_data if obj.get("className") != "All"] - best_classes = max(data, key=lambda x: x["kills"]) - best_classes_name = best_classes["className"] - return best_classes_name - - -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/hexi/plugins/nonebot_plugin_bf_bot/database_op.py b/hexi/plugins/nonebot_plugin_bf_bot/database_op.py deleted file mode 100644 index a00de79..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/database_op.py +++ /dev/null @@ -1,4 +0,0 @@ - - -def bind_user(user_info): - user_info[''] diff --git a/hexi/plugins/nonebot_plugin_bf_bot/gametools_bf6.py b/hexi/plugins/nonebot_plugin_bf_bot/gametools_bf6.py deleted file mode 100644 index 2218110..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/gametools_bf6.py +++ /dev/null @@ -1,67 +0,0 @@ -import json - -import requests - - -async def get_data_bf6(player_id, user_id, platform): - url = "https://api.gametools.network/bf6/multiple/?raw=false&format_values=true" - payload = json.dumps([ - { - "player_id": player_id, - "user_id": user_id, - "platform": platform - } - ]) - - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json' - } - - response = requests.request("POST", url, headers=headers, data=payload) - info = json.loads(response.text) - return info - - -async def get_data_user(user_name, platform): - url = f"https://api.gametools.network/bfglobal/player/?name={user_name}&platform={platform}&skip_battlelog=false" - - payload = {} - headers = { - 'accept': 'application/json' - } - - response = requests.request("GET", url, headers=headers, data=payload) - data_json = json.loads(response.text) - - player_id = data_json["personaId"] - user_id = data_json["userId"] - platform = data_json["platform"] - if 'personaName' in data_json: - user_name = data_json["personaName"] - elif 'displayName' in data_json: - user_name = data_json["displayName"] - elif 'username' in data_json: - user_name = data_json["username"] - else: - data_json = 'errors' - - if "errors" in data_json: - return False, data_json - - user_info = { - "player_id": player_id, - "user_id": user_id, - "platform": platform, - "user_name": user_name - } - - return True, user_info - - -async def get_player_game_info(user_name, platform): - flag, user_info = await get_data_user(user_name, platform) - info = await get_data_bf6(user_info['player_id'], user_info['user_id'], user_info['platform']) - info['userName'] = user_name - info_format = json.dumps(info, ensure_ascii=False, indent=2) - print(info_format) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/get_bf6_data.py b/hexi/plugins/nonebot_plugin_bf_bot/get_bf6_data.py deleted file mode 100644 index 2d0daee..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/get_bf6_data.py +++ /dev/null @@ -1,271 +0,0 @@ -from nonebot import logger -import json - -# from bf6_data import * -import requests -from curl_cffi import AsyncSession, CurlError -from nonebot import logger - -from .bf6_data import * - -url_search = "https://api.tracker.gg/api/v2/bf6/standard/search?platform={platform}&query={name}&autocomplete=true" -url_overview = "https://api.tracker.gg/api/v2/bf6/standard/profile/ign/{param}" - - -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.info(f"查询结果: \n{json.dumps(result, ensure_ascii=False, indent=2)}") - if "status" in result or "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: int(k['rank']), reverse=True) - msg = '\n'.join(f'用户名:{info["name"]}-等级:{info["rank"]}-UID:{info["uid"]}' for info in user_list) - logger.info(f"检测到多个同名用户\n{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) - logger.info(f"查询结果: {json.dumps(info, ensure_ascii=False, indent=2)}") - 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 - - -async def get_bf6_data(player_id, user_id, platform): - - url = "https://api.gametools.network/bf6/multiple/?raw=false&format_values=true" - - payload = json.dumps([ - { - "player_id": player_id, - "user_id": user_id, - "platform": platform - } - ]) - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json' - } - - response = requests.request("POST", url, headers=headers, data=payload) - - player_stat_json = json.loads(response.text) - - return player_stat_json diff --git a/hexi/plugins/nonebot_plugin_bf_bot/handlers/__init__.py b/hexi/plugins/nonebot_plugin_bf_bot/handlers/__init__.py new file mode 100644 index 0000000..b7bdf0c --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/handlers/__init__.py @@ -0,0 +1,6 @@ +"""Trigger 层:命令注册与消息发送(接触发 → 调 service → 出 View)。""" + +from . import binding as _binding # noqa: F401 +from . import query as _query # noqa: F401 + +__all__ = ["binding", "query"] diff --git a/hexi/plugins/nonebot_plugin_bf_bot/handlers/binding.py b/hexi/plugins/nonebot_plugin_bf_bot/handlers/binding.py new file mode 100644 index 0000000..c1b4c53 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/handlers/binding.py @@ -0,0 +1,39 @@ +"""绑定管理命令:/bfbind(绑定) /unbind(解绑) /update_bind(修改绑定)。""" + +from nonebot import on_command +from nonebot.adapters import Event, Message +from nonebot.params import CommandArg +from nonebot.rule import to_me +from nonebot_plugin_alconna import UniMessage + +from ..services import binding as binding_service + +bind_cmd = on_command("bfbind", rule=to_me(), aliases={"绑定"}, block=True) +unbind_cmd = on_command("unbind", rule=to_me(), aliases={"解绑"}, block=True) +update_bind_cmd = on_command( + "update_bind", rule=to_me(), aliases={"修改绑定"}, block=True +) + + +async def _reply(event: Event, text: str) -> None: + await UniMessage.at(user_id=event.get_user_id()).text(text).send() + + +@bind_cmd.handle() +async def handle_bind(event: Event, msg: Message = CommandArg()): + text = await binding_service.bind(event.get_user_id(), msg.extract_plain_text()) + await _reply(event, text) + + +@unbind_cmd.handle() +async def handle_unbind(event: Event): + text = await binding_service.unbind(event.get_user_id()) + await _reply(event, text) + + +@update_bind_cmd.handle() +async def handle_update_bind(event: Event, msg: Message = CommandArg()): + text = await binding_service.update_bind( + event.get_user_id(), msg.extract_plain_text() + ) + await _reply(event, text) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/handlers/query.py b/hexi/plugins/nonebot_plugin_bf_bot/handlers/query.py new file mode 100644 index 0000000..63f06da --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/handlers/query.py @@ -0,0 +1,54 @@ +"""`/bft` 及全部别名:命令注册 + 结果发送。""" + +from nonebot import on_command +from nonebot.adapters import Event, Message +from nonebot.log import logger +from nonebot.matcher import Matcher +from nonebot.params import CommandArg +from nonebot.rule import to_me +from nonebot_plugin_alconna import UniMessage + +from hexi.core.cooldown import cooldown + +from ..services.query import run_query + +# 群冷却 10s(全群共享,私聊退化为按用户);显式 name 保证热重载后 CD 不重置 +query = cooldown( + 10, + scope="group", + name="bf_query", + hint="查询太频繁了,{secs} 秒后再试", +)( + on_command( + "bft", + rule=to_me(), + aliases={"bf3", "bf4", "bfv", "bf1", "bf2042", "bf6"}, + block=True, + ) +) + + +@query.handle() +async def handle_query(matcher: Matcher, event: Event, msg: Message = CommandArg()): + # 实际输入的游戏别名(去掉 COMMAND_START 后的首段)决定走哪个游戏分支 + prefix = matcher.state.get("_prefix") or {} + cmd = (prefix.get("command") or ["bft"])[0] + input_text = msg.extract_plain_text().strip() + user_id = event.get_user_id() + logger.info(f"BF查询:cmd={cmd!r}, input={input_text!r}") + + async def notify(text: str) -> None: + await UniMessage.text(text).send() + + reply = await run_query( + cmd=cmd, input_text=input_text, qq_id=user_id, notify=notify + ) + if reply.image is not None: + await ( + UniMessage.image(raw=reply.image) + .at(user_id=user_id) + .text(reply.text) + .send() + ) + else: + await UniMessage.at(user_id=user_id).text(reply.text).send() diff --git a/hexi/plugins/nonebot_plugin_bf_bot/image_builder.py b/hexi/plugins/nonebot_plugin_bf_bot/image_builder.py deleted file mode 100644 index d5f32f9..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/image_builder.py +++ /dev/null @@ -1,592 +0,0 @@ -import datetime -import io -import re - -import cv2 -import numpy as np -from PIL import Image, ImageDraw, ImageFont - -from .data_utils import * -from .img_utils import * -from .param import * - -# from img_utils import * -# from data_utils import * -# from param import * - -filepath = os.path.dirname(__file__).replace("\\", "/") - -# 字体 -font_XXL = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 72) -font_XL = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 64) -font_L = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 48) -font_ML = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 42) -font_M = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 36) -font_MS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 32) -font_S = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 28) -font_XS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 24) -font_XXS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 18) -font_XXXS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 16) - -font_XXL_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 72) -font_XL_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 64) -font_L_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 48) -font_ML_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 42) -font_M_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 36) -font_MS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 32) -font_S_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28) -font_XS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 24) -font_XXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18) -font_XXXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 16) -font_S_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28) -font_XS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 20) -font_XXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18) -font_XXXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 16) - - -async def build_stats_card(game="bfv", player="Player001", pid=1145141919, kd=1.25, kpm=2.3, spm=500, acc=25.6, - 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_CH) - draw.text((745, 133), f"{pid}", fill="white", font=font_M_CH) - - # 等级游玩时长 - draw.text((740, 212), f"{rank}", fill="white", font=font_M_CH) - draw.text((985, 212), f"{time_play}H", fill="white", font=font_M_CH) - # draw_centered_text(draw=draw, text=rank, x_left=730, x_right=905, y=210, font=font_M, fill="white", ) - # draw_centered_text(draw=draw, text=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_CH, - fill="white") - - # 命中率 - draw_centered_text(draw=draw, text=round(float(acc.replace("%", "")), 1), x_left=415, x_right=495, y=343, - font=font_M_CH, - fill="white") - # 爆头率 - if head_shots: - head_shots = head_shots.replace("%", "") - else: - head_shots = 0 - draw_centered_text(draw=draw, text=round(float(head_shots), 1), x_left=395, x_right=470, y=420, font=font_M_CH, - fill="white") - # kd - draw_centered_text(draw=draw, text=kd, x_left=585, x_right=785, y=340, font=font_XL_CH, fill="white") - # kpm - draw_centered_text(draw=draw, text=round(kpm, 1), x_left=825, x_right=1025, y=340, font=font_XL_CH, fill="white") - # spm - draw_centered_text(draw=draw, text=round(spm, 1), x_left=1065, x_right=1265, y=340, font=font_XL_CH, fill="white") - - # 击杀 - draw_centered_text(draw=draw, text=kills, x_left=870, x_right=1190, y=595, font=font_L_CH, fill="white") - - # 助攻 - draw_centered_text(draw=draw, text=kill_assists, x_left=820, x_right=1145, y=770, font=font_L_CH, fill="white") - - # 急救 - draw_centered_text(draw=draw, text=revives, x_left=770, x_right=1090, y=960, font=font_L_CH, fill="white") - - # 摧毁 - draw_centered_text(draw=draw, text=destroyed, x_left=720, x_right=1040, y=1140, font=font_L_CH, fill="white") - - # 底部栏 - - # 评级 - if game == 'bf6': - logger.info("bf6不计算称号") - else: - level = level_designation['level'] - des = level_designation['designation'] - color = level_designation['color'] - draw_centered_text(draw=draw, text=level, x_left=70, x_right=200, y=1120, font=font_XL_CH, fill=color) - draw_centered_text(draw=draw, text=des, x_left=200, x_right=550, y=1120, font=font_XL_CH, fill="white") - - # 最佳兵种 - - draw_centered_text(draw=draw, text=classes[best_class], x_left=140, x_right=310, y=1290, font=font_L_CH, - fill="white") - - # 最远击杀 - draw_centered_text(draw=draw, text=f"{longest_head_shot}m", x_left=455, x_right=625, y=1290, font=font_L_CH, - fill="white") - - # 最高连杀 - draw_centered_text(draw=draw, text=highest_ill_streak, x_left=765, x_right=935, y=1290, font=font_L_CH, - fill="white") - - # 最佳⭐ - pil_img = build_best(draw, pil_img, best_weapon, best_vehicle, game) - - # 生成时间 - draw.text((1870, 1420), f"{formatted_time}", fill=(154, 132, 149), font=font_XXXS_CH) - - # ---------------- 转回 OpenCV ---------------- - img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) - - # ---------------- 雷达图 ---------------- - if game != "bf6": - center = (311, 796) - r = 185 - scale_min = 0.25 if min(stat_data) < 0.25 else min(stat_data) - img = draw_radar(img, stat_data, center=center, r=r, - buffer=0.1, scale_min=scale_min, scale_max=1.10) - # 保存 - success, buffer = cv2.imencode(".png", img) - if success: - b_io = io.BytesIO(buffer) - return b_io # 就可以直接 return 二进制流 - else: - return None - - -async def build_bf6_stats_card(game="bfv", - qq_id=2931, - player="Player001", - pid=1145141919, - rank=114, - kd=1.25, - kpm=2.3, - spm=500, - acc=25.6, - head_shots=10, - time_play=100, - kills=100, - kill_assists=150, - revives=114, - repairs=1000, - captured=300, - score=145674, - wins=10, - loses=10, - destroyed=514, - best_weapon="M1907 SF", - best_vehicle="坦克", - best_class="Support", ): - # 获取当前事件并且格式化 - now = datetime.datetime.now() - formatted_time = now.strftime("%Y-%m-%d %H:%M:%S") - - # ---------------- OpenCV 部分 ---------------- - # 载入模板 - img = cv2.imread(f"{filepath}/template/{game}.png", cv2.IMREAD_COLOR) - - # 载入头像并缩放 - avatar = cv2.imread(f"{filepath}/avatar_cache/0.png", cv2.IMREAD_UNCHANGED) - avatar = cv2.resize(avatar, (134, 134), interpolation=cv2.INTER_AREA) - - # 粘贴头像(保持原有坐标) - y, x = 65, 105 - h, w = avatar.shape[:2] - img[y:y + h, x:x + w] = avatar - - # 公告栏 - img = notice_paste_bf6(img) - img = designation_paste(player, img, - user_id=qq_id, - start_x=104, - start_y=215, - target_height=30, - spacing=10) - - # ---------------- PIL 部分 ---------------- - pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) - draw = ImageDraw.Draw(pil_img) - - # 玩家信息 105+160 - draw.text((265, 80), f"{player}", fill="white", font=font_XL) - draw.text((265, 140), f"UID: {pid}", fill="#CCCCCC", font=font_L) - - # 等级 - pil_img = paste_rank_icon(int(rank), pil_img) - draw_centered_text(draw=draw, text=f"{rank}", x_left=950, x_right=1075, y=205, - font=font_M, - fill="white") - # draw.text((740, 212), f"{rank}", fill="white", font=font_M) - - # 游玩时长 - draw.text((2300, 350), f"{time_play:.1f} H", fill="white", font=font_L) - # 场次 - draw.text((2300, 450), f"{wins + loses}", fill="white", font=font_L) - # draw_centered_text(draw=draw, text=(wins + loses), x_left=45, x_right=265, y=350, - # font=font_L, - # fill="white") - - # 生涯总览 - # 胜率 - - win_rate = round((wins / (wins + loses)) * 100, 1) - logger.info(f"胜场{wins},负场{loses},胜率{win_rate}") - - draw_centered_text(draw=draw, text=f"{win_rate}%", x_left=2090, x_right=2200, y=375, - font=font_XL, - fill="white") - - # 命中率 - acc_format = round(float(acc.replace("%", "")), 1) - draw_centered_text(draw=draw, text=f"{acc_format}%", x_left=1210, x_right=1303, y=363, - font=font_M, - fill="white") - # 爆头率 - if head_shots: - head_shots = head_shots.replace("%", "") - else: - head_shots = 0 - draw_centered_text(draw=draw, text=f"{round(float(head_shots), 1)}%", x_left=1210, x_right=1303, y=458, font=font_M, - fill="white") - # kd - draw.text((1350, 355), text=str(kd), fill="white", font=font_XXL) - # kpm - draw.text((1610, 355), text=f"{kpm:.2f}", fill="white", font=font_XXL) - # spm - draw.text((1870, 355), text=f"{int(spm)}", fill="white", font=font_XXL) - - # 击杀 - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1165, x_right=1455, y=805, - font=font_L, - fill="#444445") - draw_right_aligned_text(draw=draw, text=add_commas(str(kills)), x_left=1165, x_right=1455, y=805, - font=font_L, fill="white") - - # 急救 - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1165, x_right=1455, y=1080, - font=font_L, - fill="#444445") - draw_right_aligned_text(draw=draw, text=add_commas(str(revives)), x_left=1165, x_right=1455, y=1080, - font=font_L, fill="white") - - # 摧毁 - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1165, x_right=1455, y=1350, - font=font_L, - fill="#444445") - draw_right_aligned_text(draw=draw, text=add_commas(str(destroyed)), x_left=1165, x_right=1455, y=1350, - font=font_L, fill="white") - - # 右侧 - # 占领 - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=745, - font=font_L, - fill="#444445") - draw_right_aligned_text(draw=draw, text=add_commas(str(captured)), x_left=1485, x_right=1780, y=745, - font=font_L, fill="white") - - # 得分 - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=955, - font=font_L, - fill="#444445") - draw_right_aligned_text(draw=draw, text=add_commas(str(score)), x_left=1485, x_right=1780, y=955, - font=font_L, fill="white") - - # 助攻 - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=1160, - font=font_L, - fill="#444445") - draw_right_aligned_text(draw=draw, text=add_commas(str(kill_assists)), x_left=1485, x_right=1780, y=1160, - font=font_L, fill="white") - - # 修理 - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 10), x_left=1485, x_right=1780, y=1370, - font=font_L, - fill="#444445") - draw_right_aligned_text(draw=draw, text=add_commas(str(repairs)), x_left=1485, x_right=1780, y=1370, - font=font_L, fill="white") - - # 底部栏 - - # # 最佳兵种 - # logger.info(f"最佳兵种:{best_class}") - # draw_centered_text(draw=draw, text=classes[best_class], x_left=140, x_right=310, y=1290, font=font_L, fill="white") - - # 最佳⭐ - pil_img = build_best_bf6(draw, pil_img, best_weapon, best_vehicle, best_class, game) - - # 生成时间 - draw.text((1750, 10), f"{formatted_time}/战地中文社区/维护 SANSENHOSHI", fill=(154, 132, 149), font=font_XS_CH) - - # ---------------- 转回 OpenCV ---------------- - img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) - - # ---------------- 雷达图 ---------------- - - # 保存 - 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_CH, fill="white") - draw_centered_text(draw=draw, text=w_kills, x_left=1190, x_right=1390, y=1260, font=font_L_CH, fill="white") - draw_centered_text(draw=draw, text=w_kpm, x_left=1510, x_right=1635, y=1260, font=font_L_CH, fill="white") - draw_centered_text(draw=draw, text=acc, x_left=1735, x_right=1803, y=1030, font=font_M_CH, fill="white") - draw_centered_text(draw=draw, text=headshots, x_left=1695, x_right=1760, y=1200, font=font_M_CH, 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_CH, fill="white") - draw_centered_text(draw=draw, text=v_kills, x_left=1940, x_right=2150, y=1260, font=font_L_CH, fill="white") - draw_centered_text(draw=draw, text=v_kpm, x_left=2295, x_right=2465, y=1055, font=font_XL_CH, fill="white") - draw_centered_text(draw=draw, text=destroyed, x_left=2265, x_right=2440, y=1260, font=font_L_CH, fill="white") - - # 图片 - weapon_url = weapons["image"] - 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 build_best_bf6(draw: ImageDraw, img: Image, weapons, vehicle, classes, game: str): - # 最佳武器 - weapon_name = weapons["weaponName"] - w_kills = weapons["kills"] - w_kpm = weapons["killsPerMinute"] - acc = round(float(weapons["accuracy"].replace("%", "")), 1) - headshots = round(float(weapons["headshots"].replace("%", "")), 1) - - # 最佳载具 - vehicle_name = vehicle["vehicleName"] - v_kills = vehicle["kills"] - v_kpm = vehicle["killsPerMinute"] - destroyed = vehicle["destroyed"] - - # 绘制最佳⭐ - # 武器 - # draw_centered_text(draw=draw, text=bf_item[game][weapon_name].upper(), x_left=1240, x_right=1750, y=1355, - # font=font_MS, fill="white") - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 7), x_left=95, x_right=313, y=665, font=font_L, - fill="#313131") - draw_right_aligned_text(draw=draw, text=add_commas(str(w_kills)), x_left=95, x_right=313, y=665, - font=font_L, fill="white") - draw_centered_text(draw=draw, text=w_kpm, x_left=335, x_right=447, y=665, font=font_L, fill="white") - draw_centered_text(draw=draw, text=f"{acc}%", x_left=470, x_right=580, y=670, font=font_M, fill="white") - draw_centered_text(draw=draw, text=f"{headshots}%", x_left=610, x_right=717, y=670, font=font_M, fill="white") - - # 载具 - # draw_centered_text(draw=draw, text=bf_item[game][vehicle_name].upper(), x_left=1990, x_right=2440, y=1355, - # font=font_MS, fill="white") - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 7), x_left=125, x_right=325, y=1087, - font=font_L, - fill="#313131") - draw_right_aligned_text(draw=draw, text=add_commas(str(v_kills)), x_left=125, x_right=325, y=1087, - font=font_L, fill="white") - draw_centered_text(draw=draw, text=v_kpm, x_left=351, x_right=463, y=1087, font=font_L, fill="white") - draw_right_aligned_text(draw=draw, text=add_commas_with_padding("0", 7), x_left=477, x_right=688, y=1087, - font=font_L, - fill="#313131") - draw_right_aligned_text(draw=draw, text=destroyed, x_left=477, x_right=688, y=1087, font=font_L, fill="white") - - # 图片 - weapon_url = weapons["image"] - vehicle_url = vehicle["image"] - - wp_icon = get_save_icon(game, weapon_name, "weapon", weapon_url) - vc_icon = get_save_icon(game, vehicle_name, "vehicles", vehicle_url) - classes_icon = get_icon_from_cache(classes, 'bf6', 'classes') - wp_icon_new = png_resize(wp_icon, new_width=729 - 88) - vc_icon_new = png_resize(vc_icon, new_width=729 - 88) - classes_icon_new = png_resize(classes_icon, new_height=1165 - 326, new_width=1100 - 748) - - img = image_paste(wp_icon_new, img, (88, 325)) - img = image_paste(vc_icon_new, img, (88, 746)) - img = image_paste(classes_icon_new, img, (748, 326)) - - 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 add_commas(text): - return re.sub(r'(? hexi +_HEXI_ROOT = PLUGIN_ROOT.parents[1] +DATA_DIR = _HEXI_ROOT / "data" / "nonebot_plugin_bf_bot" + +DB_PATH = DATA_DIR / "user_data.db" +AVATAR_CACHE_DIR = DATA_DIR / "avatar_cache" +DEFAULT_COOKIES_PATH = DATA_DIR / "cookies" / "tracker.txt" + +# BF2042 直连通道(无浏览器)的 TRN OIDC 令牌文件:刷新链的唯一状态, +# 由 services/fetchers/tracker_api.py 读写(access_token 过期前自动轮换续期) +TRN_TOKENS_PATH = DATA_DIR / "trn_tokens.json" + +# BF2042 取数通道(tracker.gg):专用 Edge 配置目录 + 可执行文件 +EDGE_PROFILE_DIR = DATA_DIR / "edge_profile" +EDGE_EXECUTABLE_DEFAULT = ( + r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" +) + +# BF2042 卡片素材(旧 project 迁移,2026-10):背景库/物品图标/专家头像/兵种/logo +BF2042_RES = RES_DIR / "bf2042" +BF2042_BG_DIR = BF2042_RES / "bg" +BF2042_OBJECT_ICON_DIR = BF2042_RES / "object_icon" +BF2042_CLASSES_DIR = BF2042_RES / "classes" +BF2042_SPECIALIST_ICON_DIR = BF2042_RES / "specialist_icon" +BF2042_CLASS_ICON_DIR = BF2042_RES / "class_icon" +BF2042_PLATFORM_DIR = BF2042_RES / "platform" +BF2042_LOGO_DIR = BF2042_RES / "bf2042_logo" +BF2042_DEV_LOGO_DIR = BF2042_RES / "dev_logo" diff --git a/hexi/plugins/nonebot_plugin_bf_bot/repository.py b/hexi/plugins/nonebot_plugin_bf_bot/repository.py new file mode 100644 index 0000000..575586f --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/repository.py @@ -0,0 +1,313 @@ +"""Model 层:绑定库 / 标签库的唯一读写入口(仅标准库)。 + +- 库文件固定为 `hexi/data/nonebot_plugin_bf_bot/user_data.db`(paths.DB_PATH), + 不再写进插件源码目录。 +- 本模块顶层不 import nonebot(logger 到函数内延迟导入),保证脱离 NoneBot + 运行时也能被单测加载。 +- 表结构:users(QQ↔EA 绑定,EA 三字段 UNIQUE)、designation、medals、 + query_record(历史遗留,未用)。 +""" + +from __future__ import annotations + +import sqlite3 +from typing import Any + +from .paths import DB_PATH + + +def _logger(): + from nonebot import logger + + return logger + + +class TableManager: + """所有表管理器的基类:每次操作一条短连接 + 即时提交。""" + + def __init__(self, db_path: str | None = None): + self.db_path = str(db_path or DB_PATH) + + def _execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor: + """执行 SQL 并返回游标(调用方自行 fetch)。""" + 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): + """users 表:QQ ↔ EAID 绑定。""" + + def add_user( + self, + qq_id: str, + ea_player_name: str, + ea_player_id: str, + ea_user_id: str, + designation: str, + medals: str, + ) -> int: + """新增绑定,返回新行 id。撞 EA UNIQUE 约束会抛 sqlite3.IntegrityError, + 绑定入口需先用 get_user_by_ea_id 预检(见 services/binding.py)。""" + cursor = self._execute( + "INSERT INTO users (qq_id, ea_player_name, ea_player_id, ea_user_id," + " designation, medals) VALUES (?, ?, ?, ?, ?, ?)", + (qq_id, ea_player_name, ea_player_id, ea_user_id, designation, medals), + ) + return int(cursor.lastrowid or 0) + + def update_user( + self, + qq_id: str, + ea_player_name: str, + ea_player_id: str, + ea_user_id: str, + designation: str, + medals: str, + ) -> int: + """换绑:按 qq_id 更新 EA 信息,返回受影响行数。""" + cursor = self._execute( + "UPDATE users SET ea_player_name = ?, ea_player_id = ?, ea_user_id = ?" + " WHERE qq_id = ?", + (ea_player_name, ea_player_id, ea_user_id, qq_id), + ) + return cursor.rowcount + + def get_user_by_qq(self, qq_id: str) -> dict[str, Any] | None: + cursor = self._execute("SELECT * FROM users WHERE qq_id = ?", (qq_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + def get_user_by_ea_id(self, ea_user_id: str) -> dict[str, Any] | None: + cursor = self._execute( + "SELECT * FROM users WHERE ea_user_id = ?", (ea_user_id,) + ) + row = cursor.fetchone() + return dict(row) if row else None + + def delete_user_by_qq(self, qq_id: str) -> int: + cursor = self._execute("DELETE FROM users WHERE qq_id = ?", (qq_id,)) + return cursor.rowcount + + def list_users( + self, limit: int | None = None, offset: int = 0 + ) -> list[dict[str, Any]]: + """Web 绑定管理页用:绑定记录(按绑定时间倒序)。 + + limit=None 取全量;给 limit/offset 即分页(Web 页每页 10 条)。 + """ + sql = "SELECT * FROM users ORDER BY create_time DESC, id DESC" + params: tuple = () + if limit is not None: + sql += " LIMIT ? OFFSET ?" + params = (int(limit), int(offset)) + cursor = self._execute(sql, params) + return [dict(row) for row in cursor.fetchall()] + + def count_users(self) -> int: + """绑定总数(Web 分页用)。""" + cursor = self._execute("SELECT COUNT(*) FROM users") + row = cursor.fetchone() + return int(row[0]) if row else 0 + + +class DesignationManager(TableManager): + """designation 表:玩家标签(装饰图标)定义。""" + + def create_designation(self, name: str, icon_path: str) -> int: + cursor = self._execute( + "INSERT INTO designation (name, icon_path) VALUES (?, ?)", (name, icon_path) + ) + return int(cursor.lastrowid or 0) + + def get_all_designation(self) -> list[dict[str, Any]]: + cursor = self._execute("SELECT * FROM designation") + return [dict(row) for row in cursor.fetchall()] + + def get_designation_by_id_list(self, ids: list[int]) -> list[dict[str, Any]]: + if not ids: + return [] + placeholders = ",".join(["?"] * len(ids)) + cursor = self._execute( + f"SELECT * FROM designation WHERE id IN ({placeholders})", tuple(ids) + ) + return [dict(row) for row in cursor.fetchall()] + + +class Bf2042SettingsManager(TableManager): + """bf2042_settings 表:BF2042 卡片个人设置。 + + - support:旧 project 的"支持者"标记(1=可用个人背景图库 bg/user/{qq}/); + - bg_mode:预留(common/user),当前渲染只认 support + 个人目录是否有图。 + """ + + def get_settings(self, qq_id: str) -> dict[str, Any] | None: + cursor = self._execute( + "SELECT * FROM bf2042_settings WHERE qq_id = ?", (str(qq_id),) + ) + row = cursor.fetchone() + return dict(row) if row else None + + def is_support(self, qq_id: str) -> bool: + """无记录/查询失败一律按普通用户处理(不让装饰性功能拖垮查询)。""" + try: + row = self.get_settings(qq_id) + return bool(row and int(row.get("support") or 0) == 1) + except Exception: # noqa: BLE001 + return False + + def set_support(self, qq_id: str, support: bool) -> None: + self._execute( + """ + INSERT INTO bf2042_settings (qq_id, support) VALUES (?, ?) + ON CONFLICT(qq_id) DO UPDATE SET support = excluded.support + """, + (str(qq_id), 1 if support else 0), + ) + + def upsert_settings(self, qq_id: str, **fields: Any) -> None: + """按列 upsert(bg_mode 等扩展字段留以后用)。""" + allowed = {"support", "bg_mode"} + cols = {k: v for k, v in fields.items() if k in allowed} + if not cols: + return + qq = str(qq_id) + existing = self.get_settings(qq) + if existing is None: + names = ", ".join(cols) + marks = ", ".join(["?"] * len(cols)) + self._execute( + f"INSERT INTO bf2042_settings (qq_id, {names}) VALUES (?, {marks})", + (qq, *cols.values()), + ) + else: + sets = ", ".join(f"{k} = ?" for k in cols) + self._execute( + f"UPDATE bf2042_settings SET {sets}, update_time = CURRENT_TIMESTAMP " + "WHERE qq_id = ?", + (*cols.values(), qq), + ) + + +class DatabaseManager: + """建表 / 校验表结构(幂等)。""" + + def __init__(self, db_path: str | None = None): + # 默认与 TableManager 指向同一个库文件; + # 绝对路径,避免受启动目录影响 + self.db_path = str(db_path or DB_PATH) + self.expected_tables = { + "users": [ + ("id", "INTEGER PRIMARY KEY AUTOINCREMENT"), + ("qq_id", "TEXT NOT NULL"), + ("ea_player_name", "TEXT UNIQUE"), + ("ea_player_id", "TEXT UNIQUE"), + ("ea_user_id", "TEXT UNIQUE"), + ("designation", "TEXT NOT NULL"), + ("medals", "TEXT NOT NULL"), + ("create_time", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"), + ], + "designation": [ + ("id", "INTEGER PRIMARY KEY"), + ("name", "TEXT NOT NULL"), + ("icon_path", "TEXT NOT NULL"), + ("create_time", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"), + ], + "medals": [ + ("id", "INTEGER PRIMARY KEY"), + ("name", "TEXT NOT NULL"), + ("icon_path", "TEXT NOT NULL"), + ("create_time", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"), + ], + "query_record": [ + ("id", "INTEGER PRIMARY KEY"), + ("user_id", "TEXT NOT NULL"), + ("ea_player_name", "TEXT NOT NULL"), + ("ea_player_id", "TEXT NOT NULL"), + ("ea_user_id", "TEXT NOT NULL"), + ("status", "TEXT NOT NULL"), + ("create_time", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"), + ], + # BF2042 卡片个人设置 + 特权标记(旧 project user_bind.support 迁移而来) + "bf2042_settings": [ + ("qq_id", "TEXT PRIMARY KEY"), + ("support", "INTEGER DEFAULT 0"), + ("bg_mode", "TEXT DEFAULT 'common'"), + ("create_time", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"), + ("update_time", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"), + ], + } + + def initialize_database(self) -> bool: + """初始化数据库并创建表结构(CREATE TABLE IF NOT EXISTS,幂等)。""" + 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: # noqa: BLE001 + # 不动库文件:建表失败很可能是文件被占用一类的临时问题, + # 直接删库会连带丢掉用户的绑定数据,这里只报告失败 + _logger().error(f"Database initialization failed: {str(e)}") + return False + + def _verify_database_integrity(self, conn: sqlite3.Connection) -> bool: + """校验表齐全、列结构一致、SQLite 完整性(供排查用,不参与启动流程)。""" + cursor = conn.cursor() + + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + # 忽略 sqlite_sequence 等 SQLite 内部表,它们随 AUTOINCREMENT 自动出现 + existing_tables = { + row[0] for row in cursor.fetchall() if not row[0].startswith("sqlite_") + } + missing_tables = set(self.expected_tables.keys()) - existing_tables + if missing_tables: + _logger().warning(f"Missing tables: {missing_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 + + @staticmethod + def _type_head(declaration: str) -> str: + """取类型声明的首个词,用于忽略约束差异。""" + parts = (declaration or "").split() + return parts[0].upper() if parts else "" + + 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 + # PRAGMA table_info 只返回类型名,不带 PRIMARY KEY/AUTOINCREMENT 等约束, + # 所以只比对类型首词,否则 INTEGER 永远匹配不上 "INTEGER PRIMARY KEY ..." + if self._type_head(act_type) != self._type_head(exp_type): + return False + return True + + +def init_database() -> bool: + """建表(幂等);插件入口导入期调用,兼容新环境与热重载。""" + return DatabaseManager().initialize_database() diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/No-Pats.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/No-Pats.png new file mode 100644 index 0000000..23dc59f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/No-Pats.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo.png new file mode 100644 index 0000000..6bf9e3e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo3.png new file mode 100644 index 0000000..914f05f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo4.5.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo4.5.png new file mode 100644 index 0000000..cd3ebee Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo4.5.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo4.png new file mode 100644 index 0000000..208b452 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo4.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.5.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.5.png new file mode 100644 index 0000000..7812bd8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.5.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.6.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.6.png new file mode 100644 index 0000000..cd7b86a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.6.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.png new file mode 100644 index 0000000..eba5635 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bf2042_logo/bf2042logo5.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681591173703.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681591173703.jpg new file mode 100644 index 0000000..f45203f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681591173703.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681591719161.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681591719161.jpg new file mode 100644 index 0000000..af1ed1d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681591719161.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681873578997.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681873578997.jpg new file mode 100644 index 0000000..aa29cf1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681873578997.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681873607406.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681873607406.jpg new file mode 100644 index 0000000..23e5b9a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681873607406.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681874255421.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681874255421.png new file mode 100644 index 0000000..3292625 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1681874255421.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1682247083804.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1682247083804.jpg new file mode 100644 index 0000000..ddbbc74 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1682247083804.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699804102.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699804102.jpeg new file mode 100644 index 0000000..706fb14 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699804102.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699804112.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699804112.jpeg new file mode 100644 index 0000000..5ebd561 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699804112.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699960573.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699960573.jpeg new file mode 100644 index 0000000..bcc3892 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1699960573.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034160.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034160.jpeg new file mode 100644 index 0000000..0645761 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034160.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034169.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034169.jpeg new file mode 100644 index 0000000..3999140 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034169.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034177.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034177.jpeg new file mode 100644 index 0000000..2f9079a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034177.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034187.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034187.jpeg new file mode 100644 index 0000000..c1a523a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034187.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034199.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034199.jpeg new file mode 100644 index 0000000..9b0fc63 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034199.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034245.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034245.jpeg new file mode 100644 index 0000000..d65c879 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034245.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034256.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034256.jpeg new file mode 100644 index 0000000..be1d5d4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1700034256.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1708337857.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1708337857.jpeg new file mode 100644 index 0000000..44c6293 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/1708337857.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102442.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102442.png new file mode 100644 index 0000000..13ebefd Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102442.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102503.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102503.jpg new file mode 100644 index 0000000..ecaa809 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102503.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102521.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102521.jpg new file mode 100644 index 0000000..0b28668 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102521.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102537.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102537.png new file mode 100644 index 0000000..6ca2fac Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102537.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102555.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102555.png new file mode 100644 index 0000000..7a55376 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102555.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102614.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102614.png new file mode 100644 index 0000000..182318a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102614.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102640.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102640.jpg new file mode 100644 index 0000000..c3a757a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102640.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102700.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102700.jpg new file mode 100644 index 0000000..909fbe1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102700.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102715.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102715.jpg new file mode 100644 index 0000000..d7e471a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102715.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102728.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102728.png new file mode 100644 index 0000000..b75a86a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102728.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102756.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102756.jpg new file mode 100644 index 0000000..1854a03 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230419_102756.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230427_150641.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230427_150641.jpg new file mode 100644 index 0000000..056c727 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/IMG_20230427_150641.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/aqua.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/aqua.png new file mode 100644 index 0000000..e961378 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/aqua.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona1.png new file mode 100644 index 0000000..05b9d9d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona1.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona2.png new file mode 100644 index 0000000..a15b182 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona2.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona3.png new file mode 100644 index 0000000..d906d7c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/arona3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/bg114514.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/bg114514.jpg new file mode 100644 index 0000000..14b1688 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/bg114514.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/hanako.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/hanako.png new file mode 100644 index 0000000..023770e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/hanako.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/mika.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/mika.png new file mode 100644 index 0000000..31cfe83 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/mika.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/momo.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/momo.jpg new file mode 100644 index 0000000..5675f34 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/momo.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/sasss.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/sasss.png new file mode 100644 index 0000000..b261db3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/sasss.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/unicon.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/unicon.png new file mode 100644 index 0000000..7af65a6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/unicon.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/zi.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/zi.png new file mode 100644 index 0000000..416772c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/admin/zi.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/20231014_230902.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/20231014_230902.jpg new file mode 100644 index 0000000..80e617a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/20231014_230902.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).jpeg new file mode 100644 index 0000000..b5c97c9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).jpg new file mode 100644 index 0000000..14b1688 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).png new file mode 100644 index 0000000..19e874d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (1).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (10).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (10).jpg new file mode 100644 index 0000000..1f46423 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (10).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (10).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (10).png new file mode 100644 index 0000000..7153c9d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (10).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (11).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (11).jpg new file mode 100644 index 0000000..5bfbf97 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (11).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (11).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (11).png new file mode 100644 index 0000000..5cd555c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (11).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (12).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (12).jpg new file mode 100644 index 0000000..1c9ca5f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (12).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (12).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (12).png new file mode 100644 index 0000000..f4b96a6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (12).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (13).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (13).jpg new file mode 100644 index 0000000..44931e8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (13).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (13).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (13).png new file mode 100644 index 0000000..937611c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (13).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (14).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (14).jpg new file mode 100644 index 0000000..ae01cb9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (14).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (14).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (14).png new file mode 100644 index 0000000..cedeb00 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (14).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (15).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (15).jpg new file mode 100644 index 0000000..aa35357 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (15).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (15).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (15).png new file mode 100644 index 0000000..6c4fcd8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (15).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (16).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (16).jpg new file mode 100644 index 0000000..7b7187b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (16).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (16).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (16).png new file mode 100644 index 0000000..6571fe1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (16).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (17).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (17).png new file mode 100644 index 0000000..7936e77 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (17).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (18).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (18).png new file mode 100644 index 0000000..8785a9f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (18).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (19).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (19).png new file mode 100644 index 0000000..e49b855 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (19).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (2).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (2).jpg new file mode 100644 index 0000000..81b8895 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (2).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (20).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (20).png new file mode 100644 index 0000000..5bda3b8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (20).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (21).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (21).png new file mode 100644 index 0000000..3feeaaa Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (21).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (22).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (22).png new file mode 100644 index 0000000..0ccd635 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (22).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (23).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (23).png new file mode 100644 index 0000000..b4bf4a3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (23).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (24).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (24).png new file mode 100644 index 0000000..208c0ca Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (24).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (25).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (25).png new file mode 100644 index 0000000..11e8688 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (25).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (26).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (26).png new file mode 100644 index 0000000..0ebf44c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (26).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (27).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (27).png new file mode 100644 index 0000000..9888a1b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (27).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (28).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (28).png new file mode 100644 index 0000000..7fee39f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (28).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (29).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (29).png new file mode 100644 index 0000000..6fdbfa0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (29).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (3).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (3).jpg new file mode 100644 index 0000000..ce88fec Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (3).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (30).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (30).png new file mode 100644 index 0000000..f5734ab Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (30).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (31).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (31).png new file mode 100644 index 0000000..e438bc3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (31).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (32).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (32).png new file mode 100644 index 0000000..208c0ca Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (32).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (33).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (33).png new file mode 100644 index 0000000..0ebf44c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (33).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (34).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (34).png new file mode 100644 index 0000000..d47b65f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (34).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (35).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (35).png new file mode 100644 index 0000000..f01ddd1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (35).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (36).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (36).png new file mode 100644 index 0000000..e46fbfa Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (36).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (37).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (37).png new file mode 100644 index 0000000..43b4fc7 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (37).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (4).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (4).jpg new file mode 100644 index 0000000..72d8a4e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (4).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (5).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (5).jpg new file mode 100644 index 0000000..d08d2fc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (5).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (6).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (6).jpg new file mode 100644 index 0000000..dea087d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (6).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (6).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (6).png new file mode 100644 index 0000000..fbea609 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (6).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (7).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (7).jpg new file mode 100644 index 0000000..1fad09e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (7).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (7).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (7).png new file mode 100644 index 0000000..9f1c087 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (7).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (8).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (8).jpg new file mode 100644 index 0000000..10e52af Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (8).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (8).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (8).png new file mode 100644 index 0000000..582ad00 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (8).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (9).jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (9).jpg new file mode 100644 index 0000000..25c265e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (9).jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (9).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (9).png new file mode 100644 index 0000000..0a7ff09 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/bg (9).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/wallpaper-scence-10_17.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/wallpaper-scence-10_17.png new file mode 100644 index 0000000..db277a5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/wallpaper-scence-10_17.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/wallpaper-scence-10_17_1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/wallpaper-scence-10_17_1.png new file mode 100644 index 0000000..641cba9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/wallpaper-scence-10_17_1.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/{C005AF8C-7567-8845-D14A-1DB755534A74}.jpg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/{C005AF8C-7567-8845-D14A-1DB755534A74}.jpg new file mode 100644 index 0000000..827aab1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/common/{C005AF8C-7567-8845-D14A-1DB755534A74}.jpg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1051157345/1684916784.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1051157345/1684916784.png new file mode 100644 index 0000000..4120bf8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1051157345/1684916784.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1097829523/1684915803.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1097829523/1684915803.png new file mode 100644 index 0000000..9c85f5f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1097829523/1684915803.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1097829523/1701856104.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1097829523/1701856104.jpeg new file mode 100644 index 0000000..803ab7b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1097829523/1701856104.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329143.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329143.jpeg new file mode 100644 index 0000000..f42a34b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329143.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329343.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329343.jpeg new file mode 100644 index 0000000..39986d5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329343.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329415.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329415.jpeg new file mode 100644 index 0000000..2b5a148 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685329415.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685580175.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685580175.jpeg new file mode 100644 index 0000000..d464aa5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685580175.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685606048.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685606048.jpeg new file mode 100644 index 0000000..1ed634c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1106761850/1685606048.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1695282017.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1695282017.jpeg new file mode 100644 index 0000000..df9c9a1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1695282017.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697531308.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697531308.jpeg new file mode 100644 index 0000000..a2e5a13 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697531308.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697558457.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697558457.jpeg new file mode 100644 index 0000000..4206fc1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697558457.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697772775.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697772775.jpeg new file mode 100644 index 0000000..795c921 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697772775.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697858085.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697858085.jpeg new file mode 100644 index 0000000..5836365 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1697858085.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698893816.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698893816.jpeg new file mode 100644 index 0000000..cb01333 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698893816.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698893858.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698893858.jpeg new file mode 100644 index 0000000..3c344c2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698893858.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698894227.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698894227.jpeg new file mode 100644 index 0000000..6f51cba Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1178038085/1698894227.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1685643768.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1685643768.jpeg new file mode 100644 index 0000000..b07f455 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1685643768.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1685643886.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1685643886.jpeg new file mode 100644 index 0000000..d1a06c6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1685643886.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1687547311.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1687547311.jpeg new file mode 100644 index 0000000..d8b342f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1687547311.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1687547435.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1687547435.jpeg new file mode 100644 index 0000000..7f8cedb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1183162223/1687547435.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1192582155/1684915780.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1192582155/1684915780.png new file mode 100644 index 0000000..4a0a090 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1192582155/1684915780.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1685329088.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1685329088.jpeg new file mode 100644 index 0000000..9edb814 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1685329088.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1691299435.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1691299435.jpeg new file mode 100644 index 0000000..a48dfce Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1691299435.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1694192471.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1694192471.jpeg new file mode 100644 index 0000000..54492cf Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1215258188/1694192471.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1249734249/1699331769.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1249734249/1699331769.jpeg new file mode 100644 index 0000000..fef5925 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1249734249/1699331769.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1317687050/1700234284.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1317687050/1700234284.jpeg new file mode 100644 index 0000000..9185a17 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1317687050/1700234284.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1327093900/1699353502.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1327093900/1699353502.jpeg new file mode 100644 index 0000000..8baa76d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1327093900/1699353502.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1345066185/1706522143.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1345066185/1706522143.jpeg new file mode 100644 index 0000000..93506aa Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1345066185/1706522143.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697106447.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697106447.jpeg new file mode 100644 index 0000000..c481f1f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697106447.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697106549.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697106549.jpeg new file mode 100644 index 0000000..496c796 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697106549.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697209819.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697209819.jpeg new file mode 100644 index 0000000..c2192b9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697209819.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697209833.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697209833.jpeg new file mode 100644 index 0000000..00b5d1c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697209833.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462909.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462909.jpeg new file mode 100644 index 0000000..8aa22ef Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462909.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462920.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462920.jpeg new file mode 100644 index 0000000..4c876fb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462920.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462929.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462929.jpeg new file mode 100644 index 0000000..4e7bc09 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462929.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462951.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462951.jpeg new file mode 100644 index 0000000..1c72f35 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462951.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462968.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462968.jpeg new file mode 100644 index 0000000..f1994e3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1395551967/1697462968.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1684851694.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1684851694.png new file mode 100644 index 0000000..7082a83 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1684851694.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1684910964.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1684910964.png new file mode 100644 index 0000000..515538d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1684910964.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1685584643.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1685584643.jpeg new file mode 100644 index 0000000..e7b8bdc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1404143843/1685584643.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1422938012/1687546052.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1422938012/1687546052.jpeg new file mode 100644 index 0000000..bc6f3f0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1422938012/1687546052.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1422938012/1687546574.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1422938012/1687546574.jpeg new file mode 100644 index 0000000..d3ff67f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1422938012/1687546574.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1699353681.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1699353681.jpeg new file mode 100644 index 0000000..0e4d03f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1699353681.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1699353784.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1699353784.jpeg new file mode 100644 index 0000000..a1aceda Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1699353784.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1700055163.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1700055163.jpeg new file mode 100644 index 0000000..8f84dd1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1435068022/1700055163.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1445347899/1699353416.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1445347899/1699353416.jpeg new file mode 100644 index 0000000..8ee6f65 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1445347899/1699353416.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1461586256/1708341484.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1461586256/1708341484.jpeg new file mode 100644 index 0000000..2d1a69b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1461586256/1708341484.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699332428.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699332428.jpeg new file mode 100644 index 0000000..ae211c8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699332428.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699332439.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699332439.jpeg new file mode 100644 index 0000000..84c28ba Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699332439.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699427075.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699427075.jpeg new file mode 100644 index 0000000..a4032ee Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699427075.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699427092.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699427092.jpeg new file mode 100644 index 0000000..067d873 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699427092.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699431700.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699431700.jpeg new file mode 100644 index 0000000..067d873 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1699431700.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1701314151.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1701314151.jpeg new file mode 100644 index 0000000..2e0cf8f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1701314151.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1702370055.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1702370055.jpeg new file mode 100644 index 0000000..d618c19 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1702370055.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1708327558.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1708327558.jpeg new file mode 100644 index 0000000..a461844 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1521198648/1708327558.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1539745751/1697772762.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1539745751/1697772762.jpeg new file mode 100644 index 0000000..6e8b960 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1539745751/1697772762.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639081/1702965646.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639081/1702965646.jpeg new file mode 100644 index 0000000..acbfd54 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639081/1702965646.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1686916052.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1686916052.jpeg new file mode 100644 index 0000000..7d9d229 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1686916052.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1687580286.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1687580286.jpeg new file mode 100644 index 0000000..ddbf08a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1687580286.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1687580461.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1687580461.jpeg new file mode 100644 index 0000000..0ba7062 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1639297120/1687580461.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1651620969/1699330932.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1651620969/1699330932.jpeg new file mode 100644 index 0000000..9fbfa47 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1651620969/1699330932.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1722173847/1701328983.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1722173847/1701328983.jpeg new file mode 100644 index 0000000..fe7c9fc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1722173847/1701328983.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1723276455/1684915699.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1723276455/1684915699.png new file mode 100644 index 0000000..77a7809 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1723276455/1684915699.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1751078172/1684909397.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1751078172/1684909397.png new file mode 100644 index 0000000..bef83d9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1751078172/1684909397.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1686736987.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1686736987.jpeg new file mode 100644 index 0000000..2202989 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1686736987.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1688317315.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1688317315.jpeg new file mode 100644 index 0000000..f4aebac Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1688317315.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1688317367.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1688317367.jpeg new file mode 100644 index 0000000..f4aebac Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1688317367.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821609.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821609.jpeg new file mode 100644 index 0000000..e393e56 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821609.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821623.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821623.jpeg new file mode 100644 index 0000000..ef45966 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821623.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821663.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821663.jpeg new file mode 100644 index 0000000..0826aae Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1755052124/1689821663.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1876338245/1702965907.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1876338245/1702965907.jpeg new file mode 100644 index 0000000..a4ffe37 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1876338245/1702965907.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1918670004/1684908411.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1918670004/1684908411.png new file mode 100644 index 0000000..0c135d1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1918670004/1684908411.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1918670004/1685329117.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1918670004/1685329117.jpeg new file mode 100644 index 0000000..8eaac42 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1918670004/1685329117.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1989074512/1705501966.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1989074512/1705501966.jpeg new file mode 100644 index 0000000..630c451 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/1989074512/1705501966.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2031334716/1700117979.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2031334716/1700117979.jpeg new file mode 100644 index 0000000..63ea0e1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2031334716/1700117979.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2093197478/1687280733.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2093197478/1687280733.jpeg new file mode 100644 index 0000000..fbb8262 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2093197478/1687280733.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2111373772/1700840919.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2111373772/1700840919.jpeg new file mode 100644 index 0000000..45634cd Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2111373772/1700840919.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2111373772/1704364531.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2111373772/1704364531.jpeg new file mode 100644 index 0000000..3e0ba60 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2111373772/1704364531.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2116575746/1700400515.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2116575746/1700400515.jpeg new file mode 100644 index 0000000..e1e8c91 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2116575746/1700400515.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2157483791/1687545516.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2157483791/1687545516.jpeg new file mode 100644 index 0000000..01c7517 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2157483791/1687545516.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2283144737/1699419132.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2283144737/1699419132.jpeg new file mode 100644 index 0000000..e22ee29 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2283144737/1699419132.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2283144737/1700649579.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2283144737/1700649579.jpeg new file mode 100644 index 0000000..fb5c4db Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2283144737/1700649579.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2329797795/1684914837.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2329797795/1684914837.png new file mode 100644 index 0000000..e453fba Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2329797795/1684914837.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2415195367/1705498806.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2415195367/1705498806.jpeg new file mode 100644 index 0000000..6124b26 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2415195367/1705498806.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558469.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558469.jpeg new file mode 100644 index 0000000..2bbd8da Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558469.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558481.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558481.jpeg new file mode 100644 index 0000000..26dea30 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558481.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558491.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558491.jpeg new file mode 100644 index 0000000..d45f798 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697558491.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697772679.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697772679.jpeg new file mode 100644 index 0000000..4e241b5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1697772679.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1698893796.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1698893796.jpeg new file mode 100644 index 0000000..af19880 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2442849618/1698893796.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2474980946/1689231018.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2474980946/1689231018.jpeg new file mode 100644 index 0000000..f414da8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2474980946/1689231018.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2635940476/1699348857.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2635940476/1699348857.jpeg new file mode 100644 index 0000000..bfa728a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2635940476/1699348857.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2656360014/1686740567.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2656360014/1686740567.jpeg new file mode 100644 index 0000000..759478c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2656360014/1686740567.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2657252051/1685694651.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2657252051/1685694651.jpeg new file mode 100644 index 0000000..27fde38 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2657252051/1685694651.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2667540873/1684850155.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2667540873/1684850155.png new file mode 100644 index 0000000..b9934c5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2667540873/1684850155.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1700207392.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1700207392.jpeg new file mode 100644 index 0000000..89667cb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1700207392.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706514591.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706514591.jpeg new file mode 100644 index 0000000..aa53683 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706514591.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706541667.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706541667.jpeg new file mode 100644 index 0000000..aa53683 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706541667.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706666855.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706666855.jpeg new file mode 100644 index 0000000..aa53683 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706666855.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706775935.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706775935.jpeg new file mode 100644 index 0000000..aa53683 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1706775935.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1708654333.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1708654333.jpeg new file mode 100644 index 0000000..aa53683 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2714846430/1708654333.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2718583967/1700404668.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2718583967/1700404668.jpeg new file mode 100644 index 0000000..87848aa Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2718583967/1700404668.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2745303349/1684916091.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2745303349/1684916091.png new file mode 100644 index 0000000..dd44a59 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2745303349/1684916091.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2745303349/1689354186.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2745303349/1689354186.jpeg new file mode 100644 index 0000000..161c7ff Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2745303349/1689354186.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2756484899/1690214193.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2756484899/1690214193.jpeg new file mode 100644 index 0000000..e8b134a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2756484899/1690214193.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695198461.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695198461.jpeg new file mode 100644 index 0000000..32d3da4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695198461.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695199442.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695199442.jpeg new file mode 100644 index 0000000..928d075 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695199442.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695272148.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695272148.jpeg new file mode 100644 index 0000000..64659e9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695272148.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695277511.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695277511.jpeg new file mode 100644 index 0000000..64659e9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695277511.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363283.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363283.jpeg new file mode 100644 index 0000000..a67185f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363283.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363652.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363652.jpeg new file mode 100644 index 0000000..a67185f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363652.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363727.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363727.jpeg new file mode 100644 index 0000000..a67185f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363727.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363743.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363743.jpeg new file mode 100644 index 0000000..a67185f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1695363743.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1696520104.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1696520104.jpeg new file mode 100644 index 0000000..a67185f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1696520104.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1696663163.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1696663163.jpeg new file mode 100644 index 0000000..ccd860c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1696663163.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1697441504.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1697441504.jpeg new file mode 100644 index 0000000..8ec2971 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1697441504.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1697441627.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1697441627.jpeg new file mode 100644 index 0000000..a67185f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2776383506/1697441627.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1702965651.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1702965651.jpeg new file mode 100644 index 0000000..59511c0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1702965651.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1704364610.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1704364610.jpeg new file mode 100644 index 0000000..9dbaf5c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1704364610.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1705331248.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1705331248.jpeg new file mode 100644 index 0000000..abdeeb1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1705331248.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1705502102.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1705502102.jpeg new file mode 100644 index 0000000..730cb17 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2815144875/1705502102.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2822752177/1697772836.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2822752177/1697772836.jpeg new file mode 100644 index 0000000..8e1646e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2822752177/1697772836.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2872682608/1700404931.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2872682608/1700404931.jpeg new file mode 100644 index 0000000..6495eaa Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2872682608/1700404931.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2931589710/mika.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2931589710/mika.png new file mode 100644 index 0000000..31cfe83 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/2931589710/mika.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3115095714/1702133028.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3115095714/1702133028.jpeg new file mode 100644 index 0000000..fe1b30b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3115095714/1702133028.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3115095714/1704364236.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3115095714/1704364236.jpeg new file mode 100644 index 0000000..bf93585 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3115095714/1704364236.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699348224.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699348224.jpeg new file mode 100644 index 0000000..f0a8e34 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699348224.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699348993.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699348993.jpeg new file mode 100644 index 0000000..8d3e98a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699348993.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699349266.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699349266.jpeg new file mode 100644 index 0000000..573c896 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3187651818/1699349266.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3197397007/1695199128.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3197397007/1695199128.jpeg new file mode 100644 index 0000000..dae06b1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3197397007/1695199128.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3299411313/1684914751.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3299411313/1684914751.png new file mode 100644 index 0000000..3a66711 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3299411313/1684914751.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3377385068/1697774164.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3377385068/1697774164.jpeg new file mode 100644 index 0000000..afb5d9a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3377385068/1697774164.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3393229700/1697773953.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3393229700/1697773953.jpeg new file mode 100644 index 0000000..9074b47 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3393229700/1697773953.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691213941.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691213941.jpeg new file mode 100644 index 0000000..c3cd0e8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691213941.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214258.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214258.jpeg new file mode 100644 index 0000000..ea91b68 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214258.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214342.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214342.jpeg new file mode 100644 index 0000000..87b650d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214342.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214367.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214367.jpeg new file mode 100644 index 0000000..e674914 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3434174994/1691214367.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3466989218/1699960670.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3466989218/1699960670.jpeg new file mode 100644 index 0000000..70c6399 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/3466989218/1699960670.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/577965332/1685580458.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/577965332/1685580458.jpeg new file mode 100644 index 0000000..b8045fb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/577965332/1685580458.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/617822779/1705498533.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/617822779/1705498533.jpeg new file mode 100644 index 0000000..ab074a3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/617822779/1705498533.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684856971.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684856971.png new file mode 100644 index 0000000..3559b18 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684856971.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857323.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857323.png new file mode 100644 index 0000000..63022e3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857323.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857384.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857384.png new file mode 100644 index 0000000..18ea920 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857384.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857536.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857536.png new file mode 100644 index 0000000..9282796 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684857536.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901103.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901103.png new file mode 100644 index 0000000..bdd37fc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901103.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901851.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901851.png new file mode 100644 index 0000000..f1f5866 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901851.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901919.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901919.png new file mode 100644 index 0000000..df72863 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901919.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901967.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901967.png new file mode 100644 index 0000000..6e61d8d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684901967.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684902031.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684902031.png new file mode 100644 index 0000000..8879cd2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/724041940/1684902031.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1686742731.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1686742731.jpeg new file mode 100644 index 0000000..79d48f2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1686742731.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1687971121.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1687971121.jpeg new file mode 100644 index 0000000..a0c769f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1687971121.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688118626.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688118626.jpeg new file mode 100644 index 0000000..9310590 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688118626.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688135976.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688135976.jpeg new file mode 100644 index 0000000..4ab50d0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688135976.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688203872.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688203872.jpeg new file mode 100644 index 0000000..d2f456a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688203872.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688548263.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688548263.jpeg new file mode 100644 index 0000000..1dfbcb1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688548263.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688548632.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688548632.jpeg new file mode 100644 index 0000000..53a3fd4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1688548632.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1689587251.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1689587251.jpeg new file mode 100644 index 0000000..b7cc7f8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/730311571/1689587251.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774540.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774540.jpeg new file mode 100644 index 0000000..644fb2a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774540.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774553.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774553.jpeg new file mode 100644 index 0000000..d6f8642 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774553.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774559.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774559.jpeg new file mode 100644 index 0000000..feee3f0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774559.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774577.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774577.jpeg new file mode 100644 index 0000000..8fb14ba Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774577.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774586.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774586.jpeg new file mode 100644 index 0000000..bc4397b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1697774586.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1700130645.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1700130645.jpeg new file mode 100644 index 0000000..e4f7c0e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/757727356/1700130645.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1699441433.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1699441433.jpeg new file mode 100644 index 0000000..439f20b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1699441433.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700148546.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700148546.jpeg new file mode 100644 index 0000000..f7fce4a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700148546.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700153800.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700153800.jpeg new file mode 100644 index 0000000..ef94001 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700153800.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700232054.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700232054.jpeg new file mode 100644 index 0000000..1530f5c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/771544833/1700232054.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/906575749/1699964001.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/906575749/1699964001.jpeg new file mode 100644 index 0000000..401c1c4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/906575749/1699964001.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/906575749/1701013126.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/906575749/1701013126.jpeg new file mode 100644 index 0000000..8e57fe9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/906575749/1701013126.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1686742697.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1686742697.jpeg new file mode 100644 index 0000000..7141a19 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1686742697.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1697475468.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1697475468.jpeg new file mode 100644 index 0000000..63bf175 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1697475468.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1697610671.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1697610671.jpeg new file mode 100644 index 0000000..6d493ff Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/913318734/1697610671.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/948186339/1699960298.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/948186339/1699960298.jpeg new file mode 100644 index 0000000..53a00f3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/948186339/1699960298.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/948186339/1699960739.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/948186339/1699960739.jpeg new file mode 100644 index 0000000..24aca4e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/948186339/1699960739.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/9874665/1687545195.jpeg b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/9874665/1687545195.jpeg new file mode 100644 index 0000000..aec7192 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/bg/user/9874665/1687545195.jpeg differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/No-Pats.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/No-Pats.png new file mode 100644 index 0000000..23dc59f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/No-Pats.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Assault_Icon_small.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Assault_Icon_small.png new file mode 100644 index 0000000..bbfca7b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Assault_Icon_small.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Engineer_Icon_small.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Engineer_Icon_small.png new file mode 100644 index 0000000..cbcf24f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Engineer_Icon_small.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Recon_Icon_small.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Recon_Icon_small.png new file mode 100644 index 0000000..b466e75 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Recon_Icon_small.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Support_Icon_small.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Support_Icon_small.png new file mode 100644 index 0000000..bc95bf2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/class_icon/ui/Support_Icon_small.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Anti-tank.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Anti-tank.png new file mode 100644 index 0000000..69baa51 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Anti-tank.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Assault.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Assault.png new file mode 100644 index 0000000..398e8c2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Assault.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Engineer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Engineer.png new file mode 100644 index 0000000..c7e808c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Engineer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Medic.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Medic.png new file mode 100644 index 0000000..d7f1a35 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Medic.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Scout.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Scout.png new file mode 100644 index 0000000..6b078a7 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/1942 Scout.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Angel.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Angel.png new file mode 100644 index 0000000..5a8b1ad Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Angel.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Assault.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Assault.png new file mode 100644 index 0000000..ef087d2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Assault.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Engineer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Engineer.png new file mode 100644 index 0000000..b94bf80 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Engineer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Medic.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Medic.png new file mode 100644 index 0000000..0af1f75 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Medic.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Recon.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Recon.png new file mode 100644 index 0000000..095c38c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BC2 Recon.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Assault.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Assault.png new file mode 100644 index 0000000..9761ce5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Assault.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Engineer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Engineer.png new file mode 100644 index 0000000..ea68128 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Engineer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Recon.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Recon.png new file mode 100644 index 0000000..2a808ce Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Recon.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Support.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Support.png new file mode 100644 index 0000000..d6b2791 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/BF3 Support.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Blasco.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Blasco.png new file mode 100644 index 0000000..971f1f7 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Blasco.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Boris.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Boris.png new file mode 100644 index 0000000..77dacfb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Boris.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Casper.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Casper.png new file mode 100644 index 0000000..7f1ebf0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Casper.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Crawford.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Crawford.png new file mode 100644 index 0000000..82baf3c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Crawford.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Dozer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Dozer.png new file mode 100644 index 0000000..3a63907 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Dozer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Falck.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Falck.png new file mode 100644 index 0000000..3a66dc7 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Falck.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Irish.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Irish.png new file mode 100644 index 0000000..bd3d702 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Irish.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Lis.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Lis.png new file mode 100644 index 0000000..eafe010 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Lis.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Mackay.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Mackay.png new file mode 100644 index 0000000..5084c71 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Mackay.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Paik.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Paik.png new file mode 100644 index 0000000..ce84b7a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Paik.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Rao.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Rao.png new file mode 100644 index 0000000..7df5eec Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Rao.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Sundance.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Sundance.png new file mode 100644 index 0000000..8e1c53c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Sundance.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Zain.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Zain.png new file mode 100644 index 0000000..4302f9d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/classes/Zain.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/dev_logo/IC.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/dev_logo/IC.png new file mode 100644 index 0000000..1a2dc01 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/dev_logo/IC.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/12G Automatic.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/12G Automatic.png new file mode 100644 index 0000000..00e0fa5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/12G Automatic.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/12M Auto.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/12M Auto.png new file mode 100644 index 0000000..f05d2c0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/12M Auto.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/3937 Vodnik.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/3937 Vodnik.png new file mode 100644 index 0000000..dd4d098 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/3937 Vodnik.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/9K22 Tunguska-M.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/9K22 Tunguska-M.png new file mode 100644 index 0000000..23333e9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/9K22 Tunguska-M.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/A-10 Warthog.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/A-10 Warthog.png new file mode 100644 index 0000000..dde4011 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/A-10 Warthog.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/A-91.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/A-91.png new file mode 100644 index 0000000..1d0e9ad Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/A-91.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AAV-7A1 AMTRAC.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AAV-7A1 AMTRAC.png new file mode 100644 index 0000000..b13ea39 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AAV-7A1 AMTRAC.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AC-42.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AC-42.png new file mode 100644 index 0000000..4170f9f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AC-42.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AC9.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AC9.png new file mode 100644 index 0000000..dfc26b6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AC9.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/ACW-R.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/ACW-R.png new file mode 100644 index 0000000..6866356 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/ACW-R.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AEK-971.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AEK-971.png new file mode 100644 index 0000000..30a3bab Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AEK-971.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-64 Apache.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-64 Apache.png new file mode 100644 index 0000000..982f084 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-64 Apache.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-64GX Apache Warchief.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-64GX Apache Warchief.png new file mode 100644 index 0000000..47c6788 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-64GX Apache Warchief.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-6J Little Bird.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-6J Little Bird.png new file mode 100644 index 0000000..162dd14 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AH-6J Little Bird.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AK-24.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AK-24.png new file mode 100644 index 0000000..c36444e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AK-24.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AKS-74u.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AKS-74u.png new file mode 100644 index 0000000..5b42c4b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AKS-74u.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AM40.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AM40.png new file mode 100644 index 0000000..4e2fd0c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AM40.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AN94.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AN94.png new file mode 100644 index 0000000..6aa4088 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AN94.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AS Val.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AS Val.png new file mode 100644 index 0000000..a71f01d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AS Val.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AT Mine [Kingston].png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AT Mine [Kingston].png new file mode 100644 index 0000000..8967755 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/AT Mine [Kingston].png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ammo Box BC2 (Portal).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ammo Box BC2 (Portal).png new file mode 100644 index 0000000..4601ab6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ammo Box BC2 (Portal).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ammo Box BF3 (Portal).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ammo Box BF3 (Portal).png new file mode 100644 index 0000000..d75801c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ammo Box BF3 (Portal).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Anti Armor.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Anti Armor.png new file mode 100644 index 0000000..e4a18b5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Anti Armor.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Anti-Tank Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Anti-Tank Grenade.png new file mode 100644 index 0000000..1c84fce Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Anti-Tank Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Apache Warchief.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Apache Warchief.png new file mode 100644 index 0000000..47c6788 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Apache Warchief.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Arica Harbor.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Arica Harbor.png new file mode 100644 index 0000000..8c7a714 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Arica Harbor.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Avancys.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Avancys.png new file mode 100644 index 0000000..35d7701 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Avancys.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/B-17 Bomber.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/B-17 Bomber.png new file mode 100644 index 0000000..587116e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/B-17 Bomber.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BAR 1918.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BAR 1918.png new file mode 100644 index 0000000..ddfeaf5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BAR 1918.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BFP.50.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BFP.50.png new file mode 100644 index 0000000..9d8a2dd Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BFP.50.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BMD-3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BMD-3.png new file mode 100644 index 0000000..f3fdf97 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BMD-3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BMP-2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BMP-2.png new file mode 100644 index 0000000..03112f3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BMP-2.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BSV-M.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BSV-M.png new file mode 100644 index 0000000..6411c81 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/BSV-M.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Battle of the Bulge.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Battle of the Bulge.png new file mode 100644 index 0000000..5915237 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Battle of the Bulge.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bazooka.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bazooka.png new file mode 100644 index 0000000..69ccc58 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bazooka.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bf 109.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bf 109.png new file mode 100644 index 0000000..52f3713 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bf 109.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Breakaway.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Breakaway.png new file mode 100644 index 0000000..dbf36c9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Breakaway.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bren LMG.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bren LMG.png new file mode 100644 index 0000000..0ca3437 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Bren LMG.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/C4 [Portal].png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/C4 [Portal].png new file mode 100644 index 0000000..82145c4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/C4 [Portal].png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/C5 Explosive [Kingston].png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/C5 Explosive [Kingston].png new file mode 100644 index 0000000..85161ed Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/C5 Explosive [Kingston].png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/CAV-BRAWLER.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/CAV-BRAWLER.png new file mode 100644 index 0000000..982099b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/CAV-BRAWLER.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Carl Gustaf M5.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Carl Gustaf M5.png new file mode 100644 index 0000000..4dce302 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Carl Gustaf M5.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Caspian Border.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Caspian Border.png new file mode 100644 index 0000000..23996dc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Caspian Border.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Concussion Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Concussion Grenade.png new file mode 100644 index 0000000..f0c7e7b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Concussion Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Cyber Warfare Suite (Signal Hacker).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Cyber Warfare Suite (Signal Hacker).png new file mode 100644 index 0000000..a0be00e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Cyber Warfare Suite (Signal Hacker).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DAO-12.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DAO-12.png new file mode 100644 index 0000000..d11e25f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DAO-12.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DM7.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DM7.png new file mode 100644 index 0000000..d0c8b64 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DM7.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DXR-1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DXR-1.png new file mode 100644 index 0000000..af64166 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/DXR-1.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Defibrillator.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Defibrillator.png new file mode 100644 index 0000000..8a8a42c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Defibrillator.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Discarded.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Discarded.png new file mode 100644 index 0000000..879c402 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Discarded.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EBAA Wildcat.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EBAA Wildcat.png new file mode 100644 index 0000000..f118e8b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EBAA Wildcat.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EBLC-Ram.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EBLC-Ram.png new file mode 100644 index 0000000..9e4e953 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EBLC-Ram.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EMKV90-TOR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EMKV90-TOR.png new file mode 100644 index 0000000..3fb06da Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EMKV90-TOR.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EMP Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EMP Grenade.png new file mode 100644 index 0000000..6c863ec Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EMP Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EOD Bot - BF3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EOD Bot - BF3.png new file mode 100644 index 0000000..ff43cbb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/EOD Bot - BF3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/El Alamein.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/El Alamein.png new file mode 100644 index 0000000..5239902 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/El Alamein.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/ExpPack.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/ExpPack.png new file mode 100644 index 0000000..51ff7b3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/ExpPack.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Exposure.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Exposure.png new file mode 100644 index 0000000..85dc6c7 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Exposure.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/F-35E Panther.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/F-35E Panther.png new file mode 100644 index 0000000..363ab5d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/F-35E Panther.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/FGM-148 Javelin.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/FGM-148 Javelin.png new file mode 100644 index 0000000..6182100 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/FGM-148 Javelin.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/FXM-33 AA Missile.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/FXM-33 AA Missile.png new file mode 100644 index 0000000..1209453 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/FXM-33 AA Missile.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Flashpoint.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Flashpoint.png new file mode 100644 index 0000000..b4a1a15 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Flashpoint.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Frag Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Frag Grenade.png new file mode 100644 index 0000000..df02c10 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Frag Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G-84 TGM.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G-84 TGM.png new file mode 100644 index 0000000..c360b30 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G-84 TGM.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G3A3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G3A3.png new file mode 100644 index 0000000..6f58153 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G3A3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G428.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G428.png new file mode 100644 index 0000000..1939d97 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G428.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G57.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G57.png new file mode 100644 index 0000000..db5d61a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/G57.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GEW-46.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GEW-46.png new file mode 100644 index 0000000..6a48392 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GEW-46.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GOL Magnum.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GOL Magnum.png new file mode 100644 index 0000000..dea4ec8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GOL Magnum.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GOL Sniper Magnum.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GOL Sniper Magnum.png new file mode 100644 index 0000000..dea4ec8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GOL Sniper Magnum.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GVT 45-70.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GVT 45-70.png new file mode 100644 index 0000000..870ad7a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/GVT 45-70.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Gewerhr 43 ZF4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Gewerhr 43 ZF4.png new file mode 100644 index 0000000..c3114ac Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Gewerhr 43 ZF4.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Gewerhr 43.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Gewerhr 43.png new file mode 100644 index 0000000..ba536ea Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Gewerhr 43.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ghostmaker R10.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ghostmaker R10.png new file mode 100644 index 0000000..950ae85 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ghostmaker R10.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Grappling Hook.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Grappling Hook.png new file mode 100644 index 0000000..7cd1496 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Grappling Hook.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Hourglass.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Hourglass.png new file mode 100644 index 0000000..4bbed42 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Hourglass.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Incendiary Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Incendiary Grenade.png new file mode 100644 index 0000000..1c1750e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Incendiary Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ju-87 Stuka.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ju-87 Stuka.png new file mode 100644 index 0000000..46f585c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Ju-87 Stuka.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/K30.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/K30.png new file mode 100644 index 0000000..af7e6d5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/K30.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/K98 Sniper.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/K98 Sniper.png new file mode 100644 index 0000000..20a788f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/K98 Sniper.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/KA-520 Super Hokum.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/KA-520 Super Hokum.png new file mode 100644 index 0000000..3e6ca34 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/KA-520 Super Hokum.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Kaleidescope.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Kaleidescope.png new file mode 100644 index 0000000..088701e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Kaleidescope.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Kubelwagen.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Kubelwagen.png new file mode 100644 index 0000000..a77875e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Kubelwagen.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/L9CZ.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/L9CZ.png new file mode 100644 index 0000000..7569685 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/L9CZ.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LATV4 Recon.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LATV4 Recon.png new file mode 100644 index 0000000..3993268 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LATV4 Recon.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LAV-25.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LAV-25.png new file mode 100644 index 0000000..a0dc262 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LAV-25.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LAV-AD.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LAV-AD.png new file mode 100644 index 0000000..f44716a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LAV-AD.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LCAA Hovercraft.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LCAA Hovercraft.png new file mode 100644 index 0000000..3b5add0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LCAA Hovercraft.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LCMG.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LCMG.png new file mode 100644 index 0000000..fd5dab7 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/LCMG.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Lindmine.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Lindmine.png new file mode 100644 index 0000000..f16680b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Lindmine.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1 Garand.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1 Garand.png new file mode 100644 index 0000000..dc0c4cd Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1 Garand.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M10 Wolverine.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M10 Wolverine.png new file mode 100644 index 0000000..a2c3726 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M10 Wolverine.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1114.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1114.png new file mode 100644 index 0000000..bb2422d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1114.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1161 ITV.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1161 ITV.png new file mode 100644 index 0000000..fa22d54 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1161 ITV.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M136 AT4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M136 AT4.png new file mode 100644 index 0000000..8428840 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M136 AT4.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M16A2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M16A2.png new file mode 100644 index 0000000..f2951eb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M16A2.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M16A3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M16A3.png new file mode 100644 index 0000000..a64cb81 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M16A3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M18 Claymore.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M18 Claymore.png new file mode 100644 index 0000000..a118d4d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M18 Claymore.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1911.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1911.png new file mode 100644 index 0000000..2adddfc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1911.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1A2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1A2.png new file mode 100644 index 0000000..a4a901b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1A2.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1A5.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1A5.png new file mode 100644 index 0000000..81a5adf Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M1A5.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M2 Carbine.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M2 Carbine.png new file mode 100644 index 0000000..3d1158f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M2 Carbine.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M24 Sniper.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M24 Sniper.png new file mode 100644 index 0000000..be994bc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M24 Sniper.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M240B.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M240B.png new file mode 100644 index 0000000..c2d43a9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M240B.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M249 SAW.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M249 SAW.png new file mode 100644 index 0000000..10af887 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M249 SAW.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M3 Halftrack.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M3 Halftrack.png new file mode 100644 index 0000000..adfa30c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M3 Halftrack.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M39 EMR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M39 EMR.png new file mode 100644 index 0000000..8deaebc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M39 EMR.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M3A3 Bradley.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M3A3 Bradley.png new file mode 100644 index 0000000..adcb9f2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M3A3 Bradley.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M4 Sherman.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M4 Sherman.png new file mode 100644 index 0000000..6ff32f1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M4 Sherman.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M416.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M416.png new file mode 100644 index 0000000..c445ffc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M416.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M44.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M44.png new file mode 100644 index 0000000..23e4036 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M44.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M5A3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M5A3.png new file mode 100644 index 0000000..23237d1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M5A3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M5C Bolte.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M5C Bolte.png new file mode 100644 index 0000000..672c9e9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M5C Bolte.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M60E4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M60E4.png new file mode 100644 index 0000000..1d42ea0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M60E4.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M93R.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M93R.png new file mode 100644 index 0000000..5803181 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M93R.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M95 Sniper.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M95 Sniper.png new file mode 100644 index 0000000..bdcf0d8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M95 Sniper.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M98B.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M98B.png new file mode 100644 index 0000000..2168f82 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/M98B.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MAV.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MAV.png new file mode 100644 index 0000000..8c43627 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MAV.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MCS-880.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MCS-880.png new file mode 100644 index 0000000..73c3716 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MCS-880.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MD540 Nightbird.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MD540 Nightbird.png new file mode 100644 index 0000000..06bee65 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MD540 Nightbird.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP 40.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP 40.png new file mode 100644 index 0000000..04c528d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP 40.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP28.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP28.png new file mode 100644 index 0000000..716e9d3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP28.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP412 REX.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP412 REX.png new file mode 100644 index 0000000..e56ca1c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP412 REX.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP443.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP443.png new file mode 100644 index 0000000..b163212 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP443.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP9.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP9.png new file mode 100644 index 0000000..fdd320d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MP9.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MTAR-21.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MTAR-21.png new file mode 100644 index 0000000..d9146a8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MTAR-21.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MV-38 Condor.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MV-38 Condor.png new file mode 100644 index 0000000..6e0c789 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/MV-38 Condor.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Manifest.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Manifest.png new file mode 100644 index 0000000..40b3374 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Manifest.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Medkit BF3 (Portal).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Medkit BF3 (Portal).png new file mode 100644 index 0000000..ffa0a1e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Medkit BF3 (Portal).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Melee.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Melee.png new file mode 100644 index 0000000..898b207 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Melee.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-24 Hind.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-24 Hind.png new file mode 100644 index 0000000..6687736 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-24 Hind.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-240 Super Hind.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-240 Super Hind.png new file mode 100644 index 0000000..1c5d9bf Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-240 Super Hind.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-28 Havoc.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-28 Havoc.png new file mode 100644 index 0000000..167cd09 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mi-28 Havoc.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mini Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mini Grenade.png new file mode 100644 index 0000000..b8f3398 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mini Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mortar Strike.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mortar Strike.png new file mode 100644 index 0000000..d850b97 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mortar Strike.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mounted Vulcan.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mounted Vulcan.png new file mode 100644 index 0000000..11f5e16 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Mounted Vulcan.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NTW-50.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NTW-50.png new file mode 100644 index 0000000..374327f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NTW-50.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NVK-P125.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NVK-P125.png new file mode 100644 index 0000000..653ca88 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NVK-P125.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NVK-S22.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NVK-S22.png new file mode 100644 index 0000000..694920f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/NVK-S22.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/No 4 Sniper.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/No 4 Sniper.png new file mode 100644 index 0000000..5027133 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/No 4 Sniper.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Noshahr Canals.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Noshahr Canals.png new file mode 100644 index 0000000..0104b54 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Noshahr Canals.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/OV-P Recon Drone.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/OV-P Recon Drone.png new file mode 100644 index 0000000..24848b6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/OV-P Recon Drone.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Orbital.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Orbital.png new file mode 100644 index 0000000..08662f1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Orbital.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/P90.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/P90.png new file mode 100644 index 0000000..ef55f02 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/P90.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PBX-45.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PBX-45.png new file mode 100644 index 0000000..3ac232e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PBX-45.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PF51.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PF51.png new file mode 100644 index 0000000..7a46a92 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PF51.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PKP-BP.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PKP-BP.png new file mode 100644 index 0000000..eee01dc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PKP-BP.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PP-2000.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PP-2000.png new file mode 100644 index 0000000..44e1e2f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PP-2000.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PP-29.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PP-29.png new file mode 100644 index 0000000..892d693 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/PP-29.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Panzer IV.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Panzer IV.png new file mode 100644 index 0000000..131d98d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Panzer IV.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Panzerschreck.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Panzerschreck.png new file mode 100644 index 0000000..c948fb7 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Panzerschreck.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Prox Sensor (Motion).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Prox Sensor (Motion).png new file mode 100644 index 0000000..5f4806d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Prox Sensor (Motion).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RAH-68 Huron.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RAH-68 Huron.png new file mode 100644 index 0000000..63461e4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RAH-68 Huron.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RM68.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RM68.png new file mode 100644 index 0000000..d864d90 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RM68.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPG-7v2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPG-7v2.png new file mode 100644 index 0000000..55e5c48 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPG-7v2.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPK-74M.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPK-74M.png new file mode 100644 index 0000000..3c56747 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPK-74M.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPT-3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPT-3.png new file mode 100644 index 0000000..b58af19 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/RPT-3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Reclaimed.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Reclaimed.png new file mode 100644 index 0000000..e540d21 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Reclaimed.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Redacted.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Redacted.png new file mode 100644 index 0000000..b3cfbba Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Redacted.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Renewal.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Renewal.png new file mode 100644 index 0000000..8f8c5ea Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Renewal.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Repair Tool BF3 (Portal).png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Repair Tool BF3 (Portal).png new file mode 100644 index 0000000..5e23823 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Repair Tool BF3 (Portal).png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Repair Tool.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Repair Tool.png new file mode 100644 index 0000000..b64cf05 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Repair Tool.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Rorsch Mk-4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Rorsch Mk-4.png new file mode 100644 index 0000000..ba733d3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Rorsch Mk-4.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/S21 Syrette Pistol.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/S21 Syrette Pistol.png new file mode 100644 index 0000000..497315e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/S21 Syrette Pistol.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SA-18 Igla.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SA-18 Igla.png new file mode 100644 index 0000000..67675f4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SA-18 Igla.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SCAR-H.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SCAR-H.png new file mode 100644 index 0000000..191bdf2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SCAR-H.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SFAR-M GL.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SFAR-M GL.png new file mode 100644 index 0000000..848c345 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SFAR-M GL.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SG-36 Sentry Gun.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SG-36 Sentry Gun.png new file mode 100644 index 0000000..0ceb132 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SG-36 Sentry Gun.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SMAW.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SMAW.png new file mode 100644 index 0000000..44b716b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SMAW.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SOB-8 Ballistic Shield.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SOB-8 Ballistic Shield.png new file mode 100644 index 0000000..c702bdb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SOB-8 Ballistic Shield.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SPAS-12.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SPAS-12.png new file mode 100644 index 0000000..cd142ec Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SPAS-12.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SPH Explosive Launcher.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SPH Explosive Launcher.png new file mode 100644 index 0000000..2727c05 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SPH Explosive Launcher.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SU-57 FELON.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SU-57 FELON.png new file mode 100644 index 0000000..58e95b0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SU-57 FELON.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SVD.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SVD.png new file mode 100644 index 0000000..5d4b2d2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SVD.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SVK.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SVK.png new file mode 100644 index 0000000..01fcf07 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SVK.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SWS-10.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SWS-10.png new file mode 100644 index 0000000..27c12b1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/SWS-10.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Scatter Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Scatter Grenade.png new file mode 100644 index 0000000..c51f6ce Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Scatter Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Sd. Kfz 251 Halftrack.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Sd. Kfz 251 Halftrack.png new file mode 100644 index 0000000..6be8c5b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Sd. Kfz 251 Halftrack.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Smoke Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Smoke Grenade.png new file mode 100644 index 0000000..afb53df Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Smoke Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spearhead.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spearhead.png new file mode 100644 index 0000000..96bb0be Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spearhead.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spitfire.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spitfire.png new file mode 100644 index 0000000..15b07f5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spitfire.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spring Grenade.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spring Grenade.png new file mode 100644 index 0000000..2c0ac8e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Spring Grenade.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/StG 44.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/StG 44.png new file mode 100644 index 0000000..d59374d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/StG 44.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Sten.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Sten.png new file mode 100644 index 0000000..ccccb5f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Sten.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Stranded.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Stranded.png new file mode 100644 index 0000000..805fa61 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Stranded.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Su-25TM Frogfoot.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Su-25TM Frogfoot.png new file mode 100644 index 0000000..db98938 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Su-25TM Frogfoot.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Su-35BM.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Su-35BM.png new file mode 100644 index 0000000..e5eebe1 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Su-35BM.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Super 500.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Super 500.png new file mode 100644 index 0000000..0008398 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Super 500.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/T-90.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/T-90.png new file mode 100644 index 0000000..2e5e212 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/T-90.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/T28.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/T28.png new file mode 100644 index 0000000..142631b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/T28.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Thompson.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Thompson.png new file mode 100644 index 0000000..f3541eb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Thompson.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Throwing Knife.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Throwing Knife.png new file mode 100644 index 0000000..06af4bf Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Throwing Knife.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Tiger I.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Tiger I.png new file mode 100644 index 0000000..8899fd3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Tiger I.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Type 88.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Type 88.png new file mode 100644 index 0000000..a1d465c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Type 88.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/UH-60.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/UH-60.png new file mode 100644 index 0000000..88b0f64 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/UH-60.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VCAR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VCAR.png new file mode 100644 index 0000000..bddb3f6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VCAR.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VDV Buggy.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VDV Buggy.png new file mode 100644 index 0000000..f1841af Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VDV Buggy.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VHX D3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VHX D3.png new file mode 100644 index 0000000..74beeda Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/VHX D3.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Valparaiso.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Valparaiso.png new file mode 100644 index 0000000..90fd9fe Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Valparaiso.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Walther P38.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Walther P38.png new file mode 100644 index 0000000..ab50917 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Walther P38.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Willys MB.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Willys MB.png new file mode 100644 index 0000000..7df257d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Willys MB.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XCE BAR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XCE BAR.png new file mode 100644 index 0000000..7db81b0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XCE BAR.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM370A.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM370A.png new file mode 100644 index 0000000..9fa3995 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM370A.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 Compact.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 Compact.png new file mode 100644 index 0000000..3f03334 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 Compact.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 LMG.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 LMG.png new file mode 100644 index 0000000..7abc50f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 LMG.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 Prototype.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 Prototype.png new file mode 100644 index 0000000..efe8f50 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/XM8 Prototype.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/YG-99 Hannibal.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/YG-99 Hannibal.png new file mode 100644 index 0000000..8778484 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/YG-99 Hannibal.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/YUV-2 Pondhawk.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/YUV-2 Pondhawk.png new file mode 100644 index 0000000..3090546 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/YUV-2 Pondhawk.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Z-11W.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Z-11W.png new file mode 100644 index 0000000..d14a9c6 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/Z-11W.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/default.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/default.png new file mode 100644 index 0000000..34b667e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/object_icon/default.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/origin.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/origin.png new file mode 100644 index 0000000..a736536 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/origin.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/playstation.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/playstation.png new file mode 100644 index 0000000..0dec52f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/playstation.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/xbox.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/xbox.png new file mode 100644 index 0000000..82f3f95 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/platform/xbox.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Anti-tank.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Anti-tank.png new file mode 100644 index 0000000..74bfd84 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Anti-tank.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Assault.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Assault.png new file mode 100644 index 0000000..fed52d4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Assault.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Engineer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Engineer.png new file mode 100644 index 0000000..66a1d99 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Engineer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Medic.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Medic.png new file mode 100644 index 0000000..94f7f7e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Medic.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Scout.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Scout.png new file mode 100644 index 0000000..d4c1b72 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/1942 Scout.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Angel.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Angel.png new file mode 100644 index 0000000..9f25d58 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Angel.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Assault.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Assault.png new file mode 100644 index 0000000..5286874 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Assault.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Engineer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Engineer.png new file mode 100644 index 0000000..8929c45 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Engineer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Medic.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Medic.png new file mode 100644 index 0000000..325b647 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Medic.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Recon.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Recon.png new file mode 100644 index 0000000..52c869b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BC2 Recon.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Assault.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Assault.png new file mode 100644 index 0000000..4f4ab60 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Assault.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Engineer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Engineer.png new file mode 100644 index 0000000..21998ee Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Engineer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Recon.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Recon.png new file mode 100644 index 0000000..b397278 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Recon.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Support.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Support.png new file mode 100644 index 0000000..fdcff53 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/BF3 Support.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Blasco.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Blasco.png new file mode 100644 index 0000000..67cabad Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Blasco.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Boris.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Boris.png new file mode 100644 index 0000000..f43a2df Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Boris.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Casper.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Casper.png new file mode 100644 index 0000000..8d33c5b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Casper.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Crawford.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Crawford.png new file mode 100644 index 0000000..79efc6c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Crawford.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Dozer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Dozer.png new file mode 100644 index 0000000..71da706 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Dozer.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Falck.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Falck.png new file mode 100644 index 0000000..2aa4cdf Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Falck.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Irish.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Irish.png new file mode 100644 index 0000000..027fa71 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Irish.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Lis.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Lis.png new file mode 100644 index 0000000..8b4ad21 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Lis.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Mackay.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Mackay.png new file mode 100644 index 0000000..84d01ad Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Mackay.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/No-Pats.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/No-Pats.png new file mode 100644 index 0000000..9ed9447 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/No-Pats.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Paik.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Paik.png new file mode 100644 index 0000000..b96f8ca Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Paik.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Rao.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Rao.png new file mode 100644 index 0000000..495635b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Rao.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Sundance.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Sundance.png new file mode 100644 index 0000000..afd9131 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Sundance.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Zain.png b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Zain.png new file mode 100644 index 0000000..71f9511 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/bf2042/specialist_icon/Zain.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/font/BF_Modernista-Bold.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/BF_Modernista-Bold.ttf new file mode 100644 index 0000000..889e653 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/font/BF_Modernista-Bold.ttf differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/MYingHeiPRC-W5.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/MYingHeiPRC-W5.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/MYingHeiPRC-W5.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/MYingHeiPRC-W5.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/font/NotoSansSCMedium-4.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/NotoSansSCMedium-4.ttf new file mode 100644 index 0000000..01078d2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/font/NotoSansSCMedium-4.ttf differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Bold.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Bold.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Bold.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Bold.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-BoldItalic.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-BoldItalic.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-BoldItalic.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-BoldItalic.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Light.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Light.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Light.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Light.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-LightItalic.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-LightItalic.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-LightItalic.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-LightItalic.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Medium.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Medium.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Medium.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Medium.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-MediumItalic.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-MediumItalic.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-MediumItalic.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-MediumItalic.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-SemiBold.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-SemiBold.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-SemiBold.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-SemiBold.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-SemiBoldItalic.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-SemiBoldItalic.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-SemiBoldItalic.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-SemiBoldItalic.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Thin.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Thin.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-Thin.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-Thin.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/Purista-ThinItalic.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-ThinItalic.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/Purista-ThinItalic.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/Purista-ThinItalic.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/SourceHanSansCN-VF.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/SourceHanSansCN-VF.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/SourceHanSansCN-VF.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/SourceHanSansCN-VF.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/bf-sub-headline-bold 等宽数字.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/bf-sub-headline-bold 等宽数字.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/bf-sub-headline-bold 等宽数字.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/bf-sub-headline-bold 等宽数字.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/bf-sub-headline-bold.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/bf-sub-headline-bold.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/bf-sub-headline-bold.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/bf-sub-headline-bold.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/演示创黑.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/演示创黑.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/演示创黑.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/演示创黑.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/font/演示创黑FLY.ttf b/hexi/plugins/nonebot_plugin_bf_bot/res/font/演示创黑FLY.ttf similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/font/演示创黑FLY.ttf rename to hexi/plugins/nonebot_plugin_bf_bot/res/font/演示创黑FLY.ttf diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/Artillery truck.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Artillery truck.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/Artillery truck.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Artillery truck.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/Heavy machine gun.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Heavy machine gun.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/Heavy machine gun.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Heavy machine gun.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Horse.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Horse.png new file mode 100644 index 0000000..c21d254 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Horse.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/Mark v landship.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Mark v landship.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/Mark v landship.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/Mark v landship.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/St chamond.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/St chamond.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/vehicles/St chamond.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/vehicles/St chamond.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Annihilator Trench.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Annihilator Trench.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Annihilator Trench.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Annihilator Trench.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Automatico M1918 Factory.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Automatico M1918 Factory.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Automatico M1918 Factory.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Automatico M1918 Factory.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Cei-Rigotti Factory.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Cei-Rigotti Factory.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Cei-Rigotti Factory.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Cei-Rigotti Factory.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/M1907 SL Sweeper.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/M1907 SL Sweeper.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/M1907 SL Sweeper.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/M1907 SL Sweeper.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Parabellum MG1417 Low Weight.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Parabellum MG1417 Low Weight.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf1/weapon/Parabellum MG1417 Low Weight.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Parabellum MG1417 Low Weight.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Russian 1895 Infantry.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Russian 1895 Infantry.png new file mode 100644 index 0000000..8b754af Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf1/weapon/Russian 1895 Infantry.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf3/vehicles/VEHICLE AIR JET FIGHTER F18.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf3/vehicles/VEHICLE AIR JET FIGHTER F18.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf3/vehicles/VEHICLE AIR JET FIGHTER F18.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf3/vehicles/VEHICLE AIR JET FIGHTER F18.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf3/weapon/M16A4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf3/weapon/M16A4.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf3/weapon/M16A4.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf3/weapon/M16A4.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf4/vehicles/RHIB-BOAT.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/vehicles/RHIB-BOAT.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf4/vehicles/RHIB-BOAT.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/vehicles/RHIB-BOAT.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf4/vehicles/SUAV.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/vehicles/SUAV.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf4/vehicles/SUAV.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/vehicles/SUAV.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf4/vehicles/Z-11W.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/vehicles/Z-11W.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf4/vehicles/Z-11W.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/vehicles/Z-11W.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf4/weapon/ak-12.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/weapon/ak-12.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf4/weapon/ak-12.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/weapon/ak-12.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf4/weapon/m416.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/weapon/m416.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf4/weapon/m416.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/weapon/m416.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf4/weapon/scar-h.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/weapon/scar-h.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf4/weapon/scar-h.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf4/weapon/scar-h.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Assault.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Assault.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Assault.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Assault.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Engineer.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Engineer.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Engineer.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Engineer.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Recon.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Recon.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Recon.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Recon.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Support.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Support.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/classes/Support.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/classes/Support.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/AH-6 LITTLE BIRD.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/AH-6 LITTLE BIRD.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/AH-6 LITTLE BIRD.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/AH-6 LITTLE BIRD.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/CHEETAH 1A2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/CHEETAH 1A2.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/CHEETAH 1A2.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/CHEETAH 1A2.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/F-39E.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/F-39E.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/F-39E.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/F-39E.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/F-61V.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/F-61V.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/F-61V.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/F-61V.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/F-97 KESTREL.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/F-97 KESTREL.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/F-97 KESTREL.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/F-97 KESTREL.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/GE-26.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/GE-26.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/GE-26.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/GE-26.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/GLIDER 96.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/GLIDER 96.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/GLIDER 96.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/GLIDER 96.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/LEO A4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/LEO A4.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/LEO A4.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/LEO A4.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/M1A2 SEPV3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/M1A2 SEPV3.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/M1A2 SEPV3.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/M1A2 SEPV3.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/M3A3 BRADLEY.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/M3A3 BRADLEY.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/M3A3 BRADLEY.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/M3A3 BRADLEY.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/M77E FALCHION.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/M77E FALCHION.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/M77E FALCHION.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/M77E FALCHION.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/MD530 CAYUSE.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/MD530 CAYUSE.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/MD530 CAYUSE.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/MD530 CAYUSE.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/PANTHERA KHT.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/PANTHERA KHT.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/PANTHERA KHT.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/PANTHERA KHT.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/STRF 09 A4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/STRF 09 A4.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/STRF 09 A4.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/STRF 09 A4.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/SU-57.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/SU-57.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/SU-57.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/SU-57.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/TRAVERSER MARK 2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/TRAVERSER MARK 2.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/TRAVERSER MARK 2.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/TRAVERSER MARK 2.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/UH-79 NATO.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/UH-79 NATO.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/UH-79 NATO.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/UH-79 NATO.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/UH-79 PAX.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/UH-79 PAX.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/UH-79 PAX.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/UH-79 PAX.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/VECTOR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/VECTOR.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/vehicles/VECTOR.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/vehicles/VECTOR.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/18.5KS-K.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/18.5KS-K.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/18.5KS-K.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/18.5KS-K.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/AK-205.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/AK-205.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/AK-205.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/AK-205.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/AK4D.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/AK4D.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/AK4D.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/AK4D.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/B36A4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/B36A4.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/B36A4.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/B36A4.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/DB-12.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/DB-12.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/DB-12.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/DB-12.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/DRS-IAR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/DRS-IAR.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/DRS-IAR.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/DRS-IAR.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/ES 5.7.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/ES 5.7.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/ES 5.7.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/ES 5.7.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/GGH-22.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/GGH-22.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/GGH-22.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/GGH-22.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/GRT-C.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/GRT-C.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/GRT-C.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/GRT-C.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/KORD 6P67.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/KORD 6P67.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/KORD 6P67.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/KORD 6P67.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/KTS100 MK8.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/KTS100 MK8.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/KTS100 MK8.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/KTS100 MK8.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/KV9.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/KV9.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/KV9.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/KV9.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/L110.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/L110.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/L110.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/L110.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/L85A3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/L85A3.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/L85A3.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/L85A3.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/LMR27.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/LMR27.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/LMR27.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/LMR27.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M1014.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M1014.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M1014.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M1014.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M123K.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M123K.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M123K.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M123K.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M2010 ESR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M2010 ESR.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M2010 ESR.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M2010 ESR.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M240L.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M240L.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M240L.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M240L.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M250.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M250.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M250.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M250.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M277.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M277.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M277.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M277.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M357 TRAIT.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M357 TRAIT.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M357 TRAIT.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M357 TRAIT.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M39 EMR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M39 EMR.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M39 EMR.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M39 EMR.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M417 A2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M417 A2.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M417 A2.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M417 A2.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M433.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M433.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M433.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M433.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M44.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M44.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M44.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M44.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M45A1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M45A1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M45A1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M45A1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M4A1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M4A1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M4A1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M4A1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M60.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M60.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M60.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M60.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M87A1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M87A1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/M87A1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/M87A1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/MINI SCOUT.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/MINI SCOUT.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/MINI SCOUT.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/MINI SCOUT.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/NVO-228E.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/NVO-228E.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/NVO-228E.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/NVO-228E.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/P18.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/P18.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/P18.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/P18.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/PSR.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/PSR.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/PSR.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/PSR.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/PW5A3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/PW5A3.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/PW5A3.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/PW5A3.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/PW7A2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/PW7A2.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/PW7A2.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/PW7A2.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/QBZ-192.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/QBZ-192.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/QBZ-192.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/QBZ-192.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/RPKM.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/RPKM.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/RPKM.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/RPKM.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SCW-10.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SCW-10.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SCW-10.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SCW-10.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SG 553R.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SG 553R.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SG 553R.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SG 553R.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SGX.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SGX.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SGX.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SGX.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SL9.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SL9.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SL9.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SL9.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SOR-300SC.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SOR-300SC.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SOR-300SC.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SOR-300SC.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SOR-556 MK2.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SOR-556 MK2.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SOR-556 MK2.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SOR-556 MK2.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SV-98.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SV-98.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SV-98.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SV-98.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SVDM.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SVDM.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SVDM.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SVDM.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SVK-8.6.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SVK-8.6.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/SVK-8.6.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/SVK-8.6.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/TR-7.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/TR-7.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/TR-7.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/TR-7.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/UMG-40.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/UMG-40.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/UMG-40.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/UMG-40.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/USG-90.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/USG-90.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bf6/weapon/USG-90.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bf6/weapon/USG-90.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Lvt.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Lvt.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Lvt.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Lvt.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Panzer iv.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Panzer iv.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Panzer iv.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Panzer iv.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Sd. kfz 251 halftrack.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Sd. kfz 251 halftrack.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Sd. kfz 251 halftrack.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Sd. kfz 251 halftrack.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Sherman.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Sherman.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Sherman.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Sherman.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Stationary mg34.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Stationary mg34.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Stationary mg34.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Stationary mg34.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Tiger i.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Tiger i.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Tiger i.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Tiger i.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Type 97.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Type 97.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/vehicles/Type 97.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/vehicles/Type 97.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/Gewehr M9530.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/Gewehr M9530.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/Gewehr M9530.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/Gewehr M9530.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/M1907 SF.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/M1907 SF.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/M1907 SF.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/M1907 SF.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/StG 44.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/StG 44.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/StG 44.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/StG 44.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/Suomi KP-31.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/Suomi KP-31.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/Suomi KP-31.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/Suomi KP-31.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/Type 99 Arisaka.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/Type 99 Arisaka.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/Type 99 Arisaka.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/Type 99 Arisaka.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/ZK-383.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/ZK-383.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/bfv/weapon/ZK-383.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/bfv/weapon/ZK-383.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/DEV.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/DEV.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/DEV.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/DEV.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/DICE.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/DICE.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/DICE.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/DICE.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/IC.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/IC.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/IC.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/IC.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/OWNER.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/OWNER.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/designation/OWNER.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/designation/OWNER.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_1-9 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_1-9 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_1-9 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_1-9 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_10-24 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_10-24 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_10-24 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_10-24 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_100-149 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_100-149 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_100-149 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_100-149 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_150-190 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_150-190 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_150-190 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_150-190 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_200-240 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_200-240 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_200-240 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_200-240 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_25-44 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_25-44 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_25-44 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_25-44 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_250-290 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_250-290 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_250-290 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_250-290 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_300-340 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_300-340 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_300-340 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_300-340 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_3000-5000 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_3000-5000 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_3000-5000 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_3000-5000 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_350-390 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_350-390 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_350-390 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_350-390 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_400-450 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_400-450 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_400-450 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_400-450 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_45-49 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_45-49 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_45-49 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_45-49 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_450-490 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_450-490 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_450-490 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_450-490 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_50-99 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_50-99 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_50-99 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_50-99 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_500-3000 1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_500-3000 1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/icon/rank/t_ui_rankswatch_500-3000 1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/icon/rank/t_ui_rankswatch_500-3000 1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/vc.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/vc.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/vc.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/vc.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img/wp.png b/hexi/plugins/nonebot_plugin_bf_bot/res/img/wp.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/img/wp.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/img/wp.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/notice/notice.png b/hexi/plugins/nonebot_plugin_bf_bot/res/notice/notice.png new file mode 100644 index 0000000..207763d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/notice/notice.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/res/notice/notice_bf6.png b/hexi/plugins/nonebot_plugin_bf_bot/res/notice/notice_bf6.png new file mode 100644 index 0000000..bf5329f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_bf_bot/res/notice/notice_bf6.png differ diff --git a/hexi/plugins/nonebot_plugin_bf_bot/template/bf1.png b/hexi/plugins/nonebot_plugin_bf_bot/res/template/bf1.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/template/bf1.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/template/bf1.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/template/bf3.png b/hexi/plugins/nonebot_plugin_bf_bot/res/template/bf3.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/template/bf3.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/template/bf3.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/template/bf4.png b/hexi/plugins/nonebot_plugin_bf_bot/res/template/bf4.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/template/bf4.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/template/bf4.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/template/bf6.png b/hexi/plugins/nonebot_plugin_bf_bot/res/template/bf6.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/template/bf6.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/template/bf6.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/template/bfv.png b/hexi/plugins/nonebot_plugin_bf_bot/res/template/bfv.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/template/bfv.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/template/bfv.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/template/horizon/bf6.png b/hexi/plugins/nonebot_plugin_bf_bot/res/template/horizon/bf6.png similarity index 100% rename from hexi/plugins/nonebot_plugin_bf_bot/template/horizon/bf6.png rename to hexi/plugins/nonebot_plugin_bf_bot/res/template/horizon/bf6.png diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/__init__.py b/hexi/plugins/nonebot_plugin_bf_bot/services/__init__.py new file mode 100644 index 0000000..84b497f --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/__init__.py @@ -0,0 +1 @@ +"""业务编排层:查询/绑定等业务动作;不依赖 nonebot(View 由 handlers 负责)。""" diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/binding.py b/hexi/plugins/nonebot_plugin_bf_bot/services/binding.py new file mode 100644 index 0000000..b9831a2 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/binding.py @@ -0,0 +1,116 @@ +"""绑定编排:/bfbind、/unbind、/update_bind 的业务流程。 + +只做数据操作与文案产出(返回给用户的字符串),不直接发消息。 +""" + +from __future__ import annotations + +import sqlite3 + +from nonebot import logger + +from ..repository import UserManager +from .fetchers.gametools import ( + get_player_info_by_ea_id, + get_player_info_by_name, + player_display_name, +) + +_PLATFORM = "pc" + + +async def bind(qq_id: str, player_name: str) -> str: + """绑定 QQ ↔ EAID;已绑/被他人占用/查无此人都给出对应提示。""" + manager = UserManager() + user_record = manager.get_user_by_qq(qq_id) + if user_record: + info = await get_player_info_by_ea_id( + user_record["ea_player_id"], user_record["ea_user_id"], _PLATFORM + ) + name = player_display_name(info, user_record["ea_player_name"]) + return f"您已经将账号绑定到: {name},如果你需要更改绑定请发送 修改绑定 EAID " + + player_name = (player_name or "").strip() + if not player_name: + return "请提供要绑定的 EAID,例如:绑定 YourEAID" + + user_info = await get_player_info_by_name(player_name, _PLATFORM) + if "errors" in user_info: + return "未找到玩家,请检查是否拼写正确" + player_id = user_info["personaId"] + user_id = user_info["userId"] + player_name = player_display_name(user_info, player_name) + logger.info(f"QQ号:{qq_id},EA双ID:{player_id}-{user_id}") + + # 检测ea账号是否被绑定至其他qq(EA 三字段 UNIQUE,靠这里预检兜住) + bind_record = manager.get_user_by_ea_id(user_id) + if bind_record: + return ( + f"您的EA账号 {bind_record['ea_player_name']} 已经绑定到: " + f"{bind_record['qq_id']},请您确认绑定的EAID" + ) + + try: + record = manager.add_user(qq_id, player_name, player_id, user_id, "[]", "[]") + except sqlite3.IntegrityError as err: + logger.warning(f"绑定撞 UNIQUE 约束:{err}") + return "该 EA 账号已被绑定,请确认绑定的 EAID" + logger.info(f"插入数据返回结果:{record}") + if record > 0: + return f"您已经成功绑定至: {player_name}" + return "绑定失败,请稍后再试" + + +async def unbind(qq_id: str) -> str: + manager = UserManager() + user_record = manager.get_user_by_qq(qq_id) + if not user_record: + return "未查询到绑定记录" + info = await get_player_info_by_ea_id( + user_record["ea_player_id"], user_record["ea_user_id"], _PLATFORM + ) + name = player_display_name(info, user_record["ea_player_name"]) + rows = manager.delete_user_by_qq(qq_id) + if rows > 0: + return f"您已经与: {name} 成功解除绑定,如果你需要重新绑定请发送 修改绑定 EAID " + return "解绑失败,请稍后再试" + + +async def update_bind(qq_id: str, player_name: str) -> str: + manager = UserManager() + user_record = manager.get_user_by_qq(qq_id) + if not user_record: + return ( + f"未查询到绑定记录: {player_name},如果需要绑定," + f"请@机器人并发送 绑定 {player_name} " + ) + + player_name = (player_name or "").strip() + if not player_name: + return "请提供要换绑的 EAID,例如:修改绑定 YourEAID" + + user_info = await get_player_info_by_name(player_name, _PLATFORM) + if "errors" in user_info: + return "未找到玩家,请检查是否拼写正确" + player_id = user_info["personaId"] + user_id = user_info["userId"] + player_name = player_display_name(user_info, player_name) + logger.info(f"QQ号:{qq_id},EA双ID:{player_id}-{user_id}") + + # 与 bind 相同的占用预检;绑给同一个 QQ 时视为无操作继续 + bind_record = manager.get_user_by_ea_id(user_id) + if bind_record and str(bind_record["qq_id"]) != str(qq_id): + return ( + f"您的EA账号 {bind_record['ea_player_name']} 已经绑定到: " + f"{bind_record['qq_id']},请您确认绑定的EAID" + ) + + try: + record = manager.update_user(qq_id, player_name, player_id, user_id, "[]", "[]") + except sqlite3.IntegrityError as err: + logger.warning(f"换绑撞 UNIQUE 约束:{err}") + return "该 EA 账号已被绑定,请确认绑定的 EAID" + logger.info(f"修改数据返回结果:{record}") + if record > 0: + return f"您已经成功从:{user_record['ea_player_name']}修改绑定至: {player_name}" + return "修改绑定失败,请稍后再试" diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/__init__.py b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/__init__.py new file mode 100644 index 0000000..ff7c42d --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/__init__.py @@ -0,0 +1 @@ +"""取数层:外部数据源客户端(gametools / tracker.gg 浏览器通道 / BF2042)。""" diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/bf2042.py b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/bf2042.py new file mode 100644 index 0000000..dfe8323 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/bf2042.py @@ -0,0 +1,391 @@ +"""BF2042 战绩取数(tracker.gg,直连主通道 / 扩展桥兜底)。 + +取数调度全部走 ``tracker_edge.fetch_bf2042_segments``:主通道是 ``tracker_api`` +的无浏览器直连(curl_cffi + TRN Bearer 令牌),浏览器扩展桥仅作兜底;本模块 +只做"拿数据 → 防御式解析 → 卡片字段",对通道无感知。 + +响应结构实测: + +- 一次取数会带来 5 个关键响应: + - ``profile/{platform}/{name}`` → ``{"data": {platformInfo, segments:[overview, gamemode...]}}`` + - ``.../segments/weapon`` → ``{"data": [段数组]}`` + - ``.../segments/vehicle`` → ``{"data": [段数组]}`` + - ``.../segments/gadget`` → 装备(榜单视图用) + - ``.../segments/soldier`` → 兵种(卡片用) +- overview 里没有 SPM / 命中率 / 分数,所以卡片改展示真实存在的 + 「每分钟伤害」「场均击杀」。 + +解析全部防御式:字段缺失退化成占位符,不抛异常。 +""" + +import re + +from nonebot import logger + +from .tracker_edge import TrackerResult, fetch_bf2042_segments + +__all__ = [ + "get_bf2042_info", + "get_bf2042_list", + "get_bf2042_gamemodes", + "bf2042_list_title", +] + +_MISSING = "—" + +# 用户层用 "pc",tracker.gg 用平台 slug +_PLATFORM_ALIASES = { + "pc": "origin", + "origin": "origin", + "steam": "steam", + "xbl": "xbl", + "psn": "psn", +} + + +def _platform_slug(platform: str) -> str: + return _PLATFORM_ALIASES.get((platform or "").lower(), "origin") + + +# --------------------------------------------------------------------------- # +# 取值工具:全部容忍缺失 +# --------------------------------------------------------------------------- # +def _stat(stats: dict, *keys) -> dict | None: + """返回第一个命中的 stat。tracker 的 stat 形如 {"value":..,"displayValue":..}。""" + for key in keys: + entry = stats.get(key) + if isinstance(entry, dict): + return entry + if entry is not None: + return {"value": entry, "displayValue": entry} + return None + + +def _value(entry: dict | None): + return entry.get("value") if entry else None + + +def _display(entry: dict | None, default=_MISSING): + """优先用 displayValue(带单位/百分比,免去单位猜测)。""" + if not entry: + return default + display = entry.get("displayValue") + if display is not None: + return display + value = entry.get("value") + return default if value is None else value + + +def _number(entry: dict | None, default=_MISSING): + """数值型字段,取不到时给 0(计数类字段缺省即 0)。""" + value = _value(entry) + return default if value is None else value + + +def _segment(segments: list[dict], seg_type: str) -> dict: + for seg in segments or []: + if seg.get("type") == seg_type: + return seg + return {} + + +def _best_of(segments: list[dict], seg_type: str) -> dict: + """从同类 segment 里挑击杀最高的那个(武器/载具)。""" + candidates = [s for s in (segments or []) if s.get("type") == seg_type] + if not candidates: + return {} + + def _kills(seg: dict) -> float: + return _value(_stat(seg.get("stats") or {}, "kills")) or 0 + + return max(candidates, key=_kills) + + +def _name_of(seg: dict) -> str: + meta = seg.get("metadata") or {} + # metadata.name 常带尾随空格(如 "PKP-BP ") + name = meta.get("displayName") or meta.get("name") or "" + return name.strip() or _MISSING + + +# --------------------------------------------------------------------------- # +# 解析 +# --------------------------------------------------------------------------- # +def normalize_bf2042(profile: dict, extra_segments: list[dict] | None = None) -> dict: + """把 tracker.gg profile 响应整理成文字卡需要的字段。 + + extra_segments: 由 /segments/weapon、/segments/vehicle 另取的段,拼进来一起挑最佳。 + """ + data = (profile or {}).get("data") or {} + segments = list(data.get("segments") or []) + list(extra_segments or []) + platform_info = data.get("platformInfo") or {} + + overview = (_segment(segments, "overview").get("stats")) or {} + + wins = _value(_stat(overview, "wins")) or 0 + losses = _value(_stat(overview, "losses")) or 0 + matches = _value(_stat(overview, "matches")) or (wins + losses) + + best_weapon_seg = _best_of(segments, "weapon") + best_vehicle_seg = _best_of(segments, "vehicle") + + def _sub(seg: dict) -> dict: + stats = seg.get("stats") or {} + return { + "名称": _name_of(seg), + "类别": (seg.get("metadata") or {}).get("category") or _MISSING, + "击杀": _number(_stat(stats, "kills")), + "KPM": _value(_stat(stats, "killsPerMinute")) or _MISSING, + "时长": _display(_stat(stats, "timePlayed")), + "命中率": _display(_stat(stats, "shotsAccuracy")), + "爆头率": _display(_stat(stats, "headshotPercentage")), + "摧毁": _number(_stat(stats, "destroyed")), # 仅载具有 + } + + return { + "玩家名称": platform_info.get("platformUserHandle") or _MISSING, + "游玩平台": platform_info.get("platformSlug") or _MISSING, + "游戏等级": _display(_stat(overview, "level")), + "游玩时长": _display(_stat(overview, "timePlayed")), + "游玩场次": matches or _MISSING, + "对局胜率": _display(_stat(overview, "wlPercentage")), + "玩家K/D": _value(_stat(overview, "kdRatio")) or _MISSING, + "玩家KPM": _value(_stat(overview, "killsPerMinute")) or _MISSING, + # tracker 的 BF2042 overview 没有 SPM / 得分,用真实存在的这两项替代 + "每分钟伤害": _display(_stat(overview, "dmgPerMin")), + "场均击杀": _value(_stat(overview, "killsPerMatch")) or _MISSING, + "击杀总计": _number(_stat(overview, "kills")), + "游戏助攻": _number(_stat(overview, "assists")), + "救援数量": _number(_stat(overview, "revives")), + "爆头率": _display(_stat(overview, "headshotPercentage")), + "最佳武器": _sub(best_weapon_seg), + "最佳载具": _sub(best_vehicle_seg), + } + + +# --------------------------------------------------------------------------- # +# 榜单 / 模式视图 +# --------------------------------------------------------------------------- # +def _raw(entry: dict | None): + """原样取 value(比率类字段用,displayValue 偶尔缺失)。""" + value = _value(entry) + return _MISSING if value is None else value + + +# tracker.gg 偶尔漏译,metadata.name 会是 gad_open_equip_rpgv2 这种原始键 +_RAW_NAME_PREFIXES = ("gad_open_equip_", "gad_", "wep_", "veh_") + + +def _pretty_name(name: str) -> str: + if not name or name == _MISSING or not re.fullmatch(r"[a-z0-9_]+", name): + return name + for prefix in _RAW_NAME_PREFIXES: + if name.startswith(prefix): + name = name[len(prefix) :] + break + return name.replace("_", " ").upper() + + +# kind -> 标题 / 段名 / 排序字段 / (列名, stat 键, 取法) +_LIST_VIEWS: dict[str, dict] = { + "weapon": { + "标题": "武器", + "段": "weapon", + "排序": "kills", + "列": ( + ("击杀", "kills", _number), + ("KPM", "killsPerMinute", _raw), + ("命中率", "shotsAccuracy", _display), + ("爆头率", "headshotPercentage", _display), + ), + }, + "vehicle": { + "标题": "载具", + "段": "vehicle", + "排序": "kills", + "列": ( + ("击杀", "kills", _number), + ("KPM", "killsPerMinute", _raw), + ("摧毁", "destroyed", _number), + ("时长", "timePlayed", _display), + ), + }, + "gadget": { + "标题": "装备", + "段": "gadget", + "排序": "uses", # 装备大多 0 杀,使用次数才是主指标 + "列": ( + ("使用", "uses", _number), + ("击杀", "kills", _number), + ("部署", "deployments", _number), + ("时长", "timePlayed", _display), + ), + }, +} + +_GAMEMODE_COLUMNS = ( + ("场次", "matches", _number), + ("胜率", "wlPercentage", _display), + ("击杀", "kills", _number), + ("KPM", "killsPerMinute", _raw), +) + + +def _rows_of(segments: list[dict], columns, sort_key: str, top: int) -> list[dict]: + """按 sort_key 降序取前 top,摊平成「名称/标签/值列表」的渲染行。""" + + def rank(seg: dict) -> float: + return _value(_stat(seg.get("stats") or {}, sort_key)) or 0 + + rows = [] + for item in sorted(segments or [], key=rank, reverse=True)[:top]: + stats = item.get("stats") or {} + rows.append( + { + "名称": _pretty_name(_name_of(item)), + "标签": (item.get("metadata") or {}).get("category") or "", + "值": [str(fn(_stat(stats, key))) for _, key, fn in columns], + } + ) + return rows + + +def bf2042_list_title(kind: str) -> str: + """榜单中文标题(武器/载具/装备),指令层拼提示语用。""" + view = _LIST_VIEWS.get(kind) + return view["标题"] if view else "" + + +def normalize_bf2042_list(kind: str, segments: list[dict], top: int = 15) -> dict: + """把 weapon/vehicle/gadget 段整理成榜单卡字段(按主指标排序,取前 top)。""" + view = _LIST_VIEWS[kind] + return { + "标题": view["标题"], + "列名": [name for name, _, _ in view["列"]], + "行": _rows_of(segments, view["列"], view["排序"], top), + } + + +def normalize_bf2042_gamemodes(profile: dict, top: int = 15) -> dict: + """把 profile 里的 gamemode 段整理成榜单卡字段(按游玩时长排序)。""" + segments = ((profile or {}).get("data") or {}).get("segments") or [] + modes = [s for s in segments if s.get("type") == "gamemode"] + return { + "标题": "模式", + "列名": [name for name, _, _ in _GAMEMODE_COLUMNS], + "行": _rows_of(modes, _GAMEMODE_COLUMNS, "timePlayed", top), + } + + +# --------------------------------------------------------------------------- # +# 抓取(调度在 tracker_edge:直连 API 主通道 / 扩展桥兜底) +# --------------------------------------------------------------------------- # +def _handle(profile: dict | None, fallback: str) -> str: + info = ((profile or {}).get("data") or {}).get("platformInfo") or {} + return info.get("platformUserHandle") or fallback + + +def _failure(reason: str, detail: str) -> TrackerResult: + return TrackerResult(False, data=None, status=None, reason=reason, detail=detail) + + +async def _load_profile(slug: str, name: str, segments: tuple[str, ...] = ()): + """经调度层取数,返回 (profile 数据, 段字典, None)。 + + 段字典键:weapon / vehicle / gadget / soldier。一次取数全部拉齐, + 不做任何"按需补段"的额外请求。失败返回 (None, {}, 带 reason 结果)。 + """ + result = await fetch_bf2042_segments(name, slug) + if result.get("error"): + return ( + None, + {}, + _failure( + str(result["error"]), str(result.get("detail") or "tracker.gg 取数失败") + ), + ) + profile = result.get("profile") + if not isinstance(profile, dict) or not profile.get("data"): + logger.warning(f"BF2042 取到的 profile 为空: {name}") + return None, {}, _failure("not_found", "tracker.gg 未返回该玩家的数据") + seg_lists = { + "weapon": result.get("weapon") or [], + "vehicle": result.get("vehicle") or [], + "gadget": result.get("gadget") or [], + "soldier": result.get("soldier") or [], + } + return profile, seg_lists, None + + +async def get_bf2042_info(player: str, platform: str = "pc") -> TrackerResult: + """查询 BF2042 生涯数据,返回 TrackerResult(data 为生涯卡字段 dict)。 + + data 里同时带上原始段(``_profile``/``_weapons``/``_vehicles``/``_soldiers``), + 供新版卡片(``services.processing.build_bf2042_career_card``)构建完整槽位。 + """ + slug = _platform_slug(platform) + name = (player or "").strip() + if not name: + return TrackerResult(False, reason="error", detail="玩家名为空") + + profile, seg_lists, err = await _load_profile(slug, name) + if err is not None: + return err + + extra = seg_lists.get("weapon", []) + seg_lists.get("vehicle", []) + logger.info(f"BF2042 生涯取数成功: {name}(武器/载具段 {len(extra)} 条)") + data = normalize_bf2042(profile or {}, extra) + data["_profile"] = profile or {} + data["_weapons"] = seg_lists.get("weapon") or [] + data["_vehicles"] = seg_lists.get("vehicle") or [] + data["_soldiers"] = seg_lists.get("soldier") or [] + return TrackerResult(True, data=data, status=200, reason="ok") + + +async def get_bf2042_list(player: str, platform: str, kind: str) -> TrackerResult: + """查询武器 / 载具 / 装备榜,返回 TrackerResult(data 为榜单卡字段 dict)。 + + kind ∈ weapon / vehicle / gadget;数据全在页面一次加载的截获结果里。 + """ + slug = _platform_slug(platform) + name = (player or "").strip() + if not name: + return TrackerResult(False, reason="error", detail="玩家名为空") + view = _LIST_VIEWS.get(kind) + if view is None: + return TrackerResult(False, reason="error", detail=f"未知榜单类型 {kind}") + + segment = view["段"] + profile, seg_lists, err = await _load_profile(slug, name) + if err is not None: + return err + + rows_source = seg_lists.get(segment, []) + if not rows_source: + # 段拉取失败或该玩家确实没有这类数据(直连通道四段全取,桥兜底仅三段) + return TrackerResult( + False, + reason="error", + detail=f"当前通道暂无「{view['标题']}」数据", + ) + data = normalize_bf2042_list(kind, rows_source) + data["玩家名称"] = _handle(profile, name) + logger.info(f"BF2042 {data['标题']}榜取数成功: {name}({len(data['行'])} 条)") + return TrackerResult(True, data=data, status=200, reason="ok") + + +async def get_bf2042_gamemodes(player: str, platform: str = "pc") -> TrackerResult: + """查询各游戏模式明细(在 profile 响应的 gamemode 段里,不用额外请求)。""" + slug = _platform_slug(platform) + name = (player or "").strip() + if not name: + return TrackerResult(False, reason="error", detail="玩家名为空") + + profile, _, err = await _load_profile(slug, name) + if err is not None: + return err + + data = normalize_bf2042_gamemodes(profile or {}) + data["玩家名称"] = _handle(profile, name) + logger.info(f"BF2042 模式取数成功: {name}({len(data['行'])} 条)") + return TrackerResult(True, data=data, status=200, reason="ok") diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/gametools.py b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/gametools.py new file mode 100644 index 0000000..22fb864 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/gametools.py @@ -0,0 +1,132 @@ +"""EA 系取数(api.gametools.network)。 + +- `fetch_all_stats`:bf3/bf4/bf1/bfv/bf6 的 `/all` 战绩接口(by-id 与 by-name + 并发请求,谁先拿到有效数据用谁)。 +- `get_player_info_by_name` / `get_player_info_by_ea_id`:`bfglobal/player` 查人, + 绑定流程用。 +- `get_bf6_rank`:`bf6/profile` 取等级(playerCard.rank)。 + +gametools 是同步 requests 的部分一律 `asyncio.to_thread`,避免卡事件循环。 +""" + +import asyncio +import json + +import httpx +import requests +from nonebot import logger + +_API = "https://api.gametools.network" +_HEADERS = {"accept": "application/json"} + + +async def fetch_all_stats(game, player, player_id, user_id, platform): + """查 `{game}/all/`:by-id 与 by-name 并发,先成功(且无 errors)的胜出。""" + base = f"{_API}/{game}/all/?format_values=true" + url_by_id = ( + f"{base}&playerid={player_id}&oid={user_id}" + f"&platform={platform}&skip_battlelog=false&lang=en-us" + ) + url_by_name = ( + f"{base}&name={player}&platform={platform}&skip_battlelog=false&lang=en-us" + ) + + async with httpx.AsyncClient(timeout=10.0) as client: + # 创建两个并发任务 + task_id = asyncio.create_task(client.get(url_by_id)) + task_name = asyncio.create_task(client.get(url_by_name)) + + # 等待第一个完成的任务 + done, pending = await asyncio.wait( + [task_id, task_name], return_when=asyncio.FIRST_COMPLETED + ) + # 取消未完成的任务 + for task in pending: + task.cancel() + # 检查已完成的任务是否成功且无 errors + for task in done: + try: + response = await task + if response.status_code == 200: + data = response.json() + if "errors" not in data: + logger.info( + f"成功获取数据({'ID' if task is task_id else 'Name'}方式)" + ) + return data + except (httpx.RequestError, ValueError) as e: + logger.warning(f"请求或解析失败: {e}") + # 如果所有尝试都失败,返回错误 + return {"errors": ["未找到玩家"]} + + +async def _get_player_json(url: str) -> dict: + """requests 放线程池 + 超时;网络/解析失败统一降级成 {"errors": [...]}。""" + try: + response = await asyncio.to_thread( + requests.get, url, headers=_HEADERS, timeout=10 + ) + return json.loads(response.text) + except Exception as err: # noqa: BLE001 - 调用方按 errors 分支处理 + logger.warning(f"查询玩家信息失败: {err}") + return {"errors": [str(err)]} + + +def player_display_name(info: dict, fallback: str = "error") -> str: + """从 bfglobal/player 响应里取展示名。 + + 该接口实测有两种形态:一种给 personaName,另一种(同一玩家也会出现)只给 + name + nucleusId;漏掉 name 时卡片会把玩家名显示成 "error"(老代码的问题)。 + """ + return ( + info.get("personaName") + or info.get("name") + or info.get("displayName") + or info.get("username") + or fallback + ) + + +async def get_player_info_by_name(user_name, platform) -> dict: + url = ( + f"{_API}/bfglobal/player/?name={user_name}" + f"&platform={platform}&skip_battlelog=false" + ) + logger.info(f"查询用户请求url:{url}") + return await _get_player_json(url) + + +async def get_player_info_by_ea_id(player_id, user_id, platform) -> dict: + url = ( + f"{_API}/bfglobal/player/?playerid={player_id}&oid={user_id}" + f"&platform={platform}&skip_battlelog=false" + ) + return await _get_player_json(url) + + +async def get_bf6_rank(player, player_id, user_id, platform) -> int: + """取BF6等级:gametools profile 接口返回 playerProfiles[0].playerCard.rank""" + url = ( + f"{_API}/bf6/profile/?name={player}" + f"&playerid={player_id}&nucleus_id={user_id}&platform={platform}" + f"&skip_battlelog=true&lang=en-us" + ) + try: + response = await asyncio.to_thread( + requests.get, url, headers=_HEADERS, timeout=10 + ) + profiles = response.json().get("playerProfiles") or [] + except Exception as err: # noqa: BLE001 - 等级取不到就当 0,不打断整卡 + logger.warning(f"查询BF6等级失败: {err}") + return 0 + logger.info(f"BF6 profile 返回:{str(profiles)[:300]}") + + if not isinstance(profiles, list) or not profiles: + logger.info(f"未搜索到用户: {player}") + return 0 + + profile = profiles[0] if isinstance(profiles[0], dict) else {} + # 该接口的真实字段:playerCard.rank 是等级数字,rankName 是称号名 + rank = (profile.get("playerCard") or {}).get("rank") or 0 + logger.info(f"BF6等级: {rank}(称号:{profile.get('rankName')})") + return rank diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_api.py b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_api.py new file mode 100644 index 0000000..f678ad8 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_api.py @@ -0,0 +1,364 @@ +"""tracker.gg 直连取数通道(BF2042 主通道,无浏览器)。 + +2026-10-13 实测确立(与旧结论不同,以本文件为准): + +- api.tracker.gg 的 WAF 拦的是**没有登录凭据的匿名请求**(裸 curl/requests → + 403 "You've Been Blocked");带上 TRN 账号的 OIDC ``Bearer`` 令牌 + curl_cffi + 的 Chrome TLS/HTTP2 指纹后,profile 与各 segments 全部 200 放行 —— 无需浏览器、 + 无需 cf_clearance、无需 TRN-Api-Key。 +- 403 的另一种形态是 CF 挑战页("Just a moment…",access_token 过期时出现); + 与封禁页一样,处理方式都是强制刷新令牌后重试一次。 +- 404 = 玩家不存在(``CollectorResultStatus::NotFound``),确定性结果,不重试。 + +令牌链路(offline_access 刷新链,一次引导、持续自持): + +- 引导:从取数浏览器专用配置的 localStorage(leveldb WAL,明文)里读出 + ``auth.tokens`` 的 refresh_token;仅在令牌文件缺失/刷新失败时兜底执行, + 日常运行完全不碰浏览器。 +- 运行时:access_token 将过期或遇 401/403 时,POST + ``account.tracker.gg/connect/token``(refresh_token 授权,公共客户端, + 无需 secret)换新;TRN 每次刷新都**轮换** refresh_token,新值原子落盘 —— + 每次刷新都在续命,持续使用即永不过期。 +- 唯一状态文件:``hexi/data/nonebot_plugin_bf_bot/trn_tokens.json``。 + +顶层不 import 插件包相对路径(tests 与 debug 探针按文件裸加载本模块), +paths / config 的读取一律放函数内(与 tracker_bridge 同约定)。 +""" + +from __future__ import annotations + +import asyncio +import json +import os +import re +import threading +import time +from pathlib import Path +from urllib.parse import quote + +from curl_cffi import requests as cr +from curl_cffi.requests.exceptions import RequestException, Timeout +from nonebot import logger + +__all__ = [ + "TrnAuthError", + "fetch_bf2042_direct", +] + +_TOKEN_URL = "https://account.tracker.gg/connect/token" +_CLIENT_ID = "0af2521e-33e9-4d40-836e-6f865cb98870" +_SCOPE = "openid profile account api events offline_access" + +_API_BASE = "https://api.tracker.gg/api/v2/bf2042/standard/profile" +_SEGMENTS = ("weapon", "vehicle", "gadget", "soldier") + +_EXPIRY_MARGIN_S = 120.0 # access_token 剩余寿命低于该值即提前刷新 +_HTTP_TIMEOUT = 30 # 单请求超时(秒) +_ATTEMPT_TIMEOUT_S = 75.0 # 一次完整取数(profile + 4 段)的整体上限 + +_token_lock = threading.Lock() + + +class TrnAuthError(RuntimeError): + """TRN 登录态(令牌链)不可用:没有 refresh_token 或刷新被拒。""" + + +# --------------------------------------------------------------------------- # +# 令牌存取 +# --------------------------------------------------------------------------- # +def _token_path() -> Path: + from ...paths import TRN_TOKENS_PATH + + return Path(TRN_TOKENS_PATH) + + +def _read_token_file() -> dict: + try: + data = json.loads(_token_path().read_text(encoding="utf-8")) + except FileNotFoundError: + return {} + except Exception as exc: # noqa: BLE001 - 坏文件不致命,当作没有 + logger.warning(f"BF2042 令牌文件不可读({exc}),按缺失处理") + return {} + return data if isinstance(data, dict) else {} + + +def _write_token_file(tokens: dict) -> None: + path = _token_path() + try: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".json.tmp") + tmp.write_text( + json.dumps(tokens, ensure_ascii=False, indent=1), encoding="utf-8" + ) + os.replace(tmp, path) + except Exception as exc: # noqa: BLE001 - 落盘失败只影响下次冷启动 + logger.warning(f"BF2042 令牌文件写入失败:{exc}") + + +def _token_fresh(tokens: dict, now: float | None = None) -> bool: + access = tokens.get("access_token") + expires_at = tokens.get("expires_at") + if not access or not isinstance(expires_at, (int, float)): + return False + return float(expires_at) > (now if now is not None else time.time()) + + +def _normalize_token_payload(payload: dict, prev_refresh: str) -> dict: + """令牌端点响应 → 落盘格式;未轮换 refresh_token 时沿用旧值。""" + return { + "access_token": str(payload.get("access_token") or ""), + "refresh_token": str(payload.get("refresh_token") or prev_refresh or ""), + "expires_at": time.time() + + float(payload.get("expires_in") or 3600) + - _EXPIRY_MARGIN_S, + "updated_at": time.strftime("%Y-%m-%d %H:%M:%S"), + } + + +def _do_refresh(refresh_token: str) -> dict: + """用 refresh_token 换新令牌(公共客户端,无 secret);失败抛 TrnAuthError。""" + try: + with cr.Session(impersonate="chrome") as session: + resp = session.post( + _TOKEN_URL, + data={ + "grant_type": "refresh_token", + "client_id": _CLIENT_ID, + "refresh_token": refresh_token, + "scope": _SCOPE, + }, + timeout=_HTTP_TIMEOUT, + ) + except RequestException as exc: + raise TrnAuthError(f"令牌端点请求失败:{exc}") from exc + if resp.status_code != 200: + raise TrnAuthError(f"令牌刷新被拒:HTTP {resp.status_code} {resp.text[:120]}") + payload = resp.json() + if not payload.get("access_token"): + raise TrnAuthError("令牌刷新响应缺少 access_token") + return _normalize_token_payload(payload, refresh_token) + + +# --------------------------------------------------------------------------- # +# 浏览器档案引导(localStorage leveldb,明文 WAL) +# --------------------------------------------------------------------------- # +_STORAGE_KEY = "auth.tokens" + + +def _decode_tokens_blob(raw: bytes, key: str = _STORAGE_KEY) -> dict | None: + """从 leveldb 原始字节里找 localStorage 的 auth.tokens 并解出 JSON。 + + key 与值各有两种编码可能(ASCII/UTF-16LE、Latin-1/UTF-16LE);.log(WAL) + 里是明文,块压缩后的 .ldb 解不出(返回 None,属预期)。 + """ + decoder = json.JSONDecoder() + markers = {key.encode(), key.encode("utf-16-le")} + for marker in markers: + for match in re.finditer(re.escape(marker), raw): + window = raw[match.end() : match.end() + 8000] + for enc, head in (("utf-16-le", b'{\x00"\x00'), ("latin-1", b'{"')): + idx = window.find(head) + if idx == -1: + continue + try: + obj, _ = decoder.raw_decode( + window[idx:].decode(enc, errors="replace") + ) + except Exception: # noqa: BLE001 - 相邻记录干扰,继续找下一处 + continue + if isinstance(obj, dict): + return obj + return None + + +def _extract_tokens_from_dir(leveldb_dir: Path) -> dict | None: + """扫 leveldb 目录里的日志文件,返回带 refresh_token 的令牌对象。""" + if not leveldb_dir.is_dir(): + return None + candidates = sorted(leveldb_dir.glob("*.log")) + sorted(leveldb_dir.glob("*.ldb")) + for path in candidates: + try: + raw = path.read_bytes() + except Exception: # noqa: BLE001 - 浏览器正在写,读不到就跳过 + continue + obj = _decode_tokens_blob(raw) + if obj and obj.get("refresh_token"): + return obj + return None + + +def _bootstrap_tokens() -> dict | None: + """兜底引导:从取数浏览器专用配置的 localStorage 里找登录态。""" + try: + from ...config import edge_profile_dir + + profile_dir = Path(edge_profile_dir()) + except Exception as exc: # noqa: BLE001 - 配置不可用时不阻塞 + logger.warning(f"BF2042 令牌引导:读配置失败({exc})") + return None + found = _extract_tokens_from_dir( + profile_dir / "Default" / "Local Storage" / "leveldb" + ) + if found: + logger.info("BF2042 令牌引导:已从取数浏览器配置提取 refresh_token") + return found + + +def ensure_access_token(force: bool = False) -> str: + """取一个可用的 access_token;需要时走刷新链并落盘轮换后的令牌。""" + with _token_lock: + tokens = _read_token_file() + if not force and _token_fresh(tokens): + return str(tokens["access_token"]) + + candidates: list[str] = [] + if tokens.get("refresh_token"): + candidates.append(str(tokens["refresh_token"])) + boot = _bootstrap_tokens() or {} + if boot.get("refresh_token") and boot["refresh_token"] not in candidates: + candidates.append(str(boot["refresh_token"])) + + last_error: Exception | None = None + for refresh_token in candidates: + try: + fresh = _do_refresh(refresh_token) + except TrnAuthError as exc: + last_error = exc + logger.warning(f"BF2042 令牌刷新失败({exc}),换下一个来源") + continue + _write_token_file(fresh) + logger.info("BF2042 直连:TRN 令牌已刷新(轮换续期)") + return str(fresh["access_token"]) + raise TrnAuthError( + f"无可用 TRN 登录态:{last_error or '令牌文件与取数浏览器配置里都没有 refresh_token'}" + ) + + +# --------------------------------------------------------------------------- # +# 取数 +# --------------------------------------------------------------------------- # +def _empty_result() -> dict: + return { + "profile": None, + "weapon": [], + "vehicle": [], + "gadget": [], + "soldier": [], + "error": None, + "detail": "", + } + + +def _api_headers(access_token: str) -> dict[str, str]: + return { + "Authorization": f"Bearer {access_token}", + "Accept": "application/json, text/plain, */*", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Origin": "https://tracker.gg", + "Referer": "https://tracker.gg/", + } + + +def _errors_message(payload) -> str: + """tracker.gg 错误响应 → 可读文案(形如 {"errors":[{"message":...}]})。""" + if isinstance(payload, dict): + errors = payload.get("errors") + if isinstance(errors, list) and errors and isinstance(errors[0], dict): + return str(errors[0].get("message") or "") + return "" + + +def _get_json(session, url: str, headers: dict): + """GET 并尽量解 JSON;非 JSON(封禁页/挑战页)返回 None。""" + resp = session.get(url, headers=headers, timeout=_HTTP_TIMEOUT) + try: + payload = resp.json() + except Exception: # noqa: BLE001 - 错误页是 HTML,属预期 + payload = None + return resp.status_code, payload + + +def _fetch_sync(name: str, platform: str, *, force_refresh: bool = False) -> dict: + """同步完成一次取数:profile + 各 segments;返回调用方契约字典。""" + result = _empty_result() + try: + token = ensure_access_token(force=force_refresh) + except TrnAuthError as exc: + result["error"], result["detail"] = "auth", str(exc) + return result + + base = f"{_API_BASE}/{platform}/{quote(str(name))}" + headers = _api_headers(token) + try: + with cr.Session(impersonate="chrome") as session: + status, payload = _get_json(session, base, headers) + if status in (401, 403): + result["error"] = "auth" + result["detail"] = f"HTTP {status}(令牌被拒或触发风控)" + return result + if status == 404: + result["error"] = "not_found" + result["detail"] = _errors_message(payload) or "tracker.gg 未找到该玩家" + return result + if ( + status != 200 + or not isinstance(payload, dict) + or not payload.get("data") + ): + result["error"] = "error" + result["detail"] = f"profile 响应异常:HTTP {status}" + return result + result["profile"] = payload + + for seg in _SEGMENTS: + s_status, s_payload = _get_json( + session, f"{base}/segments/{seg}", headers + ) + if ( + s_status == 200 + and isinstance(s_payload, dict) + and isinstance(s_payload.get("data"), list) + ): + result[seg] = s_payload["data"] + else: + logger.warning( + f"BF2042 直连:{name} 的 {seg} 段取数失败(HTTP {s_status})" + ) + except Timeout as exc: + result["error"], result["detail"] = "timeout", f"直连 tracker.gg 超时:{exc}" + except RequestException as exc: + result["error"], result["detail"] = "error", f"直连 tracker.gg 请求失败:{exc}" + return result + + +async def _run_attempt(name: str, platform: str, *, force_refresh: bool) -> dict: + try: + return await asyncio.wait_for( + asyncio.to_thread(_fetch_sync, name, platform, force_refresh=force_refresh), + timeout=_ATTEMPT_TIMEOUT_S, + ) + except asyncio.TimeoutError: + result = _empty_result() + result["error"], result["detail"] = "timeout", "直连 tracker.gg 超时(整体)" + return result + + +async def fetch_bf2042_direct(name: str, platform: str = "origin") -> dict: + """无浏览器直连取数(主通道)。 + + 返回契约(与扩展桥捕获结果的归一化一致):: + + {"profile": dict | None, "weapon"/"vehicle"/"gadget"/"soldier": list, + "error": None | "auth" | "not_found" | "timeout" | "error", + "detail": str} + + 遇鉴权失败(401/403,含令牌过期触发的 CF 挑战)强制刷新令牌重试一次。 + """ + result = await _run_attempt(name, platform, force_refresh=False) + if result.get("error") == "auth": + logger.warning( + f"BF2042 直连鉴权失败({result.get('detail')}),强制刷新令牌后重试" + ) + result = await _run_attempt(name, platform, force_refresh=True) + return result diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_bridge.py b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_bridge.py new file mode 100644 index 0000000..89f84ca --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_bridge.py @@ -0,0 +1,289 @@ +"""浏览器扩展捕获桥:WS 服务端 + 取数任务客户端(BF2042 扩展通道)。 + +配套 ``browser_ext/``(Edge/Chromium MV3 扩展,见其 README):扩展在浏览器里被动 +捕获 tracker.gg 页面自己发出的 API 响应,主动连本服务端回传;bot 侧发"取数任务", +扩展负责真实访问(首页 → 站内跳转)并把响应流式送回。 + +设计约束(照插件测试/探针惯例): + +- 顶层不 import 插件包相对路径(``tests/test_bf_bot_helpers.py`` 与 + ``debug/bf2042_bridge_probe.py`` 都按文件裸加载本模块);配置读取放在函数内。 +- 不 import ``tracker_edge``(避免成环):期待 URL 表(want)与页面地址由调用方传入, + 响应归一化由调用方 ``tracker_edge._rollup`` 负责。 + +协议(与扩展 ``background.js`` 对齐): + bot→ext {"type":"fetch","id","player","platform","profileUrl","apiBase"} + ext→bot {"type":"capture","id": 任务id或null(被动),"key","url","status","body"} + ext→bot {"type":"job_done"/"job_error",...};{"type":"hello",...};{"type":"ping"} +""" + +from __future__ import annotations + +import asyncio +import json +import time +import uuid +from collections.abc import Callable + +from aiohttp import WSMsgType, web +from nonebot import logger + +__all__ = [ + "BridgeServer", + "ext_connected", + "fetch_via_extension", + "start_bridge_server", + "stop_bridge_server", +] + +_REQUIRED_KEYS = ("profile", "weapon", "vehicle") # 齐了收工;soldier 有宽限窗口 +_GRACE_S = 2.5 # 迟到响应(soldier 常在任务收工后 0~1s 才到)的收集宽限 +_WS_MAX_MSG = 16 * 1024 * 1024 # 捕获响应可达几百 KB,放大消息上限 + + +def _norm(url: str) -> str: + """归一化 URL:去掉 query 和末尾斜杠(与 tracker_edge._norm 同规则)。""" + return str(url).split("?", 1)[0].rstrip("/") + + +class _Collector: + """单次任务的捕获收集器(纯逻辑,可单测;计时用 time.monotonic)。""" + + def __init__(self, name: str, platform: str, want: dict[str, str]): + self.name = name + self.platform = platform + # URL(归一化) → key;feed 按 URL 直接命中 + self.want = {_norm(url): key for key, url in want.items()} + self.captured: dict[str, object] = {} + self.error: str | None = None + self.detail = "" + self.done_at: float | None = None # 必收键齐(或收到 job_done)的时刻 + + def feed(self, url: str, body: object) -> str | None: + """喂一条捕获;命中想要的 URL 且未收过则记录,返回命中的 key。""" + key = self.want.get(_norm(url)) + if key is None or key in self.captured: + return None + self.captured[key] = body + if self.done_at is None and all(k in self.captured for k in _REQUIRED_KEYS): + self.done_at = time.monotonic() + return key + + def mark_done(self) -> None: + """扩展报告 job_done:开始宽限计时(通常 feed 已先触发)。""" + if self.done_at is None: + self.done_at = time.monotonic() + + @property + def ready(self) -> bool: + """必收键齐且宽限窗口已过,可以收工。""" + if self.done_at is None: + return False + if not all(k in self.captured for k in _REQUIRED_KEYS): + return False + return time.monotonic() - self.done_at >= _GRACE_S + + +class BridgeServer: + """扩展桥 WS 服务端(仅回环;token 校验;配合 bot 侧单飞锁,任务在途至多一个)。""" + + def __init__( + self, + port: int, + token_getter: Callable[[], str], + *, + host: str = "127.0.0.1", + ): + self.port = port + self.host = host + self._token_getter = token_getter + self._runner: web.AppRunner | None = None + self._ws: web.WebSocketResponse | None = None + self._jobs: dict[str, _Collector] = {} + + # ------------------------------------------------------------------ 生命周期 + async def start(self) -> None: + app = web.Application() + app.router.add_get("/", self._handler) + self._runner = web.AppRunner(app, access_log=None) + await self._runner.setup() + await web.TCPSite(self._runner, self.host, self.port).start() + + async def stop(self) -> None: + if self._ws is not None and not self._ws.closed: + await self._ws.close() + if self._runner is not None: + await self._runner.cleanup() + self._runner = None + + @property + def connected(self) -> bool: + return self._ws is not None and not self._ws.closed + + # ------------------------------------------------------------------ 连接 + async def _handler(self, request: web.Request) -> web.StreamResponse: + if request.query.get("token") != self._token_getter(): + return web.Response(status=403, text="bad token") + ws = web.WebSocketResponse(max_msg_size=_WS_MAX_MSG, heartbeat=25.0) + await ws.prepare(request) + old = self._ws + self._ws = ws # 新连接顶掉旧连接(扩展 SW 重启/重载时常见) + if old is not None and not old.closed: + await old.close() + logger.info("BF2042 扩展桥:扩展已连接") + try: + async for msg in ws: + if msg.type == WSMsgType.TEXT: + try: + data = json.loads(msg.data) + except ValueError: + continue + if isinstance(data, dict): + self._on_message(data) + elif msg.type == WSMsgType.ERROR: + logger.warning(f"BF2042 扩展桥 WS 异常:{ws.exception()}") + finally: + if self._ws is ws: # 被新连接顶掉的旧连接不动任务状态 + self._ws = None + for collector in self._jobs.values(): + if collector.error is None: + collector.error, collector.detail = ( + "ext_offline", + "扩展连接断开", + ) + logger.info("BF2042 扩展桥:扩展断开") + return ws + + def _on_message(self, msg: dict) -> None: + mtype = msg.get("type") + if mtype == "capture": + body = msg.get("body") + if body is None: + return + url = str(msg.get("url") or "") + job_id = msg.get("id") + if job_id: + collector = self._jobs.get(str(job_id)) + if collector is not None: + collector.feed(url, body) + else: + # 被动捕获(人工浏览 / 任务收工后迟到的 soldier):喂给所有在途任务 + for collector in self._jobs.values(): + collector.feed(url, body) + elif mtype == "job_done": + collector = self._jobs.get(str(msg.get("id"))) + if collector is not None: + collector.mark_done() + elif mtype == "job_error": + collector = self._jobs.get(str(msg.get("id"))) + if collector is not None and collector.error is None: + collector.error = "error" + collector.detail = str(msg.get("detail") or "扩展取数失败") + elif mtype == "hello": + logger.info(f"BF2042 扩展桥:{msg.get('name')} v{msg.get('version')} 报到") + # ping 等其余消息忽略(aiohttp heartbeat 负责链路保活) + + # ------------------------------------------------------------------ 任务 + async def fetch( + self, + *, + name: str, + platform: str, + want: dict[str, str], + page_url: str, + api_base: str, + wait_s: float = 120.0, + ) -> tuple[dict, str | None, str]: + """下发一次取数任务并等待捕获,返回 (captured, error, detail)。""" + ws = self._ws + if ws is None or ws.closed: + return {}, "ext_offline", "浏览器扩展未连接" + job_id = uuid.uuid4().hex[:12] + collector = _Collector(name, platform, want) + self._jobs[job_id] = collector + try: + await ws.send_json( + { + "type": "fetch", + "id": job_id, + "player": name, + "platform": platform, + "profileUrl": page_url, + "apiBase": api_base, + } + ) + except Exception as e: # noqa: BLE001 - 发送失败按离线处理 + self._jobs.pop(job_id, None) + return {}, "ext_offline", f"发送任务失败:{e}" + + deadline = time.monotonic() + max(wait_s, 1.0) + while True: + if collector.error is not None or collector.ready: + break + if time.monotonic() >= deadline: + break + if self._ws is not ws or ws.closed: + if collector.error is None: + collector.error, collector.detail = "ext_offline", "扩展连接断开" + break + await asyncio.sleep(0.2) + self._jobs.pop(job_id, None) + return collector.captured, collector.error, collector.detail + + +# --------------------------------------------------------------------------- # +# 模块级单例 + nonebot 启停钩子(由插件 __init__.py 注册) +# --------------------------------------------------------------------------- # +_bridge: BridgeServer | None = None + + +async def start_bridge_server() -> None: + """按配置启动扩展桥;端口占用等失败只记日志,不阻断 bot 启动。""" + global _bridge + if _bridge is not None: + return + from ...config import ext_ws_port, ext_ws_token + + bridge = BridgeServer(port=ext_ws_port(), token_getter=ext_ws_token) + try: + await bridge.start() + except OSError as e: # noqa: BLE001 - 端口被占/无权限时降级 + logger.error(f"BF2042 扩展桥启动失败({e});扩展通道不可用") + return + _bridge = bridge + logger.info(f"BF2042 扩展桥已监听 ws://127.0.0.1:{bridge.port}") + + +async def stop_bridge_server() -> None: + global _bridge + if _bridge is not None: + await _bridge.stop() + _bridge = None + logger.info("BF2042 扩展桥已停止") + + +def ext_connected() -> bool: + """扩展当前是否已连上(排障/状态查询用)。""" + return _bridge is not None and _bridge.connected + + +async def fetch_via_extension( + *, + name: str, + platform: str, + want: dict[str, str], + page_url: str, + api_base: str, + wait_s: float = 120.0, +) -> tuple[dict, str | None, str]: + """经模块单例下发取数任务;服务端未启动时直接返回 ext_offline。""" + if _bridge is None: + return {}, "ext_offline", "扩展桥服务端未启动" + return await _bridge.fetch( + name=name, + platform=platform, + want=want, + page_url=page_url, + api_base=api_base, + wait_s=wait_s, + ) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_edge.py b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_edge.py new file mode 100644 index 0000000..3c81758 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/tracker_edge.py @@ -0,0 +1,180 @@ +"""tracker.gg 取数总调度(BF2042):直连 API 主通道 → 扩展桥兜底。 + +**主通道 = 直连(无浏览器)**:``tracker_api.fetch_bf2042_direct`` —— curl_cffi +Chrome 指纹 + TRN OIDC Bearer 令牌直连 api.tracker.gg。实测(2026-10-13)原先 +"You've Been Blocked" 的根因是**请求里没有登录凭据**,不是"直连"本身;旧结论 +"绝不主动 fetch API"就此修正(详见 ``tracker_api`` 模块文档)。 + +**兜底 = 浏览器扩展桥**(``tracker_bridge`` + ``browser_ext/``):仅在直连失败 +(auth / timeout / error)时启用,保持原行为不变 —— 扩展常驻浏览器主动连 WS, +bot 只发任务、只收回传,零调试端口。``not_found`` 是确定性结果,不重试也不兜底。 + +节流(风控保护,都别撤):直连通道自身是正常 API 调用,3s 轻节流;回退扩展桥 +会触发真实页面加载,保留 20s 节流。并发查询由单飞锁串行化。 + +配置项(Web 管理台可改):``bf_ext_ws_port``、``bf_ext_ws_token``、 +``bf_edge_profile_dir``(兜底桥与令牌引导诊断用)。 +""" + +from __future__ import annotations + +import asyncio +import time +from dataclasses import dataclass +from urllib.parse import quote + +from nonebot import logger + +__all__ = [ + "TrackerResult", + "fetch_bf2042_segments", + "user_action_hint", +] + +_API = "https://api.tracker.gg/api/v2/bf2042/standard" + +# 单飞 + 节流:并发查询串行化;直连通道轻节流,扩展桥通道保持 20s +# (实测:短时间连续加载页面会触发 Cloudflare 风控,连首页都开始挑战) +_fetch_lock = asyncio.Lock() +_MIN_INTERVAL_S = 20.0 +_DIRECT_MIN_INTERVAL_S = 3.0 +_last_fetch_monotonic: float = 0.0 +_last_direct_monotonic: float = 0.0 + + +@dataclass +class TrackerResult: + """抓取结果,携带失败原因便于上层给用户正确提示。 + + reason ∈ ok / ext_offline / timeout / not_found / error + """ + + ok: bool + data: object = None + status: int | None = None + reason: str = "ok" + detail: str = "" + + +def user_action_hint() -> str: + """给用户的自救指引(控制台里贴一次,CF 挑战/登录过期时人肉过一下即可)。""" + from ...config import edge_profile_dir + + return ( + "请打开取数浏览器(专用配置)访问 tracker.gg 手动完成验证/登录一次:" + f' msedge.exe --user-data-dir="{edge_profile_dir()}" https://tracker.gg/' + ) + + +def _norm(url: str) -> str: + """归一化 URL:去掉 query 和末尾斜杠,用于响应匹配。""" + return url.split("?", 1)[0].rstrip("/") + + +def _segment_urls(name: str) -> dict[str, str]: + """页面加载会发出的关键请求(URL 与页面自身发的一致,带尾部 ?)。""" + quoted = quote(name) + base = f"{_API}/profile/origin/{quoted}" + return { + "profile": f"{base}?", + "weapon": f"{base}/segments/weapon?", + "vehicle": f"{base}/segments/vehicle?", + "soldier": f"{base}/segments/soldier?", + } + + +def _page_url(name: str, platform: str) -> str: + """tracker.gg 的 profile 页面地址(站内跳转的目标)。""" + return f"https://tracker.gg/bf2042/profile/{platform}/{quote(name)}/overview" + + +def _rollup(captured: dict, err: str | None = None, detail: str = "") -> dict: + """把截获的响应归一化成调用方契约。 + + 返回 {"profile": {...} | None, "weapon": [...], "vehicle": [...], + "soldier": [...], "error": str | None, "detail": str} + """ + + def _list_of_segments(seg) -> list: + if isinstance(seg, list): + return seg + if isinstance(seg, dict): + data = seg.get("data") + return data if isinstance(data, list) else [] + return [] + + profile = captured.get("profile") + if err is None and (not isinstance(profile, dict) or not profile.get("data")): + err, detail = "error", "未截获到 profile 数据" + + return { + "profile": profile if err is None else None, + "weapon": _list_of_segments(captured.get("weapon")), + "vehicle": _list_of_segments(captured.get("vehicle")), + "gadget": _list_of_segments(captured.get("gadget")), + "soldier": _list_of_segments(captured.get("soldier")), + "error": err, + "detail": detail, + } + + +async def fetch_bf2042_segments( + name: str, + platform: str = "origin", + *, + wait_s: float = 120.0, +) -> dict: + """BF2042 取数总调度:直连 API(主)→ 扩展桥(兜底)。 + + 返回 {"profile": {...} | None, "weapon"/"vehicle"/"gadget"/"soldier": [...], + "error": str | None, "detail": str} + + 直连通道见 ``tracker_api``(无浏览器,3s 轻节流);失败(auth / timeout / + error)时回退扩展桥(20s 节流,需取数浏览器在线,``wait_s`` 为桥的等待窗口), + 两者原因都会写进 ``detail`` 便于排查。``not_found`` 是确定性结果,不重试。 + """ + global _last_fetch_monotonic, _last_direct_monotonic + + async with _fetch_lock: + # ---- 主通道:直连 API(轻节流) ---- + delta = time.monotonic() - _last_direct_monotonic + if _last_direct_monotonic and delta < _DIRECT_MIN_INTERVAL_S: + await asyncio.sleep(_DIRECT_MIN_INTERVAL_S - delta) + _last_direct_monotonic = time.monotonic() + + from . import tracker_api + + direct = await tracker_api.fetch_bf2042_direct(name, platform) + if direct.get("error") is None: + return _rollup(direct) + if direct.get("error") == "not_found": + return _rollup(direct, "not_found", str(direct.get("detail") or "")) + + logger.warning( + f"BF2042 直连不可用({direct.get('error')}:{direct.get('detail')})," + "回退扩展桥" + ) + + # ---- 兜底:扩展桥(需要取数浏览器在线) ---- + delta = time.monotonic() - _last_fetch_monotonic + if _last_fetch_monotonic and delta < _MIN_INTERVAL_S: + await asyncio.sleep(_MIN_INTERVAL_S - delta) + _last_fetch_monotonic = time.monotonic() + + from . import tracker_bridge + + want = _segment_urls(name) + captured, err, detail = await tracker_bridge.fetch_via_extension( + name=name, + platform=platform, + want=want, + page_url=_page_url(name, platform), + api_base=want["profile"][:-1], # 去掉尾部 "?" + wait_s=wait_s, + ) + if err is None and not captured: + err, detail = "timeout", "等待页面数据超时" + rolled = _rollup(captured, err, detail) + if rolled["error"] is not None: + rolled["detail"] = f"{rolled['detail']};直连失败:{direct.get('detail')}" + return rolled diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/processing.py b/hexi/plugins/nonebot_plugin_bf_bot/services/processing.py new file mode 100644 index 0000000..76ad937 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/processing.py @@ -0,0 +1,301 @@ +"""战绩加工:最佳武器/载具、载具摧毁数、六维雷达数据与称号评级。""" + +from functools import reduce + +from nonebot import logger + +from ..utils.params import SPECIALIST_NAMES, interval_table, round_data + + +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_best_classes(player_stat): + classes_data = player_stat["classes"] + data = [obj for obj in classes_data if obj.get("className") != "All"] + best_classes = max(data, key=lambda x: x["kills"]) + best_classes_name = best_classes["className"] + return best_classes_name + + +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"] + if game == "bf3": + headshots = float(0) + else: + headshots = float(player_stat["headshots"].replace("%", "")) + spm = player_stat["scorePerMinute"] + revives = player_stat["revives"] + + apr = kill_assists / match_played + rpr = revives / 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"\n生涯KD比值:{ratio_kd}\n生涯Kpm比值:{ratio_kpm}\n生涯准度比值:{ratio_hs}" + f"\n场均救援比值:{ratio_rpr}\n场均助攻比值:{ratio_apr}\n分钟得分比值:{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"\n血腥度:{player_blood} 比值:{ratio_blood}" + f"\n贡献度:{player_dedication} 比值:{ratio_dedication}" + f"\n全能度:{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} 不在任何区间内") + + +# --------------------------------------------------------------------------- # +# BF2042 生涯卡数据(旧 project 卡片槽位 → tracker.gg 数据映射) +# --------------------------------------------------------------------------- # +_PLACEHOLDER = "–" + + +def _seg_stat(seg: dict, *keys): + """从段 stats 取第一个命中的显示值(displayValue 优先)。""" + stats = (seg or {}).get("stats") or {} + for key in keys: + entry = stats.get(key) + if isinstance(entry, dict): + dv = entry.get("displayValue") + if dv is not None: + return dv + v = entry.get("value") + if v is not None: + return v + elif entry is not None: + return entry + return _PLACEHOLDER + + +def _seg_value(seg: dict, *keys) -> float: + stats = (seg or {}).get("stats") or {} + for key in keys: + entry = stats.get(key) + if isinstance(entry, dict) and entry.get("value") is not None: + return float(entry["value"]) + return 0.0 + + +def _fmt_minutes_hours(minutes: float) -> str: + """tracker timePlayed(分钟) → "X.XX" 小时字符串(旧卡习惯)。""" + try: + return f"{float(minutes) / 60:.2f}" + except (TypeError, ValueError): + return _PLACEHOLDER + + +def _s_level(level) -> str: + """等级 S 前缀:tracker 的 298 → 'S199'(与旧 gametools 一致)。""" + try: + lv = int(level) + except (TypeError, ValueError): + return str(level) + return f"S{lv - 99}" if lv - 99 > 0 else str(lv) + + +def _top_rows(segments: list[dict], top: int) -> list[dict]: + """武器/载具段 → 卡片 TOP 行(按击杀降序)。""" + ranked = sorted(segments or [], key=lambda s: _seg_value(s, "kills"), reverse=True) + rows = [] + for seg in ranked[:top]: + meta = seg.get("metadata") or {} + rows.append( + { + "名称": (meta.get("name") or "").strip() or _PLACEHOLDER, + "类别": meta.get("category") or "", + "击杀": _seg_stat(seg, "kills"), + "爆头率": _seg_stat(seg, "headshotPercentage"), + "命中率": _seg_stat(seg, "shotsAccuracy"), + "KPM": _seg_stat(seg, "killsPerMinute"), + "时长": _fmt_minutes_hours(_seg_value(seg, "timePlayed")), + "摧毁": _seg_stat(seg, "destroyedWith"), + # 原始值给排序/调试用 + "_kills": _seg_value(seg, "kills"), + } + ) + return rows + + +def build_bf2042_career_card( + profile: dict, + weapon_segs: list[dict] | None, + vehicle_segs: list[dict] | None, + soldier_segs: list[dict] | None = None, +) -> dict: + """tracker.gg 数据 → 旧卡片字段契约(dev/bf2042 的 data 结构)。 + + 字段名沿用旧版(userName/killDeath/infantryKillDeath/secondsPlayed/ + dividedKills/humanPrecentage/weapons[].headshots 等),渲染层照旧坐标绘制; + tracker 没有的数据(命中率/mvp/标记敌人数/处决等)填占位符 "–"。 + """ + data = (profile or {}).get("data") or {} + segments = data.get("segments") or [] + overview = next((s for s in segments if s.get("type") == "overview"), {}) + info = data.get("platformInfo") or {} + + kills = _seg_value(overview, "kills") + kd = _seg_value(overview, "kdRatio") + kpm = _seg_value(overview, "killsPerMinute") + human_kills = _seg_value(overview, "humanKills") + human_per = round(100.0 * human_kills / kills, 1) if kills else 0.0 + ratio = (human_kills / kills) if kills else 0.0 + + level = _seg_stat(overview, "level") + try: + level_s = int(level) - 99 + level = f"S {level_s:03d}" if level_s > 0 else f"{int(level):03d}" + except (TypeError, ValueError): + level = str(level) + + # 真实 K/D、真实 KPM(旧卡算法:按"对人击杀占比"折算) + real_kd = round(kd * ratio, 2) + real_kpm = round(kpm * ratio, 2) + + # 最佳专家(soldier 段→旧 classes/characterName 结构) + best = None + ranked = sorted( + soldier_segs or [], key=lambda s: _seg_value(s, "kills"), reverse=True + ) + if ranked: + seg = ranked[0] + raw_name = ((seg.get("metadata") or {}).get("name") or "").strip() + best = { + "characterName": raw_name, + "displayName": SPECIALIST_NAMES.get(raw_name, raw_name), + "className": (seg.get("metadata") or {}).get("category") or "", + "kills": _seg_stat(seg, "kills"), + "kpm": _seg_stat(seg, "killsPerMinute"), + "killDeath": _seg_stat(seg, "kdRatio"), + "secondsPlayed": round(_seg_value(seg, "timePlayed")), + } + + # 鉴定文案(旧阈值) + if kpm > 1.00 and kd > 2 and real_kd > 1.5: + verdict, verdict_color = "Pro哥\n你带我走吧T_T", "gold" + else: + verdict, verdict_color = "薯薯\n别拷打我了哥>_<", "skyblue" + + def _objects(segs: list[dict], name_key: str, is_vehicle: bool) -> list[dict]: + rows = [] + for seg in sorted( + segs or [], key=lambda x: _seg_value(x, "kills"), reverse=True + )[:5]: + meta = seg.get("metadata") or {} + rows.append( + { + name_key: (meta.get("name") or "").strip(), + "type": meta.get("category") or "", + "kills": _seg_stat(seg, "kills"), + "headshots": _seg_stat(seg, "headshotPercentage"), + "accuracy": _seg_stat(seg, "shotsAccuracy"), + "killsPerMinute": _seg_stat(seg, "killsPerMinute"), + "timeEquipped": round(_seg_value(seg, "timePlayed")), + "destroyed": _seg_stat(seg, "destroyedWith") if is_vehicle else "–", + } + ) + return rows + + return { + "userName": info.get("platformUserHandle") or _PLACEHOLDER, + "platform": info.get("platformSlug") or "origin", + "level": level, + "secondsPlayed": int(_seg_value(overview, "timePlayed") // 3600), + "matchesPlayed": _seg_stat(overview, "matches"), + "mvp": "–", # tracker 无 + "bestSquad": "–", # tracker 无 + "playerTakeDowns": _seg_stat(overview, "meleeKills"), + "kills": _seg_stat(overview, "kills"), + "deaths": _seg_stat(overview, "deaths"), + "killDeath": _seg_stat(overview, "kdRatio"), + "infantryKillDeath": round(kd * ratio, 2), + "killsPerMinute": _seg_stat(overview, "killsPerMinute"), + "killsPerMatch": _seg_stat(overview, "killsPerMatch"), + "headShots": _seg_stat(overview, "headshotPercentage"), + "accuracy": "–", # tracker overview 无整体命中率 + "winPercent": _seg_stat(overview, "wlPercentage"), + "humanPrecentage": f"{human_per}%", + "dividedKills": { + "ai": _seg_stat(overview, "aiKills"), + "vehicle": _seg_stat(overview, "vehicleKills"), + "longDistance": "–", # tracker 无 + "inRound": {"longDistance": "–"}, + }, + "revives": _seg_stat(overview, "revives"), + "enemiesSpotted": "–", # tracker 无 + "vehiclesDestroyed": _seg_stat(overview, "vehiclesDestroyed"), + "bestClass": best, + "realKd": real_kd, + "realKpm": real_kpm, + "verdict": verdict, + "verdictColor": verdict_color, + "weapons": _objects(weapon_segs or [], "weaponName", False), + "vehicles": _objects(vehicle_segs or [], "vehicleName", True), + } diff --git a/hexi/plugins/nonebot_plugin_bf_bot/services/query.py b/hexi/plugins/nonebot_plugin_bf_bot/services/query.py new file mode 100644 index 0000000..64e4fcc --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/services/query.py @@ -0,0 +1,295 @@ +"""查询编排:目标解析 → 取数 → 加工 → 渲染,返回给 Trigger 层发送的结果。 + +只产出结果对象(文本/图片字节),不直接发消息;"正在查询…"等进度提示通过 +`notify` 回调交给 Trigger 层发送,本层保持与 nonebot 消息 API 解耦。 +""" + +from __future__ import annotations + +import asyncio +import time +from dataclasses import dataclass +from collections.abc import Awaitable, Callable + +from nonebot import logger + +from ..repository import Bf2042SettingsManager, UserManager +from ..utils.args import ( + BF2042_ERROR_HINT, + BF_GAME_NAMES, + parse_bf2042_args, +) +from ..utils.render_bf2042 import build_bf2042_career_card, build_bf2042_list_card +from ..utils.render_ea import build_bf6_stats_card, build_stats_card +from ..utils.text import convert_to_hours +from .fetchers.bf2042 import ( + bf2042_list_title, + get_bf2042_gamemodes, + get_bf2042_info, + get_bf2042_list, +) +from .fetchers.gametools import ( + fetch_all_stats, + get_bf6_rank, + get_player_info_by_name, + player_display_name, +) +from .processing import ( + get_best_classes, + get_best_weapon_and_best_vehicle, + get_vehicle_destroyed, + stats_calculator, +) + +Notify = Callable[[str], Awaitable[None]] + +_PLATFORM = "pc" + + +@dataclass +class QueryReply: + """一次查询的最终回复:文本 +(可选)PNG 图片字节。""" + + text: str + image: bytes | None = None + + +async def run_query( + *, cmd: str, input_text: str, qq_id: str, notify: Notify +) -> QueryReply: + """`/bft` 及全部别名共用的查询入口。 + + cmd: 实际输入的游戏别名(bf3/bf4/bf1/bfv/bf2042/bf6)。 + input_text: 命令参数(EAID 或 BF2042 子命令),可为空。 + qq_id: 触发者 QQ,用于读绑定与取头像。 + notify: 进度提示回调("正在查询…")。 + """ + started = time.time() + if cmd not in BF_GAME_NAMES: + return QueryReply( + "指令异常:请使用 bf3 / bf4 / bf1 / bfv / bf2042 / bf6(可带 EAID)" + ) + if cmd == "bf2042": + reply = await _run_bf2042(input_text, qq_id, notify) + else: + reply = await _run_ea(cmd, input_text, qq_id, notify) + if reply.image is not None: + reply.text = f"本次查询耗时:{round(time.time() - started, 3)}s" + return reply + + +def _resolve_bound_record(qq_id: str) -> QueryReply | dict: + """无参数查询时取绑定记录;未绑定返回给用户的提示。""" + content = UserManager().get_user_by_qq(qq_id) + if not content: + return QueryReply( + "未检测到id,也未检测到绑定记录,请使用 绑定 EAID 进行绑定操作" + ) + return content + + +async def _run_ea(cmd: str, input_text: str, qq_id: str, notify: Notify) -> QueryReply: + """EA 系(BF3/4/1/5/6):gametools 取数 + 卡片渲染。""" + if not input_text: + content = _resolve_bound_record(qq_id) + if isinstance(content, QueryReply): + return content + player = content["ea_player_name"] + player_id = content["ea_player_id"] + user_id = content["ea_user_id"] + else: + content = await get_player_info_by_name(input_text, _PLATFORM) + if "errors" in content: + return QueryReply("未找到玩家,请检查是否拼写正确") + player_id = content["personaId"] + user_id = content["userId"] + player = player_display_name(content) + await notify(f"正在查询 {player} 的 {BF_GAME_NAMES[cmd]} 数据,请耐心等待") + + try: + rank = 0 + if cmd == "bf6": + play_stat, rank = await asyncio.gather( + fetch_all_stats(cmd, player, player_id, user_id, _PLATFORM), + get_bf6_rank(player, player_id, user_id, _PLATFORM), + ) + logger.info(f"等级:{rank}") + else: + play_stat = await fetch_all_stats( + cmd, player, player_id, user_id, _PLATFORM + ) + if "errors" in play_stat: + logger.warning(play_stat["errors"][0]) + return QueryReply(str(play_stat["errors"][0])) + + img = await _render_ea_card(cmd, play_stat, rank, player, qq_id) + if isinstance(img, QueryReply): + return img + return QueryReply("", image=img) + except Exception as e: # noqa: BLE001 - 兜底给用户可见提示,明细写日志 + logger.exception(f"异常:{e}") + return QueryReply("查询异常,请稍后再试") + + +async def _render_ea_card(cmd: str, play_stat, rank, player, qq_id): + """渲染卡片;返回 PNG 字节,或对用户的错误提示(QueryReply)。""" + weapon, vehicle = await get_best_weapon_and_best_vehicle(play_stat) + pid = play_stat["userId"] + kd = play_stat["killDeath"] + kpm = play_stat["killsPerMinute"] + acc = play_stat["accuracy"] + # 战地3 特化 + if cmd == "bf3": + head_shots = play_stat["headShots"] + else: + head_shots = play_stat["headshots"] + time_play = convert_to_hours(play_stat["timePlayed"]) + logger.info(f"游玩时长监测:{time_play}") + if time_play == 0: + return QueryReply("查询接口异常,请等待一段时间后查询") + + 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"] + destroyed = await get_vehicle_destroyed(play_stat["vehicles"]) + + if cmd == "bf6": + best_class = await get_best_classes(play_stat) + captured = play_stat["objective"]["captured"] + score = sum(item.get("score", 0) for item in play_stat["classes"]) + # 计算4兵种游玩时长 + seconds = sum(item.get("secondsPlayed", 0) for item in play_stat["classes"]) + kpm = round(kills / (int(seconds) / 60), 2) + logger.info(f"击杀数:{kills},游玩分钟数:{seconds / 60},计算kpm值:{kpm}") + spm = int(score / (int(seconds) / 60)) + logger.info(f"计算spm值:{spm}") + img = await build_bf6_stats_card( + game="bf6", + qq_id=qq_id, + player=player, + pid=pid, + rank=rank, + kd=kd, + kpm=kpm, + spm=spm, + acc=acc, + head_shots=head_shots, + time_play=time_play, + kills=kills, + kill_assists=kill_assists, + revives=revives, + repairs=play_stat["repairs"], + captured=captured, + score=score, + wins=wins, + loses=loses, + destroyed=destroyed, + best_weapon=best_weapon, + best_vehicle=best_vehicle, + best_class=best_class, + ) + else: + spm = play_stat["scorePerMinute"] + longest_head_shot = play_stat["longestHeadShot"] + highest_ill_streak = play_stat["highestKillStreak"] + stat_data, level_designation = await stats_calculator(play_stat, cmd) + img = await build_stats_card( + cmd, + player, + pid, + kd, + kpm, + spm, + acc, + head_shots, + play_stat["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, + qq_id=qq_id, + ) + if img is None: + return QueryReply("图片渲染失败,请稍后再试") + return img.getvalue() + + +async def _run_bf2042(input_text: str, qq_id: str, notify: Notify) -> QueryReply: + """BF2042 独立流程:tracker.gg 浏览器通道。 + + 与其它战地不同,BF2042 不经过 EA 接口,所以玩家名要么来自指令参数, + 要么来自绑定记录,不需要 personaId/userId。 + + 指令形式: + /bf2042 [EAID] 生涯卡 + /bf2042 武器|载具|装备 [EAID] 榜单卡 + /bf2042 模式 [EAID] 各模式明细 + """ + target = parse_bf2042_args(input_text) + player = target.player + if not player: + content = _resolve_bound_record(qq_id) + if isinstance(content, QueryReply): + return content + player = content["ea_player_name"] + + if target.view == "list": + what = f"{bf2042_list_title(target.kind)}榜" + elif target.view == "gamemode": + what = "各模式数据" + else: + what = "数据" + await notify(f"正在查询 {player} 的 {BF_GAME_NAMES['bf2042']}{what},请耐心等待") + + try: + if target.view == "list": + result = await get_bf2042_list(player, _PLATFORM, target.kind) + elif target.view == "gamemode": + result = await get_bf2042_gamemodes(player, _PLATFORM) + else: + result = await get_bf2042_info(player, _PLATFORM) + except Exception as e: # noqa: BLE001 - 兜底,别让异常静默 + logger.exception(f"BF2042 查询异常:{e}") + return QueryReply("BF2042 查询异常,请稍后再试") + + if not result.ok: + logger.warning(f"BF2042 查询失败({result.reason}):{result.detail}") + hint = BF2042_ERROR_HINT.get(result.reason, "查询失败,请稍后再试") + return QueryReply(f"BF2042 {hint}") + + info = result.data if isinstance(result.data, dict) else {} + if target.view == "career": + # 新版生涯卡:旧版布局 + tracker.gg 数据映射;背景按支持者特权选 + from .processing import build_bf2042_career_card as build_card_slots + + try: + support = Bf2042SettingsManager().is_support(qq_id) + except Exception: # noqa: BLE001 - 装饰性功能,失败退回公共背景 + support = False + card = build_card_slots( + info.get("_profile") or {}, + info.get("_weapons") or [], + info.get("_vehicles") or [], + info.get("_soldiers") or [], + ) + card["_qq_id"] = qq_id + card["_support"] = support + img = build_bf2042_career_card(card) + else: + img = build_bf2042_list_card(info) + return QueryReply("", image=img.getvalue()) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/tracker_data.py b/hexi/plugins/nonebot_plugin_bf_bot/tracker_data.py deleted file mode 100644 index c167d1a..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/tracker_data.py +++ /dev/null @@ -1,109 +0,0 @@ -import os -from typing import List, Dict, Optional - -from nonebot import logger -from playwright.async_api import async_playwright, Response - - -async def fetch_tracker_search( - search_url: str, - cookies_txt: str, - wait_seconds: int = 2, -) -> Optional[Dict]: - """ - 使用 Playwright + cookies 访问 tracker.gg API 并截获响应数据 - """ - cookies = parse_netscape_cookies(cookies_txt) - if not cookies: - logger.error("cookies.txt 解析失败或为空") - return None - - result: Optional[Dict] = None - - async with async_playwright() as p: - browser = await p.chromium.launch( - executable_path="C:/Program Files/Google/Chrome/Application/chrome.exe", - headless=True, # False 可以看到浏览器操作 - args=[ - "--autoplay-policy=no-user-gesture-required", - "--disable-features=AutoplayDisableSuppression", - "--use-fake-ui-for-media-stream", - ] - ) - - context = await browser.new_context( - viewport={"width": 640, "height": 320}, - user_agent=( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/143.0.0.0 Safari/537.36" - ), - ) - - await context.add_cookies(cookies) - page = await context.new_page() - - # 响应监听 - async def handle_response(response: Response): - nonlocal result - try: - if search_url in response.url: - # 尝试解析 JSON - data = await response.json() - if data and "data" in data: - result = data - logger.info(f"成功截获响应数据: {data}") - except Exception as e: - logger.warning(f"解析响应失败: {e}") - - page.on("response", handle_response) - - try: - # 直接访问 API 链接 - await page.goto(search_url, wait_until="domcontentloaded", timeout=60000) - # 等待响应截获 - await page.wait_for_timeout(wait_seconds * 1000) - except Exception as e: - logger.error(f"访问页面出错: {e}") - finally: - await browser.close() - - return result - - -def parse_netscape_cookies(cookies_txt_path: str) -> List[Dict]: - """ - 解析 Netscape HTTP Cookie File -> Playwright cookies - """ - cookies: List[Dict] = [] - - if not os.path.exists(cookies_txt_path): - return cookies - - with open(cookies_txt_path, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - - parts = line.split("\t") - if len(parts) != 7: - continue - - domain, include_sub, path, secure, expiry, name, value = parts - - cookie = { - "name": name, - "value": value, - "domain": domain, - "path": path, - "secure": secure.upper() == "TRUE", - "httpOnly": False, - } - - if expiry.isdigit() and int(expiry) > 0: - cookie["expires"] = int(expiry) - - cookies.append(cookie) - - return cookies diff --git a/hexi/plugins/nonebot_plugin_bf_bot/user_data/client_app.py b/hexi/plugins/nonebot_plugin_bf_bot/user_data/client_app.py deleted file mode 100644 index bee7bf9..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/user_data/client_app.py +++ /dev/null @@ -1,26 +0,0 @@ -import json -from fastapi import FastAPI, HTTPException, Header, Form, Request -from fastapi.responses import JSONResponse -from fastapi.middleware.cors import CORSMiddleware - -app = FastAPI() - -# ✅ 添加 CORS 支持 -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.post("/designation/page") -async def designation_page(): - return "1" - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=16661, log_level="info") diff --git a/hexi/plugins/nonebot_plugin_bf_bot/user_data/data_utils.py b/hexi/plugins/nonebot_plugin_bf_bot/user_data/data_utils.py deleted file mode 100644 index a07d583..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/user_data/data_utils.py +++ /dev/null @@ -1,133 +0,0 @@ -import sqlite3 -import os -from typing import List, Dict, Any, Optional - - -class TableManager: - def __init__(self, db_path: str = 'user_data.db'): - base_dir = os.path.dirname(os.path.abspath(__file__)) - self.db_path = os.path.join(base_dir, 'user_data.db') - - def _execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor: - """执行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_player_name: str, ea_player_id: str, ea_user_id: str, designation: str, - medals: str) -> int: - """添加用户""" - cursor = self._execute( - "INSERT INTO users (qq_id, ea_player_name,ea_player_id,ea_user_id, designation,medals) VALUES (?, ?, ?, ?, ?,?)", - (qq_id, ea_player_name, ea_player_id, ea_user_id, designation, medals) - ) - return cursor.lastrowid - - def update_user(self, qq_id: str, ea_player_name: str, ea_player_id: str, ea_user_id: str, designation: str, - medals: str) -> int: - """修改用户""" - cursor = self._execute( - "UPDATE users SET ea_player_name = ?, ea_player_id = ?,ea_user_id = ? WHERE qq_id = ?", - (ea_player_name, ea_player_id, ea_user_id, qq_id) - ) - return cursor.rowcount - - def get_user_by_qq(self, qq_id: str) -> Optional[Dict[str, Any]]: - cursor = self._execute( - "SELECT * FROM users WHERE qq_id = ?", - (qq_id,) - ) - row = cursor.fetchone() - return dict(row) if row else None - - def get_user_by_ea_id(self, ea_user_id: str) -> Optional[Dict[str, Any]]: - cursor = self._execute( - "SELECT * FROM users WHERE ea_user_id = ?", - (ea_user_id,) - ) - row = cursor.fetchone() - return dict(row) if row else None - - def delete_user_by_qq(self, qq_id: str) -> Optional[Dict[str, Any]]: - cursor = self._execute( - "DELETE FROM users WHERE qq_id = ?", - (qq_id,) - ) - return cursor.rowcount - - def update_dog_tags(self, qq_id: int, medals: str) -> bool: - """更新用户的狗牌列表""" - self._execute( - "UPDATE users SET medals = ? WHERE id = ?", - (medals, qq_id) - ) - return True - - -class DesignationManager(TableManager): - def create_designation(self, name: str, icon_path: str) -> int: - """创建新标签""" - cursor = self._execute( - "INSERT INTO designation (name,icon_path) VALUES (?,?)", - (name, icon_path) - ) - return cursor.lastrowid - - def get_all_designation(self) -> List[Dict[str, Any]]: - """获取所有标签""" - cursor = self._execute("SELECT * FROM designation") - return [dict(row) for row in cursor.fetchall()] - - def get_designation_by_id_list(self, ids: List[int]) -> List[Dict[str, Any]]: - """根据 ID 列表获取标签""" - - if not ids: - return [] - - placeholders = ",".join(["?"] * len(ids)) - sql = f""" - SELECT * - FROM designation - WHERE id IN ({placeholders}) - """ - - cursor = self._execute(sql, ids) - return [dict(row) for row in cursor.fetchall()] - - -# 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__": - tag_db = DesignationManager() - # tag_id = tag_db.create_designation("DEV","DEV.png") - # tag_id = tag_db.create_designation("DICE", "DICE.png") - # tag_id = tag_db.create_designation("IC", "IC.png") - # tag_id = tag_db.create_designation("OWNER", "OWNER.png") - # print(f"Created tag with ID: {tag_id}") - - record_id = tag_db.get_designation_by_id_list([1, 2, 3]) - - print(f"Logged query with ID: {record_id}") diff --git a/hexi/plugins/nonebot_plugin_bf_bot/user_data/init_database.py b/hexi/plugins/nonebot_plugin_bf_bot/user_data/init_database.py deleted file mode 100644 index ebb5bba..0000000 --- a/hexi/plugins/nonebot_plugin_bf_bot/user_data/init_database.py +++ /dev/null @@ -1,127 +0,0 @@ -import os -import sqlite3 -from typing import List, Tuple - -from nonebot import logger - - -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_player_name', 'TEXT UNIQUE'), - ('ea_player_id', 'TEXT UNIQUE'), - ('ea_user_id', 'TEXT UNIQUE'), - ('designation', 'TEXT NOT NULL'), - ('medals', 'TEXT NOT NULL'), - ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') - ], - 'designation': [ - ('id', 'INTEGER PRIMARY KEY'), - ('name', 'TEXT NOT NULL'), - ('icon_path', 'TEXT NOT NULL'), - ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') - ], - 'medals': [ - ('id', 'INTEGER PRIMARY KEY'), - ('name', 'TEXT NOT NULL'), - ('icon_path', 'TEXT NOT NULL'), - ('create_time', 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP') - ] - , - 'query_record': [ - ('id', 'INTEGER PRIMARY KEY'), - ('user_id', 'TEXT NOT NULL'), - ('ea_player_name', 'TEXT NOT NULL'), - ('ea_player_id', 'TEXT NOT NULL'), - ('ea_user_id', 'TEXT NOT NULL'), - ('status', 'TEXT NOT NULL'), - ('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/hexi/plugins/nonebot_plugin_bf_bot/utils/__init__.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/__init__.py new file mode 100644 index 0000000..e9305af --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/__init__.py @@ -0,0 +1 @@ +"""纯工具层:文本/参数/图像处理与卡片渲染(尽量无副作用,可单测)。""" diff --git a/hexi/plugins/nonebot_plugin_bf_bot/utils/args.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/args.py new file mode 100644 index 0000000..7fce2d7 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/args.py @@ -0,0 +1,68 @@ +"""指令参数解析与展示表(纯函数、无副作用,可单测)。""" + +from dataclasses import dataclass + +# 游戏代号 -> 中文展示名(回复文案用) +BF_GAME_NAMES = { + "bf3": "战地3", + "bf4": "战地4", + "bf1": "战地1", + "bfv": "战地5", + "bf2042": "战地2042", + "bf6": "战地6", +} + +# BF2042 取数失败原因 -> 给用户看的话(detail 只写日志不刷屏) +BF2042_ERROR_HINT = { + "not_found": "未找到该玩家,请检查 EAID 拼写是否正确", + "ext_offline": "查询服务未就绪(取数浏览器未连接),请联系管理员", + "timeout": "访问 tracker.gg 超时,请稍后再试", + "auth": "查询服务登录态异常,请联系管理员", + "error": "tracker.gg 取数失败,请稍后再试", +} + +# 子命令关键词 -> (视图, 榜单类型)。不打关键词就是生涯卡 +_BF2042_VIEWS = { + "武器": ("list", "weapon"), + "枪械": ("list", "weapon"), + "weapon": ("list", "weapon"), + "weapons": ("list", "weapon"), + "载具": ("list", "vehicle"), + "vehicle": ("list", "vehicle"), + "vehicles": ("list", "vehicle"), + "装备": ("list", "gadget"), + "道具": ("list", "gadget"), + "gadget": ("list", "gadget"), + "gadgets": ("list", "gadget"), + "模式": ("gamemode", ""), + "gamemode": ("gamemode", ""), +} + + +@dataclass(frozen=True) +class Bf2042Target: + """BF2042 指令解析结果。 + + view: career(生涯卡)/ list(榜单卡)/ gamemode(模式明细) + kind: 榜单类型 weapon/vehicle/gadget,仅 view=list 时非空 + player: 玩家名;空串 = 未指定,用绑定记录 + """ + + view: str = "career" + kind: str = "" + player: str = "" + + +def parse_bf2042_args(input_text: str) -> Bf2042Target: + """拆出 BF2042 子命令关键词,返回目标。 + + 首个词不是关键词时整串都当玩家名(生涯卡),所以玩家名里有空格也不受影响。 + """ + text = (input_text or "").strip() + if not text: + return Bf2042Target() + head, _, rest = text.partition(" ") + view = _BF2042_VIEWS.get(head.lower()) + if view is None: + return Bf2042Target(view="career", kind="", player=text) + return Bf2042Target(view=view[0], kind=view[1], player=rest.strip()) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/img_utils.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/images.py similarity index 58% rename from hexi/plugins/nonebot_plugin_bf_bot/img_utils.py rename to hexi/plugins/nonebot_plugin_bf_bot/utils/images.py index fdd8e31..dca1799 100644 --- a/hexi/plugins/nonebot_plugin_bf_bot/img_utils.py +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/images.py @@ -1,60 +1,22 @@ +"""图像处理工具:缩放/粘贴、头像下载缓存、物品图标、公告栏与标签绘制。 + +静态资源统一走 paths.RES_DIR;QQ 头像缓存落在 hexi/data/nonebot_plugin_bf_bot/avatar_cache。 +""" + import hashlib import os from io import BytesIO -import aiohttp import cv2 import numpy as np import requests import requests.exceptions -from PIL import Image, ImageDraw, ImageFont +from PIL import Image from nonebot import logger -from hexi.plugins.nonebot_plugin_bf_bot.user_data.data_utils import UserManager, DesignationManager -from .param import rank_pic - -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 +from ..paths import AVATAR_CACHE_DIR, RES_DIR +from ..repository import DesignationManager, UserManager +from .params import rank_pic def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=""): @@ -106,20 +68,19 @@ def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_f # 重采样方式 # ------------------------- resample_map = { - "NEAREST": Image.NEAREST, - "BILINEAR": Image.BILINEAR, - "BICUBIC": Image.BICUBIC, - "LANCZOS": Image.LANCZOS + "NEAREST": Image.Resampling.NEAREST, + "BILINEAR": Image.Resampling.BILINEAR, + "BICUBIC": Image.Resampling.BICUBIC, + "LANCZOS": Image.Resampling.LANCZOS, } - resample_method = resample_map.get(resample, Image.LANCZOS) + resample_method = resample_map.get(resample, Image.Resampling.LANCZOS) # ------------------------- # Alpha 安全 resize(逐通道) # ------------------------- bands = img.split() resized_bands = [ - band.resize((target_w, target_h), resample=resample_method) - for band in bands + band.resize((target_w, target_h), resample=resample_method) for band in bands ] return Image.merge("RGBA", resized_bands) @@ -141,7 +102,7 @@ def image_paste(paste_image, under_image, pos): return under_image -def image_paste_center(paste_image: Image, under_image: Image, pos): +def image_paste_center(paste_image, under_image, pos): """ 将一张图片粘贴到另一张图片上,并使粘贴图片的Y轴中心对齐到指定y坐标 @@ -161,54 +122,124 @@ def image_paste_center(paste_image: Image, under_image: Image, pos): return under_image -def download_icon(url): - data = download_url(url) - img = BytesIO(data) - return img - - # 下载QQ头像 -def download_avatar(user_id: str) -> bytes: +def download_avatar(user_id: str) -> bytes | None: 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": + data = download_url(url, timeout=10) + if not data or hashlib.md5(data).hexdigest() == QQ_DEFAULT_AVATAR_MD5: url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100" - data = download_url(url) + data = download_url(url, timeout=10) return data # 根据URL下载文件 -def download_url(url: str) -> bytes: +def download_url(url: str, timeout=None) -> bytes | None: for i in range(3): try: - resp = requests.get(url) + resp = requests.get(url, timeout=timeout) if resp.status_code != 200: continue return resp.content except Exception as e: - print(f"Error downloading {url}, retry {i}/3: {str(e)}") + logger.warning(f"Error downloading {url}, retry {i}/3: {str(e)}") -# 图片裁剪 -def cut_image(pic_data: bytes, target_ratio: float): +# QQ 无头像时 qlogo 返回的默认图,md5 固定 +QQ_DEFAULT_AVATAR_MD5 = "acef72340ac0e914090bd35799f5594e" + + +# 校验字节流能不能当图片打开 +def _is_image(data) -> bool: 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)}") + Image.open(BytesIO(data)).size + return True + except Exception: + return False + + +# 按QQ号获取头像字节流:先读本地缓存,未命中则下载并落盘 +def get_avatar_bytes(user_id): + if not user_id: + return None + cache_file = AVATAR_CACHE_DIR / f"{user_id}.png" + if cache_file.exists(): + try: + cached = cache_file.read_bytes() + if _is_image(cached): + return cached + # 缓存已损坏(旧版本写入或写了一半),删掉重下,避免一直读到坏图 + logger.warning(f"头像缓存已损坏,重新下载:{cache_file}") + cache_file.unlink() + except Exception as err: + logger.error(f"读取头像缓存失败:{str(err)}") + + try: + data = download_avatar(str(user_id)) + except Exception as err: + logger.error(f"下载头像失败:{str(err)}") + return None + if not data: + logger.warning(f"未获取到QQ {user_id} 的头像数据") + return None + + # 确认是图片再缓存,避免把错误页写进缓存 + if not _is_image(data): + logger.error(f"头像数据不是有效图片,QQ:{user_id}") + return None + + try: + AVATAR_CACHE_DIR.mkdir(parents=True, exist_ok=True) + cache_file.write_bytes(data) + logger.info(f"头像已缓存至:{cache_file}") + except Exception as err: + logger.error(f"写入头像缓存失败:{str(err)}") + return data + + +# 灰色圆形占位头像(BGRA,圆外透明) +def placeholder_avatar(size: int): + avatar = np.zeros((size, size, 4), dtype=np.uint8) + center = size // 2 + cv2.circle(avatar, (center, center), size // 2, (90, 90, 90, 255), -1) + cv2.circle( + avatar, (center, int(size * 0.38)), int(size * 0.16), (150, 150, 150, 255), -1 + ) + cv2.ellipse( + avatar, + (center, int(size * 0.96)), + (int(size * 0.28), int(size * 0.30)), + 0, + 180, + 360, + (150, 150, 150, 255), + -1, + ) + return avatar + + +# 载入指定尺寸的圆形头像,统一返回BGRA四通道 +def load_avatar(user_id, size: int): + data = get_avatar_bytes(user_id) + avatar = ( + cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_UNCHANGED) + if data + else None + ) + if avatar is not None and avatar.size: + if avatar.ndim == 2: # 灰度图 + avatar = cv2.cvtColor(avatar, cv2.COLOR_GRAY2BGRA) + elif avatar.shape[2] == 3: + avatar = cv2.cvtColor(avatar, cv2.COLOR_BGR2BGRA) + elif avatar.shape[2] != 4: + logger.warning(f"头像通道数异常({avatar.shape[2]}),改用占位头像") + avatar = None + else: + avatar = None + + if avatar is None: + logger.warning(f"未获取到头像(QQ:{user_id}),使用占位头像") + avatar = placeholder_avatar(size) + return cv2.resize(avatar, (size, size), interpolation=cv2.INTER_AREA) def draw_centered_text(draw, text, x_left, x_right, y, font, fill): @@ -254,7 +285,7 @@ def draw_right_aligned_text(draw, text, x_left, x_right, y, font, fill): def get_save_icon(game, name, icon_type, url): - if game == 'bf6': + if game == "bf6": name = name.replace("/", "").upper() else: name = name.replace("/", "") @@ -285,8 +316,11 @@ def get_save_icon(game, name, icon_type, url): 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") + # bf6 没有下载分支:缓存没命中时 icon 仍是 None,交给下面的占位图兜底 + if icon: + icon_path = RES_DIR / "img" / game / icon_type / f"{name}.png" + icon.save(icon_path) + logger.info(f"文件已经缓存至:{icon_path}") # 上述步骤处理完再执行一次icon判空 if icon: @@ -294,9 +328,9 @@ def get_save_icon(game, name, icon_type, url): else: # 针对空图片处理 if icon_type == "weapon": - icon = Image.open(filepath + "/img/wp.png").convert('RGBA') + icon = Image.open(RES_DIR / "img" / "wp.png").convert("RGBA") else: - icon = Image.open(filepath + "/img/vc.png").convert('RGBA') + icon = Image.open(RES_DIR / "img" / "vc.png").convert("RGBA") return icon @@ -306,21 +340,21 @@ def get_icon_from_url(url): try: # 添加10s超时判断,如果超时返回空 res = BytesIO(requests.get(url, timeout=10).content) - icon = Image.open(res).convert('RGBA') + 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}" + path = RES_DIR / "img" / game / icon_type logger.info(f"查询物品:{icon_name}物品路径:{path}") try: icon_list = os.listdir(path) # logger.info(f"所有物品:{icon_list}") if icon_name in str(icon_list): logger.info(f"本地存在{icon_name}物品") - img = Image.open(f"{path}/{icon_name}.png").convert('RGBA') + img = Image.open(path / f"{icon_name}.png").convert("RGBA") return img except Exception as err: logger.error(f"获取图标失败:{str(err)}") @@ -390,7 +424,7 @@ def paste_image_cv2(fg, bg, pos): bg = cv2.merge((b, g, r, a)) # 截取 ROI 区域 - roi = bg[y:y + fh, x:x + fw] + roi = bg[y : y + fh, x : x + fw] # 防止越界 if roi.shape[0] != fh or roi.shape[1] != fw: @@ -402,39 +436,47 @@ def paste_image_cv2(fg, bg, pos): # 混合 RGB for c in range(3): - roi[:, :, c] = (alpha_fg * fg[:, :, c] + - alpha_bg * roi[:, :, c]) + 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 + bg[y : y + fh, x : x + fw] = roi return bg def notice_paste(cv2_bg): + """贴公告栏(notice.png)到 BF1/3/4/5 卡片右上。 + + 公告图是 RGBA(透明底)素材:必须按 alpha 混合,否则透明区域会以白底 + 整块盖在卡片上。没有 alpha 通道的旧素材仍走「三角切角 + 不透明覆盖」老流程。 + """ pos = (1176, 318) - # 获取公告 - notice_path = f"{filepath}/notice/notice.png" - front_img = cv2.imread(notice_path) + front_img = cv2.imread(str(RES_DIR / "notice" / "notice.png"), cv2.IMREAD_UNCHANGED) + if front_img is None: + logger.warning("公告栏图片缺失(res/notice/notice.png),跳过公告栏绘制") + return cv2_bg + + if front_img.shape[2] == 4: + return _paste_rgba(front_img, cv2_bg, pos) + + # 旧素材(无 alpha):三角切角后按不透明贴图 + if front_img.shape[:2] != (631, 1355): + logger.warning( + f"公告栏图片尺寸 {front_img.shape[:2]} 与切角蒙版 (631, 1355) 不一致,跳过公告栏绘制" + ) + return cv2_bg 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 + return paste_image_cv2(front_img, cv2_bg, pos) -def notice_paste_bf6(cv2_bg): - x, y = 0, 0 - - notice_path = f"{filepath}/notice/notice_bf6.png" - front_img = cv2.imread(notice_path, cv2.IMREAD_UNCHANGED) - - if front_img is None: - raise RuntimeError(f"Failed to load image: {notice_path}") - +def _paste_rgba(front_img, cv2_bg, pos): + """RGBA 前景按 alpha 混合到背景(超出背景的部分自动裁剪)。""" + x, y = pos h, w = front_img.shape[:2] # ROI 边界裁剪 @@ -442,33 +484,39 @@ def notice_paste_bf6(cv2_bg): w = min(w, bg_w - x) h = min(h, bg_h - y) - roi = cv2_bg[y:y + h, x:x + w] + roi = cv2_bg[y : y + h, x : x + w] - # 有 Alpha 通道 → Alpha 混合 - if front_img.shape[2] == 4: - alpha = front_img[:h, :w, 3] / 255.0 - alpha = alpha[:, :, None] - - cv2_bg[y:y + h, x:x + w] = ( - alpha * front_img[:h, :w, :3] + - (1 - alpha) * roi - ).astype(np.uint8) - - # 无 Alpha → 直接覆盖 - else: - cv2_bg[y:y + h, x:x + w] = front_img[:h, :w] + alpha = front_img[:h, :w, 3] / 255.0 + alpha = alpha[:, :, None] + cv2_bg[y : y + h, x : x + w] = ( + alpha * front_img[:h, :w, :3] + (1 - alpha) * roi + ).astype(np.uint8) return cv2_bg -def designation_paste(player, - cv2_bg, - user_id, - start_x=10, - start_y=10, - target_height=40, - spacing=6 - ): +def notice_paste_bf6(cv2_bg): + notice_path = RES_DIR / "notice" / "notice_bf6.png" + front_img = cv2.imread(str(notice_path), cv2.IMREAD_UNCHANGED) + if front_img is None: + logger.warning(f"公告栏图片缺失({notice_path}),跳过公告栏绘制") + return cv2_bg + + if front_img.shape[2] == 4: + # 有 Alpha 通道 → Alpha 混合 + return _paste_rgba(front_img, cv2_bg, (0, 0)) + + # 无 Alpha → 直接覆盖(保留边界裁剪) + bg_h, bg_w = cv2_bg.shape[:2] + h = min(front_img.shape[0], bg_h) + w = min(front_img.shape[1], bg_w) + cv2_bg[0:h, 0:w] = front_img[:h, :w] + return cv2_bg + + +def designation_paste( + player, cv2_bg, user_id, start_x=10, start_y=10, target_height=40, spacing=6 +): designation_list_path = get_designation_list_by_user_id(user_id, player) if not designation_list_path: return cv2_bg @@ -502,7 +550,7 @@ def designation_paste(player, if x + iw > bg_w or y + ih > bg_h: break - roi = cv2_bg[y:y + ih, x:x + iw] + roi = cv2_bg[y : y + ih, x : x + iw] if icon.shape[2] == 4: alpha = icon[:, :, 3] / 255.0 @@ -511,31 +559,41 @@ def designation_paste(player, else: roi[:] = icon - cv2_bg[y:y + ih, x:x + iw] = roi + cv2_bg[y : y + ih, x : x + iw] = roi x += iw + spacing return cv2_bg def get_designation_list_by_user_id(user_id, player): - usermanager = UserManager() - designationmanager = DesignationManager() - user_info = usermanager.get_user_by_qq(user_id) - icon_no_list = [] - if user_info is not None and user_info['ea_player_name'].upper() == player.upper(): - logger.info(f"玩家标签列表:{user_info}") - icon_no_list = user_info['designation'] - icon_list = designationmanager.get_designation_by_id_list(icon_no_list) - icon_path_list = [] - for icon in icon_list: - icon_file_name = icon['icon_path'] - icon_path_list.append(f"{filepath}/img/icon/designation/{icon_file_name}") - return icon_path_list + # 标签是装饰性内容,查库失败(缺表、库损坏等)不该拖垮整张卡片,返回空即可 + try: + usermanager = UserManager() + designationmanager = DesignationManager() + user_info = usermanager.get_user_by_qq(user_id) + icon_no_list = [] + if ( + user_info is not None + and user_info["ea_player_name"].upper() == player.upper() + ): + logger.info(f"玩家标签列表:{user_info}") + icon_no_list = user_info["designation"] + icon_list = designationmanager.get_designation_by_id_list(icon_no_list) + icon_path_list = [] + for icon in icon_list: + icon_file_name = icon["icon_path"] + icon_path_list.append( + str(RES_DIR / "img" / "icon" / "designation" / icon_file_name) + ) + return icon_path_list + except Exception as err: + logger.warning(f"获取玩家标签失败(跳过标签绘制):{str(err)}") + return [] def paste_rank_icon(rank, img): rank_pic_path = get_rank_pic(rank) - rank_icon_path = f"{filepath}/img/icon/rank/{rank_pic_path}" + rank_icon_path = RES_DIR / "img" / "icon" / "rank" / rank_pic_path rank_icon = Image.open(rank_icon_path) rank_icon = png_resize(rank_icon, new_width=132, new_height=192) fm_img = image_paste(rank_icon, img, (946, 61)) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/utils/images_bf2042.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/images_bf2042.py new file mode 100644 index 0000000..c4b47e2 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/images_bf2042.py @@ -0,0 +1,267 @@ +"""BF2042 卡片素材加载(背景库/物品图标/专家头像/兵种/logo/字体)。 + +素材 2026-10 从旧 project(dev/bf2042)整体迁移到 ``res/bf2042/``, +这里提供"名字 → 图片"的统一入口: + +- 物品图标(tracker.gg 的名字 vs 本地文件名)按 精确 → 归一化(去空格/ + 连字符、大小写) → 别名表 → 占位图 四级回退; +- 背景:特权用户(bindings 表 bf2042_support=1)优先个人图库,否则公共库随机; +- 所有图片按需缩放并缓存(避免每次出图重复解码)。 + +纯 PIL,不依赖 nonebot,可脱离插件包单测(用 importlib 裸加载)。 +""" + +from __future__ import annotations + +import os +import random +import re +from functools import lru_cache +from pathlib import Path + +from PIL import Image, ImageFont + +from ..paths import ( + BF2042_BG_DIR, + BF2042_CLASS_ICON_DIR, + BF2042_DEV_LOGO_DIR, + BF2042_LOGO_DIR, + BF2042_OBJECT_ICON_DIR, + BF2042_PLATFORM_DIR, + BF2042_SPECIALIST_ICON_DIR, + RES_DIR, +) + +__all__ = [ + "object_icon", + "specialist_icon", + "class_icon", + "pick_background", + "platform_icon", + "bf2042_logo", + "dev_logo", + "no_pats_icon", + "load_bf2042_font", +] + +# tracker.gg 名字 → 本地文件名(仅收录名字对不上的;实测全量扫描的差集) +_ICON_ALIASES = { + "bren": "Bren LMG", + "ghostmaker 10": "Ghostmaker R10", + "rpt-31": "RPT-3", + "brawler": "CAV-BRAWLER", +} + +_FONT_DIR = RES_DIR / "font" + + +def _normalize(name: str) -> str: + """去空格/连字符、统一小写,用于宽松匹配。""" + return re.sub(r"[\s\-_]", "", (name or "").strip()).lower() + + +@lru_cache(maxsize=1) +def _icon_index() -> dict[str, Path]: + """本地图标索引:归一化文件名 → 路径。""" + index: dict[str, Path] = {} + if BF2042_OBJECT_ICON_DIR.exists(): + for f in BF2042_OBJECT_ICON_DIR.iterdir(): + if f.suffix.lower() in (".png", ".webp", ".jpg"): + index[_normalize(f.stem)] = f + return index + + +@lru_cache(maxsize=512) +def _load_scaled(path_str: str, width: int, height: int) -> Image.Image: + """等比缩放到 (width, height) 框内(contain),居中垫透明边。 + + 素材形状不一(专家头像 300×360、武器/载具横条),直接 resize 会拉伸; + 这里统一按比例适配后居中,给渲染层一个固定尺寸的贴图。 + """ + img = Image.open(path_str).convert("RGBA") + if width and height: + scale = min(width / img.width, height / img.height) + new_size = (max(1, round(img.width * scale)), max(1, round(img.height * scale))) + resized = img.resize(new_size, Image.Resampling.LANCZOS) + canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + canvas.alpha_composite( + resized, + dest=((width - new_size[0]) // 2, (height - new_size[1]) // 2), + ) + canvas.load() + return canvas + if width: + img = img.resize( + (width, max(1, int(img.height * width / img.width))), + Image.Resampling.LANCZOS, + ) + img.load() # 立即解码,后续缓存持有真实像素 + return img + + +def _placeholder(width: int, height: int) -> Image.Image: + return _load_scaled(str(BF2042_OBJECT_ICON_DIR / "default.png"), width, height) + + +def object_icon(name: str, width: int = 0, height: int = 0) -> Image.Image: + """物品(武器/载具)图标。四 级回退:精确 → 归一化 → 别名 → 占位图。""" + key = (name or "").strip() + if key: + # 1) 精确/大小写 + direct = BF2042_OBJECT_ICON_DIR / f"{key}.png" + if direct.exists(): + return _load_scaled(str(direct), width, height) + # 2) 归一化 + hit = _icon_index().get(_normalize(key)) + if hit: + return _load_scaled(str(hit), width, height) + # 3) 别名表 + alias = _ICON_ALIASES.get(key.lower()) or _ICON_ALIASES.get(_normalize(key)) + if alias: + alias_hit = _icon_index().get(_normalize(alias)) + if alias_hit: + return _load_scaled(str(alias_hit), width, height) + # 4) 占位 + return _placeholder(width, height) + + +@lru_cache(maxsize=64) +def _specialist_index() -> dict[str, Path]: + """specialist_icon/ 索引:专家头像(500×500 方形,卡片用这套)。""" + index: dict[str, Path] = {} + if BF2042_SPECIALIST_ICON_DIR.exists(): + for f in BF2042_SPECIALIST_ICON_DIR.iterdir(): + if f.suffix.lower() in (".png", ".webp", ".jpg"): + index[_normalize(f.stem)] = f + return index + + +def specialist_icon(name: str, width: int = 0, height: int = 0) -> Image.Image: + """专家头像(tracker.gg soldier 段的 metadata.name,如 "Falck")。 + + 用 specialist_icon/(方形头像)那套素材,不是 classes/(竖版立绘)。 + """ + key = _normalize(name or "") + hit = _specialist_index().get(key) + if hit: + return _load_scaled(str(hit), width, height) + fallback = BF2042_SPECIALIST_ICON_DIR / "No-Pats.png" + if not fallback.exists(): + fallback = BF2042_CLASS_ICON_DIR / "No-Pats.png" + return _load_scaled(str(fallback), width, height) + + +def class_icon(category: str, width: int = 0, height: int = 0) -> Image.Image: + """兵种图标(Assault/Support/Recon/Engineer);取不到回 No-Pats。""" + mapping = { + "assault": "ui/Assault_Icon_small.png", + "support": "ui/Support_Icon_small.png", + "recon": "ui/Recon_Icon_small.png", + "engineer": "ui/Engineer_Icon_small.png", + } + rel = mapping.get((category or "").strip().lower()) + if rel and (BF2042_CLASS_ICON_DIR / rel).exists(): + return _load_scaled(str(BF2042_CLASS_ICON_DIR / rel), width, height) + return _load_scaled(str(BF2042_CLASS_ICON_DIR / "No-Pats.png"), width, height) + + +def no_pats_icon(width: int = 0, height: int = 0) -> Image.Image: + """默认头像占位(EA 头像暂不可用,一律用这张)。""" + return _load_scaled(str(BF2042_CLASS_ICON_DIR / "No-Pats.png"), width, height) + + +def _random_image_from(directory: Path) -> Image.Image | None: + try: + files = [ + f + for f in os.listdir(directory) + if Path(f).suffix.lower() in (".jpg", ".jpeg", ".png", ".webp") + ] + except OSError: + return None + if not files: + return None + return _load_scaled(str(directory / random.choice(files)), 1920, 1080) + + +@lru_cache(maxsize=8) +def _common_bg_list() -> tuple[str, ...]: + try: + return tuple( + f + for f in os.listdir(BF2042_BG_DIR / "common") + if Path(f).suffix.lower() in (".jpg", ".jpeg", ".png", ".webp") + ) + except OSError: + return () + + +def pick_background(qq_id: str, support: bool) -> Image.Image: + """生涯卡背景。 + + 特权用户(bindings.bf2042_support=1)优先个人图库 ``bg/user/{qq}/``; + 没有个人图或个人图库为空 → 公共库 ``bg/common/`` 随机。 + """ + if support and qq_id: + personal = BF2042_BG_DIR / "user" / str(qq_id) + img = _random_image_from(personal) + if img is not None: + return img + + files = _common_bg_list() + if files: + return _load_scaled( + str(BF2042_BG_DIR / "common" / random.choice(files)), 1920, 1080 + ) + # 素材缺失时的兜底:纯色深底 + return Image.new("RGBA", (1920, 1080), (20, 24, 30, 255)) + + +def platform_icon(platform: str, width: int = 40, height: int = 40) -> Image.Image: + """平台图标;slug 与旧 project 文件名对齐(origin/steam/xbl→xbox/psn→playstation)。""" + slug = (platform or "").lower() + name = { + "origin": "origin.png", + "pc": "origin.png", + "steam": "steam.png", + "xbl": "xbox.png", + "xboxone": "xbox.png", + "psn": "playstation.png", + "ps4": "playstation.png", + }.get(slug, "origin.png") + path = BF2042_PLATFORM_DIR / name + if not path.exists(): + path = BF2042_PLATFORM_DIR / "origin.png" + return _load_scaled(str(path), width, height) + + +@lru_cache(maxsize=8) +def bf2042_logo(width: int = 0, height: int = 0) -> Image.Image: + path = BF2042_LOGO_DIR / "bf2042logo.png" + return _load_scaled(str(path), width, height) + + +@lru_cache(maxsize=8) +def dev_logo(width: int = 0, height: int = 0) -> Image.Image: + # 旧 project img/dev_logo 下只有一个文件,名字不固定,取第一个 + try: + files = sorted(os.listdir(BF2042_DEV_LOGO_DIR)) + files = [ + f for f in files if Path(f).suffix.lower() in (".png", ".webp", ".jpg") + ] + if files: + return _load_scaled(str(BF2042_DEV_LOGO_DIR / files[0]), width, height) + except OSError: + pass + return Image.new("RGBA", (width or 1, height or 1), (0, 0, 0, 0)) + + +@lru_cache(maxsize=64) +def load_bf2042_font(size: int, kind: str = "ch") -> ImageFont.FreeTypeFont: + """卡片字体。 + + kind: ch=中文(NotoSansSCMedium-4) / en=英数(BF_Modernista-Bold)。 + """ + name = "NotoSansSCMedium-4.ttf" if kind == "ch" else "BF_Modernista-Bold.ttf" + path = _FONT_DIR / name + return ImageFont.truetype(str(path), size) diff --git a/hexi/plugins/nonebot_plugin_bf_bot/param.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/params.py similarity index 97% rename from hexi/plugins/nonebot_plugin_bf_bot/param.py rename to hexi/plugins/nonebot_plugin_bf_bot/utils/params.py index 99e3d8d..ac73d8f 100644 --- a/hexi/plugins/nonebot_plugin_bf_bot/param.py +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/params.py @@ -1033,3 +1033,34 @@ interval_table = { "level": "SS", "color": color_select["SS"]}} } + +# BF2042 专家译名(旧 project classesList,渲染前把英文名翻中文) +SPECIALIST_NAMES = { + "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 侦察", +} diff --git a/hexi/plugins/nonebot_plugin_bf_bot/utils/render_bf2042.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/render_bf2042.py new file mode 100644 index 0000000..6fbc38b --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/render_bf2042.py @@ -0,0 +1,435 @@ +"""BF2042 战绩卡渲染(tracker.gg 通道专用)。 + +- 生涯卡 ``build_bf2042_career_card``:照旧 project(dev/bf2042)的 + `bf_2042_gen_pic` 版式重绘(1920×1080 四面板 + TOP5 武器/载具); +- 数据槽位用旧字段契约(见 ``services.processing.build_bf2042_career_card``), + 没有的字段由数据层填 "–" 占位; +- 榜单卡 ``build_bf2042_list_card``:轻量文字榜单(武器/载具/模式)。 +""" + +import io + +import cv2 +import numpy as np +from PIL import Image, ImageDraw + +from .images import load_avatar +from .images_bf2042 import ( + bf2042_logo, + load_bf2042_font, + no_pats_icon, + object_icon, + pick_background, + platform_icon, + specialist_icon, +) + +__all__ = ["build_bf2042_career_card", "build_bf2042_list_card"] + +# 配色(旧版风格) +_WHITE = (255, 255, 255, 255) +_PANEL = (0, 0, 0, 150) +_GREEN = (144, 238, 144, 255) # lightgreen 标题 +_SKY = (135, 206, 235, 255) # skyblue 名字 +_GOLD = (255, 215, 0, 255) +_LINE_TOP = (204, 255, 0, 255) # TOP3 黄绿 +_LINE_REST = (102, 204, 255, 255) # TOP4-5 蓝 +_VERDICT_COLOR = {"gold": _GOLD, "skyblue": _SKY} + + +def _draw_rect(img: Image.Image, box, radius: int, fill) -> None: + """圆角半透明面板(旧项目 draw_rect 的等价实现)。""" + overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) + ImageDraw.Draw(overlay).rounded_rectangle(box, radius=radius, fill=fill) + img.alpha_composite(overlay) + + +def _fit(draw: ImageDraw.ImageDraw, text: str, font, max_w: float) -> str: + """超宽截断加省略号。""" + text = str(text) + if draw.textlength(text, font=font) <= max_w: + return text + while text and draw.textlength(text + "…", font=font) > max_w: + text = text[:-1] + return text + "…" + + +def _paste(img: Image.Image, source: Image.Image, pos) -> None: + img.alpha_composite(source, (int(pos[0]), int(pos[1]))) + + +def _paste_circle(img: Image.Image, source: Image.Image, pos, radius: int) -> None: + """贴图并做圆角。""" + src = source.convert("RGBA") + mask = Image.new("L", src.size, 0) + ImageDraw.Draw(mask).rounded_rectangle( + (0, 0, src.width, src.height), radius=radius, fill=255 + ) + img.paste(src, (int(pos[0]), int(pos[1])), mask) + + +def _qq_avatar(qq_id: str, size: int) -> Image.Image | None: + """QQ 头像(load_avatar 带缓存与占位兜底);取不到返回 None。""" + if not qq_id: + return None + try: + bgra = load_avatar(str(qq_id), size) + if bgra is None or not getattr(bgra, "size", 0): + return None + rgba = cv2.cvtColor(bgra, cv2.COLOR_BGRA2RGBA) + return Image.fromarray(np.ascontiguousarray(rgba), "RGBA") + except Exception: # noqa: BLE001 - 头像拿不到就回内置占位 + return None + + +def _paste_ic_logo(img: Image.Image) -> None: + """底部信息栏(旧 project `paste_ic_logo` 的移植,按需精简)。 + + 保留:数据来源(tracker.gg)/ 插件作者 / 查询时间 / 友情合作(贴吧官群)。 + 已按用户要求去掉:logo 图、友情赞助两段、铁幕重工。 + """ + import datetime + + _draw_rect(img, (25, 1050, 1895, 1075), 1, _PANEL) + draw = ImageDraw.Draw(img) + f_en = load_bf2042_font(20, "en") + f_ch = load_bf2042_font(20, "ch") + f_ch_s = load_bf2042_font(18, "ch") + + # 左:数据来源 + draw.text((30, 1050), "Data Source : TRACKER.GG", fill=_WHITE, font=f_en) + + # 中:插件作者(左组与右侧块之间居中) + info1, info2 = "Plugin Designed By", "SANSENHOSHI" + f_en_s = load_bf2042_font(18, "en") + w1 = draw.textlength(info1, font=f_en_s) + w2 = draw.textlength(info2, font=f_en_s) + x0 = (360 + 1180 - (w1 + w2 + 20)) / 2 + draw.text((x0, 1052), info1, fill=_WHITE, font=f_en_s) + draw.text((x0 + w1 + 20, 1052), info2, fill=_SKY, font=f_en_s) + + # 右:查询时间 + 友情合作(从右往左排) + now = datetime.datetime.now() + t_text = "查询时间: " + now.strftime("%Y-%m-%d %H:%M:%S") + p2 = "贴吧官群:1019165018" + w_t = draw.textlength(t_text, font=f_ch) + x_p2 = 1870 - round(draw.textlength(p2, font=f_ch_s)) + x_coop = round(x_p2 - draw.textlength("友情合作:", font=f_ch_s) - 24) + x_time = round(x_coop - w_t - 40) + draw.text((x_time, 1052), t_text, fill=_WHITE, font=f_ch) + draw.text((x_coop, 1052), "友情合作:", fill=_WHITE, font=f_ch_s) + draw.text((x_p2, 1052), p2, fill=(153, 204, 0, 255), font=f_ch_s) + + +def build_bf2042_career_card(card: dict): + """生涯卡:1920×1080,返回 BytesIO(PNG)。 + + 版式照旧 `bf_2042_gen_pic`: + [玩家信息 768] [最佳专家 550] [对局数据 552] y 25~262 + [生涯数据 1870×285 四列 + 鉴定] y 275~560 + [TOP5 武器 | TOP5 载具] y 575~1044 + """ + card = card or {} + qq_id = str(card.get("_qq_id") or "") + support = bool(card.get("_support")) + + # 1) 背景(特权用户个人图库 > 公共库随机) + new_img = pick_background(qq_id, support).convert("RGBA") + + # 2) 右上角 logo + logo = bf2042_logo(130, 130) + _paste_circle(new_img, logo, (1762, 30), 10) + + # 3) 面板 + _draw_rect(new_img, (25, 25, 793, 262), 10, _PANEL) # 玩家信息 + _draw_rect(new_img, (808, 25, 1358, 262), 10, _PANEL) # 最佳专家 + _draw_rect(new_img, (1373, 25, 1895, 262), 10, _PANEL) # 对局数据 + _draw_rect(new_img, (25, 275, 1895, 560), 10, _PANEL) # 生涯数据 + _draw_rect(new_img, (25, 575, 1895, 1044), 10, _PANEL) # TOP5 + + draw = ImageDraw.Draw(new_img) + f22 = load_bf2042_font(22, "ch") + f24 = load_bf2042_font(24, "ch") + f26 = load_bf2042_font(26, "ch") + f28 = load_bf2042_font(28, "ch") + f30 = load_bf2042_font(30, "ch") + f28e = load_bf2042_font(28, "en") + f32e = load_bf2042_font(32, "en") + f38 = load_bf2042_font(38, "ch") + + # ---- ① 玩家信息 ---- + avatar = _qq_avatar(qq_id, 140) or no_pats_icon(140, 140) + _paste_circle(new_img, avatar, (30, 30), 10) + + draw.text((196, 36), "玩家:", fill=_WHITE, font=f28) + name = _fit(draw, card.get("userName", "—"), f32e, 320) + draw.text((268, 32), name, fill=_WHITE, font=f32e) + + draw.text((196, 84), "LEVEL: ", fill=_WHITE, font=f30) + draw.text((292, 84), str(card.get("level", "—")), fill=_WHITE, font=f30) + + plat = platform_icon(card.get("platform", "origin"), 34, 34) + _paste(new_img, plat, (199, 130)) + draw.text((240, 132), "游玩时长:", fill=_WHITE, font=f26) + draw.text((378, 128), f"{card.get('secondsPlayed', '—')} H", fill=_WHITE, font=f30) + + # ---- ② 最佳专家 ---- + best = card.get("bestClass") or {} + draw.text((826, 62), "最 佳", fill=_GREEN, font=f38) + draw.text((826, 117), "专 家", fill=_GREEN, font=f38) + if best: + icon = specialist_icon(best.get("characterName", ""), 88, 88) + _paste(new_img, icon, (924, 42)) + draw.text( + (1024, 46), + str(best.get("displayName") or best.get("characterName", "—")), + fill=_SKY, + font=f28, + ) + draw.text( + (1024, 86), f"K/D:{best.get('killDeath', '—')}", fill=_WHITE, font=f22 + ) + draw.text((1024, 118), f"KPM:{best.get('kpm', '—')}", fill=_WHITE, font=f22) + draw.text((1024, 150), f"击杀:{best.get('kills', '—')}", fill=_WHITE, font=f22) + draw.text( + (1024, 182), + f"时长:{best.get('secondsPlayed', 0) // 3600} H", + fill=_WHITE, + font=f22, + ) + else: + draw.text((924, 120), "暂无数据", fill=_WHITE, font=f30) + + # ---- ③ 对局数据(旧三行:场数 / MVP·最佳小队 / 处决) ---- + x3 = 1396 + draw.text( + (x3, 58), f"游玩场数: {card.get('matchesPlayed', '—')}", fill=_WHITE, font=f28 + ) + draw.text((x3, 118), f"MVP: {card.get('mvp', '—')}", fill=_WHITE, font=f28) + draw.text( + (x3, 178), f"处决: {card.get('playerTakeDowns', '—')}", fill=_WHITE, font=f28 + ) + + # ---- ④ 生涯数据(四列) ---- + def cell(x: float, y: float, label: str, value) -> None: + draw.text((x, y), label, fill=_WHITE, font=f28) + draw.text((x + 232, y), _fit(draw, str(value), f28, 205), fill=_WHITE, font=f28) + + y0 = 290 + step = 50 + col1, col2, col3, col4 = 52, 500, 948, 1373 + + cell(col1, y0, "K/D:", card.get("killDeath", "—")) + cell(col1, y0 + step, "真实 K/D:", card.get("infantryKillDeath", "—")) + cell(col1, y0 + step * 2, "击杀:", card.get("kills", "—")) + cell( + col1, + y0 + step * 3, + "载具击杀:", + (card.get("dividedKills") or {}).get("vehicle", "—"), + ) + cell(col1, y0 + step * 4, "死亡数:", card.get("deaths", "—")) + + cell(col2, y0, "KPM:", card.get("killsPerMinute", "—")) + cell(col2, y0 + step, "真实 KPM:", card.get("realKpm", "—")) + cell(col2, y0 + step * 2, "爆头率:", card.get("headShots", "—")) + cell(col2, y0 + step * 3, "命中率:", card.get("accuracy", "—")) + cell(col2, y0 + step * 4, "胜率:", card.get("winPercent", "—")) + + cell(col3, y0, "AI 击杀:", (card.get("dividedKills") or {}).get("ai", "—")) + cell(col3, y0 + step, "场均击杀:", card.get("killsPerMatch", "—")) + cell(col3, y0 + step * 2, "急救数:", card.get("revives", "—")) + cell(col3, y0 + step * 3, "标记敌人数:", card.get("enemiesSpotted", "—")) + cell(col3, y0 + step * 4, "摧毁载具数:", card.get("vehiclesDestroyed", "—")) + + draw.text((col4, y0 - 8), "机器棱鉴定(仅供参考):", fill=_WHITE, font=f22) + verdict = str(card.get("verdict") or "未知") + color = _VERDICT_COLOR.get(str(card.get("verdictColor") or ""), _WHITE) + draw.multiline_text((col4, y0 + 28), verdict, fill=color, font=f26, spacing=6) + draw.text((col4, y0 + 140), "联BAN查询:", fill=_WHITE, font=f24) + draw.text((col4, y0 + 178), "–", fill=_GOLD, font=f24) + + # ---- ⑤ TOP5 武器 / 载具 ---- + draw.line((950, 600, 950, 1032), fill=_WHITE, width=3) + + def draw_top_list( + col_x: float, + rows: list[dict], + icon_size: tuple[int, int], + name_key: str, + name_w: float, + title: str, + is_vehicle: bool, + ) -> None: + draw.text((col_x, 588), title, fill=_GREEN, font=f30) + row_y = 630 + for i, row in enumerate(rows[:5]): + color = _LINE_TOP if i < 3 else _LINE_REST + draw.line((col_x, row_y + 6, col_x, row_y + 72), fill=color, width=5) + icon = object_icon(row.get(name_key, ""), *icon_size) + _paste(new_img, icon, (col_x + 14, row_y + 4)) + name_x = col_x + 14 + icon_size[0] + 16 + label = _fit(draw, str(row.get(name_key, "—")), f28e, name_w) + draw.text((name_x, row_y + 2), label, fill=_WHITE, font=f28e) + draw.text( + (name_x, row_y + 40), + f"击杀:{row.get('kills', '—')}", + fill=_WHITE, + font=f26, + ) + if is_vehicle: + draw.text( + (name_x + 190, row_y + 2), + f"KPM:{row.get('killsPerMinute', '—')}", + fill=_WHITE, + font=f22, + ) + draw.text( + (name_x + 190, row_y + 40), + f"摧毁数:{row.get('destroyed', '—')}", + fill=_WHITE, + font=f22, + ) + else: + draw.text( + (name_x + 180, row_y + 2), + f"爆头率:{row.get('headshots', '—')}", + fill=_WHITE, + font=f22, + ) + draw.text( + (name_x + 180, row_y + 40), + f"命中率:{row.get('accuracy', '—')}", + fill=_WHITE, + font=f22, + ) + draw.text( + (name_x + 360, row_y + 2), + f"KPM:{row.get('killsPerMinute', '—')}", + fill=_WHITE, + font=f22, + ) + draw.text( + (name_x + 360, row_y + 40), + f"时长:{row.get('timeEquipped', 0) // 3600} H", + fill=_WHITE, + font=f22, + ) + row_y += 82 + + draw_top_list( + 55, card.get("weapons") or [], (150, 66), "weaponName", 170, "TOP5 武器", False + ) + draw_top_list( + 985, + card.get("vehicles") or [], + (240, 66), + "vehicleName", + 180, + "TOP5 载具", + True, + ) + + # 底部信息栏(数据来源/赞助/作者/时间/合作) + _paste_ic_logo(new_img) + + b_io = io.BytesIO() + new_img.convert("RGB").save(b_io, format="PNG") + return b_io + + +def build_bf2042_list_card(info: dict): + """榜单卡:武器 / 载具 / 模式共用(轻量文字版)。 + + info 形如 {"玩家名称":.., "标题":"武器", "列名":[...], "行":[{"名称","标签","值":[...]}]}, + 数值列统一右对齐;名称过长自动截断,类别跟在名字后面同排显示。 + """ + rows = info.get("行") or [] + columns = info.get("列名") or [] + count = max(len(columns), 1) + + width = 1100 + pad = 50 + value_left = 470 + gap = 24 + bg = (20, 24, 30, 255) + accent = (0, 168, 224, 255) + fg = (236, 240, 244, 255) + muted = (150, 160, 172, 255) + + f_s = load_bf2042_font(28, "ch") + f_xl = load_bf2042_font(44, "ch") + + header_h = 170 + rows_y = header_h + 80 + row_h = 46 + height = rows_y + max(len(rows), 1) * row_h + 30 + + card = Image.new("RGBA", (width, height), bg) + draw = ImageDraw.Draw(card) + + draw.text((pad, 40), f"战地2042 · {info.get('玩家名称', '—')}", fill=fg, font=f_xl) + draw.text( + (pad, 112), + f"{info.get('标题', '')}榜 · 前 {len(rows)} 名", + fill=muted, + font=f_s, + ) + draw.line((pad, header_h, width - pad, header_h), fill=accent, width=3) + + widths = [] + for i in range(count): + widest = draw.textlength(columns[i], font=f_s) if i < len(columns) else 0 + for row in rows: + values = row.get("值") or [] + if i < len(values): + widest = max(widest, draw.textlength(str(values[i]), font=f_s)) + widths.append(widest) + + avail = (width - pad) - value_left + room = avail - gap * (count - 1) + scale = min(1.0, room / sum(widths)) if sum(widths) else 1.0 + widths = [w * scale for w in widths] + + edges = [] + cursor = float(width - pad) + for w in reversed(widths): + edges.append(cursor) + cursor -= w + gap + edges.reverse() + + name_w = max(200.0, (edges[0] - widths[0]) - gap - pad) + + def cell(text: str, right_x: float, row_y: int, color) -> None: + text = str(text) + draw.text( + (right_x - draw.textlength(text, font=f_s), row_y), + text, + fill=color, + font=f_s, + ) + + head_y = header_h + 30 + draw.text((pad, head_y), "名称", fill=muted, font=f_s) + for name, edge in zip(columns, edges): + cell(name, edge, head_y, muted) + draw.line((pad, head_y + 34, width - pad, head_y + 34), fill=muted, width=1) + + y = rows_y + if not rows: + draw.text((pad, y), "(无数据)", fill=muted, font=f_s) + for rank, row in enumerate(rows, 1): + label = _fit(draw, f"{rank}. {row.get('名称', '—')}", f_s, name_w) + draw.text((pad, y), label, fill=fg, font=f_s) + tag = row.get("标签") + if tag: + used = draw.textlength(label, font=f_s) + suffix = _fit(draw, f" · {tag}", f_s, name_w - used) + draw.text((pad + used, y), suffix, fill=muted, font=f_s) + for value, edge in zip(row.get("值") or [], edges): + cell(value, edge, y, fg) + y += row_h + + b_io = io.BytesIO() + card.save(b_io, format="PNG") + return b_io diff --git a/hexi/plugins/nonebot_plugin_bf_bot/utils/render_ea.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/render_ea.py new file mode 100644 index 0000000..89112f1 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/render_ea.py @@ -0,0 +1,917 @@ +"""EA 系(BF1/3/4/5/6)战绩卡渲染:OpenCV 拼模板 + PIL 绘字,输出 PNG BytesIO。""" + +import datetime +import io + +import cv2 +import numpy as np +from PIL import Image, ImageDraw, ImageFont +from nonebot import logger + +from ..paths import RES_DIR +from .images import ( + designation_paste, + draw_centered_text, + draw_right_aligned_text, + get_icon_from_cache, + get_save_icon, + image_paste, + image_paste_center, + load_avatar, + notice_paste, + notice_paste_bf6, + paste_rank_icon, + png_resize, +) +from .params import bf_item, classes +from .text import add_commas, add_commas_with_padding + +# 字体 +_FONT_NUM = RES_DIR / "font" / "bf-sub-headline-bold 等宽数字.ttf" +_FONT_CH = RES_DIR / "font" / "演示创黑FLY.ttf" + +font_XXL = ImageFont.truetype(str(_FONT_NUM), 72) +font_XL = ImageFont.truetype(str(_FONT_NUM), 64) +font_L = ImageFont.truetype(str(_FONT_NUM), 48) +font_ML = ImageFont.truetype(str(_FONT_NUM), 42) +font_M = ImageFont.truetype(str(_FONT_NUM), 36) +font_MS = ImageFont.truetype(str(_FONT_NUM), 32) +font_S = ImageFont.truetype(str(_FONT_NUM), 28) +font_XS = ImageFont.truetype(str(_FONT_NUM), 24) +font_XXS = ImageFont.truetype(str(_FONT_NUM), 18) +font_XXXS = ImageFont.truetype(str(_FONT_NUM), 16) + +font_XXL_CH = ImageFont.truetype(str(_FONT_CH), 72) +font_XL_CH = ImageFont.truetype(str(_FONT_CH), 64) +font_L_CH = ImageFont.truetype(str(_FONT_CH), 48) +font_ML_CH = ImageFont.truetype(str(_FONT_CH), 42) +font_M_CH = ImageFont.truetype(str(_FONT_CH), 36) +font_MS_CH = ImageFont.truetype(str(_FONT_CH), 32) +font_S_CH = ImageFont.truetype(str(_FONT_CH), 28) +font_XS_CH = ImageFont.truetype(str(_FONT_CH), 24) +font_XXS_CH = ImageFont.truetype(str(_FONT_CH), 18) +font_XXXS_CH = ImageFont.truetype(str(_FONT_CH), 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.0, + 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, + qq_id=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(str(RES_DIR / "template" / f"{game}.png"), cv2.IMREAD_COLOR) + + # 载入头像(按QQ号取,取不到时使用占位头像,避免空图 resize 崩溃) + avatar = load_avatar(qq_id, 200) + + # 创建圆形蒙版 + mask = np.zeros((200, 200), dtype=np.uint8) + cv2.circle(mask, (100, 100), 100, 255, -1) # 绘制半径100的实心圆 + + # 合并圆形蒙版与原有alpha通道(load_avatar 保证是BGRA四通道) + alpha = cv2.bitwise_and(avatar[:, :, 3], mask) + avatar[:, :, 3] = alpha + + # 粘贴头像(保持原有坐标) + 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_CH) + draw.text((745, 133), f"{pid}", fill="white", font=font_M_CH) + + # 等级游玩时长 + draw.text((740, 212), f"{rank}", fill="white", font=font_M_CH) + draw.text((985, 212), f"{time_play}H", fill="white", font=font_M_CH) + + # 生涯总览 + + # 场次 + draw_centered_text( + draw=draw, + text=(wins + loses), + x_left=45, + x_right=265, + y=350, + font=font_L_CH, + fill="white", + ) + + # 命中率 + draw_centered_text( + draw=draw, + text=round(float(acc.replace("%", "")), 1), + x_left=415, + x_right=495, + y=343, + font=font_M_CH, + fill="white", + ) + # 爆头率 + if head_shots: + head_shots = head_shots.replace("%", "") + else: + head_shots = 0 + draw_centered_text( + draw=draw, + text=round(float(head_shots), 1), + x_left=395, + x_right=470, + y=420, + font=font_M_CH, + fill="white", + ) + # kd + draw_centered_text( + draw=draw, + text=kd, + x_left=585, + x_right=785, + y=340, + font=font_XL_CH, + fill="white", + ) + # kpm + draw_centered_text( + draw=draw, + text=round(kpm, 1), + x_left=825, + x_right=1025, + y=340, + font=font_XL_CH, + fill="white", + ) + # spm + draw_centered_text( + draw=draw, + text=round(spm, 1), + x_left=1065, + x_right=1265, + y=340, + font=font_XL_CH, + fill="white", + ) + + # 击杀 + draw_centered_text( + draw=draw, + text=kills, + x_left=870, + x_right=1190, + y=595, + font=font_L_CH, + fill="white", + ) + + # 助攻 + draw_centered_text( + draw=draw, + text=kill_assists, + x_left=820, + x_right=1145, + y=770, + font=font_L_CH, + fill="white", + ) + + # 急救 + draw_centered_text( + draw=draw, + text=revives, + x_left=770, + x_right=1090, + y=960, + font=font_L_CH, + fill="white", + ) + + # 摧毁 + draw_centered_text( + draw=draw, + text=destroyed, + x_left=720, + x_right=1040, + y=1140, + font=font_L_CH, + fill="white", + ) + + # 底部栏 + + # 评级 + if game == "bf6": + logger.info("bf6不计算称号") + elif level_designation: + level = level_designation["level"] + des = level_designation["designation"] + color = level_designation["color"] + draw_centered_text( + draw=draw, + text=level, + x_left=70, + x_right=200, + y=1120, + font=font_XL_CH, + fill=color, + ) + draw_centered_text( + draw=draw, + text=des, + x_left=200, + x_right=550, + y=1120, + font=font_XL_CH, + fill="white", + ) + + # 最佳兵种 + + draw_centered_text( + draw=draw, + text=classes[best_class], + x_left=140, + x_right=310, + y=1290, + font=font_L_CH, + fill="white", + ) + + # 最远击杀 + draw_centered_text( + draw=draw, + text=f"{longest_head_shot}m", + x_left=455, + x_right=625, + y=1290, + font=font_L_CH, + fill="white", + ) + + # 最高连杀 + draw_centered_text( + draw=draw, + text=highest_ill_streak, + x_left=765, + x_right=935, + y=1290, + font=font_L_CH, + fill="white", + ) + + # 最佳⭐ + pil_img = build_best(draw, pil_img, best_weapon, best_vehicle, game) + + # 生成时间 + draw.text( + (1870, 1420), f"{formatted_time}", fill=(154, 132, 149), font=font_XXXS_CH + ) + + # ---------------- 转回 OpenCV ---------------- + img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) + + # ---------------- 雷达图 ---------------- + if game != "bf6": + center = (311, 796) + r = 185 + scale_min = 0.25 if min(stat_data) < 0.25 else min(stat_data) + img = draw_radar( + img, + stat_data, + center=center, + r=r, + buffer=0.1, + scale_min=scale_min, + scale_max=1.10, + ) + # 保存 + success, buffer = cv2.imencode(".png", img) + if success: + b_io = io.BytesIO(buffer) + return b_io # 就可以直接 return 二进制流 + else: + return None + + +async def build_bf6_stats_card( + game="bfv", + qq_id=2931, + player="Player001", + pid=1145141919, + rank=114, + kd=1.25, + kpm=2.3, + spm=500, + acc="25.6", + head_shots="10", + time_play=100.0, + kills=100, + kill_assists=150, + revives=114, + repairs=1000, + captured=300, + score=145674, + wins=10, + loses=10, + destroyed=514, + best_weapon="M1907 SF", + best_vehicle="坦克", + best_class="Support", +): + # 获取当前事件并且格式化 + now = datetime.datetime.now() + formatted_time = now.strftime("%Y-%m-%d %H:%M:%S") + + # ---------------- OpenCV 部分 ---------------- + # 载入模板 + img = cv2.imread(str(RES_DIR / "template" / f"{game}.png"), cv2.IMREAD_COLOR) + + # 载入头像(按QQ号取,取不到时使用占位头像,避免空图 resize 崩溃) + avatar = load_avatar(qq_id, 134) + + # 粘贴头像(保持原有坐标) + y, x = 65, 105 + h, w = avatar.shape[:2] + if avatar.shape[2] == 4: + # 带透明通道时按alpha混合,避免直接赋值通道数不匹配 + alpha = avatar[:, :, 3:4] / 255.0 + img[y : y + h, x : x + w] = ( + alpha * avatar[:, :, :3] + (1 - alpha) * img[y : y + h, x : x + w] + ).astype(np.uint8) + else: + img[y : y + h, x : x + w] = avatar + + # 公告栏 + img = notice_paste_bf6(img) + img = designation_paste( + player, + img, + user_id=qq_id, + start_x=104, + start_y=215, + target_height=30, + spacing=10, + ) + + # ---------------- PIL 部分 ---------------- + pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + draw = ImageDraw.Draw(pil_img) + + # 玩家信息 105+160 + draw.text((265, 80), f"{player}", fill="white", font=font_XL) + draw.text((265, 140), f"UID: {pid}", fill="#CCCCCC", font=font_L) + + # 等级 + pil_img = paste_rank_icon(int(rank), pil_img) + draw_centered_text( + draw=draw, + text=f"{rank}", + x_left=950, + x_right=1075, + y=205, + font=font_M, + fill="white", + ) + + # 游玩时长 + draw.text((2300, 350), f"{time_play:.1f} H", fill="white", font=font_L) + # 场次 + draw.text((2300, 450), f"{wins + loses}", fill="white", font=font_L) + + # 生涯总览 + # 胜率 + + win_rate = round((wins / (wins + loses)) * 100, 1) + logger.info(f"胜场{wins},负场{loses},胜率{win_rate}") + + draw_centered_text( + draw=draw, + text=f"{win_rate}%", + x_left=2090, + x_right=2200, + y=375, + font=font_XL, + fill="white", + ) + + # 命中率 + acc_format = round(float(acc.replace("%", "")), 1) + draw_centered_text( + draw=draw, + text=f"{acc_format}%", + x_left=1210, + x_right=1303, + y=363, + font=font_M, + fill="white", + ) + # 爆头率 + if head_shots: + head_shots = head_shots.replace("%", "") + else: + head_shots = 0 + draw_centered_text( + draw=draw, + text=f"{round(float(head_shots), 1)}%", + x_left=1210, + x_right=1303, + y=458, + font=font_M, + fill="white", + ) + # kd + draw.text((1350, 355), text=str(kd), fill="white", font=font_XXL) + # kpm + draw.text((1610, 355), text=f"{kpm:.2f}", fill="white", font=font_XXL) + # spm + draw.text((1870, 355), text=f"{int(spm)}", fill="white", font=font_XXL) + + # 击杀 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 10), + x_left=1165, + x_right=1455, + y=805, + font=font_L, + fill="#444445", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(kills)), + x_left=1165, + x_right=1455, + y=805, + font=font_L, + fill="white", + ) + + # 急救 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 10), + x_left=1165, + x_right=1455, + y=1080, + font=font_L, + fill="#444445", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(revives)), + x_left=1165, + x_right=1455, + y=1080, + font=font_L, + fill="white", + ) + + # 摧毁 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 10), + x_left=1165, + x_right=1455, + y=1350, + font=font_L, + fill="#444445", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(destroyed)), + x_left=1165, + x_right=1455, + y=1350, + font=font_L, + fill="white", + ) + + # 右侧 + # 占领 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 10), + x_left=1485, + x_right=1780, + y=745, + font=font_L, + fill="#444445", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(captured)), + x_left=1485, + x_right=1780, + y=745, + font=font_L, + fill="white", + ) + + # 得分 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 10), + x_left=1485, + x_right=1780, + y=955, + font=font_L, + fill="#444445", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(score)), + x_left=1485, + x_right=1780, + y=955, + font=font_L, + fill="white", + ) + + # 助攻 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 10), + x_left=1485, + x_right=1780, + y=1160, + font=font_L, + fill="#444445", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(kill_assists)), + x_left=1485, + x_right=1780, + y=1160, + font=font_L, + fill="white", + ) + + # 修理 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 10), + x_left=1485, + x_right=1780, + y=1370, + font=font_L, + fill="#444445", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(repairs)), + x_left=1485, + x_right=1780, + y=1370, + font=font_L, + fill="white", + ) + + # 最佳⭐ + pil_img = build_best_bf6(draw, pil_img, best_weapon, best_vehicle, best_class, game) + + # 生成时间 + draw.text( + (1750, 10), + f"{formatted_time}/维护 SANSENHOSHI", + fill=(154, 132, 149), + font=font_XS_CH, + ) + + # ---------------- 转回 OpenCV ---------------- + img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) + + # 保存 + success, buffer = cv2.imencode(".png", img) + if success: + b_io = io.BytesIO(buffer) + return b_io # 就可以直接 return 二进制流 + else: + return None + + +def build_best(draw, img, 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_CH, + fill="white", + ) + draw_centered_text( + draw=draw, + text=w_kills, + x_left=1190, + x_right=1390, + y=1260, + font=font_L_CH, + fill="white", + ) + draw_centered_text( + draw=draw, + text=w_kpm, + x_left=1510, + x_right=1635, + y=1260, + font=font_L_CH, + fill="white", + ) + draw_centered_text( + draw=draw, + text=acc, + x_left=1735, + x_right=1803, + y=1030, + font=font_M_CH, + fill="white", + ) + draw_centered_text( + draw=draw, + text=headshots, + x_left=1695, + x_right=1760, + y=1200, + font=font_M_CH, + 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_CH, + fill="white", + ) + draw_centered_text( + draw=draw, + text=v_kills, + x_left=1940, + x_right=2150, + y=1260, + font=font_L_CH, + fill="white", + ) + draw_centered_text( + draw=draw, + text=v_kpm, + x_left=2295, + x_right=2465, + y=1055, + font=font_XL_CH, + fill="white", + ) + draw_centered_text( + draw=draw, + text=destroyed, + x_left=2265, + x_right=2440, + y=1260, + font=font_L_CH, + fill="white", + ) + + # 图片 + weapon_url = weapons["image"] + 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 build_best_bf6(draw, img, weapons, vehicle, classes, game: str): + # 最佳武器 + weapon_name = weapons["weaponName"] + w_kills = weapons["kills"] + w_kpm = weapons["killsPerMinute"] + acc = round(float(weapons["accuracy"].replace("%", "")), 1) + headshots = round(float(weapons["headshots"].replace("%", "")), 1) + + # 最佳载具 + vehicle_name = vehicle["vehicleName"] + v_kills = vehicle["kills"] + v_kpm = vehicle["killsPerMinute"] + destroyed = vehicle["destroyed"] + + # 绘制最佳⭐ + # 武器 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 7), + x_left=95, + x_right=313, + y=665, + font=font_L, + fill="#313131", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(w_kills)), + x_left=95, + x_right=313, + y=665, + font=font_L, + fill="white", + ) + draw_centered_text( + draw=draw, text=w_kpm, x_left=335, x_right=447, y=665, font=font_L, fill="white" + ) + draw_centered_text( + draw=draw, + text=f"{acc}%", + x_left=470, + x_right=580, + y=670, + font=font_M, + fill="white", + ) + draw_centered_text( + draw=draw, + text=f"{headshots}%", + x_left=610, + x_right=717, + y=670, + font=font_M, + fill="white", + ) + + # 载具 + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 7), + x_left=125, + x_right=325, + y=1087, + font=font_L, + fill="#313131", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas(str(v_kills)), + x_left=125, + x_right=325, + y=1087, + font=font_L, + fill="white", + ) + draw_centered_text( + draw=draw, + text=v_kpm, + x_left=351, + x_right=463, + y=1087, + font=font_L, + fill="white", + ) + draw_right_aligned_text( + draw=draw, + text=add_commas_with_padding("0", 7), + x_left=477, + x_right=688, + y=1087, + font=font_L, + fill="#313131", + ) + draw_right_aligned_text( + draw=draw, + text=destroyed, + x_left=477, + x_right=688, + y=1087, + font=font_L, + fill="white", + ) + + # 图片 + weapon_url = weapons["image"] + vehicle_url = vehicle["image"] + + wp_icon = get_save_icon(game, weapon_name, "weapon", weapon_url) + vc_icon = get_save_icon(game, vehicle_name, "vehicles", vehicle_url) + classes_icon = get_icon_from_cache(classes, "bf6", "classes") + wp_icon_new = png_resize(wp_icon, new_width=729 - 88) + vc_icon_new = png_resize(vc_icon, new_width=729 - 88) + classes_icon_new = png_resize( + classes_icon, new_height=1165 - 326, new_width=1100 - 748 + ) + + img = image_paste(wp_icon_new, img, (88, 325)) + img = image_paste(vc_icon_new, img, (88, 746)) + img = image_paste(classes_icon_new, img, (748, 326)) + + 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 diff --git a/hexi/plugins/nonebot_plugin_bf_bot/text_utils.py b/hexi/plugins/nonebot_plugin_bf_bot/utils/text.py similarity index 50% rename from hexi/plugins/nonebot_plugin_bf_bot/text_utils.py rename to hexi/plugins/nonebot_plugin_bf_bot/utils/text.py index 84511e8..be8432e 100644 --- a/hexi/plugins/nonebot_plugin_bf_bot/text_utils.py +++ b/hexi/plugins/nonebot_plugin_bf_bot/utils/text.py @@ -1,3 +1,5 @@ +"""文本/数字格式化。""" + import re @@ -9,14 +11,17 @@ def convert_to_hours(time_str): - "1:56:55" (小时:分钟:秒) - "0:06:41" (分钟:秒) - "14:11:36" (小时:分钟:秒) + + 接口偶尔会给带引号包裹的串(如 '"1 day, 1:49:15"'),先统一剥掉引号/空白。 """ total_hours = 0.0 + time_str = (time_str or "").strip().strip('"').strip("'") # 处理包含天数的部分 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 + days_match = re.search(r"\d+", days_part) + total_hours += (float(days_match.group()) if days_match else 0.0) * 24 time_str = time_part # 分割时间部分 @@ -31,3 +36,19 @@ def convert_to_hours(time_str): total_hours += parts[0] / 3600 return round(total_hours, 2) + + +def add_commas(text): + """给纯数字字符串加千分位,如 "1234567" -> "1,234,567"。""" + return re.sub(r"(? JSONResponse: + return JSONResponse({"status": 0, "msg": msg, "data": data}) + + +def _fail(msg: str, status: int = 400) -> JSONResponse: + return JSONResponse({"status": status, "msg": msg}) + + +def build_admin_app() -> FastAPI: + """构建绑定管理 API 子应用(挂载到 /api/bf_bot)。""" + app = FastAPI(title="BF查询绑定管理") + auth = require_admin + + @app.get("/bindings", response_class=JSONResponse, dependencies=[auth]) + async def list_bindings(page: int = 1, per_page: int = 10): + """绑定列表(QQ / EA 名称 / EA 双 ID / 标签 / 绑定时间),按绑定时间倒序分页。""" + page = max(1, page) + per_page = min(max(1, per_page), 100) + manager = UserManager() + total = manager.count_users() + users = manager.list_users(limit=per_page, offset=(page - 1) * per_page) + return _ok( + { + "bindings": [ + { + "qq_id": u.get("qq_id"), + "ea_player_name": u.get("ea_player_name"), + "ea_player_id": u.get("ea_player_id"), + "ea_user_id": u.get("ea_user_id"), + "designation": u.get("designation"), + "create_time": u.get("create_time"), + } + for u in users + ], + "total": total, + "page": page, + "per_page": per_page, + } + ) + + @app.delete("/bindings/{qq_id}", response_class=JSONResponse, dependencies=[auth]) + async def delete_binding(qq_id: str): + """解绑某 QQ(等价于群里的「解绑」)。""" + manager = UserManager() + record = manager.get_user_by_qq(qq_id) + if not record: + return _fail("未找到该 QQ 的绑定记录", 404) + rows = manager.delete_user_by_qq(qq_id) + if rows > 0: + return _ok( + {"qq_id": qq_id, "ea_player_name": record.get("ea_player_name")}, + f"已解绑 {record.get('ea_player_name')}", + ) + return _fail("删除失败") + + return app diff --git a/hexi/web/src/plugins/bf_bot/index.tsx b/hexi/web/src/plugins/bf_bot/index.tsx new file mode 100644 index 0000000..490fa77 --- /dev/null +++ b/hexi/web/src/plugins/bf_bot/index.tsx @@ -0,0 +1,136 @@ +import { useCallback, useEffect, useState } from 'react' +import { Button, Card, Spinner, toast } from '@heroui/react' +import { TrashBin } from '@gravity-ui/icons' +import { api } from '../../api/client' +import { PageHead, Pager, tdCls, thCls } from '../../components/ui' + +// QQ↔EAID 绑定管理(后端 hexi/plugins/nonebot_plugin_bf_bot/web/admin.py, +// 与群里的「绑定 / 解绑」命令改的是同一份 SQLite 绑定库) +const bf = api('bf_bot') +const PER_PAGE = 10 + +function data(res: any) { + if (res && typeof res === 'object' && typeof res.status === 'number') { + if (res.status !== 0) throw new Error(res.msg || '请求失败') + return res.data + } + return res +} + +interface BindingRow { + qq_id: string + ea_player_name: string + ea_player_id: string + ea_user_id: string + designation: string + create_time: string +} + +export default function BfBotPage() { + const [rows, setRows] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [err, setErr] = useState('') + const [loading, setLoading] = useState(true) + const [deleting, setDeleting] = useState('') + + const load = useCallback(async (p: number) => { + setLoading(true); setErr('') + try { + const d = data(await bf.get(`bindings?page=${p}&per_page=${PER_PAGE}`)) + setRows((d && d.bindings) || []) + setTotal((d && d.total) || 0) + } catch (e: any) { + setErr(e.message || '加载失败') + } finally { + setLoading(false) + } + }, []) + useEffect(() => { load(page) }, [load, page]) + + const remove = async (row: BindingRow) => { + if (!window.confirm(`确定解绑 ${row.qq_id}(${row.ea_player_name})?群里需要重新发「绑定 EAID」。`)) return + setDeleting(row.qq_id) + try { + const res = data(await bf.del(`bindings/${encodeURIComponent(row.qq_id)}`)) + toast.success(res?.msg || '已解绑') + // 删掉本页最后一条且不在第一页时退回上一页,否则刷新当前页 + if (rows.length === 1 && page > 1) setPage(page - 1) + else await load(page) + } catch (e: any) { + toast.danger(e.message || '解绑失败') + } finally { + setDeleting('') + } + } + + if (loading && !rows.length) return
+ + return ( +
+ + 群里发「绑定 EAID」新增绑定,这里只做查看与解绑 + + } + /> + {err &&

{err}

} + + + + + 绑定列表 + {total} 条 + + + + + + + + + + + + + + + {rows.map(r => ( + + + + + + + + ))} + {!rows.length && ( + + + + )} + +
QQEA 名称EA 双 ID绑定时间
{r.qq_id}{r.ea_player_name || '-'} + {r.ea_player_id || '-'} / {r.ea_user_id || '-'} + + {r.create_time || '-'} + + +
+ {loading ? '加载中…' : '还没有任何绑定,群里 @机器人 发「绑定 EAID」即可'} +
+ +
+
+
+ ) +} diff --git a/tests/test_bf_bot_helpers.py b/tests/test_bf_bot_helpers.py new file mode 100644 index 0000000..95e7d90 --- /dev/null +++ b/tests/test_bf_bot_helpers.py @@ -0,0 +1,246 @@ +"""BF Bot 纯函数助手单测:BF2042 参数解析 / tracker_edge URL 工具 / 文本格式化。 + +被测模块(utils/args.py、utils/text.py、services/fetchers/tracker_edge.py、 +services/fetchers/tracker_bridge.py)顶层不依赖插件包相对导入,故用 importlib +按文件路径裸加载(与 test_video_policy.py 同法),不触发插件包 __init__ 的 +NoneBot 初始化。 +""" + +import importlib.util +import sys +import time +from pathlib import Path + +import pytest + +_PLUGIN_DIR = ( + Path(__file__).resolve().parents[1] / "hexi" / "plugins" / "nonebot_plugin_bf_bot" +) + + +def _load(name: str, rel_path: str): + spec = importlib.util.spec_from_file_location(name, _PLUGIN_DIR / rel_path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +@pytest.fixture(scope="module") +def args_mod(): + return _load("bf_args_under_test", "utils/args.py") + + +@pytest.fixture(scope="module") +def text_mod(): + return _load("bf_text_under_test", "utils/text.py") + + +@pytest.fixture(scope="module") +def tracker_edge_mod(): + return _load("bf_tracker_edge_under_test", "services/fetchers/tracker_edge.py") + + +@pytest.fixture(scope="module") +def tracker_bridge_mod(): + return _load("bf_tracker_bridge_under_test", "services/fetchers/tracker_bridge.py") + + +# --------------------------------------------------------------------------- # +# utils/args.py — parse_bf2042_args +# --------------------------------------------------------------------------- # +def test_bf2042_args_empty_is_career(args_mod): + for raw in ("", " ", None): + target = args_mod.parse_bf2042_args(raw) + assert (target.view, target.kind, target.player) == ("career", "", "") + + +def test_bf2042_args_plain_name_is_career(args_mod): + target = args_mod.parse_bf2042_args("SomePlayer") + assert (target.view, target.kind, target.player) == ("career", "", "SomePlayer") + + +def test_bf2042_args_name_with_spaces_kept(args_mod): + # 首个词不是关键词时,整串(含空格)都是玩家名 + target = args_mod.parse_bf2042_args("Some Player Name") + assert target.view == "career" + assert target.player == "Some Player Name" + + +def test_bf2042_args_prefix_like_name_is_career(args_mod): + # 形近但不是关键词的词也算玩家名 + target = args_mod.parse_bf2042_args("weap") + assert (target.view, target.kind, target.player) == ("career", "", "weap") + + +@pytest.mark.parametrize( + ("head", "kind"), + [ + ("武器", "weapon"), + ("枪械", "weapon"), + ("weapon", "weapon"), + ("weapons", "weapon"), + ("载具", "vehicle"), + ("vehicle", "vehicle"), + ("vehicles", "vehicle"), + ("装备", "gadget"), + ("道具", "gadget"), + ("gadget", "gadget"), + ("gadgets", "gadget"), + ], +) +def test_bf2042_args_list_views(args_mod, head, kind): + target = args_mod.parse_bf2042_args(f"{head} SomePlayer") + assert (target.view, target.kind, target.player) == ("list", kind, "SomePlayer") + + +def test_bf2042_args_list_view_case_insensitive(args_mod): + target = args_mod.parse_bf2042_args("Weapons SomePlayer") + assert (target.view, target.kind, target.player) == ("list", "weapon", "SomePlayer") + + +def test_bf2042_args_gamemode(args_mod): + target = args_mod.parse_bf2042_args("模式") + assert (target.view, target.kind, target.player) == ("gamemode", "", "") + target = args_mod.parse_bf2042_args("模式 SomePlayer") + assert (target.view, target.kind, target.player) == ("gamemode", "", "SomePlayer") + + +def test_bf2042_args_extra_whitespace_stripped(args_mod): + target = args_mod.parse_bf2042_args(" 武器 Some Player ") + assert (target.view, target.kind, target.player) == ( + "list", + "weapon", + "Some Player", + ) + + +def test_bf2042_game_names_complete(args_mod): + assert set(args_mod.BF_GAME_NAMES) == {"bf3", "bf4", "bf1", "bfv", "bf2042", "bf6"} + + +# --------------------------------------------------------------------------- # +# services/fetchers/tracker_edge.py — URL 工具 +# --------------------------------------------------------------------------- # +def test_tracker_edge_norm_strips_query_and_slash(tracker_edge_mod): + norm = tracker_edge_mod._norm + assert norm("https://api.tracker.gg/x/?") == "https://api.tracker.gg/x" + assert norm("https://api.tracker.gg/x") == "https://api.tracker.gg/x" + assert norm("https://api.tracker.gg/x/?a=1") == "https://api.tracker.gg/x" + + +def test_tracker_edge_segment_urls_match_page_requests(tracker_edge_mod): + # 页面自己发的是这三个(带尾部 ?);URL 里玩家名须 URL 编码 + urls = tracker_edge_mod._segment_urls("Some Player") + base = "https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/Some%20Player" + assert urls["profile"] == base + "?" + assert urls["weapon"] == base + "/segments/weapon?" + assert urls["vehicle"] == base + "/segments/vehicle?" + + +def test_tracker_edge_page_url(tracker_edge_mod): + url = tracker_edge_mod._page_url("Some Player", "origin") + assert url == "https://tracker.gg/bf2042/profile/origin/Some%20Player/overview" + + +def test_tracker_edge_rollup_shapes(tracker_edge_mod): + ok = tracker_edge_mod._rollup( + {"profile": {"data": {"x": 1}}, "weapon": {"data": [1]}}, None, "" + ) + assert ok["error"] is None + assert ok["weapon"] == [1] + assert ok["vehicle"] == [] and ok["soldier"] == [] + assert ok["gadget"] == [] # 直连通道会填;桥兜底不产 gadget,也要有键 + + full = tracker_edge_mod._rollup( + {"profile": {"data": {}}, "gadget": [{"type": "gadget"}]}, None, "" + ) + assert full["gadget"] == [{"type": "gadget"}] + + missing = tracker_edge_mod._rollup({}, None, "") + assert (missing["error"], missing["profile"]) == ("error", None) + + failed = tracker_edge_mod._rollup({}, "timeout", "等待页面数据超时") + assert failed["error"] == "timeout" + assert failed["detail"] == "等待页面数据超时" + + +# --------------------------------------------------------------------------- # +# services/fetchers/tracker_bridge.py — 任务收集器(纯逻辑) +# --------------------------------------------------------------------------- # +_BRIDGE_BASE = ( + "https://api.tracker.gg/api/v2/bf2042/standard/profile/origin/Some%20Player" +) +_BRIDGE_WANT = { + "profile": _BRIDGE_BASE + "?", + "weapon": _BRIDGE_BASE + "/segments/weapon?", + "vehicle": _BRIDGE_BASE + "/segments/vehicle?", + "soldier": _BRIDGE_BASE + "/segments/soldier?", +} + + +def test_bridge_norm_strips_query_and_slash(tracker_bridge_mod): + norm = tracker_bridge_mod._norm + assert norm("https://x/y/?a=1") == "https://x/y" + assert norm("https://x/y/") == "https://x/y" + + +def test_bridge_collector_feed_matches_only_expected(tracker_bridge_mod): + c = tracker_bridge_mod._Collector("Some Player", "origin", _BRIDGE_WANT) + assert c.feed(_BRIDGE_BASE + "?extra=1", {"n": 1}) == "profile" # query 不影响 + assert c.feed(_BRIDGE_BASE + "?", {"n": 2}) is None # 重复不再收 + other = _BRIDGE_BASE.replace("Some%20Player", "Other") + assert c.feed(other + "/segments/weapon?", {}) is None # 别的玩家不命中 + assert c.feed(_BRIDGE_BASE + "/segments/weapon?", {}) == "weapon" + assert c.feed(_BRIDGE_BASE + "/segments/vehicle?", {}) == "vehicle" + + +def test_bridge_collector_ready_needs_required_and_grace(tracker_bridge_mod): + c = tracker_bridge_mod._Collector("Some Player", "origin", _BRIDGE_WANT) + for key in ("profile", "weapon", "vehicle"): + c.feed(_BRIDGE_WANT[key], {}) + assert c.done_at is not None # 必收键齐自动开始宽限计时 + assert not c.ready # 宽限窗口内继续等 soldier + c.feed(_BRIDGE_WANT["soldier"], {"s": 1}) + assert "soldier" in c.captured + c.done_at = time.monotonic() - tracker_bridge_mod._GRACE_S - 0.01 + assert c.ready + + +def test_bridge_collector_mark_done_needs_all_required(tracker_bridge_mod): + c = tracker_bridge_mod._Collector("Some Player", "origin", _BRIDGE_WANT) + c.mark_done() + assert c.done_at is not None + c.done_at = time.monotonic() - tracker_bridge_mod._GRACE_S - 0.01 + assert not c.ready # 宽限过了也没用:必收键没齐 + + +# --------------------------------------------------------------------------- # +# utils/text.py — convert_to_hours / add_commas +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize( + ("raw", "hours"), + [ + ("1 day, 1:49:15", 25.82), # bfv 实测格式 + ("1:56:55", 1.95), + ("0:06:41", 0.11), + ("14:11:36", 14.19), # bf1 实测格式 + ("14 days, 5:52:29", 341.87), # bf6 实测格式 + ('"1 day, 1:49:15"', 25.82), # 兼容带引号包裹 + (" 1:00:00 ", 1.0), # 兼容前后空白 + ], +) +def test_convert_to_hours(text_mod, raw, hours): + assert text_mod.convert_to_hours(raw) == hours + + +def test_convert_to_hours_days(text_mod): + assert text_mod.convert_to_hours("2 days, 0:00:00") == 48.0 + + +def test_add_commas(text_mod): + assert text_mod.add_commas("1234567") == "1,234,567" + assert text_mod.add_commas("123") == "123" + assert text_mod.add_commas_with_padding("0", 7) == "0,000,000" + assert text_mod.add_commas_with_padding(145674, 7) == "0,145,674" diff --git a/tests/test_bf_bot_repository.py b/tests/test_bf_bot_repository.py new file mode 100644 index 0000000..eae6a76 --- /dev/null +++ b/tests/test_bf_bot_repository.py @@ -0,0 +1,192 @@ +"""BF Bot 绑定库 (repository.py) 单测:建库幂等 / 列类型比较 / UserManager 回环。 + +repository.py 顶层只依赖标准库 + 同包 paths.py,用 importlib 造一次性包骨架 +(与 test_mc_servers.py 同法)加载,避免触发插件包 __init__ 的 NoneBot 初始化。 +**所有用例都显式传 tmp_path 下的库文件**,绝不碰 hexi/data 下的真实绑定库。 +""" + +import importlib.util +import sqlite3 +import sys +import types +from pathlib import Path + +import pytest + +_PLUGIN_DIR = ( + Path(__file__).resolve().parents[1] / "hexi" / "plugins" / "nonebot_plugin_bf_bot" +) +_PKG = "bf_repo_testpkg" + + +def _load_by_path(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +@pytest.fixture +def repo(tmp_path): + """一次性包骨架 + 全新 repository 模块 + tmp_path 下的空库文件。""" + pkg = types.ModuleType(_PKG) + pkg.__path__ = [str(_PLUGIN_DIR)] # type: ignore[attr-defined] + sys.modules[_PKG] = pkg + _load_by_path(f"{_PKG}.paths", _PLUGIN_DIR / "paths.py") + module = _load_by_path(f"{_PKG}.repository", _PLUGIN_DIR / "repository.py") + return module, str(tmp_path / "user_data.db") + + +def test_paths_defaults_point_into_hexi_data(tmp_path, repo): + """默认库路径/头像/缓存必须都在 hexi/data 下,不再落插件源码目录。""" + module, _ = repo + paths = sys.modules[f"{_PKG}.paths"] + hexi_root = _PLUGIN_DIR.parents[1] + assert paths.DATA_DIR == hexi_root / "data" / "nonebot_plugin_bf_bot" + assert paths.DB_PATH.parent == paths.DATA_DIR + assert paths.RES_DIR == _PLUGIN_DIR / "res" + assert module.TableManager().db_path == str(paths.DB_PATH) + + +def test_initialize_database_idempotent(repo): + module, db_path = repo + manager = module.DatabaseManager(db_path=db_path) + + assert manager.initialize_database() is True + assert manager.initialize_database() is True # 第二次建表仍是幂等成功 + + conn = sqlite3.connect(db_path) + tables = { + row[0] + for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + } + # 四张业务表齐全;sqlite_sequence 属内部表,完整性校验会忽略 + assert { + "users", + "designation", + "medals", + "query_record", + } <= tables + assert manager._verify_database_integrity(conn) is True + conn.close() + + +def test_initialize_database_failure_keeps_file(repo, tmp_path): + """建库失败只返回 False,不得删库/抛异常(删库会连带丢绑定)。""" + module, _ = repo + bad_path = str(tmp_path / "不存在的目录" / "x.db") # 父目录不存在,打不开 + assert module.DatabaseManager(db_path=bad_path).initialize_database() is False + + +def test_type_head(repo): + module, _ = repo + type_head = module.DatabaseManager._type_head + assert type_head("INTEGER PRIMARY KEY AUTOINCREMENT") == "INTEGER" + assert type_head("text") == "TEXT" + assert type_head("") == "" + assert type_head(None) == "" + + +def test_compare_columns(repo): + module, _ = repo + manager = module.DatabaseManager(db_path=":memory:") + # PRAGMA 返回值不带约束,只比类型首词 + assert manager._compare_columns( + [("id", "INTEGER"), ("qq_id", "TEXT")], + [("id", "INTEGER PRIMARY KEY AUTOINCREMENT"), ("qq_id", "TEXT NOT NULL")], + ) + # 列名不同 / 列数不同都要判 False + assert not manager._compare_columns([("id", "INTEGER")], [("idx", "INTEGER")]) + assert not manager._compare_columns( + [("id", "INTEGER")], [("id", "INTEGER"), ("qq_id", "TEXT")] + ) + # 类型首词不同也判 False + assert not manager._compare_columns([("id", "INTEGER")], [("id", "TEXT")]) + + +def test_user_roundtrip(repo): + module, db_path = repo + module.DatabaseManager(db_path=db_path).initialize_database() + manager = module.UserManager(db_path=db_path) + + assert manager.get_user_by_qq("10001") is None + + row_id = manager.add_user("10001", "PlayerA", "pid-1", "oid-1", "[]", "[]") + assert row_id > 0 + record = manager.get_user_by_qq("10001") + assert record is not None + assert record["ea_player_name"] == "PlayerA" + assert record["ea_player_id"] == "pid-1" + assert manager.get_user_by_ea_id("oid-1")["qq_id"] == "10001" + + # 换绑 + assert manager.update_user("10001", "PlayerB", "pid-2", "oid-2", "[]", "[]") == 1 + assert manager.get_user_by_qq("10001")["ea_player_name"] == "PlayerB" + assert manager.get_user_by_ea_id("oid-1") is None + assert manager.get_user_by_ea_id("oid-2")["qq_id"] == "10001" + + # 解绑:删到 0 行;重复删除返回 0 + assert manager.delete_user_by_qq("10001") == 1 + assert manager.get_user_by_qq("10001") is None + assert manager.delete_user_by_qq("10001") == 0 + + +def test_user_ea_unique_constraint(repo): + """EA UNIQUE 是最终的并发兜底;绑定入口需先 get_user_by_ea_id 预检。""" + module, db_path = repo + module.DatabaseManager(db_path=db_path).initialize_database() + manager = module.UserManager(db_path=db_path) + + manager.add_user("10001", "PlayerA", "pid-1", "oid-1", "[]", "[]") + with pytest.raises(sqlite3.IntegrityError): + manager.add_user("10002", "PlayerA", "pid-9", "oid-1", "[]", "[]") + + +def test_list_users_newest_first(repo): + module, db_path = repo + module.DatabaseManager(db_path=db_path).initialize_database() + manager = module.UserManager(db_path=db_path) + + manager.add_user("10001", "PlayerA", "pid-1", "oid-1", "[]", "[]") + manager.add_user("10002", "PlayerB", "pid-2", "oid-2", "[]", "[]") + + users = manager.list_users() + assert [u["qq_id"] for u in users] == ["10002", "10001"] + assert manager.count_users() == 2 + # Web 管理页需要的字段都在 + for key in ( + "qq_id", + "ea_player_name", + "ea_player_id", + "ea_user_id", + "designation", + "create_time", + ): + assert key in users[0] + + +def test_list_users_pagination(repo): + """Web 页分页:LIMIT/OFFSET 翻页不重不漏,顺序与全量一致(时间倒序 + id 兜底)。""" + module, db_path = repo + module.DatabaseManager(db_path=db_path).initialize_database() + manager = module.UserManager(db_path=db_path) + for i in range(7): + manager.add_user(f"1000{i}", f"Player{i}", f"pid-{i}", f"oid-{i}", "[]", "[]") + + assert manager.count_users() == 7 + assert [u["qq_id"] for u in manager.list_users(limit=3, offset=0)] == [ + "10006", + "10005", + "10004", + ] + assert [u["qq_id"] for u in manager.list_users(limit=3, offset=3)] == [ + "10003", + "10002", + "10001", + ] + assert [u["qq_id"] for u in manager.list_users(limit=3, offset=6)] == ["10000"] + # 越界页返回空;不带 limit 仍是全量 + assert manager.list_users(limit=3, offset=9) == [] + assert len(manager.list_users()) == 7 diff --git a/tests/test_bf_bot_tracker_api.py b/tests/test_bf_bot_tracker_api.py new file mode 100644 index 0000000..66806c4 --- /dev/null +++ b/tests/test_bf_bot_tracker_api.py @@ -0,0 +1,429 @@ +"""BF2042 直连通道(tracker_api)与通道调度(tracker_edge)单测。 + +两个模块都按文件裸加载(tests/test_bf_bot_repository.py 同法,不触发插件包 +__init__ 的 NoneBot 初始化);**全部用例零真实网络**——令牌端点、API 请求 +分别用假 ``_do_refresh`` / 假 ``cr.Session`` / 假子模块替换,令牌文件一律 +指到 tmp_path,绝不碰 hexi/data 下的真实令牌与绑定库。 +""" + +import importlib.util +import json +import sys +import types +from pathlib import Path + +import pytest + +_PLUGIN_DIR = ( + Path(__file__).resolve().parents[1] / "hexi" / "plugins" / "nonebot_plugin_bf_bot" +) + + +def _load(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _make_pkg(name: str, path: Path): + pkg = types.ModuleType(name) + pkg.__path__ = [str(path)] # type: ignore[attr-defined] + sys.modules[name] = pkg + return pkg + + +# --------------------------------------------------------------------------- # +# tracker_api — 令牌存储 / 引导提取 / 刷新链 / HTTP 错误映射 +# --------------------------------------------------------------------------- # +_API_PATH = _PLUGIN_DIR / "services" / "fetchers" / "tracker_api.py" + + +@pytest.fixture +def api_mod(tmp_path, monkeypatch): + """裸加载 tracker_api;令牌路径指到 tmp_path 下的文件。""" + module = _load("bf_tracker_api_under_test", _API_PATH) + token_path = tmp_path / "trn_tokens.json" + monkeypatch.setattr(module, "_token_path", lambda: token_path) + return module + + +def test_decode_tokens_blob_latin1(): + module = _load("bf_tracker_api_decode1", _API_PATH) + payload = {"access_token": "a1", "refresh_token": "r1"} + raw = ( + b"\x00_https://tracker.gg\x00\x01auth.tokens" + + b"\x01" + + json.dumps(payload).encode("latin-1") + + b"\x00garbage" + ) + assert module._decode_tokens_blob(raw) == payload + + +def test_decode_tokens_blob_utf16_and_garbage(): + module = _load("bf_tracker_api_decode2", _API_PATH) + payload = {"access_token": "a2", "refresh_token": "r2"} + raw = ( + "auth.tokens".encode("utf-16-le") + + b"\x00" + + json.dumps(payload).encode("utf-16-le") + ) + assert module._decode_tokens_blob(raw) == payload + assert module._decode_tokens_blob(b"\x01no keys here") is None + + +def test_extract_tokens_from_dir(tmp_path): + module = _load("bf_tracker_api_extract", _API_PATH) + + assert module._extract_tokens_from_dir(tmp_path / "missing") is None + + garbage = tmp_path / "garbage" + garbage.mkdir() + (garbage / "000003.log").write_bytes(b"nothing to see") + assert module._extract_tokens_from_dir(garbage) is None + + hit = tmp_path / "hit" + hit.mkdir() + (hit / "000003.log").write_bytes( + b"auth.tokens\x01" + + json.dumps({"access_token": "a", "refresh_token": "r"}).encode() + ) + found = module._extract_tokens_from_dir(hit) + assert found and found["refresh_token"] == "r" + + +def test_token_fresh(api_mod): + assert api_mod._token_fresh({"access_token": "x", "expires_at": 2000.0}, now=1000.0) + assert not api_mod._token_fresh( + {"access_token": "x", "expires_at": 500.0}, now=1000.0 + ) + assert not api_mod._token_fresh({"access_token": "x"}) + assert not api_mod._token_fresh({"expires_at": 9999.0}) + + +def test_normalize_token_payload_keeps_prev_refresh(api_mod): + fresh = api_mod._normalize_token_payload( + {"access_token": "a", "expires_in": 3600}, "old" + ) + assert fresh["access_token"] == "a" + assert fresh["refresh_token"] == "old" # 未轮换时沿用旧值 + assert fresh["expires_at"] > api_mod.time.time() + 3000 + + +def test_ensure_access_token_fresh_skips_refresh(api_mod, monkeypatch): + api_mod._token_path().write_text( + json.dumps({"access_token": "good", "refresh_token": "r", "expires_at": 9e9}), + encoding="utf-8", + ) + + def _boom(*_a, **_k): + raise AssertionError("fresh 令牌不应触发刷新") + + monkeypatch.setattr(api_mod, "_do_refresh", _boom) + assert api_mod.ensure_access_token() == "good" + + +def test_ensure_access_token_refreshes_and_persists(api_mod, monkeypatch): + api_mod._token_path().write_text( + json.dumps({"access_token": "stale", "refresh_token": "r1", "expires_at": 1.0}), + encoding="utf-8", + ) + monkeypatch.setattr(api_mod, "_bootstrap_tokens", lambda: None) + seen: list[str] = [] + + def _fake_refresh(refresh_token: str) -> dict: + seen.append(refresh_token) + return { + "access_token": "new", + "refresh_token": "r2", + "expires_at": api_mod.time.time() + 3000, + } + + monkeypatch.setattr(api_mod, "_do_refresh", _fake_refresh) + assert api_mod.ensure_access_token() == "new" + assert seen == ["r1"] + saved = json.loads(api_mod._token_path().read_text(encoding="utf-8")) + assert saved["refresh_token"] == "r2" # 轮换后的令牌已落盘 + assert saved["access_token"] == "new" + + +def test_ensure_access_token_bootstrap_when_file_token_rejected(api_mod, monkeypatch): + api_mod._token_path().write_text( + json.dumps( + {"access_token": "stale", "refresh_token": "dead", "expires_at": 1.0} + ), + encoding="utf-8", + ) + monkeypatch.setattr( + api_mod, "_bootstrap_tokens", lambda: {"refresh_token": "from-browser"} + ) + + def _fake_refresh(refresh_token: str) -> dict: + if refresh_token == "dead": + raise api_mod.TrnAuthError("invalid_grant") + return { + "access_token": "ok", + "refresh_token": "rt3", + "expires_at": api_mod.time.time() + 3000, + } + + monkeypatch.setattr(api_mod, "_do_refresh", _fake_refresh) + assert api_mod.ensure_access_token() == "ok" + + +def test_ensure_access_token_raises_without_any_source(api_mod, monkeypatch): + monkeypatch.setattr(api_mod, "_bootstrap_tokens", lambda: None) + with pytest.raises(api_mod.TrnAuthError): + api_mod.ensure_access_token() + + +class _FakeResp: + def __init__(self, status: int, payload=None, text: str = ""): + self.status_code = status + self._payload = payload + self.text = text + + def json(self): + if self._payload is None: + raise ValueError("not json") + return self._payload + + +class _FakeSession: + """按 URL 关键词路由的假会话;profile 无关键词时兜底到 'profile'。""" + + def __init__(self, routes: dict): + self._routes = routes + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + def get(self, url: str, headers=None, timeout=None): + for key, resp in self._routes.items(): + if key == "profile": + continue + if len(key) > len("segments/") and key in url: + return resp + return self._routes["profile"] + + +@pytest.fixture +def fake_session(api_mod, monkeypatch): + def _install(routes: dict): + fake_cr = types.SimpleNamespace(Session=lambda **kw: _FakeSession(routes)) + monkeypatch.setattr(api_mod, "cr", fake_cr) + monkeypatch.setattr(api_mod, "ensure_access_token", lambda force=False: "tok") + + return _install + + +def test_fetch_sync_maps_404_to_not_found(api_mod, fake_session): + fake_session( + {"profile": _FakeResp(404, {"errors": [{"message": "We could not find..."}]})} + ) + out = api_mod._fetch_sync("Ghost", "origin") + assert out["error"] == "not_found" + assert "could not find" in out["detail"] + assert out["profile"] is None + + +def test_fetch_sync_maps_403_to_auth(api_mod, fake_session): + fake_session({"profile": _FakeResp(403, None, text="blocked")}) + out = api_mod._fetch_sync("X", "origin") + assert out["error"] == "auth" + + +def test_fetch_sync_success_fills_all_segments(api_mod, fake_session): + fake_session( + { + "profile": _FakeResp( + 200, {"data": {"platformInfo": {"platformUserHandle": "P"}}} + ), + "segments/weapon": _FakeResp(200, {"data": [{"type": "weapon"}]}), + "segments/vehicle": _FakeResp(200, {"data": [{"type": "vehicle"}]}), + "segments/gadget": _FakeResp(200, {"data": [{"type": "gadget"}]}), + "segments/soldier": _FakeResp(200, {"data": [{"type": "soldier"}]}), + } + ) + out = api_mod._fetch_sync("P", "origin") + assert out["error"] is None + assert out["profile"]["data"]["platformInfo"]["platformUserHandle"] == "P" + assert out["weapon"] == [{"type": "weapon"}] + assert out["gadget"] == [{"type": "gadget"}] + assert out["soldier"] == [{"type": "soldier"}] + + +def test_fetch_sync_segment_failure_degrades_not_aborts(api_mod, fake_session): + fake_session( + { + "profile": _FakeResp(200, {"data": {"platformInfo": {}}}), + "segments/weapon": _FakeResp(500, None, text="err"), + } + ) + out = api_mod._fetch_sync("P", "origin") + assert out["error"] is None # profile 成功即算成功 + assert out["weapon"] == [] # 失败的段退化为空,不拖垮整次取数 + + +async def test_fetch_bf2042_direct_retries_once_on_auth(api_mod, monkeypatch): + calls: list[bool] = [] + + def _fake_fetch_sync(name, platform, *, force_refresh=False): + calls.append(force_refresh) + if not force_refresh: + return {**api_mod._empty_result(), "error": "auth", "detail": "HTTP 403"} + return {**api_mod._empty_result(), "profile": {"data": {}}} + + monkeypatch.setattr(api_mod, "_fetch_sync", _fake_fetch_sync) + out = await api_mod.fetch_bf2042_direct("P", "origin") + assert calls == [False, True] # 鉴权失败 → 强制刷新重试一次 + assert out["error"] is None + + +async def test_fetch_bf2042_direct_no_retry_on_success(api_mod, monkeypatch): + calls: list[bool] = [] + + def _fake_fetch_sync(name, platform, *, force_refresh=False): + calls.append(force_refresh) + return {**api_mod._empty_result(), "profile": {"data": {}}} + + monkeypatch.setattr(api_mod, "_fetch_sync", _fake_fetch_sync) + out = await api_mod.fetch_bf2042_direct("P", "origin") + assert calls == [False] + assert out["error"] is None + + +# --------------------------------------------------------------------------- # +# tracker_edge.fetch_bf2042_segments — 直连优先 / 桥兜底调度 +# --------------------------------------------------------------------------- # +_DISPATCH_PKG = "bf_dispatch_testpkg" + + +@pytest.fixture +def dispatch(monkeypatch): + """包骨架 + 真 tracker_edge + 假 tracker_api / tracker_bridge 子模块。""" + _make_pkg(_DISPATCH_PKG, _PLUGIN_DIR) + _make_pkg(f"{_DISPATCH_PKG}.services", _PLUGIN_DIR / "services") + fetchers_pkg = _make_pkg( + f"{_DISPATCH_PKG}.services.fetchers", _PLUGIN_DIR / "services" / "fetchers" + ) + edge = _load( + f"{_DISPATCH_PKG}.services.fetchers.tracker_edge", + _PLUGIN_DIR / "services" / "fetchers" / "tracker_edge.py", + ) + api = types.ModuleType(f"{_DISPATCH_PKG}.services.fetchers.tracker_api") + bridge = types.ModuleType(f"{_DISPATCH_PKG}.services.fetchers.tracker_bridge") + sys.modules[api.__name__] = api + sys.modules[bridge.__name__] = bridge + # 直接挂到父包上,让 edge 里 `from . import tracker_api` 稳定命中 + fetchers_pkg.tracker_api = api + fetchers_pkg.tracker_bridge = bridge + + # 关掉节流(测试里不等 3s/20s) + monkeypatch.setattr(edge, "_DIRECT_MIN_INTERVAL_S", 0.0) + monkeypatch.setattr(edge, "_MIN_INTERVAL_S", 0.0) + monkeypatch.setattr(edge, "_last_direct_monotonic", 0.0) + monkeypatch.setattr(edge, "_last_fetch_monotonic", 0.0) + return edge, api, bridge + + +def _direct_ok(**over): + base = { + "profile": {"data": {"platformInfo": {}}}, + "weapon": [{"type": "weapon"}], + "vehicle": [], + "gadget": [{"type": "gadget"}], + "soldier": [], + "error": None, + "detail": "", + } + base.update(over) + return base + + +async def test_dispatch_direct_ok_never_touches_bridge(dispatch): + edge, api, bridge = dispatch + + async def _direct(name, platform): + return _direct_ok() + + async def _bridge(**_kw): + raise AssertionError("直连成功时不应回退扩展桥") + + api.fetch_bf2042_direct = _direct + bridge.fetch_via_extension = _bridge + out = await edge.fetch_bf2042_segments("P", "origin") + assert out["error"] is None + assert out["weapon"] == [{"type": "weapon"}] + assert out["gadget"] == [{"type": "gadget"}] + + +async def test_dispatch_not_found_is_final(dispatch): + edge, api, bridge = dispatch + + async def _direct(name, platform): + return _direct_ok( + profile=None, + weapon=[], + gadget=[], + error="not_found", + detail="no such player", + ) + + async def _bridge(**_kw): + raise AssertionError("not_found 是确定性结果,不应回退扩展桥") + + api.fetch_bf2042_direct = _direct + bridge.fetch_via_extension = _bridge + out = await edge.fetch_bf2042_segments("Ghost", "origin") + assert out["error"] == "not_found" + assert out["detail"] == "no such player" + + +async def test_dispatch_falls_back_to_bridge_on_direct_error(dispatch): + edge, api, bridge = dispatch + calls = [] + + async def _direct(name, platform): + return _direct_ok( + profile=None, weapon=[], gadget=[], error="error", detail="HTTP 500" + ) + + async def _bridge(**kw): + calls.append(kw) + return ( + {"profile": {"data": {"platformInfo": {}}}, "weapon": {"data": [1]}}, + None, + "", + ) + + api.fetch_bf2042_direct = _direct + bridge.fetch_via_extension = _bridge + out = await edge.fetch_bf2042_segments("P", "origin") + assert len(calls) == 1 + assert out["error"] is None + assert out["weapon"] == [1] + + +async def test_dispatch_merges_details_when_both_fail(dispatch): + edge, api, bridge = dispatch + + async def _direct(name, platform): + return _direct_ok( + profile=None, weapon=[], gadget=[], error="auth", detail="令牌失效" + ) + + async def _bridge(**kw): + return {}, "timeout", "等待页面数据超时" + + api.fetch_bf2042_direct = _direct + bridge.fetch_via_extension = _bridge + out = await edge.fetch_bf2042_segments("P", "origin") + assert out["error"] == "timeout" + assert "等待页面数据超时" in out["detail"] + assert "令牌失效" in out["detail"]