From 2f8af49ebc1a6a3258239d8f3077011a399f5a92 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 04:42:25 +0000 Subject: [PATCH] fix: Consolidate reconnection logic and transform utilities 1. Reconnection logic (useMultiplayer + multiplayerStore): - Add isReconnectingRef to track reconnection state - Prevent joinLobby from resetting to host mode during reconnection - multiplayerStore now owns the reconnection state machine exclusively - Hook provides the reconnection action without conflicting state 2. Transform utilities (InstancedDecorations): - Import shared TransformUtils from InstancedMeshPool - Remove duplicate _tempMatrix, _tempPosition, _tempQuaternion, _tempScale from InstancedTrees, InstancedRocks, InstancedCrystals, InstancedGrass, and InstancedPebbles classes - Use shared _transformUtils module-level instance for all classes https://claude.ai/code/session_01VX6tjzQFatTTvZfSQ8RGMW --- src/hooks/useMultiplayer.ts | 51 +++++++++++++---- src/rendering/InstancedDecorations.ts | 82 ++++++++++----------------- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/src/hooks/useMultiplayer.ts b/src/hooks/useMultiplayer.ts index b0fc5c8c..a823b99b 100644 --- a/src/hooks/useMultiplayer.ts +++ b/src/hooks/useMultiplayer.ts @@ -214,6 +214,8 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { // Store joined lobby info for reconnection const joinedCodeRef = useRef(null); const joinedNameRef = useRef(null); + // Track when reconnection is in progress to prevent state conflicts with multiplayerStore + const isReconnectingRef = useRef(false); const [mySlotId, setMySlotId] = useState(null); const poolRef = useRef(null); @@ -590,8 +592,11 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { } catch (e) { debugNetworking.error('[Lobby] Failed to process offer:', e); setError('Failed to connect to host'); - setIsHost(true); // Reset to host mode - setStatus('hosting'); // Go back to hosting so user can try again + // Don't reset to host mode during reconnection - let multiplayerStore handle retry + if (!isReconnectingRef.current) { + setIsHost(true); // Reset to host mode + setStatus('hosting'); // Go back to hosting so user can try again + } } }, oneose: () => { @@ -622,12 +627,18 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { try { const data = JSON.parse(event.content); setError(data.reason || 'Lobby is full'); - setIsHost(true); // Reset to host mode so Join button reappears - setStatus('hosting'); // Go back to hosting so user can try again + // Don't reset to host mode during reconnection - let multiplayerStore handle retry + if (!isReconnectingRef.current) { + setIsHost(true); // Reset to host mode so Join button reappears + setStatus('hosting'); // Go back to hosting so user can try again + } } catch { setError('Lobby is full - no open slots available'); - setIsHost(true); // Reset to host mode so Join button reappears - setStatus('hosting'); + // Don't reset to host mode during reconnection - let multiplayerStore handle retry + if (!isReconnectingRef.current) { + setIsHost(true); // Reset to host mode so Join button reappears + setStatus('hosting'); + } } }, }); @@ -660,8 +671,11 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { offerSub.close(); rejectSub.close(); setError('No lobby found with that code'); - setIsHost(true); // Reset to host mode so Join button reappears - setStatus('hosting'); // Go back to hosting so user can try again + // Don't reset to host mode during reconnection - let multiplayerStore handle retry + if (!isReconnectingRef.current) { + setIsHost(true); // Reset to host mode so Join button reappears + setStatus('hosting'); // Go back to hosting so user can try again + } } joinTimeoutRef.current = null; }, 30000); @@ -669,8 +683,11 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { } catch (e) { debugNetworking.error('[Lobby] Join error:', e); setError(e instanceof Error ? e.message : 'Failed to join lobby'); - setIsHost(true); // Reset to host mode so Join button reappears - setStatus('hosting'); // Go back to hosting so user can try again + // Don't reset to host mode during reconnection - let multiplayerStore handle retry + if (!isReconnectingRef.current) { + setIsHost(true); // Reset to host mode so Join button reappears + setStatus('hosting'); // Go back to hosting so user can try again + } } }, []); @@ -759,6 +776,7 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { }, []); // Reconnection function for guests + // This is called by multiplayerStore.attemptReconnect() which handles exponential backoff const reconnect = useCallback(async (): Promise => { // Only guests need to reconnect - hosts wait for guests to reconnect to them if (isHost) { @@ -776,6 +794,10 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { debugNetworking.log('[Lobby] Attempting to reconnect to lobby:', code); + // Set reconnecting flag to prevent joinLobby from resetting to host mode on failure + // multiplayerStore owns the reconnection state machine and will handle retries + isReconnectingRef.current = true; + try { // Close existing peer connection pcRef.current?.close(); @@ -793,9 +815,17 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { const isConnected = currentHostConnection !== null; debugNetworking.log('[Lobby] Reconnection result:', isConnected ? 'success' : 'failed'); + + // Clear reconnecting flag on success - store will handle state + if (isConnected) { + isReconnectingRef.current = false; + } + // On failure, keep flag set - store will either retry or mark as failed + return isConnected; } catch (e) { debugNetworking.error('[Lobby] Reconnection failed:', e); + // Keep reconnecting flag set - store will handle retry or failure return false; } }, [isHost, joinLobby]); @@ -811,6 +841,7 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { return () => { // Clean up on unmount useMultiplayerStore.getState().setReconnectCallback(null); + isReconnectingRef.current = false; }; }, [status, reconnect]); diff --git a/src/rendering/InstancedDecorations.ts b/src/rendering/InstancedDecorations.ts index b0c44e7a..935074e7 100644 --- a/src/rendering/InstancedDecorations.ts +++ b/src/rendering/InstancedDecorations.ts @@ -3,6 +3,10 @@ import { BiomeConfig } from './Biomes'; import { MapData, MapDecoration } from '@/data/maps'; import AssetManager from '@/assets/AssetManager'; import { DECORATIONS } from '@/data/rendering.config'; +import { TransformUtils } from './shared/InstancedMeshPool'; + +// PERF: Shared transform utilities for all decoration classes (avoids duplicate temp objects) +const _transformUtils = new TransformUtils(); // PERF: Reusable Euler object for instanced decoration loops (avoids thousands of allocations) const _tempEuler = new THREE.Euler(); @@ -188,12 +192,6 @@ export class InstancedTrees { private materials: THREE.Material[] = []; private treeCollisions: Array<{ x: number; z: number; radius: number }> = []; - // Reusable objects for update loop - private _tempMatrix = new THREE.Matrix4(); - private _tempPosition = new THREE.Vector3(); - private _tempQuaternion = new THREE.Quaternion(); - private _tempScale = new THREE.Vector3(); - constructor( mapData: MapData, _biome: BiomeConfig, @@ -320,12 +318,12 @@ export class InstancedTrees { const inst = instances[i]; if (!isInFrustum(inst.x, inst.y, inst.z)) continue; - this._tempPosition.set(inst.x, inst.y, inst.z); + _transformUtils.tempPosition.set(inst.x, inst.y, inst.z); _tempEuler.set(0, inst.rotation, 0); - this._tempQuaternion.setFromEuler(_tempEuler); - this._tempScale.set(inst.scale, inst.scale, inst.scale); - this._tempMatrix.compose(this._tempPosition, this._tempQuaternion, this._tempScale); - mesh.setMatrixAt(visibleCount, this._tempMatrix); + _transformUtils.tempQuaternion.setFromEuler(_tempEuler); + _transformUtils.tempScale.set(inst.scale, inst.scale, inst.scale); + _transformUtils.tempMatrix.compose(_transformUtils.tempPosition, _transformUtils.tempQuaternion, _transformUtils.tempScale); + mesh.setMatrixAt(visibleCount, _transformUtils.tempMatrix); visibleCount++; } @@ -363,12 +361,6 @@ export class InstancedRocks { private materials: THREE.Material[] = []; private rockCollisions: Array<{ x: number; z: number; radius: number }> = []; - // Reusable objects for update loop - private _tempMatrix = new THREE.Matrix4(); - private _tempPosition = new THREE.Vector3(); - private _tempQuaternion = new THREE.Quaternion(); - private _tempScale = new THREE.Vector3(); - constructor( mapData: MapData, _biome: BiomeConfig, @@ -498,12 +490,12 @@ export class InstancedRocks { const inst = instances[i]; if (!isInFrustum(inst.x, inst.y, inst.z)) continue; - this._tempPosition.set(inst.x, inst.y, inst.z); + _transformUtils.tempPosition.set(inst.x, inst.y, inst.z); _tempEuler.set(0, inst.rotation, 0); - this._tempQuaternion.setFromEuler(_tempEuler); - this._tempScale.set(inst.scale, inst.scale, inst.scale); - this._tempMatrix.compose(this._tempPosition, this._tempQuaternion, this._tempScale); - mesh.setMatrixAt(visibleCount, this._tempMatrix); + _transformUtils.tempQuaternion.setFromEuler(_tempEuler); + _transformUtils.tempScale.set(inst.scale, inst.scale, inst.scale); + _transformUtils.tempMatrix.compose(_transformUtils.tempPosition, _transformUtils.tempQuaternion, _transformUtils.tempScale); + mesh.setMatrixAt(visibleCount, _transformUtils.tempMatrix); visibleCount++; } @@ -542,12 +534,6 @@ export class InstancedCrystals { private instances: InstanceData[] = []; private maxCount = 0; - // Reusable objects for update loop - private _tempMatrix = new THREE.Matrix4(); - private _tempPosition = new THREE.Vector3(); - private _tempQuaternion = new THREE.Quaternion(); - private _tempScale = new THREE.Vector3(); - constructor( mapData: MapData, biome: BiomeConfig, @@ -627,12 +613,12 @@ export class InstancedCrystals { const inst = this.instances[i]; if (!isInFrustum(inst.x, inst.y, inst.z)) continue; - this._tempPosition.set(inst.x, inst.y, inst.z); + _transformUtils.tempPosition.set(inst.x, inst.y, inst.z); _tempEuler.set(0, inst.rotation, 0); - this._tempQuaternion.setFromEuler(_tempEuler); - this._tempScale.set(inst.scale, inst.scale, inst.scale); - this._tempMatrix.compose(this._tempPosition, this._tempQuaternion, this._tempScale); - this.instancedMesh.setMatrixAt(visibleCount, this._tempMatrix); + _transformUtils.tempQuaternion.setFromEuler(_tempEuler); + _transformUtils.tempScale.set(inst.scale, inst.scale, inst.scale); + _transformUtils.tempMatrix.compose(_transformUtils.tempPosition, _transformUtils.tempQuaternion, _transformUtils.tempScale); + this.instancedMesh.setMatrixAt(visibleCount, _transformUtils.tempMatrix); visibleCount++; } @@ -663,11 +649,6 @@ export class InstancedGrass { private instances: Array<{ x: number; y: number; z: number; scale: number; rotation: number }> = []; private maxCount = 0; - private _tempMatrix = new THREE.Matrix4(); - private _tempPosition = new THREE.Vector3(); - private _tempQuaternion = new THREE.Quaternion(); - private _tempScale = new THREE.Vector3(); - constructor( mapData: MapData, biome: BiomeConfig, @@ -749,12 +730,12 @@ export class InstancedGrass { const inst = this.instances[i]; if (!isInFrustum(inst.x, inst.y, inst.z)) continue; - this._tempPosition.set(inst.x, inst.y, inst.z); + _transformUtils.tempPosition.set(inst.x, inst.y, inst.z); _tempEuler.set(-Math.PI / 2, inst.rotation, 0); - this._tempQuaternion.setFromEuler(_tempEuler); - this._tempScale.set(inst.scale, inst.scale, inst.scale); - this._tempMatrix.compose(this._tempPosition, this._tempQuaternion, this._tempScale); - this.instancedMesh.setMatrixAt(visibleCount, this._tempMatrix); + _transformUtils.tempQuaternion.setFromEuler(_tempEuler); + _transformUtils.tempScale.set(inst.scale, inst.scale, inst.scale); + _transformUtils.tempMatrix.compose(_transformUtils.tempPosition, _transformUtils.tempQuaternion, _transformUtils.tempScale); + this.instancedMesh.setMatrixAt(visibleCount, _transformUtils.tempMatrix); visibleCount++; } @@ -781,11 +762,6 @@ export class InstancedPebbles { private instances: Array<{ x: number; y: number; z: number; scale: number; rotation: number }> = []; private maxCount = 0; - private _tempMatrix = new THREE.Matrix4(); - private _tempPosition = new THREE.Vector3(); - private _tempQuaternion = new THREE.Quaternion(); - private _tempScale = new THREE.Vector3(); - constructor( mapData: MapData, biome: BiomeConfig, @@ -858,12 +834,12 @@ export class InstancedPebbles { const inst = this.instances[i]; if (!isInFrustum(inst.x, inst.y, inst.z)) continue; - this._tempPosition.set(inst.x, inst.y, inst.z); + _transformUtils.tempPosition.set(inst.x, inst.y, inst.z); _tempEuler.set(0, inst.rotation, 0); - this._tempQuaternion.setFromEuler(_tempEuler); - this._tempScale.set(inst.scale, inst.scale, inst.scale); - this._tempMatrix.compose(this._tempPosition, this._tempQuaternion, this._tempScale); - this.instancedMesh.setMatrixAt(visibleCount, this._tempMatrix); + _transformUtils.tempQuaternion.setFromEuler(_tempEuler); + _transformUtils.tempScale.set(inst.scale, inst.scale, inst.scale); + _transformUtils.tempMatrix.compose(_transformUtils.tempPosition, _transformUtils.tempQuaternion, _transformUtils.tempScale); + this.instancedMesh.setMatrixAt(visibleCount, _transformUtils.tempMatrix); visibleCount++; }