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..34379b1
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "trailingComma": "es5",
+ "tabWidth": 4,
+ "semi": true,
+ "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
new file mode 100644
index 0000000..526f229
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,15 @@
+## 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)
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..942fd44 100644
--- a/index.ts
+++ b/index.ts
@@ -2,45 +2,78 @@ 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);
server.start();
-server.on("listening", (port) => server.logger.info(`Listening on port ${port}`));
+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).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) => {
+ new PongPacket(packet).send(conn);
+});
+
+server.on("packet.StatusRequestPacket", (_, conn) => {
+ new StatusResponsePacket(server).send(conn);
+});
+
+server.on("packet.LoginAck", async (_, conn) => {
+ await new RegistryDataPacket().send(conn).then();
+ await new FinishConfigurationPacket().send(conn).then();
+});
+
+//FIXME: loginack not executed
diff --git a/package-lock.json b/package-lock.json
index 99e1c3a..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.12",
- "@vercel/ncc": "^0.38.1",
- "typescript": "^5.4.5"
- }
- },
- "node_modules/@types/node": {
- "version": "20.12.12",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
- "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
- "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 f87cb5e..d888c5b 100644
--- a/package.json
+++ b/package.json
@@ -1,22 +1,24 @@
{
- "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.12",
- "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",
+ "lint": "prettier . --write",
+ "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 2c7c35e..aabe039 100644
--- a/src/Config.ts
+++ b/src/Config.ts
@@ -1,82 +1,133 @@
import { open, access, constants, FileHandle } from "node:fs/promises";
import Logger from "./Logger.js";
-
export interface Config {
- /**
- * Port to listen on
- */
- port: number;
+ /**
+ * Port to listen on
+ */
+ port: number;
- /**
- * The level to display logs at
- */
- logLevel: Logger.Level;
+ /**
+ * The level to display logs at
+ */
+ logLevel: Logger.Level;
- /**
- * Kick reason for when the server is shutting down
- */
- shutdownKickReason: ChatComponent;
-}
+ /**
+ * Kick reason for when the server is shutting down
+ */
+ shutdownKickReason: ChatComponent;
-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();
+ /**
+ * The server config
+ */
+ server: ServerConfig;
+}
- return JSON.parse(data) as Config;
- }
+export interface ServerConfig {
+ /**
+ * The motd of the server (description)
+ * Split optionally by `\n`
+ */
+ motd: string;
- /**
- * 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"
- }
- };
+ /**
+ * 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;
- /**
- * 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;
- }
- }
+ /**
+ * Max number of players that may be logged on at the same time
+ */
+ maxPlayers: number;
- /**
- * 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();
- }
+ /**
+ * 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();
+ }
+}
diff --git a/src/Connection.ts b/src/Connection.ts
index 70bbacc..0317c37 100644
--- a/src/Connection.ts
+++ b/src/Connection.ts
@@ -2,113 +2,135 @@ 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 {
- 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,
- /**
- * Play state
- *
- * Player is online and communicating game data
- */
- PLAY
- }
+ /**
+ * Configuration state
+ *
+ * Player is connected and is awaiting configuration data
+ */
+ CONFIGURATION,
+
+ /**
+ * Play state
+ *
+ * Player is online and communicating game data
+ */
+ PLAY,
+ }
}
export default Connection;
diff --git a/src/ConnectionPool.ts b/src/ConnectionPool.ts
index 753c04b..b0cb761 100644
--- a/src/ConnectionPool.ts
+++ b/src/ConnectionPool.ts
@@ -3,64 +3,79 @@ 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..a663a39 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;
diff --git a/src/Packet.ts b/src/Packet.ts
index 8d8f730..31a30fc 100644
--- a/src/Packet.ts
+++ b/src/Packet.ts
@@ -1,232 +1,281 @@
import ParsedPacket from "./ParsedPacket.js";
-import {TypedClientPacket, TypedClientPacketStatic} from "./types/TypedPacket";
+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 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, LoginPacket];
-
-
- /**
- * 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 da3c8b8..648c20b 100644
--- a/src/ParsedPacket.ts
+++ b/src/ParsedPacket.ts
@@ -1,96 +1,112 @@
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 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 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 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 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 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 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 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;
- }
-}
\ 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..8e628d4 100644
--- a/src/Scheduler.ts
+++ b/src/Scheduler.ts
@@ -1,466 +1,495 @@
import EventEmitter from "node:events";
-import {randomUUID} from "node:crypto";
+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;
diff --git a/src/Server.ts b/src/Server.ts
index 3b53c14..998e82d 100644
--- a/src/Server.ts
+++ b/src/Server.ts
@@ -3,7 +3,7 @@ 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 { TypedClientPacket } from "./types/TypedPacket";
import TypedEventEmitter from "./types/TypedEventEmitter";
import ConnectionPool from "./ConnectionPool.js";
import Connection from "./Connection.js";
@@ -11,110 +11,160 @@ 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;
+ /**
+ * 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 constructor(config: Config) {
- super();
- this.config = Object.freeze(config);
- this.logger = new Logger("Server", this.config.logLevel);
- }
-
- public start() {
- 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..45229e8 100644
--- a/src/ServerPacket.ts
+++ b/src/ServerPacket.ts
@@ -2,21 +2,20 @@ 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..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
new file mode 100644
index 0000000..f8aa5ce
--- /dev/null
+++ b/src/nbt.ts
@@ -0,0 +1,770 @@
+// @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 };
diff --git a/src/packet/Packets.ts b/src/packet/Packets.ts
new file mode 100644
index 0000000..e164958
--- /dev/null
+++ b/src/packet/Packets.ts
@@ -0,0 +1,104 @@
+/**
+ * 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
+ */
+ StatusRequest = 0x00,
+
+ /**
+ * Login Ack
+ *
+ * State: Login
+ */
+ LoginAcknowledge = 0x03,
+}
+
+/**
+ * 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,
+
+ /**
+ * Registry Data
+ *
+ * State: join
+ */
+ RegistryData = 0x05,
+
+ /**
+ * Registry Data
+ *
+ * State: join
+ */
+ ConfigurationKeepAlive = 0x03,
+
+ /**
+ * Finish Configuration
+ *
+ * State: join->play
+ */
+ FinishConfiguration = 0x02,
+}
diff --git a/src/packet/client/HandshakePacket.ts b/src/packet/client/HandshakePacket.ts
index aaf16c9..ef913e7 100644
--- a/src/packet/client/HandshakePacket.ts
+++ b/src/packet/client/HandshakePacket.ts
@@ -1,44 +1,57 @@
-import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket";
+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 {
- conn._setState(Connection.State.LOGIN);
- }
+ 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 = 0x00;
+ 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.data.nextState === 2) ? 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
new file mode 100644
index 0000000..a4df6dc
--- /dev/null
+++ b/src/packet/client/LoginAckPacket.ts
@@ -0,0 +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";
+
+@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/LoginPacket.ts b/src/packet/client/LoginPacket.ts
index d8163a8..721fd9e 100644
--- a/src/packet/client/LoginPacket.ts
+++ b/src/packet/client/LoginPacket.ts
@@ -1,43 +1,53 @@
-import {TypedClientPacket, TypedClientPacketStatic} from "../../types/TypedPacket";
+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 = 0x00;
+ 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
new file mode 100644
index 0000000..76f9391
--- /dev/null
+++ b/src/packet/client/PingPacket.ts
@@ -0,0 +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";
+
+@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.getLong()!,
+ } as const;
+ }
+
+ execute(_conn: Connection, _server: Server): void {
+ // pass
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/src/packet/client/StatusRequestPacket.ts b/src/packet/client/StatusRequestPacket.ts
new file mode 100644
index 0000000..3b75da5
--- /dev/null
+++ b/src/packet/client/StatusRequestPacket.ts
@@ -0,0 +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";
+
+@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 = 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;
+ }
+ }
+}
diff --git a/src/packet/server/DisconnectLoginPacket.ts b/src/packet/server/DisconnectLoginPacket.ts
index ebc0cb1..aa2774f 100644
--- a/src/packet/server/DisconnectLoginPacket.ts
+++ b/src/packet/server/DisconnectLoginPacket.ts
@@ -1,12 +1,15 @@
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([
- 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 4882d3e..adb210e 100644
--- a/src/packet/server/DisconnectPlayPacket.ts
+++ b/src/packet/server/DisconnectPlayPacket.ts
@@ -1,12 +1,15 @@
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([
- 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 fff8b87..28c7134 100644
--- a/src/packet/server/LoginSuccessPacket.ts
+++ b/src/packet/server/LoginSuccessPacket.ts
@@ -1,23 +1,26 @@
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([
- 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
new file mode 100644
index 0000000..a9439e3
--- /dev/null
+++ b/src/packet/server/PongPacket.ts
@@ -0,0 +1,16 @@
+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 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
new file mode 100644
index 0000000..3bf1087
--- /dev/null
+++ b/src/packet/server/StatusResponsePacket.ts
@@ -0,0 +1,40 @@
+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 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
new file mode 100644
index 0000000..c6e822a
--- /dev/null
+++ b/src/packet/server/configuration/ConfigurationKeepAlive.ts
@@ -0,0 +1,15 @@
+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..6bbb871
--- /dev/null
+++ b/src/packet/server/configuration/FinishConfigurationPacket.ts
@@ -0,0 +1,20 @@
+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) {
+ 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..00e2415
--- /dev/null
+++ b/src/packet/server/configuration/RegistryDataPacket.ts
@@ -0,0 +1,41 @@
+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),
+ Buffer.from(writer.buffer.slice(0, writer.offset)),
+ ])
+ );
+ }
+}
diff --git a/src/types/ChatComponent.d.ts b/src/types/ChatComponent.d.ts
index 48335a8..62b95b0 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..3cb9a79 100644
--- a/src/types/TypedEventEmitter.d.ts
+++ b/src/types/TypedEventEmitter.d.ts
@@ -23,26 +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
- 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..0783b1b 100644
--- a/src/types/TypedPacket.d.ts
+++ b/src/types/TypedPacket.d.ts
@@ -3,15 +3,18 @@ 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 bc23ac0..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. */
+ }
}