将LOL国服补骰子自动化

叽里咕噜说什么呢

2025 年,英雄联盟的大乱斗做出了一次巨大更新:将原来的骰子删除,替换为全新的翻牌机制

不过,在此之前,腾讯出了一个活动,用来提前帮拳头给这个改动收集数据验证可行性

有的人可能已经猜到了,就是补骰子活动。国服在 Lobby 界面的开始匹配按钮上方,添加了一个按钮,小手点一下,就可以补充

问题是,就算 K6 合作部不舍得大刀阔斧动客户端原来的插件逻辑,你都把按钮做出来了,你自己 click 一下然后校验结果不行吗?非得玩家自己补?

这个活动直到结束,还是经常能遇到不补骰子的,也是神了…

言归正传

上面简单吐槽一下,顺便回忆一下细节,接下来我们看一下怎么实现自动补骰子

总体思路

CDP is for Chrome Devtools Protocol

Chrome 的 DevTools 其实是典型的 C/S 架构,浏览器内部跑着一个 Server,然后浏览器会访问devtools://devtools/bundled/inspector.html来从 Server 获取信息,而他们的沟通就是通过 CDP 实现的

那既然 DevTools 可以在 Console Tab 任意 Evaluate 一段 JavaScript 代码,那我们岂不是也可以?

是的,CDP 通过Runtime.evaluate来进行任意代码的求值,参考Chrome DevTools Protocol - Runtime domain

所以首先打开 F12 简单扒一下补骰子的逻辑在哪,随后使用 JS 操作即可

打开远程调试

但是在这之前,我们需要打开远程调试(Remote Debugging),不然不能通过外部连接 DevTools Server

英雄联盟于 2016 更新海克斯客户端架构,这是一个典型的 CEF 应用,所以我们可以通过 Hook 来强行给 Browser process 加一个--remote-debugging-port={PORT}来开启远程调试

具体的操作这里就不提了,直接使用 https://github.com/PenguLoader/PenguLoader (我也曾深度参与项目社区和贡献,项目至今可用,拳头大概是默许了,有兴趣的可以看看)

如果有兴趣,可以参考我另外一个对 WeGame 的 Hook 项目的说明文档,里面详细介绍了这块的知识

BetterWG/HOW_IT_WORKS.md at main · BakaFT/BetterWG

控制流构思

1> 通过游戏的 WebSocket 事件驱动模型,一旦玩家进入房间,那么执行回调函数

2> 回调函数中,首先建立 CDP 连接,找到网页对应的 Frame

​ 这里涉及到 V8 的一些概念,如果看不懂建议代码拷贝过去问 AI

3> 判断当前模式是不是可以补骰子(无限乱斗和大乱斗)

4> 连接到 Frame,使用Runtime.evaluate执行业务代码

编码

首先,怎么检测玩家到房间呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
// We use `league-connect` package on npmjs
createWebSocketConnection().then((ws=>{
console.log('连接客户端成功')
ws.subscribe('/lol-gameflow/v1/gameflow-phase', async (data, _) => {
let bflag = false
if (data === 'Lobby') {
while (bflag === false) {
bflag = await fillUpDices1()
}
}
})
console.log('开始监听,保持后台运行即可')
}))

这里搞一个 flag 简单粗暴地进行 throttle,这样不用因为各种边界条件头疼,重试就完了奥

接下来我们就可以做 step 2 和 step3 了,先放代码,可以看到下面用到了Runtime.evaluate来求值,说人话就是执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export async function fillUpDices1() {
// Find the iframe
const targets = await CDP.List({ 'host': 'localhost', port: REMOTE_DEBUGGING_PORT })
const theFrame = targets.find(t => {
return t.type === 'iframe'
// This can both catch ARAM & AURF iframe
&& t.title.includes('aram/random')
})
if (!theFrame) {
return false
}

// Tell if it's aram or aurf
const isAURF = theFrame.url.includes('infinite') ? true : false

// Connect to frame
const client = await CDP({ 'host': 'localhost', 'port': REMOTE_DEBUGGING_PORT, 'target': theFrame.id })

// Wait for Milo(:thinking any better idea?)
// blog: 看这里!
// blog: 这里使用Runtime.evaluate来检测Milo是否初始化完毕
let isMiloReady = await client.Runtime.evaluate({ 'expression': `typeof Milo !== 'undefined'` })
while (!isMiloReady.result.value) {
// console.log('MILO NOT READY')
isMiloReady = await client.Runtime.evaluate({ 'expression': `typeof Milo === 'undefined'` })
await new Promise<void>((r, j) => { setTimeout(() => { r() }, 200) })
}


let ret = false
if(isAURF){
ret = await AURF(client)
}
else{
ret= await ARAM(client)
}

await updateCount(client,isAURF)
client.close()

return ret
}

这里有一点上面没提到,就是这个 Milo,这是腾讯 IEG 的一个业务框架,简单来说就是拿来做活动上报之类的,本次活动使用 Milo 提交业务实现补骰子

所以我们要等一下 Milo 的初始化,不然后面没得玩了

最后只需要把 F12 逆向出来的业务代码扒出来,贴这里就可以了,代码较长,使用注释讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// blog: 这里就是核心业务的代码,通过Milo.emit来上报
function miloEmitExpressionString(actId:string,token:string){
// Will return stringified JSON
return `
(async function(){
return await Milo.emit({actId:'${actId}',token:'${token}',sData:{sArea:window.area}})
})()
.then((r)=>{
return JSON.stringify(r)
})
`
}

// blog:针对不同模式,业务参数有所不同,这里就不多讲了
export async function ARAM(client:Client){
// blog: awaitPromise设置为True,不要给自己找麻烦
const evalRes = await client.Runtime.evaluate({
expression: miloEmitExpressionString('705790','7b9c56'),
awaitPromise: true
})

// console.log('请求MILO')
// console.log('请求结果', evalRes)

// 原因未知,偶尔出现,应该代表了业务失败
if (evalRes.result.value === undefined) {
// console.log('调用完了,但是结果不对劲')
return false
}
// 解析结果,输出到终端
const miloResObject = JSON.parse(evalRes.result.value)
console.log(`[${new Date().toTimeString()}]尝试补充大乱斗骰子,返回结果: ${miloResObject.sMsg}`)
return true

}


export async function AURF(client:Client){
const evalRes = await client.Runtime.evaluate({
expression: miloEmitExpressionString('705790','656963'),
awaitPromise: true
})
// blog: 下面一样就不贴了
// ....
}



export async function remainingDicesCount(client:Client,isAURF:boolean){
// blog: 查询骰子数量也是一个单独的业务
const evalRes = await client.Runtime.evaluate({
expression: miloEmitExpressionString('705790','c43e85'),
awaitPromise: true
})
if (evalRes.result.value === undefined) {
return false
}
const miloResObject = JSON.parse(evalRes.result.value)
console.log(`[${new Date().toTimeString()}]查询ARAM骰子数量,返回结果: 剩余${miloResObject.details.jData.scoreNum},目前${miloResObject.details.jData.diceNum}/2`)

const evalRes2 = await client.Runtime.evaluate({
expression: miloEmitExpressionString('705790','6ae6d9'),
awaitPromise: true
})
if (evalRes2.result.value === undefined) {
return false
}
const miloResObject2 = JSON.parse(evalRes2.result.value)
console.log(`[${new Date().toTimeString()}]查询AURF骰子数量,返回结果: 剩余${miloResObject2.details.jData.scoreNum},目前${miloResObject2.details.jData.diceNum}/2`)

// blog: 把骰子数量的查询结果同步到网页中
client.Runtime.evaluate({expression:`
document.querySelector('#diceNum').innerText = ${ isAURF ? miloResObject2.details.jData.diceNum : miloResObject.details.jData.diceNum};
document.querySelector('#scoreNum').innerText = ${miloResObject.details.jData.scoreNum};
`
})
}

这样,一个简单的补骰子就做好了

其他思路

上面的代码中可以看出,补骰子的核心是执行 Milo 业务的提交,那么基于此可以发散出其他思路

客户端内直接求值

这小标题有点不明不白的,简单解释一下

上面提到的 PenguLoader 提供了运行时注入代码的能力,使用 CEF 的frame->execute_java_script直接在 Renderer process 求值即可。那有的人就会问了,那你为啥不这么干,非要绕一圈去搞 CDP 呢?

因为跨域了…但是这个理由不够合理,因为这个可以通过--disable-web-security做到无视跨域,和上面 CDP 前置要求--remote-debugging-port其实也差不多。就当多个思路吧

跨域问题

为什么跨域呢?因为腾讯通过自己定义一个 LeaueClientUx Frontend Plugin 做到了加载自己的代码,这也是为什么国服的 LCUX 并不进行完整性校验,因为自己也要插代码。包括客户端主页一些奇奇怪怪的功能在内的东西,都是通过这个实现的。

然后呢,PenguLoader 只在游戏客户端自己的localhost:{RANDOM_PORT}这个页面帧,也就是 Frame 上执行代码

但是呢,腾讯的东西都在lol.qq.com下面,看图:

所以其实写 PenguLoader 插件直接执行会被 CSP 拦下来…

代码实现

当然,通过参数关闭 CSP 之后就不是问题了,一位群友(Joi 的作者其实是,watchingfun@github)写了一个插件

原理是一样的,这里贴一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
export function init(context) {
let area;
// blog: 腾讯使用postMessage进行通讯,这里直接逮捕
window.addEventListener("message", (event) => {
if (event?.data?.messageType === "aram_reroll_main_show") {
const src = document.querySelector('#aram_reroll').src;
const type = src.substr(src.lastIndexOf('/') + 1).split('.html')[0]
if (!area) {
area = new URLSearchParams(src.split('?')[1]).get('area')
}
getDice({ ...paramsConfig[type], sArea: area });
}
})
}

const paramsConfig = {
'random-infinite': {
iChartId: '393050',
iSubChartId: '393050',
sIdeToken: '6f9Yvi'
},
'random': {
iChartId: '378916',
iSubChartId: '378916',
sIdeToken: 'Rb22Nt',
}
};

// blog: 在Network Tab抓包可以发现,Milo业务的本质是给这里提交请求
// blog: 所以这里瞪眼法观察一下请求体,即可理解
async function getDice(params) {
const url = 'https://comm.ams.game.qq.com/ide/';
const options = {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(params)
};

try {
const response = await fetch(url, options);
const data = await response.json();
console.log('尝试补充骰子', data);
Toast.success(data.sMsg)
} catch (error) {
Toast.error('尝试补充骰子失败:' + error?.message || JSON.stringify(error))
}
}

这个插件并没有做到对 UI 的骰子数量进行更新,不过无伤大雅,内部实现他就是方便啊

野路子

B 站有一位高人发了个补骰子工具(BV1QfVWzTE1S)

简单逆向了一下,发现是通过搜索 WeGame 和 QQ 的内存获取用户的某个状态 Key,然后直接使用 Key 请求https://comm.ams.game.qq.com/ide/这个接口

这个比较变态,简单看看就行了…

LeagueClient通信(0)-LCU通讯原理&如何让熊孩子玩不了LOL

缘起

之前看到 @MarioCrane 的自定义创建 5V5 训练模式工具,让我对 LOL 客户端的 API 又产生了点兴趣。

仓库地址: https://github.com/MarioCrane/LeaueLobby

为啥要说又。。因为之前对 LOL 的 wad 资源拆包的时候我曾了解过 LeagueClient 的一些运行原理

Riot 的开发者博客曾经在更新客户端的时候解析了其中的技术原理

https://technology.riotgames.com/news/architecture-league-client-update

相比之前的了解,目前 Riot 的开发文档已经完善了很多,值得去探索一番。

Read more
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×