@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.
- package/dist/__tests__/input-buffer.test.d.ts +2 -0
- package/dist/__tests__/input-buffer.test.d.ts.map +1 -0
- package/dist/__tests__/input-buffer.test.js +53 -0
- package/dist/__tests__/input-buffer.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +182 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/physics-world-collisions.test.d.ts +2 -0
- package/dist/__tests__/physics-world-collisions.test.d.ts.map +1 -0
- package/dist/__tests__/physics-world-collisions.test.js +129 -0
- package/dist/__tests__/physics-world-collisions.test.js.map +1 -0
- package/dist/__tests__/physics-world.test.d.ts +2 -0
- package/dist/__tests__/physics-world.test.d.ts.map +1 -0
- package/dist/__tests__/physics-world.test.js +164 -0
- package/dist/__tests__/physics-world.test.js.map +1 -0
- package/dist/__tests__/room.test.d.ts +2 -0
- package/dist/__tests__/room.test.d.ts.map +1 -0
- package/dist/__tests__/room.test.js +189 -0
- package/dist/__tests__/room.test.js.map +1 -0
- package/dist/__tests__/state-manager.test.d.ts +2 -0
- package/dist/__tests__/state-manager.test.d.ts.map +1 -0
- package/dist/__tests__/state-manager.test.js +122 -0
- package/dist/__tests__/state-manager.test.js.map +1 -0
- package/dist/client-connection.d.ts +18 -0
- package/dist/client-connection.d.ts.map +1 -0
- package/dist/client-connection.js +41 -0
- package/dist/client-connection.js.map +1 -0
- package/dist/clock-sync.d.ts +4 -0
- package/dist/clock-sync.d.ts.map +1 -0
- package/dist/clock-sync.js +10 -0
- package/dist/clock-sync.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/input-buffer.d.ts +11 -0
- package/dist/input-buffer.d.ts.map +1 -0
- package/dist/input-buffer.js +40 -0
- package/dist/input-buffer.js.map +1 -0
- package/dist/physics-world.d.ts +33 -0
- package/dist/physics-world.d.ts.map +1 -0
- package/dist/physics-world.js +326 -0
- package/dist/physics-world.js.map +1 -0
- package/dist/room.d.ts +60 -0
- package/dist/room.d.ts.map +1 -0
- package/dist/room.js +393 -0
- package/dist/room.js.map +1 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +268 -0
- package/dist/server.js.map +1 -0
- package/dist/simulation-loop.d.ts +14 -0
- package/dist/simulation-loop.d.ts.map +1 -0
- package/dist/simulation-loop.js +42 -0
- package/dist/simulation-loop.js.map +1 -0
- package/dist/state-manager.d.ts +20 -0
- package/dist/state-manager.d.ts.map +1 -0
- package/dist/state-manager.js +120 -0
- package/dist/state-manager.js.map +1 -0
- package/package.json +24 -0
- package/src/__tests__/input-buffer.test.ts +64 -0
- package/src/__tests__/integration.test.ts +227 -0
- package/src/__tests__/physics-world-collisions.test.ts +155 -0
- package/src/__tests__/physics-world.test.ts +197 -0
- package/src/__tests__/room.test.ts +232 -0
- package/src/__tests__/state-manager.test.ts +152 -0
- package/src/client-connection.ts +52 -0
- package/src/clock-sync.ts +16 -0
- package/src/index.ts +40 -0
- package/src/input-buffer.ts +46 -0
- package/src/physics-world.ts +400 -0
- package/src/room.ts +487 -0
- package/src/server.ts +312 -0
- package/src/simulation-loop.ts +48 -0
- package/src/state-manager.ts +136 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/room.ts
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import type RAPIER from '@dimforge/rapier3d-compat';
|
|
2
|
+
import type {
|
|
3
|
+
BodyDescriptor,
|
|
4
|
+
CollisionEventData,
|
|
5
|
+
ConstraintDescriptor,
|
|
6
|
+
RoomSnapshot,
|
|
7
|
+
Vec3,
|
|
8
|
+
} from '@rapierphysicsplugin/shared';
|
|
9
|
+
import {
|
|
10
|
+
BROADCAST_INTERVAL,
|
|
11
|
+
MessageType,
|
|
12
|
+
encodeMessage,
|
|
13
|
+
encodeRoomState,
|
|
14
|
+
readBodyIdFromMeshBinary,
|
|
15
|
+
readHashFromGeometryDef,
|
|
16
|
+
readBodyIdFromMeshRef,
|
|
17
|
+
readGeometryHashFromMeshRef,
|
|
18
|
+
readMaterialHashFromMeshRef,
|
|
19
|
+
readHashFromTextureDef,
|
|
20
|
+
readHashFromMaterialDef,
|
|
21
|
+
} from '@rapierphysicsplugin/shared';
|
|
22
|
+
import { PhysicsWorld } from './physics-world.js';
|
|
23
|
+
import { SimulationLoop } from './simulation-loop.js';
|
|
24
|
+
import { StateManager } from './state-manager.js';
|
|
25
|
+
import { InputBuffer } from './input-buffer.js';
|
|
26
|
+
import type { ClientConnection } from './client-connection.js';
|
|
27
|
+
|
|
28
|
+
export class Room {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly physicsWorld: PhysicsWorld;
|
|
31
|
+
private simulationLoop: SimulationLoop;
|
|
32
|
+
private stateManager: StateManager;
|
|
33
|
+
private clients: Map<string, ClientConnection> = new Map();
|
|
34
|
+
private inputBuffers: Map<string, InputBuffer> = new Map();
|
|
35
|
+
private initialBodies: BodyDescriptor[] = [];
|
|
36
|
+
private initialConstraints: ConstraintDescriptor[] = [];
|
|
37
|
+
private activeConstraints: Map<string, ConstraintDescriptor> = new Map();
|
|
38
|
+
private activeBodies: Map<string, BodyDescriptor> = new Map();
|
|
39
|
+
private meshBinaryStore: Map<string, Uint8Array> = new Map();
|
|
40
|
+
private geometryStore: Map<string, Uint8Array> = new Map();
|
|
41
|
+
private meshRefStore: Map<string, Uint8Array> = new Map();
|
|
42
|
+
private geometryRefCount: Map<string, Set<string>> = new Map();
|
|
43
|
+
private materialStore: Map<string, Uint8Array> = new Map();
|
|
44
|
+
private textureStore: Map<string, Uint8Array> = new Map();
|
|
45
|
+
private materialRefCount: Map<string, Set<string>> = new Map();
|
|
46
|
+
private currentTick = 0;
|
|
47
|
+
private ticksSinceLastBroadcast = 0;
|
|
48
|
+
private pendingCollisionEvents: CollisionEventData[] = [];
|
|
49
|
+
|
|
50
|
+
constructor(id: string, rapier: typeof RAPIER, gravity?: Vec3) {
|
|
51
|
+
this.id = id;
|
|
52
|
+
this.physicsWorld = new PhysicsWorld(rapier, gravity);
|
|
53
|
+
this.simulationLoop = new SimulationLoop(this);
|
|
54
|
+
this.stateManager = new StateManager();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loadInitialState(bodies: BodyDescriptor[], constraints?: ConstraintDescriptor[]): void {
|
|
58
|
+
this.initialBodies = bodies;
|
|
59
|
+
this.initialConstraints = constraints ?? [];
|
|
60
|
+
this.physicsWorld.loadState(bodies);
|
|
61
|
+
for (const b of bodies) {
|
|
62
|
+
this.activeBodies.set(b.id, b);
|
|
63
|
+
}
|
|
64
|
+
for (const c of this.initialConstraints) {
|
|
65
|
+
this.physicsWorld.addConstraint(c);
|
|
66
|
+
this.activeConstraints.set(c.id, c);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addClient(conn: ClientConnection): void {
|
|
71
|
+
this.clients.set(conn.id, conn);
|
|
72
|
+
this.inputBuffers.set(conn.id, new InputBuffer());
|
|
73
|
+
conn.roomId = this.id;
|
|
74
|
+
|
|
75
|
+
// Send full state snapshot to the joining client, including body ID mapping and constraints
|
|
76
|
+
const snapshot = this.stateManager.createSnapshot(this.physicsWorld, this.currentTick);
|
|
77
|
+
const constraints = Array.from(this.activeConstraints.values());
|
|
78
|
+
const bodies = Array.from(this.activeBodies.values());
|
|
79
|
+
conn.send(encodeMessage({
|
|
80
|
+
type: MessageType.ROOM_JOINED,
|
|
81
|
+
roomId: this.id,
|
|
82
|
+
snapshot,
|
|
83
|
+
clientId: conn.id,
|
|
84
|
+
simulationRunning: this.simulationLoop.isRunning,
|
|
85
|
+
bodyIdMap: this.stateManager.getIdToIndexRecord(),
|
|
86
|
+
constraints: constraints.length > 0 ? constraints : undefined,
|
|
87
|
+
bodies: bodies.length > 0 ? bodies : undefined,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// Send stored mesh binaries to the late joiner
|
|
91
|
+
for (const [, meshData] of this.meshBinaryStore) {
|
|
92
|
+
conn.send(meshData);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Late-joiner replay order: textures → materials → geometry defs → mesh refs
|
|
96
|
+
for (const [, texData] of this.textureStore) {
|
|
97
|
+
conn.send(texData);
|
|
98
|
+
}
|
|
99
|
+
for (const [, matData] of this.materialStore) {
|
|
100
|
+
conn.send(matData);
|
|
101
|
+
}
|
|
102
|
+
for (const [, geomData] of this.geometryStore) {
|
|
103
|
+
conn.send(geomData);
|
|
104
|
+
}
|
|
105
|
+
for (const [, refData] of this.meshRefStore) {
|
|
106
|
+
conn.send(refData);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
removeClient(conn: ClientConnection): void {
|
|
111
|
+
this.clients.delete(conn.id);
|
|
112
|
+
this.inputBuffers.delete(conn.id);
|
|
113
|
+
conn.roomId = null;
|
|
114
|
+
|
|
115
|
+
// Stop simulation if no clients remain
|
|
116
|
+
if (this.clients.size === 0) {
|
|
117
|
+
this.simulationLoop.stop();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
addBody(descriptor: BodyDescriptor): string {
|
|
122
|
+
const id = this.physicsWorld.addBody(descriptor);
|
|
123
|
+
const bodyIndex = this.stateManager.ensureBodyIndex(id);
|
|
124
|
+
// Store descriptor without meshData (mesh geometry arrives via binary channel)
|
|
125
|
+
const stored = { ...descriptor };
|
|
126
|
+
delete (stored as Record<string, unknown>).meshData;
|
|
127
|
+
this.activeBodies.set(id, stored);
|
|
128
|
+
|
|
129
|
+
// Notify all clients (include the numeric index for the new body)
|
|
130
|
+
this.broadcast(encodeMessage({
|
|
131
|
+
type: MessageType.ADD_BODY,
|
|
132
|
+
body: descriptor,
|
|
133
|
+
bodyIndex,
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
return id;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
removeBody(bodyId: string): void {
|
|
140
|
+
this.physicsWorld.removeBody(bodyId);
|
|
141
|
+
this.stateManager.removeBody(bodyId);
|
|
142
|
+
this.activeBodies.delete(bodyId);
|
|
143
|
+
this.meshBinaryStore.delete(bodyId);
|
|
144
|
+
|
|
145
|
+
// Clean up geometry + material registry refs (keep defs for reuse)
|
|
146
|
+
const refData = this.meshRefStore.get(bodyId);
|
|
147
|
+
if (refData) {
|
|
148
|
+
const geoHash = readGeometryHashFromMeshRef(refData);
|
|
149
|
+
const geoRefs = this.geometryRefCount.get(geoHash);
|
|
150
|
+
if (geoRefs) {
|
|
151
|
+
geoRefs.delete(bodyId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const matHash = readMaterialHashFromMeshRef(refData);
|
|
155
|
+
const matRefs = this.materialRefCount.get(matHash);
|
|
156
|
+
if (matRefs) {
|
|
157
|
+
matRefs.delete(bodyId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.meshRefStore.delete(bodyId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Notify all clients
|
|
164
|
+
this.broadcast(encodeMessage({
|
|
165
|
+
type: MessageType.REMOVE_BODY,
|
|
166
|
+
bodyId,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
addConstraint(descriptor: ConstraintDescriptor): string {
|
|
171
|
+
const id = this.physicsWorld.addConstraint(descriptor);
|
|
172
|
+
this.activeConstraints.set(id, descriptor);
|
|
173
|
+
|
|
174
|
+
// Broadcast to all clients
|
|
175
|
+
this.broadcast(encodeMessage({
|
|
176
|
+
type: MessageType.ADD_CONSTRAINT,
|
|
177
|
+
constraint: descriptor,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
removeConstraint(constraintId: string): void {
|
|
184
|
+
this.physicsWorld.removeConstraint(constraintId);
|
|
185
|
+
this.activeConstraints.delete(constraintId);
|
|
186
|
+
|
|
187
|
+
// Broadcast to all clients
|
|
188
|
+
this.broadcast(encodeMessage({
|
|
189
|
+
type: MessageType.REMOVE_CONSTRAINT,
|
|
190
|
+
constraintId,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
relayMeshBinary(senderId: string, data: Uint8Array): void {
|
|
195
|
+
// Extract bodyId from the header to store for late joiners
|
|
196
|
+
const bodyId = readBodyIdFromMeshBinary(data);
|
|
197
|
+
// Store a copy for late joiners
|
|
198
|
+
this.meshBinaryStore.set(bodyId, new Uint8Array(data));
|
|
199
|
+
|
|
200
|
+
// Broadcast raw bytes to all clients except sender
|
|
201
|
+
for (const [clientId, client] of this.clients) {
|
|
202
|
+
if (clientId !== senderId) {
|
|
203
|
+
client.send(data);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
relayTextureDef(senderId: string, data: Uint8Array): void {
|
|
209
|
+
const hash = readHashFromTextureDef(data);
|
|
210
|
+
|
|
211
|
+
// Store if new
|
|
212
|
+
if (!this.textureStore.has(hash)) {
|
|
213
|
+
this.textureStore.set(hash, new Uint8Array(data));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Relay to all clients except sender
|
|
217
|
+
for (const [clientId, client] of this.clients) {
|
|
218
|
+
if (clientId !== senderId) {
|
|
219
|
+
client.send(data);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
relayMaterialDef(senderId: string, data: Uint8Array): void {
|
|
225
|
+
const hash = readHashFromMaterialDef(data);
|
|
226
|
+
|
|
227
|
+
// Store if new
|
|
228
|
+
if (!this.materialStore.has(hash)) {
|
|
229
|
+
this.materialStore.set(hash, new Uint8Array(data));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Relay to all clients except sender
|
|
233
|
+
for (const [clientId, client] of this.clients) {
|
|
234
|
+
if (clientId !== senderId) {
|
|
235
|
+
client.send(data);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
relayGeometryDef(senderId: string, data: Uint8Array): void {
|
|
241
|
+
const hash = readHashFromGeometryDef(data);
|
|
242
|
+
|
|
243
|
+
// Store if new (skip if already stored — another client already sent this geometry)
|
|
244
|
+
if (!this.geometryStore.has(hash)) {
|
|
245
|
+
this.geometryStore.set(hash, new Uint8Array(data));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Relay to all clients except sender
|
|
249
|
+
for (const [clientId, client] of this.clients) {
|
|
250
|
+
if (clientId !== senderId) {
|
|
251
|
+
client.send(data);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
relayMeshRef(senderId: string, data: Uint8Array): void {
|
|
257
|
+
const bodyId = readBodyIdFromMeshRef(data);
|
|
258
|
+
const geoHash = readGeometryHashFromMeshRef(data);
|
|
259
|
+
const matHash = readMaterialHashFromMeshRef(data);
|
|
260
|
+
|
|
261
|
+
// Always store + relay
|
|
262
|
+
this.meshRefStore.set(bodyId, new Uint8Array(data));
|
|
263
|
+
|
|
264
|
+
// Track geometry ref count
|
|
265
|
+
let geoRefs = this.geometryRefCount.get(geoHash);
|
|
266
|
+
if (!geoRefs) {
|
|
267
|
+
geoRefs = new Set();
|
|
268
|
+
this.geometryRefCount.set(geoHash, geoRefs);
|
|
269
|
+
}
|
|
270
|
+
geoRefs.add(bodyId);
|
|
271
|
+
|
|
272
|
+
// Track material ref count
|
|
273
|
+
let matRefs = this.materialRefCount.get(matHash);
|
|
274
|
+
if (!matRefs) {
|
|
275
|
+
matRefs = new Set();
|
|
276
|
+
this.materialRefCount.set(matHash, matRefs);
|
|
277
|
+
}
|
|
278
|
+
matRefs.add(bodyId);
|
|
279
|
+
|
|
280
|
+
// Relay to all clients except sender
|
|
281
|
+
for (const [clientId, client] of this.clients) {
|
|
282
|
+
if (clientId !== senderId) {
|
|
283
|
+
client.send(data);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
bufferInput(clientId: string, input: import('@rapierphysicsplugin/shared').ClientInput): void {
|
|
289
|
+
const buffer = this.inputBuffers.get(clientId);
|
|
290
|
+
if (buffer) {
|
|
291
|
+
// Map client tick to the current server tick (best-effort for now)
|
|
292
|
+
buffer.addInput(input, this.currentTick);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
tick(): void {
|
|
297
|
+
// 1. Process buffered inputs for this tick
|
|
298
|
+
for (const [, buffer] of this.inputBuffers) {
|
|
299
|
+
const inputs = buffer.getInputsForTick(this.currentTick);
|
|
300
|
+
for (const input of inputs) {
|
|
301
|
+
for (const action of input.actions) {
|
|
302
|
+
this.physicsWorld.applyInput(action);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 2. Step the physics world and collect collision events
|
|
308
|
+
const collisionEvents = this.physicsWorld.step();
|
|
309
|
+
if (collisionEvents.length > 0) {
|
|
310
|
+
this.pendingCollisionEvents.push(...collisionEvents);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 3. Increment tick
|
|
314
|
+
this.currentTick++;
|
|
315
|
+
this.ticksSinceLastBroadcast++;
|
|
316
|
+
|
|
317
|
+
// 4. Broadcast state at the configured interval
|
|
318
|
+
if (this.ticksSinceLastBroadcast >= BROADCAST_INTERVAL) {
|
|
319
|
+
this.broadcastState();
|
|
320
|
+
this.ticksSinceLastBroadcast = 0;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private broadcastState(): void {
|
|
325
|
+
const delta = this.stateManager.createDelta(this.physicsWorld, this.currentTick);
|
|
326
|
+
|
|
327
|
+
if (delta.bodies.length > 0) {
|
|
328
|
+
// Encode directly with ID mapping for numeric body indices
|
|
329
|
+
const message = encodeRoomState(
|
|
330
|
+
{
|
|
331
|
+
type: MessageType.ROOM_STATE,
|
|
332
|
+
tick: delta.tick,
|
|
333
|
+
timestamp: delta.timestamp,
|
|
334
|
+
bodies: delta.bodies,
|
|
335
|
+
isDelta: true,
|
|
336
|
+
},
|
|
337
|
+
this.stateManager.getIdToIndexMap(),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
this.broadcast(message);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (this.pendingCollisionEvents.length > 0) {
|
|
344
|
+
const collisionMessage = encodeMessage({
|
|
345
|
+
type: MessageType.COLLISION_EVENTS,
|
|
346
|
+
tick: this.currentTick,
|
|
347
|
+
events: this.pendingCollisionEvents,
|
|
348
|
+
});
|
|
349
|
+
this.broadcast(collisionMessage);
|
|
350
|
+
this.pendingCollisionEvents = [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private broadcast(message: Uint8Array): void {
|
|
355
|
+
for (const [, client] of this.clients) {
|
|
356
|
+
client.send(message);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getSnapshot(): RoomSnapshot {
|
|
361
|
+
return this.stateManager.createSnapshot(this.physicsWorld, this.currentTick);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
startSimulation(): void {
|
|
365
|
+
// If already running, stop and reset
|
|
366
|
+
if (this.simulationLoop.isRunning) {
|
|
367
|
+
this.simulationLoop.stop();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Reset physics world to initial state (including constraints)
|
|
371
|
+
this.physicsWorld.reset(this.initialBodies, this.initialConstraints);
|
|
372
|
+
this.currentTick = 0;
|
|
373
|
+
this.ticksSinceLastBroadcast = 0;
|
|
374
|
+
this.pendingCollisionEvents = [];
|
|
375
|
+
this.meshBinaryStore.clear();
|
|
376
|
+
this.geometryStore.clear();
|
|
377
|
+
this.meshRefStore.clear();
|
|
378
|
+
this.geometryRefCount.clear();
|
|
379
|
+
this.materialStore.clear();
|
|
380
|
+
this.textureStore.clear();
|
|
381
|
+
this.materialRefCount.clear();
|
|
382
|
+
this.stateManager.clear();
|
|
383
|
+
for (const [, buffer] of this.inputBuffers) {
|
|
384
|
+
buffer.clear();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Reset active constraints and bodies to initial set
|
|
388
|
+
this.activeConstraints.clear();
|
|
389
|
+
for (const c of this.initialConstraints) {
|
|
390
|
+
this.activeConstraints.set(c.id, c);
|
|
391
|
+
}
|
|
392
|
+
this.activeBodies.clear();
|
|
393
|
+
for (const b of this.initialBodies) {
|
|
394
|
+
this.activeBodies.set(b.id, b);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Start simulation loop
|
|
398
|
+
this.simulationLoop.start();
|
|
399
|
+
|
|
400
|
+
// Broadcast fresh snapshot to all clients (includes updated body ID mapping, constraints, and bodies)
|
|
401
|
+
const snapshot = this.stateManager.createSnapshot(this.physicsWorld, this.currentTick);
|
|
402
|
+
const constraints = Array.from(this.activeConstraints.values());
|
|
403
|
+
const bodies = Array.from(this.activeBodies.values());
|
|
404
|
+
this.broadcast(encodeMessage({
|
|
405
|
+
type: MessageType.SIMULATION_STARTED,
|
|
406
|
+
snapshot,
|
|
407
|
+
bodyIdMap: this.stateManager.getIdToIndexRecord(),
|
|
408
|
+
constraints: constraints.length > 0 ? constraints : undefined,
|
|
409
|
+
bodies: bodies.length > 0 ? bodies : undefined,
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
get isSimulationRunning(): boolean {
|
|
414
|
+
return this.simulationLoop.isRunning;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
get clientCount(): number {
|
|
418
|
+
return this.clients.size;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
get tickNumber(): number {
|
|
422
|
+
return this.currentTick;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
destroy(): void {
|
|
426
|
+
this.simulationLoop.stop();
|
|
427
|
+
for (const [, client] of this.clients) {
|
|
428
|
+
client.roomId = null;
|
|
429
|
+
}
|
|
430
|
+
this.clients.clear();
|
|
431
|
+
this.inputBuffers.clear();
|
|
432
|
+
this.pendingCollisionEvents = [];
|
|
433
|
+
this.activeConstraints.clear();
|
|
434
|
+
this.activeBodies.clear();
|
|
435
|
+
this.meshBinaryStore.clear();
|
|
436
|
+
this.geometryStore.clear();
|
|
437
|
+
this.meshRefStore.clear();
|
|
438
|
+
this.geometryRefCount.clear();
|
|
439
|
+
this.materialStore.clear();
|
|
440
|
+
this.textureStore.clear();
|
|
441
|
+
this.materialRefCount.clear();
|
|
442
|
+
this.stateManager.clear();
|
|
443
|
+
this.physicsWorld.destroy();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export class RoomManager {
|
|
448
|
+
private rooms: Map<string, Room> = new Map();
|
|
449
|
+
private rapier: typeof RAPIER;
|
|
450
|
+
|
|
451
|
+
constructor(rapier: typeof RAPIER) {
|
|
452
|
+
this.rapier = rapier;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
createRoom(roomId: string, initialBodies: BodyDescriptor[] = [], gravity?: Vec3, initialConstraints?: ConstraintDescriptor[]): Room {
|
|
456
|
+
if (this.rooms.has(roomId)) {
|
|
457
|
+
throw new Error(`Room "${roomId}" already exists`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const room = new Room(roomId, this.rapier, gravity);
|
|
461
|
+
if (initialBodies.length > 0 || (initialConstraints && initialConstraints.length > 0)) {
|
|
462
|
+
room.loadInitialState(initialBodies, initialConstraints);
|
|
463
|
+
}
|
|
464
|
+
this.rooms.set(roomId, room);
|
|
465
|
+
return room;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
getRoom(roomId: string): Room | undefined {
|
|
469
|
+
return this.rooms.get(roomId);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
destroyRoom(roomId: string): void {
|
|
473
|
+
const room = this.rooms.get(roomId);
|
|
474
|
+
if (room) {
|
|
475
|
+
room.destroy();
|
|
476
|
+
this.rooms.delete(roomId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
get roomCount(): number {
|
|
481
|
+
return this.rooms.size;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
getAllRoomIds(): string[] {
|
|
485
|
+
return Array.from(this.rooms.keys());
|
|
486
|
+
}
|
|
487
|
+
}
|