quake2ts 0.0.561 → 0.0.563

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/packages/client/dist/browser/index.global.js +15 -15
  3. package/packages/client/dist/browser/index.global.js.map +1 -1
  4. package/packages/client/dist/cjs/index.cjs +273 -1
  5. package/packages/client/dist/cjs/index.cjs.map +1 -1
  6. package/packages/client/dist/esm/index.js +273 -1
  7. package/packages/client/dist/esm/index.js.map +1 -1
  8. package/packages/client/dist/tsconfig.tsbuildinfo +1 -1
  9. package/packages/client/dist/types/net/connection.d.ts +2 -0
  10. package/packages/client/dist/types/net/connection.d.ts.map +1 -1
  11. package/packages/server/dist/client.d.ts +51 -0
  12. package/packages/server/dist/client.js +100 -0
  13. package/packages/server/dist/dedicated.d.ts +69 -0
  14. package/packages/server/dist/dedicated.js +1013 -0
  15. package/packages/server/dist/index.cjs +27 -2
  16. package/packages/server/dist/index.d.ts +7 -161
  17. package/packages/server/dist/index.js +26 -2
  18. package/packages/server/dist/net/nodeWsDriver.d.ts +16 -0
  19. package/packages/server/dist/net/nodeWsDriver.js +122 -0
  20. package/packages/server/dist/protocol/player.d.ts +23 -0
  21. package/packages/server/dist/protocol/player.js +137 -0
  22. package/packages/server/dist/protocol/write.d.ts +7 -0
  23. package/packages/server/dist/protocol/write.js +167 -0
  24. package/packages/server/dist/protocol.d.ts +17 -0
  25. package/packages/server/dist/protocol.js +71 -0
  26. package/packages/server/dist/server.d.ts +50 -0
  27. package/packages/server/dist/server.js +12 -0
  28. package/packages/server/dist/server.test.d.ts +1 -0
  29. package/packages/server/dist/server.test.js +69 -0
  30. package/packages/server/dist/transport.d.ts +7 -0
  31. package/packages/server/dist/transport.js +1 -0
  32. package/packages/server/dist/transports/websocket.d.ts +11 -0
  33. package/packages/server/dist/transports/websocket.js +38 -0
  34. package/packages/test-utils/dist/index.cjs +1610 -1188
  35. package/packages/test-utils/dist/index.cjs.map +1 -1
  36. package/packages/test-utils/dist/index.d.cts +326 -132
  37. package/packages/test-utils/dist/index.d.ts +326 -132
  38. package/packages/test-utils/dist/index.js +1596 -1189
  39. package/packages/test-utils/dist/index.js.map +1 -1
  40. package/packages/server/dist/index.d.cts +0 -161
@@ -0,0 +1,167 @@
1
+ import { ServerCommand, TempEntity } from '@quake2ts/shared';
2
+ /**
3
+ * Writes a server command and its arguments to a BinaryWriter.
4
+ * This handles the serialization of generic arguments passed to multicast/unicast
5
+ * into the specific binary format expected by the protocol.
6
+ */
7
+ export function writeServerCommand(writer, event, ...args) {
8
+ writer.writeByte(event);
9
+ switch (event) {
10
+ case ServerCommand.print: {
11
+ // args: [level: number, text: string]
12
+ const level = args[0];
13
+ const text = args[1];
14
+ writer.writeByte(level);
15
+ writer.writeString(text);
16
+ break;
17
+ }
18
+ case ServerCommand.centerprint: {
19
+ // args: [text: string]
20
+ const text = args[0];
21
+ writer.writeString(text);
22
+ break;
23
+ }
24
+ case ServerCommand.stufftext: {
25
+ // args: [text: string]
26
+ const text = args[0];
27
+ writer.writeString(text);
28
+ break;
29
+ }
30
+ case ServerCommand.sound: {
31
+ // args: [flags, soundNum, volume?, attenuation?, offset?, ent?, pos?]
32
+ const flags = args[0];
33
+ const soundNum = args[1];
34
+ const volume = args[2];
35
+ const attenuation = args[3];
36
+ const offset = args[4];
37
+ const ent = args[5];
38
+ const pos = args[6];
39
+ writer.writeByte(flags);
40
+ writer.writeByte(soundNum);
41
+ if (flags & 1) { // SND_VOLUME
42
+ writer.writeByte(volume || 0);
43
+ }
44
+ if (flags & 2) { // SND_ATTENUATION
45
+ writer.writeByte(attenuation || 0);
46
+ }
47
+ if (flags & 16) { // SND_OFFSET
48
+ writer.writeByte(offset || 0);
49
+ }
50
+ if (flags & 8) { // SND_ENT
51
+ writer.writeShort(ent || 0);
52
+ }
53
+ if (flags & 4) { // SND_POS
54
+ if (pos) {
55
+ writer.writePos(pos);
56
+ }
57
+ else {
58
+ writer.writePos({ x: 0, y: 0, z: 0 });
59
+ }
60
+ }
61
+ break;
62
+ }
63
+ case ServerCommand.muzzleflash: {
64
+ // args: [entityIndex: number, flashType: number]
65
+ const entIndex = args[0];
66
+ const flashType = args[1];
67
+ writer.writeShort(entIndex);
68
+ writer.writeByte(flashType);
69
+ break;
70
+ }
71
+ case ServerCommand.temp_entity: {
72
+ // args: [type: TempEntity, ...params]
73
+ const type = args[0];
74
+ writer.writeByte(type);
75
+ writeTempEntity(writer, type, args.slice(1));
76
+ break;
77
+ }
78
+ default:
79
+ console.warn(`writeServerCommand: Unhandled command ${event}`);
80
+ break;
81
+ }
82
+ }
83
+ function writeTempEntity(writer, type, args) {
84
+ switch (type) {
85
+ case TempEntity.ROCKET_EXPLOSION:
86
+ case TempEntity.GRENADE_EXPLOSION:
87
+ case TempEntity.EXPLOSION1:
88
+ case TempEntity.EXPLOSION2:
89
+ case TempEntity.ROCKET_EXPLOSION_WATER:
90
+ case TempEntity.GRENADE_EXPLOSION_WATER:
91
+ case TempEntity.BFG_EXPLOSION:
92
+ case TempEntity.BFG_BIGEXPLOSION:
93
+ case TempEntity.PLASMA_EXPLOSION:
94
+ case TempEntity.PLAIN_EXPLOSION:
95
+ case TempEntity.TRACKER_EXPLOSION:
96
+ case TempEntity.EXPLOSION1_BIG:
97
+ case TempEntity.EXPLOSION1_NP:
98
+ case TempEntity.EXPLOSION1_NL:
99
+ case TempEntity.EXPLOSION2_NL:
100
+ case TempEntity.BERSERK_SLAM:
101
+ // Format: [pos]
102
+ writer.writePos(args[0]);
103
+ break;
104
+ case TempEntity.BLASTER:
105
+ case TempEntity.FLECHETTE:
106
+ // Format: [pos, dir]
107
+ writer.writePos(args[0]);
108
+ writer.writeDir(args[1]);
109
+ break;
110
+ case TempEntity.RAILTRAIL:
111
+ case TempEntity.DEBUGTRAIL:
112
+ case TempEntity.BUBBLETRAIL:
113
+ case TempEntity.BUBBLETRAIL2:
114
+ case TempEntity.BFG_LASER:
115
+ case TempEntity.LIGHTNING_BEAM:
116
+ case TempEntity.LIGHTNING:
117
+ // Format: [start, end]
118
+ writer.writePos(args[0]);
119
+ writer.writePos(args[1]);
120
+ break;
121
+ case TempEntity.LASER_SPARKS:
122
+ case TempEntity.WELDING_SPARKS:
123
+ case TempEntity.TUNNEL_SPARKS:
124
+ case TempEntity.ELECTRIC_SPARKS:
125
+ case TempEntity.HEATBEAM_SPARKS:
126
+ case TempEntity.HEATBEAM_STEAM:
127
+ case TempEntity.STEAM:
128
+ // Format: [count, pos, dir, color?]
129
+ // Q2: writeByte(count), writePos(start), writeDir(normal), writeByte(skin/color)
130
+ writer.writeByte(args[0]);
131
+ writer.writePos(args[1]);
132
+ writer.writeDir(args[2]);
133
+ writer.writeByte(args[3] || 0);
134
+ break;
135
+ case TempEntity.PARASITE_ATTACK:
136
+ case TempEntity.MEDIC_CABLE_ATTACK:
137
+ // Format: [entIndex, start, end]
138
+ // args[0] is Entity usually
139
+ const ent = args[0];
140
+ writer.writeShort(ent ? ent.index : 0);
141
+ writer.writePos(args[1]);
142
+ writer.writePos(args[2]);
143
+ break;
144
+ case TempEntity.GUNSHOT:
145
+ case TempEntity.BLOOD:
146
+ case TempEntity.SPARKS:
147
+ case TempEntity.BULLET_SPARKS:
148
+ case TempEntity.SCREEN_SPARKS:
149
+ case TempEntity.SHIELD_SPARKS:
150
+ // Format: [pos, dir]
151
+ writer.writePos(args[0]);
152
+ writer.writeDir(args[1]);
153
+ break;
154
+ case TempEntity.SPLASH:
155
+ case TempEntity.POWER_SPLASH:
156
+ case TempEntity.WIDOWSPLASH:
157
+ // Format: [count, pos, dir, color]
158
+ writer.writeByte(args[0]);
159
+ writer.writePos(args[1]);
160
+ writer.writeDir(args[2]);
161
+ writer.writeByte(args[3] || 0);
162
+ break;
163
+ default:
164
+ console.warn(`writeTempEntity: Unhandled TempEntity ${type}`);
165
+ break;
166
+ }
167
+ }
@@ -0,0 +1,17 @@
1
+ import { BinaryStream, UserCommand } from '@quake2ts/shared';
2
+ export interface ClientMessageHandler {
3
+ onMove(checksum: number, lastFrame: number, userCmd: UserCommand): void;
4
+ onUserInfo(info: string): void;
5
+ onStringCmd(cmd: string): void;
6
+ onNop(): void;
7
+ onBad(): void;
8
+ }
9
+ export declare class ClientMessageParser {
10
+ private stream;
11
+ private handler;
12
+ constructor(stream: BinaryStream, handler: ClientMessageHandler);
13
+ parseMessage(): void;
14
+ private parseMove;
15
+ private parseUserInfo;
16
+ private parseStringCmd;
17
+ }
@@ -0,0 +1,71 @@
1
+ import { ClientCommand } from '@quake2ts/shared';
2
+ export class ClientMessageParser {
3
+ constructor(stream, handler) {
4
+ this.stream = stream;
5
+ this.handler = handler;
6
+ }
7
+ parseMessage() {
8
+ while (this.stream.hasMore()) {
9
+ const cmdId = this.stream.readByte();
10
+ if (cmdId === -1)
11
+ break;
12
+ switch (cmdId) {
13
+ case ClientCommand.move:
14
+ this.parseMove();
15
+ break;
16
+ case ClientCommand.userinfo:
17
+ this.parseUserInfo();
18
+ break;
19
+ case ClientCommand.stringcmd:
20
+ this.parseStringCmd();
21
+ break;
22
+ case ClientCommand.nop:
23
+ this.handler.onNop();
24
+ break;
25
+ default:
26
+ console.warn(`Unknown client command: ${cmdId}`);
27
+ this.handler.onBad();
28
+ return;
29
+ }
30
+ }
31
+ }
32
+ parseMove() {
33
+ const checksum = this.stream.readByte();
34
+ const lastFrame = this.stream.readLong();
35
+ // Read UserCmd
36
+ // TODO: support delta compression if needed (Q2 uses delta compression for usercmds)
37
+ // For now, assume full UserCmd or implement basic read
38
+ const msec = this.stream.readByte();
39
+ const buttons = this.stream.readByte();
40
+ const angles = {
41
+ x: this.stream.readShort() * (360.0 / 65536.0),
42
+ y: this.stream.readShort() * (360.0 / 65536.0),
43
+ z: this.stream.readShort() * (360.0 / 65536.0),
44
+ };
45
+ const forwardmove = this.stream.readShort();
46
+ const sidemove = this.stream.readShort();
47
+ const upmove = this.stream.readShort();
48
+ const impulse = this.stream.readByte();
49
+ const lightlevel = this.stream.readByte(); // Used for light-based stealth, usually ignored by server logic except for stats
50
+ const userCmd = {
51
+ msec,
52
+ buttons,
53
+ angles,
54
+ forwardmove,
55
+ sidemove,
56
+ upmove,
57
+ impulse,
58
+ lightlevel,
59
+ sequence: 0 // Server doesn't read sequence from packet body in standard protocol, it tracks it
60
+ };
61
+ this.handler.onMove(checksum, lastFrame, userCmd);
62
+ }
63
+ parseUserInfo() {
64
+ const info = this.stream.readString();
65
+ this.handler.onUserInfo(info);
66
+ }
67
+ parseStringCmd() {
68
+ const cmd = this.stream.readString();
69
+ this.handler.onStringCmd(cmd);
70
+ }
71
+ }
@@ -0,0 +1,50 @@
1
+ import { CollisionModel } from "@quake2ts/shared";
2
+ import { EntityState } from "@quake2ts/shared";
3
+ import { Client } from "./client.js";
4
+ /**
5
+ * ServerState corresponds to server_state_t in the original source.
6
+ */
7
+ export declare enum ServerState {
8
+ Dead = 0,// no map loaded
9
+ Loading = 1,// spawning level edicts
10
+ Game = 2,// actively running
11
+ Cinematic = 3,
12
+ Demo = 4,
13
+ Pic = 5
14
+ }
15
+ export interface Challenge {
16
+ adr: string;
17
+ challenge: number;
18
+ time: number;
19
+ }
20
+ /**
21
+ * ServerStatic holds the state that is constant across server restarts.
22
+ * Corresponds to server_static_t in the original source.
23
+ */
24
+ export interface ServerStatic {
25
+ initialized: boolean;
26
+ realTime: number;
27
+ mapCmd: string;
28
+ spawnCount: number;
29
+ clients: (Client | null)[];
30
+ lastHeartbeat: number;
31
+ challenges: Challenge[];
32
+ demoFile?: any;
33
+ }
34
+ /**
35
+ * Server holds the state for the current running server instance.
36
+ * Corresponds to server_t in the original source.
37
+ */
38
+ export interface Server {
39
+ state: ServerState;
40
+ attractLoop: boolean;
41
+ loadGame: boolean;
42
+ startTime: number;
43
+ time: number;
44
+ frame: number;
45
+ name: string;
46
+ collisionModel: CollisionModel | null;
47
+ configStrings: string[];
48
+ baselines: (EntityState | null)[];
49
+ multicastBuf: Uint8Array;
50
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ServerState corresponds to server_state_t in the original source.
3
+ */
4
+ export var ServerState;
5
+ (function (ServerState) {
6
+ ServerState[ServerState["Dead"] = 0] = "Dead";
7
+ ServerState[ServerState["Loading"] = 1] = "Loading";
8
+ ServerState[ServerState["Game"] = 2] = "Game";
9
+ ServerState[ServerState["Cinematic"] = 3] = "Cinematic";
10
+ ServerState[ServerState["Demo"] = 4] = "Demo";
11
+ ServerState[ServerState["Pic"] = 5] = "Pic";
12
+ })(ServerState || (ServerState = {}));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { DedicatedServer, createServer } from './dedicated.js';
3
+ // Mock dependencies
4
+ vi.mock('node:fs/promises', () => ({
5
+ default: {
6
+ readFile: vi.fn().mockResolvedValue(Buffer.alloc(100))
7
+ }
8
+ }));
9
+ vi.mock('@quake2ts/engine', () => ({
10
+ parseBsp: vi.fn().mockReturnValue({
11
+ planes: [],
12
+ nodes: [],
13
+ leafs: [],
14
+ brushes: [],
15
+ models: [],
16
+ leafLists: { leafBrushes: [] },
17
+ texInfo: [],
18
+ brushSides: [],
19
+ visibility: { numClusters: 0, clusters: [] } // Add visibility to mock
20
+ })
21
+ }));
22
+ // Mock Transport
23
+ class MockTransport {
24
+ async listen(port) {
25
+ return Promise.resolve();
26
+ }
27
+ close() { }
28
+ onConnection(callback) {
29
+ this.onConnectionCallback = callback;
30
+ }
31
+ onError(callback) {
32
+ this.onErrorCallback = callback;
33
+ }
34
+ }
35
+ describe('DedicatedServer', () => {
36
+ let server;
37
+ let transport;
38
+ beforeEach(() => {
39
+ transport = new MockTransport();
40
+ });
41
+ afterEach(() => {
42
+ if (server) {
43
+ server.stopServer();
44
+ }
45
+ });
46
+ it('should create a server with default options', () => {
47
+ server = createServer({ transport });
48
+ expect(server).toBeInstanceOf(DedicatedServer);
49
+ });
50
+ it('should start and stop with custom transport', async () => {
51
+ server = createServer({ transport });
52
+ await expect(server.startServer('maps/test.bsp')).resolves.not.toThrow();
53
+ server.stopServer();
54
+ });
55
+ it('should fail to start without map', async () => {
56
+ server = createServer({ transport });
57
+ await expect(server.startServer()).rejects.toThrow('No map specified');
58
+ });
59
+ it('should kick player', () => {
60
+ server = createServer({ transport });
61
+ // Just verify method exists and runs safely on empty server
62
+ expect(() => server.kickPlayer(0)).not.toThrow();
63
+ });
64
+ it('should change map', async () => {
65
+ server = createServer({ transport });
66
+ await server.startServer('maps/q2dm1.bsp');
67
+ await expect(server.changeMap('maps/q2dm2.bsp')).resolves.not.toThrow();
68
+ });
69
+ });
@@ -0,0 +1,7 @@
1
+ import { NetDriver } from '@quake2ts/shared';
2
+ export interface NetworkTransport {
3
+ listen(port: number): Promise<void>;
4
+ close(): void;
5
+ onConnection(callback: (driver: NetDriver, info?: any) => void): void;
6
+ onError(callback: (error: Error) => void): void;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { NetworkTransport } from '../transport.js';
2
+ import { NetDriver } from '@quake2ts/shared';
3
+ export declare class WebSocketTransport implements NetworkTransport {
4
+ private wss;
5
+ private connectionCallback;
6
+ private errorCallback;
7
+ listen(port: number): Promise<void>;
8
+ close(): void;
9
+ onConnection(callback: (driver: NetDriver, info?: any) => void): void;
10
+ onError(callback: (error: Error) => void): void;
11
+ }
@@ -0,0 +1,38 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { WebSocketNetDriver } from '../net/nodeWsDriver.js';
3
+ export class WebSocketTransport {
4
+ constructor() {
5
+ this.wss = null;
6
+ this.connectionCallback = null;
7
+ this.errorCallback = null;
8
+ }
9
+ async listen(port) {
10
+ return new Promise((resolve) => {
11
+ this.wss = new WebSocketServer({ port });
12
+ this.wss.on('listening', () => resolve());
13
+ this.wss.on('connection', (ws, req) => {
14
+ const driver = new WebSocketNetDriver();
15
+ driver.attach(ws);
16
+ if (this.connectionCallback) {
17
+ this.connectionCallback(driver, req);
18
+ }
19
+ });
20
+ this.wss.on('error', (err) => {
21
+ if (this.errorCallback)
22
+ this.errorCallback(err);
23
+ });
24
+ });
25
+ }
26
+ close() {
27
+ if (this.wss) {
28
+ this.wss.close();
29
+ this.wss = null;
30
+ }
31
+ }
32
+ onConnection(callback) {
33
+ this.connectionCallback = callback;
34
+ }
35
+ onError(callback) {
36
+ this.errorCallback = callback;
37
+ }
38
+ }