diff --git a/README.md b/README.md index 98f697a8..817b35c9 100644 --- a/README.md +++ b/README.md @@ -44,4 +44,4 @@ It's recommended to also install `pm2` for process auto-restart on crashes and s 0. Clone, setup Node.js (at least v18.6.0) 1. Install dependencies: `npm install` or `pnpm install` 2. Run `npm run dev`, `npm run start` for without watch or `npm run watch` for watch mode (for prismarine-web-client) -2.1. If using [Bun](https://bun.sh) (recommended) instead: `bun --watch src/app.js` or `bun --hot src/app.js` (preview) +2.1. If using [Bun](https://bun.sh) (experimental) instead: `bun --watch src/app.js` or `bun --hot src/app.js` (preview) diff --git a/package.json b/package.json index 74cc39a2..b45cbd49 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "colors": "1.4.0", "diamond-square": "^1.2.0", "emit-then": "^2.0.0", - "@tootallnate/once": "^3.0.0", + "@tootallnate/once": "2.0.0", "exit-hook": "^2.2.1", "flatmap": "^0.0.3", "long": "^5.1.0", @@ -77,7 +77,7 @@ "mineflayer": "^4.0.0", "mocha": "^10.0.0", "pkg": "^5.8.1", - "typescript": "^5.3.3", + "typescript": "^5.4.5", "vitest": "^0.34.6" }, "pnpm": { diff --git a/src/index.ts b/src/index.ts index 7f7d980d..d42b3e01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,6 +47,7 @@ export { supportedVersions } class MCServer extends EventEmitter { pluginsReady = false + private abortController = new AbortController() constructor() { modules.initPlugins() super() @@ -54,8 +55,13 @@ class MCServer extends EventEmitter { connect (options: Options) { const server = this as unknown as Server + server.abortSignal = this.abortController.signal + server.cleanupFunctions = [ + () => this.abortController.abort() + ] const mcData = require('minecraft-data')(options.version) server.mcData = mcData + if (mcData === null) throw new Error(`Version ${options.version} is not supported as it doesn't have the data.`) const version = mcData.version if (!supportedVersions.some(v => v.includes(version.majorVersion))) { console.warn(`Version ${version.minecraftVersion} is not supported.`) @@ -92,13 +98,20 @@ class MCServer extends EventEmitter { } } +type Cleanup = () => void +type MaybePromise<T> = T | Promise<T> + declare global { interface Server { commands: Command pluginsReady: boolean _server: ProtocolServer supportFeature: IndexedData['supportFeature'] + abortSignal: AbortSignal + cleanupFunctions: Cleanup[] } + + type ServerModule = (server: Server, options: Options) => MaybePromise<void | Cleanup> } export * as Behavior from './lib/behavior' diff --git a/src/lib/modules/chat.ts b/src/lib/modules/chat.ts index bc953a27..dd2050ca 100644 --- a/src/lib/modules/chat.ts +++ b/src/lib/modules/chat.ts @@ -173,35 +173,35 @@ declare global { "broadcast": (message: any, opt?: { whitelist?: any; blacklist?: any[]; system?: boolean }) => void /** @internal */ "color": { - black: string; - dark_blue: string; - dark_green: string; - dark_cyan: string; - dark_red: string; - purple: string; - dark_purple: string; - gold: string; - gray: string; - grey: string; - dark_gray: string; - dark_grey: string; - blue: string; - green: string; - aqua: string; - cyan: string; - red: string; - pink: string; - light_purple: string; - yellow: string; - white: string; - random: string; - obfuscated: string; - bold: string; - strikethrough: string; - underlined: string; - underline: string; - italic: string; - italics: string; + black: string + dark_blue: string + dark_green: string + dark_cyan: string + dark_red: string + purple: string + dark_purple: string + gold: string + gray: string + grey: string + dark_gray: string + dark_grey: string + blue: string + green: string + aqua: string + cyan: string + red: string + pink: string + light_purple: string + yellow: string + white: string + random: string + obfuscated: string + bold: string + strikethrough: string + underlined: string + underline: string + italic: string + italics: string reset: string } /** @internal */ diff --git a/src/lib/modules/login.ts b/src/lib/modules/login.ts index c89def0f..91c8fc97 100644 --- a/src/lib/modules/login.ts +++ b/src/lib/modules/login.ts @@ -68,7 +68,7 @@ export const player = async function (player: Player, serv: Server, settings: Op await player.findSpawnPoint() player.setLoadingStatus('Reading player data') - playerData = await playerDat.read(player.uuid, player.spawnPoint, settings.worldFolder) + playerData = await playerDat.read(player.uuid, player.spawnPoint, settings.worldFolder ?? false) Object.keys(playerData.player).forEach(k => { player[k] = playerData.player[k] }) serv.players.push(player) @@ -309,8 +309,7 @@ export const player = async function (player: Player, serv: Server, settings: Op player.sendRestMap() sendChunkWhenMove() - if (playerData.new) { - // skip unnecessary fs operation + if (playerData.new) { // otherwise we skip unnecessary fs operation player.save() } } diff --git a/src/lib/modules/logout.ts b/src/lib/modules/logout.ts index b692d7a3..db724fc0 100644 --- a/src/lib/modules/logout.ts +++ b/src/lib/modules/logout.ts @@ -2,6 +2,7 @@ import once from '@tootallnate/once' export const server = function (serv: Server) { serv.quit = async (reason = 'Server closed') => { + serv.cleanupFunctions.forEach(fn => fn()) await Promise.all(serv.players.map((player) => { player.kick(reason) return once(player, 'disconnected') @@ -41,7 +42,9 @@ export const player = function (player: Player, serv: Server, { worldFolder }: O } declare global { interface Server { - /** @internal */ + /** + * Stop and dispose the server + */ "quit": (reason?: string) => Promise<void> } interface Player { diff --git a/src/lib/modules/world.ts b/src/lib/modules/world.ts index 03fab3dd..253d7d71 100644 --- a/src/lib/modules/world.ts +++ b/src/lib/modules/world.ts @@ -18,7 +18,7 @@ import { generateSpiralMatrix } from '../../utils' const fsStat = promisify(fs.stat) const fsMkdir = promisify(fs.mkdir) -export const server = async function (serv: Server, options: Options) { +export const server: ServerModule = async function (serv, options) { const { version, worldSaveVersion: _worldSaveVersion, worldFolder, generation = { name: 'diamond_square', options: { worldHeight: 80 } } } = options generation.options.worldHeight = serv.supportFeature('tallWorld') ? 384 : 256 generation.options.minY = serv.supportFeature('tallWorld') ? -64 : 0 @@ -74,6 +74,12 @@ export const server = async function (serv: Server, options: Options) { serv.netherworld = new World(generations.nether(generationOptions)) as CustomWorld // serv.endworld = new World(generations["end"]({})); + serv.worlds = { + 'overworld': serv.overworld, + 'nether': serv.netherworld + // 'end': serv.endworld + } + serv.dimensionNames = { '-1': 'minecraft:nether', 0: 'minecraft:overworld' @@ -264,6 +270,13 @@ export const server = async function (serv: Server, options: Options) { ctx.player!.world.blockEntityData[key] = blockEntities[key] }, }) + + return () => { + for (const world of Object.values(serv.worlds)) { + // world.throwOnReadWrite = true + world.stopSaving() + } + } } export const player = function (player: Player, serv: Server, settings: Options) { @@ -276,6 +289,7 @@ export const player = function (player: Player, serv: Server, settings: Options) }) player.save = async () => { + if (!settings.worldFolder) return return await playerDat.save(player, settings.worldFolder, serv.supportFeature('attributeSnakeCase'), serv.supportFeature('theFlattening')) } @@ -499,6 +513,7 @@ declare global { spawnPoint?: Vec3 /** Parsed level.dat of the loaded world (only if worldFolder is specificed) */ levelData?: LevelDatFull + worlds: Record<string, CustomWorld> /** Contains the overworld world. This is where the default spawn point is */ "overworld": CustomWorld /** Contains the nether world. This **WILL** be used when a player travels through a portal if they are in the overworld! */ diff --git a/src/lib/playerDat.js b/src/lib/playerDat.js index 68f6e41e..622fc0a4 100644 --- a/src/lib/playerDat.js +++ b/src/lib/playerDat.js @@ -16,7 +16,22 @@ const playerDefaults = { heldItemSlot: 0 } +/** + * @param {string} uuid + * @param {Vec3} spawnPoint + * @param {string | false} worldFolder + */ async function read (uuid, spawnPoint, worldFolder) { + const newPlayerData = () => { + return { + player: { ...playerDefaults, ...{ position: spawnPoint.clone() } }, + inventory: [], + new: true + } + } + + if (!worldFolder) return newPlayerData() + try { const playerDataFile = await promises.readFile(`${worldFolder}/playerdata/${uuid}.dat`) /** @type {any} */ @@ -41,11 +56,7 @@ async function read (uuid, spawnPoint, worldFolder) { new: false } } catch (e) { - return { - player: { ...playerDefaults, ...{ position: spawnPoint.clone() } }, - inventory: [], - new: true - } + return newPlayerData() } } @@ -86,6 +97,12 @@ function playerInventoryToNBT (playerInventory, theFlattening) { return nbtInventory } +/** + * @param {Player} player + * @param {string | undefined} worldFolder + * @param {boolean} snakeCase + * @param {boolean} theFlattening + */ async function save (player, worldFolder, snakeCase, theFlattening) { if (worldFolder === undefined) { return diff --git a/src/lib/requireindex.js b/src/lib/requireindex.js index bd7c24c7..3a82e35a 100644 --- a/src/lib/requireindex.js +++ b/src/lib/requireindex.js @@ -43,7 +43,7 @@ module.exports = function (dir, basenames) { const stats = fs.statSync(filepath) // Don't require non-javascript files (.txt, .md, etc.) - if (stats.isFile() && !(['.js', '.node', '.json', '.ts', '.tsx'].includes(ext))) { + if (['.d.ts'].some(ext => filename.endsWith(ext)) || (stats.isFile() && !(['.js', '.node', '.json', '.ts', '.tsx'].includes(ext)))) { return } diff --git a/src/modules.ts b/src/modules.ts index 29f4f88b..2e55c6d0 100644 --- a/src/modules.ts +++ b/src/modules.ts @@ -65,7 +65,7 @@ declare global { logging?: boolean gameMode?: number difficulty?: number - worldFolder?: string + worldFolder?: string | false generation?: { name: string options: { @@ -91,5 +91,9 @@ declare global { "max-entities": number noConsoleOutput?: boolean savingInterval?: number | false + /** + * @jsOnly + */ + oldServerInstance?: Server } }