UNPKG

@xmcl/client

Version:

Minecraft socket pipeline utilities. Support Minecraft lan server discovery.

827 lines (820 loc) 22.6 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; // index.ts var client_exports = {}; __export(client_exports, { Bool: () => Bool, Byte: () => Byte, ByteArray: () => ByteArray, Channel: () => Channel, Double: () => Double, Field: () => Field, Float: () => Float, Handshake: () => Handshake, Int: () => Int, Json: () => Json, LAN_MULTICAST_ADDR: () => LAN_MULTICAST_ADDR, LAN_MULTICAST_PORT: () => LAN_MULTICAST_PORT, Long: () => Long, MinecraftLanDiscover: () => MinecraftLanDiscover, Packet: () => Packet, PacketDecoder: () => PacketDecoder, PacketEmitter: () => PacketEmitter, PacketEncoder: () => PacketEncoder, PacketFieldsMetadata: () => PacketFieldsMetadata, PacketInBound: () => PacketInBound, PacketMetadata: () => PacketMetadata, PacketOutbound: () => PacketOutbound, Ping: () => Ping, Pong: () => Pong, ServerQuery: () => ServerQuery, ServerStatus: () => ServerStatus, Short: () => Short, Slot: () => Slot, String: () => String, UByte: () => UByte, UShort: () => UShort, UUID: () => UUID, VarInt: () => VarInt, VarLong: () => VarLong, createChannel: () => createChannel, createClient: () => createClient, getPacketRegistryEntry: () => getPacketRegistryEntry, queryStatus: () => queryStatus }); module.exports = __toCommonJS(client_exports); // coders.ts var import_nbt = require("@xmcl/nbt"); var import_varint32 = require("@xmcl/bytebuffer/varint32"); var import_varint64 = require("@xmcl/bytebuffer/varint64"); var VarInt = { decode: (buffer, inst) => buffer.readVarint32(), encode: (buffer, inst) => { buffer.writeVarint32(inst); } }; var Int = { decode: (buffer, inst) => buffer.readInt(), encode: (buffer, inst) => { buffer.writeInt(inst); } }; var Byte = { decode: (buffer, inst) => buffer.readByte(), encode: (buffer, inst) => { buffer.writeByte(inst); } }; var UByte = { decode: (buffer, inst) => buffer.readUint8(), encode: (buffer, inst) => { buffer.writeUint8(inst); } }; var Bool = { decode: (buffer, inst) => buffer.readByte() === 1, encode: (buffer, inst) => { buffer.writeByte(inst ? 1 : 0); } }; var Float = { decode: (buffer, inst) => buffer.readFloat(), encode: (buffer, inst) => { buffer.writeFloat(inst); } }; var Double = { decode: (buffer, inst) => buffer.readDouble(), encode: (buffer, inst) => { buffer.writeDouble(inst); } }; var UUID = { decode: (buffer, inst) => { const makeDigit = (hex, digit) => { if (hex.length < digit) { let d2 = ""; for (let i = 0; i < digit - hex.length; i += 1) { d2 += "0"; } return `${d2}${hex}`; } return hex; }; const hi = buffer.readUint64(); const lo = buffer.readUint64(); let a = makeDigit(Number(hi >> 32n).toString(16), 8); const b = makeDigit((hi >> 16n & 0xFFFFn).toString(16), 4); const c = makeDigit((hi & 0xFFFFn).toString(16), 4); const d = makeDigit((lo >> 48n & 0xFFFFn).toString(16), 4); const e = makeDigit((lo & 0xFFFFFFFFFFFFn).toString(16), 12); if (a.length === 16) { a = a.substring(8, 16); } return `${a}-${b}-${c}-${d}-${e}`; }, encode: (buffer, inst) => { const components = inst.split("-"); if (components.length !== 5) { throw new Error("Invalid UUID"); } let hi = BigInt(`0x${components[0]}`); hi = hi << 16n; hi = hi | BigInt(`0x${components[1]}`); hi = hi << 16n; hi = hi | BigInt(`0x${components[2]}`); let lo = BigInt(`0x${components[3]}`); lo = lo << 48n; lo = lo | BigInt(`0x${components[4]}`); buffer.writeUint64(hi); buffer.writeUint64(lo); } }; var Short = { decode: (buffer, inst) => buffer.readShort(), encode: (buffer, inst) => { buffer.writeShort(inst); } }; var UShort = { decode: (buffer, inst) => buffer.readUint16(), encode: (buffer, inst) => { buffer.writeUint16(inst); } }; var Long = { decode: (buffer, inst) => buffer.readLong(), encode: (buffer, inst) => { buffer.writeInt64(inst); } }; var VarLong = { decode: (buffer, inst) => buffer.readVarint64(), encode: (buffer, inst) => { buffer.writeVarint64(inst); } }; var String = { decode: (buffer) => { const length = buffer.readVarint32(); return Buffer.from(buffer.readBytes(length).toBuffer()).toString("utf-8"); }, encode: (buffer, inst) => { const byte = Buffer.from(inst); buffer.writeVarint32(byte.byteLength); buffer.writeBytes(Buffer.from(inst)); } }; var Json = { decode: (buffer, inst) => { return JSON.parse(String.decode(buffer, inst)); }, encode: (buffer, inst, ctx) => { String.encode(buffer, JSON.stringify(inst), ctx); } }; var Slot = { decode: (buffer, ctx) => { const blockId = Short.decode(buffer, ctx); if (blockId === -1) { return { blockId }; } const itemCount = Byte.decode(buffer) || void 0; const itemDamage = Short.decode(buffer) || void 0; if (Byte.decode(buffer, ctx) === 0) { return { blockId, itemCount, itemDamage }; } return { blockId, itemCount, itemDamage, nbt: (0, import_nbt.deserializeSync)(Buffer.from(buffer.buffer)) }; }, encode: (buffer, inst) => { Short.encode(buffer, inst.blockId); Byte.encode(buffer, inst.itemCount || 0); Byte.encode(buffer, inst.itemDamage || 0); if (inst.nbt) { Byte.encode(buffer, 1); buffer.writeBytes((0, import_nbt.serializeSync)(inst.nbt)); } else { Byte.encode(buffer, 0); } } }; var ByteArray = { decode: (buffer, inst) => { const len = buffer.readVarint32(); const arr = new Int8Array(len); for (let i = 0; i < len; i += 1) { arr[i] = buffer.readByte(); } return arr; }, encode: (buffer, inst) => { const len = inst.length; buffer.writeVarint32(len); for (let i = 0; i < len; i += 1) { buffer.writeByte(inst[i]); } } }; // packet.ts var PacketMetadata = Symbol("PacketMetadata"); var PacketFieldsMetadata = Symbol("PacketFieldsMetadata"); function getPacketRegistryEntry(clazz) { return clazz.prototype[PacketMetadata]; } function Field(type) { return (target, key) => { const container = target; if (!container[PacketFieldsMetadata]) { container[PacketFieldsMetadata] = []; } container[PacketFieldsMetadata].push({ name: key, type }); }; } function Packet(side, id, state, name = "") { return (constructor) => { const container = constructor.prototype; const fields = container[PacketFieldsMetadata] || []; container[PacketMetadata] = { id, name: name || constructor.name, side, state, coder: { encode(buffer, value) { fields.forEach((cod) => { cod.type.encode(buffer, value[cod.name]); }); }, decode(buffer) { const value = newCall(constructor); fields.forEach((cod) => { try { value[cod.name] = cod.type.decode(buffer); } catch (e) { console.error(new Error(`Exception during reciving packet [${id}]${constructor.name}`)); console.error(e); } }); return value; } } }; }; } function newCall(Cls) { return new (Function.prototype.bind.apply(Cls, arguments))(); } // channel.ts var import_bytebuffer = require("@xmcl/bytebuffer"); var import_events = require("events"); var import_net = require("net"); var import_stream = require("stream"); var import_zlib = require("zlib"); var Channel = class extends import_events.EventEmitter { state = "handshake"; states = { client: { handshake: new PacketCoders(), login: new PacketCoders(), status: new PacketCoders(), play: new PacketCoders() }, server: { handshake: new PacketCoders(), login: new PacketCoders(), status: new PacketCoders(), play: new PacketCoders() } }; connection = new import_net.Socket({ allowHalfOpen: false }); outbound; inbound; enableCompression = false; compressionThreshold = -1; constructor() { super(); this.outbound = new MinecraftPacketEncoder(this); this.outbound.pipe(new MinecraftPacketOutbound()).pipe(this.connection); this.inbound = new MinecraftPacketInBound(); this.inbound.pipe(new PacketDecompress({ get enableCompression() { return this.enableCompression; }, get compressionThreshold() { return this.compressionThreshold; } })).pipe(new MinecraftPacketDecoder(this)).pipe(new PacketEmitter(this)); this.connection.pipe(this.inbound); } /** * Is the connection ready to read and write */ get ready() { return this.connection.readable && this.connection.writable; } findCoderById(packetId, side) { const all = this.states[side][this.state]; return all.packetIdCoders[packetId]; } getPacketId(packetInst, side) { const packetName = Object.getPrototypeOf(packetInst).constructor.name; const all = this.states[side][this.state]; return all.packetNameToId[packetName]; } registerPacketType(clazz) { const entry = clazz.prototype[PacketMetadata]; const { state, side, id, name, coder } = entry; const coders = this.states[side][state]; coders.packetIdCoders[id] = coder; coders.packetNameToId[name] = id; } registerPacket(entry) { const { state, side, id, name, coder } = entry; const coders = this.states[side][state]; coders.packetIdCoders[id] = coder; coders.packetNameToId[name] = id; } /** * Open the connection and start to listen the port. */ async listen(option) { await new Promise((resolve, reject) => { this.connection.connect(option, () => { resolve(); }); if (option.timeout) { this.connection.setTimeout(option.timeout); } if (option.keepalive) { this.connection.setKeepAlive(true, typeof option.keepalive === "boolean" ? 3500 : option.keepalive); } this.connection.once("error", (e) => { reject(e); }); this.connection.once("timeout", () => { reject(new Error("Connection timeout.")); }); }); this.connection.on("error", (e) => { this.emit("error", e); }); this.emit("listen"); } disconnect() { if (!this.ready) { return Promise.resolve(); } return new Promise((resolve, reject) => { this.connection.once("close", (err) => { if (err) { reject(err); } else { resolve(); } }); this.connection.end(); }); } /** * Sent a packet to server. */ send(message, skeleton) { if (!this.connection.writable) { throw new Error("Cannot write if the connection isn't writable!"); } if (skeleton) { Object.assign(message, skeleton); } this.outbound.write(message); this.emit("send", message); } /** * Listen for sepcific packet by its class name. */ onPacket(packet, listener) { return this.on(`packet:${packet.name}`, listener); } oncePacket(packet, listener) { return this.once(`packet:${packet.name}`, listener); } }; var PacketInBound = class extends import_stream.Transform { buffer = import_bytebuffer.ByteBuffer.allocate(1024); _transform(chunk, encoding, callback) { this.buffer.ensureCapacity(chunk.length + this.buffer.offset); this.buffer.append(chunk); this.buffer.flip(); let unresolvedBytes; do { const packetLength = this.readPacketLength(this.buffer); unresolvedBytes = this.buffer.remaining(); if (packetLength <= unresolvedBytes) { const result = Buffer.alloc(packetLength); const src = Buffer.from(this.buffer.buffer); src.copy(result, 0, this.buffer.offset, this.buffer.offset + packetLength); this.push(result); src.copyWithin(0, packetLength); this.buffer.offset = 0; this.buffer.limit -= packetLength; unresolvedBytes -= packetLength; } else { this.buffer.offset = this.buffer.limit; this.buffer.limit = this.buffer.capacity(); break; } } while (unresolvedBytes > 0); callback(); } }; var MinecraftPacketInBound = class extends PacketInBound { readPacketLength(bb) { return bb.readVarint32(); } }; var PacketDecompress = class extends import_stream.Transform { constructor(option) { super(); this.option = option; } _transform(chunk, encoding, callback) { if (!this.option.enableCompression) { this.push(chunk); callback(); return; } const message = import_bytebuffer.ByteBuffer.wrap(chunk); const dataLength = message.readVarint32(); if (dataLength === 0 || dataLength < this.option.compressionThreshold) { this.push(message.buffer.slice(message.offset)); callback(); } else { const compressedContent = message.buffer.slice(message.offset); (0, import_zlib.unzip)(compressedContent, (err, result) => { if (err) { callback(err); } else { this.push(result); callback(); } }); } } }; var PacketDecoder = class extends import_stream.Transform { constructor(client) { super({ writableObjectMode: true, readableObjectMode: true }); this.client = client; } _transform(chunk, encoding, callback) { const message = import_bytebuffer.ByteBuffer.wrap(chunk); const packetId = this.readPacketId(message); const packetContent = message.slice(); const coder = this.client.findCoderById(packetId, "server"); if (coder) { this.push(coder.decode(packetContent)); } else { console.error(`Unknown packet ${packetId} : ${packetContent.buffer}.`); } callback(); } }; var MinecraftPacketDecoder = class extends PacketDecoder { readPacketId(message) { return message.readVarint32(); } }; var PacketEmitter = class extends import_stream.Writable { constructor(eventBus) { super({ objectMode: true }); this.eventBus = eventBus; } _write(inst, encoding, callback) { this.eventBus.emit(`packet:${Object.getPrototypeOf(inst).constructor.name}`, inst); callback(); } }; var PacketEncoder = class extends import_stream.Transform { constructor(client) { super({ writableObjectMode: true, readableObjectMode: true }); this.client = client; } _transform(message, encoding, callback) { const id = this.client.getPacketId(message, "client"); const coder = this.client.findCoderById(id, "client"); if (coder && coder.encode) { const buf = new import_bytebuffer.ByteBuffer(); this.writePacketId(buf, id); coder.encode(buf, message, this.client); buf.flip(); this.push(buf.buffer.slice(0, buf.limit)); callback(); } else { callback(new Error(`Cannot find coder for message. ${JSON.stringify(message)}`)); } } }; var MinecraftPacketEncoder = class extends PacketEncoder { writePacketId(bb, id) { bb.writeByte(id); } }; var PacketOutbound = class extends import_stream.Transform { _transform(packet, encoding, callback) { const buffer = new import_bytebuffer.ByteBuffer(); this.writePacketLength(buffer, packet.length); buffer.append(packet); buffer.flip(); this.push(buffer.buffer.slice(0, buffer.limit)); callback(); } }; var MinecraftPacketOutbound = class extends PacketOutbound { writePacketLength(bb, len) { bb.writeVarint32(len); } }; var PacketCoders = class { packetIdCoders = {}; packetNameToId = {}; }; // status.ts var Handshake = class { protocolVersion; serverAddress; serverPort; nextState; }; __decorateClass([ Field(VarInt) ], Handshake.prototype, "protocolVersion", 2); __decorateClass([ Field(String) ], Handshake.prototype, "serverAddress", 2); __decorateClass([ Field(Short) ], Handshake.prototype, "serverPort", 2); __decorateClass([ Field(VarInt) ], Handshake.prototype, "nextState", 2); Handshake = __decorateClass([ Packet("client", 0, "handshake") ], Handshake); var ServerQuery = class { }; ServerQuery = __decorateClass([ Packet("client", 0, "status") ], ServerQuery); var ServerStatus = class { status; }; __decorateClass([ Field(Json) ], ServerStatus.prototype, "status", 2); ServerStatus = __decorateClass([ Packet("server", 0, "status") ], ServerStatus); var Ping = class { time = BigInt(Date.now()); }; __decorateClass([ Field(Long) ], Ping.prototype, "time", 2); Ping = __decorateClass([ Packet("client", 1, "status") ], Ping); var Pong = class { ping; }; __decorateClass([ Field(Long) ], Pong.prototype, "ping", 2); Pong = __decorateClass([ Packet("server", 1, "status") ], Pong); function createChannel() { const channel = new Channel(); channel.registerPacketType(Handshake); channel.registerPacketType(ServerQuery); channel.registerPacketType(ServerStatus); channel.registerPacketType(Ping); channel.registerPacketType(Pong); return channel; } async function queryStatus(server, options = {}) { const host = server.host; const port = server.port || 25565; const timeout = options.timeout || 4e3; const protocol = options.protocol || 498; const retry = typeof options.retryTimes === "number" ? options.retryTimes : 0; let result; let error; const channel = createChannel(); for (let retryTimes = retry + 1; retryTimes > 0; retryTimes--) { try { result = await query(channel, host, port, timeout, protocol); break; } catch (e) { error = e; } } if (result) { return result; } throw error; } function createClient(protocol, timeout) { const channel = createChannel(); return { get channel() { return channel; }, set protocol(v) { protocol = v; }, get protocol() { return protocol; }, query(host, port = 25565) { return query(channel, host, port, timeout || 4e3, protocol); } }; } async function query(channel, host, port, timeout, protocol) { await channel.listen({ host, port, timeout }); const { status } = await new Promise((resolve, reject) => { channel.oncePacket(ServerStatus, (e) => { resolve(e); }); channel.once("error", reject); try { channel.send(new Handshake(), { protocolVersion: protocol, serverAddress: host, serverPort: port, nextState: 1 }); channel.state = "status"; channel.send(new ServerQuery()); setTimeout(() => { reject(new Error("Timeout")); }, timeout * 10); } catch (e) { reject(e); } }); const { ping } = await new Promise((resolve, reject) => { channel.once("packet:Pong", (e) => { resolve(e); }); channel.once("error", reject); try { channel.send(new Ping()); } catch (e) { reject(e); } }); status.ping = Number(BigInt(Date.now()) - ping); await channel.disconnect(); return status; } // lan.ts var import_dgram = require("dgram"); var import_events2 = require("events"); var LAN_MULTICAST_ADDR = "224.0.2.60"; var LAN_MULTICAST_PORT = 4445; var MinecraftLanDiscover = class extends import_events2.EventEmitter { socket; #ready = false; get isReady() { return this.#ready; } constructor() { super(); const sock = (0, import_dgram.createSocket)({ type: "udp4", reuseAddr: true }); sock.on("listening", () => { const address = sock.address(); sock.addMembership(LAN_MULTICAST_ADDR, address.address); sock.setMulticastTTL(128); sock.setBroadcast(true); this.#ready = true; }); sock.on("message", (buf, remote) => { var _a; const content = buf.toString("utf-8"); const motdRegx = /\[MOTD\](.+)\[\/MOTD\]/g; const portRegx = /\[AD\](.+)\[\/AD\]/g; const portResult = portRegx.exec(content); if (!portResult || !portResult[1]) { } else { const motd = ((_a = motdRegx.exec(content)) == null ? void 0 : _a[1]) ?? ""; const port = Number.parseInt(portResult[1]); this.emit("discover", { motd, port, remote }); } }); this.socket = sock; } broadcast(inf) { return new Promise((resolve, reject) => { this.socket.send(`[MOTD]${inf.motd}[/MOTD][AD]${inf.port}[/AD]`, LAN_MULTICAST_PORT, LAN_MULTICAST_ADDR, (err, bytes) => { if (err) reject(err); else resolve(bytes); }); }); } bind() { return new Promise((resolve, reject) => { this.socket.bind(LAN_MULTICAST_PORT, () => { resolve(); }).once("error", (e) => { reject(e); }); }); } destroy() { return new Promise((resolve) => { this.socket.close(resolve); }); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Bool, Byte, ByteArray, Channel, Double, Field, Float, Handshake, Int, Json, LAN_MULTICAST_ADDR, LAN_MULTICAST_PORT, Long, MinecraftLanDiscover, Packet, PacketDecoder, PacketEmitter, PacketEncoder, PacketFieldsMetadata, PacketInBound, PacketMetadata, PacketOutbound, Ping, Pong, ServerQuery, ServerStatus, Short, Slot, String, UByte, UShort, UUID, VarInt, VarLong, createChannel, createClient, getPacketRegistryEntry, queryStatus }); //# sourceMappingURL=index.js.map