@quake2ts/server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/client.d.ts +51 -0
  2. package/dist/client.js +100 -0
  3. package/dist/dedicated.d.ts +72 -0
  4. package/dist/dedicated.js +1104 -0
  5. package/dist/index.cjs +1586 -0
  6. package/dist/index.d.ts +7 -0
  7. package/dist/index.js +1543 -0
  8. package/dist/net/nodeWsDriver.d.ts +16 -0
  9. package/dist/net/nodeWsDriver.js +122 -0
  10. package/dist/protocol/player.d.ts +2 -0
  11. package/dist/protocol/player.js +1 -0
  12. package/dist/protocol/write.d.ts +7 -0
  13. package/dist/protocol/write.js +167 -0
  14. package/dist/protocol.d.ts +17 -0
  15. package/dist/protocol.js +71 -0
  16. package/dist/server.d.ts +50 -0
  17. package/dist/server.js +12 -0
  18. package/dist/transport.d.ts +7 -0
  19. package/dist/transport.js +1 -0
  20. package/dist/transports/websocket.d.ts +11 -0
  21. package/dist/transports/websocket.js +38 -0
  22. package/package.json +35 -0
  23. package/src/client.ts +173 -0
  24. package/src/dedicated.ts +1295 -0
  25. package/src/index.ts +8 -0
  26. package/src/net/nodeWsDriver.ts +129 -0
  27. package/src/protocol/player.ts +2 -0
  28. package/src/protocol/write.ts +185 -0
  29. package/src/protocol.ts +91 -0
  30. package/src/server.ts +76 -0
  31. package/src/transport.ts +8 -0
  32. package/src/transports/websocket.ts +42 -0
  33. package/test.bsp +0 -0
  34. package/tests/client.test.ts +20 -0
  35. package/tests/connection_flow.test.ts +93 -0
  36. package/tests/dedicated.test.ts +211 -0
  37. package/tests/dedicated_trace.test.ts +117 -0
  38. package/tests/integration/configstring_sync.test.ts +235 -0
  39. package/tests/lag.test.ts +144 -0
  40. package/tests/protocol/player.test.ts +88 -0
  41. package/tests/protocol/write.test.ts +107 -0
  42. package/tests/protocol.test.ts +102 -0
  43. package/tests/server-state.test.ts +17 -0
  44. package/tests/server.test.ts +99 -0
  45. package/tests/unit/dedicated_timeout.test.ts +142 -0
  46. package/tsconfig.json +9 -0
  47. package/tsconfig.tsbuildinfo +1 -0
  48. package/vitest.config.ts +40 -0
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { BinaryWriter } from '@quake2ts/shared';
3
+ import { writePlayerState, ProtocolPlayerState } from '../../src/protocol/player.js';
4
+ import { parseProtocolPlayerState } from '@quake2ts/test-utils';
5
+
6
+ describe('writePlayerState', () => {
7
+ it('should write an empty state as just flags (plus statbits)', () => {
8
+ const writer = new BinaryWriter();
9
+ const emptyState: ProtocolPlayerState = {
10
+ pm_type: 0, origin: {x:0,y:0,z:0}, velocity: {x:0,y:0,z:0}, pm_time: 0, pm_flags: 0, gravity: 0,
11
+ delta_angles: {x:0,y:0,z:0}, viewoffset: {x:0,y:0,z:0}, viewangles: {x:0,y:0,z:0}, kick_angles: {x:0,y:0,z:0},
12
+ gun_index: 0, gun_frame: 0, gun_offset: {x:0,y:0,z:0}, gun_angles: {x:0,y:0,z:0},
13
+ blend: [0,0,0,0], fov: 0, rdflags: 0, stats: new Array(32).fill(0), watertype: 0
14
+ };
15
+
16
+ writePlayerState(writer, emptyState);
17
+ const data = writer.getData();
18
+ const readState = parseProtocolPlayerState(data);
19
+
20
+ expect(readState.pm_type).toBe(0);
21
+ expect(readState.origin.x).toBe(0);
22
+ expect(readState.stats[0]).toBe(0);
23
+ expect(readState.watertype).toBe(0);
24
+ });
25
+
26
+ it('should correctly round-trip all fields', () => {
27
+ const writer = new BinaryWriter();
28
+ const state: ProtocolPlayerState = {
29
+ pm_type: 1,
30
+ origin: { x: 10, y: -20, z: 30 },
31
+ velocity: { x: 100, y: -50, z: 0 },
32
+ pm_time: 123,
33
+ pm_flags: 4,
34
+ gravity: 800,
35
+ delta_angles: { x: 10, y: 20, z: 30 },
36
+ viewoffset: { x: 5, y: -5, z: 10 },
37
+ viewangles: { x: 45, y: 90, z: 0 },
38
+ kick_angles: { x: 1, y: 2, z: 3 },
39
+ gun_index: 5,
40
+ gun_frame: 12,
41
+ gun_offset: { x: 1, y: 2, z: 3 },
42
+ gun_angles: { x: 4, y: 5, z: 6 },
43
+ blend: [255, 128, 64, 32],
44
+ fov: 90,
45
+ rdflags: 7,
46
+ stats: new Array(32).fill(0),
47
+ watertype: 128 // Custom flag
48
+ };
49
+ state.stats[1] = 100; // Health
50
+ state.stats[3] = 50; // Ammo
51
+
52
+ writePlayerState(writer, state);
53
+ const data = writer.getData();
54
+ const read = parseProtocolPlayerState(data);
55
+
56
+ // Verification with tolerances for precision loss (1/8 units, etc)
57
+ expect(read.pm_type).toBe(state.pm_type);
58
+ expect(read.origin.x).toBe(state.origin.x);
59
+ expect(read.velocity.x).toBe(state.velocity.x);
60
+ expect(read.pm_time).toBe(state.pm_time);
61
+ expect(read.gravity).toBe(state.gravity);
62
+ expect(read.viewangles.x).toBeCloseTo(state.viewangles.x, 0.1);
63
+ expect(read.blend).toEqual(state.blend);
64
+ expect(read.stats[1]).toBe(100);
65
+ expect(read.stats[3]).toBe(50);
66
+ expect(read.gun_index).toBe(5);
67
+ expect(read.gun_frame).toBe(12);
68
+ expect(read.watertype).toBe(128);
69
+ });
70
+
71
+ it('should ignore stats > 31', () => {
72
+ const writer = new BinaryWriter();
73
+ const state: ProtocolPlayerState = {
74
+ pm_type: 0, origin: {x:0,y:0,z:0}, velocity: {x:0,y:0,z:0}, pm_time: 0, pm_flags: 0, gravity: 0,
75
+ delta_angles: {x:0,y:0,z:0}, viewoffset: {x:0,y:0,z:0}, viewangles: {x:0,y:0,z:0}, kick_angles: {x:0,y:0,z:0},
76
+ gun_index: 0, gun_frame: 0, gun_offset: {x:0,y:0,z:0}, gun_angles: {x:0,y:0,z:0},
77
+ blend: [0,0,0,0], fov: 0, rdflags: 0, stats: new Array(64).fill(0), watertype: 0
78
+ };
79
+ state.stats[31] = 999;
80
+ state.stats[32] = 123; // Should be ignored
81
+
82
+ writePlayerState(writer, state);
83
+ const read = parseProtocolPlayerState(writer.getData());
84
+
85
+ expect(read.stats[31]).toBe(999);
86
+ expect(read.stats[32]).toBeUndefined(); // Array size is 32 in reader
87
+ });
88
+ });
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { BinaryWriter, ServerCommand, TempEntity, Vec3 } from '@quake2ts/shared';
3
+ import { writeServerCommand } from '../../src/protocol/write.js';
4
+ import { Entity } from '@quake2ts/game';
5
+
6
+ describe('writeServerCommand', () => {
7
+ it('writes ServerCommand.print', () => {
8
+ const writer = new BinaryWriter();
9
+ writeServerCommand(writer, ServerCommand.print, 1, "Hello World\n");
10
+ const data = writer.getData();
11
+
12
+ // Expect: [cmd (10), level (1), string ("Hello World\n")]
13
+ expect(data[0]).toBe(ServerCommand.print);
14
+ expect(data[1]).toBe(1);
15
+
16
+ // String check: 'H' is 72 ...
17
+ expect(String.fromCharCode(data[2])).toBe('H');
18
+ });
19
+
20
+ it('writes ServerCommand.muzzleflash', () => {
21
+ const writer = new BinaryWriter();
22
+ writeServerCommand(writer, ServerCommand.muzzleflash, 42, 128);
23
+ const data = writer.getData();
24
+
25
+ // Expect: [cmd (1), ent (42, short), flash (128, byte)]
26
+ expect(data[0]).toBe(ServerCommand.muzzleflash);
27
+ // short 42 = 42, 0
28
+ expect(data[1]).toBe(42);
29
+ expect(data[2]).toBe(0);
30
+ expect(data[3]).toBe(128);
31
+ });
32
+
33
+ it('writes TempEntity.ROCKET_EXPLOSION', () => {
34
+ const writer = new BinaryWriter();
35
+ const pos: Vec3 = { x: 10, y: 20, z: 30 };
36
+ writeServerCommand(writer, ServerCommand.temp_entity, TempEntity.ROCKET_EXPLOSION, pos);
37
+ const data = writer.getData();
38
+
39
+ // Expect: [cmd (3), type (7), pos (x, y, z as shorts * 8)]
40
+ expect(data[0]).toBe(ServerCommand.temp_entity);
41
+ expect(data[1]).toBe(TempEntity.ROCKET_EXPLOSION);
42
+
43
+ // 10 * 8 = 80
44
+ expect(data[2]).toBe(80);
45
+ expect(data[3]).toBe(0);
46
+ });
47
+
48
+ it('writes TempEntity.PARASITE_ATTACK', () => {
49
+ const writer = new BinaryWriter();
50
+ const ent = { index: 99 } as Entity;
51
+ const start: Vec3 = { x: 1, y: 1, z: 1 };
52
+ const end: Vec3 = { x: 2, y: 2, z: 2 };
53
+
54
+ writeServerCommand(writer, ServerCommand.temp_entity, TempEntity.PARASITE_ATTACK, ent, start, end);
55
+ const data = writer.getData();
56
+
57
+ // Expect: [cmd, type, short ent, pos start, pos end]
58
+ expect(data[0]).toBe(ServerCommand.temp_entity);
59
+ expect(data[1]).toBe(TempEntity.PARASITE_ATTACK);
60
+
61
+ // Ent index 99
62
+ expect(data[2]).toBe(99);
63
+ expect(data[3]).toBe(0);
64
+ });
65
+
66
+ it('writes ServerCommand.centerprint', () => {
67
+ const writer = new BinaryWriter();
68
+ writeServerCommand(writer, ServerCommand.centerprint, "Center Msg");
69
+ const data = writer.getData();
70
+
71
+ expect(data[0]).toBe(ServerCommand.centerprint);
72
+ // "Center Msg" is 10 chars + null terminator = 11 bytes
73
+ expect(data.length).toBe(1 + 11);
74
+ expect(String.fromCharCode(data[1])).toBe('C');
75
+ });
76
+
77
+ it('writes ServerCommand.stufftext', () => {
78
+ const writer = new BinaryWriter();
79
+ writeServerCommand(writer, ServerCommand.stufftext, "cmd\n");
80
+ const data = writer.getData();
81
+
82
+ expect(data[0]).toBe(ServerCommand.stufftext);
83
+ expect(data.length).toBe(1 + 5);
84
+ expect(String.fromCharCode(data[1])).toBe('c');
85
+ });
86
+
87
+ it('writes ServerCommand.sound', () => {
88
+ const writer = new BinaryWriter();
89
+ const flags = 1 | 8; // SND_VOLUME | SND_ENT
90
+ const soundNum = 5;
91
+ const volume = 255;
92
+ const ent = 10;
93
+
94
+ // args: flags, soundNum, volume, attenuation, offset, ent, pos
95
+ writeServerCommand(writer, ServerCommand.sound, flags, soundNum, volume, undefined, undefined, ent, undefined);
96
+ const data = writer.getData();
97
+
98
+ // Format: [cmd, flags, soundNum, volume (if flag), ent (short, if flag)]
99
+ let idx = 0;
100
+ expect(data[idx++]).toBe(ServerCommand.sound);
101
+ expect(data[idx++]).toBe(flags);
102
+ expect(data[idx++]).toBe(soundNum);
103
+ expect(data[idx++]).toBe(volume);
104
+ expect(data[idx++]).toBe(10); // ent low byte
105
+ expect(data[idx++]).toBe(0); // ent high byte
106
+ });
107
+ });
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ClientMessageParser, ClientMessageHandler } from '../src/protocol.js';
3
+ import { BinaryWriter, BinaryStream, ClientCommand } from '@quake2ts/shared';
4
+
5
+ describe('ClientMessageParser', () => {
6
+ it('should parse NOP command', () => {
7
+ const writer = new BinaryWriter();
8
+ writer.writeByte(ClientCommand.nop);
9
+
10
+ const stream = new BinaryStream(writer.getData().buffer);
11
+ const handler = {
12
+ onMove: vi.fn(),
13
+ onUserInfo: vi.fn(),
14
+ onStringCmd: vi.fn(),
15
+ onNop: vi.fn(),
16
+ onBad: vi.fn()
17
+ };
18
+
19
+ const parser = new ClientMessageParser(stream, handler);
20
+ parser.parseMessage();
21
+
22
+ expect(handler.onNop).toHaveBeenCalled();
23
+ });
24
+
25
+ it('should parse StringCmd', () => {
26
+ const writer = new BinaryWriter();
27
+ writer.writeByte(ClientCommand.stringcmd);
28
+ writer.writeString("status");
29
+
30
+ const stream = new BinaryStream(writer.getData().buffer);
31
+ const handler = {
32
+ onMove: vi.fn(),
33
+ onUserInfo: vi.fn(),
34
+ onStringCmd: vi.fn(),
35
+ onNop: vi.fn(),
36
+ onBad: vi.fn()
37
+ };
38
+
39
+ const parser = new ClientMessageParser(stream, handler);
40
+ parser.parseMessage();
41
+
42
+ expect(handler.onStringCmd).toHaveBeenCalledWith("status");
43
+ });
44
+
45
+ it('should parse UserInfo', () => {
46
+ const writer = new BinaryWriter();
47
+ writer.writeByte(ClientCommand.userinfo);
48
+ writer.writeString("\\name\\Player\\skin\\male/grunt");
49
+
50
+ const stream = new BinaryStream(writer.getData().buffer);
51
+ const handler = {
52
+ onMove: vi.fn(),
53
+ onUserInfo: vi.fn(),
54
+ onStringCmd: vi.fn(),
55
+ onNop: vi.fn(),
56
+ onBad: vi.fn()
57
+ };
58
+
59
+ const parser = new ClientMessageParser(stream, handler);
60
+ parser.parseMessage();
61
+
62
+ expect(handler.onUserInfo).toHaveBeenCalledWith("\\name\\Player\\skin\\male/grunt");
63
+ });
64
+
65
+ it('should parse Move command (UserCmd)', () => {
66
+ const writer = new BinaryWriter();
67
+ writer.writeByte(ClientCommand.move);
68
+ writer.writeByte(123); // Checksum
69
+ writer.writeLong(100); // LastFrame
70
+ writer.writeByte(50); // msec
71
+ writer.writeByte(1); // buttons
72
+ writer.writeShort(0); // angles x
73
+ writer.writeShort(0); // angles y
74
+ writer.writeShort(0); // angles z
75
+ writer.writeShort(200); // forward
76
+ writer.writeShort(100); // side
77
+ writer.writeShort(50); // up
78
+ writer.writeByte(0); // impulse
79
+ writer.writeByte(0); // lightlevel
80
+
81
+ const stream = new BinaryStream(writer.getData().buffer);
82
+ const handler = {
83
+ onMove: vi.fn(),
84
+ onUserInfo: vi.fn(),
85
+ onStringCmd: vi.fn(),
86
+ onNop: vi.fn(),
87
+ onBad: vi.fn()
88
+ };
89
+
90
+ const parser = new ClientMessageParser(stream, handler);
91
+ parser.parseMessage();
92
+
93
+ expect(handler.onMove).toHaveBeenCalled();
94
+ const [checksum, lastFrame, cmd] = (handler.onMove as unknown as any).mock.calls[0];
95
+ expect(checksum).toBe(123);
96
+ expect(lastFrame).toBe(100);
97
+ expect(cmd.msec).toBe(50);
98
+ expect(cmd.forwardmove).toBe(200);
99
+ expect(cmd.sidemove).toBe(100);
100
+ expect(cmd.upmove).toBe(50);
101
+ });
102
+ });
@@ -0,0 +1,17 @@
1
+
2
+ import { describe, it, expect } from 'vitest';
3
+ import { ClientState } from '../src/client.js';
4
+ import { UPDATE_BACKUP } from '@quake2ts/shared';
5
+ import { createMockServerClient } from '@quake2ts/test-utils';
6
+
7
+ describe('Server State Structures', () => {
8
+ it('Client initialization should set default values correctly', () => {
9
+ const client = createMockServerClient(0);
10
+
11
+ expect(client.index).toBe(0);
12
+ expect(client.state).toBe(ClientState.Connected);
13
+ expect(client.frames.length).toBe(0); // Mock client has empty frames by default in test-utils factory unless overridden
14
+ expect(client.rate).toBe(0); // Mock client default
15
+ expect(client.lastCmd.msec).toBe(0);
16
+ });
17
+ });
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { DedicatedServer, createServer } from '../src/dedicated.js';
3
+ import { createMockTransport, MockTransport } from '@quake2ts/test-utils';
4
+
5
+ // Mock dependencies
6
+ vi.mock('node:fs/promises', () => ({
7
+ default: {
8
+ readFile: vi.fn().mockResolvedValue(Buffer.alloc(100))
9
+ }
10
+ }));
11
+
12
+ // Must import makeBspModel inside the mock factory to avoid hoisting issues
13
+ vi.mock('@quake2ts/engine', async (importOriginal) => {
14
+ // We can't use the top-level import here because of hoisting.
15
+ // So we need to either inline the object or import dynamically if possible,
16
+ // but vitest mocks are synchronous usually.
17
+ // However, since we are mocking a return value, we can just return a plain object that looks like what makeBspModel returns
18
+ // OR we can rely on `await vi.importActual('@quake2ts/test-utils')` if we were mocking that module, but we are mocking `engine`.
19
+
20
+ // The error says "Cannot access '__vi_import_1__' before initialization".
21
+ // This is because `makeBspModel` is imported at top level but used in hoisted `vi.mock`.
22
+
23
+ // Solution: Just return a plain object matching the structure, or move `makeBspModel` call inside the test or `beforeEach` and use `vi.mocked`.
24
+ // But `parseBsp` is called internally by `DedicatedServer`.
25
+
26
+ // Let's just define a minimal object here that satisfies the requirements to avoid complexity.
27
+ return {
28
+ parseBsp: vi.fn().mockReturnValue({
29
+ planes: [],
30
+ nodes: [],
31
+ leafs: [],
32
+ brushes: [],
33
+ leafBrushes: [],
34
+ bmodels: [],
35
+ models: [],
36
+ texInfo: [],
37
+ brushSides: [],
38
+ visibility: { numClusters: 0, clusters: [] }
39
+ })
40
+ };
41
+ });
42
+
43
+ describe('DedicatedServer', () => {
44
+ let server: DedicatedServer;
45
+ let transport: MockTransport;
46
+ let consoleWarnSpy: any;
47
+ let consoleLogSpy: any;
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ transport = createMockTransport();
52
+ server = new DedicatedServer({ port: 27910, transport });
53
+
54
+ // Suppress expected console warnings and logs
55
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
56
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
57
+ });
58
+
59
+ afterEach(() => {
60
+ consoleWarnSpy.mockRestore();
61
+ consoleLogSpy.mockRestore();
62
+ if (server) {
63
+ server.stopServer();
64
+ }
65
+ });
66
+
67
+ it('should create a server with default options', () => {
68
+ server = createServer({ transport });
69
+ expect(server).toBeInstanceOf(DedicatedServer);
70
+ });
71
+
72
+ it('should initialize WebSocketServer on start', async () => {
73
+ await server.startServer('test_map');
74
+ expect(transport.listenSpy).toHaveBeenCalledWith(27910);
75
+ });
76
+
77
+ it('should be able to stop', async () => {
78
+ await server.startServer('test_map');
79
+ server.stopServer();
80
+ expect(transport.closeSpy).toHaveBeenCalled();
81
+ });
82
+
83
+ it('should fail to start without map', async () => {
84
+ server = createServer({ transport });
85
+ await expect(server.startServer()).rejects.toThrow('No map specified');
86
+ });
87
+
88
+ it('should kick player', () => {
89
+ server = createServer({ transport });
90
+ // Just verify method exists and runs safely on empty server
91
+ expect(() => server.kickPlayer(0)).not.toThrow();
92
+ });
93
+
94
+ it('should change map', async () => {
95
+ server = createServer({ transport });
96
+ await server.startServer('maps/q2dm1.bsp');
97
+ await expect(server.changeMap('maps/q2dm2.bsp')).resolves.not.toThrow();
98
+ });
99
+ });
@@ -0,0 +1,142 @@
1
+
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { DedicatedServer } from '../../src/dedicated.js';
4
+ import { ClientState } from '../../src/client.js';
5
+ import { createMockTransport, MockTransport, createMockServerClient, createMockNetDriver, createMockGameExports } from '@quake2ts/test-utils';
6
+ import { NetDriver } from '@quake2ts/shared';
7
+
8
+ // Mock dependencies
9
+ vi.mock('node:fs/promises', async () => {
10
+ return {
11
+ default: {
12
+ readFile: vi.fn().mockResolvedValue(Buffer.from(''))
13
+ },
14
+ readFile: vi.fn().mockResolvedValue(Buffer.from(''))
15
+ };
16
+ });
17
+
18
+ import { createGame } from '@quake2ts/game';
19
+
20
+ // Mock game creation
21
+ vi.mock('@quake2ts/game', async () => {
22
+ const actual = await vi.importActual('@quake2ts/game');
23
+ return {
24
+ ...actual,
25
+ createGame: vi.fn(),
26
+ createPlayerInventory: vi.fn(),
27
+ createPlayerWeaponStates: vi.fn()
28
+ };
29
+ });
30
+
31
+ // Access private properties for testing
32
+ function getPrivate(obj: any, key: string) {
33
+ return obj[key];
34
+ }
35
+
36
+ describe('DedicatedServer Timeout', () => {
37
+ let server: DedicatedServer;
38
+ let consoleLogSpy: any;
39
+ let consoleWarnSpy: any;
40
+ let transport: MockTransport;
41
+
42
+ beforeEach(async () => {
43
+ vi.useFakeTimers({
44
+ toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date']
45
+ });
46
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
47
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
48
+
49
+ // Setup mock game return value here, where we can access createMockGameExports
50
+ (createGame as vi.Mock).mockReturnValue(createMockGameExports({
51
+ clientBegin: vi.fn().mockReturnValue({ index: 1, origin: {x:0,y:0,z:0} } as any),
52
+ clientConnect: vi.fn().mockReturnValue(true)
53
+ }));
54
+
55
+ transport = createMockTransport();
56
+ server = new DedicatedServer({ port: 27910, transport });
57
+ await server.startServer('maps/test.bsp');
58
+ });
59
+
60
+ afterEach(() => {
61
+ server.stopServer();
62
+ vi.clearAllMocks();
63
+ consoleLogSpy.mockRestore();
64
+ consoleWarnSpy.mockRestore();
65
+ vi.useRealTimers();
66
+ });
67
+
68
+ it('should disconnect a client after timeout', () => {
69
+ // Access svs.clients to manually inject a client
70
+ const svs = getPrivate(server, 'svs');
71
+ const sv = getPrivate(server, 'sv');
72
+
73
+ // Mock a connected client
74
+ const mockDriver = createMockNetDriver();
75
+
76
+ const clientIndex = 0;
77
+ const client = createMockServerClient(clientIndex, {
78
+ net: mockDriver,
79
+ lastMessage: sv.frame, // Last message was NOW
80
+ lastConnect: Date.now(),
81
+ name: 'TestClient'
82
+ });
83
+
84
+ svs.clients[clientIndex] = client;
85
+
86
+ // Set up cleanup spy
87
+ const dropClientSpy = vi.spyOn(server as any, 'dropClient');
88
+
89
+ // Advance 29 seconds (290 frames)
90
+ for(let i=0; i<290; i++) {
91
+ vi.runOnlyPendingTimers();
92
+ }
93
+
94
+ // Verify still connected
95
+ expect(svs.clients[clientIndex]).not.toBeNull();
96
+ expect(dropClientSpy).not.toHaveBeenCalled();
97
+ expect(mockDriver.disconnect).not.toHaveBeenCalled();
98
+
99
+ // Advance another 20 frames (2 seconds) -> Total 31 seconds
100
+ for(let i=0; i<20; i++) {
101
+ vi.runOnlyPendingTimers();
102
+ }
103
+
104
+ // Now diff is 310 > 300. Should disconnect.
105
+ expect(dropClientSpy).toHaveBeenCalledWith(client);
106
+ expect(mockDriver.disconnect).toHaveBeenCalled();
107
+ });
108
+
109
+ it('should NOT disconnect a client if they send messages', () => {
110
+ const svs = getPrivate(server, 'svs');
111
+ const sv = getPrivate(server, 'sv');
112
+
113
+ const mockDriver = createMockNetDriver();
114
+
115
+ const clientIndex = 0;
116
+ const client = createMockServerClient(clientIndex, {
117
+ net: mockDriver,
118
+ lastMessage: sv.frame,
119
+ lastConnect: Date.now(),
120
+ name: 'TestClient'
121
+ });
122
+
123
+ svs.clients[clientIndex] = client;
124
+ const dropClientSpy = vi.spyOn(server as any, 'dropClient');
125
+
126
+ // Advance 15 seconds
127
+ for(let i=0; i<150; i++) {
128
+ vi.runOnlyPendingTimers();
129
+ }
130
+
131
+ // Simulate packet received -> updates lastMessage
132
+ client.lastMessage = sv.frame;
133
+
134
+ // Advance another 16 seconds
135
+ for(let i=0; i<160; i++) {
136
+ vi.runOnlyPendingTimers();
137
+ }
138
+
139
+ // Total time 31 seconds, but lastMessage was 16 seconds ago.
140
+ expect(dropClientSpy).not.toHaveBeenCalled();
141
+ });
142
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["src/**/*"],
4
+ "compilerOptions": {
5
+ "outDir": "dist",
6
+ "composite": true,
7
+ "rootDir": "src"
8
+ }
9
+ }