Compare commits

..

3 commits

Author SHA1 Message Date
extremeheat
41438db660
Update CONTRIBUTING.md 2024-01-27 13:12:21 -05:00
extremeheat
91b6a6c723
Update README.md 2024-01-27 12:54:08 -05:00
extremeheat
1309596fe2
Update and rename CONTRIBUTING.md to docs/CONTRIBUTING.md 2024-01-27 12:51:25 -05:00
32 changed files with 475 additions and 347 deletions

59
.github/helper-bot/github-helper.js vendored Normal file
View file

@ -0,0 +1,59 @@
if (!process.env.CI) {
// mock a bunch of things for testing locally -- https://github.com/actions/toolkit/issues/71
process.env.GITHUB_REPOSITORY = 'PrismarineJS/bedrock-protocol'
process.env.GITHUB_EVENT_NAME = 'issue_comment'
process.env.GITHUB_SHA = 'cb2fd97b6eae9f2c7fee79d5a86eb9c3b4ac80d8'
process.env.GITHUB_REF = 'refs/heads/master'
process.env.GITHUB_WORKFLOW = 'Issue comments'
process.env.GITHUB_ACTION = 'run1'
process.env.GITHUB_ACTOR = 'test-user'
module.exports = { getIssueStatus: () => ({}), updateIssue: () => {}, createIssue: () => {} }
return
}
// const { Octokit } = require('@octokit/rest') // https://github.com/octokit/rest.js
const github = require('@actions/github')
const token = process.env.GITHUB_TOKEN
const octokit = github.getOctokit(token)
const context = github.context
async function getIssueStatus (title) {
// https://docs.github.com/en/rest/reference/search#search-issues-and-pull-requests
const existingIssues = await octokit.rest.search.issuesAndPullRequests({
q: `is:issue repo:${process.env.GITHUB_REPOSITORY} in:title ${title}`
})
// console.log('Existing issues', existingIssues)
const existingIssue = existingIssues.data.items.find(issue => issue.title === title)
if (!existingIssue) return {}
return { open: existingIssue.state === 'open', closed: existingIssue.state === 'closed', id: existingIssue.number }
}
async function updateIssue (id, payload) {
const issue = await octokit.rest.issues.update({
...context.repo,
issue_number: id,
body: payload.body
})
console.log(`Updated issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`)
}
async function createIssue (payload) {
const issue = await octokit.rest.issues.create({
...context.repo,
...payload
})
console.log(`Created issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`)
}
async function close (id, reason) {
if (reason) await octokit.rest.issues.createComment({ ...context.repo, issue_number: id, body: reason })
const issue = await octokit.rest.issues.update({ ...context.repo, issue_number: id, state: 'closed' })
console.log(`Closed issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`)
}
if (process.env.CI) {
module.exports = { getIssueStatus, updateIssue, createIssue, close }
}

View file

@ -1,7 +1,7 @@
// Automatic version update checker for Minecraft bedrock edition. // Automatic version update checker for Minecraft bedrock edition.
const fs = require('fs') const fs = require('fs')
const cp = require('child_process') const cp = require('child_process')
const helper = require('gh-helpers')() const helper = require('./github-helper')
const latestVesionEndpoint = 'https://itunes.apple.com/lookup?bundleId=com.mojang.minecraftpe&time=' + Date.now() const latestVesionEndpoint = 'https://itunes.apple.com/lookup?bundleId=com.mojang.minecraftpe&time=' + Date.now()
const changelogURL = 'https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs' const changelogURL = 'https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs'
@ -102,10 +102,11 @@ async function fetchLatest () {
console.log(version, currentVersionReleaseDate, releaseNotes) console.log(version, currentVersionReleaseDate, releaseNotes)
const title = `Support Minecraft ${result.version}` const title = `Support Minecraft ${result.version}`
const issueStatus = await helper.findIssue({ titleIncludes: title }) || {}
const issueStatus = await helper.getIssueStatus(title)
if (supportedVersions.includes(version)) { if (supportedVersions.includes(version)) {
if (issueStatus.isOpen) { if (issueStatus.open) {
helper.close(issueStatus.id, `Closing as ${version} is now supported`) helper.close(issueStatus.id, `Closing as ${version} is now supported`)
} }
console.log('Latest version is supported.') console.log('Latest version is supported.')
@ -113,7 +114,7 @@ async function fetchLatest () {
} }
if (issueStatus.isClosed) { if (issueStatus.closed) {
// We already made an issue, but someone else already closed it, don't do anything else // We already made an issue, but someone else already closed it, don't do anything else
console.log('I already made an issue, but it was closed') console.log('I already made an issue, but it was closed')
return return
@ -126,7 +127,7 @@ async function fetchLatest () {
CloudburstMC: getCommitsInRepo('CloudburstMC/Protocol', version, currentVersionReleaseDate) CloudburstMC: getCommitsInRepo('CloudburstMC/Protocol', version, currentVersionReleaseDate)
}) })
if (issueStatus.isOpen) { if (issueStatus.open) {
helper.updateIssue(issueStatus.id, issuePayload) helper.updateIssue(issueStatus.id, issuePayload)
} else { } else {
helper.createIssue(issuePayload) helper.createIssue(issuePayload)

View file

@ -17,7 +17,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
node-version: [22.x] node-version: [18.x]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 14 timeout-minutes: 14
steps: steps:
@ -26,11 +26,5 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
# Old versions of bedrock use old libssl that Ubuntu no longer ships with; need manual install
- name: (Linux) Install libssl 1.1
if: runner.os == 'Linux'
run: |
wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb
sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb
- run: npm install - run: npm install
- run: npm test - run: npm test

View file

@ -14,9 +14,9 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@master uses: actions/setup-node@master
with: with:
node-version: 22.0.0 node-version: 18.0.0
- name: Install Github Actions helper - name: Install Github Actions toolkit
run: npm i gh-helpers run: npm i @actions/github
# The env vars contain the relevant trigger information, so we don't need to pass it # The env vars contain the relevant trigger information, so we don't need to pass it
- name: Runs helper - name: Runs helper
run: cd .github/helper-bot && node index.js run: cd .github/helper-bot && node index.js

View file

@ -1,79 +1,3 @@
## 3.49.0
* [1.21.111 (#649)](https://github.com/PrismarineJS/bedrock-protocol/commit/b48518a6e79e72101fe7136433cbd6277339fc5c) (thanks @Slauh)
* [Skin Data Changes (#647)](https://github.com/PrismarineJS/bedrock-protocol/commit/407756b93880cdda4fdbff194fc4163ceedf4e82) (thanks @thejfkvis)
## 3.48.1
* [Update login client skinData (#635)](https://github.com/PrismarineJS/bedrock-protocol/commit/6b1474d2c6f93b47dee9d4816de59579f82ed5a9) (thanks @TSL534)
## 3.48.0
* [1.21.100 (#632)](https://github.com/PrismarineJS/bedrock-protocol/commit/06fb3de3a0023d03201dbcee7e9178c269462766) (thanks @extremeheat)
## 3.47.0
* [1.21.93 support (#623)](https://github.com/PrismarineJS/bedrock-protocol/commit/14daa2d95aac90ffcc7b42d625e270020ec2f162) (thanks @CreeperG16)
## 3.46.0
* [1.21.90 support (#617)](https://github.com/PrismarineJS/bedrock-protocol/commit/c66cdd3d62d2fa9c581693d8c70d7b41f355b63e) (thanks @CreeperG16)
## 3.45.0
* [1.21.80 (#602)](https://github.com/PrismarineJS/bedrock-protocol/commit/e71fd513ddbd432983f221980080b61e11576965) (thanks @extremeheat)
## 3.44.0
* [1.21.70 (#594)](https://github.com/PrismarineJS/bedrock-protocol/commit/065f41db8cfc8cbd8106bd9e376c899ec25f3f77) (thanks @CreeperG16)
## 3.43.1
* [Fix server not correctly removing clients (#588)](https://github.com/PrismarineJS/bedrock-protocol/commit/47f342ca958ba87a7719783bd5c855cebdd4aa65) (thanks @EntifiedOptics)
## 3.43.0
* [1.21.60 support (#570)](https://github.com/PrismarineJS/bedrock-protocol/commit/eeb5e47e35f31cc571a9a8a491f5a89b27e637f1) (thanks @CreeperG16)
* [Fix version feature handling (#572)](https://github.com/PrismarineJS/bedrock-protocol/commit/0ed8e32be85f05926cd97d5f0317ed004ae5eefa) (thanks @ItsMax123)
## 3.42.3
* [Fix Server `maxPlayers` option (#565)](https://github.com/PrismarineJS/bedrock-protocol/commit/38dc5a256105a44786d5455570d5a130e64ef561) (thanks @extremeheat)
## 3.42.2
* Fix missing type serialization error
## 3.42.1
* [Add 1.21.40 login fields (#553)](https://github.com/PrismarineJS/bedrock-protocol/commit/24d3200181c060162b04fb233fef6e0d6d1a93aa) (thanks @extremeheat)
* [Remove protodef varint types (#552)](https://github.com/PrismarineJS/bedrock-protocol/commit/347e303ce422bdb6f6dfd4cba57d7d3937214707) (thanks @extremeheat)
## 3.42.0
* [1.21.50 support](https://github.com/PrismarineJS/bedrock-protocol/commit/1c0836bff03d50cb12a3e45763eac6c9f605e00c) (thanks @extremeheat)
* [Dynamic compression & batch header (#544)](https://github.com/PrismarineJS/bedrock-protocol/commit/911e0e890febc00102cd1e5406731e66f7bad0ef) (thanks @LucienHH)
## 3.41.0
* [1.21.42 support](https://github.com/PrismarineJS/bedrock-protocol/commit/dd5c4de4f2624c3654af66e9a40a65eb13de0850) (thanks @CreeperG16)
## 3.40.0
* [1.21.30 support (#527)](https://github.com/PrismarineJS/bedrock-protocol/commit/fc30c96135ec20dca1257f702152cba61d4a59be) (thanks @pokecosimo)
* [Update tests (#528)](https://github.com/PrismarineJS/bedrock-protocol/commit/cb530c8b45bf505f75e0e39241d88085c5564ae8) (thanks @extremeheat)
## 3.39.0
* [1.21.20](https://github.com/PrismarineJS/bedrock-protocol/commit/3be55777fab4949179d3a7108ee29bbd8fada5a7) (thanks @extremeheat)
* [update disconnect packet](https://github.com/PrismarineJS/bedrock-protocol/commit/4c3f62567e0f6ce20b70ea23238fce8606011e95) (thanks @extremeheat)
## 3.38.0
* [Support 1.21.2, and add missing versions to type definitions (#510)](https://github.com/PrismarineJS/bedrock-protocol/commit/5d3986924d3f262708d7c7e55a7f410f12c7903c) (thanks @CreeperG16)
* [Fix example in README.md for 1.21 (#506)](https://github.com/PrismarineJS/bedrock-protocol/commit/c4593aa355d6ce9e2ac65cc2102cd9285a6b6449) (thanks @Ant767)
* [Don't send now deprecated tick sync packets on 1.21 and newer (#504)](https://github.com/PrismarineJS/bedrock-protocol/commit/84c5231b92df9f5f1a09b29a05e7abfed62f1c2b) (thanks @w0ahL)
## 3.37.0
* [Support 1.21.0](https://github.com/PrismarineJS/bedrock-protocol/commit/5b2d78792c9b4c070d727a9028a6b3a266483e1c) (thanks @CreeperG16)
* [Fix typo in types (#501)](https://github.com/PrismarineJS/bedrock-protocol/commit/16e15d80a5084a19ed2fbabc023789ee38922b3a) (thanks @Kaaaaii)
## 3.36.0
* [Support 1.20.80](https://github.com/PrismarineJS/bedrock-protocol/commit/bd32aa8d04555fa2fdc4ecd6abbeb6124e2ae8bb) (thanks @extremeheat)
## 3.35.0
* [Support 1.20.71](https://github.com/PrismarineJS/bedrock-protocol/commit/d8e707112acc038b6c9564d9a21b2f977326e47f) (thanks @extremeheat)
* [Note `npm update` command in readme](https://github.com/PrismarineJS/bedrock-protocol/commit/ab93d0d0824bd0ace250fb73f703dc7b60ecd780) (thanks @extremeheat)
## 3.34.0
* [1.20.61 support (#480)](https://github.com/PrismarineJS/bedrock-protocol/commit/c278a03f952d23320b80f8c09b6372d41eeff26a) (thanks @extremeheat)
* [Compressor handling update for 1.20.60 (#479)](https://github.com/PrismarineJS/bedrock-protocol/commit/d3161badc65f2eba4b6e7c9e974ca4e3529a7e94) (thanks @extremeheat)
* [Update and rename CONTRIBUTING.md to docs/CONTRIBUTING.md (#475)](https://github.com/PrismarineJS/bedrock-protocol/commit/be6f0cde9f7970a4f9aa376c589c58d8cb4187c3) (thanks @extremeheat)
* [Add flow and deviceType options to relay (#464)](https://github.com/PrismarineJS/bedrock-protocol/commit/842e66266f09e8670a644a618d0ac4157746cd43) (thanks @GameParrot)
## 3.33.1 ## 3.33.1
* [Fix zigzag type move in prismarine-nbt (#471)](https://github.com/PrismarineJS/bedrock-protocol/commit/7b74cbf7129646adc80d50304afce6240848cfae) (thanks @extremeheat) * [Fix zigzag type move in prismarine-nbt (#471)](https://github.com/PrismarineJS/bedrock-protocol/commit/7b74cbf7129646adc80d50304afce6240848cfae) (thanks @extremeheat)

View file

@ -11,7 +11,7 @@ Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication
## Features ## Features
- Supports Minecraft Bedrock version 1.16.201, 1.16.210, 1.16.220, 1.17.0, 1.17.10, 1.17.30, 1.17.40, 1.18.0, 1.18.11, 1.18.30, 1.19.1, 1.19.10, 1.19.20, 1.19.21, 1.19.30, 1.19.40, 1.19.41, 1.19.50, 1.19.60, 1.19.62, 1.19.63, 1.19.70, 1.19.80, 1.20.0, 1.20.10, 1.20.30, 1.20.40, 1.20.50, 1.20.61, 1.20.71, 1.20.80, 1.21.0, 1.21.2, 1.21.21, 1.21.30, 1.21.42, 1.21.50, 1.21.60, 1.21.70, 1.21.80, 1.21.90, 1.21.93, 1.21.100, 1.21.111 - Supports Minecraft Bedrock version 1.16.201, 1.16.210, 1.16.220, 1.17.0, 1.17.10, 1.17.30, 1.17.40, 1.18.0, 1.18.11, 1.18.30, 1.19.1, 1.19.10, 1.19.20, 1.19.21, 1.19.30, 1.19.40, 1.19.41, 1.19.50, 1.19.60, 1.19.62, 1.19.63, 1.19.70, 1.19.80, 1.20.0, 1.20.10, 1.20.30, 1.20.40, 1.20.50
- Parse and serialize packets as JavaScript objects - Parse and serialize packets as JavaScript objects
- Automatically respond to keep-alive packets - Automatically respond to keep-alive packets
- [Proxy and mitm connections](docs/API.md#proxy-docs) - [Proxy and mitm connections](docs/API.md#proxy-docs)
@ -34,8 +34,6 @@ Want to contribute on something important for PrismarineJS ? go to https://githu
`npm install bedrock-protocol` `npm install bedrock-protocol`
To update bedrock-protocol (or any Node.js package) and its dependencies after a previous install, you must run `npm update --depth 9999`
## Usage ## Usage
### Client example ### Client example
@ -54,7 +52,7 @@ const client = bedrock.createClient({
client.on('text', (packet) => { // Listen for chat messages from the server and echo them back. client.on('text', (packet) => { // Listen for chat messages from the server and echo them back.
if (packet.source_name != client.username) { if (packet.source_name != client.username) {
client.queue('text', { client.queue('text', {
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '', type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '',
message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}` message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}`
}) })
} }

View file

@ -142,7 +142,7 @@ client.on('text', (packet) => {
// names and as explained in the "Protocol doc" section below, fields are all case sensitive! // names and as explained in the "Protocol doc" section below, fields are all case sensitive!
client.on('add_player', (packet) => { client.on('add_player', (packet) => {
client.queue('text', { client.queue('text', {
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '', type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '',
message: `Hey, ${packet.username} just joined!` message: `Hey, ${packet.username} just joined!`
}) })
}) })

View file

@ -10,7 +10,7 @@ const client = bedrock.createClient({
client.on('text', (packet) => { // Listen for chat messages and echo them back. client.on('text', (packet) => { // Listen for chat messages and echo them back.
if (packet.source_name != client.username) { if (packet.source_name != client.username) {
client.queue('text', { client.queue('text', {
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '', type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '',
message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}` message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}`
}) })
} }

6
index.d.ts vendored
View file

@ -3,7 +3,7 @@ import { Realm } from 'prismarine-realms'
import { ServerDeviceCodeResponse } from 'prismarine-auth' import { ServerDeviceCodeResponse } from 'prismarine-auth'
declare module 'bedrock-protocol' { declare module 'bedrock-protocol' {
type Version = '1.21.93' | '1.21.90' | '1.21.80' | '1.21.70' | '1.21.60' | '1.21.50' | '1.21.42' | '1.21.30' | '1.21.2' | '1.21.0' | '1.20.80' | '1.20.71' | '1.20.61' | '1.20.50' | '1.20.40' | '1.20.30' | '1.20.10' | '1.20.0' | '1.19.80' | '1.19.70' | '1.19.63' | '1.19.62' | '1.19.60' | '1.19.51' | '1.19.50' | '1.19.41' | '1.19.40' | '1.19.31' | '1.19.30' | '1.19.22' | '1.19.21' | '1.19.20' | '1.19.11' | '1.19.10' | '1.19.2' | '1.19.1' | '1.18.31' | '1.18.30' | '1.18.12' | '1.18.11' | '1.18.10' | '1.18.2' | '1.18.1' | '1.18.0' | '1.17.41' | '1.17.40' | '1.17.34' | '1.17.30' | '1.17.11' | '1.17.10' | '1.17.0' | '1.16.220' | '1.16.210' | '1.16.201' type Version = '1.20.40' | '1.20.30' | '1.20.10' | '1.20.0' | '1.19.80' | '1.19.70' | '1.19.63' | '1.19.62' | '1.19.60' | '1.19.51' | '1.19.50' | '1.19.41' | '1.19.40' | '1.19.31' | '1.19.30' | '1.19.22' | '1.19.21' | '1.19.20' | '1.19.11' | '1.19.10' | '1.19.2' | '1.19.1' | '1.18.31' | '1.18.30' | '1.18.12' | '1.18.11' | '1.18.10' | '1.18.2' | '1.18.1' | '1.18.0' | '1.17.41' | '1.17.40' | '1.17.34' | '1.17.30' | '1.17.11' | '1.17.10' | '1.17.0' | '1.16.220' | '1.16.210' | '1.16.201'
export interface Options { export interface Options {
// The string version to start the client or server as // The string version to start the client or server as
@ -63,7 +63,7 @@ declare module 'bedrock-protocol' {
} }
enum ClientStatus { enum ClientStatus {
Disconnected, Disconected,
Authenticating, Authenticating,
Initializing, Initializing,
Initialized Initialized
@ -162,7 +162,7 @@ declare module 'bedrock-protocol' {
constructor(options: Options) constructor(options: Options)
listen(): Promise<void> listen(host?: string, port?: number): Promise<void>
close(disconnectReason?: string): Promise<void> close(disconnectReason?: string): Promise<void>
on(event: 'connect', cb: (client: Player) => void): any on(event: 'connect', cb: (client: Player) => void): any

View file

@ -1,15 +1,15 @@
{ {
"name": "bedrock-protocol", "name": "bedrock-protocol",
"version": "3.49.0", "version": "3.33.1",
"description": "Minecraft Bedrock Edition protocol library", "description": "Minecraft Bedrock Edition protocol library",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"scripts": { "scripts": {
"build": "cd tools && node compileProtocol.js", "build": "cd tools && node compileProtocol.js",
"test": "mocha --retries 2 --bail --exit", "test": "mocha --bail --exit",
"pretest": "npm run lint", "pretest": "npm run lint",
"lint": "standard", "lint": "standard",
"vanillaServer": "minecraft-bedrock-server --root tools --version", "vanillaServer": "node tools/startVanillaServer.js",
"dumpPackets": "node tools/genPacketDumps.js", "dumpPackets": "node tools/genPacketDumps.js",
"fix": "standard --fix" "fix": "standard --fix"
}, },
@ -40,8 +40,7 @@
"bedrock-protocol": "file:.", "bedrock-protocol": "file:.",
"bedrock-provider": "^2.0.0", "bedrock-provider": "^2.0.0",
"leveldb-zlib": "^1.0.1", "leveldb-zlib": "^1.0.1",
"minecraft-bedrock-server": "^1.4.2", "mocha": "^10.0.0",
"mocha": "^11.0.1",
"protodef-yaml": "^1.1.0", "protodef-yaml": "^1.1.0",
"standard": "^17.0.0-2" "standard": "^17.0.0-2"
}, },

View file

@ -26,7 +26,6 @@ class Client extends Connection {
this.compressionAlgorithm = this.versionGreaterThanOrEqualTo('1.19.30') ? 'none' : 'deflate' this.compressionAlgorithm = this.versionGreaterThanOrEqualTo('1.19.30') ? 'none' : 'deflate'
this.compressionThreshold = 512 this.compressionThreshold = 512
this.compressionLevel = this.options.compressionLevel this.compressionLevel = this.options.compressionLevel
this.batchHeader = 0xfe
if (isDebug) { if (isDebug) {
this.inLog = (...args) => debug('C ->', ...args) this.inLog = (...args) => debug('C ->', ...args)
@ -43,7 +42,6 @@ class Client extends Connection {
this.validateOptions() this.validateOptions()
this.serializer = createSerializer(this.options.version) this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version) this.deserializer = createDeserializer(this.options.version)
this._loadFeatures()
KeyExchange(this, null, this.options) KeyExchange(this, null, this.options)
Login(this, null, this.options) Login(this, null, this.options)
@ -57,19 +55,6 @@ class Client extends Connection {
this.emit('connect_allowed') this.emit('connect_allowed')
} }
_loadFeatures () {
try {
const mcData = require('minecraft-data')('bedrock_' + this.options.version)
this.features = {
compressorInHeader: mcData.supportFeature('compressorInPacketHeader'),
itemRegistryPacket: mcData.supportFeature('itemRegistryPacket'),
newLoginIdentityFields: mcData.supportFeature('newLoginIdentityFields')
}
} catch (e) {
throw new Error(`Unsupported version: '${this.options.version}', no data available`)
}
}
connect () { connect () {
if (!this.connection) throw new Error('Connect not currently allowed') // must wait for `connect_allowed`, or use `createClient` if (!this.connection) throw new Error('Connect not currently allowed') // must wait for `connect_allowed`, or use `createClient`
this.on('session', this._connect) this.on('session', this._connect)
@ -135,7 +120,6 @@ class Client extends Connection {
updateCompressorSettings (packet) { updateCompressorSettings (packet) {
this.compressionAlgorithm = packet.compression_algorithm || 'deflate' this.compressionAlgorithm = packet.compression_algorithm || 'deflate'
this.compressionThreshold = packet.compression_threshold this.compressionThreshold = packet.compression_threshold
this.compressionReady = true
} }
sendLogin () { sendLogin () {
@ -147,18 +131,9 @@ class Client extends Connection {
...this.accessToken // Mojang + Xbox JWT from auth ...this.accessToken // Mojang + Xbox JWT from auth
] ]
let encodedChain const encodedChain = JSON.stringify({ chain })
if (this.features.newLoginIdentityFields) { // 1.21.90+
encodedChain = JSON.stringify({ debug('Auth chain', chain)
Certificate: JSON.stringify({ chain }),
// 0 = normal, 1 = ss, 2 = offline
AuthenticationType: this.options.offline ? 2 : 0,
Token: ''
})
} else {
encodedChain = JSON.stringify({ chain })
}
debug('Auth chain', encodedChain)
this.write('login', { this.write('login', {
protocol_version: this.options.protocolVersion, protocol_version: this.options.protocolVersion,
@ -195,8 +170,7 @@ class Client extends Connection {
if (this.status === ClientStatus.Disconnected) return if (this.status === ClientStatus.Disconnected) return
this.write('disconnect', { this.write('disconnect', {
hide_disconnect_screen: hide, hide_disconnect_screen: hide,
message: reason, message: reason
filtered_message: ''
}) })
this.close(reason) this.close(reason)
} }
@ -252,9 +226,7 @@ class Client extends Connection {
break break
case 'start_game': case 'start_game':
this.startGameData = pakData.params this.startGameData = pakData.params
// fallsthrough this.startGameData.itemstates.forEach(state => {
case 'item_registry': // 1.21.60+ send itemstates in item_registry packet
pakData.params.itemstates?.forEach(state => {
if (state.name === 'minecraft:shield') { if (state.name === 'minecraft:shield') {
this.serializer.proto.setVariable('ShieldItemID', state.runtime_id) this.serializer.proto.setVariable('ShieldItemID', state.runtime_id)
this.deserializer.proto.setVariable('ShieldItemID', state.runtime_id) this.deserializer.proto.setVariable('ShieldItemID', state.runtime_id)

View file

@ -28,25 +28,17 @@ class Connection extends EventEmitter {
} }
versionLessThan (version) { versionLessThan (version) {
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
return this.options.protocolVersion < (typeof version === 'string' ? Versions[version] : version) return this.options.protocolVersion < (typeof version === 'string' ? Versions[version] : version)
} }
versionGreaterThan (version) { versionGreaterThan (version) {
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
return this.options.protocolVersion > (typeof version === 'string' ? Versions[version] : version) return this.options.protocolVersion > (typeof version === 'string' ? Versions[version] : version)
} }
versionGreaterThanOrEqualTo (version) { versionGreaterThanOrEqualTo (version) {
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
return this.options.protocolVersion >= (typeof version === 'string' ? Versions[version] : version) return this.options.protocolVersion >= (typeof version === 'string' ? Versions[version] : version)
} }
versionLessThanOrEqualTo (version) {
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
return this.options.protocolVersion <= (typeof version === 'string' ? Versions[version] : version)
}
startEncryption (iv) { startEncryption (iv) {
this.encryptionEnabled = true this.encryptionEnabled = true
this.inLog?.('Started encryption', this.sharedSecret, iv) this.inLog?.('Started encryption', this.sharedSecret, iv)
@ -70,18 +62,10 @@ class Connection extends EventEmitter {
} }
} }
_processOutbound (name, params) {
if (name === 'item_registry') {
this.updateItemPalette(params.itemstates)
} else if (name === 'start_game' && params.itemstates) {
this.updateItemPalette(params.itemstates)
}
}
write (name, params) { write (name, params) {
this.outLog?.(name, params) this.outLog?.(name, params)
this._processOutbound(name, params) if (name === 'start_game') this.updateItemPalette(params.itemstates)
const batch = new Framer(this) const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold)
const packet = this.serializer.createPacketBuffer({ name, params }) const packet = this.serializer.createPacketBuffer({ name, params })
batch.addEncodedPacket(packet) batch.addEncodedPacket(packet)
@ -94,7 +78,7 @@ class Connection extends EventEmitter {
queue (name, params) { queue (name, params) {
this.outLog?.('Q <- ', name, params) this.outLog?.('Q <- ', name, params)
this._processOutbound(name, params) if (name === 'start_game') this.updateItemPalette(params.itemstates)
const packet = this.serializer.createPacketBuffer({ name, params }) const packet = this.serializer.createPacketBuffer({ name, params })
if (name === 'level_chunk') { if (name === 'level_chunk') {
// Skip queue, send ASAP // Skip queue, send ASAP
@ -107,7 +91,7 @@ class Connection extends EventEmitter {
_tick () { _tick () {
if (this.sendQ.length) { if (this.sendQ.length) {
const batch = new Framer(this) const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold)
batch.addEncodedPackets(this.sendQ) batch.addEncodedPackets(this.sendQ)
this.sendQ = [] this.sendQ = []
this.sendIds = [] this.sendIds = []
@ -131,7 +115,7 @@ class Connection extends EventEmitter {
*/ */
sendBuffer (buffer, immediate = false) { sendBuffer (buffer, immediate = false) {
if (immediate) { if (immediate) {
const batch = new Framer(this) const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold)
batch.addEncodedPacket(buffer) batch.addEncodedPacket(buffer)
if (this.encryptionEnabled) { if (this.encryptionEnabled) {
this.sendEncryptedBatch(batch) this.sendEncryptedBatch(batch)
@ -165,29 +149,29 @@ class Connection extends EventEmitter {
// These are callbacks called from encryption.js // These are callbacks called from encryption.js
onEncryptedPacket = (buf) => { onEncryptedPacket = (buf) => {
const packet = this.batchHeader ? Buffer.concat([Buffer.from([this.batchHeader]), buf]) : buf const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header
this.sendMCPE(packet) this.sendMCPE(packet)
} }
onDecryptedPacket = (buf) => { onDecryptedPacket = (buf) => {
const packets = Framer.getPackets(buf) const packets = Framer.getPackets(buf)
for (const packet of packets) { for (const packet of packets) {
this.readPacket(packet) this.readPacket(packet)
} }
} }
handle (buffer) { // handle encapsulated handle (buffer) { // handle encapsulated
if (!this.batchHeader || buffer[0] === this.batchHeader) { // wrapper if (buffer[0] === 0xfe) { // wrapper
if (this.encryptionEnabled) { if (this.encryptionEnabled) {
this.decrypt(buffer.slice(1)) this.decrypt(buffer.slice(1))
} else { } else {
const packets = Framer.decode(this, buffer) const packets = Framer.decode(this.compressionAlgorithm, buffer)
for (const packet of packets) { for (const packet of packets) {
this.readPacket(packet) this.readPacket(packet)
} }
} }
} else {
throw Error('Bad packet header ' + buffer[0])
} }
} }
} }

View file

@ -56,41 +56,35 @@ function connect (client) {
}) })
client.queue('client_cache_status', { enabled: false }) client.queue('client_cache_status', { enabled: false })
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
if (client.versionLessThanOrEqualTo('1.20.80')) client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
sleep(500).then(() => client.queue('request_chunk_radius', { chunk_radius: client.viewDistance || 10 })) sleep(500).then(() => client.queue('request_chunk_radius', { chunk_radius: client.viewDistance || 10 }))
}) })
if (client.versionLessThanOrEqualTo('1.20.80')) { // Send tick sync packets every 10 ticks
const keepAliveInterval = 10 const keepAliveInterval = 10
const keepAliveIntervalBig = BigInt(keepAliveInterval) const keepAliveIntervalBig = BigInt(keepAliveInterval)
let keepalive
client.tick = 0n
client.once('spawn', () => {
keepalive = setInterval(() => {
// Client fills out the request_time and the server does response_time in its reply.
client.queue('tick_sync', { request_time: client.tick, response_time: 0n })
client.tick += keepAliveIntervalBig
}, 50 * keepAliveInterval)
let keepalive client.on('tick_sync', async packet => {
client.tick = 0n client.emit('heartbeat', packet.response_time)
client.tick = packet.response_time
client.once('spawn', () => {
keepalive = setInterval(() => {
// Client fills out the request_time and the server does response_time in its reply.
client.queue('tick_sync', { request_time: client.tick, response_time: 0n })
client.tick += keepAliveIntervalBig
}, 50 * keepAliveInterval)
client.on('tick_sync', async packet => {
client.emit('heartbeat', packet.response_time)
client.tick = packet.response_time
})
}) })
})
client.once('close', () => { client.once('close', () => {
clearInterval(keepalive) clearInterval(keepalive)
}) })
}
} }
async function ping ({ host, port }) { async function ping ({ host, port }) {
const con = new RakClient({ host, port }) const con = new RakClient({ host, port })
try { try {
return advertisement.fromServerName(await con.ping()) return advertisement.fromServerName(await con.ping())
} finally { } finally {

View file

@ -1,7 +1,7 @@
/* eslint-disable */ /* eslint-disable */
const UUID = require('uuid-1345') const UUID = require('uuid-1345')
const minecraft = require('./minecraft') const minecraft = require('./minecraft')
const [Read, Write, SizeOf] = [{}, {}, {}] const { Read, Write, SizeOf } = require('./varlong')
/** /**
* UUIDs * UUIDs
@ -116,6 +116,74 @@ Read.lnbt = ['native', minecraft.lnbt[0]]
Write.lnbt = ['native', minecraft.lnbt[1]] Write.lnbt = ['native', minecraft.lnbt[1]]
SizeOf.lnbt = ['native', minecraft.lnbt[2]] SizeOf.lnbt = ['native', minecraft.lnbt[2]]
/**
* Bits
*/
Read.bitflags = ['parametrizable', (compiler, { type, flags, shift, big }) => {
let fstr = JSON.stringify(flags)
if (Array.isArray(flags)) {
fstr = '{'
flags.map((v, k) => fstr += `"${v}": ${big ? 1n << BigInt(k) : 1 << k}` + (big ? 'n,' : ','))
fstr += '}'
} else if (shift) {
fstr = '{'
for (const key in flags) fstr += `"${key}": ${1 << flags[key]},`;
fstr += '}'
}
return compiler.wrapCode(`
const { value: _value, size } = ${compiler.callType(type, 'offset')}
const value = { _value }
const flags = ${fstr}
for (const key in flags) {
value[key] = (_value & flags[key]) == flags[key]
}
return { value, size }
`.trim())
}]
Write.bitflags = ['parametrizable', (compiler, { type, flags, shift, big }) => {
let fstr = JSON.stringify(flags)
if (Array.isArray(flags)) {
fstr = '{'
flags.map((v, k) => fstr += `"${v}": ${big ? 1n << BigInt(k) : 1 << k}` + (big ? 'n,' : ','))
fstr += '}'
} else if (shift) {
fstr = '{'
for (const key in flags) fstr += `"${key}": ${1 << flags[key]},`;
fstr += '}'
}
return compiler.wrapCode(`
const flags = ${fstr}
let val = value._value ${big ? '|| 0n' : ''}
for (const key in flags) {
if (value[key]) val |= flags[key]
}
return (ctx.${type})(val, buffer, offset)
`.trim())
}]
SizeOf.bitflags = ['parametrizable', (compiler, { type, flags, shift, big }) => {
let fstr = JSON.stringify(flags)
if (Array.isArray(flags)) {
fstr = '{'
flags.map((v, k) => fstr += `"${v}": ${big ? 1n << BigInt(k) : 1 << k}` + (big ? 'n,' : ','))
fstr += '}'
} else if (shift) {
fstr = '{'
for (const key in flags) fstr += `"${key}": ${1 << flags[key]},`;
fstr += '}'
}
return compiler.wrapCode(`
const flags = ${fstr}
let val = value._value ${big ? '|| 0n' : ''}
for (const key in flags) {
if (value[key]) val |= flags[key]
}
return (ctx.${type})(val)
`.trim())
}]
/** /**
* Command Packet * Command Packet
* - used for determining the size of the following enum * - used for determining the size of the following enum

View file

@ -1,11 +1,14 @@
/* eslint-disable */
const nbt = require('prismarine-nbt') const nbt = require('prismarine-nbt')
const UUID = require('uuid-1345') const UUID = require('uuid-1345')
const protoLE = nbt.protos.little const protoLE = nbt.protos.little
const protoLEV = nbt.protos.littleVarint const protoLEV = nbt.protos.littleVarint
// TODO: deal with this:
const zigzag = require('prismarine-nbt/zigzag')
function readUUID (buffer, offset) { function readUUID (buffer, offset) {
if (offset + 16 > buffer.length) { throw new Error('Reached end of buffer') } if (offset + 16 > buffer.length) { throw new PartialReadError() }
return { return {
value: UUID.stringify(buffer.slice(offset, 16 + offset)), value: UUID.stringify(buffer.slice(offset, 16 + offset)),
size: 16 size: 16
@ -62,7 +65,7 @@ function readEntityMetadata (buffer, offset, _ref) {
const metadata = [] const metadata = []
let item let item
while (true) { while (true) {
if (offset + 1 > buffer.length) throw new Error('Reached end of buffer') if (offset + 1 > buffer.length) throw new PartialReadError()
item = buffer.readUInt8(cursor) item = buffer.readUInt8(cursor)
if (item === endVal) { if (item === endVal) {
return { return {
@ -156,5 +159,7 @@ module.exports = {
lnbt: [readNbtLE, writeNbtLE, sizeOfNbtLE], lnbt: [readNbtLE, writeNbtLE, sizeOfNbtLE],
entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata],
ipAddress: [readIpAddress, writeIpAddress, 4], ipAddress: [readIpAddress, writeIpAddress, 4],
endOfArray: [readEndOfArray, writeEndOfArray, sizeOfEndOfArray] endOfArray: [readEndOfArray, writeEndOfArray, sizeOfEndOfArray],
zigzag32: zigzag.interpret.zigzag32,
zigzag64: zigzag.interpret.zigzag64
} }

63
src/datatypes/varlong.js Normal file
View file

@ -0,0 +1,63 @@
function sizeOfVarLong (value) {
if (typeof value.valueOf() === 'object') {
value = (BigInt(value[0]) << 32n) | BigInt(value[1])
} else if (typeof value !== 'bigint') value = BigInt(value)
let cursor = 0
while (value > 127n) {
value >>= 7n
cursor++
}
return cursor + 1
}
/**
* Reads a 64-bit VarInt as a BigInt
*/
function readVarLong (buffer, offset) {
let result = BigInt(0)
let shift = 0n
let cursor = offset
let size = 0
while (true) {
if (cursor + 1 > buffer.length) { throw new Error('unexpected buffer end') }
const b = buffer.readUInt8(cursor)
result |= (BigInt(b) & 0x7fn) << shift // Add the bits to our number, except MSB
cursor++
if (!(b & 0x80)) { // If the MSB is not set, we return the number
size = cursor - offset
break
}
shift += 7n // we only have 7 bits, MSB being the return-trigger
if (shift > 63n) throw new Error(`varint is too big: ${shift}`)
}
return { value: result, size }
}
/**
* Writes a zigzag encoded 64-bit VarInt as a BigInt
*/
function writeVarLong (value, buffer, offset) {
// if an array, turn it into a BigInt
if (typeof value.valueOf() === 'object') {
value = BigInt.asIntN(64, (BigInt(value[0]) << 32n)) | BigInt(value[1])
} else if (typeof value !== 'bigint') value = BigInt(value)
let cursor = 0
while (value > 127n) { // keep writing in 7 bit slices
const num = Number(value & 0xFFn)
buffer.writeUInt8(num | 0x80, offset + cursor)
cursor++
value >>= 7n
}
buffer.writeUInt8(Number(value), offset + cursor)
return offset + cursor + 1
}
module.exports = {
Read: { varint64: ['native', readVarLong] },
Write: { varint64: ['native', writeVarLong] },
SizeOf: { varint64: ['native', sizeOfVarLong] }
}

View file

@ -36,6 +36,7 @@ module.exports = (client, server, options) => {
client.createClientUserChain = (privateKey) => { client.createClientUserChain = (privateKey) => {
let payload = { let payload = {
...skinData, ...skinData,
SkinGeometryDataEngineVersion: client.versionGreaterThanOrEqualTo('1.17.30') ? '' : undefined,
ClientRandomId: Date.now(), ClientRandomId: Date.now(),
CurrentInputMode: 1, CurrentInputMode: 1,
@ -46,30 +47,25 @@ module.exports = (client, server, options) => {
GameVersion: options.version || '1.16.201', GameVersion: options.version || '1.16.201',
GuiScale: -1, GuiScale: -1,
LanguageCode: 'en_GB', // TODO locale LanguageCode: 'en_GB', // TODO locale
GraphicsMode: 1, // 1:simple, 2:fancy, 3:advanced, 4:ray_traced
PlatformOfflineId: '', PlatformOfflineId: '',
PlatformOnlineId: '', // chat PlatformOnlineId: '', // chat
// PlayFabID is the PlayFab ID produced for the skin. PlayFab is the company that hosts the Marketplace, // PlayFabID is the PlayFab ID produced for the skin. PlayFab is the company that hosts the Marketplace,
// skins and other related features from the game. This ID is the ID of the skin used to store the skin // skins and other related features from the game. This ID is the ID of the skin used to store the skin
// inside of PlayFab.The playfab ID is always lowercased. // inside of PlayFab.
PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16).toLowerCase(), // 1.16.210 PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16), // 1.16.210
SelfSignedId: nextUUID(), SelfSignedId: nextUUID(),
ServerAddress: `${options.host}:${options.port}`, ServerAddress: `${options.host}:${options.port}`,
ThirdPartyName: client.profile.name, // Gamertag ThirdPartyName: client.profile.name,
ThirdPartyNameOnly: client.versionGreaterThanOrEqualTo('1.21.90') ? undefined : false, ThirdPartyNameOnly: false,
UIProfile: 0, UIProfile: 0,
IsEditorMode: false, IsEditorMode: false,
TrustedSkin: client.versionGreaterThanOrEqualTo('1.19.20') ? false : undefined, TrustedSkin: client.versionGreaterThanOrEqualTo('1.19.20') ? false : undefined,
OverrideSkin: client.versionGreaterThanOrEqualTo('1.19.62') ? false : undefined, OverrideSkin: client.versionGreaterThanOrEqualTo('1.19.62') ? false : undefined,
CompatibleWithClientSideChunkGen: client.versionGreaterThanOrEqualTo('1.19.80') ? false : undefined, CompatibleWithClientSideChunkGen: client.versionGreaterThanOrEqualTo('1.19.80') ? false : undefined
MaxViewDistance: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined,
MemoryTier: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined,
PlatformType: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined
} }
const customPayload = options.skinData || {} const customPayload = options.skinData || {}
payload = { ...payload, ...customPayload } payload = { ...payload, ...customPayload }

View file

@ -3,12 +3,12 @@ const mcData = require('minecraft-data')
// Minimum supported version (< will be kicked) // Minimum supported version (< will be kicked)
const MIN_VERSION = '1.16.201' const MIN_VERSION = '1.16.201'
// Currently supported verson. Note, clients with newer versions can still connect as long as data is in minecraft-data // Currently supported verson. Note, clients with newer versions can still connect as long as data is in minecraft-data
const CURRENT_VERSION = '1.21.111' const CURRENT_VERSION = '1.20.50'
const Versions = Object.fromEntries(mcData.versions.bedrock.filter(e => e.releaseType === 'release').map(e => [e.minecraftVersion, e.version])) const Versions = Object.fromEntries(mcData.versions.bedrock.filter(e => e.releaseType === 'release').map(e => [e.minecraftVersion, e.version]))
// Skip some low priority versions (middle major) on Github Actions to allow faster CI // Skip some low priority versions (middle major) on Github Actions to allow faster CI
const skippedVersionsOnGithubCI = ['1.16.210', '1.17.10', '1.17.30', '1.18.11', '1.19.10', '1.19.20', '1.19.30', '1.19.40', '1.19.50', '1.19.60', '1.19.63', '1.19.70', '1.20.10', '1.20.15', '1.20.30', '1.20.40', '1.20.50', '1.20.61', '1.20.71', '1.21.2', '1.21.20', '1.21.30', '1.21.42', '1.21.50', '1.21.60', '1.21.70', '1.21.80', '1.21.90'] const skippedVersionsOnGithubCI = ['1.16.210', '1.17.10', '1.17.30', '1.18.11', '1.19.10', '1.19.20', '1.19.30', '1.19.40', '1.19.50', '1.19.60', '1.19.63', '1.19.70', '1.20.10']
const testedVersions = process.env.CI ? Object.keys(Versions).filter(v => !skippedVersionsOnGithubCI.includes(v)) : Object.keys(Versions) const testedVersions = process.env.CI ? Object.keys(Versions).filter(v => !skippedVersionsOnGithubCI.includes(v)) : Object.keys(Versions)
const defaultOptions = { const defaultOptions = {

View file

@ -15,7 +15,6 @@ class Server extends EventEmitter {
this.RakServer = require('./rak')(this.options.raknetBackend).RakServer this.RakServer = require('./rak')(this.options.raknetBackend).RakServer
this._loadFeatures(this.options.version)
this.serializer = createSerializer(this.options.version) this.serializer = createSerializer(this.options.version)
this.deserializer = createDeserializer(this.options.version) this.deserializer = createDeserializer(this.options.version)
this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version) this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version)
@ -24,41 +23,25 @@ class Server extends EventEmitter {
this.clients = {} this.clients = {}
this.clientCount = 0 this.clientCount = 0
this.conLog = debug this.conLog = debug
this.batchHeader = 0xfe
this.setCompressor(this.options.compressionAlgorithm, this.options.compressionLevel, this.options.compressionThreshold) this.setCompressor(this.options.compressionAlgorithm, this.options.compressionLevel, this.options.compressionThreshold)
} }
_loadFeatures (version) {
try {
const mcData = require('minecraft-data')('bedrock_' + version)
this.features = {
compressorInHeader: mcData.supportFeature('compressorInPacketHeader'),
newLoginIdentityFields: mcData.supportFeature('newLoginIdentityFields')
}
} catch (e) {
throw new Error(`Unsupported version: '${version}', no data available`)
}
}
setCompressor (algorithm, level = 1, threshold = 256) { setCompressor (algorithm, level = 1, threshold = 256) {
switch (algorithm) { switch (algorithm) {
case 'none': case 'none':
this.compressionAlgorithm = 'none' this.compressionAlgorithm = 'none'
this.compressionLevel = 0 this.compressionLevel = 0
this.compressionHeader = 255
break break
case 'deflate': case 'deflate':
this.compressionAlgorithm = 'deflate' this.compressionAlgorithm = 'deflate'
this.compressionLevel = level this.compressionLevel = level
this.compressionThreshold = threshold this.compressionThreshold = threshold
this.compressionHeader = 0
break break
case 'snappy': case 'snappy':
this.compressionAlgorithm = 'snappy' this.compressionAlgorithm = 'snappy'
this.compressionLevel = level this.compressionLevel = level
this.compressionThreshold = threshold this.compressionThreshold = threshold
this.compressionHeader = 1
break break
default: default:
throw new Error(`Unknown compression algorithm: ${algorithm}`) throw new Error(`Unknown compression algorithm: ${algorithm}`)
@ -90,10 +73,12 @@ class Server extends EventEmitter {
this.emit('connect', player) this.emit('connect', player)
} }
onCloseConnection = (conn, reason) => { onCloseConnection = (inetAddr, reason) => {
this.conLog('Connection closed: ', conn.address, reason) this.conLog('Connection closed: ', inetAddr?.address, reason)
this.clients[conn.address]?.close()
delete this.clients[conn.address] delete this.clients[inetAddr]?.connection // Prevent close loop
this.clients[inetAddr?.address ?? inetAddr]?.close()
delete this.clients[inetAddr]
this.clientCount-- this.clientCount--
} }
@ -117,9 +102,8 @@ class Server extends EventEmitter {
return this.advertisement return this.advertisement
} }
async listen () { async listen (host = this.options.host, port = this.options.port) {
const { host, port, maxPlayers } = this.options this.raknet = new this.RakServer({ host, port }, this)
this.raknet = new this.RakServer({ host, port, maxPlayers }, this)
try { try {
await this.raknet.listen() await this.raknet.listen()

View file

@ -10,7 +10,6 @@ class Player extends Connection {
constructor (server, connection) { constructor (server, connection) {
super() super()
this.server = server this.server = server
this.features = server.features
this.serializer = server.serializer this.serializer = server.serializer
this.deserializer = server.deserializer this.deserializer = server.deserializer
this.connection = connection this.connection = connection
@ -24,16 +23,14 @@ class Player extends Connection {
this.status = ClientStatus.Authenticating this.status = ClientStatus.Authenticating
if (isDebug) { if (isDebug) {
this.inLog = (...args) => debug('-> S', ...args) this.inLog = (...args) => debug('S ->', ...args)
this.outLog = (...args) => debug('<- S', ...args) this.outLog = (...args) => debug('S <-', ...args)
} }
this.batchHeader = this.server.batchHeader
// Compression is server-wide // Compression is server-wide
this.compressionAlgorithm = this.server.compressionAlgorithm this.compressionAlgorithm = this.server.compressionAlgorithm
this.compressionLevel = this.server.compressionLevel this.compressionLevel = this.server.compressionLevel
this.compressionThreshold = this.server.compressionThreshold this.compressionThreshold = this.server.compressionThreshold
this.compressionHeader = this.server.compressionHeader
this._sentNetworkSettings = false // 1.19.30+ this._sentNetworkSettings = false // 1.19.30+
} }
@ -51,7 +48,6 @@ class Player extends Connection {
client_throttle_scalar: 0 client_throttle_scalar: 0
}) })
this._sentNetworkSettings = true this._sentNetworkSettings = true
this.compressionReady = true
} }
handleClientProtocolVersion (clientVersion) { handleClientProtocolVersion (clientVersion) {
@ -78,18 +74,11 @@ class Player extends Connection {
// Parse login data // Parse login data
const tokens = body.params.tokens const tokens = body.params.tokens
const authChain = JSON.parse(tokens.identity)
const skinChain = tokens.client
try { try {
const skinChain = tokens.client var { key, userData, skinData } = this.decodeLoginJWT(authChain.chain, skinChain) // eslint-disable-line
const authChain = JSON.parse(tokens.identity)
let chain
if (authChain.Certificate) { // 1.21.90+
chain = JSON.parse(authChain.Certificate).chain
} else if (authChain.chain) {
chain = authChain.chain
} else {
throw new Error('Invalid login packet: missing chain or Certificate')
}
var { key, userData, skinData } = this.decodeLoginJWT(chain, skinChain) // eslint-disable-line
} catch (e) { } catch (e) {
debug(this.address, e) debug(this.address, e)
this.disconnect('Server authentication error') this.disconnect('Server authentication error')
@ -126,8 +115,7 @@ class Player extends Connection {
if (this.status === ClientStatus.Disconnected) return if (this.status === ClientStatus.Disconnected) return
this.write('disconnect', { this.write('disconnect', {
hide_disconnect_screen: hide, hide_disconnect_screen: hide,
message: reason, message: reason
filtered_message: ''
}) })
this.server.conLog('Kicked ', this.connection?.address, reason) this.server.conLog('Kicked ', this.connection?.address, reason)
setTimeout(() => this.close('kick'), 100) // Allow time for message to be recieved. setTimeout(() => this.close('kick'), 100) // Allow time for message to be recieved.
@ -164,7 +152,7 @@ class Player extends Connection {
return return
} }
this.inLog?.(des.data.name, serialize(des.data.params)) this.inLog?.(des.data.name, serialize(des.data.params).slice(0, 200))
switch (des.data.name) { switch (des.data.name) {
// This is the first packet on 1.19.30 & above // This is the first packet on 1.19.30 & above

View file

@ -36,10 +36,7 @@ function createEncryptor (client, iv) {
// The send counter is represented as a little-endian 64-bit long and incremented after each packet. // The send counter is represented as a little-endian 64-bit long and incremented after each packet.
function process (chunk) { function process (chunk) {
const compressed = Zlib.deflateRawSync(chunk, { level: client.compressionLevel }) const buffer = Zlib.deflateRawSync(chunk, { level: client.compressionLevel })
const buffer = client.features.compressorInHeader
? Buffer.concat([Buffer.from([0]), compressed])
: compressed
const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)]) const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)])
client.sendCounter++ client.sendCounter++
client.cipher.write(packet) client.cipher.write(packet)
@ -73,22 +70,7 @@ function createDecryptor (client, iv) {
return return
} }
let buffer const buffer = Zlib.inflateRawSync(chunk, { chunkSize: 512000 })
if (client.features.compressorInHeader) {
switch (packet[0]) {
case 0:
buffer = Zlib.inflateRawSync(packet.slice(1), { chunkSize: 512000 })
break
case 255:
buffer = packet.slice(1)
break
default:
client.emit('error', Error(`Unsupported compressor: ${packet[0]}`))
}
} else {
buffer = Zlib.inflateRawSync(packet, { chunkSize: 512000 })
}
client.onDecryptedPacket(buffer) client.onDecryptedPacket(buffer)
} }

View file

@ -3,15 +3,12 @@ const zlib = require('zlib')
// Concatenates packets into one batch packet, and adds length prefixs. // Concatenates packets into one batch packet, and adds length prefixs.
class Framer { class Framer {
constructor (client) { constructor (compressor, compressionLevel, compressionThreshold) {
// Encoding // Encoding
this.packets = [] this.packets = []
this.batchHeader = client.batchHeader this.compressor = compressor || 'none'
this.compressor = client.compressionAlgorithm || 'none' this.compressionLevel = compressionLevel
this.compressionLevel = client.compressionLevel this.compressionThreshold = compressionThreshold
this.compressionThreshold = client.compressionThreshold
this.compressionHeader = client.compressionHeader || 0
this.writeCompressor = client.features.compressorInHeader && client.compressionReady
} }
// No compression in base class // No compression in base class
@ -24,46 +21,30 @@ class Framer {
} }
static decompress (algorithm, buffer) { static decompress (algorithm, buffer) {
switch (algorithm) { try {
case 0: switch (algorithm) {
case 'deflate': case 'deflate': return zlib.inflateRawSync(buffer, { chunkSize: 512000 })
return zlib.inflateRawSync(buffer, { chunkSize: 512000 }) case 'snappy': throw Error('Snappy compression not implemented')
case 1: case 'none': return buffer
case 'snappy': default: throw Error('Unknown compression type ' + this.compressor)
throw Error('Snappy compression not implemented') }
case 'none': } catch {
case 255: return buffer
return buffer
default: throw Error('Unknown compression type ' + algorithm)
} }
} }
static decode (client, buf) { static decode (compressor, buf) {
// Read header // Read header
if (this.batchHeader && buf[0] !== this.batchHeader) throw Error(`bad batch packet header, received: ${buf[0]}, expected: ${this.batchHeader}`) if (buf[0] !== 0xfe) throw Error('bad batch packet header ' + buf[0])
const buffer = buf.slice(1) const buffer = buf.slice(1)
// Decompress const decompressed = this.decompress(compressor, buffer)
let decompressed
if (client.features.compressorInHeader && client.compressionReady) {
decompressed = this.decompress(buffer[0], buffer.slice(1))
} else {
// On old versions, compressor is session-wide ; failing to decompress
// a packet will assume it's not compressed
try {
decompressed = this.decompress(client.compressionAlgorithm, buffer)
} catch (e) {
decompressed = buffer
}
}
return Framer.getPackets(decompressed) return Framer.getPackets(decompressed)
} }
encode () { encode () {
const buf = Buffer.concat(this.packets) const buf = Buffer.concat(this.packets)
const shouldCompress = buf.length > this.compressionThreshold const compressed = (buf.length > this.compressionThreshold) ? this.compress(buf) : buf
const header = this.batchHeader ? [this.batchHeader] : [] return Buffer.concat([Buffer.from([0xfe]), compressed])
if (this.writeCompressor) header.push(shouldCompress ? this.compressionHeader : 255)
return Buffer.concat([Buffer.from(header), shouldCompress ? this.compress(buf) : buf])
} }
addEncodedPacket (chunk) { addEncodedPacket (chunk) {

View file

@ -37,6 +37,7 @@ function createProtocol (version) {
const compiler = new ProtoDefCompiler() const compiler = new ProtoDefCompiler()
compiler.addTypesToCompile(protocol.types) compiler.addTypesToCompile(protocol.types)
compiler.addTypes(require('../datatypes/compiler-minecraft')) compiler.addTypes(require('../datatypes/compiler-minecraft'))
compiler.addTypes(require('prismarine-nbt/zigzag').compiler)
const compiledProto = compiler.compileProtoDefSync() const compiledProto = compiler.compileProtoDefSync()
return compiledProto return compiledProto
@ -46,6 +47,7 @@ function createProtocol (version) {
function getProtocol (version) { function getProtocol (version) {
const compiler = new ProtoDefCompiler() const compiler = new ProtoDefCompiler()
compiler.addTypes(require(join(__dirname, '../datatypes/compiler-minecraft'))) compiler.addTypes(require(join(__dirname, '../datatypes/compiler-minecraft')))
compiler.addTypes(require('prismarine-nbt/zigzag').compiler)
global.PartialReadError = require('protodef/src/utils').PartialReadError global.PartialReadError = require('protodef/src/utils').PartialReadError
const compile = (compiler, file) => require(file)(compiler.native) const compile = (compiler, file) => require(file)(compiler.native)

View file

@ -14,7 +14,7 @@ function prepare (version) {
async function startTest (version = CURRENT_VERSION, ok) { async function startTest (version = CURRENT_VERSION, ok) {
await prepare(version) await prepare(version)
// const Item = require('../types/Item')(version) const Item = require('../types/Item')(version)
const port = await getPort() const port = await getPort()
const server = new Server({ host: '0.0.0.0', port, version, offline: true }) const server = new Server({ host: '0.0.0.0', port, version, offline: true })
@ -47,7 +47,6 @@ async function startTest (version = CURRENT_VERSION, ok) {
must_accept: false, must_accept: false,
has_scripts: false, has_scripts: false,
behaviour_packs: [], behaviour_packs: [],
world_template: { uuid: '550e8400-e29b-41d4-a716-446655440000', version: '' }, // 1.21.50
texture_packs: [], texture_packs: [],
resource_pack_links: [] resource_pack_links: []
}) })
@ -57,17 +56,13 @@ async function startTest (version = CURRENT_VERSION, ok) {
client.write('network_settings', { compression_threshold: 1 }) client.write('network_settings', { compression_threshold: 1 })
// Send some inventory slots // Send some inventory slots
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
// client.queue('inventory_slot', { window_id: 'armor', slot: 0, item: new Item().toBedrock() }) client.queue('inventory_slot', { window_id: 'armor', slot: 0, item: new Item().toBedrock() })
} }
// client.queue('inventory_transaction', get('packets/inventory_transaction.json')) // client.queue('inventory_transaction', get('packets/inventory_transaction.json'))
client.queue('player_list', get('packets/player_list.json')) client.queue('player_list', get('packets/player_list.json'))
client.queue('start_game', get('packets/start_game.json')) client.queue('start_game', get('packets/start_game.json'))
if (client.versionLessThan('1.21.60')) { client.queue('item_component', { entries: [] })
client.queue('item_component', { entries: [] })
} else {
client.queue('item_registry', get('packets/item_registry.json'))
}
client.queue('set_spawn_position', get('packets/set_spawn_position.json')) client.queue('set_spawn_position', get('packets/set_spawn_position.json'))
client.queue('set_time', { time: 5433771 }) client.queue('set_time', { time: 5433771 })
client.queue('set_difficulty', { difficulty: 1 }) client.queue('set_difficulty', { difficulty: 1 })
@ -101,11 +96,11 @@ async function startTest (version = CURRENT_VERSION, ok) {
loop = setInterval(() => { loop = setInterval(() => {
client.write('network_chunk_publisher_update', { coordinates: { x: 646, y: 130, z: 77 }, radius: 64 }) client.write('network_chunk_publisher_update', { coordinates: { x: 646, y: 130, z: 77 }, radius: 64 })
}, 6500) }, 9500)
setTimeout(() => { setTimeout(() => {
client.write('play_status', { status: 'player_spawn' }) client.write('play_status', { status: 'player_spawn' })
}, 3000) }, 6000)
// Respond to tick synchronization packets // Respond to tick synchronization packets
client.on('tick_sync', (packet) => { client.on('tick_sync', (packet) => {

View file

@ -3,7 +3,6 @@
const { timedTest } = require('./internal') const { timedTest } = require('./internal')
const { testedVersions } = require('../src/options') const { testedVersions } = require('../src/options')
const { sleep } = require('../src/datatypes/util') const { sleep } = require('../src/datatypes/util')
require('events').captureRejections = true
describe('internal client/server test', function () { describe('internal client/server test', function () {
const vcount = testedVersions.length const vcount = testedVersions.length

View file

@ -22,12 +22,12 @@ function proxyTest (version, raknetBackend = 'raknet-native', timeout = 1000 * 4
console.debug('Client has authenticated') console.debug('Client has authenticated')
setTimeout(() => { setTimeout(() => {
client.disconnect('Hello world !') client.disconnect('Hello world !')
}, 500) // allow some time for client to connect }, 1000) // allow some time for client to connect
}) })
}) })
console.debug('Server started', server.options.version) console.debug('Server started', server.options.version)
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise(resolve => setTimeout(resolve, 1000))
const relay = new Relay({ const relay = new Relay({
version, version,
@ -46,7 +46,7 @@ function proxyTest (version, raknetBackend = 'raknet-native', timeout = 1000 * 4
await relay.listen() await relay.listen()
console.debug('Proxy started', server.options.version) console.debug('Proxy started', server.options.version)
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise(resolve => setTimeout(resolve, 1000))
const client = createClient({ host: '127.0.0.1', port: CLIENT_PORT, version, username: 'Boat', offline: true, raknetBackend, skipPing: true }) const client = createClient({ host: '127.0.0.1', port: CLIENT_PORT, version, username: 'Boat', offline: true, raknetBackend, skipPing: true })
console.debug('Client started') console.debug('Client started')
@ -58,7 +58,7 @@ function proxyTest (version, raknetBackend = 'raknet-native', timeout = 1000 * 4
server.close() server.close()
relay.close() relay.close()
console.log('✔ OK') console.log('✔ OK')
sleep(200).then(res) sleep(500).then(res)
}) })
}, timeout, () => { throw Error('timed out') }) }, timeout, () => { throw Error('timed out') })
} }

View file

@ -11,7 +11,7 @@ describe('proxies client/server', function () {
it('proxies ' + version, async () => { it('proxies ' + version, async () => {
console.debug(version) console.debug(version)
await proxyTest(version) await proxyTest(version)
await sleep(100) await sleep(1000)
console.debug('Done', version) console.debug('Done', version)
}) })
} }

View file

@ -5,12 +5,7 @@ const getPort = () => new Promise(resolve => {
server.listen(0, '127.0.0.1') server.listen(0, '127.0.0.1')
server.on('listening', () => { server.on('listening', () => {
const { port } = server.address() const { port } = server.address()
server.close(() => { server.close(() => resolve(port))
// Wait a bit for port to free as we try to bind right after freeing it
setTimeout(() => {
resolve(port)
}, 200)
})
}) })
}) })

View file

@ -5,12 +5,11 @@ const { waitFor } = require('../src/datatypes/util')
const { getPort } = require('./util') const { getPort } = require('./util')
async function test (version) { async function test (version) {
// const ChunkColumn = require('bedrock-provider').chunk('bedrock_' + (version.includes('1.19') ? '1.18.30' : version)) // TODO: Fix prismarine-chunk const ChunkColumn = require('bedrock-provider').chunk('bedrock_' + (version.includes('1.19') ? '1.18.30' : version)) // TODO: Fix prismarine-chunk
// Start the server, wait for it to accept clients, throws on timeout // Start the server, wait for it to accept clients, throws on timeout
const [port, v6] = [await getPort(), await getPort()] const port = await getPort()
console.log('Starting vanilla server', version, 'on port', port, v6) const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port })
const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port, 'server-portv6': v6 })
console.log('Started server') console.log('Started server')
const client = new Client({ const client = new Client({
@ -49,10 +48,10 @@ async function test (version) {
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) }) client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
}, 200) }, 200)
// client.on('level_chunk', async packet => { // Chunk read test client.on('level_chunk', async packet => { // Chunk read test
// const cc = new ChunkColumn(packet.x, packet.z) const cc = new ChunkColumn(packet.x, packet.z)
// await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count) await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count)
// }) })
console.log('Awaiting join') console.log('Awaiting join')

View file

@ -15,6 +15,7 @@ function createProtocol (version) {
const compiler = new ProtoDefCompiler() const compiler = new ProtoDefCompiler()
const protocol = mcData('bedrock_' + version).protocol.types const protocol = mcData('bedrock_' + version).protocol.types
compiler.addTypes(require('../src/datatypes/compiler-minecraft')) compiler.addTypes(require('../src/datatypes/compiler-minecraft'))
compiler.addTypes(require('prismarine-nbt/zigzag').compiler)
compiler.addTypesToCompile(protocol) compiler.addTypesToCompile(protocol)
fs.writeFileSync('./read.js', 'module.exports = ' + compiler.readCompiler.generate().replace('() =>', 'native =>')) fs.writeFileSync('./read.js', 'module.exports = ' + compiler.readCompiler.generate().replace('() =>', 'native =>'))
@ -38,7 +39,7 @@ require('minecraft-data/bin/generate_data')
// If no argument, build everything // If no argument, build everything
if (!process.argv[2]) { if (!process.argv[2]) {
convert('bedrock', 'latest') convert('latest')
for (const version of versions) { for (const version of versions) {
main(version) main(version)
} }

View file

@ -24,7 +24,7 @@ async function dump (version, force = true) {
const random = (Math.random() * 1000) | 0 const random = (Math.random() * 1000) | 0
const [port, v6] = [await getPort(), await getPort()] const [port, v6] = [await getPort(), await getPort()]
console.log('Starting dump server', version, 'on port', port, v6) console.log('Starting dump server', version)
const handle = await vanillaServer.startServerAndWait2(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port, 'server-portv6': v6 }) const handle = await vanillaServer.startServerAndWait2(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port, 'server-portv6': v6 })
console.log('Started dump server', version) console.log('Started dump server', version)

View file

@ -1,11 +1,156 @@
const bedrockServer = require('minecraft-bedrock-server') const http = require('https')
const fs = require('fs')
const cp = require('child_process')
const debug = process.env.CI ? console.debug : require('debug')('minecraft-protocol')
const https = require('https')
const { getFiles, waitFor } = require('../src/datatypes/util')
module.exports = { function head (url) {
...bedrockServer, return new Promise((resolve, reject) => {
startServerAndWait (version, withTimeout, options) { const req = http.request(url, { method: 'HEAD', timeout: 1000 }, resolve)
return bedrockServer.startServerAndWait(version, withTimeout, { ...options, root: __dirname }) req.on('error', reject)
}, req.on('timeout', () => { req.destroy(); debug('HEAD request timeout'); reject(new Error('timeout')) })
startServerAndWait2 (version, withTimeout, options) { req.end()
return bedrockServer.startServerAndWait2(version, withTimeout, { ...options, root: __dirname }) })
}
function get (url, outPath) {
const file = fs.createWriteStream(outPath)
return new Promise((resolve, reject) => {
https.get(url, { timeout: 1000 * 20 }, response => {
if (response.statusCode !== 200) return reject(new Error('Server returned code ' + response.statusCode))
response.pipe(file)
file.on('finish', () => {
file.close()
resolve()
})
})
})
}
// Get the latest versions
// TODO: once we support multi-versions
function fetchLatestStable () {
get('https://raw.githubusercontent.com/minecraft-linux/mcpelauncher-versiondb/master/versions.json', 'versions.json')
const versions = JSON.parse(fs.readFileSync('./versions.json'))
const latest = versions[0]
return latest.version_name
}
// Download + extract vanilla server and enter the directory
async function download (os, version, path = 'bds-') {
debug('Downloading server', os, version, 'into', path)
process.chdir(__dirname)
const verStr = version.split('.').slice(0, 3).join('.')
const dir = path + version
if (fs.existsSync(dir) && getFiles(dir).length) {
process.chdir(path + version) // Enter server folder
return verStr
}
try { fs.mkdirSync(dir) } catch { }
process.chdir(path + version) // Enter server folder
const url = (os, version) => `https://minecraft.azureedge.net/bin-${os}/bedrock-server-${version}.zip`
let found = false
for (let i = 0; i < 8; i++) { // Check for the latest server build for version (major.minor.patch.BUILD)
const u = url(os, `${verStr}.${String(i).padStart(2, '0')}`)
debug('Opening', u, Date.now())
let ret
try { ret = await head(u) } catch (e) { continue }
if (ret.statusCode === 200) {
found = u
debug('Found server', ret.statusCode)
break
}
}
if (!found) throw Error('did not find server bin for ' + os + ' ' + version)
console.info('🔻 Downloading', found)
await get(found, 'bds.zip')
console.info('⚡ Unzipping')
// Unzip server
if (process.platform === 'linux') cp.execSync('unzip -u bds.zip && chmod +777 ./bedrock_server')
else cp.execSync('tar -xf bds.zip')
return verStr
}
const defaultOptions = {
'level-generator': '2',
'server-port': '19130',
'online-mode': 'false'
}
// Setup the server
function configure (options = {}) {
const opts = { ...defaultOptions, ...options }
let config = fs.readFileSync('./server.properties', 'utf-8')
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)
}
function run (inheritStdout = true) {
const exe = process.platform === 'win32' ? 'bedrock_server.exe' : './bedrock_server'
return cp.spawn(exe, inheritStdout ? { stdio: 'inherit' } : {})
}
let lastHandle
// Run the server
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, options.path)
configure(options)
const handle = lastHandle = run(!onStart)
handle.on('error', (...a) => {
console.warn('*** THE MINECRAFT PROCESS CRASHED ***', a)
handle.kill('SIGKILL')
})
if (onStart) {
let stdout = ''
handle.stdout.on('data', data => {
stdout += data
if (stdout.includes('Server started')) onStart()
})
handle.stdout.pipe(process.stdout)
handle.stderr.pipe(process.stdout)
}
return handle
}
// Start the server and wait for it to be ready, with a timeout
async function startServerAndWait (version, withTimeout, options) {
let handle
await waitFor(async res => {
handle = await startServer(version, res, options)
}, withTimeout, () => {
handle?.kill()
throw new Error(`Server did not start on time (${withTimeout}ms, now ${Date.now()})`)
})
return handle
}
async function startServerAndWait2 (version, withTimeout, options) {
try {
return await startServerAndWait(version, 1000 * 60, options)
} catch (e) {
console.log(e)
console.log('^ Tring once more to start server in 10 seconds...')
lastHandle?.kill()
await new Promise(resolve => setTimeout(resolve, 10000))
process.chdir(__dirname)
fs.rmSync('bds-' + version, { recursive: true })
return await startServerAndWait(version, withTimeout, options)
} }
} }
if (!module.parent) {
// if (process.argv.length < 3) throw Error('Missing version argument')
startServer(process.argv[2] || '1.17.10', null, process.argv[3] ? { 'server-port': process.argv[3], 'online-mode': !!process.argv[4] } : undefined)
}
module.exports = { fetchLatestStable, startServer, startServerAndWait, startServerAndWait2 }