packet/sender.tstypescript
import { createPublicKey, publicEncrypt, constants as crypto_constants, randomBytes, randomInt } from "node:crypto";
import { TypedEmmiter } from "../base/event";
import { ClientEvents } from "../client/client";
import { ProtocolVersionMapping } from "../version/registry";
import { ClientStatus, ConnectionState, ServerKnownPack, SharedState } from "../world/state";
import { sliceBuffer } from "../base/buffer";
import { randomUUIDBytes } from "../base/math";
import { Angle, BaseVec3, Direction } from "../physics/direction";
import { MessageTooLong, MissingField, NotImplemented } from "../base/error";
import BitSet from "../base/bitset";
import { makeMovementFlag } from "../binary/encoder";
import { BlockFace } from "../world/block";
/**
* Packet sender
*/
export class Sender {
constructor(
private state: SharedState,
private emit: TypedEmmiter<ClientEvents>["emit"]
) { }
public sendPacket: (packetId: string, data: object) => void = () => { throw new NotImplemented(); };
public sendHandshake() {
this.state.state = ConnectionState.Handshake;
this.sendPacket("intention",
{
protocol_version: ProtocolVersionMapping[this.state.clientOptions.version]!,
server_address: this.state.clientOptions.host,
server_port: this.state.clientOptions.port,
intent: 2
}
);
}
public sendLoginStart() {
this.state.state = ConnectionState.Login;
this.state.status = ClientStatus.Logining;
this.sendPacket("hello", {
name: this.state.clientOptions.playerName,
player_uuid: Buffer.from(this.state.playerUUID!, "hex")
});
}
public sendEncryptionResponse(publicKey: Buffer, verifyToken: Buffer) {
const encryptedSecret = publicEncrypt({
key: createPublicKey({ key: publicKey, format: 'der', type: 'spki' }),
padding: crypto_constants.RSA_PKCS1_PADDING
}, this.state.sharedSecret!);
const encryptedVerifyToken = publicEncrypt({
key: createPublicKey({ key: publicKey, format: 'der', type: 'spki' }),
padding: crypto_constants.RSA_PKCS1_PADDING
}, verifyToken);
const secretBuf = sliceBuffer(encryptedSecret, 1, (buf) => buf.readInt8());
const token = sliceBuffer(encryptedVerifyToken, 1, (buf) => buf.readInt8());
this.sendPacket("key", {
shared_secret: { value: secretBuf, length: secretBuf.length },
verify_token: { value: token, length: token.length }
});
}
public sendLoginAck() {
this.sendPacket("login_acknowledged", {});
this.state.state = ConnectionState.Configure;
this.state.server = {};
}
// Configuring
public sendKnownPack(knownPacks: ServerKnownPack[]) {
this.sendPacket("select_known_packs", {
known_packs: {
value: knownPacks,
length: knownPacks.length
}
});
}
public sendConfigureAck() {
this.sendPacket("finish_configuration", {});
this.state.state = ConnectionState.Play;
}
// Play
public sendPlayerSession() {
const sessionUUID = randomUUIDBytes();
const expireTime = 15 * 60 * 1000; // 15min in ms
const expiredAt = Date.now() + expireTime;
const { publicKey, signature } = this.state.getSignature();
this.state.sessionID = sessionUUID;
this.sendPacket("chat_session_update", {
session_id: sessionUUID,
expire_at: expiredAt,
public_key: publicKey,
signature
});
}
public sendConfirmTeleportation(teleportId: number): void;
public sendConfirmTeleportation(teleportId: number, newPos: BaseVec3, newAngle: Angle): void;
public sendConfirmTeleportation(teleportId: number, newPos?: BaseVec3, newAngle?: Angle) {
if (this.state.getProtocolId() > 776) {
if (!newPos || !newAngle) throw new MissingField("newPos or newAngle", "confirm_teleportation packet on version 26.3 or over");
const { x, y, z } = newPos,
{ yaw, pitch } = newAngle;
this.sendPacket("accept_teleportation", {
teleport_id: teleportId,
x, y, z,
yaw, pitch
});
} else
this.sendPacket("accept_teleportation", {
teleport_id: teleportId
});
}
public sendKeepAlive(id: bigint) {
this.sendPacket("keep_alive", {
keep_alive_id: id
});
}
public sendPlayerPos(
position: BaseVec3,
onGround: boolean,
horizontalCollision: boolean
) {
const { x, y, z } = position;
this.state.enqueuePacket("move_player_pos", {
x, feet_y: y, z,
flags: makeMovementFlag(onGround, horizontalCollision)
});
}
public sendPlayerPosRot(
position: BaseVec3,
angle: Angle,
onGround: boolean,
horizontalCollision: boolean
) {
const { x, y, z } = position,
{ yaw, pitch } = angle;
this.state.enqueuePacket("move_player_pos_rot", {
x, feet_y: y, z,
yaw, pitch,
flags: makeMovementFlag(onGround, horizontalCollision)
});
}
public sendPlayerRot(
angle: Angle,
onGround: boolean,
horizontalCollision: boolean
) {
const { yaw, pitch } = angle;
this.state.enqueuePacket("move_player_rot", {
yaw, pitch,
flags: makeMovementFlag(onGround, horizontalCollision)
});
}
public sendPlayerStatus(
onGround: boolean,
horizontalCollision: boolean
) {
this.state.enqueuePacket("move_player_status_only", {
flags: makeMovementFlag(onGround, horizontalCollision)
});
}
public sendMessage(
content: string
) {
if (content.length > 256)
throw new MessageTooLong(content);
const bitset = new BitSet(20);
const timestamp = Date.now();
const salt = randomBytes(8).readBigInt64BE();
this.sendPacket("chat", {
message: content,
timestamp,
salt,
signature: null, // skip for now
message_count: this.state.messageCount,
acknowledged: bitset,
checksum: 0
});
this.state.messageCount = 0;
}
public sendUseItemOn(
hand: "main" | "off",
position: BaseVec3,
face: BlockFace | Direction,
cursorPositionOnBlock: BaseVec3,
isInsideBlock: boolean,
worldBorderHit: boolean,
) {
const face_ = (face instanceof Direction ? face.index3D : face) as number;
this.sendPacket("use_item_on", {
hand: hand === "off" ? 1 : 0,
location: position,
face: face_,
cursor_position_x: cursorPositionOnBlock.x,
cursor_position_y: cursorPositionOnBlock.y,
cursor_position_z: cursorPositionOnBlock.z,
inside_block: isInsideBlock,
world_border_hit: worldBorderHit,
sequence: this.state.nextSequence()
});
}
public sendClientTickEnd() {
this.sendPacket("client_tick_end", {});
}
public sendClickContainer(
windowId: number,
stateId: number,
slot: number,
button: number,
mode: number
) {
this.sendPacket("container_click", {
window_id: windowId,
state_id: stateId,
slot,
button,
mode,
array_of_changed_slots: [],
carried_item: {
has_item: false
}
});
}
public sendCloseContainer() {
if (!this.state.openingContainer) return;
this.sendPacket("container_close", {
window_id: this.state.openingContainer.id
});
}
public sendSetCarriedSlot(slot: number) {
this.sendPacket("set_carried_item", {
slot
});
}
public sendChunkBatchReceived(chunkPerTick: number) {
this.sendPacket("chunk_batch_received", {
chunks_per_tick: chunkPerTick
});
}
public sendPingRequest() {
this.sendPacket("ping_request", {
timestamp: process.hrtime.bigint()
});
}
public sendRespawn() {
this.sendPacket("client_command", {
action_id: 0
});
}
}