@xmcl/client
Version:
Minecraft socket pipeline utilities. Support Minecraft lan server discovery.
854 lines (847 loc) • 23.5 kB
JavaScript
;
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_ADDR_V6: () => LAN_MULTICAST_ADDR_V6,
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;
listened = false;
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 false;
},
get compressionThreshold() {
return -1;
}
})).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 && this.listened;
}
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");
this.listened = true;
}
disconnect() {
if (!this.listened || !this.ready) {
return Promise.resolve();
}
this.listened = false;
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);
}
return new Promise((resolve, reject) => {
this.outbound.write(message, (err) => {
if (err) {
reject(err);
} else {
this.emit("send", message);
resolve();
}
});
});
}
/**
* 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();
const arr = buf.buffer.slice(0, buf.limit);
this.push(Buffer.from(arr));
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 {
constructor(channelWidth = Number.MAX_SAFE_INTEGER, opts) {
super(opts);
this.channelWidth = channelWidth;
}
_transform(packet, encoding, callback) {
const buffer = new import_bytebuffer.ByteBuffer();
this.writePacketLength(buffer, packet.length);
buffer.append(packet);
buffer.flip();
let bytesToSend = buffer.remaining();
while (bytesToSend > 0) {
const toSend = Math.min(bytesToSend, this.channelWidth);
const chunk = buffer.readBytes(toSend);
this.push(Buffer.from(chunk.toBuffer()));
bytesToSend -= toSend;
}
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_ADDR_V6 = "FF75:230::60";
var LAN_MULTICAST_PORT = 4445;
var MinecraftLanDiscover = class extends import_events2.EventEmitter {
socket;
#ready = false;
#group;
get isReady() {
return this.#ready;
}
constructor(type = "udp4") {
super();
const sock = (0, import_dgram.createSocket)({ type, reuseAddr: true });
this.#group = type === "udp4" ? LAN_MULTICAST_ADDR : LAN_MULTICAST_ADDR_V6;
sock.on("listening", () => {
const address = sock.address();
sock.addMembership(this.#group, type === "udp4" ? address.address : void 0);
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, this.#group, (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_ADDR_V6,
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