@quake2ts/test-utils 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.
- package/README.md +454 -0
- package/dist/index.cjs +5432 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2150 -0
- package/dist/index.d.ts +2150 -0
- package/dist/index.js +5165 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/client/helpers/hud.ts +114 -0
- package/src/client/helpers/prediction.ts +136 -0
- package/src/client/helpers/view.ts +201 -0
- package/src/client/mocks/console.ts +75 -0
- package/src/client/mocks/download.ts +48 -0
- package/src/client/mocks/input.ts +246 -0
- package/src/client/mocks/network.ts +148 -0
- package/src/client/mocks/state.ts +148 -0
- package/src/e2e/network.ts +47 -0
- package/src/e2e/playwright.ts +90 -0
- package/src/e2e/visual.ts +172 -0
- package/src/engine/helpers/pipeline-test-template.ts +113 -0
- package/src/engine/helpers/webgpu-rendering.ts +251 -0
- package/src/engine/mocks/assets.ts +129 -0
- package/src/engine/mocks/audio.ts +152 -0
- package/src/engine/mocks/buffers.ts +88 -0
- package/src/engine/mocks/lighting.ts +64 -0
- package/src/engine/mocks/particles.ts +76 -0
- package/src/engine/mocks/renderer.ts +218 -0
- package/src/engine/mocks/webgl.ts +267 -0
- package/src/engine/mocks/webgpu.ts +262 -0
- package/src/engine/rendering.ts +103 -0
- package/src/game/factories.ts +204 -0
- package/src/game/helpers/physics.ts +171 -0
- package/src/game/helpers/save.ts +232 -0
- package/src/game/helpers.ts +310 -0
- package/src/game/mocks/ai.ts +67 -0
- package/src/game/mocks/combat.ts +61 -0
- package/src/game/mocks/items.ts +166 -0
- package/src/game/mocks.ts +105 -0
- package/src/index.ts +93 -0
- package/src/server/helpers/bandwidth.ts +127 -0
- package/src/server/helpers/multiplayer.ts +158 -0
- package/src/server/helpers/snapshot.ts +241 -0
- package/src/server/mockNetDriver.ts +106 -0
- package/src/server/mockTransport.ts +50 -0
- package/src/server/mocks/commands.ts +93 -0
- package/src/server/mocks/connection.ts +139 -0
- package/src/server/mocks/master.ts +97 -0
- package/src/server/mocks/physics.ts +32 -0
- package/src/server/mocks/state.ts +162 -0
- package/src/server/mocks/transport.ts +161 -0
- package/src/setup/audio.ts +118 -0
- package/src/setup/browser.ts +249 -0
- package/src/setup/canvas.ts +142 -0
- package/src/setup/node.ts +21 -0
- package/src/setup/storage.ts +60 -0
- package/src/setup/timing.ts +142 -0
- package/src/setup/webgl.ts +8 -0
- package/src/setup/webgpu.ts +113 -0
- package/src/shared/bsp.ts +145 -0
- package/src/shared/collision.ts +64 -0
- package/src/shared/factories.ts +88 -0
- package/src/shared/math.ts +65 -0
- package/src/shared/mocks.ts +243 -0
- package/src/shared/pak-loader.ts +45 -0
- package/src/visual/snapshots.ts +292 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { NetDriver } from '@quake2ts/shared';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
export interface MockNetDriverState {
|
|
5
|
+
connected: boolean;
|
|
6
|
+
messagesSent: Uint8Array[];
|
|
7
|
+
messageHandlers: ((data: Uint8Array) => void)[];
|
|
8
|
+
closeHandlers: (() => void)[];
|
|
9
|
+
errorHandlers: ((err: Error) => void)[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A mock implementation of NetDriver that provides testing hooks.
|
|
14
|
+
* Unlike the simple createMockNetDriver factory, this class maintains state
|
|
15
|
+
* and allows for stimulating events (receiving messages, triggering errors).
|
|
16
|
+
*/
|
|
17
|
+
export class MockNetDriver implements NetDriver {
|
|
18
|
+
public state: MockNetDriverState = {
|
|
19
|
+
connected: false,
|
|
20
|
+
messagesSent: [],
|
|
21
|
+
messageHandlers: [],
|
|
22
|
+
closeHandlers: [],
|
|
23
|
+
errorHandlers: []
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
public connectSpy = vi.fn().mockImplementation(async (url: string) => {
|
|
27
|
+
this.state.connected = true;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
public disconnectSpy = vi.fn().mockImplementation(() => {
|
|
31
|
+
this.state.connected = false;
|
|
32
|
+
this.state.closeHandlers.forEach(h => h());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
public sendSpy = vi.fn().mockImplementation((data: Uint8Array) => {
|
|
36
|
+
this.state.messagesSent.push(new Uint8Array(data));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
public connect(url: string): Promise<void> {
|
|
40
|
+
return this.connectSpy(url);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public disconnect(): void {
|
|
44
|
+
this.disconnectSpy();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public send(data: Uint8Array): void {
|
|
48
|
+
this.sendSpy(data);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public onMessage(cb: (data: Uint8Array) => void): void {
|
|
52
|
+
this.state.messageHandlers.push(cb);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public onClose(cb: () => void): void {
|
|
56
|
+
this.state.closeHandlers.push(cb);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public onError(cb: (err: Error) => void): void {
|
|
60
|
+
this.state.errorHandlers.push(cb);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public isConnected(): boolean {
|
|
64
|
+
return this.state.connected;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Test Helpers ---
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Simulate receiving a message from the network.
|
|
71
|
+
*/
|
|
72
|
+
public receiveMessage(data: Uint8Array): void {
|
|
73
|
+
this.state.messageHandlers.forEach(h => h(data));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Simulate a connection close event from the remote side.
|
|
78
|
+
*/
|
|
79
|
+
public simulateClose(): void {
|
|
80
|
+
this.state.connected = false;
|
|
81
|
+
this.state.closeHandlers.forEach(h => h());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Simulate a connection error.
|
|
86
|
+
*/
|
|
87
|
+
public simulateError(err: Error): void {
|
|
88
|
+
this.state.errorHandlers.forEach(h => h(err));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the last sent message.
|
|
93
|
+
*/
|
|
94
|
+
public getLastSentMessage(): Uint8Array | undefined {
|
|
95
|
+
return this.state.messagesSent.length > 0
|
|
96
|
+
? this.state.messagesSent[this.state.messagesSent.length - 1]
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clear recorded sent messages.
|
|
102
|
+
*/
|
|
103
|
+
public clearSentMessages(): void {
|
|
104
|
+
this.state.messagesSent = [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NetChan } from '@quake2ts/shared';
|
|
2
|
+
|
|
3
|
+
export interface RecordedPacket {
|
|
4
|
+
type: 'sent' | 'received';
|
|
5
|
+
timestamp: number;
|
|
6
|
+
data: Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MockNetworkTransport {
|
|
10
|
+
public netchan: NetChan;
|
|
11
|
+
public recordedPackets: RecordedPacket[] = [];
|
|
12
|
+
public sentPackets: Uint8Array[] = [];
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.netchan = new NetChan();
|
|
16
|
+
this.netchan.setup(1234, { type: 'loopback', port: 0 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
reset() {
|
|
20
|
+
this.recordedPackets = [];
|
|
21
|
+
this.sentPackets = [];
|
|
22
|
+
this.netchan.reset();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Simulate receiving data from the "server"
|
|
26
|
+
receive(data: Uint8Array) {
|
|
27
|
+
this.recordedPackets.push({
|
|
28
|
+
type: 'received',
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
data: new Uint8Array(data)
|
|
31
|
+
});
|
|
32
|
+
return this.netchan.process(data);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Simulate sending data (called by the client using this transport)
|
|
36
|
+
transmit(unreliableData?: Uint8Array) {
|
|
37
|
+
const packet = this.netchan.transmit(unreliableData);
|
|
38
|
+
this.recordedPackets.push({
|
|
39
|
+
type: 'sent',
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
data: new Uint8Array(packet)
|
|
42
|
+
});
|
|
43
|
+
this.sentPackets.push(new Uint8Array(packet));
|
|
44
|
+
return packet;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get lastSentPacket() {
|
|
48
|
+
return this.sentPackets.length > 0 ? this.sentPackets[this.sentPackets.length - 1] : undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { ServerCommand } from '@quake2ts/shared';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interface for a mocked server console that can execute commands.
|
|
6
|
+
* Reference: quake2/server/sv_main.c (SV_ExecuteUserCommand)
|
|
7
|
+
*/
|
|
8
|
+
export interface MockServerConsole {
|
|
9
|
+
exec(cmd: string): string;
|
|
10
|
+
print(text: string): void;
|
|
11
|
+
broadcast(text: string): void;
|
|
12
|
+
commandBuffer: string[];
|
|
13
|
+
outputBuffer: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a mock server console.
|
|
18
|
+
* @param overrides Optional overrides for the mock console.
|
|
19
|
+
*/
|
|
20
|
+
export function createMockServerConsole(overrides?: Partial<MockServerConsole>): MockServerConsole {
|
|
21
|
+
const outputBuffer: string[] = [];
|
|
22
|
+
const commandBuffer: string[] = [];
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
exec: vi.fn((cmd: string) => {
|
|
26
|
+
commandBuffer.push(cmd);
|
|
27
|
+
return `Executed: ${cmd}`;
|
|
28
|
+
}),
|
|
29
|
+
print: vi.fn((text: string) => {
|
|
30
|
+
outputBuffer.push(text);
|
|
31
|
+
}),
|
|
32
|
+
broadcast: vi.fn((text: string) => {
|
|
33
|
+
outputBuffer.push(`Broadcast: ${text}`);
|
|
34
|
+
}),
|
|
35
|
+
commandBuffer,
|
|
36
|
+
outputBuffer,
|
|
37
|
+
...overrides
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interface for a mocked RCON client.
|
|
43
|
+
* Reference: quake2/server/sv_user.c (SV_ExecuteUserCommand for RCON handling)
|
|
44
|
+
*/
|
|
45
|
+
export interface MockRConClient {
|
|
46
|
+
connect(address: string, port: number, password?: string): Promise<boolean>;
|
|
47
|
+
sendCommand(cmd: string): Promise<string>;
|
|
48
|
+
disconnect(): void;
|
|
49
|
+
connected: boolean;
|
|
50
|
+
lastCommand: string;
|
|
51
|
+
lastResponse: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a mock RCON client.
|
|
56
|
+
* @param password Optional password for the client to use.
|
|
57
|
+
*/
|
|
58
|
+
export function createMockRConClient(password: string = ''): MockRConClient {
|
|
59
|
+
return {
|
|
60
|
+
connected: false,
|
|
61
|
+
lastCommand: '',
|
|
62
|
+
lastResponse: '',
|
|
63
|
+
connect: vi.fn(async (address, port, pwd) => {
|
|
64
|
+
return pwd === password; // Simple mock: connect succeeds if password matches constructor password
|
|
65
|
+
}),
|
|
66
|
+
sendCommand: vi.fn(async (cmd) => {
|
|
67
|
+
return `RCON Response for: ${cmd}`;
|
|
68
|
+
}),
|
|
69
|
+
disconnect: vi.fn(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Simulates a command execution on the server.
|
|
75
|
+
* @param server The mock server instance (assumed to have some command handling capability).
|
|
76
|
+
* @param command The command string to execute.
|
|
77
|
+
* @returns The output of the command.
|
|
78
|
+
*/
|
|
79
|
+
export function simulateServerCommand(server: any, command: string): string {
|
|
80
|
+
// This assumes the server has a method to execute commands, or we simulate the effect.
|
|
81
|
+
// In a real integration, this might interact with the server's command buffer.
|
|
82
|
+
if (server.executeBuffer) {
|
|
83
|
+
server.executeBuffer(command);
|
|
84
|
+
return "Executed";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback for mocked servers
|
|
88
|
+
if (server.exec) {
|
|
89
|
+
return server.exec(command);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return "Unknown command handler";
|
|
93
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Client, ClientState } from '@quake2ts/server';
|
|
2
|
+
import { createMockServerClient } from './state.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interface representing connection state for testing.
|
|
6
|
+
*/
|
|
7
|
+
export interface Connection {
|
|
8
|
+
state: ClientState;
|
|
9
|
+
address: string;
|
|
10
|
+
challenge: number;
|
|
11
|
+
userInfo: UserInfo;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Stages of the client connection handshake.
|
|
16
|
+
*/
|
|
17
|
+
export enum HandshakeStage {
|
|
18
|
+
None = 0,
|
|
19
|
+
Challenge = 1,
|
|
20
|
+
Connect = 2,
|
|
21
|
+
Info = 3,
|
|
22
|
+
Active = 4
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Interface representing a handshake state.
|
|
27
|
+
*/
|
|
28
|
+
export interface Handshake {
|
|
29
|
+
stage: HandshakeStage;
|
|
30
|
+
clientNum: number;
|
|
31
|
+
challenge: number;
|
|
32
|
+
qport: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Interface for UserInfo structure.
|
|
37
|
+
*/
|
|
38
|
+
export interface UserInfo {
|
|
39
|
+
name: string;
|
|
40
|
+
skin: string;
|
|
41
|
+
model: string;
|
|
42
|
+
fov: number;
|
|
43
|
+
hand: number;
|
|
44
|
+
rate: number;
|
|
45
|
+
msg: number;
|
|
46
|
+
spectator?: number;
|
|
47
|
+
[key: string]: string | number | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Helper to serialize UserInfo to Quake 2 info string format.
|
|
52
|
+
* Format: \key\value\key2\value2
|
|
53
|
+
*/
|
|
54
|
+
export function serializeUserInfo(info: UserInfo): string {
|
|
55
|
+
return Object.entries(info).reduce((acc, [key, value]) => {
|
|
56
|
+
if (value !== undefined) {
|
|
57
|
+
return `${acc}\\${key}\\${value}`;
|
|
58
|
+
}
|
|
59
|
+
return acc;
|
|
60
|
+
}, '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a mock UserInfo object.
|
|
65
|
+
* @param overrides Optional overrides for the user info.
|
|
66
|
+
*/
|
|
67
|
+
export function createMockUserInfo(overrides?: Partial<UserInfo>): UserInfo {
|
|
68
|
+
return {
|
|
69
|
+
name: 'Player',
|
|
70
|
+
skin: 'male/grunt',
|
|
71
|
+
model: 'male/grunt',
|
|
72
|
+
fov: 90,
|
|
73
|
+
hand: 0,
|
|
74
|
+
rate: 25000,
|
|
75
|
+
msg: 1,
|
|
76
|
+
...overrides
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates a mock connection object (Client) with specific state.
|
|
82
|
+
* @param state The client state (default: Connected).
|
|
83
|
+
* @param overrides Optional overrides for the client.
|
|
84
|
+
*/
|
|
85
|
+
export function createMockConnection(state: ClientState = ClientState.Connected, overrides?: Partial<Client>): Client {
|
|
86
|
+
return createMockServerClient(0, {
|
|
87
|
+
state,
|
|
88
|
+
userInfo: serializeUserInfo(createMockUserInfo()),
|
|
89
|
+
challenge: Math.floor(Math.random() * 100000),
|
|
90
|
+
...overrides
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a mock handshake object.
|
|
96
|
+
* @param stage The stage of the handshake (default: None).
|
|
97
|
+
*/
|
|
98
|
+
export function createMockHandshake(stage: HandshakeStage = HandshakeStage.None): Handshake {
|
|
99
|
+
return {
|
|
100
|
+
stage,
|
|
101
|
+
clientNum: -1,
|
|
102
|
+
challenge: 0,
|
|
103
|
+
qport: Math.floor(Math.random() * 65536)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Simulates a handshake between a mock client and server.
|
|
109
|
+
* Note: This is a high-level simulation helper.
|
|
110
|
+
* @param client The mock client connection.
|
|
111
|
+
* @param server The mock server instance.
|
|
112
|
+
* @returns Promise that resolves to true if handshake succeeded.
|
|
113
|
+
*/
|
|
114
|
+
export async function simulateHandshake(client: Client, server: any): Promise<boolean> {
|
|
115
|
+
// 1. Get Challenge
|
|
116
|
+
// In a real scenario, client sends getchallenge, server responds with challenge
|
|
117
|
+
const challenge = Math.floor(Math.random() * 100000);
|
|
118
|
+
client.challenge = challenge;
|
|
119
|
+
|
|
120
|
+
// 2. Connect
|
|
121
|
+
// Client sends connect command with qport, challenge, userinfo
|
|
122
|
+
// Server assigns client slot
|
|
123
|
+
if (server && server.clients) {
|
|
124
|
+
// rudimentary check if server has space
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Update state to Connected
|
|
128
|
+
client.state = ClientState.Connected;
|
|
129
|
+
|
|
130
|
+
// 4. Send configstrings (usually handled by server)
|
|
131
|
+
|
|
132
|
+
// 5. Client enters game (prespawn)
|
|
133
|
+
// Server sends stuff
|
|
134
|
+
|
|
135
|
+
// 6. Client ready
|
|
136
|
+
client.state = ClientState.Active;
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Client } from '@quake2ts/server';
|
|
2
|
+
|
|
3
|
+
export interface MasterServer {
|
|
4
|
+
registerServer(info: ServerInfo): Promise<boolean>;
|
|
5
|
+
heartbeat(serverAddress: string): Promise<boolean>;
|
|
6
|
+
getServerList(filter?: ServerListFilter): Promise<ServerInfo[]>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ServerInfo {
|
|
10
|
+
address: string;
|
|
11
|
+
name: string;
|
|
12
|
+
map: string;
|
|
13
|
+
players: number;
|
|
14
|
+
maxPlayers: number;
|
|
15
|
+
gametype: string;
|
|
16
|
+
version: string;
|
|
17
|
+
password?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ServerListFilter {
|
|
21
|
+
gametype?: string;
|
|
22
|
+
map?: string;
|
|
23
|
+
notEmpty?: boolean;
|
|
24
|
+
notFull?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a mock master server for testing server registration and browsing
|
|
29
|
+
* @param overrides - Optional overrides for the master server behavior
|
|
30
|
+
*/
|
|
31
|
+
export function createMockMasterServer(overrides: Partial<MasterServer> = {}): MasterServer {
|
|
32
|
+
const servers = new Map<string, ServerInfo>();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
registerServer: async (info: ServerInfo): Promise<boolean> => {
|
|
36
|
+
servers.set(info.address, info);
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
heartbeat: async (serverAddress: string): Promise<boolean> => {
|
|
40
|
+
return servers.has(serverAddress);
|
|
41
|
+
},
|
|
42
|
+
getServerList: async (filter?: ServerListFilter): Promise<ServerInfo[]> => {
|
|
43
|
+
let list = Array.from(servers.values());
|
|
44
|
+
|
|
45
|
+
if (filter) {
|
|
46
|
+
if (filter.gametype) {
|
|
47
|
+
list = list.filter(s => s.gametype === filter.gametype);
|
|
48
|
+
}
|
|
49
|
+
if (filter.map) {
|
|
50
|
+
list = list.filter(s => s.map === filter.map);
|
|
51
|
+
}
|
|
52
|
+
if (filter.notEmpty) {
|
|
53
|
+
list = list.filter(s => s.players > 0);
|
|
54
|
+
}
|
|
55
|
+
if (filter.notFull) {
|
|
56
|
+
list = list.filter(s => s.players < s.maxPlayers);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return list;
|
|
61
|
+
},
|
|
62
|
+
...overrides
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a mock server info object
|
|
68
|
+
* @param overrides - Optional overrides
|
|
69
|
+
*/
|
|
70
|
+
export function createMockServerInfo(overrides: Partial<ServerInfo> = {}): ServerInfo {
|
|
71
|
+
return {
|
|
72
|
+
address: '127.0.0.1:27910',
|
|
73
|
+
name: 'Quake 2 Test Server',
|
|
74
|
+
map: 'q2dm1',
|
|
75
|
+
players: 0,
|
|
76
|
+
maxPlayers: 16,
|
|
77
|
+
gametype: 'deathmatch',
|
|
78
|
+
version: '1.0',
|
|
79
|
+
...overrides
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Simulates the process of a game server registering with the master server
|
|
85
|
+
* @param server - The mock game server instance (typed as any to accept mock)
|
|
86
|
+
* @param master - The mock master server
|
|
87
|
+
*/
|
|
88
|
+
export async function simulateServerRegistration(server: any, master: MasterServer): Promise<boolean> {
|
|
89
|
+
const info = createMockServerInfo({
|
|
90
|
+
name: server.name || 'Test Server',
|
|
91
|
+
map: server.mapName || 'q2dm1',
|
|
92
|
+
players: server.clients ? server.clients.filter((c: Client | null) => c && c.state >= 2).length : 0,
|
|
93
|
+
maxPlayers: server.maxClients || 16
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return master.registerServer(info);
|
|
97
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { TraceResult } from '@quake2ts/shared';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mock interface for CollisionEntityIndex.
|
|
6
|
+
*/
|
|
7
|
+
export interface MockCollisionEntityIndex {
|
|
8
|
+
trace: (start: any, mins: any, maxs: any, end: any, passEntity: any, contentMask: number) => TraceResult;
|
|
9
|
+
link: (entity: any) => void;
|
|
10
|
+
unlink: (entity: any) => void;
|
|
11
|
+
gatherTriggerTouches: (entity: any) => any[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a mock CollisionEntityIndex.
|
|
16
|
+
* @param overrides Optional overrides for the mock methods.
|
|
17
|
+
*/
|
|
18
|
+
export function createMockCollisionEntityIndex(overrides?: Partial<MockCollisionEntityIndex>): MockCollisionEntityIndex {
|
|
19
|
+
return {
|
|
20
|
+
trace: vi.fn().mockReturnValue({
|
|
21
|
+
fraction: 1.0,
|
|
22
|
+
allsolid: false,
|
|
23
|
+
startsolid: false,
|
|
24
|
+
endpos: { x: 0, y: 0, z: 0 },
|
|
25
|
+
entityId: null
|
|
26
|
+
}),
|
|
27
|
+
link: vi.fn(),
|
|
28
|
+
unlink: vi.fn(),
|
|
29
|
+
gatherTriggerTouches: vi.fn().mockReturnValue([]),
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Server, ServerState, ServerStatic, Client, ClientState } from '@quake2ts/server';
|
|
2
|
+
import { NetDriver, MAX_CONFIGSTRINGS, MAX_EDICTS, EntityState } from '@quake2ts/shared';
|
|
3
|
+
import { Entity } from '@quake2ts/game';
|
|
4
|
+
import { vi } from 'vitest';
|
|
5
|
+
import { createMockNetDriver } from './transport.js';
|
|
6
|
+
|
|
7
|
+
// Define GameState interface locally or import from where it should be.
|
|
8
|
+
// Based on grep, GameStateSnapshot is in @quake2ts/game.
|
|
9
|
+
// But test-utils/src/game/mocks.ts defines a local GameState interface.
|
|
10
|
+
// Let's use that one or define a compatible one here.
|
|
11
|
+
import { GameState } from '../../game/mocks.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a mock server state object.
|
|
15
|
+
* @param overrides Optional overrides for the server state.
|
|
16
|
+
*/
|
|
17
|
+
export function createMockServerState(overrides?: Partial<Server>): Server {
|
|
18
|
+
return {
|
|
19
|
+
state: ServerState.Game,
|
|
20
|
+
attractLoop: false,
|
|
21
|
+
loadGame: false,
|
|
22
|
+
startTime: Date.now(),
|
|
23
|
+
time: 0,
|
|
24
|
+
frame: 0,
|
|
25
|
+
name: 'test_map',
|
|
26
|
+
collisionModel: null,
|
|
27
|
+
configStrings: new Array(MAX_CONFIGSTRINGS).fill(''),
|
|
28
|
+
baselines: new Array(MAX_EDICTS).fill(null),
|
|
29
|
+
multicastBuf: new Uint8Array(0),
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a mock server static object.
|
|
36
|
+
* @param maxClients Maximum number of clients.
|
|
37
|
+
* @param overrides Optional overrides for the server static state.
|
|
38
|
+
*/
|
|
39
|
+
export function createMockServerStatic(maxClients: number = 16, overrides?: Partial<ServerStatic>): ServerStatic {
|
|
40
|
+
return {
|
|
41
|
+
initialized: true,
|
|
42
|
+
realTime: Date.now(),
|
|
43
|
+
mapCmd: '',
|
|
44
|
+
spawnCount: 1,
|
|
45
|
+
clients: new Array(maxClients).fill(null),
|
|
46
|
+
lastHeartbeat: 0,
|
|
47
|
+
challenges: [],
|
|
48
|
+
...overrides
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a mock server client.
|
|
54
|
+
* @param clientNum The client index.
|
|
55
|
+
* @param overrides Optional overrides for the client.
|
|
56
|
+
*/
|
|
57
|
+
export function createMockServerClient(clientNum: number, overrides?: Partial<Client>): Client {
|
|
58
|
+
// Create a minimal mock net driver
|
|
59
|
+
const mockNet: NetDriver = createMockNetDriver();
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
index: clientNum,
|
|
63
|
+
state: ClientState.Connected,
|
|
64
|
+
edict: { index: clientNum + 1 } as Entity,
|
|
65
|
+
net: mockNet,
|
|
66
|
+
netchan: {
|
|
67
|
+
qport: 0,
|
|
68
|
+
remoteAddress: '127.0.0.1',
|
|
69
|
+
incomingSequence: 0,
|
|
70
|
+
outgoingSequence: 0,
|
|
71
|
+
lastReceived: 0,
|
|
72
|
+
process: vi.fn(),
|
|
73
|
+
transmit: vi.fn(),
|
|
74
|
+
writeReliableByte: vi.fn(),
|
|
75
|
+
writeReliableShort: vi.fn(),
|
|
76
|
+
writeReliableLong: vi.fn(),
|
|
77
|
+
writeReliableString: vi.fn(),
|
|
78
|
+
writeReliableData: vi.fn(),
|
|
79
|
+
} as any, // Cast as any because NetChan might be complex to fully mock here
|
|
80
|
+
userInfo: '',
|
|
81
|
+
lastMessage: 0,
|
|
82
|
+
lastCommandTime: 0,
|
|
83
|
+
commandCount: 0,
|
|
84
|
+
messageQueue: [],
|
|
85
|
+
frames: [],
|
|
86
|
+
lastFrame: 0,
|
|
87
|
+
lastPacketEntities: [],
|
|
88
|
+
challenge: 0,
|
|
89
|
+
lastConnect: 0,
|
|
90
|
+
ping: 0,
|
|
91
|
+
rate: 0,
|
|
92
|
+
name: `Client${clientNum}`,
|
|
93
|
+
messageLevel: 0,
|
|
94
|
+
datagram: new Uint8Array(0),
|
|
95
|
+
downloadSize: 0,
|
|
96
|
+
downloadCount: 0,
|
|
97
|
+
commandMsec: 0,
|
|
98
|
+
frameLatency: [],
|
|
99
|
+
messageSize: [],
|
|
100
|
+
suppressCount: 0,
|
|
101
|
+
commandQueue: [],
|
|
102
|
+
lastCmd: {
|
|
103
|
+
msec: 0,
|
|
104
|
+
buttons: 0,
|
|
105
|
+
angles: { x: 0, y: 0, z: 0 },
|
|
106
|
+
forwardmove: 0,
|
|
107
|
+
sidemove: 0,
|
|
108
|
+
upmove: 0,
|
|
109
|
+
sequence: 0,
|
|
110
|
+
lightlevel: 0,
|
|
111
|
+
impulse: 0,
|
|
112
|
+
serverFrame: 0
|
|
113
|
+
},
|
|
114
|
+
...overrides
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Mock interface for the Server class (DedicatedServer).
|
|
120
|
+
* This allows mocking the server instance itself.
|
|
121
|
+
*/
|
|
122
|
+
export interface MockServer {
|
|
123
|
+
start(mapName: string): Promise<void>;
|
|
124
|
+
stop(): void;
|
|
125
|
+
// tick(): void; // runFrame is private in DedicatedServer, usually we simulate via game.frame
|
|
126
|
+
multicast(origin: any, type: any, event: any, ...args: any[]): void;
|
|
127
|
+
unicast(ent: Entity, reliable: boolean, event: any, ...args: any[]): void;
|
|
128
|
+
configstring(index: number, value: string): void;
|
|
129
|
+
kickPlayer(clientId: number): void;
|
|
130
|
+
changeMap(mapName: string): Promise<void>;
|
|
131
|
+
getClient(clientNum: number): Client | null;
|
|
132
|
+
broadcast(message: string): void;
|
|
133
|
+
tick(): void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates a mock server instance.
|
|
138
|
+
* @param overrides Optional overrides for server methods.
|
|
139
|
+
*/
|
|
140
|
+
export function createMockServer(overrides?: Partial<MockServer>): MockServer {
|
|
141
|
+
return {
|
|
142
|
+
start: vi.fn().mockResolvedValue(undefined),
|
|
143
|
+
stop: vi.fn(),
|
|
144
|
+
multicast: vi.fn(),
|
|
145
|
+
unicast: vi.fn(),
|
|
146
|
+
configstring: vi.fn(),
|
|
147
|
+
kickPlayer: vi.fn(),
|
|
148
|
+
changeMap: vi.fn().mockResolvedValue(undefined),
|
|
149
|
+
getClient: vi.fn().mockReturnValue(null),
|
|
150
|
+
broadcast: vi.fn(),
|
|
151
|
+
tick: vi.fn(),
|
|
152
|
+
...overrides
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Re-export GameState from game mocks if needed, or use the one from game/mocks
|
|
157
|
+
// Since we have a createMockGameState in game/mocks.ts, we should probably use that or alias it.
|
|
158
|
+
// The task says "Add createMockGameState() factory".
|
|
159
|
+
// If it is already in game/mocks.ts, we can just export it from there or re-export it here.
|
|
160
|
+
// But to avoid duplication, I will just re-export the one from game mocks if the intention is to have it available under server utils.
|
|
161
|
+
|
|
162
|
+
export { createMockGameState, type GameState } from '../../game/mocks.js';
|