import { createServer, type Server as HttpServer } from 'node:http'; import { WebSocketServer, WebSocket } from 'ws'; import type RAPIER from '@dimforge/rapier3d-compat'; import { MessageType, decodeClientMessage, encodeMessage, DEFAULT_PORT, OPCODE_MESH_BINARY, OPCODE_GEOMETRY_DEF, OPCODE_MESH_REF, OPCODE_MATERIAL_DEF, OPCODE_TEXTURE_DEF, } from '@rapierphysicsplugin/shared'; import type { ClientMessage } from '@rapierphysicsplugin/shared'; import { RoomManager } from './room-manager.js'; import { ClientConnection } from './client-connection.js'; import { handleClockSyncRequest } from './clock-sync.js'; let clientIdCounter = 0; function generateClientId(): string { return `client_${++clientIdCounter}`; } export class PhysicsServer { private httpServer: HttpServer | null = null; private wss: WebSocketServer | null = null; private connections: Map = new Map(); private roomManager: RoomManager; private rapier: typeof RAPIER; constructor(rapier: typeof RAPIER) { this.rapier = rapier; this.roomManager = new RoomManager(rapier); } start(port: number = DEFAULT_PORT): Promise { return new Promise((resolve) => { this.httpServer = createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Physics server OK'); }); this.wss = new WebSocketServer({ server: this.httpServer }); this.wss.on('connection', (ws: WebSocket) => { this.handleConnection(ws); }); this.httpServer.listen(port, () => { console.log(`Physics server listening on port ${port}`); resolve(); }); }); } private handleConnection(ws: WebSocket): void { const clientId = generateClientId(); const conn = new ClientConnection(clientId, ws); this.connections.set(clientId, conn); console.log(`Client connected: ${clientId}`); ws.on('message', (data: Buffer) => { try { const buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); // Intercept mesh binary — relay without full decode if (buf[0] === OPCODE_MESH_BINARY) { if (conn.roomId) { const room = this.roomManager.getRoom(conn.roomId); if (room) { room.relayMeshBinary(conn.id, buf); } } return; } // Intercept geometry def — relay/store without full decode if (buf[0] === OPCODE_GEOMETRY_DEF) { if (conn.roomId) { const room = this.roomManager.getRoom(conn.roomId); if (room) { room.relayGeometryDef(conn.id, buf); } } return; } // Intercept mesh ref — relay/store without full decode if (buf[0] === OPCODE_MESH_REF) { if (conn.roomId) { const room = this.roomManager.getRoom(conn.roomId); if (room) { room.relayMeshRef(conn.id, buf); } } return; } // Intercept material def — relay/store without full decode if (buf[0] === OPCODE_MATERIAL_DEF) { if (conn.roomId) { const room = this.roomManager.getRoom(conn.roomId); if (room) { room.relayMaterialDef(conn.id, buf); } } return; } // Intercept texture def — relay/store without full decode if (buf[0] === OPCODE_TEXTURE_DEF) { if (conn.roomId) { const room = this.roomManager.getRoom(conn.roomId); if (room) { room.relayTextureDef(conn.id, buf); } } return; } const message = decodeClientMessage(buf); this.handleMessage(conn, message); } catch (err) { console.error(`Error processing message from ${clientId}:`, err); conn.send(encodeMessage({ type: MessageType.ERROR, message: 'Invalid message format', })); } }); ws.on('close', () => { this.handleDisconnect(conn); }); ws.on('error', (err) => { console.error(`WebSocket error for ${clientId}:`, err); }); } private handleMessage(conn: ClientConnection, message: ClientMessage): void { switch (message.type) { case MessageType.CLOCK_SYNC_REQUEST: handleClockSyncRequest(conn, message); break; case MessageType.CREATE_ROOM: { try { const room = this.roomManager.createRoom( message.roomId, message.initialBodies, message.gravity ); conn.send(encodeMessage({ type: MessageType.ROOM_CREATED, roomId: room.id, })); } catch (err) { conn.send(encodeMessage({ type: MessageType.ERROR, message: (err as Error).message, })); } break; } case MessageType.JOIN_ROOM: { const room = this.roomManager.getRoom(message.roomId); if (!room) { conn.send(encodeMessage({ type: MessageType.ERROR, message: `Room "${message.roomId}" not found`, })); return; } // Leave current room if in one if (conn.roomId) { const currentRoom = this.roomManager.getRoom(conn.roomId); currentRoom?.removeClient(conn); } room.addClient(conn); break; } case MessageType.LEAVE_ROOM: { if (conn.roomId) { const room = this.roomManager.getRoom(conn.roomId); room?.removeClient(conn); } break; } case MessageType.CLIENT_INPUT: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.bufferInput(conn.id, message.input); } break; } case MessageType.ADD_BODY: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { try { const shape = message.body.shape; console.log(`[ADD_BODY] id=${message.body.id} shape.type=${shape.type}`); if (shape.type === 'convex_hull' || shape.type === 'mesh' || shape.type === 'heightfield') { const params = shape.params as unknown as Record; for (const [key, val] of Object.entries(params)) { if (val && typeof val === 'object') { console.log(`[ADD_BODY] params.${key}: type=${Object.prototype.toString.call(val)} constructor=${(val as any)?.constructor?.name} instanceof_F32=${val instanceof Float32Array} instanceof_U32=${val instanceof Uint32Array} isView=${ArrayBuffer.isView(val)}`); } } } if (shape.type === 'container') { const cp = shape.params as unknown as { children: Array<{ shape: { type: string; params: Record } }> }; for (const child of cp.children) { if (child.shape.type === 'convex_hull' || child.shape.type === 'mesh') { console.log(`[ADD_BODY] container child shape.type=${child.shape.type}`); for (const [key, val] of Object.entries(child.shape.params)) { if (val && typeof val === 'object') { console.log(`[ADD_BODY] params.${key}: type=${Object.prototype.toString.call(val)} constructor=${(val as any)?.constructor?.name} instanceof_F32=${val instanceof Float32Array}`); } } } } } // If the client requested ownership, overwrite with the real sender ID // to prevent spoofing. If ownerId is not set, the body has no owner. if (message.body.ownerId) { message.body.ownerId = conn.id; } room.addBody(message.body); } catch (err) { console.error(`[ADD_BODY] Error adding body ${message.body.id}:`, err); conn.send(encodeMessage({ type: MessageType.ERROR, message: (err as Error).message, })); } } break; } case MessageType.REMOVE_BODY: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.removeBody(message.bodyId); } break; } case MessageType.START_SIMULATION: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.startSimulation(); } break; } case MessageType.BODY_EVENT: { if (!conn.roomId) return; // Rebroadcast body events to all clients in the room const room = this.roomManager.getRoom(conn.roomId); if (room) { // The room broadcast is handled by forwarding the message // For now, we just log it console.log(`Body event from ${conn.id}: ${message.eventType} on ${message.bodyId}`); } break; } case MessageType.ADD_CONSTRAINT: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { try { room.addConstraint(message.constraint); } catch (err) { conn.send(encodeMessage({ type: MessageType.ERROR, message: (err as Error).message, })); } } break; } case MessageType.REMOVE_CONSTRAINT: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.removeConstraint(message.constraintId); } break; } case MessageType.UPDATE_CONSTRAINT: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.updateConstraint(message.constraintId, message.updates); } break; } case MessageType.SHAPE_CAST_REQUEST: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.handleShapeCastQuery(conn, message.request); } break; } case MessageType.SHAPE_PROXIMITY_REQUEST: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.handleShapeProximityQuery(conn, message.request); } break; } case MessageType.POINT_PROXIMITY_REQUEST: { if (!conn.roomId) return; const room = this.roomManager.getRoom(conn.roomId); if (room) { room.handlePointProximityQuery(conn, message.request); } break; } } } private handleDisconnect(conn: ClientConnection): void { console.log(`Client disconnected: ${conn.id}`); if (conn.roomId) { const room = this.roomManager.getRoom(conn.roomId); room?.removeClient(conn); } this.connections.delete(conn.id); } stop(): void { // Destroy all rooms for (const roomId of this.roomManager.getAllRoomIds()) { this.roomManager.destroyRoom(roomId); } // Close all connections for (const [, conn] of this.connections) { conn.ws.close(); } this.connections.clear(); // Close servers this.wss?.close(); this.wss = null; this.httpServer?.close(); this.httpServer = null; } getRoomManager(): RoomManager { return this.roomManager; } }