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