@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Export all test utilities
|
|
2
|
+
export * from './shared/mocks.js';
|
|
3
|
+
export * from './shared/bsp.js';
|
|
4
|
+
export * from './shared/pak-loader.js';
|
|
5
|
+
export * from './shared/math.js';
|
|
6
|
+
export * from './shared/collision.js';
|
|
7
|
+
export * from './shared/factories.js';
|
|
8
|
+
export * from './game/factories.js';
|
|
9
|
+
export * from './game/helpers.js';
|
|
10
|
+
export * from './game/helpers/physics.js';
|
|
11
|
+
export * from './game/helpers/save.js';
|
|
12
|
+
export * from './game/mocks/ai.js';
|
|
13
|
+
export * from './game/mocks/combat.js';
|
|
14
|
+
export * from './game/mocks/items.js';
|
|
15
|
+
export * from './game/mocks.js';
|
|
16
|
+
export * from './server/mocks/transport.js';
|
|
17
|
+
export * from './server/mockTransport.js';
|
|
18
|
+
export * from './server/mockNetDriver.js';
|
|
19
|
+
export * from './server/mocks/state.js';
|
|
20
|
+
export * from './server/mocks/connection.js';
|
|
21
|
+
export * from './server/mocks/commands.js';
|
|
22
|
+
export * from './server/mocks/master.js';
|
|
23
|
+
export * from './server/mocks/physics.js';
|
|
24
|
+
export * from './server/helpers/multiplayer.js';
|
|
25
|
+
export * from './server/helpers/snapshot.js';
|
|
26
|
+
export * from './server/helpers/bandwidth.js';
|
|
27
|
+
|
|
28
|
+
// Setup
|
|
29
|
+
export * from './setup/browser.js';
|
|
30
|
+
export * from './setup/canvas.js';
|
|
31
|
+
export * from './setup/webgpu.js';
|
|
32
|
+
export * from './engine/mocks/webgpu.js';
|
|
33
|
+
export * from './setup/timing.js';
|
|
34
|
+
export * from './setup/node.js';
|
|
35
|
+
export * from './engine/mocks/webgl.js';
|
|
36
|
+
export * from './engine/mocks/audio.js';
|
|
37
|
+
export * from './engine/mocks/renderer.js';
|
|
38
|
+
export * from './engine/mocks/assets.js';
|
|
39
|
+
export * from './engine/mocks/buffers.js';
|
|
40
|
+
export * from './engine/mocks/lighting.js';
|
|
41
|
+
export * from './engine/mocks/particles.js';
|
|
42
|
+
export * from './engine/rendering.js';
|
|
43
|
+
export * from './setup/storage.js';
|
|
44
|
+
export * from './setup/audio.js';
|
|
45
|
+
export * from './engine/helpers/webgpu-rendering.js';
|
|
46
|
+
export * from './engine/helpers/pipeline-test-template.js';
|
|
47
|
+
|
|
48
|
+
// Client Mocks
|
|
49
|
+
export * from './client/mocks/input.js';
|
|
50
|
+
export * from './client/helpers/view.js';
|
|
51
|
+
export * from './client/helpers/hud.js';
|
|
52
|
+
export * from './client/mocks/network.js';
|
|
53
|
+
export * from './client/mocks/download.js';
|
|
54
|
+
export * from './client/mocks/state.js';
|
|
55
|
+
export * from './client/mocks/console.js';
|
|
56
|
+
export * from './client/helpers/prediction.js';
|
|
57
|
+
|
|
58
|
+
// Visual Testing
|
|
59
|
+
export * from './visual/snapshots.js';
|
|
60
|
+
|
|
61
|
+
// E2E
|
|
62
|
+
export * from './e2e/playwright.js';
|
|
63
|
+
export * from './e2e/network.js';
|
|
64
|
+
export * from './e2e/visual.js';
|
|
65
|
+
|
|
66
|
+
// Export types
|
|
67
|
+
export type { BrowserSetupOptions } from './setup/browser.js';
|
|
68
|
+
export type { NodeSetupOptions } from './setup/node.js';
|
|
69
|
+
export type { MockRAF } from './setup/timing.js';
|
|
70
|
+
export type { StorageScenario } from './setup/storage.js';
|
|
71
|
+
export type { NetworkSimulator, NetworkCondition } from './e2e/network.js';
|
|
72
|
+
export type { VisualScenario, VisualDiff } from './e2e/visual.js';
|
|
73
|
+
export type { HeadlessWebGPUSetup, WebGPUContextState } from './setup/webgpu.js';
|
|
74
|
+
export type { RenderTestSetup, ComputeTestSetup } from './engine/helpers/webgpu-rendering.js';
|
|
75
|
+
export type { GeometryBuffers } from './engine/helpers/pipeline-test-template.js';
|
|
76
|
+
|
|
77
|
+
// Shared Types
|
|
78
|
+
export type {
|
|
79
|
+
BinaryWriterMock,
|
|
80
|
+
BinaryStreamMock,
|
|
81
|
+
MessageWriterMock,
|
|
82
|
+
MessageReaderMock,
|
|
83
|
+
PacketMock
|
|
84
|
+
} from './shared/mocks.js';
|
|
85
|
+
export type { TraceMock, SurfaceMock } from './shared/collision.js';
|
|
86
|
+
export type { Transform } from './shared/math.js';
|
|
87
|
+
export type {
|
|
88
|
+
CaptureOptions,
|
|
89
|
+
ComparisonResult,
|
|
90
|
+
ComparisonOptions,
|
|
91
|
+
SnapshotTestOptions
|
|
92
|
+
} from './visual/snapshots.js';
|
|
93
|
+
export type { MockCollisionEntityIndex } from './server/mocks/physics.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Client, ClientFrame } from '@quake2ts/server';
|
|
2
|
+
|
|
3
|
+
export interface RateLimiter {
|
|
4
|
+
bytesPerSecond: number;
|
|
5
|
+
allow(bytes: number): boolean;
|
|
6
|
+
update(deltaSeconds: number): void;
|
|
7
|
+
reset(): void;
|
|
8
|
+
getUsage(): number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Message {
|
|
12
|
+
data: Uint8Array;
|
|
13
|
+
size: number;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BandwidthScenario {
|
|
18
|
+
bandwidth: number;
|
|
19
|
+
clients: Client[];
|
|
20
|
+
duration: number;
|
|
21
|
+
totalBytesSent: number;
|
|
22
|
+
totalBytesReceived: number;
|
|
23
|
+
droppedPackets: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a mock rate limiter for bandwidth testing
|
|
28
|
+
* @param bytesPerSecond - Maximum bytes per second allowed
|
|
29
|
+
*/
|
|
30
|
+
export function createMockRateLimiter(bytesPerSecond: number): RateLimiter {
|
|
31
|
+
let bucket = bytesPerSecond;
|
|
32
|
+
const maxBucket = bytesPerSecond; // Max burst size equal to 1 second of bandwidth
|
|
33
|
+
let usage = 0;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
bytesPerSecond,
|
|
37
|
+
allow(bytes: number): boolean {
|
|
38
|
+
if (bucket >= bytes) {
|
|
39
|
+
bucket -= bytes;
|
|
40
|
+
usage += bytes;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
},
|
|
45
|
+
update(deltaSeconds: number): void {
|
|
46
|
+
bucket += bytesPerSecond * deltaSeconds;
|
|
47
|
+
if (bucket > maxBucket) {
|
|
48
|
+
bucket = maxBucket;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
reset(): void {
|
|
52
|
+
bucket = maxBucket;
|
|
53
|
+
usage = 0;
|
|
54
|
+
},
|
|
55
|
+
getUsage(): number {
|
|
56
|
+
return usage;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Simulates bandwidth limiting on a stream of messages
|
|
63
|
+
* @param messages - Array of messages to process
|
|
64
|
+
* @param bandwidth - Bandwidth limit in bytes per second
|
|
65
|
+
* @returns Filtered array of messages that passed the bandwidth check
|
|
66
|
+
*/
|
|
67
|
+
export function simulateBandwidthLimit(messages: Message[], bandwidth: number): Message[] {
|
|
68
|
+
const limiter = createMockRateLimiter(bandwidth);
|
|
69
|
+
const result: Message[] = [];
|
|
70
|
+
let lastTime = messages.length > 0 ? messages[0].timestamp : 0;
|
|
71
|
+
|
|
72
|
+
for (const msg of messages) {
|
|
73
|
+
const delta = (msg.timestamp - lastTime) / 1000;
|
|
74
|
+
if (delta > 0) {
|
|
75
|
+
limiter.update(delta);
|
|
76
|
+
}
|
|
77
|
+
lastTime = msg.timestamp;
|
|
78
|
+
|
|
79
|
+
if (limiter.allow(msg.size)) {
|
|
80
|
+
result.push(msg);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Calculates the size of a client snapshot in bytes
|
|
89
|
+
* @param snapshot - The client frame snapshot
|
|
90
|
+
*/
|
|
91
|
+
export function measureSnapshotSize(snapshot: ClientFrame): number {
|
|
92
|
+
let size = 0;
|
|
93
|
+
|
|
94
|
+
// Area bits
|
|
95
|
+
size += snapshot.areaBytes;
|
|
96
|
+
|
|
97
|
+
// Player state (approximate based on struct size or serialization)
|
|
98
|
+
// This is an estimation, as actual network serialization compresses this
|
|
99
|
+
size += 200; // Baseline size for PlayerState
|
|
100
|
+
|
|
101
|
+
// Entities
|
|
102
|
+
// Each EntityState is approx 20-30 bytes compressed
|
|
103
|
+
size += snapshot.entities.length * 20;
|
|
104
|
+
|
|
105
|
+
return size;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a scenario for testing bandwidth limits with multiple clients
|
|
110
|
+
* @param bandwidth - Bandwidth limit per client or total
|
|
111
|
+
* @param numClients - Number of clients to simulate
|
|
112
|
+
*/
|
|
113
|
+
export function createBandwidthTestScenario(bandwidth: number, numClients: number): BandwidthScenario {
|
|
114
|
+
const clients: Client[] = [];
|
|
115
|
+
// Populate with mock clients if needed (currently creating dummy objects to satisfy type)
|
|
116
|
+
// In a real scenario we might use createClient factory but that requires net driver
|
|
117
|
+
// For now we assume caller will populate fully or we return empty array if 0
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
bandwidth,
|
|
121
|
+
clients,
|
|
122
|
+
duration: 0,
|
|
123
|
+
totalBytesSent: 0,
|
|
124
|
+
totalBytesReceived: 0,
|
|
125
|
+
droppedPackets: 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Server, Client, ClientState, ServerState, ServerStatic } from '@quake2ts/server';
|
|
2
|
+
import { Entity } from '@quake2ts/game';
|
|
3
|
+
import { UserCommand } from '@quake2ts/shared';
|
|
4
|
+
import { createMockServerState } from '../mocks/state.js';
|
|
5
|
+
import { createMockServerClient } from '../mocks/state.js';
|
|
6
|
+
import { createMockUserInfo, serializeUserInfo, UserInfo } from '../mocks/connection.js';
|
|
7
|
+
|
|
8
|
+
// Define a type that combines Server and ServerStatic for convenience in tests
|
|
9
|
+
// since the real server implementation splits these but tests often need both.
|
|
10
|
+
export type MockServerContext = Server & {
|
|
11
|
+
clients: (Client | null)[];
|
|
12
|
+
entities?: Entity[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface MultiplayerScenario {
|
|
16
|
+
server: MockServerContext;
|
|
17
|
+
clients: Client[];
|
|
18
|
+
entities: Entity[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a multiplayer test scenario with a mock server and a number of clients.
|
|
23
|
+
* @param numPlayers Number of players to simulate.
|
|
24
|
+
*/
|
|
25
|
+
export function createMultiplayerTestScenario(numPlayers: number = 2): MultiplayerScenario {
|
|
26
|
+
const baseServer = createMockServerState();
|
|
27
|
+
const clients: Client[] = [];
|
|
28
|
+
const entities: Entity[] = [];
|
|
29
|
+
|
|
30
|
+
const server: MockServerContext = {
|
|
31
|
+
...baseServer,
|
|
32
|
+
clients: new Array(numPlayers).fill(null),
|
|
33
|
+
entities: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < numPlayers; i++) {
|
|
37
|
+
// Create client
|
|
38
|
+
const client = createMockServerClient(i, {
|
|
39
|
+
state: ClientState.Active,
|
|
40
|
+
userInfo: serializeUserInfo(createMockUserInfo({ name: `Player${i}` }))
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Create player entity
|
|
44
|
+
const entity = {
|
|
45
|
+
classname: 'player',
|
|
46
|
+
s: { origin: { x: 0, y: 0, z: 0 }, number: i + 1 },
|
|
47
|
+
client: client
|
|
48
|
+
} as unknown as Entity;
|
|
49
|
+
|
|
50
|
+
client.edict = entity;
|
|
51
|
+
server.clients[i] = client;
|
|
52
|
+
clients.push(client);
|
|
53
|
+
entities.push(entity);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Assign entities to server
|
|
57
|
+
server.entities = entities;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
server,
|
|
61
|
+
clients,
|
|
62
|
+
entities
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Simulates a player joining the server.
|
|
68
|
+
* @param server The mock server instance.
|
|
69
|
+
* @param userInfo Optional user info overrides.
|
|
70
|
+
*/
|
|
71
|
+
export async function simulatePlayerJoin(server: MockServerContext, userInfo?: Partial<UserInfo>): Promise<Client> {
|
|
72
|
+
// Find free client slot
|
|
73
|
+
const index = server.clients.findIndex((c) => !c || c.state === ClientState.Free);
|
|
74
|
+
if (index === -1) {
|
|
75
|
+
throw new Error('Server full');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const client = createMockServerClient(index, {
|
|
79
|
+
state: ClientState.Connected,
|
|
80
|
+
userInfo: serializeUserInfo(createMockUserInfo(userInfo))
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
server.clients[index] = client;
|
|
84
|
+
|
|
85
|
+
// Simulate connection process
|
|
86
|
+
client.state = ClientState.Active;
|
|
87
|
+
|
|
88
|
+
// Create entity
|
|
89
|
+
const entity = {
|
|
90
|
+
classname: 'player',
|
|
91
|
+
s: { origin: { x: 0, y: 0, z: 0 }, number: index + 1 },
|
|
92
|
+
client: client
|
|
93
|
+
} as unknown as Entity;
|
|
94
|
+
client.edict = entity;
|
|
95
|
+
|
|
96
|
+
// Add to entities list if possible (mock behavior)
|
|
97
|
+
if (server.entities && Array.isArray(server.entities)) {
|
|
98
|
+
// This is a simplified view, normally entities are in a fixed array
|
|
99
|
+
(server.entities as Entity[])[index + 1] = entity;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return client;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Simulates a player leaving the server.
|
|
107
|
+
* @param server The mock server instance.
|
|
108
|
+
* @param clientNum The client number to disconnect.
|
|
109
|
+
*/
|
|
110
|
+
export function simulatePlayerLeave(server: MockServerContext, clientNum: number): void {
|
|
111
|
+
const client = server.clients[clientNum];
|
|
112
|
+
if (client) {
|
|
113
|
+
client.state = ClientState.Free;
|
|
114
|
+
client.edict = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Simulates a single server frame update.
|
|
120
|
+
* @param server The mock server instance.
|
|
121
|
+
* @param deltaTime Time step in seconds (default: 0.1).
|
|
122
|
+
*/
|
|
123
|
+
export function simulateServerTick(server: MockServerContext, deltaTime: number = 0.1): void {
|
|
124
|
+
server.time += deltaTime;
|
|
125
|
+
server.frame++;
|
|
126
|
+
// In a real server, we would call RunFrame, Physics, etc.
|
|
127
|
+
// For mocks, we might just update client lastCmd times
|
|
128
|
+
|
|
129
|
+
server.clients.forEach((client: Client | null) => {
|
|
130
|
+
if (client && client.state === ClientState.Active) {
|
|
131
|
+
// Update client logic if needed
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Simulates player input for a specific client.
|
|
138
|
+
* @param client The server client.
|
|
139
|
+
* @param input The input command.
|
|
140
|
+
*/
|
|
141
|
+
export function simulatePlayerInput(client: Client, input: Partial<UserCommand>): void {
|
|
142
|
+
const cmd: UserCommand = {
|
|
143
|
+
msec: 100,
|
|
144
|
+
buttons: 0,
|
|
145
|
+
angles: { x: 0, y: 0, z: 0 },
|
|
146
|
+
forwardmove: 0,
|
|
147
|
+
sidemove: 0,
|
|
148
|
+
upmove: 0,
|
|
149
|
+
sequence: client.lastCmd.sequence + 1,
|
|
150
|
+
lightlevel: 0,
|
|
151
|
+
impulse: 0,
|
|
152
|
+
...input
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
client.lastCmd = cmd;
|
|
156
|
+
client.commandQueue.push(cmd);
|
|
157
|
+
client.commandCount++;
|
|
158
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Server } from '@quake2ts/server';
|
|
2
|
+
import { EntityState, BinaryStream, ProtocolPlayerState } from '@quake2ts/shared';
|
|
3
|
+
|
|
4
|
+
export interface Snapshot {
|
|
5
|
+
serverTime: number;
|
|
6
|
+
playerState: any; // Simplified PlayerState
|
|
7
|
+
entities: EntityState[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DeltaSnapshot {
|
|
11
|
+
snapshot: Snapshot;
|
|
12
|
+
deltaEntities: EntityState[];
|
|
13
|
+
removedEntities: number[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConsistencyReport {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
errors: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a client-specific snapshot from the server state.
|
|
23
|
+
* @param serverState The current server state.
|
|
24
|
+
* @param clientNum The client number to generate snapshot for.
|
|
25
|
+
*/
|
|
26
|
+
export function createServerSnapshot(serverState: Server, clientNum: number): Snapshot {
|
|
27
|
+
// Collect visible entities
|
|
28
|
+
const visibleEntities: EntityState[] = [];
|
|
29
|
+
|
|
30
|
+
// In a real implementation, this would use PVS/PHS
|
|
31
|
+
// For mock, we just take all entities that have a model or are solid
|
|
32
|
+
if (serverState.baselines) {
|
|
33
|
+
serverState.baselines.forEach((ent: EntityState | null, index: number) => {
|
|
34
|
+
if (ent && index !== clientNum + 1) { // Skip self in packet entities usually
|
|
35
|
+
visibleEntities.push({ ...ent });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
serverTime: serverState.time,
|
|
42
|
+
playerState: {
|
|
43
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
44
|
+
viewangles: { x: 0, y: 0, z: 0 },
|
|
45
|
+
pm_type: 0
|
|
46
|
+
},
|
|
47
|
+
entities: visibleEntities
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculates the delta between two snapshots.
|
|
53
|
+
* @param oldSnapshot The baseline snapshot.
|
|
54
|
+
* @param newSnapshot The current snapshot.
|
|
55
|
+
*/
|
|
56
|
+
export function createDeltaSnapshot(oldSnapshot: Snapshot, newSnapshot: Snapshot): DeltaSnapshot {
|
|
57
|
+
const deltaEntities: EntityState[] = [];
|
|
58
|
+
const removedEntities: number[] = [];
|
|
59
|
+
|
|
60
|
+
const oldMap = new Map(oldSnapshot.entities.map(e => [e.number, e]));
|
|
61
|
+
const newMap = new Map(newSnapshot.entities.map(e => [e.number, e]));
|
|
62
|
+
|
|
63
|
+
// Find changed or new entities
|
|
64
|
+
newSnapshot.entities.forEach(newEnt => {
|
|
65
|
+
const oldEnt = oldMap.get(newEnt.number);
|
|
66
|
+
if (!oldEnt) {
|
|
67
|
+
// New entity
|
|
68
|
+
deltaEntities.push(newEnt);
|
|
69
|
+
} else if (JSON.stringify(newEnt) !== JSON.stringify(oldEnt)) {
|
|
70
|
+
// Changed entity (simplified check)
|
|
71
|
+
deltaEntities.push(newEnt);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Find removed entities
|
|
76
|
+
oldSnapshot.entities.forEach(oldEnt => {
|
|
77
|
+
if (!newMap.has(oldEnt.number)) {
|
|
78
|
+
removedEntities.push(oldEnt.number);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
snapshot: newSnapshot,
|
|
84
|
+
deltaEntities,
|
|
85
|
+
removedEntities
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Verifies the consistency of a sequence of snapshots.
|
|
91
|
+
* @param snapshots Array of snapshots ordered by time.
|
|
92
|
+
*/
|
|
93
|
+
export function verifySnapshotConsistency(snapshots: Snapshot[]): ConsistencyReport {
|
|
94
|
+
const report: ConsistencyReport = { valid: true, errors: [] };
|
|
95
|
+
|
|
96
|
+
if (snapshots.length < 2) return report;
|
|
97
|
+
|
|
98
|
+
for (let i = 1; i < snapshots.length; i++) {
|
|
99
|
+
const prev = snapshots[i-1];
|
|
100
|
+
const curr = snapshots[i];
|
|
101
|
+
|
|
102
|
+
if (curr.serverTime <= prev.serverTime) {
|
|
103
|
+
report.valid = false;
|
|
104
|
+
report.errors.push(`Snapshot ${i} has time ${curr.serverTime} <= prev ${prev.serverTime}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return report;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Simulates network delivery of a snapshot with potential packet loss.
|
|
113
|
+
* @param snapshot The snapshot to deliver.
|
|
114
|
+
* @param reliability Probability of successful delivery (0.0 to 1.0).
|
|
115
|
+
*/
|
|
116
|
+
export async function simulateSnapshotDelivery(snapshot: Snapshot, reliability: number = 1.0): Promise<Snapshot | null> {
|
|
117
|
+
if (Math.random() > reliability) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return snapshot;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parses a ProtocolPlayerState from a binary buffer.
|
|
125
|
+
* Useful for testing player state serialization.
|
|
126
|
+
* logic adapted from packages/engine/src/demo/parser.ts
|
|
127
|
+
* @param data The binary data to parse.
|
|
128
|
+
*/
|
|
129
|
+
export function parseProtocolPlayerState(data: Uint8Array): ProtocolPlayerState {
|
|
130
|
+
const stream = new BinaryStream(data.buffer as ArrayBuffer);
|
|
131
|
+
const ps: ProtocolPlayerState = {
|
|
132
|
+
pm_type: 0,
|
|
133
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
134
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
135
|
+
pm_time: 0,
|
|
136
|
+
pm_flags: 0,
|
|
137
|
+
gravity: 0,
|
|
138
|
+
delta_angles: { x: 0, y: 0, z: 0 },
|
|
139
|
+
viewoffset: { x: 0, y: 0, z: 0 },
|
|
140
|
+
viewangles: { x: 0, y: 0, z: 0 },
|
|
141
|
+
kick_angles: { x: 0, y: 0, z: 0 },
|
|
142
|
+
gun_index: 0,
|
|
143
|
+
gun_frame: 0,
|
|
144
|
+
gun_offset: { x: 0, y: 0, z: 0 },
|
|
145
|
+
gun_angles: { x: 0, y: 0, z: 0 },
|
|
146
|
+
blend: [0, 0, 0, 0],
|
|
147
|
+
fov: 0,
|
|
148
|
+
rdflags: 0,
|
|
149
|
+
stats: new Array(32).fill(0),
|
|
150
|
+
watertype: 0
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const flags = stream.readShort();
|
|
154
|
+
|
|
155
|
+
if (flags & 1) ps.pm_type = stream.readByte();
|
|
156
|
+
|
|
157
|
+
if (flags & 2) {
|
|
158
|
+
const x = stream.readShort() * 0.125;
|
|
159
|
+
const y = stream.readShort() * 0.125;
|
|
160
|
+
const z = stream.readShort() * 0.125;
|
|
161
|
+
ps.origin = { x, y, z };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (flags & 4) {
|
|
165
|
+
const x = stream.readShort() * 0.125;
|
|
166
|
+
const y = stream.readShort() * 0.125;
|
|
167
|
+
const z = stream.readShort() * 0.125;
|
|
168
|
+
ps.velocity = { x, y, z };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (flags & 8) ps.pm_time = stream.readByte();
|
|
172
|
+
if (flags & 16) ps.pm_flags = stream.readByte();
|
|
173
|
+
if (flags & 32) ps.gravity = stream.readShort();
|
|
174
|
+
|
|
175
|
+
if (flags & 64) {
|
|
176
|
+
const x = stream.readShort() * (180 / 32768);
|
|
177
|
+
const y = stream.readShort() * (180 / 32768);
|
|
178
|
+
const z = stream.readShort() * (180 / 32768);
|
|
179
|
+
ps.delta_angles = { x, y, z };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (flags & 128) {
|
|
183
|
+
const x = stream.readChar() * 0.25;
|
|
184
|
+
const y = stream.readChar() * 0.25;
|
|
185
|
+
const z = stream.readChar() * 0.25;
|
|
186
|
+
ps.viewoffset = { x, y, z };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (flags & 256) {
|
|
190
|
+
const x = stream.readAngle16();
|
|
191
|
+
const y = stream.readAngle16();
|
|
192
|
+
const z = stream.readAngle16();
|
|
193
|
+
ps.viewangles = { x, y, z };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (flags & 512) {
|
|
197
|
+
const x = stream.readChar() * 0.25;
|
|
198
|
+
const y = stream.readChar() * 0.25;
|
|
199
|
+
const z = stream.readChar() * 0.25;
|
|
200
|
+
ps.kick_angles = { x, y, z };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (flags & 4096) ps.gun_index = stream.readByte();
|
|
204
|
+
|
|
205
|
+
if (flags & 8192) {
|
|
206
|
+
ps.gun_frame = stream.readByte();
|
|
207
|
+
const ox = stream.readChar() * 0.25;
|
|
208
|
+
const oy = stream.readChar() * 0.25;
|
|
209
|
+
const oz = stream.readChar() * 0.25;
|
|
210
|
+
ps.gun_offset = { x: ox, y: oy, z: oz };
|
|
211
|
+
|
|
212
|
+
const ax = stream.readChar() * 0.25;
|
|
213
|
+
const ay = stream.readChar() * 0.25;
|
|
214
|
+
const az = stream.readChar() * 0.25;
|
|
215
|
+
ps.gun_angles = { x: ax, y: ay, z: az };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (flags & 1024) {
|
|
219
|
+
ps.blend = [
|
|
220
|
+
stream.readByte(),
|
|
221
|
+
stream.readByte(),
|
|
222
|
+
stream.readByte(),
|
|
223
|
+
stream.readByte()
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (flags & 2048) ps.fov = stream.readByte();
|
|
228
|
+
if (flags & 16384) ps.rdflags = stream.readByte();
|
|
229
|
+
|
|
230
|
+
// New: watertype (bit 15)
|
|
231
|
+
if (flags & 32768) ps.watertype = stream.readByte();
|
|
232
|
+
|
|
233
|
+
const statbits = stream.readLong();
|
|
234
|
+
for (let i = 0; i < 32; i++) {
|
|
235
|
+
if (statbits & (1 << i)) {
|
|
236
|
+
ps.stats[i] = stream.readShort();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return ps;
|
|
241
|
+
}
|