@rapierphysicsplugin/client 1.0.0

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 (83) hide show
  1. package/dist/__tests__/clock-sync.test.d.ts +2 -0
  2. package/dist/__tests__/clock-sync.test.d.ts.map +1 -0
  3. package/dist/__tests__/clock-sync.test.js +63 -0
  4. package/dist/__tests__/clock-sync.test.js.map +1 -0
  5. package/dist/__tests__/interpolator.test.d.ts +2 -0
  6. package/dist/__tests__/interpolator.test.d.ts.map +1 -0
  7. package/dist/__tests__/interpolator.test.js +82 -0
  8. package/dist/__tests__/interpolator.test.js.map +1 -0
  9. package/dist/__tests__/state-reconciler.test.d.ts +2 -0
  10. package/dist/__tests__/state-reconciler.test.d.ts.map +1 -0
  11. package/dist/__tests__/state-reconciler.test.js +86 -0
  12. package/dist/__tests__/state-reconciler.test.js.map +1 -0
  13. package/dist/clock-sync.d.ts +17 -0
  14. package/dist/clock-sync.d.ts.map +1 -0
  15. package/dist/clock-sync.js +63 -0
  16. package/dist/clock-sync.js.map +1 -0
  17. package/dist/index.d.ts +10 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/input-manager.d.ts +18 -0
  22. package/dist/input-manager.d.ts.map +1 -0
  23. package/dist/input-manager.js +62 -0
  24. package/dist/input-manager.js.map +1 -0
  25. package/dist/interpolator.d.ts +35 -0
  26. package/dist/interpolator.d.ts.map +1 -0
  27. package/dist/interpolator.js +198 -0
  28. package/dist/interpolator.js.map +1 -0
  29. package/dist/networked-rapier-plugin.d.ts +82 -0
  30. package/dist/networked-rapier-plugin.d.ts.map +1 -0
  31. package/dist/networked-rapier-plugin.js +698 -0
  32. package/dist/networked-rapier-plugin.js.map +1 -0
  33. package/dist/rapier-body-ops.d.ts +27 -0
  34. package/dist/rapier-body-ops.d.ts.map +1 -0
  35. package/dist/rapier-body-ops.js +208 -0
  36. package/dist/rapier-body-ops.js.map +1 -0
  37. package/dist/rapier-collision-ops.d.ts +6 -0
  38. package/dist/rapier-collision-ops.d.ts.map +1 -0
  39. package/dist/rapier-collision-ops.js +200 -0
  40. package/dist/rapier-collision-ops.js.map +1 -0
  41. package/dist/rapier-constraint-ops.d.ts +29 -0
  42. package/dist/rapier-constraint-ops.d.ts.map +1 -0
  43. package/dist/rapier-constraint-ops.js +286 -0
  44. package/dist/rapier-constraint-ops.js.map +1 -0
  45. package/dist/rapier-plugin.d.ts +145 -0
  46. package/dist/rapier-plugin.d.ts.map +1 -0
  47. package/dist/rapier-plugin.js +263 -0
  48. package/dist/rapier-plugin.js.map +1 -0
  49. package/dist/rapier-shape-ops.d.ts +21 -0
  50. package/dist/rapier-shape-ops.d.ts.map +1 -0
  51. package/dist/rapier-shape-ops.js +314 -0
  52. package/dist/rapier-shape-ops.js.map +1 -0
  53. package/dist/rapier-types.d.ts +58 -0
  54. package/dist/rapier-types.d.ts.map +1 -0
  55. package/dist/rapier-types.js +4 -0
  56. package/dist/rapier-types.js.map +1 -0
  57. package/dist/state-reconciler.d.ts +28 -0
  58. package/dist/state-reconciler.d.ts.map +1 -0
  59. package/dist/state-reconciler.js +119 -0
  60. package/dist/state-reconciler.js.map +1 -0
  61. package/dist/sync-client.d.ts +110 -0
  62. package/dist/sync-client.d.ts.map +1 -0
  63. package/dist/sync-client.js +514 -0
  64. package/dist/sync-client.js.map +1 -0
  65. package/package.json +21 -0
  66. package/src/__tests__/clock-sync.test.ts +72 -0
  67. package/src/__tests__/interpolator.test.ts +98 -0
  68. package/src/__tests__/state-reconciler.test.ts +102 -0
  69. package/src/clock-sync.ts +77 -0
  70. package/src/index.ts +9 -0
  71. package/src/input-manager.ts +72 -0
  72. package/src/interpolator.ts +256 -0
  73. package/src/networked-rapier-plugin.ts +909 -0
  74. package/src/rapier-body-ops.ts +251 -0
  75. package/src/rapier-collision-ops.ts +229 -0
  76. package/src/rapier-constraint-ops.ts +327 -0
  77. package/src/rapier-plugin.ts +364 -0
  78. package/src/rapier-shape-ops.ts +369 -0
  79. package/src/rapier-types.ts +60 -0
  80. package/src/state-reconciler.ts +151 -0
  81. package/src/sync-client.ts +640 -0
  82. package/tsconfig.json +12 -0
  83. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,640 @@
1
+ import type {
2
+ BodyDescriptor,
3
+ BodyState,
4
+ CollisionEventData,
5
+ ConstraintDescriptor,
6
+ RoomSnapshot,
7
+ InputAction,
8
+ ClientMessage,
9
+ ServerMessage,
10
+ ClientInput,
11
+ } from '@rapierphysicsplugin/shared';
12
+ import {
13
+ MessageType,
14
+ encodeMessage,
15
+ decodeServerMessage,
16
+ FIELD_POSITION,
17
+ FIELD_ROTATION,
18
+ FIELD_LIN_VEL,
19
+ FIELD_ANG_VEL,
20
+ OPCODE_MESH_BINARY,
21
+ decodeMeshBinary,
22
+ OPCODE_GEOMETRY_DEF,
23
+ OPCODE_MESH_REF,
24
+ OPCODE_MATERIAL_DEF,
25
+ OPCODE_TEXTURE_DEF,
26
+ decodeGeometryDef,
27
+ decodeMeshRef,
28
+ decodeMaterialDef,
29
+ decodeTextureDef,
30
+ } from '@rapierphysicsplugin/shared';
31
+ import type { MeshBinaryMessage, GeometryDefData, MeshRefData, MaterialDefData, TextureDefData } from '@rapierphysicsplugin/shared';
32
+ import { ClockSyncClient } from './clock-sync.js';
33
+ import { StateReconciler } from './state-reconciler.js';
34
+ import { Interpolator } from './interpolator.js';
35
+ import { InputManager } from './input-manager.js';
36
+
37
+ type StateUpdateCallback = (state: RoomSnapshot) => void;
38
+ type BodyAddedCallback = (body: BodyDescriptor) => void;
39
+ type BodyRemovedCallback = (bodyId: string) => void;
40
+ type SimulationStartedCallback = (snapshot: RoomSnapshot) => void;
41
+ type CollisionEventsCallback = (events: CollisionEventData[]) => void;
42
+ type ConstraintAddedCallback = (constraint: ConstraintDescriptor) => void;
43
+ type ConstraintRemovedCallback = (constraintId: string) => void;
44
+ type MeshBinaryCallback = (msg: MeshBinaryMessage) => void;
45
+ type GeometryDefCallback = (data: GeometryDefData) => void;
46
+ type MeshRefCallback = (data: MeshRefData) => void;
47
+ type MaterialDefCallback = (data: MaterialDefData) => void;
48
+ type TextureDefCallback = (data: TextureDefData) => void;
49
+
50
+ export class PhysicsSyncClient {
51
+ private ws: WebSocket | null = null;
52
+ private clockSync: ClockSyncClient;
53
+ private reconciler: StateReconciler;
54
+ private inputManager: InputManager;
55
+ private clientId: string | null = null;
56
+ private roomId: string | null = null;
57
+
58
+ // Body ID mapping for numeric wire format
59
+ private indexToId: Map<number, string> = new Map();
60
+ private idToIndex: Map<string, number> = new Map();
61
+
62
+ // Full state map — merges partial delta updates into complete body states
63
+ private fullStateMap: Map<string, BodyState> = new Map();
64
+
65
+ private _simulationRunning = false;
66
+ private _bytesSent = 0;
67
+ private _bytesReceived = 0;
68
+ private stateUpdateCallbacks: StateUpdateCallback[] = [];
69
+ private bodyAddedCallbacks: BodyAddedCallback[] = [];
70
+ private bodyRemovedCallbacks: BodyRemovedCallback[] = [];
71
+ private simulationStartedCallbacks: SimulationStartedCallback[] = [];
72
+ private collisionEventsCallbacks: CollisionEventsCallback[] = [];
73
+ private constraintAddedCallbacks: ConstraintAddedCallback[] = [];
74
+ private constraintRemovedCallbacks: ConstraintRemovedCallback[] = [];
75
+ private meshBinaryCallbacks: MeshBinaryCallback[] = [];
76
+ private geometryDefCallbacks: GeometryDefCallback[] = [];
77
+ private meshRefCallbacks: MeshRefCallback[] = [];
78
+ private materialDefCallbacks: MaterialDefCallback[] = [];
79
+ private textureDefCallbacks: TextureDefCallback[] = [];
80
+
81
+ private connectResolve: (() => void) | null = null;
82
+ private connectReject: ((err: Error) => void) | null = null;
83
+ private joinResolve: ((snapshot: RoomSnapshot) => void) | null = null;
84
+ private createResolve: (() => void) | null = null;
85
+
86
+ constructor() {
87
+ this.clockSync = new ClockSyncClient();
88
+ const interpolator = new Interpolator();
89
+ this.reconciler = new StateReconciler(interpolator);
90
+ this.inputManager = new InputManager();
91
+ }
92
+
93
+ connect(url: string): Promise<void> {
94
+ return new Promise((resolve, reject) => {
95
+ this.connectResolve = resolve;
96
+ this.connectReject = reject;
97
+
98
+ this.ws = new WebSocket(url);
99
+ this.ws.binaryType = 'arraybuffer';
100
+
101
+ this.ws.onopen = () => {
102
+ this.clockSync.start((data) => {
103
+ this._bytesSent += data.byteLength;
104
+ this.ws?.send(data);
105
+ });
106
+ this.connectResolve?.();
107
+ this.connectResolve = null;
108
+ this.connectReject = null;
109
+ };
110
+
111
+ this.ws.onmessage = async (event) => {
112
+ let buf: Uint8Array;
113
+ if (event.data instanceof ArrayBuffer) {
114
+ buf = new Uint8Array(event.data);
115
+ } else if (event.data instanceof Blob) {
116
+ buf = new Uint8Array(await event.data.arrayBuffer());
117
+ } else {
118
+ console.warn('[SyncClient] Unexpected message data type:', typeof event.data);
119
+ return;
120
+ }
121
+ this._bytesReceived += buf.byteLength;
122
+ try {
123
+ // Intercept mesh binary messages directly — skip normal decode path
124
+ if (buf[0] === OPCODE_MESH_BINARY) {
125
+ const decoded = decodeMeshBinary(buf);
126
+ const msg: MeshBinaryMessage = { type: MessageType.MESH_BINARY, ...decoded };
127
+ for (const cb of this.meshBinaryCallbacks) {
128
+ cb(msg);
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Intercept geometry def messages
134
+ if (buf[0] === OPCODE_GEOMETRY_DEF) {
135
+ const decoded = decodeGeometryDef(buf);
136
+ for (const cb of this.geometryDefCallbacks) {
137
+ cb(decoded);
138
+ }
139
+ return;
140
+ }
141
+
142
+ // Intercept mesh ref messages
143
+ if (buf[0] === OPCODE_MESH_REF) {
144
+ const decoded = decodeMeshRef(buf);
145
+ for (const cb of this.meshRefCallbacks) {
146
+ cb(decoded);
147
+ }
148
+ return;
149
+ }
150
+
151
+ // Intercept material def messages
152
+ if (buf[0] === OPCODE_MATERIAL_DEF) {
153
+ const decoded = decodeMaterialDef(buf);
154
+ for (const cb of this.materialDefCallbacks) {
155
+ cb(decoded);
156
+ }
157
+ return;
158
+ }
159
+
160
+ // Intercept texture def messages
161
+ if (buf[0] === OPCODE_TEXTURE_DEF) {
162
+ const decoded = decodeTextureDef(buf);
163
+ for (const cb of this.textureDefCallbacks) {
164
+ cb(decoded);
165
+ }
166
+ return;
167
+ }
168
+ const message = decodeServerMessage(buf, this.indexToId);
169
+ this.handleMessage(message);
170
+ } catch (err) {
171
+ console.warn(
172
+ `[SyncClient] Failed to decode server message (${buf.byteLength} bytes, opcode=0x${buf[0]?.toString(16)}):`,
173
+ err,
174
+ );
175
+ }
176
+ };
177
+
178
+ this.ws.onclose = () => {
179
+ this.clockSync.stop();
180
+ this.inputManager.stop();
181
+ };
182
+
183
+ this.ws.onerror = (event) => {
184
+ this.connectReject?.(new Error('WebSocket connection failed'));
185
+ this.connectResolve = null;
186
+ this.connectReject = null;
187
+ };
188
+ });
189
+ }
190
+
191
+ createRoom(roomId: string, initialBodies: BodyDescriptor[], gravity?: { x: number; y: number; z: number }): Promise<void> {
192
+ return new Promise((resolve, reject) => {
193
+ if (!this.ws) {
194
+ reject(new Error('Not connected'));
195
+ return;
196
+ }
197
+
198
+ this.createResolve = resolve;
199
+
200
+ this.send({
201
+ type: MessageType.CREATE_ROOM,
202
+ roomId,
203
+ initialBodies,
204
+ gravity,
205
+ });
206
+ });
207
+ }
208
+
209
+ joinRoom(roomId: string): Promise<RoomSnapshot> {
210
+ return new Promise((resolve, reject) => {
211
+ if (!this.ws) {
212
+ reject(new Error('Not connected'));
213
+ return;
214
+ }
215
+
216
+ this.joinResolve = resolve;
217
+
218
+ this.send({
219
+ type: MessageType.JOIN_ROOM,
220
+ roomId,
221
+ });
222
+ });
223
+ }
224
+
225
+ leaveRoom(): void {
226
+ if (!this.ws || !this.roomId) return;
227
+
228
+ this.send({ type: MessageType.LEAVE_ROOM });
229
+ this.roomId = null;
230
+ this.inputManager.stop();
231
+ this.reconciler.clear();
232
+ this.fullStateMap.clear();
233
+ this.indexToId.clear();
234
+ this.idToIndex.clear();
235
+ }
236
+
237
+ sendInput(actions: InputAction[]): void {
238
+ for (const action of actions) {
239
+ this.inputManager.queueAction(action);
240
+ }
241
+ }
242
+
243
+ addLocalBody(bodyId: string): void {
244
+ this.reconciler.addLocalBody(bodyId);
245
+ }
246
+
247
+ removeLocalBody(bodyId: string): void {
248
+ this.reconciler.removeLocalBody(bodyId);
249
+ }
250
+
251
+ addBody(body: BodyDescriptor): void {
252
+ if (!this.ws) return;
253
+ this.send({ type: MessageType.ADD_BODY, body });
254
+ }
255
+
256
+ removeBody(bodyId: string): void {
257
+ if (!this.ws) return;
258
+ this.send({ type: MessageType.REMOVE_BODY, bodyId });
259
+ }
260
+
261
+ addConstraint(constraint: ConstraintDescriptor): void {
262
+ if (!this.ws) return;
263
+ this.send({ type: MessageType.ADD_CONSTRAINT, constraint });
264
+ }
265
+
266
+ removeConstraint(constraintId: string): void {
267
+ if (!this.ws) return;
268
+ this.send({ type: MessageType.REMOVE_CONSTRAINT, constraintId });
269
+ }
270
+
271
+ onStateUpdate(callback: StateUpdateCallback): void {
272
+ this.stateUpdateCallbacks.push(callback);
273
+ }
274
+
275
+ onBodyAdded(callback: BodyAddedCallback): void {
276
+ this.bodyAddedCallbacks.push(callback);
277
+ }
278
+
279
+ onBodyRemoved(callback: BodyRemovedCallback): void {
280
+ this.bodyRemovedCallbacks.push(callback);
281
+ }
282
+
283
+ onSimulationStarted(callback: SimulationStartedCallback): void {
284
+ this.simulationStartedCallbacks.push(callback);
285
+ }
286
+
287
+ onCollisionEvents(callback: CollisionEventsCallback): void {
288
+ this.collisionEventsCallbacks.push(callback);
289
+ }
290
+
291
+ onConstraintAdded(callback: ConstraintAddedCallback): void {
292
+ this.constraintAddedCallbacks.push(callback);
293
+ }
294
+
295
+ onConstraintRemoved(callback: ConstraintRemovedCallback): void {
296
+ this.constraintRemovedCallbacks.push(callback);
297
+ }
298
+
299
+ onMeshBinary(callback: MeshBinaryCallback): void {
300
+ this.meshBinaryCallbacks.push(callback);
301
+ }
302
+
303
+ onGeometryDef(callback: GeometryDefCallback): void {
304
+ this.geometryDefCallbacks.push(callback);
305
+ }
306
+
307
+ onMeshRef(callback: MeshRefCallback): void {
308
+ this.meshRefCallbacks.push(callback);
309
+ }
310
+
311
+ onMaterialDef(callback: MaterialDefCallback): void {
312
+ this.materialDefCallbacks.push(callback);
313
+ }
314
+
315
+ onTextureDef(callback: TextureDefCallback): void {
316
+ this.textureDefCallbacks.push(callback);
317
+ }
318
+
319
+ /** Send pre-encoded binary mesh data directly over the WebSocket (no msgpackr wrapping). */
320
+ sendMeshBinary(encoded: Uint8Array): void {
321
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
322
+ this._bytesSent += encoded.byteLength;
323
+ this.ws.send(encoded);
324
+ }
325
+ }
326
+
327
+ /** Send pre-encoded GEOMETRY_DEF directly over the WebSocket. */
328
+ sendGeometryDef(encoded: Uint8Array): void {
329
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
330
+ this._bytesSent += encoded.byteLength;
331
+ this.ws.send(encoded);
332
+ }
333
+ }
334
+
335
+ /** Send pre-encoded MESH_REF directly over the WebSocket. */
336
+ sendMeshRef(encoded: Uint8Array): void {
337
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
338
+ this._bytesSent += encoded.byteLength;
339
+ this.ws.send(encoded);
340
+ }
341
+ }
342
+
343
+ /** Send pre-encoded MATERIAL_DEF directly over the WebSocket. */
344
+ sendMaterialDef(encoded: Uint8Array): void {
345
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
346
+ this._bytesSent += encoded.byteLength;
347
+ this.ws.send(encoded);
348
+ }
349
+ }
350
+
351
+ /** Send pre-encoded TEXTURE_DEF directly over the WebSocket. */
352
+ sendTextureDef(encoded: Uint8Array): void {
353
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
354
+ this._bytesSent += encoded.byteLength;
355
+ this.ws.send(encoded);
356
+ }
357
+ }
358
+
359
+ startSimulation(): void {
360
+ if (!this.ws) return;
361
+ this.send({ type: MessageType.START_SIMULATION });
362
+ }
363
+
364
+ get simulationRunning(): boolean {
365
+ return this._simulationRunning;
366
+ }
367
+
368
+ /** Total number of bodies the client knows about (including sleeping/unchanged ones) */
369
+ get totalBodyCount(): number {
370
+ return this.fullStateMap.size;
371
+ }
372
+
373
+ disconnect(): void {
374
+ this.clockSync.stop();
375
+ this.inputManager.stop();
376
+ this.reconciler.clear();
377
+ this.fullStateMap.clear();
378
+ this.indexToId.clear();
379
+ this.idToIndex.clear();
380
+ this.ws?.close();
381
+ this.ws = null;
382
+ this.roomId = null;
383
+ this.clientId = null;
384
+ }
385
+
386
+ private initBodyIdMap(bodyIdMap: Record<string, number>): void {
387
+ this.indexToId.clear();
388
+ this.idToIndex.clear();
389
+ for (const [id, index] of Object.entries(bodyIdMap)) {
390
+ this.indexToId.set(index, id);
391
+ this.idToIndex.set(id, index);
392
+ }
393
+ }
394
+
395
+ private addBodyIdMapping(id: string, index: number): void {
396
+ this.indexToId.set(index, id);
397
+ this.idToIndex.set(id, index);
398
+ }
399
+
400
+ private initFullState(bodies: BodyState[]): void {
401
+ this.fullStateMap.clear();
402
+ for (const body of bodies) {
403
+ this.fullStateMap.set(body.id, { ...body });
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Merge partial delta bodies into the full state map.
409
+ * Returns the merged (complete) body states for the bodies that were in the delta.
410
+ */
411
+ private mergeDelta(bodies: BodyState[]): BodyState[] {
412
+ const merged: BodyState[] = [];
413
+ for (const body of bodies) {
414
+ const existing = this.fullStateMap.get(body.id);
415
+ if (existing) {
416
+ const mask = body.fieldMask;
417
+ if (mask !== undefined) {
418
+ if (mask & FIELD_POSITION) existing.position = body.position;
419
+ if (mask & FIELD_ROTATION) existing.rotation = body.rotation;
420
+ if (mask & FIELD_LIN_VEL) existing.linVel = body.linVel;
421
+ if (mask & FIELD_ANG_VEL) existing.angVel = body.angVel;
422
+ } else {
423
+ // No fieldMask = full update
424
+ existing.position = body.position;
425
+ existing.rotation = body.rotation;
426
+ existing.linVel = body.linVel;
427
+ existing.angVel = body.angVel;
428
+ }
429
+ merged.push(existing);
430
+ } else {
431
+ // New body — add to map
432
+ const newBody: BodyState = { ...body };
433
+ delete newBody.fieldMask;
434
+ this.fullStateMap.set(body.id, newBody);
435
+ merged.push(newBody);
436
+ }
437
+ }
438
+ return merged;
439
+ }
440
+
441
+ private handleMessage(message: ServerMessage): void {
442
+ switch (message.type) {
443
+ case MessageType.CLOCK_SYNC_RESPONSE:
444
+ this.clockSync.handleResponse(message);
445
+ break;
446
+
447
+ case MessageType.ROOM_CREATED:
448
+ this.createResolve?.();
449
+ this.createResolve = null;
450
+ break;
451
+
452
+ case MessageType.ROOM_JOINED:
453
+ this.roomId = message.roomId;
454
+ this.clientId = message.clientId;
455
+ this._simulationRunning = message.simulationRunning;
456
+
457
+ // Initialize body ID mapping
458
+ if (message.bodyIdMap) {
459
+ this.initBodyIdMap(message.bodyIdMap);
460
+ }
461
+
462
+ // Initialize full state from snapshot
463
+ this.initFullState(message.snapshot.bodies);
464
+
465
+ // Start input manager
466
+ this.inputManager.start(
467
+ (input) => this.sendClientInput(input),
468
+ () => this.clockSync.getServerTick()
469
+ );
470
+
471
+ // Notify about existing constraints
472
+ if (message.constraints) {
473
+ for (const c of message.constraints) {
474
+ for (const cb of this.constraintAddedCallbacks) {
475
+ cb(c);
476
+ }
477
+ }
478
+ }
479
+
480
+ // Replay body descriptors for late joiners
481
+ if (message.bodies) {
482
+ for (const b of message.bodies) {
483
+ if (message.bodyIdMap && message.bodyIdMap[b.id] !== undefined) {
484
+ this.addBodyIdMapping(b.id, message.bodyIdMap[b.id]);
485
+ }
486
+ for (const cb of this.bodyAddedCallbacks) {
487
+ cb(b);
488
+ }
489
+ }
490
+ }
491
+
492
+ this.joinResolve?.(message.snapshot);
493
+ this.joinResolve = null;
494
+ break;
495
+
496
+ case MessageType.ROOM_STATE: {
497
+ // Merge partial delta into full state map
498
+ const mergedBodies = this.mergeDelta(message.bodies);
499
+
500
+ const snapshot: RoomSnapshot = {
501
+ tick: message.tick,
502
+ timestamp: message.timestamp,
503
+ bodies: mergedBodies,
504
+ };
505
+
506
+ // Process through reconciler
507
+ this.reconciler.processServerState(snapshot);
508
+
509
+ // Notify listeners
510
+ for (const cb of this.stateUpdateCallbacks) {
511
+ cb(snapshot);
512
+ }
513
+ break;
514
+ }
515
+
516
+ case MessageType.ADD_BODY:
517
+ // Update body ID mapping
518
+ if (message.bodyIndex !== undefined) {
519
+ this.addBodyIdMapping(message.body.id, message.bodyIndex);
520
+ }
521
+ for (const cb of this.bodyAddedCallbacks) {
522
+ cb(message.body);
523
+ }
524
+ break;
525
+
526
+ case MessageType.REMOVE_BODY:
527
+ this.fullStateMap.delete(message.bodyId);
528
+ for (const cb of this.bodyRemovedCallbacks) {
529
+ cb(message.bodyId);
530
+ }
531
+ break;
532
+
533
+ case MessageType.SIMULATION_STARTED: {
534
+ this._simulationRunning = true;
535
+ this.reconciler.clear();
536
+
537
+ // Re-initialize body ID mapping if provided
538
+ const simMsg = message as ServerMessage & { bodyIdMap?: Record<string, number> };
539
+ if (simMsg.bodyIdMap) {
540
+ this.initBodyIdMap(simMsg.bodyIdMap);
541
+ }
542
+
543
+ // Re-initialize full state from fresh snapshot
544
+ this.initFullState(message.snapshot.bodies);
545
+
546
+ // Notify about constraints included in reset
547
+ const simConstraints = (message as ServerMessage & { constraints?: ConstraintDescriptor[] }).constraints;
548
+ if (simConstraints) {
549
+ for (const c of simConstraints) {
550
+ for (const cb of this.constraintAddedCallbacks) {
551
+ cb(c);
552
+ }
553
+ }
554
+ }
555
+
556
+ // Replay body descriptors included in reset
557
+ const simBodies = (message as ServerMessage & { bodies?: BodyDescriptor[] }).bodies;
558
+ if (simBodies) {
559
+ for (const b of simBodies) {
560
+ for (const cb of this.bodyAddedCallbacks) {
561
+ cb(b);
562
+ }
563
+ }
564
+ }
565
+
566
+ const startSnapshot = message.snapshot;
567
+ for (const cb of this.simulationStartedCallbacks) {
568
+ cb(startSnapshot);
569
+ }
570
+ break;
571
+ }
572
+
573
+ case MessageType.COLLISION_EVENTS:
574
+ for (const cb of this.collisionEventsCallbacks) {
575
+ cb(message.events);
576
+ }
577
+ break;
578
+
579
+ case MessageType.ADD_CONSTRAINT:
580
+ for (const cb of this.constraintAddedCallbacks) {
581
+ cb(message.constraint);
582
+ }
583
+ break;
584
+
585
+ case MessageType.REMOVE_CONSTRAINT:
586
+ for (const cb of this.constraintRemovedCallbacks) {
587
+ cb(message.constraintId);
588
+ }
589
+ break;
590
+
591
+ case MessageType.ERROR:
592
+ console.error(`Server error: ${message.message}`);
593
+ break;
594
+ }
595
+ }
596
+
597
+ private sendClientInput(input: ClientInput): void {
598
+ this.reconciler.addPendingInput(input);
599
+ this.send({
600
+ type: MessageType.CLIENT_INPUT,
601
+ input,
602
+ });
603
+ }
604
+
605
+ private send(message: ClientMessage): void {
606
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
607
+ const encoded = encodeMessage(message);
608
+ this._bytesSent += encoded.byteLength;
609
+ this.ws.send(encoded);
610
+ }
611
+ }
612
+
613
+ getReconciler(): StateReconciler {
614
+ return this.reconciler;
615
+ }
616
+
617
+ getClockSync(): ClockSyncClient {
618
+ return this.clockSync;
619
+ }
620
+
621
+ getInputManager(): InputManager {
622
+ return this.inputManager;
623
+ }
624
+
625
+ getClientId(): string | null {
626
+ return this.clientId;
627
+ }
628
+
629
+ getRoomId(): string | null {
630
+ return this.roomId;
631
+ }
632
+
633
+ get bytesSent(): number {
634
+ return this._bytesSent;
635
+ }
636
+
637
+ get bytesReceived(): number {
638
+ return this._bytesReceived;
639
+ }
640
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "lib": ["ES2020", "DOM"]
7
+ },
8
+ "include": ["src/**/*"],
9
+ "references": [
10
+ { "path": "../shared" }
11
+ ]
12
+ }