Skip to content

Shinym@s 初探 02 - hash

Published: at 17:55

嘛,本来只有上下两篇的,上篇讲 JS 相关,下篇试着看看能不能摸出点 WASM 的东西,结果……

这谁顶得住,正好趁这个机会了解一下 SC 音乐资源的编号方式,不也挺好的吗(

ToC

结果

总之今天的目的是(早就)达到了,结果在这里:

demo

main

资源加载

我们知道,SC 有大量的资源需要加载,这在客户端下载资源的时长以及浏览器的各种 Now Loading 就能看出来。

土豆别笑,两个 Loading 条你笑个锤子

因此,我们推测,一定有一些资源索引文件,记录了当前所有的资源文件信息。而根据源码的阅读,这个推测被证实是正确的。

总索引

由于资源实在太多,因此 SC 将索引分类两层,一层是总索引,索引的是各分索引,而各分索引对应的则是到真正数据的索引。总索引位于 /assets/asset-map.json,加密时的两个输入则为 asset-map.jsonasset-map,得到的输出是 aac011a61415a220560587aaf0177b2d98e8c7897d7697db4dd51b509162040d。根据索引的命名规则,在路径前面要加上 asset-map-,于是最终得到的路径就是 https://shinycolors.enza.fun/assets/asset-map-aac011a61415a220560587aaf0177b2d98e8c7897d7697db4dd51b509162040d,和观察的结果是一致的。

RESULT MATCH
RESULT MATCH

这时候我们不妨来看看这个文件的内容:

挺标准的分 chunk,还记录了 versiontotalSize。这里 totalSize 估算了一下,快 6 个 G 了。这就是 SC 吗,爱了爱了(不是

分索引

分索引记录的就是实际的素材了,像这种:

某一个文件
某一个文件

这个 Object 对应的 value 项就是资源的版本了,因此我们可以通过后者来判断资源是否需要更新。

最后,对于获取到的数据,SC 会把它们统一都塞进 hashMap 里。生成一个巨大的 Object,如下图所示(浏览器卡爆了,所以只截了一点):

资源的分类

在我们可以获取明文返回内容之后,我们首先想要知道的就是资源的具体分类方式。通过上面的图片我们也不难发现,SC 的资源本质是以目录的形式组织的,比如 ae/common/com_eff01_front_catastrophe/data.json 这种,对应的实际路径就是 https://shinycolors.enza.fun/assets/----经过 encryptPath 的字符串---(当然了,如果对应的后缀名较为特殊,比如 .m4a.mp4 之类的会保留后缀名)。

那这里我们可以观察到,资源的路径经历了一次 hash。在上一篇中,我简单地认为只要直接 hash 就可以了,但事实是并不能。来看代码:

1
N = {
2
createImagePath: function (e, t, n) {
3
var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "";
4
return i.default.join(m, e, t, x(e, t, n, r));
5
},
6
createMoviePath: function (e, t, n) {
7
var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "",
8
o = !(arguments.length > 4 && void 0 !== arguments[4]) || arguments[4],
9
a = i.default.join(_, e, t, R(e, n, r));
10
return o ? this.getEncryptedMoviePath(a) : a;
11
},
12
getEncryptedMoviePath: function (e) {
13
var t = i.default.basename(e, C),
14
n = i.default.join(l.default.env.ASSET_ROOT, e),
15
r = p.default.getQueryString(n);
16
return (
17
l.default.env.ENABLE_CRYPTO && (e = f.default.encryptPath(n, t) + C),
18
r && (e += r),
19
e
20
);
21
},
22
createSpinePath: function (e, t, n) {
23
return i.default.join(y, e, t, O(e, n), T);
24
},
25
createVoicePath: function (e, t, n) {
26
var r = t || n ? e + "/" + M(e, t, n) : "" + e + M(e, t, n);
27
return i.default.join(v, r);
28
},
29
createConcertMusicPath: function (e, t) {
30
return i.default.join(E, e, "" + O("unit", t) + w);
31
},
32
createTipsImagePath: function (e) {
33
return i.default.join(P, "" + e + S);
34
},
35
createAdminImagePath: function (e, t, n) {
36
return i.default.join(this.createAdminFolderPath(e, t), "" + n + A);
37
},
38
createAdminFolderPath: function (e, t) {
39
var n = i.default.join(g, e, O(e, t));
40
return n;
41
},
42
};

(这里的行号对应的是 Chrome 格式化后的行号,即 debugger:///VM493 pure-app-57696458079a3b7abba5.js.map:formatted。)

我们不妨分别来看。

图片

图片对应的函数是 createImagePath,可以看到,关键的函数是 x(e, t, n, r)。我们来看:

1
N = {
2
createImagePath: function (e, t, n) {
3
var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "";
4
return i.default.join(m, e, t, x(e, t, n, r));
5
},
6
};
7
8
x = function (e, t, n) {
9
var r = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "",
10
o = O(e, n),
11
i = (b[e] && b[e][t]) || A;
12
return "" + k(r) + o + i;
13
};

这里 r 对应的是 hasho 对应的是原名,i 对应的是后缀名,而 k(r) 函数输出的结果实际是 r !== '' ? r + '_' : r。所以我们可以发现,对于图片的路径,在 encryptPath 之前,需要将其加上对应的 hash 值。那 hash 怎么来呢?

经过进一步的挖掘,我们发现,hash 是随着 api 实时传给我们的,但这就出现了不一致了。如果 hash 只有在你有那张卡的时候才能拿到,那么客户端又是怎么缓存资源的呢?于是我们发现了 hashResourcesgetHashPrefixAssets,并得到了如下的结果:

经过整理的 hashResources 结果
经过整理的 hashResources 结果

有了这份 hash 表,我们就可以简单地计算出对应的真实路径了。

视频

视频的处理其实同理,虽然函数表示形式不同,但实际上和图片是一样的。这里就不再赘述了。

卡面 ID 的编号方式

我们(或许)知道,偶像是有其对应编号的,现在是从 001023,因此对应的卡面也有编号。我们知道,卡面有 RSRSSR 三种,其对应的 rarity 在代码中分别是 234。于是某一张卡面的 ID 就会遵循如下的格式:

1
1 0 3 001 001 0
2
^ ^ ^ ^ ^ ^
3
[CardType] [Category] [Rarity] [IdolID] [CardNum] [Unknown]

这里的 CardType 代表的是 P 卡还是 S 卡,1 为 P 卡,2 为 S 卡;Category 代表的是分类,目前 0 代表普通卡,9 代表 IDOLROADRarity 就是上面所说的 2~4(但其实是有 1 的,见下文),Idol ID 也是同理,Card Num1 开始,跟随 Rarity 大分类而增加。最后的 0 就意味不明了,至少现在大家的最后都是 0

(根据 Hash 表,S 卡里有四张以 201 开头的卡,游戏里也找到了。这四张卡相当“至少得有”的 Support 卡(如果你抽到的都是 P,虽然概率很低,但是这样的话你就没 S 卡了(或者说抽到的 S 卡数量不足 4 张:

我新来的.jpg
我新来的.jpg

结语

今天是在上次的基础上进一步的探索,在有了路径加密之后,SC 在此之上又增加了一层保护。然而世上总没有不透风的墙,客户端的出现也使得 hash 值这层保护的意义渐渐模糊了起来。

或者逻辑有可能恰恰是反过来的:SC 在原本没有路径加密的时候使用的就是 Hash,而后来才转移到以 WASM 为基础的路径加密上来,同时保留了过去的保护手段。或许是怕删除之后又出什么岔子,又或者只是单纯的想多一层保护手段,这或许只有 enza 的程序才知道了吧。

嘛,总而言之,现在我们已经可以下载所有的数据文件了。之后如果有空或许会搞一个查卡器什么的,不过这都是后话了。

总之,就到这里,去写操作系统的作业了,溜了溜了(