@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
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // Export net implementations
2
+ export * from './net/nodeWsDriver.js';
3
+ export * from './dedicated.js';
4
+ export * from './client.js';
5
+ export * from './protocol.js';
6
+ export { ServerOptions, createServer } from './dedicated.js';
7
+ export { NetworkTransport } from './transport.js';
8
+ export * from './server.js';
@@ -0,0 +1,129 @@
1
+ import { NetDriver } from '@quake2ts/shared';
2
+ import WebSocket from 'ws';
3
+
4
+ export class WebSocketNetDriver implements NetDriver {
5
+ private socket: WebSocket | null = null;
6
+ private messageCallback: ((data: Uint8Array) => void) | null = null;
7
+ private closeCallback: (() => void) | null = null;
8
+ private errorCallback: ((error: Error) => void) | null = null;
9
+
10
+ async connect(url: string): Promise<void> {
11
+ return new Promise((resolve, reject) => {
12
+ try {
13
+ this.socket = new WebSocket(url);
14
+ this.socket.binaryType = 'arraybuffer';
15
+
16
+ this.socket.onopen = () => {
17
+ resolve();
18
+ };
19
+
20
+ this.socket.onerror = (event) => {
21
+ const error = new Error('WebSocket connection error ' + event.message);
22
+ if (this.errorCallback) {
23
+ this.errorCallback(error);
24
+ }
25
+ reject(error);
26
+ };
27
+
28
+ this.socket.onclose = () => {
29
+ if (this.closeCallback) {
30
+ this.closeCallback();
31
+ }
32
+ this.socket = null;
33
+ };
34
+
35
+ this.socket.onmessage = (event) => {
36
+ if (this.messageCallback) {
37
+ if (event.data instanceof ArrayBuffer) {
38
+ this.messageCallback(new Uint8Array(event.data));
39
+ } else if (Buffer.isBuffer(event.data)) {
40
+ // ws in Node might return Buffer
41
+ this.messageCallback(new Uint8Array(event.data));
42
+ } else if (Array.isArray(event.data)) {
43
+ // Buffer[]
44
+ const totalLength = event.data.reduce((acc, buf) => acc + buf.length, 0);
45
+ const result = new Uint8Array(totalLength);
46
+ let offset = 0;
47
+ for (const buf of event.data) {
48
+ result.set(buf, offset);
49
+ offset += buf.length;
50
+ }
51
+ this.messageCallback(result);
52
+ } else {
53
+ console.warn('Received non-binary message from server', typeof event.data);
54
+ }
55
+ }
56
+ };
57
+
58
+ } catch (e) {
59
+ reject(e);
60
+ }
61
+ });
62
+ }
63
+
64
+ // Method to attach an existing socket (server-side incoming connection)
65
+ attach(socket: WebSocket) {
66
+ this.socket = socket;
67
+ this.socket.binaryType = 'arraybuffer';
68
+
69
+ this.socket.onclose = () => {
70
+ if (this.closeCallback) this.closeCallback();
71
+ this.socket = null;
72
+ };
73
+
74
+ this.socket.onerror = (event) => {
75
+ if (this.errorCallback) this.errorCallback(new Error(event.message));
76
+ };
77
+
78
+ this.socket.onmessage = (event) => {
79
+ if (this.messageCallback) {
80
+ if (event.data instanceof ArrayBuffer) {
81
+ this.messageCallback(new Uint8Array(event.data));
82
+ } else if (Buffer.isBuffer(event.data)) {
83
+ this.messageCallback(new Uint8Array(event.data));
84
+ } else if (Array.isArray(event.data)) { // ws specific
85
+ // handle fragmentation if necessary, usually it's Buffer[]
86
+ const totalLength = event.data.reduce((acc: number, buf: Buffer) => acc + buf.length, 0);
87
+ const result = new Uint8Array(totalLength);
88
+ let offset = 0;
89
+ for (const buf of event.data) {
90
+ result.set(buf, offset);
91
+ offset += buf.length;
92
+ }
93
+ this.messageCallback(result);
94
+ }
95
+ }
96
+ };
97
+ }
98
+
99
+ disconnect(): void {
100
+ if (this.socket) {
101
+ this.socket.close();
102
+ this.socket = null;
103
+ }
104
+ }
105
+
106
+ send(data: Uint8Array): void {
107
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
108
+ this.socket.send(data);
109
+ } else {
110
+ console.warn('Attempted to send data on closed or connecting socket');
111
+ }
112
+ }
113
+
114
+ onMessage(callback: (data: Uint8Array) => void): void {
115
+ this.messageCallback = callback;
116
+ }
117
+
118
+ onClose(callback: () => void): void {
119
+ this.closeCallback = callback;
120
+ }
121
+
122
+ onError(callback: (error: Error) => void): void {
123
+ this.errorCallback = callback;
124
+ }
125
+
126
+ isConnected(): boolean {
127
+ return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
128
+ }
129
+ }
@@ -0,0 +1,2 @@
1
+ export { writePlayerState } from '@quake2ts/shared';
2
+ export type { ProtocolPlayerState } from '@quake2ts/shared';
@@ -0,0 +1,185 @@
1
+ import { BinaryWriter, ServerCommand, TempEntity, Vec3 } from '@quake2ts/shared';
2
+ import { Entity } from '@quake2ts/game';
3
+
4
+ /**
5
+ * Writes a server command and its arguments to a BinaryWriter.
6
+ * This handles the serialization of generic arguments passed to multicast/unicast
7
+ * into the specific binary format expected by the protocol.
8
+ */
9
+ export function writeServerCommand(writer: BinaryWriter, event: ServerCommand, ...args: any[]): void {
10
+ writer.writeByte(event);
11
+
12
+ switch (event) {
13
+ case ServerCommand.print: {
14
+ // args: [level: number, text: string]
15
+ const level = args[0] as number;
16
+ const text = args[1] as string;
17
+ writer.writeByte(level);
18
+ writer.writeString(text);
19
+ break;
20
+ }
21
+
22
+ case ServerCommand.centerprint: {
23
+ // args: [text: string]
24
+ const text = args[0] as string;
25
+ writer.writeString(text);
26
+ break;
27
+ }
28
+
29
+ case ServerCommand.stufftext: {
30
+ // args: [text: string]
31
+ const text = args[0] as string;
32
+ writer.writeString(text);
33
+ break;
34
+ }
35
+
36
+ case ServerCommand.sound: {
37
+ // args: [flags, soundNum, volume?, attenuation?, offset?, ent?, pos?]
38
+ const flags = args[0] as number;
39
+ const soundNum = args[1] as number;
40
+ const volume = args[2] as number | undefined;
41
+ const attenuation = args[3] as number | undefined;
42
+ const offset = args[4] as number | undefined;
43
+ const ent = args[5] as number | undefined;
44
+ const pos = args[6] as Vec3 | undefined;
45
+
46
+ writer.writeByte(flags);
47
+ writer.writeByte(soundNum);
48
+
49
+ if (flags & 1) { // SND_VOLUME
50
+ writer.writeByte(volume || 0);
51
+ }
52
+ if (flags & 2) { // SND_ATTENUATION
53
+ writer.writeByte(attenuation || 0);
54
+ }
55
+ if (flags & 16) { // SND_OFFSET
56
+ writer.writeByte(offset || 0);
57
+ }
58
+ if (flags & 8) { // SND_ENT
59
+ writer.writeShort(ent || 0);
60
+ }
61
+ if (flags & 4) { // SND_POS
62
+ if (pos) {
63
+ writer.writePos(pos);
64
+ } else {
65
+ writer.writePos({x:0, y:0, z:0});
66
+ }
67
+ }
68
+ break;
69
+ }
70
+
71
+ case ServerCommand.muzzleflash: {
72
+ // args: [entityIndex: number, flashType: number]
73
+ const entIndex = args[0] as number;
74
+ const flashType = args[1] as number;
75
+ writer.writeShort(entIndex);
76
+ writer.writeByte(flashType);
77
+ break;
78
+ }
79
+
80
+ case ServerCommand.temp_entity: {
81
+ // args: [type: TempEntity, ...params]
82
+ const type = args[0] as TempEntity;
83
+ writer.writeByte(type);
84
+ writeTempEntity(writer, type, args.slice(1));
85
+ break;
86
+ }
87
+
88
+ default:
89
+ console.warn(`writeServerCommand: Unhandled command ${event}`);
90
+ break;
91
+ }
92
+ }
93
+
94
+ function writeTempEntity(writer: BinaryWriter, type: TempEntity, args: any[]): void {
95
+ switch (type) {
96
+ case TempEntity.ROCKET_EXPLOSION:
97
+ case TempEntity.GRENADE_EXPLOSION:
98
+ case TempEntity.EXPLOSION1:
99
+ case TempEntity.EXPLOSION2:
100
+ case TempEntity.ROCKET_EXPLOSION_WATER:
101
+ case TempEntity.GRENADE_EXPLOSION_WATER:
102
+ case TempEntity.BFG_EXPLOSION:
103
+ case TempEntity.BFG_BIGEXPLOSION:
104
+ case TempEntity.PLASMA_EXPLOSION:
105
+ case TempEntity.PLAIN_EXPLOSION:
106
+ case TempEntity.TRACKER_EXPLOSION:
107
+ case TempEntity.EXPLOSION1_BIG:
108
+ case TempEntity.EXPLOSION1_NP:
109
+ case TempEntity.EXPLOSION1_NL:
110
+ case TempEntity.EXPLOSION2_NL:
111
+ case TempEntity.BERSERK_SLAM:
112
+ // Format: [pos]
113
+ writer.writePos(args[0] as Vec3);
114
+ break;
115
+
116
+ case TempEntity.BLASTER:
117
+ case TempEntity.FLECHETTE:
118
+ // Format: [pos, dir]
119
+ writer.writePos(args[0] as Vec3);
120
+ writer.writeDir(args[1] as Vec3);
121
+ break;
122
+
123
+ case TempEntity.RAILTRAIL:
124
+ case TempEntity.DEBUGTRAIL:
125
+ case TempEntity.BUBBLETRAIL:
126
+ case TempEntity.BUBBLETRAIL2:
127
+ case TempEntity.BFG_LASER:
128
+ case TempEntity.LIGHTNING_BEAM:
129
+ case TempEntity.LIGHTNING:
130
+ // Format: [start, end]
131
+ writer.writePos(args[0] as Vec3);
132
+ writer.writePos(args[1] as Vec3);
133
+ break;
134
+
135
+ case TempEntity.LASER_SPARKS:
136
+ case TempEntity.WELDING_SPARKS:
137
+ case TempEntity.TUNNEL_SPARKS:
138
+ case TempEntity.ELECTRIC_SPARKS:
139
+ case TempEntity.HEATBEAM_SPARKS:
140
+ case TempEntity.HEATBEAM_STEAM:
141
+ case TempEntity.STEAM:
142
+ // Format: [count, pos, dir, color?]
143
+ // Q2: writeByte(count), writePos(start), writeDir(normal), writeByte(skin/color)
144
+ writer.writeByte(args[0] as number);
145
+ writer.writePos(args[1] as Vec3);
146
+ writer.writeDir(args[2] as Vec3);
147
+ writer.writeByte(args[3] as number || 0);
148
+ break;
149
+
150
+ case TempEntity.PARASITE_ATTACK:
151
+ case TempEntity.MEDIC_CABLE_ATTACK:
152
+ // Format: [entIndex, start, end]
153
+ // args[0] is Entity usually
154
+ const ent = args[0] as Entity;
155
+ writer.writeShort(ent ? ent.index : 0);
156
+ writer.writePos(args[1] as Vec3);
157
+ writer.writePos(args[2] as Vec3);
158
+ break;
159
+
160
+ case TempEntity.GUNSHOT:
161
+ case TempEntity.BLOOD:
162
+ case TempEntity.SPARKS:
163
+ case TempEntity.BULLET_SPARKS:
164
+ case TempEntity.SCREEN_SPARKS:
165
+ case TempEntity.SHIELD_SPARKS:
166
+ // Format: [pos, dir]
167
+ writer.writePos(args[0] as Vec3);
168
+ writer.writeDir(args[1] as Vec3);
169
+ break;
170
+
171
+ case TempEntity.SPLASH:
172
+ case TempEntity.POWER_SPLASH:
173
+ case TempEntity.WIDOWSPLASH:
174
+ // Format: [count, pos, dir, color]
175
+ writer.writeByte(args[0] as number);
176
+ writer.writePos(args[1] as Vec3);
177
+ writer.writeDir(args[2] as Vec3);
178
+ writer.writeByte(args[3] as number || 0);
179
+ break;
180
+
181
+ default:
182
+ console.warn(`writeTempEntity: Unhandled TempEntity ${type}`);
183
+ break;
184
+ }
185
+ }
@@ -0,0 +1,91 @@
1
+ import { BinaryStream, ClientCommand, UserCommand } from '@quake2ts/shared';
2
+
3
+ export interface ClientMessageHandler {
4
+ onMove(checksum: number, lastFrame: number, userCmd: UserCommand): void;
5
+ onUserInfo(info: string): void;
6
+ onStringCmd(cmd: string): void;
7
+ onNop(): void;
8
+ onBad(): void;
9
+ }
10
+
11
+ export class ClientMessageParser {
12
+ private stream: BinaryStream;
13
+ private handler: ClientMessageHandler;
14
+
15
+ constructor(stream: BinaryStream, handler: ClientMessageHandler) {
16
+ this.stream = stream;
17
+ this.handler = handler;
18
+ }
19
+
20
+ public parseMessage(): void {
21
+ while (this.stream.hasMore()) {
22
+ const cmdId = this.stream.readByte();
23
+ if (cmdId === -1) break;
24
+
25
+ switch (cmdId) {
26
+ case ClientCommand.move:
27
+ this.parseMove();
28
+ break;
29
+ case ClientCommand.userinfo:
30
+ this.parseUserInfo();
31
+ break;
32
+ case ClientCommand.stringcmd:
33
+ this.parseStringCmd();
34
+ break;
35
+ case ClientCommand.nop:
36
+ this.handler.onNop();
37
+ break;
38
+ default:
39
+ console.warn(`Unknown client command: ${cmdId}`);
40
+ this.handler.onBad();
41
+ return;
42
+ }
43
+ }
44
+ }
45
+
46
+ private parseMove(): void {
47
+ const checksum = this.stream.readByte();
48
+ const lastFrame = this.stream.readLong();
49
+
50
+ // Read UserCmd
51
+ // TODO: support delta compression if needed (Q2 uses delta compression for usercmds)
52
+ // For now, assume full UserCmd or implement basic read
53
+
54
+ const msec = this.stream.readByte();
55
+ const buttons = this.stream.readByte();
56
+ const angles = {
57
+ x: this.stream.readShort() * (360.0 / 65536.0),
58
+ y: this.stream.readShort() * (360.0 / 65536.0),
59
+ z: this.stream.readShort() * (360.0 / 65536.0),
60
+ };
61
+ const forwardmove = this.stream.readShort();
62
+ const sidemove = this.stream.readShort();
63
+ const upmove = this.stream.readShort();
64
+ const impulse = this.stream.readByte();
65
+ const lightlevel = this.stream.readByte(); // Used for light-based stealth, usually ignored by server logic except for stats
66
+
67
+ const userCmd: UserCommand = {
68
+ msec,
69
+ buttons,
70
+ angles,
71
+ forwardmove,
72
+ sidemove,
73
+ upmove,
74
+ impulse,
75
+ lightlevel,
76
+ sequence: 0 // Server doesn't read sequence from packet body in standard protocol, it tracks it
77
+ };
78
+
79
+ this.handler.onMove(checksum, lastFrame, userCmd);
80
+ }
81
+
82
+ private parseUserInfo(): void {
83
+ const info = this.stream.readString();
84
+ this.handler.onUserInfo(info);
85
+ }
86
+
87
+ private parseStringCmd(): void {
88
+ const cmd = this.stream.readString();
89
+ this.handler.onStringCmd(cmd);
90
+ }
91
+ }
package/src/server.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { CollisionModel } from "@quake2ts/shared";
2
+ import { EntityState, MAX_CONFIGSTRINGS, MAX_EDICTS, MAX_CHALLENGES } from "@quake2ts/shared";
3
+ import { Client } from "./client.js";
4
+
5
+ /**
6
+ * ServerState corresponds to server_state_t in the original source.
7
+ */
8
+ export enum ServerState {
9
+ Dead, // no map loaded
10
+ Loading, // spawning level edicts
11
+ Game, // actively running
12
+ Cinematic,
13
+ Demo,
14
+ Pic
15
+ }
16
+
17
+ export interface Challenge {
18
+ adr: string; // IP address
19
+ challenge: number;
20
+ time: number;
21
+ }
22
+
23
+ /**
24
+ * ServerStatic holds the state that is constant across server restarts.
25
+ * Corresponds to server_static_t in the original source.
26
+ */
27
+ export interface ServerStatic {
28
+ initialized: boolean;
29
+ realTime: number; // always increasing
30
+
31
+ mapCmd: string; // ie: *intro.cin+base
32
+
33
+ spawnCount: number; // incremented each server start
34
+
35
+ clients: (Client | null)[];
36
+
37
+ // In original this is: entity_state_t *client_entities;
38
+ // We might need a different approach in TS, but keeping the concept:
39
+ // This buffer holds entity states for all clients history.
40
+ // For now we might store it on the client directly or here.
41
+ // Original: client_entities[maxclients*UPDATE_BACKUP*MAX_PACKET_ENTITIES]
42
+
43
+ lastHeartbeat: number;
44
+
45
+ challenges: Challenge[];
46
+
47
+ // Demo recording stuff
48
+ demoFile?: any; // File handle
49
+ }
50
+
51
+ /**
52
+ * Server holds the state for the current running server instance.
53
+ * Corresponds to server_t in the original source.
54
+ */
55
+ export interface Server {
56
+ state: ServerState;
57
+
58
+ attractLoop: boolean;
59
+ loadGame: boolean;
60
+
61
+ startTime: number; // Added back as it was used in dedicated.ts and is useful
62
+ time: number; // sv.framenum * 100 msec
63
+ frame: number;
64
+
65
+ name: string; // map name
66
+
67
+ // Models are handled by AssetManager in engine usually,
68
+ // but server needs collision models.
69
+ collisionModel: CollisionModel | null;
70
+
71
+ configStrings: string[]; // [MAX_CONFIGSTRINGS]
72
+ baselines: (EntityState | null)[]; // [MAX_EDICTS]
73
+
74
+ // Multicast buffer
75
+ multicastBuf: Uint8Array;
76
+ }
@@ -0,0 +1,8 @@
1
+ import { NetDriver } from '@quake2ts/shared';
2
+
3
+ export interface NetworkTransport {
4
+ listen(port: number): Promise<void>;
5
+ close(): void;
6
+ onConnection(callback: (driver: NetDriver, info?: any) => void): void;
7
+ onError(callback: (error: Error) => void): void;
8
+ }
@@ -0,0 +1,42 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { WebSocketNetDriver } from '../net/nodeWsDriver.js';
3
+ import { NetworkTransport } from '../transport.js';
4
+ import { NetDriver } from '@quake2ts/shared';
5
+
6
+ export class WebSocketTransport implements NetworkTransport {
7
+ private wss: WebSocketServer | null = null;
8
+ private connectionCallback: ((driver: NetDriver, info?: any) => void) | null = null;
9
+ private errorCallback: ((error: Error) => void) | null = null;
10
+
11
+ async listen(port: number): Promise<void> {
12
+ return new Promise((resolve) => {
13
+ this.wss = new WebSocketServer({ port });
14
+ this.wss.on('listening', () => resolve());
15
+ this.wss.on('connection', (ws, req) => {
16
+ const driver = new WebSocketNetDriver();
17
+ driver.attach(ws);
18
+ if (this.connectionCallback) {
19
+ this.connectionCallback(driver, req);
20
+ }
21
+ });
22
+ this.wss.on('error', (err) => {
23
+ if (this.errorCallback) this.errorCallback(err);
24
+ });
25
+ });
26
+ }
27
+
28
+ close() {
29
+ if (this.wss) {
30
+ this.wss.close();
31
+ this.wss = null;
32
+ }
33
+ }
34
+
35
+ onConnection(callback: (driver: NetDriver, info?: any) => void) {
36
+ this.connectionCallback = callback;
37
+ }
38
+
39
+ onError(callback: (error: Error) => void) {
40
+ this.errorCallback = callback;
41
+ }
42
+ }
package/test.bsp ADDED
Binary file
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createClient, ClientState } from '../src/client.js';
3
+ import { NetChan } from '@quake2ts/shared';
4
+ import { createMockNetDriver } from '@quake2ts/test-utils';
5
+
6
+ describe('Server Client', () => {
7
+ it('should initialize with NetChan', () => {
8
+ const mockNetDriver = createMockNetDriver();
9
+
10
+ const client = createClient(0, mockNetDriver);
11
+
12
+ expect(client).toBeDefined();
13
+ expect(client.netchan).toBeDefined();
14
+ expect(client.netchan).toBeInstanceOf(NetChan);
15
+ expect(client.state).toBe(ClientState.Connected);
16
+
17
+ // Verify qport is set (it's random, but should be a number)
18
+ expect(typeof client.netchan.qport).toBe('number');
19
+ });
20
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { DedicatedServer } from '../src/dedicated.js';
3
+ import { createGame, GameExports } from '@quake2ts/game';
4
+ import { ClientState } from '../src/client.js';
5
+ import { createMockTransport, MockTransport, createMockServerClient, createMockNetDriver, createMockGameExports, createMockConnection } from '@quake2ts/test-utils';
6
+
7
+ // Mock dependencies
8
+ // ws mock removed
9
+ vi.mock('node:fs/promises', () => ({
10
+ default: {
11
+ readFile: vi.fn().mockResolvedValue(Buffer.from([0])),
12
+ },
13
+ }));
14
+ vi.mock('@quake2ts/engine', () => ({
15
+ parseBsp: vi.fn().mockReturnValue({}),
16
+ }));
17
+ vi.mock('@quake2ts/game', () => ({
18
+ createGame: vi.fn(),
19
+ createPlayerInventory: vi.fn(),
20
+ createPlayerWeaponStates: vi.fn(),
21
+ }));
22
+
23
+ describe('DedicatedServer Connection Flow', () => {
24
+ let server: DedicatedServer;
25
+ let mockGame: GameExports;
26
+ let sentMessages: Uint8Array[] = [];
27
+ let consoleLogSpy: any;
28
+ let consoleWarnSpy: any;
29
+ let transport: MockTransport;
30
+
31
+ beforeEach(async () => {
32
+ sentMessages = [];
33
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
34
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
35
+
36
+ mockGame = createMockGameExports({
37
+ clientConnect: vi.fn().mockReturnValue(true),
38
+ clientBegin: vi.fn(() => ({ id: 1, classname: 'player' } as any)),
39
+ });
40
+
41
+ (createGame as vi.Mock).mockReturnValue(mockGame);
42
+
43
+ transport = createMockTransport();
44
+ server = new DedicatedServer({ transport });
45
+ await server.startServer('test.bsp');
46
+ });
47
+
48
+ afterEach(() => {
49
+ server.stopServer();
50
+ vi.clearAllMocks();
51
+ consoleLogSpy.mockRestore();
52
+ consoleWarnSpy.mockRestore();
53
+ });
54
+
55
+ it('should handle "connect" command', () => {
56
+ // 1. Setup a client using proper factory
57
+ const mockNet = createMockNetDriver({
58
+ send: vi.fn((data) => {
59
+ sentMessages.push(data);
60
+ })
61
+ });
62
+
63
+ // Use createMockConnection for more semantic setup, overriding with our mockNet
64
+ const client = createMockConnection(ClientState.Connected, {
65
+ net: mockNet,
66
+ edict: null, // Explicitly null edict as player hasn't entered game yet
67
+ index: 0
68
+ });
69
+
70
+ // Make sure transmit returns something so client.net.send is called with it
71
+ (client.netchan.transmit as any).mockReturnValue(new Uint8Array([1, 2, 3]));
72
+
73
+
74
+ // Inject client
75
+ // @ts-ignore
76
+ server.svs.clients[0] = client;
77
+
78
+ // 2. Simulate "connect" string command
79
+ // Use private access for unit testing
80
+ // @ts-ignore
81
+ server.handleStringCmd(client, 'connect \\name\\Player\\skin\\male/grunt');
82
+
83
+ // 3. Verify clientConnect was called
84
+ expect(mockGame.clientConnect).toHaveBeenCalledWith(null, '\\name\\Player\\skin\\male/grunt');
85
+
86
+ // 4. Verify response (ServerData)
87
+ // Check if any messages were sent
88
+ expect(sentMessages.length).toBeGreaterThan(0);
89
+
90
+ // Just verify that client.net.send was called.
91
+ expect(mockNet.send).toHaveBeenCalled();
92
+ });
93
+ });