Add packet dumper, new server example, internal client/server test (#4)
* Add packet dumper, configuable vanilla server, client events * Fix server/client closing * Add internal server test * protocol: use WindowID types * Add internal client/server test * test timeout fixes * client example updates * update server example, use protocol updates Server example with bedrock-provider Use 64-bit varints for entity runtime ids * fix internal test packet path
This commit is contained in:
parent
184de537f5
commit
a55eaddc98
21 changed files with 718 additions and 243 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -8,8 +8,8 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
|
|
|||
|
|
@ -4531,7 +4531,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "window_type",
|
||||
|
|
@ -4552,7 +4552,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
|
|
@ -4595,7 +4595,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "varint"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "slot",
|
||||
|
|
@ -5481,7 +5481,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "window_type",
|
||||
|
|
@ -5526,7 +5526,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "window_type",
|
||||
|
|
|
|||
|
|
@ -2546,6 +2546,41 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"WindowIDVarint": [
|
||||
"mapper",
|
||||
{
|
||||
"type": "varint",
|
||||
"mappings": {
|
||||
"0": "inventory",
|
||||
"1": "first",
|
||||
"100": "last",
|
||||
"119": "offhand",
|
||||
"120": "armor",
|
||||
"121": "creative",
|
||||
"122": "hotbar",
|
||||
"123": "fixed_inventory",
|
||||
"124": "ui",
|
||||
"-100": "drop_contents",
|
||||
"-24": "beacon",
|
||||
"-23": "trading_output",
|
||||
"-22": "trading_use_inputs",
|
||||
"-21": "trading_input_2",
|
||||
"-20": "trading_input_1",
|
||||
"-17": "enchant_output",
|
||||
"-16": "enchant_material",
|
||||
"-15": "enchant_input",
|
||||
"-13": "anvil_output",
|
||||
"-12": "anvil_result",
|
||||
"-11": "anvil_material",
|
||||
"-10": "container_input",
|
||||
"-5": "crafting_use_ingredient",
|
||||
"-4": "crafting_result",
|
||||
"-3": "crafting_remove_ingredient",
|
||||
"-2": "crafting_add_ingredient",
|
||||
"-1": "none"
|
||||
}
|
||||
}
|
||||
],
|
||||
"WindowType": [
|
||||
"mapper",
|
||||
{
|
||||
|
|
@ -3721,7 +3756,7 @@
|
|||
},
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "platform_chat_id",
|
||||
|
|
@ -3818,7 +3853,7 @@
|
|||
},
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "entity_type",
|
||||
|
|
@ -3892,7 +3927,7 @@
|
|||
},
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "item",
|
||||
|
|
@ -3937,7 +3972,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "target",
|
||||
|
|
@ -3950,7 +3985,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "flags",
|
||||
|
|
@ -4094,7 +4129,7 @@
|
|||
},
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "coordinates",
|
||||
|
|
@ -4268,7 +4303,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "event_id",
|
||||
|
|
@ -4346,7 +4381,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "event_id",
|
||||
|
|
@ -4401,7 +4436,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "item",
|
||||
|
|
@ -4426,7 +4461,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "helmet",
|
||||
|
|
@ -4465,7 +4500,7 @@
|
|||
},
|
||||
{
|
||||
"name": "target_runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
|
|
@ -4526,7 +4561,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "action",
|
||||
|
|
@ -4556,7 +4591,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "metadata",
|
||||
|
|
@ -4573,7 +4608,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "velocity",
|
||||
|
|
@ -4638,7 +4673,7 @@
|
|||
},
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
@ -4663,7 +4698,7 @@
|
|||
},
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
|
@ -4672,7 +4707,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "window_type",
|
||||
|
|
@ -4693,7 +4728,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
|
|
@ -4736,7 +4771,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "varint"
|
||||
"type": "WindowIDVarint"
|
||||
},
|
||||
{
|
||||
"name": "slot",
|
||||
|
|
@ -5286,7 +5321,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
|
|
@ -5653,7 +5688,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "window_type",
|
||||
|
|
@ -5698,7 +5733,7 @@
|
|||
[
|
||||
{
|
||||
"name": "window_id",
|
||||
"type": "u8"
|
||||
"type": "WindowID"
|
||||
},
|
||||
{
|
||||
"name": "window_type",
|
||||
|
|
@ -6065,7 +6100,7 @@
|
|||
[
|
||||
{
|
||||
"name": "runtime_entity_id",
|
||||
"type": "varint"
|
||||
"type": "varint64"
|
||||
},
|
||||
{
|
||||
"name": "unknown0",
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ packet_add_player:
|
|||
entity_id_self: zigzag64
|
||||
# The runtime ID of the player. The runtime ID is unique for each world session,
|
||||
# and entities are generally identified in packets using this runtime ID.
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
# An identifier only set for particular platforms when chatting (presumably only for
|
||||
# Nintendo Switch). It is otherwise an empty string, and is used to decide which players
|
||||
# are able to chat with each other.
|
||||
|
|
@ -423,7 +423,7 @@ packet_add_entity:
|
|||
!id: 0x0d
|
||||
!bound: client
|
||||
entity_id_self: zigzag64
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
entity_type: string
|
||||
x: lf32
|
||||
y: lf32
|
||||
|
|
@ -447,7 +447,7 @@ packet_add_item_entity:
|
|||
!id: 0x0f
|
||||
!bound: client
|
||||
entity_id_self: zigzag64
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
item: Item
|
||||
x: lf32
|
||||
y: lf32
|
||||
|
|
@ -461,13 +461,13 @@ packet_add_item_entity:
|
|||
packet_take_item_entity:
|
||||
!id: 0x11
|
||||
!bound: client
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
target: varint
|
||||
|
||||
packet_move_entity:
|
||||
!id: 0x12
|
||||
!bound: both
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
flags: u8
|
||||
position: vec3f
|
||||
rotation: Rotation
|
||||
|
|
@ -559,7 +559,7 @@ packet_add_painting:
|
|||
!id: 0x16
|
||||
!bound: client
|
||||
entity_id_self: zigzag64
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
coordinates: BlockCoordinates
|
||||
direction: zigzag32
|
||||
title: string
|
||||
|
|
@ -675,7 +675,7 @@ packet_block_event:
|
|||
packet_entity_event:
|
||||
!id: 0x1b
|
||||
!bound: both
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
event_id: u8 =>
|
||||
1: jump
|
||||
2: hurt_animation
|
||||
|
|
@ -739,7 +739,7 @@ packet_entity_event:
|
|||
packet_mob_effect:
|
||||
!id: 0x1c
|
||||
!bound: client
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
event_id: u8
|
||||
effect_id: zigzag32
|
||||
amplifier: zigzag32
|
||||
|
|
@ -765,7 +765,7 @@ packet_inventory_transaction:
|
|||
packet_mob_equipment:
|
||||
!id: 0x1f
|
||||
!bound: both
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
item: Item
|
||||
slot: u8
|
||||
selected_slot: u8
|
||||
|
|
@ -774,7 +774,7 @@ packet_mob_equipment:
|
|||
packet_mob_armor_equipment:
|
||||
!id: 0x20
|
||||
!bound: both
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
helmet: Item
|
||||
chestplate: Item
|
||||
leggings: Item
|
||||
|
|
@ -793,7 +793,7 @@ packet_interact:
|
|||
6: open_inventory
|
||||
# TargetEntityRuntimeID is the runtime ID of the entity that the player interacted with. This is empty
|
||||
# for the InteractActionOpenInventory action type.
|
||||
target_runtime_entity_id: varint
|
||||
target_runtime_entity_id: varint64
|
||||
# Position associated with the ActionType above. For the InteractActionMouseOverEntity, this is the
|
||||
# position relative to the entity moused over over which the player hovered with its mouse/touch. For the
|
||||
# InteractActionLeaveVehicle, this is the position that the player spawns at after leaving the vehicle.
|
||||
|
|
@ -822,7 +822,7 @@ packet_player_action:
|
|||
!bound: server
|
||||
# EntityRuntimeID is the runtime ID of the player. The runtime ID is unique for each world session, and
|
||||
# entities are generally identified in packets using this runtime ID.
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
# ActionType is the ID of the action that was executed by the player. It is one of the constants that may
|
||||
# be found above.
|
||||
action: Action
|
||||
|
|
@ -841,14 +841,14 @@ packet_hurt_armor:
|
|||
packet_set_entity_data:
|
||||
!id: 0x27
|
||||
!bound: both
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
metadata: MetadataDictionary
|
||||
tick: varint
|
||||
|
||||
packet_set_entity_motion:
|
||||
!id: 0x28
|
||||
!bound: both
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
velocity: vec3f
|
||||
|
||||
# SetActorLink is sent by the server to initiate an entity link client-side, meaning one entity will start
|
||||
|
|
@ -877,7 +877,7 @@ packet_animate:
|
|||
!id: 0x2c
|
||||
!bound: both
|
||||
action_id: zigzag32
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
|
||||
packet_respawn:
|
||||
!id: 0x2d
|
||||
|
|
@ -886,7 +886,7 @@ packet_respawn:
|
|||
y: lf32
|
||||
z: lf32
|
||||
state: u8
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
|
||||
# ContainerOpen is sent by the server to open a container client-side. This container must be physically
|
||||
# present in the world, for the packet to have any effect. Unlike Java Edition, Bedrock Edition requires that
|
||||
|
|
@ -896,7 +896,7 @@ packet_container_open:
|
|||
!bound: client
|
||||
# WindowID is the ID representing the window that is being opened. It may be used later to close the
|
||||
# container using a ContainerClose packet.
|
||||
window_id: u8
|
||||
window_id: WindowID
|
||||
# ContainerType is the type ID of the container that is being opened when opening the container at the
|
||||
# position of the packet. It depends on the block/entity, and could, for example, be the window type of
|
||||
# a chest or a hopper, but also a horse inventory.
|
||||
|
|
@ -917,7 +917,7 @@ packet_container_close:
|
|||
!bound: both
|
||||
# WindowID is the ID representing the window of the container that should be closed. It must be equal to
|
||||
# the one sent in the ContainerOpen packet to close the designated window.
|
||||
window_id: u8
|
||||
window_id: WindowID
|
||||
# ServerSide determines whether or not the container was force-closed by the server. If this value is
|
||||
# not set correctly, the client may ignore the packet and respond with a PacketViolationWarning.
|
||||
server: bool
|
||||
|
|
@ -954,7 +954,7 @@ packet_inventory_slot:
|
|||
!bound: both
|
||||
# WindowID is the ID of the window that the packet modifies. It must point to one of the windows that the
|
||||
# client currently has opened.
|
||||
window_id: varint
|
||||
window_id: WindowIDVarint
|
||||
# Slot is the index of the slot that the packet modifies. The new item will be set to the slot at this
|
||||
# index.
|
||||
slot: varint
|
||||
|
|
@ -1287,7 +1287,7 @@ packet_boss_event:
|
|||
packet_show_credits:
|
||||
!id: 0x4b
|
||||
!bound: client
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
status: zigzag32
|
||||
|
||||
# This packet sends a list of commands to the client. Commands can have
|
||||
|
|
@ -1448,7 +1448,7 @@ packet_update_trade:
|
|||
!id: 0x50
|
||||
!bound: client
|
||||
# WindowID is the ID that identifies the trading window that the client currently has opened.
|
||||
window_id: u8
|
||||
window_id: WindowID
|
||||
# WindowType is an identifier specifying the type of the window opened. In vanilla, it appears this is
|
||||
# always filled out with 15.
|
||||
window_type: WindowType
|
||||
|
|
@ -1486,7 +1486,7 @@ packet_update_equipment:
|
|||
!bound: client
|
||||
# WindowID is the identifier associated with the window that the UpdateEquip packet concerns. It is the
|
||||
# ID sent for the horse inventory that was opened before this packet was sent.
|
||||
window_id: u8
|
||||
window_id: WindowID
|
||||
# WindowType is the type of the window that was opened. Generally, this is the type of a horse inventory,
|
||||
# as the packet is specifically made for that.
|
||||
window_type: WindowType
|
||||
|
|
@ -1647,7 +1647,7 @@ packet_book_edit:
|
|||
packet_npc_request:
|
||||
!id: 0x62
|
||||
!bound: both
|
||||
runtime_entity_id: varint
|
||||
runtime_entity_id: varint64
|
||||
unknown0: u8
|
||||
unknown1: string
|
||||
unknown2: u8
|
||||
|
|
|
|||
|
|
@ -996,6 +996,35 @@ WindowID: i8 =>
|
|||
123: fixed_inventory
|
||||
124: ui
|
||||
|
||||
WindowIDVarint: varint =>
|
||||
-100: drop_contents
|
||||
-24: beacon
|
||||
-23: trading_output
|
||||
-22: trading_use_inputs
|
||||
-21: trading_input_2
|
||||
-20: trading_input_1
|
||||
-17: enchant_output
|
||||
-16: enchant_material
|
||||
-15: enchant_input
|
||||
-13: anvil_output
|
||||
-12: anvil_result
|
||||
-11: anvil_material
|
||||
-10: container_input
|
||||
-5: crafting_use_ingredient
|
||||
-4: crafting_result
|
||||
-3: crafting_remove_ingredient
|
||||
-2: crafting_add_ingredient
|
||||
-1: none
|
||||
0: inventory
|
||||
1: first
|
||||
100: last
|
||||
119: offhand
|
||||
120: armor
|
||||
121: creative
|
||||
122: hotbar
|
||||
123: fixed_inventory
|
||||
124: ui
|
||||
|
||||
WindowType: u8 =>
|
||||
0: container
|
||||
1: workbench
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const { Versions } = require('../src/options')
|
|||
const { getFiles } = require('../src/datatypes/util')
|
||||
const { join } = require('path')
|
||||
|
||||
const fileMap = {}
|
||||
let fileMap = {}
|
||||
|
||||
// Walks all the directories for each of the supported versions in options.js
|
||||
// then builds a file map for each version
|
||||
|
|
@ -23,6 +23,8 @@ function loadVersions () {
|
|||
}
|
||||
|
||||
module.exports = (protocolVersion) => {
|
||||
fileMap = {}
|
||||
loadVersions()
|
||||
return {
|
||||
// Returns the most recent file based on the specified protocolVersion
|
||||
// e.g. if `version` is 1.16 and a file for 1.16 doesn't exist, load from 1.15 file
|
||||
|
|
@ -40,5 +42,3 @@ module.exports = (protocolVersion) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadVersions()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
process.env.DEBUG = 'minecraft-protocol raknet'
|
||||
const { Client } = require('bedrock-protocol')
|
||||
const { ChunkColumn, Version } = require('bedrock-provider')
|
||||
|
||||
async function test () {
|
||||
const client = new Client({
|
||||
hostname: '127.0.0.1',
|
||||
port: 19132
|
||||
// You can specify version by adding :
|
||||
// version: '1.16.210'
|
||||
})
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
|
|
@ -20,23 +23,21 @@ async function test () {
|
|||
})
|
||||
})
|
||||
|
||||
// client.once('resource_packs_info', (packet) => {
|
||||
// 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 })
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
|
||||
// var read = 0;
|
||||
// client.on('level_chunk', (packet) => {
|
||||
// read++
|
||||
// fs.writeFileSync(`level_chunk-${read}.json`, JSON.stringify(packet, null, 2))
|
||||
// })
|
||||
client.on('level_chunk', async packet => {
|
||||
const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z)
|
||||
await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count)
|
||||
const blocks = []
|
||||
for (let x = 0; x < 16; x++) {
|
||||
for (let z = 0; z < 16; z++) {
|
||||
blocks.push(cc.getBlock(x, 0, z)) // Read some blocks in this chunk
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test()
|
||||
|
|
|
|||
48
examples/serverChunks.js
Normal file
48
examples/serverChunks.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// CHUNKS
|
||||
const { WorldProvider } = require('bedrock-provider')
|
||||
const { LevelDB } = require('leveldb-zlib')
|
||||
const { join } = require('path')
|
||||
|
||||
async function loadWorld (version) {
|
||||
const path = join(__dirname, `../tools/bds-${version}/worlds/Bedrock level/db`)
|
||||
console.log('Loading world at path', path) // Load world from testing server
|
||||
const db = new LevelDB(path, { createIfMissing: false })
|
||||
await db.open()
|
||||
const wp = new WorldProvider(db, { dimension: 0 })
|
||||
|
||||
async function requestChunks (x, z, radius) {
|
||||
const chunks = []
|
||||
const cxStart = (x >> 4) - radius
|
||||
const cxEnd = (x >> 4) + radius
|
||||
const czStart = (z >> 4) - radius
|
||||
const czEnd = (z >> 4) + radius
|
||||
|
||||
for (let cx = cxStart; cx < cxEnd; cx++) {
|
||||
for (let cz = czStart; cz < czEnd; cz++) {
|
||||
// console.log('reading chunk at ', cx, cz)
|
||||
|
||||
const cc = await wp.load(cx, cz, true)
|
||||
if (!cc) {
|
||||
// console.log('no chunk')
|
||||
continue
|
||||
}
|
||||
const cbuf = await cc.networkEncodeNoCache()
|
||||
chunks.push({
|
||||
x: cx,
|
||||
z: cz,
|
||||
sub_chunk_count: cc.sectionsLen,
|
||||
cache_enabled: false,
|
||||
blobs: [],
|
||||
payload: cbuf
|
||||
})
|
||||
// console.log('Ht',cc.sectionsLen,cc.sections)
|
||||
}
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
return { requestChunks }
|
||||
}
|
||||
|
||||
module.exports = { loadWorld }
|
||||
|
|
@ -1,39 +1,57 @@
|
|||
const fs = require('fs')
|
||||
process.env.DEBUG = 'minecraft-protocol raknet'
|
||||
/**
|
||||
* bedrock-protocol server example; to run this example you need to clone this repo from git.
|
||||
* first need to dump some packets from the vanilla server as there is alot of boilerplate
|
||||
* to send to clients.
|
||||
*
|
||||
* In your server implementation, you need to implement each of the following packets to
|
||||
* get a client to spawn like vanilla. You can look at the dumped packets in `data/1.16.10/sample`
|
||||
*
|
||||
* First, dump packets for version 1.16.210 by running `npm run dumpPackets`.
|
||||
*/
|
||||
process.env.DEBUG = 'minecraft-protocol' // packet logging
|
||||
// const fs = require('fs')
|
||||
const { Server } = require('../src/server')
|
||||
// const CreativeItems = require('../data/creativeitems.json')
|
||||
const { hasDumps } = require('../tools/genPacketDumps')
|
||||
const DataProvider = require('../data/provider')
|
||||
const { waitFor } = require('../src/datatypes/util')
|
||||
const { loadWorld } = require('./serverChunks')
|
||||
|
||||
const server = new Server({
|
||||
async function startServer (version = '1.16.210', ok) {
|
||||
if (!hasDumps(version)) {
|
||||
throw Error('You need to dump some packets first. Run tools/genPacketDumps.js')
|
||||
}
|
||||
|
||||
})
|
||||
server.create('0.0.0.0', 19132)
|
||||
const Item = require('../types/Item')(version)
|
||||
const port = 19132
|
||||
const server = new Server({ hostname: '0.0.0.0', port, version })
|
||||
let loop
|
||||
|
||||
function getPath (packetPath) {
|
||||
return DataProvider(server.options.protocolVersion).getPath(packetPath)
|
||||
}
|
||||
const getPath = (packetPath) => DataProvider(server.options.protocolVersion).getPath(packetPath)
|
||||
const get = (packetPath) => require(getPath('sample/' + packetPath))
|
||||
|
||||
function get (packetPath) {
|
||||
return require(getPath('sample/' + packetPath))
|
||||
}
|
||||
server.listen()
|
||||
console.log('Started server')
|
||||
|
||||
// const ran = false
|
||||
// Find the center position from the dumped packets
|
||||
const respawnPacket = get('packets/respawn.json')
|
||||
const world = await loadWorld(version)
|
||||
const chunks = await world.requestChunks(respawnPacket.x, respawnPacket.z, 2)
|
||||
|
||||
server.on('connect', ({ client }) => {
|
||||
/** @type {Player} */
|
||||
client.on('join', () => {
|
||||
console.log('Client joined', client.getData())
|
||||
// Connect is emitted when a client first joins our server, before authing them
|
||||
server.on('connect', client => {
|
||||
// Join is emitted after the client has been authenticated and encryption has started
|
||||
client.on('join', () => {
|
||||
console.log('Client joined', client.getData())
|
||||
|
||||
// ResourcePacksInfo is sent by the server to inform the client on what resource packs the server has. It
|
||||
// sends a list of the resource packs it has and basic information on them like the version and description.
|
||||
client.write('resource_packs_info', {
|
||||
must_accept: false,
|
||||
has_scripts: false,
|
||||
behaviour_packs: [],
|
||||
texture_packs: []
|
||||
})
|
||||
// ResourcePacksInfo is sent by the server to inform the client on what resource packs the server has. It
|
||||
// sends a list of the resource packs it has and basic information on them like the version and description.
|
||||
client.write('resource_packs_info', {
|
||||
must_accept: false,
|
||||
has_scripts: false,
|
||||
behaviour_packs: [],
|
||||
texture_packs: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_client_response', async (packet) => {
|
||||
// ResourcePackStack is sent by the server to send the order in which resource packs and behaviour packs
|
||||
// should be applied (and downloaded) by the client.
|
||||
client.write('resource_pack_stack', {
|
||||
|
|
@ -45,109 +63,83 @@ server.on('connect', ({ client }) => {
|
|||
experiments_previously_used: false
|
||||
})
|
||||
|
||||
client.once('resource_pack_client_response', async (packet) => {
|
||||
client.once('resource_pack_client_response', async rp => {
|
||||
// Tell the server we will compress everything (>=1 byte)
|
||||
client.write('network_settings', { compression_threshold: 1 })
|
||||
// Send some inventory slots
|
||||
for (let i = 0; i < 3; i++) {
|
||||
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('item_component', { entries: [] })
|
||||
client.write('set_spawn_position', get('packets/set_spawn_position.json'))
|
||||
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('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('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('network_settings', {
|
||||
compression_threshold: 1
|
||||
})
|
||||
for (const chunk of chunks) {
|
||||
client.queue('level_chunk', chunk)
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
client.queue('inventory_slot', { inventory_id: 120, slot: i, uniqueid: 0, item: { network_id: 0 } })
|
||||
}
|
||||
// Uncomment below and comment above to send dumped chunks. We use bedrock-provider in this example which is still a WIP, some blocks may be broken.
|
||||
// for (const file of fs.readdirSync(`../data/${server.options.version}/sample/chunks`)) {
|
||||
// const buffer = fs.readFileSync(`../data/${server.options.version}/sample/chunks/` + file)
|
||||
// // console.log('Sending chunk', buffer)
|
||||
// client.sendBuffer(buffer)
|
||||
// }
|
||||
|
||||
client.queue('inventory_transaction', get('packets/inventory_transaction.json'))
|
||||
client.queue('player_list', get('packets/player_list.json'))
|
||||
client.queue('start_game', get('packets/start_game.json'))
|
||||
client.queue('item_component', { entries: [] })
|
||||
client.queue('set_spawn_position', get('packets/set_spawn_position.json'))
|
||||
client.queue('set_time', { time: 5433771 })
|
||||
client.queue('set_difficulty', { difficulty: 1 })
|
||||
client.queue('set_commands_enabled', { enabled: true })
|
||||
client.queue('adventure_settings', get('packets/adventure_settings.json'))
|
||||
// Constantly send this packet to the client to tell it the center position for chunks. The client should then request these
|
||||
// missing chunks from the us if it's missing any within the radius. `radius` is in blocks.
|
||||
loop = setInterval(() => {
|
||||
client.write('network_chunk_publisher_update', { coordinates: { x: respawnPacket.x, y: 130, z: respawnPacket.z }, radius: 80 })
|
||||
}, 4500)
|
||||
|
||||
client.queue('biome_definition_list', get('packets/biome_definition_list.json'))
|
||||
client.queue('available_entity_identifiers', get('packets/available_entity_identifiers.json'))
|
||||
// Wait some time to allow for the client to recieve and load all the chunks
|
||||
setTimeout(() => {
|
||||
// Allow the client to spawn
|
||||
client.write('play_status', { status: 'player_spawn' })
|
||||
}, 6000)
|
||||
|
||||
client.queue('update_attributes', get('packets/update_attributes.json'))
|
||||
client.queue('creative_content', get('packets/creative_content.json'))
|
||||
client.queue('inventory_content', get('packets/inventory_content.json'))
|
||||
client.queue('player_hotbar', { selected_slot: 3, window_id: 0, select_slot: true })
|
||||
|
||||
client.queue('crafting_data', get('packets/crafting_data.json'))
|
||||
client.queue('available_commands', get('packets/available_commands.json'))
|
||||
client.queue('chunk_radius_update', { chunk_radius: 5 })
|
||||
|
||||
client.queue('set_entity_data', get('packets/set_entity_data.json'))
|
||||
|
||||
client.queue('game_rules_changed', get('packets/game_rules_changed.json'))
|
||||
client.queue('respawn', { x: 646.9405517578125, y: 65.62001037597656, z: 77.86255645751953, state: 0, runtime_entity_id: 0 })
|
||||
|
||||
for (const file of fs.readdirSync(`../data/${server.options.version}/sample/chunks`)) {
|
||||
const buffer = Buffer.from(fs.readFileSync(`../data/${server.options.version}/sample/chunks/` + file, 'utf8'), 'hex')
|
||||
// console.log('Sending chunk', chunk)
|
||||
client.sendBuffer(buffer)
|
||||
}
|
||||
|
||||
// for (const chunk of chunks) {
|
||||
// client.queue('level_chunk', chunk)
|
||||
// }
|
||||
|
||||
setInterval(() => {
|
||||
client.write('network_chunk_publisher_update', { coordinates: { x: 646, y: 130, z: 77 }, radius: 64 })
|
||||
}, 9500)
|
||||
|
||||
setTimeout(() => {
|
||||
client.write('play_status', { status: 'player_spawn' })
|
||||
}, 8000)
|
||||
|
||||
// Respond to tick synchronization packets
|
||||
client.on('tick_sync', (packet) => {
|
||||
client.queue('tick_sync', {
|
||||
request_time: packet.request_time,
|
||||
response_time: BigInt(Date.now())
|
||||
// Respond to tick synchronization packets
|
||||
client.on('tick_sync', (packet) => {
|
||||
client.queue('tick_sync', {
|
||||
request_time: packet.request_time,
|
||||
response_time: BigInt(Date.now())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ok()
|
||||
|
||||
return {
|
||||
kill: () => {
|
||||
clearInterval(loop)
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let server
|
||||
waitFor((res) => {
|
||||
server = startServer(process.argv[2], res)
|
||||
}, 1000 * 60 /* Wait 60 seconds for the server to start */, function onTimeout () {
|
||||
console.error('Server did not start in time')
|
||||
server?.close()
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
// CHUNKS
|
||||
// const { ChunkColumn, Version } = require('bedrock-provider')
|
||||
// const mcData = require('minecraft-data')('1.16')
|
||||
// const chunks = []
|
||||
// async function buildChunks () {
|
||||
// // "x": 40,
|
||||
// // "z": 4,
|
||||
|
||||
// const stone = mcData.blocksByName.stone
|
||||
|
||||
// for (let cx = 35; cx < 45; cx++) {
|
||||
// for (let cz = 0; cz < 8; cz++) {
|
||||
// const column = new ChunkColumn(Version.v1_2_0_bis, x, z)
|
||||
// for (let x = 0; x < 16; x++) {
|
||||
// for (let y = 0; y < 60; y++) {
|
||||
// for (let z = 0; z < 16; z++) {
|
||||
// column.setBlock(x, y, z, stone)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const ser = await column.networkEncodeNoCache()
|
||||
|
||||
// chunks.push({
|
||||
// x: cx,
|
||||
// z: cz,
|
||||
// sub_chunk_count: column.sectionsLen,
|
||||
// cache_enabled: false,
|
||||
// blobs: [],
|
||||
// payload: ser
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// // console.log('Chunks',chunks)
|
||||
// }
|
||||
|
||||
// // buildChunks()
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@
|
|||
"scripts": {
|
||||
"build": "cd tools && node compileProtocol.js",
|
||||
"prepare": "npm run build",
|
||||
"test": "mocha",
|
||||
"test": "mocha --bail",
|
||||
"pretest": "npm run lint",
|
||||
"lint": "standard",
|
||||
"vanillaServer": "node tools/startVanillaServer.js",
|
||||
"dumpPackets": "node tools/genPacketDumps.js",
|
||||
"fix": "standard --fix"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
"node-fetch": "^2.6.1",
|
||||
"prismarine-nbt": "^1.5.0",
|
||||
"protodef": "^1.11.0",
|
||||
"raknet-native": "^0.1.0",
|
||||
"raknet-native": "^0.2.0",
|
||||
"uuid-1345": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ module.exports = (client, server, options) => {
|
|||
|
||||
let pubKey = mcPubKeyToPem(getX5U(chain[0])) // the first one is client signed, allow it
|
||||
let finalKey = null
|
||||
console.log(pubKey)
|
||||
// console.log(pubKey)
|
||||
for (const token of chain) {
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' })
|
||||
console.log('Decoded', decoded)
|
||||
// console.log('Decoded', decoded)
|
||||
|
||||
// Check if signed by Mojang key
|
||||
const x5u = getX5U(token)
|
||||
|
|
@ -69,7 +69,6 @@ function getX5U (token) {
|
|||
}
|
||||
|
||||
function mcPubKeyToPem (mcPubKeyBuffer) {
|
||||
console.log(mcPubKeyBuffer)
|
||||
if (mcPubKeyBuffer[0] === '-') return mcPubKeyBuffer
|
||||
let pem = '-----BEGIN PUBLIC KEY-----\n'
|
||||
let base64PubKey = mcPubKeyBuffer.toString('base64')
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class Client extends Connection {
|
|||
}
|
||||
|
||||
this.startGameData = {}
|
||||
this.clientRuntimeId = null
|
||||
|
||||
this.startQueue()
|
||||
this.inLog = (...args) => debug('C ->', ...args)
|
||||
|
|
@ -60,6 +61,10 @@ class Client extends Connection {
|
|||
}
|
||||
}
|
||||
|
||||
get entityId () {
|
||||
return this.startGameData.runtime_entity_id
|
||||
}
|
||||
|
||||
onEncapsulated = (encapsulated, inetAddr) => {
|
||||
const buffer = Buffer.from(encapsulated.buffer)
|
||||
this.handle(buffer)
|
||||
|
|
@ -108,30 +113,29 @@ class Client extends Connection {
|
|||
}
|
||||
|
||||
onDisconnectRequest (packet) {
|
||||
// We're talking over UDP, so there is no connection to close, instead
|
||||
// we stop communicating with the server
|
||||
console.warn(`Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`)
|
||||
process.exit(1) // TODO: handle
|
||||
console.warn(`C Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`)
|
||||
this.emit('kick', packet)
|
||||
}
|
||||
|
||||
onPlayStatus (statusPacket) {
|
||||
if (this.status === ClientStatus.Initializing && this.options.autoInitPlayer === true) {
|
||||
if (statusPacket.status === 'player_spawn') {
|
||||
this.status = ClientStatus.Initialized
|
||||
this.write('set_local_player_as_initialized', { runtime_entity_id: this.startGameData.runtime_entity_id })
|
||||
this.write('set_local_player_as_initialized', { runtime_entity_id: this.entityId })
|
||||
this.emit('spawn')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close () {
|
||||
this.emit('close')
|
||||
clearInterval(this.loop)
|
||||
clearTimeout(this.connectTimeout)
|
||||
this.q = []
|
||||
this.q2 = []
|
||||
this.connection?.close()
|
||||
this.removeAllListeners()
|
||||
console.log('Closed!')
|
||||
console.log('Client closed!')
|
||||
}
|
||||
|
||||
tryRencode (name, params, actual) {
|
||||
|
|
@ -155,22 +159,13 @@ class Client extends Connection {
|
|||
const des = this.deserializer.parsePacketBuffer(packet)
|
||||
const pakData = { name: des.data.name, params: des.data.params }
|
||||
this.inLog('-> C', pakData.name/*, serialize(pakData.params).slice(0, 100) */)
|
||||
this.emit('packet', des)
|
||||
|
||||
if (debugging) {
|
||||
// Packet verifying (decode + re-encode + match test)
|
||||
if (pakData.name) {
|
||||
this.tryRencode(pakData.name, pakData.params, packet)
|
||||
}
|
||||
|
||||
// console.info('->', JSON.stringify(pakData, (k,v) => typeof v == 'bigint' ? v.toString() : v))
|
||||
// Packet dumping
|
||||
try {
|
||||
const root = __dirname + `../data/${this.options.version}/sample/`
|
||||
if (!fs.existsSync(root + `packets/${pakData.name}.json`)) {
|
||||
fs.writeFileSync(root + `packets/${pakData.name}.json`, serialize(pakData.params, 2))
|
||||
fs.writeFileSync(root + `packets/${pakData.name}.txt`, packet.toString('hex'))
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// Abstract some boilerplate before sending to listeners
|
||||
|
|
@ -188,7 +183,10 @@ class Client extends Connection {
|
|||
this.onPlayStatus(pakData.params)
|
||||
break
|
||||
default:
|
||||
// console.log('Sending to listeners')
|
||||
if (this.status !== ClientStatus.Initializing && this.status !== ClientStatus.Initialized) {
|
||||
this.inLog(`Can't accept ${des.data.name}, client not yet authenticated : ${this.status}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Emit packet
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ const ClientStatus = {
|
|||
|
||||
class Connection extends EventEmitter {
|
||||
status = ClientStatus.Disconnected
|
||||
q = []
|
||||
q2 = []
|
||||
|
||||
versionLessThan (version) {
|
||||
if (typeof version === 'string') {
|
||||
|
|
@ -121,7 +123,7 @@ class Connection extends EventEmitter {
|
|||
|
||||
// TODO: Rename this to sendEncapsulated
|
||||
sendMCPE (buffer, immediate) {
|
||||
if (this.connection.connected === false) return
|
||||
if (this.connection.connected === false || this.status === ClientStatus.Disconnected) return
|
||||
this.connection.sendReliable(buffer, immediate)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ class Server extends EventEmitter {
|
|||
/** @type {Object<string, Player>} */
|
||||
this.clients = {}
|
||||
this.clientCount = 0
|
||||
this.inLog = (...args) => debug('C -> S', ...args)
|
||||
this.outLog = (...args) => debug('S -> C', ...args)
|
||||
this.inLog = (...args) => debug('S ->', ...args)
|
||||
this.outLog = (...args) => debug('S <-', ...args)
|
||||
}
|
||||
|
||||
validateOptions () {
|
||||
|
|
@ -40,6 +40,8 @@ class Server extends EventEmitter {
|
|||
|
||||
onCloseConnection = (inetAddr, reason) => {
|
||||
console.debug('close connection', inetAddr, reason)
|
||||
delete this.clients[inetAddr]?.connection // Prevent close loop
|
||||
this.clients[inetAddr]?.close()
|
||||
delete this.clients[inetAddr]
|
||||
this.clientCount--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const { ClientStatus, Connection } = require('./connection')
|
||||
const fs = require('fs')
|
||||
const Options = require('./options')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
const { Encrypt } = require('./auth/encryption')
|
||||
const Login = require('./auth/login')
|
||||
|
|
@ -21,8 +22,8 @@ class Player extends Connection {
|
|||
|
||||
this.startQueue()
|
||||
this.status = ClientStatus.Authenticating
|
||||
this.inLog = (...args) => console.info('S -> C', ...args)
|
||||
this.outLog = (...args) => console.info('C -> S', ...args)
|
||||
this.inLog = (...args) => debug('S ->', ...args)
|
||||
this.outLog = (...args) => debug('S <-', ...args)
|
||||
}
|
||||
|
||||
getData () {
|
||||
|
|
@ -82,7 +83,7 @@ class Player extends Connection {
|
|||
/**
|
||||
* Disconnects a client
|
||||
*/
|
||||
disconnect (reason, hide = false) {
|
||||
disconnect (reason = 'Server closed', hide = false) {
|
||||
if ([ClientStatus.Authenticating, ClientStatus.Initializing].includes(this.status)) {
|
||||
this.sendDisconnectStatus('failed_server_full')
|
||||
} else {
|
||||
|
|
@ -110,6 +111,7 @@ class Player extends Connection {
|
|||
clearInterval(this.loop)
|
||||
this.connection?.close()
|
||||
this.removeAllListeners()
|
||||
this.status = ClientStatus.Disconnected
|
||||
}
|
||||
|
||||
readPacket (packet) {
|
||||
|
|
@ -124,10 +126,9 @@ class Player extends Connection {
|
|||
throw e
|
||||
}
|
||||
|
||||
console.log('-> S', des)
|
||||
switch (des.data.name) {
|
||||
case 'login':
|
||||
console.log(des)
|
||||
// console.log(des)
|
||||
this.onLogin(des)
|
||||
return
|
||||
case 'client_to_server_handshake':
|
||||
|
|
@ -136,11 +137,12 @@ class Player extends Connection {
|
|||
break
|
||||
case 'set_local_player_as_initialized':
|
||||
this.status = ClientStatus.Initialized
|
||||
this.inLog('Server client spawned')
|
||||
// Emit the 'spawn' event
|
||||
this.emit('spawn')
|
||||
break
|
||||
default:
|
||||
console.log('ignoring, unhandled')
|
||||
// console.log('ignoring, unhandled')
|
||||
}
|
||||
this.emit(des.data.name, des.data.params)
|
||||
}
|
||||
|
|
|
|||
204
test/internal.js
Normal file
204
test/internal.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// process.env.DEBUG = 'minecraft-protocol raknet'
|
||||
const { Server, Client } = require('../')
|
||||
const { dumpPackets, hasDumps } = require('../tools/genPacketDumps')
|
||||
const DataProvider = require('../data/provider')
|
||||
|
||||
// First we need to dump some packets that a vanilla server would send a vanilla
|
||||
// client. Then we can replay those back in our custom server.
|
||||
function prepare (version) {
|
||||
if (!hasDumps(version)) {
|
||||
return dumpPackets(version)
|
||||
}
|
||||
}
|
||||
|
||||
async function startTest (version = '1.16.210', ok) {
|
||||
await prepare(version)
|
||||
const Item = require('../types/Item')(version)
|
||||
const port = 19130
|
||||
const server = new Server({ hostname: '0.0.0.0', port, version })
|
||||
|
||||
function getPath (packetPath) {
|
||||
return DataProvider(server.options.protocolVersion).getPath(packetPath)
|
||||
}
|
||||
|
||||
function get (packetPath) {
|
||||
return require(getPath('sample/' + packetPath))
|
||||
}
|
||||
|
||||
server.listen()
|
||||
console.log('Started server')
|
||||
|
||||
const respawnPacket = get('packets/respawn.json')
|
||||
const chunks = await requestChunks(respawnPacket.x, respawnPacket.z, 1)
|
||||
|
||||
let loop
|
||||
|
||||
// server logic
|
||||
server.on('connect', client => {
|
||||
client.on('join', () => {
|
||||
console.log('Client joined', client.getData())
|
||||
|
||||
client.write('resource_packs_info', {
|
||||
must_accept: false,
|
||||
has_scripts: false,
|
||||
behaviour_packs: [],
|
||||
texture_packs: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_client_response', async rp => {
|
||||
// Tell the server we will compress everything (>=1 byte)
|
||||
client.write('network_settings', { compression_threshold: 1 })
|
||||
// Send some inventory slots
|
||||
for (let i = 0; i < 3; i++) {
|
||||
client.queue('inventory_slot', { window_id: 'armor', slot: 0, item: new Item().toBedrock() })
|
||||
}
|
||||
|
||||
// client.queue('inventory_transaction', get('packets/inventory_transaction.json'))
|
||||
client.queue('player_list', get('packets/player_list.json'))
|
||||
client.queue('start_game', get('packets/start_game.json'))
|
||||
client.queue('item_component', { entries: [] })
|
||||
client.queue('set_spawn_position', get('packets/set_spawn_position.json'))
|
||||
client.queue('set_time', { time: 5433771 })
|
||||
client.queue('set_difficulty', { difficulty: 1 })
|
||||
client.queue('set_commands_enabled', { enabled: true })
|
||||
client.queue('adventure_settings', get('packets/adventure_settings.json'))
|
||||
|
||||
client.queue('biome_definition_list', get('packets/biome_definition_list.json'))
|
||||
client.queue('available_entity_identifiers', get('packets/available_entity_identifiers.json'))
|
||||
|
||||
client.queue('update_attributes', get('packets/update_attributes.json'))
|
||||
client.queue('creative_content', get('packets/creative_content.json'))
|
||||
client.queue('inventory_content', get('packets/inventory_content.json'))
|
||||
|
||||
client.queue('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true })
|
||||
|
||||
client.queue('crafting_data', get('packets/crafting_data.json'))
|
||||
client.queue('available_commands', get('packets/available_commands.json'))
|
||||
client.queue('chunk_radius_update', { chunk_radius: 5 })
|
||||
|
||||
// client.queue('set_entity_data', get('packets/set_entity_data.json'))
|
||||
|
||||
client.queue('game_rules_changed', get('packets/game_rules_changed.json'))
|
||||
client.queue('respawn', get('packets/respawn.json'))
|
||||
|
||||
for (const chunk of chunks) {
|
||||
client.queue('level_chunk', chunk)
|
||||
}
|
||||
|
||||
loop = setInterval(() => {
|
||||
client.write('network_chunk_publisher_update', { coordinates: { x: 646, y: 130, z: 77 }, radius: 64 })
|
||||
}, 9500)
|
||||
|
||||
setTimeout(() => {
|
||||
client.write('play_status', { status: 'player_spawn' })
|
||||
}, 6000)
|
||||
|
||||
// Respond to tick synchronization packets
|
||||
client.on('tick_sync', (packet) => {
|
||||
client.queue('tick_sync', {
|
||||
request_time: packet.request_time,
|
||||
response_time: BigInt(Date.now())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// client logic
|
||||
const client = new Client({
|
||||
hostname: '127.0.0.1',
|
||||
port,
|
||||
username: 'Notch',
|
||||
version,
|
||||
offline: true
|
||||
})
|
||||
|
||||
console.log('Started client')
|
||||
|
||||
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 })
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
|
||||
client.once('spawn', () => {
|
||||
console.info('Client spawend!')
|
||||
setTimeout(() => {
|
||||
client.close()
|
||||
|
||||
server.close()
|
||||
ok?.()
|
||||
}, 500)
|
||||
clearInterval(loop)
|
||||
})
|
||||
}
|
||||
|
||||
const { ChunkColumn, Version } = require('bedrock-provider')
|
||||
const { waitFor } = require('../src/datatypes/util')
|
||||
const mcData = require('minecraft-data')('1.16')
|
||||
|
||||
async function requestChunks (x, z, radius) {
|
||||
const cxStart = (x >> 4) - radius
|
||||
const cxEnd = (x >> 4) + radius
|
||||
const czStart = (z >> 4) - radius
|
||||
const czEnd = (z >> 4) + radius
|
||||
|
||||
const stone = mcData.blocksByName.stone
|
||||
const chunks = []
|
||||
|
||||
for (let cx = cxStart; cx < cxEnd; cx++) {
|
||||
for (let cz = czStart; cz < czEnd; cz++) {
|
||||
console.log('reading chunk at ', cx, cz)
|
||||
const cc = new ChunkColumn(Version.v1_2_0_bis, x, z)
|
||||
|
||||
for (let x = 0; x < 16; x++) {
|
||||
for (let y = 0; y < 60; y++) {
|
||||
for (let z = 0; z < 16; z++) {
|
||||
cc.setBlock(x, y, z, stone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cc) {
|
||||
console.log('no chunk')
|
||||
continue
|
||||
}
|
||||
const cbuf = await cc.networkEncodeNoCache()
|
||||
chunks.push({
|
||||
x: cx,
|
||||
z: cz,
|
||||
sub_chunk_count: cc.sectionsLen,
|
||||
cache_enabled: false,
|
||||
blobs: [],
|
||||
payload: cbuf
|
||||
})
|
||||
// console.log('Ht',cc.sectionsLen,cc.sections)
|
||||
}
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
async function timedTest (version, timeout = 1000 * 120) {
|
||||
await waitFor((res) => {
|
||||
startTest(version, res)
|
||||
}, timeout, () => {
|
||||
throw Error('timed out')
|
||||
})
|
||||
console.info('✔ ok')
|
||||
}
|
||||
|
||||
if (!module.parent) timedTest()
|
||||
module.exports = { startTest, timedTest, requestChunks }
|
||||
11
test/internal.test.js
Normal file
11
test/internal.test.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
const { timedTest } = require('./internal')
|
||||
|
||||
describe('internal client/server test', function () {
|
||||
this.timeout(120 * 1000)
|
||||
|
||||
it('connects', async () => {
|
||||
await timedTest()
|
||||
})
|
||||
})
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
const vanillaServer = require('../tools/startVanillaServer')
|
||||
const { Client } = require('../src/client')
|
||||
const { waitFor } = require('../src/datatypes/util')
|
||||
const { ChunkColumn, Version } = require('bedrock-provider')
|
||||
|
||||
async function test (version) {
|
||||
// Start the server, wait for it to accept clients, throws on timeout
|
||||
|
|
@ -42,6 +43,11 @@ async function test (version) {
|
|||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
|
||||
}, 200)
|
||||
|
||||
client.on('level_chunk', async packet => { // Chunk read test
|
||||
const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z)
|
||||
await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count)
|
||||
})
|
||||
|
||||
console.log('Awaiting join')
|
||||
|
||||
client.on('spawn', () => {
|
||||
|
|
|
|||
100
tools/genPacketDumps.js
Normal file
100
tools/genPacketDumps.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Collect sample packets needed for `serverTest.js`
|
||||
// process.env.DEBUG = 'minecraft-protocol'
|
||||
const fs = require('fs')
|
||||
const vanillaServer = require('../tools/startVanillaServer')
|
||||
const { Client } = require('../src/client')
|
||||
const { serialize, waitFor, getFiles } = require('../src/datatypes/util')
|
||||
const { CURRENT_VERSION } = require('../src/options')
|
||||
const { join } = require('path')
|
||||
|
||||
function hasDumps (version) {
|
||||
const root = join(__dirname, `../data/${version}/sample/packets/`)
|
||||
if (!fs.existsSync(root) || getFiles(root).length < 10) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
let loop
|
||||
|
||||
async function dump (version, force) {
|
||||
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: 'Boat' + random,
|
||||
offline: true
|
||||
})
|
||||
|
||||
return waitFor(async res => {
|
||||
const root = join(__dirname, `../data/${client.options.version}/sample/`)
|
||||
if (!fs.existsSync(root + 'packets') || !fs.existsSync(root + 'chunks')) {
|
||||
fs.mkdirSync(root + 'packets', { recursive: true })
|
||||
fs.mkdirSync(root + 'chunks', { recursive: true })
|
||||
}
|
||||
|
||||
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 })
|
||||
// client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
|
||||
clearInterval(loop)
|
||||
loop = setInterval(() => {
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
|
||||
}, 200)
|
||||
})
|
||||
|
||||
let i = 0
|
||||
|
||||
client.on('packet', async packet => { // Packet dumping
|
||||
const { name, params } = packet.data
|
||||
if (name === 'level_chunk') {
|
||||
fs.writeFileSync(root + `chunks/${name}-${i++}.bin`, packet.buffer)
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(root + `packets/${name}.json`) || force) {
|
||||
fs.writeFileSync(root + `packets/${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()
|
||||
})
|
||||
}, 1000 * 60, () => {
|
||||
clearInterval(loop)
|
||||
handle.kill()
|
||||
throw Error('timed out')
|
||||
})
|
||||
}
|
||||
|
||||
if (!module.parent) {
|
||||
dump(null, true).then(() => {
|
||||
console.log('Successfully dumped packets')
|
||||
})
|
||||
}
|
||||
module.exports = { dumpPackets: dump, hasDumps }
|
||||
|
|
@ -17,18 +17,18 @@ function fetchLatestStable () {
|
|||
}
|
||||
|
||||
// Download + extract vanilla server and enter the directory
|
||||
async function download (os, version) {
|
||||
async function download (os, version, path = 'bds-') {
|
||||
process.chdir(__dirname)
|
||||
const verStr = version.split('.').slice(0, 3).join('.')
|
||||
const dir = 'bds-' + version
|
||||
const dir = path + version
|
||||
|
||||
if (fs.existsSync(dir) && getFiles(dir).length) {
|
||||
process.chdir('bds-' + version) // Enter server folder
|
||||
process.chdir(path + version) // Enter server folder
|
||||
return verStr
|
||||
}
|
||||
try { fs.mkdirSync(dir) } catch { }
|
||||
|
||||
process.chdir('bds-' + version) // Enter server folder
|
||||
process.chdir(path + version) // Enter server folder
|
||||
const url = (os, version) => `https://minecraft.azureedge.net/bin-${os}/bedrock-server-${version}.zip`
|
||||
|
||||
let found = false
|
||||
|
|
@ -53,10 +53,18 @@ async function download (os, version) {
|
|||
return verStr
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
'level-generator': '2',
|
||||
'server-port': '19130',
|
||||
'online-mode': 'false'
|
||||
}
|
||||
|
||||
// Setup the server
|
||||
function configure () {
|
||||
function configure (options = {}) {
|
||||
const opts = { ...defaultOptions, ...options }
|
||||
let config = fs.readFileSync('./server.properties', 'utf-8')
|
||||
config += '\nlevel-generator=2\nserver-port=19130\nplayer-idle-timeout=1\nallow-cheats=true\ndefault-player-permission-level=operator\nonline-mode=false'
|
||||
config += '\nplayer-idle-timeout=1\nallow-cheats=true\ndefault-player-permission-level=operator'
|
||||
for (const o in opts) config += `\n${o}=${opts[o]}`
|
||||
fs.writeFileSync('./server.properties', config)
|
||||
}
|
||||
|
||||
|
|
@ -66,13 +74,13 @@ function run (inheritStdout = true) {
|
|||
}
|
||||
|
||||
// Run the server
|
||||
async function startServer (version, onStart) {
|
||||
async function startServer (version, onStart, options = {}) {
|
||||
const os = process.platform === 'win32' ? 'win' : process.platform
|
||||
if (os !== 'win' && os !== 'linux') {
|
||||
throw Error('unsupported os ' + os)
|
||||
}
|
||||
await download(os, version)
|
||||
configure()
|
||||
await download(os, version, options.path)
|
||||
configure(options)
|
||||
const handle = run(!onStart)
|
||||
if (onStart) {
|
||||
handle.stdout.on('data', data => data.includes('Server started.') ? onStart() : null)
|
||||
|
|
@ -83,10 +91,10 @@ async function startServer (version, onStart) {
|
|||
}
|
||||
|
||||
// Start the server and wait for it to be ready, with a timeout
|
||||
async function startServerAndWait (version, withTimeout) {
|
||||
async function startServerAndWait (version, withTimeout, options) {
|
||||
let handle
|
||||
await waitFor(async res => {
|
||||
handle = await startServer(version, res)
|
||||
handle = await startServer(version, res, options)
|
||||
}, withTimeout, () => {
|
||||
handle?.kill()
|
||||
throw new Error('Server did not start on time ' + withTimeout)
|
||||
|
|
|
|||
37
types/Item.js
Normal file
37
types/Item.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
module.exports = (version) =>
|
||||
class Item {
|
||||
nbt
|
||||
constructor (obj) {
|
||||
this.networkId = 0
|
||||
this.runtimeId = 0
|
||||
this.count = 0
|
||||
this.metadata = 0
|
||||
Object.assign(this, obj)
|
||||
this.version = version
|
||||
}
|
||||
|
||||
static fromBedrock (obj) {
|
||||
return new Item({
|
||||
runtimeId: obj.runtime_id,
|
||||
networkId: obj.item?.network_id,
|
||||
count: obj.item?.auxiliary_value & 0xff,
|
||||
metadata: obj.item?.auxiliary_value >> 8,
|
||||
nbt: obj.item?.nbt?.nbt
|
||||
})
|
||||
}
|
||||
|
||||
toBedrock () {
|
||||
return {
|
||||
runtime_id: this.runtimeId,
|
||||
item: {
|
||||
network_id: this.networkId,
|
||||
auxiliary_value: (this.metadata << 8) | (this.count & 0xff),
|
||||
has_nbt: !!this.nbt,
|
||||
nbt: { version: 1, nbt: this.nbt },
|
||||
can_place_on: [],
|
||||
can_destroy: [],
|
||||
blocking_tick: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue