@quake2ts/server 0.0.739 → 0.0.741

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