MineJSv0.5.1
version/codec.tstypescript
import z from "zod";
import { MissingField, MissingPacketField, NotImplemented, SkippableNotImplemented, UnexpectedValue } from "../base/error";
import { BinaryDecoder } from "../binary/decoder";
import { ConnectionState, SharedState } from "../world/state";
import { FieldNode, PacketObject, PacketRegistry, VersionDefinitions } from "./registry";
import { BinaryEncoder } from "../binary/encoder";
import { BitSet } from "../base/bitset";

/** @hidden */
const TCPStateMapping: Record<ConnectionState, keyof z.infer<typeof VersionDefinitions> | null> = {
    [ConnectionState.Disconnected]: null,
    [ConnectionState.Handshake]: "handshaking",
    [ConnectionState.Login]: "login",
    [ConnectionState.Configure]: "configuration",
    [ConnectionState.Play]: "play",
};

/** @hidden */
export interface EncodeResult {
    buffer: Buffer,
    packet: z.infer<typeof PacketObject>,
}

/**
 * A codec that help encode or decode packets
 */
export class VersionCodec {
    constructor(private readonly state: SharedState) { }

    public consumePacket: (packetId: string, data: object) => void = () => { throw new NotImplemented(); };

    public handlePacket(packetId: number, decoder: BinaryDecoder) {
        const mappedState = TCPStateMapping[this.state.state];
        if (!mappedState) return; // console.log("no state");
        const packet = PacketRegistry.getPacket(mappedState, "clientbound", packetId);
        if (!packet) return; // console.log("no packet");

        const { name, structure, skipForNow } = packet;

        if (skipForNow) return;

        const resolvedObject: Record<string, any> = {};
        if (Object.keys(structure).length > 0)
            for (const [name, field] of Object.entries(structure))
                try {
                    resolvedObject[name] = this.readField(packetId, resolvedObject, name, field, decoder);
                } catch (err) {
                    if (err instanceof SkippableNotImplemented) break;
                    if (err instanceof NotImplemented) return;
                    throw err;
                }
        if (this.state.clientOptions.debug?.packetLogger)
            console.dir({
                dir: "to client",
                state: mappedState,
                name,
                data: resolvedObject,
            });

        this.consumePacket(`${TCPStateMapping[this.state.state]}:${name}`, resolvedObject);
    }

    public encodePacket(packetId: number | string, data: Record<string, any>): EncodeResult {
        const mappedState = TCPStateMapping[this.state.state];
        if (!mappedState) throw new UnexpectedValue("map-able state", this.state.state.toString());
        const packet = PacketRegistry.getPacket(mappedState, "serverbound", packetId);
        if (!packet) throw new UnexpectedValue("valid serverbound packet id", packetId.toString());

        if (this.state.clientOptions.debug?.packetLogger)
            console.dir({
                dir: "to server",
                state: mappedState,
                id: packetId,
                data,
            });

        const { name: packetName, structure } = packet;
        let encoder = new BinaryEncoder();
        for (const [name, field] of Object.entries(structure)) {
            if (!(name in data))
                throw new MissingField(name, `packet ${packetName}`);
            if (data[name] !== null && data[name] !== undefined && typeof data[name] === "object")
                if ("value" in data[name])
                    if ("metadata" in data[name])
                        encoder = this.writeField(field, encoder, data[name]["value"], data[name]["metadata"]);
                    else if ("length" in data[name])
                        encoder = this.writeField(field, encoder, data[name]["value"], { length: data[name]["length"] });
                    else
                        encoder = this.writeField(field, encoder, data[name]["value"]);
                else
                    encoder = this.writeField(field, encoder, data[name]);
            else
                encoder = this.writeField(field, encoder, data[name]);
        }
        return { buffer: encoder.getBuffer(), packet };
    }

    private readField(
        // Current reading state
        packetId: number,
        readObject: Record<string, any>,

        // Current field
        fieldName: string,
        field: FieldNode,
        decoder: BinaryDecoder
    ): any {
        switch (field.type) {
            case "string": return decoder.readString();
            case "boolean": return decoder.readBoolean();
            case "byte": return decoder.readByte();
            case "unsigned_byte": return decoder.readUByte();
            case "short": return decoder.readShort();
            case "unsigned_short": return decoder.readUShort();
            case "int": return decoder.readInt();
            case "long": return decoder.readLong();
            case "float": return decoder.readFloat();
            case "double": return decoder.readDouble();
            case "var_int": return decoder.readVarInt();
            case "var_long": return decoder.readVarLong();
            case "uuid": return decoder.readUUID();
            case "position": return decoder.readPosition();
            case "array": {
                let length;
                if (!isNaN(Number(field.length)))
                    length = Number(field.length);
                else
                    length = readObject[field.length];
                if (length === undefined) throw new MissingPacketField(packetId, fieldName, field.length.toString());
                return decoder.readArray(length, (decoder) => this.readField(packetId, readObject, fieldName, field.subType, decoder));
            }
            case "prefixed_array": return decoder.readPrefixedArray((decoder) => this.readField(packetId, readObject, fieldName, field.subType, decoder));
            case "teleport_flag": return decoder.readTeleportFlag();
            case "prefixed_optional": return decoder.readPrefixedOptional((decoder) => this.readField(packetId, readObject, fieldName, field.subType, decoder));
            case "nbt": return decoder.readNBT();
            case "lpvec3": return decoder.readLpVec3();
            case "fixed_point": {
                const value = this.readField(packetId, readObject, fieldName, field.subType, decoder);
                return decoder.readFixedPoint(value, field.fractionBits);
            }
            case "angle": return decoder.readAngle();
            case "id_or_x": return decoder.readIdOrX((decoder) => this.readField(packetId, readObject, fieldName, field.subType, decoder));
            case "id_set": return decoder.readIdSet();
            case "chat_type_decoration": return decoder.readChatTypeDecoration();
            case "chat_type": return decoder.readChatType();
            case "enum": return this.readField(packetId, readObject, fieldName, field.subType, decoder);
            case "json_text": return JSON.parse(decoder.readString());
            case "object": {
                const obj: Record<string, any> = {};
                for (const [key, type] of Object.entries(field.fields))
                    obj[key] = this.readField(packetId, readObject, key, type, decoder);
                return obj;
            }
            case "game_profile": {
                return {
                    uuid: decoder.readUUID(),
                    username: decoder.readString(),
                    pproperties: decoder.readPrefixedArray((decoder) => ({
                        name: decoder.readString(),
                        value: decoder.readString(),
                        signature: decoder.readPrefixedOptional((decoder) => decoder.readString())
                    }))
                };
            }
            case "heightmap": {
                return {
                    type: decoder.readVarInt(),
                    data: decoder.readPrefixedArray((decoder) => decoder.readLong())
                };
            }
            case "bitset": return decoder.readPrefixedArray((decoder) => decoder.readLong());
            case "teleport_flags": return decoder.readTeleportFlag();
            case "null": return null;
            case "not_implemented": {
                if (field.skip_able === true)
                    throw new SkippableNotImplemented();
                throw new NotImplemented();
            }
            case "switch": {
                if (!(field.dependsOn in readObject)) throw new MissingPacketField(packetId, fieldName, field.dependsOn);
                const dependencyValue = readObject[field.dependsOn];
                for (const [value, type] of field.cases)
                    if (dependencyValue === value)
                        return this.readField(packetId, readObject, fieldName, type, decoder);
                if (field.default)
                    return this.readField(packetId, readObject, fieldName, field.default, decoder);
                return null;
            }
        }
    }


    private writeField(
        field: FieldNode,
        encoder: BinaryEncoder,
        value: any,
        metadata: Record<string, any> = {}
    ): BinaryEncoder {
        switch (field.type) {
            case "string": return encoder.writeString(value);
            case "boolean": return encoder.writeBoolean(value);
            case "byte": return encoder.writeByte(value);
            case "unsigned_byte": return encoder.writeUByte(value);
            case "short": return encoder.writeShort(value);
            case "unsigned_short": return encoder.writeUShort(value);
            case "int": return encoder.writeInt(value);
            case "long": return encoder.writeLong(value);
            case "float": return encoder.writeFloat(value);
            case "double": return encoder.writeDouble(value);
            case "var_int": return encoder.writeVarInt(value);
            case "var_long": return encoder.writeVarLong(value);
            case "uuid": return encoder.writeRaw(value);
            case "position": return encoder.writePosition(value);
            // case "array": {
            //     const length = metadata["length"];
            //     // if (!length) throw new MissingField(packetId, fieldName, field.length);
            //     return encoder.writeArray(length, (decoder) => this.readField(packetId, readObject, fieldName, field.subType, decoder));
            // }
            case "prefixed_array": {
                const length = metadata["length"] ?? field.length;
                if (length === null || length === undefined) throw new MissingField("length", "prefixed array metadata object");
                if (!Array.isArray(value))
                    throw new UnexpectedValue("an array for prefixed array", typeof value);
                return encoder.writePrefixedArray(length, (encoder, index) => this.writeField(field.subType, encoder, value[index], metadata));
            }
            // case "teleport_flag": return encoder.writeTeleportFlag();
            case "prefixed_optional":
                if (value === null || value === undefined)
                    return encoder.writeBoolean(false);
                else {
                    encoder.writeBoolean(true);
                    return this.writeField(field.subType, encoder, field, metadata);
                }
            // case "nbt": return encoder.writeNBT(value);
            case "lpvec3": return encoder.writeLpVec3(value);
            // case "fixed_point": {
            //     const value = this.readField(packetId, readObject, fieldName, field.subType, encoder);
            //     return encoder.writeFixedPoint(value, field.fractionBits);
            // }
            // case "angle": return encoder.writeAngle();
            // case "id_or_x": return encoder.writeIdOrX((decoder) => this.readField(packetId, readObject, fieldName, field.subType, decoder));
            // case "chat_type_decoration": return encoder.writeChatTypeDecoration();
            // case "chat_type": return encoder.writeChatType();
            case "enum": return this.writeField(field.subType, encoder, value, metadata);
            case "json_text": return encoder.writeString(JSON.parse(value));
            case "object": {
                for (const [name, subField] of Object.entries(field.fields))
                    encoder = this.writeField(subField, encoder, value[name], metadata);
                return encoder;
            }
            case "fixed_bitset": {
                if (!(value instanceof BitSet))
                    throw new UnexpectedValue("bitset instance", typeof value);
                const arr = value.toByteArray();
                if ("length" in field)
                    while (Math.ceil(field.length / 8) > arr.length)
                        arr.push(0);
                return encoder.writeRaw(Buffer.from(arr));
            }
            // case "switch": {
            //     const dependencyValue = readObject[field.dependsOn];
            //     if (!dependencyValue) throw new MissingField(packetId, fieldName, field.dependsOn);
            //     for (const [value, type] of field.cases)
            //         if (dependencyValue === value)
            //             return this.readField(packetId, readObject, fieldName, type, encoder);
            //     if (field.default)
            //         return this.readField(packetId, readObject, fieldName, field.default, encoder);
            //     break;
            // }
            default:
                throw new UnexpectedValue("encoding type", field.type);
        }
    }
}