🆕 Disconnection handler, when you got disconnected you automatic go back to the room

This commit is contained in:
duckietm
2026-03-19 15:04:47 +01:00
parent eee289e8f2
commit 5aef7a3de2
16 changed files with 498 additions and 87 deletions
@@ -1,7 +1,7 @@
import { ICommunicationManager, IConnection, IMessageConfiguration, IMessageEvent } from '@nitrots/api';
import { GetConfiguration } from '@nitrots/configuration';
import { GetEventDispatcher, NitroEventType } from '@nitrots/events';
import { GetTickerTime } from '@nitrots/utils';
import { GetEventDispatcher, NitroEvent, NitroEventType } from '@nitrots/events';
import { GetTickerTime, NitroLogger } from '@nitrots/utils';
import { NitroMessages } from './NitroMessages';
import { SocketConnection } from './SocketConnection';
import { AuthenticatedEvent, ClientHelloMessageComposer, ClientPingEvent, InfoRetrieveMessageComposer, PongMessageComposer, SSOTicketMessageComposer, UniqueIDMessageComposer } from './messages';
@@ -17,7 +17,11 @@ export class CommunicationManager implements ICommunicationManager
private _socketClosedCallback: () => void = null;
private _socketOpenedCallback: () => void = null;
private _socketErrorCallback: () => void = null;
private _socketReconnectedCallback: () => void = null;
private _machineId: string = null;
private _initResolved: boolean = false;
private getGpu(): string {
const e = document.createElement('canvas');
let t, s, i, r;
@@ -41,7 +45,7 @@ export class CommunicationManager implements ICommunicationManager
return '<mathroutines>Error</mathroutines>';
}
}
private getCanvas(): any {
const e = document.createElement('canvas'), t = e.getContext('2d'), userAgent = navigator.userAgent, screenInfo = '${window.screen.width}x${window.screen.height}', currentDate = new Date().toString(), s = 'ThiosIsVerrySeCuRe02938883721moreStuff! | ${userAgent} | ${screenInfo} | ${currentDate}';
t.textBaseline = 'top';
@@ -67,24 +71,33 @@ export class CommunicationManager implements ICommunicationManager
}
return r;
}
private generateMachineID(): string {
const fp = new ClientJS();
const uniqueId = fp.getCustomFingerprint(
fp.getAvailableResolution(),
fp.getAvailableResolution(),
fp.getOS(),
fp.getCPU(),
fp.getColorDepth(),
this.getGpu(),
fp.getSilverlightVersion(),
fp.getOSVersion(),
this.getMathResult(),
fp.getCanvasPrint(),
fp.getCPU(),
fp.getColorDepth(),
this.getGpu(),
fp.getSilverlightVersion(),
fp.getOSVersion(),
this.getMathResult(),
fp.getCanvasPrint(),
this.getCanvas()
);
return uniqueId == null ? 'FAILED' : `IID-${uniqueId}`;
}
private sendHandshake(): void
{
if(!this._machineId) this._machineId = this.generateMachineID();
this._connection.send(new ClientHelloMessageComposer(null, null, null, null));
this._connection.send(new SSOTicketMessageComposer(GetConfiguration().getValue('sso.ticket', null), GetTickerTime()));
this._connection.send(new UniqueIDMessageComposer(this._machineId, '', ''));
}
constructor()
{
this._connection.registerMessages(this._messages);
@@ -99,6 +112,17 @@ export class CommunicationManager implements ICommunicationManager
};
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, this._socketClosedCallback);
// Handle reconnection - re-authenticate when socket reconnects
this._socketReconnectedCallback = () =>
{
NitroLogger.log('[CommunicationManager] Socket reconnected, re-authenticating...');
if(GetConfiguration().getValue<boolean>('system.pong.manually', false)) this.startPong();
this.sendHandshake();
};
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTED, this._socketReconnectedCallback);
return new Promise((resolve, reject) =>
{
// Store callback for cleanup
@@ -106,18 +130,14 @@ export class CommunicationManager implements ICommunicationManager
{
if(GetConfiguration().getValue<boolean>('system.pong.manually', false)) this.startPong();
const machineId = this.generateMachineID();
this._connection.send(new ClientHelloMessageComposer(null, null, null, null));
this._connection.send(new SSOTicketMessageComposer(GetConfiguration().getValue('sso.ticket', null), GetTickerTime()));
this._connection.send(new UniqueIDMessageComposer(machineId, '', ''));
this.sendHandshake();
};
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_OPENED, this._socketOpenedCallback);
// Store callback for cleanup
this._socketErrorCallback = () =>
{
reject();
if(!this._initResolved) reject();
};
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_ERROR, this._socketErrorCallback);
@@ -125,11 +145,30 @@ export class CommunicationManager implements ICommunicationManager
const pingEvent = new ClientPingEvent((event: ClientPingEvent) => this.sendPong());
const authEvent = new AuthenticatedEvent((event: AuthenticatedEvent) =>
{
const isReconnect = this._initResolved;
NitroLogger.log('[CommunicationManager] AuthenticatedEvent received (isReconnect=' + isReconnect + ')');
this._connection.authenticated();
resolve();
if(!this._initResolved)
{
this._initResolved = true;
resolve();
}
if(isReconnect)
{
this._connection.ready();
}
event.connection.send(new InfoRetrieveMessageComposer());
if(isReconnect)
{
NitroLogger.log('[CommunicationManager] Dispatching SOCKET_REAUTHENTICATED');
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_REAUTHENTICATED));
}
});
this._messageEvents.push(pingEvent, authEvent);
@@ -164,6 +203,12 @@ export class CommunicationManager implements ICommunicationManager
this._socketErrorCallback = null;
}
if(this._socketReconnectedCallback)
{
GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_RECONNECTED, this._socketReconnectedCallback);
this._socketReconnectedCallback = null;
}
// Remove message events
for(const event of this._messageEvents)
{
File diff suppressed because one or more lines are too long
+171 -18
View File
@@ -1,5 +1,5 @@
import { ICodec, IConnection, IMessageComposer, IMessageConfiguration, IMessageDataWrapper, IMessageEvent, WebSocketEventEnum } from '@nitrots/api';
import { GetEventDispatcher, NitroEvent, NitroEventType } from '@nitrots/events';
import { GetEventDispatcher, NitroEvent, NitroEventType, ReconnectEvent } from '@nitrots/events';
import { NitroLogger } from '@nitrots/utils';
import { EvaWireFormat } from './codec';
import { MessageClassManager } from './messages';
@@ -23,19 +23,39 @@ export class SocketConnection implements IConnection
private _onErrorCallback: (event: Event) => void = null;
private _onMessageCallback: (event: MessageEvent) => void = null;
// Reconnection state
private _socketUrl: string = null;
private _reconnectAttempt: number = 0;
private _reconnectTimer: ReturnType<typeof setTimeout> = null;
private _isReconnecting: boolean = false;
private _intentionalClose: boolean = false;
private _wasAuthenticated: boolean = false;
public static readonly MAX_RECONNECT_ATTEMPTS: number = 7;
public static readonly BASE_RECONNECT_DELAY_MS: number = 1000;
public static readonly MAX_RECONNECT_DELAY_MS: number = 30000;
public init(socketUrl: string): void
{
if(!socketUrl || !socketUrl.length) return;
this._socketUrl = socketUrl;
this._intentionalClose = false;
this.createSocket(socketUrl);
}
private createSocket(socketUrl: string): void
{
this._dataBuffer = new ArrayBuffer(0);
this._socket = new WebSocket(socketUrl);
this._socket.binaryType = 'arraybuffer';
// Store callbacks for cleanup
this._onOpenCallback = () => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_OPENED));
this._onCloseCallback = () => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_CLOSED));
this._onErrorCallback = () => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_ERROR));
this._onOpenCallback = () => this.onSocketOpened();
this._onCloseCallback = (event: Event) => this.onSocketClosed(event as CloseEvent);
this._onErrorCallback = () => this.onSocketError();
this._onMessageCallback = (event: MessageEvent) =>
{
this._dataBuffer = this.concatArrayBuffers(this._dataBuffer, event.data);
@@ -48,29 +68,152 @@ export class SocketConnection implements IConnection
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_MESSAGE, this._onMessageCallback);
}
public dispose(): void
private onSocketOpened(): void
{
if(this._socket)
if(this._isReconnecting)
{
// Remove all event listeners
if(this._onOpenCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_OPENED, this._onOpenCallback);
if(this._onCloseCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_CLOSED, this._onCloseCallback);
if(this._onErrorCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_ERROR, this._onErrorCallback);
if(this._onMessageCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_MESSAGE, this._onMessageCallback);
NitroLogger.log('[SocketConnection] Reconnected successfully after ' + this._reconnectAttempt + ' attempt(s)');
// Close socket if still open
if(this._socket.readyState === WebSocket.OPEN || this._socket.readyState === WebSocket.CONNECTING)
{
this._socket.close();
}
this._reconnectAttempt = 0;
this._isReconnecting = false;
this._socket = null;
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_RECONNECTED));
}
else
{
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_OPENED));
}
}
private onSocketClosed(event: CloseEvent): void
{
NitroLogger.log('[SocketConnection] Socket closed, code: ' + (event?.code ?? 'unknown') + ', reason: ' + (event?.reason || 'none'));
if(this._intentionalClose)
{
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_CLOSED));
return;
}
const code = event?.code ?? 0;
if(code === 1000 || code === 1001)
{
NitroLogger.log('[SocketConnection] Server closed cleanly (code ' + code + ') - not reconnecting');
this._isAuthenticated = false;
this._isReady = false;
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_CLOSED));
return;
}
if(this._isAuthenticated) this._wasAuthenticated = true;
this._isAuthenticated = false;
this._isReady = false;
this._pendingClientMessages = [];
this._pendingServerMessages = [];
this.attemptReconnect();
}
private onSocketError(): void
{
if(this._isReconnecting)
{
NitroLogger.log('[SocketConnection] Reconnect attempt ' + this._reconnectAttempt + ' failed');
return;
}
if(!this._wasAuthenticated && !this._isAuthenticated)
{
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_ERROR));
}
}
private attemptReconnect(): void
{
if(this._reconnectAttempt >= SocketConnection.MAX_RECONNECT_ATTEMPTS)
{
NitroLogger.log('[SocketConnection] Max reconnect attempts reached (' + SocketConnection.MAX_RECONNECT_ATTEMPTS + ')');
this._isReconnecting = false;
this._wasAuthenticated = false;
GetEventDispatcher().dispatchEvent(new ReconnectEvent(
NitroEventType.SOCKET_RECONNECT_FAILED,
this._reconnectAttempt,
SocketConnection.MAX_RECONNECT_ATTEMPTS
));
GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_CLOSED));
return;
}
this._isReconnecting = true;
this._reconnectAttempt++;
const delay = Math.min(
SocketConnection.BASE_RECONNECT_DELAY_MS * Math.pow(2, this._reconnectAttempt - 1) + Math.random() * 1000,
SocketConnection.MAX_RECONNECT_DELAY_MS
);
NitroLogger.log('[SocketConnection] Reconnecting in ' + Math.round(delay) + 'ms (attempt ' + this._reconnectAttempt + '/' + SocketConnection.MAX_RECONNECT_ATTEMPTS + ')');
GetEventDispatcher().dispatchEvent(new ReconnectEvent(
NitroEventType.SOCKET_RECONNECTING,
this._reconnectAttempt,
SocketConnection.MAX_RECONNECT_ATTEMPTS
));
this._reconnectTimer = setTimeout(() =>
{
this._reconnectTimer = null;
this.cleanupSocket();
this.createSocket(this._socketUrl);
}, delay);
}
private cleanupSocket(): void
{
if(!this._socket) return;
if(this._onOpenCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_OPENED, this._onOpenCallback);
if(this._onCloseCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_CLOSED, this._onCloseCallback);
if(this._onErrorCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_ERROR, this._onErrorCallback);
if(this._onMessageCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_MESSAGE, this._onMessageCallback);
if(this._socket.readyState === WebSocket.OPEN || this._socket.readyState === WebSocket.CONNECTING)
{
try { this._socket.close(); } catch(e) { /* ignore */ }
}
this._socket = null;
this._onOpenCallback = null;
this._onCloseCallback = null;
this._onErrorCallback = null;
this._onMessageCallback = null;
}
public dispose(): void
{
this._intentionalClose = true;
if(this._reconnectTimer)
{
clearTimeout(this._reconnectTimer);
this._reconnectTimer = null;
}
this._isReconnecting = false;
this._reconnectAttempt = 0;
this._wasAuthenticated = false;
this.cleanupSocket();
this._pendingClientMessages = [];
this._pendingServerMessages = [];
@@ -142,7 +285,7 @@ export class SocketConnection implements IConnection
private write(buffer: ArrayBuffer): void
{
if(this._socket.readyState !== WebSocket.OPEN) return;
if(!this._socket || this._socket.readyState !== WebSocket.OPEN) return;
this._socket.send(buffer);
}
@@ -286,6 +429,16 @@ export class SocketConnection implements IConnection
return this._isAuthenticated;
}
public get isReconnecting(): boolean
{
return this._isReconnecting;
}
public get wasAuthenticated(): boolean
{
return this._wasAuthenticated;
}
public get dataBuffer(): ArrayBuffer
{
return this._dataBuffer;
@@ -1,7 +1,6 @@
export class OutgoingHeader
{
public static CLICK_FURNI = 6002;
public static CLICK_USER = 10020;
public static ACHIEVEMENT_LIST = 219;
public static AUTHENTICATION = -1;
@@ -1,21 +0,0 @@
import { IMessageComposer } from '@nitrots/api';
export class ClickUserMessageComposer implements IMessageComposer<ConstructorParameters<typeof ClickUserMessageComposer>>
{
private _data: ConstructorParameters<typeof ClickUserMessageComposer>;
constructor(roomUnitId: number)
{
this._data = [ roomUnitId ];
}
public getMessageArray()
{
return this._data;
}
public dispose(): void
{
return;
}
}
@@ -2,7 +2,6 @@ export * from './BotPlaceComposer';
export * from './BotRemoveComposer';
export * from './BotSkillSaveComposer';
export * from './ClickFurniMessageComposer';
export * from './ClickUserMessageComposer';
export * from './CompostPlantMessageComposer';
export * from './GetItemDataComposer';
export * from './HarvestPetMessageComposer';