quake2ts 0.0.561 → 0.0.563
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/package.json +1 -1
- package/packages/client/dist/browser/index.global.js +15 -15
- package/packages/client/dist/browser/index.global.js.map +1 -1
- package/packages/client/dist/cjs/index.cjs +273 -1
- package/packages/client/dist/cjs/index.cjs.map +1 -1
- package/packages/client/dist/esm/index.js +273 -1
- package/packages/client/dist/esm/index.js.map +1 -1
- package/packages/client/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/client/dist/types/net/connection.d.ts +2 -0
- package/packages/client/dist/types/net/connection.d.ts.map +1 -1
- package/packages/server/dist/client.d.ts +51 -0
- package/packages/server/dist/client.js +100 -0
- package/packages/server/dist/dedicated.d.ts +69 -0
- package/packages/server/dist/dedicated.js +1013 -0
- package/packages/server/dist/index.cjs +27 -2
- package/packages/server/dist/index.d.ts +7 -161
- package/packages/server/dist/index.js +26 -2
- package/packages/server/dist/net/nodeWsDriver.d.ts +16 -0
- package/packages/server/dist/net/nodeWsDriver.js +122 -0
- package/packages/server/dist/protocol/player.d.ts +23 -0
- package/packages/server/dist/protocol/player.js +137 -0
- package/packages/server/dist/protocol/write.d.ts +7 -0
- package/packages/server/dist/protocol/write.js +167 -0
- package/packages/server/dist/protocol.d.ts +17 -0
- package/packages/server/dist/protocol.js +71 -0
- package/packages/server/dist/server.d.ts +50 -0
- package/packages/server/dist/server.js +12 -0
- package/packages/server/dist/server.test.d.ts +1 -0
- package/packages/server/dist/server.test.js +69 -0
- package/packages/server/dist/transport.d.ts +7 -0
- package/packages/server/dist/transport.js +1 -0
- package/packages/server/dist/transports/websocket.d.ts +11 -0
- package/packages/server/dist/transports/websocket.js +38 -0
- package/packages/test-utils/dist/index.cjs +1610 -1188
- package/packages/test-utils/dist/index.cjs.map +1 -1
- package/packages/test-utils/dist/index.d.cts +326 -132
- package/packages/test-utils/dist/index.d.ts +326 -132
- package/packages/test-utils/dist/index.js +1596 -1189
- package/packages/test-utils/dist/index.js.map +1 -1
- package/packages/server/dist/index.d.cts +0 -161
|
@@ -1,1289 +1,1685 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
cancelable: true,
|
|
82
|
-
view: this.win
|
|
83
|
-
});
|
|
84
|
-
const target = this.doc.pointerLockElement || this.doc;
|
|
85
|
-
target.dispatchEvent(event);
|
|
86
|
-
}
|
|
87
|
-
wheel(deltaY) {
|
|
88
|
-
const event = new this.win.WheelEvent("wheel", {
|
|
89
|
-
deltaY,
|
|
90
|
-
bubbles: true,
|
|
91
|
-
cancelable: true,
|
|
92
|
-
view: this.win
|
|
93
|
-
});
|
|
94
|
-
const target = this.doc.pointerLockElement || this.doc;
|
|
95
|
-
target.dispatchEvent(event);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// src/setup/webgl.ts
|
|
100
|
-
function createMockWebGL2Context(canvas) {
|
|
101
|
-
const gl = {
|
|
102
|
-
canvas,
|
|
103
|
-
drawingBufferWidth: canvas.width,
|
|
104
|
-
drawingBufferHeight: canvas.height,
|
|
105
|
-
// Constants
|
|
106
|
-
VERTEX_SHADER: 35633,
|
|
107
|
-
FRAGMENT_SHADER: 35632,
|
|
108
|
-
COMPILE_STATUS: 35713,
|
|
109
|
-
LINK_STATUS: 35714,
|
|
110
|
-
ARRAY_BUFFER: 34962,
|
|
111
|
-
ELEMENT_ARRAY_BUFFER: 34963,
|
|
112
|
-
STATIC_DRAW: 35044,
|
|
113
|
-
DYNAMIC_DRAW: 35048,
|
|
114
|
-
FLOAT: 5126,
|
|
115
|
-
DEPTH_TEST: 2929,
|
|
116
|
-
BLEND: 3042,
|
|
117
|
-
SRC_ALPHA: 770,
|
|
118
|
-
ONE_MINUS_SRC_ALPHA: 771,
|
|
119
|
-
TEXTURE_2D: 3553,
|
|
120
|
-
RGBA: 6408,
|
|
121
|
-
UNSIGNED_BYTE: 5121,
|
|
122
|
-
COLOR_BUFFER_BIT: 16384,
|
|
123
|
-
DEPTH_BUFFER_BIT: 256,
|
|
124
|
-
TRIANGLES: 4,
|
|
125
|
-
TRIANGLE_STRIP: 5,
|
|
126
|
-
TRIANGLE_FAN: 6,
|
|
127
|
-
// Methods
|
|
128
|
-
createShader: () => ({}),
|
|
129
|
-
shaderSource: () => {
|
|
130
|
-
},
|
|
131
|
-
compileShader: () => {
|
|
132
|
-
},
|
|
133
|
-
getShaderParameter: (_, param) => {
|
|
134
|
-
if (param === 35713) return true;
|
|
135
|
-
return true;
|
|
136
|
-
},
|
|
137
|
-
getShaderInfoLog: () => "",
|
|
138
|
-
createProgram: () => ({}),
|
|
139
|
-
attachShader: () => {
|
|
140
|
-
},
|
|
141
|
-
linkProgram: () => {
|
|
142
|
-
},
|
|
143
|
-
getProgramParameter: (_, param) => {
|
|
144
|
-
if (param === 35714) return true;
|
|
145
|
-
return true;
|
|
146
|
-
},
|
|
147
|
-
getProgramInfoLog: () => "",
|
|
148
|
-
useProgram: () => {
|
|
149
|
-
},
|
|
150
|
-
createBuffer: () => ({}),
|
|
151
|
-
bindBuffer: () => {
|
|
152
|
-
},
|
|
153
|
-
bufferData: () => {
|
|
154
|
-
},
|
|
155
|
-
enableVertexAttribArray: () => {
|
|
156
|
-
},
|
|
157
|
-
vertexAttribPointer: () => {
|
|
158
|
-
},
|
|
159
|
-
enable: () => {
|
|
160
|
-
},
|
|
161
|
-
disable: () => {
|
|
162
|
-
},
|
|
163
|
-
depthMask: () => {
|
|
164
|
-
},
|
|
165
|
-
blendFunc: () => {
|
|
166
|
-
},
|
|
167
|
-
viewport: () => {
|
|
168
|
-
},
|
|
169
|
-
clearColor: () => {
|
|
170
|
-
},
|
|
171
|
-
clear: () => {
|
|
172
|
-
},
|
|
173
|
-
createTexture: () => ({}),
|
|
174
|
-
bindTexture: () => {
|
|
175
|
-
},
|
|
176
|
-
texImage2D: () => {
|
|
177
|
-
},
|
|
178
|
-
texParameteri: () => {
|
|
179
|
-
},
|
|
180
|
-
activeTexture: () => {
|
|
181
|
-
},
|
|
182
|
-
uniform1i: () => {
|
|
183
|
-
},
|
|
184
|
-
uniform1f: () => {
|
|
185
|
-
},
|
|
186
|
-
uniform2f: () => {
|
|
187
|
-
},
|
|
188
|
-
uniform3f: () => {
|
|
189
|
-
},
|
|
190
|
-
uniform4f: () => {
|
|
191
|
-
},
|
|
192
|
-
uniformMatrix4fv: () => {
|
|
193
|
-
},
|
|
194
|
-
getUniformLocation: () => ({}),
|
|
195
|
-
getAttribLocation: () => 0,
|
|
196
|
-
drawArrays: () => {
|
|
197
|
-
},
|
|
198
|
-
drawElements: () => {
|
|
199
|
-
},
|
|
200
|
-
createVertexArray: () => ({}),
|
|
201
|
-
bindVertexArray: () => {
|
|
202
|
-
},
|
|
203
|
-
deleteShader: () => {
|
|
204
|
-
},
|
|
205
|
-
deleteProgram: () => {
|
|
206
|
-
},
|
|
207
|
-
deleteBuffer: () => {
|
|
208
|
-
},
|
|
209
|
-
deleteTexture: () => {
|
|
210
|
-
},
|
|
211
|
-
deleteVertexArray: () => {
|
|
212
|
-
},
|
|
213
|
-
// WebGL2 specific
|
|
214
|
-
texImage3D: () => {
|
|
215
|
-
},
|
|
216
|
-
uniformBlockBinding: () => {
|
|
217
|
-
},
|
|
218
|
-
getExtension: () => null
|
|
219
|
-
};
|
|
220
|
-
return gl;
|
|
221
|
-
}
|
|
1
|
+
// src/shared/mocks.ts
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
var createBinaryWriterMock = () => ({
|
|
4
|
+
writeByte: vi.fn(),
|
|
5
|
+
writeShort: vi.fn(),
|
|
6
|
+
writeLong: vi.fn(),
|
|
7
|
+
writeString: vi.fn(),
|
|
8
|
+
writeBytes: vi.fn(),
|
|
9
|
+
getBuffer: vi.fn(() => new Uint8Array(0)),
|
|
10
|
+
reset: vi.fn(),
|
|
11
|
+
// Legacy methods (if any)
|
|
12
|
+
writeInt8: vi.fn(),
|
|
13
|
+
writeUint8: vi.fn(),
|
|
14
|
+
writeInt16: vi.fn(),
|
|
15
|
+
writeUint16: vi.fn(),
|
|
16
|
+
writeInt32: vi.fn(),
|
|
17
|
+
writeUint32: vi.fn(),
|
|
18
|
+
writeFloat: vi.fn(),
|
|
19
|
+
getData: vi.fn(() => new Uint8Array(0))
|
|
20
|
+
});
|
|
21
|
+
var createNetChanMock = () => ({
|
|
22
|
+
qport: 1234,
|
|
23
|
+
// Sequencing
|
|
24
|
+
incomingSequence: 0,
|
|
25
|
+
outgoingSequence: 0,
|
|
26
|
+
incomingAcknowledged: 0,
|
|
27
|
+
// Reliable messaging
|
|
28
|
+
incomingReliableAcknowledged: false,
|
|
29
|
+
incomingReliableSequence: 0,
|
|
30
|
+
outgoingReliableSequence: 0,
|
|
31
|
+
reliableMessage: createBinaryWriterMock(),
|
|
32
|
+
reliableLength: 0,
|
|
33
|
+
// Fragmentation
|
|
34
|
+
fragmentSendOffset: 0,
|
|
35
|
+
fragmentBuffer: null,
|
|
36
|
+
fragmentLength: 0,
|
|
37
|
+
fragmentReceived: 0,
|
|
38
|
+
// Timing
|
|
39
|
+
lastReceived: 0,
|
|
40
|
+
lastSent: 0,
|
|
41
|
+
remoteAddress: { type: "IP", port: 1234 },
|
|
42
|
+
// Methods
|
|
43
|
+
setup: vi.fn(),
|
|
44
|
+
reset: vi.fn(),
|
|
45
|
+
transmit: vi.fn(),
|
|
46
|
+
process: vi.fn(),
|
|
47
|
+
canSendReliable: vi.fn(() => true),
|
|
48
|
+
writeReliableByte: vi.fn(),
|
|
49
|
+
writeReliableShort: vi.fn(),
|
|
50
|
+
writeReliableLong: vi.fn(),
|
|
51
|
+
writeReliableString: vi.fn(),
|
|
52
|
+
getReliableData: vi.fn(() => new Uint8Array(0)),
|
|
53
|
+
needsKeepalive: vi.fn(() => false),
|
|
54
|
+
isTimedOut: vi.fn(() => false)
|
|
55
|
+
});
|
|
56
|
+
var createBinaryStreamMock = () => ({
|
|
57
|
+
getPosition: vi.fn(() => 0),
|
|
58
|
+
getReadPosition: vi.fn(() => 0),
|
|
59
|
+
getLength: vi.fn(() => 0),
|
|
60
|
+
getRemaining: vi.fn(() => 0),
|
|
61
|
+
seek: vi.fn(),
|
|
62
|
+
setReadPosition: vi.fn(),
|
|
63
|
+
hasMore: vi.fn(() => true),
|
|
64
|
+
hasBytes: vi.fn((amount) => true),
|
|
65
|
+
readChar: vi.fn(() => 0),
|
|
66
|
+
readByte: vi.fn(() => 0),
|
|
67
|
+
readShort: vi.fn(() => 0),
|
|
68
|
+
readUShort: vi.fn(() => 0),
|
|
69
|
+
readLong: vi.fn(() => 0),
|
|
70
|
+
readULong: vi.fn(() => 0),
|
|
71
|
+
readFloat: vi.fn(() => 0),
|
|
72
|
+
readString: vi.fn(() => ""),
|
|
73
|
+
readStringLine: vi.fn(() => ""),
|
|
74
|
+
readCoord: vi.fn(() => 0),
|
|
75
|
+
readAngle: vi.fn(() => 0),
|
|
76
|
+
readAngle16: vi.fn(() => 0),
|
|
77
|
+
readData: vi.fn((length) => new Uint8Array(length)),
|
|
78
|
+
readPos: vi.fn(),
|
|
79
|
+
readDir: vi.fn()
|
|
80
|
+
});
|
|
222
81
|
|
|
223
|
-
// src/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
pretendToBeVisual,
|
|
235
|
-
resources
|
|
236
|
-
});
|
|
237
|
-
global.window = dom.window;
|
|
238
|
-
global.document = dom.window.document;
|
|
239
|
-
try {
|
|
240
|
-
global.navigator = dom.window.navigator;
|
|
241
|
-
} catch (e) {
|
|
242
|
-
try {
|
|
243
|
-
Object.defineProperty(global, "navigator", {
|
|
244
|
-
value: dom.window.navigator,
|
|
245
|
-
writable: true,
|
|
246
|
-
configurable: true
|
|
247
|
-
});
|
|
248
|
-
} catch (e2) {
|
|
249
|
-
console.warn("Could not assign global.navigator, skipping.");
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
global.location = dom.window.location;
|
|
253
|
-
global.HTMLElement = dom.window.HTMLElement;
|
|
254
|
-
global.HTMLCanvasElement = dom.window.HTMLCanvasElement;
|
|
255
|
-
global.Event = dom.window.Event;
|
|
256
|
-
global.CustomEvent = dom.window.CustomEvent;
|
|
257
|
-
global.DragEvent = dom.window.DragEvent;
|
|
258
|
-
global.MouseEvent = dom.window.MouseEvent;
|
|
259
|
-
global.KeyboardEvent = dom.window.KeyboardEvent;
|
|
260
|
-
global.FocusEvent = dom.window.FocusEvent;
|
|
261
|
-
global.WheelEvent = dom.window.WheelEvent;
|
|
262
|
-
global.InputEvent = dom.window.InputEvent;
|
|
263
|
-
global.UIEvent = dom.window.UIEvent;
|
|
264
|
-
try {
|
|
265
|
-
global.localStorage = dom.window.localStorage;
|
|
266
|
-
} catch (e) {
|
|
267
|
-
}
|
|
268
|
-
if (!global.localStorage) {
|
|
269
|
-
const storage = /* @__PURE__ */ new Map();
|
|
270
|
-
global.localStorage = {
|
|
271
|
-
getItem: (key) => storage.get(key) || null,
|
|
272
|
-
setItem: (key, value) => storage.set(key, value),
|
|
273
|
-
removeItem: (key) => storage.delete(key),
|
|
274
|
-
clear: () => storage.clear(),
|
|
275
|
-
key: (index) => Array.from(storage.keys())[index] || null,
|
|
276
|
-
get length() {
|
|
277
|
-
return storage.size;
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
const originalCreateElement = document.createElement.bind(document);
|
|
282
|
-
document.createElement = function(tagName, options2) {
|
|
283
|
-
if (tagName.toLowerCase() === "canvas") {
|
|
284
|
-
const napiCanvas = new Canvas(300, 150);
|
|
285
|
-
const domCanvas = originalCreateElement("canvas", options2);
|
|
286
|
-
Object.defineProperty(domCanvas, "width", {
|
|
287
|
-
get: () => napiCanvas.width,
|
|
288
|
-
set: (value) => {
|
|
289
|
-
napiCanvas.width = value;
|
|
290
|
-
},
|
|
291
|
-
enumerable: true,
|
|
292
|
-
configurable: true
|
|
293
|
-
});
|
|
294
|
-
Object.defineProperty(domCanvas, "height", {
|
|
295
|
-
get: () => napiCanvas.height,
|
|
296
|
-
set: (value) => {
|
|
297
|
-
napiCanvas.height = value;
|
|
298
|
-
},
|
|
299
|
-
enumerable: true,
|
|
300
|
-
configurable: true
|
|
301
|
-
});
|
|
302
|
-
const originalGetContext = domCanvas.getContext.bind(domCanvas);
|
|
303
|
-
domCanvas.getContext = function(contextId, options3) {
|
|
304
|
-
if (contextId === "2d") {
|
|
305
|
-
return napiCanvas.getContext("2d", options3);
|
|
306
|
-
}
|
|
307
|
-
if (enableWebGL2 && contextId === "webgl2") {
|
|
308
|
-
return createMockWebGL2Context(domCanvas);
|
|
309
|
-
}
|
|
310
|
-
if (contextId === "webgl" || contextId === "webgl2") {
|
|
311
|
-
return originalGetContext(contextId, options3);
|
|
312
|
-
}
|
|
313
|
-
return napiCanvas.getContext(contextId, options3);
|
|
314
|
-
};
|
|
315
|
-
domCanvas.__napiCanvas = napiCanvas;
|
|
316
|
-
return domCanvas;
|
|
317
|
-
}
|
|
318
|
-
return originalCreateElement(tagName, options2);
|
|
82
|
+
// src/shared/bsp.ts
|
|
83
|
+
import {
|
|
84
|
+
computePlaneSignBits,
|
|
85
|
+
CONTENTS_SOLID
|
|
86
|
+
} from "@quake2ts/shared";
|
|
87
|
+
function makePlane(normal, dist) {
|
|
88
|
+
return {
|
|
89
|
+
normal,
|
|
90
|
+
dist,
|
|
91
|
+
type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
|
|
92
|
+
signbits: computePlaneSignBits(normal)
|
|
319
93
|
};
|
|
320
|
-
if (enableWebGL2) {
|
|
321
|
-
const originalProtoGetContext = global.HTMLCanvasElement.prototype.getContext;
|
|
322
|
-
global.HTMLCanvasElement.prototype.getContext = function(contextId, options2) {
|
|
323
|
-
if (contextId === "webgl2") {
|
|
324
|
-
return createMockWebGL2Context(this);
|
|
325
|
-
}
|
|
326
|
-
return originalProtoGetContext.call(this, contextId, options2);
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
global.Image = Image;
|
|
330
|
-
global.ImageData = ImageData;
|
|
331
|
-
if (typeof global.createImageBitmap === "undefined") {
|
|
332
|
-
global.createImageBitmap = async function(image, _options) {
|
|
333
|
-
if (image && typeof image.width === "number" && typeof image.height === "number") {
|
|
334
|
-
const canvas2 = new Canvas(image.width, image.height);
|
|
335
|
-
const ctx = canvas2.getContext("2d");
|
|
336
|
-
if (image.data) {
|
|
337
|
-
ctx.putImageData(image, 0, 0);
|
|
338
|
-
}
|
|
339
|
-
return canvas2;
|
|
340
|
-
}
|
|
341
|
-
const canvas = new Canvas(100, 100);
|
|
342
|
-
return canvas;
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
if (typeof global.btoa === "undefined") {
|
|
346
|
-
global.btoa = function(str) {
|
|
347
|
-
return Buffer.from(str, "binary").toString("base64");
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
if (typeof global.atob === "undefined") {
|
|
351
|
-
global.atob = function(str) {
|
|
352
|
-
return Buffer.from(str, "base64").toString("binary");
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
if (enablePointerLock) {
|
|
356
|
-
MockPointerLock.setup(global.document);
|
|
357
|
-
}
|
|
358
|
-
if (typeof global.requestAnimationFrame === "undefined") {
|
|
359
|
-
let lastTime = 0;
|
|
360
|
-
global.requestAnimationFrame = (callback) => {
|
|
361
|
-
const currTime = Date.now();
|
|
362
|
-
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
|
363
|
-
const id = setTimeout(() => {
|
|
364
|
-
callback(currTime + timeToCall);
|
|
365
|
-
}, timeToCall);
|
|
366
|
-
lastTime = currTime + timeToCall;
|
|
367
|
-
return id;
|
|
368
|
-
};
|
|
369
|
-
global.cancelAnimationFrame = (id) => {
|
|
370
|
-
clearTimeout(id);
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
94
|
}
|
|
374
|
-
function
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
delete global.DragEvent;
|
|
388
|
-
delete global.MouseEvent;
|
|
389
|
-
delete global.KeyboardEvent;
|
|
390
|
-
delete global.FocusEvent;
|
|
391
|
-
delete global.WheelEvent;
|
|
392
|
-
delete global.InputEvent;
|
|
393
|
-
delete global.UIEvent;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// src/setup/canvas.ts
|
|
397
|
-
import { Canvas as Canvas2, Image as Image2, ImageData as ImageData2 } from "@napi-rs/canvas";
|
|
398
|
-
function createMockCanvas(width = 300, height = 150) {
|
|
399
|
-
if (typeof document !== "undefined" && document.createElement) {
|
|
400
|
-
const canvas2 = document.createElement("canvas");
|
|
401
|
-
canvas2.width = width;
|
|
402
|
-
canvas2.height = height;
|
|
403
|
-
return canvas2;
|
|
404
|
-
}
|
|
405
|
-
const canvas = new Canvas2(width, height);
|
|
406
|
-
const originalGetContext = canvas.getContext.bind(canvas);
|
|
407
|
-
canvas.getContext = function(contextId, options) {
|
|
408
|
-
if (contextId === "webgl2") {
|
|
409
|
-
return createMockWebGL2Context(canvas);
|
|
410
|
-
}
|
|
411
|
-
if (contextId === "2d") {
|
|
412
|
-
return originalGetContext("2d", options);
|
|
413
|
-
}
|
|
414
|
-
return originalGetContext(contextId, options);
|
|
95
|
+
function makeAxisBrush(size, contents = CONTENTS_SOLID) {
|
|
96
|
+
const half = size / 2;
|
|
97
|
+
const planes = [
|
|
98
|
+
makePlane({ x: 1, y: 0, z: 0 }, half),
|
|
99
|
+
makePlane({ x: -1, y: 0, z: 0 }, half),
|
|
100
|
+
makePlane({ x: 0, y: 1, z: 0 }, half),
|
|
101
|
+
makePlane({ x: 0, y: -1, z: 0 }, half),
|
|
102
|
+
makePlane({ x: 0, y: 0, z: 1 }, half),
|
|
103
|
+
makePlane({ x: 0, y: 0, z: -1 }, half)
|
|
104
|
+
];
|
|
105
|
+
return {
|
|
106
|
+
contents,
|
|
107
|
+
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
415
108
|
};
|
|
416
|
-
return canvas;
|
|
417
109
|
}
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
canvas = createMockCanvas();
|
|
421
|
-
}
|
|
422
|
-
return canvas.getContext("2d");
|
|
110
|
+
function makeNode(plane, children) {
|
|
111
|
+
return { plane, children };
|
|
423
112
|
}
|
|
424
|
-
function
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
"beginPath",
|
|
434
|
-
"closePath",
|
|
435
|
-
"moveTo",
|
|
436
|
-
"lineTo",
|
|
437
|
-
"arc",
|
|
438
|
-
"arcTo",
|
|
439
|
-
"bezierCurveTo",
|
|
440
|
-
"quadraticCurveTo",
|
|
441
|
-
"stroke",
|
|
442
|
-
"fill",
|
|
443
|
-
"putImageData"
|
|
444
|
-
];
|
|
445
|
-
methodsToSpy.forEach((method) => {
|
|
446
|
-
const original = context[method];
|
|
447
|
-
if (typeof original === "function") {
|
|
448
|
-
context[method] = function(...args) {
|
|
449
|
-
drawCalls.push({ method, args });
|
|
450
|
-
return original.apply(this, args);
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
return drawCalls;
|
|
113
|
+
function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
|
|
114
|
+
return {
|
|
115
|
+
planes,
|
|
116
|
+
nodes,
|
|
117
|
+
leaves,
|
|
118
|
+
brushes,
|
|
119
|
+
leafBrushes,
|
|
120
|
+
bmodels: []
|
|
121
|
+
};
|
|
455
122
|
}
|
|
456
|
-
function
|
|
457
|
-
|
|
458
|
-
if (fillColor) {
|
|
459
|
-
const [r, g, b, a] = fillColor;
|
|
460
|
-
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
461
|
-
imageData.data[i] = r;
|
|
462
|
-
imageData.data[i + 1] = g;
|
|
463
|
-
imageData.data[i + 2] = b;
|
|
464
|
-
imageData.data[i + 3] = a;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
return imageData;
|
|
123
|
+
function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
|
|
124
|
+
return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
|
|
468
125
|
}
|
|
469
|
-
function
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
126
|
+
function makeLeafModel(brushes) {
|
|
127
|
+
const planes = brushes.flatMap((brush) => brush.sides.map((side) => side.plane));
|
|
128
|
+
return {
|
|
129
|
+
planes,
|
|
130
|
+
nodes: [],
|
|
131
|
+
leaves: [makeLeaf(0, 0, brushes.length)],
|
|
132
|
+
brushes,
|
|
133
|
+
leafBrushes: brushes.map((_, i) => i),
|
|
134
|
+
bmodels: []
|
|
135
|
+
};
|
|
475
136
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
137
|
+
function makeBrushFromMinsMaxs(mins, maxs, contents = CONTENTS_SOLID) {
|
|
138
|
+
const planes = [
|
|
139
|
+
makePlane({ x: 1, y: 0, z: 0 }, maxs.x),
|
|
140
|
+
makePlane({ x: -1, y: 0, z: 0 }, -mins.x),
|
|
141
|
+
makePlane({ x: 0, y: 1, z: 0 }, maxs.y),
|
|
142
|
+
makePlane({ x: 0, y: -1, z: 0 }, -mins.y),
|
|
143
|
+
makePlane({ x: 0, y: 0, z: 1 }, maxs.z),
|
|
144
|
+
makePlane({ x: 0, y: 0, z: -1 }, -mins.z)
|
|
145
|
+
];
|
|
146
|
+
return {
|
|
147
|
+
contents,
|
|
148
|
+
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
149
|
+
};
|
|
481
150
|
}
|
|
482
151
|
|
|
483
|
-
// src/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
152
|
+
// src/game/factories.ts
|
|
153
|
+
var createPlayerStateFactory = (overrides) => ({
|
|
154
|
+
pm_type: 0,
|
|
155
|
+
pm_time: 0,
|
|
156
|
+
pm_flags: 0,
|
|
157
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
158
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
159
|
+
viewAngles: { x: 0, y: 0, z: 0 },
|
|
160
|
+
onGround: false,
|
|
161
|
+
waterLevel: 0,
|
|
162
|
+
watertype: 0,
|
|
163
|
+
mins: { x: 0, y: 0, z: 0 },
|
|
164
|
+
maxs: { x: 0, y: 0, z: 0 },
|
|
165
|
+
damageAlpha: 0,
|
|
166
|
+
damageIndicators: [],
|
|
167
|
+
blend: [0, 0, 0, 0],
|
|
168
|
+
stats: [],
|
|
169
|
+
kick_angles: { x: 0, y: 0, z: 0 },
|
|
170
|
+
kick_origin: { x: 0, y: 0, z: 0 },
|
|
171
|
+
gunoffset: { x: 0, y: 0, z: 0 },
|
|
172
|
+
gunangles: { x: 0, y: 0, z: 0 },
|
|
173
|
+
gunindex: 0,
|
|
174
|
+
gun_frame: 0,
|
|
175
|
+
rdflags: 0,
|
|
176
|
+
fov: 90,
|
|
177
|
+
renderfx: 0,
|
|
178
|
+
...overrides
|
|
179
|
+
});
|
|
180
|
+
var createEntityStateFactory = (overrides) => ({
|
|
181
|
+
number: 0,
|
|
182
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
183
|
+
angles: { x: 0, y: 0, z: 0 },
|
|
184
|
+
oldOrigin: { x: 0, y: 0, z: 0 },
|
|
185
|
+
modelIndex: 0,
|
|
186
|
+
modelIndex2: 0,
|
|
187
|
+
modelIndex3: 0,
|
|
188
|
+
modelIndex4: 0,
|
|
189
|
+
frame: 0,
|
|
190
|
+
skinNum: 0,
|
|
191
|
+
effects: 0,
|
|
192
|
+
renderfx: 0,
|
|
193
|
+
solid: 0,
|
|
194
|
+
sound: 0,
|
|
195
|
+
event: 0,
|
|
196
|
+
...overrides
|
|
197
|
+
});
|
|
198
|
+
var createGameStateSnapshotFactory = (overrides) => ({
|
|
199
|
+
gravity: { x: 0, y: 0, z: -800 },
|
|
200
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
201
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
202
|
+
viewangles: { x: 0, y: 0, z: 0 },
|
|
203
|
+
level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
|
|
204
|
+
entities: {
|
|
205
|
+
activeCount: 0,
|
|
206
|
+
worldClassname: "worldspawn"
|
|
207
|
+
},
|
|
208
|
+
packetEntities: [],
|
|
209
|
+
pmFlags: 0,
|
|
210
|
+
pmType: 0,
|
|
211
|
+
waterlevel: 0,
|
|
212
|
+
watertype: 0,
|
|
213
|
+
deltaAngles: { x: 0, y: 0, z: 0 },
|
|
214
|
+
health: 100,
|
|
215
|
+
armor: 0,
|
|
216
|
+
ammo: 0,
|
|
217
|
+
blend: [0, 0, 0, 0],
|
|
218
|
+
damageAlpha: 0,
|
|
219
|
+
damageIndicators: [],
|
|
220
|
+
stats: [],
|
|
221
|
+
kick_angles: { x: 0, y: 0, z: 0 },
|
|
222
|
+
kick_origin: { x: 0, y: 0, z: 0 },
|
|
223
|
+
gunoffset: { x: 0, y: 0, z: 0 },
|
|
224
|
+
gunangles: { x: 0, y: 0, z: 0 },
|
|
225
|
+
gunindex: 0,
|
|
226
|
+
pm_time: 0,
|
|
227
|
+
gun_frame: 0,
|
|
228
|
+
rdflags: 0,
|
|
229
|
+
fov: 90,
|
|
230
|
+
renderfx: 0,
|
|
231
|
+
pm_flags: 0,
|
|
232
|
+
pm_type: 0,
|
|
233
|
+
...overrides
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// src/game/helpers.ts
|
|
237
|
+
import { vi as vi2 } from "vitest";
|
|
238
|
+
import { Entity, SpawnRegistry, ScriptHookRegistry } from "@quake2ts/game";
|
|
239
|
+
import { createRandomGenerator } from "@quake2ts/shared";
|
|
240
|
+
import { intersects, stairTrace, ladderTrace } from "@quake2ts/shared";
|
|
241
|
+
var createMockEngine = () => ({
|
|
242
|
+
sound: vi2.fn(),
|
|
243
|
+
soundIndex: vi2.fn((sound) => 0),
|
|
244
|
+
modelIndex: vi2.fn((model) => 0),
|
|
245
|
+
centerprintf: vi2.fn()
|
|
246
|
+
});
|
|
247
|
+
var createMockGame = (seed = 12345) => {
|
|
248
|
+
const spawnRegistry = new SpawnRegistry();
|
|
249
|
+
const hooks = new ScriptHookRegistry();
|
|
250
|
+
const game = {
|
|
251
|
+
random: createRandomGenerator({ seed }),
|
|
252
|
+
registerEntitySpawn: vi2.fn((classname, spawnFunc) => {
|
|
253
|
+
spawnRegistry.register(classname, (entity) => spawnFunc(entity));
|
|
254
|
+
}),
|
|
255
|
+
unregisterEntitySpawn: vi2.fn((classname) => {
|
|
256
|
+
spawnRegistry.unregister(classname);
|
|
257
|
+
}),
|
|
258
|
+
getCustomEntities: vi2.fn(() => Array.from(spawnRegistry.keys())),
|
|
259
|
+
hooks,
|
|
260
|
+
registerHooks: vi2.fn((newHooks) => hooks.register(newHooks)),
|
|
261
|
+
spawnWorld: vi2.fn(() => {
|
|
262
|
+
hooks.onMapLoad("q2dm1");
|
|
263
|
+
}),
|
|
264
|
+
clientBegin: vi2.fn((client) => {
|
|
265
|
+
hooks.onPlayerSpawn({});
|
|
266
|
+
}),
|
|
267
|
+
damage: vi2.fn((amount) => {
|
|
268
|
+
hooks.onDamage({}, null, null, amount, 0, 0);
|
|
269
|
+
})
|
|
517
270
|
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
271
|
+
return { game, spawnRegistry };
|
|
272
|
+
};
|
|
273
|
+
function createTestContext(options) {
|
|
274
|
+
const engine = createMockEngine();
|
|
275
|
+
const seed = options?.seed ?? 12345;
|
|
276
|
+
const { game, spawnRegistry } = createMockGame(seed);
|
|
277
|
+
const traceFn = vi2.fn((start, end, mins, maxs) => ({
|
|
278
|
+
fraction: 1,
|
|
279
|
+
ent: null,
|
|
280
|
+
allsolid: false,
|
|
281
|
+
startsolid: false,
|
|
282
|
+
endpos: end,
|
|
283
|
+
plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
|
|
284
|
+
surfaceFlags: 0,
|
|
285
|
+
contents: 0
|
|
286
|
+
}));
|
|
287
|
+
const entityList = options?.initialEntities ? [...options.initialEntities] : [];
|
|
288
|
+
const hooks = game.hooks;
|
|
289
|
+
const entities = {
|
|
290
|
+
spawn: vi2.fn(() => {
|
|
291
|
+
const ent = new Entity(entityList.length + 1);
|
|
292
|
+
entityList.push(ent);
|
|
293
|
+
hooks.onEntitySpawn(ent);
|
|
294
|
+
return ent;
|
|
528
295
|
}),
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
},
|
|
536
|
-
frequency: { value: 440 }
|
|
296
|
+
free: vi2.fn((ent) => {
|
|
297
|
+
const idx = entityList.indexOf(ent);
|
|
298
|
+
if (idx !== -1) {
|
|
299
|
+
entityList.splice(idx, 1);
|
|
300
|
+
}
|
|
301
|
+
hooks.onEntityRemove(ent);
|
|
537
302
|
}),
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
},
|
|
545
|
-
buffer: null,
|
|
546
|
-
playbackRate: { value: 1 },
|
|
547
|
-
loop: false
|
|
303
|
+
finalizeSpawn: vi2.fn(),
|
|
304
|
+
freeImmediate: vi2.fn((ent) => {
|
|
305
|
+
const idx = entityList.indexOf(ent);
|
|
306
|
+
if (idx !== -1) {
|
|
307
|
+
entityList.splice(idx, 1);
|
|
308
|
+
}
|
|
548
309
|
}),
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
310
|
+
setSpawnRegistry: vi2.fn(),
|
|
311
|
+
timeSeconds: 10,
|
|
312
|
+
deltaSeconds: 0.1,
|
|
313
|
+
modelIndex: vi2.fn(() => 0),
|
|
314
|
+
scheduleThink: vi2.fn((entity, time) => {
|
|
315
|
+
entity.nextthink = time;
|
|
316
|
+
}),
|
|
317
|
+
linkentity: vi2.fn(),
|
|
318
|
+
trace: traceFn,
|
|
319
|
+
pointcontents: vi2.fn(() => 0),
|
|
320
|
+
multicast: vi2.fn(),
|
|
321
|
+
unicast: vi2.fn(),
|
|
322
|
+
engine,
|
|
323
|
+
game,
|
|
324
|
+
sound: vi2.fn((ent, chan, sound, vol, attn, timeofs) => {
|
|
325
|
+
engine.sound(ent, chan, sound, vol, attn, timeofs);
|
|
326
|
+
}),
|
|
327
|
+
soundIndex: vi2.fn((sound) => engine.soundIndex(sound)),
|
|
328
|
+
useTargets: vi2.fn((entity, activator) => {
|
|
329
|
+
}),
|
|
330
|
+
findByTargetName: vi2.fn(() => []),
|
|
331
|
+
pickTarget: vi2.fn(() => null),
|
|
332
|
+
killBox: vi2.fn(),
|
|
333
|
+
rng: createRandomGenerator({ seed }),
|
|
334
|
+
imports: {
|
|
335
|
+
configstring: vi2.fn(),
|
|
336
|
+
trace: traceFn,
|
|
337
|
+
pointcontents: vi2.fn(() => 0)
|
|
555
338
|
},
|
|
556
|
-
|
|
339
|
+
level: {
|
|
340
|
+
intermission_angle: { x: 0, y: 0, z: 0 },
|
|
341
|
+
intermission_origin: { x: 0, y: 0, z: 0 },
|
|
342
|
+
next_auto_save: 0,
|
|
343
|
+
health_bar_entities: null
|
|
557
344
|
},
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
sampleRate: 44100,
|
|
562
|
-
numberOfChannels: 2,
|
|
563
|
-
getChannelData: () => new Float32Array(44100)
|
|
345
|
+
targetNameIndex: /* @__PURE__ */ new Map(),
|
|
346
|
+
forEachEntity: vi2.fn((callback) => {
|
|
347
|
+
entityList.forEach(callback);
|
|
564
348
|
}),
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
349
|
+
find: vi2.fn((predicate) => {
|
|
350
|
+
return entityList.find(predicate);
|
|
351
|
+
}),
|
|
352
|
+
findByClassname: vi2.fn((classname) => {
|
|
353
|
+
return entityList.find((e) => e.classname === classname);
|
|
354
|
+
}),
|
|
355
|
+
beginFrame: vi2.fn((timeSeconds) => {
|
|
356
|
+
entities.timeSeconds = timeSeconds;
|
|
357
|
+
}),
|
|
358
|
+
targetAwareness: {
|
|
359
|
+
timeSeconds: 10,
|
|
360
|
+
frameNumber: 1,
|
|
361
|
+
sightEntity: null,
|
|
362
|
+
soundEntity: null
|
|
363
|
+
},
|
|
364
|
+
// Adding missing properties to satisfy EntitySystem interface partially or fully
|
|
365
|
+
// We cast to unknown first anyway, but filling these in makes it safer for consumers
|
|
366
|
+
skill: 1,
|
|
367
|
+
deathmatch: false,
|
|
368
|
+
coop: false,
|
|
369
|
+
activeCount: entityList.length,
|
|
370
|
+
world: entityList.find((e) => e.classname === "worldspawn") || new Entity(0)
|
|
371
|
+
// ... other EntitySystem properties would go here
|
|
372
|
+
};
|
|
373
|
+
return {
|
|
374
|
+
keyValues: {},
|
|
375
|
+
entities,
|
|
376
|
+
game,
|
|
377
|
+
engine,
|
|
378
|
+
health_multiplier: 1,
|
|
379
|
+
warn: vi2.fn(),
|
|
380
|
+
free: vi2.fn(),
|
|
381
|
+
// Mock precache functions if they are part of SpawnContext in future or TestContext extensions
|
|
382
|
+
precacheModel: vi2.fn(),
|
|
383
|
+
precacheSound: vi2.fn(),
|
|
384
|
+
precacheImage: vi2.fn()
|
|
572
385
|
};
|
|
573
386
|
}
|
|
574
|
-
function
|
|
575
|
-
|
|
576
|
-
global.AudioContext = class {
|
|
577
|
-
constructor() {
|
|
578
|
-
return createMockAudioContext();
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
global.window.AudioContext = global.AudioContext;
|
|
582
|
-
global.window.webkitAudioContext = global.AudioContext;
|
|
583
|
-
}
|
|
387
|
+
function createSpawnContext() {
|
|
388
|
+
return createTestContext();
|
|
584
389
|
}
|
|
585
|
-
function
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
390
|
+
function createEntity() {
|
|
391
|
+
return new Entity(1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/game/mocks.ts
|
|
395
|
+
function createMockGameState(overrides) {
|
|
396
|
+
return {
|
|
397
|
+
levelName: "test_level",
|
|
398
|
+
time: 0,
|
|
399
|
+
entities: [],
|
|
400
|
+
clients: [],
|
|
401
|
+
...overrides
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/server/mocks/transport.ts
|
|
406
|
+
import { vi as vi3 } from "vitest";
|
|
407
|
+
var MockTransport = class {
|
|
408
|
+
constructor() {
|
|
409
|
+
this.address = "127.0.0.1";
|
|
410
|
+
this.port = 27910;
|
|
411
|
+
this.sentMessages = [];
|
|
412
|
+
this.receivedMessages = [];
|
|
413
|
+
this.listening = false;
|
|
414
|
+
this.listenSpy = vi3.fn().mockImplementation(async (port) => {
|
|
415
|
+
this.port = port;
|
|
416
|
+
this.listening = true;
|
|
417
|
+
});
|
|
418
|
+
this.closeSpy = vi3.fn().mockImplementation(() => {
|
|
419
|
+
this.listening = false;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Start listening on the specified port.
|
|
424
|
+
*/
|
|
425
|
+
async listen(port) {
|
|
426
|
+
return this.listenSpy(port);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Close the transport.
|
|
430
|
+
*/
|
|
431
|
+
close() {
|
|
432
|
+
this.closeSpy();
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Register a callback for new connections.
|
|
436
|
+
*/
|
|
437
|
+
onConnection(callback) {
|
|
438
|
+
this.onConnectionCallback = callback;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Register a callback for errors.
|
|
442
|
+
*/
|
|
443
|
+
onError(callback) {
|
|
444
|
+
this.onErrorCallback = callback;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Check if the transport is currently listening.
|
|
448
|
+
*/
|
|
449
|
+
isListening() {
|
|
450
|
+
return this.listening;
|
|
590
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Helper to simulate a new connection.
|
|
454
|
+
* @param driver The network driver for the connection.
|
|
455
|
+
* @param info Optional connection info.
|
|
456
|
+
*/
|
|
457
|
+
simulateConnection(driver, info) {
|
|
458
|
+
if (this.onConnectionCallback) {
|
|
459
|
+
this.onConnectionCallback(driver, info);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Helper to simulate an error.
|
|
464
|
+
* @param error The error to simulate.
|
|
465
|
+
*/
|
|
466
|
+
simulateError(error) {
|
|
467
|
+
if (this.onErrorCallback) {
|
|
468
|
+
this.onErrorCallback(error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
function createMockUDPSocket(overrides) {
|
|
473
|
+
const socket = {
|
|
474
|
+
send: vi3.fn(),
|
|
475
|
+
on: vi3.fn(),
|
|
476
|
+
close: vi3.fn(),
|
|
477
|
+
bind: vi3.fn(),
|
|
478
|
+
address: vi3.fn().mockReturnValue({ address: "127.0.0.1", family: "IPv4", port: 0 }),
|
|
479
|
+
...overrides
|
|
480
|
+
};
|
|
481
|
+
return socket;
|
|
591
482
|
}
|
|
592
|
-
function
|
|
593
|
-
return
|
|
483
|
+
function createMockNetworkAddress(ip = "127.0.0.1", port = 27910) {
|
|
484
|
+
return { ip, port };
|
|
485
|
+
}
|
|
486
|
+
function createMockTransport(address = "127.0.0.1", port = 27910, overrides) {
|
|
487
|
+
const transport = new MockTransport();
|
|
488
|
+
transport.address = address;
|
|
489
|
+
transport.port = port;
|
|
490
|
+
Object.assign(transport, overrides);
|
|
491
|
+
return transport;
|
|
594
492
|
}
|
|
595
493
|
|
|
596
|
-
// src/
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
494
|
+
// src/server/mocks/state.ts
|
|
495
|
+
import { ServerState, ClientState } from "@quake2ts/server";
|
|
496
|
+
import { MAX_CONFIGSTRINGS, MAX_EDICTS } from "@quake2ts/shared";
|
|
497
|
+
import { vi as vi4 } from "vitest";
|
|
498
|
+
function createMockServerState(overrides) {
|
|
499
|
+
return {
|
|
500
|
+
state: ServerState.Game,
|
|
501
|
+
attractLoop: false,
|
|
502
|
+
loadGame: false,
|
|
503
|
+
startTime: Date.now(),
|
|
504
|
+
time: 0,
|
|
505
|
+
frame: 0,
|
|
506
|
+
name: "test_map",
|
|
507
|
+
collisionModel: null,
|
|
508
|
+
configStrings: new Array(MAX_CONFIGSTRINGS).fill(""),
|
|
509
|
+
baselines: new Array(MAX_EDICTS).fill(null),
|
|
510
|
+
multicastBuf: new Uint8Array(0),
|
|
511
|
+
...overrides
|
|
608
512
|
};
|
|
609
|
-
|
|
610
|
-
|
|
513
|
+
}
|
|
514
|
+
function createMockServerStatic(maxClients = 16, overrides) {
|
|
515
|
+
return {
|
|
516
|
+
initialized: true,
|
|
517
|
+
realTime: Date.now(),
|
|
518
|
+
mapCmd: "",
|
|
519
|
+
spawnCount: 1,
|
|
520
|
+
clients: new Array(maxClients).fill(null),
|
|
521
|
+
lastHeartbeat: 0,
|
|
522
|
+
challenges: [],
|
|
523
|
+
...overrides
|
|
611
524
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
525
|
+
}
|
|
526
|
+
function createMockServerClient(clientNum, overrides) {
|
|
527
|
+
const mockNet = {
|
|
528
|
+
connect: vi4.fn(),
|
|
529
|
+
disconnect: vi4.fn(),
|
|
530
|
+
send: vi4.fn(),
|
|
531
|
+
onMessage: vi4.fn(),
|
|
532
|
+
onClose: vi4.fn(),
|
|
533
|
+
onError: vi4.fn(),
|
|
534
|
+
isConnected: vi4.fn().mockReturnValue(true)
|
|
535
|
+
};
|
|
536
|
+
return {
|
|
537
|
+
index: clientNum,
|
|
538
|
+
state: ClientState.Connected,
|
|
539
|
+
edict: { index: clientNum + 1 },
|
|
540
|
+
net: mockNet,
|
|
541
|
+
netchan: {
|
|
542
|
+
qport: 0,
|
|
543
|
+
remoteAddress: "127.0.0.1",
|
|
544
|
+
incomingSequence: 0,
|
|
545
|
+
outgoingSequence: 0,
|
|
546
|
+
lastReceived: 0,
|
|
547
|
+
process: vi4.fn(),
|
|
548
|
+
transmit: vi4.fn(),
|
|
549
|
+
writeReliableByte: vi4.fn(),
|
|
550
|
+
writeReliableShort: vi4.fn(),
|
|
551
|
+
writeReliableLong: vi4.fn(),
|
|
552
|
+
writeReliableString: vi4.fn(),
|
|
553
|
+
writeReliableData: vi4.fn()
|
|
554
|
+
},
|
|
555
|
+
// Cast as any because NetChan might be complex to fully mock here
|
|
556
|
+
userInfo: "",
|
|
557
|
+
lastMessage: 0,
|
|
558
|
+
lastCommandTime: 0,
|
|
559
|
+
commandCount: 0,
|
|
560
|
+
messageQueue: [],
|
|
561
|
+
frames: [],
|
|
562
|
+
lastFrame: 0,
|
|
563
|
+
lastPacketEntities: [],
|
|
564
|
+
challenge: 0,
|
|
565
|
+
lastConnect: 0,
|
|
566
|
+
ping: 0,
|
|
567
|
+
rate: 0,
|
|
568
|
+
name: `Client${clientNum}`,
|
|
569
|
+
messageLevel: 0,
|
|
570
|
+
datagram: new Uint8Array(0),
|
|
571
|
+
downloadSize: 0,
|
|
572
|
+
downloadCount: 0,
|
|
573
|
+
commandMsec: 0,
|
|
574
|
+
frameLatency: [],
|
|
575
|
+
messageSize: [],
|
|
576
|
+
suppressCount: 0,
|
|
577
|
+
commandQueue: [],
|
|
578
|
+
lastCmd: {
|
|
579
|
+
msec: 0,
|
|
580
|
+
buttons: 0,
|
|
581
|
+
angles: { x: 0, y: 0, z: 0 },
|
|
582
|
+
forwardmove: 0,
|
|
583
|
+
sidemove: 0,
|
|
584
|
+
upmove: 0,
|
|
585
|
+
sequence: 0,
|
|
586
|
+
lightlevel: 0,
|
|
587
|
+
impulse: 0,
|
|
588
|
+
serverFrame: 0
|
|
589
|
+
},
|
|
590
|
+
...overrides
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function createMockServer(overrides) {
|
|
594
|
+
return {
|
|
595
|
+
start: vi4.fn().mockResolvedValue(void 0),
|
|
596
|
+
stop: vi4.fn(),
|
|
597
|
+
multicast: vi4.fn(),
|
|
598
|
+
unicast: vi4.fn(),
|
|
599
|
+
configstring: vi4.fn(),
|
|
600
|
+
kickPlayer: vi4.fn(),
|
|
601
|
+
changeMap: vi4.fn().mockResolvedValue(void 0),
|
|
602
|
+
...overrides
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/setup/browser.ts
|
|
607
|
+
import { JSDOM } from "jsdom";
|
|
608
|
+
import { Canvas, Image, ImageData } from "@napi-rs/canvas";
|
|
609
|
+
import "fake-indexeddb/auto";
|
|
610
|
+
|
|
611
|
+
// src/e2e/input.ts
|
|
612
|
+
var MockPointerLock = class {
|
|
613
|
+
static setup(doc) {
|
|
614
|
+
let _pointerLockElement = null;
|
|
615
|
+
Object.defineProperty(doc, "pointerLockElement", {
|
|
616
|
+
get: () => _pointerLockElement,
|
|
617
|
+
configurable: true
|
|
618
|
+
});
|
|
619
|
+
doc.exitPointerLock = () => {
|
|
620
|
+
if (_pointerLockElement) {
|
|
621
|
+
_pointerLockElement = null;
|
|
622
|
+
doc.dispatchEvent(new Event("pointerlockchange"));
|
|
618
623
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
+
};
|
|
625
|
+
global.HTMLElement.prototype.requestPointerLock = function() {
|
|
626
|
+
_pointerLockElement = this;
|
|
627
|
+
doc.dispatchEvent(new Event("pointerlockchange"));
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
var InputInjector = class {
|
|
632
|
+
constructor(doc, win) {
|
|
633
|
+
this.doc = doc;
|
|
634
|
+
this.win = win;
|
|
635
|
+
}
|
|
636
|
+
keyDown(key, code) {
|
|
637
|
+
const event = new this.win.KeyboardEvent("keydown", {
|
|
638
|
+
key,
|
|
639
|
+
code: code || key,
|
|
640
|
+
bubbles: true,
|
|
641
|
+
cancelable: true,
|
|
642
|
+
view: this.win
|
|
643
|
+
});
|
|
644
|
+
this.doc.dispatchEvent(event);
|
|
645
|
+
}
|
|
646
|
+
keyUp(key, code) {
|
|
647
|
+
const event = new this.win.KeyboardEvent("keyup", {
|
|
648
|
+
key,
|
|
649
|
+
code: code || key,
|
|
650
|
+
bubbles: true,
|
|
651
|
+
cancelable: true,
|
|
652
|
+
view: this.win
|
|
653
|
+
});
|
|
654
|
+
this.doc.dispatchEvent(event);
|
|
655
|
+
}
|
|
656
|
+
mouseMove(movementX, movementY, clientX = 0, clientY = 0) {
|
|
657
|
+
const event = new this.win.MouseEvent("mousemove", {
|
|
658
|
+
bubbles: true,
|
|
659
|
+
cancelable: true,
|
|
660
|
+
view: this.win,
|
|
661
|
+
clientX,
|
|
662
|
+
clientY,
|
|
663
|
+
movementX,
|
|
664
|
+
// Note: JSDOM might not support this standard property fully on event init
|
|
665
|
+
movementY
|
|
666
|
+
});
|
|
667
|
+
Object.defineProperty(event, "movementX", { value: movementX });
|
|
668
|
+
Object.defineProperty(event, "movementY", { value: movementY });
|
|
669
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
670
|
+
target.dispatchEvent(event);
|
|
671
|
+
}
|
|
672
|
+
mouseDown(button = 0) {
|
|
673
|
+
const event = new this.win.MouseEvent("mousedown", {
|
|
674
|
+
button,
|
|
675
|
+
bubbles: true,
|
|
676
|
+
cancelable: true,
|
|
677
|
+
view: this.win
|
|
678
|
+
});
|
|
679
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
680
|
+
target.dispatchEvent(event);
|
|
681
|
+
}
|
|
682
|
+
mouseUp(button = 0) {
|
|
683
|
+
const event = new this.win.MouseEvent("mouseup", {
|
|
684
|
+
button,
|
|
685
|
+
bubbles: true,
|
|
686
|
+
cancelable: true,
|
|
687
|
+
view: this.win
|
|
688
|
+
});
|
|
689
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
690
|
+
target.dispatchEvent(event);
|
|
691
|
+
}
|
|
692
|
+
wheel(deltaY) {
|
|
693
|
+
const event = new this.win.WheelEvent("wheel", {
|
|
694
|
+
deltaY,
|
|
695
|
+
bubbles: true,
|
|
696
|
+
cancelable: true,
|
|
697
|
+
view: this.win
|
|
698
|
+
});
|
|
699
|
+
const target = this.doc.pointerLockElement || this.doc;
|
|
700
|
+
target.dispatchEvent(event);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
// src/setup/webgl.ts
|
|
705
|
+
function createMockWebGL2Context(canvas) {
|
|
706
|
+
const gl = {
|
|
707
|
+
canvas,
|
|
708
|
+
drawingBufferWidth: canvas.width,
|
|
709
|
+
drawingBufferHeight: canvas.height,
|
|
710
|
+
// Constants
|
|
711
|
+
VERTEX_SHADER: 35633,
|
|
712
|
+
FRAGMENT_SHADER: 35632,
|
|
713
|
+
COMPILE_STATUS: 35713,
|
|
714
|
+
LINK_STATUS: 35714,
|
|
715
|
+
ARRAY_BUFFER: 34962,
|
|
716
|
+
ELEMENT_ARRAY_BUFFER: 34963,
|
|
717
|
+
STATIC_DRAW: 35044,
|
|
718
|
+
DYNAMIC_DRAW: 35048,
|
|
719
|
+
FLOAT: 5126,
|
|
720
|
+
DEPTH_TEST: 2929,
|
|
721
|
+
BLEND: 3042,
|
|
722
|
+
SRC_ALPHA: 770,
|
|
723
|
+
ONE_MINUS_SRC_ALPHA: 771,
|
|
724
|
+
TEXTURE_2D: 3553,
|
|
725
|
+
RGBA: 6408,
|
|
726
|
+
UNSIGNED_BYTE: 5121,
|
|
727
|
+
COLOR_BUFFER_BIT: 16384,
|
|
728
|
+
DEPTH_BUFFER_BIT: 256,
|
|
729
|
+
TRIANGLES: 4,
|
|
730
|
+
TRIANGLE_STRIP: 5,
|
|
731
|
+
TRIANGLE_FAN: 6,
|
|
732
|
+
// Methods
|
|
733
|
+
createShader: () => ({}),
|
|
734
|
+
shaderSource: () => {
|
|
735
|
+
},
|
|
736
|
+
compileShader: () => {
|
|
737
|
+
},
|
|
738
|
+
getShaderParameter: (_, param) => {
|
|
739
|
+
if (param === 35713) return true;
|
|
740
|
+
return true;
|
|
741
|
+
},
|
|
742
|
+
getShaderInfoLog: () => "",
|
|
743
|
+
createProgram: () => ({}),
|
|
744
|
+
attachShader: () => {
|
|
745
|
+
},
|
|
746
|
+
linkProgram: () => {
|
|
747
|
+
},
|
|
748
|
+
getProgramParameter: (_, param) => {
|
|
749
|
+
if (param === 35714) return true;
|
|
750
|
+
return true;
|
|
751
|
+
},
|
|
752
|
+
getProgramInfoLog: () => "",
|
|
753
|
+
useProgram: () => {
|
|
754
|
+
},
|
|
755
|
+
createBuffer: () => ({}),
|
|
756
|
+
bindBuffer: () => {
|
|
757
|
+
},
|
|
758
|
+
bufferData: () => {
|
|
759
|
+
},
|
|
760
|
+
enableVertexAttribArray: () => {
|
|
761
|
+
},
|
|
762
|
+
vertexAttribPointer: () => {
|
|
763
|
+
},
|
|
764
|
+
enable: () => {
|
|
765
|
+
},
|
|
766
|
+
disable: () => {
|
|
767
|
+
},
|
|
768
|
+
depthMask: () => {
|
|
769
|
+
},
|
|
770
|
+
blendFunc: () => {
|
|
771
|
+
},
|
|
772
|
+
viewport: () => {
|
|
773
|
+
},
|
|
774
|
+
clearColor: () => {
|
|
775
|
+
},
|
|
776
|
+
clear: () => {
|
|
624
777
|
},
|
|
625
|
-
|
|
626
|
-
|
|
778
|
+
createTexture: () => ({}),
|
|
779
|
+
bindTexture: () => {
|
|
627
780
|
},
|
|
628
|
-
|
|
629
|
-
return callbacks.map((c) => c.callback);
|
|
781
|
+
texImage2D: () => {
|
|
630
782
|
},
|
|
631
|
-
|
|
632
|
-
callbacks = [];
|
|
633
|
-
nextId = 1;
|
|
634
|
-
currentTime = 0;
|
|
783
|
+
texParameteri: () => {
|
|
635
784
|
},
|
|
636
|
-
|
|
637
|
-
activeMockRAF = this;
|
|
638
|
-
global.requestAnimationFrame = raf;
|
|
639
|
-
global.cancelAnimationFrame = cancel;
|
|
785
|
+
activeTexture: () => {
|
|
640
786
|
},
|
|
641
|
-
|
|
642
|
-
if (activeMockRAF === this) {
|
|
643
|
-
activeMockRAF = void 0;
|
|
644
|
-
}
|
|
645
|
-
if (originalRAF) {
|
|
646
|
-
global.requestAnimationFrame = originalRAF;
|
|
647
|
-
} else {
|
|
648
|
-
delete global.requestAnimationFrame;
|
|
649
|
-
}
|
|
650
|
-
if (originalCancelRAF) {
|
|
651
|
-
global.cancelAnimationFrame = originalCancelRAF;
|
|
652
|
-
} else {
|
|
653
|
-
delete global.cancelAnimationFrame;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
return mock;
|
|
658
|
-
}
|
|
659
|
-
function createMockPerformance(startTime = 0) {
|
|
660
|
-
let currentTime = startTime;
|
|
661
|
-
const mockPerf = {
|
|
662
|
-
now: () => currentTime,
|
|
663
|
-
timeOrigin: startTime,
|
|
664
|
-
timing: {
|
|
665
|
-
navigationStart: startTime
|
|
787
|
+
uniform1i: () => {
|
|
666
788
|
},
|
|
667
|
-
|
|
789
|
+
uniform1f: () => {
|
|
668
790
|
},
|
|
669
|
-
|
|
791
|
+
uniform2f: () => {
|
|
670
792
|
},
|
|
671
|
-
|
|
793
|
+
uniform3f: () => {
|
|
672
794
|
},
|
|
673
|
-
|
|
674
|
-
getEntriesByName: () => [],
|
|
675
|
-
getEntriesByType: () => [],
|
|
676
|
-
mark: () => {
|
|
795
|
+
uniform4f: () => {
|
|
677
796
|
},
|
|
678
|
-
|
|
797
|
+
uniformMatrix4fv: () => {
|
|
679
798
|
},
|
|
680
|
-
|
|
799
|
+
getUniformLocation: () => ({}),
|
|
800
|
+
getAttribLocation: () => 0,
|
|
801
|
+
drawArrays: () => {
|
|
681
802
|
},
|
|
682
|
-
|
|
683
|
-
addEventListener: () => {
|
|
803
|
+
drawElements: () => {
|
|
684
804
|
},
|
|
685
|
-
|
|
805
|
+
createVertexArray: () => ({}),
|
|
806
|
+
bindVertexArray: () => {
|
|
686
807
|
},
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
808
|
+
deleteShader: () => {
|
|
809
|
+
},
|
|
810
|
+
deleteProgram: () => {
|
|
811
|
+
},
|
|
812
|
+
deleteBuffer: () => {
|
|
813
|
+
},
|
|
814
|
+
deleteTexture: () => {
|
|
815
|
+
},
|
|
816
|
+
deleteVertexArray: () => {
|
|
817
|
+
},
|
|
818
|
+
// WebGL2 specific
|
|
819
|
+
texImage3D: () => {
|
|
820
|
+
},
|
|
821
|
+
uniformBlockBinding: () => {
|
|
822
|
+
},
|
|
823
|
+
getExtension: () => null
|
|
694
824
|
};
|
|
695
|
-
return
|
|
825
|
+
return gl;
|
|
696
826
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
827
|
+
|
|
828
|
+
// src/setup/browser.ts
|
|
829
|
+
function setupBrowserEnvironment(options = {}) {
|
|
830
|
+
const {
|
|
831
|
+
url = "http://localhost",
|
|
832
|
+
pretendToBeVisual = true,
|
|
833
|
+
resources = void 0,
|
|
834
|
+
enableWebGL2 = false,
|
|
835
|
+
enablePointerLock = false
|
|
836
|
+
} = options;
|
|
837
|
+
const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
|
|
838
|
+
url,
|
|
839
|
+
pretendToBeVisual,
|
|
840
|
+
resources
|
|
841
|
+
});
|
|
842
|
+
global.window = dom.window;
|
|
843
|
+
global.document = dom.window.document;
|
|
844
|
+
try {
|
|
845
|
+
global.navigator = dom.window.navigator;
|
|
846
|
+
} catch (e) {
|
|
847
|
+
try {
|
|
848
|
+
Object.defineProperty(global, "navigator", {
|
|
849
|
+
value: dom.window.navigator,
|
|
850
|
+
writable: true,
|
|
851
|
+
configurable: true
|
|
852
|
+
});
|
|
853
|
+
} catch (e2) {
|
|
854
|
+
console.warn("Could not assign global.navigator, skipping.");
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
global.location = dom.window.location;
|
|
858
|
+
global.HTMLElement = dom.window.HTMLElement;
|
|
859
|
+
global.HTMLCanvasElement = dom.window.HTMLCanvasElement;
|
|
860
|
+
global.Event = dom.window.Event;
|
|
861
|
+
global.CustomEvent = dom.window.CustomEvent;
|
|
862
|
+
global.DragEvent = dom.window.DragEvent;
|
|
863
|
+
global.MouseEvent = dom.window.MouseEvent;
|
|
864
|
+
global.KeyboardEvent = dom.window.KeyboardEvent;
|
|
865
|
+
global.FocusEvent = dom.window.FocusEvent;
|
|
866
|
+
global.WheelEvent = dom.window.WheelEvent;
|
|
867
|
+
global.InputEvent = dom.window.InputEvent;
|
|
868
|
+
global.UIEvent = dom.window.UIEvent;
|
|
869
|
+
try {
|
|
870
|
+
global.localStorage = dom.window.localStorage;
|
|
871
|
+
} catch (e) {
|
|
872
|
+
}
|
|
873
|
+
if (!global.localStorage) {
|
|
874
|
+
const storage = /* @__PURE__ */ new Map();
|
|
875
|
+
global.localStorage = {
|
|
876
|
+
getItem: (key) => storage.get(key) || null,
|
|
877
|
+
setItem: (key, value) => storage.set(key, value),
|
|
878
|
+
removeItem: (key) => storage.delete(key),
|
|
879
|
+
clear: () => storage.clear(),
|
|
880
|
+
key: (index) => Array.from(storage.keys())[index] || null,
|
|
881
|
+
get length() {
|
|
882
|
+
return storage.size;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const originalCreateElement = document.createElement.bind(document);
|
|
887
|
+
document.createElement = function(tagName, options2) {
|
|
888
|
+
if (tagName.toLowerCase() === "canvas") {
|
|
889
|
+
const napiCanvas = new Canvas(300, 150);
|
|
890
|
+
const domCanvas = originalCreateElement("canvas", options2);
|
|
891
|
+
Object.defineProperty(domCanvas, "width", {
|
|
892
|
+
get: () => napiCanvas.width,
|
|
893
|
+
set: (value) => {
|
|
894
|
+
napiCanvas.width = value;
|
|
895
|
+
},
|
|
896
|
+
enumerable: true,
|
|
897
|
+
configurable: true
|
|
898
|
+
});
|
|
899
|
+
Object.defineProperty(domCanvas, "height", {
|
|
900
|
+
get: () => napiCanvas.height,
|
|
901
|
+
set: (value) => {
|
|
902
|
+
napiCanvas.height = value;
|
|
903
|
+
},
|
|
904
|
+
enumerable: true,
|
|
905
|
+
configurable: true
|
|
906
|
+
});
|
|
907
|
+
const originalGetContext = domCanvas.getContext.bind(domCanvas);
|
|
908
|
+
domCanvas.getContext = function(contextId, options3) {
|
|
909
|
+
if (contextId === "2d") {
|
|
910
|
+
return napiCanvas.getContext("2d", options3);
|
|
737
911
|
}
|
|
738
|
-
if (
|
|
739
|
-
|
|
912
|
+
if (enableWebGL2 && contextId === "webgl2") {
|
|
913
|
+
return createMockWebGL2Context(domCanvas);
|
|
740
914
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if (interval !== void 0) {
|
|
744
|
-
earliest.dueTime += interval;
|
|
745
|
-
if (interval === 0) earliest.dueTime += 1;
|
|
746
|
-
} else {
|
|
747
|
-
timers = timers.filter((t) => t.id !== id);
|
|
915
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
916
|
+
return originalGetContext(contextId, options3);
|
|
748
917
|
}
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
clear() {
|
|
754
|
-
timers = [];
|
|
755
|
-
},
|
|
756
|
-
restore() {
|
|
757
|
-
global.setTimeout = originalSetTimeout;
|
|
758
|
-
global.clearTimeout = originalClearTimeout;
|
|
759
|
-
global.setInterval = originalSetInterval;
|
|
760
|
-
global.clearInterval = originalClearInterval;
|
|
918
|
+
return napiCanvas.getContext(contextId, options3);
|
|
919
|
+
};
|
|
920
|
+
domCanvas.__napiCanvas = napiCanvas;
|
|
921
|
+
return domCanvas;
|
|
761
922
|
}
|
|
923
|
+
return originalCreateElement(tagName, options2);
|
|
762
924
|
};
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
925
|
+
if (enableWebGL2) {
|
|
926
|
+
const originalProtoGetContext = global.HTMLCanvasElement.prototype.getContext;
|
|
927
|
+
global.HTMLCanvasElement.prototype.getContext = function(contextId, options2) {
|
|
928
|
+
if (contextId === "webgl2") {
|
|
929
|
+
return createMockWebGL2Context(this);
|
|
930
|
+
}
|
|
931
|
+
return originalProtoGetContext.call(this, contextId, options2);
|
|
932
|
+
};
|
|
767
933
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
934
|
+
global.Image = Image;
|
|
935
|
+
global.ImageData = ImageData;
|
|
936
|
+
if (typeof global.createImageBitmap === "undefined") {
|
|
937
|
+
global.createImageBitmap = async function(image, _options) {
|
|
938
|
+
if (image && typeof image.width === "number" && typeof image.height === "number") {
|
|
939
|
+
const canvas2 = new Canvas(image.width, image.height);
|
|
940
|
+
const ctx = canvas2.getContext("2d");
|
|
941
|
+
if (image.data) {
|
|
942
|
+
ctx.putImageData(image, 0, 0);
|
|
943
|
+
}
|
|
944
|
+
return canvas2;
|
|
945
|
+
}
|
|
946
|
+
const canvas = new Canvas(100, 100);
|
|
947
|
+
return canvas;
|
|
948
|
+
};
|
|
771
949
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
950
|
+
if (typeof global.btoa === "undefined") {
|
|
951
|
+
global.btoa = function(str) {
|
|
952
|
+
return Buffer.from(str, "binary").toString("base64");
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (typeof global.atob === "undefined") {
|
|
956
|
+
global.atob = function(str) {
|
|
957
|
+
return Buffer.from(str, "base64").toString("binary");
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (enablePointerLock) {
|
|
961
|
+
MockPointerLock.setup(global.document);
|
|
962
|
+
}
|
|
963
|
+
if (typeof global.requestAnimationFrame === "undefined") {
|
|
964
|
+
let lastTime = 0;
|
|
965
|
+
global.requestAnimationFrame = (callback) => {
|
|
966
|
+
const currTime = Date.now();
|
|
967
|
+
const timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
|
968
|
+
const id = setTimeout(() => {
|
|
969
|
+
callback(currTime + timeToCall);
|
|
970
|
+
}, timeToCall);
|
|
971
|
+
lastTime = currTime + timeToCall;
|
|
972
|
+
return id;
|
|
973
|
+
};
|
|
974
|
+
global.cancelAnimationFrame = (id) => {
|
|
975
|
+
clearTimeout(id);
|
|
976
|
+
};
|
|
777
977
|
}
|
|
778
978
|
}
|
|
979
|
+
function teardownBrowserEnvironment() {
|
|
980
|
+
delete global.window;
|
|
981
|
+
delete global.document;
|
|
982
|
+
delete global.navigator;
|
|
983
|
+
delete global.localStorage;
|
|
984
|
+
delete global.location;
|
|
985
|
+
delete global.HTMLElement;
|
|
986
|
+
delete global.HTMLCanvasElement;
|
|
987
|
+
delete global.Image;
|
|
988
|
+
delete global.ImageData;
|
|
989
|
+
delete global.createImageBitmap;
|
|
990
|
+
delete global.Event;
|
|
991
|
+
delete global.CustomEvent;
|
|
992
|
+
delete global.DragEvent;
|
|
993
|
+
delete global.MouseEvent;
|
|
994
|
+
delete global.KeyboardEvent;
|
|
995
|
+
delete global.FocusEvent;
|
|
996
|
+
delete global.WheelEvent;
|
|
997
|
+
delete global.InputEvent;
|
|
998
|
+
delete global.UIEvent;
|
|
999
|
+
}
|
|
779
1000
|
|
|
780
|
-
// src/
|
|
781
|
-
import {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (!clientUrl) {
|
|
789
|
-
staticServer = createServer((request, response) => {
|
|
790
|
-
return handler(request, response, {
|
|
791
|
-
public: rootPath,
|
|
792
|
-
cleanUrls: false,
|
|
793
|
-
headers: [
|
|
794
|
-
{
|
|
795
|
-
source: "**/*",
|
|
796
|
-
headers: [
|
|
797
|
-
{ key: "Cache-Control", value: "no-cache" },
|
|
798
|
-
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
799
|
-
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
|
800
|
-
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
|
|
801
|
-
]
|
|
802
|
-
}
|
|
803
|
-
]
|
|
804
|
-
});
|
|
805
|
-
});
|
|
806
|
-
await new Promise((resolve) => {
|
|
807
|
-
if (!staticServer) return;
|
|
808
|
-
staticServer.listen(0, () => {
|
|
809
|
-
const addr = staticServer?.address();
|
|
810
|
-
const port = typeof addr === "object" ? addr?.port : 0;
|
|
811
|
-
clientUrl = `http://localhost:${port}`;
|
|
812
|
-
console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
|
|
813
|
-
resolve();
|
|
814
|
-
});
|
|
815
|
-
});
|
|
1001
|
+
// src/setup/canvas.ts
|
|
1002
|
+
import { Canvas as Canvas2, Image as Image2, ImageData as ImageData2 } from "@napi-rs/canvas";
|
|
1003
|
+
function createMockCanvas(width = 300, height = 150) {
|
|
1004
|
+
if (typeof document !== "undefined" && document.createElement) {
|
|
1005
|
+
const canvas2 = document.createElement("canvas");
|
|
1006
|
+
canvas2.width = width;
|
|
1007
|
+
canvas2.height = height;
|
|
1008
|
+
return canvas2;
|
|
816
1009
|
}
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
...options.launchOptions?.args || []
|
|
823
|
-
],
|
|
824
|
-
...options.launchOptions
|
|
825
|
-
});
|
|
826
|
-
const width = options.width || 1280;
|
|
827
|
-
const height = options.height || 720;
|
|
828
|
-
const context = await browser.newContext({
|
|
829
|
-
viewport: { width, height },
|
|
830
|
-
deviceScaleFactor: 1,
|
|
831
|
-
...options.contextOptions
|
|
832
|
-
});
|
|
833
|
-
const page = await context.newPage();
|
|
834
|
-
const close = async () => {
|
|
835
|
-
await browser.close();
|
|
836
|
-
if (staticServer) {
|
|
837
|
-
staticServer.close();
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
const navigate = async (url) => {
|
|
841
|
-
const targetUrl = url || clientUrl;
|
|
842
|
-
if (!targetUrl) throw new Error("No URL to navigate to");
|
|
843
|
-
let finalUrl = targetUrl;
|
|
844
|
-
if (options.serverUrl && !targetUrl.includes("connect=")) {
|
|
845
|
-
const separator = targetUrl.includes("?") ? "&" : "?";
|
|
846
|
-
finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
|
|
1010
|
+
const canvas = new Canvas2(width, height);
|
|
1011
|
+
const originalGetContext = canvas.getContext.bind(canvas);
|
|
1012
|
+
canvas.getContext = function(contextId, options) {
|
|
1013
|
+
if (contextId === "webgl2") {
|
|
1014
|
+
return createMockWebGL2Context(canvas);
|
|
847
1015
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
};
|
|
851
|
-
return {
|
|
852
|
-
browser,
|
|
853
|
-
context,
|
|
854
|
-
page,
|
|
855
|
-
server: staticServer,
|
|
856
|
-
close,
|
|
857
|
-
navigate,
|
|
858
|
-
waitForGame: async (timeout = 1e4) => {
|
|
859
|
-
await waitForGameReady(page, timeout);
|
|
860
|
-
},
|
|
861
|
-
injectInput: async (type, data) => {
|
|
862
|
-
await page.evaluate(({ type: type2, data: data2 }) => {
|
|
863
|
-
if (window.injectGameInput) window.injectGameInput(type2, data2);
|
|
864
|
-
}, { type, data });
|
|
1016
|
+
if (contextId === "2d") {
|
|
1017
|
+
return originalGetContext("2d", options);
|
|
865
1018
|
}
|
|
1019
|
+
return originalGetContext(contextId, options);
|
|
866
1020
|
};
|
|
1021
|
+
return canvas;
|
|
867
1022
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
return window.gameInstance && window.gameInstance.isReady;
|
|
872
|
-
}, null, { timeout });
|
|
873
|
-
} catch (e) {
|
|
874
|
-
await page.waitForSelector("canvas", { timeout });
|
|
1023
|
+
function createMockCanvasContext2D(canvas) {
|
|
1024
|
+
if (!canvas) {
|
|
1025
|
+
canvas = createMockCanvas();
|
|
875
1026
|
}
|
|
1027
|
+
return canvas.getContext("2d");
|
|
876
1028
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1029
|
+
function captureCanvasDrawCalls(context) {
|
|
1030
|
+
const drawCalls = [];
|
|
1031
|
+
const methodsToSpy = [
|
|
1032
|
+
"fillRect",
|
|
1033
|
+
"strokeRect",
|
|
1034
|
+
"clearRect",
|
|
1035
|
+
"fillText",
|
|
1036
|
+
"strokeText",
|
|
1037
|
+
"drawImage",
|
|
1038
|
+
"beginPath",
|
|
1039
|
+
"closePath",
|
|
1040
|
+
"moveTo",
|
|
1041
|
+
"lineTo",
|
|
1042
|
+
"arc",
|
|
1043
|
+
"arcTo",
|
|
1044
|
+
"bezierCurveTo",
|
|
1045
|
+
"quadraticCurveTo",
|
|
1046
|
+
"stroke",
|
|
1047
|
+
"fill",
|
|
1048
|
+
"putImageData"
|
|
1049
|
+
];
|
|
1050
|
+
methodsToSpy.forEach((method) => {
|
|
1051
|
+
const original = context[method];
|
|
1052
|
+
if (typeof original === "function") {
|
|
1053
|
+
context[method] = function(...args) {
|
|
1054
|
+
drawCalls.push({ method, args });
|
|
1055
|
+
return original.apply(this, args);
|
|
1056
|
+
};
|
|
881
1057
|
}
|
|
882
|
-
return {};
|
|
883
1058
|
});
|
|
1059
|
+
return drawCalls;
|
|
884
1060
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
function makeAxisBrush(size, contents = CONTENTS_SOLID) {
|
|
900
|
-
const half = size / 2;
|
|
901
|
-
const planes = [
|
|
902
|
-
makePlane({ x: 1, y: 0, z: 0 }, half),
|
|
903
|
-
makePlane({ x: -1, y: 0, z: 0 }, half),
|
|
904
|
-
makePlane({ x: 0, y: 1, z: 0 }, half),
|
|
905
|
-
makePlane({ x: 0, y: -1, z: 0 }, half),
|
|
906
|
-
makePlane({ x: 0, y: 0, z: 1 }, half),
|
|
907
|
-
makePlane({ x: 0, y: 0, z: -1 }, half)
|
|
908
|
-
];
|
|
909
|
-
return {
|
|
910
|
-
contents,
|
|
911
|
-
sides: planes.map((plane) => ({ plane, surfaceFlags: 0 }))
|
|
912
|
-
};
|
|
1061
|
+
function createMockImageData(width, height, fillColor) {
|
|
1062
|
+
const imageData = new ImageData2(width, height);
|
|
1063
|
+
if (fillColor) {
|
|
1064
|
+
const [r, g, b, a] = fillColor;
|
|
1065
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
1066
|
+
imageData.data[i] = r;
|
|
1067
|
+
imageData.data[i + 1] = g;
|
|
1068
|
+
imageData.data[i + 2] = b;
|
|
1069
|
+
imageData.data[i + 3] = a;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return imageData;
|
|
913
1073
|
}
|
|
914
|
-
function
|
|
915
|
-
|
|
1074
|
+
function createMockImage(width, height, src) {
|
|
1075
|
+
const img = new Image2();
|
|
1076
|
+
if (width) img.width = width;
|
|
1077
|
+
if (height) img.height = height;
|
|
1078
|
+
if (src) img.src = src;
|
|
1079
|
+
return img;
|
|
916
1080
|
}
|
|
917
|
-
|
|
1081
|
+
|
|
1082
|
+
// src/setup/node.ts
|
|
1083
|
+
function setupNodeEnvironment(options = {}) {
|
|
1084
|
+
if (options.polyfillFetch && typeof global.fetch === "undefined") {
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/setup/storage.ts
|
|
1089
|
+
import "fake-indexeddb/auto";
|
|
1090
|
+
function createMockLocalStorage(initialData = {}) {
|
|
1091
|
+
const storage = new Map(Object.entries(initialData));
|
|
918
1092
|
return {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1093
|
+
getItem: (key) => storage.get(key) || null,
|
|
1094
|
+
setItem: (key, value) => storage.set(key, value),
|
|
1095
|
+
removeItem: (key) => storage.delete(key),
|
|
1096
|
+
clear: () => storage.clear(),
|
|
1097
|
+
key: (index) => Array.from(storage.keys())[index] || null,
|
|
1098
|
+
get length() {
|
|
1099
|
+
return storage.size;
|
|
1100
|
+
}
|
|
925
1101
|
};
|
|
926
1102
|
}
|
|
927
|
-
function
|
|
928
|
-
return
|
|
1103
|
+
function createMockSessionStorage(initialData = {}) {
|
|
1104
|
+
return createMockLocalStorage(initialData);
|
|
929
1105
|
}
|
|
930
|
-
function
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
leaves: [makeLeaf(0, 0, brushes.length)],
|
|
936
|
-
brushes,
|
|
937
|
-
leafBrushes: brushes.map((_, i) => i),
|
|
938
|
-
bmodels: []
|
|
939
|
-
};
|
|
1106
|
+
function createMockIndexedDB() {
|
|
1107
|
+
if (typeof indexedDB === "undefined") {
|
|
1108
|
+
throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
|
|
1109
|
+
}
|
|
1110
|
+
return indexedDB;
|
|
940
1111
|
}
|
|
941
|
-
function
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1112
|
+
function createStorageTestScenario(storageType = "local") {
|
|
1113
|
+
if (storageType === "indexed") {
|
|
1114
|
+
const dbName = `test-db-${Math.random().toString(36).substring(7)}`;
|
|
1115
|
+
const storeName = "test-store";
|
|
1116
|
+
const storage2 = createMockIndexedDB();
|
|
1117
|
+
return {
|
|
1118
|
+
storage: storage2,
|
|
1119
|
+
populate: async (data) => {
|
|
1120
|
+
return new Promise((resolve, reject) => {
|
|
1121
|
+
const req = storage2.open(dbName, 1);
|
|
1122
|
+
req.onupgradeneeded = (e) => {
|
|
1123
|
+
const db = e.target.result;
|
|
1124
|
+
db.createObjectStore(storeName);
|
|
1125
|
+
};
|
|
1126
|
+
req.onsuccess = (e) => {
|
|
1127
|
+
const db = e.target.result;
|
|
1128
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
1129
|
+
const store = tx.objectStore(storeName);
|
|
1130
|
+
Object.entries(data).forEach(([k, v]) => store.put(v, k));
|
|
1131
|
+
tx.oncomplete = () => {
|
|
1132
|
+
db.close();
|
|
1133
|
+
resolve();
|
|
1134
|
+
};
|
|
1135
|
+
tx.onerror = () => reject(tx.error);
|
|
1136
|
+
};
|
|
1137
|
+
req.onerror = () => reject(req.error);
|
|
1138
|
+
});
|
|
1139
|
+
},
|
|
1140
|
+
verify: async (key, value) => {
|
|
1141
|
+
return new Promise((resolve, reject) => {
|
|
1142
|
+
const req = storage2.open(dbName, 1);
|
|
1143
|
+
req.onsuccess = (e) => {
|
|
1144
|
+
const db = e.target.result;
|
|
1145
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
1146
|
+
db.close();
|
|
1147
|
+
resolve(false);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const tx = db.transaction(storeName, "readonly");
|
|
1151
|
+
const store = tx.objectStore(storeName);
|
|
1152
|
+
const getReq = store.get(key);
|
|
1153
|
+
getReq.onsuccess = () => {
|
|
1154
|
+
const result = getReq.result === value;
|
|
1155
|
+
db.close();
|
|
1156
|
+
resolve(result);
|
|
1157
|
+
};
|
|
1158
|
+
getReq.onerror = () => {
|
|
1159
|
+
db.close();
|
|
1160
|
+
resolve(false);
|
|
1161
|
+
};
|
|
1162
|
+
};
|
|
1163
|
+
req.onerror = () => reject(req.error);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
|
|
950
1169
|
return {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
writeInt16: vi.fn(),
|
|
970
|
-
writeUint16: vi.fn(),
|
|
971
|
-
writeInt32: vi.fn(),
|
|
972
|
-
writeUint32: vi.fn(),
|
|
973
|
-
writeFloat: vi.fn(),
|
|
974
|
-
getData: vi.fn(() => new Uint8Array(0))
|
|
975
|
-
});
|
|
976
|
-
var createNetChanMock = () => ({
|
|
977
|
-
qport: 1234,
|
|
978
|
-
// Sequencing
|
|
979
|
-
incomingSequence: 0,
|
|
980
|
-
outgoingSequence: 0,
|
|
981
|
-
incomingAcknowledged: 0,
|
|
982
|
-
// Reliable messaging
|
|
983
|
-
incomingReliableAcknowledged: false,
|
|
984
|
-
incomingReliableSequence: 0,
|
|
985
|
-
outgoingReliableSequence: 0,
|
|
986
|
-
reliableMessage: createBinaryWriterMock(),
|
|
987
|
-
reliableLength: 0,
|
|
988
|
-
// Fragmentation
|
|
989
|
-
fragmentSendOffset: 0,
|
|
990
|
-
fragmentBuffer: null,
|
|
991
|
-
fragmentLength: 0,
|
|
992
|
-
fragmentReceived: 0,
|
|
993
|
-
// Timing
|
|
994
|
-
lastReceived: 0,
|
|
995
|
-
lastSent: 0,
|
|
996
|
-
remoteAddress: { type: "IP", port: 1234 },
|
|
997
|
-
// Methods
|
|
998
|
-
setup: vi.fn(),
|
|
999
|
-
reset: vi.fn(),
|
|
1000
|
-
transmit: vi.fn(),
|
|
1001
|
-
process: vi.fn(),
|
|
1002
|
-
canSendReliable: vi.fn(() => true),
|
|
1003
|
-
writeReliableByte: vi.fn(),
|
|
1004
|
-
writeReliableShort: vi.fn(),
|
|
1005
|
-
writeReliableLong: vi.fn(),
|
|
1006
|
-
writeReliableString: vi.fn(),
|
|
1007
|
-
getReliableData: vi.fn(() => new Uint8Array(0)),
|
|
1008
|
-
needsKeepalive: vi.fn(() => false),
|
|
1009
|
-
isTimedOut: vi.fn(() => false)
|
|
1010
|
-
});
|
|
1011
|
-
var createBinaryStreamMock = () => ({
|
|
1012
|
-
getPosition: vi.fn(() => 0),
|
|
1013
|
-
getReadPosition: vi.fn(() => 0),
|
|
1014
|
-
getLength: vi.fn(() => 0),
|
|
1015
|
-
getRemaining: vi.fn(() => 0),
|
|
1016
|
-
seek: vi.fn(),
|
|
1017
|
-
setReadPosition: vi.fn(),
|
|
1018
|
-
hasMore: vi.fn(() => true),
|
|
1019
|
-
hasBytes: vi.fn((amount) => true),
|
|
1020
|
-
readChar: vi.fn(() => 0),
|
|
1021
|
-
readByte: vi.fn(() => 0),
|
|
1022
|
-
readShort: vi.fn(() => 0),
|
|
1023
|
-
readUShort: vi.fn(() => 0),
|
|
1024
|
-
readLong: vi.fn(() => 0),
|
|
1025
|
-
readULong: vi.fn(() => 0),
|
|
1026
|
-
readFloat: vi.fn(() => 0),
|
|
1027
|
-
readString: vi.fn(() => ""),
|
|
1028
|
-
readStringLine: vi.fn(() => ""),
|
|
1029
|
-
readCoord: vi.fn(() => 0),
|
|
1030
|
-
readAngle: vi.fn(() => 0),
|
|
1031
|
-
readAngle16: vi.fn(() => 0),
|
|
1032
|
-
readData: vi.fn((length) => new Uint8Array(length)),
|
|
1033
|
-
readPos: vi.fn(),
|
|
1034
|
-
readDir: vi.fn()
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
// src/game/factories.ts
|
|
1038
|
-
var createPlayerStateFactory = (overrides) => ({
|
|
1039
|
-
pm_type: 0,
|
|
1040
|
-
pm_time: 0,
|
|
1041
|
-
pm_flags: 0,
|
|
1042
|
-
origin: { x: 0, y: 0, z: 0 },
|
|
1043
|
-
velocity: { x: 0, y: 0, z: 0 },
|
|
1044
|
-
viewAngles: { x: 0, y: 0, z: 0 },
|
|
1045
|
-
onGround: false,
|
|
1046
|
-
waterLevel: 0,
|
|
1047
|
-
watertype: 0,
|
|
1048
|
-
mins: { x: 0, y: 0, z: 0 },
|
|
1049
|
-
maxs: { x: 0, y: 0, z: 0 },
|
|
1050
|
-
damageAlpha: 0,
|
|
1051
|
-
damageIndicators: [],
|
|
1052
|
-
blend: [0, 0, 0, 0],
|
|
1053
|
-
stats: [],
|
|
1054
|
-
kick_angles: { x: 0, y: 0, z: 0 },
|
|
1055
|
-
kick_origin: { x: 0, y: 0, z: 0 },
|
|
1056
|
-
gunoffset: { x: 0, y: 0, z: 0 },
|
|
1057
|
-
gunangles: { x: 0, y: 0, z: 0 },
|
|
1058
|
-
gunindex: 0,
|
|
1059
|
-
gun_frame: 0,
|
|
1060
|
-
rdflags: 0,
|
|
1061
|
-
fov: 90,
|
|
1062
|
-
renderfx: 0,
|
|
1063
|
-
...overrides
|
|
1064
|
-
});
|
|
1065
|
-
var createEntityStateFactory = (overrides) => ({
|
|
1066
|
-
number: 0,
|
|
1067
|
-
origin: { x: 0, y: 0, z: 0 },
|
|
1068
|
-
angles: { x: 0, y: 0, z: 0 },
|
|
1069
|
-
oldOrigin: { x: 0, y: 0, z: 0 },
|
|
1070
|
-
modelIndex: 0,
|
|
1071
|
-
modelIndex2: 0,
|
|
1072
|
-
modelIndex3: 0,
|
|
1073
|
-
modelIndex4: 0,
|
|
1074
|
-
frame: 0,
|
|
1075
|
-
skinNum: 0,
|
|
1076
|
-
effects: 0,
|
|
1077
|
-
renderfx: 0,
|
|
1078
|
-
solid: 0,
|
|
1079
|
-
sound: 0,
|
|
1080
|
-
event: 0,
|
|
1081
|
-
...overrides
|
|
1082
|
-
});
|
|
1083
|
-
var createGameStateSnapshotFactory = (overrides) => ({
|
|
1084
|
-
gravity: { x: 0, y: 0, z: -800 },
|
|
1085
|
-
origin: { x: 0, y: 0, z: 0 },
|
|
1086
|
-
velocity: { x: 0, y: 0, z: 0 },
|
|
1087
|
-
viewangles: { x: 0, y: 0, z: 0 },
|
|
1088
|
-
level: { timeSeconds: 0, frameNumber: 0, previousTimeSeconds: 0, deltaSeconds: 0.1 },
|
|
1089
|
-
entities: {
|
|
1090
|
-
activeCount: 0,
|
|
1091
|
-
worldClassname: "worldspawn"
|
|
1092
|
-
},
|
|
1093
|
-
packetEntities: [],
|
|
1094
|
-
pmFlags: 0,
|
|
1095
|
-
pmType: 0,
|
|
1096
|
-
waterlevel: 0,
|
|
1097
|
-
watertype: 0,
|
|
1098
|
-
deltaAngles: { x: 0, y: 0, z: 0 },
|
|
1099
|
-
health: 100,
|
|
1100
|
-
armor: 0,
|
|
1101
|
-
ammo: 0,
|
|
1102
|
-
blend: [0, 0, 0, 0],
|
|
1103
|
-
damageAlpha: 0,
|
|
1104
|
-
damageIndicators: [],
|
|
1105
|
-
stats: [],
|
|
1106
|
-
kick_angles: { x: 0, y: 0, z: 0 },
|
|
1107
|
-
kick_origin: { x: 0, y: 0, z: 0 },
|
|
1108
|
-
gunoffset: { x: 0, y: 0, z: 0 },
|
|
1109
|
-
gunangles: { x: 0, y: 0, z: 0 },
|
|
1110
|
-
gunindex: 0,
|
|
1111
|
-
pm_time: 0,
|
|
1112
|
-
gun_frame: 0,
|
|
1113
|
-
rdflags: 0,
|
|
1114
|
-
fov: 90,
|
|
1115
|
-
renderfx: 0,
|
|
1116
|
-
pm_flags: 0,
|
|
1117
|
-
pm_type: 0,
|
|
1118
|
-
...overrides
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
// src/game/helpers.ts
|
|
1122
|
-
import { vi as vi2 } from "vitest";
|
|
1123
|
-
import { Entity, SpawnRegistry, ScriptHookRegistry } from "@quake2ts/game";
|
|
1124
|
-
import { createRandomGenerator } from "@quake2ts/shared";
|
|
1125
|
-
import { intersects, stairTrace, ladderTrace } from "@quake2ts/shared";
|
|
1126
|
-
var createMockEngine = () => ({
|
|
1127
|
-
sound: vi2.fn(),
|
|
1128
|
-
soundIndex: vi2.fn((sound) => 0),
|
|
1129
|
-
modelIndex: vi2.fn((model) => 0),
|
|
1130
|
-
centerprintf: vi2.fn()
|
|
1131
|
-
});
|
|
1132
|
-
var createMockGame = (seed = 12345) => {
|
|
1133
|
-
const spawnRegistry = new SpawnRegistry();
|
|
1134
|
-
const hooks = new ScriptHookRegistry();
|
|
1135
|
-
const game = {
|
|
1136
|
-
random: createRandomGenerator({ seed }),
|
|
1137
|
-
registerEntitySpawn: vi2.fn((classname, spawnFunc) => {
|
|
1138
|
-
spawnRegistry.register(classname, (entity) => spawnFunc(entity));
|
|
1170
|
+
storage,
|
|
1171
|
+
populate(data) {
|
|
1172
|
+
Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
|
|
1173
|
+
},
|
|
1174
|
+
verify(key, value) {
|
|
1175
|
+
return storage.getItem(key) === value;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/setup/audio.ts
|
|
1181
|
+
function createMockAudioContext() {
|
|
1182
|
+
const context = {
|
|
1183
|
+
createGain: () => ({
|
|
1184
|
+
connect: () => {
|
|
1185
|
+
},
|
|
1186
|
+
gain: { value: 1, setValueAtTime: () => {
|
|
1187
|
+
} }
|
|
1139
1188
|
}),
|
|
1140
|
-
|
|
1141
|
-
|
|
1189
|
+
createOscillator: () => ({
|
|
1190
|
+
connect: () => {
|
|
1191
|
+
},
|
|
1192
|
+
start: () => {
|
|
1193
|
+
},
|
|
1194
|
+
stop: () => {
|
|
1195
|
+
},
|
|
1196
|
+
frequency: { value: 440 }
|
|
1142
1197
|
}),
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1198
|
+
createBufferSource: () => ({
|
|
1199
|
+
connect: () => {
|
|
1200
|
+
},
|
|
1201
|
+
start: () => {
|
|
1202
|
+
},
|
|
1203
|
+
stop: () => {
|
|
1204
|
+
},
|
|
1205
|
+
buffer: null,
|
|
1206
|
+
playbackRate: { value: 1 },
|
|
1207
|
+
loop: false
|
|
1148
1208
|
}),
|
|
1149
|
-
|
|
1150
|
-
|
|
1209
|
+
destination: {},
|
|
1210
|
+
currentTime: 0,
|
|
1211
|
+
state: "running",
|
|
1212
|
+
resume: async () => {
|
|
1213
|
+
},
|
|
1214
|
+
suspend: async () => {
|
|
1215
|
+
},
|
|
1216
|
+
close: async () => {
|
|
1217
|
+
},
|
|
1218
|
+
decodeAudioData: async (buffer) => ({
|
|
1219
|
+
duration: 1,
|
|
1220
|
+
length: 44100,
|
|
1221
|
+
sampleRate: 44100,
|
|
1222
|
+
numberOfChannels: 2,
|
|
1223
|
+
getChannelData: () => new Float32Array(44100)
|
|
1151
1224
|
}),
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
function createTestContext(options) {
|
|
1159
|
-
const engine = createMockEngine();
|
|
1160
|
-
const seed = options?.seed ?? 12345;
|
|
1161
|
-
const { game, spawnRegistry } = createMockGame(seed);
|
|
1162
|
-
const traceFn = vi2.fn((start, end, mins, maxs) => ({
|
|
1163
|
-
fraction: 1,
|
|
1164
|
-
ent: null,
|
|
1165
|
-
allsolid: false,
|
|
1166
|
-
startsolid: false,
|
|
1167
|
-
endpos: end,
|
|
1168
|
-
plane: { normal: { x: 0, y: 0, z: 1 }, dist: 0 },
|
|
1169
|
-
surfaceFlags: 0,
|
|
1170
|
-
contents: 0
|
|
1171
|
-
}));
|
|
1172
|
-
const entityList = options?.initialEntities ? [...options.initialEntities] : [];
|
|
1173
|
-
const hooks = game.hooks;
|
|
1174
|
-
const entities = {
|
|
1175
|
-
spawn: vi2.fn(() => {
|
|
1176
|
-
const ent = new Entity(entityList.length + 1);
|
|
1177
|
-
entityList.push(ent);
|
|
1178
|
-
hooks.onEntitySpawn(ent);
|
|
1179
|
-
return ent;
|
|
1225
|
+
createBuffer: (channels, length, sampleRate) => ({
|
|
1226
|
+
duration: length / sampleRate,
|
|
1227
|
+
length,
|
|
1228
|
+
sampleRate,
|
|
1229
|
+
numberOfChannels: channels,
|
|
1230
|
+
getChannelData: () => new Float32Array(length)
|
|
1180
1231
|
}),
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1232
|
+
// Helper to track events if needed
|
|
1233
|
+
_events: []
|
|
1234
|
+
};
|
|
1235
|
+
return new Proxy(context, {
|
|
1236
|
+
get(target, prop, receiver) {
|
|
1237
|
+
if (prop === "_events") return target._events;
|
|
1238
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1239
|
+
if (typeof value === "function") {
|
|
1240
|
+
return (...args) => {
|
|
1241
|
+
target._events.push({ type: String(prop), args });
|
|
1242
|
+
return Reflect.apply(value, target, args);
|
|
1243
|
+
};
|
|
1185
1244
|
}
|
|
1186
|
-
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1245
|
+
return value;
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
function setupMockAudioContext() {
|
|
1250
|
+
if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
|
|
1251
|
+
global.AudioContext = class {
|
|
1252
|
+
constructor() {
|
|
1253
|
+
return createMockAudioContext();
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
global.window.AudioContext = global.AudioContext;
|
|
1257
|
+
global.window.webkitAudioContext = global.AudioContext;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function teardownMockAudioContext() {
|
|
1261
|
+
if (global.AudioContext && global.AudioContext.toString().includes("class")) {
|
|
1262
|
+
delete global.AudioContext;
|
|
1263
|
+
delete global.window.AudioContext;
|
|
1264
|
+
delete global.window.webkitAudioContext;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
function captureAudioEvents(context) {
|
|
1268
|
+
return context._events || [];
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/setup/timing.ts
|
|
1272
|
+
var activeMockRAF;
|
|
1273
|
+
function createMockRAF() {
|
|
1274
|
+
let callbacks = [];
|
|
1275
|
+
let nextId = 1;
|
|
1276
|
+
let currentTime = 0;
|
|
1277
|
+
const originalRAF = global.requestAnimationFrame;
|
|
1278
|
+
const originalCancelRAF = global.cancelAnimationFrame;
|
|
1279
|
+
const raf = (callback) => {
|
|
1280
|
+
const id = nextId++;
|
|
1281
|
+
callbacks.push({ id, callback });
|
|
1282
|
+
return id;
|
|
1283
|
+
};
|
|
1284
|
+
const cancel = (id) => {
|
|
1285
|
+
callbacks = callbacks.filter((cb) => cb.id !== id);
|
|
1286
|
+
};
|
|
1287
|
+
const mock = {
|
|
1288
|
+
tick(timestamp) {
|
|
1289
|
+
if (typeof timestamp !== "number") {
|
|
1290
|
+
currentTime += 16.6;
|
|
1291
|
+
} else {
|
|
1292
|
+
currentTime = timestamp;
|
|
1293
|
+
}
|
|
1294
|
+
const currentCallbacks = [...callbacks];
|
|
1295
|
+
callbacks = [];
|
|
1296
|
+
currentCallbacks.forEach(({ callback }) => {
|
|
1297
|
+
callback(currentTime);
|
|
1298
|
+
});
|
|
1299
|
+
},
|
|
1300
|
+
advance(deltaMs = 16.6) {
|
|
1301
|
+
this.tick(currentTime + deltaMs);
|
|
1302
|
+
},
|
|
1303
|
+
getCallbacks() {
|
|
1304
|
+
return callbacks.map((c) => c.callback);
|
|
1305
|
+
},
|
|
1306
|
+
reset() {
|
|
1307
|
+
callbacks = [];
|
|
1308
|
+
nextId = 1;
|
|
1309
|
+
currentTime = 0;
|
|
1310
|
+
},
|
|
1311
|
+
enable() {
|
|
1312
|
+
activeMockRAF = this;
|
|
1313
|
+
global.requestAnimationFrame = raf;
|
|
1314
|
+
global.cancelAnimationFrame = cancel;
|
|
1315
|
+
},
|
|
1316
|
+
disable() {
|
|
1317
|
+
if (activeMockRAF === this) {
|
|
1318
|
+
activeMockRAF = void 0;
|
|
1319
|
+
}
|
|
1320
|
+
if (originalRAF) {
|
|
1321
|
+
global.requestAnimationFrame = originalRAF;
|
|
1322
|
+
} else {
|
|
1323
|
+
delete global.requestAnimationFrame;
|
|
1324
|
+
}
|
|
1325
|
+
if (originalCancelRAF) {
|
|
1326
|
+
global.cancelAnimationFrame = originalCancelRAF;
|
|
1327
|
+
} else {
|
|
1328
|
+
delete global.cancelAnimationFrame;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
return mock;
|
|
1333
|
+
}
|
|
1334
|
+
function createMockPerformance(startTime = 0) {
|
|
1335
|
+
let currentTime = startTime;
|
|
1336
|
+
const mockPerf = {
|
|
1337
|
+
now: () => currentTime,
|
|
1338
|
+
timeOrigin: startTime,
|
|
1339
|
+
timing: {
|
|
1340
|
+
navigationStart: startTime
|
|
1341
|
+
},
|
|
1342
|
+
clearMarks: () => {
|
|
1343
|
+
},
|
|
1344
|
+
clearMeasures: () => {
|
|
1345
|
+
},
|
|
1346
|
+
clearResourceTimings: () => {
|
|
1347
|
+
},
|
|
1348
|
+
getEntries: () => [],
|
|
1349
|
+
getEntriesByName: () => [],
|
|
1350
|
+
getEntriesByType: () => [],
|
|
1351
|
+
mark: () => {
|
|
1352
|
+
},
|
|
1353
|
+
measure: () => {
|
|
1354
|
+
},
|
|
1355
|
+
setResourceTimingBufferSize: () => {
|
|
1356
|
+
},
|
|
1357
|
+
toJSON: () => ({}),
|
|
1358
|
+
addEventListener: () => {
|
|
1359
|
+
},
|
|
1360
|
+
removeEventListener: () => {
|
|
1361
|
+
},
|
|
1362
|
+
dispatchEvent: () => true
|
|
1363
|
+
};
|
|
1364
|
+
mockPerf.advance = (deltaMs) => {
|
|
1365
|
+
currentTime += deltaMs;
|
|
1366
|
+
};
|
|
1367
|
+
mockPerf.setTime = (time) => {
|
|
1368
|
+
currentTime = time;
|
|
1369
|
+
};
|
|
1370
|
+
return mockPerf;
|
|
1371
|
+
}
|
|
1372
|
+
function createControlledTimer() {
|
|
1373
|
+
let currentTime = 0;
|
|
1374
|
+
let timers = [];
|
|
1375
|
+
let nextId = 1;
|
|
1376
|
+
const originalSetTimeout = global.setTimeout;
|
|
1377
|
+
const originalClearTimeout = global.clearTimeout;
|
|
1378
|
+
const originalSetInterval = global.setInterval;
|
|
1379
|
+
const originalClearInterval = global.clearInterval;
|
|
1380
|
+
const mockSetTimeout = (callback, delay = 0, ...args) => {
|
|
1381
|
+
const id = nextId++;
|
|
1382
|
+
timers.push({ id, callback, dueTime: currentTime + delay, args });
|
|
1383
|
+
return id;
|
|
1384
|
+
};
|
|
1385
|
+
const mockClearTimeout = (id) => {
|
|
1386
|
+
timers = timers.filter((t) => t.id !== id);
|
|
1387
|
+
};
|
|
1388
|
+
const mockSetInterval = (callback, delay = 0, ...args) => {
|
|
1389
|
+
const id = nextId++;
|
|
1390
|
+
timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
|
|
1391
|
+
return id;
|
|
1392
|
+
};
|
|
1393
|
+
const mockClearInterval = (id) => {
|
|
1394
|
+
timers = timers.filter((t) => t.id !== id);
|
|
1395
|
+
};
|
|
1396
|
+
global.setTimeout = mockSetTimeout;
|
|
1397
|
+
global.clearTimeout = mockClearTimeout;
|
|
1398
|
+
global.setInterval = mockSetInterval;
|
|
1399
|
+
global.clearInterval = mockClearInterval;
|
|
1400
|
+
return {
|
|
1401
|
+
tick() {
|
|
1402
|
+
this.advanceBy(0);
|
|
1403
|
+
},
|
|
1404
|
+
advanceBy(ms) {
|
|
1405
|
+
const targetTime = currentTime + ms;
|
|
1406
|
+
while (true) {
|
|
1407
|
+
let earliest = null;
|
|
1408
|
+
for (const t of timers) {
|
|
1409
|
+
if (!earliest || t.dueTime < earliest.dueTime) {
|
|
1410
|
+
earliest = t;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (!earliest || earliest.dueTime > targetTime) {
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
currentTime = earliest.dueTime;
|
|
1417
|
+
const { callback, args, interval, id } = earliest;
|
|
1418
|
+
if (interval !== void 0) {
|
|
1419
|
+
earliest.dueTime += interval;
|
|
1420
|
+
if (interval === 0) earliest.dueTime += 1;
|
|
1421
|
+
} else {
|
|
1422
|
+
timers = timers.filter((t) => t.id !== id);
|
|
1423
|
+
}
|
|
1424
|
+
callback(...args);
|
|
1193
1425
|
}
|
|
1194
|
-
|
|
1195
|
-
setSpawnRegistry: vi2.fn(),
|
|
1196
|
-
timeSeconds: 10,
|
|
1197
|
-
deltaSeconds: 0.1,
|
|
1198
|
-
modelIndex: vi2.fn(() => 0),
|
|
1199
|
-
scheduleThink: vi2.fn((entity, time) => {
|
|
1200
|
-
entity.nextthink = time;
|
|
1201
|
-
}),
|
|
1202
|
-
linkentity: vi2.fn(),
|
|
1203
|
-
trace: traceFn,
|
|
1204
|
-
pointcontents: vi2.fn(() => 0),
|
|
1205
|
-
multicast: vi2.fn(),
|
|
1206
|
-
unicast: vi2.fn(),
|
|
1207
|
-
engine,
|
|
1208
|
-
game,
|
|
1209
|
-
sound: vi2.fn((ent, chan, sound, vol, attn, timeofs) => {
|
|
1210
|
-
engine.sound(ent, chan, sound, vol, attn, timeofs);
|
|
1211
|
-
}),
|
|
1212
|
-
soundIndex: vi2.fn((sound) => engine.soundIndex(sound)),
|
|
1213
|
-
useTargets: vi2.fn((entity, activator) => {
|
|
1214
|
-
}),
|
|
1215
|
-
findByTargetName: vi2.fn(() => []),
|
|
1216
|
-
pickTarget: vi2.fn(() => null),
|
|
1217
|
-
killBox: vi2.fn(),
|
|
1218
|
-
rng: createRandomGenerator({ seed }),
|
|
1219
|
-
imports: {
|
|
1220
|
-
configstring: vi2.fn(),
|
|
1221
|
-
trace: traceFn,
|
|
1222
|
-
pointcontents: vi2.fn(() => 0)
|
|
1426
|
+
currentTime = targetTime;
|
|
1223
1427
|
},
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
intermission_origin: { x: 0, y: 0, z: 0 },
|
|
1227
|
-
next_auto_save: 0,
|
|
1228
|
-
health_bar_entities: null
|
|
1428
|
+
clear() {
|
|
1429
|
+
timers = [];
|
|
1229
1430
|
},
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1431
|
+
restore() {
|
|
1432
|
+
global.setTimeout = originalSetTimeout;
|
|
1433
|
+
global.clearTimeout = originalClearTimeout;
|
|
1434
|
+
global.setInterval = originalSetInterval;
|
|
1435
|
+
global.clearInterval = originalClearInterval;
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function simulateFrames(count, frameTimeMs = 16.6, callback) {
|
|
1440
|
+
if (!activeMockRAF) {
|
|
1441
|
+
throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
|
|
1442
|
+
}
|
|
1443
|
+
for (let i = 0; i < count; i++) {
|
|
1444
|
+
if (callback) callback(i);
|
|
1445
|
+
activeMockRAF.advance(frameTimeMs);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
|
|
1449
|
+
for (let i = 0; i < count; i++) {
|
|
1450
|
+
if (callback) callback(i);
|
|
1451
|
+
mock.advance(frameTimeMs);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/e2e/playwright.ts
|
|
1456
|
+
import { chromium } from "playwright";
|
|
1457
|
+
import { createServer } from "http";
|
|
1458
|
+
import handler from "serve-handler";
|
|
1459
|
+
async function createPlaywrightTestClient(options = {}) {
|
|
1460
|
+
let staticServer;
|
|
1461
|
+
let clientUrl = options.clientUrl;
|
|
1462
|
+
const rootPath = options.rootPath || process.cwd();
|
|
1463
|
+
if (!clientUrl) {
|
|
1464
|
+
staticServer = createServer((request, response) => {
|
|
1465
|
+
return handler(request, response, {
|
|
1466
|
+
public: rootPath,
|
|
1467
|
+
cleanUrls: false,
|
|
1468
|
+
headers: [
|
|
1469
|
+
{
|
|
1470
|
+
source: "**/*",
|
|
1471
|
+
headers: [
|
|
1472
|
+
{ key: "Cache-Control", value: "no-cache" },
|
|
1473
|
+
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
1474
|
+
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
|
1475
|
+
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
|
|
1476
|
+
]
|
|
1477
|
+
}
|
|
1478
|
+
]
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
await new Promise((resolve) => {
|
|
1482
|
+
if (!staticServer) return;
|
|
1483
|
+
staticServer.listen(0, () => {
|
|
1484
|
+
const addr = staticServer?.address();
|
|
1485
|
+
const port = typeof addr === "object" ? addr?.port : 0;
|
|
1486
|
+
clientUrl = `http://localhost:${port}`;
|
|
1487
|
+
console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
|
|
1488
|
+
resolve();
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
const browser = await chromium.launch({
|
|
1493
|
+
headless: options.headless ?? true,
|
|
1494
|
+
args: [
|
|
1495
|
+
"--use-gl=egl",
|
|
1496
|
+
"--ignore-gpu-blocklist",
|
|
1497
|
+
...options.launchOptions?.args || []
|
|
1498
|
+
],
|
|
1499
|
+
...options.launchOptions
|
|
1500
|
+
});
|
|
1501
|
+
const width = options.width || 1280;
|
|
1502
|
+
const height = options.height || 720;
|
|
1503
|
+
const context = await browser.newContext({
|
|
1504
|
+
viewport: { width, height },
|
|
1505
|
+
deviceScaleFactor: 1,
|
|
1506
|
+
...options.contextOptions
|
|
1507
|
+
});
|
|
1508
|
+
const page = await context.newPage();
|
|
1509
|
+
const close = async () => {
|
|
1510
|
+
await browser.close();
|
|
1511
|
+
if (staticServer) {
|
|
1512
|
+
staticServer.close();
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
const navigate = async (url) => {
|
|
1516
|
+
const targetUrl = url || clientUrl;
|
|
1517
|
+
if (!targetUrl) throw new Error("No URL to navigate to");
|
|
1518
|
+
let finalUrl = targetUrl;
|
|
1519
|
+
if (options.serverUrl && !targetUrl.includes("connect=")) {
|
|
1520
|
+
const separator = targetUrl.includes("?") ? "&" : "?";
|
|
1521
|
+
finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
|
|
1522
|
+
}
|
|
1523
|
+
console.log(`Navigating to: ${finalUrl}`);
|
|
1524
|
+
await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
|
|
1525
|
+
};
|
|
1526
|
+
return {
|
|
1527
|
+
browser,
|
|
1528
|
+
context,
|
|
1529
|
+
page,
|
|
1530
|
+
server: staticServer,
|
|
1531
|
+
close,
|
|
1532
|
+
navigate,
|
|
1533
|
+
waitForGame: async (timeout = 1e4) => {
|
|
1534
|
+
await waitForGameReady(page, timeout);
|
|
1248
1535
|
},
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
activeCount: entityList.length,
|
|
1255
|
-
world: entityList.find((e) => e.classname === "worldspawn") || new Entity(0)
|
|
1256
|
-
// ... other EntitySystem properties would go here
|
|
1536
|
+
injectInput: async (type, data) => {
|
|
1537
|
+
await page.evaluate(({ type: type2, data: data2 }) => {
|
|
1538
|
+
if (window.injectGameInput) window.injectGameInput(type2, data2);
|
|
1539
|
+
}, { type, data });
|
|
1540
|
+
}
|
|
1257
1541
|
};
|
|
1542
|
+
}
|
|
1543
|
+
async function waitForGameReady(page, timeout = 1e4) {
|
|
1544
|
+
try {
|
|
1545
|
+
await page.waitForFunction(() => {
|
|
1546
|
+
return window.gameInstance && window.gameInstance.isReady;
|
|
1547
|
+
}, null, { timeout });
|
|
1548
|
+
} catch (e) {
|
|
1549
|
+
await page.waitForSelector("canvas", { timeout });
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async function captureGameState(page) {
|
|
1553
|
+
return await page.evaluate(() => {
|
|
1554
|
+
if (window.gameInstance && window.gameInstance.getState) {
|
|
1555
|
+
return window.gameInstance.getState();
|
|
1556
|
+
}
|
|
1557
|
+
return {};
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/e2e/network.ts
|
|
1562
|
+
var CONDITIONS = {
|
|
1563
|
+
"good": {
|
|
1564
|
+
offline: false,
|
|
1565
|
+
downloadThroughput: 10 * 1024 * 1024,
|
|
1566
|
+
// 10 Mbps
|
|
1567
|
+
uploadThroughput: 5 * 1024 * 1024,
|
|
1568
|
+
// 5 Mbps
|
|
1569
|
+
latency: 20
|
|
1570
|
+
},
|
|
1571
|
+
"slow": {
|
|
1572
|
+
offline: false,
|
|
1573
|
+
downloadThroughput: 500 * 1024,
|
|
1574
|
+
// 500 Kbps
|
|
1575
|
+
uploadThroughput: 500 * 1024,
|
|
1576
|
+
latency: 400
|
|
1577
|
+
},
|
|
1578
|
+
"unstable": {
|
|
1579
|
+
offline: false,
|
|
1580
|
+
downloadThroughput: 1 * 1024 * 1024,
|
|
1581
|
+
uploadThroughput: 1 * 1024 * 1024,
|
|
1582
|
+
latency: 100
|
|
1583
|
+
},
|
|
1584
|
+
"offline": {
|
|
1585
|
+
offline: true,
|
|
1586
|
+
downloadThroughput: 0,
|
|
1587
|
+
uploadThroughput: 0,
|
|
1588
|
+
latency: 0
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
function simulateNetworkCondition(condition) {
|
|
1592
|
+
const config = CONDITIONS[condition];
|
|
1593
|
+
return createCustomNetworkCondition(config.latency, 0, 0, config);
|
|
1594
|
+
}
|
|
1595
|
+
function createCustomNetworkCondition(latency, jitter = 0, packetLoss = 0, baseConfig) {
|
|
1258
1596
|
return {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1597
|
+
async apply(page) {
|
|
1598
|
+
const client = await page.context().newCDPSession(page);
|
|
1599
|
+
await client.send("Network.enable");
|
|
1600
|
+
await client.send("Network.emulateNetworkConditions", {
|
|
1601
|
+
offline: baseConfig?.offline || false,
|
|
1602
|
+
latency: latency + Math.random() * jitter,
|
|
1603
|
+
downloadThroughput: baseConfig?.downloadThroughput || -1,
|
|
1604
|
+
uploadThroughput: baseConfig?.uploadThroughput || -1
|
|
1605
|
+
});
|
|
1606
|
+
},
|
|
1607
|
+
async clear(page) {
|
|
1608
|
+
const client = await page.context().newCDPSession(page);
|
|
1609
|
+
await client.send("Network.emulateNetworkConditions", {
|
|
1610
|
+
offline: false,
|
|
1611
|
+
latency: 0,
|
|
1612
|
+
downloadThroughput: -1,
|
|
1613
|
+
uploadThroughput: -1
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1270
1616
|
};
|
|
1271
1617
|
}
|
|
1272
|
-
function
|
|
1273
|
-
|
|
1618
|
+
async function throttleBandwidth(page, bytesPerSecond) {
|
|
1619
|
+
const simulator = createCustomNetworkCondition(0, 0, 0, {
|
|
1620
|
+
offline: false,
|
|
1621
|
+
latency: 0,
|
|
1622
|
+
downloadThroughput: bytesPerSecond,
|
|
1623
|
+
uploadThroughput: bytesPerSecond
|
|
1624
|
+
});
|
|
1625
|
+
await simulator.apply(page);
|
|
1274
1626
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1627
|
+
|
|
1628
|
+
// src/e2e/visual.ts
|
|
1629
|
+
import path from "path";
|
|
1630
|
+
import fs from "fs/promises";
|
|
1631
|
+
async function captureGameScreenshot(page, name, options = {}) {
|
|
1632
|
+
const dir = options.dir || "__screenshots__";
|
|
1633
|
+
const screenshotPath = path.join(dir, `${name}.png`);
|
|
1634
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1635
|
+
return await page.screenshot({
|
|
1636
|
+
path: screenshotPath,
|
|
1637
|
+
fullPage: options.fullPage ?? false,
|
|
1638
|
+
animations: "disabled",
|
|
1639
|
+
caret: "hide"
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
async function compareScreenshots(baseline, current, threshold = 0.1) {
|
|
1643
|
+
if (baseline.equals(current)) {
|
|
1644
|
+
return { pixelDiff: 0, matched: true };
|
|
1645
|
+
}
|
|
1646
|
+
return {
|
|
1647
|
+
pixelDiff: -1,
|
|
1648
|
+
// Unknown magnitude
|
|
1649
|
+
matched: false
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function createVisualTestScenario(page, sceneName) {
|
|
1653
|
+
return {
|
|
1654
|
+
async capture(snapshotName) {
|
|
1655
|
+
return await captureGameScreenshot(page, `${sceneName}-${snapshotName}`);
|
|
1656
|
+
},
|
|
1657
|
+
async compare(snapshotName, baselineDir) {
|
|
1658
|
+
const name = `${sceneName}-${snapshotName}`;
|
|
1659
|
+
const current = await captureGameScreenshot(page, name, { dir: "__screenshots__/current" });
|
|
1660
|
+
try {
|
|
1661
|
+
const baselinePath = path.join(baselineDir, `${name}.png`);
|
|
1662
|
+
const baseline = await fs.readFile(baselinePath);
|
|
1663
|
+
return await compareScreenshots(baseline, current);
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
return { pixelDiff: -1, matched: false };
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1277
1669
|
}
|
|
1278
1670
|
export {
|
|
1279
1671
|
InputInjector,
|
|
1280
1672
|
MockPointerLock,
|
|
1673
|
+
MockTransport,
|
|
1281
1674
|
captureAudioEvents,
|
|
1282
1675
|
captureCanvasDrawCalls,
|
|
1676
|
+
captureGameScreenshot,
|
|
1283
1677
|
captureGameState,
|
|
1678
|
+
compareScreenshots,
|
|
1284
1679
|
createBinaryStreamMock,
|
|
1285
1680
|
createBinaryWriterMock,
|
|
1286
1681
|
createControlledTimer,
|
|
1682
|
+
createCustomNetworkCondition,
|
|
1287
1683
|
createEntity,
|
|
1288
1684
|
createEntityStateFactory,
|
|
1289
1685
|
createGameStateSnapshotFactory,
|
|
@@ -1292,13 +1688,21 @@ export {
|
|
|
1292
1688
|
createMockCanvasContext2D,
|
|
1293
1689
|
createMockEngine,
|
|
1294
1690
|
createMockGame,
|
|
1691
|
+
createMockGameState,
|
|
1295
1692
|
createMockImage,
|
|
1296
1693
|
createMockImageData,
|
|
1297
1694
|
createMockIndexedDB,
|
|
1298
1695
|
createMockLocalStorage,
|
|
1696
|
+
createMockNetworkAddress,
|
|
1299
1697
|
createMockPerformance,
|
|
1300
1698
|
createMockRAF,
|
|
1699
|
+
createMockServer,
|
|
1700
|
+
createMockServerClient,
|
|
1701
|
+
createMockServerState,
|
|
1702
|
+
createMockServerStatic,
|
|
1301
1703
|
createMockSessionStorage,
|
|
1704
|
+
createMockTransport,
|
|
1705
|
+
createMockUDPSocket,
|
|
1302
1706
|
createMockWebGL2Context,
|
|
1303
1707
|
createNetChanMock,
|
|
1304
1708
|
createPlayerStateFactory,
|
|
@@ -1306,6 +1710,7 @@ export {
|
|
|
1306
1710
|
createSpawnContext,
|
|
1307
1711
|
createStorageTestScenario,
|
|
1308
1712
|
createTestContext,
|
|
1713
|
+
createVisualTestScenario,
|
|
1309
1714
|
intersects,
|
|
1310
1715
|
ladderTrace,
|
|
1311
1716
|
makeAxisBrush,
|
|
@@ -1320,9 +1725,11 @@ export {
|
|
|
1320
1725
|
setupNodeEnvironment,
|
|
1321
1726
|
simulateFrames,
|
|
1322
1727
|
simulateFramesWithMock,
|
|
1728
|
+
simulateNetworkCondition,
|
|
1323
1729
|
stairTrace,
|
|
1324
1730
|
teardownBrowserEnvironment,
|
|
1325
1731
|
teardownMockAudioContext,
|
|
1732
|
+
throttleBandwidth,
|
|
1326
1733
|
waitForGameReady
|
|
1327
1734
|
};
|
|
1328
1735
|
//# sourceMappingURL=index.js.map
|