This commit is contained in:
extremeheat 2021-04-14 07:25:38 -04:00
commit aec81efba8
3 changed files with 331 additions and 14 deletions

152
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,152 @@
CONTRIBUTING.md
Contributions are always welcome :)
## Updating
Good sources for the Minecraft bedrock protocol are [gophertunnel](https://github.com/Sandertv/gophertunnel/tree/master/minecraft/protocol/packet), [ClouburstMC's protocol library](https://github.com/CloudburstMC/Protocol) and [PocketMine](https://github.com/pmmp/PocketMine-MP/tree/stable/src/pocketmine/network/mcpe/protocol).
Steps to update:
* Add the version to src/options.js
* Open [data/latest/proto.yml](https://github.com/PrismarineJS/bedrock-protocol/tree/new/data/latest) and add, remove or modify the updated packets (see the [Packet serialization](#Packet_serialization) notes at the bottom for info on syntax)
* Save and make sure to update the !version field at the top of the file
* Run `npm run build` and `npm test` to test
## Packet serialization
This project uses ProtoDef to serialize and deserialize Minecraft packets. See the documentation [here](https://github.com/ProtoDef-io/node-protodef).
The ProtoDef schema is JSON can be found [here](https://github.com/PrismarineJS/bedrock-protocol/blob/4169453835790de7eeaa8fb6f5a6b4344f71036b/data/1.16.210/protocol.json) for use in other languages.
In bedrock-protocol, JavaScript code is generated from the JSON through the node-protodef compiler.
#### YAML syntax
For easier maintainability, the JSON is generated from a more human readable YAML format. You can read more [here](https://github.com/extremeheat/protodef-yaml).
Some documentation is below.
Packets should go in proto.yml and extra types should go in types.yml.
```yml
# This defines a new data structure, a ProtoDef container.
PlayerPosition:
# Variable `x` in this struct has a type of `li32`, a little-endian 32-bit integer
x: li32
# `z` is a 32-bit LE *unsigned* integer
z: lu32
# `b` is a 32-bit LE floating point
y: lf32
# Fields starting with `packet_` are structs representing Minecraft packets
packet_player_position:
# Fields starting with ! are ignored by the parser. '!id' is used by the parser when generating the packet map
!id: 0x29 # This packet is ID #0x29
!bound: client # `client` or `server` bound, just for documentation purposes. This has no other effect.
# Read `on_ground` as a boolean
on_ground: bool
# Read `position` as custom data type `PlayerPosition` defined above.
position: Position
# Reads a 8-bit unsigned integer, then maps it to a string
movement_reason: u8 =>
0: player_jump
1: player_autojump
2: player_sneak
3: player_sprint
4: player_fall
# A `_` as a field name declares an anonymous data structure which will be inlined. Adding a '?' at the end will start a `switch` statement
_: movement_reason ?
# if the condition matches to the string "player_jump" or "player_autojump", there is a data struct that needs to be read
if player_jump or player_autojump:
# read `original_position` as a `Position`
original_position: Position
jump_tick: li64
# if the condition matches "player_fall", read the containing field
if player_fall:
original_position: Position
default: void
# Another way to declare a switch, without an anonymous structure. `player_hunger` will be read as a 8-bit int if movement_reason == "player_sprint"
player_hunger: movement_reason ?
if player_sprint: u8
# The default statement as in a switch statement
default: void
# Square brackets notate an array. At the left is the type of the array values, at the right is the type of
# the length prefix. If no type at the left is specified, the type is defined below.
# Reads an array of `Position`, length-prefixed with a ProtoBuf-type unsigned variable length integer (VarInt)
last_positions: Position[]varint
# Reads an array, length-prefixed with a zigzag-encoded signed VarInt
# The data structure for the array is defined underneath
keys_down: []zigzag32
up: bool
down: bool
shift: bool
```
The above roughly translates to the following JavaScript code to read a packet:
```js
function read_player_position(stream) {
const ret = {}
ret.x = stream.readSignedInt32LE()
ret.z = stream.readUnsignedInt32LE()
ret.y = stream.readFloat32LE()
return ret
}
function read_player_position(stream) {
const ret = {}
ret.on_ground = Boolean(stream.readU8())
ret.position = read_player_position(stream)
let __movement_reason = stream.readU8()
let movement_reason = { 0: 'player_jump', 1: 'player_autojump', 2: 'player_sneak', 3: 'player_sprint', 4: 'player_fall' }[__movement_reason]
switch (movement_reason) {
case 'player_jump':
case 'player_autojump':
ret.original_position = read_player_position(stream)
ret.jump_tick = stream.readInt64LE(stream)
break
case 'player_fall':
ret.original_position = read_player_position(stream)
break
default: break
}
ret.player_hunger = undefined
if (movement_reason == 'player_sprint') ret.player_hunger = stream.readU8()
ret.last_positions = []
for (let i = 0; i < stream.readUnsignedVarInt(); i++) {
ret.last_positions.push(read_player_position(stream))
}
ret.keys_down = []
for (let i = 0; i < stream.readZigZagVarInt(); i++) {
const ret1 = {}
ret1.up = Boolean(stream.readU8())
ret1.down = Boolean(stream.readU8())
ret1.shift = Boolean(stream.readU8())
ret.keys_down.push(ret1)
}
return ret
}
```
and the results in the following JSON for the packet:
```json
{
"on_ground": false,
"position": { "x": 0, "y": 2, "z": 0 },
"movement_reason": "player_jump",
"original_position": { "x": 0, "y": 0, "z": 0 },
"jump_tick": 494894984,
"last_positions": [{ "x": 0, "y": 1, "z": 0 }],
"keys_down": []
}
```
Custom ProtoDef types can be inlined as JSON:
```yml
string: ["pstring",{"countType":"varint"}]
```

View file

@ -27,7 +27,7 @@ async function startServer (version = '1.16.210', ok) {
let loop
const getPath = (packetPath) => DataProvider(server.options.protocolVersion).getPath(packetPath)
const get = (packetPath) => require(getPath('sample/' + packetPath))
const get = (packetName) => require(getPath(`sample/packets/${packetName}.json`))
server.listen()
console.log('Started server')
@ -71,25 +71,25 @@ async function startServer (version = '1.16.210', ok) {
client.queue('inventory_slot', { window_id: 120, slot: 0, item: new Item().toBedrock() })
}
client.write('player_list', get('packets/player_list.json'))
client.write('start_game', get('packets/start_game.json'))
client.write('player_list', get('player_list'))
client.write('start_game', get('start_game'))
client.write('item_component', { entries: [] })
client.write('set_spawn_position', get('packets/set_spawn_position.json'))
client.write('set_spawn_position', get('set_spawn_position'))
client.write('set_time', { time: 5433771 })
client.write('set_difficulty', { difficulty: 1 })
client.write('set_commands_enabled', { enabled: true })
client.write('adventure_settings', get('packets/adventure_settings.json'))
client.write('biome_definition_list', get('packets/biome_definition_list.json'))
client.write('available_entity_identifiers', get('packets/available_entity_identifiers.json'))
client.write('update_attributes', get('packets/update_attributes.json'))
client.write('creative_content', get('packets/creative_content.json'))
client.write('inventory_content', get('packets/inventory_content.json'))
client.write('adventure_settings', get('adventure_settings'))
client.write('biome_definition_list', get('biome_definition_list'))
client.write('available_entity_identifiers', get('available_entity_identifiers'))
client.write('update_attributes', get('update_attributes'))
client.write('creative_content', get('creative_content'))
client.write('inventory_content', get('inventory_content'))
client.write('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true })
client.write('crafting_data', get('packets/crafting_data.json'))
client.write('available_commands', get('packets/available_commands.json'))
client.write('crafting_data', get('crafting_data'))
client.write('available_commands', get('available_commands'))
client.write('chunk_radius_update', { chunk_radius: 1 })
client.write('game_rules_changed', get('packets/game_rules_changed.json'))
client.write('respawn', get('packets/respawn.json'))
client.write('game_rules_changed', get('game_rules_changed'))
client.write('respawn', get('respawn'))
for (const chunk of chunks) {
client.queue('level_chunk', chunk)

165
tools/dumpPackets.js Normal file
View file

@ -0,0 +1,165 @@
// dumps (up to 5 of each) packet encountered until 'spawn' event
// uses the same format as prismarine-packet-dumper
const assert = require('assert')
const fs = require('fs')
const vanillaServer = require('../tools/startVanillaServer')
const { Client } = require('../src/client')
const { serialize, waitFor } = require('../src/datatypes/util')
const { CURRENT_VERSION } = require('../src/options')
const path = require('path')
const output = path.resolve(process.argv[3] ?? 'output')
let loop
async function dump (version) {
const random = ((Math.random() * 100) | 0)
const port = 19130 + random
const handle = await vanillaServer.startServerAndWait(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port })
console.log('Started server')
const client = new Client({
hostname: '127.0.0.1',
port,
username: 'dumpBot',
offline: true
})
return waitFor(async res => {
await fs.promises.mkdir(output)
await fs.promises.mkdir(path.join(output, 'from-server'))
client.once('resource_packs_info', (packet) => {
client.write('resource_pack_client_response', {
response_status: 'completed',
resourcepackids: []
})
client.once('resource_pack_stack', (stack) => {
client.write('resource_pack_client_response', {
response_status: 'completed',
resourcepackids: []
})
})
client.queue('client_cache_status', { enabled: false })
client.queue('request_chunk_radius', { chunk_radius: 1 })
clearInterval(loop)
loop = setInterval(() => {
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
}, 200)
})
const kindCounter = {}
const MAX_PACKETS_PER_TYPE = 5
client.on('packet', async packet => { // Packet dumping
const { fullBuffer, data: { name, params } } = packet
if (!packet.data.name) return
if (!kindCounter[packet.name]) {
await fs.promises.mkdir(path.join(output, 'from-server', name), { recursive: true })
kindCounter[name] = 0
}
if (kindCounter[name] === MAX_PACKETS_PER_TYPE) return
kindCounter[name]++
await fs.promises.writeFile(path.join(output, 'from-server', name, `${kindCounter[name]}.bin`), fullBuffer)
try {
fs.writeFileSync(path.join(output, 'from-server', name, `${kindCounter[name]}.json`), serialize(params, 2))
} catch (e) {
console.log(e)
}
})
console.log('Awaiting join...')
client.on('spawn', () => {
console.log('Spawned!')
clearInterval(loop)
client.close()
handle.kill()
res(kindCounter)
})
}, 1000 * 60, () => {
clearInterval(loop)
handle.kill()
throw Error('timed out')
})
}
const makeDropdownStart = (name, arr) => {
arr.push(`<details><summary>${name}</summary>`)
arr.push('<p>')
arr.push('')
}
const makeDropdownEnd = (arr) => {
arr.push('')
arr.push('</p>')
arr.push('</details>')
}
function makeMarkdown (data) {
const str = []
const { collected, missing } = data
makeDropdownStart(`Collected (${collected.length})`, str)
str.push('| Packet |')
str.push('| --- |')
collected.forEach(elem => {
str.push(`| ${elem} |`)
})
makeDropdownEnd(str)
makeDropdownStart(`Missing (${missing.length})`, str)
str.push('| Packet |')
str.push('| --- |')
missing.forEach(elem => {
str.push(`| ${elem} |`)
})
makeDropdownEnd(str)
return str.join('\n')
}
function parsePacketCounter (version, kindCounter) {
const protocol = require(`../data/${version}/protocol.json`)
// record packets
return {
collectedPackets: Object.keys(kindCounter),
allPackets: Object.keys(protocol)
.filter(o => o.startsWith('packet_'))
.map(o => o.replace('packet_', ''))
}
}
async function makeStats (kindCounter, version) {
const { collectedPackets, allPackets } = parsePacketCounter(version, kindCounter)
// write packet data
const data = {
collected: collectedPackets,
missing: allPackets.filter(o => !collectedPackets.includes(o))
}
const metadataFolder = path.join(output, 'metadata')
await fs.promises.writeFile(path.join(output, 'README.md'), makeMarkdown(data))
await fs.promises.mkdir(metadataFolder)
await fs.promises.writeFile(path.join(metadataFolder, 'packets_info.json'), JSON.stringify(data, null, 2))
}
async function main () {
const version = process.argv[2]
if (!version) {
console.error('Usage: node dumpPackets.js <version> [outputPath]')
}
const vers = Object.keys(require('../src/options').Versions)
assert(vers.includes(version), 'Version not supported')
if (fs.existsSync(output)) fs.promises.rm(output, { force: true, recursive: true })
const kindCounter = await dump(version)
await fs.promises.rm(path.join(output, '..', `bds-${version}`), { recursive: true })
await makeStats(kindCounter, version)
console.log('Successfully dumped packets')
}
main()