* 1.20.5

* update examples

* Update for 1.20.5 chat_command_signed with seperateSignedChatCommandPacket feature

* updates

* update java

* re-enable packet tests

* Update client.js

add debug code after decompress

* Update client.js

* Update ci.yml

* Add `arrayWithLengthOffset` type to interpeter

* Update minecraft.js

* Update compiler-minecraft.js

* Update minecraft.js

* lint

* remote custom ci install

* Update package.json

* Update packetTest.js add Slot, SlotComponent

* Update packetTest.js

* Update packetTest.js

* Fix lint.

* Fix declare_recipes, Slot

* Update package.json

---------

Co-authored-by: Romain Beaumont <romain.rom1@gmail.com>
This commit is contained in:
extremeheat 2024-10-12 17:55:36 -04:00 committed by GitHub
parent 7057ad979b
commit 9b029e8b6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 232 additions and 46 deletions

View file

@ -52,7 +52,7 @@ jobs:
- name: Setup Java JDK
uses: actions/setup-java@v1.4.3
with:
java-version: '17'
java-version: '21'
distribution: 'adopt'
- name: Install dependencies
run: npm install

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),
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.16.5), 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, 1.20.3 and 1.20.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.16.5), 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, 1.20.2, 1.20.3, 1.20.4, 1.20.5
* Parses all packets and emits events with packet fields as JavaScript
objects.
* Send a packet by supplying fields as a JavaScript object.
@ -142,6 +142,7 @@ server.on('playerJoin', function(client) {
client.write('login', {
...loginPacket,
enforceSecureChat: false,
entityId: client.id,
hashedSeed: [0, 0],
maxPlayers: server.maxPlayers,

View file

@ -30,6 +30,7 @@ server.on('playerJoin', function (client) {
// send init data so client will start rendering world
client.write('login', {
...loginPacket,
enforceSecureChat: false,
entityId: client.id,
isHardcore: false,
gameMode: 0,

View file

@ -14,6 +14,7 @@ server.on('playerJoin', function (client) {
client.write('login', {
...loginPacket,
enforceSecureChat: false,
entityId: client.id,
isHardcore: false,
gameMode: 0,

View file

@ -11,6 +11,7 @@ const loginPacket = mcData.loginPacket
server.on('playerJoin', function (client) {
client.write('login', {
...loginPacket,
enforceSecureChat: false,
entityId: client.id,
isHardcore: false,
gameMode: 0,

View file

@ -41,7 +41,8 @@
"minecraft-wrap": "^1.2.3",
"mocha": "^10.0.0",
"power-assert": "^1.0.0",
"standard": "^17.0.0"
"standard": "^17.0.0",
"prismarine-registry": "^1.8.0"
},
"dependencies": {
"@types/readable-stream": "^4.0.0",
@ -51,7 +52,7 @@
"endian-toggle": "^0.0.0",
"lodash.get": "^4.1.2",
"lodash.merge": "^4.3.0",
"minecraft-data": "^3.55.0",
"minecraft-data": "^3.71.0",
"minecraft-folder-path": "^1.2.0",
"node-fetch": "^2.6.1",
"node-rsa": "^0.4.2",
@ -59,7 +60,7 @@
"prismarine-chat": "^1.10.0",
"prismarine-nbt": "^2.5.0",
"prismarine-realms": "^1.2.0",
"protodef": "^1.8.0",
"protodef": "^1.17.0",
"readable-stream": "^4.1.0",
"uuid-1345": "^1.0.1",
"yggdrasil": "^1.4.0"

View file

@ -137,6 +137,7 @@ class Client extends EventEmitter {
this.splitter.pipe(this.deserializer)
} else {
this.serializer.pipe(this.compressor)
if (globalThis.debugNMP) this.decompressor.on('data', (data) => { console.log('DES>', data.toString('hex')) })
this.decompressor.pipe(this.deserializer)
}

View file

@ -176,7 +176,7 @@ module.exports = function (client, options) {
})
})
client.on('message_header', (packet) => {
client.on('message_header', (packet) => { // [1.19.2]
updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash)
client._lastChatHistory.push({
@ -369,13 +369,14 @@ module.exports = function (client, options) {
if (message.startsWith('/')) {
const command = message.slice(1)
if (mcData.supportFeature('useChatSessions')) {
if (mcData.supportFeature('useChatSessions')) { // 1.19.3+
const { acknowledged, acknowledgements } = getAcknowledgements()
client.write('chat_command', {
const canSign = client.profileKeys && client._session
client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', {
command,
timestamp: options.timestamp,
salt: options.salt,
argumentSignatures: (client.profileKeys && client._session) ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [],
argumentSignatures: canSign ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [],
messageCount: client._lastSeenMessages.pending,
acknowledged
})

View file

@ -7,11 +7,12 @@ module.exports = function (client, options) {
client.on('server_data', (packet) => {
client.serverFeatures = {
chatPreview: packet.previewsChat,
enforcesSecureChat: packet.enforcesSecureChat
enforcesSecureChat: packet.enforcesSecureChat // in LoginPacket v>=1.20.5
}
})
client.once('login', () => {
client.once('login', (packet) => {
if (packet.enforcesSecureChat) client.serverFeatures.enforcesSecureChat = packet.enforcesSecureChat
const mcData = require('minecraft-data')(client.version)
if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher && client.session.selectedProfile.id === client.uuid.replace(/-/g, '')) {
client._session = {
@ -52,6 +53,9 @@ module.exports = function (client, options) {
client.write('configuration_acknowledged', {})
}
client.state = states.CONFIGURATION
client.on('select_known_packs', () => {
client.write('select_known_packs', { packs: [] })
})
// 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', () => {

View file

@ -40,6 +40,27 @@ module.exports = {
code += ' if ((item & 128) === 0) return { value: data, size: cursor - offset }\n'
code += '}'
return compiler.wrapCode(code)
}],
arrayWithLengthOffset: ['parametrizable', (compiler, array) => {
let code = ''
if (array.countType) {
code += 'const { value: count, size: countSize } = ' + compiler.callType(array.countType) + '\n'
} else if (array.count) {
code += 'const count = ' + array.count + '\n'
code += 'const countSize = 0\n'
} else {
throw new Error('Array must contain either count or countType')
}
code += 'if (count > 0xffffff) throw new Error("array size is abnormally large, not reading: " + count)\n'
code += 'const data = []\n'
code += 'let size = countSize\n'
code += `for (let i = 0; i < count + ${array.lengthOffset}; i++) {\n`
code += ' const elem = ' + compiler.callType(array.type, 'offset + size') + '\n'
code += ' data.push(elem.value)\n'
code += ' size += elem.size\n'
code += '}\n'
code += 'return { value: data, size }'
return compiler.wrapCode(code)
}]
},
Write: {
@ -72,6 +93,19 @@ module.exports = {
code += '}\n'
code += 'return offset'
return compiler.wrapCode(code)
}],
arrayWithLengthOffset: ['parametrizable', (compiler, array) => {
let code = ''
if (array.countType) {
code += 'offset = ' + compiler.callType('value.length', array.countType) + '\n'
} else if (array.count === null) {
throw new Error('Array must contain either count or countType')
}
code += 'for (let i = 0; i < value.length; i++) {\n'
code += ' offset = ' + compiler.callType('value[i]', array.type) + '\n'
code += '}\n'
code += 'return offset'
return compiler.wrapCode(code)
}]
},
SizeOf: {
@ -96,6 +130,25 @@ module.exports = {
code += '}\n'
code += 'return size'
return compiler.wrapCode(code)
}],
arrayWithLengthOffset: ['parametrizable', (compiler, array) => {
let code = ''
if (array.countType) {
code += 'let size = ' + compiler.callType('value.length', array.countType) + '\n'
} else if (array.count) {
code += 'let size = 0\n'
} else {
throw new Error('Array must contain either count or countType')
}
if (!isNaN(compiler.callType('value[i]', array.type))) {
code += 'size += value.length * ' + compiler.callType('value[i]', array.type) + '\n'
} else {
code += 'for (let i = 0; i < value.length; i++) {\n'
code += ' size += ' + compiler.callType('value[i]', array.type) + '\n'
code += '}\n'
}
code += 'return size'
return compiler.wrapCode(code)
}]
}
}

View file

@ -11,7 +11,8 @@ module.exports = {
compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt],
restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer],
entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata],
topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray]
topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray],
arrayWithLengthOffset: [readArrayWithLengthOffset, writeArrayWithLengthOffset, sizeOfArrayWithLengthOffset]
}
const PartialReadError = require('protodef').utils.PartialReadError
@ -180,3 +181,36 @@ function sizeOfTopBitSetTerminatedArray (value, { type }) {
}
return size
}
//
const { getCount, sendCount, calcCount, tryDoc } = require('protodef/src/utils')
function readArrayWithLengthOffset (buffer, offset, typeArgs, rootNode) {
const results = {
value: [],
size: 0
}
let value
let { count, size } = getCount.call(this, buffer, offset, typeArgs, rootNode)
offset += size
results.size += size
for (let i = 0; i < count + typeArgs.lengthOffset; i++) {
({ size, value } = tryDoc(() => this.read(buffer, offset, typeArgs.type, rootNode), i))
results.size += size
offset += size
results.value.push(value)
}
return results
}
// no changes
function writeArrayWithLengthOffset (value, buffer, offset, typeArgs, rootNode) {
offset = sendCount.call(this, value.length, buffer, offset, typeArgs, rootNode)
return value.reduce((offset, v, index) => tryDoc(() => this.write(v, buffer, offset, typeArgs.type, rootNode), index), offset)
}
function sizeOfArrayWithLengthOffset (value, typeArgs, rootNode) {
let size = calcCount.call(this, value.length, typeArgs, rootNode)
size = value.reduce((size, v, index) => tryDoc(() => size + this.sizeOf(v, typeArgs.type, rootNode), index), size)
return size + typeArgs
}

View file

@ -193,7 +193,8 @@ module.exports = function (client, server, options) {
if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+
client.write('server_data', {
previewsChat: options.enableChatPreview,
enforceSecureProfile: options.enforceSecureProfile
// Note: in 1.20.5+ user must send this with `login`
enforcesSecureChat: options.enforceSecureProfile
})
}
@ -211,7 +212,14 @@ module.exports = function (client, server, options) {
function onClientLoginAck () {
client.state = states.CONFIGURATION
client.write('registry_data', { codec: options.registryCodec || {} })
if (client.supportFeature('segmentedRegistryCodecData')) {
for (const key in options.registryCodec) {
const entry = options.registryCodec[key]
client.write('registry_data', entry)
}
} else {
client.write('registry_data', { codec: options.registryCodec || {} })
}
client.once('finish_configuration', () => {
client.state = states.PLAY
server.emit('playerJoin', client)

View file

@ -1,6 +1,6 @@
'use strict'
module.exports = {
defaultVersion: '1.20.4',
supportedVersions: ['1.7', '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.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4']
defaultVersion: '1.20.5',
supportedVersions: ['1.7', '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.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4', '1.20.5']
}

View file

@ -107,6 +107,43 @@ for (const supportedVersion of mc.supportedVersions) {
auth: 'offline'
}))
client.on('error', err => done(err))
client.on('state', (state) => {
console.log('Client now in state', state)
})
// ** Dump some server data **
fs.rmSync(MC_SERVER_DIR + '_registry_data.json', { force: true })
client.on('raw.registry_data', (buffer) => {
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.bin', buffer)
})
client.on('registry_data', (json) => {
if (json.codec) { // Pre 1.20.5, codec is 1 json
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json))
} else { // 1.20.5+, codec is many nbt's each with their own ids, merge them
let currentData = {}
if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) {
currentData = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json', 'utf8'))
}
currentData[json.id] = json
fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(currentData))
}
console.log('Wrote registry data')
})
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
const codec = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json'))
fs.writeFileSync(MC_SERVER_DIR + '_loginPacket.json', JSON.stringify({
...packet,
dimensionCodec: codec.codec || codec
}, null, 2))
console.log('Wrote loginPacket.json')
}
})
// ** End dumping code **
const lineListener = function (line) {
const match = line.match(/\[Server thread\/INFO\]: (?:\[Not Secure\] )?<(.+?)> (.+)/)
if (!match) return
@ -117,26 +154,14 @@ for (const supportedVersion of mc.supportedVersions) {
}
wrap.on('line', lineListener)
let chatCount = 0
client.on('login', function (packet) {
assert.strictEqual(packet.gameMode, 0)
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))
if (packet.worldState) { // 1.20.5+
assert.strictEqual(packet.worldState.gamemode, 'survival')
} else {
assert.strictEqual(packet.gameMode, 0)
}
client.chat('hello everyone; I have logged in.')
})
client.on('playerChat', function (data) {
chatCount += 1

View file

@ -29,7 +29,7 @@ module.exports = client => {
})
client.on('registry_data', (data) => {
client.registry ??= Registry(client.version)
client.registry.loadDimensionCodec(data.codec)
client.registry.loadDimensionCodec(data.codec || data)
})
client.on('playerJoin', () => {

View file

@ -18,7 +18,6 @@ function evalCount (count, fields) {
const slotValue = {
present: true,
blockId: 5,
itemCount: 56,
itemDamage: 2,
nbtData: {
type: 'compound',
@ -32,7 +31,14 @@ const slotValue = {
test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } },
test7: { type: 'intArray', value: [12, 42] }
}
}
},
// 1.20.5
itemCount: 1,
itemId: 1111,
addedComponentCount: 0,
removedComponentCount: 0,
components: [],
removeComponents: []
}
const nbtValue = {
@ -49,6 +55,24 @@ const nbtValue = {
}
}
function getFixedPacketPayload (version, packetName) {
if (packetName === 'declare_recipes') {
if (version['>=']('1.20.5')) {
return {
recipes: [
{
name: 'minecraft:crafting_decorated_pot',
type: 'minecraft:crafting_decorated_pot',
data: {
category: 0
}
}
]
}
}
}
}
const values = {
i32: 123456,
i16: -123,
@ -57,6 +81,7 @@ const values = {
varlong: -20,
i8: -10,
u8: 8,
ByteArray: [],
string: 'hi hi this is my client string',
buffer: function (typeArgs, context) {
let count
@ -124,6 +149,11 @@ const values = {
f64: 99999.2222,
f32: -333.444,
slot: slotValue,
Slot: slotValue,
SlotComponent: {
type: 'hide_tooltip'
},
SlotComponentType: 0,
nbt: nbtValue,
optionalNbt: nbtValue,
compressedNbt: nbtValue,
@ -161,8 +191,8 @@ const values = {
const i = typeArgs.fields[getField(typeArgs.compareTo, context)]
if (i === undefined) {
if (typeArgs.default === undefined) {
throw new Error("couldn't find the field " + typeArgs.compareTo +
' of the compareTo and the default is not defined')
typeArgs.default = 'void'
// throw new Error("couldn't find the field " + typeArgs.compareTo + ' of the compareTo and the default is not defined')
}
return getValue(typeArgs.default, context)
} else { return getValue(i, context) }
@ -177,6 +207,7 @@ const values = {
})
return results
},
mapper: '',
tags: [{ tagName: 'hi', entries: [1, 2, 3, 4, 5] }],
ingredient: [slotValue],
particleData: null,
@ -212,6 +243,20 @@ const values = {
particle: {
particleId: 0,
data: null
},
Particle: {},
SpawnInfo: {
dimension: 0,
name: 'minecraft:overworld',
hashedSeed: [
572061085,
1191958278
],
gamemode: 'survival',
previousGamemode: 255,
isDebug: false,
isFlat: false,
portalCooldown: 0
}
}
@ -274,24 +319,28 @@ for (const supportedVersion of mc.supportedVersions) {
.forEach(function (packetName) {
packetInfo = packets[state][direction].types[packetName]
packetInfo = packetInfo || null
if (packetName.includes('bundle_delimiter')) return // not a real packet
if (['packet_set_projectile_power', 'packet_debug_sample_subscription'].includes(packetName)) return
it(state + ',' + (direction === 'toServer' ? 'Server' : 'Client') + 'Bound,' + packetName,
callTestPacket(packetName.substr(7), packetInfo, state, direction === 'toServer'))
callTestPacket(mcData, packetName.substr(7), packetInfo, state, direction === 'toServer'))
})
})
})
function callTestPacket (packetName, packetInfo, state, toServer) {
function callTestPacket (mcData, packetName, packetInfo, state, toServer) {
return function (done) {
client.state = state
serverClient.state = state
testPacket(packetName, packetInfo, state, toServer, done)
testPacket(mcData, packetName, packetInfo, state, toServer, done)
}
}
function testPacket (packetName, packetInfo, state, toServer, done) {
function testPacket (mcData, packetName, packetInfo, state, toServer, done) {
// empty object uses default values
const packet = getValue(packetInfo, {})
const packet = getFixedPacketPayload(mcData.version, packetName) || getValue(packetInfo, {})
if (toServer) {
console.log('Writing to server', packetName, JSON.stringify(packet))
serverClient.once(packetName, function (receivedPacket) {
console.log('Recv', packetName)
try {
assertPacketsMatch(packet, receivedPacket)
} catch (e) {
@ -302,7 +351,9 @@ for (const supportedVersion of mc.supportedVersions) {
})
client.write(packetName, packet)
} else {
console.log('Writing to client', packetName, JSON.stringify(packet))
client.once(packetName, function (receivedPacket) {
console.log('Recv', packetName)
assertPacketsMatch(packet, receivedPacket)
done()
})

View file

@ -30,6 +30,9 @@ for (const supportedVersion of mc.supportedVersions) {
const version = mcData.version
const loginPacket = (client, server) => {
if (mcData.loginPacket) {
return mcData.loginPacket
}
return {
// 1.7
entityId: client.id,
@ -67,7 +70,9 @@ for (const supportedVersion of mc.supportedVersions) {
value: {}
},
worldType: 'minecraft:overworld',
death: undefined
death: undefined,
// 1.20.5
enforceSecureChat: false
// more to be added
}
}
@ -342,7 +347,6 @@ for (const supportedVersion of mc.supportedVersions) {
player1.on('login', async function (packet) {
console.log('ChatTest: Player 1 has joined')
assert.strictEqual(packet.gameMode, 1)
const player2 = applyClientHelpers(mc.createClient({
username: 'player2',
host: '127.0.0.1',