Skip to content

Commit da41e0c

Browse files
committed
feat: 新增游戏配置守护进程
1 parent 069f590 commit da41e0c

File tree

6 files changed

+450
-11
lines changed

6 files changed

+450
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [1.2.1] - 2026-xx
5+
## [1.3.0] - 2026-02-08
66

7-
- 修复重复恢复设置的问题
8-
- 修复匹配重试次数过少的问题
7+
- 修复发条鸟模式偶尔卡住的问题
8+
- 支持福星模式
9+
- 重构代码,支持多赛季
10+
- 新增游戏配置守护进程,避免游戏过程中停止挂机功能后,游戏结束时LOL自动重新覆盖设置导致设置恢复失效
11+
- 修复一些已知bug.
912

1013
## [1.2.1] - 2026-01-31
1114

electron/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ app.on('will-quit', async (event) => {
305305
event.preventDefault();
306306
console.log('🔄 [Main] 检测到程序正在运行,正在恢复游戏设置...');
307307

308+
// 停止可能正在运行的配置守护,避免与 restore 冲突
309+
GameConfigHelper.stopConfigGuard();
310+
308311
try {
309312
// 恢复设置
310313
await GameConfigHelper.restore();

out/main/index.js

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import require$$5 from "assert";
1515
import WebSocket from "ws";
1616
import https from "https";
1717
import axios from "axios";
18+
import * as crypto from "crypto";
1819
import Store from "electron-store";
1920
import { is, optimizer } from "@electron-toolkit/utils";
2021
import __cjs_mod__ from "node:module";
@@ -5695,6 +5696,16 @@ class GameConfigHelper {
56955696
tftConfigPath;
56965697
// 预设的云顶设置
56975698
isTFTConfig = false;
5699+
/** 文件监听器实例,用于守护恢复后的配置不被 LOL 客户端覆盖 */
5700+
configWatcher = null;
5701+
/** 防抖定时器,避免短时间内触发多次恢复 */
5702+
watcherDebounceTimer = null;
5703+
/** 守护超时定时器,到期后自动停止监听 */
5704+
guardTimeoutTimer = null;
5705+
/** 守护期间允许的最大自动恢复次数,防止无限循环 */
5706+
MAX_GUARD_RESTORES = 5;
5707+
/** 守护期间已执行的自动恢复次数 */
5708+
guardRestoreCount = 0;
56985709
constructor(installPath) {
56995710
if (!installPath) {
57005711
throw new Error("初始化失败,必须提供一个有效的游戏安装路径!");
@@ -5831,7 +5842,17 @@ class GameConfigHelper {
58315842
try {
58325843
await fs.copy(backupPath, instance.gameConfigPath);
58335844
instance.isTFTConfig = false;
5834-
logger.info(`[GameConfigHelper] 设置恢复成功!`);
5845+
const verified = await instance.verifyRestore(backupPath);
5846+
if (verified) {
5847+
logger.info(`[GameConfigHelper] 设置恢复成功,文件验证通过!`);
5848+
} else {
5849+
logger.warn(`[GameConfigHelper] 设置恢复完成,但文件验证不一致!可能被外部程序覆盖`);
5850+
if (attempt < retryCount) {
5851+
logger.info(`[GameConfigHelper] 将在 ${retryDelay}ms 后重试...`);
5852+
await sleep(retryDelay);
5853+
continue;
5854+
}
5855+
}
58355856
return true;
58365857
} catch (err) {
58375858
const errMsg = err instanceof Error ? err.message : String(err);
@@ -5849,6 +5870,146 @@ class GameConfigHelper {
58495870
}
58505871
return false;
58515872
}
5873+
/**
5874+
* 验证恢复结果:对比备份目录和游戏配置目录中的关键文件哈希值
5875+
*
5876+
* 只对比最关键的 game.cfg 文件,因为它包含分辨率、画质等核心设置
5877+
* 使用 MD5 哈希快速比较文件内容是否一致
5878+
*
5879+
* @param backupPath 备份目录路径
5880+
* @returns true 表示恢复后的文件与备份一致
5881+
*/
5882+
async verifyRestore(backupPath) {
5883+
const keyFile = "game.cfg";
5884+
const backupFile = path__default.join(backupPath, keyFile);
5885+
const gameFile = path__default.join(this.gameConfigPath, keyFile);
5886+
try {
5887+
const [backupExists, gameExists] = await Promise.all([
5888+
fs.pathExists(backupFile),
5889+
fs.pathExists(gameFile)
5890+
]);
5891+
if (!backupExists || !gameExists) {
5892+
logger.warn(`[ConfigGuard] 验证跳过:文件不存在 (备份: ${backupExists}, 游戏: ${gameExists})`);
5893+
return true;
5894+
}
5895+
const [backupHash, gameHash] = await Promise.all([
5896+
this.getFileHash(backupFile),
5897+
this.getFileHash(gameFile)
5898+
]);
5899+
const match = backupHash === gameHash;
5900+
if (!match) {
5901+
logger.warn(`[ConfigGuard] game.cfg 哈希不匹配!备份: ${backupHash}, 游戏: ${gameHash}`);
5902+
}
5903+
return match;
5904+
} catch (err) {
5905+
logger.warn(`[ConfigGuard] 验证过程出错: ${err}`);
5906+
return true;
5907+
}
5908+
}
5909+
/**
5910+
* 计算文件的 MD5 哈希值
5911+
*
5912+
* crypto.createHash('md5') 创建一个哈希计算器
5913+
* digest('hex') 将计算结果转为十六进制字符串(如 "d41d8cd98f00b204e9800998ecf8427e")
5914+
*
5915+
* @param filePath 文件路径
5916+
* @returns 文件的 MD5 哈希字符串
5917+
*/
5918+
async getFileHash(filePath) {
5919+
const content = await fs.readFile(filePath);
5920+
return crypto.createHash("md5").update(content).digest("hex");
5921+
}
5922+
/**
5923+
* 启动配置守护监听器
5924+
*
5925+
* 在 restore 成功后调用,监听游戏配置目录的文件变化。
5926+
* 如果检测到 LOL 客户端在恢复后又改写了配置文件,会自动重新恢复。
5927+
*
5928+
* 守护机制会在指定时间后自动停止,防止长期占用资源。
5929+
* 同时有最大恢复次数限制,防止与 LOL 客户端无限互相覆盖。
5930+
*
5931+
* @param guardDuration 守护持续时间(毫秒),默认 30 秒
5932+
*/
5933+
static startConfigGuard(guardDuration = 3e4) {
5934+
const instance = GameConfigHelper.getInstance();
5935+
if (!instance) return;
5936+
GameConfigHelper.stopConfigGuard();
5937+
instance.guardRestoreCount = 0;
5938+
logger.info(`[ConfigGuard] 启动配置守护,持续 ${guardDuration / 1e3} 秒`);
5939+
try {
5940+
instance.configWatcher = fs.watch(
5941+
instance.gameConfigPath,
5942+
{ recursive: true },
5943+
(eventType, filename) => {
5944+
if (!filename || !filename.toLowerCase().includes("game.cfg")) return;
5945+
if (instance.isTFTConfig) return;
5946+
if (instance.guardRestoreCount >= instance.MAX_GUARD_RESTORES) {
5947+
logger.warn(`[ConfigGuard] 已达最大自动恢复次数 (${instance.MAX_GUARD_RESTORES}),停止守护`);
5948+
GameConfigHelper.stopConfigGuard();
5949+
return;
5950+
}
5951+
if (instance.watcherDebounceTimer) {
5952+
clearTimeout(instance.watcherDebounceTimer);
5953+
}
5954+
instance.watcherDebounceTimer = setTimeout(async () => {
5955+
logger.info(`[ConfigGuard] 检测到 ${filename} 被外部修改,正在验证...`);
5956+
let backupPath = instance.currentBackupPath;
5957+
if (!await fs.pathExists(backupPath)) {
5958+
backupPath = instance.primaryBackupPath;
5959+
}
5960+
if (!await fs.pathExists(backupPath)) {
5961+
backupPath = instance.fallbackBackupPath;
5962+
}
5963+
const isConsistent = await instance.verifyRestore(backupPath);
5964+
if (!isConsistent) {
5965+
instance.guardRestoreCount++;
5966+
logger.warn(`[ConfigGuard] 配置被篡改!自动恢复中... (第 ${instance.guardRestoreCount} 次)`);
5967+
try {
5968+
await fs.copy(backupPath, instance.gameConfigPath);
5969+
const verified = await instance.verifyRestore(backupPath);
5970+
if (verified) {
5971+
logger.info(`[ConfigGuard] 自动恢复成功,验证通过`);
5972+
} else {
5973+
logger.warn(`[ConfigGuard] 自动恢复后验证仍不一致`);
5974+
}
5975+
} catch (err) {
5976+
logger.error(`[ConfigGuard] 自动恢复失败: ${err}`);
5977+
}
5978+
} else {
5979+
logger.debug(`[ConfigGuard] ${filename} 变更但内容验证一致,无需恢复`);
5980+
}
5981+
}, 500);
5982+
}
5983+
);
5984+
instance.guardTimeoutTimer = setTimeout(() => {
5985+
logger.info(`[ConfigGuard] 守护时间到,停止监听`);
5986+
GameConfigHelper.stopConfigGuard();
5987+
}, guardDuration);
5988+
} catch (err) {
5989+
logger.error(`[ConfigGuard] 启动监听失败: ${err}`);
5990+
}
5991+
}
5992+
/**
5993+
* 停止配置守护监听器
5994+
* 清理所有定时器和文件监听器,释放资源
5995+
*/
5996+
static stopConfigGuard() {
5997+
const instance = GameConfigHelper.getInstance();
5998+
if (!instance) return;
5999+
if (instance.configWatcher) {
6000+
instance.configWatcher.close();
6001+
instance.configWatcher = null;
6002+
logger.debug(`[ConfigGuard] 文件监听器已关闭`);
6003+
}
6004+
if (instance.watcherDebounceTimer) {
6005+
clearTimeout(instance.watcherDebounceTimer);
6006+
instance.watcherDebounceTimer = null;
6007+
}
6008+
if (instance.guardTimeoutTimer) {
6009+
clearTimeout(instance.guardTimeoutTimer);
6010+
instance.guardTimeoutTimer = null;
6011+
}
6012+
}
58526013
}
58536014
var IpcChannel = /* @__PURE__ */ ((IpcChannel2) => {
58546015
IpcChannel2["CONFIG_BACKUP"] = "config-backup";
@@ -10636,6 +10797,7 @@ app.on("will-quit", async (event) => {
1063610797
if (hexService && hexService.isRunning) {
1063710798
event.preventDefault();
1063810799
console.log("🔄 [Main] 检测到程序正在运行,正在恢复游戏设置...");
10800+
GameConfigHelper.stopConfigGuard();
1063910801
try {
1064010802
await GameConfigHelper.restore();
1064110803
console.log("✅ [Main] 游戏设置已恢复");
@@ -10679,11 +10841,11 @@ app.whenReady().then(async () => {
1067910841
console.log("✅ [Main] 原生模块检查通过");
1068010842
console.log("🚀 [Main] 正在加载业务模块...");
1068110843
try {
10682-
const ServicesModule = await import("./chunks/index-B2eu_A9a.js");
10844+
const ServicesModule = await import("./chunks/index-BqMcWydW.js");
1068310845
hexService = ServicesModule.hexService;
1068410846
const TftOperatorModule = await import("./chunks/TftOperator-Bv5E9wfl.js").then((n) => n.T);
1068510847
tftOperator = TftOperatorModule.tftOperator;
10686-
const LineupModule = await import("./chunks/index-DViKavGK.js");
10848+
const LineupModule = await import("./chunks/index-BkP-NETh.js");
1068710849
lineupLoader = LineupModule.lineupLoader;
1068810850
const GlobalHotkeyManagerModule = await import("./chunks/GlobalHotkeyManager-Cbcy0EP4.js");
1068910851
globalHotkeyManager = GlobalHotkeyManagerModule.globalHotkeyManager;

src-backend/states/EndState.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export class EndState implements IState {
3434

3535
if (success) {
3636
logger.info("[EndState] 客户端设置恢复完成");
37+
// 启动长期配置守护:持续监听游戏配置目录
38+
// 防止 LOL 客户端在游戏结束时又把配置改回下棋设置
39+
// 守护会一直运行直到下次开始挂机或软件退出
40+
GameConfigHelper.startConfigGuard();
3741
} else {
3842
// 恢复失败,打印醒目的警告和操作指引
3943
logger.error("═══════════════════════════════════════════════════════════");

src-backend/states/StartState.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export class StartState implements IState {
7676
*/
7777
private async applyTFTConfig(): Promise<void> {
7878
logger.info("[StartState] 正在应用 TFT 专用配置...");
79+
80+
// 先停止上一轮可能残留的配置守护监听器
81+
GameConfigHelper.stopConfigGuard();
7982

8083
const success = await GameConfigHelper.applyTFTConfig();
8184

0 commit comments

Comments
 (0)