Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 41 additions & 10 deletions src/hooks/useMultiplayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn {
// Store joined lobby info for reconnection
const joinedCodeRef = useRef<string | null>(null);
const joinedNameRef = useRef<string | null>(null);
// Track when reconnection is in progress to prevent state conflicts with multiplayerStore
const isReconnectingRef = useRef<boolean>(false);
const [mySlotId, setMySlotId] = useState<string | null>(null);

const poolRef = useRef<SimplePool | null>(null);
Expand Down Expand Up @@ -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: () => {
Expand Down Expand Up @@ -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');
}
}
},
});
Expand Down Expand Up @@ -660,17 +671,23 @@ 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);

} 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
}
}
}, []);

Expand Down Expand Up @@ -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<boolean> => {
// Only guests need to reconnect - hosts wait for guests to reconnect to them
if (isHost) {
Expand All @@ -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();
Expand All @@ -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]);
Expand All @@ -811,6 +841,7 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn {
return () => {
// Clean up on unmount
useMultiplayerStore.getState().setReconnectCallback(null);
isReconnectingRef.current = false;
};
}, [status, reconnect]);

Expand Down
82 changes: 29 additions & 53 deletions src/rendering/InstancedDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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++;
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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++;
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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++;
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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++;
}

Expand All @@ -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,
Expand Down Expand Up @@ -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++;
}

Expand Down