Skip to content

Commit f2d5c62

Browse files
authored
Merge pull request #150 from Windows81/csgmdl5
feat: implement CSGMDL5
2 parents 23e0be0 + 9dcd5f1 commit f2d5c62

8 files changed

Lines changed: 546 additions & 99 deletions

File tree

Lines changed: 28 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,5 @@
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

775
def 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-
11030
def 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

Comments
 (0)