Skip to content

Commit 01ed274

Browse files
committed
feat:修仙插件(5)
1 parent 9759042 commit 01ed274

2 files changed

Lines changed: 363 additions & 61 deletions

File tree

src/plugins/XianNiUpgrade/plugin.py

Lines changed: 220 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88
import math
99
import json
1010
import base64
11+
import hashlib
12+
import subprocess
13+
from pathlib import Path
1114
from datetime import datetime
1215

13-
from PyQt5.QtWidgets import QWidget
16+
from PyQt5.QtWidgets import QWidget, QMessageBox
1417

1518
from Crypto.Cipher import AES
1619
from Crypto.Random import get_random_bytes
1720

21+
import ms_toollib as ms
22+
1823
from plugin_sdk import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
1924
from shared_types.events import CloseEvent, GameFinishedEvent
2025

@@ -26,6 +31,8 @@
2631
_ENCRYPT_KEY = b"f[{gr!%$%^65sr60"
2732

2833

34+
35+
2936
def _encrypt(data: bytes) -> bytes:
3037
nonce = get_random_bytes(12)
3138
cipher = AES.new(_ENCRYPT_KEY, AES.MODE_GCM, nonce=nonce)
@@ -72,6 +79,7 @@ def _setup_subscriptions(self) -> None:
7279
def _create_widget(self) -> QWidget:
7380
self._ui = XianNiUpgradeUI()
7481
self._ui.set_image_dir(self.data_dir / "asserts")
82+
self._ui.set_absorb_callbacks(self.validate_replays, self.absorb_replays)
7583
return self._ui
7684

7785
def on_initialized(self) -> None:
@@ -89,6 +97,10 @@ def _calc_xp(self, event: GameFinishedEvent) -> int:
8997
"""每局获得的经验值"""
9098
return 8000
9199

100+
def _calc_xp2(self, video: ms.EvfVideo) -> int:
101+
"""通过录像计算经验值"""
102+
return 1
103+
92104
# ═══════════════════════════════════════════════════════════
93105
# 数据管理
94106
# ═══════════════════════════════════════════════════════════
@@ -98,55 +110,237 @@ def _load_data(self):
98110
if path.exists():
99111
try:
100112
raw = _decrypt(path.read_bytes())
101-
self._player_data = json.loads(raw)
102-
self.logger.info(f"已加载存档: Lv.{self._player_data['level']}")
103-
return
113+
data = json.loads(raw)
114+
if "identifiers" in data and "players" in data:
115+
self._identifiers = data["identifiers"]
116+
self._players = data["players"]
117+
self._history = data.get("history", [])
118+
self._current_pid = data.get("current_pid", 0)
119+
self._imported = set(tuple(v) for v in data.get("imported_videos", []))
120+
self.logger.info(f"已加载存档,{len(self._identifiers)} 个玩家")
121+
return
122+
self.logger.info("旧存档格式,忽略")
104123
except Exception as e:
105124
self.logger.warning(f"读取存档失败: {e}")
106125

107-
self._player_data = {
108-
"level": 0,
109-
"xp": 0,
110-
"history": [],
111-
}
126+
self._identifiers = []
127+
self._players = []
128+
self._history = []
129+
self._current_pid = 0
130+
self._imported: set[tuple[str, str]] = set()
112131

113132
def _save_data(self):
114133
path = self.data_dir / "player_data.dat"
134+
data = {
135+
"identifiers": self._identifiers,
136+
"players": self._players,
137+
"history": self._history,
138+
"current_pid": self._current_pid,
139+
"imported_videos": [list(v) for v in self._imported],
140+
}
141+
raw = json.dumps(data, ensure_ascii=False).encode("utf-8")
142+
path.write_bytes(_encrypt(raw))
143+
144+
def _get_or_create_pid(self, identifier: str) -> int:
145+
identifier = identifier.strip()
146+
if not identifier:
147+
identifier = "匿名玩家"
148+
try:
149+
return self._identifiers.index(identifier)
150+
except ValueError:
151+
pid = len(self._identifiers)
152+
self._identifiers.append(identifier)
153+
self._players.append({"level": 0, "xp": 0})
154+
return pid
155+
156+
# ═══════════════════════════════════════════════════════════
157+
# 吸收灵气(导入其他版本录像获得经验)
158+
# ═══════════════════════════════════════════════════════════
159+
160+
def validate_replays(self, exe_path: str, replay_path: str) -> dict | None:
161+
"""校验录像并返回预览数据,失败返回 None"""
162+
try:
163+
exe = Path(exe_path)
164+
if not exe.exists():
165+
self.logger.error(f"校验程序不存在: {exe_path}")
166+
return None
167+
168+
actual_md5 = hashlib.md5(exe.read_bytes()).hexdigest()
169+
170+
match actual_md5:
171+
case "d5fd61ae1372297aa7008d7b7cd8a13b":
172+
return self._validate_metasweeper322(exe, replay_path)
173+
case _:
174+
self.logger.error(f"未知法器 MD5: {actual_md5}")
175+
return None
176+
except Exception as e:
177+
self.logger.error(f"校验失败: {e}")
178+
return None
179+
180+
def _validate_metasweeper322(self, exe: Path, replay_path: str) -> dict | None:
181+
"""元扫雷 3.2.2 的录像校验与解析"""
115182
try:
116-
raw = json.dumps(self._player_data, ensure_ascii=False).encode("utf-8")
117-
path.write_bytes(_encrypt(raw))
183+
cmd = [str(exe), "-c", replay_path]
184+
self.logger.info(f"执行: {' '.join(cmd)}")
185+
result = subprocess.run(cmd, capture_output=True, timeout=120)
186+
if result.returncode != 0:
187+
self.logger.error(f"校验程序返回非零: {result.returncode}")
188+
return None
189+
190+
out_path = exe.parent / "_internal" / "out.json"
191+
if not out_path.exists():
192+
self.logger.error(f"未找到结果文件: {out_path}")
193+
return None
194+
195+
report = json.loads(out_path.read_bytes())
196+
if report.get("error"):
197+
self.logger.error(f"校验报告错误: {report['error']}")
198+
return None
199+
200+
new_files = []
201+
dup_files = []
202+
for d in report.get("data", []):
203+
if d.get("status") != 0:
204+
continue
205+
fp = d["file"]
206+
try:
207+
v = ms.EvfVideo(fp)
208+
v.parse()
209+
key = (str(v.start_time), v.player_identifier)
210+
entry = {
211+
"file": fp,
212+
"player": v.player_identifier,
213+
"start_time": v.start_time,
214+
"level": getattr(v, "level", 3),
215+
"mode": getattr(v, "mode", 0),
216+
"rtime": getattr(v, "rtime", 0.0),
217+
"bbbv": getattr(v, "bbbv", 0),
218+
"xp": self._calc_xp2(v),
219+
}
220+
if key in self._imported:
221+
dup_files.append(entry)
222+
elif entry["xp"] > 0:
223+
new_files.append(entry)
224+
except Exception as e:
225+
self.logger.warning(f"解析录像失败 {fp}: {e}")
226+
227+
return {
228+
"md5": "d5fd61ae1372297aa7008d7b7cd8a13b",
229+
"new_files": new_files,
230+
"duplicates": dup_files,
231+
"total_new_xp": sum(n["xp"] for n in new_files),
232+
}
118233
except Exception as e:
119-
self.logger.error(f"保存存档失败: {e}")
234+
self.logger.error(f"元扫雷 3.2.2 校验失败: {e}")
235+
return None
236+
237+
def absorb_replays(self, preview: dict) -> int:
238+
"""根据预览数据实际吸收经验,返回获得的总经验"""
239+
gained_total = 0
240+
241+
for entry in preview["new_files"]:
242+
xp_per = entry.get("xp", 0)
243+
key = (str(entry["start_time"]), entry["player"])
244+
self._imported.add(key)
245+
246+
pid = self._get_or_create_pid(entry["player"])
247+
self._current_pid = pid
248+
player = self._players[pid]
249+
player["xp"] += xp_per
250+
gained_total += xp_per
251+
252+
while player["level"] < 100:
253+
need = _total_xp(player["level"] + 1)
254+
if player["xp"] < need:
255+
break
256+
player["level"] += 1
257+
258+
top = _total_xp(100)
259+
if player["xp"] > top:
260+
player["xp"] = top
261+
262+
self._history.append({
263+
"pid": pid,
264+
"time": int(datetime.now().timestamp()),
265+
"level": entry["level"],
266+
"mode": entry["mode"],
267+
"rtime": round(entry["rtime"], 2),
268+
"bbbv": entry["bbbv"],
269+
"xp": xp_per,
270+
})
271+
272+
if len(self._history) > 1000:
273+
self._history = self._history[-1000:]
274+
275+
self._save_data()
276+
self._push_ui_update()
277+
return gained_total
278+
279+
def _build_update_data(self) -> dict:
280+
if not self._players:
281+
return {
282+
"player_name": "",
283+
"level": 0,
284+
"total_xp": 0,
285+
"xp_curr": 0,
286+
"xp_need": _total_xp(1),
287+
"rank": LEVEL_NAMES.get(0, ""),
288+
"image_index": get_image_index(0),
289+
"history": [],
290+
}
291+
pid = self._current_pid
292+
if pid >= len(self._players):
293+
pid = 0
294+
self._current_pid = 0
295+
player = self._players[pid]
296+
level = player["level"]
297+
xp = player["xp"]
298+
xp_base = _total_xp(level)
299+
xp_next = _total_xp(level + 1) if level < 100 else _total_xp(100)
300+
return {
301+
"player_name": self._identifiers[pid] if self._identifiers else "",
302+
"level": level,
303+
"total_xp": xp,
304+
"xp_curr": xp - xp_base,
305+
"xp_need": xp_next - xp_base,
306+
"rank": LEVEL_NAMES.get(level, ""),
307+
"image_index": get_image_index(level),
308+
"history": self._history[::-1][:100],
309+
}
120310

121311
def _on_game_finished(self, event: GameFinishedEvent):
122312
if event.game_state != 6:
123313
return
124314

315+
pid = self._get_or_create_pid(event.player_identifier)
316+
self._current_pid = pid
317+
player = self._players[pid]
318+
125319
xp_gained = self._calc_xp(event)
126-
self._player_data["xp"] += xp_gained
320+
player["xp"] += xp_gained
127321

128-
while self._player_data["level"] < 100:
129-
need = _total_xp(self._player_data["level"] + 1)
130-
if self._player_data["xp"] < need:
322+
while player["level"] < 100:
323+
need = _total_xp(player["level"] + 1)
324+
if player["xp"] < need:
131325
break
132-
self._player_data["level"] += 1
326+
player["level"] += 1
133327

134-
# 封顶
135328
top = _total_xp(100)
136-
if self._player_data["xp"] > top:
137-
self._player_data["xp"] = top
329+
if player["xp"] > top:
330+
player["xp"] = top
138331

139-
self._player_data["history"].append({
332+
self._history.append({
333+
"pid": pid,
140334
"time": int(datetime.now().timestamp()),
141-
"level": LEVEL_LABELS.get(event.level, str(event.level)),
142-
"mode": MODE_LABELS.get(event.mode, str(event.mode)),
335+
"level": event.level,
336+
"mode": event.mode,
143337
"rtime": round(event.rtime, 2),
144338
"bbbv": event.bbbv,
145339
"xp": xp_gained,
146340
})
147341

148-
if len(self._player_data["history"]) > 1000:
149-
self._player_data["history"] = self._player_data["history"][-1000:]
342+
if len(self._history) > 1000:
343+
self._history = self._history[-1000:]
150344

151345
self._save_data()
152346
self._push_ui_update()
@@ -155,18 +349,4 @@ def _on_close(self, event: CloseEvent):
155349
self._save_data()
156350

157351
def _push_ui_update(self):
158-
level = self._player_data["level"]
159-
xp = self._player_data["xp"]
160-
xp_base = _total_xp(level)
161-
xp_next = _total_xp(level + 1) if level < 100 else _total_xp(100)
162-
163-
data = {
164-
"level": level,
165-
"total_xp": xp,
166-
"xp_curr": xp - xp_base,
167-
"xp_need": xp_next - xp_base,
168-
"rank": LEVEL_NAMES.get(level, ""),
169-
"image_index": get_image_index(level),
170-
"history": self._player_data["history"][::-1][:100],
171-
}
172-
self._ui._signal_update.emit(data)
352+
self._ui._signal_update.emit(self._build_update_data())

0 commit comments

Comments
 (0)