You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-20 07:26:18 +00:00
🆙 Stage 1 reconnect
This commit is contained in:
@@ -52,6 +52,13 @@ export class EventDispatcher implements IEventDispatcher
|
|||||||
{
|
{
|
||||||
if(!event) return false;
|
if(!event) return false;
|
||||||
|
|
||||||
|
// Debug: log SOCKET_ events to trace reconnection flow
|
||||||
|
if(event.type && event.type.startsWith('SOCKET_'))
|
||||||
|
{
|
||||||
|
const listenerCount = this._listeners.get(event.type)?.length ?? 0;
|
||||||
|
console.log('[EventDispatcher] Dispatching ' + event.type + ' (listeners: ' + listenerCount + ')');
|
||||||
|
}
|
||||||
|
|
||||||
NitroLogger.events('Dispatched Event', event.type);
|
NitroLogger.events('Dispatched Event', event.type);
|
||||||
|
|
||||||
this.processEvent(event);
|
this.processEvent(event);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export class RoomMessageHandler
|
|||||||
|
|
||||||
private _currentRoomId: number = 0;
|
private _currentRoomId: number = 0;
|
||||||
private _ownUserId: number = 0;
|
private _ownUserId: number = 0;
|
||||||
|
private _ownRoomIndex: number = -1;
|
||||||
private _initialConnection: boolean = true;
|
private _initialConnection: boolean = true;
|
||||||
private _guideId: number = -1;
|
private _guideId: number = -1;
|
||||||
private _requesterId: number = -1;
|
private _requesterId: number = -1;
|
||||||
@@ -635,6 +636,7 @@ export class RoomMessageHandler
|
|||||||
|
|
||||||
if(user.webID === this._ownUserId)
|
if(user.webID === this._ownUserId)
|
||||||
{
|
{
|
||||||
|
this._ownRoomIndex = user.roomIndex;
|
||||||
this._roomEngine.setRoomSessionOwnUser(this._currentRoomId, user.roomIndex);
|
this._roomEngine.setRoomSessionOwnUser(this._currentRoomId, user.roomIndex);
|
||||||
this._roomEngine.updateRoomObjectUserOwn(this._currentRoomId, user.roomIndex);
|
this._roomEngine.updateRoomObjectUserOwn(this._currentRoomId, user.roomIndex);
|
||||||
}
|
}
|
||||||
@@ -735,6 +737,22 @@ export class RoomMessageHandler
|
|||||||
this._roomEngine.updateRoomObjectUserLocation(this._currentRoomId, status.id, location, goal, status.canStandUp, height, direction, status.headDirection);
|
this._roomEngine.updateRoomObjectUserLocation(this._currentRoomId, status.id, location, goal, status.canStandUp, height, direction, status.headDirection);
|
||||||
this._roomEngine.updateRoomObjectUserFlatControl(this._currentRoomId, status.id, null);
|
this._roomEngine.updateRoomObjectUserFlatControl(this._currentRoomId, status.id, null);
|
||||||
|
|
||||||
|
// Save own user's position for reconnection (only when not locked by reconnect flow)
|
||||||
|
if(status.id === this._ownRoomIndex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const locked = sessionStorage.getItem('nitro.session.posLocked');
|
||||||
|
|
||||||
|
if(!locked)
|
||||||
|
{
|
||||||
|
sessionStorage.setItem('nitro.session.lastPosX', status.x.toString());
|
||||||
|
sessionStorage.setItem('nitro.session.lastPosY', status.y.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
let isPosture = true;
|
let isPosture = true;
|
||||||
let postureUpdate = false;
|
let postureUpdate = false;
|
||||||
let postureType = RoomObjectVariable.STD;
|
let postureType = RoomObjectVariable.STD;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IRoomHandlerListener, IRoomSession, IRoomSessionManager } from '@nitrots/api';
|
import { IRoomHandlerListener, IRoomSession, IRoomSessionManager } from '@nitrots/api';
|
||||||
import { GetCommunication } from '@nitrots/communication';
|
import { GetCommunication, RoomEnterComposer, RoomUnitWalkComposer } from '@nitrots/communication';
|
||||||
import { GetEventDispatcher, NitroEventType, RoomSessionEvent } from '@nitrots/events';
|
import { GetEventDispatcher, NitroEventType, RoomSessionEvent } from '@nitrots/events';
|
||||||
import { NitroLogger } from '@nitrots/utils';
|
import { NitroLogger } from '@nitrots/utils';
|
||||||
import { RoomSession } from './RoomSession';
|
import { RoomSession } from './RoomSession';
|
||||||
@@ -7,24 +7,36 @@ import { BaseHandler, GenericErrorHandler, PetPackageHandler, PollHandler, RoomC
|
|||||||
|
|
||||||
const STORAGE_KEY_ROOM_ID = 'nitro.session.lastRoomId';
|
const STORAGE_KEY_ROOM_ID = 'nitro.session.lastRoomId';
|
||||||
const STORAGE_KEY_ROOM_PASSWORD = 'nitro.session.lastRoomPassword';
|
const STORAGE_KEY_ROOM_PASSWORD = 'nitro.session.lastRoomPassword';
|
||||||
|
const STORAGE_KEY_POS_X = 'nitro.session.lastPosX';
|
||||||
|
const STORAGE_KEY_POS_Y = 'nitro.session.lastPosY';
|
||||||
|
|
||||||
export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerListener
|
export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerListener
|
||||||
{
|
{
|
||||||
private _handlers: BaseHandler[] = [];
|
private _handlers: BaseHandler[] = [];
|
||||||
private _sessions: Map<string, IRoomSession> = new Map();
|
private _sessions: Map<string, IRoomSession> = new Map();
|
||||||
private _pendingSession: IRoomSession = null;
|
private _pendingSession: IRoomSession = null;
|
||||||
|
|
||||||
private _sessionStarting: boolean = false;
|
private _sessionStarting: boolean = false;
|
||||||
private _viewerSession: IRoomSession = null;
|
private _viewerSession: IRoomSession = null;
|
||||||
|
|
||||||
|
// Reconnection state tracking
|
||||||
private _lastRoomId: number = -1;
|
private _lastRoomId: number = -1;
|
||||||
private _lastRoomPassword: string = null;
|
private _lastRoomPassword: string = null;
|
||||||
private _isReconnecting: boolean = false;
|
private _isReconnecting: boolean = false;
|
||||||
private _reconnectGuardTimer: ReturnType<typeof setTimeout> = null;
|
private _reconnectGuardTimer: ReturnType<typeof setTimeout> = null;
|
||||||
|
private _pendingRoomClear: ReturnType<typeof setTimeout> = null;
|
||||||
|
private _savedPosX: number = -1;
|
||||||
|
private _savedPosY: number = -1;
|
||||||
|
|
||||||
public async init(): Promise<void>
|
public async init(): Promise<void>
|
||||||
{
|
{
|
||||||
|
console.log('[RoomSessionManager] init() called');
|
||||||
this.createHandlers();
|
this.createHandlers();
|
||||||
this.processPendingSession();
|
this.processPendingSession();
|
||||||
this.setupReconnectListener();
|
this.setupReconnectListener();
|
||||||
|
|
||||||
|
// Check if there's a persisted room from a network disconnect (Vite page reload).
|
||||||
|
// sessionStorage survives same-tab reloads but is cleared on browser close.
|
||||||
this.checkPersistedRoom();
|
this.checkPersistedRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,11 +51,20 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
const roomId = parseInt(storedRoomId, 10);
|
const roomId = parseInt(storedRoomId, 10);
|
||||||
|
|
||||||
if(isNaN(roomId) || roomId <= 0) return;
|
if(isNaN(roomId) || roomId <= 0) return;
|
||||||
|
|
||||||
|
NitroLogger.log('[RoomSessionManager] Found persisted room ' + roomId + ' - setting guard for page-reload restore');
|
||||||
|
|
||||||
|
// Pre-load memory state so attemptRoomReEntry and tryRestoreSession can use it
|
||||||
this._lastRoomId = roomId;
|
this._lastRoomId = roomId;
|
||||||
this._lastRoomPassword = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
|
this._lastRoomPassword = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
|
||||||
|
|
||||||
|
// Enable guard to block DesktopViewEvent until we enter the stored room
|
||||||
this._isReconnecting = true;
|
this._isReconnecting = true;
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{
|
||||||
|
// sessionStorage not available
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createHandlers(): void
|
private createHandlers(): void
|
||||||
@@ -69,45 +90,82 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
private setupReconnectListener(): void
|
private setupReconnectListener(): void
|
||||||
{
|
{
|
||||||
|
console.log('[RoomSessionManager] setupReconnectListener() - registering event listeners');
|
||||||
|
|
||||||
|
// Mark reconnecting state early so DesktopViewEvent / home room redirects
|
||||||
|
// don't clear the tracked room before we can re-enter it
|
||||||
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTING, () =>
|
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTING, () =>
|
||||||
{
|
{
|
||||||
this._isReconnecting = true;
|
// Cancel any pending room ID clear from removeSession().
|
||||||
|
// The server sends DesktopViewEvent before closing the socket, which
|
||||||
|
// schedules a delayed clear. We need to preserve the room ID for re-entry.
|
||||||
|
this.cancelRoomIdClear();
|
||||||
|
|
||||||
|
// Re-persist room to sessionStorage (it was cleared in removeSession)
|
||||||
|
if(this._lastRoomId > 0)
|
||||||
|
{
|
||||||
|
this.persistRoom(this._lastRoomId, this._lastRoomPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RoomSessionManager] SOCKET_RECONNECTING fired! lastRoomId=' + this._lastRoomId);
|
||||||
|
this._isReconnecting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SOCKET_RECONNECTED: the WebSocket is open but NOT yet authenticated.
|
||||||
|
// We set up a fallback timer here in case the server doesn't send
|
||||||
|
// AuthenticatedEvent (e.g. SSO ticket consumed).
|
||||||
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTED, () =>
|
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTED, () =>
|
||||||
{
|
{
|
||||||
|
console.log('[RoomSessionManager] SOCKET_RECONNECTED fired! lastRoomId=' + this._lastRoomId);
|
||||||
|
|
||||||
|
// Fallback: if REAUTHENTICATED doesn't fire within 5 seconds,
|
||||||
|
// try to re-enter the room anyway (the connection might still work)
|
||||||
this.clearGuardTimer();
|
this.clearGuardTimer();
|
||||||
this._reconnectGuardTimer = setTimeout(() =>
|
this._reconnectGuardTimer = setTimeout(() =>
|
||||||
{
|
{
|
||||||
this._reconnectGuardTimer = null;
|
this._reconnectGuardTimer = null;
|
||||||
|
|
||||||
if(!this._isReconnecting) return;
|
if(!this._isReconnecting) return;
|
||||||
|
|
||||||
|
NitroLogger.log('[RoomSessionManager] REAUTHENTICATED timeout - attempting fallback room re-entry for room ' + this._lastRoomId);
|
||||||
this.attemptRoomReEntry();
|
this.attemptRoomReEntry();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// REAUTHENTICATED: SSO handshake completed, connection is ready to send messages
|
||||||
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_REAUTHENTICATED, () =>
|
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_REAUTHENTICATED, () =>
|
||||||
{
|
{
|
||||||
|
console.log('[RoomSessionManager] SOCKET_REAUTHENTICATED fired! lastRoomId=' + this._lastRoomId);
|
||||||
|
|
||||||
|
// Snapshot the saved position BEFORE re-entering (re-entry overwrites sessionStorage)
|
||||||
|
this.snapshotSavedPosition();
|
||||||
|
|
||||||
this.clearGuardTimer();
|
this.clearGuardTimer();
|
||||||
this.attemptRoomReEntry();
|
this.attemptRoomReEntry();
|
||||||
});
|
});
|
||||||
|
|
||||||
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECT_FAILED, () =>
|
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECT_FAILED, () =>
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] SOCKET_RECONNECT_FAILED - clearing state');
|
||||||
this.clearGuardTimer();
|
this.clearGuardTimer();
|
||||||
this._isReconnecting = false;
|
this._isReconnecting = false;
|
||||||
this._lastRoomId = -1;
|
this._lastRoomId = -1;
|
||||||
this._lastRoomPassword = null;
|
this._lastRoomPassword = null;
|
||||||
this.clearPersistedRoom();
|
this.clearPersistedRoom();
|
||||||
|
this.clearPersistedPosition();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When the socket is permanently closed (server shutdown, max retries),
|
||||||
|
// clear the persisted room so the next page load uses normal navigation
|
||||||
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, () =>
|
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, () =>
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] SOCKET_CLOSED - clearing persisted room');
|
||||||
this.clearGuardTimer();
|
this.clearGuardTimer();
|
||||||
this._isReconnecting = false;
|
this._isReconnecting = false;
|
||||||
this._lastRoomId = -1;
|
this._lastRoomId = -1;
|
||||||
this._lastRoomPassword = null;
|
this._lastRoomPassword = null;
|
||||||
this.clearPersistedRoom();
|
this.clearPersistedRoom();
|
||||||
|
this.clearPersistedPosition();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +178,30 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private scheduleRoomIdClear(): void
|
||||||
|
{
|
||||||
|
if(this._pendingRoomClear)
|
||||||
|
{
|
||||||
|
clearTimeout(this._pendingRoomClear);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pendingRoomClear = setTimeout(() =>
|
||||||
|
{
|
||||||
|
this._pendingRoomClear = null;
|
||||||
|
this._lastRoomId = -1;
|
||||||
|
this._lastRoomPassword = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelRoomIdClear(): void
|
||||||
|
{
|
||||||
|
if(this._pendingRoomClear)
|
||||||
|
{
|
||||||
|
clearTimeout(this._pendingRoomClear);
|
||||||
|
this._pendingRoomClear = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private attemptRoomReEntry(): void
|
private attemptRoomReEntry(): void
|
||||||
{
|
{
|
||||||
const roomId = this._lastRoomId;
|
const roomId = this._lastRoomId;
|
||||||
@@ -127,14 +209,62 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
if(roomId <= 0)
|
if(roomId <= 0)
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] No room to re-enter (lastRoomId=' + roomId + '), dropping guard');
|
||||||
this._isReconnecting = false;
|
this._isReconnecting = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we already have a session for this room (seamless reconnection).
|
||||||
|
// The server-side SessionResumeManager kept the habbo alive in the room
|
||||||
|
// during the grace period. The client's room view is still rendered behind
|
||||||
|
// the reconnection overlay. Instead of tearing it down and rebuilding,
|
||||||
|
// just drop the guard so the room view "unfreezes" in place.
|
||||||
|
const existingSession = this.getSession(roomId);
|
||||||
|
|
||||||
|
if(existingSession)
|
||||||
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] Existing session found for room ' + roomId + ' — sending room enter request');
|
||||||
|
|
||||||
|
// Re-send room enter request to the server. This handles two cases:
|
||||||
|
// 1. Session resume (habbo still in room on server): server treats it
|
||||||
|
// as a no-op or re-entry to the same room — harmless.
|
||||||
|
// 2. Server restart (habbo not in any room): server places the habbo
|
||||||
|
// in the room so the client view matches server state.
|
||||||
|
GetCommunication().connection.send(new RoomEnterComposer(roomId, password));
|
||||||
|
|
||||||
|
// Keep the guard up briefly to absorb any stray server-side redirects
|
||||||
|
// (DesktopViewEvent, etc.) from the login packet sequence, then drop it.
|
||||||
|
this.clearGuardTimer();
|
||||||
|
this._reconnectGuardTimer = setTimeout(() =>
|
||||||
|
{
|
||||||
|
this._reconnectGuardTimer = null;
|
||||||
|
|
||||||
|
if(this._isReconnecting)
|
||||||
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] Session resume guard timeout - dropping guard');
|
||||||
|
this._isReconnecting = false;
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NitroLogger.log('[RoomSessionManager] Re-entering room ' + roomId);
|
||||||
|
|
||||||
|
// No existing session — full room entry (e.g. page reload restore)
|
||||||
this._sessions.clear();
|
this._sessions.clear();
|
||||||
this._viewerSession = null;
|
this._viewerSession = null;
|
||||||
|
|
||||||
|
// Send the room enter request. The guard stays active to block
|
||||||
|
// DesktopViewEvent / home room redirects from the server's login sequence.
|
||||||
this.createSession(roomId, password);
|
this.createSession(roomId, password);
|
||||||
|
|
||||||
|
// Keep the guard up for a generous window to absorb any DesktopViewEvent
|
||||||
|
// or other server-side redirects that arrive after authentication.
|
||||||
|
// The guard drops when:
|
||||||
|
// 1. RS_CONNECTED/RS_READY fires (positive room entry confirmation), OR
|
||||||
|
// 2. This safety timeout expires (10 seconds)
|
||||||
this.clearGuardTimer();
|
this.clearGuardTimer();
|
||||||
this._reconnectGuardTimer = setTimeout(() =>
|
this._reconnectGuardTimer = setTimeout(() =>
|
||||||
{
|
{
|
||||||
@@ -142,11 +272,19 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
if(this._isReconnecting)
|
if(this._isReconnecting)
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] Guard timeout (10s) - dropping guard');
|
||||||
this._isReconnecting = false;
|
this._isReconnecting = false;
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on page load (from MainView). Checks sessionStorage for a
|
||||||
|
* persisted room ID from a network disconnect and enters it instead
|
||||||
|
* of following the normal home room / hotel view flow.
|
||||||
|
*
|
||||||
|
* Returns true if a room restore was initiated.
|
||||||
|
*/
|
||||||
public tryRestoreSession(): boolean
|
public tryRestoreSession(): boolean
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -161,10 +299,15 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
const password = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
|
const password = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
|
||||||
|
|
||||||
|
NitroLogger.log('[RoomSessionManager] Restoring session for room ' + roomId + ' from sessionStorage');
|
||||||
|
|
||||||
|
// Set the guard so DesktopViewEvent from the server's login sequence
|
||||||
|
// doesn't kick us to hotel view before we enter the room
|
||||||
this._isReconnecting = true;
|
this._isReconnecting = true;
|
||||||
|
|
||||||
this.createSession(roomId, password);
|
this.createSession(roomId, password);
|
||||||
|
|
||||||
|
// Drop the guard when room entry succeeds or after timeout
|
||||||
this.clearGuardTimer();
|
this.clearGuardTimer();
|
||||||
this._reconnectGuardTimer = setTimeout(() =>
|
this._reconnectGuardTimer = setTimeout(() =>
|
||||||
{
|
{
|
||||||
@@ -172,6 +315,7 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
if(this._isReconnecting)
|
if(this._isReconnecting)
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] Restore guard timeout (10s) - dropping guard');
|
||||||
this._isReconnecting = false;
|
this._isReconnecting = false;
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@@ -206,7 +350,10 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
this.clearPersistedRoom();
|
this.clearPersistedRoom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{
|
||||||
|
// sessionStorage not available (private browsing, etc.) - fail silently
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearPersistedRoom(): void
|
private clearPersistedRoom(): void
|
||||||
@@ -215,8 +362,70 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
{
|
{
|
||||||
sessionStorage.removeItem(STORAGE_KEY_ROOM_ID);
|
sessionStorage.removeItem(STORAGE_KEY_ROOM_ID);
|
||||||
sessionStorage.removeItem(STORAGE_KEY_ROOM_PASSWORD);
|
sessionStorage.removeItem(STORAGE_KEY_ROOM_PASSWORD);
|
||||||
|
// Note: position keys (POS_X, POS_Y) are NOT cleared here.
|
||||||
|
// They persist across the disconnect→reconnect cycle and are
|
||||||
|
// consumed by walkToSavedPosition() after successful re-entry.
|
||||||
}
|
}
|
||||||
catch(e) {}
|
catch(e)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearPersistedPosition(): void
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sessionStorage.removeItem(STORAGE_KEY_POS_X);
|
||||||
|
sessionStorage.removeItem(STORAGE_KEY_POS_Y);
|
||||||
|
sessionStorage.removeItem('nitro.session.posLocked');
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private snapshotSavedPosition(): void
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const posX = sessionStorage.getItem(STORAGE_KEY_POS_X);
|
||||||
|
const posY = sessionStorage.getItem(STORAGE_KEY_POS_Y);
|
||||||
|
|
||||||
|
if(!posX || !posY) return;
|
||||||
|
|
||||||
|
this._savedPosX = parseInt(posX, 10);
|
||||||
|
this._savedPosY = parseInt(posY, 10);
|
||||||
|
|
||||||
|
// Lock position saving so room re-entry doesn't overwrite saved position
|
||||||
|
sessionStorage.setItem('nitro.session.posLocked', '1');
|
||||||
|
|
||||||
|
NitroLogger.log('[RoomSessionManager] Snapshot saved position (' + this._savedPosX + ', ' + this._savedPosY + ')');
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
this._savedPosX = -1;
|
||||||
|
this._savedPosY = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private walkToSavedPosition(): void
|
||||||
|
{
|
||||||
|
const x = this._savedPosX;
|
||||||
|
const y = this._savedPosY;
|
||||||
|
|
||||||
|
// Reset after use
|
||||||
|
this._savedPosX = -1;
|
||||||
|
this._savedPosY = -1;
|
||||||
|
|
||||||
|
// Unlock position saving so normal movement is tracked again
|
||||||
|
try { sessionStorage.removeItem('nitro.session.posLocked'); } catch(e) { /* ignore */ }
|
||||||
|
|
||||||
|
if(x < 0 || y < 0 || isNaN(x) || isNaN(y)) return;
|
||||||
|
|
||||||
|
NitroLogger.log('[RoomSessionManager] Walking to saved position (' + x + ', ' + y + ')');
|
||||||
|
GetCommunication().connection.send(new RoomUnitWalkComposer(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
private setHandlers(session: IRoomSession): void
|
private setHandlers(session: IRoomSession): void
|
||||||
@@ -271,6 +480,7 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
this._viewerSession = roomSession;
|
this._viewerSession = roomSession;
|
||||||
|
|
||||||
|
// Track room for reconnection (memory + sessionStorage)
|
||||||
this._lastRoomId = roomSession.roomId;
|
this._lastRoomId = roomSession.roomId;
|
||||||
this._lastRoomPassword = roomSession.password;
|
this._lastRoomPassword = roomSession.password;
|
||||||
this.persistRoom(roomSession.roomId, roomSession.password);
|
this.persistRoom(roomSession.roomId, roomSession.password);
|
||||||
@@ -306,20 +516,29 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
if(!session) return;
|
if(!session) return;
|
||||||
|
|
||||||
this._sessions.delete(this.getRoomId(id));
|
// During reconnection, block BOTH the session map deletion AND the ENDED event.
|
||||||
|
// This preserves the session so attemptRoomReEntry can find it, and prevents
|
||||||
if(openLandingView && !this._isReconnecting)
|
// the UI from flashing hotel view during the reconnection flow.
|
||||||
{
|
|
||||||
this._lastRoomId = -1;
|
|
||||||
this._lastRoomPassword = null;
|
|
||||||
this.clearPersistedRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this._isReconnecting)
|
if(this._isReconnecting)
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] removeSession fully blocked by reconnect guard (room=' + id + ', openLandingView=' + openLandingView + ')');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._sessions.delete(this.getRoomId(id));
|
||||||
|
|
||||||
|
if(openLandingView)
|
||||||
|
{
|
||||||
|
// Don't clear _lastRoomId immediately. During server shutdown the server
|
||||||
|
// sends DesktopViewEvent (which triggers removeSession) BEFORE closing the
|
||||||
|
// socket. If we clear the room ID now, the SOCKET_RECONNECTING handler
|
||||||
|
// won't know which room to re-enter. Instead, delay the clear so that
|
||||||
|
// SOCKET_RECONNECTING can cancel it and preserve the room info.
|
||||||
|
this.clearPersistedRoom();
|
||||||
|
this.scheduleRoomIdClear();
|
||||||
|
}
|
||||||
|
|
||||||
GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.ENDED, session, openLandingView));
|
GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.ENDED, session, openLandingView));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,30 +548,60 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
if(!session)
|
if(!session)
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] sessionUpdate(' + type + ') - no session found for id ' + id);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(type)
|
switch(type)
|
||||||
{
|
{
|
||||||
case RoomSessionHandler.RS_CONNECTED:
|
case RoomSessionHandler.RS_CONNECTED:
|
||||||
|
NitroLogger.log('[RoomSessionManager] RS_CONNECTED for room ' + id);
|
||||||
|
|
||||||
|
// Positive signal: we successfully entered the room.
|
||||||
|
// Do NOT drop the guard yet — the server's login sequence may still
|
||||||
|
// send a DesktopViewEvent that arrives after this. Keep the guard up
|
||||||
|
// and let RS_READY (or the existing timeout) handle the final drop.
|
||||||
if(this._isReconnecting)
|
if(this._isReconnecting)
|
||||||
{
|
{
|
||||||
this.clearGuardTimer();
|
NitroLogger.log('[RoomSessionManager] Room entry confirmed - guard stays up until RS_READY');
|
||||||
this._isReconnecting = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case RoomSessionHandler.RS_READY:
|
case RoomSessionHandler.RS_READY:
|
||||||
|
NitroLogger.log('[RoomSessionManager] RS_READY for room ' + id);
|
||||||
|
|
||||||
|
// Room is fully loaded. Keep the guard up for a short grace period
|
||||||
|
// to absorb any late DesktopViewEvent from the server's login sequence,
|
||||||
|
// then drop it. This prevents a race where the login sequence's
|
||||||
|
// DesktopViewEvent arrives after the room entry confirmation.
|
||||||
if(this._isReconnecting)
|
if(this._isReconnecting)
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] Room ready confirmed - dropping guard in 3s');
|
||||||
|
|
||||||
|
// Walk to the saved position (where the user was before disconnect).
|
||||||
|
// Delay briefly so the server finishes placing the avatar in the room.
|
||||||
|
setTimeout(() => this.walkToSavedPosition(), 1000);
|
||||||
|
|
||||||
this.clearGuardTimer();
|
this.clearGuardTimer();
|
||||||
this._isReconnecting = false;
|
this._reconnectGuardTimer = setTimeout(() =>
|
||||||
|
{
|
||||||
|
this._reconnectGuardTimer = null;
|
||||||
|
|
||||||
|
if(this._isReconnecting)
|
||||||
|
{
|
||||||
|
NitroLogger.log('[RoomSessionManager] Post-ready grace period elapsed - dropping guard');
|
||||||
|
this._isReconnecting = false;
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case RoomSessionHandler.RS_DISCONNECTED:
|
case RoomSessionHandler.RS_DISCONNECTED:
|
||||||
|
NitroLogger.log('[RoomSessionManager] RS_DISCONNECTED for room ' + id + ' (isReconnecting=' + this._isReconnecting + ')');
|
||||||
|
|
||||||
|
// During reconnection, don't process server-side disconnects
|
||||||
|
// (DesktopViewEvent / home room redirect) - we'll re-enter the room
|
||||||
if(this._isReconnecting) return;
|
if(this._isReconnecting) return;
|
||||||
|
|
||||||
this.removeSession(id);
|
this.removeSession(id);
|
||||||
@@ -372,6 +621,7 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
|
|
||||||
this._sessions.set(this.getRoomId(toRoomId), existing);
|
this._sessions.set(this.getRoomId(toRoomId), existing);
|
||||||
|
|
||||||
|
// Update tracked room
|
||||||
this._lastRoomId = toRoomId;
|
this._lastRoomId = toRoomId;
|
||||||
this.persistRoom(toRoomId, existing.password);
|
this.persistRoom(toRoomId, existing.password);
|
||||||
|
|
||||||
@@ -387,4 +637,9 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
|
|||||||
{
|
{
|
||||||
return this._viewerSession;
|
return this._viewerSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isReconnecting(): boolean
|
||||||
|
{
|
||||||
|
return this._isReconnecting;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IConnection, IRoomHandlerListener } from '@nitrots/api';
|
import { IConnection, IRoomHandlerListener } from '@nitrots/api';
|
||||||
import { DesktopViewEvent, FlatAccessDeniedMessageEvent, GoToFlatMessageComposer, RoomDoorbellAcceptedEvent, RoomEnterEvent, RoomReadyMessageEvent, YouAreSpectatorMessageEvent } from '@nitrots/communication';
|
import { DesktopViewEvent, FlatAccessDeniedMessageEvent, GoToFlatMessageComposer, RoomDoorbellAcceptedEvent, RoomEnterEvent, RoomReadyMessageEvent, YouAreSpectatorMessageEvent } from '@nitrots/communication';
|
||||||
import { GetEventDispatcher, RoomSessionDoorbellEvent, RoomSessionSpectatorModeEvent } from '@nitrots/events';
|
import { GetEventDispatcher, RoomSessionDoorbellEvent, RoomSessionSpectatorModeEvent } from '@nitrots/events';
|
||||||
|
import { NitroLogger } from '@nitrots/utils';
|
||||||
import { BaseHandler } from './BaseHandler';
|
import { BaseHandler } from './BaseHandler';
|
||||||
|
|
||||||
export class RoomSessionHandler extends BaseHandler
|
export class RoomSessionHandler extends BaseHandler
|
||||||
@@ -46,6 +47,8 @@ export class RoomSessionHandler extends BaseHandler
|
|||||||
{
|
{
|
||||||
if(!(event instanceof DesktopViewEvent)) return;
|
if(!(event instanceof DesktopViewEvent)) return;
|
||||||
|
|
||||||
|
NitroLogger.log('[RoomSessionHandler] DesktopViewEvent received (roomId=' + this.roomId + ')');
|
||||||
|
|
||||||
if(this.listener) this.listener.sessionUpdate(this.roomId, RoomSessionHandler.RS_DISCONNECTED);
|
if(this.listener) this.listener.sessionUpdate(this.roomId, RoomSessionHandler.RS_DISCONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +88,7 @@ export class RoomSessionHandler extends BaseHandler
|
|||||||
|
|
||||||
if(!username || !username.length)
|
if(!username || !username.length)
|
||||||
{
|
{
|
||||||
|
NitroLogger.log('[RoomSessionHandler] FlatAccessDenied (empty username) → RS_DISCONNECTED (roomId=' + this.roomId + ')');
|
||||||
this.listener.sessionUpdate(this.roomId, RoomSessionHandler.RS_DISCONNECTED);
|
this.listener.sessionUpdate(this.roomId, RoomSessionHandler.RS_DISCONNECTED);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user