Skip to content

Commit 7375f43

Browse files
committed
feat: implement payload compression support with PKLib integration
- Add compression flag detection and manipulation methods to ServerMessageHeader - isPayloadCompressed(): Check if payload compression flag (0x02) is set - setPayloadCompression(): Enable/disable payload compression flag - Extend ServerPacket with compression support methods - isPayloadCompressed(): Wrapper for header compression check - setPayloadCompression(): Wrapper for header compression control - Integrate PKLib decompression in transaction processing - Add explode() function import from pklib-ts package - Implement decompressMessage() function with proper error handling - Update receiveTransactionsData() to handle compressed payloads - Add 64KB output buffer with overflow protection - Add pklib-ts package as local dependency for compression algorithms - Link packages/pklib-ts for PKWare-compatible compression/decompression - Includes TypeScript implementation of explode/implode algorithms - Add MC_CRC_PRE_RACE_DATA message ID (455/0x1C7) to message catalog - Update dependencies and lock files for new compression functionality This enables the server to handle compressed game packets, improving bandwidth efficiency for large payloads while maintaining compatibility with the original Motor City Online protocol.
1 parent 0a2de5c commit 7375f43

File tree

7 files changed

+1880
-71
lines changed

7 files changed

+1880
-71
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"fastify": "^5.4.0",
4747
"moment": "^2.30.1",
4848
"pino-debug": "^3.0.0",
49+
"pklib-ts": "link:packages/pklib-ts",
4950
"rusty-motors-chat": "link:src/chat",
5051
"rusty-motors-cli": "link:packages/cli",
5152
"rusty-motors-database": "link:packages/database",

packages/shared-packets/src/ServerMessageHeader.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export class ServerMessageHeader
7373
return (this.flags & 0x08) != 0;
7474
}
7575

76+
isPayloadCompressed(): boolean {
77+
return (this.flags & 0x02) != 0;
78+
}
79+
7680
setPayloadEncryption(encrypted: boolean): ServerMessageHeader {
7781
if (encrypted) {
7882
this.flags |= 0x08;
@@ -82,6 +86,15 @@ export class ServerMessageHeader
8286
return this;
8387
}
8488

89+
setPayloadCompression(encrypted: boolean): ServerMessageHeader {
90+
if (encrypted) {
91+
this.flags |= 0x02;
92+
} else {
93+
this.flags &= ~0x02;
94+
}
95+
return this;
96+
}
97+
8598
override toString(): string {
8699
return `ServerMessageHeader {length: ${this.length}, signature: ${this.signature}, sequence: ${this.sequence}, flags: ${this.flags}}`;
87100
}

packages/shared-packets/src/ServerPacket.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export class ServerPacket extends BasePacket implements SerializableMessage {
7272
return this;
7373
}
7474

75+
setPayloadCompression(compressed: boolean): ServerPacket {
76+
this.header.setPayloadCompression(compressed);
77+
return this;
78+
}
79+
7580
getMessageId(): number {
7681
return this.data.getMessageId();
7782
}
@@ -93,6 +98,10 @@ export class ServerPacket extends BasePacket implements SerializableMessage {
9398
return this.header.isPayloadEncrypted();
9499
}
95100

101+
isPayloadCompressed(): boolean {
102+
return this.header.isPayloadCompressed();
103+
}
104+
96105
isValidSignature(): boolean {
97106
return this.header.isValidSignature();
98107
}

packages/transactions/src/_MSG_STRING.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function _MSG_STRING(messageID: number): string {
3131
{ id: 391, name: "MC_CLUB_GET_INVITATIONS" }, // 0x187
3232
{ id: 438, name: "MC_CLIENT_CONNECT_MSG" }, // 0x1b6
3333
{ id: 440, name: "MC_TRACKING_MSG" },
34+
{ id: 455, name: "MC_CRC_PRE_RACE_DATA" },
3435
];
3536
const result = messageIds.find((id) => id.id === messageID);
3637

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// mcos is a game server, written from scratch, for an old game
2+
// Copyright (C) <2017> <Drazi Crendraven>
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published
6+
// by the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
type read_buf_func = (buf: Buffer, size: number, param: any) => number // uint
18+
19+
type write_buf_func = (buf: Buffer, size: number, param: any) => void
20+
21+
enum LUTSizesEnum {
22+
DIST_SIZES=0x40,
23+
CH_BITS_ASC_SIZE=0x100,
24+
LENS_SIZES=0x10,
25+
};
26+
27+
enum CommonSizes {
28+
OUT_BUFF_SIZE = 0x802,
29+
BUFF_SIZE=0x2204,
30+
};
31+
32+
enum ExplodeSizesEnum {
33+
IN_BUFF_SIZE=0x800,
34+
CODES_SIZE=0x100,
35+
OFFSS_SIZE=0x100,
36+
OFFSS_SIZE1=0x80,
37+
};
38+
39+
enum {
40+
EXP_BUFFER_SIZE = sizeof(TDcmpStruct), // Size of decompression structure
41+
// Defined as 12596 in pkware headers
42+
};
43+
44+
class TDcmpStruct {
45+
private offs0000 // ulong 0000
46+
private ctype // ulong 0004: Compression type (CMP_BINARY or CMP_ASCII)
47+
private outputPos // ulong 0008: Position in output buffer
48+
private dsize_bits // ulong 000C: Dict size (4, 5, 6 for 0x400, 0x800, 0x1000)
49+
private dsize_mask // ulong 0010: Dict size bitmask (0x0F, 0x1F, 0x3F for 0x400, 0x800, 0x1000)
50+
private bit_buff // ulong 0014: 16-bit buffer for processing input data
51+
private extra_bits // ulong 0018: Number of extra (above 8) bits in bit buffer
52+
private extra_bits // uint 001C: Position in in_buff
53+
private in_bytes // ulong 0020: Number of bytes in input buffer
54+
private param // void* 0024: Custom parameter
55+
private read_buf: read_buf_func // read_buf_func // Pointer to function that reads data from the input stream
56+
private write_buf: write_buf_func // write_buf_func // Pointer to function that writes data to the output stream
57+
private out_buff // uchar[BUFF_SIZE] 0030: Output circle buffer.
58+
// 0x0000 - 0x0FFF: Previous uncompressed data, kept for repetitions
59+
// 0x1000 - 0x1FFF: Currently decompressed data
60+
// 0x2000 - 0x2203: Reserve space for the longest possible repetition
61+
private in_buff // uchar[IN_BUFF_SIZE] 2234: Buffer for data to be decompressed
62+
private DistPosCodes // uchar[CODES_SIZE] 2A34: Table of distance position codes
63+
private LengthCodes // uchar[CODES_SIZE] 2B34: Table of length codes
64+
private offs2C34 // uchar[OFFSS_SIZE] 2C34: Buffer for
65+
private offs2D34 // uchar[OFFSS_SIZE] 2D34: Buffer for
66+
private offs2E34 // uchar[OFFSS_SIZE1] 2E34: Buffer for
67+
private offs2EB4 // uchar[OFFSS_SIZE] 2EB4: Buffer fo
68+
private ChBitsAsc // uchar[CH_BITS_ASC_SIZE] 2FB4: Buffer for
69+
private DistBits // uchar[DIST_SIZES] 30B4: Numbers of bytes to skip copied block length
70+
private LenBits // uchar[LENS_SIZES] 30F4: Numbers of bits for skip copied block length
71+
private ExLenBits // uchar[LENS_SIZES] 3104: Number of valid bits for copied block
72+
private LenBase // ushort[LENS_SIZES] 3114: Buffer fo
73+
74+
}
75+
76+
export function explode(read_buf: read_buf_func, write_buf: write_buf_func, work_buf: Buffer, param: any) {}
77+
78+
export function implode() {}

packages/transactions/src/internal.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
type BufferSerializer,
3535
} from "rusty-motors-shared-packets";
3636
import { _MSG_STRING } from "./_MSG_STRING.js";
37+
import { explode } from "pklib-ts"
3738

3839

3940

@@ -148,11 +149,24 @@ export async function receiveTransactionsData({
148149
decryptedMessage = inboundMessage;
149150
}
150151

152+
let decompressedMessage: ServerPacket;
153+
154+
if (decryptedMessage.isPayloadCompressed()) {
155+
156+
decompressedMessage = decompressMessage(
157+
decryptedMessage,
158+
connectionId,
159+
);
160+
} else {
161+
log.debug(`[${connectionId}] Message is not encrypted`);
162+
decompressedMessage = decryptedMessage;
163+
}
164+
151165
// Process the message
152166

153167
const response = await processInput({
154168
connectionId,
155-
inboundMessage: decryptedMessage,
169+
inboundMessage: decompressedMessage,
156170
log,
157171
});
158172

@@ -299,6 +313,59 @@ function encryptOutboundMessage(
299313
}
300314
}
301315

316+
function decompressMessage(
317+
compressedMessage: ServerPacket,
318+
connectionId: string,
319+
log = getServerLogger("transactionServer.decompressInboundMessage"),
320+
): ServerPacket {
321+
log.debug(`Decompressing message with initial messageId of ${compressedMessage.getMessageId()}`)
322+
323+
const uncompressedLen = compressedMessage.data.getMessageId()
324+
325+
const outputBuffer = new Uint8Array(64 * 1024); // 64KB buffer
326+
let outputPos = 0;
327+
328+
const compressedPayload = compressedMessage.getDataBuffer().subarray(2)
329+
330+
const writeCallback = (data: Uint8Array, bytesToWrite: number): number => {
331+
if (outputPos + bytesToWrite > outputBuffer.length) {
332+
throw new Error('Output buffer overflow');
333+
}
334+
outputBuffer.set(data.slice(0, bytesToWrite), outputPos);
335+
outputPos += bytesToWrite;
336+
return bytesToWrite;
337+
};
338+
339+
let inputPos = 0;
340+
const readCallback = (buffer: Uint8Array, bytesToRead: number): number => {
341+
const available = Math.min(bytesToRead, compressedPayload.length - inputPos);
342+
if (available <= 0) return 0;
343+
344+
buffer.set(compressedPayload.subarray(inputPos, inputPos + available));
345+
inputPos += available;
346+
return available;
347+
};
348+
349+
const result = explode(readCallback, writeCallback);
350+
351+
if (result.success) {
352+
const outputData = Buffer.from(outputBuffer.slice(0, outputPos));
353+
354+
log.debug(`DecompressedPayload: ${outputData.toString("hex")}`)
355+
356+
// Output raw binary data to stdout
357+
const uncompressedMessage = ServerPacket.copy(compressedMessage, outputData);
358+
uncompressedMessage.setPayloadCompression(false)
359+
return uncompressedMessage
360+
} else {
361+
log.error(`returned data len: ${result.decompressedData?.length}`)
362+
363+
throw new Error(result.errorCode.valueOf().toString() ?? 'Oh no!')
364+
}
365+
366+
367+
}
368+
302369
/**
303370
* @param {Buffer} buffer
304371
* @param {Buffer} buffer2

0 commit comments

Comments
 (0)