@quake2ts/server 0.0.1 → 0.0.739
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/dist/cjs/index.cjs +1537 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.js +1505 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/client.d.ts +52 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/dedicated.d.ts +73 -0
- package/dist/types/dedicated.d.ts.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/net/nodeWsDriver.d.ts +17 -0
- package/dist/types/net/nodeWsDriver.d.ts.map +1 -0
- package/dist/types/protocol/player.d.ts +3 -0
- package/dist/types/protocol/player.d.ts.map +1 -0
- package/dist/types/protocol/write.d.ts +8 -0
- package/dist/types/protocol/write.d.ts.map +1 -0
- package/dist/types/protocol.d.ts +18 -0
- package/dist/types/protocol.d.ts.map +1 -0
- package/dist/types/server.d.ts +51 -0
- package/dist/types/server.d.ts.map +1 -0
- package/dist/types/transport.d.ts +8 -0
- package/dist/types/transport.d.ts.map +1 -0
- package/dist/types/transports/websocket.d.ts +12 -0
- package/dist/types/transports/websocket.d.ts.map +1 -0
- package/package.json +26 -14
- package/test-results/junit.xml +111 -0
- package/tsconfig.json +11 -5
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,1505 @@
|
|
|
1
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
2
|
+
import { MulticastType, createGame, Solid, createPlayerWeaponStates, createPlayerInventory } from '@quake2ts/game';
|
|
3
|
+
import { UPDATE_BACKUP, NetChan, ClientCommand, MAX_EDICTS, MAX_CONFIGSTRINGS, CollisionEntityIndex, BinaryWriter, ServerCommand, writeDeltaEntity, BinaryStream, writePlayerState, writeRemoveEntity, crc8, ConfigStringIndex, inPHS, inPVS, lerpAngle, traceBox, TempEntity } from '@quake2ts/shared';
|
|
4
|
+
import { PakArchive, parseBsp } from '@quake2ts/engine';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
// src/net/nodeWsDriver.ts
|
|
9
|
+
var WebSocketNetDriver = class {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.socket = null;
|
|
12
|
+
this.messageCallback = null;
|
|
13
|
+
this.closeCallback = null;
|
|
14
|
+
this.errorCallback = null;
|
|
15
|
+
}
|
|
16
|
+
async connect(url) {
|
|
17
|
+
return new Promise((resolve2, reject) => {
|
|
18
|
+
try {
|
|
19
|
+
this.socket = new WebSocket(url);
|
|
20
|
+
this.socket.binaryType = "arraybuffer";
|
|
21
|
+
this.socket.onopen = () => {
|
|
22
|
+
resolve2();
|
|
23
|
+
};
|
|
24
|
+
this.socket.onerror = (event) => {
|
|
25
|
+
const error = new Error("WebSocket connection error " + event.message);
|
|
26
|
+
if (this.errorCallback) {
|
|
27
|
+
this.errorCallback(error);
|
|
28
|
+
}
|
|
29
|
+
reject(error);
|
|
30
|
+
};
|
|
31
|
+
this.socket.onclose = () => {
|
|
32
|
+
if (this.closeCallback) {
|
|
33
|
+
this.closeCallback();
|
|
34
|
+
}
|
|
35
|
+
this.socket = null;
|
|
36
|
+
};
|
|
37
|
+
this.socket.onmessage = (event) => {
|
|
38
|
+
if (this.messageCallback) {
|
|
39
|
+
if (event.data instanceof ArrayBuffer) {
|
|
40
|
+
this.messageCallback(new Uint8Array(event.data));
|
|
41
|
+
} else if (Buffer.isBuffer(event.data)) {
|
|
42
|
+
this.messageCallback(new Uint8Array(event.data));
|
|
43
|
+
} else if (Array.isArray(event.data)) {
|
|
44
|
+
const totalLength = event.data.reduce((acc, buf) => acc + buf.length, 0);
|
|
45
|
+
const result = new Uint8Array(totalLength);
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const buf of event.data) {
|
|
48
|
+
result.set(buf, offset);
|
|
49
|
+
offset += buf.length;
|
|
50
|
+
}
|
|
51
|
+
this.messageCallback(result);
|
|
52
|
+
} else {
|
|
53
|
+
console.warn("Received non-binary message from server", typeof event.data);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
} catch (e) {
|
|
58
|
+
reject(e);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Method to attach an existing socket (server-side incoming connection)
|
|
63
|
+
attach(socket) {
|
|
64
|
+
this.socket = socket;
|
|
65
|
+
this.socket.binaryType = "arraybuffer";
|
|
66
|
+
this.socket.onclose = () => {
|
|
67
|
+
if (this.closeCallback) this.closeCallback();
|
|
68
|
+
this.socket = null;
|
|
69
|
+
};
|
|
70
|
+
this.socket.onerror = (event) => {
|
|
71
|
+
if (this.errorCallback) this.errorCallback(new Error(event.message));
|
|
72
|
+
};
|
|
73
|
+
this.socket.onmessage = (event) => {
|
|
74
|
+
if (this.messageCallback) {
|
|
75
|
+
if (event.data instanceof ArrayBuffer) {
|
|
76
|
+
this.messageCallback(new Uint8Array(event.data));
|
|
77
|
+
} else if (Buffer.isBuffer(event.data)) {
|
|
78
|
+
this.messageCallback(new Uint8Array(event.data));
|
|
79
|
+
} else if (Array.isArray(event.data)) {
|
|
80
|
+
const totalLength = event.data.reduce((acc, buf) => acc + buf.length, 0);
|
|
81
|
+
const result = new Uint8Array(totalLength);
|
|
82
|
+
let offset = 0;
|
|
83
|
+
for (const buf of event.data) {
|
|
84
|
+
result.set(buf, offset);
|
|
85
|
+
offset += buf.length;
|
|
86
|
+
}
|
|
87
|
+
this.messageCallback(result);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
disconnect() {
|
|
93
|
+
if (this.socket) {
|
|
94
|
+
this.socket.close();
|
|
95
|
+
this.socket = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
send(data) {
|
|
99
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
100
|
+
this.socket.send(data);
|
|
101
|
+
} else {
|
|
102
|
+
console.warn("Attempted to send data on closed or connecting socket");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
onMessage(callback) {
|
|
106
|
+
this.messageCallback = callback;
|
|
107
|
+
}
|
|
108
|
+
onClose(callback) {
|
|
109
|
+
this.closeCallback = callback;
|
|
110
|
+
}
|
|
111
|
+
onError(callback) {
|
|
112
|
+
this.errorCallback = callback;
|
|
113
|
+
}
|
|
114
|
+
isConnected() {
|
|
115
|
+
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var ClientState = /* @__PURE__ */ ((ClientState2) => {
|
|
119
|
+
ClientState2[ClientState2["Free"] = 0] = "Free";
|
|
120
|
+
ClientState2[ClientState2["Zombie"] = 1] = "Zombie";
|
|
121
|
+
ClientState2[ClientState2["Connected"] = 2] = "Connected";
|
|
122
|
+
ClientState2[ClientState2["Spawned"] = 3] = "Spawned";
|
|
123
|
+
ClientState2[ClientState2["Active"] = 4] = "Active";
|
|
124
|
+
return ClientState2;
|
|
125
|
+
})(ClientState || {});
|
|
126
|
+
function createClient(index, net) {
|
|
127
|
+
const frames = [];
|
|
128
|
+
for (let i = 0; i < UPDATE_BACKUP; i++) {
|
|
129
|
+
frames.push({
|
|
130
|
+
areaBytes: 0,
|
|
131
|
+
areaBits: new Uint8Array(0),
|
|
132
|
+
// Size depends on map areas
|
|
133
|
+
playerState: createEmptyPlayerState(),
|
|
134
|
+
numEntities: 0,
|
|
135
|
+
firstEntity: 0,
|
|
136
|
+
sentTime: 0,
|
|
137
|
+
entities: [],
|
|
138
|
+
packetCRC: 0
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const netchan = new NetChan();
|
|
142
|
+
netchan.setup(Math.floor(Math.random() * 65536));
|
|
143
|
+
return {
|
|
144
|
+
index,
|
|
145
|
+
state: 2 /* Connected */,
|
|
146
|
+
net,
|
|
147
|
+
netchan,
|
|
148
|
+
userInfo: "",
|
|
149
|
+
lastFrame: 0,
|
|
150
|
+
lastCmd: createEmptyUserCommand(),
|
|
151
|
+
commandMsec: 0,
|
|
152
|
+
frameLatency: [],
|
|
153
|
+
ping: 0,
|
|
154
|
+
messageSize: [],
|
|
155
|
+
rate: 25e3,
|
|
156
|
+
// Default rate
|
|
157
|
+
suppressCount: 0,
|
|
158
|
+
edict: null,
|
|
159
|
+
name: `Player ${index}`,
|
|
160
|
+
messageLevel: 0,
|
|
161
|
+
datagram: new Uint8Array(0),
|
|
162
|
+
frames,
|
|
163
|
+
downloadSize: 0,
|
|
164
|
+
downloadCount: 0,
|
|
165
|
+
lastMessage: 0,
|
|
166
|
+
lastConnect: Date.now(),
|
|
167
|
+
challenge: 0,
|
|
168
|
+
messageQueue: [],
|
|
169
|
+
lastPacketEntities: [],
|
|
170
|
+
commandQueue: [],
|
|
171
|
+
lastCommandTime: 0,
|
|
172
|
+
commandCount: 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function createEmptyUserCommand() {
|
|
176
|
+
return {
|
|
177
|
+
msec: 0,
|
|
178
|
+
buttons: 0,
|
|
179
|
+
angles: { x: 0, y: 0, z: 0 },
|
|
180
|
+
forwardmove: 0,
|
|
181
|
+
sidemove: 0,
|
|
182
|
+
upmove: 0,
|
|
183
|
+
sequence: 0,
|
|
184
|
+
lightlevel: 0,
|
|
185
|
+
impulse: 0
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function createEmptyPlayerState() {
|
|
189
|
+
return {
|
|
190
|
+
origin: { x: 0, y: 0, z: 0 },
|
|
191
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
192
|
+
viewAngles: { x: 0, y: 0, z: 0 },
|
|
193
|
+
onGround: false,
|
|
194
|
+
waterLevel: 0,
|
|
195
|
+
watertype: 0,
|
|
196
|
+
mins: { x: -16, y: -16, z: -24 },
|
|
197
|
+
maxs: { x: 16, y: 16, z: 32 },
|
|
198
|
+
damageAlpha: 0,
|
|
199
|
+
damageIndicators: [],
|
|
200
|
+
blend: [0, 0, 0, 0],
|
|
201
|
+
// Stubs for new fields
|
|
202
|
+
stats: [],
|
|
203
|
+
kick_angles: { x: 0, y: 0, z: 0 },
|
|
204
|
+
kick_origin: { x: 0, y: 0, z: 0 },
|
|
205
|
+
gunoffset: { x: 0, y: 0, z: 0 },
|
|
206
|
+
gunangles: { x: 0, y: 0, z: 0 },
|
|
207
|
+
gunindex: 0,
|
|
208
|
+
pm_type: 0,
|
|
209
|
+
pm_time: 0,
|
|
210
|
+
pm_flags: 0,
|
|
211
|
+
gun_frame: 0,
|
|
212
|
+
rdflags: 0,
|
|
213
|
+
fov: 90,
|
|
214
|
+
renderfx: 0
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
var ClientMessageParser = class {
|
|
218
|
+
constructor(stream, handler) {
|
|
219
|
+
this.stream = stream;
|
|
220
|
+
this.handler = handler;
|
|
221
|
+
}
|
|
222
|
+
parseMessage() {
|
|
223
|
+
while (this.stream.hasMore()) {
|
|
224
|
+
const cmdId = this.stream.readByte();
|
|
225
|
+
if (cmdId === -1) break;
|
|
226
|
+
switch (cmdId) {
|
|
227
|
+
case ClientCommand.move:
|
|
228
|
+
this.parseMove();
|
|
229
|
+
break;
|
|
230
|
+
case ClientCommand.userinfo:
|
|
231
|
+
this.parseUserInfo();
|
|
232
|
+
break;
|
|
233
|
+
case ClientCommand.stringcmd:
|
|
234
|
+
this.parseStringCmd();
|
|
235
|
+
break;
|
|
236
|
+
case ClientCommand.nop:
|
|
237
|
+
this.handler.onNop();
|
|
238
|
+
break;
|
|
239
|
+
default:
|
|
240
|
+
console.warn(`Unknown client command: ${cmdId}`);
|
|
241
|
+
this.handler.onBad();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
parseMove() {
|
|
247
|
+
const checksum = this.stream.readByte();
|
|
248
|
+
const lastFrame = this.stream.readLong();
|
|
249
|
+
const msec = this.stream.readByte();
|
|
250
|
+
const buttons = this.stream.readByte();
|
|
251
|
+
const angles = {
|
|
252
|
+
x: this.stream.readShort() * (360 / 65536),
|
|
253
|
+
y: this.stream.readShort() * (360 / 65536),
|
|
254
|
+
z: this.stream.readShort() * (360 / 65536)
|
|
255
|
+
};
|
|
256
|
+
const forwardmove = this.stream.readShort();
|
|
257
|
+
const sidemove = this.stream.readShort();
|
|
258
|
+
const upmove = this.stream.readShort();
|
|
259
|
+
const impulse = this.stream.readByte();
|
|
260
|
+
const lightlevel = this.stream.readByte();
|
|
261
|
+
const userCmd = {
|
|
262
|
+
msec,
|
|
263
|
+
buttons,
|
|
264
|
+
angles,
|
|
265
|
+
forwardmove,
|
|
266
|
+
sidemove,
|
|
267
|
+
upmove,
|
|
268
|
+
impulse,
|
|
269
|
+
lightlevel,
|
|
270
|
+
sequence: 0
|
|
271
|
+
// Server doesn't read sequence from packet body in standard protocol, it tracks it
|
|
272
|
+
};
|
|
273
|
+
this.handler.onMove(checksum, lastFrame, userCmd);
|
|
274
|
+
}
|
|
275
|
+
parseUserInfo() {
|
|
276
|
+
const info = this.stream.readString();
|
|
277
|
+
this.handler.onUserInfo(info);
|
|
278
|
+
}
|
|
279
|
+
parseStringCmd() {
|
|
280
|
+
const cmd = this.stream.readString();
|
|
281
|
+
this.handler.onStringCmd(cmd);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/server.ts
|
|
286
|
+
var ServerState = /* @__PURE__ */ ((ServerState2) => {
|
|
287
|
+
ServerState2[ServerState2["Dead"] = 0] = "Dead";
|
|
288
|
+
ServerState2[ServerState2["Loading"] = 1] = "Loading";
|
|
289
|
+
ServerState2[ServerState2["Game"] = 2] = "Game";
|
|
290
|
+
ServerState2[ServerState2["Cinematic"] = 3] = "Cinematic";
|
|
291
|
+
ServerState2[ServerState2["Demo"] = 4] = "Demo";
|
|
292
|
+
ServerState2[ServerState2["Pic"] = 5] = "Pic";
|
|
293
|
+
return ServerState2;
|
|
294
|
+
})(ServerState || {});
|
|
295
|
+
function writeServerCommand(writer, event, ...args) {
|
|
296
|
+
writer.writeByte(event);
|
|
297
|
+
switch (event) {
|
|
298
|
+
case ServerCommand.print: {
|
|
299
|
+
const level = args[0];
|
|
300
|
+
const text = args[1];
|
|
301
|
+
writer.writeByte(level);
|
|
302
|
+
writer.writeString(text);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case ServerCommand.centerprint: {
|
|
306
|
+
const text = args[0];
|
|
307
|
+
writer.writeString(text);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case ServerCommand.stufftext: {
|
|
311
|
+
const text = args[0];
|
|
312
|
+
writer.writeString(text);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case ServerCommand.sound: {
|
|
316
|
+
const flags = args[0];
|
|
317
|
+
const soundNum = args[1];
|
|
318
|
+
const volume = args[2];
|
|
319
|
+
const attenuation = args[3];
|
|
320
|
+
const offset = args[4];
|
|
321
|
+
const ent = args[5];
|
|
322
|
+
const pos = args[6];
|
|
323
|
+
writer.writeByte(flags);
|
|
324
|
+
writer.writeByte(soundNum);
|
|
325
|
+
if (flags & 1) {
|
|
326
|
+
writer.writeByte(volume || 0);
|
|
327
|
+
}
|
|
328
|
+
if (flags & 2) {
|
|
329
|
+
writer.writeByte(attenuation || 0);
|
|
330
|
+
}
|
|
331
|
+
if (flags & 16) {
|
|
332
|
+
writer.writeByte(offset || 0);
|
|
333
|
+
}
|
|
334
|
+
if (flags & 8) {
|
|
335
|
+
writer.writeShort(ent || 0);
|
|
336
|
+
}
|
|
337
|
+
if (flags & 4) {
|
|
338
|
+
if (pos) {
|
|
339
|
+
writer.writePos(pos);
|
|
340
|
+
} else {
|
|
341
|
+
writer.writePos({ x: 0, y: 0, z: 0 });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case ServerCommand.muzzleflash: {
|
|
347
|
+
const entIndex = args[0];
|
|
348
|
+
const flashType = args[1];
|
|
349
|
+
writer.writeShort(entIndex);
|
|
350
|
+
writer.writeByte(flashType);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case ServerCommand.temp_entity: {
|
|
354
|
+
const type = args[0];
|
|
355
|
+
writer.writeByte(type);
|
|
356
|
+
writeTempEntity(writer, type, args.slice(1));
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
default:
|
|
360
|
+
console.warn(`writeServerCommand: Unhandled command ${event}`);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function writeTempEntity(writer, type, args) {
|
|
365
|
+
switch (type) {
|
|
366
|
+
case TempEntity.ROCKET_EXPLOSION:
|
|
367
|
+
case TempEntity.GRENADE_EXPLOSION:
|
|
368
|
+
case TempEntity.EXPLOSION1:
|
|
369
|
+
case TempEntity.EXPLOSION2:
|
|
370
|
+
case TempEntity.ROCKET_EXPLOSION_WATER:
|
|
371
|
+
case TempEntity.GRENADE_EXPLOSION_WATER:
|
|
372
|
+
case TempEntity.BFG_EXPLOSION:
|
|
373
|
+
case TempEntity.BFG_BIGEXPLOSION:
|
|
374
|
+
case TempEntity.PLASMA_EXPLOSION:
|
|
375
|
+
case TempEntity.PLAIN_EXPLOSION:
|
|
376
|
+
case TempEntity.TRACKER_EXPLOSION:
|
|
377
|
+
case TempEntity.EXPLOSION1_BIG:
|
|
378
|
+
case TempEntity.EXPLOSION1_NP:
|
|
379
|
+
case TempEntity.EXPLOSION1_NL:
|
|
380
|
+
case TempEntity.EXPLOSION2_NL:
|
|
381
|
+
case TempEntity.BERSERK_SLAM:
|
|
382
|
+
writer.writePos(args[0]);
|
|
383
|
+
break;
|
|
384
|
+
case TempEntity.BLASTER:
|
|
385
|
+
case TempEntity.FLECHETTE:
|
|
386
|
+
writer.writePos(args[0]);
|
|
387
|
+
writer.writeDir(args[1]);
|
|
388
|
+
break;
|
|
389
|
+
case TempEntity.RAILTRAIL:
|
|
390
|
+
case TempEntity.DEBUGTRAIL:
|
|
391
|
+
case TempEntity.BUBBLETRAIL:
|
|
392
|
+
case TempEntity.BUBBLETRAIL2:
|
|
393
|
+
case TempEntity.BFG_LASER:
|
|
394
|
+
case TempEntity.LIGHTNING_BEAM:
|
|
395
|
+
case TempEntity.LIGHTNING:
|
|
396
|
+
writer.writePos(args[0]);
|
|
397
|
+
writer.writePos(args[1]);
|
|
398
|
+
break;
|
|
399
|
+
case TempEntity.LASER_SPARKS:
|
|
400
|
+
case TempEntity.WELDING_SPARKS:
|
|
401
|
+
case TempEntity.TUNNEL_SPARKS:
|
|
402
|
+
case TempEntity.ELECTRIC_SPARKS:
|
|
403
|
+
case TempEntity.HEATBEAM_SPARKS:
|
|
404
|
+
case TempEntity.HEATBEAM_STEAM:
|
|
405
|
+
case TempEntity.STEAM:
|
|
406
|
+
writer.writeByte(args[0]);
|
|
407
|
+
writer.writePos(args[1]);
|
|
408
|
+
writer.writeDir(args[2]);
|
|
409
|
+
writer.writeByte(args[3] || 0);
|
|
410
|
+
break;
|
|
411
|
+
case TempEntity.PARASITE_ATTACK:
|
|
412
|
+
case TempEntity.MEDIC_CABLE_ATTACK:
|
|
413
|
+
const ent = args[0];
|
|
414
|
+
writer.writeShort(ent ? ent.index : 0);
|
|
415
|
+
writer.writePos(args[1]);
|
|
416
|
+
writer.writePos(args[2]);
|
|
417
|
+
break;
|
|
418
|
+
case TempEntity.GUNSHOT:
|
|
419
|
+
case TempEntity.BLOOD:
|
|
420
|
+
case TempEntity.SPARKS:
|
|
421
|
+
case TempEntity.BULLET_SPARKS:
|
|
422
|
+
case TempEntity.SCREEN_SPARKS:
|
|
423
|
+
case TempEntity.SHIELD_SPARKS:
|
|
424
|
+
writer.writePos(args[0]);
|
|
425
|
+
writer.writeDir(args[1]);
|
|
426
|
+
break;
|
|
427
|
+
case TempEntity.SPLASH:
|
|
428
|
+
case TempEntity.POWER_SPLASH:
|
|
429
|
+
case TempEntity.WIDOWSPLASH:
|
|
430
|
+
writer.writeByte(args[0]);
|
|
431
|
+
writer.writePos(args[1]);
|
|
432
|
+
writer.writeDir(args[2]);
|
|
433
|
+
writer.writeByte(args[3] || 0);
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
console.warn(`writeTempEntity: Unhandled TempEntity ${type}`);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
var WebSocketTransport = class {
|
|
441
|
+
constructor() {
|
|
442
|
+
this.wss = null;
|
|
443
|
+
this.connectionCallback = null;
|
|
444
|
+
this.errorCallback = null;
|
|
445
|
+
}
|
|
446
|
+
async listen(port) {
|
|
447
|
+
return new Promise((resolve2) => {
|
|
448
|
+
this.wss = new WebSocketServer({ port });
|
|
449
|
+
this.wss.on("listening", () => resolve2());
|
|
450
|
+
this.wss.on("connection", (ws, req) => {
|
|
451
|
+
const driver = new WebSocketNetDriver();
|
|
452
|
+
driver.attach(ws);
|
|
453
|
+
if (this.connectionCallback) {
|
|
454
|
+
this.connectionCallback(driver, req);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
this.wss.on("error", (err) => {
|
|
458
|
+
if (this.errorCallback) this.errorCallback(err);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
close() {
|
|
463
|
+
if (this.wss) {
|
|
464
|
+
this.wss.close();
|
|
465
|
+
this.wss = null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
onConnection(callback) {
|
|
469
|
+
this.connectionCallback = callback;
|
|
470
|
+
}
|
|
471
|
+
onError(callback) {
|
|
472
|
+
this.errorCallback = callback;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/dedicated.ts
|
|
477
|
+
var DEFAULT_MAX_CLIENTS = 16;
|
|
478
|
+
var FRAME_RATE = 10;
|
|
479
|
+
var FRAME_TIME_MS = 1e3 / FRAME_RATE;
|
|
480
|
+
var DedicatedServer = class {
|
|
481
|
+
constructor(optionsOrPort = {}) {
|
|
482
|
+
this.game = null;
|
|
483
|
+
this.frameTimeout = null;
|
|
484
|
+
this.entityIndex = null;
|
|
485
|
+
// History buffer: Map<EntityIndex, HistoryArray>
|
|
486
|
+
this.history = /* @__PURE__ */ new Map();
|
|
487
|
+
this.backup = /* @__PURE__ */ new Map();
|
|
488
|
+
const options = typeof optionsOrPort === "number" ? { port: optionsOrPort } : optionsOrPort;
|
|
489
|
+
this.options = {
|
|
490
|
+
port: 27910,
|
|
491
|
+
maxPlayers: DEFAULT_MAX_CLIENTS,
|
|
492
|
+
deathmatch: true,
|
|
493
|
+
...options
|
|
494
|
+
};
|
|
495
|
+
this.transport = this.options.transport || new WebSocketTransport();
|
|
496
|
+
this.svs = {
|
|
497
|
+
initialized: false,
|
|
498
|
+
realTime: 0,
|
|
499
|
+
mapCmd: "",
|
|
500
|
+
spawnCount: 0,
|
|
501
|
+
clients: new Array(this.options.maxPlayers).fill(null),
|
|
502
|
+
lastHeartbeat: 0,
|
|
503
|
+
challenges: []
|
|
504
|
+
};
|
|
505
|
+
this.sv = {
|
|
506
|
+
state: 0 /* Dead */,
|
|
507
|
+
attractLoop: false,
|
|
508
|
+
loadGame: false,
|
|
509
|
+
startTime: 0,
|
|
510
|
+
// Initialize startTime
|
|
511
|
+
time: 0,
|
|
512
|
+
frame: 0,
|
|
513
|
+
name: "",
|
|
514
|
+
collisionModel: null,
|
|
515
|
+
configStrings: new Array(MAX_CONFIGSTRINGS).fill(""),
|
|
516
|
+
baselines: new Array(MAX_EDICTS).fill(null),
|
|
517
|
+
multicastBuf: new Uint8Array(0)
|
|
518
|
+
};
|
|
519
|
+
this.entityIndex = new CollisionEntityIndex();
|
|
520
|
+
}
|
|
521
|
+
setTransport(transport) {
|
|
522
|
+
if (this.svs.initialized) {
|
|
523
|
+
throw new Error("Cannot set transport after server started");
|
|
524
|
+
}
|
|
525
|
+
this.transport = transport;
|
|
526
|
+
}
|
|
527
|
+
async startServer(mapName) {
|
|
528
|
+
const map = mapName || this.options.mapName;
|
|
529
|
+
if (!map) {
|
|
530
|
+
throw new Error("No map specified");
|
|
531
|
+
}
|
|
532
|
+
await this.start(map);
|
|
533
|
+
}
|
|
534
|
+
stopServer() {
|
|
535
|
+
this.stop();
|
|
536
|
+
}
|
|
537
|
+
kickPlayer(clientId) {
|
|
538
|
+
if (clientId < 0 || clientId >= this.svs.clients.length) return;
|
|
539
|
+
const client = this.svs.clients[clientId];
|
|
540
|
+
if (client && client.state >= 2 /* Connected */) {
|
|
541
|
+
console.log(`Kicking client ${clientId}`);
|
|
542
|
+
if (client.netchan) {
|
|
543
|
+
const writer = new BinaryWriter();
|
|
544
|
+
writer.writeByte(ServerCommand.print);
|
|
545
|
+
writer.writeByte(2);
|
|
546
|
+
writer.writeString("Kicked by server.\n");
|
|
547
|
+
try {
|
|
548
|
+
const packet = client.netchan.transmit(writer.getData());
|
|
549
|
+
client.net.send(packet);
|
|
550
|
+
} catch (e) {
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
this.dropClient(client);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async changeMap(mapName) {
|
|
557
|
+
console.log(`Changing map to ${mapName}`);
|
|
558
|
+
this.multicast(
|
|
559
|
+
{ x: 0, y: 0, z: 0 },
|
|
560
|
+
MulticastType.All,
|
|
561
|
+
ServerCommand.print,
|
|
562
|
+
2,
|
|
563
|
+
`Changing map to ${mapName}...
|
|
564
|
+
`
|
|
565
|
+
);
|
|
566
|
+
if (this.frameTimeout) clearTimeout(this.frameTimeout);
|
|
567
|
+
this.sv.state = 1 /* Loading */;
|
|
568
|
+
this.sv.collisionModel = null;
|
|
569
|
+
this.sv.time = 0;
|
|
570
|
+
this.sv.frame = 0;
|
|
571
|
+
this.sv.configStrings.fill("");
|
|
572
|
+
this.sv.baselines.fill(null);
|
|
573
|
+
this.history.clear();
|
|
574
|
+
this.entityIndex = new CollisionEntityIndex();
|
|
575
|
+
await this.loadMap(mapName);
|
|
576
|
+
this.initGame();
|
|
577
|
+
for (const client of this.svs.clients) {
|
|
578
|
+
if (client && client.state >= 2 /* Connected */) {
|
|
579
|
+
client.edict = null;
|
|
580
|
+
client.state = 2 /* Connected */;
|
|
581
|
+
this.sendServerData(client);
|
|
582
|
+
client.netchan.writeReliableByte(ServerCommand.stufftext);
|
|
583
|
+
client.netchan.writeReliableString(`map ${mapName}
|
|
584
|
+
`);
|
|
585
|
+
this.handleBegin(client);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
this.runFrame();
|
|
589
|
+
}
|
|
590
|
+
getConnectedClients() {
|
|
591
|
+
const list = [];
|
|
592
|
+
for (const client of this.svs.clients) {
|
|
593
|
+
if (client && client.state >= 2 /* Connected */) {
|
|
594
|
+
list.push({
|
|
595
|
+
id: client.index,
|
|
596
|
+
name: "Player",
|
|
597
|
+
// TODO: Parse userinfo for name
|
|
598
|
+
ping: client.ping,
|
|
599
|
+
address: "unknown"
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return list;
|
|
604
|
+
}
|
|
605
|
+
async start(mapName) {
|
|
606
|
+
console.log(`Starting Dedicated Server on port ${this.options.port}...`);
|
|
607
|
+
this.sv.name = mapName;
|
|
608
|
+
this.svs.initialized = true;
|
|
609
|
+
this.svs.spawnCount++;
|
|
610
|
+
this.transport.onConnection((driver, info) => {
|
|
611
|
+
console.log("New connection", info ? `from ${info.socket?.remoteAddress}` : "");
|
|
612
|
+
this.handleConnection(driver, info);
|
|
613
|
+
});
|
|
614
|
+
this.transport.onError((err) => {
|
|
615
|
+
if (this.onServerError) this.onServerError(err);
|
|
616
|
+
});
|
|
617
|
+
await this.transport.listen(this.options.port);
|
|
618
|
+
await this.loadMap(mapName);
|
|
619
|
+
this.initGame();
|
|
620
|
+
this.runFrame();
|
|
621
|
+
console.log("Server started.");
|
|
622
|
+
}
|
|
623
|
+
async loadMap(mapName) {
|
|
624
|
+
try {
|
|
625
|
+
console.log(`Loading map ${mapName}...`);
|
|
626
|
+
this.sv.state = 1 /* Loading */;
|
|
627
|
+
this.sv.name = mapName;
|
|
628
|
+
let arrayBuffer;
|
|
629
|
+
try {
|
|
630
|
+
await fs.access(mapName);
|
|
631
|
+
const mapData = await fs.readFile(mapName);
|
|
632
|
+
arrayBuffer = mapData.buffer.slice(mapData.byteOffset, mapData.byteOffset + mapData.byteLength);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
console.log(`Map file ${mapName} not found on disk, checking pak.pak...`);
|
|
635
|
+
const possiblePakPaths = [
|
|
636
|
+
path.resolve(process.cwd(), "pak.pak"),
|
|
637
|
+
path.resolve(process.cwd(), "../pak.pak"),
|
|
638
|
+
path.resolve(process.cwd(), "../../pak.pak"),
|
|
639
|
+
path.resolve("baseq2/pak.pak")
|
|
640
|
+
];
|
|
641
|
+
let pakPath = null;
|
|
642
|
+
for (const p of possiblePakPaths) {
|
|
643
|
+
try {
|
|
644
|
+
await fs.access(p);
|
|
645
|
+
pakPath = p;
|
|
646
|
+
break;
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (!pakPath) {
|
|
651
|
+
throw new Error(`Map ${mapName} not found and pak.pak not found.`);
|
|
652
|
+
}
|
|
653
|
+
const pakBuffer = await fs.readFile(pakPath);
|
|
654
|
+
const pakArrayBuffer = pakBuffer.buffer.slice(pakBuffer.byteOffset, pakBuffer.byteOffset + pakBuffer.byteLength);
|
|
655
|
+
const pak = PakArchive.fromArrayBuffer("pak.pak", pakArrayBuffer);
|
|
656
|
+
const entry = pak.getEntry(mapName);
|
|
657
|
+
if (!entry) {
|
|
658
|
+
throw new Error(`Map ${mapName} not found in pak.pak`);
|
|
659
|
+
}
|
|
660
|
+
const data = pak.readFile(mapName);
|
|
661
|
+
arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
662
|
+
}
|
|
663
|
+
const bspMap = parseBsp(arrayBuffer);
|
|
664
|
+
const planes = bspMap.planes.map((p) => {
|
|
665
|
+
const normal = { x: p.normal[0], y: p.normal[1], z: p.normal[2] };
|
|
666
|
+
let signbits = 0;
|
|
667
|
+
if (normal.x < 0) signbits |= 1;
|
|
668
|
+
if (normal.y < 0) signbits |= 2;
|
|
669
|
+
if (normal.z < 0) signbits |= 4;
|
|
670
|
+
return {
|
|
671
|
+
normal,
|
|
672
|
+
dist: p.dist,
|
|
673
|
+
type: p.type,
|
|
674
|
+
signbits
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
const nodes = bspMap.nodes.map((n) => ({
|
|
678
|
+
plane: planes[n.planeIndex],
|
|
679
|
+
children: n.children
|
|
680
|
+
}));
|
|
681
|
+
const leafBrushes = [];
|
|
682
|
+
const leaves = bspMap.leafs.map((l, i) => {
|
|
683
|
+
const brushes2 = bspMap.leafLists.leafBrushes[i];
|
|
684
|
+
const firstLeafBrush = leafBrushes.length;
|
|
685
|
+
leafBrushes.push(...brushes2);
|
|
686
|
+
return {
|
|
687
|
+
contents: l.contents,
|
|
688
|
+
cluster: l.cluster,
|
|
689
|
+
area: l.area,
|
|
690
|
+
firstLeafBrush,
|
|
691
|
+
numLeafBrushes: brushes2.length
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
const brushes = bspMap.brushes.map((b) => {
|
|
695
|
+
const sides = [];
|
|
696
|
+
for (let i = 0; i < b.numSides; i++) {
|
|
697
|
+
const sideIndex = b.firstSide + i;
|
|
698
|
+
const bspSide = bspMap.brushSides[sideIndex];
|
|
699
|
+
const plane = planes[bspSide.planeIndex];
|
|
700
|
+
const texInfo = bspMap.texInfo[bspSide.texInfo];
|
|
701
|
+
const surfaceFlags = texInfo ? texInfo.flags : 0;
|
|
702
|
+
sides.push({
|
|
703
|
+
plane,
|
|
704
|
+
surfaceFlags
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
contents: b.contents,
|
|
709
|
+
sides,
|
|
710
|
+
checkcount: 0
|
|
711
|
+
};
|
|
712
|
+
});
|
|
713
|
+
const bmodels = bspMap.models.map((m) => ({
|
|
714
|
+
mins: { x: m.mins[0], y: m.mins[1], z: m.mins[2] },
|
|
715
|
+
maxs: { x: m.maxs[0], y: m.maxs[1], z: m.maxs[2] },
|
|
716
|
+
origin: { x: m.origin[0], y: m.origin[1], z: m.origin[2] },
|
|
717
|
+
headnode: m.headNode
|
|
718
|
+
}));
|
|
719
|
+
let visibility;
|
|
720
|
+
if (bspMap.visibility) {
|
|
721
|
+
visibility = {
|
|
722
|
+
numClusters: bspMap.visibility.numClusters,
|
|
723
|
+
clusters: bspMap.visibility.clusters
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
this.sv.collisionModel = {
|
|
727
|
+
planes,
|
|
728
|
+
nodes,
|
|
729
|
+
leaves,
|
|
730
|
+
brushes,
|
|
731
|
+
leafBrushes,
|
|
732
|
+
bmodels,
|
|
733
|
+
visibility
|
|
734
|
+
};
|
|
735
|
+
console.log(`Map loaded successfully.`);
|
|
736
|
+
} catch (e) {
|
|
737
|
+
console.warn("Failed to load map:", e);
|
|
738
|
+
if (this.onServerError) this.onServerError(e);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
initGame() {
|
|
742
|
+
this.sv.startTime = Date.now();
|
|
743
|
+
const imports = {
|
|
744
|
+
trace: (start, mins, maxs, end, passent, contentmask) => {
|
|
745
|
+
if (this.entityIndex) {
|
|
746
|
+
const result = this.entityIndex.trace({
|
|
747
|
+
start,
|
|
748
|
+
end,
|
|
749
|
+
mins: mins || void 0,
|
|
750
|
+
maxs: maxs || void 0,
|
|
751
|
+
model: this.sv.collisionModel,
|
|
752
|
+
passId: passent ? passent.index : void 0,
|
|
753
|
+
contentMask: contentmask
|
|
754
|
+
});
|
|
755
|
+
let hitEntity = null;
|
|
756
|
+
if (result.entityId !== null && result.entityId !== void 0 && this.game) {
|
|
757
|
+
hitEntity = this.game.entities.getByIndex(result.entityId) ?? null;
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
allsolid: result.allsolid,
|
|
761
|
+
startsolid: result.startsolid,
|
|
762
|
+
fraction: result.fraction,
|
|
763
|
+
endpos: result.endpos,
|
|
764
|
+
plane: result.plane || null,
|
|
765
|
+
surfaceFlags: result.surfaceFlags || 0,
|
|
766
|
+
contents: result.contents || 0,
|
|
767
|
+
ent: hitEntity
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const worldResult = this.sv.collisionModel ? traceBox({
|
|
771
|
+
start,
|
|
772
|
+
end,
|
|
773
|
+
mins: mins || void 0,
|
|
774
|
+
maxs: maxs || void 0,
|
|
775
|
+
model: this.sv.collisionModel,
|
|
776
|
+
contentMask: contentmask
|
|
777
|
+
}) : {
|
|
778
|
+
fraction: 1,
|
|
779
|
+
endpos: { ...end },
|
|
780
|
+
allsolid: false,
|
|
781
|
+
startsolid: false,
|
|
782
|
+
plane: null,
|
|
783
|
+
surfaceFlags: 0,
|
|
784
|
+
contents: 0
|
|
785
|
+
};
|
|
786
|
+
return {
|
|
787
|
+
allsolid: worldResult.allsolid,
|
|
788
|
+
startsolid: worldResult.startsolid,
|
|
789
|
+
fraction: worldResult.fraction,
|
|
790
|
+
endpos: worldResult.endpos,
|
|
791
|
+
plane: worldResult.plane || null,
|
|
792
|
+
surfaceFlags: worldResult.surfaceFlags || 0,
|
|
793
|
+
contents: worldResult.contents || 0,
|
|
794
|
+
ent: null
|
|
795
|
+
};
|
|
796
|
+
},
|
|
797
|
+
pointcontents: (p) => 0,
|
|
798
|
+
linkentity: (ent) => {
|
|
799
|
+
if (!this.entityIndex) return;
|
|
800
|
+
this.entityIndex.link({
|
|
801
|
+
id: ent.index,
|
|
802
|
+
origin: ent.origin,
|
|
803
|
+
mins: ent.mins,
|
|
804
|
+
maxs: ent.maxs,
|
|
805
|
+
contents: ent.solid === 0 ? 0 : 1,
|
|
806
|
+
surfaceFlags: 0
|
|
807
|
+
});
|
|
808
|
+
},
|
|
809
|
+
areaEdicts: (mins, maxs) => {
|
|
810
|
+
if (!this.entityIndex) return [];
|
|
811
|
+
return this.entityIndex.gatherTriggerTouches({ x: 0, y: 0, z: 0 }, mins, maxs, 4294967295);
|
|
812
|
+
},
|
|
813
|
+
multicast: (origin, type, event, ...args) => this.multicast(origin, type, event, ...args),
|
|
814
|
+
unicast: (ent, reliable, event, ...args) => this.unicast(ent, reliable, event, ...args),
|
|
815
|
+
configstring: (index, value) => this.SV_SetConfigString(index, value),
|
|
816
|
+
serverCommand: (cmd) => {
|
|
817
|
+
console.log(`Server command: ${cmd}`);
|
|
818
|
+
},
|
|
819
|
+
setLagCompensation: (active, client, lagMs) => this.setLagCompensation(active, client, lagMs)
|
|
820
|
+
};
|
|
821
|
+
this.game = createGame(imports, this, {
|
|
822
|
+
gravity: { x: 0, y: 0, z: -800 },
|
|
823
|
+
deathmatch: this.options.deathmatch !== false
|
|
824
|
+
});
|
|
825
|
+
this.game.init(0);
|
|
826
|
+
this.game.spawnWorld();
|
|
827
|
+
this.populateBaselines();
|
|
828
|
+
this.sv.state = 2 /* Game */;
|
|
829
|
+
}
|
|
830
|
+
populateBaselines() {
|
|
831
|
+
if (!this.game) return;
|
|
832
|
+
this.game.entities.forEachEntity((ent) => {
|
|
833
|
+
if (ent.index >= MAX_EDICTS) return;
|
|
834
|
+
if (ent.modelindex > 0 || ent.solid !== Solid.Not) {
|
|
835
|
+
this.sv.baselines[ent.index] = this.entityToState(ent);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
entityToState(ent) {
|
|
840
|
+
return {
|
|
841
|
+
number: ent.index,
|
|
842
|
+
origin: { ...ent.origin },
|
|
843
|
+
angles: { ...ent.angles },
|
|
844
|
+
modelIndex: ent.modelindex,
|
|
845
|
+
frame: ent.frame,
|
|
846
|
+
skinNum: ent.skin,
|
|
847
|
+
effects: ent.effects,
|
|
848
|
+
renderfx: ent.renderfx,
|
|
849
|
+
solid: ent.solid,
|
|
850
|
+
sound: ent.sounds,
|
|
851
|
+
event: 0
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
stop() {
|
|
855
|
+
if (this.frameTimeout) clearTimeout(this.frameTimeout);
|
|
856
|
+
this.transport.close();
|
|
857
|
+
this.game?.shutdown();
|
|
858
|
+
this.sv.state = 0 /* Dead */;
|
|
859
|
+
}
|
|
860
|
+
handleConnection(driver, info) {
|
|
861
|
+
let clientIndex = -1;
|
|
862
|
+
for (let i = 0; i < this.options.maxPlayers; i++) {
|
|
863
|
+
if (this.svs.clients[i] === null || this.svs.clients[i].state === 0 /* Free */) {
|
|
864
|
+
clientIndex = i;
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (clientIndex === -1) {
|
|
869
|
+
console.log("Server full, rejecting connection");
|
|
870
|
+
driver.disconnect();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const client = createClient(clientIndex, driver);
|
|
874
|
+
client.lastMessage = this.sv.frame;
|
|
875
|
+
client.lastCommandTime = Date.now();
|
|
876
|
+
this.svs.clients[clientIndex] = client;
|
|
877
|
+
console.log(`Client ${clientIndex} attached to slot from ${info?.socket?.remoteAddress || "unknown"}`);
|
|
878
|
+
driver.onMessage((data) => this.onClientMessage(client, data));
|
|
879
|
+
driver.onClose(() => this.onClientDisconnect(client));
|
|
880
|
+
}
|
|
881
|
+
onClientMessage(client, data) {
|
|
882
|
+
const buffer = data.byteOffset === 0 && data.byteLength === data.buffer.byteLength ? data.buffer : data.slice().buffer;
|
|
883
|
+
if (buffer instanceof ArrayBuffer) {
|
|
884
|
+
client.messageQueue.push(new Uint8Array(buffer));
|
|
885
|
+
} else {
|
|
886
|
+
client.messageQueue.push(new Uint8Array(buffer));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
onClientDisconnect(client) {
|
|
890
|
+
console.log(`Client ${client.index} disconnected`);
|
|
891
|
+
if (client.edict && this.game) {
|
|
892
|
+
this.game.clientDisconnect(client.edict);
|
|
893
|
+
}
|
|
894
|
+
if (this.onClientDisconnected) {
|
|
895
|
+
this.onClientDisconnected(client.index);
|
|
896
|
+
}
|
|
897
|
+
client.state = 0 /* Free */;
|
|
898
|
+
this.svs.clients[client.index] = null;
|
|
899
|
+
if (this.entityIndex && client.edict) {
|
|
900
|
+
this.entityIndex.unlink(client.edict.index);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
dropClient(client) {
|
|
904
|
+
if (client.net) {
|
|
905
|
+
client.net.disconnect();
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
handleMove(client, cmd, checksum, lastFrame) {
|
|
909
|
+
if (lastFrame > 0 && lastFrame <= client.lastFrame && lastFrame > client.lastFrame - UPDATE_BACKUP) {
|
|
910
|
+
const frameIdx = lastFrame % UPDATE_BACKUP;
|
|
911
|
+
const frame = client.frames[frameIdx];
|
|
912
|
+
if (frame.packetCRC !== checksum) {
|
|
913
|
+
console.warn(`Client ${client.index} checksum mismatch for frame ${lastFrame}: expected ${frame.packetCRC}, got ${checksum}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
client.lastCmd = cmd;
|
|
917
|
+
client.lastMessage = this.sv.frame;
|
|
918
|
+
client.commandCount++;
|
|
919
|
+
}
|
|
920
|
+
handleUserInfo(client, info) {
|
|
921
|
+
client.userInfo = info;
|
|
922
|
+
}
|
|
923
|
+
handleStringCmd(client, cmd) {
|
|
924
|
+
console.log(`Client ${client.index} stringcmd: ${cmd}`);
|
|
925
|
+
if (cmd === "getchallenge") {
|
|
926
|
+
this.handleGetChallenge(client);
|
|
927
|
+
} else if (cmd.startsWith("connect ")) {
|
|
928
|
+
const userInfo = cmd.substring(8);
|
|
929
|
+
this.handleConnect(client, userInfo);
|
|
930
|
+
} else if (cmd === "begin") {
|
|
931
|
+
this.handleBegin(client);
|
|
932
|
+
} else if (cmd === "status") {
|
|
933
|
+
this.handleStatus(client);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
handleStatus(client) {
|
|
937
|
+
let activeClients = 0;
|
|
938
|
+
for (const c of this.svs.clients) {
|
|
939
|
+
if (c && c.state >= 2 /* Connected */) {
|
|
940
|
+
activeClients++;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
let status = `map: ${this.sv.name}
|
|
944
|
+
`;
|
|
945
|
+
status += `players: ${activeClients} active (${this.options.maxPlayers} max)
|
|
946
|
+
|
|
947
|
+
`;
|
|
948
|
+
status += `num score ping name lastmsg address qport rate
|
|
949
|
+
`;
|
|
950
|
+
status += `--- ----- ---- --------------- ------- --------------------- ----- -----
|
|
951
|
+
`;
|
|
952
|
+
for (const c of this.svs.clients) {
|
|
953
|
+
if (c && c.state >= 2 /* Connected */) {
|
|
954
|
+
const score = 0;
|
|
955
|
+
const ping = 0;
|
|
956
|
+
const lastMsg = this.sv.frame - c.lastMessage;
|
|
957
|
+
const address = "unknown";
|
|
958
|
+
status += `${c.index.toString().padStart(3)} ${score.toString().padStart(5)} ${ping.toString().padStart(4)} ${c.userInfo.substring(0, 15).padEnd(15)} ${lastMsg.toString().padStart(7)} ${address.padEnd(21)} ${c.netchan.qport.toString().padStart(5)} 0
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const writer = new BinaryWriter();
|
|
963
|
+
writer.writeByte(ServerCommand.print);
|
|
964
|
+
writer.writeByte(2);
|
|
965
|
+
writer.writeString(status);
|
|
966
|
+
const packet = client.netchan.transmit(writer.getData());
|
|
967
|
+
client.net.send(packet);
|
|
968
|
+
}
|
|
969
|
+
handleGetChallenge(client) {
|
|
970
|
+
const challenge = Math.floor(Math.random() * 1e6) + 1;
|
|
971
|
+
client.challenge = challenge;
|
|
972
|
+
const writer = new BinaryWriter();
|
|
973
|
+
writer.writeByte(ServerCommand.stufftext);
|
|
974
|
+
writer.writeString(`challenge ${challenge}
|
|
975
|
+
`);
|
|
976
|
+
const packet = client.netchan.transmit(writer.getData());
|
|
977
|
+
client.net.send(packet);
|
|
978
|
+
}
|
|
979
|
+
handleConnect(client, userInfo) {
|
|
980
|
+
if (!this.game) return;
|
|
981
|
+
const result = this.game.clientConnect(client.edict || null, userInfo);
|
|
982
|
+
if (result === true) {
|
|
983
|
+
client.state = 2 /* Connected */;
|
|
984
|
+
client.userInfo = userInfo;
|
|
985
|
+
console.log(`Client ${client.index} connected: ${userInfo}`);
|
|
986
|
+
if (this.onClientConnected) {
|
|
987
|
+
this.onClientConnected(client.index, "Player");
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
this.sendServerData(client);
|
|
991
|
+
client.netchan.writeReliableByte(ServerCommand.stufftext);
|
|
992
|
+
client.netchan.writeReliableString("precache\n");
|
|
993
|
+
const packet = client.netchan.transmit();
|
|
994
|
+
client.net.send(packet);
|
|
995
|
+
} catch (e) {
|
|
996
|
+
console.warn(`Client ${client.index} reliable buffer overflow or connection error`);
|
|
997
|
+
this.dropClient(client);
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
console.log(`Client ${client.index} rejected: ${result}`);
|
|
1001
|
+
const writer = new BinaryWriter();
|
|
1002
|
+
writer.writeByte(ServerCommand.print);
|
|
1003
|
+
writer.writeByte(2);
|
|
1004
|
+
writer.writeString(`Connection rejected: ${result}
|
|
1005
|
+
`);
|
|
1006
|
+
const packet = client.netchan.transmit(writer.getData());
|
|
1007
|
+
client.net.send(packet);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
handleBegin(client) {
|
|
1011
|
+
if (client.state === 2 /* Connected */) {
|
|
1012
|
+
this.spawnClient(client);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
spawnClient(client) {
|
|
1016
|
+
if (!this.game) return;
|
|
1017
|
+
const ent = this.game.clientBegin({
|
|
1018
|
+
inventory: createPlayerInventory(),
|
|
1019
|
+
weaponStates: createPlayerWeaponStates(),
|
|
1020
|
+
buttons: 0,
|
|
1021
|
+
pm_type: 0,
|
|
1022
|
+
pm_time: 0,
|
|
1023
|
+
pm_flags: 0,
|
|
1024
|
+
gun_frame: 0,
|
|
1025
|
+
rdflags: 0,
|
|
1026
|
+
fov: 90,
|
|
1027
|
+
pers: {
|
|
1028
|
+
connected: true,
|
|
1029
|
+
inventory: [],
|
|
1030
|
+
health: 100,
|
|
1031
|
+
max_health: 100,
|
|
1032
|
+
savedFlags: 0,
|
|
1033
|
+
selected_item: 0
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
client.edict = ent;
|
|
1037
|
+
client.state = 4 /* Active */;
|
|
1038
|
+
console.log(`Client ${client.index} entered game`);
|
|
1039
|
+
}
|
|
1040
|
+
sendServerData(client) {
|
|
1041
|
+
client.netchan.writeReliableByte(ServerCommand.serverdata);
|
|
1042
|
+
client.netchan.writeReliableLong(34);
|
|
1043
|
+
client.netchan.writeReliableLong(this.sv.frame);
|
|
1044
|
+
client.netchan.writeReliableByte(0);
|
|
1045
|
+
client.netchan.writeReliableString("baseq2");
|
|
1046
|
+
client.netchan.writeReliableShort(client.index);
|
|
1047
|
+
client.netchan.writeReliableString(this.sv.name || "maps/test.bsp");
|
|
1048
|
+
for (let i = 0; i < MAX_CONFIGSTRINGS; i++) {
|
|
1049
|
+
if (this.sv.configStrings[i]) {
|
|
1050
|
+
client.netchan.writeReliableByte(ServerCommand.configstring);
|
|
1051
|
+
client.netchan.writeReliableShort(i);
|
|
1052
|
+
client.netchan.writeReliableString(this.sv.configStrings[i]);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const baselineWriter = new BinaryWriter();
|
|
1056
|
+
for (let i = 0; i < MAX_EDICTS; i++) {
|
|
1057
|
+
if (this.sv.baselines[i]) {
|
|
1058
|
+
baselineWriter.reset();
|
|
1059
|
+
baselineWriter.writeByte(ServerCommand.spawnbaseline);
|
|
1060
|
+
writeDeltaEntity({}, this.sv.baselines[i], baselineWriter, true, true);
|
|
1061
|
+
const data = baselineWriter.getData();
|
|
1062
|
+
for (let j = 0; j < data.length; j++) {
|
|
1063
|
+
client.netchan.writeReliableByte(data[j]);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
SV_SetConfigString(index, value) {
|
|
1069
|
+
if (index < 0 || index >= MAX_CONFIGSTRINGS) return;
|
|
1070
|
+
this.sv.configStrings[index] = value;
|
|
1071
|
+
for (const client of this.svs.clients) {
|
|
1072
|
+
if (client && client.state >= 2 /* Connected */) {
|
|
1073
|
+
if (client.netchan) {
|
|
1074
|
+
try {
|
|
1075
|
+
client.netchan.writeReliableByte(ServerCommand.configstring);
|
|
1076
|
+
client.netchan.writeReliableShort(index);
|
|
1077
|
+
client.netchan.writeReliableString(value);
|
|
1078
|
+
const packet = client.netchan.transmit();
|
|
1079
|
+
client.net.send(packet);
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
console.warn(`Client ${client.index} reliable buffer overflow`);
|
|
1082
|
+
this.dropClient(client);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
SV_WriteConfigString(writer, index, value) {
|
|
1089
|
+
writer.writeByte(ServerCommand.configstring);
|
|
1090
|
+
writer.writeShort(index);
|
|
1091
|
+
writer.writeString(value);
|
|
1092
|
+
}
|
|
1093
|
+
SV_ReadPackets() {
|
|
1094
|
+
for (const client of this.svs.clients) {
|
|
1095
|
+
if (!client || client.state === 0 /* Free */) continue;
|
|
1096
|
+
while (client.messageQueue.length > 0) {
|
|
1097
|
+
const rawData = client.messageQueue.shift();
|
|
1098
|
+
if (!rawData) continue;
|
|
1099
|
+
if (rawData.byteLength >= 10) {
|
|
1100
|
+
const view = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength);
|
|
1101
|
+
const incomingQPort = view.getUint16(8, true);
|
|
1102
|
+
if (client.netchan.qport !== incomingQPort) {
|
|
1103
|
+
client.netchan.qport = incomingQPort;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const data = client.netchan.process(rawData);
|
|
1107
|
+
if (!data) {
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
if (data.length === 0) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
let buffer;
|
|
1114
|
+
if (data.buffer instanceof ArrayBuffer) {
|
|
1115
|
+
buffer = data.buffer;
|
|
1116
|
+
} else {
|
|
1117
|
+
buffer = new Uint8Array(data).buffer;
|
|
1118
|
+
}
|
|
1119
|
+
const reader = new BinaryStream(buffer);
|
|
1120
|
+
const parser = new ClientMessageParser(reader, {
|
|
1121
|
+
onMove: (checksum, lastFrame, cmd) => this.handleMove(client, cmd, checksum, lastFrame),
|
|
1122
|
+
onUserInfo: (info) => this.handleUserInfo(client, info),
|
|
1123
|
+
onStringCmd: (cmd) => this.handleStringCmd(client, cmd),
|
|
1124
|
+
onNop: () => {
|
|
1125
|
+
},
|
|
1126
|
+
onBad: () => {
|
|
1127
|
+
console.warn(`Bad command from client ${client.index}`);
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
try {
|
|
1131
|
+
parser.parseMessage();
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
console.error(`Error parsing message from client ${client.index}:`, e);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
runFrame() {
|
|
1139
|
+
if (!this.game) return;
|
|
1140
|
+
const startTime = Date.now();
|
|
1141
|
+
this.sv.frame++;
|
|
1142
|
+
this.sv.time += 100;
|
|
1143
|
+
this.SV_ReadPackets();
|
|
1144
|
+
for (const client of this.svs.clients) {
|
|
1145
|
+
if (!client || client.state === 0 /* Free */) continue;
|
|
1146
|
+
if (client.edict && client.edict.client) {
|
|
1147
|
+
client.edict.client.ping = client.ping;
|
|
1148
|
+
}
|
|
1149
|
+
if (client.state >= 2 /* Connected */) {
|
|
1150
|
+
const timeoutFrames = 300;
|
|
1151
|
+
if (this.sv.frame - client.lastMessage > timeoutFrames) {
|
|
1152
|
+
console.log(`Client ${client.index} timed out`);
|
|
1153
|
+
this.dropClient(client);
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (client && client.state === 4 /* Active */ && client.edict) {
|
|
1158
|
+
const now = Date.now();
|
|
1159
|
+
if (now - client.lastCommandTime >= 1e3) {
|
|
1160
|
+
client.lastCommandTime = now;
|
|
1161
|
+
client.commandCount = 0;
|
|
1162
|
+
}
|
|
1163
|
+
if (client.commandCount > 200) {
|
|
1164
|
+
console.warn(`Client ${client.index} kicked for command flooding (count: ${client.commandCount})`);
|
|
1165
|
+
this.dropClient(client);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
this.game.clientThink(client.edict, client.lastCmd);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const snapshot = this.game.frame({
|
|
1172
|
+
frame: this.sv.frame,
|
|
1173
|
+
deltaMs: FRAME_TIME_MS,
|
|
1174
|
+
nowMs: Date.now()
|
|
1175
|
+
});
|
|
1176
|
+
this.recordHistory();
|
|
1177
|
+
if (snapshot && snapshot.state) {
|
|
1178
|
+
this.SV_SendClientMessages(snapshot.state);
|
|
1179
|
+
}
|
|
1180
|
+
const endTime = Date.now();
|
|
1181
|
+
const elapsed = endTime - startTime;
|
|
1182
|
+
const sleepTime = Math.max(0, FRAME_TIME_MS - elapsed);
|
|
1183
|
+
if (this.sv.state === 2 /* Game */) {
|
|
1184
|
+
this.frameTimeout = setTimeout(() => this.runFrame(), sleepTime);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
SV_SendClientMessages(snapshot) {
|
|
1188
|
+
for (const client of this.svs.clients) {
|
|
1189
|
+
if (client && client.state === 4 /* Active */) {
|
|
1190
|
+
this.SV_SendClientFrame(client, snapshot);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
SV_SendClientFrame(client, snapshot) {
|
|
1195
|
+
const MTU = 1400;
|
|
1196
|
+
const writer = new BinaryWriter(MTU);
|
|
1197
|
+
writer.writeByte(ServerCommand.frame);
|
|
1198
|
+
writer.writeLong(this.sv.frame);
|
|
1199
|
+
let deltaFrame = 0;
|
|
1200
|
+
if (client.lastFrame && client.lastFrame < this.sv.frame && client.lastFrame >= this.sv.frame - UPDATE_BACKUP) {
|
|
1201
|
+
deltaFrame = client.lastFrame;
|
|
1202
|
+
}
|
|
1203
|
+
writer.writeLong(deltaFrame);
|
|
1204
|
+
writer.writeByte(0);
|
|
1205
|
+
writer.writeByte(0);
|
|
1206
|
+
writer.writeByte(ServerCommand.playerinfo);
|
|
1207
|
+
const ps = {
|
|
1208
|
+
pm_type: snapshot.pmType,
|
|
1209
|
+
origin: snapshot.origin,
|
|
1210
|
+
velocity: snapshot.velocity,
|
|
1211
|
+
pm_time: snapshot.pm_time,
|
|
1212
|
+
pm_flags: snapshot.pmFlags,
|
|
1213
|
+
gravity: Math.abs(snapshot.gravity.z),
|
|
1214
|
+
delta_angles: snapshot.deltaAngles,
|
|
1215
|
+
viewoffset: { x: 0, y: 0, z: 22 },
|
|
1216
|
+
viewangles: snapshot.viewangles,
|
|
1217
|
+
kick_angles: snapshot.kick_angles,
|
|
1218
|
+
gun_index: snapshot.gunindex,
|
|
1219
|
+
gun_frame: snapshot.gun_frame,
|
|
1220
|
+
gun_offset: snapshot.gunoffset,
|
|
1221
|
+
gun_angles: snapshot.gunangles,
|
|
1222
|
+
blend: snapshot.blend,
|
|
1223
|
+
fov: snapshot.fov,
|
|
1224
|
+
rdflags: snapshot.rdflags,
|
|
1225
|
+
stats: snapshot.stats,
|
|
1226
|
+
watertype: snapshot.watertype
|
|
1227
|
+
// Populate watertype
|
|
1228
|
+
};
|
|
1229
|
+
writePlayerState(writer, ps);
|
|
1230
|
+
writer.writeByte(ServerCommand.packetentities);
|
|
1231
|
+
const entities = snapshot.packetEntities || [];
|
|
1232
|
+
const currentEntityIds = [];
|
|
1233
|
+
const frameIdx = this.sv.frame % UPDATE_BACKUP;
|
|
1234
|
+
const currentFrame = client.frames[frameIdx];
|
|
1235
|
+
currentFrame.entities = entities;
|
|
1236
|
+
let oldEntities = [];
|
|
1237
|
+
if (deltaFrame > 0) {
|
|
1238
|
+
const oldFrameIdx = deltaFrame % UPDATE_BACKUP;
|
|
1239
|
+
oldEntities = client.frames[oldFrameIdx].entities;
|
|
1240
|
+
}
|
|
1241
|
+
for (const entityState of currentFrame.entities) {
|
|
1242
|
+
if (writer.getOffset() > MTU - 200) {
|
|
1243
|
+
console.warn("Packet MTU limit reached, dropping remaining entities");
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
currentEntityIds.push(entityState.number);
|
|
1247
|
+
const oldState = oldEntities.find((e) => e.number === entityState.number);
|
|
1248
|
+
if (oldState) {
|
|
1249
|
+
writeDeltaEntity(oldState, entityState, writer, false, false);
|
|
1250
|
+
} else {
|
|
1251
|
+
writeDeltaEntity({}, entityState, writer, false, true);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
for (const oldId of client.lastPacketEntities) {
|
|
1255
|
+
if (writer.getOffset() > MTU - 10) {
|
|
1256
|
+
console.warn("Packet MTU limit reached, dropping remaining removals");
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
if (!currentEntityIds.includes(oldId)) {
|
|
1260
|
+
writeRemoveEntity(oldId, writer);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
writer.writeShort(0);
|
|
1264
|
+
const frameData = writer.getData();
|
|
1265
|
+
currentFrame.packetCRC = crc8(frameData);
|
|
1266
|
+
const packet = client.netchan.transmit(frameData);
|
|
1267
|
+
client.net.send(packet);
|
|
1268
|
+
client.lastFrame = this.sv.frame;
|
|
1269
|
+
client.lastPacketEntities = currentEntityIds;
|
|
1270
|
+
}
|
|
1271
|
+
// GameEngine Implementation
|
|
1272
|
+
trace(start, end) {
|
|
1273
|
+
return { fraction: 1 };
|
|
1274
|
+
}
|
|
1275
|
+
modelIndex(name) {
|
|
1276
|
+
console.log(`modelIndex(${name}) called`);
|
|
1277
|
+
const start = ConfigStringIndex.Models;
|
|
1278
|
+
const end = ConfigStringIndex.Sounds;
|
|
1279
|
+
for (let i = start + 1; i < end; i++) {
|
|
1280
|
+
if (this.sv.configStrings[i] === name) {
|
|
1281
|
+
return i - start;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
for (let i = start + 1; i < end; i++) {
|
|
1285
|
+
if (!this.sv.configStrings[i]) {
|
|
1286
|
+
this.SV_SetConfigString(i, name);
|
|
1287
|
+
return i - start;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
console.warn(`MAX_MODELS overflow for ${name}`);
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
soundIndex(name) {
|
|
1294
|
+
const start = ConfigStringIndex.Sounds;
|
|
1295
|
+
const end = ConfigStringIndex.Images;
|
|
1296
|
+
for (let i = start + 1; i < end; i++) {
|
|
1297
|
+
if (this.sv.configStrings[i] === name) {
|
|
1298
|
+
return i - start;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
for (let i = start + 1; i < end; i++) {
|
|
1302
|
+
if (!this.sv.configStrings[i]) {
|
|
1303
|
+
this.SV_SetConfigString(i, name);
|
|
1304
|
+
return i - start;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
console.warn(`MAX_SOUNDS overflow for ${name}`);
|
|
1308
|
+
return 0;
|
|
1309
|
+
}
|
|
1310
|
+
imageIndex(name) {
|
|
1311
|
+
const start = ConfigStringIndex.Images;
|
|
1312
|
+
const end = ConfigStringIndex.Lights;
|
|
1313
|
+
for (let i = start + 1; i < end; i++) {
|
|
1314
|
+
if (this.sv.configStrings[i] === name) {
|
|
1315
|
+
return i - start;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
for (let i = start + 1; i < end; i++) {
|
|
1319
|
+
if (!this.sv.configStrings[i]) {
|
|
1320
|
+
this.SV_SetConfigString(i, name);
|
|
1321
|
+
return i - start;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
console.warn(`MAX_IMAGES overflow for ${name}`);
|
|
1325
|
+
return 0;
|
|
1326
|
+
}
|
|
1327
|
+
multicast(origin, type, event, ...args) {
|
|
1328
|
+
const writer = new BinaryWriter();
|
|
1329
|
+
writeServerCommand(writer, event, ...args);
|
|
1330
|
+
const data = writer.getData();
|
|
1331
|
+
for (const client of this.svs.clients) {
|
|
1332
|
+
if (!client || client.state < 4 /* Active */ || !client.edict) {
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
let send = false;
|
|
1336
|
+
switch (type) {
|
|
1337
|
+
case MulticastType.All:
|
|
1338
|
+
send = true;
|
|
1339
|
+
break;
|
|
1340
|
+
case MulticastType.Pvs:
|
|
1341
|
+
if (this.sv.collisionModel) {
|
|
1342
|
+
send = inPVS(origin, client.edict.origin, this.sv.collisionModel);
|
|
1343
|
+
} else {
|
|
1344
|
+
send = true;
|
|
1345
|
+
}
|
|
1346
|
+
break;
|
|
1347
|
+
case MulticastType.Phs:
|
|
1348
|
+
if (this.sv.collisionModel) {
|
|
1349
|
+
send = inPHS(origin, client.edict.origin, this.sv.collisionModel);
|
|
1350
|
+
} else {
|
|
1351
|
+
send = true;
|
|
1352
|
+
}
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
if (send) {
|
|
1356
|
+
{
|
|
1357
|
+
const packet = client.netchan.transmit(data);
|
|
1358
|
+
client.net.send(packet);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
unicast(ent, reliable, event, ...args) {
|
|
1364
|
+
const client = this.svs.clients.find((c) => c?.edict === ent);
|
|
1365
|
+
if (client && client.state >= 2 /* Connected */) {
|
|
1366
|
+
const writer = new BinaryWriter();
|
|
1367
|
+
writeServerCommand(writer, event, ...args);
|
|
1368
|
+
const data = writer.getData();
|
|
1369
|
+
if (reliable) {
|
|
1370
|
+
try {
|
|
1371
|
+
for (let i = 0; i < data.length; i++) {
|
|
1372
|
+
client.netchan.writeReliableByte(data[i]);
|
|
1373
|
+
}
|
|
1374
|
+
const packet = client.netchan.transmit();
|
|
1375
|
+
client.net.send(packet);
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
console.warn(`Client ${client.index} reliable buffer overflow in unicast`);
|
|
1378
|
+
this.dropClient(client);
|
|
1379
|
+
}
|
|
1380
|
+
} else {
|
|
1381
|
+
const packet = client.netchan.transmit(data);
|
|
1382
|
+
client.net.send(packet);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
configstring(index, value) {
|
|
1387
|
+
this.SV_SetConfigString(index, value);
|
|
1388
|
+
}
|
|
1389
|
+
recordHistory() {
|
|
1390
|
+
if (!this.game) return;
|
|
1391
|
+
const now = Date.now();
|
|
1392
|
+
const HISTORY_MAX_MS = 1e3;
|
|
1393
|
+
this.game.entities.forEachEntity((ent) => {
|
|
1394
|
+
if (ent.solid !== Solid.Not || ent.takedamage) {
|
|
1395
|
+
let hist = this.history.get(ent.index);
|
|
1396
|
+
if (!hist) {
|
|
1397
|
+
hist = [];
|
|
1398
|
+
this.history.set(ent.index, hist);
|
|
1399
|
+
}
|
|
1400
|
+
hist.push({
|
|
1401
|
+
time: now,
|
|
1402
|
+
origin: { ...ent.origin },
|
|
1403
|
+
mins: { ...ent.mins },
|
|
1404
|
+
maxs: { ...ent.maxs },
|
|
1405
|
+
angles: { ...ent.angles }
|
|
1406
|
+
});
|
|
1407
|
+
while (hist.length > 0 && hist[0].time < now - HISTORY_MAX_MS) {
|
|
1408
|
+
hist.shift();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
setLagCompensation(active, client, lagMs) {
|
|
1414
|
+
if (!this.game || !this.entityIndex) return;
|
|
1415
|
+
if (active) {
|
|
1416
|
+
if (!client || lagMs === void 0) return;
|
|
1417
|
+
const now = Date.now();
|
|
1418
|
+
const targetTime = now - lagMs;
|
|
1419
|
+
this.game.entities.forEachEntity((ent) => {
|
|
1420
|
+
if (ent === client) return;
|
|
1421
|
+
if (ent.solid === Solid.Not && !ent.takedamage) return;
|
|
1422
|
+
const hist = this.history.get(ent.index);
|
|
1423
|
+
if (!hist || hist.length === 0) return;
|
|
1424
|
+
let i = hist.length - 1;
|
|
1425
|
+
while (i >= 0 && hist[i].time > targetTime) {
|
|
1426
|
+
i--;
|
|
1427
|
+
}
|
|
1428
|
+
if (i < 0) {
|
|
1429
|
+
i = 0;
|
|
1430
|
+
}
|
|
1431
|
+
const s1 = hist[i];
|
|
1432
|
+
const s2 = i + 1 < hist.length ? hist[i + 1] : s1;
|
|
1433
|
+
let frac = 0;
|
|
1434
|
+
if (s1.time !== s2.time) {
|
|
1435
|
+
frac = (targetTime - s1.time) / (s2.time - s1.time);
|
|
1436
|
+
}
|
|
1437
|
+
if (frac < 0) frac = 0;
|
|
1438
|
+
if (frac > 1) frac = 1;
|
|
1439
|
+
const origin = {
|
|
1440
|
+
x: s1.origin.x + (s2.origin.x - s1.origin.x) * frac,
|
|
1441
|
+
y: s1.origin.y + (s2.origin.y - s1.origin.y) * frac,
|
|
1442
|
+
z: s1.origin.z + (s2.origin.z - s1.origin.z) * frac
|
|
1443
|
+
};
|
|
1444
|
+
const angles = {
|
|
1445
|
+
x: lerpAngle(s1.angles.x, s2.angles.x, frac),
|
|
1446
|
+
y: lerpAngle(s1.angles.y, s2.angles.y, frac),
|
|
1447
|
+
z: lerpAngle(s1.angles.z, s2.angles.z, frac)
|
|
1448
|
+
};
|
|
1449
|
+
this.backup.set(ent.index, {
|
|
1450
|
+
origin: { ...ent.origin },
|
|
1451
|
+
mins: { ...ent.mins },
|
|
1452
|
+
maxs: { ...ent.maxs },
|
|
1453
|
+
angles: { ...ent.angles },
|
|
1454
|
+
link: true
|
|
1455
|
+
});
|
|
1456
|
+
ent.origin = origin;
|
|
1457
|
+
ent.angles = angles;
|
|
1458
|
+
ent.mins = {
|
|
1459
|
+
x: s1.mins.x + (s2.mins.x - s1.mins.x) * frac,
|
|
1460
|
+
y: s1.mins.y + (s2.mins.y - s1.mins.y) * frac,
|
|
1461
|
+
z: s1.mins.z + (s2.mins.z - s1.mins.z) * frac
|
|
1462
|
+
};
|
|
1463
|
+
ent.maxs = {
|
|
1464
|
+
x: s1.maxs.x + (s2.maxs.x - s1.maxs.x) * frac,
|
|
1465
|
+
y: s1.maxs.y + (s2.maxs.y - s1.maxs.y) * frac,
|
|
1466
|
+
z: s1.maxs.z + (s2.maxs.z - s1.maxs.z) * frac
|
|
1467
|
+
};
|
|
1468
|
+
this.entityIndex.link({
|
|
1469
|
+
id: ent.index,
|
|
1470
|
+
origin: ent.origin,
|
|
1471
|
+
mins: ent.mins,
|
|
1472
|
+
maxs: ent.maxs,
|
|
1473
|
+
contents: ent.solid === 0 ? 0 : 1,
|
|
1474
|
+
surfaceFlags: 0
|
|
1475
|
+
});
|
|
1476
|
+
});
|
|
1477
|
+
} else {
|
|
1478
|
+
this.backup.forEach((state, id) => {
|
|
1479
|
+
const ent = this.game?.entities.getByIndex(id);
|
|
1480
|
+
if (ent) {
|
|
1481
|
+
ent.origin = state.origin;
|
|
1482
|
+
ent.mins = state.mins;
|
|
1483
|
+
ent.maxs = state.maxs;
|
|
1484
|
+
ent.angles = state.angles;
|
|
1485
|
+
this.entityIndex.link({
|
|
1486
|
+
id: ent.index,
|
|
1487
|
+
origin: ent.origin,
|
|
1488
|
+
mins: ent.mins,
|
|
1489
|
+
maxs: ent.maxs,
|
|
1490
|
+
contents: ent.solid === 0 ? 0 : 1,
|
|
1491
|
+
surfaceFlags: 0
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
this.backup.clear();
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
function createServer(options = {}) {
|
|
1500
|
+
return new DedicatedServer(options);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
export { ClientMessageParser, ClientState, DedicatedServer, ServerState, WebSocketNetDriver, createClient, createServer };
|
|
1504
|
+
//# sourceMappingURL=index.js.map
|
|
1505
|
+
//# sourceMappingURL=index.js.map
|