From 968786dc26bc3f3325bde823163c6e427d1f3cfe Mon Sep 17 00:00:00 2001 From: km127pl Date: Sun, 26 May 2024 19:38:51 +0200 Subject: [PATCH 01/13] server list ping init --- assets/.gitignore | 1 + assets/.gitkeep | 0 index.ts | 9 ++++ src/Config.ts | 52 ++++++++++++++++++++++- src/ParsedPacket.ts | 2 +- src/Server.ts | 20 ++++++++- src/packet/client/PingPacket.ts | 39 +++++++++++++++++ src/packet/server/PongPacket.ts | 13 ++++++ src/packet/server/StatusResponsePacket.ts | 34 +++++++++++++++ 9 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 assets/.gitignore create mode 100644 assets/.gitkeep create mode 100644 src/packet/client/PingPacket.ts create mode 100644 src/packet/server/PongPacket.ts create mode 100644 src/packet/server/StatusResponsePacket.ts diff --git a/assets/.gitignore b/assets/.gitignore new file mode 100644 index 0000000..12585c9 --- /dev/null +++ b/assets/.gitignore @@ -0,0 +1 @@ +icon.png \ No newline at end of file diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/index.ts b/index.ts index 1363788..7b8bcaa 100644 --- a/index.ts +++ b/index.ts @@ -2,6 +2,8 @@ import { Config, ConfigLoader } from "./src/Config.js"; import Server from "./src/Server.js"; import LoginSuccessPacket from "./src/packet/server/LoginSuccessPacket.js"; import Connection from "./src/Connection.js"; +import StatusResponsePacket from "./src/packet/server/StatusResponsePacket.js"; +import PongPacket from "./src/packet/server/PongPacket.js"; const config: Config = await ConfigLoader.fromFile("config.json"); const server = new Server(config); @@ -21,6 +23,8 @@ server.on("connection", (conn) => { ip: conn.socket.remoteAddress, port: conn.socket.remotePort }); + + new StatusResponsePacket(server).send(conn); }); server.on("disconnect", (conn) => { @@ -44,3 +48,8 @@ process.on("SIGINT", () => { server.on("packet.LoginPacket", (packet, conn) => { new LoginSuccessPacket(packet.data.uuid ?? Buffer.from("OfflinePlayer:" + packet.data.username, "utf-8").toString("hex").slice(0, 32), packet.data.username).send(conn).then(); }); + +server.on("packet.PingPacket", (packet, conn) => { + console.log(packet.data) + new PongPacket(packet).send(conn); +}); diff --git a/src/Config.ts b/src/Config.ts index 2c7c35e..ab299e9 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,7 +1,6 @@ import { open, access, constants, FileHandle } from "node:fs/promises"; import Logger from "./Logger.js"; - export interface Config { /** * Port to listen on @@ -17,6 +16,48 @@ export interface Config { * Kick reason for when the server is shutting down */ shutdownKickReason: ChatComponent; + + /** + * The server config + */ + server: ServerConfig +} + +export interface ServerConfig { + /** + * The motd of the server (description) + * Split optionally by `\n` + */ + motd: string, + + /** + * A path to the icon of the server + * @description Must be 64x64 and a PNG + */ + favicon?: string, + + /** + * Enforce message signing (since 1.18+) + */ + enforcesSecureChat: boolean, + + /** + * Max number of players that may be logged on at the same time + */ + maxPlayers: number, + + /** + * The protocol version & number + * @example + * ```json + * "name": "1.20.6", + * "protocol": 766 + * ``` + */ + version: { + name: string, + protocol: number + } } export class ConfigLoader { @@ -51,6 +92,15 @@ export class ConfigLoader { logLevel: Logger.Level.INFO, shutdownKickReason: { text: "Server closed" + }, + server: { + motd: "A Minecraft server", + enforcesSecureChat: false, + maxPlayers: 100, + version: { + name: "1.20.6", + protocol: 766 + } } }; diff --git a/src/ParsedPacket.ts b/src/ParsedPacket.ts index da3c8b8..19e518b 100644 --- a/src/ParsedPacket.ts +++ b/src/ParsedPacket.ts @@ -37,7 +37,7 @@ export default class ParsedPacket { this.packetData.splice(index, Packet.writeVarInt(result).byteLength); return result; } - + /** * Parse String (n) * After parsing, the buffer will be sliced diff --git a/src/Server.ts b/src/Server.ts index 3b53c14..28edd24 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -11,6 +11,8 @@ import HandshakePacket from "./packet/client/HandshakePacket"; import LoginPacket from "./packet/client/LoginPacket"; import { Config } from "./Config.js"; import Scheduler from "./Scheduler.js"; +import { readFile } from "node:fs/promises"; +import PingPacket from "./packet/client/PingPacket.js"; type ServerEvents = { /** @@ -63,6 +65,13 @@ type ServerEvents = { * @param connection Connection the packet was received from */ "packet.LoginPacket": (packet: LoginPacket, connection: Connection) => void; + + /** + * Ping packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.PingPacket": (packet: PingPacket, connection: Connection) => void; }; export default class Server extends (EventEmitter as new () => TypedEventEmitter) { @@ -74,13 +83,22 @@ export default class Server extends (EventEmitter as new () => TypedEventEmitter public static readonly path: string = path.dirname(path.join(new URL(import.meta.url).pathname, "..")); public readonly config: Config; + public favicon: string = "data:image/png;base64,"; + public constructor(config: Config) { super(); this.config = Object.freeze(config); this.logger = new Logger("Server", this.config.logLevel); } - public start() { + public async start() { + + // add a favicon if such is specified + if (this.config.server.favicon) { + const data = await readFile(this.config.server.favicon); + this.favicon += Buffer.from(data).toString("base64"); + } + this.scheduler.on("started", () => this.logger.debug("Scheduler started, freq=" + this.scheduler.frequency + "Hz")); this.scheduler.on("paused", () => this.logger.debug("Scheduler paused, age=" + this.scheduler.age)); this.scheduler.on("terminating", () => this.logger.debug("Scheduler terminated, age=" + this.scheduler.age)); diff --git a/src/packet/client/PingPacket.ts b/src/packet/client/PingPacket.ts new file mode 100644 index 0000000..c5b1f4d --- /dev/null +++ b/src/packet/client/PingPacket.ts @@ -0,0 +1,39 @@ +import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import Server from "../../Server"; +import ParsedPacket from "../../ParsedPacket"; +import Connection from "../../Connection.js"; + +@StaticImplements() +export default class PingPacket { + public readonly packet: ParsedPacket; + + public readonly data; + + /** + * Create a new PingPacket + * @param packet + */ + public constructor(packet: import("../../ParsedPacket").default) { + this.packet = packet; + + this.data = { + payload: this.packet.getVarInt()! + } as const; + } + + execute(_conn: Connection, _server: Server): void { + } + + public static readonly id = 0x01; + + public static isThisPacket(data: ParsedPacket, _conn: Connection): TypedClientPacket | null { + try { + const p = new this(data); + return (p.packet.id === this.id) ? p : null; + } + catch { + return null; + } + } +} diff --git a/src/packet/server/PongPacket.ts b/src/packet/server/PongPacket.ts new file mode 100644 index 0000000..5a5ca96 --- /dev/null +++ b/src/packet/server/PongPacket.ts @@ -0,0 +1,13 @@ +import ServerPacket from "../../ServerPacket.js"; +import PingPacket from "../client/PingPacket.js"; + +export default class PongPacket extends ServerPacket { + public static readonly id = 0x00; + + public constructor(c2s: PingPacket) { + super(Buffer.concat([ + ServerPacket.writeVarInt(0x01), + ServerPacket.writeVarInt(c2s.data.payload), + ])); + } +} diff --git a/src/packet/server/StatusResponsePacket.ts b/src/packet/server/StatusResponsePacket.ts new file mode 100644 index 0000000..585dcef --- /dev/null +++ b/src/packet/server/StatusResponsePacket.ts @@ -0,0 +1,34 @@ +import Server from "../../Server.js"; +import ServerPacket from "../../ServerPacket.js"; + +export default class StatusResponsePacket extends ServerPacket { + public static readonly id = 0x00; + + public constructor(server: Server) { + super(Buffer.concat([ + ServerPacket.writeVarInt(0x00), + ServerPacket.writeString(JSON.stringify({ + "version": server.config.server.version, + "players": { + "max": server.config.server.maxPlayers, + "online": 2, //todo: set to NaN and see if it dies in misery + "sample": [ + { + "name": "lp721mk", + "uuid": "372a65f92685477d84eb0d55e78668cc" + }, + { + "name": "km127pl", + "uuid": "d8fac45d0c7d46b7ba406a6921186bda" + } + ] + }, + "description": { + "text": server.config.server.motd + }, + "favicon": server.favicon, + "enforcesSecureChat": server.config.server.enforcesSecureChat + })) + ])); + } +} From e990b3ba4281ab9945f945a40fe04cfbb42574bb Mon Sep 17 00:00:00 2001 From: km127pl Date: Sun, 26 May 2024 19:52:45 +0200 Subject: [PATCH 02/13] long --- src/Packet.ts | 36 +++++++++++++++++++++++++++++++++ src/ParsedPacket.ts | 13 ++++++++++++ src/packet/client/PingPacket.ts | 5 ++++- src/packet/server/PongPacket.ts | 4 ++-- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/Packet.ts b/src/Packet.ts index 8d8f730..3a197e7 100644 --- a/src/Packet.ts +++ b/src/Packet.ts @@ -179,6 +179,42 @@ export default class Packet { return buffer; } + /** + * Parse ULong + * @param buffer + */ + public static parseULong(buffer: Buffer): bigint { + return buffer.readBigUint64BE(0); + } + + /** + * Write ULong + * @param value + */ + public static writeULong(value: bigint): Buffer { + const buffer = Buffer.alloc(8); + buffer.writeBigUint64BE(value); + return buffer; + } + + /** + * Parse Long + * @param buffer + */ + public static parseLong(buffer: Buffer): bigint { + return buffer.readBigInt64BE(0); + } + + /** + * Write Long + * @param value + */ + public static writeLong(value: bigint): Buffer { + const buffer = Buffer.alloc(8); + buffer.writeBigInt64BE(value); + return buffer; + } + /** * Parse chat * @param buffer diff --git a/src/ParsedPacket.ts b/src/ParsedPacket.ts index 19e518b..882056a 100644 --- a/src/ParsedPacket.ts +++ b/src/ParsedPacket.ts @@ -93,4 +93,17 @@ export default class ParsedPacket { this.packetData.splice(index, 2); return result; } + + /** + * Parse Long + * After parsing, the buffer will be sliced + * + * @param [index=0] Index in the packet + */ + public getLong(index = 0): bigint | null { + if (this.isOutOfRange(index)) return null; + const result = Packet.parseLong(this.packetBuffer.subarray(index)); + this.packetData.splice(index, 8); + return result; + } } \ No newline at end of file diff --git a/src/packet/client/PingPacket.ts b/src/packet/client/PingPacket.ts index c5b1f4d..71f0a2e 100644 --- a/src/packet/client/PingPacket.ts +++ b/src/packet/client/PingPacket.ts @@ -18,11 +18,14 @@ export default class PingPacket { this.packet = packet; this.data = { - payload: this.packet.getVarInt()! + payload: this.packet.getLong()! } as const; + + console.log(this); } execute(_conn: Connection, _server: Server): void { + // pass } public static readonly id = 0x01; diff --git a/src/packet/server/PongPacket.ts b/src/packet/server/PongPacket.ts index 5a5ca96..e0ba135 100644 --- a/src/packet/server/PongPacket.ts +++ b/src/packet/server/PongPacket.ts @@ -2,12 +2,12 @@ import ServerPacket from "../../ServerPacket.js"; import PingPacket from "../client/PingPacket.js"; export default class PongPacket extends ServerPacket { - public static readonly id = 0x00; + public static readonly id = 0x01; public constructor(c2s: PingPacket) { super(Buffer.concat([ ServerPacket.writeVarInt(0x01), - ServerPacket.writeVarInt(c2s.data.payload), + ServerPacket.writeLong(c2s.data.payload), ])); } } From def74cdf9ee2795f28a2ccce96444ccd7e7b2ec1 Mon Sep 17 00:00:00 2001 From: km127pl Date: Sun, 26 May 2024 19:56:08 +0200 Subject: [PATCH 03/13] ping works :3 --- index.ts | 1 - src/Packet.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 7b8bcaa..e0e6b55 100644 --- a/index.ts +++ b/index.ts @@ -50,6 +50,5 @@ server.on("packet.LoginPacket", (packet, conn) => { }); server.on("packet.PingPacket", (packet, conn) => { - console.log(packet.data) new PongPacket(packet).send(conn); }); diff --git a/src/Packet.ts b/src/Packet.ts index 3a197e7..17c2106 100644 --- a/src/Packet.ts +++ b/src/Packet.ts @@ -3,6 +3,7 @@ import {TypedClientPacket, TypedClientPacketStatic} from "./types/TypedPacket"; import HandshakePacket from "./packet/client/HandshakePacket.js"; import LoginPacket from "./packet/client/LoginPacket.js"; import Connection from "./Connection"; +import PingPacket from "./packet/client/PingPacket.js"; export default class Packet { readonly #data: number[]; @@ -245,7 +246,7 @@ export default class Packet { /** * Packet types */ - public static readonly clientTypes: TypedClientPacketStatic[] = [HandshakePacket, LoginPacket]; + public static readonly clientTypes: TypedClientPacketStatic[] = [HandshakePacket, LoginPacket, PingPacket]; /** From febf27df9f7b3f90907eaeab3fb1140b491599b8 Mon Sep 17 00:00:00 2001 From: km127pl Date: Sun, 26 May 2024 19:57:13 +0200 Subject: [PATCH 04/13] missed this --- src/packet/client/PingPacket.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/packet/client/PingPacket.ts b/src/packet/client/PingPacket.ts index 71f0a2e..36e2f3d 100644 --- a/src/packet/client/PingPacket.ts +++ b/src/packet/client/PingPacket.ts @@ -20,8 +20,6 @@ export default class PingPacket { this.data = { payload: this.packet.getLong()! } as const; - - console.log(this); } execute(_conn: Connection, _server: Server): void { From ec8d0ac0ad4863fb322fbc56416469842b575c9a Mon Sep 17 00:00:00 2001 From: km127pl Date: Sun, 26 May 2024 21:04:10 +0200 Subject: [PATCH 05/13] data --- src/packet/server/StatusResponsePacket.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/packet/server/StatusResponsePacket.ts b/src/packet/server/StatusResponsePacket.ts index 585dcef..262ff12 100644 --- a/src/packet/server/StatusResponsePacket.ts +++ b/src/packet/server/StatusResponsePacket.ts @@ -11,15 +11,15 @@ export default class StatusResponsePacket extends ServerPacket { "version": server.config.server.version, "players": { "max": server.config.server.maxPlayers, - "online": 2, //todo: set to NaN and see if it dies in misery + "online": 2, "sample": [ { "name": "lp721mk", - "uuid": "372a65f92685477d84eb0d55e78668cc" + "id": "c73d1477-a7c9-40c0-a86c-387a95917332" }, { "name": "km127pl", - "uuid": "d8fac45d0c7d46b7ba406a6921186bda" + "id": "4ab89680-76a0-4e82-b90a-56cff4b38290" } ] }, From 15ddcbf92f7b5343385798a397c2cedfc119c1d0 Mon Sep 17 00:00:00 2001 From: km127pl Date: Sun, 26 May 2024 21:25:03 +0200 Subject: [PATCH 06/13] fix server crash on join --- index.ts | 6 ++-- src/Packet.ts | 3 +- src/Server.ts | 8 +++++ src/packet/client/HandshakePacket.ts | 12 ++++++-- src/packet/client/StatusRequestPacket.ts | 39 ++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 src/packet/client/StatusRequestPacket.ts diff --git a/index.ts b/index.ts index e0e6b55..7157535 100644 --- a/index.ts +++ b/index.ts @@ -23,8 +23,6 @@ server.on("connection", (conn) => { ip: conn.socket.remoteAddress, port: conn.socket.remotePort }); - - new StatusResponsePacket(server).send(conn); }); server.on("disconnect", (conn) => { @@ -52,3 +50,7 @@ server.on("packet.LoginPacket", (packet, conn) => { server.on("packet.PingPacket", (packet, conn) => { new PongPacket(packet).send(conn); }); + +server.on("packet.StatusRequestPacket", (_, conn) => { + new StatusResponsePacket(server).send(conn); +}) \ No newline at end of file diff --git a/src/Packet.ts b/src/Packet.ts index 17c2106..2072b77 100644 --- a/src/Packet.ts +++ b/src/Packet.ts @@ -4,6 +4,7 @@ import HandshakePacket from "./packet/client/HandshakePacket.js"; import LoginPacket from "./packet/client/LoginPacket.js"; import Connection from "./Connection"; import PingPacket from "./packet/client/PingPacket.js"; +import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; export default class Packet { readonly #data: number[]; @@ -246,7 +247,7 @@ export default class Packet { /** * Packet types */ - public static readonly clientTypes: TypedClientPacketStatic[] = [HandshakePacket, LoginPacket, PingPacket]; + public static readonly clientTypes: TypedClientPacketStatic[] = [HandshakePacket, StatusRequestPacket, LoginPacket, PingPacket]; /** diff --git a/src/Server.ts b/src/Server.ts index 28edd24..4f821fc 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -13,6 +13,7 @@ import { Config } from "./Config.js"; import Scheduler from "./Scheduler.js"; import { readFile } from "node:fs/promises"; import PingPacket from "./packet/client/PingPacket.js"; +import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; type ServerEvents = { /** @@ -66,6 +67,13 @@ type ServerEvents = { */ "packet.LoginPacket": (packet: LoginPacket, connection: Connection) => void; + /** + * Status request packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.StatusRequestPacket": (packet: StatusRequestPacket, connection: Connection) => void; + /** * Ping packet received * @param packet Packet that was received diff --git a/src/packet/client/HandshakePacket.ts b/src/packet/client/HandshakePacket.ts index aaf16c9..844920c 100644 --- a/src/packet/client/HandshakePacket.ts +++ b/src/packet/client/HandshakePacket.ts @@ -26,7 +26,15 @@ export default class HandshakePacket { } execute(conn: Connection, _server: Server): void { - conn._setState(Connection.State.LOGIN); + switch (this.data.nextState) { + case 1: + conn._setState(Connection.State.STATUS); + break; + case 2: + conn._setState(Connection.State.LOGIN); + break; + } + } public static readonly id = 0x00; @@ -35,7 +43,7 @@ export default class HandshakePacket { if (conn.state !== Connection.State.NONE) return null; try { const p = new this(data); - return (p.packet.id === this.id && p.data.nextState === 2) ? p : null; + return (p.packet.id === this.id) ? p : null; } catch { return null; diff --git a/src/packet/client/StatusRequestPacket.ts b/src/packet/client/StatusRequestPacket.ts new file mode 100644 index 0000000..b440e15 --- /dev/null +++ b/src/packet/client/StatusRequestPacket.ts @@ -0,0 +1,39 @@ +import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket.js"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import Server from "../../Server.js"; +import ParsedPacket from "../../ParsedPacket.js"; +import Connection from "../../Connection.js"; + +@StaticImplements() +export default class StatusRequestPacket { + public readonly packet: ParsedPacket; + + public readonly data; + + /** + * Create a new StatusRequest + * @param packet + */ + public constructor(packet: import("../../ParsedPacket.js").default) { + this.packet = packet; + + this.data = {} as const; + } + + execute(_conn: Connection, _server: Server): void { + // pass + } + + public static readonly id = 0x00; + + public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { + if (conn.state !== Connection.State.STATUS) return null; + try { + const p = new this(data); + return (p.packet.id === this.id) ? p : null; + } + catch { + return null; + } + } +} From 910d67b065be244431abfa75bc555a9ff36b460e Mon Sep 17 00:00:00 2001 From: km127pl Date: Sun, 26 May 2024 21:48:06 +0200 Subject: [PATCH 07/13] typed packets --- src/packet/Packets.ts | 76 ++++++++++++++++++++++ src/packet/client/HandshakePacket.ts | 3 +- src/packet/client/LoginPacket.ts | 3 +- src/packet/client/PingPacket.ts | 3 +- src/packet/client/StatusRequestPacket.ts | 3 +- src/packet/server/DisconnectLoginPacket.ts | 3 +- src/packet/server/DisconnectPlayPacket.ts | 3 +- src/packet/server/LoginSuccessPacket.ts | 3 +- src/packet/server/PongPacket.ts | 3 +- src/packet/server/StatusResponsePacket.ts | 3 +- 10 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 src/packet/Packets.ts diff --git a/src/packet/Packets.ts b/src/packet/Packets.ts new file mode 100644 index 0000000..fb1312f --- /dev/null +++ b/src/packet/Packets.ts @@ -0,0 +1,76 @@ +/** + * Client -> Server Packets (C2S) + * + * @abstract Serverbound + */ +export enum C2S { + /** + * Handshake + * + * State: None + */ + Handshake = 0x00, + + /** + * Login + * + * State: Login + */ + Login = 0x00, + + /** + * Ping + * + * State: any + */ + Ping = 0x01, + + /** + * Status + * + * State: Status + */ + Status = 0x00, +} + +/** + * Server -> Client Packets (S2C) + * + * @abstract Clientbound + */ +export enum S2C { + /** + * Disconnect + * + * State: Login + */ + DisconnectLogin = 0x00, + + /** + * Disconnect + * + * State: Play + */ + DisconnectPlay = 0x1A, + + /** + * Login Success + * + * State: Login + */ + LoginSuccess = 0x02, + + /** + * Pong + * + * State: any + */ + Pong = 0x01, + + /** + * Status Response + * + * State: status + */ + StatusResponse = 0x00, +} \ No newline at end of file diff --git a/src/packet/client/HandshakePacket.ts b/src/packet/client/HandshakePacket.ts index 844920c..43c0ffc 100644 --- a/src/packet/client/HandshakePacket.ts +++ b/src/packet/client/HandshakePacket.ts @@ -3,6 +3,7 @@ import StaticImplements from "../../decorator/StaticImplements.js"; import Server from "../../Server"; import ParsedPacket from "../../ParsedPacket"; import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class HandshakePacket { @@ -37,7 +38,7 @@ export default class HandshakePacket { } - public static readonly id = 0x00; + public static readonly id = C2S.Handshake; public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { if (conn.state !== Connection.State.NONE) return null; diff --git a/src/packet/client/LoginPacket.ts b/src/packet/client/LoginPacket.ts index d8163a8..6a14082 100644 --- a/src/packet/client/LoginPacket.ts +++ b/src/packet/client/LoginPacket.ts @@ -3,6 +3,7 @@ import StaticImplements from "../../decorator/StaticImplements.js"; import ParsedPacket from "../../ParsedPacket.js"; import Server from "../../Server"; import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class LoginPacket { @@ -28,7 +29,7 @@ export default class LoginPacket { conn._setState(Connection.State.LOGIN); } - public static readonly id = 0x00; + public static readonly id = C2S.Login; public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { if (conn.state !== Connection.State.LOGIN) return null; diff --git a/src/packet/client/PingPacket.ts b/src/packet/client/PingPacket.ts index 36e2f3d..55de60c 100644 --- a/src/packet/client/PingPacket.ts +++ b/src/packet/client/PingPacket.ts @@ -3,6 +3,7 @@ import StaticImplements from "../../decorator/StaticImplements.js"; import Server from "../../Server"; import ParsedPacket from "../../ParsedPacket"; import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class PingPacket { @@ -26,7 +27,7 @@ export default class PingPacket { // pass } - public static readonly id = 0x01; + public static readonly id = C2S.Ping; public static isThisPacket(data: ParsedPacket, _conn: Connection): TypedClientPacket | null { try { diff --git a/src/packet/client/StatusRequestPacket.ts b/src/packet/client/StatusRequestPacket.ts index b440e15..c1a91d1 100644 --- a/src/packet/client/StatusRequestPacket.ts +++ b/src/packet/client/StatusRequestPacket.ts @@ -3,6 +3,7 @@ import StaticImplements from "../../decorator/StaticImplements.js"; import Server from "../../Server.js"; import ParsedPacket from "../../ParsedPacket.js"; import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class StatusRequestPacket { @@ -24,7 +25,7 @@ export default class StatusRequestPacket { // pass } - public static readonly id = 0x00; + public static readonly id = C2S.Status; public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { if (conn.state !== Connection.State.STATUS) return null; diff --git a/src/packet/server/DisconnectLoginPacket.ts b/src/packet/server/DisconnectLoginPacket.ts index ebc0cb1..9697071 100644 --- a/src/packet/server/DisconnectLoginPacket.ts +++ b/src/packet/server/DisconnectLoginPacket.ts @@ -1,7 +1,8 @@ import ServerPacket from "../../ServerPacket.js"; +import { S2C } from "../Packets.js"; export default class DisconnectLoginPacket extends ServerPacket { - public static readonly id = 0x00; + public static readonly id = S2C.DisconnectLogin; public constructor(reason: ChatComponent) { super(Buffer.concat([ diff --git a/src/packet/server/DisconnectPlayPacket.ts b/src/packet/server/DisconnectPlayPacket.ts index 4882d3e..e47ed87 100644 --- a/src/packet/server/DisconnectPlayPacket.ts +++ b/src/packet/server/DisconnectPlayPacket.ts @@ -1,7 +1,8 @@ import ServerPacket from "../../ServerPacket.js"; +import { S2C } from "../Packets.js"; export default class DisconnectPlayPacket extends ServerPacket { - public static readonly id = 0x1a; + public static readonly id = S2C.DisconnectPlay; public constructor(reason: ChatComponent) { super(Buffer.concat([ diff --git a/src/packet/server/LoginSuccessPacket.ts b/src/packet/server/LoginSuccessPacket.ts index fff8b87..5434ee8 100644 --- a/src/packet/server/LoginSuccessPacket.ts +++ b/src/packet/server/LoginSuccessPacket.ts @@ -1,11 +1,12 @@ import ServerPacket from "../../ServerPacket.js"; import Connection from "../../Connection.js"; +import { S2C } from "../Packets.js"; /** * A Minecraft protocol client-bound LoginSuccess packet. */ export default class LoginSuccessPacket extends ServerPacket { - public static readonly id = 0x02; + public static readonly id = S2C.LoginSuccess; public constructor(uuid: string, username: string) { super(Buffer.concat([ diff --git a/src/packet/server/PongPacket.ts b/src/packet/server/PongPacket.ts index e0ba135..2ac6f50 100644 --- a/src/packet/server/PongPacket.ts +++ b/src/packet/server/PongPacket.ts @@ -1,8 +1,9 @@ import ServerPacket from "../../ServerPacket.js"; import PingPacket from "../client/PingPacket.js"; +import { S2C } from "../Packets.js"; export default class PongPacket extends ServerPacket { - public static readonly id = 0x01; + public static readonly id = S2C.Pong; public constructor(c2s: PingPacket) { super(Buffer.concat([ diff --git a/src/packet/server/StatusResponsePacket.ts b/src/packet/server/StatusResponsePacket.ts index 262ff12..0c8336f 100644 --- a/src/packet/server/StatusResponsePacket.ts +++ b/src/packet/server/StatusResponsePacket.ts @@ -1,8 +1,9 @@ import Server from "../../Server.js"; import ServerPacket from "../../ServerPacket.js"; +import { S2C } from "../Packets.js"; export default class StatusResponsePacket extends ServerPacket { - public static readonly id = 0x00; + public static readonly id = S2C.StatusResponse; public constructor(server: Server) { super(Buffer.concat([ From 198ba9d71d675ac12c53e957661d9b2fc28723a2 Mon Sep 17 00:00:00 2001 From: km127pl Date: Mon, 27 May 2024 12:09:23 +0200 Subject: [PATCH 08/13] configuration state finish --- index.ts | 9 +++- src/Connection.ts | 9 ++++ src/Packet.ts | 3 +- src/Server.ts | 8 ++++ src/packet/Packets.ts | 30 +++++++++++++- src/packet/client/LoginAckPacket.ts | 41 +++++++++++++++++++ src/packet/client/StatusRequestPacket.ts | 2 +- src/packet/server/PongPacket.ts | 2 +- src/packet/server/StatusResponsePacket.ts | 2 +- .../configuration/ConfigurationKeepAlive.ts | 13 ++++++ .../FinishConfigurationPacket.ts | 19 +++++++++ .../configuration/RegistryDataPacket.ts | 13 ++++++ tsconfig.json | 2 +- 13 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 src/packet/client/LoginAckPacket.ts create mode 100644 src/packet/server/configuration/ConfigurationKeepAlive.ts create mode 100644 src/packet/server/configuration/FinishConfigurationPacket.ts create mode 100644 src/packet/server/configuration/RegistryDataPacket.ts diff --git a/index.ts b/index.ts index 7157535..2e63f08 100644 --- a/index.ts +++ b/index.ts @@ -4,6 +4,9 @@ import LoginSuccessPacket from "./src/packet/server/LoginSuccessPacket.js"; import Connection from "./src/Connection.js"; import StatusResponsePacket from "./src/packet/server/StatusResponsePacket.js"; import PongPacket from "./src/packet/server/PongPacket.js"; +import ServerPacket from "./src/ServerPacket.js"; +import { setTimeout } from "timers/promises"; +import FinishConfigurationPacket from "./src/packet/server/configuration/FinishConfigurationPacket.js"; const config: Config = await ConfigLoader.fromFile("config.json"); const server = new Server(config); @@ -44,7 +47,7 @@ process.on("SIGINT", () => { }); server.on("packet.LoginPacket", (packet, conn) => { - new LoginSuccessPacket(packet.data.uuid ?? Buffer.from("OfflinePlayer:" + packet.data.username, "utf-8").toString("hex").slice(0, 32), packet.data.username).send(conn).then(); + new LoginSuccessPacket(packet.data.uuid ?? Buffer.from("OfflinePlayer:" + packet.data.username, "utf-8").toString("hex").slice(0, 32), packet.data.username).send(conn) }); server.on("packet.PingPacket", (packet, conn) => { @@ -53,4 +56,8 @@ server.on("packet.PingPacket", (packet, conn) => { server.on("packet.StatusRequestPacket", (_, conn) => { new StatusResponsePacket(server).send(conn); +}) + +server.on("packet.LoginAck", (_, conn) => { + new FinishConfigurationPacket().send(conn); }) \ No newline at end of file diff --git a/src/Connection.ts b/src/Connection.ts index 70bbacc..258b163 100644 --- a/src/Connection.ts +++ b/src/Connection.ts @@ -2,6 +2,7 @@ import net from "node:net"; import * as crypto from "node:crypto"; import Server from "./Server"; import Packet from "./Packet.js"; +import Logger from "./Logger.js"; /** * A TCP socket connection to the server. @@ -33,6 +34,7 @@ class Connection { /** @internal */ public _setState(state: Connection.State): void { + new Logger("State", Logger.Level.DEBUG).debug(`Switching state from ${this.#state} to ${state}`) this.#state = state; } @@ -102,6 +104,13 @@ namespace Connection { */ LOGIN, + /** + * Configuration state + * + * Player is connected and is awaiting configuration data + */ + CONFIGURATION, + /** * Play state * diff --git a/src/Packet.ts b/src/Packet.ts index 2072b77..0dd9317 100644 --- a/src/Packet.ts +++ b/src/Packet.ts @@ -5,6 +5,7 @@ import LoginPacket from "./packet/client/LoginPacket.js"; import Connection from "./Connection"; import PingPacket from "./packet/client/PingPacket.js"; import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; +import LoginAckPacket from "./packet/client/LoginAckPacket.js"; export default class Packet { readonly #data: number[]; @@ -247,7 +248,7 @@ export default class Packet { /** * Packet types */ - public static readonly clientTypes: TypedClientPacketStatic[] = [HandshakePacket, StatusRequestPacket, LoginPacket, PingPacket]; + public static readonly clientTypes: TypedClientPacketStatic[] = [HandshakePacket, StatusRequestPacket, LoginAckPacket, LoginPacket, PingPacket]; /** diff --git a/src/Server.ts b/src/Server.ts index 4f821fc..a900bf8 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -14,6 +14,7 @@ import Scheduler from "./Scheduler.js"; import { readFile } from "node:fs/promises"; import PingPacket from "./packet/client/PingPacket.js"; import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; +import LoginAckPacket from "./packet/client/LoginAckPacket.js"; type ServerEvents = { /** @@ -80,6 +81,13 @@ type ServerEvents = { * @param connection Connection the packet was received from */ "packet.PingPacket": (packet: PingPacket, connection: Connection) => void; + + /** + * Login acknowledge packet + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.LoginAck": (packet: LoginAckPacket, connection: Connection) => void; }; export default class Server extends (EventEmitter as new () => TypedEventEmitter) { diff --git a/src/packet/Packets.ts b/src/packet/Packets.ts index fb1312f..4c8787e 100644 --- a/src/packet/Packets.ts +++ b/src/packet/Packets.ts @@ -30,7 +30,14 @@ export enum C2S { * * State: Status */ - Status = 0x00, + StatusRequest = 0x00, + + /** + * Login Ack + * + * State: Login + */ + LoginAcknowledge = 0x03, } /** @@ -73,4 +80,25 @@ export enum S2C { * State: status */ StatusResponse = 0x00, + + /** + * Registry Data + * + * State: join + */ + RegistryData = 0x05, + + /** + * Registry Data + * + * State: join + */ + ConfigurationKeepAlive = 0x03, + + /** + * Finish Configuration + * + * State: join->play + */ + FinishConfiguration = 0x02, } \ No newline at end of file diff --git a/src/packet/client/LoginAckPacket.ts b/src/packet/client/LoginAckPacket.ts new file mode 100644 index 0000000..7b0f48c --- /dev/null +++ b/src/packet/client/LoginAckPacket.ts @@ -0,0 +1,41 @@ +import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import Server from "../../Server"; +import ParsedPacket from "../../ParsedPacket"; +import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; + +@StaticImplements() +export default class LoginAckPacket { + public readonly packet: ParsedPacket; + + public readonly data; + + /** + * Create a new HandshakePacket + * @param packet + */ + public constructor(packet: import("../../ParsedPacket").default) { + this.packet = packet; + + this.data = {} as const; + } + + execute(conn: Connection, _server: Server): void { + conn._setState(Connection.State.CONFIGURATION); + + } + + public static readonly id = C2S.LoginAcknowledge; + + public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { + if (conn.state !== Connection.State.LOGIN) return null; + try { + const p = new this(data); + return (p.packet.id === this.id) ? p : null; + } + catch { + return null; + } + } +} diff --git a/src/packet/client/StatusRequestPacket.ts b/src/packet/client/StatusRequestPacket.ts index c1a91d1..431b740 100644 --- a/src/packet/client/StatusRequestPacket.ts +++ b/src/packet/client/StatusRequestPacket.ts @@ -25,7 +25,7 @@ export default class StatusRequestPacket { // pass } - public static readonly id = C2S.Status; + public static readonly id = C2S.StatusRequest; public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { if (conn.state !== Connection.State.STATUS) return null; diff --git a/src/packet/server/PongPacket.ts b/src/packet/server/PongPacket.ts index 2ac6f50..774732c 100644 --- a/src/packet/server/PongPacket.ts +++ b/src/packet/server/PongPacket.ts @@ -7,7 +7,7 @@ export default class PongPacket extends ServerPacket { public constructor(c2s: PingPacket) { super(Buffer.concat([ - ServerPacket.writeVarInt(0x01), + ServerPacket.writeVarInt(PongPacket.id), ServerPacket.writeLong(c2s.data.payload), ])); } diff --git a/src/packet/server/StatusResponsePacket.ts b/src/packet/server/StatusResponsePacket.ts index 0c8336f..c94a6cf 100644 --- a/src/packet/server/StatusResponsePacket.ts +++ b/src/packet/server/StatusResponsePacket.ts @@ -7,7 +7,7 @@ export default class StatusResponsePacket extends ServerPacket { public constructor(server: Server) { super(Buffer.concat([ - ServerPacket.writeVarInt(0x00), + ServerPacket.writeVarInt(StatusResponsePacket.id), ServerPacket.writeString(JSON.stringify({ "version": server.config.server.version, "players": { diff --git a/src/packet/server/configuration/ConfigurationKeepAlive.ts b/src/packet/server/configuration/ConfigurationKeepAlive.ts new file mode 100644 index 0000000..2e4ef18 --- /dev/null +++ b/src/packet/server/configuration/ConfigurationKeepAlive.ts @@ -0,0 +1,13 @@ +import ServerPacket from "../../../ServerPacket.js"; +import { S2C } from "../../Packets.js"; + +export default class ConfgirationKeepAlive extends ServerPacket { + public static readonly id = S2C.ConfigurationKeepAlive; + + public constructor() { + super(Buffer.concat([ + ServerPacket.writeVarInt(ConfgirationKeepAlive.id), + ServerPacket.writeLong(BigInt(Date.now())) + ])); + } +} diff --git a/src/packet/server/configuration/FinishConfigurationPacket.ts b/src/packet/server/configuration/FinishConfigurationPacket.ts new file mode 100644 index 0000000..4c79fdd --- /dev/null +++ b/src/packet/server/configuration/FinishConfigurationPacket.ts @@ -0,0 +1,19 @@ +import Connection from "../../../Connection.js"; +import ServerPacket from "../../../ServerPacket.js"; +import { S2C } from "../../Packets.js"; + +export default class FinishConfigurationPacket extends ServerPacket { + public static readonly id = S2C.FinishConfiguration; + + public constructor() { + super(Buffer.concat([ + ServerPacket.writeVarInt(FinishConfigurationPacket.id), + ])); + } + + public override send(connection: Connection) { + console.log('a') + connection._setState(Connection.State.PLAY); + return super.send(connection); + } +} diff --git a/src/packet/server/configuration/RegistryDataPacket.ts b/src/packet/server/configuration/RegistryDataPacket.ts new file mode 100644 index 0000000..9246835 --- /dev/null +++ b/src/packet/server/configuration/RegistryDataPacket.ts @@ -0,0 +1,13 @@ +import ServerPacket from "../../../ServerPacket.js"; +import { S2C } from "../../Packets.js"; + +export default class RegistryDataPacket extends ServerPacket { + public static readonly id = S2C.RegistryData; + + public constructor() { + super(Buffer.concat([ + ServerPacket.writeVarInt(RegistryDataPacket.id), + //FIXME: send registry + ])); + } +} diff --git a/tsconfig.json b/tsconfig.json index bc23ac0..be16aa6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -85,7 +85,7 @@ "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ From fb9c9f4a77377db06a96deb6a22a95d45a1adb06 Mon Sep 17 00:00:00 2001 From: km127pl Date: Mon, 27 May 2024 12:21:09 +0200 Subject: [PATCH 09/13] Create TODO.md --- TODO.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..aa53f18 --- /dev/null +++ b/TODO.md @@ -0,0 +1,14 @@ +## TODO + +### -> Login + #### State `CONFIGURATION` + - Send [registry data](https://wiki.vg/Protocol#Registry_Data) *nbt* + - Send feature flag packet (0x08) specifying no special features + ```json + { + "id": 0x08, + "varint0": 0 + } + ``` + - Receive client information (i.e. locale, view distance, allow server listing) [packet](https://wiki.vg/Protocol#Client_Information_.28configuration.29) + - Receive acknowledge finish configuration (switching state to play) \ No newline at end of file From 3bcff54944744b50281dabb64cc8f3bbf22bea96 Mon Sep 17 00:00:00 2001 From: km127pl Date: Mon, 27 May 2024 13:41:37 +0200 Subject: [PATCH 10/13] nbt --- index.ts | 10 +- src/nbt.ts | 752 ++++++++++++++++++ .../FinishConfigurationPacket.ts | 1 - .../configuration/RegistryDataPacket.ts | 28 +- 4 files changed, 786 insertions(+), 5 deletions(-) create mode 100644 src/nbt.ts diff --git a/index.ts b/index.ts index 2e63f08..f2ec842 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,7 @@ import PongPacket from "./src/packet/server/PongPacket.js"; import ServerPacket from "./src/ServerPacket.js"; import { setTimeout } from "timers/promises"; import FinishConfigurationPacket from "./src/packet/server/configuration/FinishConfigurationPacket.js"; +import RegistryDataPacket from "./src/packet/server/configuration/RegistryDataPacket.js"; const config: Config = await ConfigLoader.fromFile("config.json"); const server = new Server(config); @@ -58,6 +59,9 @@ server.on("packet.StatusRequestPacket", (_, conn) => { new StatusResponsePacket(server).send(conn); }) -server.on("packet.LoginAck", (_, conn) => { - new FinishConfigurationPacket().send(conn); -}) \ No newline at end of file +server.on("packet.LoginAck", async (_, conn) => { + await new RegistryDataPacket().send(conn).then(); + await new FinishConfigurationPacket().send(conn).then(); +}) + +//FIXME: loginack not executed \ No newline at end of file diff --git a/src/nbt.ts b/src/nbt.ts new file mode 100644 index 0000000..cd4eb73 --- /dev/null +++ b/src/nbt.ts @@ -0,0 +1,752 @@ +// @ts-nocheck +/* + NBT.js - a JavaScript parser for NBT archives + by Sijmen Mulder + + I, the copyright holder of this work, hereby release it into the public + domain. This applies worldwide. + + In case this is not legally possible: I grant anyone the right to use this + work for any purpose, without any conditions, unless such conditions are + required by law. +*/ + +'use strict'; + +if (typeof ArrayBuffer === 'undefined') { + throw new Error('Missing required type ArrayBuffer'); +} +if (typeof DataView === 'undefined') { + throw new Error('Missing required type DataView'); +} +if (typeof Uint8Array === 'undefined') { + throw new Error('Missing required type Uint8Array'); +} + +const nbt: { tagTypes: NbtTagTypes } = { + tagTypes: { + end: 0, + byte: 1, + short: 2, + int: 3, + long: 4, + float: 5, + double: 6, + byteArray: 7, + string: 8, + list: 9, + compound: 10, + intArray: 11, + longArray: 12 + } +}; + +export const tagTypes = { + 'end': 0, + 'byte': 1, + 'short': 2, + 'int': 3, + 'long': 4, + 'float': 5, + 'double': 6, + 'byteArray': 7, + 'string': 8, + 'list': 9, + 'compound': 10, + 'intArray': 11, + 'longArray': 12 +}; + + +/** + * A mapping from type names to NBT type numbers. + * {@link module:nbt.Writer} and {@link module:nbt.Reader} + * have corresponding methods (e.g. {@link module:nbt.Writer#int}) + * for every type. + * + * @type Object + * @see module:nbt.tagTypeNames */ +nbt.tagTypes = tagTypes; + +/** + * A mapping from NBT type numbers to type names. + * + * @type Object + * @see module:nbt.tagTypes */ +nbt.tagTypeNames = {}; +(function() { + for (let typeName in nbt.tagTypes) { + if (nbt.tagTypes.hasOwnProperty(typeName)) { + nbt.tagTypeNames[nbt.tagTypes[typeName]] = typeName; + } + } +})(); + +function hasGzipHeader(data) { + const head = new Uint8Array(data.slice(0, 2)); + return head.length === 2 && head[0] === 0x1f && head[1] === 0x8b; +} + +function encodeUTF8(str) { + const array = []; + let i, c; + for (i = 0; i < str.length; i++) { + c = str.charCodeAt(i); + if (c < 0x80) { + array.push(c); + } else if (c < 0x800) { + array.push(0xC0 | c >> 6); + array.push(0x80 | c & 0x3F); + } else if (c < 0x10000) { + array.push(0xE0 | c >> 12); + array.push(0x80 | (c >> 6) & 0x3F); + array.push(0x80 | c & 0x3F); + } else { + array.push(0xF0 | (c >> 18) & 0x07); + array.push(0x80 | (c >> 12) & 0x3F); + array.push(0x80 | (c >> 6) & 0x3F); + array.push(0x80 | c & 0x3F); + } + } + return array; +} + +function decodeUTF8(array) { + let codepoints = [], i; + for (i = 0; i < array.length; i++) { + if ((array[i] & 0x80) === 0) { + codepoints.push(array[i] & 0x7F); + } else if (i+1 < array.length && + (array[i] & 0xE0) === 0xC0 && + (array[i+1] & 0xC0) === 0x80) { + codepoints.push( + ((array[i] & 0x1F) << 6) | + ( array[i+1] & 0x3F)); + } else if (i+2 < array.length && + (array[i] & 0xF0) === 0xE0 && + (array[i+1] & 0xC0) === 0x80 && + (array[i+2] & 0xC0) === 0x80) { + codepoints.push( + ((array[i] & 0x0F) << 12) | + ((array[i+1] & 0x3F) << 6) | + ( array[i+2] & 0x3F)); + } else if (i+3 < array.length && + (array[i] & 0xF8) === 0xF0 && + (array[i+1] & 0xC0) === 0x80 && + (array[i+2] & 0xC0) === 0x80 && + (array[i+3] & 0xC0) === 0x80) { + codepoints.push( + ((array[i] & 0x07) << 18) | + ((array[i+1] & 0x3F) << 12) | + ((array[i+2] & 0x3F) << 6) | + ( array[i+3] & 0x3F)); + } + } + return String.fromCharCode.apply(null, codepoints); +} + +/* Not all environments, in particular PhantomJS, supply + Uint8Array.slice() */ +function sliceUint8Array(array, begin, end) { + if ('slice' in array) { + return array.slice(begin, end); + } else { + return new Uint8Array([].slice.call(array, begin, end)); + } +} + +/** + * In addition to the named writing methods documented below, + * the same methods are indexed by the NBT type number as well, + * as shown in the example below. + * + * @constructor + * @see module:nbt.Reader + * + * @example + * var writer = new nbt.Writer(); + * + * // all equivalent + * writer.int(42); + * writer ; + * writer(nbt.tagTypes.int)(42); + * + * // overwrite the second int + * writer.offset = 0; + * writer.int(999); + * + * return writer.buffer; */ +nbt.Writer = function() { + const self = this; + + /* Will be resized (x2) on write if necessary. */ + let buffer = new ArrayBuffer(1024); + + /* These are recreated when the buffer is */ + let dataView = new DataView(buffer); + let arrayView = new Uint8Array(buffer); + + /** + * The location in the buffer where bytes are written or read. + * This increases after every write, but can be freely changed. + * The buffer will be resized when necessary. + * + * @type number */ + this.offset = 0; + + // Ensures that the buffer is large enough to write `size` bytes + // at the current `self.offset`. + function accommodate(size) { + const requiredLength = self.offset + size; + if (buffer.byteLength >= requiredLength) { + return; + } + + let newLength = buffer.byteLength; + while (newLength < requiredLength) { + newLength *= 2; + } + + const newBuffer = new ArrayBuffer(newLength); + const newArrayView = new Uint8Array(newBuffer); + newArrayView.set(arrayView); + + // If there's a gap between the end of the old buffer + // and the start of the new one, we need to zero it out + if (self.offset > buffer.byteLength) { + newArrayView.fill(0, buffer.byteLength, self.offset); + } + + buffer = newBuffer; + dataView = new DataView(newBuffer); + arrayView = newArrayView; + } + + function write(dataType, size, value) { + accommodate(size); + dataView['set' + dataType](self.offset, value); + self.offset += size; + return self; + } + + /** + * Returns the written data as a slice from the internal buffer, + * cutting off any padding at the end. + * + * @returns {ArrayBuffer} a [0, offset] slice of the internal buffer */ + this.getData = function() { + accommodate(0); /* make sure the offset is inside the buffer */ + return buffer.slice(0, self.offset); + }; + + /** + * @method module:nbt.Writer#byte + * @param {number} value - a signed byte + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.byte] = write.bind + +(null, 'Int8', 1); + + /** + * @method module:nbt.Writer#short + * @param {number} value - a signed 16-bit integer + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.short] = write.bind(null, 'Int16', 2); + + /** + * @method module:nbt.Writer#int + * @param {number} value - a signed 32-bit integer + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.int] = write.bind(null, 'Int32', 4); + + /** + * @method module:nbt.Writer#long + * @param {number} value - a signed 64-bit integer + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.long] = function(value) { + // Ensure value is a 64-bit BigInt + if (typeof value !== 'bigint') { + throw new Error('Value must be a BigInt'); + } + const hi = Number(value >> 32n) & 0xffffffff; + const lo = Number(value & 0xffffffffn); + self[nbt.tagTypes.int](hi); + self[nbt.tagTypes.int](lo); + return self; + }; + + /** + * @method module:nbt.Writer#float + * @param {number} value - a 32-bit IEEE 754 floating point number + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.float] = write.bind(null, 'Float32', 4); + + /** + * @method module:nbt.Writer#double + * @param {number} value - a 64-bit IEEE 754 floating point number + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.double] = write.bind(null, 'Float64', 8); + + /** + * @method module:nbt.Writer#byteArray + * @param {Uint8Array} value - an array of signed bytes + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.byteArray] = function(value) { + self[nbt.tagTypes.int](value.length); + accommodate(value.length); + arrayView.set(value, self.offset); + self.offset += value.length; + return self; + }; + + /** + * @method module:nbt.Writer#string + * @param {string} value - an unprefixed UTF-8 string + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.string] = function(value) { + const encoded = encodeUTF8(value); + self[nbt.tagTypes.short](encoded.length); + accommodate(encoded.length); + arrayView.set(encoded, self.offset); + self.offset += encoded.length; + return self; + }; + + /** + * @method module:nbt.Writer#list + * @param {number} type - an NBT type number + * @param {Array} value - an array of values of the given type + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.list] = function(type, value) { + self[nbt.tagTypes.byte](type); + self[nbt.tagTypes.int](value.length); + value.forEach(function(element) { + self[type](element); + }); + return self; + }; + + /** + * @method module:nbt.Writer#compound + * @param {Object} value - an object of key-value pairs + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.compound] = function(value) { + Object.keys(value).forEach(function(key) { + const elementType = value[key].type; + self[nbt.tagTypes.byte](elementType); + self[nbt.tagTypes.string](key); + self[elementType](value[key].value); + }); + self[nbt.tagTypes.byte](nbt.tagTypes.end); + return self; + }; + + /** + * @method module:nbt.Writer#intArray + * @param {Int32Array} value - an array of signed 32-bit integers + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.intArray] = function(value) { + self[nbt.tagTypes.int](value.length); + for (let i = 0; i < value.length; i++) { + self[nbt.tagTypes.int](value[i]); + } + return self; + }; + + /** + * @method module:nbt.Writer#longArray + * @param {BigInt64Array} value - an array of signed 64-bit integers + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.longArray] = function(value) { + self[nbt.tagTypes.int](value.length); + for (let i = 0; i < value.length; i++) { + self[nbt.tagTypes.long](value[i]); + } + return self; + }; + + // Alias the methods by NBT type number + for (const type in nbt.tagTypes) { + if (nbt.tagTypes.hasOwnProperty(type) && typeof this[nbt.tagTypes[type]] === 'function') { + this[type] = this[nbt.tagTypes[type]]; + } + } +}; + +/** + * @param {ArrayBuffer} data - the NBT data to read + * @constructor + * @see module:nbt.Writer */ +nbt.Reader = function(data) { + const self = this; + + let buffer = data; + + /* These are recreated when the buffer is */ + let dataView = new DataView(buffer); + let arrayView = new Uint8Array(buffer); + + /** + * The location in the buffer where bytes are written or read. + * This increases after every read, but can be freely changed. + * The buffer will be resized when necessary. + * + * @type number */ + this.offset = 0; + + function read(dataType, size) { + const value = dataView['get' + dataType](self.offset); + self.offset += size; + return value; + } + + /** + * @method module:nbt.Reader#byte + * @returns {number} a signed byte */ + this[nbt.tagTypes.byte] = read.bind(null, 'Int8', 1); + + /** + * @method module:nbt.Reader#short + * @returns {number} a signed 16-bit integer */ + this[nbt.tagTypes.short] = read.bind(null, 'Int16', 2); + + /** + * @method module:nbt.Reader#int + * @returns {number} a signed 32-bit integer */ + this[nbt.tagTypes.int] = read.bind(null, 'Int32', 4); + + /** + * @method module:nbt.Reader#long + * @returns {bigint} a signed 64-bit integer */ + this[nbt.tagTypes.long] = function() { + const hi = self[nbt.tagTypes.int](); + const lo = self[nbt.tagTypes.int](); + return BigInt(hi) << 32n | BigInt(lo); + }; + + /** + * @method module:nbt.Reader#float + * @returns {number} a 32-bit IEEE 754 floating point number */ + this[nbt.tagTypes.float] = read.bind(null, 'Float32', 4); + + /** + * @method module:nbt.Reader#double + * @returns {number} a 64-bit IEEE 754 floating point number */ + this[nbt.tagTypes.double] = read.bind(null, 'Float64', 8); + + /** + * @method module:nbt.Reader#byteArray + * @returns {Uint8Array} an array of signed bytes */ + this[nbt.tagTypes.byteArray] = function() { + const length = self[nbt.tagTypes.int](); + const value = sliceUint8Array(arrayView, self.offset, self.offset + length); + self.offset += length; + return value; + }; + + /** + * @method module:nbt.Reader#string + * @returns {string} an unprefixed UTF-8 string */ + this[nbt.tagTypes.string] = function() { + const length = self[nbt.tagTypes.short](); + const value = sliceUint8Array(arrayView, self.offset, self.offset + length); + self.offset += length; + return decodeUTF8(value); + }; + + /** + * @method module:nbt.Reader#list + * @returns {Array} an array of values of the given type */ + this[nbt.tagTypes.list] = function() { + const type = self[nbt.tagTypes.byte](); + const length = self[nbt.tagTypes.int](); + const value = []; + for (let i = 0; i < length; i++) { + value.push(self[type]()); + } + return value; + }; + + /** + * @method module:nbt.Reader#compound + * @returns {Object} an object of key-value pairs */ + this[nbt.tagTypes.compound] = function() { + const value = {}; + let type; + while ((type = self[nbt.tagTypes.byte]()) !== nbt.tagTypes.end) + + { + const key = self[nbt.tagTypes.string](); + value[key] = { type, value: self[type]() }; + } + return value; + }; + + /** + * @method module:nbt.Reader#intArray + * @returns {Int32Array} an array of signed 32-bit integers */ + this[nbt.tagTypes.intArray] = function() { + const length = self[nbt.tagTypes.int](); + const value = new Int32Array(length); + for (let i = 0; i < length; i++) { + value[i] = self[nbt.tagTypes.int](); + } + return value; + }; + + /** + * @method module:nbt.Reader#longArray + * @returns {BigInt64Array} an array of signed 64-bit integers */ + this[nbt.tagTypes.longArray] = function() { + const length = self[nbt.tagTypes.int](); + const value = new BigInt64Array(length); + for (let i = 0; i < length; i++) { + value[i] = self[nbt.tagTypes.long](); + } + return value; + }; + + // Alias the methods by NBT type number + for (const type in nbt.tagTypes) { + if (nbt.tagTypes.hasOwnProperty(type) && typeof this[nbt.tagTypes[type]] === 'function') { + this[type] = this[nbt.tagTypes[type]]; + } + } +}; + +export default nbt; + +type NbtTagTypes = { + end: number; + byte: number; + short: number; + int: number; + long: number; + float: number; + double: number; + byteArray: number; + string: number; + list: number; + compound: number; + intArray: number; + longArray: number; +}; + +class NbtWriter { + offset: number; + buffer: ArrayBuffer; + arrayView: Uint8Array; + dataView: DataView; + + constructor(data?: ArrayBuffer) { + this.offset = 0; + this.buffer = data || new ArrayBuffer(1024); + this.arrayView = new Uint8Array(this.buffer); + this.dataView = new DataView(this.buffer); + } + + accommodate(size: number): void { + if (this.buffer.byteLength - this.offset < size) { + const newBuffer = new ArrayBuffer(this.buffer.byteLength * 2 + size); + new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)); + this.buffer = newBuffer; + this.arrayView = new Uint8Array(this.buffer); + this.dataView = new DataView(this.buffer); + } + } + + write(dataType: string, size: number, value: number | bigint): void { + this.accommodate(size); + (this.dataView as any)[`set${dataType}`](this.offset, value); + this.offset += size; + } + + byte(value: number): NbtWriter { + this.write('Int8', 1, value); + return this; + } + + short(value: number): NbtWriter { + this.write('Int16', 2, value); + return this; + } + + int(value: number): NbtWriter { + this.write('Int32', 4, value); + return this; + } + + long(value: bigint): NbtWriter { + if (typeof value !== 'bigint') { + throw new Error('Value must be a BigInt'); + } + const hi = Number(value >> 32n) & 0xffffffff; + const lo = Number(value & 0xffffffffn); + this.int(hi); + this.int(lo); + return this; + } + + float(value: number): NbtWriter { + this.write('Float32', 4, value); + return this; + } + + double(value: number): NbtWriter { + this.write('Float64', 8, value); + return this; + } + + byteArray(value: Uint8Array): NbtWriter { + this.int(value.length); + this.accommodate(value.length); + this.arrayView.set(value, this.offset); + this.offset += value.length; + return this; + } + + string(value: string): NbtWriter { + const encoded = new TextEncoder().encode(value); + this.short(encoded.length); + this.accommodate(encoded.length); + this.arrayView.set(encoded, this.offset); + this.offset += encoded.length; + return this; + } + + list(type: number, value: any[]): NbtWriter { + this.byte(type); + this.int(value.length); + value.forEach((element) => { + (this as any)[type](element); + }); + return this; + } + + compound(value: Record): NbtWriter { + Object.keys(value).forEach((key) => { + const elementType = value[key].type; + this.byte(elementType); + this.string(key); + (this as any)[elementType](value[key].value); + }); + this.byte(nbt.tagTypes.end); + return this; + } + + intArray(value: Int32Array): NbtWriter { + this.int(value.length); + for (let i = 0; i < value.length; i++) { + this.int(value[i]); + } + return this; + } + + longArray(value: BigInt64Array): NbtWriter { + this.int(value.length); + for (let i = 0; i < value.length; i++) { + this.long(value[i]); + } + return this; + } +} + +class NbtReader { + offset: number; + buffer: ArrayBuffer; + dataView: DataView; + arrayView: Uint8Array; + + constructor(data: ArrayBuffer) { + this.offset = 0; + this.buffer = data; + this.dataView = new DataView(data); + this.arrayView = new Uint8Array(data); + } + + read(dataType: string, size: number): number | bigint { + const value = (this.dataView as any)[`get${dataType}`](this.offset); + this.offset += size; + return value; + } + + byte(): number { + return this.read('Int8', 1) as number; + } + + short(): number { + return this.read('Int16', 2) as number; + } + + int(): number { + return this.read('Int32', 4) as number; + } + + long(): bigint { + const hi = this.int(); + const lo = this.int(); + return BigInt(hi) << 32n | BigInt(lo); + } + + float(): number { + return this.read('Float32', 4) as number; + } + + double(): number { + return this.read('Float64', 8) as number; + } + + byteArray(): Uint8Array { + const length = this.int(); + const value = this.arrayView.slice(this.offset, this.offset + length); + this.offset += length; + return value; + } + + string(): string { + const length = this.short(); + const value = this.arrayView.slice(this.offset, this.offset + length); + this.offset += length; + return new TextDecoder().decode(value); + } + + list(): any[] { + const type = this.byte(); + const length = this.int(); + const value: any[] = []; + for (let i = 0; i < length; i++) { + value.push((this as any)[type]()); + } + return value; + } + + compound(): Record { + const value: Record = {}; + let type; + while ((type = this.byte()) !== nbt.tagTypes.end) { + const key = this.string(); + value[key] = { type, value: (this as any)[type]() }; + } + return value; + } + + intArray(): Int32Array { + const length = this.int(); + const value = new Int32Array(length); + for (let i = 0; i < length; i++) { + value[i] = this.int(); + } + return value; + } + + longArray(): BigInt64Array { + const length = this.int(); + const value = new BigInt64Array(length); + for (let i = 0; i < length; i++) { + value[i] = this.long(); + } + return value; + } +} + +export { NbtWriter, NbtReader, nbt }; \ No newline at end of file diff --git a/src/packet/server/configuration/FinishConfigurationPacket.ts b/src/packet/server/configuration/FinishConfigurationPacket.ts index 4c79fdd..028a616 100644 --- a/src/packet/server/configuration/FinishConfigurationPacket.ts +++ b/src/packet/server/configuration/FinishConfigurationPacket.ts @@ -12,7 +12,6 @@ export default class FinishConfigurationPacket extends ServerPacket { } public override send(connection: Connection) { - console.log('a') connection._setState(Connection.State.PLAY); return super.send(connection); } diff --git a/src/packet/server/configuration/RegistryDataPacket.ts b/src/packet/server/configuration/RegistryDataPacket.ts index 9246835..97ff71d 100644 --- a/src/packet/server/configuration/RegistryDataPacket.ts +++ b/src/packet/server/configuration/RegistryDataPacket.ts @@ -1,13 +1,39 @@ import ServerPacket from "../../../ServerPacket.js"; import { S2C } from "../../Packets.js"; +import { NbtReader, NbtWriter, tagTypes } from "../../../nbt.js"; export default class RegistryDataPacket extends ServerPacket { public static readonly id = S2C.RegistryData; public constructor() { + const writer = new NbtWriter(); + writer.compound({ + "minecraft:worldgen/biome": { + type: tagTypes.compound, + value: { + "type": { + type: tagTypes.string, + value: "minecraft:worldgen/biome" + }, + "value": { + type: tagTypes.compound, + value: { + "name": { + type: tagTypes.string, + value: "minecraft:plains" + }, + "id": { + type: tagTypes.int, + value: 0 + } + } + } + } + } + }); super(Buffer.concat([ ServerPacket.writeVarInt(RegistryDataPacket.id), - //FIXME: send registry + Buffer.from(writer.buffer.slice(0, writer.offset)) ])); } } From 9e0a60c3b516d0ce249378c8ab20151b167cdcd4 Mon Sep 17 00:00:00 2001 From: km127pl Date: Mon, 27 May 2024 13:44:08 +0200 Subject: [PATCH 11/13] add prettier --- .github/ISSUE_TEMPLATE/bug_report.yaml | 74 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 28 +- .github/dependabot.yml | 8 +- .github/workflows/nodejs.yml | 42 +- .github/workflows/release.yml | 60 +- .prettierignore | 1 + .prettierrc | 8 + CODE_OF_CONDUCT.md | 32 +- README.md | 1 + SECURITY.md | 6 +- TODO.md | 3 +- index.ts | 98 +- package-lock.json | 122 +- package.json | 41 +- src/Config.ts | 247 +-- src/Connection.ts | 213 +-- src/ConnectionPool.ts | 133 +- src/Logger.ts | 493 +++--- src/Packet.ts | 543 +++---- src/ParsedPacket.ts | 201 +-- src/Scheduler.ts | 941 ++++++------ src/Server.ts | 314 ++-- src/ServerPacket.ts | 35 +- src/decorator/StaticImplements.ts | 2 +- src/nbt.ts | 1348 +++++++++-------- src/packet/Packets.ts | 162 +- src/packet/client/HandshakePacket.ts | 90 +- src/packet/client/LoginAckPacket.ts | 78 +- src/packet/client/LoginPacket.ts | 77 +- src/packet/client/PingPacket.ts | 67 +- src/packet/client/StatusRequestPacket.ts | 65 +- src/packet/server/DisconnectLoginPacket.ts | 20 +- src/packet/server/DisconnectPlayPacket.ts | 20 +- src/packet/server/LoginSuccessPacket.ts | 34 +- src/packet/server/PongPacket.ts | 22 +- src/packet/server/StatusResponsePacket.ts | 67 +- .../configuration/ConfigurationKeepAlive.ts | 20 +- .../FinishConfigurationPacket.ts | 28 +- .../configuration/RegistryDataPacket.ts | 72 +- src/types/ChatComponent.d.ts | 60 +- src/types/TypedEventEmitter.d.ts | 40 +- src/types/TypedPacket.d.ts | 18 +- tsconfig.json | 186 +-- 43 files changed, 3163 insertions(+), 2957 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 23ca7ee..a25ee49 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -3,41 +3,41 @@ description: File a bug report title: "[Bug]: " labels: ["bug", "triage"] assignees: - - octocat + - octocat body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! - - type: textarea - id: what-happened - attributes: - label: What happened? - description: Also tell us, what did you expect to happen? - placeholder: Tell us what you see! - value: "A bug happened!" - validations: - required: true - - type: dropdown - id: version - attributes: - label: Version - description: What version of our server are you running? - options: - - v1.0.0 - validations: - required: false - - type: dropdown - id: browsers - attributes: - label: What Minecraft version are you running? - multiple: true - options: - - 1.20.x and above - - 1.19.x and below - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. - render: "JavaScript" + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: dropdown + id: version + attributes: + label: Version + description: What version of our server are you running? + options: + - v1.0.0 + validations: + required: false + - type: dropdown + id: browsers + attributes: + label: What Minecraft version are you running? + multiple: true + options: + - 1.20.x and above + - 1.19.x and below + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: "JavaScript" diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 6eba735..1af7c8c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -3,18 +3,18 @@ description: Request a feature title: "[Feature]: " labels: ["enhancement"] assignees: - - octocat + - octocat body: - - type: markdown - attributes: - value: | - Thanks for taking the time to request a feature - - type: textarea - id: feature - attributes: - label: What feature would you like to see? - description: Also tell us, what do you expect of this feature? - placeholder: Tell us what you want to see! - value: "A feature!" - validations: - required: true + - type: markdown + attributes: + value: | + Thanks for taking the time to request a feature + - type: textarea + id: feature + attributes: + label: What feature would you like to see? + description: Also tell us, what do you expect of this feature? + placeholder: Tell us what you want to see! + value: "A feature!" + validations: + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a3cce5..909ed6f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "weekly" + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 80ab2c9..4683751 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,30 +1,30 @@ name: Node.js CI on: - push: - branches: [main] - pull_request: - branches: [main] + push: + branches: [main] + pull_request: + branches: [main] jobs: - build: - runs-on: ubuntu-latest + build: + runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x, 20.x] + strategy: + matrix: + node-version: [18.x, 20.x] - steps: - - name: Checkout repo - uses: actions/checkout@v3 + steps: + - name: Checkout repo + uses: actions/checkout@v3 - - name: Setup Node ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa4af27..0b3eda7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,35 +1,35 @@ name: Release on: - release: - types: [published] + release: + types: [published] jobs: - package-and-upload: - name: Build and upload - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - - name: Set up Node 18 - uses: actions/setup-node@v3 - with: - node-version: 18.x - - - name: Install dependencies - run: npm install - - - name: Bundle with NCC - run: npm run bundle - - - name: Rename bundle - run: mv build/index.js build/server-${{ github.event.release.tag_name }}.mjs - - - name: Upload to release - uses: JasonEtco/upload-to-release@master - with: - args: build/server-${{ github.event.release.tag_name }}.mjs text/javascript - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + package-and-upload: + name: Build and upload + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Node 18 + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Install dependencies + run: npm install + + - name: Bundle with NCC + run: npm run bundle + + - name: Rename bundle + run: mv build/index.js build/server-${{ github.event.release.tag_name }}.mjs + + - name: Upload to release + uses: JasonEtco/upload-to-release@master + with: + args: build/server-${{ github.event.release.tag_name }}.mjs text/javascript + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8522529 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "useTabs": true, + "singleQuote": false, + "endOfLine": "lf" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6823173..754ba44 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,24 +17,24 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/README.md b/README.md index d38c4ce..18fe373 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Fixing bugs is our utmost priority. I can't join the world! > Arciera server is a bare-bones server and does not have a world. If you want to connect to a world, you need to use a plugin that provides a world. Any additional features beyond simply establishing a connection require a plugin. + ## Contributing diff --git a/SECURITY.md b/SECURITY.md index 73d984e..c796d4b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,8 +10,8 @@ Only the latest release version of the repository is officially supported. Keepi If you discover any security vulnerabilities within this repository, we appreciate your responsible disclosure. Please report the vulnerability privately through one of the following channels: -- Via GitHub (Preferred Method): Submit a security advisory through the ["Security" tab of this repository](https://github.com/arciera/server/security). Create a new security advisory, provide a detailed description of the vulnerability, steps to reproduce, and potential impact. We will respond to your report promptly and work with you to address the issue. -- Via Email: Alternatively, you can also send a security advisory report directly to contact+arciera@zefir.pro. Please include all the necessary information as outlined above. +- Via GitHub (Preferred Method): Submit a security advisory through the ["Security" tab of this repository](https://github.com/arciera/server/security). Create a new security advisory, provide a detailed description of the vulnerability, steps to reproduce, and potential impact. We will respond to your report promptly and work with you to address the issue. +- Via Email: Alternatively, you can also send a security advisory report directly to contact+arciera@zefir.pro. Please include all the necessary information as outlined above. ### 3. Responsible Disclosure @@ -21,7 +21,7 @@ We kindly request that you give us reasonable time to assess and address the rep This security policy covers the code and components within this repository. Please refrain from attempting to access, modify, or compromise any external systems, accounts, or data beyond the scope of this repository. - --- +--- By following these guidelines, you contribute to the overall security and stability of this repository. Your commitment to responsible disclosure is vital in creating a safer environment for all users. diff --git a/TODO.md b/TODO.md index aa53f18..526f229 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ ## TODO ### -> Login + #### State `CONFIGURATION` - Send [registry data](https://wiki.vg/Protocol#Registry_Data) *nbt* - Send feature flag packet (0x08) specifying no special features @@ -11,4 +12,4 @@ } ``` - Receive client information (i.e. locale, view distance, allow server listing) [packet](https://wiki.vg/Protocol#Client_Information_.28configuration.29) - - Receive acknowledge finish configuration (switching state to play) \ No newline at end of file + - Receive acknowledge finish configuration (switching state to play) diff --git a/index.ts b/index.ts index f2ec842..256a2d9 100644 --- a/index.ts +++ b/index.ts @@ -1,67 +1,79 @@ -import { Config, ConfigLoader } from "./src/Config.js"; -import Server from "./src/Server.js"; -import LoginSuccessPacket from "./src/packet/server/LoginSuccessPacket.js"; -import Connection from "./src/Connection.js"; -import StatusResponsePacket from "./src/packet/server/StatusResponsePacket.js"; -import PongPacket from "./src/packet/server/PongPacket.js"; -import ServerPacket from "./src/ServerPacket.js"; -import { setTimeout } from "timers/promises"; -import FinishConfigurationPacket from "./src/packet/server/configuration/FinishConfigurationPacket.js"; -import RegistryDataPacket from "./src/packet/server/configuration/RegistryDataPacket.js"; +import { Config, ConfigLoader } from "./src/Config.js" +import Server from "./src/Server.js" +import LoginSuccessPacket from "./src/packet/server/LoginSuccessPacket.js" +import Connection from "./src/Connection.js" +import StatusResponsePacket from "./src/packet/server/StatusResponsePacket.js" +import PongPacket from "./src/packet/server/PongPacket.js" +import ServerPacket from "./src/ServerPacket.js" +import { setTimeout } from "timers/promises" +import FinishConfigurationPacket from "./src/packet/server/configuration/FinishConfigurationPacket.js" +import RegistryDataPacket from "./src/packet/server/configuration/RegistryDataPacket.js" -const config: Config = await ConfigLoader.fromFile("config.json"); -const server = new Server(config); +const config: Config = await ConfigLoader.fromFile("config.json") +const server = new Server(config) -server.start(); -server.on("listening", (port) => server.logger.info(`Listening on port ${port}`)); +server.start() +server.on("listening", (port) => + server.logger.info(`Listening on port ${port}`) +) server.on("unknownPacket", (packet, conn) => { - server.logger.debug("Unknown packet", `{state=${Connection.State[conn.state]}}`, packet.dataBuffer); -}); + server.logger.debug( + "Unknown packet", + `{state=${Connection.State[conn.state]}}`, + packet.dataBuffer + ) +}) server.on("packet", (packet, _conn) => { - server.logger.debug(packet.constructor.name, packet.data); -}); + server.logger.debug(packet.constructor.name, packet.data) +}) server.on("connection", (conn) => { - server.logger.debug("Connection", { - ip: conn.socket.remoteAddress, - port: conn.socket.remotePort - }); -}); + server.logger.debug("Connection", { + ip: conn.socket.remoteAddress, + port: conn.socket.remotePort, + }) +}) server.on("disconnect", (conn) => { - server.logger.debug("Disconnect", { - ip: conn.socket.remoteAddress, - port: conn.socket.remotePort - }); -}); + server.logger.debug("Disconnect", { + ip: conn.socket.remoteAddress, + port: conn.socket.remotePort, + }) +}) server.on("closed", () => { - server.logger.info("Server closed"); - process.exit(0); -}); + server.logger.info("Server closed") + process.exit(0) +}) process.on("SIGINT", () => { - process.stdout.write("\x1b[2D"); // Move cursor 2 characters left (clears ^C) - if (server.isRunning) server.stop().then(); - else process.exit(0); -}); + process.stdout.write("\x1b[2D") // Move cursor 2 characters left (clears ^C) + if (server.isRunning) server.stop().then() + else process.exit(0) +}) server.on("packet.LoginPacket", (packet, conn) => { - new LoginSuccessPacket(packet.data.uuid ?? Buffer.from("OfflinePlayer:" + packet.data.username, "utf-8").toString("hex").slice(0, 32), packet.data.username).send(conn) -}); + new LoginSuccessPacket( + packet.data.uuid ?? + Buffer.from("OfflinePlayer:" + packet.data.username, "utf-8") + .toString("hex") + .slice(0, 32), + packet.data.username + ).send(conn) +}) server.on("packet.PingPacket", (packet, conn) => { - new PongPacket(packet).send(conn); -}); + new PongPacket(packet).send(conn) +}) server.on("packet.StatusRequestPacket", (_, conn) => { - new StatusResponsePacket(server).send(conn); + new StatusResponsePacket(server).send(conn) }) server.on("packet.LoginAck", async (_, conn) => { - await new RegistryDataPacket().send(conn).then(); - await new FinishConfigurationPacket().send(conn).then(); + await new RegistryDataPacket().send(conn).then() + await new FinishConfigurationPacket().send(conn).then() }) -//FIXME: loginack not executed \ No newline at end of file +//FIXME: loginack not executed diff --git a/package-lock.json b/package-lock.json index 3d8553f..d17ee49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,55 +1,71 @@ { - "name": "archcraft", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "archcraft", - "version": "1.0.0", - "license": "GNU GPLv3", - "devDependencies": { - "@types/node": "^20.12.7", - "@vercel/ncc": "^0.38.1", - "typescript": "^5.4.5" - } - }, - "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@vercel/ncc": { - "version": "0.38.1", - "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz", - "integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==", - "dev": true, - "bin": { - "ncc": "dist/ncc/cli.js" - } - }, - "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - } - } + "name": "archcraft", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "archcraft", + "version": "1.0.0", + "license": "GNU GPLv3", + "devDependencies": { + "@types/node": "^20.12.7", + "@vercel/ncc": "^0.38.1", + "prettier": "3.2.5", + "typescript": "^5.4.5" + } + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz", + "integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } } diff --git a/package.json b/package.json index 4ac572d..2bd9ac5 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,23 @@ { - "name": "archcraft", - "version": "1.0.0", - "description": "", - "main": "dist/index.js", - "type": "module", - "scripts": { - "build": "tsc", - "build:start": "npm run build && npm run start", - "bundle": "ncc build ./index.ts -o build -m", - "start": "node dist/index.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "GNU GPLv3", - "devDependencies": { - "@types/node": "^20.12.7", - "typescript": "^5.4.5", - "@vercel/ncc": "^0.38.1" - } + "name": "archcraft", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "build:start": "npm run build && npm run start", + "bundle": "ncc build ./index.ts -o build -m", + "start": "node dist/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "GNU GPLv3", + "devDependencies": { + "@types/node": "^20.12.7", + "@vercel/ncc": "^0.38.1", + "prettier": "3.2.5", + "typescript": "^5.4.5" + } } diff --git a/src/Config.ts b/src/Config.ts index ab299e9..c4fa3f5 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,132 +1,133 @@ -import { open, access, constants, FileHandle } from "node:fs/promises"; -import Logger from "./Logger.js"; +import { open, access, constants, FileHandle } from "node:fs/promises" +import Logger from "./Logger.js" export interface Config { - /** - * Port to listen on - */ - port: number; - - /** - * The level to display logs at - */ - logLevel: Logger.Level; - - /** - * Kick reason for when the server is shutting down - */ - shutdownKickReason: ChatComponent; - - /** - * The server config - */ - server: ServerConfig + /** + * Port to listen on + */ + port: number + + /** + * The level to display logs at + */ + logLevel: Logger.Level + + /** + * Kick reason for when the server is shutting down + */ + shutdownKickReason: ChatComponent + + /** + * The server config + */ + server: ServerConfig } export interface ServerConfig { - /** - * The motd of the server (description) - * Split optionally by `\n` - */ - motd: string, - - /** - * A path to the icon of the server - * @description Must be 64x64 and a PNG - */ - favicon?: string, - - /** - * Enforce message signing (since 1.18+) - */ - enforcesSecureChat: boolean, - - /** - * Max number of players that may be logged on at the same time - */ - maxPlayers: number, - - /** - * The protocol version & number - * @example - * ```json - * "name": "1.20.6", - * "protocol": 766 - * ``` - */ - version: { - name: string, - protocol: number - } + /** + * The motd of the server (description) + * Split optionally by `\n` + */ + motd: string + + /** + * A path to the icon of the server + * @description Must be 64x64 and a PNG + */ + favicon?: string + + /** + * Enforce message signing (since 1.18+) + */ + enforcesSecureChat: boolean + + /** + * Max number of players that may be logged on at the same time + */ + maxPlayers: number + + /** + * The protocol version & number + * @example + * ```json + * "name": "1.20.6", + * "protocol": 766 + * ``` + */ + version: { + name: string + protocol: number + } } export class ConfigLoader { - /** - * Get a Config instance from a json file - * @param file The file to read from - * @returns a promise that resolves to a Config instance - * @throws {Error & {CODE: "EACCESS"}} failed to read config - * @throws {SyntaxError} failed to parse config - */ - public static async fromFile(file: string): Promise { - if (!(await ConfigLoader.exists(file))) { - await ConfigLoader.createDefault(file); - const config = ConfigLoader.getDefault(); - new Logger("Config", config.logLevel).warn("Config does not exist, creating default '%s'", file); - return config; - } - const fd: FileHandle = await open(file, "r"); - const data: string = await fd.readFile("utf-8"); - fd.close(); - - return JSON.parse(data) as Config; - } - - /** - * Get a default config instance - * @returns a default config instance - */ - public static getDefault(): Config { - return { - port: 25565, - logLevel: Logger.Level.INFO, - shutdownKickReason: { - text: "Server closed" - }, - server: { - motd: "A Minecraft server", - enforcesSecureChat: false, - maxPlayers: 100, - version: { - name: "1.20.6", - protocol: 766 - } - } - }; - - } - - /** - * Checks if a config exists - * @param file The file to check - * @returns a promise that resolves to a boolean - */ - public static async exists(file: string): Promise { - try { - await access(file, constants.F_OK); - return true; - } catch { - return false; - } - } - - /** - * Create the default config file - */ - public static async createDefault(file: string): Promise { - const fd = await open(file, "w"); - await fd.writeFile(JSON.stringify(ConfigLoader.getDefault(), null, 4)); - fd.close(); - } + /** + * Get a Config instance from a json file + * @param file The file to read from + * @returns a promise that resolves to a Config instance + * @throws {Error & {CODE: "EACCESS"}} failed to read config + * @throws {SyntaxError} failed to parse config + */ + public static async fromFile(file: string): Promise { + if (!(await ConfigLoader.exists(file))) { + await ConfigLoader.createDefault(file) + const config = ConfigLoader.getDefault() + new Logger("Config", config.logLevel).warn( + "Config does not exist, creating default '%s'", + file + ) + return config + } + const fd: FileHandle = await open(file, "r") + const data: string = await fd.readFile("utf-8") + fd.close() + + return JSON.parse(data) as Config + } + + /** + * Get a default config instance + * @returns a default config instance + */ + public static getDefault(): Config { + return { + port: 25565, + logLevel: Logger.Level.INFO, + shutdownKickReason: { + text: "Server closed", + }, + server: { + motd: "A Minecraft server", + enforcesSecureChat: false, + maxPlayers: 100, + version: { + name: "1.20.6", + protocol: 766, + }, + }, + } + } + + /** + * Checks if a config exists + * @param file The file to check + * @returns a promise that resolves to a boolean + */ + public static async exists(file: string): Promise { + try { + await access(file, constants.F_OK) + return true + } catch { + return false + } + } + + /** + * Create the default config file + */ + public static async createDefault(file: string): Promise { + const fd = await open(file, "w") + await fd.writeFile(JSON.stringify(ConfigLoader.getDefault(), null, 4)) + fd.close() + } } - diff --git a/src/Connection.ts b/src/Connection.ts index 258b163..9d70505 100644 --- a/src/Connection.ts +++ b/src/Connection.ts @@ -1,123 +1,132 @@ -import net from "node:net"; -import * as crypto from "node:crypto"; -import Server from "./Server"; -import Packet from "./Packet.js"; -import Logger from "./Logger.js"; +import net from "node:net" +import * as crypto from "node:crypto" +import Server from "./Server" +import Packet from "./Packet.js" +import Logger from "./Logger.js" /** * A TCP socket connection to the server. */ class Connection { - /** - * A unique identifier for this connection. - */ - public readonly id: string; - /** - * The TCP socket for this connection. - */ - public readonly socket: net.Socket; - /** - * The server to which this connection belongs. - */ - public readonly server: Server; - /** - * The state of the connection. - */ - #state: Connection.State = Connection.State.NONE; + /** + * A unique identifier for this connection. + */ + public readonly id: string + /** + * The TCP socket for this connection. + */ + public readonly socket: net.Socket + /** + * The server to which this connection belongs. + */ + public readonly server: Server + /** + * The state of the connection. + */ + #state: Connection.State = Connection.State.NONE - /** - * The state of the connection. - */ - public get state(): Connection.State { - return this.#state; - } + /** + * The state of the connection. + */ + public get state(): Connection.State { + return this.#state + } - /** @internal */ - public _setState(state: Connection.State): void { - new Logger("State", Logger.Level.DEBUG).debug(`Switching state from ${this.#state} to ${state}`) - this.#state = state; - } + /** @internal */ + public _setState(state: Connection.State): void { + new Logger("State", Logger.Level.DEBUG).debug( + `Switching state from ${this.#state} to ${state}` + ) + this.#state = state + } - /** - * Packet fragment this connection is currently sending to the server. - * @internal - */ - private currentPacketFragment: Packet = new Packet(); + /** + * Packet fragment this connection is currently sending to the server. + * @internal + */ + private currentPacketFragment: Packet = new Packet() - constructor(socket: net.Socket, server: Server) { - this.id = crypto.randomBytes(16).toString("hex"); - this.socket = socket; - this.server = server; - } + constructor(socket: net.Socket, server: Server) { + this.id = crypto.randomBytes(16).toString("hex") + this.socket = socket + this.server = server + } - /** @internal */ - public incomingPacketFragment(data: number) { - if (this.currentPacketFragment.push(data)) { - const p = this.currentPacketFragment.getTypedClient(this); - if (p) { - p.execute(this, this.server); - this.server.emit("packet", p, this); - this.server.emit(`packet.${p.constructor.name}` as any, p, this); - } - else this.server.emit("unknownPacket", this.currentPacketFragment, this); - this.currentPacketFragment = new Packet(); - } - } + /** @internal */ + public incomingPacketFragment(data: number) { + if (this.currentPacketFragment.push(data)) { + const p = this.currentPacketFragment.getTypedClient(this) + if (p) { + p.execute(this, this.server) + this.server.emit("packet", p, this) + this.server.emit(`packet.${p.constructor.name}` as any, p, this) + } else + this.server.emit( + "unknownPacket", + this.currentPacketFragment, + this + ) + this.currentPacketFragment = new Packet() + } + } - /** - * Disconnect this connection. - * @param [reason] The reason for the disconnect. - */ - public disconnect(reason?: string): Promise { - return this.server.connections.disconnect(this.id, reason); - } + /** + * Disconnect this connection. + * @param [reason] The reason for the disconnect. + */ + public disconnect(reason?: string): Promise { + return this.server.connections.disconnect(this.id, reason) + } - /** - * Whether this connection is connected (i.e. it can send and receive data). - */ - public get connected(): boolean { - return !this.socket.destroyed && this.server.connections.get(this.id) !== null; - } + /** + * Whether this connection is connected (i.e. it can send and receive data). + */ + public get connected(): boolean { + return ( + !this.socket.destroyed && + this.server.connections.get(this.id) !== null + ) + } } namespace Connection { - /** - * Connection state - */ - export enum State { - /** - * None / unknown - */ - NONE, + /** + * Connection state + */ + export enum State { + /** + * None / unknown + */ + NONE, - /** - * Status state - * - * Sender is checking server status - */ - STATUS, + /** + * Status state + * + * Sender is checking server status + */ + STATUS, - /** - * Login state - * - * Player is connecting to the server - */ - LOGIN, + /** + * Login state + * + * Player is connecting to the server + */ + LOGIN, - /** - * Configuration state - * - * Player is connected and is awaiting configuration data - */ - CONFIGURATION, + /** + * Configuration state + * + * Player is connected and is awaiting configuration data + */ + CONFIGURATION, - /** - * Play state - * - * Player is online and communicating game data - */ - PLAY - } + /** + * Play state + * + * Player is online and communicating game data + */ + PLAY, + } } -export default Connection; +export default Connection diff --git a/src/ConnectionPool.ts b/src/ConnectionPool.ts index 753c04b..6a236df 100644 --- a/src/ConnectionPool.ts +++ b/src/ConnectionPool.ts @@ -1,66 +1,81 @@ -import Connection from "./Connection.js"; -import DisconnectLoginPacket from "./packet/server/DisconnectLoginPacket.js"; -import DisconnectPlayPacket from "./packet/server/DisconnectPlayPacket.js"; +import Connection from "./Connection.js" +import DisconnectLoginPacket from "./packet/server/DisconnectLoginPacket.js" +import DisconnectPlayPacket from "./packet/server/DisconnectPlayPacket.js" export default class ConnectionPool { - private readonly connections: Connection[] = []; + private readonly connections: Connection[] = [] - /** - * Add a connection to the pool - * @param connection - */ - public add(connection: Connection): void { - this.connections.push(connection); - connection.socket.on("close", () => this.disconnect(connection.id)); - } + /** + * Add a connection to the pool + * @param connection + */ + public add(connection: Connection): void { + this.connections.push(connection) + connection.socket.on("close", () => this.disconnect(connection.id)) + } - /** - * Get connection by ID - * @param id The ID of the connection to get - */ - public get(id: string): Connection | null { - return this.connections.find(connection => connection.id === id) ?? null; - } + /** + * Get connection by ID + * @param id The ID of the connection to get + */ + public get(id: string): Connection | null { + return ( + this.connections.find((connection) => connection.id === id) ?? null + ) + } - /** - * Disconnect all connections - * @param [reason] The reason for the disconnect - * @returns Whether all connections disconnected successfully - */ - public async disconnectAll (reason?: string | ChatComponent): Promise { - const promises: Promise[] = []; - for (const connection of this.connections) - promises.push(this.disconnect(connection.id, reason)); - return (await Promise.all(promises)).every(result => result); - } + /** + * Disconnect all connections + * @param [reason] The reason for the disconnect + * @returns Whether all connections disconnected successfully + */ + public async disconnectAll( + reason?: string | ChatComponent + ): Promise { + const promises: Promise[] = [] + for (const connection of this.connections) + promises.push(this.disconnect(connection.id, reason)) + return (await Promise.all(promises)).every((result) => result) + } - /** - * Disconnect a connection - * @param id The ID of the connection to disconnect - * @param [reason] The reason for the disconnect - * @returns Whether the connection was found and disconnected - */ - public async disconnect(id: string, reason?: string | ChatComponent): Promise { - const connection = this.get(id); - if (!connection) return false; - const index = this.connections.indexOf(connection); - if (index === -1) return false; - const message = typeof reason === "string" ? {text: reason} : reason!; - if (reason) switch (connection.state) { - case Connection.State.LOGIN: { - await new DisconnectLoginPacket(message).send(connection); - break; - } - case Connection.State.PLAY: { - await new DisconnectPlayPacket(message).send(connection); - break; - } - default: { - connection.server.logger.warn("Cannot set disconnect reason for state " + Connection.State[connection.state] + " on connection " + connection.id); - } - } - this.connections.splice(index, 1); - connection.server.emit("disconnect", connection); - return new Promise(resolve => connection.socket.end(() => resolve(true))); - } + /** + * Disconnect a connection + * @param id The ID of the connection to disconnect + * @param [reason] The reason for the disconnect + * @returns Whether the connection was found and disconnected + */ + public async disconnect( + id: string, + reason?: string | ChatComponent + ): Promise { + const connection = this.get(id) + if (!connection) return false + const index = this.connections.indexOf(connection) + if (index === -1) return false + const message = typeof reason === "string" ? { text: reason } : reason! + if (reason) + switch (connection.state) { + case Connection.State.LOGIN: { + await new DisconnectLoginPacket(message).send(connection) + break + } + case Connection.State.PLAY: { + await new DisconnectPlayPacket(message).send(connection) + break + } + default: { + connection.server.logger.warn( + "Cannot set disconnect reason for state " + + Connection.State[connection.state] + + " on connection " + + connection.id + ) + } + } + this.connections.splice(index, 1) + connection.server.emit("disconnect", connection) + return new Promise((resolve) => + connection.socket.end(() => resolve(true)) + ) + } } diff --git a/src/Logger.ts b/src/Logger.ts index bd8fc63..97f02b8 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,248 +1,257 @@ class Logger { - private readonly name: string; - private readonly logLevel: Logger.Level; - - public constructor(name: string, logLevel: Logger.Level) { - this.name = name; - this.logLevel = logLevel; - } - - /** - * Print object without any log level to STDOUT - * @param obj Object to print - */ - private stdout(...obj: any[]): void { - console.log(...obj); - } - - /** - * Print object without any log level to STDERR - * @param obj Object to print - */ - private stderr(...obj: any[]): void { - console.error(...obj); - } - - /** - * Format string with log level and prefix - * @param level Log level - * @param message Message to format - */ - private format(level: Logger.Level, message: string): string { - return `${Logger.text256(240)}[${new Date().toISOString()}] ${Logger.ansi.format.reset}${Logger.level[level]}[${this.name}/${level}]${Logger.ansi.format.reset} ${message}${Logger.ansi.format.reset}`; - } - - /** - * Log message - * @param level Log level - * @param message Message to log - * @param [obj] Objects to print - */ - public log(level: Logger.Level, message: string, ...obj: any[]): void { - if (this.shouldLog(level)) { - this[level === Logger.Level.ERROR ? "stderr" : "stdout"](this.format(level, message), ...obj); - } - } - - /** - * Log info message - * @param message Message to log - * @param [obj] Objects to print - */ - public info(message: string, ...obj: any[]): void { - this.log(Logger.Level.INFO, message, ...obj); - } - - /** - * Log warning message - * @param message Message to log - * @param [obj] Objects to print - */ - public warn(message: string, ...obj: any[]): void { - this.log(Logger.Level.WARN, message, ...obj); - } - - /** - * Log error message - * @param message Message to log - * @param [obj] Objects to print - */ - public error(message: string, ...obj: any[]): void { - this.log(Logger.Level.ERROR, message, ...obj); - } - - /** - * Log success message - * @param message Message to log - * @param [obj] Objects to print - */ - public success(message: string, ...obj: any[]): void { - this.log(Logger.Level.SUCCESS, message, ...obj); - } - - /** - * Check if a log level should be logged - * @param level Log level - * @returns true if the log level should be logged - */ - public shouldLog(level: Logger.Level): boolean { - return Logger.LevelHierarchy.indexOf(level) >= Logger.LevelHierarchy.indexOf(this.logLevel); - } - - - /** - * Log debug message - * @param message Message to log - * @param [obj] Objects to print - */ - public debug(message: string, ...obj: any[]): void { - this.log(Logger.Level.DEBUG, message, ...obj); - } - - /** - * ANSI escape codes - */ - public static readonly ansi = Object.freeze({ - text: { - black: "\x1b[30m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - cyan: "\x1b[36m", - white: "\x1b[37m", - bright: { - black: "\x1b[30;1m", - red: "\x1b[31;1m", - green: "\x1b[32;1m", - yellow: "\x1b[33;1m", - blue: "\x1b[34;1m", - magenta: "\x1b[35;1m", - cyan: "\x1b[36;1m", - white: "\x1b[37;1m", - } - }, - background: { - black: "\x1b[40m", - red: "\x1b[41m", - green: "\x1b[42m", - yellow: "\x1b[43m", - blue: "\x1b[44m", - magenta: "\x1b[45m", - cyan: "\x1b[46m", - white: "\x1b[47m", - bright: { - black: "\x1b[40;1m", - red: "\x1b[41;1m", - green: "\x1b[42;1m", - yellow: "\x1b[43;1m", - blue: "\x1b[44;1m", - magenta: "\x1b[45;1m", - cyan: "\x1b[46;1m", - white: "\x1b[47;1m", - } - }, - format: { - reset: "\x1b[0m", - bold: "\x1b[1m", - underline: "\x1b[4m", - blink: "\x1b[5m", - reverse: "\x1b[7m", - hidden: "\x1b[8m", - dim: "\x1b[2m" - } - }); - - /** - * Level formatting - */ - public static readonly level: Record = Object.freeze({ - "DEBUG": Logger.ansi.text.bright.magenta, - "INFO": Logger.ansi.text.bright.blue, - "SUCCESS": Logger.ansi.text.bright.green, - "WARN": Logger.ansi.text.bright.yellow, - "ERROR": Logger.ansi.text.bright.red, - }); - - /** - * Level hierarchy - */ - public static readonly LevelHierarchy: readonly string[] = Object.freeze([ - "DEBUG", - "INFO", - "SUCCESS", - "WARN", - "ERROR" - ]); - - /** - * 256 colors - * @param colour Colour ID from 0 to 255 - */ - public static text256(colour: number): string { - return `\x1b[38;5;${colour}m`; - } - - /** - * 256 colors - * @param colour Colour ID from 0 to 255 - */ - public static background256(colour: number): string { - return `\x1b[48;5;${colour}m`; - } - - /** - * RGB colors - * @param red Red value from 0 to 255 - * @param green Green value from 0 to 255 - * @param blue Blue value from 0 to 255 - */ - public static textRGB(red: number, green: number, blue: number): string { - return `\x1b[38;2;${red};${green};${blue}m`; - } - - /** - * RGB colors - * @param red Red value from 0 to 255 - * @param green Green value from 0 to 255 - * @param blue Blue value from 0 to 255 - */ - public static backgroundRGB(red: number, green: number, blue: number): string { - return `\x1b[48;2;${red};${green};${blue}m`; - } + private readonly name: string + private readonly logLevel: Logger.Level + + public constructor(name: string, logLevel: Logger.Level) { + this.name = name + this.logLevel = logLevel + } + + /** + * Print object without any log level to STDOUT + * @param obj Object to print + */ + private stdout(...obj: any[]): void { + console.log(...obj) + } + + /** + * Print object without any log level to STDERR + * @param obj Object to print + */ + private stderr(...obj: any[]): void { + console.error(...obj) + } + + /** + * Format string with log level and prefix + * @param level Log level + * @param message Message to format + */ + private format(level: Logger.Level, message: string): string { + return `${Logger.text256(240)}[${new Date().toISOString()}] ${Logger.ansi.format.reset}${Logger.level[level]}[${this.name}/${level}]${Logger.ansi.format.reset} ${message}${Logger.ansi.format.reset}` + } + + /** + * Log message + * @param level Log level + * @param message Message to log + * @param [obj] Objects to print + */ + public log(level: Logger.Level, message: string, ...obj: any[]): void { + if (this.shouldLog(level)) { + this[level === Logger.Level.ERROR ? "stderr" : "stdout"]( + this.format(level, message), + ...obj + ) + } + } + + /** + * Log info message + * @param message Message to log + * @param [obj] Objects to print + */ + public info(message: string, ...obj: any[]): void { + this.log(Logger.Level.INFO, message, ...obj) + } + + /** + * Log warning message + * @param message Message to log + * @param [obj] Objects to print + */ + public warn(message: string, ...obj: any[]): void { + this.log(Logger.Level.WARN, message, ...obj) + } + + /** + * Log error message + * @param message Message to log + * @param [obj] Objects to print + */ + public error(message: string, ...obj: any[]): void { + this.log(Logger.Level.ERROR, message, ...obj) + } + + /** + * Log success message + * @param message Message to log + * @param [obj] Objects to print + */ + public success(message: string, ...obj: any[]): void { + this.log(Logger.Level.SUCCESS, message, ...obj) + } + + /** + * Check if a log level should be logged + * @param level Log level + * @returns true if the log level should be logged + */ + public shouldLog(level: Logger.Level): boolean { + return ( + Logger.LevelHierarchy.indexOf(level) >= + Logger.LevelHierarchy.indexOf(this.logLevel) + ) + } + + /** + * Log debug message + * @param message Message to log + * @param [obj] Objects to print + */ + public debug(message: string, ...obj: any[]): void { + this.log(Logger.Level.DEBUG, message, ...obj) + } + + /** + * ANSI escape codes + */ + public static readonly ansi = Object.freeze({ + text: { + black: "\x1b[30m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + white: "\x1b[37m", + bright: { + black: "\x1b[30;1m", + red: "\x1b[31;1m", + green: "\x1b[32;1m", + yellow: "\x1b[33;1m", + blue: "\x1b[34;1m", + magenta: "\x1b[35;1m", + cyan: "\x1b[36;1m", + white: "\x1b[37;1m", + }, + }, + background: { + black: "\x1b[40m", + red: "\x1b[41m", + green: "\x1b[42m", + yellow: "\x1b[43m", + blue: "\x1b[44m", + magenta: "\x1b[45m", + cyan: "\x1b[46m", + white: "\x1b[47m", + bright: { + black: "\x1b[40;1m", + red: "\x1b[41;1m", + green: "\x1b[42;1m", + yellow: "\x1b[43;1m", + blue: "\x1b[44;1m", + magenta: "\x1b[45;1m", + cyan: "\x1b[46;1m", + white: "\x1b[47;1m", + }, + }, + format: { + reset: "\x1b[0m", + bold: "\x1b[1m", + underline: "\x1b[4m", + blink: "\x1b[5m", + reverse: "\x1b[7m", + hidden: "\x1b[8m", + dim: "\x1b[2m", + }, + }) + + /** + * Level formatting + */ + public static readonly level: Record = Object.freeze({ + DEBUG: Logger.ansi.text.bright.magenta, + INFO: Logger.ansi.text.bright.blue, + SUCCESS: Logger.ansi.text.bright.green, + WARN: Logger.ansi.text.bright.yellow, + ERROR: Logger.ansi.text.bright.red, + }) + + /** + * Level hierarchy + */ + public static readonly LevelHierarchy: readonly string[] = Object.freeze([ + "DEBUG", + "INFO", + "SUCCESS", + "WARN", + "ERROR", + ]) + + /** + * 256 colors + * @param colour Colour ID from 0 to 255 + */ + public static text256(colour: number): string { + return `\x1b[38;5;${colour}m` + } + + /** + * 256 colors + * @param colour Colour ID from 0 to 255 + */ + public static background256(colour: number): string { + return `\x1b[48;5;${colour}m` + } + + /** + * RGB colors + * @param red Red value from 0 to 255 + * @param green Green value from 0 to 255 + * @param blue Blue value from 0 to 255 + */ + public static textRGB(red: number, green: number, blue: number): string { + return `\x1b[38;2;${red};${green};${blue}m` + } + + /** + * RGB colors + * @param red Red value from 0 to 255 + * @param green Green value from 0 to 255 + * @param blue Blue value from 0 to 255 + */ + public static backgroundRGB( + red: number, + green: number, + blue: number + ): string { + return `\x1b[48;2;${red};${green};${blue}m` + } } namespace Logger { - /** - * Log Level - */ - export enum Level { - /** - * Info - */ - INFO = "INFO", - - /** - * Warning - */ - WARN = "WARN", - - /** - * Error - */ - ERROR = "ERROR", - - /** - * Success - */ - SUCCESS = "SUCCESS", - - /** - * Debug - */ - DEBUG = "DEBUG" - } + /** + * Log Level + */ + export enum Level { + /** + * Info + */ + INFO = "INFO", + + /** + * Warning + */ + WARN = "WARN", + + /** + * Error + */ + ERROR = "ERROR", + + /** + * Success + */ + SUCCESS = "SUCCESS", + + /** + * Debug + */ + DEBUG = "DEBUG", + } } -export default Logger; +export default Logger diff --git a/src/Packet.ts b/src/Packet.ts index 0dd9317..0dbb5bb 100644 --- a/src/Packet.ts +++ b/src/Packet.ts @@ -1,271 +1,278 @@ -import ParsedPacket from "./ParsedPacket.js"; -import {TypedClientPacket, TypedClientPacketStatic} from "./types/TypedPacket"; -import HandshakePacket from "./packet/client/HandshakePacket.js"; -import LoginPacket from "./packet/client/LoginPacket.js"; -import Connection from "./Connection"; -import PingPacket from "./packet/client/PingPacket.js"; -import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; -import LoginAckPacket from "./packet/client/LoginAckPacket.js"; +import ParsedPacket from "./ParsedPacket.js" +import { TypedClientPacket, TypedClientPacketStatic } from "./types/TypedPacket" +import HandshakePacket from "./packet/client/HandshakePacket.js" +import LoginPacket from "./packet/client/LoginPacket.js" +import Connection from "./Connection" +import PingPacket from "./packet/client/PingPacket.js" +import StatusRequestPacket from "./packet/client/StatusRequestPacket.js" +import LoginAckPacket from "./packet/client/LoginAckPacket.js" export default class Packet { - readonly #data: number[]; - - /** - * Create a new packet - * @param [data] Packet data - */ - public constructor(data: number[] = []) { - this.#data = data; - } - - /** - * Check if the packet is complete - * - * The first byte in the packet is the length of the complete packet. - */ - public get isComplete(): boolean { - const length = this.expectedLength; - if (!length) return false; - return this.dataBuffer.byteLength - 1 === length; - } - - public get expectedLength(): number { - return Packet.parseVarInt(Buffer.from(this.#data)); - } - - /** - * Get packet data - */ - public get data(): number[] { - return this.#data; - } - - /** - * Get packet data - */ - public get dataBuffer(): Buffer { - return Buffer.from(this.#data); - } - - /** - * Push data to packet - * @param data - * @returns whether the packet is complete - */ - public push(data: number): boolean { - this.#data.push(data); - return this.isComplete; - } - - /** - * Parse packet - */ - public parse(): ParsedPacket { - return new ParsedPacket(this); - } - - /** - * Parse VarInt - * @param buffer - */ - public static parseVarInt(buffer: Buffer): number { - let result = 0; - let shift = 0; - let index = 0; - - while (true) { - const byte = buffer[index++]!; - result |= (byte & 0x7F) << shift; - shift += 7; - - if ((byte & 0x80) === 0) { - break; - } - } - - return result; - } - - /** - * Write VarInt - * @param value - */ - public static writeVarInt(value: number): Buffer { - const buffer = Buffer.alloc(5); - let index = 0; - - while (true) { - let byte = value & 0x7F; - value >>>= 7; - - if (value !== 0) { - byte |= 0x80; - } - - buffer[index++] = byte; - - if (value === 0) { - break; - } - } - - return buffer.subarray(0, index); - } - - /** - * Parse String (n) - * @param buffer - */ - public static parseString(buffer: Buffer): string { - const length = Packet.parseVarInt(buffer); - buffer = buffer.subarray(Packet.writeVarInt(length).length, Packet.writeVarInt(length).length + length); - return buffer.toString(); - } - - - /** - * Write String (n) - * @param value - */ - public static writeString(value: string): Buffer { - const length = Buffer.byteLength(value); - return Buffer.concat([Packet.writeVarInt(length), Buffer.from(value)]); - } - - /** - * Parse boolean - * @param buffer - */ - public static parseBoolean(buffer: Buffer): boolean { - return !!buffer.readUInt8(0); - } - - /** - * Write boolean - * @param value - */ - public static writeBoolean(value: boolean): Buffer { - return Buffer.from([value ? 1 : 0]); - } - - /** - * Parse UUID - * @param buffer - */ - public static parseUUID(buffer: Buffer): string { - return buffer.toString("hex", 0, 16); - } - - /** - * Write UUID - * @param value - */ - public static writeUUID(value: string): Buffer { - return Buffer.from(value, "hex"); - } - - /** - * Parse Unsigned Short - * @param buffer - */ - public static parseUShort(buffer: Buffer): number { - return buffer.readUInt16BE(0); - } - - /** - * Write Unsigned Short - * @param value - */ - public static writeUShort(value: number): Buffer { - const buffer = Buffer.alloc(2); - buffer.writeUInt16BE(value); - return buffer; - } - - /** - * Parse ULong - * @param buffer - */ - public static parseULong(buffer: Buffer): bigint { - return buffer.readBigUint64BE(0); - } - - /** - * Write ULong - * @param value - */ - public static writeULong(value: bigint): Buffer { - const buffer = Buffer.alloc(8); - buffer.writeBigUint64BE(value); - return buffer; - } - - /** - * Parse Long - * @param buffer - */ - public static parseLong(buffer: Buffer): bigint { - return buffer.readBigInt64BE(0); - } - - /** - * Write Long - * @param value - */ - public static writeLong(value: bigint): Buffer { - const buffer = Buffer.alloc(8); - buffer.writeBigInt64BE(value); - return buffer; - } - - /** - * Parse chat - * @param buffer - */ - public static parseChat(buffer: Buffer): ChatComponent { - return JSON.parse(Packet.parseString(buffer)) as ChatComponent; - } - - /** - * Write chat - * @param value - */ - public static writeChat(value: ChatComponent): Buffer { - return Packet.writeString(JSON.stringify(value)); - } - - /** - * Get typed client packet - */ - public getTypedClient(conn: Connection): TypedClientPacket | null { - for (const type of Packet.clientTypes) { - const p = type.isThisPacket(this.parse(), conn); - if (p !== null) return p; - } - return null; - } - - /** - * Packet types - */ - public static readonly clientTypes: TypedClientPacketStatic[] = [HandshakePacket, StatusRequestPacket, LoginAckPacket, LoginPacket, PingPacket]; - - - /** - * Split buffer - * @param buffer - * @param splitByte - */ - public static split(buffer: Buffer, splitByte: number): Buffer[] { - const buffers: Buffer[] = []; - let lastPosition = 0; - for (let i = 0; i < buffer.length; i++) { - if (buffer[i] === splitByte) { - buffers.push(buffer.subarray(lastPosition, i)); - lastPosition = i + 1; - } - } - buffers.push(buffer.subarray(lastPosition)); - return buffers; - } + readonly #data: number[] + + /** + * Create a new packet + * @param [data] Packet data + */ + public constructor(data: number[] = []) { + this.#data = data + } + + /** + * Check if the packet is complete + * + * The first byte in the packet is the length of the complete packet. + */ + public get isComplete(): boolean { + const length = this.expectedLength + if (!length) return false + return this.dataBuffer.byteLength - 1 === length + } + + public get expectedLength(): number { + return Packet.parseVarInt(Buffer.from(this.#data)) + } + + /** + * Get packet data + */ + public get data(): number[] { + return this.#data + } + + /** + * Get packet data + */ + public get dataBuffer(): Buffer { + return Buffer.from(this.#data) + } + + /** + * Push data to packet + * @param data + * @returns whether the packet is complete + */ + public push(data: number): boolean { + this.#data.push(data) + return this.isComplete + } + + /** + * Parse packet + */ + public parse(): ParsedPacket { + return new ParsedPacket(this) + } + + /** + * Parse VarInt + * @param buffer + */ + public static parseVarInt(buffer: Buffer): number { + let result = 0 + let shift = 0 + let index = 0 + + while (true) { + const byte = buffer[index++]! + result |= (byte & 0x7f) << shift + shift += 7 + + if ((byte & 0x80) === 0) { + break + } + } + + return result + } + + /** + * Write VarInt + * @param value + */ + public static writeVarInt(value: number): Buffer { + const buffer = Buffer.alloc(5) + let index = 0 + + while (true) { + let byte = value & 0x7f + value >>>= 7 + + if (value !== 0) { + byte |= 0x80 + } + + buffer[index++] = byte + + if (value === 0) { + break + } + } + + return buffer.subarray(0, index) + } + + /** + * Parse String (n) + * @param buffer + */ + public static parseString(buffer: Buffer): string { + const length = Packet.parseVarInt(buffer) + buffer = buffer.subarray( + Packet.writeVarInt(length).length, + Packet.writeVarInt(length).length + length + ) + return buffer.toString() + } + + /** + * Write String (n) + * @param value + */ + public static writeString(value: string): Buffer { + const length = Buffer.byteLength(value) + return Buffer.concat([Packet.writeVarInt(length), Buffer.from(value)]) + } + + /** + * Parse boolean + * @param buffer + */ + public static parseBoolean(buffer: Buffer): boolean { + return !!buffer.readUInt8(0) + } + + /** + * Write boolean + * @param value + */ + public static writeBoolean(value: boolean): Buffer { + return Buffer.from([value ? 1 : 0]) + } + + /** + * Parse UUID + * @param buffer + */ + public static parseUUID(buffer: Buffer): string { + return buffer.toString("hex", 0, 16) + } + + /** + * Write UUID + * @param value + */ + public static writeUUID(value: string): Buffer { + return Buffer.from(value, "hex") + } + + /** + * Parse Unsigned Short + * @param buffer + */ + public static parseUShort(buffer: Buffer): number { + return buffer.readUInt16BE(0) + } + + /** + * Write Unsigned Short + * @param value + */ + public static writeUShort(value: number): Buffer { + const buffer = Buffer.alloc(2) + buffer.writeUInt16BE(value) + return buffer + } + + /** + * Parse ULong + * @param buffer + */ + public static parseULong(buffer: Buffer): bigint { + return buffer.readBigUint64BE(0) + } + + /** + * Write ULong + * @param value + */ + public static writeULong(value: bigint): Buffer { + const buffer = Buffer.alloc(8) + buffer.writeBigUint64BE(value) + return buffer + } + + /** + * Parse Long + * @param buffer + */ + public static parseLong(buffer: Buffer): bigint { + return buffer.readBigInt64BE(0) + } + + /** + * Write Long + * @param value + */ + public static writeLong(value: bigint): Buffer { + const buffer = Buffer.alloc(8) + buffer.writeBigInt64BE(value) + return buffer + } + + /** + * Parse chat + * @param buffer + */ + public static parseChat(buffer: Buffer): ChatComponent { + return JSON.parse(Packet.parseString(buffer)) as ChatComponent + } + + /** + * Write chat + * @param value + */ + public static writeChat(value: ChatComponent): Buffer { + return Packet.writeString(JSON.stringify(value)) + } + + /** + * Get typed client packet + */ + public getTypedClient(conn: Connection): TypedClientPacket | null { + for (const type of Packet.clientTypes) { + const p = type.isThisPacket(this.parse(), conn) + if (p !== null) return p + } + return null + } + + /** + * Packet types + */ + public static readonly clientTypes: TypedClientPacketStatic[] = [ + HandshakePacket, + StatusRequestPacket, + LoginAckPacket, + LoginPacket, + PingPacket, + ] + + /** + * Split buffer + * @param buffer + * @param splitByte + */ + public static split(buffer: Buffer, splitByte: number): Buffer[] { + const buffers: Buffer[] = [] + let lastPosition = 0 + for (let i = 0; i < buffer.length; i++) { + if (buffer[i] === splitByte) { + buffers.push(buffer.subarray(lastPosition, i)) + lastPosition = i + 1 + } + } + buffers.push(buffer.subarray(lastPosition)) + return buffers + } } diff --git a/src/ParsedPacket.ts b/src/ParsedPacket.ts index 882056a..002ad9b 100644 --- a/src/ParsedPacket.ts +++ b/src/ParsedPacket.ts @@ -1,109 +1,112 @@ -import Packet from "./Packet.js"; +import Packet from "./Packet.js" export default class ParsedPacket { - public readonly packet: Packet; - private readonly packetData: number[]; - private get packetBuffer(): Buffer { - return Buffer.from(this.packetData); - } + public readonly packet: Packet + private readonly packetData: number[] + private get packetBuffer(): Buffer { + return Buffer.from(this.packetData) + } - public readonly length; - public readonly id; + public readonly length + public readonly id - constructor(packet: Packet) { - this.packet = packet; - this.packetData = [...packet.data]; - this.length = this.getVarInt(); - this.id = this.getVarInt(); - } + constructor(packet: Packet) { + this.packet = packet + this.packetData = [...packet.data] + this.length = this.getVarInt() + this.id = this.getVarInt() + } - /** - * Check if buffer index is out of range - * @param index - */ - private isOutOfRange(index: number): boolean { - return index >= this.packetBuffer.byteLength; - } + /** + * Check if buffer index is out of range + * @param index + */ + private isOutOfRange(index: number): boolean { + return index >= this.packetBuffer.byteLength + } - /** - * Parse VarInt - * After parsing, the buffer will be sliced - * - * @param [index=0] Index in the packet - */ - public getVarInt(index = 0): number | null { - if (this.isOutOfRange(index)) return null; - const result = Packet.parseVarInt(this.packetBuffer.subarray(index)); - this.packetData.splice(index, Packet.writeVarInt(result).byteLength); - return result; - } - - /** - * Parse String (n) - * After parsing, the buffer will be sliced - * - * @param [index=0] Index in the packet - */ - public getString(index = 0): string | null { - if (this.isOutOfRange(index)) return null; - const length = this.getVarInt(index); - if (length === null) return null; - const offset = index + Packet.writeVarInt(length).byteLength - 1; - if (this.isOutOfRange(offset) || this.isOutOfRange(offset + length)) return null; - const result = this.packetBuffer.subarray(offset, offset + length).toString(); - this.packetData.splice(index, offset + length - index); - return result; - } + /** + * Parse VarInt + * After parsing, the buffer will be sliced + * + * @param [index=0] Index in the packet + */ + public getVarInt(index = 0): number | null { + if (this.isOutOfRange(index)) return null + const result = Packet.parseVarInt(this.packetBuffer.subarray(index)) + this.packetData.splice(index, Packet.writeVarInt(result).byteLength) + return result + } - /** - * Parse Boolean - * After parsing, the buffer will be sliced - * - * @param [index=0] Index in the packet - */ - public getBoolean(index = 0): boolean | null { - if (this.isOutOfRange(index)) return null; - const result = Packet.parseBoolean(this.packetBuffer.subarray(index)); - this.packetData.splice(index, 1); - return result; - } + /** + * Parse String (n) + * After parsing, the buffer will be sliced + * + * @param [index=0] Index in the packet + */ + public getString(index = 0): string | null { + if (this.isOutOfRange(index)) return null + const length = this.getVarInt(index) + if (length === null) return null + const offset = index + Packet.writeVarInt(length).byteLength - 1 + if (this.isOutOfRange(offset) || this.isOutOfRange(offset + length)) + return null + const result = this.packetBuffer + .subarray(offset, offset + length) + .toString() + this.packetData.splice(index, offset + length - index) + return result + } - /** - * Parse UUID - * After parsing, the buffer will be sliced - * - * @param [index=0] Index in the packet - */ - public getUUID(index = 0): string | null { - if (this.isOutOfRange(index)) return null; - const result = Packet.parseUUID(this.packetBuffer.subarray(index)); - this.packetData.splice(index, 16); - return result; - } + /** + * Parse Boolean + * After parsing, the buffer will be sliced + * + * @param [index=0] Index in the packet + */ + public getBoolean(index = 0): boolean | null { + if (this.isOutOfRange(index)) return null + const result = Packet.parseBoolean(this.packetBuffer.subarray(index)) + this.packetData.splice(index, 1) + return result + } - /** - * Parse Unsigned Short - * After parsing, the buffer will be sliced - * - * @param [index=0] Index in the packet - */ - public getUShort(index = 0): number | null { - if (this.isOutOfRange(index)) return null; - const result = Packet.parseUShort(this.packetBuffer.subarray(index)); - this.packetData.splice(index, 2); - return result; - } + /** + * Parse UUID + * After parsing, the buffer will be sliced + * + * @param [index=0] Index in the packet + */ + public getUUID(index = 0): string | null { + if (this.isOutOfRange(index)) return null + const result = Packet.parseUUID(this.packetBuffer.subarray(index)) + this.packetData.splice(index, 16) + return result + } - /** - * Parse Long - * After parsing, the buffer will be sliced - * - * @param [index=0] Index in the packet - */ - public getLong(index = 0): bigint | null { - if (this.isOutOfRange(index)) return null; - const result = Packet.parseLong(this.packetBuffer.subarray(index)); - this.packetData.splice(index, 8); - return result; - } -} \ No newline at end of file + /** + * Parse Unsigned Short + * After parsing, the buffer will be sliced + * + * @param [index=0] Index in the packet + */ + public getUShort(index = 0): number | null { + if (this.isOutOfRange(index)) return null + const result = Packet.parseUShort(this.packetBuffer.subarray(index)) + this.packetData.splice(index, 2) + return result + } + + /** + * Parse Long + * After parsing, the buffer will be sliced + * + * @param [index=0] Index in the packet + */ + public getLong(index = 0): bigint | null { + if (this.isOutOfRange(index)) return null + const result = Packet.parseLong(this.packetBuffer.subarray(index)) + this.packetData.splice(index, 8) + return result + } +} diff --git a/src/Scheduler.ts b/src/Scheduler.ts index 560bf66..6fcadd6 100644 --- a/src/Scheduler.ts +++ b/src/Scheduler.ts @@ -1,466 +1,495 @@ -import EventEmitter from "node:events"; -import {randomUUID} from "node:crypto"; -import TypedEventEmitter from "./types/TypedEventEmitter"; +import EventEmitter from "node:events" +import { randomUUID } from "node:crypto" +import TypedEventEmitter from "./types/TypedEventEmitter" type SchedulerEvents = { - /** - * Scheduler is paused - */ - paused: () => void; - - /** - * Scheduler is started/resumed - */ - started: () => void; - - /** - * Scheduler terminated - */ - terminating: () => void; + /** + * Scheduler is paused + */ + paused: () => void + + /** + * Scheduler is started/resumed + */ + started: () => void + + /** + * Scheduler terminated + */ + terminating: () => void } class Scheduler extends (EventEmitter as new () => TypedEventEmitter) { - /** - * Scheduler age (in ticks) - */ - #age: number = 0; - - /** - * Whether the scheduler is running - */ - #running: boolean = false; - - /** - * Scheduler tasks - */ - readonly #tasks: Scheduler.Task[] = []; - - #schedulerStopResolve: ((value: true | PromiseLike) => void) | null = null; - #schedulerStopPromise: Promise | null = null; - - /** - * Time of last tick - */ - private lastTick: Date = new Date(); - - /** - * Create scheduler - * - * @param frequency Scheduler clock frequency in Hz - * @param [start] Start scheduler - */ - public constructor(public readonly frequency: number, start?: boolean) { - super(); - if (start) this.start(); - } - - /** - * Start scheduler - */ - public start(): void { - this.#running = true; - this.#schedulerStopPromise = new Promise(r => this.#schedulerStopResolve = r); - this._nextTick(); - this.emit("started"); - } - - /** - * Stop scheduler. The scheduler can be re-started afterwards and any previously scheduled tasks will continue being executed. - * @returns Promise that resolves when the scheduler has paused. The promise resolves to false if the scheduler was not running. - */ - public pause(): Promise { - if (!this.#running) return Promise.resolve(false); - this.#running = false; - this.emit("paused"); - return this.#schedulerStopPromise!; - } - - /** - * Terminate scheduler. The scheduler is stopped and any previously scheduled tasks are marked as not planned and then deleted. - * @returns Promise that resolves when the scheduler has stopped. The promise resolves to false if the scheduler was not running. - */ - public stop(): Promise { - if (!this.#running) return Promise.resolve(false); - this.#running = false; - while (this.#tasks.length > 0) { - const task = this.#tasks.pop()!; - task.emit("notPlanned"); - this.delete(task); - task.removeAllListeners(); - } - this.emit("terminating"); - return this.#schedulerStopPromise!; - } - - /** - * Scheduler age - */ - public get age(): number { - return this.#age; - } - - /** - * Whether the scheduler is running - */ - public get running(): boolean { - return this.#running; - } - - /** - * Convert milliseconds to scheduler ticks - * - * @param ms Milliseconds - */ - public msToTicks(ms: number): number { - return ms / (1000 / this.frequency); - } - - /** - * Convert scheduler ticks to milliseconds - * - * @param ticks Ticks - */ - public ticksToMs(ticks: number): number { - return ticks * (1000 / this.frequency); - } - - /** - * Estimate scheduler age at a specific date - * - * > [!NOTE] - * > If the scheduler is paused, IRL time will pass without the scheduler aging, resulting in incorrect estimation. - * > This estimation will only be correct if the scheduler is not paused (or terminated) before the given date. - * - * @param date Date to estimate scheduler age at - */ - public estimateAge(date: Date): number { - return this.age + this.msToTicks(date.getTime() - this.lastTick.getTime()); - } - - /** - * Scheduler tick - */ - private tick(): void { - const now = new Date(); - if (now.getTime() - this.lastTick.getTime() < this.ticksToMs(1)) return this._nextTick(); - ++this.#age; - this.lastTick = now; - const tasks = this.#tasks.filter(task => task.targetAge <= this.#age).sort((a, b) => a.targetAge - b.targetAge); - for (const task of tasks) { - this.delete(task); - task.run(); - } - - this._nextTick(); - } - - /** - * Request next tick - */ - private _nextTick(): void { - if (!this.#running) { - if (this.#schedulerStopResolve) { - this.#schedulerStopResolve(true); - this.#schedulerStopResolve = null; - } - return; - } - setImmediate(this.tick.bind(this)); - } - - /** - * Schedule task to run at a specific scheduler age (tick) - * - * @param code Task code - * @param targetAge Target scheduler age (tick) to run task at - */ - public scheduleAge(code: () => void, targetAge: number): Scheduler.Task { - const task = new Scheduler.Task(code, targetAge, this); - this.#tasks.push(task); - return task; - } - - /** - * Schedule task to run after the specified amount of ticks - * - * @param code Task code - * @param ticks Number of ticks to wait before running the task - */ - public scheduleTicks(code: () => void, ticks: number): Scheduler.Task { - return this.scheduleAge(code, this.age + ticks); - } - - /** - * Schedule task to be executed as soon as possible - * - * @param code Task code - */ - public schedule(code: () => void): Scheduler.Task; - /** - * Schedule a task - * - * @param task The task - */ - public schedule(task: Scheduler.Task): Scheduler.Task; - public schedule(a: (() => void) | Scheduler.Task): Scheduler.Task { - if (a instanceof Scheduler.Task) { - this.#tasks.push(a); - return a; - } - else return this.scheduleTicks(a, 0); - } - - /** - * Delete task from the scheduler queue - * - * @param task Task to cancel - * @internal - */ - private delete(task: Scheduler.Task): boolean { - const index = this.#tasks.indexOf(task); - if (index < 0) return false; - this.#tasks.splice(index, 1); - return true; - } - - /** - * Cancel execution of a task - * - * @param task Task to cancel - * @returns `false` if the task was not found in the scheduler queue (possibly already executed), `true` otherwise - */ - public cancel(task: Scheduler.Task): boolean { - const deleted = this.delete(task); - if (deleted) task.emit("cancelled"); - return deleted; - } - - /** - * Get task from the scheduler queue by ID - * - * @param id Task ID - */ - public getTaskById(id: string): Scheduler.Task | undefined { - return this.#tasks.find(task => task.id === id); - } + /** + * Scheduler age (in ticks) + */ + #age: number = 0 + + /** + * Whether the scheduler is running + */ + #running: boolean = false + + /** + * Scheduler tasks + */ + readonly #tasks: Scheduler.Task[] = [] + + #schedulerStopResolve: ((value: true | PromiseLike) => void) | null = + null + #schedulerStopPromise: Promise | null = null + + /** + * Time of last tick + */ + private lastTick: Date = new Date() + + /** + * Create scheduler + * + * @param frequency Scheduler clock frequency in Hz + * @param [start] Start scheduler + */ + public constructor( + public readonly frequency: number, + start?: boolean + ) { + super() + if (start) this.start() + } + + /** + * Start scheduler + */ + public start(): void { + this.#running = true + this.#schedulerStopPromise = new Promise( + (r) => (this.#schedulerStopResolve = r) + ) + this._nextTick() + this.emit("started") + } + + /** + * Stop scheduler. The scheduler can be re-started afterwards and any previously scheduled tasks will continue being executed. + * @returns Promise that resolves when the scheduler has paused. The promise resolves to false if the scheduler was not running. + */ + public pause(): Promise { + if (!this.#running) return Promise.resolve(false) + this.#running = false + this.emit("paused") + return this.#schedulerStopPromise! + } + + /** + * Terminate scheduler. The scheduler is stopped and any previously scheduled tasks are marked as not planned and then deleted. + * @returns Promise that resolves when the scheduler has stopped. The promise resolves to false if the scheduler was not running. + */ + public stop(): Promise { + if (!this.#running) return Promise.resolve(false) + this.#running = false + while (this.#tasks.length > 0) { + const task = this.#tasks.pop()! + task.emit("notPlanned") + this.delete(task) + task.removeAllListeners() + } + this.emit("terminating") + return this.#schedulerStopPromise! + } + + /** + * Scheduler age + */ + public get age(): number { + return this.#age + } + + /** + * Whether the scheduler is running + */ + public get running(): boolean { + return this.#running + } + + /** + * Convert milliseconds to scheduler ticks + * + * @param ms Milliseconds + */ + public msToTicks(ms: number): number { + return ms / (1000 / this.frequency) + } + + /** + * Convert scheduler ticks to milliseconds + * + * @param ticks Ticks + */ + public ticksToMs(ticks: number): number { + return ticks * (1000 / this.frequency) + } + + /** + * Estimate scheduler age at a specific date + * + * > [!NOTE] + * > If the scheduler is paused, IRL time will pass without the scheduler aging, resulting in incorrect estimation. + * > This estimation will only be correct if the scheduler is not paused (or terminated) before the given date. + * + * @param date Date to estimate scheduler age at + */ + public estimateAge(date: Date): number { + return ( + this.age + this.msToTicks(date.getTime() - this.lastTick.getTime()) + ) + } + + /** + * Scheduler tick + */ + private tick(): void { + const now = new Date() + if (now.getTime() - this.lastTick.getTime() < this.ticksToMs(1)) + return this._nextTick() + ++this.#age + this.lastTick = now + const tasks = this.#tasks + .filter((task) => task.targetAge <= this.#age) + .sort((a, b) => a.targetAge - b.targetAge) + for (const task of tasks) { + this.delete(task) + task.run() + } + + this._nextTick() + } + + /** + * Request next tick + */ + private _nextTick(): void { + if (!this.#running) { + if (this.#schedulerStopResolve) { + this.#schedulerStopResolve(true) + this.#schedulerStopResolve = null + } + return + } + setImmediate(this.tick.bind(this)) + } + + /** + * Schedule task to run at a specific scheduler age (tick) + * + * @param code Task code + * @param targetAge Target scheduler age (tick) to run task at + */ + public scheduleAge(code: () => void, targetAge: number): Scheduler.Task { + const task = new Scheduler.Task(code, targetAge, this) + this.#tasks.push(task) + return task + } + + /** + * Schedule task to run after the specified amount of ticks + * + * @param code Task code + * @param ticks Number of ticks to wait before running the task + */ + public scheduleTicks(code: () => void, ticks: number): Scheduler.Task { + return this.scheduleAge(code, this.age + ticks) + } + + /** + * Schedule task to be executed as soon as possible + * + * @param code Task code + */ + public schedule(code: () => void): Scheduler.Task + /** + * Schedule a task + * + * @param task The task + */ + public schedule(task: Scheduler.Task): Scheduler.Task + public schedule(a: (() => void) | Scheduler.Task): Scheduler.Task { + if (a instanceof Scheduler.Task) { + this.#tasks.push(a) + return a + } else return this.scheduleTicks(a, 0) + } + + /** + * Delete task from the scheduler queue + * + * @param task Task to cancel + * @internal + */ + private delete(task: Scheduler.Task): boolean { + const index = this.#tasks.indexOf(task) + if (index < 0) return false + this.#tasks.splice(index, 1) + return true + } + + /** + * Cancel execution of a task + * + * @param task Task to cancel + * @returns `false` if the task was not found in the scheduler queue (possibly already executed), `true` otherwise + */ + public cancel(task: Scheduler.Task): boolean { + const deleted = this.delete(task) + if (deleted) task.emit("cancelled") + return deleted + } + + /** + * Get task from the scheduler queue by ID + * + * @param id Task ID + */ + public getTaskById(id: string): Scheduler.Task | undefined { + return this.#tasks.find((task) => task.id === id) + } } namespace Scheduler { - type TaskEvents = { - /** - * Task is not planned to be executed due to the scheduler being terminated - */ - "notPlanned": () => void; - - /** - * Task is cancelled - */ - "cancelled": () => void; - } - - /** - * Scheduler task - */ - export class Task extends (EventEmitter as new () => TypedEventEmitter) { - /** - * Task ID - */ - public readonly id = randomUUID(); - - /** - * Task code - */ - private readonly code: () => void; - - /** - * Target scheduler age (tick) to run task at - */ - public readonly targetAge: number; - - /** - * Task scheduler - */ - public readonly scheduler: Scheduler; - - /** - * Whether the task has been executed - */ - #executed: boolean = false; - - /** - * Create scheduler task - * - * @param code Task code - * @param targetAge Target scheduler age - * @param scheduler Scheduler - */ - public constructor(code: () => void, targetAge: number, scheduler: Scheduler) { - super(); - this.code = code; - this.targetAge = targetAge; - this.scheduler = scheduler; - } - - /** - * Whether the task has been executed - */ - public get executed(): boolean { - return this.#executed; - } - - /** - * The remaining ticks before the task is run. - * - * - `0`: the task is being run - * - positive int: the task will be run in this many ticks - * - negative int: the task was run this many ticks ago - * - * To check if the task was actually run, use {@link Task#executed} - */ - public get remainingTicks(): number { - return this.targetAge - this.scheduler.age; - } - - /** - * Cancel execution of this task - * @see Scheduler#cancel - */ - public cancel(): boolean { - return this.scheduler.cancel(this); - } - - /** - * Run task - * @internal - */ - public run(): void { - this.code(); - this.#executed = true; - this.removeAllListeners(); - } - } - - type RepeatingTaskEvents = { - /** - * All repeats have been executed - */ - "completed": () => void; - } - - /** - * A repeating task - */ - export class RepeatingTask extends (EventEmitter as new () => TypedEventEmitter) { - /** - * Number of times the task will repeat. This may be `Infinity`, in which case the tasks repeats until the scheduler is terminated. - */ - public readonly repeats: number; - - /** - * Interval between each repeat - */ - public readonly interval: number; - - /** - * Target scheduler age (tick) for first execution - */ - public readonly targetAge: number; - - /** - * Task scheduler - */ - public readonly scheduler: Scheduler; - - /** - * Task code - */ - private readonly code: () => void; - - /** - * Current task - */ - #task: Task | null = null; - - /** - * Number of tasks that have been executed - */ - #executed: number = 0; - - /** - * Whether this repeating task has been cancelled - */ - #cancelled: boolean = false; - - /** - * Create repeating task - * - * @param code Task code - * @param interval Interval between each repeat - * @param scheduler Scheduler - * @param [repeats] Number of times the task will repeat. This may be `Infinity`, in which case the tasks repeats until the scheduler is terminated. Default: `Infinity` - * @param [targetAge] Target scheduler age (tick) for first execution. Default: `0` (next tick) - */ - public constructor(code: () => void, interval: number, scheduler: Scheduler, repeats: number = Infinity, targetAge: number = 0) { - super(); - this.code = code; - this.interval = interval; - this.scheduler = scheduler; - this.repeats = repeats; - this.targetAge = targetAge; - if (this.repeats > 0) this.createTask(); - - this.scheduler.on("terminating", () => { - //console.log(this.task?.executed, this.task); - if (this.task?.executed) this.emit("notPlanned"); - }); - } - - /** - * Cancel this repeating task - */ - public cancel(): void { - if (this.#cancelled) return; - this.#cancelled = true; - if (this.#task) this.#task.cancel(); - else this.emit("cancelled"); - } - - /** - * Create task - */ - private createTask(): void { - if (this.executed === 0) this.#task = this.scheduler.scheduleAge(() => this.taskCode(), this.targetAge); - else this.#task = this.scheduler.scheduleAge(() => this.taskCode(), this.scheduler.age + this.interval); - this.#task.once("cancelled", () => this.emit("cancelled")); - this.#task.once("notPlanned", () => this.emit("notPlanned")); - } - - /** - * Scheduled task code - */ - private taskCode(): void { - this.code(); - ++this.#executed; - if (this.#executed < this.repeats) { - if (!this.#cancelled) this.createTask(); - } - else this.emit("completed"); - } - - /** - * Get current task - */ - public get task(): Task | null { - return this.#task; - } - - /** - * Number of times the task has been executed - */ - public get executed(): number { - return this.#executed; - } - } + type TaskEvents = { + /** + * Task is not planned to be executed due to the scheduler being terminated + */ + notPlanned: () => void + + /** + * Task is cancelled + */ + cancelled: () => void + } + + /** + * Scheduler task + */ + export class Task extends (EventEmitter as new () => TypedEventEmitter) { + /** + * Task ID + */ + public readonly id = randomUUID() + + /** + * Task code + */ + private readonly code: () => void + + /** + * Target scheduler age (tick) to run task at + */ + public readonly targetAge: number + + /** + * Task scheduler + */ + public readonly scheduler: Scheduler + + /** + * Whether the task has been executed + */ + #executed: boolean = false + + /** + * Create scheduler task + * + * @param code Task code + * @param targetAge Target scheduler age + * @param scheduler Scheduler + */ + public constructor( + code: () => void, + targetAge: number, + scheduler: Scheduler + ) { + super() + this.code = code + this.targetAge = targetAge + this.scheduler = scheduler + } + + /** + * Whether the task has been executed + */ + public get executed(): boolean { + return this.#executed + } + + /** + * The remaining ticks before the task is run. + * + * - `0`: the task is being run + * - positive int: the task will be run in this many ticks + * - negative int: the task was run this many ticks ago + * + * To check if the task was actually run, use {@link Task#executed} + */ + public get remainingTicks(): number { + return this.targetAge - this.scheduler.age + } + + /** + * Cancel execution of this task + * @see Scheduler#cancel + */ + public cancel(): boolean { + return this.scheduler.cancel(this) + } + + /** + * Run task + * @internal + */ + public run(): void { + this.code() + this.#executed = true + this.removeAllListeners() + } + } + + type RepeatingTaskEvents = { + /** + * All repeats have been executed + */ + completed: () => void + } + + /** + * A repeating task + */ + export class RepeatingTask extends (EventEmitter as new () => TypedEventEmitter< + TaskEvents & RepeatingTaskEvents + >) { + /** + * Number of times the task will repeat. This may be `Infinity`, in which case the tasks repeats until the scheduler is terminated. + */ + public readonly repeats: number + + /** + * Interval between each repeat + */ + public readonly interval: number + + /** + * Target scheduler age (tick) for first execution + */ + public readonly targetAge: number + + /** + * Task scheduler + */ + public readonly scheduler: Scheduler + + /** + * Task code + */ + private readonly code: () => void + + /** + * Current task + */ + #task: Task | null = null + + /** + * Number of tasks that have been executed + */ + #executed: number = 0 + + /** + * Whether this repeating task has been cancelled + */ + #cancelled: boolean = false + + /** + * Create repeating task + * + * @param code Task code + * @param interval Interval between each repeat + * @param scheduler Scheduler + * @param [repeats] Number of times the task will repeat. This may be `Infinity`, in which case the tasks repeats until the scheduler is terminated. Default: `Infinity` + * @param [targetAge] Target scheduler age (tick) for first execution. Default: `0` (next tick) + */ + public constructor( + code: () => void, + interval: number, + scheduler: Scheduler, + repeats: number = Infinity, + targetAge: number = 0 + ) { + super() + this.code = code + this.interval = interval + this.scheduler = scheduler + this.repeats = repeats + this.targetAge = targetAge + if (this.repeats > 0) this.createTask() + + this.scheduler.on("terminating", () => { + //console.log(this.task?.executed, this.task); + if (this.task?.executed) this.emit("notPlanned") + }) + } + + /** + * Cancel this repeating task + */ + public cancel(): void { + if (this.#cancelled) return + this.#cancelled = true + if (this.#task) this.#task.cancel() + else this.emit("cancelled") + } + + /** + * Create task + */ + private createTask(): void { + if (this.executed === 0) + this.#task = this.scheduler.scheduleAge( + () => this.taskCode(), + this.targetAge + ) + else + this.#task = this.scheduler.scheduleAge( + () => this.taskCode(), + this.scheduler.age + this.interval + ) + this.#task.once("cancelled", () => this.emit("cancelled")) + this.#task.once("notPlanned", () => this.emit("notPlanned")) + } + + /** + * Scheduled task code + */ + private taskCode(): void { + this.code() + ++this.#executed + if (this.#executed < this.repeats) { + if (!this.#cancelled) this.createTask() + } else this.emit("completed") + } + + /** + * Get current task + */ + public get task(): Task | null { + return this.#task + } + + /** + * Number of times the task has been executed + */ + public get executed(): number { + return this.#executed + } + } } -export default Scheduler; +export default Scheduler diff --git a/src/Server.ts b/src/Server.ts index a900bf8..c6d1304 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -1,154 +1,170 @@ -import * as net from "node:net"; -import EventEmitter from "node:events"; -import path from "node:path"; -import Packet from "./Packet.js"; -import Logger from "./Logger.js"; -import {TypedClientPacket} from "./types/TypedPacket"; -import TypedEventEmitter from "./types/TypedEventEmitter"; -import ConnectionPool from "./ConnectionPool.js"; -import Connection from "./Connection.js"; -import HandshakePacket from "./packet/client/HandshakePacket"; -import LoginPacket from "./packet/client/LoginPacket"; -import { Config } from "./Config.js"; -import Scheduler from "./Scheduler.js"; -import { readFile } from "node:fs/promises"; -import PingPacket from "./packet/client/PingPacket.js"; -import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; -import LoginAckPacket from "./packet/client/LoginAckPacket.js"; +import * as net from "node:net" +import EventEmitter from "node:events" +import path from "node:path" +import Packet from "./Packet.js" +import Logger from "./Logger.js" +import { TypedClientPacket } from "./types/TypedPacket" +import TypedEventEmitter from "./types/TypedEventEmitter" +import ConnectionPool from "./ConnectionPool.js" +import Connection from "./Connection.js" +import HandshakePacket from "./packet/client/HandshakePacket" +import LoginPacket from "./packet/client/LoginPacket" +import { Config } from "./Config.js" +import Scheduler from "./Scheduler.js" +import { readFile } from "node:fs/promises" +import PingPacket from "./packet/client/PingPacket.js" +import StatusRequestPacket from "./packet/client/StatusRequestPacket.js" +import LoginAckPacket from "./packet/client/LoginAckPacket.js" type ServerEvents = { - /** - * Server is ready to accept connections - * @param port Port the server is listening on - */ - listening: (port: Number) => void; - - /** - * Unknown packet received - * @param packet Packet that was received - * @param connection Connection the packet was received from - */ - unknownPacket: (packet: Packet, connection: Connection) => void; - - /** - * Known packet received - * @param packet Packet that was received - * @param connection Connection the packet was received from - */ - packet: (packet: TypedClientPacket, connection: Connection) => void; - - /** - * New connection established - * @param connection Connection that was established - */ - connection: (connection: Connection) => void; - - /** - * Server closed - */ - closed: () => void; - - /** - * Connection closed - * @param connection Connection that was closed - */ - disconnect: (connection: Connection) => void; - - /** - * Handshake packet received - * @param packet Packet that was received - * @param connection Connection the packet was received from - */ - "packet.HandshakePacket": (packet: HandshakePacket, connection: Connection) => void; - - /** - * Login packet received - * @param packet Packet that was received - * @param connection Connection the packet was received from - */ - "packet.LoginPacket": (packet: LoginPacket, connection: Connection) => void; - - /** - * Status request packet received - * @param packet Packet that was received - * @param connection Connection the packet was received from - */ - "packet.StatusRequestPacket": (packet: StatusRequestPacket, connection: Connection) => void; - - /** - * Ping packet received - * @param packet Packet that was received - * @param connection Connection the packet was received from - */ - "packet.PingPacket": (packet: PingPacket, connection: Connection) => void; - - /** - * Login acknowledge packet - * @param packet Packet that was received - * @param connection Connection the packet was received from - */ - "packet.LoginAck": (packet: LoginAckPacket, connection: Connection) => void; -}; + /** + * Server is ready to accept connections + * @param port Port the server is listening on + */ + listening: (port: Number) => void + + /** + * Unknown packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + unknownPacket: (packet: Packet, connection: Connection) => void + + /** + * Known packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + packet: (packet: TypedClientPacket, connection: Connection) => void + + /** + * New connection established + * @param connection Connection that was established + */ + connection: (connection: Connection) => void + + /** + * Server closed + */ + closed: () => void + + /** + * Connection closed + * @param connection Connection that was closed + */ + disconnect: (connection: Connection) => void + + /** + * Handshake packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.HandshakePacket": ( + packet: HandshakePacket, + connection: Connection + ) => void + + /** + * Login packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.LoginPacket": (packet: LoginPacket, connection: Connection) => void + + /** + * Status request packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.StatusRequestPacket": ( + packet: StatusRequestPacket, + connection: Connection + ) => void + + /** + * Ping packet received + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.PingPacket": (packet: PingPacket, connection: Connection) => void + + /** + * Login acknowledge packet + * @param packet Packet that was received + * @param connection Connection the packet was received from + */ + "packet.LoginAck": (packet: LoginAckPacket, connection: Connection) => void +} export default class Server extends (EventEmitter as new () => TypedEventEmitter) { - private readonly server = net.createServer(); - public readonly logger: Logger; - public readonly scheduler: Scheduler = new Scheduler(20); - public readonly connections: ConnectionPool = new ConnectionPool(); - - public static readonly path: string = path.dirname(path.join(new URL(import.meta.url).pathname, "..")); - public readonly config: Config; - - public favicon: string = "data:image/png;base64,"; - - public constructor(config: Config) { - super(); - this.config = Object.freeze(config); - this.logger = new Logger("Server", this.config.logLevel); - } - - public async start() { - - // add a favicon if such is specified - if (this.config.server.favicon) { - const data = await readFile(this.config.server.favicon); - this.favicon += Buffer.from(data).toString("base64"); - } - - this.scheduler.on("started", () => this.logger.debug("Scheduler started, freq=" + this.scheduler.frequency + "Hz")); - this.scheduler.on("paused", () => this.logger.debug("Scheduler paused, age=" + this.scheduler.age)); - this.scheduler.on("terminating", () => this.logger.debug("Scheduler terminated, age=" + this.scheduler.age)); - this.scheduler.start(); - this.server.listen(this.config.port, () => this.emit("listening", this.config.port)); - this.server.on("connection", this.onConnection.bind(this)); - } - - public async stop(): Promise { - this.logger.debug("Closing server..."); - await Promise.all([ - new Promise((resolve, reject) => { - this.server.close((err) => { - if (err) reject(err); - else resolve(void 0); - }); - }), - this.connections.disconnectAll(this.config.shutdownKickReason), - ]); - await this.scheduler.stop(); - this.emit("closed"); - } - - public get isRunning(): boolean { - return this.server.listening; - } - - private onConnection(socket: net.Socket) { - const conn = new Connection(socket, this); - this.connections.add(conn); - this.emit("connection", conn); - socket.on("data", (data) => { - for (const byte of data) - conn.incomingPacketFragment(byte); - }); - } + private readonly server = net.createServer() + public readonly logger: Logger + public readonly scheduler: Scheduler = new Scheduler(20) + public readonly connections: ConnectionPool = new ConnectionPool() + + public static readonly path: string = path.dirname( + path.join(new URL(import.meta.url).pathname, "..") + ) + public readonly config: Config + + public favicon: string = "data:image/png;base64," + + public constructor(config: Config) { + super() + this.config = Object.freeze(config) + this.logger = new Logger("Server", this.config.logLevel) + } + + public async start() { + // add a favicon if such is specified + if (this.config.server.favicon) { + const data = await readFile(this.config.server.favicon) + this.favicon += Buffer.from(data).toString("base64") + } + + this.scheduler.on("started", () => + this.logger.debug( + "Scheduler started, freq=" + this.scheduler.frequency + "Hz" + ) + ) + this.scheduler.on("paused", () => + this.logger.debug("Scheduler paused, age=" + this.scheduler.age) + ) + this.scheduler.on("terminating", () => + this.logger.debug("Scheduler terminated, age=" + this.scheduler.age) + ) + this.scheduler.start() + this.server.listen(this.config.port, () => + this.emit("listening", this.config.port) + ) + this.server.on("connection", this.onConnection.bind(this)) + } + + public async stop(): Promise { + this.logger.debug("Closing server...") + await Promise.all([ + new Promise((resolve, reject) => { + this.server.close((err) => { + if (err) reject(err) + else resolve(void 0) + }) + }), + this.connections.disconnectAll(this.config.shutdownKickReason), + ]) + await this.scheduler.stop() + this.emit("closed") + } + + public get isRunning(): boolean { + return this.server.listening + } + + private onConnection(socket: net.Socket) { + const conn = new Connection(socket, this) + this.connections.add(conn) + this.emit("connection", conn) + socket.on("data", (data) => { + for (const byte of data) conn.incomingPacketFragment(byte) + }) + } } diff --git a/src/ServerPacket.ts b/src/ServerPacket.ts index 4226d7f..1dc23bd 100644 --- a/src/ServerPacket.ts +++ b/src/ServerPacket.ts @@ -1,22 +1,21 @@ -import Packet from "./Packet.js"; -import Connection from "./Connection"; +import Packet from "./Packet.js" +import Connection from "./Connection" export default abstract class ServerPacket extends Packet { + protected constructor(data: Buffer) { + super([...Buffer.concat([Packet.writeVarInt(data.byteLength), data])]) + } - protected constructor(data: Buffer) { - super([...Buffer.concat([Packet.writeVarInt(data.byteLength), data])]); - } - - /** - * Send packet to a connection - * @param connection - */ - public send(connection: Connection): Promise { - return new Promise((resolve, reject) => { - connection.socket.write(this.dataBuffer, (err) => { - if (err) reject(err); - else resolve(); - }); - }); - } + /** + * Send packet to a connection + * @param connection + */ + public send(connection: Connection): Promise { + return new Promise((resolve, reject) => { + connection.socket.write(this.dataBuffer, (err) => { + if (err) reject(err) + else resolve() + }) + }) + } } diff --git a/src/decorator/StaticImplements.ts b/src/decorator/StaticImplements.ts index 609f454..200b539 100644 --- a/src/decorator/StaticImplements.ts +++ b/src/decorator/StaticImplements.ts @@ -1,3 +1,3 @@ export default function StaticImplements() { - return (constructor: U) => constructor; + return (constructor: U) => constructor } diff --git a/src/nbt.ts b/src/nbt.ts index cd4eb73..c7a1da9 100644 --- a/src/nbt.ts +++ b/src/nbt.ts @@ -11,52 +11,51 @@ required by law. */ -'use strict'; +"use strict" -if (typeof ArrayBuffer === 'undefined') { - throw new Error('Missing required type ArrayBuffer'); +if (typeof ArrayBuffer === "undefined") { + throw new Error("Missing required type ArrayBuffer") } -if (typeof DataView === 'undefined') { - throw new Error('Missing required type DataView'); +if (typeof DataView === "undefined") { + throw new Error("Missing required type DataView") } -if (typeof Uint8Array === 'undefined') { - throw new Error('Missing required type Uint8Array'); +if (typeof Uint8Array === "undefined") { + throw new Error("Missing required type Uint8Array") } const nbt: { tagTypes: NbtTagTypes } = { - tagTypes: { - end: 0, - byte: 1, - short: 2, - int: 3, - long: 4, - float: 5, - double: 6, - byteArray: 7, - string: 8, - list: 9, - compound: 10, - intArray: 11, - longArray: 12 - } -}; + tagTypes: { + end: 0, + byte: 1, + short: 2, + int: 3, + long: 4, + float: 5, + double: 6, + byteArray: 7, + string: 8, + list: 9, + compound: 10, + intArray: 11, + longArray: 12, + }, +} export const tagTypes = { - 'end': 0, - 'byte': 1, - 'short': 2, - 'int': 3, - 'long': 4, - 'float': 5, - 'double': 6, - 'byteArray': 7, - 'string': 8, - 'list': 9, - 'compound': 10, - 'intArray': 11, - 'longArray': 12 -}; - + end: 0, + byte: 1, + short: 2, + int: 3, + long: 4, + float: 5, + double: 6, + byteArray: 7, + string: 8, + list: 9, + compound: 10, + intArray: 11, + longArray: 12, +} /** * A mapping from type names to NBT type numbers. @@ -66,93 +65,100 @@ export const tagTypes = { * * @type Object * @see module:nbt.tagTypeNames */ -nbt.tagTypes = tagTypes; +nbt.tagTypes = tagTypes /** * A mapping from NBT type numbers to type names. * * @type Object * @see module:nbt.tagTypes */ -nbt.tagTypeNames = {}; -(function() { - for (let typeName in nbt.tagTypes) { - if (nbt.tagTypes.hasOwnProperty(typeName)) { - nbt.tagTypeNames[nbt.tagTypes[typeName]] = typeName; - } - } -})(); +nbt.tagTypeNames = {} +;(function () { + for (let typeName in nbt.tagTypes) { + if (nbt.tagTypes.hasOwnProperty(typeName)) { + nbt.tagTypeNames[nbt.tagTypes[typeName]] = typeName + } + } +})() function hasGzipHeader(data) { - const head = new Uint8Array(data.slice(0, 2)); - return head.length === 2 && head[0] === 0x1f && head[1] === 0x8b; + const head = new Uint8Array(data.slice(0, 2)) + return head.length === 2 && head[0] === 0x1f && head[1] === 0x8b } function encodeUTF8(str) { - const array = []; - let i, c; - for (i = 0; i < str.length; i++) { - c = str.charCodeAt(i); - if (c < 0x80) { - array.push(c); - } else if (c < 0x800) { - array.push(0xC0 | c >> 6); - array.push(0x80 | c & 0x3F); - } else if (c < 0x10000) { - array.push(0xE0 | c >> 12); - array.push(0x80 | (c >> 6) & 0x3F); - array.push(0x80 | c & 0x3F); - } else { - array.push(0xF0 | (c >> 18) & 0x07); - array.push(0x80 | (c >> 12) & 0x3F); - array.push(0x80 | (c >> 6) & 0x3F); - array.push(0x80 | c & 0x3F); - } - } - return array; + const array = [] + let i, c + for (i = 0; i < str.length; i++) { + c = str.charCodeAt(i) + if (c < 0x80) { + array.push(c) + } else if (c < 0x800) { + array.push(0xc0 | (c >> 6)) + array.push(0x80 | (c & 0x3f)) + } else if (c < 0x10000) { + array.push(0xe0 | (c >> 12)) + array.push(0x80 | ((c >> 6) & 0x3f)) + array.push(0x80 | (c & 0x3f)) + } else { + array.push(0xf0 | ((c >> 18) & 0x07)) + array.push(0x80 | ((c >> 12) & 0x3f)) + array.push(0x80 | ((c >> 6) & 0x3f)) + array.push(0x80 | (c & 0x3f)) + } + } + return array } function decodeUTF8(array) { - let codepoints = [], i; - for (i = 0; i < array.length; i++) { - if ((array[i] & 0x80) === 0) { - codepoints.push(array[i] & 0x7F); - } else if (i+1 < array.length && - (array[i] & 0xE0) === 0xC0 && - (array[i+1] & 0xC0) === 0x80) { - codepoints.push( - ((array[i] & 0x1F) << 6) | - ( array[i+1] & 0x3F)); - } else if (i+2 < array.length && - (array[i] & 0xF0) === 0xE0 && - (array[i+1] & 0xC0) === 0x80 && - (array[i+2] & 0xC0) === 0x80) { - codepoints.push( - ((array[i] & 0x0F) << 12) | - ((array[i+1] & 0x3F) << 6) | - ( array[i+2] & 0x3F)); - } else if (i+3 < array.length && - (array[i] & 0xF8) === 0xF0 && - (array[i+1] & 0xC0) === 0x80 && - (array[i+2] & 0xC0) === 0x80 && - (array[i+3] & 0xC0) === 0x80) { - codepoints.push( - ((array[i] & 0x07) << 18) | - ((array[i+1] & 0x3F) << 12) | - ((array[i+2] & 0x3F) << 6) | - ( array[i+3] & 0x3F)); - } - } - return String.fromCharCode.apply(null, codepoints); + let codepoints = [], + i + for (i = 0; i < array.length; i++) { + if ((array[i] & 0x80) === 0) { + codepoints.push(array[i] & 0x7f) + } else if ( + i + 1 < array.length && + (array[i] & 0xe0) === 0xc0 && + (array[i + 1] & 0xc0) === 0x80 + ) { + codepoints.push(((array[i] & 0x1f) << 6) | (array[i + 1] & 0x3f)) + } else if ( + i + 2 < array.length && + (array[i] & 0xf0) === 0xe0 && + (array[i + 1] & 0xc0) === 0x80 && + (array[i + 2] & 0xc0) === 0x80 + ) { + codepoints.push( + ((array[i] & 0x0f) << 12) | + ((array[i + 1] & 0x3f) << 6) | + (array[i + 2] & 0x3f) + ) + } else if ( + i + 3 < array.length && + (array[i] & 0xf8) === 0xf0 && + (array[i + 1] & 0xc0) === 0x80 && + (array[i + 2] & 0xc0) === 0x80 && + (array[i + 3] & 0xc0) === 0x80 + ) { + codepoints.push( + ((array[i] & 0x07) << 18) | + ((array[i + 1] & 0x3f) << 12) | + ((array[i + 2] & 0x3f) << 6) | + (array[i + 3] & 0x3f) + ) + } + } + return String.fromCharCode.apply(null, codepoints) } /* Not all environments, in particular PhantomJS, supply Uint8Array.slice() */ function sliceUint8Array(array, begin, end) { - if ('slice' in array) { - return array.slice(begin, end); - } else { - return new Uint8Array([].slice.call(array, begin, end)); - } + if ("slice" in array) { + return array.slice(begin, end) + } else { + return new Uint8Array([].slice.call(array, begin, end)) + } } /** @@ -176,577 +182,587 @@ function sliceUint8Array(array, begin, end) { * writer.int(999); * * return writer.buffer; */ -nbt.Writer = function() { - const self = this; - - /* Will be resized (x2) on write if necessary. */ - let buffer = new ArrayBuffer(1024); - - /* These are recreated when the buffer is */ - let dataView = new DataView(buffer); - let arrayView = new Uint8Array(buffer); - - /** - * The location in the buffer where bytes are written or read. - * This increases after every write, but can be freely changed. - * The buffer will be resized when necessary. - * - * @type number */ - this.offset = 0; - - // Ensures that the buffer is large enough to write `size` bytes - // at the current `self.offset`. - function accommodate(size) { - const requiredLength = self.offset + size; - if (buffer.byteLength >= requiredLength) { - return; - } - - let newLength = buffer.byteLength; - while (newLength < requiredLength) { - newLength *= 2; - } - - const newBuffer = new ArrayBuffer(newLength); - const newArrayView = new Uint8Array(newBuffer); - newArrayView.set(arrayView); - - // If there's a gap between the end of the old buffer - // and the start of the new one, we need to zero it out - if (self.offset > buffer.byteLength) { - newArrayView.fill(0, buffer.byteLength, self.offset); - } - - buffer = newBuffer; - dataView = new DataView(newBuffer); - arrayView = newArrayView; - } - - function write(dataType, size, value) { - accommodate(size); - dataView['set' + dataType](self.offset, value); - self.offset += size; - return self; - } - - /** - * Returns the written data as a slice from the internal buffer, - * cutting off any padding at the end. - * - * @returns {ArrayBuffer} a [0, offset] slice of the internal buffer */ - this.getData = function() { - accommodate(0); /* make sure the offset is inside the buffer */ - return buffer.slice(0, self.offset); - }; - - /** - * @method module:nbt.Writer#byte - * @param {number} value - a signed byte - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.byte] = write.bind - -(null, 'Int8', 1); - - /** - * @method module:nbt.Writer#short - * @param {number} value - a signed 16-bit integer - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.short] = write.bind(null, 'Int16', 2); - - /** - * @method module:nbt.Writer#int - * @param {number} value - a signed 32-bit integer - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.int] = write.bind(null, 'Int32', 4); - - /** - * @method module:nbt.Writer#long - * @param {number} value - a signed 64-bit integer - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.long] = function(value) { - // Ensure value is a 64-bit BigInt - if (typeof value !== 'bigint') { - throw new Error('Value must be a BigInt'); - } - const hi = Number(value >> 32n) & 0xffffffff; - const lo = Number(value & 0xffffffffn); - self[nbt.tagTypes.int](hi); - self[nbt.tagTypes.int](lo); - return self; - }; - - /** - * @method module:nbt.Writer#float - * @param {number} value - a 32-bit IEEE 754 floating point number - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.float] = write.bind(null, 'Float32', 4); - - /** - * @method module:nbt.Writer#double - * @param {number} value - a 64-bit IEEE 754 floating point number - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.double] = write.bind(null, 'Float64', 8); - - /** - * @method module:nbt.Writer#byteArray - * @param {Uint8Array} value - an array of signed bytes - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.byteArray] = function(value) { - self[nbt.tagTypes.int](value.length); - accommodate(value.length); - arrayView.set(value, self.offset); - self.offset += value.length; - return self; - }; - - /** - * @method module:nbt.Writer#string - * @param {string} value - an unprefixed UTF-8 string - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.string] = function(value) { - const encoded = encodeUTF8(value); - self[nbt.tagTypes.short](encoded.length); - accommodate(encoded.length); - arrayView.set(encoded, self.offset); - self.offset += encoded.length; - return self; - }; - - /** - * @method module:nbt.Writer#list - * @param {number} type - an NBT type number - * @param {Array} value - an array of values of the given type - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.list] = function(type, value) { - self[nbt.tagTypes.byte](type); - self[nbt.tagTypes.int](value.length); - value.forEach(function(element) { - self[type](element); - }); - return self; - }; - - /** - * @method module:nbt.Writer#compound - * @param {Object} value - an object of key-value pairs - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.compound] = function(value) { - Object.keys(value).forEach(function(key) { - const elementType = value[key].type; - self[nbt.tagTypes.byte](elementType); - self[nbt.tagTypes.string](key); - self[elementType](value[key].value); - }); - self[nbt.tagTypes.byte](nbt.tagTypes.end); - return self; - }; - - /** - * @method module:nbt.Writer#intArray - * @param {Int32Array} value - an array of signed 32-bit integers - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.intArray] = function(value) { - self[nbt.tagTypes.int](value.length); - for (let i = 0; i < value.length; i++) { - self[nbt.tagTypes.int](value[i]); - } - return self; - }; - - /** - * @method module:nbt.Writer#longArray - * @param {BigInt64Array} value - an array of signed 64-bit integers - * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.longArray] = function(value) { - self[nbt.tagTypes.int](value.length); - for (let i = 0; i < value.length; i++) { - self[nbt.tagTypes.long](value[i]); - } - return self; - }; - - // Alias the methods by NBT type number - for (const type in nbt.tagTypes) { - if (nbt.tagTypes.hasOwnProperty(type) && typeof this[nbt.tagTypes[type]] === 'function') { - this[type] = this[nbt.tagTypes[type]]; - } - } -}; +nbt.Writer = function () { + const self = this + + /* Will be resized (x2) on write if necessary. */ + let buffer = new ArrayBuffer(1024) + + /* These are recreated when the buffer is */ + let dataView = new DataView(buffer) + let arrayView = new Uint8Array(buffer) + + /** + * The location in the buffer where bytes are written or read. + * This increases after every write, but can be freely changed. + * The buffer will be resized when necessary. + * + * @type number */ + this.offset = 0 + + // Ensures that the buffer is large enough to write `size` bytes + // at the current `self.offset`. + function accommodate(size) { + const requiredLength = self.offset + size + if (buffer.byteLength >= requiredLength) { + return + } + + let newLength = buffer.byteLength + while (newLength < requiredLength) { + newLength *= 2 + } + + const newBuffer = new ArrayBuffer(newLength) + const newArrayView = new Uint8Array(newBuffer) + newArrayView.set(arrayView) + + // If there's a gap between the end of the old buffer + // and the start of the new one, we need to zero it out + if (self.offset > buffer.byteLength) { + newArrayView.fill(0, buffer.byteLength, self.offset) + } + + buffer = newBuffer + dataView = new DataView(newBuffer) + arrayView = newArrayView + } + + function write(dataType, size, value) { + accommodate(size) + dataView["set" + dataType](self.offset, value) + self.offset += size + return self + } + + /** + * Returns the written data as a slice from the internal buffer, + * cutting off any padding at the end. + * + * @returns {ArrayBuffer} a [0, offset] slice of the internal buffer */ + this.getData = function () { + accommodate(0) /* make sure the offset is inside the buffer */ + return buffer.slice(0, self.offset) + } + + /** + * @method module:nbt.Writer#byte + * @param {number} value - a signed byte + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.byte] = write.bind(null, "Int8", 1) + + /** + * @method module:nbt.Writer#short + * @param {number} value - a signed 16-bit integer + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.short] = write.bind(null, "Int16", 2) + + /** + * @method module:nbt.Writer#int + * @param {number} value - a signed 32-bit integer + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.int] = write.bind(null, "Int32", 4) + + /** + * @method module:nbt.Writer#long + * @param {number} value - a signed 64-bit integer + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.long] = function (value) { + // Ensure value is a 64-bit BigInt + if (typeof value !== "bigint") { + throw new Error("Value must be a BigInt") + } + const hi = Number(value >> 32n) & 0xffffffff + const lo = Number(value & 0xffffffffn) + self[nbt.tagTypes.int](hi) + self[nbt.tagTypes.int](lo) + return self + } + + /** + * @method module:nbt.Writer#float + * @param {number} value - a 32-bit IEEE 754 floating point number + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.float] = write.bind(null, "Float32", 4) + + /** + * @method module:nbt.Writer#double + * @param {number} value - a 64-bit IEEE 754 floating point number + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.double] = write.bind(null, "Float64", 8) + + /** + * @method module:nbt.Writer#byteArray + * @param {Uint8Array} value - an array of signed bytes + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.byteArray] = function (value) { + self[nbt.tagTypes.int](value.length) + accommodate(value.length) + arrayView.set(value, self.offset) + self.offset += value.length + return self + } + + /** + * @method module:nbt.Writer#string + * @param {string} value - an unprefixed UTF-8 string + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.string] = function (value) { + const encoded = encodeUTF8(value) + self[nbt.tagTypes.short](encoded.length) + accommodate(encoded.length) + arrayView.set(encoded, self.offset) + self.offset += encoded.length + return self + } + + /** + * @method module:nbt.Writer#list + * @param {number} type - an NBT type number + * @param {Array} value - an array of values of the given type + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.list] = function (type, value) { + self[nbt.tagTypes.byte](type) + self[nbt.tagTypes.int](value.length) + value.forEach(function (element) { + self[type](element) + }) + return self + } + + /** + * @method module:nbt.Writer#compound + * @param {Object} value - an object of key-value pairs + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.compound] = function (value) { + Object.keys(value).forEach(function (key) { + const elementType = value[key].type + self[nbt.tagTypes.byte](elementType) + self[nbt.tagTypes.string](key) + self[elementType](value[key].value) + }) + self[nbt.tagTypes.byte](nbt.tagTypes.end) + return self + } + + /** + * @method module:nbt.Writer#intArray + * @param {Int32Array} value - an array of signed 32-bit integers + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.intArray] = function (value) { + self[nbt.tagTypes.int](value.length) + for (let i = 0; i < value.length; i++) { + self[nbt.tagTypes.int](value[i]) + } + return self + } + + /** + * @method module:nbt.Writer#longArray + * @param {BigInt64Array} value - an array of signed 64-bit integers + * @returns {module:nbt.Writer} itself */ + this[nbt.tagTypes.longArray] = function (value) { + self[nbt.tagTypes.int](value.length) + for (let i = 0; i < value.length; i++) { + self[nbt.tagTypes.long](value[i]) + } + return self + } + + // Alias the methods by NBT type number + for (const type in nbt.tagTypes) { + if ( + nbt.tagTypes.hasOwnProperty(type) && + typeof this[nbt.tagTypes[type]] === "function" + ) { + this[type] = this[nbt.tagTypes[type]] + } + } +} /** * @param {ArrayBuffer} data - the NBT data to read * @constructor * @see module:nbt.Writer */ -nbt.Reader = function(data) { - const self = this; - - let buffer = data; - - /* These are recreated when the buffer is */ - let dataView = new DataView(buffer); - let arrayView = new Uint8Array(buffer); - - /** - * The location in the buffer where bytes are written or read. - * This increases after every read, but can be freely changed. - * The buffer will be resized when necessary. - * - * @type number */ - this.offset = 0; - - function read(dataType, size) { - const value = dataView['get' + dataType](self.offset); - self.offset += size; - return value; - } - - /** - * @method module:nbt.Reader#byte - * @returns {number} a signed byte */ - this[nbt.tagTypes.byte] = read.bind(null, 'Int8', 1); - - /** - * @method module:nbt.Reader#short - * @returns {number} a signed 16-bit integer */ - this[nbt.tagTypes.short] = read.bind(null, 'Int16', 2); - - /** - * @method module:nbt.Reader#int - * @returns {number} a signed 32-bit integer */ - this[nbt.tagTypes.int] = read.bind(null, 'Int32', 4); - - /** - * @method module:nbt.Reader#long - * @returns {bigint} a signed 64-bit integer */ - this[nbt.tagTypes.long] = function() { - const hi = self[nbt.tagTypes.int](); - const lo = self[nbt.tagTypes.int](); - return BigInt(hi) << 32n | BigInt(lo); - }; - - /** - * @method module:nbt.Reader#float - * @returns {number} a 32-bit IEEE 754 floating point number */ - this[nbt.tagTypes.float] = read.bind(null, 'Float32', 4); - - /** - * @method module:nbt.Reader#double - * @returns {number} a 64-bit IEEE 754 floating point number */ - this[nbt.tagTypes.double] = read.bind(null, 'Float64', 8); - - /** - * @method module:nbt.Reader#byteArray - * @returns {Uint8Array} an array of signed bytes */ - this[nbt.tagTypes.byteArray] = function() { - const length = self[nbt.tagTypes.int](); - const value = sliceUint8Array(arrayView, self.offset, self.offset + length); - self.offset += length; - return value; - }; - - /** - * @method module:nbt.Reader#string - * @returns {string} an unprefixed UTF-8 string */ - this[nbt.tagTypes.string] = function() { - const length = self[nbt.tagTypes.short](); - const value = sliceUint8Array(arrayView, self.offset, self.offset + length); - self.offset += length; - return decodeUTF8(value); - }; - - /** - * @method module:nbt.Reader#list - * @returns {Array} an array of values of the given type */ - this[nbt.tagTypes.list] = function() { - const type = self[nbt.tagTypes.byte](); - const length = self[nbt.tagTypes.int](); - const value = []; - for (let i = 0; i < length; i++) { - value.push(self[type]()); - } - return value; - }; - - /** - * @method module:nbt.Reader#compound - * @returns {Object} an object of key-value pairs */ - this[nbt.tagTypes.compound] = function() { - const value = {}; - let type; - while ((type = self[nbt.tagTypes.byte]()) !== nbt.tagTypes.end) - - { - const key = self[nbt.tagTypes.string](); - value[key] = { type, value: self[type]() }; - } - return value; - }; - - /** - * @method module:nbt.Reader#intArray - * @returns {Int32Array} an array of signed 32-bit integers */ - this[nbt.tagTypes.intArray] = function() { - const length = self[nbt.tagTypes.int](); - const value = new Int32Array(length); - for (let i = 0; i < length; i++) { - value[i] = self[nbt.tagTypes.int](); - } - return value; - }; - - /** - * @method module:nbt.Reader#longArray - * @returns {BigInt64Array} an array of signed 64-bit integers */ - this[nbt.tagTypes.longArray] = function() { - const length = self[nbt.tagTypes.int](); - const value = new BigInt64Array(length); - for (let i = 0; i < length; i++) { - value[i] = self[nbt.tagTypes.long](); - } - return value; - }; - - // Alias the methods by NBT type number - for (const type in nbt.tagTypes) { - if (nbt.tagTypes.hasOwnProperty(type) && typeof this[nbt.tagTypes[type]] === 'function') { - this[type] = this[nbt.tagTypes[type]]; - } - } -}; - -export default nbt; +nbt.Reader = function (data) { + const self = this + + let buffer = data + + /* These are recreated when the buffer is */ + let dataView = new DataView(buffer) + let arrayView = new Uint8Array(buffer) + + /** + * The location in the buffer where bytes are written or read. + * This increases after every read, but can be freely changed. + * The buffer will be resized when necessary. + * + * @type number */ + this.offset = 0 + + function read(dataType, size) { + const value = dataView["get" + dataType](self.offset) + self.offset += size + return value + } + + /** + * @method module:nbt.Reader#byte + * @returns {number} a signed byte */ + this[nbt.tagTypes.byte] = read.bind(null, "Int8", 1) + + /** + * @method module:nbt.Reader#short + * @returns {number} a signed 16-bit integer */ + this[nbt.tagTypes.short] = read.bind(null, "Int16", 2) + + /** + * @method module:nbt.Reader#int + * @returns {number} a signed 32-bit integer */ + this[nbt.tagTypes.int] = read.bind(null, "Int32", 4) + + /** + * @method module:nbt.Reader#long + * @returns {bigint} a signed 64-bit integer */ + this[nbt.tagTypes.long] = function () { + const hi = self[nbt.tagTypes.int]() + const lo = self[nbt.tagTypes.int]() + return (BigInt(hi) << 32n) | BigInt(lo) + } + + /** + * @method module:nbt.Reader#float + * @returns {number} a 32-bit IEEE 754 floating point number */ + this[nbt.tagTypes.float] = read.bind(null, "Float32", 4) + + /** + * @method module:nbt.Reader#double + * @returns {number} a 64-bit IEEE 754 floating point number */ + this[nbt.tagTypes.double] = read.bind(null, "Float64", 8) + + /** + * @method module:nbt.Reader#byteArray + * @returns {Uint8Array} an array of signed bytes */ + this[nbt.tagTypes.byteArray] = function () { + const length = self[nbt.tagTypes.int]() + const value = sliceUint8Array( + arrayView, + self.offset, + self.offset + length + ) + self.offset += length + return value + } + + /** + * @method module:nbt.Reader#string + * @returns {string} an unprefixed UTF-8 string */ + this[nbt.tagTypes.string] = function () { + const length = self[nbt.tagTypes.short]() + const value = sliceUint8Array( + arrayView, + self.offset, + self.offset + length + ) + self.offset += length + return decodeUTF8(value) + } + + /** + * @method module:nbt.Reader#list + * @returns {Array} an array of values of the given type */ + this[nbt.tagTypes.list] = function () { + const type = self[nbt.tagTypes.byte]() + const length = self[nbt.tagTypes.int]() + const value = [] + for (let i = 0; i < length; i++) { + value.push(self[type]()) + } + return value + } + + /** + * @method module:nbt.Reader#compound + * @returns {Object} an object of key-value pairs */ + this[nbt.tagTypes.compound] = function () { + const value = {} + let type + while ((type = self[nbt.tagTypes.byte]()) !== nbt.tagTypes.end) { + const key = self[nbt.tagTypes.string]() + value[key] = { type, value: self[type]() } + } + return value + } + + /** + * @method module:nbt.Reader#intArray + * @returns {Int32Array} an array of signed 32-bit integers */ + this[nbt.tagTypes.intArray] = function () { + const length = self[nbt.tagTypes.int]() + const value = new Int32Array(length) + for (let i = 0; i < length; i++) { + value[i] = self[nbt.tagTypes.int]() + } + return value + } + + /** + * @method module:nbt.Reader#longArray + * @returns {BigInt64Array} an array of signed 64-bit integers */ + this[nbt.tagTypes.longArray] = function () { + const length = self[nbt.tagTypes.int]() + const value = new BigInt64Array(length) + for (let i = 0; i < length; i++) { + value[i] = self[nbt.tagTypes.long]() + } + return value + } + + // Alias the methods by NBT type number + for (const type in nbt.tagTypes) { + if ( + nbt.tagTypes.hasOwnProperty(type) && + typeof this[nbt.tagTypes[type]] === "function" + ) { + this[type] = this[nbt.tagTypes[type]] + } + } +} + +export default nbt type NbtTagTypes = { - end: number; - byte: number; - short: number; - int: number; - long: number; - float: number; - double: number; - byteArray: number; - string: number; - list: number; - compound: number; - intArray: number; - longArray: number; -}; + end: number + byte: number + short: number + int: number + long: number + float: number + double: number + byteArray: number + string: number + list: number + compound: number + intArray: number + longArray: number +} class NbtWriter { - offset: number; - buffer: ArrayBuffer; - arrayView: Uint8Array; - dataView: DataView; - - constructor(data?: ArrayBuffer) { - this.offset = 0; - this.buffer = data || new ArrayBuffer(1024); - this.arrayView = new Uint8Array(this.buffer); - this.dataView = new DataView(this.buffer); - } - - accommodate(size: number): void { - if (this.buffer.byteLength - this.offset < size) { - const newBuffer = new ArrayBuffer(this.buffer.byteLength * 2 + size); - new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)); - this.buffer = newBuffer; - this.arrayView = new Uint8Array(this.buffer); - this.dataView = new DataView(this.buffer); - } - } - - write(dataType: string, size: number, value: number | bigint): void { - this.accommodate(size); - (this.dataView as any)[`set${dataType}`](this.offset, value); - this.offset += size; - } - - byte(value: number): NbtWriter { - this.write('Int8', 1, value); - return this; - } - - short(value: number): NbtWriter { - this.write('Int16', 2, value); - return this; - } - - int(value: number): NbtWriter { - this.write('Int32', 4, value); - return this; - } - - long(value: bigint): NbtWriter { - if (typeof value !== 'bigint') { - throw new Error('Value must be a BigInt'); - } - const hi = Number(value >> 32n) & 0xffffffff; - const lo = Number(value & 0xffffffffn); - this.int(hi); - this.int(lo); - return this; - } - - float(value: number): NbtWriter { - this.write('Float32', 4, value); - return this; - } - - double(value: number): NbtWriter { - this.write('Float64', 8, value); - return this; - } - - byteArray(value: Uint8Array): NbtWriter { - this.int(value.length); - this.accommodate(value.length); - this.arrayView.set(value, this.offset); - this.offset += value.length; - return this; - } - - string(value: string): NbtWriter { - const encoded = new TextEncoder().encode(value); - this.short(encoded.length); - this.accommodate(encoded.length); - this.arrayView.set(encoded, this.offset); - this.offset += encoded.length; - return this; - } - - list(type: number, value: any[]): NbtWriter { - this.byte(type); - this.int(value.length); - value.forEach((element) => { - (this as any)[type](element); - }); - return this; - } - - compound(value: Record): NbtWriter { - Object.keys(value).forEach((key) => { - const elementType = value[key].type; - this.byte(elementType); - this.string(key); - (this as any)[elementType](value[key].value); - }); - this.byte(nbt.tagTypes.end); - return this; - } - - intArray(value: Int32Array): NbtWriter { - this.int(value.length); - for (let i = 0; i < value.length; i++) { - this.int(value[i]); - } - return this; - } - - longArray(value: BigInt64Array): NbtWriter { - this.int(value.length); - for (let i = 0; i < value.length; i++) { - this.long(value[i]); - } - return this; - } + offset: number + buffer: ArrayBuffer + arrayView: Uint8Array + dataView: DataView + + constructor(data?: ArrayBuffer) { + this.offset = 0 + this.buffer = data || new ArrayBuffer(1024) + this.arrayView = new Uint8Array(this.buffer) + this.dataView = new DataView(this.buffer) + } + + accommodate(size: number): void { + if (this.buffer.byteLength - this.offset < size) { + const newBuffer = new ArrayBuffer(this.buffer.byteLength * 2 + size) + new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)) + this.buffer = newBuffer + this.arrayView = new Uint8Array(this.buffer) + this.dataView = new DataView(this.buffer) + } + } + + write(dataType: string, size: number, value: number | bigint): void { + this.accommodate(size) + ;(this.dataView as any)[`set${dataType}`](this.offset, value) + this.offset += size + } + + byte(value: number): NbtWriter { + this.write("Int8", 1, value) + return this + } + + short(value: number): NbtWriter { + this.write("Int16", 2, value) + return this + } + + int(value: number): NbtWriter { + this.write("Int32", 4, value) + return this + } + + long(value: bigint): NbtWriter { + if (typeof value !== "bigint") { + throw new Error("Value must be a BigInt") + } + const hi = Number(value >> 32n) & 0xffffffff + const lo = Number(value & 0xffffffffn) + this.int(hi) + this.int(lo) + return this + } + + float(value: number): NbtWriter { + this.write("Float32", 4, value) + return this + } + + double(value: number): NbtWriter { + this.write("Float64", 8, value) + return this + } + + byteArray(value: Uint8Array): NbtWriter { + this.int(value.length) + this.accommodate(value.length) + this.arrayView.set(value, this.offset) + this.offset += value.length + return this + } + + string(value: string): NbtWriter { + const encoded = new TextEncoder().encode(value) + this.short(encoded.length) + this.accommodate(encoded.length) + this.arrayView.set(encoded, this.offset) + this.offset += encoded.length + return this + } + + list(type: number, value: any[]): NbtWriter { + this.byte(type) + this.int(value.length) + value.forEach((element) => { + ;(this as any)[type](element) + }) + return this + } + + compound(value: Record): NbtWriter { + Object.keys(value).forEach((key) => { + const elementType = value[key].type + this.byte(elementType) + this.string(key) + ;(this as any)[elementType](value[key].value) + }) + this.byte(nbt.tagTypes.end) + return this + } + + intArray(value: Int32Array): NbtWriter { + this.int(value.length) + for (let i = 0; i < value.length; i++) { + this.int(value[i]) + } + return this + } + + longArray(value: BigInt64Array): NbtWriter { + this.int(value.length) + for (let i = 0; i < value.length; i++) { + this.long(value[i]) + } + return this + } } class NbtReader { - offset: number; - buffer: ArrayBuffer; - dataView: DataView; - arrayView: Uint8Array; - - constructor(data: ArrayBuffer) { - this.offset = 0; - this.buffer = data; - this.dataView = new DataView(data); - this.arrayView = new Uint8Array(data); - } - - read(dataType: string, size: number): number | bigint { - const value = (this.dataView as any)[`get${dataType}`](this.offset); - this.offset += size; - return value; - } - - byte(): number { - return this.read('Int8', 1) as number; - } - - short(): number { - return this.read('Int16', 2) as number; - } - - int(): number { - return this.read('Int32', 4) as number; - } - - long(): bigint { - const hi = this.int(); - const lo = this.int(); - return BigInt(hi) << 32n | BigInt(lo); - } - - float(): number { - return this.read('Float32', 4) as number; - } - - double(): number { - return this.read('Float64', 8) as number; - } - - byteArray(): Uint8Array { - const length = this.int(); - const value = this.arrayView.slice(this.offset, this.offset + length); - this.offset += length; - return value; - } - - string(): string { - const length = this.short(); - const value = this.arrayView.slice(this.offset, this.offset + length); - this.offset += length; - return new TextDecoder().decode(value); - } - - list(): any[] { - const type = this.byte(); - const length = this.int(); - const value: any[] = []; - for (let i = 0; i < length; i++) { - value.push((this as any)[type]()); - } - return value; - } - - compound(): Record { - const value: Record = {}; - let type; - while ((type = this.byte()) !== nbt.tagTypes.end) { - const key = this.string(); - value[key] = { type, value: (this as any)[type]() }; - } - return value; - } - - intArray(): Int32Array { - const length = this.int(); - const value = new Int32Array(length); - for (let i = 0; i < length; i++) { - value[i] = this.int(); - } - return value; - } - - longArray(): BigInt64Array { - const length = this.int(); - const value = new BigInt64Array(length); - for (let i = 0; i < length; i++) { - value[i] = this.long(); - } - return value; - } + offset: number + buffer: ArrayBuffer + dataView: DataView + arrayView: Uint8Array + + constructor(data: ArrayBuffer) { + this.offset = 0 + this.buffer = data + this.dataView = new DataView(data) + this.arrayView = new Uint8Array(data) + } + + read(dataType: string, size: number): number | bigint { + const value = (this.dataView as any)[`get${dataType}`](this.offset) + this.offset += size + return value + } + + byte(): number { + return this.read("Int8", 1) as number + } + + short(): number { + return this.read("Int16", 2) as number + } + + int(): number { + return this.read("Int32", 4) as number + } + + long(): bigint { + const hi = this.int() + const lo = this.int() + return (BigInt(hi) << 32n) | BigInt(lo) + } + + float(): number { + return this.read("Float32", 4) as number + } + + double(): number { + return this.read("Float64", 8) as number + } + + byteArray(): Uint8Array { + const length = this.int() + const value = this.arrayView.slice(this.offset, this.offset + length) + this.offset += length + return value + } + + string(): string { + const length = this.short() + const value = this.arrayView.slice(this.offset, this.offset + length) + this.offset += length + return new TextDecoder().decode(value) + } + + list(): any[] { + const type = this.byte() + const length = this.int() + const value: any[] = [] + for (let i = 0; i < length; i++) { + value.push((this as any)[type]()) + } + return value + } + + compound(): Record { + const value: Record = {} + let type + while ((type = this.byte()) !== nbt.tagTypes.end) { + const key = this.string() + value[key] = { type, value: (this as any)[type]() } + } + return value + } + + intArray(): Int32Array { + const length = this.int() + const value = new Int32Array(length) + for (let i = 0; i < length; i++) { + value[i] = this.int() + } + return value + } + + longArray(): BigInt64Array { + const length = this.int() + const value = new BigInt64Array(length) + for (let i = 0; i < length; i++) { + value[i] = this.long() + } + return value + } } -export { NbtWriter, NbtReader, nbt }; \ No newline at end of file +export { NbtWriter, NbtReader, nbt } diff --git a/src/packet/Packets.ts b/src/packet/Packets.ts index 4c8787e..e164958 100644 --- a/src/packet/Packets.ts +++ b/src/packet/Packets.ts @@ -1,104 +1,104 @@ /** * Client -> Server Packets (C2S) - * + * * @abstract Serverbound */ export enum C2S { - /** - * Handshake - * - * State: None - */ - Handshake = 0x00, + /** + * Handshake + * + * State: None + */ + Handshake = 0x00, - /** - * Login - * - * State: Login - */ - Login = 0x00, + /** + * Login + * + * State: Login + */ + Login = 0x00, - /** - * Ping - * - * State: any - */ - Ping = 0x01, + /** + * Ping + * + * State: any + */ + Ping = 0x01, - /** - * Status - * - * State: Status - */ - StatusRequest = 0x00, + /** + * Status + * + * State: Status + */ + StatusRequest = 0x00, - /** - * Login Ack - * - * State: Login - */ - LoginAcknowledge = 0x03, + /** + * Login Ack + * + * State: Login + */ + LoginAcknowledge = 0x03, } /** * Server -> Client Packets (S2C) - * + * * @abstract Clientbound */ export enum S2C { - /** - * Disconnect - * - * State: Login - */ - DisconnectLogin = 0x00, + /** + * Disconnect + * + * State: Login + */ + DisconnectLogin = 0x00, - /** - * Disconnect - * - * State: Play - */ - DisconnectPlay = 0x1A, + /** + * Disconnect + * + * State: Play + */ + DisconnectPlay = 0x1a, - /** - * Login Success - * - * State: Login - */ - LoginSuccess = 0x02, + /** + * Login Success + * + * State: Login + */ + LoginSuccess = 0x02, - /** - * Pong - * - * State: any - */ - Pong = 0x01, + /** + * Pong + * + * State: any + */ + Pong = 0x01, - /** - * Status Response - * - * State: status - */ - StatusResponse = 0x00, + /** + * Status Response + * + * State: status + */ + StatusResponse = 0x00, - /** - * Registry Data - * - * State: join - */ - RegistryData = 0x05, + /** + * Registry Data + * + * State: join + */ + RegistryData = 0x05, - /** - * Registry Data - * - * State: join - */ - ConfigurationKeepAlive = 0x03, + /** + * Registry Data + * + * State: join + */ + ConfigurationKeepAlive = 0x03, - /** - * Finish Configuration - * - * State: join->play - */ - FinishConfiguration = 0x02, -} \ No newline at end of file + /** + * Finish Configuration + * + * State: join->play + */ + FinishConfiguration = 0x02, +} diff --git a/src/packet/client/HandshakePacket.ts b/src/packet/client/HandshakePacket.ts index 43c0ffc..b5b4f91 100644 --- a/src/packet/client/HandshakePacket.ts +++ b/src/packet/client/HandshakePacket.ts @@ -1,53 +1,57 @@ -import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket"; -import StaticImplements from "../../decorator/StaticImplements.js"; -import Server from "../../Server"; -import ParsedPacket from "../../ParsedPacket"; -import Connection from "../../Connection.js"; -import { C2S } from "../Packets.js"; +import { + TypedClientPacket, + TypedClientPacketStatic, +} from "../../types/TypedPacket" +import StaticImplements from "../../decorator/StaticImplements.js" +import Server from "../../Server" +import ParsedPacket from "../../ParsedPacket" +import Connection from "../../Connection.js" +import { C2S } from "../Packets.js" @StaticImplements() export default class HandshakePacket { - public readonly packet: ParsedPacket; + public readonly packet: ParsedPacket - public readonly data; + public readonly data - /** - * Create a new HandshakePacket - * @param packet - */ - public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet; + /** + * Create a new HandshakePacket + * @param packet + */ + public constructor(packet: import("../../ParsedPacket").default) { + this.packet = packet - this.data = { - protocolVersion: this.packet.getVarInt()!, - serverAddress: this.packet.getString()!, - serverPort: this.packet.getUShort()!, - nextState: this.packet.getVarInt()! - } as const; - } + this.data = { + protocolVersion: this.packet.getVarInt()!, + serverAddress: this.packet.getString()!, + serverPort: this.packet.getUShort()!, + nextState: this.packet.getVarInt()!, + } as const + } - execute(conn: Connection, _server: Server): void { - switch (this.data.nextState) { - case 1: - conn._setState(Connection.State.STATUS); - break; - case 2: - conn._setState(Connection.State.LOGIN); - break; - } - - } + execute(conn: Connection, _server: Server): void { + switch (this.data.nextState) { + case 1: + conn._setState(Connection.State.STATUS) + break + case 2: + conn._setState(Connection.State.LOGIN) + break + } + } - public static readonly id = C2S.Handshake; + public static readonly id = C2S.Handshake - public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { - if (conn.state !== Connection.State.NONE) return null; - try { - const p = new this(data); - return (p.packet.id === this.id) ? p : null; - } - catch { - return null; - } - } + public static isThisPacket( + data: ParsedPacket, + conn: Connection + ): TypedClientPacket | null { + if (conn.state !== Connection.State.NONE) return null + try { + const p = new this(data) + return p.packet.id === this.id ? p : null + } catch { + return null + } + } } diff --git a/src/packet/client/LoginAckPacket.ts b/src/packet/client/LoginAckPacket.ts index 7b0f48c..b49890a 100644 --- a/src/packet/client/LoginAckPacket.ts +++ b/src/packet/client/LoginAckPacket.ts @@ -1,41 +1,45 @@ -import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket"; -import StaticImplements from "../../decorator/StaticImplements.js"; -import Server from "../../Server"; -import ParsedPacket from "../../ParsedPacket"; -import Connection from "../../Connection.js"; -import { C2S } from "../Packets.js"; +import { + TypedClientPacket, + TypedClientPacketStatic, +} from "../../types/TypedPacket" +import StaticImplements from "../../decorator/StaticImplements.js" +import Server from "../../Server" +import ParsedPacket from "../../ParsedPacket" +import Connection from "../../Connection.js" +import { C2S } from "../Packets.js" @StaticImplements() export default class LoginAckPacket { - public readonly packet: ParsedPacket; - - public readonly data; - - /** - * Create a new HandshakePacket - * @param packet - */ - public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet; - - this.data = {} as const; - } - - execute(conn: Connection, _server: Server): void { - conn._setState(Connection.State.CONFIGURATION); - - } - - public static readonly id = C2S.LoginAcknowledge; - - public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { - if (conn.state !== Connection.State.LOGIN) return null; - try { - const p = new this(data); - return (p.packet.id === this.id) ? p : null; - } - catch { - return null; - } - } + public readonly packet: ParsedPacket + + public readonly data + + /** + * Create a new HandshakePacket + * @param packet + */ + public constructor(packet: import("../../ParsedPacket").default) { + this.packet = packet + + this.data = {} as const + } + + execute(conn: Connection, _server: Server): void { + conn._setState(Connection.State.CONFIGURATION) + } + + public static readonly id = C2S.LoginAcknowledge + + public static isThisPacket( + data: ParsedPacket, + conn: Connection + ): TypedClientPacket | null { + if (conn.state !== Connection.State.LOGIN) return null + try { + const p = new this(data) + return p.packet.id === this.id ? p : null + } catch { + return null + } + } } diff --git a/src/packet/client/LoginPacket.ts b/src/packet/client/LoginPacket.ts index 6a14082..b042f6b 100644 --- a/src/packet/client/LoginPacket.ts +++ b/src/packet/client/LoginPacket.ts @@ -1,44 +1,53 @@ -import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket"; -import StaticImplements from "../../decorator/StaticImplements.js"; -import ParsedPacket from "../../ParsedPacket.js"; -import Server from "../../Server"; -import Connection from "../../Connection.js"; -import { C2S } from "../Packets.js"; +import { + TypedClientPacket, + TypedClientPacketStatic, +} from "../../types/TypedPacket" +import StaticImplements from "../../decorator/StaticImplements.js" +import ParsedPacket from "../../ParsedPacket.js" +import Server from "../../Server" +import Connection from "../../Connection.js" +import { C2S } from "../Packets.js" @StaticImplements() export default class LoginPacket { - public readonly packet: ParsedPacket; + public readonly packet: ParsedPacket - public readonly data; + public readonly data - /** - * Create a new HandshakePacket - * @param packet - */ - public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet; + /** + * Create a new HandshakePacket + * @param packet + */ + public constructor(packet: import("../../ParsedPacket").default) { + this.packet = packet - this.data = { - username: this.packet.getString()!, - hasUUID: this.packet.getBoolean()!, - uuid: this.packet.getUUID()! - } - } + this.data = { + username: this.packet.getString()!, + hasUUID: this.packet.getBoolean()!, + uuid: this.packet.getUUID()!, + } + } - execute(conn: Connection, _server: Server): void { - conn._setState(Connection.State.LOGIN); - } + execute(conn: Connection, _server: Server): void { + conn._setState(Connection.State.LOGIN) + } - public static readonly id = C2S.Login; + public static readonly id = C2S.Login - public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { - if (conn.state !== Connection.State.LOGIN) return null; - try { - const p = new this(data); - return (p.packet.id === this.id && p.data.username !== null && p.data.username.match(/^[.*]?[A-Za-z0-9_]{3,16}$/) !== null) ? p : null; - } - catch { - return null; - } - } + public static isThisPacket( + data: ParsedPacket, + conn: Connection + ): TypedClientPacket | null { + if (conn.state !== Connection.State.LOGIN) return null + try { + const p = new this(data) + return p.packet.id === this.id && + p.data.username !== null && + p.data.username.match(/^[.*]?[A-Za-z0-9_]{3,16}$/) !== null + ? p + : null + } catch { + return null + } + } } diff --git a/src/packet/client/PingPacket.ts b/src/packet/client/PingPacket.ts index 55de60c..aef16fb 100644 --- a/src/packet/client/PingPacket.ts +++ b/src/packet/client/PingPacket.ts @@ -1,41 +1,46 @@ -import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket"; -import StaticImplements from "../../decorator/StaticImplements.js"; -import Server from "../../Server"; -import ParsedPacket from "../../ParsedPacket"; -import Connection from "../../Connection.js"; -import { C2S } from "../Packets.js"; +import { + TypedClientPacket, + TypedClientPacketStatic, +} from "../../types/TypedPacket" +import StaticImplements from "../../decorator/StaticImplements.js" +import Server from "../../Server" +import ParsedPacket from "../../ParsedPacket" +import Connection from "../../Connection.js" +import { C2S } from "../Packets.js" @StaticImplements() export default class PingPacket { - public readonly packet: ParsedPacket; + public readonly packet: ParsedPacket - public readonly data; + public readonly data - /** - * Create a new PingPacket - * @param packet - */ - public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet; + /** + * Create a new PingPacket + * @param packet + */ + public constructor(packet: import("../../ParsedPacket").default) { + this.packet = packet - this.data = { - payload: this.packet.getLong()! - } as const; - } + this.data = { + payload: this.packet.getLong()!, + } as const + } - execute(_conn: Connection, _server: Server): void { - // pass - } + execute(_conn: Connection, _server: Server): void { + // pass + } - public static readonly id = C2S.Ping; + public static readonly id = C2S.Ping - public static isThisPacket(data: ParsedPacket, _conn: Connection): TypedClientPacket | null { - try { - const p = new this(data); - return (p.packet.id === this.id) ? p : null; - } - catch { - return null; - } - } + public static isThisPacket( + data: ParsedPacket, + _conn: Connection + ): TypedClientPacket | null { + try { + const p = new this(data) + return p.packet.id === this.id ? p : null + } catch { + return null + } + } } diff --git a/src/packet/client/StatusRequestPacket.ts b/src/packet/client/StatusRequestPacket.ts index 431b740..2acb2bf 100644 --- a/src/packet/client/StatusRequestPacket.ts +++ b/src/packet/client/StatusRequestPacket.ts @@ -1,40 +1,45 @@ -import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket.js"; -import StaticImplements from "../../decorator/StaticImplements.js"; -import Server from "../../Server.js"; -import ParsedPacket from "../../ParsedPacket.js"; -import Connection from "../../Connection.js"; -import { C2S } from "../Packets.js"; +import { + TypedClientPacket, + TypedClientPacketStatic, +} from "../../types/TypedPacket.js" +import StaticImplements from "../../decorator/StaticImplements.js" +import Server from "../../Server.js" +import ParsedPacket from "../../ParsedPacket.js" +import Connection from "../../Connection.js" +import { C2S } from "../Packets.js" @StaticImplements() export default class StatusRequestPacket { - public readonly packet: ParsedPacket; + public readonly packet: ParsedPacket - public readonly data; + public readonly data - /** - * Create a new StatusRequest - * @param packet - */ - public constructor(packet: import("../../ParsedPacket.js").default) { - this.packet = packet; + /** + * Create a new StatusRequest + * @param packet + */ + public constructor(packet: import("../../ParsedPacket.js").default) { + this.packet = packet - this.data = {} as const; - } + this.data = {} as const + } - execute(_conn: Connection, _server: Server): void { - // pass - } + execute(_conn: Connection, _server: Server): void { + // pass + } - public static readonly id = C2S.StatusRequest; + public static readonly id = C2S.StatusRequest - public static isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null { - if (conn.state !== Connection.State.STATUS) return null; - try { - const p = new this(data); - return (p.packet.id === this.id) ? p : null; - } - catch { - return null; - } - } + public static isThisPacket( + data: ParsedPacket, + conn: Connection + ): TypedClientPacket | null { + if (conn.state !== Connection.State.STATUS) return null + try { + const p = new this(data) + return p.packet.id === this.id ? p : null + } catch { + return null + } + } } diff --git a/src/packet/server/DisconnectLoginPacket.ts b/src/packet/server/DisconnectLoginPacket.ts index 9697071..6fe870c 100644 --- a/src/packet/server/DisconnectLoginPacket.ts +++ b/src/packet/server/DisconnectLoginPacket.ts @@ -1,13 +1,15 @@ -import ServerPacket from "../../ServerPacket.js"; -import { S2C } from "../Packets.js"; +import ServerPacket from "../../ServerPacket.js" +import { S2C } from "../Packets.js" export default class DisconnectLoginPacket extends ServerPacket { - public static readonly id = S2C.DisconnectLogin; + public static readonly id = S2C.DisconnectLogin - public constructor(reason: ChatComponent) { - super(Buffer.concat([ - ServerPacket.writeVarInt(DisconnectLoginPacket.id), - ServerPacket.writeChat(reason) - ])); - } + public constructor(reason: ChatComponent) { + super( + Buffer.concat([ + ServerPacket.writeVarInt(DisconnectLoginPacket.id), + ServerPacket.writeChat(reason), + ]) + ) + } } diff --git a/src/packet/server/DisconnectPlayPacket.ts b/src/packet/server/DisconnectPlayPacket.ts index e47ed87..3c1e1f1 100644 --- a/src/packet/server/DisconnectPlayPacket.ts +++ b/src/packet/server/DisconnectPlayPacket.ts @@ -1,13 +1,15 @@ -import ServerPacket from "../../ServerPacket.js"; -import { S2C } from "../Packets.js"; +import ServerPacket from "../../ServerPacket.js" +import { S2C } from "../Packets.js" export default class DisconnectPlayPacket extends ServerPacket { - public static readonly id = S2C.DisconnectPlay; + public static readonly id = S2C.DisconnectPlay - public constructor(reason: ChatComponent) { - super(Buffer.concat([ - ServerPacket.writeVarInt(DisconnectPlayPacket.id), - ServerPacket.writeChat(reason) - ])); - } + public constructor(reason: ChatComponent) { + super( + Buffer.concat([ + ServerPacket.writeVarInt(DisconnectPlayPacket.id), + ServerPacket.writeChat(reason), + ]) + ) + } } diff --git a/src/packet/server/LoginSuccessPacket.ts b/src/packet/server/LoginSuccessPacket.ts index 5434ee8..86c2391 100644 --- a/src/packet/server/LoginSuccessPacket.ts +++ b/src/packet/server/LoginSuccessPacket.ts @@ -1,24 +1,26 @@ -import ServerPacket from "../../ServerPacket.js"; -import Connection from "../../Connection.js"; -import { S2C } from "../Packets.js"; +import ServerPacket from "../../ServerPacket.js" +import Connection from "../../Connection.js" +import { S2C } from "../Packets.js" /** * A Minecraft protocol client-bound LoginSuccess packet. */ export default class LoginSuccessPacket extends ServerPacket { - public static readonly id = S2C.LoginSuccess; + public static readonly id = S2C.LoginSuccess - public constructor(uuid: string, username: string) { - super(Buffer.concat([ - ServerPacket.writeVarInt(LoginSuccessPacket.id), - ServerPacket.writeUUID(uuid), - ServerPacket.writeString(username), - ServerPacket.writeVarInt(0) - ])); - } + public constructor(uuid: string, username: string) { + super( + Buffer.concat([ + ServerPacket.writeVarInt(LoginSuccessPacket.id), + ServerPacket.writeUUID(uuid), + ServerPacket.writeString(username), + ServerPacket.writeVarInt(0), + ]) + ) + } - public override send(connection: Connection) { - connection._setState(Connection.State.PLAY); - return super.send(connection); - } + public override send(connection: Connection) { + connection._setState(Connection.State.PLAY) + return super.send(connection) + } } diff --git a/src/packet/server/PongPacket.ts b/src/packet/server/PongPacket.ts index 774732c..172ea4b 100644 --- a/src/packet/server/PongPacket.ts +++ b/src/packet/server/PongPacket.ts @@ -1,14 +1,16 @@ -import ServerPacket from "../../ServerPacket.js"; -import PingPacket from "../client/PingPacket.js"; -import { S2C } from "../Packets.js"; +import ServerPacket from "../../ServerPacket.js" +import PingPacket from "../client/PingPacket.js" +import { S2C } from "../Packets.js" export default class PongPacket extends ServerPacket { - public static readonly id = S2C.Pong; + public static readonly id = S2C.Pong - public constructor(c2s: PingPacket) { - super(Buffer.concat([ - ServerPacket.writeVarInt(PongPacket.id), - ServerPacket.writeLong(c2s.data.payload), - ])); - } + public constructor(c2s: PingPacket) { + super( + Buffer.concat([ + ServerPacket.writeVarInt(PongPacket.id), + ServerPacket.writeLong(c2s.data.payload), + ]) + ) + } } diff --git a/src/packet/server/StatusResponsePacket.ts b/src/packet/server/StatusResponsePacket.ts index c94a6cf..a8d6e55 100644 --- a/src/packet/server/StatusResponsePacket.ts +++ b/src/packet/server/StatusResponsePacket.ts @@ -1,35 +1,40 @@ -import Server from "../../Server.js"; -import ServerPacket from "../../ServerPacket.js"; -import { S2C } from "../Packets.js"; +import Server from "../../Server.js" +import ServerPacket from "../../ServerPacket.js" +import { S2C } from "../Packets.js" export default class StatusResponsePacket extends ServerPacket { - public static readonly id = S2C.StatusResponse; + public static readonly id = S2C.StatusResponse - public constructor(server: Server) { - super(Buffer.concat([ - ServerPacket.writeVarInt(StatusResponsePacket.id), - ServerPacket.writeString(JSON.stringify({ - "version": server.config.server.version, - "players": { - "max": server.config.server.maxPlayers, - "online": 2, - "sample": [ - { - "name": "lp721mk", - "id": "c73d1477-a7c9-40c0-a86c-387a95917332" - }, - { - "name": "km127pl", - "id": "4ab89680-76a0-4e82-b90a-56cff4b38290" - } - ] - }, - "description": { - "text": server.config.server.motd - }, - "favicon": server.favicon, - "enforcesSecureChat": server.config.server.enforcesSecureChat - })) - ])); - } + public constructor(server: Server) { + super( + Buffer.concat([ + ServerPacket.writeVarInt(StatusResponsePacket.id), + ServerPacket.writeString( + JSON.stringify({ + version: server.config.server.version, + players: { + max: server.config.server.maxPlayers, + online: 2, + sample: [ + { + name: "lp721mk", + id: "c73d1477-a7c9-40c0-a86c-387a95917332", + }, + { + name: "km127pl", + id: "4ab89680-76a0-4e82-b90a-56cff4b38290", + }, + ], + }, + description: { + text: server.config.server.motd, + }, + favicon: server.favicon, + enforcesSecureChat: + server.config.server.enforcesSecureChat, + }) + ), + ]) + ) + } } diff --git a/src/packet/server/configuration/ConfigurationKeepAlive.ts b/src/packet/server/configuration/ConfigurationKeepAlive.ts index 2e4ef18..e534ef4 100644 --- a/src/packet/server/configuration/ConfigurationKeepAlive.ts +++ b/src/packet/server/configuration/ConfigurationKeepAlive.ts @@ -1,13 +1,15 @@ -import ServerPacket from "../../../ServerPacket.js"; -import { S2C } from "../../Packets.js"; +import ServerPacket from "../../../ServerPacket.js" +import { S2C } from "../../Packets.js" export default class ConfgirationKeepAlive extends ServerPacket { - public static readonly id = S2C.ConfigurationKeepAlive; + public static readonly id = S2C.ConfigurationKeepAlive - public constructor() { - super(Buffer.concat([ - ServerPacket.writeVarInt(ConfgirationKeepAlive.id), - ServerPacket.writeLong(BigInt(Date.now())) - ])); - } + public constructor() { + super( + Buffer.concat([ + ServerPacket.writeVarInt(ConfgirationKeepAlive.id), + ServerPacket.writeLong(BigInt(Date.now())), + ]) + ) + } } diff --git a/src/packet/server/configuration/FinishConfigurationPacket.ts b/src/packet/server/configuration/FinishConfigurationPacket.ts index 028a616..caf478b 100644 --- a/src/packet/server/configuration/FinishConfigurationPacket.ts +++ b/src/packet/server/configuration/FinishConfigurationPacket.ts @@ -1,18 +1,20 @@ -import Connection from "../../../Connection.js"; -import ServerPacket from "../../../ServerPacket.js"; -import { S2C } from "../../Packets.js"; +import Connection from "../../../Connection.js" +import ServerPacket from "../../../ServerPacket.js" +import { S2C } from "../../Packets.js" export default class FinishConfigurationPacket extends ServerPacket { - public static readonly id = S2C.FinishConfiguration; + public static readonly id = S2C.FinishConfiguration - public constructor() { - super(Buffer.concat([ - ServerPacket.writeVarInt(FinishConfigurationPacket.id), - ])); - } + public constructor() { + super( + Buffer.concat([ + ServerPacket.writeVarInt(FinishConfigurationPacket.id), + ]) + ) + } - public override send(connection: Connection) { - connection._setState(Connection.State.PLAY); - return super.send(connection); - } + public override send(connection: Connection) { + connection._setState(Connection.State.PLAY) + return super.send(connection) + } } diff --git a/src/packet/server/configuration/RegistryDataPacket.ts b/src/packet/server/configuration/RegistryDataPacket.ts index 97ff71d..9b2de85 100644 --- a/src/packet/server/configuration/RegistryDataPacket.ts +++ b/src/packet/server/configuration/RegistryDataPacket.ts @@ -1,39 +1,41 @@ -import ServerPacket from "../../../ServerPacket.js"; -import { S2C } from "../../Packets.js"; -import { NbtReader, NbtWriter, tagTypes } from "../../../nbt.js"; +import ServerPacket from "../../../ServerPacket.js" +import { S2C } from "../../Packets.js" +import { NbtReader, NbtWriter, tagTypes } from "../../../nbt.js" export default class RegistryDataPacket extends ServerPacket { - public static readonly id = S2C.RegistryData; + public static readonly id = S2C.RegistryData - public constructor() { - const writer = new NbtWriter(); - writer.compound({ - "minecraft:worldgen/biome": { - type: tagTypes.compound, - value: { - "type": { - type: tagTypes.string, - value: "minecraft:worldgen/biome" - }, - "value": { - type: tagTypes.compound, - value: { - "name": { - type: tagTypes.string, - value: "minecraft:plains" - }, - "id": { - type: tagTypes.int, - value: 0 - } - } - } - } - } - }); - super(Buffer.concat([ - ServerPacket.writeVarInt(RegistryDataPacket.id), - Buffer.from(writer.buffer.slice(0, writer.offset)) - ])); - } + public constructor() { + const writer = new NbtWriter() + writer.compound({ + "minecraft:worldgen/biome": { + type: tagTypes.compound, + value: { + type: { + type: tagTypes.string, + value: "minecraft:worldgen/biome", + }, + value: { + type: tagTypes.compound, + value: { + name: { + type: tagTypes.string, + value: "minecraft:plains", + }, + id: { + type: tagTypes.int, + value: 0, + }, + }, + }, + }, + }, + }) + super( + Buffer.concat([ + ServerPacket.writeVarInt(RegistryDataPacket.id), + Buffer.from(writer.buffer.slice(0, writer.offset)), + ]) + ) + } } diff --git a/src/types/ChatComponent.d.ts b/src/types/ChatComponent.d.ts index 48335a8..9ef3aeb 100644 --- a/src/types/ChatComponent.d.ts +++ b/src/types/ChatComponent.d.ts @@ -1,25 +1,35 @@ -type ChatComponent = { - bold?: boolean; - italic?: boolean; - underlined?: boolean; - strikethrough?: boolean; - obfuscated?: boolean; - font?: string; - color?: string; - insertion?: string; - clickEvent?: { - action: "open_url" | "open_file" | "run_command" | "suggest_command" | "change_page" | "copy_to_clipboard"; - value: string; - } - hoverEvent?: { - action: "show_text" | "show_item" | "show_entity"; - } - text?: string; - extra?: [ChatComponent, ...ChatComponent[]]; -} | {translate: string; with?: ChatComponent[]} | {keybind: string} | { - score: { - name: string; - objective: string; - value?: string; - } -}; +type ChatComponent = + | { + bold?: boolean + italic?: boolean + underlined?: boolean + strikethrough?: boolean + obfuscated?: boolean + font?: string + color?: string + insertion?: string + clickEvent?: { + action: + | "open_url" + | "open_file" + | "run_command" + | "suggest_command" + | "change_page" + | "copy_to_clipboard" + value: string + } + hoverEvent?: { + action: "show_text" | "show_item" | "show_entity" + } + text?: string + extra?: [ChatComponent, ...ChatComponent[]] + } + | { translate: string; with?: ChatComponent[] } + | { keybind: string } + | { + score: { + name: string + objective: string + value?: string + } + } diff --git a/src/types/TypedEventEmitter.d.ts b/src/types/TypedEventEmitter.d.ts index 6704088..93c69dd 100644 --- a/src/types/TypedEventEmitter.d.ts +++ b/src/types/TypedEventEmitter.d.ts @@ -23,26 +23,32 @@ * @see https://github.com/andywer/typed-emitter */ export default interface TypedEventEmitter { - addListener (event: E, listener: Events[E]): this - on (event: E, listener: Events[E]): this - once (event: E, listener: Events[E]): this - prependListener (event: E, listener: Events[E]): this - prependOnceListener (event: E, listener: Events[E]): this + addListener(event: E, listener: Events[E]): this + on(event: E, listener: Events[E]): this + once(event: E, listener: Events[E]): this + prependListener(event: E, listener: Events[E]): this + prependOnceListener( + event: E, + listener: Events[E] + ): this - off(event: E, listener: Events[E]): this - removeAllListeners (event?: E): this - removeListener (event: E, listener: Events[E]): this + off(event: E, listener: Events[E]): this + removeAllListeners(event?: E): this + removeListener(event: E, listener: Events[E]): this - emit (event: E, ...args: Parameters): boolean - // The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5 - eventNames (): (keyof Events | string | symbol)[] - rawListeners (event: E): Events[E][] - listeners (event: E): Events[E][] - listenerCount (event: E): number + emit( + event: E, + ...args: Parameters + ): boolean + // The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5 + eventNames(): (keyof Events | string | symbol)[] + rawListeners(event: E): Events[E][] + listeners(event: E): Events[E][] + listenerCount(event: E): number - getMaxListeners (): number - setMaxListeners (maxListeners: number): this + getMaxListeners(): number + setMaxListeners(maxListeners: number): this } export type EventMap = { - [key: string]: (...args: any[]) => void + [key: string]: (...args: any[]) => void } diff --git a/src/types/TypedPacket.d.ts b/src/types/TypedPacket.d.ts index d7f9d3b..9541f33 100644 --- a/src/types/TypedPacket.d.ts +++ b/src/types/TypedPacket.d.ts @@ -1,17 +1,17 @@ -import Server from "../Server"; -import ParsedPacket from "../ParsedPacket"; -import Connection from "../Connection"; +import Server from "../Server" +import ParsedPacket from "../ParsedPacket" +import Connection from "../Connection" export interface TypedClientPacket { - readonly packet: ParsedPacket; - readonly data: Record; - execute(connection: Connection, server: Server): void; + readonly packet: ParsedPacket + readonly data: Record + execute(connection: Connection, server: Server): void } export interface TypedClientPacketStatic { - new(packet: ParsedPacket): TypedClientPacket; + new (packet: ParsedPacket): TypedClientPacket - readonly id: number; + readonly id: number - isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null; + isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null } diff --git a/tsconfig.json b/tsconfig.json index be16aa6..883120e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,103 +1,103 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Projects */ + "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */, + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "ESNext", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + "importsNotUsedAsValues": "remove" /* Specify emit/checking behavior for imports that are only used for types. */, + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + "stripInternal": true /* Disable emitting declarations that have '@internal' in their JSDoc comments. */, + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + "noEmitOnError": true /* Disable emitting files if any type checking errors are reported. */, + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "preserveSymlinks": true /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - "noPropertyAccessFromIndexSignature": false, /* Enforces using indexed accessors for keys declared using an indexed type. */ - "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */, + "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, + "strictBindCallApply": true /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */, + "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, + "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */, + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + "noUnusedParameters": true /* Raise an error when a function parameter isn't read. */, + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, + "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, + "noPropertyAccessFromIndexSignature": false /* Enforces using indexed accessors for keys declared using an indexed type. */, + "allowUnusedLabels": true /* Disable error reporting for unused labels. */, + "allowUnreachableCode": true /* Disable error reporting for unreachable code. */, - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From e75ce3bdf3be4a361e79bfcb5b22493c76ed0dd8 Mon Sep 17 00:00:00 2001 From: km127pl Date: Mon, 27 May 2024 13:44:40 +0200 Subject: [PATCH 12/13] add script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2bd9ac5..d888c5b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build:start": "npm run build && npm run start", "bundle": "ncc build ./index.ts -o build -m", "start": "node dist/index.js", + "lint": "prettier . --write", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], From 50092a0ce16bd200486ba32180a3d4df68bafdb2 Mon Sep 17 00:00:00 2001 From: km127pl Date: Wed, 5 Jun 2024 16:49:19 +0200 Subject: [PATCH 13/13] add semicolons back --- .prettierrc | 2 +- index.ts | 76 +-- src/Config.ts | 56 +- src/Connection.ts | 54 +- src/ConnectionPool.ts | 46 +- src/Logger.ts | 44 +- src/Packet.ts | 139 ++--- src/ParsedPacket.ts | 78 +-- src/Scheduler.ts | 234 ++++---- src/Server.ts | 120 ++-- src/ServerPacket.ts | 14 +- src/decorator/StaticImplements.ts | 2 +- src/nbt.ts | 562 +++++++++--------- src/packet/client/HandshakePacket.ts | 38 +- src/packet/client/LoginAckPacket.ts | 32 +- src/packet/client/LoginPacket.ts | 32 +- src/packet/client/PingPacket.ts | 28 +- src/packet/client/StatusRequestPacket.ts | 30 +- src/packet/server/DisconnectLoginPacket.ts | 8 +- src/packet/server/DisconnectPlayPacket.ts | 8 +- src/packet/server/LoginSuccessPacket.ts | 14 +- src/packet/server/PongPacket.ts | 10 +- src/packet/server/StatusResponsePacket.ts | 10 +- .../configuration/ConfigurationKeepAlive.ts | 8 +- .../FinishConfigurationPacket.ts | 14 +- .../configuration/RegistryDataPacket.ts | 14 +- src/types/ChatComponent.d.ts | 40 +- src/types/TypedEventEmitter.d.ts | 37 +- src/types/TypedPacket.d.ts | 21 +- 29 files changed, 893 insertions(+), 878 deletions(-) diff --git a/.prettierrc b/.prettierrc index 8522529..34379b1 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "trailingComma": "es5", "tabWidth": 4, - "semi": false, + "semi": true, "useTabs": true, "singleQuote": false, "endOfLine": "lf" diff --git a/index.ts b/index.ts index 256a2d9..942fd44 100644 --- a/index.ts +++ b/index.ts @@ -1,57 +1,57 @@ -import { Config, ConfigLoader } from "./src/Config.js" -import Server from "./src/Server.js" -import LoginSuccessPacket from "./src/packet/server/LoginSuccessPacket.js" -import Connection from "./src/Connection.js" -import StatusResponsePacket from "./src/packet/server/StatusResponsePacket.js" -import PongPacket from "./src/packet/server/PongPacket.js" -import ServerPacket from "./src/ServerPacket.js" -import { setTimeout } from "timers/promises" -import FinishConfigurationPacket from "./src/packet/server/configuration/FinishConfigurationPacket.js" -import RegistryDataPacket from "./src/packet/server/configuration/RegistryDataPacket.js" +import { Config, ConfigLoader } from "./src/Config.js"; +import Server from "./src/Server.js"; +import LoginSuccessPacket from "./src/packet/server/LoginSuccessPacket.js"; +import Connection from "./src/Connection.js"; +import StatusResponsePacket from "./src/packet/server/StatusResponsePacket.js"; +import PongPacket from "./src/packet/server/PongPacket.js"; +import ServerPacket from "./src/ServerPacket.js"; +import { setTimeout } from "timers/promises"; +import FinishConfigurationPacket from "./src/packet/server/configuration/FinishConfigurationPacket.js"; +import RegistryDataPacket from "./src/packet/server/configuration/RegistryDataPacket.js"; -const config: Config = await ConfigLoader.fromFile("config.json") -const server = new Server(config) +const config: Config = await ConfigLoader.fromFile("config.json"); +const server = new Server(config); -server.start() +server.start(); server.on("listening", (port) => server.logger.info(`Listening on port ${port}`) -) +); server.on("unknownPacket", (packet, conn) => { server.logger.debug( "Unknown packet", `{state=${Connection.State[conn.state]}}`, packet.dataBuffer - ) -}) + ); +}); server.on("packet", (packet, _conn) => { - server.logger.debug(packet.constructor.name, packet.data) -}) + server.logger.debug(packet.constructor.name, packet.data); +}); server.on("connection", (conn) => { server.logger.debug("Connection", { ip: conn.socket.remoteAddress, port: conn.socket.remotePort, - }) -}) + }); +}); server.on("disconnect", (conn) => { server.logger.debug("Disconnect", { ip: conn.socket.remoteAddress, port: conn.socket.remotePort, - }) -}) + }); +}); server.on("closed", () => { - server.logger.info("Server closed") - process.exit(0) -}) + server.logger.info("Server closed"); + process.exit(0); +}); process.on("SIGINT", () => { - process.stdout.write("\x1b[2D") // Move cursor 2 characters left (clears ^C) - if (server.isRunning) server.stop().then() - else process.exit(0) -}) + process.stdout.write("\x1b[2D"); // Move cursor 2 characters left (clears ^C) + if (server.isRunning) server.stop().then(); + else process.exit(0); +}); server.on("packet.LoginPacket", (packet, conn) => { new LoginSuccessPacket( @@ -60,20 +60,20 @@ server.on("packet.LoginPacket", (packet, conn) => { .toString("hex") .slice(0, 32), packet.data.username - ).send(conn) -}) + ).send(conn); +}); server.on("packet.PingPacket", (packet, conn) => { - new PongPacket(packet).send(conn) -}) + new PongPacket(packet).send(conn); +}); server.on("packet.StatusRequestPacket", (_, conn) => { - new StatusResponsePacket(server).send(conn) -}) + new StatusResponsePacket(server).send(conn); +}); server.on("packet.LoginAck", async (_, conn) => { - await new RegistryDataPacket().send(conn).then() - await new FinishConfigurationPacket().send(conn).then() -}) + await new RegistryDataPacket().send(conn).then(); + await new FinishConfigurationPacket().send(conn).then(); +}); //FIXME: loginack not executed diff --git a/src/Config.ts b/src/Config.ts index c4fa3f5..aabe039 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,26 +1,26 @@ -import { open, access, constants, FileHandle } from "node:fs/promises" -import Logger from "./Logger.js" +import { open, access, constants, FileHandle } from "node:fs/promises"; +import Logger from "./Logger.js"; export interface Config { /** * Port to listen on */ - port: number + port: number; /** * The level to display logs at */ - logLevel: Logger.Level + logLevel: Logger.Level; /** * Kick reason for when the server is shutting down */ - shutdownKickReason: ChatComponent + shutdownKickReason: ChatComponent; /** * The server config */ - server: ServerConfig + server: ServerConfig; } export interface ServerConfig { @@ -28,23 +28,23 @@ export interface ServerConfig { * The motd of the server (description) * Split optionally by `\n` */ - motd: string + motd: string; /** * A path to the icon of the server * @description Must be 64x64 and a PNG */ - favicon?: string + favicon?: string; /** * Enforce message signing (since 1.18+) */ - enforcesSecureChat: boolean + enforcesSecureChat: boolean; /** * Max number of players that may be logged on at the same time */ - maxPlayers: number + maxPlayers: number; /** * The protocol version & number @@ -55,9 +55,9 @@ export interface ServerConfig { * ``` */ version: { - name: string - protocol: number - } + name: string; + protocol: number; + }; } export class ConfigLoader { @@ -70,19 +70,19 @@ export class ConfigLoader { */ public static async fromFile(file: string): Promise { if (!(await ConfigLoader.exists(file))) { - await ConfigLoader.createDefault(file) - const config = ConfigLoader.getDefault() + await ConfigLoader.createDefault(file); + const config = ConfigLoader.getDefault(); new Logger("Config", config.logLevel).warn( "Config does not exist, creating default '%s'", file - ) - return config + ); + return config; } - const fd: FileHandle = await open(file, "r") - const data: string = await fd.readFile("utf-8") - fd.close() + const fd: FileHandle = await open(file, "r"); + const data: string = await fd.readFile("utf-8"); + fd.close(); - return JSON.parse(data) as Config + return JSON.parse(data) as Config; } /** @@ -105,7 +105,7 @@ export class ConfigLoader { protocol: 766, }, }, - } + }; } /** @@ -115,10 +115,10 @@ export class ConfigLoader { */ public static async exists(file: string): Promise { try { - await access(file, constants.F_OK) - return true + await access(file, constants.F_OK); + return true; } catch { - return false + return false; } } @@ -126,8 +126,8 @@ export class ConfigLoader { * Create the default config file */ public static async createDefault(file: string): Promise { - const fd = await open(file, "w") - await fd.writeFile(JSON.stringify(ConfigLoader.getDefault(), null, 4)) - fd.close() + const fd = await open(file, "w"); + await fd.writeFile(JSON.stringify(ConfigLoader.getDefault(), null, 4)); + fd.close(); } } diff --git a/src/Connection.ts b/src/Connection.ts index 9d70505..0317c37 100644 --- a/src/Connection.ts +++ b/src/Connection.ts @@ -1,8 +1,8 @@ -import net from "node:net" -import * as crypto from "node:crypto" -import Server from "./Server" -import Packet from "./Packet.js" -import Logger from "./Logger.js" +import net from "node:net"; +import * as crypto from "node:crypto"; +import Server from "./Server"; +import Packet from "./Packet.js"; +import Logger from "./Logger.js"; /** * A TCP socket connection to the server. @@ -11,62 +11,66 @@ class Connection { /** * A unique identifier for this connection. */ - public readonly id: string + public readonly id: string; /** * The TCP socket for this connection. */ - public readonly socket: net.Socket + public readonly socket: net.Socket; /** * The server to which this connection belongs. */ - public readonly server: Server + public readonly server: Server; /** * The state of the connection. */ - #state: Connection.State = Connection.State.NONE + #state: Connection.State = Connection.State.NONE; /** * The state of the connection. */ public get state(): Connection.State { - return this.#state + return this.#state; } /** @internal */ public _setState(state: Connection.State): void { new Logger("State", Logger.Level.DEBUG).debug( `Switching state from ${this.#state} to ${state}` - ) - this.#state = state + ); + this.#state = state; } /** * Packet fragment this connection is currently sending to the server. * @internal */ - private currentPacketFragment: Packet = new Packet() + private currentPacketFragment: Packet = new Packet(); constructor(socket: net.Socket, server: Server) { - this.id = crypto.randomBytes(16).toString("hex") - this.socket = socket - this.server = server + this.id = crypto.randomBytes(16).toString("hex"); + this.socket = socket; + this.server = server; } /** @internal */ public incomingPacketFragment(data: number) { if (this.currentPacketFragment.push(data)) { - const p = this.currentPacketFragment.getTypedClient(this) + const p = this.currentPacketFragment.getTypedClient(this); if (p) { - p.execute(this, this.server) - this.server.emit("packet", p, this) - this.server.emit(`packet.${p.constructor.name}` as any, p, this) + p.execute(this, this.server); + this.server.emit("packet", p, this); + this.server.emit( + `packet.${p.constructor.name}` as any, + p, + this + ); } else this.server.emit( "unknownPacket", this.currentPacketFragment, this - ) - this.currentPacketFragment = new Packet() + ); + this.currentPacketFragment = new Packet(); } } @@ -75,7 +79,7 @@ class Connection { * @param [reason] The reason for the disconnect. */ public disconnect(reason?: string): Promise { - return this.server.connections.disconnect(this.id, reason) + return this.server.connections.disconnect(this.id, reason); } /** @@ -85,7 +89,7 @@ class Connection { return ( !this.socket.destroyed && this.server.connections.get(this.id) !== null - ) + ); } } @@ -129,4 +133,4 @@ namespace Connection { } } -export default Connection +export default Connection; diff --git a/src/ConnectionPool.ts b/src/ConnectionPool.ts index 6a236df..b0cb761 100644 --- a/src/ConnectionPool.ts +++ b/src/ConnectionPool.ts @@ -1,17 +1,17 @@ -import Connection from "./Connection.js" -import DisconnectLoginPacket from "./packet/server/DisconnectLoginPacket.js" -import DisconnectPlayPacket from "./packet/server/DisconnectPlayPacket.js" +import Connection from "./Connection.js"; +import DisconnectLoginPacket from "./packet/server/DisconnectLoginPacket.js"; +import DisconnectPlayPacket from "./packet/server/DisconnectPlayPacket.js"; export default class ConnectionPool { - private readonly connections: Connection[] = [] + private readonly connections: Connection[] = []; /** * Add a connection to the pool * @param connection */ public add(connection: Connection): void { - this.connections.push(connection) - connection.socket.on("close", () => this.disconnect(connection.id)) + this.connections.push(connection); + connection.socket.on("close", () => this.disconnect(connection.id)); } /** @@ -21,7 +21,7 @@ export default class ConnectionPool { public get(id: string): Connection | null { return ( this.connections.find((connection) => connection.id === id) ?? null - ) + ); } /** @@ -32,10 +32,10 @@ export default class ConnectionPool { public async disconnectAll( reason?: string | ChatComponent ): Promise { - const promises: Promise[] = [] + const promises: Promise[] = []; for (const connection of this.connections) - promises.push(this.disconnect(connection.id, reason)) - return (await Promise.all(promises)).every((result) => result) + promises.push(this.disconnect(connection.id, reason)); + return (await Promise.all(promises)).every((result) => result); } /** @@ -48,20 +48,20 @@ export default class ConnectionPool { id: string, reason?: string | ChatComponent ): Promise { - const connection = this.get(id) - if (!connection) return false - const index = this.connections.indexOf(connection) - if (index === -1) return false - const message = typeof reason === "string" ? { text: reason } : reason! + const connection = this.get(id); + if (!connection) return false; + const index = this.connections.indexOf(connection); + if (index === -1) return false; + const message = typeof reason === "string" ? { text: reason } : reason!; if (reason) switch (connection.state) { case Connection.State.LOGIN: { - await new DisconnectLoginPacket(message).send(connection) - break + await new DisconnectLoginPacket(message).send(connection); + break; } case Connection.State.PLAY: { - await new DisconnectPlayPacket(message).send(connection) - break + await new DisconnectPlayPacket(message).send(connection); + break; } default: { connection.server.logger.warn( @@ -69,13 +69,13 @@ export default class ConnectionPool { Connection.State[connection.state] + " on connection " + connection.id - ) + ); } } - this.connections.splice(index, 1) - connection.server.emit("disconnect", connection) + this.connections.splice(index, 1); + connection.server.emit("disconnect", connection); return new Promise((resolve) => connection.socket.end(() => resolve(true)) - ) + ); } } diff --git a/src/Logger.ts b/src/Logger.ts index 97f02b8..a663a39 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,10 +1,10 @@ class Logger { - private readonly name: string - private readonly logLevel: Logger.Level + private readonly name: string; + private readonly logLevel: Logger.Level; public constructor(name: string, logLevel: Logger.Level) { - this.name = name - this.logLevel = logLevel + this.name = name; + this.logLevel = logLevel; } /** @@ -12,7 +12,7 @@ class Logger { * @param obj Object to print */ private stdout(...obj: any[]): void { - console.log(...obj) + console.log(...obj); } /** @@ -20,7 +20,7 @@ class Logger { * @param obj Object to print */ private stderr(...obj: any[]): void { - console.error(...obj) + console.error(...obj); } /** @@ -29,7 +29,7 @@ class Logger { * @param message Message to format */ private format(level: Logger.Level, message: string): string { - return `${Logger.text256(240)}[${new Date().toISOString()}] ${Logger.ansi.format.reset}${Logger.level[level]}[${this.name}/${level}]${Logger.ansi.format.reset} ${message}${Logger.ansi.format.reset}` + return `${Logger.text256(240)}[${new Date().toISOString()}] ${Logger.ansi.format.reset}${Logger.level[level]}[${this.name}/${level}]${Logger.ansi.format.reset} ${message}${Logger.ansi.format.reset}`; } /** @@ -43,7 +43,7 @@ class Logger { this[level === Logger.Level.ERROR ? "stderr" : "stdout"]( this.format(level, message), ...obj - ) + ); } } @@ -53,7 +53,7 @@ class Logger { * @param [obj] Objects to print */ public info(message: string, ...obj: any[]): void { - this.log(Logger.Level.INFO, message, ...obj) + this.log(Logger.Level.INFO, message, ...obj); } /** @@ -62,7 +62,7 @@ class Logger { * @param [obj] Objects to print */ public warn(message: string, ...obj: any[]): void { - this.log(Logger.Level.WARN, message, ...obj) + this.log(Logger.Level.WARN, message, ...obj); } /** @@ -71,7 +71,7 @@ class Logger { * @param [obj] Objects to print */ public error(message: string, ...obj: any[]): void { - this.log(Logger.Level.ERROR, message, ...obj) + this.log(Logger.Level.ERROR, message, ...obj); } /** @@ -80,7 +80,7 @@ class Logger { * @param [obj] Objects to print */ public success(message: string, ...obj: any[]): void { - this.log(Logger.Level.SUCCESS, message, ...obj) + this.log(Logger.Level.SUCCESS, message, ...obj); } /** @@ -92,7 +92,7 @@ class Logger { return ( Logger.LevelHierarchy.indexOf(level) >= Logger.LevelHierarchy.indexOf(this.logLevel) - ) + ); } /** @@ -101,7 +101,7 @@ class Logger { * @param [obj] Objects to print */ public debug(message: string, ...obj: any[]): void { - this.log(Logger.Level.DEBUG, message, ...obj) + this.log(Logger.Level.DEBUG, message, ...obj); } /** @@ -157,7 +157,7 @@ class Logger { hidden: "\x1b[8m", dim: "\x1b[2m", }, - }) + }); /** * Level formatting @@ -168,7 +168,7 @@ class Logger { SUCCESS: Logger.ansi.text.bright.green, WARN: Logger.ansi.text.bright.yellow, ERROR: Logger.ansi.text.bright.red, - }) + }); /** * Level hierarchy @@ -179,14 +179,14 @@ class Logger { "SUCCESS", "WARN", "ERROR", - ]) + ]); /** * 256 colors * @param colour Colour ID from 0 to 255 */ public static text256(colour: number): string { - return `\x1b[38;5;${colour}m` + return `\x1b[38;5;${colour}m`; } /** @@ -194,7 +194,7 @@ class Logger { * @param colour Colour ID from 0 to 255 */ public static background256(colour: number): string { - return `\x1b[48;5;${colour}m` + return `\x1b[48;5;${colour}m`; } /** @@ -204,7 +204,7 @@ class Logger { * @param blue Blue value from 0 to 255 */ public static textRGB(red: number, green: number, blue: number): string { - return `\x1b[38;2;${red};${green};${blue}m` + return `\x1b[38;2;${red};${green};${blue}m`; } /** @@ -218,7 +218,7 @@ class Logger { green: number, blue: number ): string { - return `\x1b[48;2;${red};${green};${blue}m` + return `\x1b[48;2;${red};${green};${blue}m`; } } @@ -254,4 +254,4 @@ namespace Logger { } } -export default Logger +export default Logger; diff --git a/src/Packet.ts b/src/Packet.ts index 0dbb5bb..31a30fc 100644 --- a/src/Packet.ts +++ b/src/Packet.ts @@ -1,21 +1,24 @@ -import ParsedPacket from "./ParsedPacket.js" -import { TypedClientPacket, TypedClientPacketStatic } from "./types/TypedPacket" -import HandshakePacket from "./packet/client/HandshakePacket.js" -import LoginPacket from "./packet/client/LoginPacket.js" -import Connection from "./Connection" -import PingPacket from "./packet/client/PingPacket.js" -import StatusRequestPacket from "./packet/client/StatusRequestPacket.js" -import LoginAckPacket from "./packet/client/LoginAckPacket.js" +import ParsedPacket from "./ParsedPacket.js"; +import { + TypedClientPacket, + TypedClientPacketStatic, +} from "./types/TypedPacket"; +import HandshakePacket from "./packet/client/HandshakePacket.js"; +import LoginPacket from "./packet/client/LoginPacket.js"; +import Connection from "./Connection"; +import PingPacket from "./packet/client/PingPacket.js"; +import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; +import LoginAckPacket from "./packet/client/LoginAckPacket.js"; export default class Packet { - readonly #data: number[] + readonly #data: number[]; /** * Create a new packet * @param [data] Packet data */ public constructor(data: number[] = []) { - this.#data = data + this.#data = data; } /** @@ -24,27 +27,27 @@ export default class Packet { * The first byte in the packet is the length of the complete packet. */ public get isComplete(): boolean { - const length = this.expectedLength - if (!length) return false - return this.dataBuffer.byteLength - 1 === length + const length = this.expectedLength; + if (!length) return false; + return this.dataBuffer.byteLength - 1 === length; } public get expectedLength(): number { - return Packet.parseVarInt(Buffer.from(this.#data)) + return Packet.parseVarInt(Buffer.from(this.#data)); } /** * Get packet data */ public get data(): number[] { - return this.#data + return this.#data; } /** * Get packet data */ public get dataBuffer(): Buffer { - return Buffer.from(this.#data) + return Buffer.from(this.#data); } /** @@ -53,15 +56,15 @@ export default class Packet { * @returns whether the packet is complete */ public push(data: number): boolean { - this.#data.push(data) - return this.isComplete + this.#data.push(data); + return this.isComplete; } /** * Parse packet */ public parse(): ParsedPacket { - return new ParsedPacket(this) + return new ParsedPacket(this); } /** @@ -69,21 +72,21 @@ export default class Packet { * @param buffer */ public static parseVarInt(buffer: Buffer): number { - let result = 0 - let shift = 0 - let index = 0 + let result = 0; + let shift = 0; + let index = 0; while (true) { - const byte = buffer[index++]! - result |= (byte & 0x7f) << shift - shift += 7 + const byte = buffer[index++]!; + result |= (byte & 0x7f) << shift; + shift += 7; if ((byte & 0x80) === 0) { - break + break; } } - return result + return result; } /** @@ -91,25 +94,25 @@ export default class Packet { * @param value */ public static writeVarInt(value: number): Buffer { - const buffer = Buffer.alloc(5) - let index = 0 + const buffer = Buffer.alloc(5); + let index = 0; while (true) { - let byte = value & 0x7f - value >>>= 7 + let byte = value & 0x7f; + value >>>= 7; if (value !== 0) { - byte |= 0x80 + byte |= 0x80; } - buffer[index++] = byte + buffer[index++] = byte; if (value === 0) { - break + break; } } - return buffer.subarray(0, index) + return buffer.subarray(0, index); } /** @@ -117,12 +120,12 @@ export default class Packet { * @param buffer */ public static parseString(buffer: Buffer): string { - const length = Packet.parseVarInt(buffer) + const length = Packet.parseVarInt(buffer); buffer = buffer.subarray( Packet.writeVarInt(length).length, Packet.writeVarInt(length).length + length - ) - return buffer.toString() + ); + return buffer.toString(); } /** @@ -130,8 +133,8 @@ export default class Packet { * @param value */ public static writeString(value: string): Buffer { - const length = Buffer.byteLength(value) - return Buffer.concat([Packet.writeVarInt(length), Buffer.from(value)]) + const length = Buffer.byteLength(value); + return Buffer.concat([Packet.writeVarInt(length), Buffer.from(value)]); } /** @@ -139,7 +142,7 @@ export default class Packet { * @param buffer */ public static parseBoolean(buffer: Buffer): boolean { - return !!buffer.readUInt8(0) + return !!buffer.readUInt8(0); } /** @@ -147,7 +150,7 @@ export default class Packet { * @param value */ public static writeBoolean(value: boolean): Buffer { - return Buffer.from([value ? 1 : 0]) + return Buffer.from([value ? 1 : 0]); } /** @@ -155,7 +158,7 @@ export default class Packet { * @param buffer */ public static parseUUID(buffer: Buffer): string { - return buffer.toString("hex", 0, 16) + return buffer.toString("hex", 0, 16); } /** @@ -163,7 +166,7 @@ export default class Packet { * @param value */ public static writeUUID(value: string): Buffer { - return Buffer.from(value, "hex") + return Buffer.from(value, "hex"); } /** @@ -171,7 +174,7 @@ export default class Packet { * @param buffer */ public static parseUShort(buffer: Buffer): number { - return buffer.readUInt16BE(0) + return buffer.readUInt16BE(0); } /** @@ -179,9 +182,9 @@ export default class Packet { * @param value */ public static writeUShort(value: number): Buffer { - const buffer = Buffer.alloc(2) - buffer.writeUInt16BE(value) - return buffer + const buffer = Buffer.alloc(2); + buffer.writeUInt16BE(value); + return buffer; } /** @@ -189,7 +192,7 @@ export default class Packet { * @param buffer */ public static parseULong(buffer: Buffer): bigint { - return buffer.readBigUint64BE(0) + return buffer.readBigUint64BE(0); } /** @@ -197,9 +200,9 @@ export default class Packet { * @param value */ public static writeULong(value: bigint): Buffer { - const buffer = Buffer.alloc(8) - buffer.writeBigUint64BE(value) - return buffer + const buffer = Buffer.alloc(8); + buffer.writeBigUint64BE(value); + return buffer; } /** @@ -207,7 +210,7 @@ export default class Packet { * @param buffer */ public static parseLong(buffer: Buffer): bigint { - return buffer.readBigInt64BE(0) + return buffer.readBigInt64BE(0); } /** @@ -215,9 +218,9 @@ export default class Packet { * @param value */ public static writeLong(value: bigint): Buffer { - const buffer = Buffer.alloc(8) - buffer.writeBigInt64BE(value) - return buffer + const buffer = Buffer.alloc(8); + buffer.writeBigInt64BE(value); + return buffer; } /** @@ -225,7 +228,7 @@ export default class Packet { * @param buffer */ public static parseChat(buffer: Buffer): ChatComponent { - return JSON.parse(Packet.parseString(buffer)) as ChatComponent + return JSON.parse(Packet.parseString(buffer)) as ChatComponent; } /** @@ -233,7 +236,7 @@ export default class Packet { * @param value */ public static writeChat(value: ChatComponent): Buffer { - return Packet.writeString(JSON.stringify(value)) + return Packet.writeString(JSON.stringify(value)); } /** @@ -241,10 +244,10 @@ export default class Packet { */ public getTypedClient(conn: Connection): TypedClientPacket | null { for (const type of Packet.clientTypes) { - const p = type.isThisPacket(this.parse(), conn) - if (p !== null) return p + const p = type.isThisPacket(this.parse(), conn); + if (p !== null) return p; } - return null + return null; } /** @@ -256,7 +259,7 @@ export default class Packet { LoginAckPacket, LoginPacket, PingPacket, - ] + ]; /** * Split buffer @@ -264,15 +267,15 @@ export default class Packet { * @param splitByte */ public static split(buffer: Buffer, splitByte: number): Buffer[] { - const buffers: Buffer[] = [] - let lastPosition = 0 + const buffers: Buffer[] = []; + let lastPosition = 0; for (let i = 0; i < buffer.length; i++) { if (buffer[i] === splitByte) { - buffers.push(buffer.subarray(lastPosition, i)) - lastPosition = i + 1 + buffers.push(buffer.subarray(lastPosition, i)); + lastPosition = i + 1; } } - buffers.push(buffer.subarray(lastPosition)) - return buffers + buffers.push(buffer.subarray(lastPosition)); + return buffers; } } diff --git a/src/ParsedPacket.ts b/src/ParsedPacket.ts index 002ad9b..648c20b 100644 --- a/src/ParsedPacket.ts +++ b/src/ParsedPacket.ts @@ -1,20 +1,20 @@ -import Packet from "./Packet.js" +import Packet from "./Packet.js"; export default class ParsedPacket { - public readonly packet: Packet - private readonly packetData: number[] + public readonly packet: Packet; + private readonly packetData: number[]; private get packetBuffer(): Buffer { - return Buffer.from(this.packetData) + return Buffer.from(this.packetData); } - public readonly length - public readonly id + public readonly length; + public readonly id; constructor(packet: Packet) { - this.packet = packet - this.packetData = [...packet.data] - this.length = this.getVarInt() - this.id = this.getVarInt() + this.packet = packet; + this.packetData = [...packet.data]; + this.length = this.getVarInt(); + this.id = this.getVarInt(); } /** @@ -22,7 +22,7 @@ export default class ParsedPacket { * @param index */ private isOutOfRange(index: number): boolean { - return index >= this.packetBuffer.byteLength + return index >= this.packetBuffer.byteLength; } /** @@ -32,10 +32,10 @@ export default class ParsedPacket { * @param [index=0] Index in the packet */ public getVarInt(index = 0): number | null { - if (this.isOutOfRange(index)) return null - const result = Packet.parseVarInt(this.packetBuffer.subarray(index)) - this.packetData.splice(index, Packet.writeVarInt(result).byteLength) - return result + if (this.isOutOfRange(index)) return null; + const result = Packet.parseVarInt(this.packetBuffer.subarray(index)); + this.packetData.splice(index, Packet.writeVarInt(result).byteLength); + return result; } /** @@ -45,17 +45,17 @@ export default class ParsedPacket { * @param [index=0] Index in the packet */ public getString(index = 0): string | null { - if (this.isOutOfRange(index)) return null - const length = this.getVarInt(index) - if (length === null) return null - const offset = index + Packet.writeVarInt(length).byteLength - 1 + if (this.isOutOfRange(index)) return null; + const length = this.getVarInt(index); + if (length === null) return null; + const offset = index + Packet.writeVarInt(length).byteLength - 1; if (this.isOutOfRange(offset) || this.isOutOfRange(offset + length)) - return null + return null; const result = this.packetBuffer .subarray(offset, offset + length) - .toString() - this.packetData.splice(index, offset + length - index) - return result + .toString(); + this.packetData.splice(index, offset + length - index); + return result; } /** @@ -65,10 +65,10 @@ export default class ParsedPacket { * @param [index=0] Index in the packet */ public getBoolean(index = 0): boolean | null { - if (this.isOutOfRange(index)) return null - const result = Packet.parseBoolean(this.packetBuffer.subarray(index)) - this.packetData.splice(index, 1) - return result + if (this.isOutOfRange(index)) return null; + const result = Packet.parseBoolean(this.packetBuffer.subarray(index)); + this.packetData.splice(index, 1); + return result; } /** @@ -78,10 +78,10 @@ export default class ParsedPacket { * @param [index=0] Index in the packet */ public getUUID(index = 0): string | null { - if (this.isOutOfRange(index)) return null - const result = Packet.parseUUID(this.packetBuffer.subarray(index)) - this.packetData.splice(index, 16) - return result + if (this.isOutOfRange(index)) return null; + const result = Packet.parseUUID(this.packetBuffer.subarray(index)); + this.packetData.splice(index, 16); + return result; } /** @@ -91,10 +91,10 @@ export default class ParsedPacket { * @param [index=0] Index in the packet */ public getUShort(index = 0): number | null { - if (this.isOutOfRange(index)) return null - const result = Packet.parseUShort(this.packetBuffer.subarray(index)) - this.packetData.splice(index, 2) - return result + if (this.isOutOfRange(index)) return null; + const result = Packet.parseUShort(this.packetBuffer.subarray(index)); + this.packetData.splice(index, 2); + return result; } /** @@ -104,9 +104,9 @@ export default class ParsedPacket { * @param [index=0] Index in the packet */ public getLong(index = 0): bigint | null { - if (this.isOutOfRange(index)) return null - const result = Packet.parseLong(this.packetBuffer.subarray(index)) - this.packetData.splice(index, 8) - return result + if (this.isOutOfRange(index)) return null; + const result = Packet.parseLong(this.packetBuffer.subarray(index)); + this.packetData.splice(index, 8); + return result; } } diff --git a/src/Scheduler.ts b/src/Scheduler.ts index 6fcadd6..8e628d4 100644 --- a/src/Scheduler.ts +++ b/src/Scheduler.ts @@ -1,48 +1,48 @@ -import EventEmitter from "node:events" -import { randomUUID } from "node:crypto" -import TypedEventEmitter from "./types/TypedEventEmitter" +import EventEmitter from "node:events"; +import { randomUUID } from "node:crypto"; +import TypedEventEmitter from "./types/TypedEventEmitter"; type SchedulerEvents = { /** * Scheduler is paused */ - paused: () => void + paused: () => void; /** * Scheduler is started/resumed */ - started: () => void + started: () => void; /** * Scheduler terminated */ - terminating: () => void -} + terminating: () => void; +}; class Scheduler extends (EventEmitter as new () => TypedEventEmitter) { /** * Scheduler age (in ticks) */ - #age: number = 0 + #age: number = 0; /** * Whether the scheduler is running */ - #running: boolean = false + #running: boolean = false; /** * Scheduler tasks */ - readonly #tasks: Scheduler.Task[] = [] + readonly #tasks: Scheduler.Task[] = []; #schedulerStopResolve: ((value: true | PromiseLike) => void) | null = - null - #schedulerStopPromise: Promise | null = null + null; + #schedulerStopPromise: Promise | null = null; /** * Time of last tick */ - private lastTick: Date = new Date() + private lastTick: Date = new Date(); /** * Create scheduler @@ -54,20 +54,20 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter( (r) => (this.#schedulerStopResolve = r) - ) - this._nextTick() - this.emit("started") + ); + this._nextTick(); + this.emit("started"); } /** @@ -75,10 +75,10 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter { - if (!this.#running) return Promise.resolve(false) - this.#running = false - this.emit("paused") - return this.#schedulerStopPromise! + if (!this.#running) return Promise.resolve(false); + this.#running = false; + this.emit("paused"); + return this.#schedulerStopPromise!; } /** @@ -86,30 +86,30 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter { - if (!this.#running) return Promise.resolve(false) - this.#running = false + if (!this.#running) return Promise.resolve(false); + this.#running = false; while (this.#tasks.length > 0) { - const task = this.#tasks.pop()! - task.emit("notPlanned") - this.delete(task) - task.removeAllListeners() + const task = this.#tasks.pop()!; + task.emit("notPlanned"); + this.delete(task); + task.removeAllListeners(); } - this.emit("terminating") - return this.#schedulerStopPromise! + this.emit("terminating"); + return this.#schedulerStopPromise!; } /** * Scheduler age */ public get age(): number { - return this.#age + return this.#age; } /** * Whether the scheduler is running */ public get running(): boolean { - return this.#running + return this.#running; } /** @@ -118,7 +118,7 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter TypedEventEmitter task.targetAge <= this.#age) - .sort((a, b) => a.targetAge - b.targetAge) + .sort((a, b) => a.targetAge - b.targetAge); for (const task of tasks) { - this.delete(task) - task.run() + this.delete(task); + task.run(); } - this._nextTick() + this._nextTick(); } /** @@ -171,12 +171,12 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter void, targetAge: number): Scheduler.Task { - const task = new Scheduler.Task(code, targetAge, this) - this.#tasks.push(task) - return task + const task = new Scheduler.Task(code, targetAge, this); + this.#tasks.push(task); + return task; } /** @@ -198,7 +198,7 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter void, ticks: number): Scheduler.Task { - return this.scheduleAge(code, this.age + ticks) + return this.scheduleAge(code, this.age + ticks); } /** @@ -206,18 +206,18 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter void): Scheduler.Task + public schedule(code: () => void): Scheduler.Task; /** * Schedule a task * * @param task The task */ - public schedule(task: Scheduler.Task): Scheduler.Task + public schedule(task: Scheduler.Task): Scheduler.Task; public schedule(a: (() => void) | Scheduler.Task): Scheduler.Task { if (a instanceof Scheduler.Task) { - this.#tasks.push(a) - return a - } else return this.scheduleTicks(a, 0) + this.#tasks.push(a); + return a; + } else return this.scheduleTicks(a, 0); } /** @@ -227,10 +227,10 @@ class Scheduler extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter TypedEventEmitter task.id === id) + return this.#tasks.find((task) => task.id === id); } } @@ -260,13 +260,13 @@ namespace Scheduler { /** * Task is not planned to be executed due to the scheduler being terminated */ - notPlanned: () => void + notPlanned: () => void; /** * Task is cancelled */ - cancelled: () => void - } + cancelled: () => void; + }; /** * Scheduler task @@ -275,27 +275,27 @@ namespace Scheduler { /** * Task ID */ - public readonly id = randomUUID() + public readonly id = randomUUID(); /** * Task code */ - private readonly code: () => void + private readonly code: () => void; /** * Target scheduler age (tick) to run task at */ - public readonly targetAge: number + public readonly targetAge: number; /** * Task scheduler */ - public readonly scheduler: Scheduler + public readonly scheduler: Scheduler; /** * Whether the task has been executed */ - #executed: boolean = false + #executed: boolean = false; /** * Create scheduler task @@ -309,17 +309,17 @@ namespace Scheduler { targetAge: number, scheduler: Scheduler ) { - super() - this.code = code - this.targetAge = targetAge - this.scheduler = scheduler + super(); + this.code = code; + this.targetAge = targetAge; + this.scheduler = scheduler; } /** * Whether the task has been executed */ public get executed(): boolean { - return this.#executed + return this.#executed; } /** @@ -332,7 +332,7 @@ namespace Scheduler { * To check if the task was actually run, use {@link Task#executed} */ public get remainingTicks(): number { - return this.targetAge - this.scheduler.age + return this.targetAge - this.scheduler.age; } /** @@ -340,7 +340,7 @@ namespace Scheduler { * @see Scheduler#cancel */ public cancel(): boolean { - return this.scheduler.cancel(this) + return this.scheduler.cancel(this); } /** @@ -348,9 +348,9 @@ namespace Scheduler { * @internal */ public run(): void { - this.code() - this.#executed = true - this.removeAllListeners() + this.code(); + this.#executed = true; + this.removeAllListeners(); } } @@ -358,8 +358,8 @@ namespace Scheduler { /** * All repeats have been executed */ - completed: () => void - } + completed: () => void; + }; /** * A repeating task @@ -370,42 +370,42 @@ namespace Scheduler { /** * Number of times the task will repeat. This may be `Infinity`, in which case the tasks repeats until the scheduler is terminated. */ - public readonly repeats: number + public readonly repeats: number; /** * Interval between each repeat */ - public readonly interval: number + public readonly interval: number; /** * Target scheduler age (tick) for first execution */ - public readonly targetAge: number + public readonly targetAge: number; /** * Task scheduler */ - public readonly scheduler: Scheduler + public readonly scheduler: Scheduler; /** * Task code */ - private readonly code: () => void + private readonly code: () => void; /** * Current task */ - #task: Task | null = null + #task: Task | null = null; /** * Number of tasks that have been executed */ - #executed: number = 0 + #executed: number = 0; /** * Whether this repeating task has been cancelled */ - #cancelled: boolean = false + #cancelled: boolean = false; /** * Create repeating task @@ -423,28 +423,28 @@ namespace Scheduler { repeats: number = Infinity, targetAge: number = 0 ) { - super() - this.code = code - this.interval = interval - this.scheduler = scheduler - this.repeats = repeats - this.targetAge = targetAge - if (this.repeats > 0) this.createTask() + super(); + this.code = code; + this.interval = interval; + this.scheduler = scheduler; + this.repeats = repeats; + this.targetAge = targetAge; + if (this.repeats > 0) this.createTask(); this.scheduler.on("terminating", () => { //console.log(this.task?.executed, this.task); - if (this.task?.executed) this.emit("notPlanned") - }) + if (this.task?.executed) this.emit("notPlanned"); + }); } /** * Cancel this repeating task */ public cancel(): void { - if (this.#cancelled) return - this.#cancelled = true - if (this.#task) this.#task.cancel() - else this.emit("cancelled") + if (this.#cancelled) return; + this.#cancelled = true; + if (this.#task) this.#task.cancel(); + else this.emit("cancelled"); } /** @@ -455,41 +455,41 @@ namespace Scheduler { this.#task = this.scheduler.scheduleAge( () => this.taskCode(), this.targetAge - ) + ); else this.#task = this.scheduler.scheduleAge( () => this.taskCode(), this.scheduler.age + this.interval - ) - this.#task.once("cancelled", () => this.emit("cancelled")) - this.#task.once("notPlanned", () => this.emit("notPlanned")) + ); + this.#task.once("cancelled", () => this.emit("cancelled")); + this.#task.once("notPlanned", () => this.emit("notPlanned")); } /** * Scheduled task code */ private taskCode(): void { - this.code() - ++this.#executed + this.code(); + ++this.#executed; if (this.#executed < this.repeats) { - if (!this.#cancelled) this.createTask() - } else this.emit("completed") + if (!this.#cancelled) this.createTask(); + } else this.emit("completed"); } /** * Get current task */ public get task(): Task | null { - return this.#task + return this.#task; } /** * Number of times the task has been executed */ public get executed(): number { - return this.#executed + return this.#executed; } } } -export default Scheduler +export default Scheduler; diff --git a/src/Server.ts b/src/Server.ts index c6d1304..998e82d 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -1,58 +1,58 @@ -import * as net from "node:net" -import EventEmitter from "node:events" -import path from "node:path" -import Packet from "./Packet.js" -import Logger from "./Logger.js" -import { TypedClientPacket } from "./types/TypedPacket" -import TypedEventEmitter from "./types/TypedEventEmitter" -import ConnectionPool from "./ConnectionPool.js" -import Connection from "./Connection.js" -import HandshakePacket from "./packet/client/HandshakePacket" -import LoginPacket from "./packet/client/LoginPacket" -import { Config } from "./Config.js" -import Scheduler from "./Scheduler.js" -import { readFile } from "node:fs/promises" -import PingPacket from "./packet/client/PingPacket.js" -import StatusRequestPacket from "./packet/client/StatusRequestPacket.js" -import LoginAckPacket from "./packet/client/LoginAckPacket.js" +import * as net from "node:net"; +import EventEmitter from "node:events"; +import path from "node:path"; +import Packet from "./Packet.js"; +import Logger from "./Logger.js"; +import { TypedClientPacket } from "./types/TypedPacket"; +import TypedEventEmitter from "./types/TypedEventEmitter"; +import ConnectionPool from "./ConnectionPool.js"; +import Connection from "./Connection.js"; +import HandshakePacket from "./packet/client/HandshakePacket"; +import LoginPacket from "./packet/client/LoginPacket"; +import { Config } from "./Config.js"; +import Scheduler from "./Scheduler.js"; +import { readFile } from "node:fs/promises"; +import PingPacket from "./packet/client/PingPacket.js"; +import StatusRequestPacket from "./packet/client/StatusRequestPacket.js"; +import LoginAckPacket from "./packet/client/LoginAckPacket.js"; type ServerEvents = { /** * Server is ready to accept connections * @param port Port the server is listening on */ - listening: (port: Number) => void + listening: (port: Number) => void; /** * Unknown packet received * @param packet Packet that was received * @param connection Connection the packet was received from */ - unknownPacket: (packet: Packet, connection: Connection) => void + unknownPacket: (packet: Packet, connection: Connection) => void; /** * Known packet received * @param packet Packet that was received * @param connection Connection the packet was received from */ - packet: (packet: TypedClientPacket, connection: Connection) => void + packet: (packet: TypedClientPacket, connection: Connection) => void; /** * New connection established * @param connection Connection that was established */ - connection: (connection: Connection) => void + connection: (connection: Connection) => void; /** * Server closed */ - closed: () => void + closed: () => void; /** * Connection closed * @param connection Connection that was closed */ - disconnect: (connection: Connection) => void + disconnect: (connection: Connection) => void; /** * Handshake packet received @@ -62,14 +62,14 @@ type ServerEvents = { "packet.HandshakePacket": ( packet: HandshakePacket, connection: Connection - ) => void + ) => void; /** * Login packet received * @param packet Packet that was received * @param connection Connection the packet was received from */ - "packet.LoginPacket": (packet: LoginPacket, connection: Connection) => void + "packet.LoginPacket": (packet: LoginPacket, connection: Connection) => void; /** * Status request packet received @@ -79,92 +79,92 @@ type ServerEvents = { "packet.StatusRequestPacket": ( packet: StatusRequestPacket, connection: Connection - ) => void + ) => void; /** * Ping packet received * @param packet Packet that was received * @param connection Connection the packet was received from */ - "packet.PingPacket": (packet: PingPacket, connection: Connection) => void + "packet.PingPacket": (packet: PingPacket, connection: Connection) => void; /** * Login acknowledge packet * @param packet Packet that was received * @param connection Connection the packet was received from */ - "packet.LoginAck": (packet: LoginAckPacket, connection: Connection) => void -} + "packet.LoginAck": (packet: LoginAckPacket, connection: Connection) => void; +}; export default class Server extends (EventEmitter as new () => TypedEventEmitter) { - private readonly server = net.createServer() - public readonly logger: Logger - public readonly scheduler: Scheduler = new Scheduler(20) - public readonly connections: ConnectionPool = new ConnectionPool() + private readonly server = net.createServer(); + public readonly logger: Logger; + public readonly scheduler: Scheduler = new Scheduler(20); + public readonly connections: ConnectionPool = new ConnectionPool(); public static readonly path: string = path.dirname( path.join(new URL(import.meta.url).pathname, "..") - ) - public readonly config: Config + ); + public readonly config: Config; - public favicon: string = "data:image/png;base64," + public favicon: string = "data:image/png;base64,"; public constructor(config: Config) { - super() - this.config = Object.freeze(config) - this.logger = new Logger("Server", this.config.logLevel) + super(); + this.config = Object.freeze(config); + this.logger = new Logger("Server", this.config.logLevel); } public async start() { // add a favicon if such is specified if (this.config.server.favicon) { - const data = await readFile(this.config.server.favicon) - this.favicon += Buffer.from(data).toString("base64") + const data = await readFile(this.config.server.favicon); + this.favicon += Buffer.from(data).toString("base64"); } this.scheduler.on("started", () => this.logger.debug( "Scheduler started, freq=" + this.scheduler.frequency + "Hz" ) - ) + ); this.scheduler.on("paused", () => this.logger.debug("Scheduler paused, age=" + this.scheduler.age) - ) + ); this.scheduler.on("terminating", () => this.logger.debug("Scheduler terminated, age=" + this.scheduler.age) - ) - this.scheduler.start() + ); + this.scheduler.start(); this.server.listen(this.config.port, () => this.emit("listening", this.config.port) - ) - this.server.on("connection", this.onConnection.bind(this)) + ); + this.server.on("connection", this.onConnection.bind(this)); } public async stop(): Promise { - this.logger.debug("Closing server...") + this.logger.debug("Closing server..."); await Promise.all([ new Promise((resolve, reject) => { this.server.close((err) => { - if (err) reject(err) - else resolve(void 0) - }) + if (err) reject(err); + else resolve(void 0); + }); }), this.connections.disconnectAll(this.config.shutdownKickReason), - ]) - await this.scheduler.stop() - this.emit("closed") + ]); + await this.scheduler.stop(); + this.emit("closed"); } public get isRunning(): boolean { - return this.server.listening + return this.server.listening; } private onConnection(socket: net.Socket) { - const conn = new Connection(socket, this) - this.connections.add(conn) - this.emit("connection", conn) + const conn = new Connection(socket, this); + this.connections.add(conn); + this.emit("connection", conn); socket.on("data", (data) => { - for (const byte of data) conn.incomingPacketFragment(byte) - }) + for (const byte of data) conn.incomingPacketFragment(byte); + }); } } diff --git a/src/ServerPacket.ts b/src/ServerPacket.ts index 1dc23bd..45229e8 100644 --- a/src/ServerPacket.ts +++ b/src/ServerPacket.ts @@ -1,9 +1,9 @@ -import Packet from "./Packet.js" -import Connection from "./Connection" +import Packet from "./Packet.js"; +import Connection from "./Connection"; export default abstract class ServerPacket extends Packet { protected constructor(data: Buffer) { - super([...Buffer.concat([Packet.writeVarInt(data.byteLength), data])]) + super([...Buffer.concat([Packet.writeVarInt(data.byteLength), data])]); } /** @@ -13,9 +13,9 @@ export default abstract class ServerPacket extends Packet { public send(connection: Connection): Promise { return new Promise((resolve, reject) => { connection.socket.write(this.dataBuffer, (err) => { - if (err) reject(err) - else resolve() - }) - }) + if (err) reject(err); + else resolve(); + }); + }); } } diff --git a/src/decorator/StaticImplements.ts b/src/decorator/StaticImplements.ts index 200b539..43f8332 100644 --- a/src/decorator/StaticImplements.ts +++ b/src/decorator/StaticImplements.ts @@ -1,3 +1,3 @@ export default function StaticImplements() { - return (constructor: U) => constructor + return (constructor: U) => constructor; } diff --git a/src/nbt.ts b/src/nbt.ts index c7a1da9..f8aa5ce 100644 --- a/src/nbt.ts +++ b/src/nbt.ts @@ -11,16 +11,16 @@ required by law. */ -"use strict" +"use strict"; if (typeof ArrayBuffer === "undefined") { - throw new Error("Missing required type ArrayBuffer") + throw new Error("Missing required type ArrayBuffer"); } if (typeof DataView === "undefined") { - throw new Error("Missing required type DataView") + throw new Error("Missing required type DataView"); } if (typeof Uint8Array === "undefined") { - throw new Error("Missing required type Uint8Array") + throw new Error("Missing required type Uint8Array"); } const nbt: { tagTypes: NbtTagTypes } = { @@ -39,7 +39,7 @@ const nbt: { tagTypes: NbtTagTypes } = { intArray: 11, longArray: 12, }, -} +}; export const tagTypes = { end: 0, @@ -55,7 +55,7 @@ export const tagTypes = { compound: 10, intArray: 11, longArray: 12, -} +}; /** * A mapping from type names to NBT type numbers. @@ -65,63 +65,63 @@ export const tagTypes = { * * @type Object * @see module:nbt.tagTypeNames */ -nbt.tagTypes = tagTypes +nbt.tagTypes = tagTypes; /** * A mapping from NBT type numbers to type names. * * @type Object * @see module:nbt.tagTypes */ -nbt.tagTypeNames = {} -;(function () { +nbt.tagTypeNames = {}; +(function () { for (let typeName in nbt.tagTypes) { if (nbt.tagTypes.hasOwnProperty(typeName)) { - nbt.tagTypeNames[nbt.tagTypes[typeName]] = typeName + nbt.tagTypeNames[nbt.tagTypes[typeName]] = typeName; } } -})() +})(); function hasGzipHeader(data) { - const head = new Uint8Array(data.slice(0, 2)) - return head.length === 2 && head[0] === 0x1f && head[1] === 0x8b + const head = new Uint8Array(data.slice(0, 2)); + return head.length === 2 && head[0] === 0x1f && head[1] === 0x8b; } function encodeUTF8(str) { - const array = [] - let i, c + const array = []; + let i, c; for (i = 0; i < str.length; i++) { - c = str.charCodeAt(i) + c = str.charCodeAt(i); if (c < 0x80) { - array.push(c) + array.push(c); } else if (c < 0x800) { - array.push(0xc0 | (c >> 6)) - array.push(0x80 | (c & 0x3f)) + array.push(0xc0 | (c >> 6)); + array.push(0x80 | (c & 0x3f)); } else if (c < 0x10000) { - array.push(0xe0 | (c >> 12)) - array.push(0x80 | ((c >> 6) & 0x3f)) - array.push(0x80 | (c & 0x3f)) + array.push(0xe0 | (c >> 12)); + array.push(0x80 | ((c >> 6) & 0x3f)); + array.push(0x80 | (c & 0x3f)); } else { - array.push(0xf0 | ((c >> 18) & 0x07)) - array.push(0x80 | ((c >> 12) & 0x3f)) - array.push(0x80 | ((c >> 6) & 0x3f)) - array.push(0x80 | (c & 0x3f)) + array.push(0xf0 | ((c >> 18) & 0x07)); + array.push(0x80 | ((c >> 12) & 0x3f)); + array.push(0x80 | ((c >> 6) & 0x3f)); + array.push(0x80 | (c & 0x3f)); } } - return array + return array; } function decodeUTF8(array) { let codepoints = [], - i + i; for (i = 0; i < array.length; i++) { if ((array[i] & 0x80) === 0) { - codepoints.push(array[i] & 0x7f) + codepoints.push(array[i] & 0x7f); } else if ( i + 1 < array.length && (array[i] & 0xe0) === 0xc0 && (array[i + 1] & 0xc0) === 0x80 ) { - codepoints.push(((array[i] & 0x1f) << 6) | (array[i + 1] & 0x3f)) + codepoints.push(((array[i] & 0x1f) << 6) | (array[i + 1] & 0x3f)); } else if ( i + 2 < array.length && (array[i] & 0xf0) === 0xe0 && @@ -132,7 +132,7 @@ function decodeUTF8(array) { ((array[i] & 0x0f) << 12) | ((array[i + 1] & 0x3f) << 6) | (array[i + 2] & 0x3f) - ) + ); } else if ( i + 3 < array.length && (array[i] & 0xf8) === 0xf0 && @@ -145,19 +145,19 @@ function decodeUTF8(array) { ((array[i + 1] & 0x3f) << 12) | ((array[i + 2] & 0x3f) << 6) | (array[i + 3] & 0x3f) - ) + ); } } - return String.fromCharCode.apply(null, codepoints) + return String.fromCharCode.apply(null, codepoints); } /* Not all environments, in particular PhantomJS, supply Uint8Array.slice() */ function sliceUint8Array(array, begin, end) { if ("slice" in array) { - return array.slice(begin, end) + return array.slice(begin, end); } else { - return new Uint8Array([].slice.call(array, begin, end)) + return new Uint8Array([].slice.call(array, begin, end)); } } @@ -183,14 +183,14 @@ function sliceUint8Array(array, begin, end) { * * return writer.buffer; */ nbt.Writer = function () { - const self = this + const self = this; /* Will be resized (x2) on write if necessary. */ - let buffer = new ArrayBuffer(1024) + let buffer = new ArrayBuffer(1024); /* These are recreated when the buffer is */ - let dataView = new DataView(buffer) - let arrayView = new Uint8Array(buffer) + let dataView = new DataView(buffer); + let arrayView = new Uint8Array(buffer); /** * The location in the buffer where bytes are written or read. @@ -198,41 +198,41 @@ nbt.Writer = function () { * The buffer will be resized when necessary. * * @type number */ - this.offset = 0 + this.offset = 0; // Ensures that the buffer is large enough to write `size` bytes // at the current `self.offset`. function accommodate(size) { - const requiredLength = self.offset + size + const requiredLength = self.offset + size; if (buffer.byteLength >= requiredLength) { - return + return; } - let newLength = buffer.byteLength + let newLength = buffer.byteLength; while (newLength < requiredLength) { - newLength *= 2 + newLength *= 2; } - const newBuffer = new ArrayBuffer(newLength) - const newArrayView = new Uint8Array(newBuffer) - newArrayView.set(arrayView) + const newBuffer = new ArrayBuffer(newLength); + const newArrayView = new Uint8Array(newBuffer); + newArrayView.set(arrayView); // If there's a gap between the end of the old buffer // and the start of the new one, we need to zero it out if (self.offset > buffer.byteLength) { - newArrayView.fill(0, buffer.byteLength, self.offset) + newArrayView.fill(0, buffer.byteLength, self.offset); } - buffer = newBuffer - dataView = new DataView(newBuffer) - arrayView = newArrayView + buffer = newBuffer; + dataView = new DataView(newBuffer); + arrayView = newArrayView; } function write(dataType, size, value) { - accommodate(size) - dataView["set" + dataType](self.offset, value) - self.offset += size - return self + accommodate(size); + dataView["set" + dataType](self.offset, value); + self.offset += size; + return self; } /** @@ -241,27 +241,27 @@ nbt.Writer = function () { * * @returns {ArrayBuffer} a [0, offset] slice of the internal buffer */ this.getData = function () { - accommodate(0) /* make sure the offset is inside the buffer */ - return buffer.slice(0, self.offset) - } + accommodate(0); /* make sure the offset is inside the buffer */ + return buffer.slice(0, self.offset); + }; /** * @method module:nbt.Writer#byte * @param {number} value - a signed byte * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.byte] = write.bind(null, "Int8", 1) + this[nbt.tagTypes.byte] = write.bind(null, "Int8", 1); /** * @method module:nbt.Writer#short * @param {number} value - a signed 16-bit integer * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.short] = write.bind(null, "Int16", 2) + this[nbt.tagTypes.short] = write.bind(null, "Int16", 2); /** * @method module:nbt.Writer#int * @param {number} value - a signed 32-bit integer * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.int] = write.bind(null, "Int32", 4) + this[nbt.tagTypes.int] = write.bind(null, "Int32", 4); /** * @method module:nbt.Writer#long @@ -270,51 +270,51 @@ nbt.Writer = function () { this[nbt.tagTypes.long] = function (value) { // Ensure value is a 64-bit BigInt if (typeof value !== "bigint") { - throw new Error("Value must be a BigInt") + throw new Error("Value must be a BigInt"); } - const hi = Number(value >> 32n) & 0xffffffff - const lo = Number(value & 0xffffffffn) - self[nbt.tagTypes.int](hi) - self[nbt.tagTypes.int](lo) - return self - } + const hi = Number(value >> 32n) & 0xffffffff; + const lo = Number(value & 0xffffffffn); + self[nbt.tagTypes.int](hi); + self[nbt.tagTypes.int](lo); + return self; + }; /** * @method module:nbt.Writer#float * @param {number} value - a 32-bit IEEE 754 floating point number * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.float] = write.bind(null, "Float32", 4) + this[nbt.tagTypes.float] = write.bind(null, "Float32", 4); /** * @method module:nbt.Writer#double * @param {number} value - a 64-bit IEEE 754 floating point number * @returns {module:nbt.Writer} itself */ - this[nbt.tagTypes.double] = write.bind(null, "Float64", 8) + this[nbt.tagTypes.double] = write.bind(null, "Float64", 8); /** * @method module:nbt.Writer#byteArray * @param {Uint8Array} value - an array of signed bytes * @returns {module:nbt.Writer} itself */ this[nbt.tagTypes.byteArray] = function (value) { - self[nbt.tagTypes.int](value.length) - accommodate(value.length) - arrayView.set(value, self.offset) - self.offset += value.length - return self - } + self[nbt.tagTypes.int](value.length); + accommodate(value.length); + arrayView.set(value, self.offset); + self.offset += value.length; + return self; + }; /** * @method module:nbt.Writer#string * @param {string} value - an unprefixed UTF-8 string * @returns {module:nbt.Writer} itself */ this[nbt.tagTypes.string] = function (value) { - const encoded = encodeUTF8(value) - self[nbt.tagTypes.short](encoded.length) - accommodate(encoded.length) - arrayView.set(encoded, self.offset) - self.offset += encoded.length - return self - } + const encoded = encodeUTF8(value); + self[nbt.tagTypes.short](encoded.length); + accommodate(encoded.length); + arrayView.set(encoded, self.offset); + self.offset += encoded.length; + return self; + }; /** * @method module:nbt.Writer#list @@ -322,13 +322,13 @@ nbt.Writer = function () { * @param {Array} value - an array of values of the given type * @returns {module:nbt.Writer} itself */ this[nbt.tagTypes.list] = function (type, value) { - self[nbt.tagTypes.byte](type) - self[nbt.tagTypes.int](value.length) + self[nbt.tagTypes.byte](type); + self[nbt.tagTypes.int](value.length); value.forEach(function (element) { - self[type](element) - }) - return self - } + self[type](element); + }); + return self; + }; /** * @method module:nbt.Writer#compound @@ -336,38 +336,38 @@ nbt.Writer = function () { * @returns {module:nbt.Writer} itself */ this[nbt.tagTypes.compound] = function (value) { Object.keys(value).forEach(function (key) { - const elementType = value[key].type - self[nbt.tagTypes.byte](elementType) - self[nbt.tagTypes.string](key) - self[elementType](value[key].value) - }) - self[nbt.tagTypes.byte](nbt.tagTypes.end) - return self - } + const elementType = value[key].type; + self[nbt.tagTypes.byte](elementType); + self[nbt.tagTypes.string](key); + self[elementType](value[key].value); + }); + self[nbt.tagTypes.byte](nbt.tagTypes.end); + return self; + }; /** * @method module:nbt.Writer#intArray * @param {Int32Array} value - an array of signed 32-bit integers * @returns {module:nbt.Writer} itself */ this[nbt.tagTypes.intArray] = function (value) { - self[nbt.tagTypes.int](value.length) + self[nbt.tagTypes.int](value.length); for (let i = 0; i < value.length; i++) { - self[nbt.tagTypes.int](value[i]) + self[nbt.tagTypes.int](value[i]); } - return self - } + return self; + }; /** * @method module:nbt.Writer#longArray * @param {BigInt64Array} value - an array of signed 64-bit integers * @returns {module:nbt.Writer} itself */ this[nbt.tagTypes.longArray] = function (value) { - self[nbt.tagTypes.int](value.length) + self[nbt.tagTypes.int](value.length); for (let i = 0; i < value.length; i++) { - self[nbt.tagTypes.long](value[i]) + self[nbt.tagTypes.long](value[i]); } - return self - } + return self; + }; // Alias the methods by NBT type number for (const type in nbt.tagTypes) { @@ -375,23 +375,23 @@ nbt.Writer = function () { nbt.tagTypes.hasOwnProperty(type) && typeof this[nbt.tagTypes[type]] === "function" ) { - this[type] = this[nbt.tagTypes[type]] + this[type] = this[nbt.tagTypes[type]]; } } -} +}; /** * @param {ArrayBuffer} data - the NBT data to read * @constructor * @see module:nbt.Writer */ nbt.Reader = function (data) { - const self = this + const self = this; - let buffer = data + let buffer = data; /* These are recreated when the buffer is */ - let dataView = new DataView(buffer) - let arrayView = new Uint8Array(buffer) + let dataView = new DataView(buffer); + let arrayView = new Uint8Array(buffer); /** * The location in the buffer where bytes are written or read. @@ -399,125 +399,125 @@ nbt.Reader = function (data) { * The buffer will be resized when necessary. * * @type number */ - this.offset = 0 + this.offset = 0; function read(dataType, size) { - const value = dataView["get" + dataType](self.offset) - self.offset += size - return value + const value = dataView["get" + dataType](self.offset); + self.offset += size; + return value; } /** * @method module:nbt.Reader#byte * @returns {number} a signed byte */ - this[nbt.tagTypes.byte] = read.bind(null, "Int8", 1) + this[nbt.tagTypes.byte] = read.bind(null, "Int8", 1); /** * @method module:nbt.Reader#short * @returns {number} a signed 16-bit integer */ - this[nbt.tagTypes.short] = read.bind(null, "Int16", 2) + this[nbt.tagTypes.short] = read.bind(null, "Int16", 2); /** * @method module:nbt.Reader#int * @returns {number} a signed 32-bit integer */ - this[nbt.tagTypes.int] = read.bind(null, "Int32", 4) + this[nbt.tagTypes.int] = read.bind(null, "Int32", 4); /** * @method module:nbt.Reader#long * @returns {bigint} a signed 64-bit integer */ this[nbt.tagTypes.long] = function () { - const hi = self[nbt.tagTypes.int]() - const lo = self[nbt.tagTypes.int]() - return (BigInt(hi) << 32n) | BigInt(lo) - } + const hi = self[nbt.tagTypes.int](); + const lo = self[nbt.tagTypes.int](); + return (BigInt(hi) << 32n) | BigInt(lo); + }; /** * @method module:nbt.Reader#float * @returns {number} a 32-bit IEEE 754 floating point number */ - this[nbt.tagTypes.float] = read.bind(null, "Float32", 4) + this[nbt.tagTypes.float] = read.bind(null, "Float32", 4); /** * @method module:nbt.Reader#double * @returns {number} a 64-bit IEEE 754 floating point number */ - this[nbt.tagTypes.double] = read.bind(null, "Float64", 8) + this[nbt.tagTypes.double] = read.bind(null, "Float64", 8); /** * @method module:nbt.Reader#byteArray * @returns {Uint8Array} an array of signed bytes */ this[nbt.tagTypes.byteArray] = function () { - const length = self[nbt.tagTypes.int]() + const length = self[nbt.tagTypes.int](); const value = sliceUint8Array( arrayView, self.offset, self.offset + length - ) - self.offset += length - return value - } + ); + self.offset += length; + return value; + }; /** * @method module:nbt.Reader#string * @returns {string} an unprefixed UTF-8 string */ this[nbt.tagTypes.string] = function () { - const length = self[nbt.tagTypes.short]() + const length = self[nbt.tagTypes.short](); const value = sliceUint8Array( arrayView, self.offset, self.offset + length - ) - self.offset += length - return decodeUTF8(value) - } + ); + self.offset += length; + return decodeUTF8(value); + }; /** * @method module:nbt.Reader#list * @returns {Array} an array of values of the given type */ this[nbt.tagTypes.list] = function () { - const type = self[nbt.tagTypes.byte]() - const length = self[nbt.tagTypes.int]() - const value = [] + const type = self[nbt.tagTypes.byte](); + const length = self[nbt.tagTypes.int](); + const value = []; for (let i = 0; i < length; i++) { - value.push(self[type]()) + value.push(self[type]()); } - return value - } + return value; + }; /** * @method module:nbt.Reader#compound * @returns {Object} an object of key-value pairs */ this[nbt.tagTypes.compound] = function () { - const value = {} - let type + const value = {}; + let type; while ((type = self[nbt.tagTypes.byte]()) !== nbt.tagTypes.end) { - const key = self[nbt.tagTypes.string]() - value[key] = { type, value: self[type]() } + const key = self[nbt.tagTypes.string](); + value[key] = { type, value: self[type]() }; } - return value - } + return value; + }; /** * @method module:nbt.Reader#intArray * @returns {Int32Array} an array of signed 32-bit integers */ this[nbt.tagTypes.intArray] = function () { - const length = self[nbt.tagTypes.int]() - const value = new Int32Array(length) + const length = self[nbt.tagTypes.int](); + const value = new Int32Array(length); for (let i = 0; i < length; i++) { - value[i] = self[nbt.tagTypes.int]() + value[i] = self[nbt.tagTypes.int](); } - return value - } + return value; + }; /** * @method module:nbt.Reader#longArray * @returns {BigInt64Array} an array of signed 64-bit integers */ this[nbt.tagTypes.longArray] = function () { - const length = self[nbt.tagTypes.int]() - const value = new BigInt64Array(length) + const length = self[nbt.tagTypes.int](); + const value = new BigInt64Array(length); for (let i = 0; i < length; i++) { - value[i] = self[nbt.tagTypes.long]() + value[i] = self[nbt.tagTypes.long](); } - return value - } + return value; + }; // Alias the methods by NBT type number for (const type in nbt.tagTypes) { @@ -525,244 +525,246 @@ nbt.Reader = function (data) { nbt.tagTypes.hasOwnProperty(type) && typeof this[nbt.tagTypes[type]] === "function" ) { - this[type] = this[nbt.tagTypes[type]] + this[type] = this[nbt.tagTypes[type]]; } } -} +}; -export default nbt +export default nbt; type NbtTagTypes = { - end: number - byte: number - short: number - int: number - long: number - float: number - double: number - byteArray: number - string: number - list: number - compound: number - intArray: number - longArray: number -} + end: number; + byte: number; + short: number; + int: number; + long: number; + float: number; + double: number; + byteArray: number; + string: number; + list: number; + compound: number; + intArray: number; + longArray: number; +}; class NbtWriter { - offset: number - buffer: ArrayBuffer - arrayView: Uint8Array - dataView: DataView + offset: number; + buffer: ArrayBuffer; + arrayView: Uint8Array; + dataView: DataView; constructor(data?: ArrayBuffer) { - this.offset = 0 - this.buffer = data || new ArrayBuffer(1024) - this.arrayView = new Uint8Array(this.buffer) - this.dataView = new DataView(this.buffer) + this.offset = 0; + this.buffer = data || new ArrayBuffer(1024); + this.arrayView = new Uint8Array(this.buffer); + this.dataView = new DataView(this.buffer); } accommodate(size: number): void { if (this.buffer.byteLength - this.offset < size) { - const newBuffer = new ArrayBuffer(this.buffer.byteLength * 2 + size) - new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)) - this.buffer = newBuffer - this.arrayView = new Uint8Array(this.buffer) - this.dataView = new DataView(this.buffer) + const newBuffer = new ArrayBuffer( + this.buffer.byteLength * 2 + size + ); + new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)); + this.buffer = newBuffer; + this.arrayView = new Uint8Array(this.buffer); + this.dataView = new DataView(this.buffer); } } write(dataType: string, size: number, value: number | bigint): void { - this.accommodate(size) - ;(this.dataView as any)[`set${dataType}`](this.offset, value) - this.offset += size + this.accommodate(size); + (this.dataView as any)[`set${dataType}`](this.offset, value); + this.offset += size; } byte(value: number): NbtWriter { - this.write("Int8", 1, value) - return this + this.write("Int8", 1, value); + return this; } short(value: number): NbtWriter { - this.write("Int16", 2, value) - return this + this.write("Int16", 2, value); + return this; } int(value: number): NbtWriter { - this.write("Int32", 4, value) - return this + this.write("Int32", 4, value); + return this; } long(value: bigint): NbtWriter { if (typeof value !== "bigint") { - throw new Error("Value must be a BigInt") + throw new Error("Value must be a BigInt"); } - const hi = Number(value >> 32n) & 0xffffffff - const lo = Number(value & 0xffffffffn) - this.int(hi) - this.int(lo) - return this + const hi = Number(value >> 32n) & 0xffffffff; + const lo = Number(value & 0xffffffffn); + this.int(hi); + this.int(lo); + return this; } float(value: number): NbtWriter { - this.write("Float32", 4, value) - return this + this.write("Float32", 4, value); + return this; } double(value: number): NbtWriter { - this.write("Float64", 8, value) - return this + this.write("Float64", 8, value); + return this; } byteArray(value: Uint8Array): NbtWriter { - this.int(value.length) - this.accommodate(value.length) - this.arrayView.set(value, this.offset) - this.offset += value.length - return this + this.int(value.length); + this.accommodate(value.length); + this.arrayView.set(value, this.offset); + this.offset += value.length; + return this; } string(value: string): NbtWriter { - const encoded = new TextEncoder().encode(value) - this.short(encoded.length) - this.accommodate(encoded.length) - this.arrayView.set(encoded, this.offset) - this.offset += encoded.length - return this + const encoded = new TextEncoder().encode(value); + this.short(encoded.length); + this.accommodate(encoded.length); + this.arrayView.set(encoded, this.offset); + this.offset += encoded.length; + return this; } list(type: number, value: any[]): NbtWriter { - this.byte(type) - this.int(value.length) + this.byte(type); + this.int(value.length); value.forEach((element) => { - ;(this as any)[type](element) - }) - return this + (this as any)[type](element); + }); + return this; } compound(value: Record): NbtWriter { Object.keys(value).forEach((key) => { - const elementType = value[key].type - this.byte(elementType) - this.string(key) - ;(this as any)[elementType](value[key].value) - }) - this.byte(nbt.tagTypes.end) - return this + const elementType = value[key].type; + this.byte(elementType); + this.string(key); + (this as any)[elementType](value[key].value); + }); + this.byte(nbt.tagTypes.end); + return this; } intArray(value: Int32Array): NbtWriter { - this.int(value.length) + this.int(value.length); for (let i = 0; i < value.length; i++) { - this.int(value[i]) + this.int(value[i]); } - return this + return this; } longArray(value: BigInt64Array): NbtWriter { - this.int(value.length) + this.int(value.length); for (let i = 0; i < value.length; i++) { - this.long(value[i]) + this.long(value[i]); } - return this + return this; } } class NbtReader { - offset: number - buffer: ArrayBuffer - dataView: DataView - arrayView: Uint8Array + offset: number; + buffer: ArrayBuffer; + dataView: DataView; + arrayView: Uint8Array; constructor(data: ArrayBuffer) { - this.offset = 0 - this.buffer = data - this.dataView = new DataView(data) - this.arrayView = new Uint8Array(data) + this.offset = 0; + this.buffer = data; + this.dataView = new DataView(data); + this.arrayView = new Uint8Array(data); } read(dataType: string, size: number): number | bigint { - const value = (this.dataView as any)[`get${dataType}`](this.offset) - this.offset += size - return value + const value = (this.dataView as any)[`get${dataType}`](this.offset); + this.offset += size; + return value; } byte(): number { - return this.read("Int8", 1) as number + return this.read("Int8", 1) as number; } short(): number { - return this.read("Int16", 2) as number + return this.read("Int16", 2) as number; } int(): number { - return this.read("Int32", 4) as number + return this.read("Int32", 4) as number; } long(): bigint { - const hi = this.int() - const lo = this.int() - return (BigInt(hi) << 32n) | BigInt(lo) + const hi = this.int(); + const lo = this.int(); + return (BigInt(hi) << 32n) | BigInt(lo); } float(): number { - return this.read("Float32", 4) as number + return this.read("Float32", 4) as number; } double(): number { - return this.read("Float64", 8) as number + return this.read("Float64", 8) as number; } byteArray(): Uint8Array { - const length = this.int() - const value = this.arrayView.slice(this.offset, this.offset + length) - this.offset += length - return value + const length = this.int(); + const value = this.arrayView.slice(this.offset, this.offset + length); + this.offset += length; + return value; } string(): string { - const length = this.short() - const value = this.arrayView.slice(this.offset, this.offset + length) - this.offset += length - return new TextDecoder().decode(value) + const length = this.short(); + const value = this.arrayView.slice(this.offset, this.offset + length); + this.offset += length; + return new TextDecoder().decode(value); } list(): any[] { - const type = this.byte() - const length = this.int() - const value: any[] = [] + const type = this.byte(); + const length = this.int(); + const value: any[] = []; for (let i = 0; i < length; i++) { - value.push((this as any)[type]()) + value.push((this as any)[type]()); } - return value + return value; } compound(): Record { - const value: Record = {} - let type + const value: Record = {}; + let type; while ((type = this.byte()) !== nbt.tagTypes.end) { - const key = this.string() - value[key] = { type, value: (this as any)[type]() } + const key = this.string(); + value[key] = { type, value: (this as any)[type]() }; } - return value + return value; } intArray(): Int32Array { - const length = this.int() - const value = new Int32Array(length) + const length = this.int(); + const value = new Int32Array(length); for (let i = 0; i < length; i++) { - value[i] = this.int() + value[i] = this.int(); } - return value + return value; } longArray(): BigInt64Array { - const length = this.int() - const value = new BigInt64Array(length) + const length = this.int(); + const value = new BigInt64Array(length); for (let i = 0; i < length; i++) { - value[i] = this.long() + value[i] = this.long(); } - return value + return value; } } -export { NbtWriter, NbtReader, nbt } +export { NbtWriter, NbtReader, nbt }; diff --git a/src/packet/client/HandshakePacket.ts b/src/packet/client/HandshakePacket.ts index b5b4f91..ef913e7 100644 --- a/src/packet/client/HandshakePacket.ts +++ b/src/packet/client/HandshakePacket.ts @@ -1,57 +1,57 @@ import { TypedClientPacket, TypedClientPacketStatic, -} from "../../types/TypedPacket" -import StaticImplements from "../../decorator/StaticImplements.js" -import Server from "../../Server" -import ParsedPacket from "../../ParsedPacket" -import Connection from "../../Connection.js" -import { C2S } from "../Packets.js" +} from "../../types/TypedPacket"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import Server from "../../Server"; +import ParsedPacket from "../../ParsedPacket"; +import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class HandshakePacket { - public readonly packet: ParsedPacket + public readonly packet: ParsedPacket; - public readonly data + public readonly data; /** * Create a new HandshakePacket * @param packet */ public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet + this.packet = packet; this.data = { protocolVersion: this.packet.getVarInt()!, serverAddress: this.packet.getString()!, serverPort: this.packet.getUShort()!, nextState: this.packet.getVarInt()!, - } as const + } as const; } execute(conn: Connection, _server: Server): void { switch (this.data.nextState) { case 1: - conn._setState(Connection.State.STATUS) - break + conn._setState(Connection.State.STATUS); + break; case 2: - conn._setState(Connection.State.LOGIN) - break + conn._setState(Connection.State.LOGIN); + break; } } - public static readonly id = C2S.Handshake + public static readonly id = C2S.Handshake; public static isThisPacket( data: ParsedPacket, conn: Connection ): TypedClientPacket | null { - if (conn.state !== Connection.State.NONE) return null + if (conn.state !== Connection.State.NONE) return null; try { - const p = new this(data) - return p.packet.id === this.id ? p : null + const p = new this(data); + return p.packet.id === this.id ? p : null; } catch { - return null + return null; } } } diff --git a/src/packet/client/LoginAckPacket.ts b/src/packet/client/LoginAckPacket.ts index b49890a..a4df6dc 100644 --- a/src/packet/client/LoginAckPacket.ts +++ b/src/packet/client/LoginAckPacket.ts @@ -1,45 +1,45 @@ import { TypedClientPacket, TypedClientPacketStatic, -} from "../../types/TypedPacket" -import StaticImplements from "../../decorator/StaticImplements.js" -import Server from "../../Server" -import ParsedPacket from "../../ParsedPacket" -import Connection from "../../Connection.js" -import { C2S } from "../Packets.js" +} from "../../types/TypedPacket"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import Server from "../../Server"; +import ParsedPacket from "../../ParsedPacket"; +import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class LoginAckPacket { - public readonly packet: ParsedPacket + public readonly packet: ParsedPacket; - public readonly data + public readonly data; /** * Create a new HandshakePacket * @param packet */ public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet + this.packet = packet; - this.data = {} as const + this.data = {} as const; } execute(conn: Connection, _server: Server): void { - conn._setState(Connection.State.CONFIGURATION) + conn._setState(Connection.State.CONFIGURATION); } - public static readonly id = C2S.LoginAcknowledge + public static readonly id = C2S.LoginAcknowledge; public static isThisPacket( data: ParsedPacket, conn: Connection ): TypedClientPacket | null { - if (conn.state !== Connection.State.LOGIN) return null + if (conn.state !== Connection.State.LOGIN) return null; try { - const p = new this(data) - return p.packet.id === this.id ? p : null + const p = new this(data); + return p.packet.id === this.id ? p : null; } catch { - return null + return null; } } } diff --git a/src/packet/client/LoginPacket.ts b/src/packet/client/LoginPacket.ts index b042f6b..721fd9e 100644 --- a/src/packet/client/LoginPacket.ts +++ b/src/packet/client/LoginPacket.ts @@ -1,53 +1,53 @@ import { TypedClientPacket, TypedClientPacketStatic, -} from "../../types/TypedPacket" -import StaticImplements from "../../decorator/StaticImplements.js" -import ParsedPacket from "../../ParsedPacket.js" -import Server from "../../Server" -import Connection from "../../Connection.js" -import { C2S } from "../Packets.js" +} from "../../types/TypedPacket"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import ParsedPacket from "../../ParsedPacket.js"; +import Server from "../../Server"; +import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class LoginPacket { - public readonly packet: ParsedPacket + public readonly packet: ParsedPacket; - public readonly data + public readonly data; /** * Create a new HandshakePacket * @param packet */ public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet + this.packet = packet; this.data = { username: this.packet.getString()!, hasUUID: this.packet.getBoolean()!, uuid: this.packet.getUUID()!, - } + }; } execute(conn: Connection, _server: Server): void { - conn._setState(Connection.State.LOGIN) + conn._setState(Connection.State.LOGIN); } - public static readonly id = C2S.Login + public static readonly id = C2S.Login; public static isThisPacket( data: ParsedPacket, conn: Connection ): TypedClientPacket | null { - if (conn.state !== Connection.State.LOGIN) return null + if (conn.state !== Connection.State.LOGIN) return null; try { - const p = new this(data) + const p = new this(data); return p.packet.id === this.id && p.data.username !== null && p.data.username.match(/^[.*]?[A-Za-z0-9_]{3,16}$/) !== null ? p - : null + : null; } catch { - return null + return null; } } } diff --git a/src/packet/client/PingPacket.ts b/src/packet/client/PingPacket.ts index aef16fb..76f9391 100644 --- a/src/packet/client/PingPacket.ts +++ b/src/packet/client/PingPacket.ts @@ -1,46 +1,46 @@ import { TypedClientPacket, TypedClientPacketStatic, -} from "../../types/TypedPacket" -import StaticImplements from "../../decorator/StaticImplements.js" -import Server from "../../Server" -import ParsedPacket from "../../ParsedPacket" -import Connection from "../../Connection.js" -import { C2S } from "../Packets.js" +} from "../../types/TypedPacket"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import Server from "../../Server"; +import ParsedPacket from "../../ParsedPacket"; +import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class PingPacket { - public readonly packet: ParsedPacket + public readonly packet: ParsedPacket; - public readonly data + public readonly data; /** * Create a new PingPacket * @param packet */ public constructor(packet: import("../../ParsedPacket").default) { - this.packet = packet + this.packet = packet; this.data = { payload: this.packet.getLong()!, - } as const + } as const; } execute(_conn: Connection, _server: Server): void { // pass } - public static readonly id = C2S.Ping + public static readonly id = C2S.Ping; public static isThisPacket( data: ParsedPacket, _conn: Connection ): TypedClientPacket | null { try { - const p = new this(data) - return p.packet.id === this.id ? p : null + const p = new this(data); + return p.packet.id === this.id ? p : null; } catch { - return null + return null; } } } diff --git a/src/packet/client/StatusRequestPacket.ts b/src/packet/client/StatusRequestPacket.ts index 2acb2bf..3b75da5 100644 --- a/src/packet/client/StatusRequestPacket.ts +++ b/src/packet/client/StatusRequestPacket.ts @@ -1,45 +1,45 @@ import { TypedClientPacket, TypedClientPacketStatic, -} from "../../types/TypedPacket.js" -import StaticImplements from "../../decorator/StaticImplements.js" -import Server from "../../Server.js" -import ParsedPacket from "../../ParsedPacket.js" -import Connection from "../../Connection.js" -import { C2S } from "../Packets.js" +} from "../../types/TypedPacket.js"; +import StaticImplements from "../../decorator/StaticImplements.js"; +import Server from "../../Server.js"; +import ParsedPacket from "../../ParsedPacket.js"; +import Connection from "../../Connection.js"; +import { C2S } from "../Packets.js"; @StaticImplements() export default class StatusRequestPacket { - public readonly packet: ParsedPacket + public readonly packet: ParsedPacket; - public readonly data + public readonly data; /** * Create a new StatusRequest * @param packet */ public constructor(packet: import("../../ParsedPacket.js").default) { - this.packet = packet + this.packet = packet; - this.data = {} as const + this.data = {} as const; } execute(_conn: Connection, _server: Server): void { // pass } - public static readonly id = C2S.StatusRequest + public static readonly id = C2S.StatusRequest; public static isThisPacket( data: ParsedPacket, conn: Connection ): TypedClientPacket | null { - if (conn.state !== Connection.State.STATUS) return null + if (conn.state !== Connection.State.STATUS) return null; try { - const p = new this(data) - return p.packet.id === this.id ? p : null + const p = new this(data); + return p.packet.id === this.id ? p : null; } catch { - return null + return null; } } } diff --git a/src/packet/server/DisconnectLoginPacket.ts b/src/packet/server/DisconnectLoginPacket.ts index 6fe870c..aa2774f 100644 --- a/src/packet/server/DisconnectLoginPacket.ts +++ b/src/packet/server/DisconnectLoginPacket.ts @@ -1,8 +1,8 @@ -import ServerPacket from "../../ServerPacket.js" -import { S2C } from "../Packets.js" +import ServerPacket from "../../ServerPacket.js"; +import { S2C } from "../Packets.js"; export default class DisconnectLoginPacket extends ServerPacket { - public static readonly id = S2C.DisconnectLogin + public static readonly id = S2C.DisconnectLogin; public constructor(reason: ChatComponent) { super( @@ -10,6 +10,6 @@ export default class DisconnectLoginPacket extends ServerPacket { ServerPacket.writeVarInt(DisconnectLoginPacket.id), ServerPacket.writeChat(reason), ]) - ) + ); } } diff --git a/src/packet/server/DisconnectPlayPacket.ts b/src/packet/server/DisconnectPlayPacket.ts index 3c1e1f1..adb210e 100644 --- a/src/packet/server/DisconnectPlayPacket.ts +++ b/src/packet/server/DisconnectPlayPacket.ts @@ -1,8 +1,8 @@ -import ServerPacket from "../../ServerPacket.js" -import { S2C } from "../Packets.js" +import ServerPacket from "../../ServerPacket.js"; +import { S2C } from "../Packets.js"; export default class DisconnectPlayPacket extends ServerPacket { - public static readonly id = S2C.DisconnectPlay + public static readonly id = S2C.DisconnectPlay; public constructor(reason: ChatComponent) { super( @@ -10,6 +10,6 @@ export default class DisconnectPlayPacket extends ServerPacket { ServerPacket.writeVarInt(DisconnectPlayPacket.id), ServerPacket.writeChat(reason), ]) - ) + ); } } diff --git a/src/packet/server/LoginSuccessPacket.ts b/src/packet/server/LoginSuccessPacket.ts index 86c2391..28c7134 100644 --- a/src/packet/server/LoginSuccessPacket.ts +++ b/src/packet/server/LoginSuccessPacket.ts @@ -1,12 +1,12 @@ -import ServerPacket from "../../ServerPacket.js" -import Connection from "../../Connection.js" -import { S2C } from "../Packets.js" +import ServerPacket from "../../ServerPacket.js"; +import Connection from "../../Connection.js"; +import { S2C } from "../Packets.js"; /** * A Minecraft protocol client-bound LoginSuccess packet. */ export default class LoginSuccessPacket extends ServerPacket { - public static readonly id = S2C.LoginSuccess + public static readonly id = S2C.LoginSuccess; public constructor(uuid: string, username: string) { super( @@ -16,11 +16,11 @@ export default class LoginSuccessPacket extends ServerPacket { ServerPacket.writeString(username), ServerPacket.writeVarInt(0), ]) - ) + ); } public override send(connection: Connection) { - connection._setState(Connection.State.PLAY) - return super.send(connection) + connection._setState(Connection.State.PLAY); + return super.send(connection); } } diff --git a/src/packet/server/PongPacket.ts b/src/packet/server/PongPacket.ts index 172ea4b..a9439e3 100644 --- a/src/packet/server/PongPacket.ts +++ b/src/packet/server/PongPacket.ts @@ -1,9 +1,9 @@ -import ServerPacket from "../../ServerPacket.js" -import PingPacket from "../client/PingPacket.js" -import { S2C } from "../Packets.js" +import ServerPacket from "../../ServerPacket.js"; +import PingPacket from "../client/PingPacket.js"; +import { S2C } from "../Packets.js"; export default class PongPacket extends ServerPacket { - public static readonly id = S2C.Pong + public static readonly id = S2C.Pong; public constructor(c2s: PingPacket) { super( @@ -11,6 +11,6 @@ export default class PongPacket extends ServerPacket { ServerPacket.writeVarInt(PongPacket.id), ServerPacket.writeLong(c2s.data.payload), ]) - ) + ); } } diff --git a/src/packet/server/StatusResponsePacket.ts b/src/packet/server/StatusResponsePacket.ts index a8d6e55..3bf1087 100644 --- a/src/packet/server/StatusResponsePacket.ts +++ b/src/packet/server/StatusResponsePacket.ts @@ -1,9 +1,9 @@ -import Server from "../../Server.js" -import ServerPacket from "../../ServerPacket.js" -import { S2C } from "../Packets.js" +import Server from "../../Server.js"; +import ServerPacket from "../../ServerPacket.js"; +import { S2C } from "../Packets.js"; export default class StatusResponsePacket extends ServerPacket { - public static readonly id = S2C.StatusResponse + public static readonly id = S2C.StatusResponse; public constructor(server: Server) { super( @@ -35,6 +35,6 @@ export default class StatusResponsePacket extends ServerPacket { }) ), ]) - ) + ); } } diff --git a/src/packet/server/configuration/ConfigurationKeepAlive.ts b/src/packet/server/configuration/ConfigurationKeepAlive.ts index e534ef4..c6e822a 100644 --- a/src/packet/server/configuration/ConfigurationKeepAlive.ts +++ b/src/packet/server/configuration/ConfigurationKeepAlive.ts @@ -1,8 +1,8 @@ -import ServerPacket from "../../../ServerPacket.js" -import { S2C } from "../../Packets.js" +import ServerPacket from "../../../ServerPacket.js"; +import { S2C } from "../../Packets.js"; export default class ConfgirationKeepAlive extends ServerPacket { - public static readonly id = S2C.ConfigurationKeepAlive + public static readonly id = S2C.ConfigurationKeepAlive; public constructor() { super( @@ -10,6 +10,6 @@ export default class ConfgirationKeepAlive extends ServerPacket { ServerPacket.writeVarInt(ConfgirationKeepAlive.id), ServerPacket.writeLong(BigInt(Date.now())), ]) - ) + ); } } diff --git a/src/packet/server/configuration/FinishConfigurationPacket.ts b/src/packet/server/configuration/FinishConfigurationPacket.ts index caf478b..6bbb871 100644 --- a/src/packet/server/configuration/FinishConfigurationPacket.ts +++ b/src/packet/server/configuration/FinishConfigurationPacket.ts @@ -1,20 +1,20 @@ -import Connection from "../../../Connection.js" -import ServerPacket from "../../../ServerPacket.js" -import { S2C } from "../../Packets.js" +import Connection from "../../../Connection.js"; +import ServerPacket from "../../../ServerPacket.js"; +import { S2C } from "../../Packets.js"; export default class FinishConfigurationPacket extends ServerPacket { - public static readonly id = S2C.FinishConfiguration + public static readonly id = S2C.FinishConfiguration; public constructor() { super( Buffer.concat([ ServerPacket.writeVarInt(FinishConfigurationPacket.id), ]) - ) + ); } public override send(connection: Connection) { - connection._setState(Connection.State.PLAY) - return super.send(connection) + connection._setState(Connection.State.PLAY); + return super.send(connection); } } diff --git a/src/packet/server/configuration/RegistryDataPacket.ts b/src/packet/server/configuration/RegistryDataPacket.ts index 9b2de85..00e2415 100644 --- a/src/packet/server/configuration/RegistryDataPacket.ts +++ b/src/packet/server/configuration/RegistryDataPacket.ts @@ -1,12 +1,12 @@ -import ServerPacket from "../../../ServerPacket.js" -import { S2C } from "../../Packets.js" -import { NbtReader, NbtWriter, tagTypes } from "../../../nbt.js" +import ServerPacket from "../../../ServerPacket.js"; +import { S2C } from "../../Packets.js"; +import { NbtReader, NbtWriter, tagTypes } from "../../../nbt.js"; export default class RegistryDataPacket extends ServerPacket { - public static readonly id = S2C.RegistryData + public static readonly id = S2C.RegistryData; public constructor() { - const writer = new NbtWriter() + const writer = new NbtWriter(); writer.compound({ "minecraft:worldgen/biome": { type: tagTypes.compound, @@ -30,12 +30,12 @@ export default class RegistryDataPacket extends ServerPacket { }, }, }, - }) + }); super( Buffer.concat([ ServerPacket.writeVarInt(RegistryDataPacket.id), Buffer.from(writer.buffer.slice(0, writer.offset)), ]) - ) + ); } } diff --git a/src/types/ChatComponent.d.ts b/src/types/ChatComponent.d.ts index 9ef3aeb..62b95b0 100644 --- a/src/types/ChatComponent.d.ts +++ b/src/types/ChatComponent.d.ts @@ -1,13 +1,13 @@ type ChatComponent = | { - bold?: boolean - italic?: boolean - underlined?: boolean - strikethrough?: boolean - obfuscated?: boolean - font?: string - color?: string - insertion?: string + bold?: boolean; + italic?: boolean; + underlined?: boolean; + strikethrough?: boolean; + obfuscated?: boolean; + font?: string; + color?: string; + insertion?: string; clickEvent?: { action: | "open_url" @@ -15,21 +15,21 @@ type ChatComponent = | "run_command" | "suggest_command" | "change_page" - | "copy_to_clipboard" - value: string - } + | "copy_to_clipboard"; + value: string; + }; hoverEvent?: { - action: "show_text" | "show_item" | "show_entity" - } - text?: string - extra?: [ChatComponent, ...ChatComponent[]] + action: "show_text" | "show_item" | "show_entity"; + }; + text?: string; + extra?: [ChatComponent, ...ChatComponent[]]; } | { translate: string; with?: ChatComponent[] } | { keybind: string } | { score: { - name: string - objective: string - value?: string - } - } + name: string; + objective: string; + value?: string; + }; + }; diff --git a/src/types/TypedEventEmitter.d.ts b/src/types/TypedEventEmitter.d.ts index 93c69dd..3cb9a79 100644 --- a/src/types/TypedEventEmitter.d.ts +++ b/src/types/TypedEventEmitter.d.ts @@ -23,32 +23,35 @@ * @see https://github.com/andywer/typed-emitter */ export default interface TypedEventEmitter { - addListener(event: E, listener: Events[E]): this - on(event: E, listener: Events[E]): this - once(event: E, listener: Events[E]): this - prependListener(event: E, listener: Events[E]): this + addListener(event: E, listener: Events[E]): this; + on(event: E, listener: Events[E]): this; + once(event: E, listener: Events[E]): this; + prependListener( + event: E, + listener: Events[E] + ): this; prependOnceListener( event: E, listener: Events[E] - ): this + ): this; - off(event: E, listener: Events[E]): this - removeAllListeners(event?: E): this - removeListener(event: E, listener: Events[E]): this + off(event: E, listener: Events[E]): this; + removeAllListeners(event?: E): this; + removeListener(event: E, listener: Events[E]): this; emit( event: E, ...args: Parameters - ): boolean + ): boolean; // The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5 - eventNames(): (keyof Events | string | symbol)[] - rawListeners(event: E): Events[E][] - listeners(event: E): Events[E][] - listenerCount(event: E): number + eventNames(): (keyof Events | string | symbol)[]; + rawListeners(event: E): Events[E][]; + listeners(event: E): Events[E][]; + listenerCount(event: E): number; - getMaxListeners(): number - setMaxListeners(maxListeners: number): this + getMaxListeners(): number; + setMaxListeners(maxListeners: number): this; } export type EventMap = { - [key: string]: (...args: any[]) => void -} + [key: string]: (...args: any[]) => void; +}; diff --git a/src/types/TypedPacket.d.ts b/src/types/TypedPacket.d.ts index 9541f33..0783b1b 100644 --- a/src/types/TypedPacket.d.ts +++ b/src/types/TypedPacket.d.ts @@ -1,17 +1,20 @@ -import Server from "../Server" -import ParsedPacket from "../ParsedPacket" -import Connection from "../Connection" +import Server from "../Server"; +import ParsedPacket from "../ParsedPacket"; +import Connection from "../Connection"; export interface TypedClientPacket { - readonly packet: ParsedPacket - readonly data: Record - execute(connection: Connection, server: Server): void + readonly packet: ParsedPacket; + readonly data: Record; + execute(connection: Connection, server: Server): void; } export interface TypedClientPacketStatic { - new (packet: ParsedPacket): TypedClientPacket + new (packet: ParsedPacket): TypedClientPacket; - readonly id: number + readonly id: number; - isThisPacket(data: ParsedPacket, conn: Connection): TypedClientPacket | null + isThisPacket( + data: ParsedPacket, + conn: Connection + ): TypedClientPacket | null; }