MineJSv0.5.1
net/tcp.tstypescript
import { Socket, SocketConstructorOpts } from "node:net";
import { EventEmitter } from "node:events";
import { deflateSync, inflateSync } from "node:zlib";

import { TypedEmmiter } from "../base/event";
import { BinaryDecoder } from "../binary/decoder";
import { BinaryEncoder } from "../binary/encoder";
import { AuthOption } from "../client/auth";
import { ClientStatus, SharedState } from "../world/state";
import { EncodeResult } from "../version/codec";
import { TextComponent } from "../base/typing";
import { NotImplemented, SockerIsNotWritable, UnexpectedValue } from "../base/error";

// TCP related typing
export interface TCPClientOption {
    /** Server host */
    host: string,
    /** Server port */
    port: number,
    /**
     * Server protocol version.
     * 
     * Currentlt support `776` or version `26.2`
     * 
     * @see https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_version_numbers
     */
    protocolVersion: number,
    /** Player name */
    playerName: string,
    /**
     * Send empty `Known Packet` to let server send all Registry data, which may consume a lot of bandwith
     */
    loadRegistry?: boolean,

    debug?: {
        /** Log the incoming packet */
        packetLogger: boolean
    },

    // For premium account
    auth?: AuthOption
}

export interface TCPClientEvents {
    connect: [],
    raw: [buffer: Buffer]
    destroy: [],
}

export interface Message {
    sender: string,
    target: string | undefined,
    content: string,
    raw: {
        sender: TextComponent,
        target: TextComponent | undefined,
        content: TextComponent | undefined
    }
}


// export enum TCPServerIntent {
//     Status = 1,
//     Login = 2,
//     Transfer = 3,
// }

/**
 * Low-level client. You SHOULD NOT use this client unless you know what are you doing
 * 
 * @see https://minecraft.wiki/w/Java_Edition_protocol/Packets
 */
export class TCPClient extends (EventEmitter as new () => TypedEmmiter<TCPClientEvents>) {
    public readonly socket: Socket;

    // Internal var
    /**
     * | Value | Meaning |
     * |-------|---------|
     * | 0     |  not set yet |
     * | -1    | dont compress |
     * | > 0   | compression threshold |
     */
    private bufferPool: Buffer = Buffer.alloc(0);

    constructor(
        private state: SharedState,
        public readonly option: TCPClientOption,
        socketOption?: SocketConstructorOpts,
    ) {
        super();
        this.socket = new Socket(socketOption);
    }

    public sendInitPacket: () => void = () => { throw new NotImplemented(); };
    public forwardPacket: (packetId: number, decoder: BinaryDecoder) => void = () => { throw new NotImplemented(); };
    public parsePacket: (packetId: string | number, data: object) => EncodeResult = () => { throw new NotImplemented(); };

    /**
     * Connect to server
     */
    public connect() {
        this.state.status = ClientStatus.Connecting;
        const connection = this.socket.connect({
            host: this.option.host,
            port: this.option.port
        });

        connection.on("connect", () => {
            this.emit("connect");
            this.sendInitPacket();
        });
        connection.on("data", (data) => {
            let buf = Buffer.from(data);
            if (this.state.decipher)
                buf = this.state.decipher.update(buf);
            this.emit("raw", Buffer.from(buf));
            this.bufferPool = Buffer.concat([this.bufferPool, buf]);
            this.handlePacket();
        });
        connection.once("end", () => {
            this.emit("destroy");
        });
        connection.once("close", () => {
            this.emit("destroy");
        });
    }

    public disconnect() {
        if (!this.socket.closed)
            this.socket.destroy();
    }

    /*
    * Read packet
    */

    private handlePacket() {
        while (this.bufferPool.length > 0) {
            let decoder = new BinaryDecoder(this.bufferPool);
            let packetLength: number;
            try {
                packetLength = decoder.readVarInt();
            } catch (e) {
                break; // Not enough data to read VarInt
            }
            if (this.bufferPool.length - decoder.offset < packetLength) break;

            const expectedPacketEnd = decoder.offset + packetLength;

            let packetID: number;
            if (this.state.compressionThreshold === 0)
                packetID = decoder.readVarInt();
            else if (this.state.compressionThreshold === -1) {
                decoder.readVarInt();
                packetID = decoder.readVarInt();
            } else {
                const dataLength = decoder.readVarInt();
                if (dataLength === 0) {
                    packetID = decoder.readVarInt();
                } else {
                    const dataBuffer = this.bufferPool.subarray(decoder.offset, expectedPacketEnd);
                    const decompressed = inflateSync(dataBuffer);
                    if (decompressed.length !== dataLength)
                        throw new UnexpectedValue("decompressed packet data", decompressed.length.toString(), dataLength.toString());
                    decoder = new BinaryDecoder(decompressed);
                    packetID = decoder.readVarInt();
                }
            }
            if (this.state.clientOptions.debug?.packetLogger == true)
                console.log("packet", packetID, "length", decoder.buffer.length);
            this.forwardPacket(packetID, decoder);

            this.bufferPool = this.bufferPool.subarray(expectedPacketEnd);
        }
    }

    /*
     * Send packet
     */
    public write(buf: Buffer) {
        if (!this.socket.writable)
            throw new SockerIsNotWritable();

        let sendBuffer = buf;
        if (this.state.cipher)
            sendBuffer = this.state.cipher.update(sendBuffer);

        this.socket.write(sendBuffer);
    }

    public sendPacket(packetId: number | string, data: Record<string, any>) {
        const { buffer: content, packet } = this.parsePacket(packetId, data);
        const encodePacketId = new BinaryEncoder();
        encodePacketId.writeVarInt(packet.id);
        const sendData = Buffer.concat([encodePacketId.getBuffer(), content]);

        let sendingPacket: Buffer;
        if (this.state.compressionThreshold !== 0) {
            let data: Buffer;
            let uncompressedLength: number;

            if (this.state.compressionThreshold > 0 && sendData.length > this.state.compressionThreshold) {
                uncompressedLength = sendData.length;
                data = deflateSync(sendData);
            } else {
                uncompressedLength = 0;
                data = sendData;
            }

            const dataLengthEncoder = new BinaryEncoder();
            dataLengthEncoder.writeVarInt(uncompressedLength);
            const dataLengthBuf = dataLengthEncoder.getBuffer();

            const packetLengthEncoder = new BinaryEncoder();
            packetLengthEncoder.writeVarInt(dataLengthBuf.length + data.length);
            sendingPacket = Buffer.concat([packetLengthEncoder.getBuffer(), dataLengthBuf, data]);
        } else {
            const packetLengthEncoder = new BinaryEncoder();
            packetLengthEncoder.writeVarInt(sendData.length);
            sendingPacket = Buffer.concat([packetLengthEncoder.getBuffer(), sendData]);
        }

        this.write(sendingPacket);
    }

}