* Initial changes for 1.20.2

* add NBT serialize tag type handling

* update tests

* Update pnbt and mcdata for nbt change

* lint

* fix wrong param to sizeOfNbt

* fix dupe NBT types

* move nbt logic to prismarine-nbt

* update tests

* update tests

* disable protodef validator in pluginChannel

* Fix state desync

* dump loginPacket.json in test output

* enable validation

* remove testing line in ci.yml

* update pnbt to 2.5.0

* update doc for `playerJoin`

* Update serializer.js

* update examples

* lint

* disable client bundle handling if bundle becomes too big

* Update client.js

* bump mcdata

* add soundSource and packedChunkPos example test values

---------

Co-authored-by: Romain Beaumont <romain.rom1@gmail.com>
This commit is contained in:
extremeheat 2023-12-27 18:48:10 -05:00 committed by GitHub
parent 1740124c47
commit 112926da0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 212 additions and 178 deletions

View file

@ -89,6 +89,11 @@ Called when a client connects, but before any login has happened. Takes a
Called when a client is logged in against server. Takes a `Client` parameter. Called when a client is logged in against server. Takes a `Client` parameter.
### `playerJoin` event
Emitted after a player joins and enters the PLAY protocol state and can send and recieve game packets. This is emitted after the `login` event. On 1.20.2 and above after we emit the `login` event, the player will enter a CONFIG state, as opposed to the PLAY state (where game packets can be sent), so you must instead now wait for `playerJoin`.
### `listening` event ### `listening` event
Called when the server is listening for connections. This means that the server is ready to accept incoming connections. Called when the server is listening for connections. This means that the server is ready to accept incoming connections.
@ -261,6 +266,10 @@ Called when user authentication is resolved. Takes session data as parameter.
Called when the protocol changes state. Takes the new state and old state as Called when the protocol changes state. Takes the new state and old state as
parameters. parameters.
### `playerJoin` event
Emitted after the player enters the PLAY protocol state and can send and recieve game packets
### `error` event ### `error` event
Called when an error occurs within the client. Takes an Error as parameter. Called when an error occurs within the client. Takes an Error as parameter.

View file

@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption.
* Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4),
1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4)
, 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1) , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2)
* Parses all packets and emits events with packet fields as JavaScript * Parses all packets and emits events with packet fields as JavaScript
objects. objects.
* Send a packet by supplying fields as a JavaScript object. * Send a packet by supplying fields as a JavaScript object.
@ -115,6 +115,8 @@ const client = mc.createClient({
### Hello World server example ### Hello World server example
For a more up to date example, see examples/server/server.js.
```js ```js
const mc = require('minecraft-protocol'); const mc = require('minecraft-protocol');
const server = mc.createServer({ const server = mc.createServer({
@ -126,18 +128,12 @@ const server = mc.createServer({
}); });
const mcData = require('minecraft-data')(server.version) const mcData = require('minecraft-data')(server.version)
server.on('login', function(client) { server.on('playerJoin', function(client) {
const loginPacket = mcData.loginPacket const loginPacket = mcData.loginPacket
client.write('login', { client.write('login', {
...loginPacket,
entityId: client.id, entityId: client.id,
isHardcore: false,
gameMode: 0,
previousGameMode: 255,
worldNames: loginPacket.worldNames,
dimensionCodec: loginPacket.dimensionCodec,
dimension: loginPacket.dimension,
worldName: 'minecraft:overworld',
hashedSeed: [0, 0], hashedSeed: [0, 0],
maxPlayers: server.maxPlayers, maxPlayers: server.maxPlayers,
viewDistance: 10, viewDistance: 10,

View file

@ -11,7 +11,7 @@ const server = mc.createServer(options)
const mcData = require('minecraft-data')(server.version) const mcData = require('minecraft-data')(server.version)
const loginPacket = mcData.loginPacket const loginPacket = mcData.loginPacket
server.on('login', function (client) { server.on('playerJoin', function (client) {
broadcast(client.username + ' joined the game.') broadcast(client.username + ' joined the game.')
const addr = client.socket.remoteAddress + ':' + client.socket.remotePort const addr = client.socket.remoteAddress + ':' + client.socket.remotePort
console.log(client.username + ' connected', '(' + addr + ')') console.log(client.username + ' connected', '(' + addr + ')')
@ -23,14 +23,11 @@ server.on('login', function (client) {
// send init data so client will start rendering world // send init data so client will start rendering world
client.write('login', { client.write('login', {
...loginPacket,
entityId: client.id, entityId: client.id,
isHardcore: false, isHardcore: false,
gameMode: 0, gameMode: 0,
previousGameMode: 1, previousGameMode: 1,
worldNames: loginPacket.worldNames,
dimensionCodec: loginPacket.dimensionCodec,
dimension: loginPacket.dimension,
worldName: 'minecraft:overworld',
hashedSeed: [0, 0], hashedSeed: [0, 0],
maxPlayers: server.maxPlayers, maxPlayers: server.maxPlayers,
viewDistance: 10, viewDistance: 10,
@ -48,11 +45,13 @@ server.on('login', function (client) {
flags: 0x00 flags: 0x00
}) })
client.on('chat', function (data) { function handleChat (data) {
const message = '<' + client.username + '>' + ' ' + data.message const message = '<' + client.username + '>' + ' ' + data.message
broadcast(message, null, client.username) broadcast(message, null, client.username)
console.log(message) console.log(message)
}) }
client.on('chat', handleChat) // pre-1.19
client.on('chat_message', handleChat) // post 1.19
}) })
server.on('error', function (error) { server.on('error', function (error) {
@ -63,27 +62,28 @@ server.on('listening', function () {
console.log('Server listening on port', server.socketServer.address().port) console.log('Server listening on port', server.socketServer.address().port)
}) })
function broadcast (message, exclude, username) { function sendBroadcastMessage (server, clients, message, sender) {
let client if (mcData.supportFeature('signedChat')) {
const translate = username ? 'chat.type.announcement' : 'chat.type.text' server.writeToClients(clients, 'player_chat', {
username = username || 'Server' plainMessage: message,
for (const clientId in server.clients) { signedChatContent: '',
if (server.clients[clientId] === undefined) continue unsignedChatContent: JSON.stringify({ text: message }),
type: 0,
client = server.clients[clientId] senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random
if (client !== exclude) { senderName: JSON.stringify({ text: sender }),
const msg = { senderTeam: undefined,
translate, timestamp: Date.now(),
with: [ salt: 0n,
username, signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0),
message previousMessages: [],
] filterType: 0,
} networkName: JSON.stringify({ text: sender })
client.write('chat', { })
message: JSON.stringify(msg), } else {
position: 0, server.writeToClients(clients, 'chat', { message: JSON.stringify({ text: message }), position: 0, sender: sender || '0' })
sender: '0'
})
}
} }
} }
function broadcast (message, exclude, username) {
sendBroadcastMessage(server, Object.values(server.clients).filter(client => client !== exclude), message)
}

View file

@ -8,18 +8,16 @@ const server = mc.createServer({
const mcData = require('minecraft-data')(server.version) const mcData = require('minecraft-data')(server.version)
const loginPacket = mcData.loginPacket const loginPacket = mcData.loginPacket
server.on('login', function (client) { server.on('playerJoin', function (client) {
client.registerChannel('minecraft:brand', ['string', []]) client.registerChannel('minecraft:brand', ['string', []])
client.on('minecraft:brand', console.log) client.on('minecraft:brand', console.log)
client.write('login', { client.write('login', {
...loginPacket,
entityId: client.id, entityId: client.id,
isHardcore: false, isHardcore: false,
gameMode: 0, gameMode: 0,
previousGameMode: 1, previousGameMode: 1,
worldNames: loginPacket.worldNames,
dimensionCodec: loginPacket.dimensionCodec,
dimension: loginPacket.dimension,
worldName: 'minecraft:overworld', worldName: 'minecraft:overworld',
hashedSeed: [0, 0], hashedSeed: [0, 0],
maxPlayers: server.maxPlayers, maxPlayers: server.maxPlayers,

View file

@ -8,15 +8,13 @@ const server = mc.createServer({
const mcData = require('minecraft-data')(server.version) const mcData = require('minecraft-data')(server.version)
const loginPacket = mcData.loginPacket const loginPacket = mcData.loginPacket
server.on('login', function (client) { server.on('playerJoin', function (client) {
client.write('login', { client.write('login', {
...loginPacket,
entityId: client.id, entityId: client.id,
isHardcore: false, isHardcore: false,
gameMode: 0, gameMode: 0,
previousGameMode: 1, previousGameMode: 1,
worldNames: loginPacket.worldNames,
dimensionCodec: loginPacket.dimensionCodec,
dimension: loginPacket.dimension,
worldName: 'minecraft:overworld', worldName: 'minecraft:overworld',
hashedSeed: [0, 0], hashedSeed: [0, 0],
maxPlayers: server.maxPlayers, maxPlayers: server.maxPlayers,

View file

@ -9,7 +9,7 @@ const server = mc.createServer(options)
const mcData = require('minecraft-data')(server.version) const mcData = require('minecraft-data')(server.version)
const loginPacket = mcData.loginPacket const loginPacket = mcData.loginPacket
server.on('login', function (client) { server.on('playerJoin', function (client) {
const addr = client.socket.remoteAddress const addr = client.socket.remoteAddress
console.log('Incoming connection', '(' + addr + ')') console.log('Incoming connection', '(' + addr + ')')

View file

@ -22,15 +22,13 @@ for (let x = 0; x < 16; x++) {
} }
} }
server.on('login', function (client) { server.on('playerJoin', function (client) {
client.write('login', { client.write('login', {
...loginPacket,
entityId: client.id, entityId: client.id,
isHardcore: false, isHardcore: false,
gameMode: 0, gameMode: 0,
previousGameMode: 1, previousGameMode: 1,
worldNames: loginPacket.worldNames,
dimensionCodec: loginPacket.dimensionCodec,
dimension: loginPacket.dimension,
worldName: 'minecraft:overworld', worldName: 'minecraft:overworld',
hashedSeed: [0, 0], hashedSeed: [0, 0],
maxPlayers: server.maxPlayers, maxPlayers: server.maxPlayers,

View file

@ -51,12 +51,12 @@
"endian-toggle": "^0.0.0", "endian-toggle": "^0.0.0",
"lodash.get": "^4.1.2", "lodash.get": "^4.1.2",
"lodash.merge": "^4.3.0", "lodash.merge": "^4.3.0",
"minecraft-data": "^3.37.0", "minecraft-data": "^3.53.0",
"minecraft-folder-path": "^1.2.0", "minecraft-folder-path": "^1.2.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-rsa": "^0.4.2", "node-rsa": "^0.4.2",
"prismarine-auth": "^2.2.0", "prismarine-auth": "^2.2.0",
"prismarine-nbt": "^2.0.0", "prismarine-nbt": "^2.5.0",
"prismarine-realms": "^1.2.0", "prismarine-realms": "^1.2.0",
"protodef": "^1.8.0", "protodef": "^1.8.0",
"readable-stream": "^4.1.0", "readable-stream": "^4.1.0",

View file

@ -93,7 +93,7 @@ class Client extends EventEmitter {
const s = JSON.stringify(parsed.data, null, 2) const s = JSON.stringify(parsed.data, null, 2)
debug(s && s.length > 10000 ? parsed.data : s) debug(s && s.length > 10000 ? parsed.data : s)
} }
if (parsed.metadata.name === 'bundle_delimiter') { if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
if (this._mcBundle.length) { // End bundle if (this._mcBundle.length) { // End bundle
this._mcBundle.forEach(emitPacket) this._mcBundle.forEach(emitPacket)
emitPacket(parsed) emitPacket(parsed)
@ -103,6 +103,11 @@ class Client extends EventEmitter {
} }
} else if (this._mcBundle.length) { } else if (this._mcBundle.length) {
this._mcBundle.push(parsed) this._mcBundle.push(parsed)
if (this._mcBundle.length > 32) {
this._mcBundle.forEach(emitPacket)
this._mcBundle = []
this._hasBundlePacket = false
}
} else { } else {
emitPacket(parsed) emitPacket(parsed)
} }

View file

@ -32,30 +32,54 @@ module.exports = function (client, options) {
function onLogin (packet) { function onLogin (packet) {
const mcData = require('minecraft-data')(client.version) const mcData = require('minecraft-data')(client.version)
client.state = states.PLAY
client.uuid = packet.uuid client.uuid = packet.uuid
client.username = packet.username client.username = packet.username
if (mcData.supportFeature('signedChat')) { if (mcData.supportFeature('hasConfigurationState')) {
if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { client.write('login_acknowledged', {})
throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') enterConfigState()
} // Server can tell client to re-enter config state
signedChatPlugin(client, options) client.on('start_configuration', enterConfigState)
} else { } else {
client.on('chat', (packet) => { client.state = states.PLAY
client.emit(packet.position === 0 ? 'playerChat' : 'systemChat', { onReady()
formattedMessage: packet.message, }
sender: packet.sender,
positionId: packet.position, function enterConfigState () {
verified: false if (client.state === states.CONFIGURATION) return
}) client.state = states.CONFIGURATION
// Server should send finish_configuration on its own right after sending the client a dimension codec
// for login (that has data about world height, world gen, etc) after getting a login success from client
client.once('finish_configuration', () => {
client.write('finish_configuration', {})
client.state = states.PLAY
onReady()
}) })
} }
function unsignedChat (message) { function onReady () {
client.write('chat', { message }) client.emit('playerJoin')
} if (mcData.supportFeature('signedChat')) {
if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) {
throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat')
}
signedChatPlugin(client, options)
} else {
client.on('chat', (packet) => {
client.emit(packet.position === 0 ? 'playerChat' : 'systemChat', {
formattedMessage: packet.message,
sender: packet.sender,
positionId: packet.position,
verified: false
})
})
}
client.chat = client._signedChat || unsignedChat function unsignedChat (message) {
client.write('chat', { message })
}
client.chat = client._signedChat || unsignedChat
}
} }
} }

View file

@ -1,11 +1,13 @@
const ProtoDef = require('protodef').ProtoDef const ProtoDef = require('protodef').ProtoDef
const minecraft = require('../datatypes/minecraft') const minecraft = require('../datatypes/minecraft')
const debug = require('debug')('minecraft-protocol') const debug = require('debug')('minecraft-protocol')
const nbt = require('prismarine-nbt')
module.exports = function (client, options) { module.exports = function (client, options) {
const mcdata = require('minecraft-data')(options.version || require('../version').defaultVersion) const mcdata = require('minecraft-data')(options.version || require('../version').defaultVersion)
const channels = [] const channels = []
const proto = new ProtoDef(options.validateChannelProtocol ?? true) const proto = new ProtoDef(options.validateChannelProtocol ?? true)
nbt.addTypesToInterpreter('big', proto)
proto.addTypes(mcdata.protocol.types) proto.addTypes(mcdata.protocol.types)
proto.addTypes(minecraft) proto.addTypes(minecraft)
proto.addType('registerarr', [readDumbArr, writeDumbArr, sizeOfDumbArr]) proto.addType('registerarr', [readDumbArr, writeDumbArr, sizeOfDumbArr])

View file

@ -37,7 +37,7 @@ module.exports = function (client, options) {
: client.profileKeys.signature : client.profileKeys.signature
} }
: null, : null,
playerUUID: client.session?.selectedProfile?.id playerUUID: client.session?.selectedProfile?.id ?? client.uuid
}) })
} }
} }

View file

@ -14,6 +14,7 @@ const tcpDns = require('./client/tcp_dns')
const autoVersion = require('./client/autoVersion') const autoVersion = require('./client/autoVersion')
const pluginChannels = require('./client/pluginChannels') const pluginChannels = require('./client/pluginChannels')
const versionChecking = require('./client/versionChecking') const versionChecking = require('./client/versionChecking')
const uuid = require('./datatypes/uuid')
module.exports = createClient module.exports = createClient
@ -54,6 +55,8 @@ function createClient (options) {
case 'offline': case 'offline':
default: default:
client.username = options.username client.username = options.username
client.uuid = uuid.nameToMcOfflineUUID(client.username)
options.auth = 'offline'
options.connect(client) options.connect(client)
break break
} }

View file

@ -46,6 +46,7 @@ function createServer (options = {}) {
server.onlineModeExceptions = Object.create(null) server.onlineModeExceptions = Object.create(null)
server.favicon = favicon server.favicon = favicon
server.options = options server.options = options
options.registryCodec = options.registryCodec || mcData.registryCodec || mcData.loginPacket?.dimensionCodec
// The RSA keypair can take some time to generate // The RSA keypair can take some time to generate
// and is only needed for online-mode // and is only needed for online-mode

View file

@ -16,8 +16,6 @@ module.exports = {
size: buffer.length - offset size: buffer.length - offset
} }
}], }],
nbt: ['native', minecraft.nbt[0]],
optionalNbt: ['native', minecraft.optionalNbt[0]],
compressedNbt: ['native', minecraft.compressedNbt[0]], compressedNbt: ['native', minecraft.compressedNbt[0]],
entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => { entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => {
let code = 'let cursor = offset\n' let code = 'let cursor = offset\n'
@ -55,8 +53,6 @@ module.exports = {
value.copy(buffer, offset) value.copy(buffer, offset)
return offset + value.length return offset + value.length
}], }],
nbt: ['native', minecraft.nbt[1]],
optionalNbt: ['native', minecraft.optionalNbt[1]],
compressedNbt: ['native', minecraft.compressedNbt[1]], compressedNbt: ['native', minecraft.compressedNbt[1]],
entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => { entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => {
let code = 'for (const i in value) {\n' let code = 'for (const i in value) {\n'
@ -84,8 +80,6 @@ module.exports = {
restBuffer: ['native', (value) => { restBuffer: ['native', (value) => {
return value.length return value.length
}], }],
nbt: ['native', minecraft.nbt[2]],
optionalNbt: ['native', minecraft.optionalNbt[2]],
compressedNbt: ['native', minecraft.compressedNbt[2]], compressedNbt: ['native', minecraft.compressedNbt[2]],
entityMetadataLoop: ['parametrizable', (compiler, { type }) => { entityMetadataLoop: ['parametrizable', (compiler, { type }) => {
let code = 'let size = 1\n' let code = 'let size = 1\n'

View file

@ -8,8 +8,6 @@ const [readVarInt, writeVarInt, sizeOfVarInt] = require('protodef').types.varint
module.exports = { module.exports = {
varlong: [readVarLong, writeVarLong, sizeOfVarLong], varlong: [readVarLong, writeVarLong, sizeOfVarLong],
UUID: [readUUID, writeUUID, 16], UUID: [readUUID, writeUUID, 16],
nbt: [readNbt, writeNbt, sizeOfNbt],
optionalNbt: [readOptionalNbt, writeOptionalNbt, sizeOfOptionalNbt],
compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt], compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt],
restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer],
entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata],
@ -43,35 +41,8 @@ function writeUUID (value, buffer, offset) {
return offset + 16 return offset + 16
} }
function readNbt (buffer, offset) { function sizeOfNbt (value, { tagType } = { tagType: 'nbt' }) {
return nbt.proto.read(buffer, offset, 'nbt') return nbt.proto.sizeOf(value, tagType)
}
function writeNbt (value, buffer, offset) {
return nbt.proto.write(value, buffer, offset, 'nbt')
}
function sizeOfNbt (value) {
return nbt.proto.sizeOf(value, 'nbt')
}
function readOptionalNbt (buffer, offset) {
if (offset + 1 > buffer.length) { throw new PartialReadError() }
if (buffer.readInt8(offset) === 0) return { size: 1 }
return nbt.proto.read(buffer, offset, 'nbt')
}
function writeOptionalNbt (value, buffer, offset) {
if (value === undefined) {
buffer.writeInt8(0, offset)
return offset + 1
}
return nbt.proto.write(value, buffer, offset, 'nbt')
}
function sizeOfOptionalNbt (value) {
if (value === undefined) { return 1 }
return nbt.proto.sizeOf(value, 'nbt')
} }
// Length-prefixed compressed NBT, see differences: http://wiki.vg/index.php?title=Slot_Data&diff=6056&oldid=4753 // Length-prefixed compressed NBT, see differences: http://wiki.vg/index.php?title=Slot_Data&diff=6056&oldid=4753
@ -111,7 +82,7 @@ function writeCompressedNbt (value, buffer, offset) {
function sizeOfCompressedNbt (value) { function sizeOfCompressedNbt (value) {
if (value === undefined) { return 2 } if (value === undefined) { return 2 }
const nbtBuffer = Buffer.alloc(sizeOfNbt(value, 'nbt')) const nbtBuffer = Buffer.alloc(sizeOfNbt(value, { tagType: 'nbt' }))
nbt.proto.write(value, nbtBuffer, 0, 'nbt') nbt.proto.write(value, nbtBuffer, 0, 'nbt')
const compressedNbt = zlib.gzipSync(nbtBuffer) // TODO: async const compressedNbt = zlib.gzipSync(nbtBuffer) // TODO: async

18
src/datatypes/uuid.js Normal file
View file

@ -0,0 +1,18 @@
const crypto = require('crypto')
const UUID = require('uuid-1345')
// https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163
function javaUUID (s) {
const hash = crypto.createHash('md5')
hash.update(s, 'utf8')
const buffer = hash.digest()
buffer[6] = (buffer[6] & 0x0f) | 0x30
buffer[8] = (buffer[8] & 0x3f) | 0x80
return buffer
}
function nameToMcOfflineUUID (name) {
return (new UUID(javaUUID('OfflinePlayer:' + name))).toString()
}
module.exports = { nameToMcOfflineUUID }

4
src/index.d.ts vendored
View file

@ -51,6 +51,8 @@ declare module 'minecraft-protocol' {
on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this
on(event: 'playerChat', handler: (data: { formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this on(event: 'playerChat', handler: (data: { formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this
on(event: 'systemChat', handler: (data: { positionId: number, formattedMessage: string }) => PromiseLike): this on(event: 'systemChat', handler: (data: { positionId: number, formattedMessage: string }) => PromiseLike): this
// Emitted after the player enters the PLAY state and can send and recieve game packets
on(event: 'playerJoin', handler: () => void): this
once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'error', listener: (error: Error) => PromiseLike): this
once(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this once(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this
once(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this once(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this
@ -156,6 +158,8 @@ declare module 'minecraft-protocol' {
on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'error', listener: (error: Error) => PromiseLike): this
on(event: 'login', handler: (client: ServerClient) => PromiseLike): this on(event: 'login', handler: (client: ServerClient) => PromiseLike): this
on(event: 'listening', listener: () => PromiseLike): this on(event: 'listening', listener: () => PromiseLike): this
// Emitted after the player enters the PLAY state and can send and recieve game packets
on(event: 'playerJoin', handler: (client: ServerClient) => void): this
once(event: 'connection', handler: (client: ServerClient) => PromiseLike): this once(event: 'connection', handler: (client: ServerClient) => PromiseLike): this
once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'error', listener: (error: Error) => PromiseLike): this
once(event: 'login', handler: (client: ServerClient) => PromiseLike): this once(event: 'login', handler: (client: ServerClient) => PromiseLike): this

View file

@ -1,4 +1,4 @@
const UUID = require('uuid-1345') const uuid = require('../datatypes/uuid')
const crypto = require('crypto') const crypto = require('crypto')
const pluginChannels = require('../client/pluginChannels') const pluginChannels = require('../client/pluginChannels')
const states = require('../states') const states = require('../states')
@ -166,37 +166,28 @@ module.exports = function (client, server, options) {
} }
} }
// https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163
function javaUUID (s) {
const hash = crypto.createHash('md5')
hash.update(s, 'utf8')
const buffer = hash.digest()
buffer[6] = (buffer[6] & 0x0f) | 0x30
buffer[8] = (buffer[8] & 0x3f) | 0x80
return buffer
}
function nameToMcOfflineUUID (name) {
return (new UUID(javaUUID('OfflinePlayer:' + name))).toString()
}
function loginClient () { function loginClient () {
const isException = !!server.onlineModeExceptions[client.username.toLowerCase()] const isException = !!server.onlineModeExceptions[client.username.toLowerCase()]
if (onlineMode === false || isException) { if (onlineMode === false || isException) {
client.uuid = nameToMcOfflineUUID(client.username) client.uuid = uuid.nameToMcOfflineUUID(client.username)
} }
options.beforeLogin?.(client) options.beforeLogin?.(client)
if (client.protocolVersion >= 27) { // 14w28a (27) added whole-protocol compression (http://wiki.vg/Protocol_History#14w28a), earlier versions per-packet compressed TODO: refactor into minecraft-data if (client.protocolVersion >= 27) { // 14w28a (27) added whole-protocol compression (http://wiki.vg/Protocol_History#14w28a), earlier versions per-packet compressed TODO: refactor into minecraft-data
client.write('compress', { threshold: 256 }) // Default threshold is 256 client.write('compress', { threshold: 256 }) // Default threshold is 256
client.compressionThreshold = 256 client.compressionThreshold = 256
} }
// TODO: find out what properties are on 'success' packet
client.write('success', { client.write('success', {
uuid: client.uuid, uuid: client.uuid,
username: client.username, username: client.username,
properties: [] properties: []
}) })
// TODO: find out what properties are on 'success' packet if (client.supportFeature('hasConfigurationState')) {
client.state = states.PLAY client.once('login_acknowledged', onClientLoginAck)
} else {
client.state = states.PLAY
server.emit('playerJoin', client)
}
client.settings = {} client.settings = {}
if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+
@ -217,6 +208,16 @@ module.exports = function (client, server, options) {
if (client.supportFeature('signedChat')) chatPlugin(client, server, options) if (client.supportFeature('signedChat')) chatPlugin(client, server, options)
server.emit('login', client) server.emit('login', client)
} }
function onClientLoginAck () {
client.state = states.CONFIGURATION
client.write('registry_data', { codec: options.registryCodec || {} })
client.once('finish_configuration', () => {
client.state = states.PLAY
server.emit('playerJoin', client)
})
client.write('finish_configuration', {})
}
} }
function mcPubKeyToPem (mcPubKeyBuffer) { function mcPubKeyToPem (mcPubKeyBuffer) {

View file

@ -4,6 +4,7 @@ const states = {
HANDSHAKING: 'handshaking', HANDSHAKING: 'handshaking',
STATUS: 'status', STATUS: 'status',
LOGIN: 'login', LOGIN: 'login',
CONFIGURATION: 'configuration',
PLAY: 'play' PLAY: 'play'
} }

View file

@ -5,6 +5,7 @@ const Serializer = require('protodef').Serializer
const Parser = require('protodef').FullPacketParser const Parser = require('protodef').FullPacketParser
const { ProtoDefCompiler } = require('protodef').Compiler const { ProtoDefCompiler } = require('protodef').Compiler
const nbt = require('prismarine-nbt')
const minecraft = require('../datatypes/minecraft') const minecraft = require('../datatypes/minecraft')
const states = require('../states') const states = require('../states')
const merge = require('lodash.merge') const merge = require('lodash.merge')
@ -30,6 +31,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr
const compiler = new ProtoDefCompiler() const compiler = new ProtoDefCompiler()
compiler.addTypes(require('../datatypes/compiler-minecraft')) compiler.addTypes(require('../datatypes/compiler-minecraft'))
compiler.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) compiler.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction])
nbt.addTypesToCompiler('big', compiler)
const proto = compiler.compileProtoDefSync() const proto = compiler.compileProtoDefSync()
protocols[key] = proto protocols[key] = proto
return proto return proto
@ -38,6 +40,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr
const proto = new ProtoDef(false) const proto = new ProtoDef(false)
proto.addTypes(minecraft) proto.addTypes(minecraft)
proto.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) proto.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction])
nbt.addTypesToInterperter('big', proto)
protocols[key] = proto protocols[key] = proto
return proto return proto
} }

View file

@ -1,6 +1,6 @@
'use strict' 'use strict'
module.exports = { module.exports = {
defaultVersion: '1.20.1', defaultVersion: '1.20.2',
supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1'] supportedVersions: ['1.7', '1.8', '1.9', '1.10', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2']
} }

View file

@ -2,6 +2,7 @@
const mc = require('../') const mc = require('../')
const os = require('os') const os = require('os')
const fs = require('fs')
const path = require('path') const path = require('path')
const assert = require('power-assert') const assert = require('power-assert')
const util = require('util') const util = require('util')
@ -20,7 +21,8 @@ for (const supportedVersion of mc.supportedVersions) {
const version = mcData.version const version = mcData.version
const MC_SERVER_JAR_DIR = process.env.MC_SERVER_JAR_DIR || os.tmpdir() const MC_SERVER_JAR_DIR = process.env.MC_SERVER_JAR_DIR || os.tmpdir()
const MC_SERVER_JAR = MC_SERVER_JAR_DIR + '/minecraft_server.' + version.minecraftVersion + '.jar' const MC_SERVER_JAR = MC_SERVER_JAR_DIR + '/minecraft_server.' + version.minecraftVersion + '.jar'
const wrap = new Wrap(MC_SERVER_JAR, MC_SERVER_PATH + '_' + supportedVersion, { const MC_SERVER_DIR = MC_SERVER_PATH + '_' + supportedVersion
const wrap = new Wrap(MC_SERVER_JAR, MC_SERVER_DIR, {
minMem: 1024, minMem: 1024,
maxMem: 1024 maxMem: 1024
}) })
@ -118,7 +120,23 @@ for (const supportedVersion of mc.supportedVersions) {
assert.strictEqual(packet.gameMode, 0) assert.strictEqual(packet.gameMode, 0)
client.chat('hello everyone; I have logged in.') client.chat('hello everyone; I have logged in.')
}) })
// Dump some data for easier debugging
client.on('raw.registry_data', (buffer) => {
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.bin', buffer)
})
client.on('registry_data', (json) => {
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json))
})
client.on('login', (packet) => {
fs.writeFileSync(MC_SERVER_DIR + '_login.json', JSON.stringify(packet))
if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) {
// generate a loginPacket.json for minecraft-data
fs.writeFileSync(MC_SERVER_DIR + '_loginPacket.json', JSON.stringify({
...packet,
dimensionCodec: JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json')).codec
}, null, 2))
}
})
client.on('playerChat', function (data) { client.on('playerChat', function (data) {
chatCount += 1 chatCount += 1
assert.ok(chatCount <= 2) assert.ok(chatCount <= 2)

View file

@ -35,6 +35,20 @@ const slotValue = {
} }
} }
const nbtValue = {
type: 'compound',
name: 'test',
value: {
test1: { type: 'int', value: 4 },
test2: { type: 'long', value: [12, 42] },
test3: { type: 'byteArray', value: [32] },
test4: { type: 'string', value: 'ohi' },
test5: { type: 'list', value: { type: 'int', value: [4] } },
test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } },
test7: { type: 'intArray', value: [12, 42] }
}
}
const values = { const values = {
i32: 123456, i32: 123456,
i16: -123, i16: -123,
@ -110,46 +124,12 @@ const values = {
f64: 99999.2222, f64: 99999.2222,
f32: -333.444, f32: -333.444,
slot: slotValue, slot: slotValue,
nbt: { nbt: nbtValue,
type: 'compound', optionalNbt: nbtValue,
name: 'test', compressedNbt: nbtValue,
value: { anonymousNbt: nbtValue,
test1: { type: 'int', value: 4 }, anonOptionalNbt: nbtValue,
test2: { type: 'long', value: [12, 42] },
test3: { type: 'byteArray', value: [32] },
test4: { type: 'string', value: 'ohi' },
test5: { type: 'list', value: { type: 'int', value: [4] } },
test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } },
test7: { type: 'intArray', value: [12, 42] }
}
},
optionalNbt: {
type: 'compound',
name: 'test',
value: {
test1: { type: 'int', value: 4 },
test2: { type: 'long', value: [12, 42] },
test3: { type: 'byteArray', value: [32] },
test4: { type: 'string', value: 'ohi' },
test5: { type: 'list', value: { type: 'int', value: [4] } },
test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } },
test7: { type: 'intArray', value: [12, 42] }
}
},
previousMessages: [], previousMessages: [],
compressedNbt: {
type: 'compound',
name: 'test',
value: {
test1: { type: 'int', value: 4 },
test2: { type: 'long', value: [12, 42] },
test3: { type: 'byteArray', value: [32] },
test4: { type: 'string', value: 'ohi' },
test5: { type: 'list', value: { type: 'int', value: [4] } },
test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } },
test7: { type: 'intArray', value: [12, 42] }
}
},
i64: [0, 1], i64: [0, 1],
u64: [0, 1], u64: [0, 1],
entityMetadata: [ entityMetadata: [
@ -223,6 +203,11 @@ const values = {
}, },
suggestionType: 'minecraft:summonable_entities' suggestionType: 'minecraft:summonable_entities'
} }
},
soundSource: 'master',
packedChunkPos: {
x: 10,
z: 12
} }
} }

View file

@ -96,7 +96,7 @@ for (const supportedVersion of mc.supportedVersions) {
describe('mc-server ' + supportedVersion + 'v', function () { describe('mc-server ' + supportedVersion + 'v', function () {
this.timeout(5000) this.timeout(5000)
this.beforeAll(async function () { this.beforeEach(async function () {
PORT = await getPort() PORT = await getPort()
console.log(`Using port for tests: ${PORT}`) console.log(`Using port for tests: ${PORT}`)
}) })
@ -299,7 +299,8 @@ for (const supportedVersion of mc.supportedVersions) {
const username = ['player1', 'player2'] const username = ['player1', 'player2']
let index = 0 let index = 0
server.on('login', function (client) { server.on('playerJoin', function (client) {
console.log('ChatTest: Player has joined')
assert.notEqual(client.id, null) assert.notEqual(client.id, null)
assert.strictEqual(client.username, username[index++]) assert.strictEqual(client.username, username[index++])
broadcast(client.username + ' joined the game.') broadcast(client.username + ' joined the game.')
@ -322,8 +323,10 @@ for (const supportedVersion of mc.supportedVersions) {
version: version.minecraftVersion, version: version.minecraftVersion,
port: PORT port: PORT
})) }))
console.log('ChatTest: Player1 is joining...')
player1.on('login', async function (packet) { player1.on('login', async function (packet) {
console.log('ChatTest: Player 1 has joined')
assert.strictEqual(packet.gameMode, 1) assert.strictEqual(packet.gameMode, 1)
const player2 = applyClientHelpers(mc.createClient({ const player2 = applyClientHelpers(mc.createClient({
username: 'player2', username: 'player2',
@ -332,14 +335,16 @@ for (const supportedVersion of mc.supportedVersions) {
port: PORT port: PORT
})) }))
console.log('ChatTest: waiting for next message from P2')
const p1Join = await player1.nextMessage('player2') const p1Join = await player1.nextMessage('player2')
assert.strictEqual(p1Join, '{"text":"player2 joined the game."}') assert.strictEqual(p1Join, '{"text":"player2 joined the game."}')
console.log('ChatTest: Got message from P2')
player2.chat('hi') player2.chat('hi')
const p2hi = await player1.nextMessage('player2') const p2hi = await player1.nextMessage('player2')
assert.strictEqual(p2hi, '{"text":"<player2> hi"}') assert.strictEqual(p2hi, '{"text":"<player2> hi"}')
console.log('ChatTest: Waiting again for next message from P2')
player1.chat('hello') player1.chat('hello')
const p1hello = await player2.nextMessage('player1') const p1hello = await player2.nextMessage('player1')
assert.strictEqual(p1hello, '{"text":"<player1> hello"}') assert.strictEqual(p1hello, '{"text":"<player1> hello"}')
@ -389,7 +394,7 @@ for (const supportedVersion of mc.supportedVersions) {
port: PORT port: PORT
}) })
let count = 2 let count = 2
server.on('login', function (client) { server.on('playerJoin', function (client) {
client.on('end', function (reason) { client.on('end', function (reason) {
assert.strictEqual(reason, 'ServerShutdown') assert.strictEqual(reason, 'ServerShutdown')
resolve() resolve()
@ -404,7 +409,7 @@ for (const supportedVersion of mc.supportedVersions) {
version: version.minecraftVersion, version: version.minecraftVersion,
port: PORT port: PORT
}) })
client.on('login', function () { client.on('playerJoin', function () {
server.close() server.close()
}) })
}) })
@ -420,7 +425,7 @@ for (const supportedVersion of mc.supportedVersions) {
version: version.minecraftVersion, version: version.minecraftVersion,
port: PORT port: PORT
}) })
server.on('login', function (client) { server.on('playerJoin', function (client) {
client.write('login', loginPacket(client, server)) client.write('login', loginPacket(client, server))
}) })
server.on('close', done) server.on('close', done)
@ -459,7 +464,7 @@ for (const supportedVersion of mc.supportedVersions) {
version: version.minecraftVersion, version: version.minecraftVersion,
port: PORT port: PORT
}) })
server.on('login', function (client) { server.on('playerJoin', function (client) {
client.on('end', function (reason) { client.on('end', function (reason) {
assert.strictEqual(reason, 'ServerShutdown') assert.strictEqual(reason, 'ServerShutdown')
}) })