import { EventEmitter } from "events" ; import net from "net" ; import inject from "reconnect-core" ; import type { CommunicationMessage } from "./communication" ; import { CommunicationType , ConsoleCommunication } from "./communication" ; import type { Connection , ConnectionDetails , ConnectionSettings } from "./types" ; import { ConnectionEvent , ConnectionStatus , Ports } from "./types" ; export const NETWORK_MESSAGE = "HELO\0" ; const DEFAULT_CONNECTION_TIMEOUT_MS = 20000 ; enum CommunicationState { INITIAL = "initial" , LEGACY = "legacy" , NORMAL = "normal" } const defaultConnectionDetails = { consoleNick : "unknown" , gameDataCursor : Uint8Array . from ( [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ) , version : "" , clientToken : 0 , } ; const consoleConnectionOptions = { autoReconnect : true , } ; export type ConsoleConnectionOptions = typeof consoleConnectionOptions ; /**
 * Responsible for maintaining connection to a Slippi relay connection or Wii connection.
 * Events are emitted whenever data is received.
 *
 * Basic usage example:
 *
 * ```javascript
 * const { ConsoleConnection } = require("@slippi/slippi-js");
 *
 * const connection = new ConsoleConnection();
 * connection.connect("localhost", 667); // You should set these values appropriately
 *
 * connection.on("data", (data) => {
 *   // Received data from console
 *   console.log(data);
 * });
 *
 * connection.on("statusChange", (status) => {
 *   console.log(`status changed: ${status}`);
 * });
 * ```
 */ export class ConsoleConnection extends EventEmitter implements Connection { private ipAddress : string ; private port : number ; private isRealtime : boolean ; private connectionStatus = ConnectionStatus . DISCONNECTED ; private connDetails : ConnectionDetails = { ... defaultConnectionDetails } ; private client : net . Socket | null = null ; private connection : inject . Instance < unknown , net . Socket > | null = null ; private options : ConsoleConnectionOptions ; private shouldReconnect = false ; public constructor ( options ? ) { super ( ) ; this . ipAddress = "0.0.0.0" ; this . port = Ports . DEFAULT ; this . isRealtime = false ; this . options = Object . assign ( { } , consoleConnectionOptions , options ) ; } returns public getStatus ( ) { return this . connectionStatus ; } returns public getSettings ( ) { return { ipAddress : this . ipAddress , port : this . port , } ; } returns public getDetails ( ) { return { ... this . connDetails } ; } param ip param port param isRealtime param timeout public connect ( ip , port , isRealtime = false , timeout = DEFAULT_CONNECTION_TIMEOUT_MS ) { this . ipAddress = ip ; this . port = port ; this . isRealtime = isRealtime ; this . _connectOnPort ( ip , port , timeout ) ; } private _connectOnPort ( ip , port , timeout ) { const reconnect = inject ( ( ) => net . connect ( { host : ip , port : port , timeout : timeout , } ) ) ; this . _setStatus ( ConnectionStatus . CONNECTING ) ; const consoleComms = new ConsoleCommunication ( ) ; const connection = reconnect ( { initialDelay : 2000 , maxDelay : 10000 , strategy : "fibonacci" , failAfter : Infinity , } , ( client ) => { this . emit ( ConnectionEvent . CONNECT ) ; this . shouldReconnect = this . options . autoReconnect ; this . client = client ; let commState = CommunicationState . INITIAL ; client . on ( "data" , ( data ) => { if ( commState === CommunicationState . INITIAL ) { commState = this . _getInitialCommState ( data ) ; console . log ( `Connected to ${ ip }:${ port } with type: ${ commState }` ) ; this . _setStatus ( ConnectionStatus . CONNECTED ) ; console . log ( data . toString ( "hex" ) ) ; } if ( commState === CommunicationState . LEGACY ) { this . _handleReplayData ( data ) ; return ; } try { consoleComms . receive ( data ) ; } catch ( err ) { console . error ( "Failed to process new data from server..." , { error : err , prevDataBuf : consoleComms . getReceiveBuffer ( ) , rcvData : data , } ) ; client . destroy ( ) ; this . emit ( ConnectionEvent . ERROR , err ) ; return ; } const messages = consoleComms . getMessages ( ) ; try { messages . forEach ( ( message ) => this . _processMessage ( message ) ) ; } catch ( err ) { console . error ( err ) ; client . destroy ( ) ; this . emit ( ConnectionEvent . ERROR , err ) ; } } ) ; client . on ( "timeout" , ( ) => { console . warn ( `Attempted connection to ${ ip }:${ port } timed out after ${ timeout }ms` ) ; client . destroy ( ) ; } ) ; client . on ( "end" , ( ) => { console . log ( "disconnect" ) ; if ( ! this . shouldReconnect ) { client . destroy ( ) ; } } ) ; client . on ( "close" , ( ) => { console . log ( "connection was closed" ) ; } ) ; const handshakeMsgOut = consoleComms . genHandshakeOut ( this . connDetails . gameDataCursor as Uint8Array , this . connDetails . clientToken ?? 0 , this . isRealtime ) ; client . write ( handshakeMsgOut ) ; } ) ; const setConnectingStatus = ( ) : void => { this . _setStatus ( this . shouldReconnect ? ConnectionStatus . RECONNECT_WAIT : ConnectionStatus . CONNECTING ) ; } ; connection . on ( "connect" , setConnectingStatus ) ; connection . on ( "reconnect" , setConnectingStatus ) ; connection . on ( "disconnect" , ( ) => { if ( ! this . shouldReconnect ) { connection . reconnect = false ; connection . disconnect ( ) ; this . _setStatus ( ConnectionStatus . DISCONNECTED ) ; } } ) ; connection . on ( "error" , ( err ) => { console . warn ( `Connection on port ${ port } encountered an error.` , err ) ; this . _setStatus ( ConnectionStatus . DISCONNECTED ) ; this . emit ( ConnectionEvent . ERROR , `Connection on port ${ port } encountered an error.\n${ err }` ) ; } ) ; this . connection = connection ; connection . connect ( port ) ; } /**
     * Terminate the current connection.
     */ public disconnect ( ) { if ( this . connection ) { this . connection . reconnect = false ; this . connection . disconnect ( ) ; this . connection = null ; } if ( this . client ) { this . client . destroy ( ) ; } } private _getInitialCommState ( data ) { if ( data . length < 13 ) { return CommunicationState . LEGACY ; } const openingBytes = Buffer . from ( [ 0x7b , 0x69 , 0x04 , 0x74 , 0x79 , 0x70 , 0x65 , 0x55 , 0x01 ] ) ; const dataStart = data . slice ( 4 , 13 ) ; return dataStart . equals ( openingBytes ) ? CommunicationState . NORMAL : CommunicationState . LEGACY ; } private _processMessage ( message ) { this . emit ( ConnectionEvent . MESSAGE , message ) ; switch ( message . type ) { case CommunicationType . KEEP_ALIVE : const fakeKeepAlive = Buffer . from ( NETWORK_MESSAGE ) ; this . _handleReplayData ( fakeKeepAlive ) ; break ; case CommunicationType . REPLAY : const readPos = Uint8Array . from ( message . payload . pos ) ; const cmp = Buffer . compare ( this . connDetails . gameDataCursor as Uint8Array , readPos ) ; if ( ! message . payload . forcePos && cmp !== 0 ) { throw new Error ( `Position of received data is incorrect. Expected: ${ this . connDetails . gameDataCursor . toString ( ) }, Received: ${ readPos . toString ( ) }` ) ; } if ( message . payload . forcePos ) { console . warn ( "Overflow occured in Nintendont, data has likely been skipped and replay corrupted. " + "Expected, Received:" , this . connDetails . gameDataCursor , readPos ) ; } this . connDetails . gameDataCursor = Uint8Array . from ( message . payload . nextPos ) ; const data = Uint8Array . from ( message . payload . data ) ; this . _handleReplayData ( data ) ; break ; case CommunicationType . HANDSHAKE : const { nick , nintendontVersion } = message . payload ; if ( nick ) { this . connDetails . consoleNick = nick ; } const tokenBuf = Buffer . from ( message . payload . clientToken ) ; this . connDetails . clientToken = tokenBuf . readUInt32BE ( 0 ) ; if ( nintendontVersion ) { this . connDetails . version = nintendontVersion ; } this . connDetails . gameDataCursor = Uint8Array . from ( message . payload . pos ) ; this . emit ( ConnectionEvent . HANDSHAKE , this . connDetails ) ; break ; default : break ; } } private _handleReplayData ( data ) { this . emit ( ConnectionEvent . DATA , data ) ; } private _setStatus ( status ) { if ( this . connectionStatus !== status ) { this . connectionStatus = status ; this . emit ( ConnectionEvent . STATUS_CHANGE , this . connectionStatus ) ; } } }