import ByteBuffer from "bytebuffer" ; import { EventEmitter } from "events" ; import { NetConnectOpts , Socket } from "net" ; import { Transform , TransformCallback , Writable } from "stream" ; import { unzip } from "zlib" ; import { Coder } from "./coders" ; import { PacketRegistryEntry , Side , PacketMetadata } from "./packet" ; export type State = keyof States ; interface States { handshake : PacketCoders ; login : PacketCoders ; status : PacketCoders ; play : PacketCoders ; } /**
 * The channel for send and listen the Minecraft packet.
 */ export class Channel extends EventEmitter { state : State = "handshake" ; private readonly states = { client : { handshake : new PacketCoders ( ) , login : new PacketCoders ( ) , status : new PacketCoders ( ) , play : new PacketCoders ( ) , } , server : { handshake : new PacketCoders ( ) , login : new PacketCoders ( ) , status : new PacketCoders ( ) , play : new PacketCoders ( ) , } , } ; private connection : Socket = new Socket ( { allowHalfOpen : false } ) ; private outbound : Writable ; private inbound : Writable ; private enableCompression : boolean = false ; private compressionThreshold : number = - 1 ; constructor ( ) { super ( ) ; const self = this ; this . outbound = new MinecraftPacketEncoder ( this ) ; this . outbound . pipe ( new MinecraftPacketOutbound ( ) ) . pipe ( this . connection ) ; this . inbound = new MinecraftPacketInBound ( ) ; this . inbound . pipe ( new PacketDecompress ( { get enableCompression ( ) { return self . enableCompression ; } , get compressionThreshold ( ) { return self . compressionThreshold ; } , } ) ) . pipe ( new MinecraftPacketDecoder ( this ) ) . pipe ( new PacketEmitter ( this ) ) ; this . connection . pipe ( this . inbound ) ; } /**
     * Is the connection ready to read and write
     */ get ready ( ) { return this . connection . readable && this . connection . writable ; } findCoderById ( packetId , side ) { const all = this . states [ side ] [ this . state ] ; return all . packetIdCoders [ packetId ] ; } getPacketId ( packetInst , side ) { const packetName = Object . getPrototypeOf ( packetInst ) . constructor . name ; const all = this . states [ side ] [ this . state ] ; return all . packetNameToId [ packetName ] ; } registerPacketType ( clazz ) { const entry = clazz . prototype [ PacketMetadata ] ; const { state , side , id , name , coder } = entry ; const coders = this . states [ side ] [ state ] ; coders . packetIdCoders [ id ] = coder ; coders . packetNameToId [ name ] = id ; } registerPacket ( entry ) { const { state , side , id , name , coder } = entry ; const coders = this . states [ side ] [ state ] ; coders . packetIdCoders [ id ] = coder ; coders . packetNameToId [ name ] = id ; } /**
     * Open the connection and start to listen the port.
     */ async listen ( option ) { await new Promise < void > ( ( resolve , reject ) => { this . connection . connect ( option , ( ) => { resolve ( ) ; } ) ; if ( option . timeout ) { this . connection . setTimeout ( option . timeout ) ; } if ( option . keepalive ) { this . connection . setKeepAlive ( true , typeof option . keepalive === "boolean" ? 3500 : option . keepalive ) ; } this . connection . once ( "error" , ( e ) => { reject ( e ) ; } ) ; this . connection . once ( "timeout" , ( ) => { reject ( new Error ( "Connection timeout." ) ) ; } ) ; } ) ; this . connection . on ( "error" , ( e ) => { this . emit ( "error" , e ) ; } ) ; this . emit ( "listen" ) ; } disconnect ( ) { if ( ! this . ready ) { return Promise . resolve ( ) ; } return new Promise < void > ( ( resolve , reject ) => { this . connection . once ( "close" , ( err ) => { if ( err ) { reject ( err ) ; } else { resolve ( ) ; } } ) ; this . connection . end ( ) ; } ) ; } /**
     * Sent a packet to server.
     */ send < T > ( message , skeleton ? ) { if ( ! this . connection . writable ) { throw new Error ( "Cannot write if the connection isn't writable!" ) ; } if ( skeleton ) { Object . assign ( message , skeleton ) ; } this . outbound . write ( message ) ; this . emit ( "send" , message ) ; } /**
     * Listen for sepcific packet by its class name.
     */ onPacket < T > ( packet , listener ) { return this . on ( `packet:${ packet . name }` , listener ) ; } oncePacket < T > ( packet , listener ) { return this . once ( `packet:${ packet . name }` , listener ) ; } } export interface Channel extends EventEmitter { on < T > ( channel , listener ) : this ; once < T > ( channel , listener ) : this ; } export abstract class PacketInBound extends Transform { private buffer : ByteBuffer = ByteBuffer . allocate ( 1024 ) ; protected abstract readPacketLength ( bb ) ; _transform ( chunk , encoding , callback ) { this . buffer . ensureCapacity ( chunk . length + this . buffer . offset ) ; this . buffer . append ( chunk ) ; this . buffer . flip ( ) ; let unresolvedBytes ; do { const packetLength = this . readPacketLength ( this . buffer ) ; unresolvedBytes = this . buffer . remaining ( ) ; if ( packetLength <= unresolvedBytes ) { const result = Buffer . alloc ( packetLength ) ; this . buffer . buffer . copy ( result , 0 , this . buffer . offset , this . buffer . offset + packetLength ) ; this . push ( result ) ; this . buffer . buffer . copyWithin ( 0 , packetLength ) ; this . buffer . offset = 0 ; this . buffer . limit -= packetLength ; unresolvedBytes -= packetLength ; } else { this . buffer . offset = this . buffer . limit ; this . buffer . limit = this . buffer . capacity ( ) ; break ; } } while ( unresolvedBytes > 0 ) ; callback ( ) ; } } class MinecraftPacketInBound extends PacketInBound { protected readPacketLength ( bb ) { return bb . readVarint32 ( ) ; } } class PacketDecompress extends Transform { constructor ( private option ) { super ( ) ; } _transform ( chunk , encoding , callback ) { if ( ! this . option . enableCompression ) { this . push ( chunk ) ; callback ( ) ; return ; } const message = ByteBuffer . wrap ( chunk ) ; const dataLength = message . readVarint32 ( ) ; if ( dataLength === 0 || dataLength < this . option . compressionThreshold ) { this . push ( message . buffer . slice ( message . offset ) ) ; callback ( ) ; } else { const compressedContent = message . buffer . slice ( message . offset ) ; unzip ( compressedContent , ( err , result ) => { if ( err ) { callback ( err ) ; } else { this . push ( result ) ; callback ( ) ; } } ) ; } } } export interface PacketRegistry { findCoderById ( packetId , side ) : Coder < any > ; getPacketId ( message , side ) : number ; } export abstract class PacketDecoder extends Transform { constructor ( private client ) { super ( { writableObjectMode : true , readableObjectMode : true } ) ; } abstract readPacketId ( message ) ; _transform ( chunk , encoding , callback ) { const message = ByteBuffer . wrap ( chunk ) ; const packetId = this . readPacketId ( message ) ; const packetContent = message . slice ( ) ; const coder = this . client . findCoderById ( packetId , "server" ) ; if ( coder ) { this . push ( coder . decode ( packetContent ) ) ; } else { console . error ( `Unknown packet ${ packetId } : ${ packetContent . buffer }.` ) ; } callback ( ) ; } } class MinecraftPacketDecoder extends PacketDecoder { readPacketId ( message ) { return message . readVarint32 ( ) ; } } export class PacketEmitter extends Writable { constructor ( private eventBus ) { super ( { objectMode : true } ) ; } _write ( inst , encoding , callback ) { this . eventBus . emit ( `packet:${ Object . getPrototypeOf ( inst ) . constructor . name }` , inst ) ; callback ( ) ; } } export abstract class PacketEncoder extends Transform { constructor ( private client ) { super ( { writableObjectMode : true , readableObjectMode : true } ) ; } protected abstract writePacketId ( bb , id ) ; _transform ( message , encoding , callback ) { const id = this . client . getPacketId ( message , "client" ) ; const coder = this . client . findCoderById ( id , "client" ) ; if ( coder && coder . encode ) { const buf = new ByteBuffer ( ) ; this . writePacketId ( buf , id ) ; coder . encode ( buf , message , this . client ) ; buf . flip ( ) ; this . push ( buf . buffer . slice ( 0 , buf . limit ) ) ; callback ( ) ; } else { callback ( new Error ( `Cannot find coder for message. ${ JSON . stringify ( message ) }` ) ) ; } } } class MinecraftPacketEncoder extends PacketEncoder { protected writePacketId ( bb , id ) { bb . writeByte ( id ) ; } } export abstract class PacketOutbound extends Transform { protected abstract writePacketLength ( bb , len ) ; _transform ( packet , encoding , callback ) { const buffer = new ByteBuffer ( ) ; this . writePacketLength ( buffer , packet . length ) ; buffer . append ( packet ) ; buffer . flip ( ) ; this . push ( buffer . buffer . slice ( 0 , buffer . limit ) ) ; callback ( ) ; } } class MinecraftPacketOutbound extends PacketOutbound { protected writePacketLength ( bb , len ) { bb . writeVarint32 ( len ) ; } } class PacketCoders { packetIdCoders : { [ packetId ] : Coder < any > ; } = { } ; packetNameToId : { [ name ] : number ; } = { } ; }