@@ -5,6 +5,7 @@ import type { AssetObject } from './classes';
5
5
import { concatArrayBuffer , ensureArrayBuffer } from './utils/buffer' ;
6
6
import { ArrayBufferReader } from './utils/reader' ;
7
7
import { UnityCN } from './utils/unitycn' ;
8
+ import { isVersionLargerThanOrEqual , parseVersion } from './utils/version' ;
8
9
import { unzipIfNeed } from './utils/zip' ;
9
10
import { AssetType } from '.' ;
10
11
import type { AssetBundle , Texture2D } from '.' ;
@@ -60,7 +61,8 @@ enum CompressionType {
60
61
LZMA ,
61
62
LZ4 ,
62
63
LZ4_HC ,
63
- LZHAM ,
64
+ CUSTOM_4 ,
65
+ CUSTOM_5 ,
64
66
}
65
67
66
68
enum FileType {
@@ -73,10 +75,15 @@ enum FileType {
73
75
ZIP_FILE ,
74
76
}
75
77
78
+ export enum BundleEnv {
79
+ ARKNIGHTS ,
80
+ }
81
+
76
82
export interface BundleLoadOptions {
77
83
/** 有些 Sprite 可能不会给出 AlphaTexture 的 PathID,可以传入自定义函数去寻找 */
78
84
findAlphaTexture ?: ( texture : Texture2D , assets : Texture2D [ ] ) => Texture2D | undefined ;
79
85
unityCNKey ?: string ;
86
+ env ?: BundleEnv ;
80
87
}
81
88
82
89
export class Bundle {
@@ -170,7 +177,7 @@ export class Bundle {
170
177
private readUnityCN ( r : ArrayBufferReader , key : string ) {
171
178
let mask : ArchiveFlags ;
172
179
173
- const version = this . parseVersion ( this . header . unityReversion ) ;
180
+ const version = parseVersion ( this . header . unityReversion ) ;
174
181
if (
175
182
version [ 0 ] < 2020 || // 2020 and earlier
176
183
( version [ 0 ] === 2020 && version [ 1 ] === 3 && version [ 2 ] <= 34 ) || // 2020.3.34 and earlier
@@ -195,11 +202,19 @@ export class Bundle {
195
202
throw new Error ( `Unsupported bundle flags: ${ ArchiveFlags [ flags ] || flags } ` ) ;
196
203
}
197
204
205
+ const reversion = parseVersion ( this . header . unityReversion ) ;
206
+
198
207
if ( version >= 7 ) r . align ( 16 ) ;
208
+ else if ( isVersionLargerThanOrEqual ( reversion , [ 2019 , 4 ] ) ) {
209
+ const preAlign = r . position ;
210
+ const align = ( 16 - ( preAlign % 16 ) ) % 16 ;
211
+ if ( align ) r . move ( align ) ;
212
+ }
199
213
200
214
const blockInfoBuffer = r . readBuffer ( compressedBlocksInfoSize ) ;
201
215
const compressionType = flags & ArchiveFlags . COMPRESSION_TYPE_MASK ;
202
- const blockInfoUncompressedBuffer = decompressBuffer (
216
+
217
+ const blockInfoUncompressedBuffer = this . decompressBuffer (
203
218
blockInfoBuffer ,
204
219
compressionType ,
205
220
uncompressedBlocksInfoSize ,
@@ -237,13 +252,15 @@ export class Bundle {
237
252
private readBlocks ( r : ArrayBufferReader ) {
238
253
const results : ArrayBuffer [ ] = [ ] ;
239
254
255
+ if ( this . header . flags & ArchiveFlags . BLOCK_INFO_NEED_PADDING_AT_START ) r . align ( 16 ) ;
256
+
240
257
for ( const [ i , { flags, compressedSize, uncompressedSize } ] of this . blockInfos . entries ( ) ) {
241
258
const compressionType = flags & StorageBlockFlags . COMPRESSION_TYPE_MASK ;
242
259
const compressedBuffer = r . readBuffer ( compressedSize ) ;
243
260
if ( this . unityCN && flags & 0x100 ) {
244
261
this . unityCN . decryptBlock ( compressedBuffer , i ) ;
245
262
}
246
- const uncompressedBuffer = decompressBuffer (
263
+ const uncompressedBuffer = this . decompressBuffer (
247
264
compressedBuffer ,
248
265
compressionType ,
249
266
uncompressedSize ,
@@ -266,38 +283,90 @@ export class Bundle {
266
283
return files ;
267
284
}
268
285
269
- private parseVersion ( str : string ) {
270
- return str
271
- . replace ( / \D / g, '.' )
272
- . split ( '.' )
273
- . filter ( Boolean )
274
- . map ( v => parseInt ( v ) ) ;
286
+ private decompressBuffer (
287
+ data : ArrayBuffer ,
288
+ type : number ,
289
+ uncompressedSize ?: number ,
290
+ ) : ArrayBuffer {
291
+ if ( type === CompressionType . NONE ) return data ;
292
+
293
+ if ( ! uncompressedSize ) throw new Error ( 'Uncompressed size not provided' ) ;
294
+
295
+ switch ( type ) {
296
+ case CompressionType . LZMA :
297
+ return decompressLzmaWithSize ( new Uint8Array ( data ) , uncompressedSize ) ;
298
+
299
+ case CompressionType . LZ4 :
300
+ case CompressionType . LZ4_HC :
301
+ return decompressLz4 ( new Uint8Array ( data ) , uncompressedSize ) . buffer ;
302
+ }
303
+
304
+ const isArknights = this . options ?. env === BundleEnv . ARKNIGHTS ;
305
+
306
+ if ( isArknights && ( type === CompressionType . CUSTOM_4 || type === CompressionType . CUSTOM_5 ) ) {
307
+ return decompressArkLz4 ( data , uncompressedSize ) . buffer ;
308
+ }
309
+
310
+ throw new Error ( `Unsupported compression type: ${ CompressionType [ type ] || type } ` ) ;
275
311
}
276
312
}
277
313
278
- const decompressBuffer = (
279
- data : ArrayBuffer ,
280
- type : number ,
281
- uncompressedSize ?: number ,
282
- ) : ArrayBuffer => {
283
- if ( type === CompressionType . NONE ) return data ;
314
+ const readLongLengthNoCheck = ( ip : Uint8Array , pos : number ) : [ number , number ] => {
315
+ let b = 0 ;
316
+ let l = 0 ;
317
+ while ( true ) {
318
+ b = ip [ pos ] ;
319
+ pos ++ ;
320
+ l += b ;
321
+ if ( b !== 255 ) break ;
322
+ }
323
+ return [ l , pos ] ;
324
+ } ;
325
+
326
+ // From https://github.com/MooncellWiki/UnityPy by Kengxxiao
327
+ const decompressArkLz4 = ( data : ArrayBuffer , uncompressedSize : number ) => {
328
+ const AK_LITERAL_LENGTH_MASK = ( ( 1 << 4 ) - 1 ) & 0xff ;
329
+ const AK_MATCH_LENGTH_MASK = ~ AK_LITERAL_LENGTH_MASK & 0xff ;
330
+
331
+ const fixedCompressedData = new Uint8Array ( data ) ;
332
+
333
+ let ip = 0 ;
334
+ let op = 0 ;
284
335
285
- if ( ! uncompressedSize ) throw new Error ( 'Uncompressed size not provided' ) ;
336
+ while ( true ) {
337
+ let literalLength = fixedCompressedData [ ip ] & AK_LITERAL_LENGTH_MASK ;
338
+ let matchLength = ( ( fixedCompressedData [ ip ] & AK_MATCH_LENGTH_MASK ) >> 4 ) & 0xff ;
286
339
287
- switch ( type ) {
288
- case CompressionType . LZMA :
289
- return decompressLzmaWithSize ( new Uint8Array ( data ) , uncompressedSize ) ;
340
+ fixedCompressedData [ ip ] = ( ( literalLength << 4 ) | matchLength ) & 0xff ;
341
+ ip ++ ;
290
342
291
- case CompressionType . LZ4 :
292
- case CompressionType . LZ4_HC :
293
- return decompressLz4 ( new Uint8Array ( data ) , uncompressedSize ) . buffer ;
343
+ if ( literalLength === 15 ) {
344
+ const [ l , newIp ] = readLongLengthNoCheck ( fixedCompressedData , ip ) ;
345
+ literalLength += l ;
346
+ ip = newIp ;
347
+ }
348
+
349
+ op += literalLength ;
350
+ ip += literalLength ;
351
+
352
+ if ( uncompressedSize <= op ) break ;
294
353
295
- case CompressionType . LZHAM :
296
- throw new Error ( 'Not implemented' ) ;
354
+ const offset = fixedCompressedData [ ip + 1 ] | ( fixedCompressedData [ ip ] << 8 ) ;
355
+ fixedCompressedData [ ip ] = offset & 0xff ;
356
+ fixedCompressedData [ ip + 1 ] = ( offset >> 8 ) & 0xff ;
357
+ ip += 2 ;
297
358
298
- default :
299
- throw new Error ( `Unsupported compression type: ${ CompressionType [ type ] || type } ` ) ;
359
+ if ( matchLength === 15 ) {
360
+ const [ m , newIp ] = readLongLengthNoCheck ( fixedCompressedData , ip ) ;
361
+ matchLength += m ;
362
+ ip = newIp ;
363
+ }
364
+
365
+ matchLength += 4 ;
366
+ op += matchLength ;
300
367
}
368
+
369
+ return decompressLz4 ( fixedCompressedData , uncompressedSize ) ;
301
370
} ;
302
371
303
372
const getFileType = ( data : ArrayBuffer ) => {
0 commit comments