Compare commits

...
Sign in to create a new pull request.

51 commits

Author SHA1 Message Date
rom1504bot
fa6acab0f3
Release 3.49.0 (#650)
* Release 3.49.0

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-10-01 21:21:34 -04:00
extremeheat
b48518a6e7
1.21.111 (#649) 2025-10-01 20:52:10 -04:00
thejfkvis
407756b938
Skin Data Changes (#647) 2025-10-01 20:51:53 -04:00
rom1504bot
0b9c49fedc
Release 3.48.1 (#638)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-10 02:31:46 -04:00
TSL534
6b1474d2c6
Update login client skinData (#635)
* Update for the Skin Data in the Login system

* Fix for CI

* CI Fix2

* Fixed comments again for CI

* Update login.js

---------

Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-08-10 01:47:18 -04:00
rom1504bot
6c659feb5d
Release 3.48.0 (#633)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-06 14:16:25 -04:00
extremeheat
06fb3de3a0
1.21.100 (#632)
* Update options.js

* Update README.md
2025-08-06 14:05:20 -04:00
rom1504bot
6f06a8996e
Release 3.47.0 (#625)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-04 16:48:07 -04:00
Tamás Papp
14daa2d95a
1.21.93 support (#623) 2025-07-04 16:47:12 -04:00
rom1504bot
80751d58a7
Release 3.46.0 (#618)
* Release 3.46.0

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-06-18 21:51:20 -04:00
extremeheat
c66cdd3d62
1.21.90 (#617)
* 1.21.90

* Update README.md
2025-06-18 21:31:21 -04:00
extremeheat
e503c47c79
Start work for 1.21.90 (#616)
* 1.21.90

* update

* remove 1.21.90 in ci
2025-06-18 20:14:38 -04:00
rom1504bot
29ba39343a
Release 3.45.0 (#603)
* Release 3.45.0

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-05-08 20:19:26 -04:00
extremeheat
e71fd513dd
1.21.80 (#602) 2025-05-08 19:51:20 -04:00
LunaryNet2
b6b0bcdd70
Update index.d.ts (#596) 2025-03-31 20:13:27 -04:00
rom1504bot
d88309507d
Release 3.44.0 (#595)
* Release 3.44.0

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-03-26 00:20:38 -04:00
extremeheat
065f41db8c
1.21.70 (#594)
* Update options.js

* Update README.md
2025-03-25 23:38:56 -04:00
rom1504bot
9f11f21991
Release 3.43.1 (#592)
* Release 3.43.1

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-03-19 14:03:47 -04:00
EntifiedOptics
47f342ca95
Fix server not correctly removing clients (#588)
* Properly remove connection from clients list

* Changed misname in onCloseConnection

* Update server.js fix

---------

Co-authored-by: JSbETms <137791538+JSbETms@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-03-18 17:01:49 -04:00
extremeheat
328785d8af
Quicken tests 2025-02-13 15:36:33 -05:00
rom1504bot
d4a9faf153
Release 3.43.0 (#577)
* Release 3.43.0

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-02-11 22:08:50 -05:00
Tamás Papp
eeb5e47e35
1.21.60 support (#570)
* Add support for ItemRegistryPacket (for shield item ID)

* simplify

* lint

* internal test

* update start_game handling

* test: capture events rejections

---------

Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-02-11 21:42:31 -05:00
Slauh!
2d7d32dfd1
Update API.md fix chat message handling (#575) 2025-02-06 13:54:38 -05:00
Max
0ed8e32be8
Fix version feature handling (#572)
* Replace unsupported version with next supported version

* add validity check

---------

Co-authored-by: extremeheat <extreme@protonmail.ch>
2025-01-31 14:02:04 -05:00
Romain Beaumont
75fa085e86
node 22 (#568) 2025-01-26 23:06:36 +01:00
dependabot[bot]
8e728255c7
Bump mocha from 10.8.2 to 11.0.1 (#548)
Bumps [mocha](https://github.com/mochajs/mocha) from 10.8.2 to 11.0.1.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v10.8.2...v11.0.1)

---
updated-dependencies:
- dependency-name: mocha
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-24 23:06:04 +01:00
rom1504bot
cd9a31a435
Release 3.42.3 (#567) 2025-01-18 05:09:53 -05:00
extremeheat
38dc5a2561
Fix Server maxPlayers option (#565) 2025-01-18 02:51:08 -05:00
extremeheat
a79be99c0f
Update ci.yml 2025-01-02 18:53:50 -05:00
extremeheat
a37bc5054e
Add retry to tests 2025-01-02 16:16:33 -05:00
github-actions[bot]
e7bb990aea Release 3.42.2 (#558)
Update HISTORY.md
2024-12-15 13:22:17 +00:00
poekcosimo
dfcf5b2f1b Fix missing type serialization error (#557) 2024-12-15 13:21:21 +00:00
rom1504bot
aa3dafa5f4
Release 3.42.1 (#554)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-09 22:40:12 -05:00
extremeheat
24d3200181
Add 1.21.40 login fields (#553) 2024-12-09 17:56:03 -05:00
extremeheat
347e303ce4
Remove protodef varint types (#552)
Remove protodef types which are now natively in ProtoDef
2024-12-09 17:55:31 -05:00
rom1504bot
f48b50849a
Release 3.42.0 (#551)
* Release 3.42.0

* Update HISTORY.md

* Update README.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2024-12-05 01:08:37 -05:00
extremeheat
1c0836bff0
1.21.50 2024-12-05 00:44:03 -05:00
extremeheat
b4adf04845
Update vanilla test server setup (#550)
* Update test server

* lint
2024-12-05 00:05:30 -05:00
extremeheat
52358d7277
Update internal tests for 1.21.50 2024-12-04 23:13:16 -05:00
extremeheat
29c7b92d96
Update startVanillaServer.js 2024-12-04 22:47:15 -05:00
LucienHH
911e0e890f
Dynamic compression & batch header (#544) 2024-12-03 17:23:32 -05:00
rom1504bot
fa730786d5
Release 3.41.0 (#538)
* Release 3.41.0

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2024-10-24 23:00:42 -04:00
extremeheat
dd5c4de4f2
1.21.42 2024-10-24 22:46:20 -04:00
extremeheat
a4023b987b
Update options.js 2024-10-24 22:14:34 -04:00
extremeheat
cd937a45d0
Update startVanillaServer.js 2024-10-24 22:02:07 -04:00
rom1504bot
391ea9814c
Release 3.40.0 (#529)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-09-20 04:04:13 -04:00
poekcosimo
fc30c96135
1.21.30 support (#527) 2024-09-20 03:50:40 -04:00
extremeheat
cb530c8b45
Update tests (#528)
* Debug

* Update internal.js

* Update internal.js

* Update internal.js

* Update internal.js
2024-09-20 01:07:47 -04:00
rom1504bot
418efa1da6
Release 3.39.0 (#522) 2024-08-17 21:18:52 -04:00
extremeheat
3be55777fa
1.21.20 2024-08-17 20:52:54 -04:00
extremeheat
4c3f62567e
update disconnect packet 2024-08-17 20:03:02 -04:00
27 changed files with 194 additions and 357 deletions

View file

@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18.x]
node-version: [22.x]
runs-on: ${{ matrix.os }}
timeout-minutes: 14
steps:
@ -26,5 +26,11 @@ jobs:
uses: actions/setup-node@v1
with:
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 test

View file

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

View file

@ -1,3 +1,57 @@
## 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)

View file

@ -11,7 +11,7 @@ Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication
## 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
- 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
- Parse and serialize packets as JavaScript objects
- Automatically respond to keep-alive packets
- [Proxy and mitm connections](docs/API.md#proxy-docs)

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!
client.on('add_player', (packet) => {
client.queue('text', {
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '',
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '',
message: `Hey, ${packet.username} just joined!`
})
})

4
index.d.ts vendored
View file

@ -3,7 +3,7 @@ import { Realm } from 'prismarine-realms'
import { ServerDeviceCodeResponse } from 'prismarine-auth'
declare module 'bedrock-protocol' {
type Version = '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.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'
export interface Options {
// The string version to start the client or server as
@ -162,7 +162,7 @@ declare module 'bedrock-protocol' {
constructor(options: Options)
listen(host?: string, port?: number): Promise<void>
listen(): Promise<void>
close(disconnectReason?: string): Promise<void>
on(event: 'connect', cb: (client: Player) => void): any

View file

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

View file

@ -26,6 +26,7 @@ class Client extends Connection {
this.compressionAlgorithm = this.versionGreaterThanOrEqualTo('1.19.30') ? 'none' : 'deflate'
this.compressionThreshold = 512
this.compressionLevel = this.options.compressionLevel
this.batchHeader = 0xfe
if (isDebug) {
this.inLog = (...args) => debug('C ->', ...args)
@ -60,7 +61,9 @@ class Client extends Connection {
try {
const mcData = require('minecraft-data')('bedrock_' + this.options.version)
this.features = {
compressorInHeader: mcData.supportFeature('compressorInPacketHeader')
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`)
@ -144,9 +147,18 @@ class Client extends Connection {
...this.accessToken // Mojang + Xbox JWT from auth
]
const encodedChain = JSON.stringify({ chain })
debug('Auth chain', chain)
let encodedChain
if (this.features.newLoginIdentityFields) { // 1.21.90+
encodedChain = JSON.stringify({
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', {
protocol_version: this.options.protocolVersion,
@ -183,7 +195,8 @@ class Client extends Connection {
if (this.status === ClientStatus.Disconnected) return
this.write('disconnect', {
hide_disconnect_screen: hide,
message: reason
message: reason,
filtered_message: ''
})
this.close(reason)
}
@ -239,7 +252,9 @@ class Client extends Connection {
break
case 'start_game':
this.startGameData = pakData.params
this.startGameData.itemstates.forEach(state => {
// fallsthrough
case 'item_registry': // 1.21.60+ send itemstates in item_registry packet
pakData.params.itemstates?.forEach(state => {
if (state.name === 'minecraft:shield') {
this.serializer.proto.setVariable('ShieldItemID', state.runtime_id)
this.deserializer.proto.setVariable('ShieldItemID', state.runtime_id)

View file

@ -28,18 +28,22 @@ class Connection extends EventEmitter {
}
versionLessThan (version) {
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
return this.options.protocolVersion < (typeof version === 'string' ? Versions[version] : version)
}
versionGreaterThan (version) {
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
return this.options.protocolVersion > (typeof version === 'string' ? Versions[version] : version)
}
versionGreaterThanOrEqualTo (version) {
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown 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)
}
@ -66,9 +70,17 @@ 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) {
this.outLog?.(name, params)
if (name === 'start_game') this.updateItemPalette(params.itemstates)
this._processOutbound(name, params)
const batch = new Framer(this)
const packet = this.serializer.createPacketBuffer({ name, params })
batch.addEncodedPacket(packet)
@ -82,7 +94,7 @@ class Connection extends EventEmitter {
queue (name, params) {
this.outLog?.('Q <- ', name, params)
if (name === 'start_game') this.updateItemPalette(params.itemstates)
this._processOutbound(name, params)
const packet = this.serializer.createPacketBuffer({ name, params })
if (name === 'level_chunk') {
// Skip queue, send ASAP
@ -153,7 +165,7 @@ class Connection extends EventEmitter {
// These are callbacks called from encryption.js
onEncryptedPacket = (buf) => {
const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header
const packet = this.batchHeader ? Buffer.concat([Buffer.from([this.batchHeader]), buf]) : buf
this.sendMCPE(packet)
}
@ -165,7 +177,7 @@ class Connection extends EventEmitter {
}
handle (buffer) { // handle encapsulated
if (buffer[0] === 0xfe) { // wrapper
if (!this.batchHeader || buffer[0] === this.batchHeader) { // wrapper
if (this.encryptionEnabled) {
this.decrypt(buffer.slice(1))
} else {

View file

@ -1,7 +1,7 @@
/* eslint-disable */
const UUID = require('uuid-1345')
const minecraft = require('./minecraft')
const { Read, Write, SizeOf } = require('./varlong')
const [Read, Write, SizeOf] = [{}, {}, {}]
/**
* UUIDs
@ -116,74 +116,6 @@ Read.lnbt = ['native', minecraft.lnbt[0]]
Write.lnbt = ['native', minecraft.lnbt[1]]
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
* - used for determining the size of the following enum

View file

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

View file

@ -1,63 +0,0 @@
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,7 +36,6 @@ module.exports = (client, server, options) => {
client.createClientUserChain = (privateKey) => {
let payload = {
...skinData,
SkinGeometryDataEngineVersion: client.versionGreaterThanOrEqualTo('1.17.30') ? '' : undefined,
ClientRandomId: Date.now(),
CurrentInputMode: 1,
@ -47,25 +46,30 @@ module.exports = (client, server, options) => {
GameVersion: options.version || '1.16.201',
GuiScale: -1,
LanguageCode: 'en_GB', // TODO locale
GraphicsMode: 1, // 1:simple, 2:fancy, 3:advanced, 4:ray_traced
PlatformOfflineId: '',
PlatformOnlineId: '', // chat
// 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
// inside of PlayFab.
PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16), // 1.16.210
// inside of PlayFab.The playfab ID is always lowercased.
PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16).toLowerCase(), // 1.16.210
SelfSignedId: nextUUID(),
ServerAddress: `${options.host}:${options.port}`,
ThirdPartyName: client.profile.name,
ThirdPartyNameOnly: false,
ThirdPartyName: client.profile.name, // Gamertag
ThirdPartyNameOnly: client.versionGreaterThanOrEqualTo('1.21.90') ? undefined : false,
UIProfile: 0,
IsEditorMode: false,
TrustedSkin: client.versionGreaterThanOrEqualTo('1.19.20') ? 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 || {}
payload = { ...payload, ...customPayload }

View file

@ -3,12 +3,12 @@ const mcData = require('minecraft-data')
// Minimum supported version (< will be kicked)
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
const CURRENT_VERSION = '1.21.2'
const CURRENT_VERSION = '1.21.111'
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
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 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 testedVersions = process.env.CI ? Object.keys(Versions).filter(v => !skippedVersionsOnGithubCI.includes(v)) : Object.keys(Versions)
const defaultOptions = {

View file

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

View file

@ -28,10 +28,12 @@ class Player extends Connection {
this.outLog = (...args) => debug('<- S', ...args)
}
this.batchHeader = this.server.batchHeader
// Compression is server-wide
this.compressionAlgorithm = this.server.compressionAlgorithm
this.compressionLevel = this.server.compressionLevel
this.compressionThreshold = this.server.compressionThreshold
this.compressionHeader = this.server.compressionHeader
this._sentNetworkSettings = false // 1.19.30+
}
@ -76,11 +78,18 @@ class Player extends Connection {
// Parse login data
const tokens = body.params.tokens
const authChain = JSON.parse(tokens.identity)
const skinChain = tokens.client
try {
var { key, userData, skinData } = this.decodeLoginJWT(authChain.chain, skinChain) // eslint-disable-line
const skinChain = tokens.client
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) {
debug(this.address, e)
this.disconnect('Server authentication error')
@ -117,7 +126,8 @@ class Player extends Connection {
if (this.status === ClientStatus.Disconnected) return
this.write('disconnect', {
hide_disconnect_screen: hide,
message: reason
message: reason,
filtered_message: ''
})
this.server.conLog('Kicked ', this.connection?.address, reason)
setTimeout(() => this.close('kick'), 100) // Allow time for message to be recieved.

View file

@ -6,9 +6,11 @@ class Framer {
constructor (client) {
// Encoding
this.packets = []
this.batchHeader = client.batchHeader
this.compressor = client.compressionAlgorithm || 'none'
this.compressionLevel = client.compressionLevel
this.compressionThreshold = client.compressionThreshold
this.compressionHeader = client.compressionHeader || 0
this.writeCompressor = client.features.compressorInHeader && client.compressionReady
}
@ -38,7 +40,7 @@ class Framer {
static decode (client, buf) {
// Read header
if (buf[0] !== 0xfe) throw Error('bad batch packet header ' + buf[0])
if (this.batchHeader && buf[0] !== this.batchHeader) throw Error(`bad batch packet header, received: ${buf[0]}, expected: ${this.batchHeader}`)
const buffer = buf.slice(1)
// Decompress
let decompressed
@ -58,9 +60,10 @@ class Framer {
encode () {
const buf = Buffer.concat(this.packets)
const compressed = (buf.length > this.compressionThreshold) ? this.compress(buf) : buf
const header = this.writeCompressor ? [0xfe, 0] : [0xfe]
return Buffer.concat([Buffer.from(header), compressed])
const shouldCompress = buf.length > this.compressionThreshold
const header = this.batchHeader ? [this.batchHeader] : []
if (this.writeCompressor) header.push(shouldCompress ? this.compressionHeader : 255)
return Buffer.concat([Buffer.from(header), shouldCompress ? this.compress(buf) : buf])
}
addEncodedPacket (chunk) {

View file

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

View file

@ -14,7 +14,7 @@ function prepare (version) {
async function startTest (version = CURRENT_VERSION, ok) {
await prepare(version)
const Item = require('../types/Item')(version)
// const Item = require('../types/Item')(version)
const port = await getPort()
const server = new Server({ host: '0.0.0.0', port, version, offline: true })
@ -47,6 +47,7 @@ async function startTest (version = CURRENT_VERSION, ok) {
must_accept: false,
has_scripts: false,
behaviour_packs: [],
world_template: { uuid: '550e8400-e29b-41d4-a716-446655440000', version: '' }, // 1.21.50
texture_packs: [],
resource_pack_links: []
})
@ -56,13 +57,17 @@ async function startTest (version = CURRENT_VERSION, ok) {
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_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: [] })
if (client.versionLessThan('1.21.60')) {
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_time', { time: 5433771 })
client.queue('set_difficulty', { difficulty: 1 })
@ -96,11 +101,11 @@ async function startTest (version = CURRENT_VERSION, ok) {
loop = setInterval(() => {
client.write('network_chunk_publisher_update', { coordinates: { x: 646, y: 130, z: 77 }, radius: 64 })
}, 9500)
}, 6500)
setTimeout(() => {
client.write('play_status', { status: 'player_spawn' })
}, 6000)
}, 3000)
// Respond to tick synchronization packets
client.on('tick_sync', (packet) => {

View file

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

View file

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

View file

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

View file

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

View file

@ -8,8 +8,9 @@ async function test (version) {
// 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
const port = await getPort()
const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port })
const [port, v6] = [await getPort(), await getPort()]
console.log('Starting vanilla server', version, 'on port', port, v6)
const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port, 'server-portv6': v6 })
console.log('Started server')
const client = new Client({

View file

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

View file

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

View file

@ -1,156 +1,11 @@
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')
const bedrockServer = require('minecraft-bedrock-server')
function head (url) {
return new Promise((resolve, reject) => {
const req = http.request(url, { method: 'HEAD', timeout: 1000 }, resolve)
req.on('error', reject)
req.on('timeout', () => { req.destroy(); debug('HEAD request timeout'); reject(new Error('timeout')) })
req.end()
})
}
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)
module.exports = {
...bedrockServer,
startServerAndWait (version, withTimeout, options) {
return bedrockServer.startServerAndWait(version, withTimeout, { ...options, root: __dirname })
},
startServerAndWait2 (version, withTimeout, options) {
return bedrockServer.startServerAndWait2(version, withTimeout, { ...options, root: __dirname })
}
}
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 }