class MeshNet { constructor() { this.myPeerId = generatePeerId(); this.connections = new Map(); this._pendingMeshPeers = new Map(); this._pendingPeer = null; this._currentQRData = null; this._fullSDPData = null; this.isHost = false; this.hostPeerId = null; this.roster = []; this.readyPeers = new Set(); this._meshReadySent = false; this.playerName = ''; this.names = new Map(); this.names.set(this.myPeerId, ''); this.onpeerconnect = null; this.onpeerdisconnect = null; this.ondata = null; this.onqrupdate = null; this.onanswerready = null; this.onreadyupdate = null; this.onchat = null; this.onlog = null; } _log(msg) { if (this.onlog) this.onlog(msg); } /* ─── Host: room ─── */ createRoom() { this.isHost = true; this._createPendingPeer(); } _createPendingPeer() { if (this._pendingPeer) { try { this._pendingPeer.peer.destroy(); } catch (e) {} } const peer = new SimplePeer({ initiator: true, trickle: false, config: { iceServers: [] } }); let signalSent = false; peer.on('signal', async signal => { if (signalSent) return; signalSent = true; this._log('SDP-Signal (Offer) empfangen'); const packed = await pack({ v: 1, id: this.myPeerId, s: signal }); this._currentQRData = encode45(packed); this._fullSDPData = this._currentQRData; if (this.onqrupdate) this.onqrupdate(this._currentQRData); this._log('Offer QR erstellt'); }); peer.on('connect', () => { peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName })); this._log('DataChannel offen, sende Identity'); }); peer.on('data', data => { try { const msg = JSON.parse(data.toString()); this._handleMessage(msg, peer); } catch (e) { console.error('p2p data error:', e); } }); peer.on('close', () => this._cleanupPeer(peer)); peer.on('error', err => console.error('p2p error:', err)); this._pendingPeer = { peer, signalSent, connected: false, peerId: null }; } async feedAnswer(answerStr) { if (!this._pendingPeer || this._pendingPeer.signalReceived) { this._log('feedAnswer ignoriert (bereits verbunden)'); return; } try { const data = await unpack(decode45(answerStr)); this._pendingPeer.peerId = data.id; this._pendingPeer.peer.signal(data.s); this._pendingPeer.signalReceived = true; this._log(`Answer verarbeitet von ${data.id.slice(0, 8)}`); } catch (e) { console.error('feedAnswer error:', e); this._log(`feedAnswer Fehler: ${e.message}`); } } /* ─── Peer: join ─── */ async joinFromQR(qrContent) { const data = await unpack(decode45(qrContent)); const remoteId = data.id; this.hostPeerId = remoteId; const offer = data.s; this._log(`QR gelesen, signalisiere Host ${remoteId.slice(0, 8)}`); const peer = new SimplePeer({ initiator: false, trickle: false, config: { iceServers: [] } }); let answerSent = false; peer.on('signal', async signal => { if (answerSent) return; answerSent = true; const packed = await pack({ v: 1, id: this.myPeerId, s: signal, to: remoteId }); const answerData = encode45(packed); if (this.onanswerready) this.onanswerready(answerData); this._log('Answer QR erstellt'); }); peer.on('connect', () => { this.connections.set(remoteId, peer); peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId, name: this.playerName })); this._log(`DataChannel zu Host ${remoteId.slice(0, 8)} offen`); if (this.onpeerconnect) this.onpeerconnect(remoteId); }); peer.on('data', data => { try { const msg = JSON.parse(data.toString()); this._handleMessage(msg, peer); } catch (e) { console.error('p2p data error:', e); } }); peer.on('close', () => this._cleanupPeer(peer)); peer.on('error', err => { console.error('p2p error:', err); this._log(`Peer-Fehler: ${err.message}`); }); peer.signal(offer); this.connections.set(remoteId, peer); this._pendingPeer = { peer, peerId: remoteId, connected: answerSent }; } /* ─── Mesh helpers ─── */ _createSimplePeer(initiator, onSignal, onConnect) { const peer = new SimplePeer({ initiator, trickle: false, config: { iceServers: [] } }); peer.on('signal', signal => { if (onSignal) onSignal(signal); }); peer.on('connect', () => { if (onConnect) onConnect(); this._log('DataChannel (Mesh) offen'); }); peer.on('data', data => { try { const msg = JSON.parse(data.toString()); this._handleMessage(msg, peer); } catch (e) { console.error('mesh data error:', e); } }); peer.on('close', () => this._cleanupPeer(peer)); peer.on('error', e => { console.error('mesh error:', e); this._log(`Mesh-Fehler: ${e.message}`); }); return peer; } _initiateMeshConnection(targetPeerId) { if (this.connections.has(targetPeerId)) return; if (this._pendingMeshPeers.has(targetPeerId)) return; if (this.myPeerId >= targetPeerId) return; this._log(`Mesh zu ${targetPeerId.slice(0, 8)} initiiert`); const peer = this._createSimplePeer(true, signal => { this.sendTo(this.hostPeerId, { type: 'signal_relay', to: targetPeerId, signal }); }, () => { this.connections.set(targetPeerId, peer); this._pendingMeshPeers.delete(targetPeerId); if (this.onpeerconnect) this.onpeerconnect(targetPeerId); this._checkMeshReady(); } ); this._pendingMeshPeers.set(targetPeerId, peer); } _handleRelayedSignal(fromPeerId, signal) { this._log(`Relayed-Signal von ${fromPeerId.slice(0, 8)}`); let peer = this.connections.get(fromPeerId) || this._pendingMeshPeers.get(fromPeerId); if (!peer) { peer = this._createSimplePeer(false, signal => { this.sendTo(this.hostPeerId, { type: 'signal_relay', to: fromPeerId, signal }); }, () => { this.connections.set(fromPeerId, peer); this._pendingMeshPeers.delete(fromPeerId); if (this.onpeerconnect) this.onpeerconnect(fromPeerId); this._checkMeshReady(); } ); this._pendingMeshPeers.set(fromPeerId, peer); } peer.signal(signal); } _checkMeshConnections() { const missing = this.roster.filter(pid => pid !== this.myPeerId && !this.connections.has(pid) && this.myPeerId < pid); if (missing.length) this._log(`Prüfe Mesh: ${missing.length} fehlende Verbindungen`); for (const pid of this.roster) { if (pid === this.myPeerId) continue; if (this.connections.has(pid)) continue; if (this.myPeerId < pid) { this._initiateMeshConnection(pid); } } this._checkMeshReady(); } _checkMeshReady() { if (this._meshReadySent) return; if (!this.hostPeerId) return; if (!this.connections.has(this.hostPeerId)) return; const allConnected = this.roster.every(pid => pid === this.myPeerId || this.connections.has(pid) ); if (allConnected && this.roster.length > 0) { this._meshReadySent = true; this.sendTo(this.hostPeerId, { type: 'mesh_ready' }); this._log('Mesh ready gesendet'); } } _broadcastRoster() { this.roster = this.getPeerIds(); this.readyPeers.clear(); this.broadcast({ type: 'roster', peers: this.roster.map(id => ({ id, name: this.names.get(id) || '' })) }); this.readyPeers.clear(); this._log(`Roster broadcast an ${this.roster.length} Peers`); if (this.onreadyupdate) this.onreadyupdate(0, this.roster.length); } /* ─── Message routing ─── */ _handleMessage(msg, peer) { const peerId = this._findPeerId(peer) || 'unknown'; if (msg.type === 'identity') { if (msg.name) this.names.set(msg.peerId, msg.name); const existing = this._findPeerId(peer); if (existing && existing !== msg.peerId) { this.connections.delete(existing); } this.connections.set(msg.peerId, peer); this._log(`Identity: ${msg.peerId.slice(0, 8)}`); if (this._pendingPeer && this._pendingPeer.peer === peer) { this._pendingPeer.peerId = msg.peerId; this._pendingPeer.connected = true; this._pendingPeer = null; if (this.onpeerconnect) this.onpeerconnect(msg.peerId); this._log(`Host: Peer ${msg.peerId.slice(0, 8)} promoted`); if (this.isHost) { setTimeout(() => this._createPendingPeer(), 300); this._broadcastRoster(); } } else { if (this.onpeerconnect) this.onpeerconnect(msg.peerId); this._log(`Mesh: Peer ${msg.peerId.slice(0, 8)} verbunden`); } return; } if (msg.type === 'signal_relay') { this._log(`Signal-Relay weitergeleitet an ${msg.to.slice(0, 8)}`); const target = this.connections.get(msg.to) || this._pendingMeshPeers.get(msg.to); if (target) { target.send(JSON.stringify({ type: 'signal_relayed', from: peerId, signal: msg.signal })); } return; } if (msg.type === 'signal_relayed') { this._handleRelayedSignal(msg.from, msg.signal); return; } if (msg.type === 'roster') { this.roster = (msg.peers || []).map(p => p.id || p); if (msg.peers) msg.peers.forEach(p => { if (p.name) this.names.set(p.id, p.name); }); this._meshReadySent = false; this._checkMeshConnections(); this._log(`Roster empfangen: ${this.roster.length} Peers`); return; } if (msg.type === 'mesh_ready' && this.isHost) { this.readyPeers.add(peerId); this._log(`Mesh ready von ${peerId.slice(0, 8)}`); if (this.onreadyupdate) { this.onreadyupdate(this.readyPeers.size, this.roster.length); } return; } if (msg.type === 'chat') { if (this.onchat) this.onchat(peerId, msg.text); return; } if (this.ondata) { this.ondata(peerId, msg); } } /* ─── Peer management ─── */ _findPeerId(peer) { for (const [id, p] of this.connections) { if (p === peer) return id; } for (const [id, p] of this._pendingMeshPeers) { if (p === peer) return id; } return null; } _cleanupPeer(peer) { const peerId = this._findPeerId(peer); if (peerId) { this.connections.delete(peerId); this._log(`Peer getrennt: ${peerId.slice(0, 8)}`); if (this.onpeerdisconnect) this.onpeerdisconnect(peerId); } for (const [id, p] of this._pendingMeshPeers) { if (p === peer) { this._pendingMeshPeers.delete(id); break; } } if (this._pendingPeer && this._pendingPeer.peer === peer) { this._pendingPeer = null; } } /* ─── Send ─── */ broadcast(msg) { const str = JSON.stringify(msg); for (const [id, peer] of this.connections) { try { peer.send(str); } catch (e) { console.error('broadcast to', id, 'failed:', e); } } } sendTo(peerId, msg) { const peer = this.connections.get(peerId); if (peer) { try { peer.send(JSON.stringify(msg)); } catch (e) { console.error('sendTo', peerId, 'failed:', e); } } } relaySignal(from, to, signal) { const target = this.connections.get(to); if (target) { target.send(JSON.stringify({ type: 'signal_relay', from, to, signal })); } } getPeerIds() { return Array.from(this.connections.keys()); } getCount() { return this.connections.size; } getPeerName(peerId) { return this.names.get(peerId) || peerId.slice(0, 8); } getFullSDPData() { return this._fullSDPData; } destroy() { this._log('MeshNet wird zerstört'); for (const peer of this.connections.values()) { try { peer.destroy(); } catch (e) {} } for (const peer of this._pendingMeshPeers.values()) { try { peer.destroy(); } catch (e) {} } this.connections.clear(); this._pendingMeshPeers.clear(); if (this._pendingPeer) { try { this._pendingPeer.peer.destroy(); } catch (e) {} this._pendingPeer = null; } } }