quake2ts 0.0.561 → 0.0.562
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 +1422 -1192
- package/packages/test-utils/dist/index.cjs.map +1 -1
- package/packages/test-utils/dist/index.d.cts +268 -131
- package/packages/test-utils/dist/index.d.ts +268 -131
- package/packages/test-utils/dist/index.js +1415 -1194
- package/packages/test-utils/dist/index.js.map +1 -1
- package/packages/server/dist/index.d.cts +0 -161
|
@@ -1,1283 +1,1496 @@
|
|
|
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
|
-
};
|
|
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
|
+
});
|
|
98
81
|
|
|
99
|
-
// src/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
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 }))
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function makeNode(plane, children) {
|
|
111
|
+
return { plane, children };
|
|
112
|
+
}
|
|
113
|
+
function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
|
|
114
|
+
return {
|
|
115
|
+
planes,
|
|
116
|
+
nodes,
|
|
117
|
+
leaves,
|
|
118
|
+
brushes,
|
|
119
|
+
leafBrushes,
|
|
120
|
+
bmodels: []
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function makeLeaf(contents, firstLeafBrush, numLeafBrushes) {
|
|
124
|
+
return { contents, cluster: 0, area: 0, firstLeafBrush, numLeafBrushes };
|
|
125
|
+
}
|
|
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: []
|
|
219
135
|
};
|
|
220
|
-
return gl;
|
|
221
136
|
}
|
|
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
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
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
|
+
});
|
|
222
235
|
|
|
223
|
-
// src/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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);
|
|
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
|
+
})
|
|
319
270
|
};
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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;
|
|
295
|
+
}),
|
|
296
|
+
free: vi2.fn((ent) => {
|
|
297
|
+
const idx = entityList.indexOf(ent);
|
|
298
|
+
if (idx !== -1) {
|
|
299
|
+
entityList.splice(idx, 1);
|
|
325
300
|
}
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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;
|
|
301
|
+
hooks.onEntityRemove(ent);
|
|
302
|
+
}),
|
|
303
|
+
finalizeSpawn: vi2.fn(),
|
|
304
|
+
freeImmediate: vi2.fn((ent) => {
|
|
305
|
+
const idx = entityList.indexOf(ent);
|
|
306
|
+
if (idx !== -1) {
|
|
307
|
+
entityList.splice(idx, 1);
|
|
340
308
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
309
|
+
}),
|
|
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)
|
|
338
|
+
},
|
|
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
|
|
344
|
+
},
|
|
345
|
+
targetNameIndex: /* @__PURE__ */ new Map(),
|
|
346
|
+
forEachEntity: vi2.fn((callback) => {
|
|
347
|
+
entityList.forEach(callback);
|
|
348
|
+
}),
|
|
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()
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function createSpawnContext() {
|
|
388
|
+
return createTestContext();
|
|
389
|
+
}
|
|
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
|
+
});
|
|
344
421
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Start listening on the specified port.
|
|
424
|
+
*/
|
|
425
|
+
async listen(port) {
|
|
426
|
+
return this.listenSpy(port);
|
|
349
427
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Close the transport.
|
|
430
|
+
*/
|
|
431
|
+
close() {
|
|
432
|
+
this.closeSpy();
|
|
354
433
|
}
|
|
355
|
-
|
|
356
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Register a callback for new connections.
|
|
436
|
+
*/
|
|
437
|
+
onConnection(callback) {
|
|
438
|
+
this.onConnectionCallback = callback;
|
|
357
439
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
};
|
|
440
|
+
/**
|
|
441
|
+
* Register a callback for errors.
|
|
442
|
+
*/
|
|
443
|
+
onError(callback) {
|
|
444
|
+
this.onErrorCallback = callback;
|
|
372
445
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
delete global.localStorage;
|
|
379
|
-
delete global.location;
|
|
380
|
-
delete global.HTMLElement;
|
|
381
|
-
delete global.HTMLCanvasElement;
|
|
382
|
-
delete global.Image;
|
|
383
|
-
delete global.ImageData;
|
|
384
|
-
delete global.createImageBitmap;
|
|
385
|
-
delete global.Event;
|
|
386
|
-
delete global.CustomEvent;
|
|
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;
|
|
446
|
+
/**
|
|
447
|
+
* Check if the transport is currently listening.
|
|
448
|
+
*/
|
|
449
|
+
isListening() {
|
|
450
|
+
return this.listening;
|
|
404
451
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
|
|
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);
|
|
413
460
|
}
|
|
414
|
-
return originalGetContext(contextId, options);
|
|
415
|
-
};
|
|
416
|
-
return canvas;
|
|
417
|
-
}
|
|
418
|
-
function createMockCanvasContext2D(canvas) {
|
|
419
|
-
if (!canvas) {
|
|
420
|
-
canvas = createMockCanvas();
|
|
421
461
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
"clearRect",
|
|
430
|
-
"fillText",
|
|
431
|
-
"strokeText",
|
|
432
|
-
"drawImage",
|
|
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;
|
|
455
|
-
}
|
|
456
|
-
function createMockImageData(width, height, fillColor) {
|
|
457
|
-
const imageData = new ImageData2(width, height);
|
|
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;
|
|
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);
|
|
465
469
|
}
|
|
466
470
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// src/setup/node.ts
|
|
478
|
-
function setupNodeEnvironment() {
|
|
479
|
-
if (typeof global.fetch === "undefined") {
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// src/setup/storage.ts
|
|
484
|
-
import "fake-indexeddb/auto";
|
|
485
|
-
function createMockLocalStorage(initialData = {}) {
|
|
486
|
-
const storage = new Map(Object.entries(initialData));
|
|
487
|
-
return {
|
|
488
|
-
getItem: (key) => storage.get(key) || null,
|
|
489
|
-
setItem: (key, value) => storage.set(key, value),
|
|
490
|
-
removeItem: (key) => storage.delete(key),
|
|
491
|
-
clear: () => storage.clear(),
|
|
492
|
-
key: (index) => Array.from(storage.keys())[index] || null,
|
|
493
|
-
get length() {
|
|
494
|
-
return storage.size;
|
|
495
|
-
}
|
|
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
|
|
496
480
|
};
|
|
481
|
+
return socket;
|
|
497
482
|
}
|
|
498
|
-
function
|
|
499
|
-
return
|
|
500
|
-
}
|
|
501
|
-
function createMockIndexedDB() {
|
|
502
|
-
if (typeof indexedDB === "undefined") {
|
|
503
|
-
throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
|
|
504
|
-
}
|
|
505
|
-
return indexedDB;
|
|
483
|
+
function createMockNetworkAddress(ip = "127.0.0.1", port = 27910) {
|
|
484
|
+
return { ip, port };
|
|
506
485
|
}
|
|
507
|
-
function
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
},
|
|
514
|
-
verify(key, value) {
|
|
515
|
-
return storage.getItem(key) === value;
|
|
516
|
-
}
|
|
517
|
-
};
|
|
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;
|
|
518
492
|
}
|
|
519
493
|
|
|
520
|
-
// src/
|
|
521
|
-
|
|
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) {
|
|
522
499
|
return {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
512
|
+
};
|
|
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
|
|
524
|
+
};
|
|
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()
|
|
555
554
|
},
|
|
556
|
-
|
|
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
|
|
557
589
|
},
|
|
558
|
-
|
|
559
|
-
duration: 1,
|
|
560
|
-
length: 44100,
|
|
561
|
-
sampleRate: 44100,
|
|
562
|
-
numberOfChannels: 2,
|
|
563
|
-
getChannelData: () => new Float32Array(44100)
|
|
564
|
-
}),
|
|
565
|
-
createBuffer: (channels, length, sampleRate) => ({
|
|
566
|
-
duration: length / sampleRate,
|
|
567
|
-
length,
|
|
568
|
-
sampleRate,
|
|
569
|
-
numberOfChannels: channels,
|
|
570
|
-
getChannelData: () => new Float32Array(length)
|
|
571
|
-
})
|
|
590
|
+
...overrides
|
|
572
591
|
};
|
|
573
592
|
}
|
|
574
|
-
function
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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"));
|
|
579
623
|
}
|
|
580
624
|
};
|
|
581
|
-
global.
|
|
582
|
-
|
|
625
|
+
global.HTMLElement.prototype.requestPointerLock = function() {
|
|
626
|
+
_pointerLockElement = this;
|
|
627
|
+
doc.dispatchEvent(new Event("pointerlockchange"));
|
|
628
|
+
};
|
|
583
629
|
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
delete global.window.webkitAudioContext;
|
|
630
|
+
};
|
|
631
|
+
var InputInjector = class {
|
|
632
|
+
constructor(doc, win) {
|
|
633
|
+
this.doc = doc;
|
|
634
|
+
this.win = win;
|
|
590
635
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
+
};
|
|
595
703
|
|
|
596
|
-
// src/setup/
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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: () => {
|
|
624
763
|
},
|
|
625
|
-
|
|
626
|
-
this.tick(currentTime + deltaMs);
|
|
764
|
+
enable: () => {
|
|
627
765
|
},
|
|
628
|
-
|
|
629
|
-
return callbacks.map((c) => c.callback);
|
|
766
|
+
disable: () => {
|
|
630
767
|
},
|
|
631
|
-
|
|
632
|
-
callbacks = [];
|
|
633
|
-
nextId = 1;
|
|
634
|
-
currentTime = 0;
|
|
768
|
+
depthMask: () => {
|
|
635
769
|
},
|
|
636
|
-
|
|
637
|
-
activeMockRAF = this;
|
|
638
|
-
global.requestAnimationFrame = raf;
|
|
639
|
-
global.cancelAnimationFrame = cancel;
|
|
770
|
+
blendFunc: () => {
|
|
640
771
|
},
|
|
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
|
|
772
|
+
viewport: () => {
|
|
666
773
|
},
|
|
667
|
-
|
|
774
|
+
clearColor: () => {
|
|
668
775
|
},
|
|
669
|
-
|
|
776
|
+
clear: () => {
|
|
670
777
|
},
|
|
671
|
-
|
|
778
|
+
createTexture: () => ({}),
|
|
779
|
+
bindTexture: () => {
|
|
672
780
|
},
|
|
673
|
-
|
|
674
|
-
getEntriesByName: () => [],
|
|
675
|
-
getEntriesByType: () => [],
|
|
676
|
-
mark: () => {
|
|
781
|
+
texImage2D: () => {
|
|
677
782
|
},
|
|
678
|
-
|
|
783
|
+
texParameteri: () => {
|
|
679
784
|
},
|
|
680
|
-
|
|
785
|
+
activeTexture: () => {
|
|
681
786
|
},
|
|
682
|
-
|
|
683
|
-
addEventListener: () => {
|
|
787
|
+
uniform1i: () => {
|
|
684
788
|
},
|
|
685
|
-
|
|
789
|
+
uniform1f: () => {
|
|
686
790
|
},
|
|
687
|
-
|
|
688
|
-
};
|
|
689
|
-
mockPerf.advance = (deltaMs) => {
|
|
690
|
-
currentTime += deltaMs;
|
|
691
|
-
};
|
|
692
|
-
mockPerf.setTime = (time) => {
|
|
693
|
-
currentTime = time;
|
|
694
|
-
};
|
|
695
|
-
return mockPerf;
|
|
696
|
-
}
|
|
697
|
-
function createControlledTimer() {
|
|
698
|
-
let currentTime = 0;
|
|
699
|
-
let timers = [];
|
|
700
|
-
let nextId = 1;
|
|
701
|
-
const originalSetTimeout = global.setTimeout;
|
|
702
|
-
const originalClearTimeout = global.clearTimeout;
|
|
703
|
-
const originalSetInterval = global.setInterval;
|
|
704
|
-
const originalClearInterval = global.clearInterval;
|
|
705
|
-
const mockSetTimeout = (callback, delay = 0, ...args) => {
|
|
706
|
-
const id = nextId++;
|
|
707
|
-
timers.push({ id, callback, dueTime: currentTime + delay, args });
|
|
708
|
-
return id;
|
|
709
|
-
};
|
|
710
|
-
const mockClearTimeout = (id) => {
|
|
711
|
-
timers = timers.filter((t) => t.id !== id);
|
|
712
|
-
};
|
|
713
|
-
const mockSetInterval = (callback, delay = 0, ...args) => {
|
|
714
|
-
const id = nextId++;
|
|
715
|
-
timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
|
|
716
|
-
return id;
|
|
717
|
-
};
|
|
718
|
-
const mockClearInterval = (id) => {
|
|
719
|
-
timers = timers.filter((t) => t.id !== id);
|
|
720
|
-
};
|
|
721
|
-
global.setTimeout = mockSetTimeout;
|
|
722
|
-
global.clearTimeout = mockClearTimeout;
|
|
723
|
-
global.setInterval = mockSetInterval;
|
|
724
|
-
global.clearInterval = mockClearInterval;
|
|
725
|
-
return {
|
|
726
|
-
tick() {
|
|
727
|
-
this.advanceBy(0);
|
|
791
|
+
uniform2f: () => {
|
|
728
792
|
},
|
|
729
|
-
|
|
730
|
-
const targetTime = currentTime + ms;
|
|
731
|
-
while (true) {
|
|
732
|
-
let earliest = null;
|
|
733
|
-
for (const t of timers) {
|
|
734
|
-
if (!earliest || t.dueTime < earliest.dueTime) {
|
|
735
|
-
earliest = t;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
if (!earliest || earliest.dueTime > targetTime) {
|
|
739
|
-
break;
|
|
740
|
-
}
|
|
741
|
-
currentTime = earliest.dueTime;
|
|
742
|
-
const { callback, args, interval, id } = earliest;
|
|
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);
|
|
748
|
-
}
|
|
749
|
-
callback(...args);
|
|
750
|
-
}
|
|
751
|
-
currentTime = targetTime;
|
|
793
|
+
uniform3f: () => {
|
|
752
794
|
},
|
|
753
|
-
|
|
754
|
-
timers = [];
|
|
795
|
+
uniform4f: () => {
|
|
755
796
|
},
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
mock.advance(frameTimeMs);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// src/e2e/playwright.ts
|
|
781
|
-
import { chromium } from "playwright";
|
|
782
|
-
import { createServer } from "http";
|
|
783
|
-
import handler from "serve-handler";
|
|
784
|
-
async function createPlaywrightTestClient(options = {}) {
|
|
785
|
-
let staticServer;
|
|
786
|
-
let clientUrl = options.clientUrl;
|
|
787
|
-
const rootPath = options.rootPath || process.cwd();
|
|
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
|
-
});
|
|
816
|
-
}
|
|
817
|
-
const browser = await chromium.launch({
|
|
818
|
-
headless: options.headless ?? true,
|
|
819
|
-
args: [
|
|
820
|
-
"--use-gl=egl",
|
|
821
|
-
"--ignore-gpu-blocklist",
|
|
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)}`;
|
|
847
|
-
}
|
|
848
|
-
console.log(`Navigating to: ${finalUrl}`);
|
|
849
|
-
await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
|
|
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);
|
|
797
|
+
uniformMatrix4fv: () => {
|
|
798
|
+
},
|
|
799
|
+
getUniformLocation: () => ({}),
|
|
800
|
+
getAttribLocation: () => 0,
|
|
801
|
+
drawArrays: () => {
|
|
802
|
+
},
|
|
803
|
+
drawElements: () => {
|
|
804
|
+
},
|
|
805
|
+
createVertexArray: () => ({}),
|
|
806
|
+
bindVertexArray: () => {
|
|
807
|
+
},
|
|
808
|
+
deleteShader: () => {
|
|
809
|
+
},
|
|
810
|
+
deleteProgram: () => {
|
|
811
|
+
},
|
|
812
|
+
deleteBuffer: () => {
|
|
813
|
+
},
|
|
814
|
+
deleteTexture: () => {
|
|
815
|
+
},
|
|
816
|
+
deleteVertexArray: () => {
|
|
860
817
|
},
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
818
|
+
// WebGL2 specific
|
|
819
|
+
texImage3D: () => {
|
|
820
|
+
},
|
|
821
|
+
uniformBlockBinding: () => {
|
|
822
|
+
},
|
|
823
|
+
getExtension: () => null
|
|
866
824
|
};
|
|
825
|
+
return gl;
|
|
867
826
|
}
|
|
868
|
-
|
|
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;
|
|
869
844
|
try {
|
|
870
|
-
|
|
871
|
-
return window.gameInstance && window.gameInstance.isReady;
|
|
872
|
-
}, null, { timeout });
|
|
845
|
+
global.navigator = dom.window.navigator;
|
|
873
846
|
} catch (e) {
|
|
874
|
-
|
|
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
|
+
}
|
|
875
856
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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);
|
|
911
|
+
}
|
|
912
|
+
if (enableWebGL2 && contextId === "webgl2") {
|
|
913
|
+
return createMockWebGL2Context(domCanvas);
|
|
914
|
+
}
|
|
915
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
916
|
+
return originalGetContext(contextId, options3);
|
|
917
|
+
}
|
|
918
|
+
return napiCanvas.getContext(contextId, options3);
|
|
919
|
+
};
|
|
920
|
+
domCanvas.__napiCanvas = napiCanvas;
|
|
921
|
+
return domCanvas;
|
|
881
922
|
}
|
|
882
|
-
return
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// src/shared/bsp.ts
|
|
887
|
-
import {
|
|
888
|
-
computePlaneSignBits,
|
|
889
|
-
CONTENTS_SOLID
|
|
890
|
-
} from "@quake2ts/shared";
|
|
891
|
-
function makePlane(normal, dist) {
|
|
892
|
-
return {
|
|
893
|
-
normal,
|
|
894
|
-
dist,
|
|
895
|
-
type: Math.abs(normal.x) === 1 ? 0 : Math.abs(normal.y) === 1 ? 1 : Math.abs(normal.z) === 1 ? 2 : 3,
|
|
896
|
-
signbits: computePlaneSignBits(normal)
|
|
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
|
-
};
|
|
913
|
-
}
|
|
914
|
-
function makeNode(plane, children) {
|
|
915
|
-
return { plane, children };
|
|
916
|
-
}
|
|
917
|
-
function makeBspModel(planes, nodes, leaves, brushes, leafBrushes) {
|
|
918
|
-
return {
|
|
919
|
-
planes,
|
|
920
|
-
nodes,
|
|
921
|
-
leaves,
|
|
922
|
-
brushes,
|
|
923
|
-
leafBrushes,
|
|
924
|
-
bmodels: []
|
|
923
|
+
return originalCreateElement(tagName, options2);
|
|
925
924
|
};
|
|
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
|
+
};
|
|
933
|
+
}
|
|
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
|
+
};
|
|
949
|
+
}
|
|
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
|
+
};
|
|
977
|
+
}
|
|
926
978
|
}
|
|
927
|
-
function
|
|
928
|
-
|
|
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;
|
|
929
999
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1000
|
+
|
|
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;
|
|
1009
|
+
}
|
|
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);
|
|
1015
|
+
}
|
|
1016
|
+
if (contextId === "2d") {
|
|
1017
|
+
return originalGetContext("2d", options);
|
|
1018
|
+
}
|
|
1019
|
+
return originalGetContext(contextId, options);
|
|
939
1020
|
};
|
|
1021
|
+
return canvas;
|
|
940
1022
|
}
|
|
941
|
-
function
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1023
|
+
function createMockCanvasContext2D(canvas) {
|
|
1024
|
+
if (!canvas) {
|
|
1025
|
+
canvas = createMockCanvas();
|
|
1026
|
+
}
|
|
1027
|
+
return canvas.getContext("2d");
|
|
1028
|
+
}
|
|
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"
|
|
949
1049
|
];
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
return drawCalls;
|
|
1060
|
+
}
|
|
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;
|
|
1073
|
+
}
|
|
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;
|
|
954
1080
|
}
|
|
955
1081
|
|
|
956
|
-
// src/
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
writeLong: vi.fn(),
|
|
962
|
-
writeString: vi.fn(),
|
|
963
|
-
writeBytes: vi.fn(),
|
|
964
|
-
getBuffer: vi.fn(() => new Uint8Array(0)),
|
|
965
|
-
reset: vi.fn(),
|
|
966
|
-
// Legacy methods (if any)
|
|
967
|
-
writeInt8: vi.fn(),
|
|
968
|
-
writeUint8: vi.fn(),
|
|
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
|
-
});
|
|
1082
|
+
// src/setup/node.ts
|
|
1083
|
+
function setupNodeEnvironment() {
|
|
1084
|
+
if (typeof global.fetch === "undefined") {
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1036
1087
|
|
|
1037
|
-
// src/
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
});
|
|
1088
|
+
// src/setup/storage.ts
|
|
1089
|
+
import "fake-indexeddb/auto";
|
|
1090
|
+
function createMockLocalStorage(initialData = {}) {
|
|
1091
|
+
const storage = new Map(Object.entries(initialData));
|
|
1092
|
+
return {
|
|
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
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function createMockSessionStorage(initialData = {}) {
|
|
1104
|
+
return createMockLocalStorage(initialData);
|
|
1105
|
+
}
|
|
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;
|
|
1111
|
+
}
|
|
1112
|
+
function createStorageTestScenario(storageType = "local") {
|
|
1113
|
+
const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
|
|
1114
|
+
return {
|
|
1115
|
+
storage,
|
|
1116
|
+
populate(data) {
|
|
1117
|
+
Object.entries(data).forEach(([k, v]) => storage.setItem(k, v));
|
|
1118
|
+
},
|
|
1119
|
+
verify(key, value) {
|
|
1120
|
+
return storage.getItem(key) === value;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1120
1124
|
|
|
1121
|
-
// src/
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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));
|
|
1125
|
+
// src/setup/audio.ts
|
|
1126
|
+
function createMockAudioContext() {
|
|
1127
|
+
return {
|
|
1128
|
+
createGain: () => ({
|
|
1129
|
+
connect: () => {
|
|
1130
|
+
},
|
|
1131
|
+
gain: { value: 1, setValueAtTime: () => {
|
|
1132
|
+
} }
|
|
1139
1133
|
}),
|
|
1140
|
-
|
|
1141
|
-
|
|
1134
|
+
createOscillator: () => ({
|
|
1135
|
+
connect: () => {
|
|
1136
|
+
},
|
|
1137
|
+
start: () => {
|
|
1138
|
+
},
|
|
1139
|
+
stop: () => {
|
|
1140
|
+
},
|
|
1141
|
+
frequency: { value: 440 }
|
|
1142
1142
|
}),
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1143
|
+
createBufferSource: () => ({
|
|
1144
|
+
connect: () => {
|
|
1145
|
+
},
|
|
1146
|
+
start: () => {
|
|
1147
|
+
},
|
|
1148
|
+
stop: () => {
|
|
1149
|
+
},
|
|
1150
|
+
buffer: null,
|
|
1151
|
+
playbackRate: { value: 1 },
|
|
1152
|
+
loop: false
|
|
1148
1153
|
}),
|
|
1149
|
-
|
|
1150
|
-
|
|
1154
|
+
destination: {},
|
|
1155
|
+
currentTime: 0,
|
|
1156
|
+
state: "running",
|
|
1157
|
+
resume: async () => {
|
|
1158
|
+
},
|
|
1159
|
+
suspend: async () => {
|
|
1160
|
+
},
|
|
1161
|
+
close: async () => {
|
|
1162
|
+
},
|
|
1163
|
+
decodeAudioData: async (buffer) => ({
|
|
1164
|
+
duration: 1,
|
|
1165
|
+
length: 44100,
|
|
1166
|
+
sampleRate: 44100,
|
|
1167
|
+
numberOfChannels: 2,
|
|
1168
|
+
getChannelData: () => new Float32Array(44100)
|
|
1151
1169
|
}),
|
|
1152
|
-
|
|
1153
|
-
|
|
1170
|
+
createBuffer: (channels, length, sampleRate) => ({
|
|
1171
|
+
duration: length / sampleRate,
|
|
1172
|
+
length,
|
|
1173
|
+
sampleRate,
|
|
1174
|
+
numberOfChannels: channels,
|
|
1175
|
+
getChannelData: () => new Float32Array(length)
|
|
1154
1176
|
})
|
|
1155
1177
|
};
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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;
|
|
1180
|
-
}),
|
|
1181
|
-
free: vi2.fn((ent) => {
|
|
1182
|
-
const idx = entityList.indexOf(ent);
|
|
1183
|
-
if (idx !== -1) {
|
|
1184
|
-
entityList.splice(idx, 1);
|
|
1178
|
+
}
|
|
1179
|
+
function setupMockAudioContext() {
|
|
1180
|
+
if (typeof global.AudioContext === "undefined" && typeof global.window !== "undefined") {
|
|
1181
|
+
global.AudioContext = class {
|
|
1182
|
+
constructor() {
|
|
1183
|
+
return createMockAudioContext();
|
|
1185
1184
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1185
|
+
};
|
|
1186
|
+
global.window.AudioContext = global.AudioContext;
|
|
1187
|
+
global.window.webkitAudioContext = global.AudioContext;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function teardownMockAudioContext() {
|
|
1191
|
+
if (global.AudioContext && global.AudioContext.toString().includes("class")) {
|
|
1192
|
+
delete global.AudioContext;
|
|
1193
|
+
delete global.window.AudioContext;
|
|
1194
|
+
delete global.window.webkitAudioContext;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function captureAudioEvents(context) {
|
|
1198
|
+
return [];
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/setup/timing.ts
|
|
1202
|
+
var activeMockRAF;
|
|
1203
|
+
function createMockRAF() {
|
|
1204
|
+
let callbacks = [];
|
|
1205
|
+
let nextId = 1;
|
|
1206
|
+
let currentTime = 0;
|
|
1207
|
+
const originalRAF = global.requestAnimationFrame;
|
|
1208
|
+
const originalCancelRAF = global.cancelAnimationFrame;
|
|
1209
|
+
const raf = (callback) => {
|
|
1210
|
+
const id = nextId++;
|
|
1211
|
+
callbacks.push({ id, callback });
|
|
1212
|
+
return id;
|
|
1213
|
+
};
|
|
1214
|
+
const cancel = (id) => {
|
|
1215
|
+
callbacks = callbacks.filter((cb) => cb.id !== id);
|
|
1216
|
+
};
|
|
1217
|
+
const mock = {
|
|
1218
|
+
tick(timestamp) {
|
|
1219
|
+
if (typeof timestamp !== "number") {
|
|
1220
|
+
currentTime += 16.6;
|
|
1221
|
+
} else {
|
|
1222
|
+
currentTime = timestamp;
|
|
1193
1223
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1224
|
+
const currentCallbacks = [...callbacks];
|
|
1225
|
+
callbacks = [];
|
|
1226
|
+
currentCallbacks.forEach(({ callback }) => {
|
|
1227
|
+
callback(currentTime);
|
|
1228
|
+
});
|
|
1229
|
+
},
|
|
1230
|
+
advance(deltaMs = 16.6) {
|
|
1231
|
+
this.tick(currentTime + deltaMs);
|
|
1232
|
+
},
|
|
1233
|
+
getCallbacks() {
|
|
1234
|
+
return callbacks.map((c) => c.callback);
|
|
1235
|
+
},
|
|
1236
|
+
reset() {
|
|
1237
|
+
callbacks = [];
|
|
1238
|
+
nextId = 1;
|
|
1239
|
+
currentTime = 0;
|
|
1240
|
+
},
|
|
1241
|
+
enable() {
|
|
1242
|
+
activeMockRAF = this;
|
|
1243
|
+
global.requestAnimationFrame = raf;
|
|
1244
|
+
global.cancelAnimationFrame = cancel;
|
|
1245
|
+
},
|
|
1246
|
+
disable() {
|
|
1247
|
+
if (activeMockRAF === this) {
|
|
1248
|
+
activeMockRAF = void 0;
|
|
1249
|
+
}
|
|
1250
|
+
if (originalRAF) {
|
|
1251
|
+
global.requestAnimationFrame = originalRAF;
|
|
1252
|
+
} else {
|
|
1253
|
+
delete global.requestAnimationFrame;
|
|
1254
|
+
}
|
|
1255
|
+
if (originalCancelRAF) {
|
|
1256
|
+
global.cancelAnimationFrame = originalCancelRAF;
|
|
1257
|
+
} else {
|
|
1258
|
+
delete global.cancelAnimationFrame;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
return mock;
|
|
1263
|
+
}
|
|
1264
|
+
function createMockPerformance(startTime = 0) {
|
|
1265
|
+
let currentTime = startTime;
|
|
1266
|
+
const mockPerf = {
|
|
1267
|
+
now: () => currentTime,
|
|
1268
|
+
timeOrigin: startTime,
|
|
1269
|
+
timing: {
|
|
1270
|
+
navigationStart: startTime
|
|
1223
1271
|
},
|
|
1224
|
-
|
|
1225
|
-
intermission_angle: { x: 0, y: 0, z: 0 },
|
|
1226
|
-
intermission_origin: { x: 0, y: 0, z: 0 },
|
|
1227
|
-
next_auto_save: 0,
|
|
1228
|
-
health_bar_entities: null
|
|
1272
|
+
clearMarks: () => {
|
|
1229
1273
|
},
|
|
1230
|
-
|
|
1231
|
-
forEachEntity: vi2.fn((callback) => {
|
|
1232
|
-
entityList.forEach(callback);
|
|
1233
|
-
}),
|
|
1234
|
-
find: vi2.fn((predicate) => {
|
|
1235
|
-
return entityList.find(predicate);
|
|
1236
|
-
}),
|
|
1237
|
-
findByClassname: vi2.fn((classname) => {
|
|
1238
|
-
return entityList.find((e) => e.classname === classname);
|
|
1239
|
-
}),
|
|
1240
|
-
beginFrame: vi2.fn((timeSeconds) => {
|
|
1241
|
-
entities.timeSeconds = timeSeconds;
|
|
1242
|
-
}),
|
|
1243
|
-
targetAwareness: {
|
|
1244
|
-
timeSeconds: 10,
|
|
1245
|
-
frameNumber: 1,
|
|
1246
|
-
sightEntity: null,
|
|
1247
|
-
soundEntity: null
|
|
1274
|
+
clearMeasures: () => {
|
|
1248
1275
|
},
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1276
|
+
clearResourceTimings: () => {
|
|
1277
|
+
},
|
|
1278
|
+
getEntries: () => [],
|
|
1279
|
+
getEntriesByName: () => [],
|
|
1280
|
+
getEntriesByType: () => [],
|
|
1281
|
+
mark: () => {
|
|
1282
|
+
},
|
|
1283
|
+
measure: () => {
|
|
1284
|
+
},
|
|
1285
|
+
setResourceTimingBufferSize: () => {
|
|
1286
|
+
},
|
|
1287
|
+
toJSON: () => ({}),
|
|
1288
|
+
addEventListener: () => {
|
|
1289
|
+
},
|
|
1290
|
+
removeEventListener: () => {
|
|
1291
|
+
},
|
|
1292
|
+
dispatchEvent: () => true
|
|
1293
|
+
};
|
|
1294
|
+
mockPerf.advance = (deltaMs) => {
|
|
1295
|
+
currentTime += deltaMs;
|
|
1296
|
+
};
|
|
1297
|
+
mockPerf.setTime = (time) => {
|
|
1298
|
+
currentTime = time;
|
|
1299
|
+
};
|
|
1300
|
+
return mockPerf;
|
|
1301
|
+
}
|
|
1302
|
+
function createControlledTimer() {
|
|
1303
|
+
let currentTime = 0;
|
|
1304
|
+
let timers = [];
|
|
1305
|
+
let nextId = 1;
|
|
1306
|
+
const originalSetTimeout = global.setTimeout;
|
|
1307
|
+
const originalClearTimeout = global.clearTimeout;
|
|
1308
|
+
const originalSetInterval = global.setInterval;
|
|
1309
|
+
const originalClearInterval = global.clearInterval;
|
|
1310
|
+
const mockSetTimeout = (callback, delay = 0, ...args) => {
|
|
1311
|
+
const id = nextId++;
|
|
1312
|
+
timers.push({ id, callback, dueTime: currentTime + delay, args });
|
|
1313
|
+
return id;
|
|
1314
|
+
};
|
|
1315
|
+
const mockClearTimeout = (id) => {
|
|
1316
|
+
timers = timers.filter((t) => t.id !== id);
|
|
1317
|
+
};
|
|
1318
|
+
const mockSetInterval = (callback, delay = 0, ...args) => {
|
|
1319
|
+
const id = nextId++;
|
|
1320
|
+
timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
|
|
1321
|
+
return id;
|
|
1322
|
+
};
|
|
1323
|
+
const mockClearInterval = (id) => {
|
|
1324
|
+
timers = timers.filter((t) => t.id !== id);
|
|
1257
1325
|
};
|
|
1326
|
+
global.setTimeout = mockSetTimeout;
|
|
1327
|
+
global.clearTimeout = mockClearTimeout;
|
|
1328
|
+
global.setInterval = mockSetInterval;
|
|
1329
|
+
global.clearInterval = mockClearInterval;
|
|
1258
1330
|
return {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1331
|
+
tick() {
|
|
1332
|
+
this.advanceBy(0);
|
|
1333
|
+
},
|
|
1334
|
+
advanceBy(ms) {
|
|
1335
|
+
const targetTime = currentTime + ms;
|
|
1336
|
+
while (true) {
|
|
1337
|
+
let earliest = null;
|
|
1338
|
+
for (const t of timers) {
|
|
1339
|
+
if (!earliest || t.dueTime < earliest.dueTime) {
|
|
1340
|
+
earliest = t;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (!earliest || earliest.dueTime > targetTime) {
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
currentTime = earliest.dueTime;
|
|
1347
|
+
const { callback, args, interval, id } = earliest;
|
|
1348
|
+
if (interval !== void 0) {
|
|
1349
|
+
earliest.dueTime += interval;
|
|
1350
|
+
if (interval === 0) earliest.dueTime += 1;
|
|
1351
|
+
} else {
|
|
1352
|
+
timers = timers.filter((t) => t.id !== id);
|
|
1353
|
+
}
|
|
1354
|
+
callback(...args);
|
|
1355
|
+
}
|
|
1356
|
+
currentTime = targetTime;
|
|
1357
|
+
},
|
|
1358
|
+
clear() {
|
|
1359
|
+
timers = [];
|
|
1360
|
+
},
|
|
1361
|
+
restore() {
|
|
1362
|
+
global.setTimeout = originalSetTimeout;
|
|
1363
|
+
global.clearTimeout = originalClearTimeout;
|
|
1364
|
+
global.setInterval = originalSetInterval;
|
|
1365
|
+
global.clearInterval = originalClearInterval;
|
|
1366
|
+
}
|
|
1270
1367
|
};
|
|
1271
1368
|
}
|
|
1272
|
-
function
|
|
1273
|
-
|
|
1369
|
+
function simulateFrames(count, frameTimeMs = 16.6, callback) {
|
|
1370
|
+
if (!activeMockRAF) {
|
|
1371
|
+
throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
|
|
1372
|
+
}
|
|
1373
|
+
for (let i = 0; i < count; i++) {
|
|
1374
|
+
if (callback) callback(i);
|
|
1375
|
+
activeMockRAF.advance(frameTimeMs);
|
|
1376
|
+
}
|
|
1274
1377
|
}
|
|
1275
|
-
function
|
|
1276
|
-
|
|
1378
|
+
function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
|
|
1379
|
+
for (let i = 0; i < count; i++) {
|
|
1380
|
+
if (callback) callback(i);
|
|
1381
|
+
mock.advance(frameTimeMs);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/e2e/playwright.ts
|
|
1386
|
+
import { chromium } from "playwright";
|
|
1387
|
+
import { createServer } from "http";
|
|
1388
|
+
import handler from "serve-handler";
|
|
1389
|
+
async function createPlaywrightTestClient(options = {}) {
|
|
1390
|
+
let staticServer;
|
|
1391
|
+
let clientUrl = options.clientUrl;
|
|
1392
|
+
const rootPath = options.rootPath || process.cwd();
|
|
1393
|
+
if (!clientUrl) {
|
|
1394
|
+
staticServer = createServer((request, response) => {
|
|
1395
|
+
return handler(request, response, {
|
|
1396
|
+
public: rootPath,
|
|
1397
|
+
cleanUrls: false,
|
|
1398
|
+
headers: [
|
|
1399
|
+
{
|
|
1400
|
+
source: "**/*",
|
|
1401
|
+
headers: [
|
|
1402
|
+
{ key: "Cache-Control", value: "no-cache" },
|
|
1403
|
+
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
1404
|
+
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
|
1405
|
+
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
|
|
1406
|
+
]
|
|
1407
|
+
}
|
|
1408
|
+
]
|
|
1409
|
+
});
|
|
1410
|
+
});
|
|
1411
|
+
await new Promise((resolve) => {
|
|
1412
|
+
if (!staticServer) return;
|
|
1413
|
+
staticServer.listen(0, () => {
|
|
1414
|
+
const addr = staticServer?.address();
|
|
1415
|
+
const port = typeof addr === "object" ? addr?.port : 0;
|
|
1416
|
+
clientUrl = `http://localhost:${port}`;
|
|
1417
|
+
console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
|
|
1418
|
+
resolve();
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
const browser = await chromium.launch({
|
|
1423
|
+
headless: options.headless ?? true,
|
|
1424
|
+
args: [
|
|
1425
|
+
"--use-gl=egl",
|
|
1426
|
+
"--ignore-gpu-blocklist",
|
|
1427
|
+
...options.launchOptions?.args || []
|
|
1428
|
+
],
|
|
1429
|
+
...options.launchOptions
|
|
1430
|
+
});
|
|
1431
|
+
const width = options.width || 1280;
|
|
1432
|
+
const height = options.height || 720;
|
|
1433
|
+
const context = await browser.newContext({
|
|
1434
|
+
viewport: { width, height },
|
|
1435
|
+
deviceScaleFactor: 1,
|
|
1436
|
+
...options.contextOptions
|
|
1437
|
+
});
|
|
1438
|
+
const page = await context.newPage();
|
|
1439
|
+
const close = async () => {
|
|
1440
|
+
await browser.close();
|
|
1441
|
+
if (staticServer) {
|
|
1442
|
+
staticServer.close();
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
const navigate = async (url) => {
|
|
1446
|
+
const targetUrl = url || clientUrl;
|
|
1447
|
+
if (!targetUrl) throw new Error("No URL to navigate to");
|
|
1448
|
+
let finalUrl = targetUrl;
|
|
1449
|
+
if (options.serverUrl && !targetUrl.includes("connect=")) {
|
|
1450
|
+
const separator = targetUrl.includes("?") ? "&" : "?";
|
|
1451
|
+
finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
|
|
1452
|
+
}
|
|
1453
|
+
console.log(`Navigating to: ${finalUrl}`);
|
|
1454
|
+
await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
|
|
1455
|
+
};
|
|
1456
|
+
return {
|
|
1457
|
+
browser,
|
|
1458
|
+
context,
|
|
1459
|
+
page,
|
|
1460
|
+
server: staticServer,
|
|
1461
|
+
close,
|
|
1462
|
+
navigate,
|
|
1463
|
+
waitForGame: async (timeout = 1e4) => {
|
|
1464
|
+
await waitForGameReady(page, timeout);
|
|
1465
|
+
},
|
|
1466
|
+
injectInput: async (type, data) => {
|
|
1467
|
+
await page.evaluate(({ type: type2, data: data2 }) => {
|
|
1468
|
+
if (window.injectGameInput) window.injectGameInput(type2, data2);
|
|
1469
|
+
}, { type, data });
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
async function waitForGameReady(page, timeout = 1e4) {
|
|
1474
|
+
try {
|
|
1475
|
+
await page.waitForFunction(() => {
|
|
1476
|
+
return window.gameInstance && window.gameInstance.isReady;
|
|
1477
|
+
}, null, { timeout });
|
|
1478
|
+
} catch (e) {
|
|
1479
|
+
await page.waitForSelector("canvas", { timeout });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async function captureGameState(page) {
|
|
1483
|
+
return await page.evaluate(() => {
|
|
1484
|
+
if (window.gameInstance && window.gameInstance.getState) {
|
|
1485
|
+
return window.gameInstance.getState();
|
|
1486
|
+
}
|
|
1487
|
+
return {};
|
|
1488
|
+
});
|
|
1277
1489
|
}
|
|
1278
1490
|
export {
|
|
1279
1491
|
InputInjector,
|
|
1280
1492
|
MockPointerLock,
|
|
1493
|
+
MockTransport,
|
|
1281
1494
|
captureAudioEvents,
|
|
1282
1495
|
captureCanvasDrawCalls,
|
|
1283
1496
|
captureGameState,
|
|
@@ -1292,13 +1505,21 @@ export {
|
|
|
1292
1505
|
createMockCanvasContext2D,
|
|
1293
1506
|
createMockEngine,
|
|
1294
1507
|
createMockGame,
|
|
1508
|
+
createMockGameState,
|
|
1295
1509
|
createMockImage,
|
|
1296
1510
|
createMockImageData,
|
|
1297
1511
|
createMockIndexedDB,
|
|
1298
1512
|
createMockLocalStorage,
|
|
1513
|
+
createMockNetworkAddress,
|
|
1299
1514
|
createMockPerformance,
|
|
1300
1515
|
createMockRAF,
|
|
1516
|
+
createMockServer,
|
|
1517
|
+
createMockServerClient,
|
|
1518
|
+
createMockServerState,
|
|
1519
|
+
createMockServerStatic,
|
|
1301
1520
|
createMockSessionStorage,
|
|
1521
|
+
createMockTransport,
|
|
1522
|
+
createMockUDPSocket,
|
|
1302
1523
|
createMockWebGL2Context,
|
|
1303
1524
|
createNetChanMock,
|
|
1304
1525
|
createPlayerStateFactory,
|