This document could be written mainly thanks to the following projects:
Note
|
|
BIN Header |
JSON Metadata |
Payload Header |
Payload |
|||||||||
Segments headers |
Segments data (encrypted) |
|||||||||||
Header Chunk 1 |
Header Chunk 2 |
Header Keyframe 1 |
Header Chunk 3 |
Header Keyframe 2 |
Chunk 1 |
Chunk 2 |
Keyframe 1 |
Chunk 3 |
Keyframe 2 |
|||
Sections |
Sections |
Sections |
Sections |
Sections |
A file’s overall structure is the following:
POS | SIZE | FORMAT | DESCRIPTION |
---|---|---|---|
0 |
288 |
BIN Header |
The base file Header |
288 |
A |
Base JSON |
The game’s metadata for quick overviews |
288+A |
34+X (66) |
Payload Header |
The game’s binary information header |
322+A+X |
? (until EOF) |
Payload |
The actual game data |
Start at position 0 in file
POS | SIZE | FORMAT | DESCRIPTION |
---|---|---|---|
0 |
4 |
RIOT |
Magic constant string |
4 |
2 |
? |
constant 0 bytes with unknown usage, part of magic ? |
6 |
256 |
HEX |
Data signature |
262 |
2 |
u16 |
BIN Header size |
264 |
4 |
u32 |
File size |
268 |
4 |
u32 |
JSON Metadata offset from file start |
272 |
4 |
u32 |
JSON Metadata size |
276 |
4 |
u32 |
Payload header offset from file start |
280 |
4 |
u32 |
Payload header size |
284 |
4 |
u32 |
Payload offset from payload header start |
The JSON Metadata section is an UTF-8 JSON String that contains metadata on the game. Note that most actual information is in the statsjson
json string attribute.
Caution
|
The String is NOT null-terminated |
KEY | FORMAT | DESCRIPTION |
---|---|---|
.gameLength |
Integer |
Game duration in milliseconds |
.gameVersion |
String |
Patch version the game was played with |
.lastGameChunkId |
Integer |
Last chunk’s ID |
.lastKeyFrameId |
Integer |
Last keyframe’s ID |
.statsJson |
String |
Stringified JSON statistics on the game to parse before use |
KEY | FORMAT | DESCRIPTION |
---|
The header’s position is provided in the BIN header
POS | SIZE | FORMAT | DESCRIPTION |
---|---|---|---|
0 |
8 |
u64 |
Match ID |
8 |
4 |
u32 |
Match duration in ms |
12 |
4 |
u32 |
Keyframes count |
16 |
4 |
u32 |
Chunk count |
20 |
4 |
u32 |
Last chunk’s ID |
24 |
4 |
u32 |
First chunk’s ID |
28 |
4 |
u32 |
Keyframe interval |
32 |
2 |
u16 |
Encryption key length |
34 |
X |
String |
Encryption key (current length is always 32 bytes) |
Hereafter, we use Segment to talk about a data range that may either be a Chunk or a Keyframe.
The payload’s position is provided in the BIN header. The payload runs until EOF and each of its data segment is encrypted with the encryption key.
POS | SIZE | FORMAT | DESCRIPTION |
---|---|---|---|
0 |
17*n |
Segment Header |
Segment Headers (n is the total number of segments) |
17*n |
? |
Chunk/Keyframe |
Encrypted Segments whose size and position relative to the header list’s end is provided in the segment header |
The payload starts with chunk count + keyframe count Segment headers whose structure is the following :
POS | SIZE | FORMAT | DESCRIPTION |
---|---|---|---|
0 |
4 |
u32 |
Segment ID |
4 |
1 |
u8 |
Segment type (Chunk = 1, Keyframe = 2) |
5 |
4 |
u32 |
Segment data Length |
9 |
4 |
u32 |
Associated Chunk ID (Keyframes only, is 0 with chunks) |
13 |
4 |
u32 |
Segment data offset (from end of segment headers) |
Note
|
From here, all information is speculative and needs to be verified before usage. Any game update may invalidate this information |
Payload data’s sub-sections are split in two parts :
-
One header section
-
One data section whose length is provided in the section header
To read a segment, get the Encryption Key and the game’s ID as a string as well as the segment’s data, then:
-
Base64 decode the encryption key string
-
Use Blowfish to decrypt the decoded encryption key with the game ID string as the key (and remove the padding whose length is provided in the last byte of the decrypted data)
-
Use Blowfish to decrypt the segment data with the decrypted encryption key (and remove the padding whose length is provided in the last byte of the decrypted data)
-
Use Gzip to unpack the decrypted segment data
### Variables
# game_id = "5000000000"
# encryption_key = "Ezyoyu7dqcDbGXsVV6Vg1vAkiuFuirFD"
# segment_encrypted_data = [...]
### Functions
# byte[] blowfish_decrypt(byte encrypted_data[], byte key[], bool remove_padding)
# byte[] gunzip(byte compressed_data[])
raw_encryption_key = base64_decode(encryption_key)
chunk_key = blowfish_decrypt(raw_encryption_key, game_id, true)
segment_zipped_data = blowfish_decrypt(segment_encrypted_data, chunk_key, true)
segment_data = gunzip(segment_zipped_data)
Each section of a segment’s data has a varying-size header determined by its first byte and an optional varying-size data section.
Configuration byte (1 byte) |
Game time (1/4 bytes) |
Data Length (1/4 bytes) |
Type (2 bytes ?) |
Parameters (1/4 bytes) |
Data (? bytes) |
ID | Loading Chunk | Loading Keyframe | Game Chunk | Game Keyframe | Note |
---|---|---|---|---|---|
1 |
X |
X |
TODO |
POS | SIZE | FORMAT | DESCRIPTION |
---|---|---|---|
0 |
1 |
u8 |
Header configuration byte |
1 |
4 if |
f32 else u8 |
Game time, either absolute (f32, in s) or relative to last section (u8, in ms) |
Varying |
4 if |
u8 else u32 |
Section’s data’s length |
Varying |
2 if |
u16 else None |
Section’s data’s type (equals last section’s type if absent) |
Varying |
4 if |
u32 else u8 |
Section’s type’s parameters |
Varying |
? |
Variable |
Section’s data |
Current version: ID = 397 [141,1]
POS |
SIZE |
FORMAT |
DESCRIPTION |
0 |
2 |
? |
Ping config ? |
2 |
4 |
f32 |
Ping position X |
6 |
4 |
f32 |
Ping position Y |
10 |
? |
? |
Inject specifics here |
END |
5 |
? |
= [246, 176, 176, 176, 112] (Player ID (4 bytes) + specifier ?) |
POS |
SIZE |
FORMAT |
DESCRIPTION |
0 |
1 |
u8 |
Ping type ID |
POS |
SIZE |
FORMAT |
DESCRIPTION |
0 |
1 |
u8 |
Ping type ID |
1 |
4 |
u32 |
Target unit’s ID |
Information pings are pings performed from the TAB menu
POS |
SIZE |
FORMAT |
DESCRIPTION |