@rapierphysicsplugin/server 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 (78) hide show
  1. package/dist/__tests__/input-buffer.test.d.ts +2 -0
  2. package/dist/__tests__/input-buffer.test.d.ts.map +1 -0
  3. package/dist/__tests__/input-buffer.test.js +53 -0
  4. package/dist/__tests__/input-buffer.test.js.map +1 -0
  5. package/dist/__tests__/integration.test.d.ts +2 -0
  6. package/dist/__tests__/integration.test.d.ts.map +1 -0
  7. package/dist/__tests__/integration.test.js +182 -0
  8. package/dist/__tests__/integration.test.js.map +1 -0
  9. package/dist/__tests__/physics-world-collisions.test.d.ts +2 -0
  10. package/dist/__tests__/physics-world-collisions.test.d.ts.map +1 -0
  11. package/dist/__tests__/physics-world-collisions.test.js +129 -0
  12. package/dist/__tests__/physics-world-collisions.test.js.map +1 -0
  13. package/dist/__tests__/physics-world.test.d.ts +2 -0
  14. package/dist/__tests__/physics-world.test.d.ts.map +1 -0
  15. package/dist/__tests__/physics-world.test.js +164 -0
  16. package/dist/__tests__/physics-world.test.js.map +1 -0
  17. package/dist/__tests__/room.test.d.ts +2 -0
  18. package/dist/__tests__/room.test.d.ts.map +1 -0
  19. package/dist/__tests__/room.test.js +189 -0
  20. package/dist/__tests__/room.test.js.map +1 -0
  21. package/dist/__tests__/state-manager.test.d.ts +2 -0
  22. package/dist/__tests__/state-manager.test.d.ts.map +1 -0
  23. package/dist/__tests__/state-manager.test.js +122 -0
  24. package/dist/__tests__/state-manager.test.js.map +1 -0
  25. package/dist/client-connection.d.ts +18 -0
  26. package/dist/client-connection.d.ts.map +1 -0
  27. package/dist/client-connection.js +41 -0
  28. package/dist/client-connection.js.map +1 -0
  29. package/dist/clock-sync.d.ts +4 -0
  30. package/dist/clock-sync.d.ts.map +1 -0
  31. package/dist/clock-sync.js +10 -0
  32. package/dist/clock-sync.js.map +1 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +35 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/input-buffer.d.ts +11 -0
  38. package/dist/input-buffer.d.ts.map +1 -0
  39. package/dist/input-buffer.js +40 -0
  40. package/dist/input-buffer.js.map +1 -0
  41. package/dist/physics-world.d.ts +33 -0
  42. package/dist/physics-world.d.ts.map +1 -0
  43. package/dist/physics-world.js +326 -0
  44. package/dist/physics-world.js.map +1 -0
  45. package/dist/room.d.ts +60 -0
  46. package/dist/room.d.ts.map +1 -0
  47. package/dist/room.js +393 -0
  48. package/dist/room.js.map +1 -0
  49. package/dist/server.d.ts +17 -0
  50. package/dist/server.d.ts.map +1 -0
  51. package/dist/server.js +268 -0
  52. package/dist/server.js.map +1 -0
  53. package/dist/simulation-loop.d.ts +14 -0
  54. package/dist/simulation-loop.d.ts.map +1 -0
  55. package/dist/simulation-loop.js +42 -0
  56. package/dist/simulation-loop.js.map +1 -0
  57. package/dist/state-manager.d.ts +20 -0
  58. package/dist/state-manager.d.ts.map +1 -0
  59. package/dist/state-manager.js +120 -0
  60. package/dist/state-manager.js.map +1 -0
  61. package/package.json +24 -0
  62. package/src/__tests__/input-buffer.test.ts +64 -0
  63. package/src/__tests__/integration.test.ts +227 -0
  64. package/src/__tests__/physics-world-collisions.test.ts +155 -0
  65. package/src/__tests__/physics-world.test.ts +197 -0
  66. package/src/__tests__/room.test.ts +232 -0
  67. package/src/__tests__/state-manager.test.ts +152 -0
  68. package/src/client-connection.ts +52 -0
  69. package/src/clock-sync.ts +16 -0
  70. package/src/index.ts +40 -0
  71. package/src/input-buffer.ts +46 -0
  72. package/src/physics-world.ts +400 -0
  73. package/src/room.ts +487 -0
  74. package/src/server.ts +312 -0
  75. package/src/simulation-loop.ts +48 -0
  76. package/src/state-manager.ts +136 -0
  77. package/tsconfig.json +11 -0
  78. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,232 @@
1
+ import { describe, it, expect, beforeAll, afterEach } from 'vitest';
2
+ import RAPIER from '@dimforge/rapier3d-compat';
3
+ import { Room, RoomManager } from '../room.js';
4
+ import { decodeMessage } from '@rapierphysicsplugin/shared';
5
+ import type { BodyDescriptor, ServerMessage } from '@rapierphysicsplugin/shared';
6
+
7
+ describe('Room', () => {
8
+ let rapier: typeof RAPIER;
9
+
10
+ beforeAll(async () => {
11
+ await RAPIER.init();
12
+ rapier = RAPIER;
13
+ });
14
+
15
+ function makeBox(id: string, y: number = 5): BodyDescriptor {
16
+ return {
17
+ id,
18
+ shape: { type: 'box', params: { halfExtents: { x: 0.5, y: 0.5, z: 0.5 } } },
19
+ motionType: 'dynamic',
20
+ position: { x: 0, y, z: 0 },
21
+ rotation: { x: 0, y: 0, z: 0, w: 1 },
22
+ mass: 1.0,
23
+ };
24
+ }
25
+
26
+ it('should create a room with initial bodies', () => {
27
+ const room = new Room('test', rapier);
28
+ room.loadInitialState([makeBox('box1', 5), makeBox('box2', 10)]);
29
+
30
+ const snapshot = room.getSnapshot();
31
+ expect(snapshot.bodies).toHaveLength(2);
32
+ room.destroy();
33
+ });
34
+
35
+ it('should add and remove bodies', () => {
36
+ const room = new Room('test', rapier);
37
+ room.loadInitialState([makeBox('box1')]);
38
+
39
+ const snapshot1 = room.getSnapshot();
40
+ expect(snapshot1.bodies).toHaveLength(1);
41
+
42
+ room.destroy();
43
+ });
44
+
45
+ it('should step physics and advance tick', () => {
46
+ const room = new Room('test', rapier);
47
+ room.loadInitialState([makeBox('box1', 10)]);
48
+
49
+ expect(room.tickNumber).toBe(0);
50
+ room.tick();
51
+ expect(room.tickNumber).toBe(1);
52
+ room.tick();
53
+ expect(room.tickNumber).toBe(2);
54
+
55
+ room.destroy();
56
+ });
57
+
58
+ it('should process buffered inputs during tick', () => {
59
+ const room = new Room('test', rapier);
60
+ room.loadInitialState([makeBox('box1', 5)]);
61
+
62
+ // Simulate a client connection by directly buffering input
63
+ // We need to add a mock client first
64
+ const mockConn = {
65
+ id: 'client1',
66
+ roomId: null as string | null,
67
+ ws: { readyState: 1, OPEN: 1, send: () => {} } as any,
68
+ send: () => {},
69
+ rtt: 0,
70
+ clockOffset: 0,
71
+ lastAcknowledgedTick: 0,
72
+ inputSequence: 0,
73
+ updateClockSync: () => {},
74
+ mapClientTickToServerTick: () => 0,
75
+ };
76
+
77
+ room.addClient(mockConn as any);
78
+ room.bufferInput('client1', {
79
+ tick: 0,
80
+ sequenceNum: 0,
81
+ actions: [
82
+ { type: 'applyImpulse', bodyId: 'box1', data: { impulse: { x: 10, y: 0, z: 0 } } },
83
+ ],
84
+ });
85
+
86
+ room.tick();
87
+
88
+ const state = room.getSnapshot();
89
+ const box1 = state.bodies.find(b => b.id === 'box1')!;
90
+ expect(box1.linVel.x).toBeGreaterThan(0);
91
+
92
+ room.destroy();
93
+ });
94
+
95
+ it('should not auto-start simulation when a client joins', () => {
96
+ const room = new Room('test', rapier);
97
+ room.loadInitialState([makeBox('box1', 5)]);
98
+
99
+ const mockConn = {
100
+ id: 'client1',
101
+ roomId: null as string | null,
102
+ ws: { readyState: 1, OPEN: 1, send: () => {} } as any,
103
+ send: () => {},
104
+ rtt: 0,
105
+ clockOffset: 0,
106
+ lastAcknowledgedTick: 0,
107
+ inputSequence: 0,
108
+ updateClockSync: () => {},
109
+ mapClientTickToServerTick: () => 0,
110
+ };
111
+
112
+ room.addClient(mockConn as any);
113
+ expect(room.isSimulationRunning).toBe(false);
114
+
115
+ room.destroy();
116
+ });
117
+
118
+ it('should start simulation on startSimulation call', () => {
119
+ const room = new Room('test', rapier);
120
+ room.loadInitialState([makeBox('box1', 5)]);
121
+
122
+ const sentMessages: ServerMessage[] = [];
123
+ const mockConn = {
124
+ id: 'client1',
125
+ roomId: null as string | null,
126
+ ws: { readyState: 1, OPEN: 1, send: () => {} } as any,
127
+ send: (msg: Uint8Array) => { sentMessages.push(decodeMessage(msg) as ServerMessage); },
128
+ rtt: 0,
129
+ clockOffset: 0,
130
+ lastAcknowledgedTick: 0,
131
+ inputSequence: 0,
132
+ updateClockSync: () => {},
133
+ mapClientTickToServerTick: () => 0,
134
+ };
135
+
136
+ room.addClient(mockConn as any);
137
+ expect(room.isSimulationRunning).toBe(false);
138
+
139
+ room.startSimulation();
140
+ expect(room.isSimulationRunning).toBe(true);
141
+
142
+ // Should have broadcast SIMULATION_STARTED
143
+ const startedMsg = sentMessages.find(m => m.type === 'simulation_started');
144
+ expect(startedMsg).toBeDefined();
145
+
146
+ room.destroy();
147
+ });
148
+
149
+ it('should reset physics on startSimulation when already running', () => {
150
+ const room = new Room('test', rapier);
151
+ room.loadInitialState([makeBox('box1', 10)]);
152
+
153
+ const mockConn = {
154
+ id: 'client1',
155
+ roomId: null as string | null,
156
+ ws: { readyState: 1, OPEN: 1, send: () => {} } as any,
157
+ send: () => {},
158
+ rtt: 0,
159
+ clockOffset: 0,
160
+ lastAcknowledgedTick: 0,
161
+ inputSequence: 0,
162
+ updateClockSync: () => {},
163
+ mapClientTickToServerTick: () => 0,
164
+ };
165
+
166
+ room.addClient(mockConn as any);
167
+ room.startSimulation();
168
+
169
+ // Run some ticks so physics changes
170
+ for (let i = 0; i < 60; i++) {
171
+ room.tick();
172
+ }
173
+ expect(room.tickNumber).toBeGreaterThan(0);
174
+ const stateBefore = room.getSnapshot();
175
+ const box = stateBefore.bodies.find(b => b.id === 'box1')!;
176
+ expect(box.position.y).not.toBeCloseTo(10);
177
+
178
+ // Reset
179
+ room.startSimulation();
180
+ expect(room.tickNumber).toBe(0);
181
+ const stateAfter = room.getSnapshot();
182
+ const boxAfter = stateAfter.bodies.find(b => b.id === 'box1')!;
183
+ expect(boxAfter.position.y).toBeCloseTo(10);
184
+
185
+ room.destroy();
186
+ });
187
+ });
188
+
189
+ describe('RoomManager', () => {
190
+ let rapier: typeof RAPIER;
191
+
192
+ beforeAll(async () => {
193
+ await RAPIER.init();
194
+ rapier = RAPIER;
195
+ });
196
+
197
+ it('should create and retrieve rooms', () => {
198
+ const manager = new RoomManager(rapier);
199
+ manager.createRoom('room1');
200
+
201
+ expect(manager.getRoom('room1')).toBeDefined();
202
+ expect(manager.roomCount).toBe(1);
203
+
204
+ manager.destroyRoom('room1');
205
+ });
206
+
207
+ it('should throw on duplicate room creation', () => {
208
+ const manager = new RoomManager(rapier);
209
+ manager.createRoom('room1');
210
+ expect(() => manager.createRoom('room1')).toThrow('already exists');
211
+ manager.destroyRoom('room1');
212
+ });
213
+
214
+ it('should destroy rooms', () => {
215
+ const manager = new RoomManager(rapier);
216
+ manager.createRoom('room1');
217
+ manager.destroyRoom('room1');
218
+ expect(manager.getRoom('room1')).toBeUndefined();
219
+ expect(manager.roomCount).toBe(0);
220
+ });
221
+
222
+ it('should list all room ids', () => {
223
+ const manager = new RoomManager(rapier);
224
+ manager.createRoom('room1');
225
+ manager.createRoom('room2');
226
+
227
+ expect(manager.getAllRoomIds().sort()).toEqual(['room1', 'room2']);
228
+
229
+ manager.destroyRoom('room1');
230
+ manager.destroyRoom('room2');
231
+ });
232
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
2
+ import RAPIER from '@dimforge/rapier3d-compat';
3
+ import { PhysicsWorld } from '../physics-world.js';
4
+ import { StateManager } from '../state-manager.js';
5
+ import type { BodyDescriptor } from '@rapierphysicsplugin/shared';
6
+ import { FIELD_POSITION, FIELD_ROTATION, FIELD_LIN_VEL, FIELD_ANG_VEL, FIELD_ALL } from '@rapierphysicsplugin/shared';
7
+
8
+ describe('StateManager', () => {
9
+ let world: PhysicsWorld;
10
+ let stateManager: StateManager;
11
+
12
+ beforeAll(async () => {
13
+ await RAPIER.init();
14
+ });
15
+
16
+ beforeEach(() => {
17
+ world = new PhysicsWorld(RAPIER);
18
+ stateManager = new StateManager();
19
+ });
20
+
21
+ afterEach(() => {
22
+ world.destroy();
23
+ });
24
+
25
+ function makeBox(id: string, y: number = 5): BodyDescriptor {
26
+ return {
27
+ id,
28
+ shape: { type: 'box', params: { halfExtents: { x: 0.5, y: 0.5, z: 0.5 } } },
29
+ motionType: 'dynamic',
30
+ position: { x: 0, y, z: 0 },
31
+ rotation: { x: 0, y: 0, z: 0, w: 1 },
32
+ mass: 1.0,
33
+ };
34
+ }
35
+
36
+ function makeStaticBox(id: string, y: number = 0): BodyDescriptor {
37
+ return {
38
+ id,
39
+ shape: { type: 'box', params: { halfExtents: { x: 5, y: 0.5, z: 5 } } },
40
+ motionType: 'static',
41
+ position: { x: 0, y, z: 0 },
42
+ rotation: { x: 0, y: 0, z: 0, w: 1 },
43
+ };
44
+ }
45
+
46
+ it('should create a full snapshot', () => {
47
+ world.addBody(makeBox('box1', 5));
48
+ world.addBody(makeBox('box2', 10));
49
+
50
+ const snapshot = stateManager.createSnapshot(world, 42);
51
+ expect(snapshot.tick).toBe(42);
52
+ expect(snapshot.bodies).toHaveLength(2);
53
+ expect(snapshot.timestamp).toBeGreaterThan(0);
54
+ });
55
+
56
+ it('should create delta with all bodies on first call (fieldMask = FIELD_ALL)', () => {
57
+ world.addBody(makeBox('box1', 5));
58
+ const delta = stateManager.createDelta(world, 1);
59
+ expect(delta.bodies).toHaveLength(1);
60
+ expect(delta.bodies[0].fieldMask).toBe(FIELD_ALL);
61
+ expect(delta.isDelta).toBe(true);
62
+ });
63
+
64
+ it('should create empty delta when nothing changed', () => {
65
+ world.addBody(makeBox('box1', 5));
66
+
67
+ // First delta includes everything
68
+ stateManager.createDelta(world, 1);
69
+
70
+ // No stepping, nothing changed — delta should be empty
71
+ const delta2 = stateManager.createDelta(world, 2);
72
+ expect(delta2.bodies).toHaveLength(0);
73
+ });
74
+
75
+ it('should detect changes after physics step', () => {
76
+ world.addBody(makeBox('box1', 5));
77
+
78
+ // First delta
79
+ stateManager.createDelta(world, 1);
80
+
81
+ // Step physics — body should fall
82
+ for (let i = 0; i < 10; i++) {
83
+ world.step();
84
+ }
85
+
86
+ const delta2 = stateManager.createDelta(world, 2);
87
+ expect(delta2.bodies).toHaveLength(1);
88
+ expect(delta2.bodies[0].id).toBe('box1');
89
+ });
90
+
91
+ it('should set per-field mask for position+linVel change (falling body)', () => {
92
+ world.addBody(makeBox('box1', 5));
93
+
94
+ // First delta
95
+ stateManager.createDelta(world, 1);
96
+
97
+ // Step physics — body falls (position and linVel change, rotation stays identity)
98
+ for (let i = 0; i < 10; i++) {
99
+ world.step();
100
+ }
101
+
102
+ const delta2 = stateManager.createDelta(world, 2);
103
+ expect(delta2.bodies).toHaveLength(1);
104
+ const mask = delta2.bodies[0].fieldMask!;
105
+ // A falling body should have position and linVel changed
106
+ expect(mask & FIELD_POSITION).toBeTruthy();
107
+ expect(mask & FIELD_LIN_VEL).toBeTruthy();
108
+ });
109
+
110
+ it('should assign body indices', () => {
111
+ world.addBody(makeBox('box1', 5));
112
+ world.addBody(makeBox('box2', 10));
113
+
114
+ stateManager.createSnapshot(world, 0);
115
+
116
+ expect(stateManager.getBodyIndex('box1')).toBe(0);
117
+ expect(stateManager.getBodyIndex('box2')).toBe(1);
118
+ });
119
+
120
+ it('should produce ID mapping record', () => {
121
+ world.addBody(makeBox('box1', 5));
122
+ world.addBody(makeBox('box2', 10));
123
+ stateManager.createSnapshot(world, 0);
124
+
125
+ const record = stateManager.getIdToIndexRecord();
126
+ expect(record).toEqual({ box1: 0, box2: 1 });
127
+ });
128
+
129
+ it('should keep index after body removal (indices are never reused)', () => {
130
+ world.addBody(makeBox('box1', 5));
131
+ world.addBody(makeBox('box2', 10));
132
+ stateManager.createSnapshot(world, 0);
133
+
134
+ stateManager.removeBody('box1');
135
+ // box1 index is still in the map (not reused)
136
+ expect(stateManager.getBodyIndex('box1')).toBe(0);
137
+
138
+ // Adding a new body gets the next index
139
+ world.addBody(makeBox('box3', 15));
140
+ stateManager.ensureBodyIndex('box3');
141
+ expect(stateManager.getBodyIndex('box3')).toBe(2);
142
+ });
143
+
144
+ it('should clear all state on clear()', () => {
145
+ world.addBody(makeBox('box1', 5));
146
+ stateManager.createSnapshot(world, 0);
147
+
148
+ stateManager.clear();
149
+ expect(stateManager.getBodyIndex('box1')).toBeUndefined();
150
+ expect(stateManager.getIdToIndexRecord()).toEqual({});
151
+ });
152
+ });
@@ -0,0 +1,52 @@
1
+ import type { WebSocket } from 'ws';
2
+
3
+ export class ClientConnection {
4
+ readonly id: string;
5
+ readonly ws: WebSocket;
6
+ roomId: string | null = null;
7
+ rtt = 0;
8
+ clockOffset = 0;
9
+ lastAcknowledgedTick = 0;
10
+ inputSequence = 0;
11
+
12
+ private rttSamples: number[] = [];
13
+ private offsetSamples: number[] = [];
14
+ private static readonly MAX_SAMPLES = 10;
15
+
16
+ constructor(id: string, ws: WebSocket) {
17
+ this.id = id;
18
+ this.ws = ws;
19
+ }
20
+
21
+ send(data: Uint8Array): void {
22
+ if (this.ws.readyState === this.ws.OPEN) {
23
+ this.ws.send(data);
24
+ }
25
+ }
26
+
27
+ updateClockSync(clientTimestamp: number, serverTimestamp: number): void {
28
+ const now = Date.now();
29
+ const rtt = now - clientTimestamp;
30
+
31
+ this.rttSamples.push(rtt);
32
+ if (this.rttSamples.length > ClientConnection.MAX_SAMPLES) {
33
+ this.rttSamples.shift();
34
+ }
35
+
36
+ const offset = serverTimestamp - clientTimestamp - rtt / 2;
37
+ this.offsetSamples.push(offset);
38
+ if (this.offsetSamples.length > ClientConnection.MAX_SAMPLES) {
39
+ this.offsetSamples.shift();
40
+ }
41
+
42
+ // Rolling average
43
+ this.rtt = this.rttSamples.reduce((a, b) => a + b, 0) / this.rttSamples.length;
44
+ this.clockOffset = this.offsetSamples.reduce((a, b) => a + b, 0) / this.offsetSamples.length;
45
+ }
46
+
47
+ mapClientTickToServerTick(clientTick: number, serverTickRate: number): number {
48
+ // Use clock offset to map client tick to server tick
49
+ const offsetInTicks = Math.round((this.clockOffset / 1000) * serverTickRate);
50
+ return clientTick + offsetInTicks;
51
+ }
52
+ }
@@ -0,0 +1,16 @@
1
+ import type { ClockSyncRequestMessage, ClockSyncResponseMessage } from '@rapierphysicsplugin/shared';
2
+ import { MessageType, encodeMessage } from '@rapierphysicsplugin/shared';
3
+ import type { ClientConnection } from './client-connection.js';
4
+
5
+ export function handleClockSyncRequest(
6
+ conn: ClientConnection,
7
+ message: ClockSyncRequestMessage
8
+ ): void {
9
+ const response: ClockSyncResponseMessage = {
10
+ type: MessageType.CLOCK_SYNC_RESPONSE,
11
+ clientTimestamp: message.clientTimestamp,
12
+ serverTimestamp: Date.now(),
13
+ };
14
+
15
+ conn.send(encodeMessage(response));
16
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { DEFAULT_PORT, ComputeBackend, loadRapier } from '@rapierphysicsplugin/shared';
2
+ import { PhysicsServer } from './server.js';
3
+
4
+ export { PhysicsServer } from './server.js';
5
+ export { PhysicsWorld } from './physics-world.js';
6
+ export { Room, RoomManager } from './room.js';
7
+ export { ClientConnection } from './client-connection.js';
8
+ export { SimulationLoop } from './simulation-loop.js';
9
+ export { StateManager } from './state-manager.js';
10
+ export { InputBuffer } from './input-buffer.js';
11
+
12
+ async function main(): Promise<void> {
13
+ const backend = (process.env.PHYSICS_BACKEND as ComputeBackend) ?? ComputeBackend.WASM_SIMD;
14
+ console.log(`Initializing Rapier WASM (${backend})...`);
15
+ const RAPIER = await loadRapier({ backend });
16
+ console.log('Rapier initialized.');
17
+
18
+ const port = parseInt(process.env.PORT || String(DEFAULT_PORT), 10);
19
+ const server = new PhysicsServer(RAPIER);
20
+ await server.start(port);
21
+
22
+ // Graceful shutdown
23
+ process.on('SIGINT', () => {
24
+ console.log('\nShutting down...');
25
+ server.stop();
26
+ process.exit(0);
27
+ });
28
+
29
+ process.on('SIGTERM', () => {
30
+ console.log('\nShutting down...');
31
+ server.stop();
32
+ process.exit(0);
33
+ });
34
+ }
35
+
36
+ // Only run main when executed directly
37
+ const isMainModule = process.argv[1]?.endsWith('index.ts') || process.argv[1]?.endsWith('index.js');
38
+ if (isMainModule) {
39
+ main().catch(console.error);
40
+ }
@@ -0,0 +1,46 @@
1
+ import type { ClientInput } from '@rapierphysicsplugin/shared';
2
+ import { MAX_INPUT_BUFFER } from '@rapierphysicsplugin/shared';
3
+
4
+ export class InputBuffer {
5
+ private buffer: Map<number, ClientInput[]> = new Map();
6
+ private oldestTick = 0;
7
+
8
+ addInput(input: ClientInput, serverTick: number): void {
9
+ // Map client tick to server tick using provided mapping
10
+ const targetTick = serverTick;
11
+
12
+ if (!this.buffer.has(targetTick)) {
13
+ this.buffer.set(targetTick, []);
14
+ }
15
+ this.buffer.get(targetTick)!.push(input);
16
+
17
+ // Clean up old entries
18
+ this.pruneOldEntries(targetTick);
19
+ }
20
+
21
+ getInputsForTick(tick: number): ClientInput[] {
22
+ const inputs = this.buffer.get(tick);
23
+ if (inputs) {
24
+ this.buffer.delete(tick);
25
+ return inputs;
26
+ }
27
+ return [];
28
+ }
29
+
30
+ private pruneOldEntries(currentTick: number): void {
31
+ const cutoff = currentTick - MAX_INPUT_BUFFER;
32
+ for (const tick of this.buffer.keys()) {
33
+ if (tick < cutoff) {
34
+ this.buffer.delete(tick);
35
+ }
36
+ }
37
+ }
38
+
39
+ clear(): void {
40
+ this.buffer.clear();
41
+ }
42
+
43
+ get size(): number {
44
+ return this.buffer.size;
45
+ }
46
+ }