@quake2ts/shared 0.0.1
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/browser/index.global.js +2 -0
- package/dist/browser/index.global.js.map +1 -0
- package/dist/cjs/index.cjs +6569 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.js +6200 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/audio/constants.d.ts +24 -0
- package/dist/types/audio/constants.d.ts.map +1 -0
- package/dist/types/bsp/collision.d.ts +201 -0
- package/dist/types/bsp/collision.d.ts.map +1 -0
- package/dist/types/bsp/contents.d.ts +72 -0
- package/dist/types/bsp/contents.d.ts.map +1 -0
- package/dist/types/bsp/spatial.d.ts +13 -0
- package/dist/types/bsp/spatial.d.ts.map +1 -0
- package/dist/types/index.d.ts +38 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/inventory-helpers.d.ts +19 -0
- package/dist/types/inventory-helpers.d.ts.map +1 -0
- package/dist/types/io/binaryStream.d.ts +38 -0
- package/dist/types/io/binaryStream.d.ts.map +1 -0
- package/dist/types/io/binaryWriter.d.ts +26 -0
- package/dist/types/io/binaryWriter.d.ts.map +1 -0
- package/dist/types/io/index.d.ts +4 -0
- package/dist/types/io/index.d.ts.map +1 -0
- package/dist/types/io/messageBuilder.d.ts +21 -0
- package/dist/types/io/messageBuilder.d.ts.map +1 -0
- package/dist/types/items/ammo.d.ts +40 -0
- package/dist/types/items/ammo.d.ts.map +1 -0
- package/dist/types/items/index.d.ts +8 -0
- package/dist/types/items/index.d.ts.map +1 -0
- package/dist/types/items/powerups.d.ts +31 -0
- package/dist/types/items/powerups.d.ts.map +1 -0
- package/dist/types/items/weaponInfo.d.ts +5 -0
- package/dist/types/items/weaponInfo.d.ts.map +1 -0
- package/dist/types/items/weapons.d.ts +27 -0
- package/dist/types/items/weapons.d.ts.map +1 -0
- package/dist/types/math/angles.d.ts +19 -0
- package/dist/types/math/angles.d.ts.map +1 -0
- package/dist/types/math/anorms.d.ts +2 -0
- package/dist/types/math/anorms.d.ts.map +1 -0
- package/dist/types/math/color.d.ts +12 -0
- package/dist/types/math/color.d.ts.map +1 -0
- package/dist/types/math/mat4.d.ts +7 -0
- package/dist/types/math/mat4.d.ts.map +1 -0
- package/dist/types/math/random.d.ts +60 -0
- package/dist/types/math/random.d.ts.map +1 -0
- package/dist/types/math/vec3.d.ts +79 -0
- package/dist/types/math/vec3.d.ts.map +1 -0
- package/dist/types/net/driver.d.ts +10 -0
- package/dist/types/net/driver.d.ts.map +1 -0
- package/dist/types/net/index.d.ts +3 -0
- package/dist/types/net/index.d.ts.map +1 -0
- package/dist/types/net/netchan.d.ts +85 -0
- package/dist/types/net/netchan.d.ts.map +1 -0
- package/dist/types/pmove/apply.d.ts +5 -0
- package/dist/types/pmove/apply.d.ts.map +1 -0
- package/dist/types/pmove/categorize.d.ts +36 -0
- package/dist/types/pmove/categorize.d.ts.map +1 -0
- package/dist/types/pmove/config.d.ts +5 -0
- package/dist/types/pmove/config.d.ts.map +1 -0
- package/dist/types/pmove/constants.d.ts +76 -0
- package/dist/types/pmove/constants.d.ts.map +1 -0
- package/dist/types/pmove/currents.d.ts +58 -0
- package/dist/types/pmove/currents.d.ts.map +1 -0
- package/dist/types/pmove/dimensions.d.ts +14 -0
- package/dist/types/pmove/dimensions.d.ts.map +1 -0
- package/dist/types/pmove/duck.d.ts +39 -0
- package/dist/types/pmove/duck.d.ts.map +1 -0
- package/dist/types/pmove/fly.d.ts +34 -0
- package/dist/types/pmove/fly.d.ts.map +1 -0
- package/dist/types/pmove/index.d.ts +18 -0
- package/dist/types/pmove/index.d.ts.map +1 -0
- package/dist/types/pmove/jump.d.ts +28 -0
- package/dist/types/pmove/jump.d.ts.map +1 -0
- package/dist/types/pmove/move.d.ts +78 -0
- package/dist/types/pmove/move.d.ts.map +1 -0
- package/dist/types/pmove/pmove.d.ts +40 -0
- package/dist/types/pmove/pmove.d.ts.map +1 -0
- package/dist/types/pmove/slide.d.ts +63 -0
- package/dist/types/pmove/slide.d.ts.map +1 -0
- package/dist/types/pmove/snap.d.ts +40 -0
- package/dist/types/pmove/snap.d.ts.map +1 -0
- package/dist/types/pmove/special.d.ts +39 -0
- package/dist/types/pmove/special.d.ts.map +1 -0
- package/dist/types/pmove/stuck.d.ts +21 -0
- package/dist/types/pmove/stuck.d.ts.map +1 -0
- package/dist/types/pmove/types.d.ts +72 -0
- package/dist/types/pmove/types.d.ts.map +1 -0
- package/dist/types/pmove/view.d.ts +19 -0
- package/dist/types/pmove/view.d.ts.map +1 -0
- package/dist/types/pmove/water.d.ts +21 -0
- package/dist/types/pmove/water.d.ts.map +1 -0
- package/dist/types/protocol/bitpack.d.ts +17 -0
- package/dist/types/protocol/bitpack.d.ts.map +1 -0
- package/dist/types/protocol/configstrings.d.ts +73 -0
- package/dist/types/protocol/configstrings.d.ts.map +1 -0
- package/dist/types/protocol/constants.d.ts +36 -0
- package/dist/types/protocol/constants.d.ts.map +1 -0
- package/dist/types/protocol/contracts.d.ts +17 -0
- package/dist/types/protocol/contracts.d.ts.map +1 -0
- package/dist/types/protocol/crc.d.ts +5 -0
- package/dist/types/protocol/crc.d.ts.map +1 -0
- package/dist/types/protocol/cvar.d.ts +15 -0
- package/dist/types/protocol/cvar.d.ts.map +1 -0
- package/dist/types/protocol/effects.d.ts +33 -0
- package/dist/types/protocol/effects.d.ts.map +1 -0
- package/dist/types/protocol/entity.d.ts +46 -0
- package/dist/types/protocol/entity.d.ts.map +1 -0
- package/dist/types/protocol/entityEvent.d.ts +13 -0
- package/dist/types/protocol/entityEvent.d.ts.map +1 -0
- package/dist/types/protocol/entityState.d.ts +26 -0
- package/dist/types/protocol/entityState.d.ts.map +1 -0
- package/dist/types/protocol/index.d.ts +19 -0
- package/dist/types/protocol/index.d.ts.map +1 -0
- package/dist/types/protocol/layout.d.ts +9 -0
- package/dist/types/protocol/layout.d.ts.map +1 -0
- package/dist/types/protocol/ops.d.ts +44 -0
- package/dist/types/protocol/ops.d.ts.map +1 -0
- package/dist/types/protocol/player-state.d.ts +40 -0
- package/dist/types/protocol/player-state.d.ts.map +1 -0
- package/dist/types/protocol/player.d.ts +28 -0
- package/dist/types/protocol/player.d.ts.map +1 -0
- package/dist/types/protocol/renderFx.d.ts +23 -0
- package/dist/types/protocol/renderFx.d.ts.map +1 -0
- package/dist/types/protocol/stats.d.ts +61 -0
- package/dist/types/protocol/stats.d.ts.map +1 -0
- package/dist/types/protocol/tempEntity.d.ts +67 -0
- package/dist/types/protocol/tempEntity.d.ts.map +1 -0
- package/dist/types/protocol/usercmd.d.ts +33 -0
- package/dist/types/protocol/usercmd.d.ts.map +1 -0
- package/dist/types/protocol/writeUserCmd.d.ts +4 -0
- package/dist/types/protocol/writeUserCmd.d.ts.map +1 -0
- package/dist/types/replay/index.d.ts +3 -0
- package/dist/types/replay/index.d.ts.map +1 -0
- package/dist/types/replay/io.d.ts +7 -0
- package/dist/types/replay/io.d.ts.map +1 -0
- package/dist/types/replay/schema.d.ts +41 -0
- package/dist/types/replay/schema.d.ts.map +1 -0
- package/dist/types/testing.d.ts +6 -0
- package/dist/types/testing.d.ts.map +1 -0
- package/package.json +43 -0
- package/src/audio/constants.ts +35 -0
- package/src/bsp/collision.ts +1075 -0
- package/src/bsp/contents.ts +108 -0
- package/src/bsp/spatial.ts +116 -0
- package/src/index.ts +37 -0
- package/src/inventory-helpers.ts +81 -0
- package/src/io/binaryStream.ts +159 -0
- package/src/io/binaryWriter.ts +146 -0
- package/src/io/index.ts +3 -0
- package/src/io/messageBuilder.ts +117 -0
- package/src/items/ammo.ts +47 -0
- package/src/items/index.ts +8 -0
- package/src/items/powerups.ts +32 -0
- package/src/items/weaponInfo.ts +45 -0
- package/src/items/weapons.ts +28 -0
- package/src/math/angles.ts +135 -0
- package/src/math/anorms.ts +165 -0
- package/src/math/color.ts +42 -0
- package/src/math/mat4.ts +58 -0
- package/src/math/random.ts +182 -0
- package/src/math/vec3.ts +379 -0
- package/src/net/driver.ts +9 -0
- package/src/net/index.ts +2 -0
- package/src/net/netchan.ts +451 -0
- package/src/pmove/apply.ts +151 -0
- package/src/pmove/categorize.ts +162 -0
- package/src/pmove/config.ts +5 -0
- package/src/pmove/constants.ts +94 -0
- package/src/pmove/currents.ts +287 -0
- package/src/pmove/dimensions.ts +40 -0
- package/src/pmove/duck.ts +154 -0
- package/src/pmove/fly.ts +197 -0
- package/src/pmove/index.ts +18 -0
- package/src/pmove/jump.ts +92 -0
- package/src/pmove/move.ts +527 -0
- package/src/pmove/pmove.ts +446 -0
- package/src/pmove/slide.ts +267 -0
- package/src/pmove/snap.ts +89 -0
- package/src/pmove/special.ts +207 -0
- package/src/pmove/stuck.ts +258 -0
- package/src/pmove/types.ts +82 -0
- package/src/pmove/view.ts +57 -0
- package/src/pmove/water.ts +56 -0
- package/src/protocol/bitpack.ts +139 -0
- package/src/protocol/configstrings.ts +104 -0
- package/src/protocol/constants.ts +40 -0
- package/src/protocol/contracts.ts +149 -0
- package/src/protocol/crc.ts +32 -0
- package/src/protocol/cvar.ts +15 -0
- package/src/protocol/effects.ts +33 -0
- package/src/protocol/entity.ts +304 -0
- package/src/protocol/entityEvent.ts +14 -0
- package/src/protocol/entityState.ts +28 -0
- package/src/protocol/index.ts +19 -0
- package/src/protocol/layout.ts +9 -0
- package/src/protocol/ops.ts +49 -0
- package/src/protocol/player-state.ts +51 -0
- package/src/protocol/player.ts +165 -0
- package/src/protocol/renderFx.ts +22 -0
- package/src/protocol/stats.ts +161 -0
- package/src/protocol/tempEntity.ts +69 -0
- package/src/protocol/usercmd.ts +63 -0
- package/src/protocol/writeUserCmd.ts +30 -0
- package/src/replay/index.ts +2 -0
- package/src/replay/io.ts +37 -0
- package/src/replay/schema.ts +42 -0
- package/src/testing.ts +200 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { BinaryWriter } from '../io/binaryWriter.js';
|
|
2
|
+
|
|
3
|
+
export interface NetAddress {
|
|
4
|
+
type: string;
|
|
5
|
+
port: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* NetChan handles reliable message delivery over an unreliable channel (UDP/WebSocket).
|
|
10
|
+
* Fragmentation support is planned but not fully implemented.
|
|
11
|
+
*
|
|
12
|
+
* Ported from qcommon/net_chan.c
|
|
13
|
+
*/
|
|
14
|
+
export class NetChan {
|
|
15
|
+
// Constants from net_chan.c
|
|
16
|
+
static readonly MAX_MSGLEN = 1400;
|
|
17
|
+
static readonly FRAGMENT_SIZE = 1024;
|
|
18
|
+
static readonly PACKET_HEADER = 10; // sequence(4) + ack(4) + qport(2)
|
|
19
|
+
static readonly HEADER_OVERHEAD = NetChan.PACKET_HEADER + 2; // +2 for reliable length prefix
|
|
20
|
+
|
|
21
|
+
// Increase internal reliable buffer to support large messages (fragmentation)
|
|
22
|
+
// Quake 2 uses MAX_MSGLEN for the reliable buffer, limiting single messages to ~1400 bytes.
|
|
23
|
+
// We expand this to allow larger messages (e.g. snapshots, downloads) which are then fragmented.
|
|
24
|
+
static readonly MAX_RELIABLE_BUFFER = 0x40000; // 256KB
|
|
25
|
+
|
|
26
|
+
// Public state
|
|
27
|
+
qport = 0; // qport value to distinguish multiple clients from same IP
|
|
28
|
+
|
|
29
|
+
// Sequencing
|
|
30
|
+
incomingSequence = 0;
|
|
31
|
+
outgoingSequence = 0;
|
|
32
|
+
incomingAcknowledged = 0;
|
|
33
|
+
|
|
34
|
+
// Reliable messaging
|
|
35
|
+
incomingReliableAcknowledged = false; // single bit
|
|
36
|
+
incomingReliableSequence = 0; // last reliable message sequence received
|
|
37
|
+
outgoingReliableSequence = 0; // reliable message sequence number to send
|
|
38
|
+
reliableMessage: BinaryWriter;
|
|
39
|
+
reliableLength = 0;
|
|
40
|
+
|
|
41
|
+
// Fragmentation State (Sending)
|
|
42
|
+
fragmentSendOffset = 0;
|
|
43
|
+
|
|
44
|
+
// Fragmentation State (Receiving)
|
|
45
|
+
fragmentBuffer: Uint8Array | null = null;
|
|
46
|
+
fragmentLength = 0;
|
|
47
|
+
fragmentReceived = 0;
|
|
48
|
+
|
|
49
|
+
// Timing
|
|
50
|
+
lastReceived = 0;
|
|
51
|
+
lastSent = 0;
|
|
52
|
+
|
|
53
|
+
remoteAddress: NetAddress | null = null;
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
// Initialize buffers
|
|
57
|
+
this.reliableMessage = new BinaryWriter(NetChan.MAX_RELIABLE_BUFFER);
|
|
58
|
+
|
|
59
|
+
// Set initial timestamps
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
this.lastReceived = now;
|
|
62
|
+
this.lastSent = now;
|
|
63
|
+
|
|
64
|
+
// Random qport by default (can be overridden)
|
|
65
|
+
// Ensure we use global Math.random which is usually seeded or random enough for basic collision avoidance
|
|
66
|
+
this.qport = Math.floor(Math.random() * 65536);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Setup the netchan with specific settings
|
|
71
|
+
*/
|
|
72
|
+
setup(qport: number, address: NetAddress | null = null): void {
|
|
73
|
+
this.qport = qport;
|
|
74
|
+
this.remoteAddress = address;
|
|
75
|
+
this.reset();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reset the netchan state
|
|
80
|
+
*/
|
|
81
|
+
reset(): void {
|
|
82
|
+
this.incomingSequence = 0;
|
|
83
|
+
this.outgoingSequence = 0;
|
|
84
|
+
this.incomingAcknowledged = 0;
|
|
85
|
+
this.incomingReliableAcknowledged = false;
|
|
86
|
+
this.incomingReliableSequence = 0;
|
|
87
|
+
this.outgoingReliableSequence = 0;
|
|
88
|
+
this.reliableLength = 0;
|
|
89
|
+
this.reliableMessage.reset();
|
|
90
|
+
|
|
91
|
+
this.fragmentSendOffset = 0;
|
|
92
|
+
this.fragmentBuffer = null;
|
|
93
|
+
this.fragmentLength = 0;
|
|
94
|
+
this.fragmentReceived = 0;
|
|
95
|
+
|
|
96
|
+
this.lastReceived = Date.now();
|
|
97
|
+
this.lastSent = Date.now();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Transmits a packet containing reliable and unreliable data
|
|
102
|
+
*/
|
|
103
|
+
transmit(unreliableData?: Uint8Array): Uint8Array {
|
|
104
|
+
this.outgoingSequence++;
|
|
105
|
+
this.lastSent = Date.now();
|
|
106
|
+
|
|
107
|
+
// Determine how much reliable data to send in this packet
|
|
108
|
+
let sendReliableLength = 0;
|
|
109
|
+
let isFragment = false;
|
|
110
|
+
let fragmentStart = 0;
|
|
111
|
+
|
|
112
|
+
if (this.reliableLength > 0) {
|
|
113
|
+
// Check if we need to fragment
|
|
114
|
+
if (this.reliableLength > NetChan.FRAGMENT_SIZE) {
|
|
115
|
+
// We are in fragment mode
|
|
116
|
+
isFragment = true;
|
|
117
|
+
|
|
118
|
+
// If we have finished sending all fragments but still haven't received ACK,
|
|
119
|
+
// we must loop back to the beginning to retransmit.
|
|
120
|
+
if (this.fragmentSendOffset >= this.reliableLength) {
|
|
121
|
+
this.fragmentSendOffset = 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Calculate chunk size
|
|
125
|
+
const remaining = this.reliableLength - this.fragmentSendOffset;
|
|
126
|
+
sendReliableLength = remaining;
|
|
127
|
+
if (sendReliableLength > NetChan.FRAGMENT_SIZE) {
|
|
128
|
+
sendReliableLength = NetChan.FRAGMENT_SIZE;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fragmentStart = this.fragmentSendOffset;
|
|
132
|
+
|
|
133
|
+
// Advance offset for the next packet
|
|
134
|
+
this.fragmentSendOffset += sendReliableLength;
|
|
135
|
+
} else {
|
|
136
|
+
// Fits in one packet
|
|
137
|
+
sendReliableLength = this.reliableLength;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Calculate total size
|
|
142
|
+
// Header + Reliable + Unreliable
|
|
143
|
+
const headerSize = NetChan.PACKET_HEADER;
|
|
144
|
+
const reliableHeaderSize = sendReliableLength > 0 ? 2 + (isFragment ? 8 : 0) : 0; // +2 length, +8 fragment info
|
|
145
|
+
|
|
146
|
+
let unreliableSize = unreliableData ? unreliableData.length : 0;
|
|
147
|
+
|
|
148
|
+
// Check for overflow
|
|
149
|
+
if (headerSize + reliableHeaderSize + sendReliableLength + unreliableSize > NetChan.MAX_MSGLEN) {
|
|
150
|
+
unreliableSize = NetChan.MAX_MSGLEN - headerSize - reliableHeaderSize - sendReliableLength;
|
|
151
|
+
// We truncate unreliable data if it doesn't fit with reliable data
|
|
152
|
+
if (unreliableSize < 0) unreliableSize = 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const buffer = new ArrayBuffer(headerSize + reliableHeaderSize + sendReliableLength + unreliableSize);
|
|
156
|
+
const view = new DataView(buffer);
|
|
157
|
+
const result = new Uint8Array(buffer);
|
|
158
|
+
|
|
159
|
+
// Write Header
|
|
160
|
+
// Sequence
|
|
161
|
+
let sequence = this.outgoingSequence;
|
|
162
|
+
|
|
163
|
+
// Set reliable bit if we are sending reliable data
|
|
164
|
+
if (sendReliableLength > 0) {
|
|
165
|
+
sequence |= 0x80000000;
|
|
166
|
+
// Also set the reliable sequence bit (0/1 toggle) at bit 30
|
|
167
|
+
if ((this.outgoingReliableSequence & 1) !== 0) {
|
|
168
|
+
sequence |= 0x40000000;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
view.setUint32(0, sequence, true);
|
|
173
|
+
|
|
174
|
+
// Acknowledge
|
|
175
|
+
// Set reliable ack bit at bit 31
|
|
176
|
+
let ack = this.incomingSequence;
|
|
177
|
+
if ((this.incomingReliableSequence & 1) !== 0) {
|
|
178
|
+
ack |= 0x80000000;
|
|
179
|
+
}
|
|
180
|
+
view.setUint32(4, ack, true);
|
|
181
|
+
|
|
182
|
+
view.setUint16(8, this.qport, true);
|
|
183
|
+
|
|
184
|
+
// Copy Reliable Data
|
|
185
|
+
let offset = headerSize;
|
|
186
|
+
if (sendReliableLength > 0) {
|
|
187
|
+
// Write length of reliable data (2 bytes)
|
|
188
|
+
// Extension: If length has high bit (0x8000), it's a fragment.
|
|
189
|
+
let lengthField = sendReliableLength;
|
|
190
|
+
if (isFragment) {
|
|
191
|
+
lengthField |= 0x8000;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
view.setUint16(offset, lengthField, true);
|
|
195
|
+
offset += 2;
|
|
196
|
+
|
|
197
|
+
if (isFragment) {
|
|
198
|
+
// Write fragment info: 4 bytes start offset, 4 bytes total length
|
|
199
|
+
view.setUint32(offset, fragmentStart, true);
|
|
200
|
+
offset += 4;
|
|
201
|
+
view.setUint32(offset, this.reliableLength, true);
|
|
202
|
+
offset += 4;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Copy data
|
|
206
|
+
const reliableBuffer = this.reliableMessage.getBuffer();
|
|
207
|
+
const reliableBytes = reliableBuffer.subarray(fragmentStart, fragmentStart + sendReliableLength);
|
|
208
|
+
result.set(reliableBytes, offset);
|
|
209
|
+
offset += sendReliableLength;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Copy Unreliable Data
|
|
213
|
+
if (unreliableData && unreliableSize > 0) {
|
|
214
|
+
const chunk = unreliableData.slice(0, unreliableSize);
|
|
215
|
+
result.set(chunk, offset);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Processes a received packet
|
|
223
|
+
* Returns the payload data (reliable + unreliable) to be processed, or null if discarded
|
|
224
|
+
*/
|
|
225
|
+
process(packet: Uint8Array): Uint8Array | null {
|
|
226
|
+
if (packet.length < NetChan.PACKET_HEADER) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.lastReceived = Date.now();
|
|
231
|
+
|
|
232
|
+
const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
|
|
233
|
+
const sequence = view.getUint32(0, true);
|
|
234
|
+
const ack = view.getUint32(4, true);
|
|
235
|
+
const qport = view.getUint16(8, true);
|
|
236
|
+
|
|
237
|
+
if (this.qport !== qport) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check for duplicate or out of order
|
|
242
|
+
const seqNumberClean = sequence & ~(0x80000000 | 0x40000000); // Mask out flags
|
|
243
|
+
|
|
244
|
+
// Handle wrapping using signed difference
|
|
245
|
+
if (((seqNumberClean - this.incomingSequence) | 0) <= 0) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Update incoming sequence
|
|
250
|
+
this.incomingSequence = seqNumberClean;
|
|
251
|
+
|
|
252
|
+
// Handle reliable acknowledgment
|
|
253
|
+
const ackNumber = ack & ~0x80000000;
|
|
254
|
+
const ackReliable = (ack & 0x80000000) !== 0;
|
|
255
|
+
|
|
256
|
+
if (ackNumber > this.incomingAcknowledged) {
|
|
257
|
+
this.incomingAcknowledged = ackNumber;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check if our reliable message was acknowledged
|
|
261
|
+
// If the receiver has toggled their reliable bit, it means they got the WHOLE message
|
|
262
|
+
if (this.reliableLength > 0) {
|
|
263
|
+
const receivedAckBit = ackReliable ? 1 : 0;
|
|
264
|
+
const currentReliableBit = this.outgoingReliableSequence & 1;
|
|
265
|
+
|
|
266
|
+
if (receivedAckBit !== currentReliableBit) {
|
|
267
|
+
// Acked!
|
|
268
|
+
this.reliableLength = 0;
|
|
269
|
+
this.reliableMessage.reset();
|
|
270
|
+
this.outgoingReliableSequence ^= 1;
|
|
271
|
+
this.fragmentSendOffset = 0; // Reset fragment offset
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Handle incoming reliable data
|
|
276
|
+
const hasReliableData = (sequence & 0x80000000) !== 0;
|
|
277
|
+
const reliableSeqBit = (sequence & 0x40000000) !== 0 ? 1 : 0;
|
|
278
|
+
|
|
279
|
+
let payloadOffset = NetChan.PACKET_HEADER;
|
|
280
|
+
let reliableData: Uint8Array | null = null;
|
|
281
|
+
|
|
282
|
+
if (hasReliableData) {
|
|
283
|
+
if (payloadOffset + 2 > packet.byteLength) return null; // Malformed
|
|
284
|
+
|
|
285
|
+
let reliableLen = view.getUint16(payloadOffset, true);
|
|
286
|
+
payloadOffset += 2;
|
|
287
|
+
|
|
288
|
+
const isFragment = (reliableLen & 0x8000) !== 0;
|
|
289
|
+
reliableLen &= 0x7FFF;
|
|
290
|
+
|
|
291
|
+
// Check if this is the expected reliable sequence
|
|
292
|
+
const expectedBit = this.incomingReliableSequence & 1;
|
|
293
|
+
|
|
294
|
+
if (reliableSeqBit === expectedBit) {
|
|
295
|
+
// It's the sequence we are waiting for
|
|
296
|
+
|
|
297
|
+
if (isFragment) {
|
|
298
|
+
// Read fragment info
|
|
299
|
+
if (payloadOffset + 8 > packet.byteLength) return null;
|
|
300
|
+
const fragStart = view.getUint32(payloadOffset, true);
|
|
301
|
+
payloadOffset += 4;
|
|
302
|
+
const fragTotal = view.getUint32(payloadOffset, true);
|
|
303
|
+
payloadOffset += 4;
|
|
304
|
+
|
|
305
|
+
// Validate fragTotal against MAX_RELIABLE_BUFFER
|
|
306
|
+
if (fragTotal > NetChan.MAX_RELIABLE_BUFFER) {
|
|
307
|
+
console.warn(`NetChan: received invalid fragment total ${fragTotal} > ${NetChan.MAX_RELIABLE_BUFFER}`);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Initialize fragment buffer if needed
|
|
312
|
+
if (!this.fragmentBuffer || this.fragmentBuffer.length !== fragTotal) {
|
|
313
|
+
this.fragmentBuffer = new Uint8Array(fragTotal);
|
|
314
|
+
this.fragmentLength = fragTotal;
|
|
315
|
+
this.fragmentReceived = 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check for valid fragment offset
|
|
319
|
+
if (payloadOffset + reliableLen > packet.byteLength) return null;
|
|
320
|
+
const data = packet.subarray(payloadOffset, payloadOffset + reliableLen);
|
|
321
|
+
|
|
322
|
+
// Only accept if it matches our expected offset (enforce in-order delivery for simplicity)
|
|
323
|
+
// or check if we haven't received this part yet.
|
|
324
|
+
// Since we use a simple 'fragmentReceived' counter, we effectively expect in-order delivery
|
|
325
|
+
// of streams if we just use append logic.
|
|
326
|
+
// BUT UDP can reorder.
|
|
327
|
+
// To be robust, we should enforce strict ordering: fragStart must equal fragmentReceived.
|
|
328
|
+
// If we miss a chunk, we ignore subsequent chunks until the missing one arrives (via retransmit loop).
|
|
329
|
+
|
|
330
|
+
if (fragStart === this.fragmentReceived && fragStart + reliableLen <= fragTotal) {
|
|
331
|
+
this.fragmentBuffer.set(data, fragStart);
|
|
332
|
+
this.fragmentReceived += reliableLen;
|
|
333
|
+
|
|
334
|
+
// Check if complete
|
|
335
|
+
if (this.fragmentReceived >= fragTotal) {
|
|
336
|
+
reliableData = this.fragmentBuffer;
|
|
337
|
+
this.incomingReliableSequence++;
|
|
338
|
+
this.fragmentBuffer = null;
|
|
339
|
+
this.fragmentLength = 0;
|
|
340
|
+
this.fragmentReceived = 0;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
} else {
|
|
345
|
+
// Not a fragment (standard)
|
|
346
|
+
this.incomingReliableSequence++;
|
|
347
|
+
if (payloadOffset + reliableLen > packet.byteLength) return null;
|
|
348
|
+
reliableData = packet.slice(payloadOffset, payloadOffset + reliableLen);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Advance past reliable data regardless
|
|
353
|
+
payloadOffset += reliableLen;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Get unreliable data
|
|
357
|
+
const unreliableData = packet.slice(payloadOffset);
|
|
358
|
+
|
|
359
|
+
// Combine if we have reliable data
|
|
360
|
+
if (reliableData && reliableData.length > 0) {
|
|
361
|
+
const totalLen = reliableData.length + unreliableData.length;
|
|
362
|
+
const result = new Uint8Array(totalLen);
|
|
363
|
+
result.set(reliableData, 0);
|
|
364
|
+
result.set(unreliableData, reliableData.length);
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (unreliableData) {
|
|
369
|
+
return unreliableData;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return new Uint8Array(0);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Checks if reliable message buffer is empty and ready for new data
|
|
377
|
+
*/
|
|
378
|
+
canSendReliable(): boolean {
|
|
379
|
+
return this.reliableLength === 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Writes a byte to the reliable message buffer
|
|
384
|
+
*/
|
|
385
|
+
writeReliableByte(value: number): void {
|
|
386
|
+
if (this.reliableLength + 1 > NetChan.MAX_RELIABLE_BUFFER) {
|
|
387
|
+
throw new Error('NetChan reliable buffer overflow');
|
|
388
|
+
}
|
|
389
|
+
this.reliableMessage.writeByte(value);
|
|
390
|
+
this.reliableLength++;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Writes a short to the reliable message buffer
|
|
395
|
+
*/
|
|
396
|
+
writeReliableShort(value: number): void {
|
|
397
|
+
if (this.reliableLength + 2 > NetChan.MAX_RELIABLE_BUFFER) {
|
|
398
|
+
throw new Error('NetChan reliable buffer overflow');
|
|
399
|
+
}
|
|
400
|
+
this.reliableMessage.writeShort(value);
|
|
401
|
+
this.reliableLength += 2;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Writes a long to the reliable message buffer
|
|
406
|
+
*/
|
|
407
|
+
writeReliableLong(value: number): void {
|
|
408
|
+
if (this.reliableLength + 4 > NetChan.MAX_RELIABLE_BUFFER) {
|
|
409
|
+
throw new Error('NetChan reliable buffer overflow');
|
|
410
|
+
}
|
|
411
|
+
this.reliableMessage.writeLong(value);
|
|
412
|
+
this.reliableLength += 4;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Writes a string to the reliable message buffer
|
|
417
|
+
*/
|
|
418
|
+
writeReliableString(value: string): void {
|
|
419
|
+
const len = value.length + 1; // +1 for null terminator
|
|
420
|
+
if (this.reliableLength + len > NetChan.MAX_RELIABLE_BUFFER) {
|
|
421
|
+
throw new Error('NetChan reliable buffer overflow');
|
|
422
|
+
}
|
|
423
|
+
this.reliableMessage.writeString(value);
|
|
424
|
+
this.reliableLength += len;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Returns the current reliable data buffer
|
|
429
|
+
*/
|
|
430
|
+
getReliableData(): Uint8Array {
|
|
431
|
+
if (this.reliableLength === 0) {
|
|
432
|
+
return new Uint8Array(0);
|
|
433
|
+
}
|
|
434
|
+
const buffer = this.reliableMessage.getBuffer();
|
|
435
|
+
return buffer.subarray(0, this.reliableLength);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Checks if we need to send a keepalive packet
|
|
440
|
+
*/
|
|
441
|
+
needsKeepalive(currentTime: number): boolean {
|
|
442
|
+
return (currentTime - this.lastSent) > 1000;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Checks if the connection has timed out
|
|
447
|
+
*/
|
|
448
|
+
isTimedOut(currentTime: number, timeoutMs: number = 30000): boolean {
|
|
449
|
+
return (currentTime - this.lastReceived) > timeoutMs;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
import { PmoveCmd, PmoveTraceFn } from './types.js';
|
|
3
|
+
import { Vec3 } from '../math/vec3.js';
|
|
4
|
+
|
|
5
|
+
import { applyPmoveAccelerate, applyPmoveFriction, buildAirGroundWish, buildWaterWish } from './pmove.js';
|
|
6
|
+
import { PlayerState } from '../protocol/player-state.js';
|
|
7
|
+
import { angleVectors } from '../math/angles.js';
|
|
8
|
+
import { MASK_WATER } from '../bsp/contents.js';
|
|
9
|
+
|
|
10
|
+
const FRAMETIME = 0.025;
|
|
11
|
+
|
|
12
|
+
// Local definition to avoid dependency issues if constants.ts is missing
|
|
13
|
+
// Matches packages/shared/src/pmove/constants.ts
|
|
14
|
+
const WaterLevel = {
|
|
15
|
+
None: 0,
|
|
16
|
+
Feet: 1,
|
|
17
|
+
Waist: 2,
|
|
18
|
+
Under: 3,
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
const categorizePosition = (state: PlayerState, trace: PmoveTraceFn): PlayerState => {
|
|
22
|
+
const point = { ...state.origin };
|
|
23
|
+
point.z -= 0.25;
|
|
24
|
+
const traceResult = trace(state.origin, point);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
onGround: traceResult.fraction < 1,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const checkWater = (state: PlayerState, pointContents: (point: Vec3) => number): PlayerState => {
|
|
33
|
+
const point = { ...state.origin };
|
|
34
|
+
const { mins, maxs } = state;
|
|
35
|
+
|
|
36
|
+
// Default to feet
|
|
37
|
+
point.z = state.origin.z + mins.z + 1;
|
|
38
|
+
|
|
39
|
+
const feetContents = pointContents(point);
|
|
40
|
+
|
|
41
|
+
if (!(feetContents & MASK_WATER)) {
|
|
42
|
+
return { ...state, waterLevel: WaterLevel.None, watertype: 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let waterLevel: number = WaterLevel.Feet;
|
|
46
|
+
let watertype = feetContents;
|
|
47
|
+
|
|
48
|
+
// Check waist
|
|
49
|
+
const waist = state.origin.z + (mins.z + maxs.z) * 0.5;
|
|
50
|
+
point.z = waist;
|
|
51
|
+
const waistContents = pointContents(point);
|
|
52
|
+
|
|
53
|
+
if (waistContents & MASK_WATER) {
|
|
54
|
+
waterLevel = WaterLevel.Waist;
|
|
55
|
+
watertype = waistContents;
|
|
56
|
+
|
|
57
|
+
// Check head (eyes)
|
|
58
|
+
// Standard Quake 2 viewheight is 22. maxs.z is typically 32.
|
|
59
|
+
// So eyes are roughly at origin.z + 22.
|
|
60
|
+
// We'll use origin.z + 22 to check if eyes are underwater.
|
|
61
|
+
// If viewheight was available in PlayerState, we'd use that.
|
|
62
|
+
const head = state.origin.z + 22;
|
|
63
|
+
point.z = head;
|
|
64
|
+
const headContents = pointContents(point);
|
|
65
|
+
|
|
66
|
+
if (headContents & MASK_WATER) {
|
|
67
|
+
waterLevel = WaterLevel.Under;
|
|
68
|
+
watertype = headContents;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { ...state, waterLevel, watertype };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export const applyPmove = (
|
|
77
|
+
state: PlayerState,
|
|
78
|
+
cmd: PmoveCmd,
|
|
79
|
+
trace: PmoveTraceFn,
|
|
80
|
+
pointContents: (point: Vec3) => number
|
|
81
|
+
): PlayerState => {
|
|
82
|
+
let newState = { ...state };
|
|
83
|
+
newState = categorizePosition(newState, trace);
|
|
84
|
+
newState = checkWater(newState, pointContents);
|
|
85
|
+
|
|
86
|
+
const { origin, velocity, onGround, waterLevel, viewAngles } = newState;
|
|
87
|
+
|
|
88
|
+
// Calculate forward and right vectors from view angles
|
|
89
|
+
// For water movement, use full view angles including pitch
|
|
90
|
+
// For ground/air movement, reduce pitch influence by dividing by 3
|
|
91
|
+
// See: rerelease/p_move.cpp lines 1538, 1686-1691, 800, 858
|
|
92
|
+
const adjustedAngles = waterLevel >= 2
|
|
93
|
+
? viewAngles
|
|
94
|
+
: {
|
|
95
|
+
// For ground/air movement, reduce pitch influence (rerelease/p_move.cpp:1689)
|
|
96
|
+
x: viewAngles.x > 180 ? (viewAngles.x - 360) / 3 : viewAngles.x / 3,
|
|
97
|
+
y: viewAngles.y,
|
|
98
|
+
z: viewAngles.z,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const { forward, right } = angleVectors(adjustedAngles);
|
|
102
|
+
|
|
103
|
+
// Apply friction BEFORE acceleration to match original Quake 2 rerelease behavior
|
|
104
|
+
// See: rerelease/src/game/player/pmove.c lines 1678 (PM_Friction) then 1693 (PM_AirMove->PM_Accelerate)
|
|
105
|
+
const frictionedVelocity = applyPmoveFriction({
|
|
106
|
+
velocity,
|
|
107
|
+
frametime: FRAMETIME,
|
|
108
|
+
onGround,
|
|
109
|
+
groundIsSlick: false,
|
|
110
|
+
onLadder: false,
|
|
111
|
+
waterlevel: waterLevel,
|
|
112
|
+
pmFriction: 6,
|
|
113
|
+
pmStopSpeed: 100,
|
|
114
|
+
pmWaterFriction: 1,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const wish = waterLevel >= 2
|
|
118
|
+
? buildWaterWish({
|
|
119
|
+
forward,
|
|
120
|
+
right,
|
|
121
|
+
cmd,
|
|
122
|
+
maxSpeed: 320,
|
|
123
|
+
})
|
|
124
|
+
: buildAirGroundWish({
|
|
125
|
+
forward,
|
|
126
|
+
right,
|
|
127
|
+
cmd,
|
|
128
|
+
maxSpeed: 320,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const finalVelocity = applyPmoveAccelerate({
|
|
132
|
+
velocity: frictionedVelocity,
|
|
133
|
+
wishdir: wish.wishdir,
|
|
134
|
+
wishspeed: wish.wishspeed,
|
|
135
|
+
// Water movement uses ground acceleration (10), not air acceleration (1)
|
|
136
|
+
accel: (onGround || waterLevel >= 2) ? 10 : 1,
|
|
137
|
+
frametime: FRAMETIME,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const traceResult = trace(origin, {
|
|
141
|
+
x: origin.x + finalVelocity.x * FRAMETIME,
|
|
142
|
+
y: origin.y + finalVelocity.y * FRAMETIME,
|
|
143
|
+
z: origin.z + finalVelocity.z * FRAMETIME,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
...newState,
|
|
148
|
+
origin: traceResult.endpos,
|
|
149
|
+
velocity: finalVelocity,
|
|
150
|
+
};
|
|
151
|
+
};
|