From c11dff3b2a9fe4bbe5f85c54e07c2e9ae7a8c288 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 4 Jan 2025 14:13:08 -0500 Subject: [PATCH] 1.19 (#663) * 1.18 and 1.18.2 support * test fix * wait for spawn event in tests over login * revert world sendNearbyChunks change * 1.19 support * fix consolidatedEntitySpawnPacket to use spawn_entity * fix player_info handling for 1.19 * cleanup * mocha --retry 2 * typo * bump mocha * Update ci.yml * Update login.js re-add player_info with latency for 1.17- --- config/default-settings.json | 2 +- docs/README.md | 2 +- package.json | 2 +- src/index.js | 8 +++-- src/lib/plugins/chat.js | 53 ++++++++++++++++++++++++++--- src/lib/plugins/digging.js | 18 ++++++---- src/lib/plugins/login.js | 61 +++++++++++++++++++++++++-------- src/lib/plugins/logout.js | 7 +--- src/lib/plugins/sound.js | 39 +++++++++++++++------ src/lib/plugins/spawn.js | 66 ++++++++++++------------------------ src/lib/plugins/world.js | 3 +- src/lib/version.js | 2 +- test/mineflayer.test.js | 6 +++- 13 files changed, 174 insertions(+), 95 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 89d1dc60..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, 1.17 and 1.18 +* 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/package.json b/package.json index 2fd6219b..cf508686 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 --retries 2 --exit", "test": "npm run mocha_test", "pretest": "npm run lint" }, 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..4aacf0b7 100644 --- a/src/lib/plugins/chat.js +++ b/src/lib/plugins/chat.js @@ -120,6 +120,31 @@ 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: [] }) + } + + 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 +169,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 +188,19 @@ 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' + }) + } } } 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..6ae5f055 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,10 +161,21 @@ 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 () { + serv._sendPlayerEventNewJoin = function (player) { + if (serv.registry.version['>=']('1.19')) return player._writeOthers('player_info', { action: 0, data: [{ @@ -164,8 +186,11 @@ module.exports.player = async function (player, serv, settings) { ping: player._client.latency }] }) + } - 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, @@ -175,13 +200,19 @@ module.exports.player = async function (player, serv, settings) { ping: otherPlayer._client.latency })) }) - 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) + 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 () { 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/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..341779c0 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 = 'spawn_entity' + 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], diff --git a/test/mineflayer.test.js b/test/mineflayer.test.js index 4f6fd460..539a6912 100644 --- a/test/mineflayer.test.js +++ b/test/mineflayer.test.js @@ -244,7 +244,11 @@ squid.testedVersions.forEach((testedVersion, i) => { }) it('can use /playsound', async () => { bot.chat('/playsound ambient.weather.rain') - 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 () {