• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rustymotors / server / 25538948182

08 May 2026 05:37AM UTC coverage: 44.206% (+6.2%) from 37.969%
25538948182

Pull #2831

github

drazisil
Generate lcov coverage so the Coveralls upload step has a file to push

CI's "Upload coverage to Coveralls" step has been failing with

  🚨 ERROR: Couldn't find specified file: ./coverage/lcov.info

The Coveralls action expects ./coverage/lcov.info, but the workflow
runs `npm run coverage` and the script was just `vitest run` — no
--coverage flag, so coverage was never collected. Even with the flag,
Vitest's default reporters are text + html, neither of which write
lcov.info.

Two-line fix:

- package.json: "coverage" script now passes --coverage.
- vitest.config.ts: explicit `coverage: { provider: "v8", reporter:
  ["text", "lcov", "json"] }`. The `lcov` reporter writes
  ./coverage/lcov.info; `json` keeps Codecov's auto-detection paths
  happy.

Verified locally: `npm run coverage` produces ./coverage/lcov.info
(~231 KB) with full statement/branch/function/line summaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull Request #2831: Race flow: MCOTS handlers, packet reassembly, Sentry observability

657 of 1706 branches covered (38.51%)

Branch coverage included in aggregate %.

1004 of 1385 new or added lines in 59 files covered. (72.49%)

47 existing lines in 27 files now uncovered.

4173 of 9220 relevant lines covered (45.26%)

131.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.84
/packages/shared/src/UserData.ts
1
import type { Serializable, NPSMessage } from './types.js';
2
import { RawMessageHeader } from './RawMessage.js';
3
import {
4
    align4,
5
    checkMinLength,
6
    checkSize2,
7
    checkSize4,
8
    CString,
9
    Long,
10
    padBuffer,
11
    setByte,
12
    sliceBuff,
13
} from './helpers.js';
14

15
// function real2Int(r: number): number {
16
//         // C cast to int truncates toward zero; Math.trunc mirrors that behaviour
17
//         return Math.trunc(r);
18
// }
19

20
// function real2Fixed(f: number): number {
21
//         // REAL2FIXED(x) -> REAL2INT((x)*65536.0f)
22
//         return real2Int(f * 65536.0);
23
// }
24

25
export function int2Real(x: number): number {
26
    return x;
×
27
} // INT2REAL
28
export function fixed2Real(x: number): number {
29
    return x * (1.0 / 65536.0);
×
30
} // FIXED2REAL
31
export function float2Real(x: number): number {
32
    return x;
×
33
} // FLOAT2REAL
34
export function real2Float(x: number): number {
35
    return x;
×
36
} // REAL2FLOAT
37
export function real(x: number): number {
38
    return x;
×
39
} // real()
40

41
// Angle / unit helpers
42
export const RMDEGREE2RAD = 0.01745328; // as in the original macro
43✔
43
export function rmDegree2Real(a: number): number {
44
    return a * (1.0 / 360.0);
×
45
} // RMDEGREE2REAL
46
export function rmReal2Rad(a: number): number {
47
    return a * (2.0 * 3.14159265359);
×
48
} // RMREAL2RAD
49
export function rmReal2Degree(a: number): number {
50
    return a * 360.0;
×
51
} // RMREAL2DEGREE
52

53
export function floatFrom5Dot3(b: number): number {
54
    // FLOATFROM5DOT3(b) -> FIXED2REAL(((int)b)<<13)
55
    // simplifies to b / 8
56
    return (b & 0xff) / 8;
×
57
}
58

59
export function floatTo5Dot3(f: number): number {
60
    // FLOATTO5DOT3(f) -> ((REAL2FIXED(f)>>13)&0xFF)
61
    // REAL2FIXED(f) ~= Math.trunc(f * 65536) so this simplifies to Math.trunc(f * 8) & 0xFF
62
    return Math.trunc(f * 8) & 0xff;
×
63
}
64

65
export class CarDecal implements Serializable {
66
    private _backgroundImage: Buffer; // byte
67
    private _forgroundImage: Buffer; // byte
68
    private _color0: Buffer; // byte
69
    private _color1: Buffer; // byte
70

71
    constructor() {
72
        this._backgroundImage = Buffer.alloc(1);
30✔
73
        this._forgroundImage = Buffer.alloc(1);
30✔
74
        this._color0 = Buffer.alloc(1);
30✔
75
        this._color1 = Buffer.alloc(1);
30✔
76
    }
77

78
    get compressed() {
79
        return Buffer.concat([
5✔
80
            this._backgroundImage,
81
            this._forgroundImage,
82
            this._color0,
83
            this._color1,
84
        ]);
85
    }
86

87
    set compressed(val: Buffer) {
88
        if (val.byteLength !== 4) {
6!
89
            throw new Error('compressed must be exactly 4 bytes');
×
90
        }
91

92
        this._backgroundImage = Buffer.from(val.subarray(0, 1));
6✔
93
        this._forgroundImage = Buffer.from(val.subarray(1, 2));
6✔
94
        this._color0 = Buffer.from(val.subarray(2, 3));
6✔
95
        this._color1 = Buffer.from(val.subarray(3, 4));
6✔
96
    }
97

98
    get sizeOf() {
99
        return 4;
6✔
100
    }
101

102
    serialize() {
103
        return this.compressed;
5✔
104
    }
105

106
    deserialize(buf: Buffer) {
107
        if (buf.byteLength !== 4) {
6!
108
            throw new Error('input must be exactly 4 bytes long');
×
109
        }
110
        this.compressed = buf;
6✔
111
    }
112
}
113

114
export class CarIds implements Serializable {
115
    private dbCarID; // ulong         // DB part instance (may be zero, if this is just a stock car def)
116
    private dbBptID; // ulong       // branded part type ID (stock car definitions)
117
    private dbSkinID; // ulong   // skin ID
118
    private _driverModelType; // byte
119
    private _driverSkinColor; // int // 32-bit argb
120
    private _driverHairColor; // int// 32-bit argb
121
    private _driverShirtColor; //int  // 32-bit argb
122
    private _driverPantsColor; // int  // 32-bit argb
123
    private _flags; // char        // (for multicar) if not 0, force the car to reload, will get reset to 0 when the car starts reloading
124
    private _skinFlags; // char
125
    private _CarDecal: CarDecal;
126

127
    constructor() {
128
        this.dbCarID = Buffer.alloc(4);
30✔
129
        this.dbBptID = Buffer.alloc(4);
30✔
130
        this.dbSkinID = Buffer.alloc(4);
30✔
131
        this._driverModelType = Buffer.alloc(1);
30✔
132
        this._driverSkinColor = Buffer.alloc(4);
30✔
133
        this._driverHairColor = Buffer.alloc(4);
30✔
134
        this._driverShirtColor = Buffer.alloc(4);
30✔
135
        this._driverPantsColor = Buffer.alloc(4);
30✔
136
        this._flags = Buffer.alloc(1);
30✔
137
        this._skinFlags = Buffer.alloc(1);
30✔
138
        this._CarDecal = new CarDecal();
30✔
139
    }
140

141
    /**
142
     * Sets the driver model type for the user.
143
     *
144
     * @param val - The numeric value representing the driver model type.
145
     *              This value is processed by the `setByte` function before assignment.
146
     */
147
    set driverModelType(val: number) {
148
        this._driverModelType = setByte(val);
×
149
    }
150

151
    set skinFlags(val: number) {
152
        this._skinFlags = setByte(val);
×
153
    }
154

155
    set flags(val: number) {
156
        this._flags = setByte(val);
×
157
    }
158

159
    get sizeOf() {
160
        return 40;
11✔
161
    }
162

163
    serialize() {
164
        const data = Buffer.concat([
5✔
165
            this.dbCarID,
166
            this.dbBptID,
167
            this.dbSkinID,
168
            padBuffer(this._driverModelType),
169
            this._driverSkinColor,
170
            this._driverHairColor,
171
            this._driverShirtColor,
172
            this._driverPantsColor,
173
            padBuffer(Buffer.concat([this._flags, this._skinFlags])),
174
            this._CarDecal.serialize(),
175
        ]);
176

177
        if (data.byteLength > this.sizeOf) {
5!
178
            throw new Error(`CarIds exceeds max size!: ${data.byteLength} > ${this.sizeOf}`);
×
179
        }
180

181
        return data;
5✔
182
    }
183

184
    deserialize(buf: Buffer) {
185
        let offset = 0;
6✔
186
        this.dbCarID = sliceBuff(buf, offset, 4);
6✔
187
        offset += 4;
6✔
188
        this.dbBptID = sliceBuff(buf, offset, 4);
6✔
189
        offset += 4;
6✔
190
        this.dbSkinID = sliceBuff(buf, offset, 4);
6✔
191
        offset += 4;
6✔
192
        this._driverModelType = sliceBuff(buf, offset, 1);
6✔
193
        offset += 4;
6✔
194
        this._driverSkinColor = sliceBuff(buf, offset, 4);
6✔
195
        offset += 4;
6✔
196
        this._driverHairColor = sliceBuff(buf, offset, 4);
6✔
197
        offset += 4;
6✔
198
        this._driverShirtColor = sliceBuff(buf, offset, 4);
6✔
199
        offset += 4;
6✔
200
        this._driverPantsColor = sliceBuff(buf, offset, 4);
6✔
201
        offset += 4;
6✔
202
        this._flags = sliceBuff(buf, offset, 1);
6✔
203
        offset += 1;
6✔
204
        this._skinFlags = sliceBuff(buf, offset, 1);
6✔
205
        offset += 3;
6✔
206
        this._CarDecal.deserialize(
6✔
207
            sliceBuff(buf, offset, this._CarDecal.sizeOf),
208
        );
209
    }
210
}
211

212
export class UserData implements Serializable {
213
    private _carIds: CarIds; //", field: "Structure" },
214
    private _lobbyId: Buffer; //", field: "Dword" },
215
    private _clubId: Buffer; //", field: "Dword" },
216
    private _flags: Buffer; // 1 byte bitfield
217
    private _performance: Buffer; //", field: "Dword" },
218
    private _points: Buffer; //", field: "Dword" },
219
    private _level: Buffer; //", field: "Short" },
220

221
    constructor() {
222
        this._carIds = new CarIds();
30✔
223
        this._lobbyId = Buffer.alloc(4);
30✔
224
        this._clubId = Buffer.alloc(4);
30✔
225
        this._flags = Buffer.alloc(1);
30✔
226
        this._performance = Buffer.alloc(4);
30✔
227
        this._points = Buffer.alloc(4);
30✔
228
        this._level = Buffer.alloc(2);
30✔
229
    }
230

231
    get sizeOf() {
232
        return 64;
28✔
233
    }
234

235
    serialize() {
236
        const data = Buffer.concat([
5✔
237
            this._carIds.serialize(),
238
            this._lobbyId,
239
            this._clubId,
240
            this._flags,
241
            this._performance,
242
            this._points,
243
            this._level,
244
        ]);
245

246
        if (data.byteLength > this.sizeOf) {
5!
247
            throw new Error(`UserData exceedd max size!`);
×
248
        }
249

250
        const outBuff = Buffer.alloc(this.sizeOf);
5✔
251
        data.copy(outBuff);
5✔
252
        return outBuff;
5✔
253
    }
254

255
    deserialize(buf: Buffer) {
256
        if (buf.byteLength < 64) {
6!
257
            throw new Error(`Buffer must be 64 bytes: ${buf.byteLength}`);
×
258
        }
259
        let offset = 0;
6✔
260
        this._carIds.deserialize(buf.subarray(offset));
6✔
261
        offset += this._carIds.sizeOf;
6✔
262
        this._lobbyId = sliceBuff(buf, offset, 4);
6✔
263
        offset += 4;
6✔
264
        this._clubId = sliceBuff(buf, offset, 4);
6✔
265
        offset += 4;
6✔
266
        this._flags = sliceBuff(buf, offset, 1);
6✔
267
        offset += 1;
6✔
268
        this._performance = sliceBuff(buf, offset, 4);
6✔
269
        offset += 4;
6✔
270
        this._points = sliceBuff(buf, offset, 4);
6✔
271
        offset += 4;
6✔
272
        this._level = sliceBuff(buf, offset, 2);
6✔
273
    }
274

275
    get lobbyId() {
276
        return this._lobbyId.readInt32BE();
×
277
    }
278
}
279

280
export class UserInfo implements Serializable {
281
    private _userId: Buffer;
282
    private _username: CString;
283
    private _userData: UserData;
284

285
    constructor() {
286
        this._userId = Buffer.alloc(4);
20✔
287
        this._username = new CString(32);
20✔
288
        this._userData = new UserData();
20✔
289
    }
290

291
    get sizeOf() {
292
        const nameLen = this._username.toString().length + 1; // strlen + null
5✔
293
        return 4 + 4 + align4(nameLen) + this._userData.sizeOf;
5✔
294
    }
295

296
    serialize() {
297
        const nameStr = this._username.toString();
5✔
298
        const nameLen = nameStr.length + 1;
5✔
299
        const nameLenBuf = Buffer.alloc(4);
5✔
300
        nameLenBuf.writeUInt32BE(nameLen);
5✔
301
        const nameBuf = Buffer.alloc(align4(nameLen));
5✔
302
        nameBuf.write(nameStr, 0, 'utf8');
5✔
303
        return Buffer.from(
5✔
304
            Buffer.concat([
305
                this._userId,
306
                nameLenBuf,
307
                nameBuf,
308
                this._userData.serialize(),
309
            ]),
310
        );
311
    }
312

313
    deserialize(buf: Buffer) {
314
        // NPS wire format: userId (4) + nameLen (4) + name aligned to 4 + userData (64)
315
        checkMinLength(buf, 4 + 4 + 4 + this._userData.sizeOf);
6✔
316
        let offset = 0;
6✔
317
        this._userId = sliceBuff(buf, offset, 4);
6✔
318
        offset += 4;
6✔
319
        const nameLen = buf.readUInt32BE(offset);
6✔
320
        offset += 4;
6✔
321
        const nameStr = sliceBuff(buf, offset, nameLen - 1).toString('utf8');
6✔
322
        this._username.set(nameStr);
6✔
323
        offset += align4(nameLen);
6✔
324
        this._userData.deserialize(sliceBuff(buf, offset, this._userData.sizeOf));
6✔
325
    }
326

327
    get userId() {
328
        return this._userId.readInt32BE();
10✔
329
    }
330

331
    set userId(val: number) {
332
        checkSize4(val);
9✔
333
        this._userId.writeInt32BE(val);
9✔
334
    }
335

336
    get userName() {
337
        return this._username.toString().trimEnd();
×
338
    }
339

340
    set userName(val: string) {
341
        this._username.set(val);
9✔
342
    }
343

344
    get userData(): UserData {
345
        return this._userData;
5✔
346
    }
347

348
    set userData(val: UserData) {
349
        this._userData = val;
9✔
350
    }
351

352
    get personaId(): number {
353
        return this._userId.readInt32BE();
×
354
    }
355

356
    set personaId(val: number) {
357
        checkSize4(val);
×
358
        this._userId.writeInt32BE(val);
×
359
    }
360

361
    setUserName(username: string) {
362
        this._username.set(username);
1✔
363
    }
364
    getUserName() {
365
        return this._username.toString();
2✔
366
    }
367
}
368

369
export class UserInfoMessage implements NPSMessage {
370
    private _header: RawMessageHeader;
371
    private _userInfo: UserInfo;
372

373
    constructor() {
374
        this._header = new RawMessageHeader();
14✔
375
        this._userInfo = new UserInfo();
14✔
376
    }
377

378
    get sizeOf() {
379
        return this._header.sizeOf + this._userInfo.sizeOf;
5✔
380
    }
381

382
    serialize() {
383
        this._header.length = this.sizeOf
5✔
384
        return Buffer.from(
5✔
385
            Buffer.concat([
386
                this._header.serialize(),
387
                this._userInfo.serialize(),
388
            ]),
389
        );
390
    }
391

392
    deserialize(buf: Buffer) {
393
        if (buf.byteLength < this._header.sizeOf) {
5!
394
            throw new Error(
×
395
                `unable to deserialize header, need 4 bytes, got ${buf.byteLength} bytes`,
396
            );
397
        }
398
        let offset = 0;
5✔
399
        this._header.deserialize(sliceBuff(buf, offset, 4));
5✔
400
        offset += 4;
5✔
401
        this._userInfo.deserialize(buf.subarray(offset));
5✔
402
        // Update header length to match actual message size
403
        this._header.length = buf.byteLength;
5✔
404
    }
405

406
    get id() {
407
        return this._header.id;
×
408
    }
409

410
    setOpCode(val: number) {
411
        checkSize2(val)
4✔
412
        this._header.id = val
4✔
413
    }
414

415
    get length() {
416
        return this._header.length;
×
417
    }
418

419
    get userInfo(): UserInfo {
420
        return this._userInfo;
45✔
421
    }
422

423
    setUserInfo(val: UserInfo) {
424
        this._userInfo = val
4✔
425
    }
426

427
    get header() {
428
        return this._header
×
429
    }
430
}
431

432
const NPS_USERNAME_LEN = 32;
43✔
433

434
export class UserJoinedChannelMessage implements Serializable {
UNCOV
435
    private _userName = new CString(NPS_USERNAME_LEN);
×
436
    private _userId = new Long();
×
437
    private _commId = new Long();
×
438
    private _userData = new UserData();
×
439
    private _personaId = new Long();
×
440

441
    constructor(
442
        userName: string,
443
        userId: number,
444
        commId: number,
445
        userData: UserData,
446
        personaId = 0,
×
447
    ) {
448
        this._userName.set(userName);
×
449
        this._userId.value = userId;
×
450
        this._commId.value = commId;
×
451
        this._userData = userData;
×
452
        this._personaId.value = personaId;
×
453
    }
454

455
    get sizeOf() {
456
        return (
×
457
            this._userName.sizeOf +
458
            this._userId.sizeOf +
459
            this._commId.sizeOf +
460
            this._userData.sizeOf +
461
            this._personaId.sizeOf
462
        );
463
    }
464

465
    serialize() {
466
        return Buffer.concat([
×
467
            this._userId.serialize(),
468
            this._userName.serialize(),
469
            this._commId.serialize(),
470
            this._userData.serialize(),
471
            this._personaId.serialize(),
472
        ]);
473
    }
474

475
    deserialize(buf: Buffer) {
476
        checkMinLength(buf, this.sizeOf);
×
477
        let offset = 0;
×
478
        this._userId.deserialize(sliceBuff(buf, offset, this._userId.sizeOf));
×
479
        offset += this._userId.sizeOf;
×
480
        this._userName.deserialize(buf.subarray(offset));
×
481
        offset += this._userName.sizeOf;
×
482
        this._commId.deserialize(sliceBuff(buf, offset, this._commId.sizeOf));
×
483
        offset += this._commId.sizeOf;
×
484
        this._userData.deserialize(
×
485
            sliceBuff(buf, offset, this._userData.sizeOf),
486
        );
487
        offset += this._userData.sizeOf;
×
488
        this._personaId.deserialize(
×
489
            sliceBuff(buf, offset, this._personaId.sizeOf),
490
        );
491
    }
492
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc