MineJSv0.5.1
packet/listener.tstypescript
import zod from "zod";
import { createCipheriv, createDecipheriv, randomBytes, createVerify } from "node:crypto";
import { BlockEntity, ChunkSection, ClientPlayer, ClientStatus, Entity, GameMode, ServerKnownPack, ServerWorld, SharedState } from "../world/state";
import { BinaryDecoder, getTextFromTextComponent } from "../binary/decoder";
import { packBlockPos, SectionsPerChunk } from "../client/static";
import { Vec3 } from "../physics/direction";
import { TypedEmmiter } from "../base/event";
import { ClientEvents } from "../client/client";
import { makeMovementFlag } from "../binary/encoder";
import { TextComponent } from "../base/typing";
import { AuthRelatedNotFound, HaveSignatureButNotIndex, MessageLinkNotFound, NotImplemented, UnexpectedValue } from "../base/error";
import { uuidToBuffer } from "../base/math";
import { MessageLink } from "../message/link";
import { Sender } from "./sender";

function zodParse<Type extends zod.ZodType>(data: object, zod: Type): zod.infer<Type> {
    return zod.parse(data);
}

/**
 * Packet listener
 */
export class Listener {
    private readonly mapping: Record<string, (data: object) => void>;

    constructor(
        private state: SharedState,
        private emit: TypedEmmiter<ClientEvents>["emit"],
        private sender: Sender,
    ) {
        this.mapping = {
            "login:login_disconnect": this.handleLoginDisconnect,
            "login:hello": this.handleEncryption,
            "login:login_finished": this.handleLoginSucess,
            "login:login_compression": this.handleSetCompression,

            "configuration:disconnect": this.handleConfiguarionPlayDisconnect,
            "configuration:keep_alive": this.handleKeepAlive,
            "configuration:registry_data": this.handleRegistryData,
            "configuration:select_known_packs": this.handleKnownPack,
            "configuration:finish_configuration": this.handleFinishConfiguration,

            "play:login": this.handlePlayLogin,
            "play:disconnect": this.handleConfiguarionPlayDisconnect,
            "play:keep_alive": this.handleKeepAlive,

            "play:player_chat": this.handlePlayerChat,
            "play:disguised_chat": this.handleDisguisedChatMessage,
            "play:system_chat": this.handleSystemMessage,
            "play:set_action_bar_text": this.handleSetActionBar,

            "play:level_chunk_with_light": this.handleChunkData,
            "play:forget_level_chunk": this.handleUnloadChunk,

            "play:add_entity": this.handleSpawnEntity,
            "play:move_entity_pos": this.handleUpdateEntityPosition,
            "play:move_entity_pos_rot": this.handleUpdateEntityPositionRotation,
            "play:entity_position_sync": this.handleTeleportEntity,
            "play:player_position": this.handleSynchronizePlayerPosition,
            "play:update_mob_effect": this.handleUpdateMobEffect,
            "play:remove_entities": this.handleRemoveEntity,
            "play:remove_mob_effect": this.handleRemoveMobEffect,
            // "play:rotate_head": ,
            "play:set_entity_motion": this.handleSetEntityVelocity,
        };
    }

    public disconnect: (reason: string, reasonRaw?: TextComponent) => void = () => { throw new NotImplemented(); };


    public handlePacket(packetId: string, data: object) {
        if (packetId in this.mapping)
            this.mapping[packetId]!.bind(this)(data);
    }

    // Login

    private handleLoginDisconnect(data: object) {
        const { reason } = zodParse(data, zod.object({ reason: zod.record(zod.string(), zod.any()) }));
        const text = getTextFromTextComponent(reason);

        this.disconnect(text, reason);
    }

    private handleEncryption(data: object) {
        const {
            server_id,
            public_key,
            verify_token,
            should_authenticate
        } = zodParse(data, zod.object({
            server_id: zod.string(),
            public_key: zod.array(zod.number()),
            verify_token: zod.array(zod.number()),
            should_authenticate: zod.boolean()
        }));

        const sharedSecret = randomBytes(16);
        this.state.sharedSecret = sharedSecret;
        this.state.cipher = createCipheriv("aes-128-cfb8", sharedSecret, sharedSecret);
        this.state.decipher = createDecipheriv("aes-128-cfb8", sharedSecret, sharedSecret);
        this.state.useEncryption = true;

        const publicKey = Buffer.from(public_key),
            verifyToken = Buffer.from(verify_token);
        if (should_authenticate)
            if (!this.state.authClient)
                throw new AuthRelatedNotFound("auth client");
            else
                this.state.authClient.sendAuth(server_id, sharedSecret, publicKey)
                    .then(() => this.sender.sendEncryptionResponse(publicKey, verifyToken));
        else
            this.sender.sendEncryptionResponse(publicKey, verifyToken);
    }

    private handleSetCompression(data: object) {
        const { threshold } = zodParse(data, zod.object({ threshold: zod.number() }));
        if (threshold < 0) this.state.compressionThreshold = -1;
        else this.state.compressionThreshold = threshold;
    }

    private handleLoginSucess(data: object) {
        const { profile: { uuid, username } } = zodParse(data,
            zod.object({
                profile: zod.object({ uuid: zod.string(), username: zod.string() }),
                session_id: zod.string()
            }));
        this.state.player = {
            uuid,
            username,
            entityId: 0,
            dimension: "",
            gameMode: GameMode.Survival,

            position: {
                x: 0,
                y: 0,
                z: 0,
            },
            velocity: {
                x: 0,
                y: 0,
                z: 0,
            },
            angle: {
                yaw: 0,
                pitch: 0,
            },
            effects: {}
        } satisfies ClientPlayer as any;
        this.sender.sendLoginAck();
    }

    // Configure

    private handleKnownPack(data: object) {
        const { known_packs } = zodParse(data, zod.object({
            known_packs: zod.array(zod.object({
                namespace: zod.string(),
                id: zod.string(),
                version: zod.string(),
            }))
        }));
        this.state.server!.knownPacks = known_packs satisfies ServerKnownPack[] as any;
        this.sender.sendKnownPack(this.state.clientOptions.loadRegistry === true ? [] : known_packs);
        // this.sender.sendKnownPack([]);
    }

    private handleRegistryData(data: object) {
        const { registry_id, entries } = zodParse(data,
            zod.object({
                registry_id: zod.string(),
                entries: zod.array(zod.object({
                    entry_id: zod.string(),
                    data: zod.record(zod.string(), zod.any()).nullable()
                }))
            })
        );

        if (!this.state.registry) this.state.registry = {} as any;
        const registry = this.state.registry as Record<string, any[]>;
        if (!(registry_id in registry)) registry[registry_id] = [];
        registry[registry_id]!.push(...entries);
        this.emit("registryData", { registry_id, entries });
    }

    private handleFinishConfiguration(data: object) {
        this.sender.sendConfigureAck();
    }

    private handleConfiguarionPlayDisconnect(data: object) {
        const { reason } = zodParse(data, zod.object({ reason: zod.record(zod.string(), zod.any()) }));
        const text = getTextFromTextComponent(reason);

        this.disconnect(text, reason);
    }

    // Play

    private handlePlayLogin(data: object) {
        const {
            entity_id,
            is_hardcore,
            dimension_names,
            max_players,
            view_distance,
            simulation_distance,
            dimension_name,
            game_mode
        } = zodParse(data,
            zod.object({
                entity_id: zod.int(),
                is_hardcore: zod.boolean(),
                dimension_names: zod.array(zod.string()),
                max_players: zod.int(),
                view_distance: zod.int(),
                simulation_distance: zod.int(),
                dimension_name: zod.string(),
                game_mode: zod.int()
            })
        );


        this.state.world = {
            hardcore: is_hardcore,
            dimensions: dimension_names,
            maxPlayers: max_players,
            viewDistance: view_distance,
            simulationDistance: simulation_distance,
            chunks: {},
            entities: {}
        } satisfies ServerWorld as any; // To avoid type conflict
        this.state.player!.dimension = dimension_name;
        this.state.player!.entityId = entity_id;
        this.state.player!.gameMode = game_mode;
        this.state.status = ClientStatus.Ready;

        if (this.state.useEncryption)
            this.sender.sendPlayerSession();

        this.emit("ready");
    }

    private handleSynchronizePlayerPosition(data: object) {
        const {
            teleport_id,
            x,
            y,
            z,
            velocity_x,
            velocity_y,
            velocity_z,
            yaw,
            pitch,
            flags
        } = zodParse(data,
            zod.object({
                teleport_id: zod.int(),
                x: zod.number(),
                y: zod.number(),
                z: zod.number(),
                velocity_x: zod.number(),
                velocity_y: zod.number(),
                velocity_z: zod.number(),
                yaw: zod.number(),
                pitch: zod.number(),
                flags: zod.object({
                    x: zod.boolean(),
                    y: zod.boolean(),
                    z: zod.boolean(),
                    yaw: zod.boolean(),
                    pitch: zod.boolean(),
                    velX: zod.boolean(),
                    velY: zod.boolean(),
                    velZ: zod.boolean(),
                    rotateVelocity: zod.boolean(),
                }),
            })
        );

        const { position, velocity, angle } = this.state.player!;
        let { x: newPosX, y: newPosY, z: newPosZ } = position;
        let { x: newVelX, y: newVelY, z: newVelZ } = velocity;
        let { yaw: newYaw, pitch: newPitch } = angle;
        if (flags.x) newPosX += x; else newPosX = x;
        if (flags.y) newPosY += y; else newPosY = y;
        if (flags.z) newPosZ += z; else newPosZ = z;
        if (flags.velX) newVelX += velocity_x; else newVelX = velocity_x;
        if (flags.velY) newVelY += velocity_y; else newVelY = velocity_y;
        if (flags.velZ) newVelZ += velocity_z; else newVelZ = velocity_z;
        if (flags.yaw) newYaw += yaw; else newYaw = yaw;
        if (flags.pitch) newPitch += pitch; else newPitch = pitch;
        const newPosition = { x: newPosX, y: newPosY, z: newPosZ },
            newVelocity = { x: newVelX, y: newVelY, z: newVelZ },
            newAngle = { yaw: newYaw, pitch: newPitch };
        this.state.enqueueMutation(state =>
            state.player = {
                ...state.player!,
                position: newPosition,
                velocity: newVelocity,
                angle: newAngle,
            } satisfies ClientPlayer as any
        );
        this.state.enqueueEvent("playerPosition", { x: newPosX, y: newPosY, z: newPosZ });
        this.sender.sendConfirmTeleportation(teleport_id);
        this.sender.sendPacket("move_player_pos_rot", {
            x: newPosition.x,
            feet_y: newPosition.y,
            z: newPosition.z,
            yaw: newAngle.yaw,
            pitch: newAngle.pitch,
            flags: makeMovementFlag(true, false)
        });
    }

    private handleKeepAlive(data: object) {
        const { keep_alive_id } = zodParse(data,
            zod.object({
                keep_alive_id: zod.bigint().or(zod.number())
            })
        );
        this.sender.sendKeepAlive(BigInt(keep_alive_id));
    }

    private handleChangeGameMode(data: object) {
        const { gameMode } = zodParse(data, zod.object({ gameMode: zod.number() }));
        this.state.enqueueMutation(state => state.player!.gameMode = gameMode);
    }

    // Chunk
    private handleChunkData(data_: object) {
        const {
            chunk_x,
            chunk_z,
            heightmaps,
            data,
            block_entities,
            light,
        } = zodParse(data_,
            zod.object({
                chunk_x: zod.int(),
                chunk_z: zod.int(),
                heightmaps: zod.array(zod.any()),
                data: zod.array(zod.int()),
                block_entities: zod.array(zod.object({
                    packed_xz: zod.int(),
                    y: zod.int(),
                    type: zod.int(),
                    data: zod.record(zod.string(), zod.any())
                })),
                light: zod.any().optional(),
            })
        );

        const chunkSections: Record<number, ChunkSection> = {};

        const chunkDataDecoder = new BinaryDecoder(Buffer.from(data));
        for (let height = 0; height < (SectionsPerChunk[this.state.player!.dimension] || 0); height++) {
            const blockCount = chunkDataDecoder.readShort();
            const fluidCount = chunkDataDecoder.readShort();

            // Blocks
            let blockStateBPE = chunkDataDecoder.readUByte();
            let blockStatePalettes: number[] = [];

            if (blockStateBPE === 0) {
                blockStatePalettes = [chunkDataDecoder.readVarInt()];
            } else if (blockStateBPE <= 8) {
                // Indirect palette
                if (blockStateBPE < 4) blockStateBPE = 4; // BPE smaller than 4 will be rounđe up to 4
                blockStatePalettes = chunkDataDecoder.readPrefixedArray(decoder => decoder.readVarInt());
            } else {
                // Direct palette (no palette array)
            }

            const blockEntriesPerSection = 4096;
            const blockEntriesPerLong = Math.floor(64 / blockStateBPE);
            let blockDataArray: BigInt64Array | null = null;
            if (blockStateBPE !== 0) {
                const blockDataArrayLength = Math.floor((blockEntriesPerSection + blockEntriesPerLong - 1) / blockEntriesPerLong);
                blockDataArray = new BigInt64Array(chunkDataDecoder.readArray(blockDataArrayLength, (decoder) => decoder.readLong()));
            }

            // Biomes - Not used yet, but need to be read to advance decoder
            const biomeBPE = chunkDataDecoder.readUByte();
            let biomePalettes: number[] = [];
            if (biomeBPE === 0) {
                biomePalettes = [chunkDataDecoder.readVarInt()]; // Palette value
            } else if (biomeBPE <= 3) {
                // Indirect palette
                biomePalettes = chunkDataDecoder.readPrefixedArray(decoder => decoder.readVarInt());
            } else {
                // Direct palette (no palette array)
            }

            const biomeEntriesPerSection = 64;
            const biomeEntriesPerLong = Math.floor(64 / biomeBPE);
            let biomeDataArray: BigInt64Array | null = null;
            if (biomeBPE !== 0) {
                const biomeDataArrayLength = Math.floor((biomeEntriesPerSection + biomeEntriesPerLong - 1) / biomeEntriesPerLong);
                biomeDataArray = new BigInt64Array(chunkDataDecoder.readArray(biomeDataArrayLength, (decoder) => decoder.readLong()));
            }

            chunkSections[height] = {
                block: {
                    bpe: blockStateBPE,
                    palette: blockStatePalettes,
                    data: blockDataArray
                },
                biome: {
                    bpe: biomeBPE,
                    palette: biomePalettes,
                    data: biomeDataArray
                }
            };

            this.state.enqueueEvent("loadChunk", chunk_x, height, chunk_z);
        }

        const blockEntitiesObj: Record<number, BlockEntity> = Object.fromEntries(
            block_entities.map(val => [val.packed_xz, { type: val.type, data: val.data }])
        );


        this.state.enqueueMutation(state =>
            state.world!.chunks[`${state.player!.dimension}:${chunk_x}:${chunk_z}`] = {
                sections: chunkSections,
                blockEntities: blockEntitiesObj
            });
    }

    private handleUnloadChunk(data: object) {
        const {
            chunk_x,
            chunk_z
        } = zodParse(data, zod.object({
            chunk_x: zod.number(),
            chunk_z: zod.number()
        }));
        this.state.enqueueMutation(state => delete state.world!.chunks[`${state.player!.dimension}:${chunk_x}:${chunk_z}`]);
        this.state.enqueueEvent("unloadChunk", chunk_x, chunk_z);
    }

    private handleBlockEntityData(data_: object) {
        const {
            location,
            type,
            data
        } = zodParse(data_, zod.object({
            location: zod.object({ x: zod.number(), y: zod.number(), z: zod.number() }),
            type: zod.int(),
            data: zod.record(zod.string(), zod.any())
        }));

        const chunkX = Math.floor(location.x / 16),
            chunkZ = Math.floor(location.z / 16);
        const xWitinChunk = location.x % 16,
            zWitinChunk = location.z % 16;
        const packedPosition = packBlockPos(xWitinChunk, location.y, zWitinChunk);
        if (`${this.state.player!.dimension}:${chunkX}:${chunkZ}` in this.state.world!.chunks)
            this.state.enqueueMutation(state =>
                state.world!.chunks[`${state.player!.dimension}:${chunkX}:${chunkZ}`]!.blockEntities[packedPosition] = {
                    type,
                    data
                }
            );
    }

    // Entity
    private handleSpawnEntity(data_: object) {
        const {
            entity_id,
            entity_uuid,
            type,
            x,
            y,
            z,
            velocity,
            pitch,
            yaw,
            head_yaw,
            data
        } = zodParse(data_, zod.object({
            entity_id: zod.int(),
            entity_uuid: zod.string(),
            type: zod.int(),
            x: zod.number(),
            y: zod.number(),
            z: zod.number(),
            velocity: zod.instanceof(Vec3),
            pitch: zod.number(),
            yaw: zod.number(),
            head_yaw: zod.number(),
            data: zod.int(),
        }));

        const entity: Entity = {
            id: entity_id,
            type,
            position: { x, y, z },
            velocity,
            angle: {
                pitch,
                yaw
            },
            data
        };
        this.state.enqueueMutation(state => state.world!.entities[entity_id] = entity);
        this.state.enqueueEvent("spawnEntity", entity);
    }

    private handleTeleportEntity(data: object) {
        const {
            entity_id,
            x,
            y,
            z,
            velocity_x,
            velocity_y,
            velocity_z,
            pitch,
            yaw
        } = zodParse(data, zod.object({
            entity_id: zod.int(),
            x: zod.number(),
            y: zod.number(),
            z: zod.number(),
            velocity_x: zod.number(),
            velocity_y: zod.number(),
            velocity_z: zod.number(),
            pitch: zod.number(),
            yaw: zod.number(),
        }));

        if (entity_id in this.state.world!.entities) this.state.enqueueMutation(state => {
            state.world!.entities[entity_id]!.position = { x, y, z };
            state.world!.entities[entity_id]!.velocity = { x: velocity_x, y: velocity_y, z: velocity_z };
            state.world!.entities[entity_id]!.angle = { pitch, yaw };
        });
        this.state.enqueueEvent("updateEntity", entity_id);
    }

    private handleUpdateEntityPosition(data: object) {
        const {
            entity_id,
            delta_x,
            delta_y,
            delta_z
        } = zodParse(data, zod.object({
            entity_id: zod.number(),
            delta_x: zod.number(),
            delta_y: zod.number(),
            delta_z: zod.number(),
        }));

        if (entity_id in this.state.world!.entities) this.state.enqueueMutation(state => {
            let { x, y, z } = state.world!.entities[entity_id]!.position;
            x += delta_x;
            y += delta_y;
            z += delta_z;
            state.world!.entities[entity_id]!.position = { x, y, z };
        });
        this.state.enqueueEvent("updateEntity", entity_id);
    }

    private handleUpdateEntityPositionRotation(data: object) {
        const {
            entity_id,
            delta_x,
            delta_y,
            delta_z,
            pitch,
            yaw
        } = zodParse(data, zod.object({
            entity_id: zod.number(),
            delta_x: zod.number(),
            delta_y: zod.number(),
            delta_z: zod.number(),
            pitch: zod.number(),
            yaw: zod.number(),
        }));
        if (entity_id in this.state.world!.entities) {
            this.state.enqueueMutation(state => {
                let { x, y, z } = state.world!.entities[entity_id]!.position;
                x += delta_x;
                y += delta_y;
                z += delta_z;
                state.world!.entities[entity_id]!.position = { x, y, z };
                state.world!.entities[entity_id]!.angle = { yaw, pitch };
            });
            this.state.enqueueEvent("updateEntity", entity_id);
        }
    }

    private handleSetEntityVelocity(data: object) {
        const { entity_id, velocity } = zodParse(data, zod.object({
            entity_id: zod.number(),
            velocity: zod.instanceof(Vec3)
        }));

        if (entity_id in this.state.world!.entities)
            this.state.enqueueMutation(state => state.world!.entities[entity_id]!.velocity = velocity);
    }

    private handleRemoveEntity(data: object) {
        const { entity_ids } = zodParse(data, zod.object({ entity_ids: zod.array(zod.number()) }));

        this.state.enqueueMutation(state => {
            for (const entityId of entity_ids) {
                delete state.world!.entities[entityId];
                this.state.enqueueEvent("removeEntity", entityId);
            }
        });
    }

    private handleUpdateMobEffect(data: object) {
        const {
            entity_id,
            effect_id,
            amplifier,
            duration,
            flags
        } = zodParse(data, zod.object({
            entity_id: zod.number(),
            effect_id: zod.number(),
            amplifier: zod.number(),
            duration: zod.number(),
            flags: zod.number(),
        }));
        const isAmbient = (flags & 0x01) == 1,
            showParticles = (flags & 0x02) == 1,
            showIcon = (flags & 0x04) == 1,
            blend = (flags & 0x08) == 1;
        this.state.enqueueMutation((state) => {
            if (state.player!.entityId === entity_id)
                state.player!.effects[effect_id] = {
                    amplifier,
                    duration,
                    id: effect_id,
                    flag: {
                        blend,
                        isAmbient,
                        showIcon,
                        showParticles
                    },
                    date: Date.now()
                };
        });
    }

    private handleRemoveMobEffect(data: object) {
        const { entity_id, effect_id } = zodParse(data, zod.object({
            entity_id: zod.number(),
            effect_id: zod.number(),
        }));

        this.state.enqueueMutation((state) => {
            if (state.player!.entityId === entity_id)
                delete state.player!.effects[effect_id];
        });
    }

    // Chat
    private handlePlayerChat(data: object) {
        const {
            global_index,
            sender,
            index,
            message_signature_bytes,
            message,
            timestamp,
            salt,
            previous_messages,
            unsigned_content,
            filter_type,
            filter_type_bits,
            chat_type,
            sender_name,
            target_name
        } = zodParse(data, zod.object({
            global_index: zod.int(),
            sender: zod.string(),
            index: zod.int(),
            message_signature_bytes: zod.array(zod.number()).nullable(),
            message: zod.string(),
            timestamp: zod.bigint(),
            salt: zod.bigint(),
            previous_messages: zod.array(zod.object({
                message_id: zod.number(),
                signature: zod.array(zod.number()).optional()
            })),
            unsigned_content: zod.record(zod.string(), zod.any()).nullable(),
            filter_type: zod.int(),
            filter_type_bits: zod.array(zod.bigint()).nullable(),
            chat_type: zod.int().or(zod.record(zod.string(), zod.any())),
            sender_name: zod.record(zod.string(), zod.any()),
            target_name: zod.record(zod.string(), zod.any()).nullable()
        }));

        const senderNameText = getTextFromTextComponent(sender_name).toString(),
            targetNameText = target_name ? getTextFromTextComponent(target_name).toString() : undefined;

        const emitObject = {
            sender: senderNameText,
            target: targetNameText,
            content: message,
            raw: {
                sender: sender_name,
                target: target_name || undefined,
                content: unsigned_content || undefined,
            }
        };

        let link: MessageLink | undefined = undefined;
        if (this.state.clientOptions.shouldVerifyMessageOrder && this.state.messageLinks) {
            if (index === 0) {
                if (this.state.sessionID)
                    link = MessageLink.root(sender, this.state.sessionID.toString("hex"));
                else
                    link = MessageLink.unsinged(sender);
                this.state.messageLinks[sender] = link;
            } else {
                const oldLink = this.state.messageLinks[sender];
                if (!oldLink)
                    throw new MessageLinkNotFound(sender);
                let newLink;
                if (this.state.sessionID)
                    newLink = new MessageLink(global_index, sender, this.state.sessionID.toString("hex"));
                else
                    newLink = MessageLink.unsinged(sender);
                if (!newLink.isDescendantOf(oldLink))
                    throw new UnexpectedValue("message order", newLink.index.toString(), (oldLink.index + 1).toString());
                this.state.messageLinks[sender] = newLink;
                link = newLink;
            }
        }

        if (message_signature_bytes !== null)
            this.state.messageCount += 1;
        if (this.state.clientOptions.shouldVerifyMessageSignature === true && message_signature_bytes) {
            if (!this.state.sessionID)
                throw new AuthRelatedNotFound("session uuid");
            if (!this.state.messageSignatureCache)
                throw new AuthRelatedNotFound("signature cache");
            if (!link)
                throw new HaveSignatureButNotIndex();
            const { publicKey } = this.state.getSignature();

            const formatVersion = Buffer.alloc(4); formatVersion.writeInt32BE(1);
            const senderUUIDBuf = uuidToBuffer(sender);
            const sessionUUIDBuf = this.state.sessionID;
            const indexBuf = Buffer.alloc(4); indexBuf.writeInt32BE(link.index);
            const saltBuf = Buffer.alloc(8); saltBuf.writeBigInt64BE(salt);
            const timestampBuf = Buffer.alloc(8); timestampBuf.writeBigInt64BE(timestamp);
            const _messageBuf = Buffer.from(message, "utf-8");
            const lengthBuf = Buffer.alloc(4); lengthBuf.writeInt32BE(_messageBuf.length);
            const messageBuf = _messageBuf;
            const previousMessagesLenghtBuf = Buffer.alloc(4); previousMessagesLenghtBuf.writeInt32BE(previous_messages.length);
            const previousMessagesBuf = Buffer.concat(this.state.messageSignatureCache.unpack(previous_messages));
            const signaturePayload = Buffer.concat([
                formatVersion,
                senderUUIDBuf,
                sessionUUIDBuf,
                indexBuf,
                saltBuf,
                timestampBuf,
                lengthBuf,
                messageBuf,
                previousMessagesLenghtBuf,
                previousMessagesBuf
            ]);

            const verify = createVerify("RSA-SHA256");
            verify.update(signaturePayload);
            verify.end();
            const signature = Buffer.from(message_signature_bytes);

            if (!verify.verify(publicKey!, signature))
                return this.emit("failedMessage", emitObject);
        }

        this.emit("message", emitObject);
    }

    private handleSystemMessage(data: object) {
        const {
            content,
            overlay: isActionbar
        } = zodParse(data, zod.object({
            content: zod.record(zod.string(), zod.any()),
            overlay: zod.boolean()
        }));
        const text = getTextFromTextComponent(content);

        if (isActionbar) {
            this.emit("actionBar", text.toString());
            this.emit("actionBarRaw", content);
        } else {
            this.emit("systemMessage", text.toString());
            this.emit("systemMessageRaw", content);
        }
    }

    private handleSetActionBar(data: object) {
        const { action_bar_text: content } = zodParse(data, zod.object({ action_bar_text: zod.record(zod.string(), zod.any()) }));
        const text = getTextFromTextComponent(content);
        this.emit("actionBar", text.toString());
        this.emit("actionBarRaw", content);
    }

    private handleDisguisedChatMessage(data: object) {
        const {
            message,
            chatType,
            senderName,
            targetName
        } = zodParse(data, zod.object({
            message: zod.record(zod.string(), zod.any()),
            chatType: zod.string().or(zod.record(zod.string(), zod.any())),
            senderName: zod.record(zod.string(), zod.any()),
            targetName: zod.record(zod.string(), zod.any()).nullable(),
        }));
        const messageText = getTextFromTextComponent(message).toString(),
            senderNameText = getTextFromTextComponent(senderName).toString(),
            targetNameText = targetName ? getTextFromTextComponent(targetName).toString() : undefined;
        this.emit("message", {
            sender: senderNameText,
            target: targetNameText,
            content: messageText,
            raw: {
                sender: senderName,
                target: targetName || undefined,
                content: message,
            }
        });
    }
}