Merge branch 'master' of https://github.com/PrismarineJS/bedrock-protocol into examples
This commit is contained in:
commit
aec81efba8
3 changed files with 331 additions and 14 deletions
152
CONTRIBUTING.md
Normal file
152
CONTRIBUTING.md
Normal 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"}]
|
||||
```
|
||||
|
|
@ -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
165
tools/dumpPackets.js
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue