From 3369fc279062d2c6095288752dbc81b0d10af32b Mon Sep 17 00:00:00 2001 From: extremeheat Date: Fri, 26 Mar 2021 04:41:42 -0400 Subject: [PATCH] update server example, use protocol updates Server example with bedrock-provider Use 64-bit varints for entity runtime ids --- data/1.16.210/protocol.json | 36 +++--- data/latest/proto.yml | 36 +++--- examples/clientTest.js | 5 +- examples/serverChunks.js | 48 ++++++++ examples/serverTest.js | 234 +++++++++++++++++------------------- src/client.js | 2 +- 6 files changed, 200 insertions(+), 161 deletions(-) create mode 100644 examples/serverChunks.js diff --git a/data/1.16.210/protocol.json b/data/1.16.210/protocol.json index 27a31ae..8c34b12 100644 --- a/data/1.16.210/protocol.json +++ b/data/1.16.210/protocol.json @@ -3756,7 +3756,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "platform_chat_id", @@ -3853,7 +3853,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "entity_type", @@ -3927,7 +3927,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "item", @@ -3972,7 +3972,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "target", @@ -3985,7 +3985,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "flags", @@ -4129,7 +4129,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "coordinates", @@ -4303,7 +4303,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "event_id", @@ -4381,7 +4381,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "event_id", @@ -4436,7 +4436,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "item", @@ -4461,7 +4461,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "helmet", @@ -4500,7 +4500,7 @@ }, { "name": "target_runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "position", @@ -4561,7 +4561,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "action", @@ -4591,7 +4591,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "metadata", @@ -4608,7 +4608,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "velocity", @@ -4673,7 +4673,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" } ] ], @@ -4698,7 +4698,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" } ] ], @@ -5321,7 +5321,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "status", @@ -6100,7 +6100,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "unknown0", diff --git a/data/latest/proto.yml b/data/latest/proto.yml index 713dfff..68a7bbe 100644 --- a/data/latest/proto.yml +++ b/data/latest/proto.yml @@ -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 @@ -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 @@ -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 diff --git a/examples/clientTest.js b/examples/clientTest.js index b523d19..bcb644e 100644 --- a/examples/clientTest.js +++ b/examples/clientTest.js @@ -5,7 +5,7 @@ const { ChunkColumn, Version } = require('bedrock-provider') async function test () { const client = new Client({ hostname: '127.0.0.1', - port: 19130 + port: 19132 // You can specify version by adding : // version: '1.16.210' }) @@ -23,7 +23,6 @@ async function test () { }) }) - 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 }) @@ -32,7 +31,7 @@ async function test () { 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) - let blocks = [] + 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 diff --git a/examples/serverChunks.js b/examples/serverChunks.js new file mode 100644 index 0000000..0b711c4 --- /dev/null +++ b/examples/serverChunks.js @@ -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 } diff --git a/examples/serverTest.js b/examples/serverTest.js index 5006736..58b083e 100644 --- a/examples/serverTest.js +++ b/examples/serverTest.js @@ -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() diff --git a/src/client.js b/src/client.js index 829eb0d..2fb3ce1 100644 --- a/src/client.js +++ b/src/client.js @@ -61,7 +61,7 @@ class Client extends Connection { } } - get entityId() { + get entityId () { return this.startGameData.runtime_entity_id }