1- import functools
2- import hashlib
3- import collections .abc
4- from typing import Any , Never
5- OBFUSCATION_NOISE_CYCLE_XOR = bytes ([
6- 86 , 46 , 110 , 88 , 49 , 32 , 48 , 4 , 52 , 105 , 12 , 119 , 12 , 1 , 94 , 0 , 26 , 96 , 55 , 105 , 29 , 82 , 43 , 7 , 79 , 36 , 89 , 101 , 83 , 4 , 122 ,
7- ])
8-
9- INT_SIZE = 4
10-
11- LCM_A = 0b0000_0011_0100_0011_1111_1101
12- LCM_C = 0b0010_0110_1001_1110_1100_0011
13-
14-
15- def lcm_rand () -> collections .abc .Generator [int , Any , Never ]:
16- s = 0b0000_0000_0000_0101_0011_1001 # 1337
17- while True :
18- s = s * LCM_A + LCM_C
19- yield (s >> 16 ) & 0x7FFF
20-
21-
22- saltSize : int = 0x10
23- hashSize : int = 0x10
24-
25-
26- def createHash (data : bytes , saltIn : bytes = b'rfd' ) -> str :
27- '''
28- TODO: decide whether we should `jmp` this function in the EXEs or to actually use `createHash` and have the client validate the hash.
29- '''
30- verticesSize : int = 0 # vertices.size() * sizeof(CSGVertex);
31- indicesSize : int = 0 # indices.size() * sizeof(unsigned int);
32- buffSize : int = verticesSize + indicesSize + saltSize
33-
34- salt = saltIn .rjust (16 )
35- byteBuffer = list (data + salt )
36-
37- # size_t copyOffset = 0;
38- # memcpy(&byteBuffer[copyOffset], &vertices[0], verticesSize);
39-
40- # copyOffset += verticesSize;
41- # memcpy(&byteBuffer[copyOffset], &indices[0], indicesSize);
42-
43- # copyOffset += indicesSize;
44- # memcpy(&byteBuffer[copyOffset], salt.c_str(), salt.size());
45-
46- randGen = lcm_rand ()
47- for i in range (buffSize ):
48- j = next (randGen ) % buffSize
49- byteBuffer [i ], byteBuffer [j ] = byteBuffer [j ], byteBuffer [i ]
50-
51- hashlib .md5 (bytes (byteBuffer ))
52- # boost::scoped_ptr<RBX::MD5Hasher> hasher(RBX::MD5Hasher::create());
53- # hasher->addData((const char*)&byteBuffer[0], byteBuffer.size());
54-
55- # memcpy(&hash[0], hasher->toString().c_str(), hashSize);
56- # memcpy(&hash[hashSize], salt.c_str(), saltSize);
57-
58- # std::string hashStr(&hash[0], hashSize + saltSize);
59-
60- return hashStr
61-
62-
63- @functools .cache
64- def xor_encrypt (code : bytes , key = OBFUSCATION_NOISE_CYCLE_XOR , offset : int = 0 ) -> bytes :
65- l = len (key )
66- return bytes (
67- c ^ key [i % l ]
68- for i , c in enumerate (code , offset )
69- )
70-
71-
72- @functools .cache
73- def get_header (prefix : bytes , version : int ) -> bytes :
74- return prefix + version .to_bytes (length = INT_SIZE , byteorder = 'little' )
1+ from .util import INT_SIZE , CSG_HEADER
2+ from . import csgmdl5
753
764
775def replace_header_version (data : bytes , versioned_header : bytes , from_version : int , to_version : int ) -> bytes :
@@ -92,33 +20,28 @@ def replace_header_version(data: bytes, versioned_header: bytes, from_version: i
9220 ])
9321
9422
95- def splice_without (data : bytes , fr : int , ln : int ) -> bytes :
23+ def splice_without_middle_elements (data : bytes , fr : int , ln : int ) -> bytes :
9624 return b'' .join ([
9725 data [:fr ],
9826 data [fr + ln :],
9927 ])
10028
10129
102- HEADER_CSG2 = xor_encrypt (get_header (b'CSGMDL' , 2 ))
103- HEADER_CSG4 = xor_encrypt (get_header (b'CSGMDL' , 4 ))
104- HEADER_CSG5 = xor_encrypt (get_header (b'CSGMDL' , 5 ))
105- HEADER_CSGPHYS5 = get_header (b'CSGPHS' , 5 )
106- HEADER_CSGPHYS6 = get_header (b'CSGPHS' , 6 )
107- HEADER_CSGPHYS7 = get_header (b'CSGPHS' , 7 )
108-
109-
11030def parse (data : bytes ) -> bytes | None :
111- if data .startswith (HEADER_CSG4 ):
112- return replace_header_version (data , HEADER_CSG4 , 4 , 2 )
31+ if data .startswith (CSG_HEADER . MDL4 . value ):
32+ return replace_header_version (data , CSG_HEADER . MDL4 . value , 4 , 2 )
11333
114- if data .startswith (HEADER_CSGPHYS5 ):
34+ elif data .startswith (CSG_HEADER .MDL5 .value ):
35+ return csgmdl5 .convert_to_csgmdl2 (data )
36+
37+ elif data .startswith (CSG_HEADER .PHS5 .value ):
11538 '''
11639 CSGPHYS5 is identical in data format to CSGPHYS3.
11740 https://github.com/krakow10/rbx_mesh/blob/d10bcdf727dd9c2504560189a5cb106aa9107ec5/src/physics_data.rs#L71
11841 '''
119- return replace_header_version (data , HEADER_CSGPHYS5 , 5 , 3 )
42+ return replace_header_version (data , CSG_HEADER . PHS5 . value , 5 , 3 )
12043
121- if data .startswith (HEADER_CSGPHYS6 ):
44+ elif data .startswith (CSG_HEADER . PHS6 . value ):
12245 '''
12346 Why 40 bytes?
12447 ```rs
@@ -135,19 +58,29 @@ def parse(data: bytes) -> bytes | None:
13558 CSGPHYS6 and CSGPHYS7 both contain `PhysicsInfo` structs, which as above indicate a length of 40 bytes.
13659 https://github.com/krakow10/rbx_mesh/blob/d10bcdf727dd9c2504560189a5cb106aa9107ec5/src/physics_data.rs#L8-L16
13760 '''
138- return splice_without (
139- replace_header_version (data , HEADER_CSGPHYS6 , 6 , 3 ),
140- len (HEADER_CSGPHYS6 ), 40 ,
61+ return splice_without_middle_elements (
62+ replace_header_version (data , CSG_HEADER . PHS6 . value , 6 , 3 ),
63+ len (CSG_HEADER . PHS6 . value ), 40 ,
14164 )
14265
143- if data .startswith (HEADER_CSGPHYS7 ):
66+ elif data .startswith (CSG_HEADER . PHS7 . value ):
14467 '''
14568 Why 41 bytes in CSGPHYS7?
14669 +40: `PhysicsInfo`, as per above.
14770 + 1: the mysterious magic number `03` (one byte) that takes place after the versioned header.
14871 https://github.com/krakow10/rbx_mesh/blob/d10bcdf727dd9c2504560189a5cb106aa9107ec5/src/physics_data.rs#L54
14972 '''
150- return splice_without (
151- replace_header_version (data , HEADER_CSGPHYS7 , 7 , 3 ),
152- len (HEADER_CSGPHYS7 ), 41 ,
73+ return splice_without_middle_elements (
74+ replace_header_version (data , CSG_HEADER .PHS7 .value , 7 , 3 ),
75+ len (CSG_HEADER .PHS7 .value ), 41 ,
76+ )
77+
78+ elif data .startswith (CSG_HEADER .PHS8 .value ):
79+ # TODO: implement CSGPHS8; returns an empty CSPHS object for now.
80+ return (
81+ b'CSGPHS\x03 \x00 \x00 \x00 ' +
82+ b'\x10 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \x10 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \x80 \x3F ' +
83+ b'\x00 \x00 \x00 \x00 ' +
84+ b'\x04 \x00 \x00 \x00 ' +
85+ b'\x00 \x00 \x00 \x00 '
15386 )
0 commit comments