diff --git a/config.json b/config.json index 94c746c..53785f6 100644 --- a/config.json +++ b/config.json @@ -1,3 +1,4 @@ { - "port": "25565" + "port": "25565", + "shutdownKickReason": "Server closed" } diff --git a/index.ts b/index.ts index 8a75da6..1bbead0 100644 --- a/index.ts +++ b/index.ts @@ -42,5 +42,5 @@ process.on("SIGINT", () => { }); server.on("packet.LoginPacket", (packet, conn) => { - new LoginSuccessPacket(packet.data.uuid, packet.data.username).send(conn); + new LoginSuccessPacket(packet.data.uuid, packet.data.username).send(conn).then(); }); diff --git a/src/Config.ts b/src/Config.ts index e74fef0..e96e689 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -4,6 +4,11 @@ import Logger from "./Logger.js"; export default class Config { public port: number = 25565; + /** + * Kick reason for when the server is shutting down + */ + public shutdownKickReason: string = "Server closed"; + /** * Get a Config instance from a json file * @param file The file to read from diff --git a/src/Connection.ts b/src/Connection.ts index eadd4e0..7b439fe 100644 --- a/src/Connection.ts +++ b/src/Connection.ts @@ -6,7 +6,7 @@ import Packet from "./Packet.js"; /** * A TCP socket connection to the server. */ -export default class Connection { +class Connection { /** * A unique identifier for this connection. */ @@ -19,6 +19,22 @@ export default class Connection { * 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; + } + + /** @internal */ + public _setState(state: Connection.State): void { + this.#state = state; + } /** * Packet fragment this connection is currently sending to the server. @@ -37,9 +53,9 @@ export default class Connection { if (this.currentPacketFragment.push(data)) { const p = this.currentPacketFragment.getTypedClient(); if (p) { + p.execute(this, this.server); this.server.emit("packet", p, this); this.server.emit(`packet.${p.constructor.name}` as any, p, this); - p.execute(this, this.server); } else this.server.emit("unknownPacket", this.currentPacketFragment, this); this.currentPacketFragment = new Packet(); @@ -48,9 +64,10 @@ export default class Connection { /** * Disconnect this connection. + * @param [reason] The reason for the disconnect. */ - public disconnect(): Promise { - return this.server.connections.disconnect(this.id); + public disconnect(reason?: string): Promise { + return this.server.connections.disconnect(this.id, reason); } /** @@ -60,3 +77,38 @@ export default class Connection { return !this.socket.destroyed && this.server.connections.get(this.id) !== null; } } + +namespace Connection { + /** + * Connection state + */ + export enum State { + /** + * None / unknown + */ + NONE, + + /** + * Status state + * + * Sender is checking server status + */ + STATUS, + + /** + * Login state + * + * Player is connecting to the server + */ + LOGIN, + + /** + * Play state + * + * Player is online and communicating game data + */ + PLAY + } +} + +export default Connection; diff --git a/src/ConnectionPool.ts b/src/ConnectionPool.ts index 3de0d0c..ae361e3 100644 --- a/src/ConnectionPool.ts +++ b/src/ConnectionPool.ts @@ -1,4 +1,6 @@ -import Connection from "./Connection"; +import Connection from "./Connection.js"; +import DisconnectLoginPacket from "./packet/server/DisconnectLoginPacket.js"; +import DisconnectPlayPacket from "./packet/server/DisconnectPlayPacket.js"; export default class ConnectionPool { private readonly connections: Connection[] = []; @@ -22,28 +24,42 @@ export default class ConnectionPool { /** * Disconnect all connections + * @param [reason] The reason for the disconnect * @returns Whether all connections disconnected successfully */ - public async disconnect(): Promise; + public async disconnectAll (reason?: string): 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): Promise; - public async disconnect(id?: string): Promise { - const promises: Promise[] = []; - if (id) { - const connection = this.get(id); - if (!connection) return false; - const index = this.connections.indexOf(connection); - if (index === -1) return false; - this.connections.splice(index, 1); - connection.server.emit("disconnect", connection); - promises.push(new Promise(resolve => connection.socket.end(() => resolve(true)))); + public async disconnect(id: string, reason?: string): Promise { + const connection = this.get(id); + if (!connection) return false; + const index = this.connections.indexOf(connection); + if (index === -1) return false; + if (reason) switch (connection.state) { + case Connection.State.LOGIN: { + await new DisconnectLoginPacket({text: reason}).send(connection); + break; + } + case Connection.State.PLAY: { + await new DisconnectPlayPacket({text: reason}).send(connection); + break; + } + default: { + connection.server.logger.warn("Cannot set disconnect reason for state " + Connection.State[connection.state] + " on connection " + connection.id); + } } - else for (const connection of this.connections) - promises.push(this.disconnect(connection.id)); - return (await Promise.all(promises)).every(result => result); + this.connections.splice(index, 1); + connection.server.emit("disconnect", connection); + return new Promise(resolve => connection.socket.end(() => resolve(true))); } } diff --git a/src/Packet.ts b/src/Packet.ts index e999ef7..d09fc0d 100644 --- a/src/Packet.ts +++ b/src/Packet.ts @@ -1,5 +1,5 @@ import ParsedPacket from "./ParsedPacket.js"; -import {TypedClientPacket, TypedClientPacketStatic} from "./TypedPacket"; +import {TypedClientPacket, TypedClientPacketStatic} from "./types/TypedPacket"; import HandshakePacket from "./packet/client/HandshakePacket.js"; import LoginPacket from "./packet/client/LoginPacket.js"; @@ -178,6 +178,22 @@ export default class Packet { 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 */ diff --git a/src/Server.ts b/src/Server.ts index 67dd717..2d70fa1 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -4,8 +4,8 @@ import path from "node:path"; import Packet from "./Packet.js"; import Config from "./Config.js"; import Logger from "./Logger.js"; -import {TypedClientPacket} from "./TypedPacket"; -import TypedEventEmitter from "./TypedEventEmitter"; +import {TypedClientPacket} from "./types/TypedPacket"; +import TypedEventEmitter from "./types/TypedEventEmitter"; import ConnectionPool from "./ConnectionPool.js"; import Connection from "./Connection.js"; import HandshakePacket from "./packet/client/HandshakePacket"; @@ -91,8 +91,8 @@ export default class Server extends (EventEmitter as new () => TypedEventEmitter else resolve(void 0); }); }), - this.connections.disconnect(), - ]).then(() => void 0); + this.connections.disconnectAll(this.config.shutdownKickReason), + ]); this.emit("closed"); } diff --git a/src/ServerPacket.ts b/src/ServerPacket.ts index 1504206..4226d7f 100644 --- a/src/ServerPacket.ts +++ b/src/ServerPacket.ts @@ -11,7 +11,12 @@ export default abstract class ServerPacket extends Packet { * Send packet to a connection * @param connection */ - public send(connection: Connection): void { - connection.socket.write(this.dataBuffer); + 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/packet/client/HandshakePacket.ts b/src/packet/client/HandshakePacket.ts index b7160dd..2dbaf3a 100644 --- a/src/packet/client/HandshakePacket.ts +++ b/src/packet/client/HandshakePacket.ts @@ -1,8 +1,8 @@ -import {TypedClientPacket, TypedClientPacketStatic} from "../../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"; +import Connection from "../../Connection.js"; @StaticImplements() export default class HandshakePacket { @@ -25,7 +25,9 @@ export default class HandshakePacket { } as const; } - execute(_conn: Connection, _server: Server): void {} + execute(conn: Connection, _server: Server): void { + conn._setState(Connection.State.LOGIN); + } public static readonly id = 0x00; diff --git a/src/packet/client/LoginPacket.ts b/src/packet/client/LoginPacket.ts index 92d7176..e644f86 100644 --- a/src/packet/client/LoginPacket.ts +++ b/src/packet/client/LoginPacket.ts @@ -1,8 +1,8 @@ -import {TypedClientPacket, TypedClientPacketStatic} from "../../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"; +import Connection from "../../Connection.js"; @StaticImplements() export default class LoginPacket { @@ -24,7 +24,9 @@ export default class LoginPacket { } } - execute(_conn: Connection, _server: Server): void {} + execute(conn: Connection, _server: Server): void { + conn._setState(Connection.State.LOGIN); + } public static readonly id = 0x00; diff --git a/src/packet/server/DisconnectLoginPacket.ts b/src/packet/server/DisconnectLoginPacket.ts new file mode 100644 index 0000000..ebc0cb1 --- /dev/null +++ b/src/packet/server/DisconnectLoginPacket.ts @@ -0,0 +1,12 @@ +import ServerPacket from "../../ServerPacket.js"; + +export default class DisconnectLoginPacket extends ServerPacket { + public static readonly id = 0x00; + + 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 new file mode 100644 index 0000000..4882d3e --- /dev/null +++ b/src/packet/server/DisconnectPlayPacket.ts @@ -0,0 +1,12 @@ +import ServerPacket from "../../ServerPacket.js"; + +export default class DisconnectPlayPacket extends ServerPacket { + public static readonly id = 0x1a; + + 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 d7e8aeb..fff8b87 100644 --- a/src/packet/server/LoginSuccessPacket.ts +++ b/src/packet/server/LoginSuccessPacket.ts @@ -1,4 +1,5 @@ import ServerPacket from "../../ServerPacket.js"; +import Connection from "../../Connection.js"; /** * A Minecraft protocol client-bound LoginSuccess packet. @@ -14,4 +15,9 @@ export default class LoginSuccessPacket extends ServerPacket { ServerPacket.writeVarInt(0) ])); } + + public override send(connection: Connection) { + connection._setState(Connection.State.PLAY); + return super.send(connection); + } } diff --git a/src/types/ChatComponent.ts b/src/types/ChatComponent.ts new file mode 100644 index 0000000..48335a8 --- /dev/null +++ b/src/types/ChatComponent.ts @@ -0,0 +1,25 @@ +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/TypedEventEmitter.ts b/src/types/TypedEventEmitter.ts similarity index 100% rename from src/TypedEventEmitter.ts rename to src/types/TypedEventEmitter.ts diff --git a/src/TypedPacket.ts b/src/types/TypedPacket.ts similarity index 75% rename from src/TypedPacket.ts rename to src/types/TypedPacket.ts index bcae3e7..23ab06e 100644 --- a/src/TypedPacket.ts +++ b/src/types/TypedPacket.ts @@ -1,6 +1,6 @@ -import Server from "./Server"; -import ParsedPacket from "./ParsedPacket"; -import Connection from "./Connection"; +import Server from "../Server"; +import ParsedPacket from "../ParsedPacket"; +import Connection from "../Connection"; export interface TypedClientPacket { readonly packet: ParsedPacket;