@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.
Files changed (209) hide show
  1. package/dist/browser/index.global.js +2 -0
  2. package/dist/browser/index.global.js.map +1 -0
  3. package/dist/cjs/index.cjs +6569 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/esm/index.js +6200 -0
  6. package/dist/esm/index.js.map +1 -0
  7. package/dist/tsconfig.tsbuildinfo +1 -0
  8. package/dist/types/audio/constants.d.ts +24 -0
  9. package/dist/types/audio/constants.d.ts.map +1 -0
  10. package/dist/types/bsp/collision.d.ts +201 -0
  11. package/dist/types/bsp/collision.d.ts.map +1 -0
  12. package/dist/types/bsp/contents.d.ts +72 -0
  13. package/dist/types/bsp/contents.d.ts.map +1 -0
  14. package/dist/types/bsp/spatial.d.ts +13 -0
  15. package/dist/types/bsp/spatial.d.ts.map +1 -0
  16. package/dist/types/index.d.ts +38 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/inventory-helpers.d.ts +19 -0
  19. package/dist/types/inventory-helpers.d.ts.map +1 -0
  20. package/dist/types/io/binaryStream.d.ts +38 -0
  21. package/dist/types/io/binaryStream.d.ts.map +1 -0
  22. package/dist/types/io/binaryWriter.d.ts +26 -0
  23. package/dist/types/io/binaryWriter.d.ts.map +1 -0
  24. package/dist/types/io/index.d.ts +4 -0
  25. package/dist/types/io/index.d.ts.map +1 -0
  26. package/dist/types/io/messageBuilder.d.ts +21 -0
  27. package/dist/types/io/messageBuilder.d.ts.map +1 -0
  28. package/dist/types/items/ammo.d.ts +40 -0
  29. package/dist/types/items/ammo.d.ts.map +1 -0
  30. package/dist/types/items/index.d.ts +8 -0
  31. package/dist/types/items/index.d.ts.map +1 -0
  32. package/dist/types/items/powerups.d.ts +31 -0
  33. package/dist/types/items/powerups.d.ts.map +1 -0
  34. package/dist/types/items/weaponInfo.d.ts +5 -0
  35. package/dist/types/items/weaponInfo.d.ts.map +1 -0
  36. package/dist/types/items/weapons.d.ts +27 -0
  37. package/dist/types/items/weapons.d.ts.map +1 -0
  38. package/dist/types/math/angles.d.ts +19 -0
  39. package/dist/types/math/angles.d.ts.map +1 -0
  40. package/dist/types/math/anorms.d.ts +2 -0
  41. package/dist/types/math/anorms.d.ts.map +1 -0
  42. package/dist/types/math/color.d.ts +12 -0
  43. package/dist/types/math/color.d.ts.map +1 -0
  44. package/dist/types/math/mat4.d.ts +7 -0
  45. package/dist/types/math/mat4.d.ts.map +1 -0
  46. package/dist/types/math/random.d.ts +60 -0
  47. package/dist/types/math/random.d.ts.map +1 -0
  48. package/dist/types/math/vec3.d.ts +79 -0
  49. package/dist/types/math/vec3.d.ts.map +1 -0
  50. package/dist/types/net/driver.d.ts +10 -0
  51. package/dist/types/net/driver.d.ts.map +1 -0
  52. package/dist/types/net/index.d.ts +3 -0
  53. package/dist/types/net/index.d.ts.map +1 -0
  54. package/dist/types/net/netchan.d.ts +85 -0
  55. package/dist/types/net/netchan.d.ts.map +1 -0
  56. package/dist/types/pmove/apply.d.ts +5 -0
  57. package/dist/types/pmove/apply.d.ts.map +1 -0
  58. package/dist/types/pmove/categorize.d.ts +36 -0
  59. package/dist/types/pmove/categorize.d.ts.map +1 -0
  60. package/dist/types/pmove/config.d.ts +5 -0
  61. package/dist/types/pmove/config.d.ts.map +1 -0
  62. package/dist/types/pmove/constants.d.ts +76 -0
  63. package/dist/types/pmove/constants.d.ts.map +1 -0
  64. package/dist/types/pmove/currents.d.ts +58 -0
  65. package/dist/types/pmove/currents.d.ts.map +1 -0
  66. package/dist/types/pmove/dimensions.d.ts +14 -0
  67. package/dist/types/pmove/dimensions.d.ts.map +1 -0
  68. package/dist/types/pmove/duck.d.ts +39 -0
  69. package/dist/types/pmove/duck.d.ts.map +1 -0
  70. package/dist/types/pmove/fly.d.ts +34 -0
  71. package/dist/types/pmove/fly.d.ts.map +1 -0
  72. package/dist/types/pmove/index.d.ts +18 -0
  73. package/dist/types/pmove/index.d.ts.map +1 -0
  74. package/dist/types/pmove/jump.d.ts +28 -0
  75. package/dist/types/pmove/jump.d.ts.map +1 -0
  76. package/dist/types/pmove/move.d.ts +78 -0
  77. package/dist/types/pmove/move.d.ts.map +1 -0
  78. package/dist/types/pmove/pmove.d.ts +40 -0
  79. package/dist/types/pmove/pmove.d.ts.map +1 -0
  80. package/dist/types/pmove/slide.d.ts +63 -0
  81. package/dist/types/pmove/slide.d.ts.map +1 -0
  82. package/dist/types/pmove/snap.d.ts +40 -0
  83. package/dist/types/pmove/snap.d.ts.map +1 -0
  84. package/dist/types/pmove/special.d.ts +39 -0
  85. package/dist/types/pmove/special.d.ts.map +1 -0
  86. package/dist/types/pmove/stuck.d.ts +21 -0
  87. package/dist/types/pmove/stuck.d.ts.map +1 -0
  88. package/dist/types/pmove/types.d.ts +72 -0
  89. package/dist/types/pmove/types.d.ts.map +1 -0
  90. package/dist/types/pmove/view.d.ts +19 -0
  91. package/dist/types/pmove/view.d.ts.map +1 -0
  92. package/dist/types/pmove/water.d.ts +21 -0
  93. package/dist/types/pmove/water.d.ts.map +1 -0
  94. package/dist/types/protocol/bitpack.d.ts +17 -0
  95. package/dist/types/protocol/bitpack.d.ts.map +1 -0
  96. package/dist/types/protocol/configstrings.d.ts +73 -0
  97. package/dist/types/protocol/configstrings.d.ts.map +1 -0
  98. package/dist/types/protocol/constants.d.ts +36 -0
  99. package/dist/types/protocol/constants.d.ts.map +1 -0
  100. package/dist/types/protocol/contracts.d.ts +17 -0
  101. package/dist/types/protocol/contracts.d.ts.map +1 -0
  102. package/dist/types/protocol/crc.d.ts +5 -0
  103. package/dist/types/protocol/crc.d.ts.map +1 -0
  104. package/dist/types/protocol/cvar.d.ts +15 -0
  105. package/dist/types/protocol/cvar.d.ts.map +1 -0
  106. package/dist/types/protocol/effects.d.ts +33 -0
  107. package/dist/types/protocol/effects.d.ts.map +1 -0
  108. package/dist/types/protocol/entity.d.ts +46 -0
  109. package/dist/types/protocol/entity.d.ts.map +1 -0
  110. package/dist/types/protocol/entityEvent.d.ts +13 -0
  111. package/dist/types/protocol/entityEvent.d.ts.map +1 -0
  112. package/dist/types/protocol/entityState.d.ts +26 -0
  113. package/dist/types/protocol/entityState.d.ts.map +1 -0
  114. package/dist/types/protocol/index.d.ts +19 -0
  115. package/dist/types/protocol/index.d.ts.map +1 -0
  116. package/dist/types/protocol/layout.d.ts +9 -0
  117. package/dist/types/protocol/layout.d.ts.map +1 -0
  118. package/dist/types/protocol/ops.d.ts +44 -0
  119. package/dist/types/protocol/ops.d.ts.map +1 -0
  120. package/dist/types/protocol/player-state.d.ts +40 -0
  121. package/dist/types/protocol/player-state.d.ts.map +1 -0
  122. package/dist/types/protocol/player.d.ts +28 -0
  123. package/dist/types/protocol/player.d.ts.map +1 -0
  124. package/dist/types/protocol/renderFx.d.ts +23 -0
  125. package/dist/types/protocol/renderFx.d.ts.map +1 -0
  126. package/dist/types/protocol/stats.d.ts +61 -0
  127. package/dist/types/protocol/stats.d.ts.map +1 -0
  128. package/dist/types/protocol/tempEntity.d.ts +67 -0
  129. package/dist/types/protocol/tempEntity.d.ts.map +1 -0
  130. package/dist/types/protocol/usercmd.d.ts +33 -0
  131. package/dist/types/protocol/usercmd.d.ts.map +1 -0
  132. package/dist/types/protocol/writeUserCmd.d.ts +4 -0
  133. package/dist/types/protocol/writeUserCmd.d.ts.map +1 -0
  134. package/dist/types/replay/index.d.ts +3 -0
  135. package/dist/types/replay/index.d.ts.map +1 -0
  136. package/dist/types/replay/io.d.ts +7 -0
  137. package/dist/types/replay/io.d.ts.map +1 -0
  138. package/dist/types/replay/schema.d.ts +41 -0
  139. package/dist/types/replay/schema.d.ts.map +1 -0
  140. package/dist/types/testing.d.ts +6 -0
  141. package/dist/types/testing.d.ts.map +1 -0
  142. package/package.json +43 -0
  143. package/src/audio/constants.ts +35 -0
  144. package/src/bsp/collision.ts +1075 -0
  145. package/src/bsp/contents.ts +108 -0
  146. package/src/bsp/spatial.ts +116 -0
  147. package/src/index.ts +37 -0
  148. package/src/inventory-helpers.ts +81 -0
  149. package/src/io/binaryStream.ts +159 -0
  150. package/src/io/binaryWriter.ts +146 -0
  151. package/src/io/index.ts +3 -0
  152. package/src/io/messageBuilder.ts +117 -0
  153. package/src/items/ammo.ts +47 -0
  154. package/src/items/index.ts +8 -0
  155. package/src/items/powerups.ts +32 -0
  156. package/src/items/weaponInfo.ts +45 -0
  157. package/src/items/weapons.ts +28 -0
  158. package/src/math/angles.ts +135 -0
  159. package/src/math/anorms.ts +165 -0
  160. package/src/math/color.ts +42 -0
  161. package/src/math/mat4.ts +58 -0
  162. package/src/math/random.ts +182 -0
  163. package/src/math/vec3.ts +379 -0
  164. package/src/net/driver.ts +9 -0
  165. package/src/net/index.ts +2 -0
  166. package/src/net/netchan.ts +451 -0
  167. package/src/pmove/apply.ts +151 -0
  168. package/src/pmove/categorize.ts +162 -0
  169. package/src/pmove/config.ts +5 -0
  170. package/src/pmove/constants.ts +94 -0
  171. package/src/pmove/currents.ts +287 -0
  172. package/src/pmove/dimensions.ts +40 -0
  173. package/src/pmove/duck.ts +154 -0
  174. package/src/pmove/fly.ts +197 -0
  175. package/src/pmove/index.ts +18 -0
  176. package/src/pmove/jump.ts +92 -0
  177. package/src/pmove/move.ts +527 -0
  178. package/src/pmove/pmove.ts +446 -0
  179. package/src/pmove/slide.ts +267 -0
  180. package/src/pmove/snap.ts +89 -0
  181. package/src/pmove/special.ts +207 -0
  182. package/src/pmove/stuck.ts +258 -0
  183. package/src/pmove/types.ts +82 -0
  184. package/src/pmove/view.ts +57 -0
  185. package/src/pmove/water.ts +56 -0
  186. package/src/protocol/bitpack.ts +139 -0
  187. package/src/protocol/configstrings.ts +104 -0
  188. package/src/protocol/constants.ts +40 -0
  189. package/src/protocol/contracts.ts +149 -0
  190. package/src/protocol/crc.ts +32 -0
  191. package/src/protocol/cvar.ts +15 -0
  192. package/src/protocol/effects.ts +33 -0
  193. package/src/protocol/entity.ts +304 -0
  194. package/src/protocol/entityEvent.ts +14 -0
  195. package/src/protocol/entityState.ts +28 -0
  196. package/src/protocol/index.ts +19 -0
  197. package/src/protocol/layout.ts +9 -0
  198. package/src/protocol/ops.ts +49 -0
  199. package/src/protocol/player-state.ts +51 -0
  200. package/src/protocol/player.ts +165 -0
  201. package/src/protocol/renderFx.ts +22 -0
  202. package/src/protocol/stats.ts +161 -0
  203. package/src/protocol/tempEntity.ts +69 -0
  204. package/src/protocol/usercmd.ts +63 -0
  205. package/src/protocol/writeUserCmd.ts +30 -0
  206. package/src/replay/index.ts +2 -0
  207. package/src/replay/io.ts +37 -0
  208. package/src/replay/schema.ts +42 -0
  209. 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
+ };