MineJSv0.5.1
world/state.tstypescript
import { Cipheriv, Decipheriv } from "node:crypto";
import { If } from "../base/typing";
import { Angle, BaseVec3 } from "../physics/direction";
import { AuthRelatedNotFound, ClientNotReady } from "../base/error";
import { ClientEvents, ClientOption } from "../client/client";
import { AuthClient } from "../client/auth";
import { MessageLink } from "../message/link";
import { SignatureCache } from "../message/sigCache";
import { CacheImplementation, LRUCache } from "../base/cache";

export interface Server {
    knownPacks?: ServerKnownPack
}

export interface ServerKnownPack {
    namespace: string,
    id: string,
    version: string
}

/**
 * Represent world information
 */
export interface ServerWorld {
    /** Is hardcore enabled */
    hardcore: boolean,
    /** All dimensions */
    dimensions: string[],
    /** Max players allowed */
    maxPlayers: number,
    /** View distances */
    viewDistance: number,
    /** Simulation distance */
    simulationDistance: number,

    /**
     * The record key is a chunk section position packed as `Player Dimension:Chunk X:Chunk Z[Section Y]`
     * 
     * For `Section Y`, a chunk is splitted into several cubic 16x16x16 sections, or in other word, a chunk is a bund of 16x16x16 sections stacked on each other.
     */
    chunks: Record<string, ChunkColumn>,
    /**
     * The record key is entity ID
     */
    entities: Record<number, Entity>,
}

/** 
 * Represent a chunk collumns, which may contains multiple chunks sections 
 */
export interface ChunkColumn {
    sections: Record<number, ChunkSection>,
    blockEntities: Record<number, BlockEntity>
}

export interface ChunkSection {
    block: PaletteContainer,
    biome: PaletteContainer,
}

/**
 * Used for indirect mapping
 * 
 * @see https://minecraft.wiki/w/Java_Edition_protocol/Chunk_format#Paletted_Container_structure
 */
export interface PaletteContainer {
    bpe: number, // Bit per entry
    palette: number[],
    data: BigInt64Array | null, // null if the whole section contains one 1 type of block
}

/** Represent a block entity */
export interface BlockEntity {
    type: number,
    data: Record<string, any>
}

export interface Effect {
    id: number,
    amplifier: number,
    duration: number,
    flag: {
        isAmbient: boolean,
        showParticles: boolean,
        showIcon: boolean,
        blend: boolean
    },
    /**
     * The epoch ms when the effect update packet is recieved, for validating duration
     */
    date: number
}

/** Represent an entity */
export interface Entity {
    id: number,
    type: number,
    position: BaseVec3,
    velocity: BaseVec3,
    angle: Angle,
    data: number
}

export enum GameMode {
    Survival = 0,
    Creative = 1,
    Adventure = 2,
    Spectator = 3
}

export interface ClientPlayer {
    /** Player UUID */
    uuid: string,
    /** Player username */
    username: string,
    /** Player entity ID */
    entityId: number,
    /** Current game mode */
    gameMode: GameMode,

    position: BaseVec3,
    velocity: BaseVec3,
    angle: Angle,

    /** Current dimension */
    dimension: string,

    effects: Record<number, Effect>
}

export enum ClientStatus {
    Disconnected,
    Connecting,
    Logining,
    Ready,
}

export enum ConnectionState {
    Disconnected,
    Handshake,
    Login,
    Configure,
    Play
}

export interface ServerRegistryEntry {
    id: string,
    data: object
}

export type MutateState = (state: SharedState) => void;
export type EmittedEvent = {
    [K in keyof ClientEvents]: { event: K, args: ClientEvents[K] }
}[keyof ClientEvents]
export type SendingPacket = {
    id: string | number,
    data: Record<string, any>
}

/**
 * For managing shared data between classes
 */
export class SharedState<IsReady extends boolean = boolean> {
    private mutationQueue: MutateState[] = [];
    private eventQueue: EmittedEvent[] = [];
    private packetQueue: SendingPacket[] = [];

    public state: ConnectionState = ConnectionState.Disconnected;
    public status: ClientStatus = ClientStatus.Disconnected;
    public compressionThreshold: number = 0;

    // Auth stuffs
    public authClient: AuthClient | undefined = undefined;
    public playerUUID: string | undefined = undefined;
    public publicKey: Buffer | undefined = undefined;
    public signature: Buffer | undefined = undefined;
    public signatureExpireTime: number | undefined = undefined;
    // eslint-disable-next-line no-undef
    private signatureTimeoutInstance: NodeJS.Timeout | undefined = undefined;

    public sharedSecret: Buffer | undefined = undefined;
    public cipher: Cipheriv | undefined = undefined;
    public decipher: Decipheriv | undefined = undefined;
    public useEncryption: boolean = false;
    public sessionID: Buffer | undefined = undefined;

    public server: Server | undefined = undefined;
    public registry: Record<string, ServerRegistryEntry[]> | undefined = undefined;
    public world: If<IsReady, ServerWorld> = null as any;
    public player: If<IsReady, ClientPlayer> = null as any;
    public messageCount: number = 0;
    
    public cache: CacheImplementation<Uint16Array> | undefined = undefined;

    /**
     * For maintaining message order.
     * 
     * A UUID map to a MessageLink.
     */
    public messageLinks: Record<string, MessageLink> | undefined = undefined;
    public messageSignatureCache: SignatureCache | undefined = undefined;

    constructor(
        public clientOptions: ClientOption,
    ) {
        if (this.clientOptions.loadAndCacheChunk) {
            if (this.clientOptions.cacheImplementation)
                this.cache = new this.clientOptions.cacheImplementation();
            else
                this.cache = new LRUCache(32);
        }
    }

    public isReady(): this is SharedState<true> {
        return this.status === ClientStatus.Ready;
    }

    public checkReady() {
        if (!this.isReady())
            throw new ClientNotReady();
    }

    public pruneStates() {
        this.mutationQueue = [];
        this.eventQueue = [];
        this.packetQueue = [];

        this.state = ConnectionState.Disconnected;
        this.status = ClientStatus.Disconnected;
        this.compressionThreshold = 0;

        this.authClient = undefined;
        this.playerUUID = undefined;
        this.publicKey = undefined;
        this.signature = undefined;

        this.sharedSecret = undefined;
        this.cipher = undefined;
        this.decipher = undefined;
        this.useEncryption = false;
        this.sessionID = undefined;
        this.signatureTimeoutInstance = undefined;

        this.server = undefined;
        this.registry = undefined;
        this.world = undefined as any;
        this.player = undefined as any;
        this.messageCount = 0;

        if (this.clientOptions.shouldVerifyMessageSignature)
            this.messageSignatureCache = SignatureCache.default();
        else
            this.messageSignatureCache = undefined;
        if (this.clientOptions.shouldVerifyMessageOrder)
            this.messageLinks = {};
        else
            this.messageLinks = undefined;

        if (this.clientOptions.loadAndCacheChunk) {
            if (this.clientOptions.cacheImplementation)
                this.cache = new this.clientOptions.cacheImplementation();
            else
                this.cache = new LRUCache(32);
        }
    }

    public getSignature() {
        if (this.publicKey && this.signature)
            return {
                publicKey: this.publicKey,
                signature: this.signature
            };
        throw new AuthRelatedNotFound("public key and signature");
    }

    private async fetchSignature() {
        if (!this.authClient)
            throw new AuthRelatedNotFound("instance of class AuthClient");
        const { publicKey, signature, expiresAt } = await this.authClient.getKeyPair();
        this.publicKey = Buffer.from(publicKey);
        this.signature = Buffer.from(signature);
        return expiresAt;
    }
    public async startSignatureLoop() {
        const expiresAt = await this.fetchSignature();
        this.signatureTimeoutInstance = setTimeout(
            async () => await this.startSignatureLoop(), 
            expiresAt - Date.now() - 10_000
        );
    }
    public stopSignatureLoop() {
        clearTimeout(this.signatureTimeoutInstance);
    }

    public enqueueMutation(mutation: MutateState) { this.mutationQueue.push(mutation); }

    public enqueuePacket(packet: string, data: object): void
    public enqueuePacket(packet: SendingPacket): void
    public enqueuePacket(packet: SendingPacket | string, data?: object) {
        if (typeof packet === "string")
            this.packetQueue.push({
                id: packet,
                data: data!
            });
        else
            this.packetQueue.push(packet);
    }

    public enqueueEvent<K extends keyof ClientEvents>(event: K, ...args: ClientEvents[K]): void
    public enqueueEvent(event: EmittedEvent): void
    public enqueueEvent(event: EmittedEvent, ...args: any[]) {
        if (typeof event === "string")
            this.eventQueue.push({
                event,
                args: args as any
            });
        else
            this.eventQueue.push(event);
    }

    public drainMutation() {
        while (true) {
            const mutation = this.mutationQueue.shift();
            if (mutation)
                mutation(this);
            else
                break;
        }
    }
    public drainEvent(consumer: (event: EmittedEvent) => void) {
        while (true) {
            const event = this.eventQueue.shift();
            if (event)
                consumer(event);
            else
                break;
        }
    }
    public drainPacket(consumer: (packet: SendingPacket) => void) {
        while (true) {
            const packet = this.packetQueue.shift();
            if (packet)
                consumer(packet);
            else
                break;
        }
    }
}