From 273938c2a75f1e2394a6c649e143a70ea0e94e22 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 02:08:21 -0500 Subject: [PATCH 01/13] 1.18 and 1.18.2 support --- .github/workflows/ci.yml | 2 + config/default-settings.json | 2 +- docs/README.md | 2 +- src/index.js | 7 +++ src/lib/plugins/digging.js | 2 +- src/lib/plugins/log.js | 8 ++- src/lib/plugins/login.js | 1 + src/lib/plugins/world.js | 101 ++++++++++++++++++----------------- src/lib/utils.js | 13 +++++ src/lib/version.js | 2 +- test/mineflayer.test.js | 95 +++++++++++++++++++++----------- 11 files changed, 151 insertions(+), 84 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a403a871..b0630ea8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,5 +51,7 @@ jobs: node-version: 18.x - name: Install Dependencies run: npm install + # TODO: Remove this when prismarine-chunk is updated + - run: curl "https://raw.githubusercontent.com/PrismarineJS/prismarine-chunk/refs/heads/pc1.18fixes/src/pc/1.18/ChunkColumn.js" -o node_modules/prismarine-chunk/src/pc/1.18/ChunkColumn.js - name: Start Tests run: npm run mocha_test -- -g ${{ matrix.mcVersion }}v \ No newline at end of file diff --git a/config/default-settings.json b/config/default-settings.json index 17d76aec..f4cbb834 100644 --- a/config/default-settings.json +++ b/config/default-settings.json @@ -25,5 +25,5 @@ }, "everybody-op": false, "max-entities":100, - "version": "1.17.1" + "version": "1.18.2" } diff --git a/docs/README.md b/docs/README.md index 913b3e68..a2478344 100644 --- a/docs/README.md +++ b/docs/README.md @@ -139,7 +139,7 @@ mcServer.createMCServer({ }, 'everybody-op': true, 'max-entities': 100, - 'version': '1.17.1' + 'version': '1.18' }) ``` diff --git a/src/index.js b/src/index.js index e524cb22..646e6f30 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ const { EventEmitter } = require('events') const { testedVersions, latestSupportedVersion, oldestSupportedVersion } = require('./lib/version') const Command = require('./lib/command') const plugins = require('./lib/plugins') +const { onceWithTimeout } = require('./lib/utils') module.exports = { createMCServer, @@ -60,8 +61,14 @@ class MCServer extends EventEmitter { Promise.all(promises).then(() => { this.emit('pluginsReady') this.pluginsReady = true + this.debug?.('Loaded plugins') }) + this.waitForReady = (timeout) => { + if (this.pluginsReady) return Promise.resolve() + return onceWithTimeout(this, 'pluginsReady', timeout) + } + if (options.logging === true) this.createLog() this._server.on('error', error => this.emit('error', error)) this._server.on('listening', () => this.emit('listening', this._server.socketServer.address().port)) diff --git a/src/lib/plugins/digging.js b/src/lib/plugins/digging.js index 01f65714..5b8d06df 100644 --- a/src/lib/plugins/digging.js +++ b/src/lib/plugins/digging.js @@ -1,7 +1,7 @@ const Vec3 = require('vec3').Vec3 module.exports.player = function (player, serv, { version }) { - const { registry } = serv.registry + const { registry } = serv function cancelDig ({ position, block }) { player.sendBlock(position, block.type) } diff --git a/src/lib/plugins/log.js b/src/lib/plugins/log.js index 3c280c4d..e13548ac 100644 --- a/src/lib/plugins/log.js +++ b/src/lib/plugins/log.js @@ -41,6 +41,12 @@ module.exports.server = function (serv, settings) { serv.info(banner.username + ' banned ' + bannedUsername + (reason ? ' (' + reason + ')' : ''))) serv.on('seed', (seed) => serv.info('World seed: ' + seed)) + serv._server.on('close', function () { + serv.info('Server is closed.') + // Remove server from managed list so GC can clean it up + _servers.splice(_servers.indexOf(serv), 1) + }) + const logFile = path.join('logs', timeStarted + '.log') serv.log = message => { @@ -69,7 +75,7 @@ module.exports.server = function (serv, settings) { serv.log('[' + colors.yellow('WARN') + ']: ' + message) } - if (isInNode) { + if (isInNode && !process.env.CI) { console.log = (function () { const orig = console.log return function () { diff --git a/src/lib/plugins/login.js b/src/lib/plugins/login.js index 287623bb..ee31c6e9 100644 --- a/src/lib/plugins/login.js +++ b/src/lib/plugins/login.js @@ -12,6 +12,7 @@ module.exports.server = function (serv, options) { serv._server.on('login', async (client) => { if (!serv.pluginsReady) { client.end('Server is still starting! Please wait before reconnecting.') + serv.info(`[${client.socket.remoteAddress}] ${client.username} (${client.uuid}) disconnected as server is still starting`) return } serv.debug?.(`[login] ${client.socket?.remoteAddress} - ${client.username} (${client.uuid}) connected`, client.version, client.protocolVersion) diff --git a/src/lib/plugins/world.js b/src/lib/plugins/world.js index 1157ba4d..71b37b21 100644 --- a/src/lib/plugins/world.js +++ b/src/lib/plugins/world.js @@ -4,6 +4,7 @@ const generations = require('../generations') const playerDat = require('../playerDat') const spiralloop = require('spiralloop') const { level } = require('prismarine-provider-anvil') +const nbt = require('prismarine-nbt') module.exports.server = async function (serv, options = {}) { const { version, worldFolder, generation = { name: 'diamond_square', options: { worldHeight: 80 } } } = options @@ -30,7 +31,7 @@ module.exports.server = async function (serv, options = {}) { await level.writeLevel(worldFolder + '/level.dat', { RandomSeed: [seed, 0], Version: { Name: options.version }, - generatorName: { superfalt: 'flat', diamond_square: 'default' }[generation.name] || 'customized' + generatorName: { superflat: 'flat', diamond_square: 'default' }[generation.name] || 'customized' }) } } else { @@ -170,52 +171,60 @@ module.exports.player = function (player, serv, settings) { } } - // let chunkSendCtr = 0 player.sendChunk = (chunkX, chunkZ, column) => { - // console.log(chunkSendCtr++, 'Sending chunk at', chunkX, chunkZ, 'to', player.username, chunkX * 16, chunkZ * 16) return player.behavior('sendChunk', { x: chunkX, z: chunkZ, chunk: column }, ({ x, z, chunk }) => { - player._client.write('map_chunk', { - x, - z, - groundUp: true, - bitMap: chunk.getMask(), - biomes: chunk.dumpBiomes(), - ignoreOldData: true, // should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that? - heightmaps: { - type: 'compound', - name: '', - value: { - MOTION_BLOCKING: { type: 'longArray', value: new Array(36).fill([0, 0]) } - } - }, // FIXME: fake heightmap - chunkData: chunk.dump(), - blockEntities: [] + // FIXME: fake heightmap + const heightmaps = nbt.comp({ + MOTION_BLOCKING: nbt.longArray(new Array(36).fill([0, 0])) }) - - if (serv.supportFeature('newLightingDataFormat')) { // 1.17+ - player._client.write('update_light', { - chunkX: x, - chunkZ: z, - trustEdges: true, // trust edges for lighting updates + const trustEdges = true // trust edges for lighting updates - should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that? + if (serv.supportFeature('tallWorld')) { // 1.18+ - merged chunk and light data + player._client.write('map_chunk', { + x, + z, + heightmaps, + chunkData: chunk.dump(), + blockEntities: [], + trustEdges, ...chunk.dumpLight() }) - } else if (serv.supportFeature('lightSentSeparately')) { // -1.16.5 - player._client.write('update_light', { - chunkX: x, - chunkZ: z, - trustEdges: true, // should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that? - skyLightMask: chunk.skyLightMask, - blockLightMask: chunk.blockLightMask, - emptySkyLightMask: 0, - emptyBlockLightMask: 0, - data: chunk.dumpLight() + } else { + player._client.write('map_chunk', { + x, + z, + groundUp: true, + bitMap: chunk.getMask(), + biomes: chunk.dumpBiomes(), + ignoreOldData: true, // should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that? + heightmaps, + chunkData: chunk.dump(), + blockEntities: [] }) + + if (serv.supportFeature('newLightingDataFormat')) { // 1.17+ + player._client.write('update_light', { + chunkX: x, + chunkZ: z, + trustEdges, + ...chunk.dumpLight() + }) + } else if (serv.supportFeature('lightSentSeparately')) { // -1.16.5 + player._client.write('update_light', { + chunkX: x, + chunkZ: z, + trustEdges, + skyLightMask: chunk.skyLightMask, + blockLightMask: chunk.blockLightMask, + emptySkyLightMask: 0, + emptyBlockLightMask: 0, + data: chunk.dumpLight() + }) + } } - return Promise.resolve() }) } @@ -227,7 +236,7 @@ module.exports.player = function (player, serv, settings) { return t } - async function sendNearbyChunks (view, group) { + async function sendNearbyChunks (view) { player.lastPositionChunkUpdated = player.position const playerChunkX = Math.floor(player.position.x / 16) const playerChunkZ = Math.floor(player.position.z / 16) @@ -237,23 +246,17 @@ module.exports.player = function (player, serv, settings) { .filter(([x, z]) => Math.abs(x - playerChunkX) > view || Math.abs(z - playerChunkZ) > view) .forEach(([x, z]) => player._unloadChunk(x, z)) - return spiral([view * 2, view * 2]) + const promises = [] + spiral([view * 2, view * 2]) .map(t => ({ chunkX: playerChunkX + t[0] - view, chunkZ: playerChunkZ + t[1] - view })) .filter(({ chunkX, chunkZ }) => serv._worldLoadPlayerChunk(chunkX, chunkZ, player)) - .reduce((acc, { chunkX, chunkZ }) => { - const p = acc - .then(() => player.world.getColumn(chunkX, chunkZ)) - .then((column) => player.sendChunk(chunkX, chunkZ, column)) - return group ? p.then(() => sleep(5)) : p - } - , Promise.resolve()) - } - - function sleep (ms = 0) { - return new Promise(resolve => setTimeout(resolve, ms)) + .forEach(({ chunkX, chunkZ }) => { + promises.push(player.world.getColumn(chunkX, chunkZ).then((column) => player.sendChunk(chunkX, chunkZ, column))) + }) + return Promise.all(promises) } player.worldSendInitialChunks = () => { diff --git a/src/lib/utils.js b/src/lib/utils.js index 06c471a8..57407ef0 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -4,9 +4,22 @@ function emitAsync (listener, event, ...args) { return Promise.all(listeners.map(listener => listener(...args))) } +function onceWithTimeout (emitter, event, timeout = 10000) { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`Timeout waiting for '${event}' event`)) + }, timeout) + emitter.once(event, (data) => { + clearTimeout(timeoutId) + resolve(data) + }) + }) +} + const skipMcPrefix = (name) => typeof name === 'string' ? name.replace(/^minecraft:/, '') : name module.exports = { + onceWithTimeout, skipMcPrefix, emitAsync } diff --git a/src/lib/version.js b/src/lib/version.js index cf566e43..24a51d8c 100644 --- a/src/lib/version.js +++ b/src/lib/version.js @@ -1,4 +1,4 @@ -const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1'] +const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18', '1.18.2'] module.exports = { testedVersions, latestSupportedVersion: testedVersions[testedVersions.length - 1], diff --git a/test/mineflayer.test.js b/test/mineflayer.test.js index 1a9c7e6c..e355aea3 100644 --- a/test/mineflayer.test.js +++ b/test/mineflayer.test.js @@ -1,5 +1,7 @@ /* eslint-env mocha */ +const fs = require('fs') +const { join } = require('path') const squid = require('flying-squid') const settings = require('../config/default-settings.json') const mineflayer = require('mineflayer') @@ -10,6 +12,10 @@ function assertPosEqual (actual, expected, precision = 1) { expect(actual.distanceTo(expected)).toBeLessThan(precision) } +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + const { once } = require('events') squid.testedVersions.forEach((testedVersion, i) => { @@ -43,7 +49,14 @@ squid.testedVersions.forEach((testedVersion, i) => { expect(msg1.extra[0].text).toEqual(message) } - beforeEach(async () => { + // Clear the world dir before each test + const worldFolder = 'world/test_' + testedVersion + const dir = join(__dirname, '../', worldFolder) + console.log('Clearing world dir', dir) + fs.rmSync(dir, { recursive: true, force: true }) + + beforeEach(async function () { + console.log('🔻 Running test: ' + this.currentTest.title) const options = settings options['online-mode'] = false options['everybody-op'] = true @@ -59,6 +72,8 @@ squid.testedVersions.forEach((testedVersion, i) => { } } + options.worldFolder = worldFolder + options.debug = console.log serv = squid.createMCServer(options) if (registry.supportFeature('entityCamelCase')) { entityName = 'EnderDragon' @@ -66,8 +81,10 @@ squid.testedVersions.forEach((testedVersion, i) => { entityName = 'ender_dragon' } - await once(serv, 'listening') - const port = serv._server.socketServer.address().port + console.log('[test] Waiting for server to start') + const [port] = await once(serv, 'listening') + await serv.waitForReady() + console.log('[test] Server is started on', port, version.minecraftVersion) bot = mineflayer.createBot({ host: 'localhost', port, @@ -81,50 +98,69 @@ squid.testedVersions.forEach((testedVersion, i) => { version: version.minecraftVersion }) - await Promise.all([once(bot, 'login'), once(bot2, 'login')]) + await Promise.all([once(bot, 'login'), once(bot2, 'login'), waitForReady(bot), waitForReady(bot2)]) bot.entity.onGround = false bot2.entity.onGround = false - }) - afterEach(async () => { - await serv.quit() + // log what the bot is standing on for debugging + const bot1StandingOn = bot.blockAt(bot.entity.position.floored().offset(0, -1, 0)) + const bot2StandingOn = bot2.blockAt(bot2.entity.position.floored().offset(0, -1, 0)) + console.log('bot1 is standing on', bot1StandingOn) + console.log('bot2 is standing on', bot2StandingOn) }) - function waitSpawnZone (bot, view) { - const nbChunksExpected = (view * 2) * (view * 2) - let c = 0 - return new Promise(resolve => { - const listener = () => { - c++ - if (c === nbChunksExpected) { - bot.removeListener('chunkColumnLoad', listener) + function waitForReady (bot) { + const viewDistance = 2 + const testExpectedNoChunks = (viewDistance * 2) * (viewDistance * 2) + return new Promise((resolve) => { + let recvChunks = 0 + function onColumnLoad () { + recvChunks++ + if (recvChunks === testExpectedNoChunks) { + bot.removeListener('chunkColumnLoad', onColumnLoad) resolve() } } - bot.on('chunkColumnLoad', listener) + bot.on('chunkColumnLoad', onColumnLoad) }) } + afterEach(async () => { + console.log('Quitting server...') + await serv.quit() + console.log('Quit server!') + }) + describe('actions', () => { - it('can dig', async () => { - await Promise.all([waitSpawnZone(bot, 2), waitSpawnZone(bot2, 2), onGround(bot), onGround(bot2)]) + // Log the name of the test being run + beforeEach(function () { + console.log('🔻 Running actions test: ' + this.currentTest.title) + }) + it('can dig', async () => { const pos = bot.entity.position.offset(0, -1, 0).floored() - const p = once(bot2, 'blockUpdate', { array: true }) + // Set a dirt block below the bot so we can easily dig + bot.chat(`/setblock ${pos.x} ${pos.y} ${pos.z} dirt`) + await sleep(1000) + console.log('Block at', bot.entity.position, bot.blockAt(pos)) + + const p = once(bot2, 'blockUpdate') bot.dig(bot.blockAt(pos)) + console.log('Digging...') const [, newBlock] = await p + console.log('Dug.') assertPosEqual(newBlock.position, pos) expect(newBlock.type).toEqual(0) }) it('can place a block', async () => { - await Promise.all([waitSpawnZone(bot, 2), waitSpawnZone(bot2, 2), onGround(bot), onGround(bot2)]) - const pos = bot.entity.position.offset(0, -2, 0).floored() - const digPromise = once(bot2, 'blockUpdate', { array: true }) + const digPromise = once(bot2, 'blockUpdate') bot.dig(bot.blockAt(pos)) + console.log(' ✔️ dug block at', pos) + let [, newBlock] = await digPromise assertPosEqual(newBlock.position, pos) expect(newBlock.type).toEqual(0) @@ -137,16 +173,17 @@ squid.testedVersions.forEach((testedVersion, i) => { bot.creative.setInventorySlot(36, new Item(1, 1)) await invPromise + console.log(' ✔️ updated inventory') + const placePromise = once(bot2, 'blockUpdate', { array: true }) bot.placeBlock(bot.blockAt(pos.offset(0, -1, 0)), new Vec3(0, 1, 0)); [, newBlock] = await placePromise + console.log(' ✔️ placed block at', pos) assertPosEqual(newBlock.position, pos) expect(newBlock.type).toEqual(1) }) it('can open and close a chest', async () => { - await Promise.all([waitSpawnZone(bot, 2), onGround(bot), waitSpawnZone(bot2, 2), onGround(bot2)]) - const chestId = registry.blocksByName.chest.id const [x, y, z] = [1, 2, 3] @@ -188,8 +225,11 @@ squid.testedVersions.forEach((testedVersion, i) => { }) describe('commands', () => { + beforeEach(function () { + console.log('🔻 Running commands test: ' + this.currentTest.title) + }) + it('has an help command', async () => { - await waitMessagePromise('bot joined the game.') bot.chat('/help') await once(bot, 'message') }) @@ -243,14 +283,12 @@ squid.testedVersions.forEach((testedVersion, i) => { assertPosEqual(bot2.entity.position, bot.entity.position) }) it('can tp with relative positions', async () => { - await onGround(bot) const initialPosition = bot.entity.position.clone() bot.chat('/tp ~1 ~-2 ~3') await once(bot, 'forcedMove') assertPosEqual(bot.entity.position, initialPosition.offset(1, -2, 3), 2) }) it('can tp somebody else with relative positions', async () => { - await Promise.all([onGround(bot), onGround(bot2)]) const initialPosition = bot2.entity.position.clone() bot.chat('/tp bot2 ~1 ~-2 ~3') await once(bot2, 'forcedMove') @@ -258,7 +296,6 @@ squid.testedVersions.forEach((testedVersion, i) => { }) }) it('can use /deop', async () => { - await waitMessagePromise('bot joined the game.') bot.chat('/deop bot') await waitMessage(bot, '§7§o[Server: Deopped bot]') bot.chat('/op bot') @@ -266,7 +303,6 @@ squid.testedVersions.forEach((testedVersion, i) => { serv.getPlayer('bot').op = true }) it('can use /setblock', async () => { - await Promise.all([waitSpawnZone(bot, 2), onGround(bot)]) const chestId = registry.blocksByName.chest.id const p = once(bot, 'blockUpdate:' + new Vec3(1, 2, 3), { array: true }) bot.chat(`/setblock 1 2 3 ${chestId} 0`) @@ -301,7 +337,6 @@ squid.testedVersions.forEach((testedVersion, i) => { } it('can use /banlist, /ban, /pardon', async () => { - await waitMessagePromise('bot joined the game.') bot.chat('/banlist') await waitMessagePromise('There are 0 total banned players') bot.chat('/ban bot2') From fa9df6c891f81945d4547d412907af0a6b059e81 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 02:27:46 -0500 Subject: [PATCH 02/13] test fix --- src/lib/plugins/errorHandler.js | 1 + src/lib/plugins/world.js | 2 +- src/lib/utils.js | 3 ++- test/mineflayer.test.js | 11 ++++++++--- test/portal.test.js | 2 +- test/simple.test.js | 2 +- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/lib/plugins/errorHandler.js b/src/lib/plugins/errorHandler.js index 4efd8509..d3f804bc 100644 --- a/src/lib/plugins/errorHandler.js +++ b/src/lib/plugins/errorHandler.js @@ -1,4 +1,5 @@ module.exports.player = async function (player, serv) { + if (globalThis.isMocha || serv.debug) return // Don't eat errors when debugging function unhandledRejection (promise) { serv.warn('-------------------------------') serv.warn('Please report this error to flying-squid! This is can be bug') diff --git a/src/lib/plugins/world.js b/src/lib/plugins/world.js index 71b37b21..6d6ca1ed 100644 --- a/src/lib/plugins/world.js +++ b/src/lib/plugins/world.js @@ -189,7 +189,7 @@ module.exports.player = function (player, serv, settings) { heightmaps, chunkData: chunk.dump(), blockEntities: [], - trustEdges, + trustEdges, ...chunk.dumpLight() }) } else { diff --git a/src/lib/utils.js b/src/lib/utils.js index 57407ef0..280f9570 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -4,12 +4,13 @@ function emitAsync (listener, event, ...args) { return Promise.all(listeners.map(listener => listener(...args))) } -function onceWithTimeout (emitter, event, timeout = 10000) { +function onceWithTimeout (emitter, event, timeout = 10000, checkCondition) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Timeout waiting for '${event}' event`)) }, timeout) emitter.once(event, (data) => { + if (checkCondition && !checkCondition(data)) return clearTimeout(timeoutId) resolve(data) }) diff --git a/test/mineflayer.test.js b/test/mineflayer.test.js index e355aea3..7305462a 100644 --- a/test/mineflayer.test.js +++ b/test/mineflayer.test.js @@ -1,11 +1,12 @@ /* eslint-env mocha */ - +globalThis.isMocha = true const fs = require('fs') const { join } = require('path') const squid = require('flying-squid') const settings = require('../config/default-settings.json') const mineflayer = require('mineflayer') const { Vec3 } = require('vec3') +const { onceWithTimeout } = require('../src/lib/utils') const expect = require('expect').default function assertPosEqual (actual, expected, precision = 1) { @@ -45,8 +46,12 @@ squid.testedVersions.forEach((testedVersion, i) => { } async function waitMessage (bot, message) { - const [msg1] = await once(bot, 'message') - expect(msg1.extra[0].text).toEqual(message) + // const [msg1] = await once(bot, 'message') + // expect(msg1.extra[0].text).toEqual(message) + onceWithTimeout(bot, 'message', 5000, (msg) => { + console.log('*msg', msg) + return msg.toString() === message + }) } // Clear the world dir before each test diff --git a/test/portal.test.js b/test/portal.test.js index 0bbc5c6d..9d7434d4 100644 --- a/test/portal.test.js +++ b/test/portal.test.js @@ -1,5 +1,5 @@ /* eslint-env mocha */ - +globalThis.isMocha = true const squid = require('flying-squid') const PortalDetector = require('../src/lib/portal_detector') const expect = require('expect').default diff --git a/test/simple.test.js b/test/simple.test.js index 30c91dfa..6b7bf414 100644 --- a/test/simple.test.js +++ b/test/simple.test.js @@ -1,5 +1,5 @@ /* eslint-env mocha */ - +globalThis.isMocha = true const net = require('net') const squid = require('flying-squid') From c21bf20032322e084fdbca19a827d9420bcae7fb Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 03:04:38 -0500 Subject: [PATCH 03/13] wait for spawn event in tests over login --- test/mineflayer.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mineflayer.test.js b/test/mineflayer.test.js index 7305462a..4f6fd460 100644 --- a/test/mineflayer.test.js +++ b/test/mineflayer.test.js @@ -103,7 +103,7 @@ squid.testedVersions.forEach((testedVersion, i) => { version: version.minecraftVersion }) - await Promise.all([once(bot, 'login'), once(bot2, 'login'), waitForReady(bot), waitForReady(bot2)]) + await Promise.all([once(bot, 'spawn'), once(bot2, 'spawn'), waitForReady(bot), waitForReady(bot2)]) bot.entity.onGround = false bot2.entity.onGround = false From 5e06a0463cbdd8de07b5ef0c364497ec9659f3f6 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 03:11:34 -0500 Subject: [PATCH 04/13] revert world sendNearbyChunks change --- src/lib/plugins/world.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/lib/plugins/world.js b/src/lib/plugins/world.js index 6d6ca1ed..56613f91 100644 --- a/src/lib/plugins/world.js +++ b/src/lib/plugins/world.js @@ -6,6 +6,10 @@ const spiralloop = require('spiralloop') const { level } = require('prismarine-provider-anvil') const nbt = require('prismarine-nbt') +function sleep (ms = 0) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + module.exports.server = async function (serv, options = {}) { const { version, worldFolder, generation = { name: 'diamond_square', options: { worldHeight: 80 } } } = options const { registry } = serv @@ -236,7 +240,7 @@ module.exports.player = function (player, serv, settings) { return t } - async function sendNearbyChunks (view) { + async function sendNearbyChunks (view, group) { player.lastPositionChunkUpdated = player.position const playerChunkX = Math.floor(player.position.x / 16) const playerChunkZ = Math.floor(player.position.z / 16) @@ -246,17 +250,18 @@ module.exports.player = function (player, serv, settings) { .filter(([x, z]) => Math.abs(x - playerChunkX) > view || Math.abs(z - playerChunkZ) > view) .forEach(([x, z]) => player._unloadChunk(x, z)) - const promises = [] - spiral([view * 2, view * 2]) + return spiral([view * 2, view * 2]) .map(t => ({ chunkX: playerChunkX + t[0] - view, chunkZ: playerChunkZ + t[1] - view })) .filter(({ chunkX, chunkZ }) => serv._worldLoadPlayerChunk(chunkX, chunkZ, player)) - .forEach(({ chunkX, chunkZ }) => { - promises.push(player.world.getColumn(chunkX, chunkZ).then((column) => player.sendChunk(chunkX, chunkZ, column))) - }) - return Promise.all(promises) + .reduce((acc, { chunkX, chunkZ }) => { + const p = acc + .then(() => player.world.getColumn(chunkX, chunkZ)) + .then((column) => player.sendChunk(chunkX, chunkZ, column)) + return group ? p.then(() => sleep(5)) : p + }, Promise.resolve()) } player.worldSendInitialChunks = () => { From e745b80d19259ecf516cac5ee3f4fc20fd60b816 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 16:23:37 -0500 Subject: [PATCH 05/13] 1.19 support --- config/default-settings.json | 2 +- docs/README.md | 2 +- src/index.js | 8 ++-- src/lib/plugins/chat.js | 80 ++++++++++++++++++++++++++++++++++-- src/lib/plugins/digging.js | 18 ++++---- src/lib/plugins/login.js | 29 +++++++++++++ src/lib/plugins/sound.js | 39 +++++++++++++----- src/lib/plugins/spawn.js | 66 ++++++++++------------------- src/lib/plugins/world.js | 3 +- src/lib/version.js | 2 +- 10 files changed, 177 insertions(+), 72 deletions(-) diff --git a/config/default-settings.json b/config/default-settings.json index f4cbb834..640c82e1 100644 --- a/config/default-settings.json +++ b/config/default-settings.json @@ -25,5 +25,5 @@ }, "everybody-op": false, "max-entities":100, - "version": "1.18.2" + "version": "1.19.4" } diff --git a/docs/README.md b/docs/README.md index a2478344..942fe9a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ flying-squid Create Minecraft servers with a powerful, stable, and high level JavaScript API. ## Features -* Support for Minecraft 1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16 and 1.17 +* Support for Minecraft 1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19 * Players can see the world * Players see each other in-game and in tab * Digging diff --git a/src/index.js b/src/index.js index 646e6f30..91f664b1 100644 --- a/src/index.js +++ b/src/index.js @@ -46,13 +46,15 @@ class MCServer extends EventEmitter { const versionData = registry.version if (versionData['>'](latestSupportedVersion)) { - throw new Error(`Server version '${registry?.version}' is not supported. Latest supported version is '${latestSupportedVersion}'.`) + throw new Error(`Server version '${options.version}' is not supported. Latest supported version is '${latestSupportedVersion}'.`) } else if (versionData['<'](oldestSupportedVersion)) { - throw new Error(`Server version '${registry?.version}' is not supported. Oldest supported version is '${oldestSupportedVersion}'.`) + throw new Error(`Server version '${options.version}' is not supported. Oldest supported version is '${oldestSupportedVersion}'.`) } + // internal features until merged into minecraft-data + const customFeatures = {} this.registry = registry - this.supportFeature = registry.supportFeature + this.supportFeature = feature => customFeatures[feature] ?? registry.supportFeature(feature) const promises = [] for (const plugin of plugins.builtinPlugins) { diff --git a/src/lib/plugins/chat.js b/src/lib/plugins/chat.js index 9721f2fc..d79ec5c8 100644 --- a/src/lib/plugins/chat.js +++ b/src/lib/plugins/chat.js @@ -120,6 +120,30 @@ module.exports.server = function (serv) { } module.exports.player = function (player, serv) { + // 1.19+ -- from nmp server example - not implementing chat singing yet, so all messages are sent as system_chat + function handleChatMessage (data) { + const fmtMessage = `<${player.username}> ${data.message}` + serv.broadcast(fmtMessage, { whitelist: serv.players, blacklist: [] }) + // broadcast(fmtMessage, null, player.username) + } + + player._client.on('chat_message', (data) => { + player.behavior('chat', { + message: data.message, + prefix: '<' + player.username + '> ', + text: data.message, + whitelist: serv.players, + blacklist: [], + data + }, ({ data }) => { + handleChatMessage(data) + }) + }) + player._client.on('chat_command', (data) => { + const command = data.command + player.behavior('command', { command }, ({ command }) => player.handleCommand(command)) + }) + player._client.on('chat', ({ message } = {}) => { if (message[0] === '/') { player.behavior('command', { command: message.slice(1) }, ({ command }) => player.handleCommand(command)) @@ -144,8 +168,16 @@ module.exports.player = function (player, serv) { }) player.chat = message => { - if (typeof message === 'string') message = serv.parseClassic(message) - player._client.write('chat', { message: JSON.stringify(message), position: 0, sender: '0' }) + if (serv.supportFeature('signedChat')) { + return player.system(message) + } else { + const chatComponent = typeof message === 'string' ? serv.parseClassic(message) : message + player._client.write('chat', { + message: JSON.stringify(chatComponent), + position: 0, + sender: '0' + }) + } } player.emptyChat = (count = 1) => { @@ -155,7 +187,47 @@ module.exports.player = function (player, serv) { } player.system = message => { - if (typeof message === 'string') message = serv.parseClassic(message) - player._client.write('chat', { message: JSON.stringify(message), position: 2, sender: '0' }) + const chatComponent = typeof message === 'string' ? serv.parseClassic(message) : message + if (serv.supportFeature('signedChat')) { + player._client.write('system_chat', { + content: JSON.stringify(chatComponent), + type: 1, // chat + isActionBar: false + }) + } else { + player._client.write('chat', { + message: JSON.stringify(chatComponent), + position: 2, + sender: '0' + }) + } } + + // const nbt = require('prismarine-nbt') + // function sendBroadcastMessage (server, clients, message, sender) { + // function chatText (text) { + // return serv.supportFeature('chatPacketsUseNbtComponents') + // ? nbt.comp({ text: nbt.string(text) }) + // : JSON.stringify({ text }) + // } + // console.log('sendBroadcastMessage', message, sender, chatText(message)) + // server.writeToClients(clients, 'player_chat', { + // plainMessage: message, + // signedChatContent: '{"text":""}', + // //unsignedChatContent: chatText(message), + // type: 0, + // senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + // senderName: JSON.stringify({ text: 'x' }), + // senderTeam: undefined, + // timestamp: Date.now(), + // salt: 0n, + // signature: serv.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + // previousMessages: [], + // filterType: 0, + // networkName: JSON.stringify({ text: 'v' }) + // }) + // } + // function broadcast (message, exclude, username) { + // sendBroadcastMessage(serv._server, Object.values(serv._server.clients).filter(client => client !== exclude), message, username) + // } } diff --git a/src/lib/plugins/digging.js b/src/lib/plugins/digging.js index 5b8d06df..872b4aff 100644 --- a/src/lib/plugins/digging.js +++ b/src/lib/plugins/digging.js @@ -6,7 +6,7 @@ module.exports.player = function (player, serv, { version }) { player.sendBlock(position, block.type) } - player._client.on('block_dig', async ({ location, status, face }) => { + player._client.on('block_dig', async ({ location, status, face, sequence }) => { if (status === 3 || status === 4) { const heldItem = player.inventory.slots[36 + player.heldItemSlot] if (!heldItem || heldItem.type === -1) return @@ -56,12 +56,12 @@ module.exports.player = function (player, serv, { version }) { if (player.gameMode === 1) { creativeDigging(pos) } else { - startDigging(pos) + startDigging(pos, sequence) } } else if (status === 1 || player.gameMode >= 2) { - cancelDigging(pos) + cancelDigging(pos, sequence) } else if (status === 2) { - completeDigging(pos) + completeDigging(pos, sequence) } } }) @@ -77,7 +77,7 @@ module.exports.player = function (player, serv, { version }) { let expectedDiggingTime let lastDestroyState let currentAnimationId - function startDigging (location) { + function startDigging (location, sequenceId) { serv.entityMaxId++ currentAnimationId = serv.entityMaxId expectedDiggingTime = diggingTime(location) @@ -108,6 +108,7 @@ module.exports.player = function (player, serv, { version }) { } if (serv.supportFeature('acknowledgePlayerDigging')) { player._client.write('acknowledge_player_digging', { + sequenceId, // 1.19 location, block: currentlyDugBlock.stateId, status: 0, @@ -116,7 +117,7 @@ module.exports.player = function (player, serv, { version }) { } } - function cancelDigging (location) { + function cancelDigging (location, sequenceId) { clearInterval(animationInterval) player._writeOthersNearby('block_break_animation', { entityId: currentAnimationId, @@ -125,6 +126,7 @@ module.exports.player = function (player, serv, { version }) { }) if (serv.supportFeature('acknowledgePlayerDigging')) { player._client.write('acknowledge_player_digging', { + sequenceId, // 1.19 location, block: currentlyDugBlock.stateId, status: 1, @@ -133,7 +135,7 @@ module.exports.player = function (player, serv, { version }) { } } - async function completeDigging (location) { + async function completeDigging (location, sequenceId) { clearInterval(animationInterval) const diggingTime = new Date() - startDiggingTime let stop = false @@ -187,6 +189,7 @@ module.exports.player = function (player, serv, { version }) { } if (serv.supportFeature('acknowledgePlayerDigging')) { player._client.write('acknowledge_player_digging', { + sequenceId, // 1.19 location, block: 0, status: 2, @@ -201,6 +204,7 @@ module.exports.player = function (player, serv, { version }) { }) if (serv.supportFeature('acknowledgePlayerDigging')) { player._client.write('acknowledge_player_digging', { + sequenceId, // 1.19 location, block: currentlyDugBlock.stateId, status: 2, diff --git a/src/lib/plugins/login.js b/src/lib/plugins/login.js index ee31c6e9..c3aa73db 100644 --- a/src/lib/plugins/login.js +++ b/src/lib/plugins/login.js @@ -154,25 +154,54 @@ module.exports.player = async function (player, serv, settings) { } function fillTabList () { + if (serv.registry.version['>=']('1.19')) { + // TODO: The structure of player_info changes alot between versions and is messy + // https://github.com/PrismarineJS/minecraft-data/pull/948 will fix some of it but + // merging that will also require updating mineflayer. In the meantime we can skip this + // packet in 1.19+ as it also requires some chat signing key logic to be implemented + return + } player._writeOthers('player_info', { action: 0, data: [{ + // uuid: player.uuid, UUID: player.uuid, name: player.username, properties: player.profileProperties, + // chatSession: undefined, // this was renamed from crypto in 1.19.3 but not applied to old versions + // player: { + // name: player.username, + // properties: player.profileProperties + // }, gamemode: player.gameMode, ping: player._client.latency + // crypto: { // 1.19 + // timestamp: Date.now(), + // publicKey: player._client.profileKeys?.public ?? Buffer.from([0]), + // signature: Buffer.from([0]) + // } }] }) player._client.write('player_info', { action: 0, data: serv.players.map((otherPlayer) => ({ + // uuid: otherPlayer.uuid, UUID: otherPlayer.uuid, name: otherPlayer.username, properties: otherPlayer.profileProperties, gamemode: otherPlayer.gameMode, + // chatSession: undefined, + // player: { + // name: otherPlayer.username, + // properties: otherPlayer.profileProperties + // }, ping: otherPlayer._client.latency + // crypto: { + // timestamp: Date.now(), + // publicKey: Buffer.from([0]), + // signature: Buffer.from([0]) + // } })) }) setInterval(() => player._client.write('player_info', { diff --git a/src/lib/plugins/sound.js b/src/lib/plugins/sound.js index d6573ebd..0e9bb69a 100644 --- a/src/lib/plugins/sound.js +++ b/src/lib/plugins/sound.js @@ -16,16 +16,34 @@ module.exports.server = function (serv, { version }) { .forEach(player => { const iniPos = position ? position.scaled(1 / 32) : player.position.scaled(1 / 32) const pos = iniPos.scaled(8).floored() + if (serv.supportFeature('removedNamedSoundEffectPacket')) { // 1.19.3 removes named_sound_effect + player._client.write('sound_effect', { + soundId: 0, + soundEvent: { + resource: sound, + range: undefined + }, + soundCategory, + x: pos.x, + y: pos.y, + z: pos.z, + volume, + pitch: Math.round(pitch * 63), + seed: 0 + }) + } else { // only packet still in fixed position in all versions - player._client.write('named_sound_effect', { - soundName: sound, - soundCategory, - x: pos.x, - y: pos.y, - z: pos.z, - volume, - pitch: Math.round(pitch * 63) - }) + player._client.write('named_sound_effect', { + soundName: sound, + soundCategory, + x: pos.x, + y: pos.y, + z: pos.z, + volume, + pitch: Math.round(pitch * 63), + seed: 0 + }) + } }) } @@ -49,7 +67,8 @@ module.exports.server = function (serv, { version }) { y: pos.y, z: pos.z, volume, - pitch: Math.round(pitch * 63) + pitch: Math.round(pitch * 63), + seed: 0 }) }) } diff --git a/src/lib/plugins/spawn.js b/src/lib/plugins/spawn.js index 5aa0b5db..c7c50ffd 100644 --- a/src/lib/plugins/spawn.js +++ b/src/lib/plugins/spawn.js @@ -299,7 +299,10 @@ module.exports.entity = function (entity, serv) { if (entity.type === 'player') entity.spawnPacketName = 'named_entity_spawn' else if (entity.type === 'object') entity.spawnPacketName = 'spawn_entity' - else if (entity.type === 'mob') entity.spawnPacketName = 'spawn_entity_living' + else if (entity.type === 'mob') { + if (serv.supportFeature('consolidatedEntitySpawnPacket')) entity.spawnPacketName = 'named_entity_spawn' + else entity.spawnPacketName = 'spawn_entity_living' + } } entity.getSpawnPacket = () => { @@ -316,49 +319,24 @@ module.exports.entity = function (entity, serv) { entityPosition = entity.position } - if (entity.type === 'player') { - return { - entityId: entity.id, - playerUUID: entity.uuid, - x: entityPosition.x, - y: entityPosition.y, - z: entityPosition.z, - yaw: entity.yaw, - pitch: entity.pitch, - currentItem: 0, - metadata: entity.metadata - } - } else if (entity.type === 'object') { - return { - entityId: entity.id, - objectUUID: entity.uuid, - type: entity.entityType, - x: entityPosition.x, - y: entityPosition.y, - z: entityPosition.z, - pitch: entity.pitch, - yaw: entity.yaw, - objectData: entity.data, - velocityX: scaledVelocity.x, - velocityY: scaledVelocity.y, - velocityZ: scaledVelocity.z - } - } else if (entity.type === 'mob') { - return { - entityId: entity.id, - entityUUID: entity.uuid, - type: entity.entityType, - x: entityPosition.x, - y: entityPosition.y, - z: entityPosition.z, - yaw: entity.yaw, - pitch: entity.pitch, - headPitch: entity.headPitch, - velocityX: scaledVelocity.x, - velocityY: scaledVelocity.y, - velocityZ: scaledVelocity.z, - metadata: entity.metadata - } + return { + entityId: entity.id, + playerUUID: entity.uuid, + entityUUID: entity.uuid, + objectUUID: entity.uuid, + type: entity.entityType, + x: entityPosition.x, + y: entityPosition.y, + z: entityPosition.z, + yaw: entity.yaw, + pitch: entity.pitch, + headPitch: entity.headPitch, + currentItem: 0, + objectData: entity.data, + velocityX: scaledVelocity.x, + velocityY: scaledVelocity.y, + velocityZ: scaledVelocity.z, + metadata: entity.metadata } } diff --git a/src/lib/plugins/world.js b/src/lib/plugins/world.js index 56613f91..0a1b4072 100644 --- a/src/lib/plugins/world.js +++ b/src/lib/plugins/world.js @@ -194,6 +194,7 @@ module.exports.player = function (player, serv, settings) { chunkData: chunk.dump(), blockEntities: [], trustEdges, + suppressLightUpdates: trustEdges, // 1.19.2 ...chunk.dumpLight() }) } else { @@ -306,7 +307,7 @@ module.exports.player = function (player, serv, settings) { } player._client.write('respawn', { previousGameMode: player.prevGameMode, - dimension: (serv.supportFeature('dimensionIsAString') || serv.supportFeature('dimensionIsAWorld')) ? serv.dimensionNames[opt.dimension || 0] : opt.dimension || 0, + dimension: serv.registry.loginPacket?.dimension || 0, worldName: serv.dimensionNames[opt.dimension || 0], difficulty: opt.difficulty || serv.difficulty, hashedSeed: serv.hashedSeed, diff --git a/src/lib/version.js b/src/lib/version.js index 24a51d8c..fad473b4 100644 --- a/src/lib/version.js +++ b/src/lib/version.js @@ -1,4 +1,4 @@ -const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18', '1.18.2'] +const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4'] module.exports = { testedVersions, latestSupportedVersion: testedVersions[testedVersions.length - 1], From daac854c2c09a79740630854dc9ff2c9998c39ed Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 16:50:24 -0500 Subject: [PATCH 06/13] fix consolidatedEntitySpawnPacket to use spawn_entity --- src/lib/plugins/chat.js | 4 +++- src/lib/plugins/spawn.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/plugins/chat.js b/src/lib/plugins/chat.js index d79ec5c8..beeaf8db 100644 --- a/src/lib/plugins/chat.js +++ b/src/lib/plugins/chat.js @@ -141,7 +141,9 @@ module.exports.player = function (player, serv) { }) player._client.on('chat_command', (data) => { const command = data.command - player.behavior('command', { command }, ({ command }) => player.handleCommand(command)) + player.behavior('command', { command }, ({ command }) => { + player.handleCommand(command) + }) }) player._client.on('chat', ({ message } = {}) => { diff --git a/src/lib/plugins/spawn.js b/src/lib/plugins/spawn.js index c7c50ffd..341779c0 100644 --- a/src/lib/plugins/spawn.js +++ b/src/lib/plugins/spawn.js @@ -300,7 +300,7 @@ module.exports.entity = function (entity, serv) { if (entity.type === 'player') entity.spawnPacketName = 'named_entity_spawn' else if (entity.type === 'object') entity.spawnPacketName = 'spawn_entity' else if (entity.type === 'mob') { - if (serv.supportFeature('consolidatedEntitySpawnPacket')) entity.spawnPacketName = 'named_entity_spawn' + if (serv.supportFeature('consolidatedEntitySpawnPacket')) entity.spawnPacketName = 'spawn_entity' else entity.spawnPacketName = 'spawn_entity_living' } } From 9a52c1c80ea6f855f81eab47a163e194e74fe0f3 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 22:13:48 -0500 Subject: [PATCH 07/13] fix player_info handling for 1.19 --- src/lib/plugins/login.js | 81 ++++++++++++++++++--------------------- src/lib/plugins/logout.js | 7 +--- test/mineflayer.test.js | 7 +++- 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/lib/plugins/login.js b/src/lib/plugins/login.js index c3aa73db..8cede056 100644 --- a/src/lib/plugins/login.js +++ b/src/lib/plugins/login.js @@ -136,13 +136,24 @@ module.exports.player = async function (player, serv, settings) { }) } - player.setGameMode = (gameMode) => { - if (gameMode !== player.gameMode) player.prevGameMode = player.gameMode - player.gameMode = gameMode - player._client.write('game_state_change', { - reason: 3, - gameMode: player.gameMode + // TODO: The structure of player_info changes alot between versions and is messy + // https://github.com/PrismarineJS/minecraft-data/pull/948 will fix some of it but + // merging that will also require updating mineflayer. In the meantime we can skip this + // packet in 1.19+ as it also requires some chat signing key logic to be implemented + + serv._sendPlayerEventLeave = function (player) { + if (serv.registry.version['>=']('1.19')) return + player._writeOthers('player_info', { + action: 4, + data: [{ + UUID: player.uuid, + uuid: player.uuid // 1.19.3+ + }] }) + } + + serv._sendPlayerEventUpdateGameMode = function (player) { + if (serv.registry.version['>=']('1.19')) return serv._writeAll('player_info', { action: 1, data: [{ @@ -150,67 +161,49 @@ module.exports.player = async function (player, serv, settings) { gamemode: player.gameMode }] }) + } + + player.setGameMode = (gameMode) => { + if (gameMode !== player.gameMode) player.prevGameMode = player.gameMode + player.gameMode = gameMode + player._client.write('game_state_change', { + reason: 3, + gameMode: player.gameMode + }) + serv._sendPlayerEventUpdateGameMode(player) player.sendAbilities() } - function fillTabList () { - if (serv.registry.version['>=']('1.19')) { - // TODO: The structure of player_info changes alot between versions and is messy - // https://github.com/PrismarineJS/minecraft-data/pull/948 will fix some of it but - // merging that will also require updating mineflayer. In the meantime we can skip this - // packet in 1.19+ as it also requires some chat signing key logic to be implemented - return - } + serv._sendPlayerEventNewJoin = function (player) { + if (serv.registry.version['>=']('1.19')) return player._writeOthers('player_info', { action: 0, data: [{ - // uuid: player.uuid, UUID: player.uuid, name: player.username, properties: player.profileProperties, - // chatSession: undefined, // this was renamed from crypto in 1.19.3 but not applied to old versions - // player: { - // name: player.username, - // properties: player.profileProperties - // }, gamemode: player.gameMode, ping: player._client.latency - // crypto: { // 1.19 - // timestamp: Date.now(), - // publicKey: player._client.profileKeys?.public ?? Buffer.from([0]), - // signature: Buffer.from([0]) - // } }] }) + } - player._client.write('player_info', { + serv._sendPlayerList = function (toPlayer) { + if (serv.registry.version['>=']('1.19')) return + toPlayer._writeOthers('player_info', { action: 0, data: serv.players.map((otherPlayer) => ({ - // uuid: otherPlayer.uuid, UUID: otherPlayer.uuid, name: otherPlayer.username, properties: otherPlayer.profileProperties, gamemode: otherPlayer.gameMode, - // chatSession: undefined, - // player: { - // name: otherPlayer.username, - // properties: otherPlayer.profileProperties - // }, ping: otherPlayer._client.latency - // crypto: { - // timestamp: Date.now(), - // publicKey: Buffer.from([0]), - // signature: Buffer.from([0]) - // } })) }) - setInterval(() => player._client.write('player_info', { - action: 2, - data: serv.players.map(otherPlayer => ({ - UUID: otherPlayer.uuid, - ping: otherPlayer._client.latency - })) - }), 5000) + } + + function fillTabList () { + serv._sendPlayerList(player) } function announceJoin () { diff --git a/src/lib/plugins/logout.js b/src/lib/plugins/logout.js index fb1660f4..3a6a8466 100644 --- a/src/lib/plugins/logout.js +++ b/src/lib/plugins/logout.js @@ -20,12 +20,7 @@ module.exports.player = function (player, serv, { worldFolder }) { if (player && player.username) { player._unloadAllChunks() serv.broadcast(serv.color.yellow + player.username + ' left the game.') - player._writeOthers('player_info', { - action: 4, - data: [{ - UUID: player.uuid - }] - }) + serv._sendPlayerEventLeave(player) player.nearbyPlayers().forEach(otherPlayer => otherPlayer.despawnEntities([player])) delete serv.entities[player.id] player.emit('disconnected') diff --git a/test/mineflayer.test.js b/test/mineflayer.test.js index 4f6fd460..aad10db3 100644 --- a/test/mineflayer.test.js +++ b/test/mineflayer.test.js @@ -244,7 +244,12 @@ squid.testedVersions.forEach((testedVersion, i) => { }) it('can use /playsound', async () => { bot.chat('/playsound ambient.weather.rain') - await once(bot, 'soundEffectHeard') + // TODO: why are there two events for this as opposed to one with extra fields? + if (serv.supportFeature('removedNamedSoundEffectPacket')) { // 1.19.3+ + await once(bot, 'hardcodedSoundEffectHeard') + } else { + await once(bot, 'soundEffectHeard') + } }) function waitDragon () { From 0c1963f083894bc77edef7fdceb9ac59d1a088b1 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 22:18:54 -0500 Subject: [PATCH 08/13] cleanup --- src/lib/plugins/chat.js | 29 ----------------------------- test/mineflayer.test.js | 11 +++++------ 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/lib/plugins/chat.js b/src/lib/plugins/chat.js index beeaf8db..4aacf0b7 100644 --- a/src/lib/plugins/chat.js +++ b/src/lib/plugins/chat.js @@ -124,7 +124,6 @@ module.exports.player = function (player, serv) { function handleChatMessage (data) { const fmtMessage = `<${player.username}> ${data.message}` serv.broadcast(fmtMessage, { whitelist: serv.players, blacklist: [] }) - // broadcast(fmtMessage, null, player.username) } player._client.on('chat_message', (data) => { @@ -204,32 +203,4 @@ module.exports.player = function (player, serv) { }) } } - - // const nbt = require('prismarine-nbt') - // function sendBroadcastMessage (server, clients, message, sender) { - // function chatText (text) { - // return serv.supportFeature('chatPacketsUseNbtComponents') - // ? nbt.comp({ text: nbt.string(text) }) - // : JSON.stringify({ text }) - // } - // console.log('sendBroadcastMessage', message, sender, chatText(message)) - // server.writeToClients(clients, 'player_chat', { - // plainMessage: message, - // signedChatContent: '{"text":""}', - // //unsignedChatContent: chatText(message), - // type: 0, - // senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random - // senderName: JSON.stringify({ text: 'x' }), - // senderTeam: undefined, - // timestamp: Date.now(), - // salt: 0n, - // signature: serv.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), - // previousMessages: [], - // filterType: 0, - // networkName: JSON.stringify({ text: 'v' }) - // }) - // } - // function broadcast (message, exclude, username) { - // sendBroadcastMessage(serv._server, Object.values(serv._server.clients).filter(client => client !== exclude), message, username) - // } } diff --git a/test/mineflayer.test.js b/test/mineflayer.test.js index aad10db3..539a6912 100644 --- a/test/mineflayer.test.js +++ b/test/mineflayer.test.js @@ -244,12 +244,11 @@ squid.testedVersions.forEach((testedVersion, i) => { }) it('can use /playsound', async () => { bot.chat('/playsound ambient.weather.rain') - // TODO: why are there two events for this as opposed to one with extra fields? - if (serv.supportFeature('removedNamedSoundEffectPacket')) { // 1.19.3+ - await once(bot, 'hardcodedSoundEffectHeard') - } else { - await once(bot, 'soundEffectHeard') - } + // TODO: why are there 2 mineflayer events for this as opposed to one with extra fields? + await once(bot, serv.supportFeature('removedNamedSoundEffectPacket') + ? 'hardcodedSoundEffectHeard' // 1.19.3+ + : 'soundEffectHeard' + ) }) function waitDragon () { From 460878324d337763f89d2177a4d9c53813ed794c Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 22:21:24 -0500 Subject: [PATCH 09/13] mocha --retry 2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47a62f5d..43ac6093 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "prepublishOnly": "cp docs/README.md README.md", "lint": "standard", "fix": "standard --fix", - "mocha_test": "mocha --reporter spec --timeout 30000 --exit", + "mocha_test": "mocha --reporter spec --timeout 30000 --retry 2 --exit", "test": "npm run mocha_test", "pretest": "npm run lint" }, From 0190402b046da5a7fe980a363d7f719c30d9e3ea Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 22:39:55 -0500 Subject: [PATCH 10/13] typo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43ac6093..600d2205 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "prepublishOnly": "cp docs/README.md README.md", "lint": "standard", "fix": "standard --fix", - "mocha_test": "mocha --reporter spec --timeout 30000 --retry 2 --exit", + "mocha_test": "mocha --reporter spec --timeout 30000 --retries 2 --exit", "test": "npm run mocha_test", "pretest": "npm run lint" }, From 00e1e17c0ea92f1305d896598cfd7dbc532720a6 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 2 Jan 2025 22:52:36 -0500 Subject: [PATCH 11/13] bump mocha --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 600d2205..cf508686 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "flying-squid": "file:.", "minecraft-wrap": "^1.2.3", "mineflayer": "^4.17.0", - "mocha": "^10.0.0", + "mocha": "^11.0.1", "standard": "^17.0.0" } } From 19a5dc42848fe54e811d98cb5be5238191e04834 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 4 Jan 2025 13:24:07 -0500 Subject: [PATCH 12/13] Update ci.yml --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3aaac3e..c18669d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,5 @@ jobs: node-version: 18.x - name: Install Dependencies run: npm install - # TODO: Remove this when prismarine-chunk is updated - - run: curl "https://raw.githubusercontent.com/PrismarineJS/prismarine-chunk/refs/heads/pc1.18fixes/src/pc/1.18/ChunkColumn.js" -o node_modules/prismarine-chunk/src/pc/1.18/ChunkColumn.js - name: Start Tests run: npm run mocha_test -- -g ${{ matrix.mcVersion }}v From da78e22c24a541c7ac4575c8bb78b011bdd75164 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 4 Jan 2025 14:00:22 -0500 Subject: [PATCH 13/13] Update login.js re-add player_info with latency for 1.17- --- src/lib/plugins/login.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/plugins/login.js b/src/lib/plugins/login.js index 8cede056..6ae5f055 100644 --- a/src/lib/plugins/login.js +++ b/src/lib/plugins/login.js @@ -204,6 +204,15 @@ module.exports.player = async function (player, serv, settings) { function fillTabList () { serv._sendPlayerList(player) + if (serv.registry.version['<=']('1.18')) { + setInterval(() => player._client.write('player_info', { + action: 2, + data: serv.players.map(otherPlayer => ({ + UUID: otherPlayer.uuid, + ping: otherPlayer._client.latency + })) + }), 5000) + } } function announceJoin () {