client/client.tstypescript
import { EventEmitter } from "node:events";
import { BlockManager, BlockState } from "../world/block";
import { EntitiesManager } from "../world/entity";
import { Input, Player } from "../physics/player";
import { TickLoop } from "../world/tick";
import { Message, TCPClient, TCPClientOption } from "../net/tcp";
import { TypedEmmiter as TypedEmitter } from "../base/event";
import { BlockRegistry, EffectRegistry, EntityRegistry, PacketRegistry, ProtocolVersionMapping } from "../version/registry";
import { BaseVec3, Vec3 } from "../physics/direction";
import { RegistryData, TextComponent } from "../base/typing";
import { ClientStatus, ConnectionState, EmittedEvent as EmittedEvent, Entity, SharedState } from "../world/state";
import { Listener } from "../packet/listener";
import { VersionCodec } from "../version/codec";
import { AuthClient } from "./auth";
import { computeUUID } from "../base/math";
import { HaveSignatureButNotIndex } from "../base/error";
import { Sender } from "../packet/sender";
import { CacheImplementation } from "../base/cache";
export type ClientOption = Omit<TCPClientOption, "protocolVersion"> & {
version: string,
/**
* Whenever should the client verify the message against the signature, if any
*
* When set this option to true, you HAVE TO set `shouldVerifyMessageOrder` to true
*/
shouldVerifyMessageSignature?: boolean,
/**
* Should verify the message orders to keep the sync with server.
*/
shouldVerifyMessageOrder?: boolean,
/**
* Whether to load the whole chunk section (16x16x16) and keep them in the cache or just load the needed block.
*
* Please consider this carefully since this can consume a lot of resource to calculate the whole chunk section and store it while you may not need that much.
*
* This option is useful when you have to constantly access to the world chunk, e.g pathfinding.
*/
loadAndCacheChunk?: boolean,
/**
* Custom cache class. Default to `LRUCache`
*/
cacheImplementation?: new () => CacheImplementation<Uint16Array>,
}
export interface ClientEvents {
// TCP Events
/** When the TCP socket connect is initted sucessfully */
connect: [],
/** When the TCP socket connection is destroyed */
destroy: [],
/**
* When the client is ready.
*
* Specifically, when the packet `Login (play)` is received
*/
ready: [],
/**
* When the client got disconnected from sevrer
*/
disconnect: [reason: string],
/**
* When the disconection come with the raw Text Component
*/
disconnectRaw: [textComponent: TextComponent],
/**
* When server send `Player Position Synchronization` packet
*
* Note: this event is not fired when the client is moved by holding the input
*/
playerPosition: [position: BaseVec3],
/** When the chunk is loaded */
loadChunk: [chunkX: number, sectionY: number, chunkZ: number],
/** When the chunk got deleted by the server */
unloadChunk: [chunkX: number, chunkZ: number],
/** When an entity is spawned or appeared in the client view distance */
spawnEntity: [entity: Entity],
/** When an entity is updated, including position, rotation, etc... */
updateEntity: [entityId: number],
/** When an entity go out of client view distance or despawned */
removeEntity: [entityId: number],
/** The registry_data packet */
registryData: [data: RegistryData],
// Chat
/** When a chat message got broadcasted */
message: [message: Message],
/** When a message is failed to be verified against the signature sent with it */
failedMessage: [message: Message],
/** The system message */
systemMessage: [message: string],
/** The raw system message */
systemMessageRaw: [textComponent: TextComponent],
/** The action bar */
actionBar: [message: string],
/** The raw action bar */
actionBarRaw: [textComponent: TextComponent],
}
/**
* High-level client.
*/
export class Client<IsReady extends boolean = boolean> extends (EventEmitter as new () => TypedEmitter<ClientEvents>) {
private static loadRegistry: boolean = false;
private tickLoop: TickLoop;
private blocks: BlockManager;
private entities: EntitiesManager;
private player: Player;
private tcp: TCPClient;
private state: SharedState<IsReady>;
private packetListener: Listener;
private packetSender: Sender;
constructor(private options: ClientOption) {
if (options.shouldVerifyMessageSignature === true && options.shouldVerifyMessageOrder !== true)
throw new HaveSignatureButNotIndex();
super();
// For managing shared data
this.state = new SharedState(options);
// For handling packet
this.packetSender = new Sender(this.state, this.emit.bind(this));
this.packetListener = new Listener(this.state, this.emit.bind(this), this.packetSender);
// For reading packet
const versionCodec = new VersionCodec(this.state);
// For throwing packet through internet
this.tcp = new TCPClient(
this.state,
{
...options,
protocolVersion: ProtocolVersionMapping[options.version]!
}
);
this.tcp.forwardPacket = versionCodec.handlePacket.bind(versionCodec);
this.tcp.parsePacket = versionCodec.encodePacket.bind(versionCodec);
this.tcp.sendInitPacket = () => {
this.packetSender.sendHandshake();
this.packetSender.sendLoginStart();
};
this.packetSender.sendPacket = this.tcp.sendPacket.bind(this.tcp);
this.packetListener.disconnect = this.disconnect.bind(this);
versionCodec.consumePacket = this.packetListener.handlePacket.bind(this.packetListener);
// For querying data
this.blocks = new BlockManager(this.state);
this.entities = new EntitiesManager(this.state);
// For physics things
this.player = new Player(
this.state,
this.on,
this.entities,
this.blocks,
);
this.player.sendPlayerPos = this.packetSender.sendPlayerPos;
this.player.sendPlayerPosRot = this.packetSender.sendPlayerPosRot;
this.player.sendPlayerRot = this.packetSender.sendPlayerRot;
this.player.sendPlayerStatus = this.packetSender.sendPlayerStatus;
// For tick-tock tick-tock
this.tickLoop = new TickLoop(() => {
this.state.drainMutation();
this.state.drainEvent(event => this.emitEvent(event));
this.state.drainPacket((packet) => this.tcp.sendPacket(packet.id, packet.data));
this.player.tick();
});
// Forward TCP events
this.tcp.on("connect", () => this.emit("connect"));
this.tcp.on("destroy", () => this.emit("destroy"));
this.on("loadChunk", (sx, sy, sz) => this.blocks.deleteChunkCache(sx, sy, sz));
this.on("unloadChunk", (sx, sz) => {
for (let sy = 0; sy < 16; sy++)
this.blocks.deleteChunkCache(sx, sy, sz);
});
}
private emitEvent(event: EmittedEvent) {
this.emit(event.event, ...event.args);
}
private async loadRegistries() {
if (Client.loadRegistry) return;
Client.loadRegistry = true;
const { version } = this.options;
await BlockRegistry.load(version);
await EntityRegistry.load(version);
await PacketRegistry.load(version);
await EffectRegistry.load(version);
}
// Start / stop
/**
* Create a connection to the server
*/
public async connect() {
await this.loadRegistries();
if (!this.state.playerUUID) {
if (this.options.auth) {
this.state.authClient = new AuthClient(this.options.auth);
const { uuid } = await this.state.authClient.getUUID();
this.state.playerUUID = uuid.replaceAll("-", "");
await this.state.startSignatureLoop();
} else
this.state.playerUUID = computeUUID(this.options.playerName);
}
this.tcp.connect();
this.once("ready", () => {
this.player.pruneInitialVal();
this.player.setInitialVal();
this.tickLoop.start();
});
this.tcp.once("destroy", () => {
if (this.state.state === ConnectionState.Disconnected) return;
this.disconnect("socket close");
});
}
/**
* Destroy the connection
*/
public disconnect(reason: string = "Disconnect", raw?: TextComponent) {
this.state.state = ConnectionState.Disconnected;
this.state.status = ClientStatus.Disconnected;
this.tickLoop.stop();
this.player.pruneInitialVal();
this.state.pruneStates();
this.emit("disconnect", reason);
if (raw)
this.emit("disconnectRaw", raw);
this.tcp.disconnect();
if (this.options.auth)
this.state.stopSignatureLoop();
}
// Inputs
/**
* Press and hold a key
*/
public hold(inout: Input) {
this.player.input(inout);
}
/**
* Release a key
*/
public release(input: Input) {
this.player.releaseInput(input);
}
/**
* Release all key
*/
public stopMoving() {
this.player.releaseAllInputs();
}
/**
* Send a message
*/
public chat(message: string) {
this.packetSender.sendMessage(message);
}
// World
/**
* Get the block state at position
*/
public at(x: number, y: number, z: number): BlockState | null;
public at(position: BaseVec3): BlockState | null;
public at(a: BaseVec3 | number, b?: number, c?: number): BlockState | null {
const vec3 = Vec3.loadArgs(a, b, c);
return this.blocks.at(vec3);
}
/**
* Rotate your face
* @param yaw In degree
* @param pitch In degree
*/
public lookAt(yaw: number, pitch: number) {
this.player.setAngle(yaw, pitch);
}
}