Compare commits

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

114 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
rom1504bot
a797201ecb
Release 3.38.0 (#514) 2024-07-11 00:16:58 -04:00
Tamás Papp
5d3986924d
Support 1.21.2, and add missing versions to type definitions (#510)
* Add missing versions to type definitions

* Add support for 1.21.2

* Add 1.21.2 to README
2024-07-10 20:58:47 -04:00
TheLegendaryTrashcan
c4593aa355
Fix example in README.md for 1.21 (#506)
* Fix example  in README.md for 1.21

* Update client.js

* Update README.md

---------

Co-authored-by: extremeheat <extreme@protonmail.ch>
2024-07-05 07:22:05 -04:00
w0ahL
84c5231b92
Don't send now deprecated tick sync packets on 1.21 and newer (#504)
* Tick Sync is now deprecated

* Add new function here

* Add check

* Remove useless require by me

* Fix
2024-06-16 16:51:22 -04:00
rom1504bot
1fee54f517
Release 3.37.0 (#505)
* Release 3.37.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-06-14 04:57:51 -04:00
extremeheat
5b2d78792c
Support 1.21.0 2024-06-14 04:43:57 -04:00
extremeheat
19ade45ea5
Update vanilla.js disable chunk tests
Should be done separately in p-chunk repo or in mineflayer
2024-06-13 23:07:17 -04:00
Kaaaaii
16e15d80a5
Fix typo in types (#501)
`ClientStatus.Disconected` is now `ClientStatus.Disconnected`
2024-05-31 10:20:59 -04:00
extremeheat
1ddc525169
Update README 2024-04-26 01:44:18 -04:00
rom1504bot
a4346f9638
Release 3.36.0 (#498)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-25 03:09:02 -04:00
extremeheat
bd32aa8d04
1.20.80 2024-04-25 02:47:16 -04:00
extremeheat
c65fea2916
helper-bot fix 2024-03-29 01:32:53 -04:00
extremeheat
3e5158bebe
helper-bot fix 2024-03-28 16:14:09 -04:00
extremeheat
a7ae528c16
update helper-bot for gh-helpers v0.2 2024-03-28 16:06:06 -04:00
rom1504bot
184339b9d4
Release 3.35.0 (#489)
* Release 3.35.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-03-13 23:32:19 -04:00
extremeheat
d8e707112a
1.20.71 2024-03-13 23:19:21 -04:00
extremeheat
5ee5744ec9
Update update helper for gh-helpers v0.1
Handle breaking changes to API
2024-03-09 15:46:17 -05:00
extremeheat
914ed6840b
Move github-helper script to new gh-helpers (#485) 2024-03-09 11:51:36 -05:00
extremeheat
ab93d0d082
Note npm update command in readme 2024-02-10 02:11:32 -05:00
rom1504bot
0132302e53
Release 3.34.0 (#481)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-07 21:05:28 -05:00
extremeheat
c278a03f95
1.20.61 support (#480)
* Update README.md

* Update options.js
2024-02-07 21:04:39 -05:00
extremeheat
d3161badc6
Compressor handling update for 1.20.60 (#479)
* support compressor in header

* use mcdata features

* cleanup
2024-02-07 20:03:26 -05:00
extremeheat
be6f0cde9f
Update and rename CONTRIBUTING.md to docs/CONTRIBUTING.md (#475) 2024-01-31 11:24:08 -05:00
GameParrot
842e66266f
Add flow and deviceType options to relay (#464)
* add flow and deviceType

for when authTitle is changed

* Update index.d.ts

---------

Co-authored-by: extremeheat <extreme@protonmail.ch>
2024-01-02 14:37:32 -05:00
rom1504bot
54840f818c
Release 3.33.1 (#472) 2023-12-27 13:06:30 -05:00
extremeheat
7b74cbf712
Fix zigzag type move in prismarine-nbt (#471)
* Fix rename of zigzag varint type in prismarine-nbt

* fix
2023-12-27 12:53:39 -05:00
rom1504bot
fd165b8908
Release 3.33.0 (#467)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-06 23:21:00 -05:00
extremeheat
d53211c6a1
1.20.50 (#466)
* Update options.js

* update readme
2023-12-06 22:41:23 -05:00
Tamas Papp
2ecf01d63e
Add 1.20.30 and 1.20.40 to index.d.ts (#461) 2023-10-26 22:41:43 -04:00
rom1504bot
d66dc1b237
Release 3.32.0 (#460) 2023-10-26 02:10:51 -04:00
extremeheat
63eb673c1f
1.20.40 (#459) 2023-10-26 00:53:20 -04:00
extremeheat
50911d2463
Update workflow node version 2023-10-24 15:16:35 -04:00
Spongecade
689658c4ab
Update Minecraft wiki link to new domain (#455) 2023-10-06 22:05:28 -04:00
rom1504bot
755ba31ea7
Release 3.31.0 (#450)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-20 02:40:49 -04:00
extremeheat
22502b90fd
1.20.30 2023-09-20 02:22:25 -04:00
extremeheat
f92db61c89
Add links field to server resource_packs_info 2023-09-20 00:47:37 -04:00
YeemiRouth
8f3b6c5aec
Update API.md (#448) 2023-09-18 13:56:42 -04:00
rom1504bot
4c7bb65ad0
Release 3.30.1 (#444)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-23 04:51:53 -04:00
GameParrot
f0f1351d40
Update Mojang public key used for logins (#443) 2023-08-23 04:43:02 -04:00
kotinash
2c00402a9e
index.d.ts: Fixed a typo (#441) 2023-08-09 20:26:55 -04:00
MrSterdy
50cd489f6e
Mark listen and close as async (#440) 2023-08-05 15:33:25 +02:00
MrSterdy
1414420574
Stop disconnecting when upstream packet deserialization fails (#435)
* Remove disconnect on upstream packet deserialization error

* Add disconnectOnParseError option

* Rename disconnectOnParseError option to omitParseErrors

* Update index.d.ts

* Update index.d.ts

---------

Co-authored-by: extremeheat <extreme@protonmail.ch>
2023-07-29 10:14:10 +02:00
Tamas Papp
010d57e78a
Add 1.20.0 and 1.20.10 to index.d.ts (#431) 2023-07-19 12:59:38 -04:00
rom1504bot
d063a6427a
Release 3.30.0 (#429)
* Release 3.30.0

* Update HISTORY.md

* Update HISTORY.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: extremeheat <extreme@protonmail.ch>
2023-07-12 14:05:46 -04:00
extremeheat
46f11204ba
1.20.10 support (#428)
* 1.20.10 support

* Update README.md
2023-07-12 14:01:10 -04:00
extremeheat
fe3c3b01fc
Add commands workflow 2023-07-12 13:21:25 -04:00
GameParrot
b2c141c25f
Fix upstream relay batchingInterval (#425) 2023-07-04 07:27:40 -04:00
Nyrok
aa0b5743b5
Release 3.29.1 (#421) 2023-06-26 18:27:04 -04:00
extremeheat
d2c4430833
Add missing data to client login user chain (#420)
Fix connecting to pocketmine servers
2023-06-25 14:20:50 -04:00
extremeheat
d31b2b2aec
Add FAQ entry and replit warning on client ping error (#415) 2023-06-17 23:04:23 -04:00
Visual1mpact
0b7f3e037d
types: Fix Relay authTitle type (#418)
authType is a string. It was corrected in commit 8507286895 but this particular change was overlooked.

This solves the error:

node_modules/bedrock-protocol/index.d.ts:177:17 - error TS2304: Cannot find name 'title'.

177     authTitle?: title | string
                    ~~~~~


Found 1 error in node_modules/bedrock-protocol/index.d.ts:177
2023-06-13 09:42:31 -04:00
extremeheat
6d708339aa
Release 3.29.0 (#411)
* Release 3.29.0

* tests: swap UAs to see if fixes flaky CI

* lint

* tests: increase head timeout

* undo ua changes
2023-06-08 05:26:22 -04:00
extremeheat
97b8140fdd
1.20.0 support (#410) 2023-06-08 03:47:46 -04:00
Hitesh. V
79a6391709
Sponsoring (#402) 2023-05-20 15:04:32 +02:00
kinash
cd0ddc16e1
Update basicServer example to 1.19.80 (#399) 2023-05-20 15:00:52 +02:00
Hitesh. V
2cea7d6912
"JWT not active" FAQ (#397) 2023-05-09 20:07:06 -04:00
LucienHH
3b59446780
Release v3.28.1 (#396)
* Release v3.28.1

* Update HISTORY.md

Only include information relevant to package users

---------

Co-authored-by: extremeheat <extreme@protonmail.ch>
2023-05-04 15:23:03 -04:00
andriycraft
17d5b15bac
Update genPacketDumps.js (#393)
* Update genPacketDumps.js

* Update genPacketDumps.js
2023-05-04 12:59:56 -04:00
LucienHH
32e0c66997
Fix followPort option (#394)
* Fix `followPort` option

* Use correct default
2023-05-04 12:58:55 -04:00
extremeheat
8507286895
Update index.d.ts
inline versions list for helper-bot workflow
2023-04-30 19:21:11 -04:00
Hitesh
a8f1349c83
Update type definitions (#391)
* Better Types

* Fixed version

got that issue/information from U9G

Co-Authored-By: u9g <git@u9g.dev>

* removing undocumented type

---------

Co-authored-by: u9g <git@u9g.dev>
2023-04-30 19:19:04 -04:00
extremeheat
d96f1a1a69
Release 3.28.0 (#387) 2023-04-27 02:47:14 -04:00
extremeheat
d4226e0ffd
1.19.80 support (#386) 2023-04-27 02:34:03 -04:00
43 changed files with 566 additions and 543 deletions

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
github: PrismarineJS
open_collective: prismarinejs
custom: https://rysolv.com/repos/detail/74691b23-938d-4b2f-b65a-5c47bf5b3f0f

View file

@ -1,59 +0,0 @@
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('./github-helper') const helper = require('gh-helpers')()
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,11 +102,10 @@ 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.open) { if (issueStatus.isOpen) {
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.')
@ -114,7 +113,7 @@ async function fetchLatest () {
} }
if (issueStatus.closed) { if (issueStatus.isClosed) {
// 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
@ -127,7 +126,7 @@ async function fetchLatest () {
CloudburstMC: getCommitsInRepo('CloudburstMC/Protocol', version, currentVersionReleaseDate) CloudburstMC: getCommitsInRepo('CloudburstMC/Protocol', version, currentVersionReleaseDate)
}) })
if (issueStatus.open) { if (issueStatus.isOpen) {
helper.updateIssue(issueStatus.id, issuePayload) helper.updateIssue(issueStatus.id, issuePayload)
} else { } else {
helper.createIssue(issuePayload) helper.createIssue(issuePayload)

View file

@ -17,14 +17,20 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
node-version: [16.x] node-version: [22.x]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 12 timeout-minutes: 14
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
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

22
.github/workflows/commands.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Repo Commands
on:
issue_comment: # Handle comment commands
types: [created]
pull_request_target: # Handle renamed PRs
types: [edited]
jobs:
comment-trigger:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
- name: Run command handlers
uses: PrismarineJS/prismarine-repo-actions@master
with:
# NOTE: You must specify a Personal Access Token (PAT) with repo access here. While you can use the default GITHUB_TOKEN, actions taken with it will not trigger other actions, so if you have a CI workflow, commits created by this action will not trigger it.
token: ${{ secrets.PAT_PASSWORD }}
# See `Options` section below for more info on these options
install-command: npm install
/fixlint.fix-command: npm run fix

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: 16.0.0 node-version: 22.0.0
- name: Install Github Actions toolkit - name: Install Github Actions helper
run: npm i @actions/github run: npm i gh-helpers
# 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

2
.gitignore vendored
View file

@ -5,4 +5,4 @@ __*
# Runtime generated data # Runtime generated data
data/ data/
tools/bds* tools/bds*
*.txt tools/pmmp*

View file

@ -1,8 +1,126 @@
## 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
* [Fix zigzag type move in prismarine-nbt (#471)](https://github.com/PrismarineJS/bedrock-protocol/commit/7b74cbf7129646adc80d50304afce6240848cfae) (thanks @extremeheat)
## 3.33.0
* [1.20.50 (#466)](https://github.com/PrismarineJS/bedrock-protocol/commit/d53211c6a1fe5f941ce547886ad6ec031ae05d9d) (thanks @extremeheat)
* [Add 1.20.30 and 1.20.40 to index.d.ts (#461)](https://github.com/PrismarineJS/bedrock-protocol/commit/2ecf01d63e64b910b87f303fc4fb2b30f392cb28) (thanks @CreeperG16)
## 3.32.0
* [1.20.40 support (#459)](https://github.com/PrismarineJS/bedrock-protocol/commit/63eb673c1f30beb58f97e3b37295129000bf6a10) (thanks @CreeperG16)
* [Update Minecraft wiki link to new domain (#455)](https://github.com/PrismarineJS/bedrock-protocol/commit/689658c4ab1ccb3ef1ae812d78d090212b1acf3f) (thanks @Spongecade)
## 3.31.0
* [1.20.30](https://github.com/PrismarineJS/bedrock-protocol/commit/22502b90fdc29f6327239c6c201370c8f839c892) (thanks @extremeheat)
* [Add links field to server resource_packs_info](https://github.com/PrismarineJS/bedrock-protocol/commit/f92db61c89851dfbdbc906f926fc1433162854d0) (thanks @extremeheat)
* [Update API.md (#448)](https://github.com/PrismarineJS/bedrock-protocol/commit/8f3b6c5aecf24d6f8d235afe2a9d911840e6a3f8) (thanks @Laamy)
## 3.30.1
* [Update Mojang public key used for logins (#443)](https://github.com/PrismarineJS/bedrock-protocol/commit/f0f1351d40966192e38ee9fe21b7c37754abba04) (thanks @GameParrot)
* [index.d.ts: Fixed a typo (#441)](https://github.com/PrismarineJS/bedrock-protocol/commit/2c00402a9e9a0a283e712bf4f52190a57ea12c3f) (thanks @kotinash)
* [Mark `listen` and `close` as async (#440)](https://github.com/PrismarineJS/bedrock-protocol/commit/50cd489f6e16fa6fe04b1825617d8246bd3935f4) (thanks @MrSterdy)
* [Stop disconnecting when upstream packet deserialization fails (#435)](https://github.com/PrismarineJS/bedrock-protocol/commit/141442057464b3247ace8468863f27a3c334306e) (thanks @MrSterdy)
* [Add 1.20.0 and 1.20.10 to index.d.ts (#431)](https://github.com/PrismarineJS/bedrock-protocol/commit/010d57e78a9130c612e48db7a32f841de83e9c68) (thanks @CreeperG16)
## 3.30.0
* 1.20.10 support (thanks @CreeperG16)
* [Fix upstream relay batchingInterval (#425)](https://github.com/PrismarineJS/bedrock-protocol/commit/b2c141c25f3fad9641644742b6cc1a71bc601d61) (thanks @GameParrot)
## 3.29.1
* Add missing data to client login user chain (#420)
* Add FAQ entry and replit warning on client ping error (#415)
* Types: Fix Relay authTitle type (#418)
## 3.29.0
* 1.20.0 support
## 3.28.1
* Fix `followPort` option (@LucienHH)
* Typescript definition fixes (@hvlxh)
## 3.28.0
* 1.19.80 support
## 3.27.1 ## 3.27.1
* Fix `raknetBackend` option not being applied correctly * Fix `raknetBackend` option not being applied correctly
## 3.27.0 ## 3.27.0
* Corrections to types * Corrections to types (@stevarino)
* Expose ServerAdvertisement class (#368) @hvlxh * Expose ServerAdvertisement class (#368) @hvlxh
* Update mc-data links * Update mc-data links

View file

@ -5,13 +5,13 @@
[![Official Discord](https://img.shields.io/static/v1.svg?label=OFFICIAL&message=DISCORD&color=blue&logo=discord&style=for-the-badge)](https://discord.gg/GsEFRM8) [![Official Discord](https://img.shields.io/static/v1.svg?label=OFFICIAL&message=DISCORD&color=blue&logo=discord&style=for-the-badge)](https://discord.gg/GsEFRM8)
Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication and encryption. Help [contribute](CONTRIBUTING.md). Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication and encryption. Help [contribute](docs/CONTRIBUTING.md).
[Protocol doc](https://prismarinejs.github.io/minecraft-data/?v=bedrock_1.19.10&d=protocol) [Protocol doc](https://prismarinejs.github.io/minecraft-data/?v=bedrock_1.19.10&d=protocol)
## 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 - 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 - 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,6 +34,8 @@ 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
@ -52,7 +54,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: '', type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '',
message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}` message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}`
}) })
} }
@ -126,7 +128,7 @@ Through node.js, add `process.env.DEBUG = 'minecraft-protocol'` at the top of yo
## Contribute ## Contribute
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and https://github.com/PrismarineJS/prismarine-contribute Please read [CONTRIBUTING.md](docs/CONTRIBUTING.md) and https://github.com/PrismarineJS/prismarine-contribute
## History ## History

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: '', type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '',
message: `Hey, ${packet.username} just joined!` message: `Hey, ${packet.username} just joined!`
}) })
}) })
@ -206,16 +206,22 @@ relay.on('connect', player => {
console.log('New connection', player.connection.address) console.log('New connection', player.connection.address)
// Server is sending a message to the client. // Server is sending a message to the client.
player.on('clientbound', ({ name, params }) => { player.on('clientbound', ({ name, params }, des) => {
if (name === 'disconnect') { // Intercept kick if (name === 'disconnect') { // Intercept kick
params.message = 'Intercepted' // Change kick message to "Intercepted" params.message = 'Intercepted' // Change kick message to "Intercepted"
} }
}) })
// Client is sending a message to the server // Client is sending a message to the server
player.on('serverbound', ({ name, params }) => { player.on('serverbound', ({ name, params }, des) => {
if (name === 'text') { // Intercept chat message to server and append time. if (name === 'text') { // Intercept chat message to server and append time.
params.message += `, on ${new Date().toLocaleString()}` params.message += `, on ${new Date().toLocaleString()}`
} }
if (name === 'command_request') { // Intercept command request to server and cancel if its "/test"
if (params.command == "/test") {
des.canceled = true
}
}
}) })
}) })
``` ```

View file

@ -110,37 +110,39 @@ The above roughly translates to the following JavaScript code to read a packet:
```js ```js
function read_position(stream) { function read_position(stream) {
const ret = {} const ret = {}
ret.x = stream.readSignedInt32LE() ret.x = stream.readLI32()
ret.z = stream.readUnsignedInt32LE() ret.z = stream.readLU32()
ret.y = stream.readFloat32LE() ret.y = stream.readLF32()
return ret return ret
} }
function read_player_position(stream) { function read_player_position(stream) {
const ret = {} const ret = {}
ret.on_ground = Boolean(stream.readU8()) ret.on_ground = Boolean(stream.readU8())
ret.position = read_player_position(stream) ret.position = read_position(stream)
let __movement_reason = stream.readU8() let __movement_reason = stream.readU8()
let movement_reason = { 0: 'player_jump', 1: 'player_autojump', 2: 'player_sneak', 3: 'player_sprint', 4: 'player_fall' }[__movement_reason] let movement_reason = { 0: 'player_jump', 1: 'player_autojump', 2: 'player_sneak', 3: 'player_sprint', 4: 'player_fall' }[__movement_reason]
switch (movement_reason) { switch (movement_reason) {
case 'player_jump': case 'player_jump':
case 'player_autojump': case 'player_autojump':
ret.original_position = read_player_position(stream) ret.original_position = read_position(stream)
ret.jump_tick = stream.readInt64LE(stream) ret.jump_tick = stream.readLI64()
break break
case 'player_fall': case 'player_fall':
ret.original_position = read_player_position(stream) ret.original_position = read_position(stream)
break break
default: break default: break
} }
ret.player_hunger = undefined ret.player_hunger = undefined
if (movement_reason == 'player_sprint') ret.player_hunger = stream.readU8() if (movement_reason == 'player_sprint') ret.player_hunger = stream.readU8()
ret.last_positions = [] ret.last_positions = []
for (let i = 0; i < stream.readUnsignedVarInt(); i++) { let __latest_positions_len = stream.readUnsignedVarInt()
for (let i = 0; i < __latest_positions_len; i++) {
ret.last_positions.push(read_player_position(stream)) ret.last_positions.push(read_player_position(stream))
} }
ret.keys_down = [] ret.keys_down = []
for (let i = 0; i < stream.readZigZagVarInt(); i++) { let __keys_down_len = stream.readZigZagVarInt()
for (let i = 0; i < __keys_down_len; i++) {
const ret1 = {} const ret1 = {}
ret1.up = Boolean(stream.readU8()) ret1.up = Boolean(stream.readU8())
ret1.down = Boolean(stream.readU8()) ret1.down = Boolean(stream.readU8())

View file

@ -20,6 +20,23 @@ Get-AppxPackage -AllUsers | Where Name -Match ".*Minecraft.*" | Select Name,Inst
Use the PackageFullName field in place of the `Microsoft.MinecraftUWP_8wekyb3d8bbwe` for the command above. Use the PackageFullName field in place of the `Microsoft.MinecraftUWP_8wekyb3d8bbwe` for the command above.
## Replit
Replit may [not support](https://github.com/PrismarineJS/bedrock-protocol/issues/363) the necessary outbound UDP connections required to connect to a Minecraft server. For further assistance using Replit, please contact Replit support or consider using an alternative hosting service if hosting locally is not possible.
Some alternatives:
* [Gitpod](https://www.gitpod.io/)
* Gitpod is a cloud development environment for teams to efficiently and securely develop software, right from your browser.
* [Github Codespaces](https://github.com/features/codespaces)
* A Codespace is a developer environment like Gitpod that's hosted in the cloud, accessed in your browser.
* [Google Colab](https://colab.research.google.com/)
* Google Colab is a Jupyter notebook environment. Jupyter notebook offer a Python environment where you can write, explain, visualize and execute code straight from a web-based developer environment. For more information on using Colab for JavaScript projects, see [Mineflayer on Google Colab](https://colab.research.google.com/github/PrismarineJS/mineflayer/blob/master/docs/mineflayer.ipynb).
## Kicked during login ## Kicked during login
NOTE: If you not receiving any errors, the error probably logged in debug mode which is not enabled. To enable it, set `process.env.DEBUG = 'minecraft-protocol'` to the top of the file
Some servers can kick you if you don't set `authTitle` as explained in the README. Some servers can kick you if you don't set `authTitle` as explained in the README.
## Server clients kicked due to "jwt not active"
The system time is incorrect and needs to be corrected.

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: '', type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '',
message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}` message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}`
}) })
} }

View file

@ -3,7 +3,7 @@ const bedrock = require('bedrock-protocol')
const server = bedrock.createServer({ const server = bedrock.createServer({
host: '0.0.0.0', // optional host: '0.0.0.0', // optional
port: 19132, // optional port: 19132, // optional
version: '1.17.10', // The server version version: '1.19.80', // The server version
motd: { // The message of the day motd: { // The message of the day
motd: 'Funtime Server', motd: 'Funtime Server',
levelName: 'Wonderland' levelName: 'Wonderland'
@ -12,7 +12,7 @@ const server = bedrock.createServer({
server.on('connect', client => { server.on('connect', client => {
client.on('join', () => { // The client has joined the server. client.on('join', () => { // The client has joined the server.
const d = new Date() // Once client is in the server, send a colorful kick message const date = new Date() // Once client is in the server, send a colorful kick message
client.disconnect(`Good ${d.getHours() < 12 ? '§emorning§r' : '§3afternoon§r'} :)\n\nMy time is ${d.toLocaleString()} !`) client.disconnect(`Good ${date.getHours() < 12 ? '§emorning§r' : '§3afternoon§r'}\n\nMy time is ${date.toLocaleString()} !`)
}) })
}) })

View file

@ -53,7 +53,8 @@ async function startServer (version = '1.17.10', ok) {
must_accept: false, must_accept: false,
has_scripts: false, has_scripts: false,
behaviour_packs: [], behaviour_packs: [],
texture_packs: [] texture_packs: [],
resource_pack_links: []
}) })
// ResourcePackStack is sent by the server to send the order in which resource packs and behaviour packs // ResourcePackStack is sent by the server to send the order in which resource packs and behaviour packs

104
index.d.ts vendored
View file

@ -1,23 +1,21 @@
import EventEmitter from "events" import EventEmitter from 'events'
import { Realm } from "prismarine-realms" 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.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'
enum title { MinecraftNintendoSwitch, MinecraftJava }
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
version?: string version?: Version
// For the client, the host of the server to connect to (default: 127.0.0.1) // For the client, the host of the server to connect to (default: 127.0.0.1)
// For the server, the host to bind to (default: 0.0.0.0) // For the server, the host to bind to (default: 0.0.0.0)
host: string host: string
// The port to connect or bind to, default: 19132 // The port to connect or bind to, default: 19132
port?: number port: number
// For the client, if we should login with Microsoft/Xbox Live. // For the client, if we should login with Microsoft/Xbox Live.
// For the server, if we should verify client's authentication with Xbox Live. // For the server, if we should verify client's authentication with Xbox Live.
offline?: boolean, offline?: boolean
// Which raknet backend to use // Which raknet backend to use
raknetBackend?: 'jsp-raknet' | 'raknet-native' | 'raknet-node' raknetBackend?: 'jsp-raknet' | 'raknet-native' | 'raknet-node'
@ -31,11 +29,11 @@ declare module "bedrock-protocol" {
export interface ClientOptions extends Options { export interface ClientOptions extends Options {
// The username to connect to the server as // The username to connect to the server as
username: string, username: string
// The view distance in chunks // The view distance in chunks
viewDistance?: number, viewDistance?: number
// Specifies which game edition to sign in as. Optional, but some servers verify this. // Specifies which game edition to sign in as. Optional, but some servers verify this.
authTitle?: title | string, authTitle?: string
// How long to wait in milliseconds while trying to connect to the server. // How long to wait in milliseconds while trying to connect to the server.
connectTimeout?: number connectTimeout?: number
// whether to skip initial ping and immediately connect // whether to skip initial ping and immediately connect
@ -49,31 +47,35 @@ declare module "bedrock-protocol" {
// the path to store authentication caches, defaults to .minecraft // the path to store authentication caches, defaults to .minecraft
profilesFolder?: string | false profilesFolder?: string | false
// Called when microsoft authorization is needed when not provided it will the information log to the console instead // Called when microsoft authorization is needed when not provided it will the information log to the console instead
onMsaCode?: (data: ServerDeviceCodeResponse) => void; onMsaCode?: (data: ServerDeviceCodeResponse) => void
} }
export interface ServerOptions extends Options { export interface ServerOptions extends Options {
// The maximum number of players allowed on the server at any time. // The maximum number of players allowed on the server at any time.
maxPlayers: number maxPlayers?: number
motd: { motd?: {
// The header for the MOTD shown in the server list. // The header for the MOTD shown in the server list.
motd: string, motd: string
// The sub-header for the MOTD shown in the server list. // The sub-header for the MOTD shown in the server list.
levelName: string levelName?: string
} }
advertisementFn: () => ServerAdvertisement advertisementFn?: () => ServerAdvertisement
} }
enum ClientStatus { enum ClientStatus {
Disconected, Authenticating, Initializing, Initialized Disconnected,
Authenticating,
Initializing,
Initialized
} }
export class Connection extends EventEmitter { export class Connection extends EventEmitter {
readonly status: ClientStatus; readonly status: ClientStatus
// Check if the passed version is less than or greater than the current connected client version. // Check if the passed version is less than or greater than the current connected client version.
versionLessThan(version: string | number): boolean versionLessThan(version: string | number): boolean
versionGreaterThan(version: string | number): boolean versionGreaterThan(version: string | number): boolean
versionGreaterThanOrEqualTo(version: string | number): boolean
// Writes a Minecraft bedrock packet and sends it without queue batching // Writes a Minecraft bedrock packet and sends it without queue batching
write(name: string, params: object): void write(name: string, params: object): void
@ -100,7 +102,6 @@ declare module "bedrock-protocol" {
// # Displays "Wow this server is popular! Check back later to see if space opens up. Server Full" // # Displays "Wow this server is popular! Check back later to see if space opens up. Server Full"
| 'failed_server_full' | 'failed_server_full'
export class Client extends Connection { export class Client extends Connection {
constructor(options: Options) constructor(options: Options)
// The client's EntityID returned by the server // The client's EntityID returned by the server
@ -109,7 +110,7 @@ declare module "bedrock-protocol" {
/** /**
* Close the connection, leave the server. * Close the connection, leave the server.
*/ */
close(): void close(reason?: string): void
/** /**
* Send a disconnect packet and close the connection * Send a disconnect packet and close the connection
@ -121,6 +122,15 @@ declare module "bedrock-protocol" {
* `Player` represents a player connected to the server. * `Player` represents a player connected to the server.
*/ */
export class Player extends Connection { export class Player extends Connection {
profile?: {
xuid: string
uuid: string
name: string
}
version: string
getUserData(): object
/** /**
* Disconnects a client before it has logged in via a PlayStatus packet. * Disconnects a client before it has logged in via a PlayStatus packet.
* @param {string} playStatus * @param {string} playStatus
@ -142,43 +152,53 @@ declare module "bedrock-protocol" {
on(event: 'login', cb: () => void): any on(event: 'login', cb: () => void): any
on(event: 'join', cb: () => void): any on(event: 'join', cb: () => void): any
on(event: 'close', cb: (reason: string) => void): any on(event: 'close', cb: (reason: string) => void): any
on(event: 'packet', cb: (packet: object) => void): any
on(event: 'spawn', cb: (reason: string) => void): any
} }
export class Server extends EventEmitter { export class Server extends EventEmitter {
clients: Map<string, Player> clients: Map<string, Player>
// Connection logging function
conLog: Function conLog: Function
constructor(options: Options) constructor(options: Options)
listen(host?: string, port?: number): void
// Disconnects all currently connected clients listen(): Promise<void>
close(disconnectReason: string): void close(disconnectReason?: string): Promise<void>
on(event: 'connect', cb: (client: Player) => void): any
} }
type RelayOptions = Options & { type RelayOptions = Options & {
// Toggle packet logging. // Toggle packet logging.
logging?: boolean, logging?: boolean
// Skip authentication for connecting clients? // Skip authentication for connecting clients?
offline?: false, offline?: false
// Specifies which game edition to sign in as to the destination server. Optional, but some servers verify this. // Specifies which game edition to sign in as to the destination server. Optional, but some servers verify this.
authTitle?: title | string authTitle?: string
// Where to proxy requests to. // Where to proxy requests to.
destination: { destination: {
realms?: RealmsOptions realms?: RealmsOptions
host: string, host: string
port: number, port: number
// Skip authentication connecting to the remote server? // Skip authentication connecting to the remote server?
offline?: boolean, offline?: boolean
} }
// Whether to enable chunk caching (default: false) // Whether to enable chunk caching (default: false)
enableChunkCaching?: boolean enableChunkCaching?: boolean
// Only allow one client to connect at a time (default: false) // Only allow one client to connect at a time (default: false)
forceSinge?: boolean forceSingle?: boolean
// Do not disconnect clients on server packet parsing errors and drop the packet instead (default: false)
omitParseErrors?: boolean
// Dispatched when a new client has logged in, and we need authentication // Dispatched when a new client has logged in, and we need authentication
// tokens to join the backend server. Cached after the first login. // tokens to join the backend server. Cached after the first login.
// If this is not specified, the client will be disconnected with a login prompt. // If this is not specified, the client will be disconnected with a login prompt.
onMsaCode?(data: ServerDeviceCodeResponse, client: Client): any onMsaCode?(data: ServerDeviceCodeResponse, client: Client): any
// prismarine-auth configuration
flow?: string,
deviceType?: string
} }
export class Relay extends Server { export class Relay extends Server {
@ -192,9 +212,13 @@ declare module "bedrock-protocol" {
version: string version: string
playersOnline: number playersOnline: number
playersMax: number playersMax: number
gamemode: string
serverId: string serverId: string
levelName:string levelName: string
gamemodeId: number
portV4: number
portV6: number
constructor(obj: object, port: number, version: string)
} }
export interface RealmsOptions { export interface RealmsOptions {
@ -206,5 +230,11 @@ declare module "bedrock-protocol" {
export function createClient(options: ClientOptions): Client export function createClient(options: ClientOptions): Client
export function createServer(options: ServerOptions): Server export function createServer(options: ServerOptions): Server
export function ping({ host, port }: { host: string, port: number }): Promise<ServerAdvertisement> export function ping({
host,
port
}: {
host: string
port: number
}): Promise<ServerAdvertisement>
} }

View file

@ -1,14 +1,15 @@
{ {
"name": "bedrock-protocol", "name": "bedrock-protocol",
"version": "3.27.1", "version": "3.49.0",
"description": "Minecraft Bedrock Edition protocol library", "description": "Minecraft Bedrock Edition protocol library",
"main": "index.js", "main": "index.js",
"types": "index.d.ts",
"scripts": { "scripts": {
"build": "cd tools && node compileProtocol.js", "build": "cd tools && node compileProtocol.js",
"test": "mocha --bail --exit", "test": "mocha --retries 2 --bail --exit",
"pretest": "npm run lint", "pretest": "npm run lint",
"lint": "standard", "lint": "standard",
"vanillaServer": "node tools/startVanillaServer.js", "vanillaServer": "minecraft-bedrock-server --root tools --version",
"dumpPackets": "node tools/genPacketDumps.js", "dumpPackets": "node tools/genPacketDumps.js",
"fix": "standard --fix" "fix": "standard --fix"
}, },
@ -39,7 +40,8 @@
"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",
"mocha": "^10.0.0", "minecraft-bedrock-server": "^1.4.2",
"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,6 +26,7 @@ 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)
@ -42,6 +43,7 @@ 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)
@ -55,6 +57,19 @@ 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)
@ -120,6 +135,7 @@ 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 () {
@ -131,9 +147,18 @@ class Client extends Connection {
...this.accessToken // Mojang + Xbox JWT from auth ...this.accessToken // Mojang + Xbox JWT from auth
] ]
const encodedChain = JSON.stringify({ chain }) let encodedChain
if (this.features.newLoginIdentityFields) { // 1.21.90+
debug('Auth chain', chain) 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', { this.write('login', {
protocol_version: this.options.protocolVersion, protocol_version: this.options.protocolVersion,
@ -170,7 +195,8 @@ 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)
} }
@ -226,7 +252,9 @@ class Client extends Connection {
break break
case 'start_game': case 'start_game':
this.startGameData = pakData.params 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') { 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,17 +28,25 @@ 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)
@ -62,10 +70,18 @@ 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)
if (name === 'start_game') this.updateItemPalette(params.itemstates) this._processOutbound(name, params)
const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold) const batch = new Framer(this)
const packet = this.serializer.createPacketBuffer({ name, params }) const packet = this.serializer.createPacketBuffer({ name, params })
batch.addEncodedPacket(packet) batch.addEncodedPacket(packet)
@ -78,7 +94,7 @@ class Connection extends EventEmitter {
queue (name, params) { queue (name, params) {
this.outLog?.('Q <- ', 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 }) const packet = this.serializer.createPacketBuffer({ name, params })
if (name === 'level_chunk') { if (name === 'level_chunk') {
// Skip queue, send ASAP // Skip queue, send ASAP
@ -91,7 +107,7 @@ class Connection extends EventEmitter {
_tick () { _tick () {
if (this.sendQ.length) { if (this.sendQ.length) {
const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold) const batch = new Framer(this)
batch.addEncodedPackets(this.sendQ) batch.addEncodedPackets(this.sendQ)
this.sendQ = [] this.sendQ = []
this.sendIds = [] this.sendIds = []
@ -115,7 +131,7 @@ class Connection extends EventEmitter {
*/ */
sendBuffer (buffer, immediate = false) { sendBuffer (buffer, immediate = false) {
if (immediate) { if (immediate) {
const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold) const batch = new Framer(this)
batch.addEncodedPacket(buffer) batch.addEncodedPacket(buffer)
if (this.encryptionEnabled) { if (this.encryptionEnabled) {
this.sendEncryptedBatch(batch) this.sendEncryptedBatch(batch)
@ -149,29 +165,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 = Buffer.concat([Buffer.from([0xfe]), buf]) // add header const packet = this.batchHeader ? Buffer.concat([Buffer.from([this.batchHeader]), buf]) : buf
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 (buffer[0] === 0xfe) { // wrapper if (!this.batchHeader || buffer[0] === this.batchHeader) { // wrapper
if (this.encryptionEnabled) { if (this.encryptionEnabled) {
this.decrypt(buffer.slice(1)) this.decrypt(buffer.slice(1))
} else { } else {
const packets = Framer.decode(this.compressionAlgorithm, buffer) const packets = Framer.decode(this, 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

@ -20,8 +20,8 @@ function createClient (options) {
const adVersion = ad.version?.split('.').slice(0, 3).join('.') // Only 3 version units const adVersion = ad.version?.split('.').slice(0, 3).join('.') // Only 3 version units
client.options.version = options.version ?? (Options.Versions[adVersion] ? adVersion : Options.CURRENT_VERSION) client.options.version = options.version ?? (Options.Versions[adVersion] ? adVersion : Options.CURRENT_VERSION)
if (ad.port && options.followPort) { if (ad.portV4 && client.options.followPort) {
client.options.port = ad.port client.options.port = ad.portV4
} }
client.conLog?.(`Connecting to ${client.options.host}:${client.options.port} ${ad.motd} (${ad.levelName}), version ${ad.version} ${client.options.version !== ad.version ? ` (as ${client.options.version})` : ''}`) client.conLog?.(`Connecting to ${client.options.host}:${client.options.port} ${ad.motd} (${ad.levelName}), version ${ad.version} ${client.options.version !== ad.version ? ` (as ${client.options.version})` : ''}`)
@ -56,35 +56,41 @@ 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 }))
}) })
// Send tick sync packets every 10 ticks if (client.versionLessThanOrEqualTo('1.20.80')) {
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)
client.on('tick_sync', async packet => { let keepalive
client.emit('heartbeat', packet.response_time) client.tick = 0n
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 } = require('./varlong') const [Read, Write, SizeOf] = [{}, {}, {}]
/** /**
* UUIDs * UUIDs
@ -116,74 +116,6 @@ 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,14 +1,11 @@
/* 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/compiler-zigzag')
function readUUID (buffer, offset) { 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 { return {
value: UUID.stringify(buffer.slice(offset, 16 + offset)), value: UUID.stringify(buffer.slice(offset, 16 + offset)),
size: 16 size: 16
@ -65,7 +62,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 PartialReadError() if (offset + 1 > buffer.length) throw new Error('Reached end of buffer')
item = buffer.readUInt8(cursor) item = buffer.readUInt8(cursor)
if (item === endVal) { if (item === endVal) {
return { return {
@ -159,7 +156,5 @@ 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.zigzag32,
zigzag64: zigzag.zigzag64
} }

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

@ -1,3 +1,3 @@
module.exports = { module.exports = {
PUBLIC_KEY: 'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V' PUBLIC_KEY: 'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp'
} }

View file

@ -36,7 +36,6 @@ module.exports = (client, server, options) => {
client.createClientUserChain = (privateKey) => { client.createClientUserChain = (privateKey) => {
let payload = { let payload = {
...skinData, ...skinData,
SkinGeometryDataEngineVersion: '', // 1.17.30
ClientRandomId: Date.now(), ClientRandomId: Date.now(),
CurrentInputMode: 1, CurrentInputMode: 1,
@ -47,23 +46,30 @@ 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. // inside of PlayFab.The playfab ID is always lowercased.
PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16), // 1.16.210 PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16).toLowerCase(), // 1.16.210
SelfSignedId: nextUUID(), SelfSignedId: nextUUID(),
ServerAddress: `${options.host}:${options.port}`, ServerAddress: `${options.host}:${options.port}`,
ThirdPartyName: client.profile.name, ThirdPartyName: client.profile.name, // Gamertag
ThirdPartyNameOnly: false, ThirdPartyNameOnly: client.versionGreaterThanOrEqualTo('1.21.90') ? undefined : false,
UIProfile: 0, UIProfile: 0,
IsEditorMode: false, IsEditorMode: false,
TrustedSkin: 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,
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,16 +3,16 @@ 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.19.70' const CURRENT_VERSION = '1.21.111'
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'] 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 testedVersions = process.env.CI ? Object.keys(Versions).filter(v => !skippedVersionsOnGithubCI.includes(v)) : Object.keys(Versions)
const defaultOptions = { const defaultOptions = {
// https://minecraft.gamepedia.com/Protocol_version#Bedrock_Edition_2 // https://minecraft.wiki/w/Protocol_version#Bedrock_Edition_2
version: CURRENT_VERSION, version: CURRENT_VERSION,
// client: If we should send SetPlayerInitialized to the server after getting play_status spawn. // client: If we should send SetPlayerInitialized to the server after getting play_status spawn.
// if this is disabled, no 'spawn' event will be emitted, you should manually set // if this is disabled, no 'spawn' event will be emitted, you should manually set

View file

@ -68,7 +68,12 @@ class RakNativeClient extends EventEmitter {
done(ret.extra.toString()) done(ret.extra.toString())
} }
}) })
}, timeout, () => { throw new RakTimeout('Ping timed out') }) }, timeout, () => {
if ('REPLIT_ENVIRONMENT' in process.env) {
console.warn('A Replit environment was detected. Replit may not support the necessary outbound UDP connections required to connect to a Minecraft server. Please see https://github.com/PrismarineJS/bedrock-protocol/blob/master/docs/FAQ.md for more information.')
}
throw new RakTimeout('Ping timed out')
})
} }
connect () { connect () {

View file

@ -49,7 +49,11 @@ class RelayPlayer extends Player {
} catch (e) { } catch (e) {
this.server.deserializer.dumpFailedBuffer(packet, this.connection.address) this.server.deserializer.dumpFailedBuffer(packet, this.connection.address)
console.error(this.connection.address, e) console.error(this.connection.address, e)
this.disconnect('Server packet parse error')
if (!this.options.omitParseErrors) {
this.disconnect('Server packet parse error')
}
return return
} }
const name = des.data.name const name = des.data.name
@ -182,12 +186,15 @@ class Relay extends Server {
async openUpstreamConnection (ds, clientAddr) { async openUpstreamConnection (ds, clientAddr) {
const options = { const options = {
authTitle: this.options.authTitle, authTitle: this.options.authTitle,
flow: this.options.flow,
deviceType: this.options.deviceType,
offline: this.options.destination.offline ?? this.options.offline, offline: this.options.destination.offline ?? this.options.offline,
username: this.options.offline ? ds.profile.name : ds.profile.xuid, username: this.options.offline ? ds.profile.name : ds.profile.xuid,
version: this.options.version, version: this.options.version,
realms: this.options.destination.realms, realms: this.options.destination.realms,
host: this.options.destination.host, host: this.options.destination.host,
port: this.options.destination.port, port: this.options.destination.port,
batchingInterval: this.options.batchingInterval,
onMsaCode: (code) => { onMsaCode: (code) => {
if (this.options.onMsaCode) { if (this.options.onMsaCode) {
this.options.onMsaCode(code, ds) this.options.onMsaCode(code, ds)

View file

@ -15,6 +15,7 @@ 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)
@ -23,25 +24,41 @@ 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}`)
@ -73,12 +90,10 @@ class Server extends EventEmitter {
this.emit('connect', player) this.emit('connect', player)
} }
onCloseConnection = (inetAddr, reason) => { onCloseConnection = (conn, reason) => {
this.conLog('Connection closed: ', inetAddr?.address, reason) this.conLog('Connection closed: ', conn.address, reason)
this.clients[conn.address]?.close()
delete this.clients[inetAddr]?.connection // Prevent close loop delete this.clients[conn.address]
this.clients[inetAddr?.address ?? inetAddr]?.close()
delete this.clients[inetAddr]
this.clientCount-- this.clientCount--
} }
@ -102,8 +117,9 @@ class Server extends EventEmitter {
return this.advertisement return this.advertisement
} }
async listen (host = this.options.host, port = this.options.port) { async listen () {
this.raknet = new this.RakServer({ host, port }, this) const { host, port, maxPlayers } = this.options
this.raknet = new this.RakServer({ host, port, maxPlayers }, this)
try { try {
await this.raknet.listen() await this.raknet.listen()

View file

@ -10,6 +10,7 @@ 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
@ -23,14 +24,16 @@ 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+
} }
@ -48,6 +51,7 @@ 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) {
@ -74,11 +78,18 @@ 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 {
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) { } catch (e) {
debug(this.address, e) debug(this.address, e)
this.disconnect('Server authentication error') this.disconnect('Server authentication error')
@ -115,7 +126,8 @@ 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.
@ -152,7 +164,7 @@ class Player extends Connection {
return return
} }
this.inLog?.(des.data.name, serialize(des.data.params).slice(0, 200)) this.inLog?.(des.data.name, serialize(des.data.params))
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,7 +36,10 @@ 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 buffer = Zlib.deflateRawSync(chunk, { level: client.compressionLevel }) const compressed = 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)
@ -70,7 +73,22 @@ function createDecryptor (client, iv) {
return return
} }
const buffer = Zlib.inflateRawSync(chunk, { chunkSize: 512000 }) let buffer
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,12 +3,15 @@ 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 (compressor, compressionLevel, compressionThreshold) { constructor (client) {
// Encoding // Encoding
this.packets = [] this.packets = []
this.compressor = compressor || 'none' this.batchHeader = client.batchHeader
this.compressionLevel = compressionLevel this.compressor = client.compressionAlgorithm || 'none'
this.compressionThreshold = compressionThreshold this.compressionLevel = client.compressionLevel
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
@ -21,30 +24,46 @@ class Framer {
} }
static decompress (algorithm, buffer) { static decompress (algorithm, buffer) {
try { switch (algorithm) {
switch (algorithm) { case 0:
case 'deflate': return zlib.inflateRawSync(buffer, { chunkSize: 512000 }) case 'deflate':
case 'snappy': throw Error('Snappy compression not implemented') return zlib.inflateRawSync(buffer, { chunkSize: 512000 })
case 'none': return buffer case 1:
default: throw Error('Unknown compression type ' + this.compressor) case 'snappy':
} throw Error('Snappy compression not implemented')
} catch { case 'none':
return buffer case 255:
return buffer
default: throw Error('Unknown compression type ' + algorithm)
} }
} }
static decode (compressor, buf) { static decode (client, buf) {
// Read header // 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) const buffer = buf.slice(1)
const decompressed = this.decompress(compressor, buffer) // Decompress
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 compressed = (buf.length > this.compressionThreshold) ? this.compress(buf) : buf const shouldCompress = buf.length > this.compressionThreshold
return Buffer.concat([Buffer.from([0xfe]), compressed]) 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) { addEncodedPacket (chunk) {

View file

@ -37,7 +37,6 @@ 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/compiler-zigzag'))
const compiledProto = compiler.compileProtoDefSync() const compiledProto = compiler.compileProtoDefSync()
return compiledProto return compiledProto
@ -47,7 +46,6 @@ 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/compiler-zigzag'))
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,9 @@ async function startTest (version = CURRENT_VERSION, ok) {
must_accept: false, must_accept: false,
has_scripts: false, has_scripts: false,
behaviour_packs: [], behaviour_packs: [],
texture_packs: [] world_template: { uuid: '550e8400-e29b-41d4-a716-446655440000', version: '' }, // 1.21.50
texture_packs: [],
resource_pack_links: []
}) })
client.once('resource_pack_client_response', async rp => { client.once('resource_pack_client_response', async rp => {
@ -55,13 +57,17 @@ 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'))
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_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 })
@ -95,11 +101,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 })
}, 9500) }, 6500)
setTimeout(() => { setTimeout(() => {
client.write('play_status', { status: 'player_spawn' }) client.write('play_status', { status: 'player_spawn' })
}, 6000) }, 3000)
// Respond to tick synchronization packets // Respond to tick synchronization packets
client.on('tick_sync', (packet) => { client.on('tick_sync', (packet) => {

View file

@ -3,6 +3,7 @@
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 !')
}, 1000) // allow some time for client to connect }, 500) // 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, 1000)) await new Promise(resolve => setTimeout(resolve, 500))
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, 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 }) 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(500).then(res) sleep(200).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(1000) await sleep(100)
console.debug('Done', version) console.debug('Done', version)
}) })
} }

View file

@ -5,7 +5,12 @@ 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(() => 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

@ -5,11 +5,12 @@ 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 = await getPort() const [port, v6] = [await getPort(), await getPort()]
const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port }) 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') console.log('Started server')
const client = new Client({ const client = new Client({
@ -48,10 +49,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,7 +15,6 @@ 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/compiler-zigzag'))
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 =>'))
@ -39,7 +38,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('latest') convert('bedrock', 'latest')
for (const version of versions) { for (const version of versions) {
main(version) main(version)
} }

View file

@ -10,9 +10,11 @@ const { getPort } = require('../test/util')
function hasDumps (version) { function hasDumps (version) {
const root = join(__dirname, `../data/${version}/sample/packets/`) const root = join(__dirname, `../data/${version}/sample/packets/`)
if (!fs.existsSync(root) || getFiles(root).length < 10) { if (!fs.existsSync(root) || getFiles(root).length < 10) {
return false return false
} }
return true return true
} }
@ -22,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) 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 }) 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)
@ -30,16 +32,15 @@ async function dump (version, force = true) {
host: '127.0.0.1', host: '127.0.0.1',
port, port,
version, version,
username: 'Boat' + random, username: 'Packet' + random,
offline: true offline: true
}) })
client.connect() client.connect()
return waitFor(async res => { return waitFor(async res => {
const root = join(__dirname, `../data/${client.options.version}/sample/`) const root = join(__dirname, `../data/${client.options.version}/sample/`)
if (!fs.existsSync(root + 'packets') || !fs.existsSync(root + 'chunks')) {
fs.mkdirSync(root + 'packets', { recursive: true }) fs.mkdirSync(root + 'packets', { recursive: true })
fs.mkdirSync(root + 'chunks', { recursive: true }) fs.mkdirSync(root + 'chunks', { recursive: true })
}
client.once('resource_packs_info', (packet) => { client.once('resource_packs_info', (packet) => {
client.write('resource_pack_client_response', { client.write('resource_pack_client_response', {
@ -56,9 +57,9 @@ async function dump (version, force = true) {
client.queue('client_cache_status', { enabled: false }) client.queue('client_cache_status', { enabled: false })
client.queue('request_chunk_radius', { chunk_radius: 1 }) client.queue('request_chunk_radius', { chunk_radius: 1 })
// client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
clearInterval(loop) clearInterval(loop)
loop = setInterval(() => { loop = setInterval(() => {
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)
@ -68,10 +69,12 @@ async function dump (version, force = true) {
client.on('packet', async packet => { // Packet dumping client.on('packet', async packet => { // Packet dumping
const { name, params } = packet.data const { name, params } = packet.data
if (name === 'level_chunk') { if (name === 'level_chunk') {
fs.writeFileSync(root + `chunks/${name}-${i++}.bin`, packet.buffer) fs.writeFileSync(root + `chunks/${name}-${i++}.bin`, packet.buffer)
return return
} }
try { try {
if (!fs.existsSync(root + `packets/${name}.json`) || force) { if (!fs.existsSync(root + `packets/${name}.json`) || force) {
fs.writeFileSync(root + `packets/${name}.json`, serialize(params, 2)) fs.writeFileSync(root + `packets/${name}.json`, serialize(params, 2))
@ -83,6 +86,7 @@ async function dump (version, force = true) {
client.on('spawn', () => { client.on('spawn', () => {
console.log('Spawned!') console.log('Spawned!')
clearInterval(loop) clearInterval(loop)
client.close() client.close()
handle.kill() handle.kill()
@ -91,7 +95,7 @@ async function dump (version, force = true) {
}, 1000 * 60, () => { }, 1000 * 60, () => {
clearInterval(loop) clearInterval(loop)
handle.kill() handle.kill()
throw Error('timed out') throw Error('Timed out')
}) })
} }

8
tools/installPMMP.sh Normal file
View file

@ -0,0 +1,8 @@
rm -fr pmmp
mkdir pmmp && cd pmmp
wget https://github.com/pmmp/PHP-Binaries/releases/download/php-8.1-latest/PHP-Linux-x86_64-PM5.tar.gz
tar -xvf PHP-Linux-x86_64-PM5.tar.gz bin/
git clone https://github.com/pmmp/PocketMine-MP.git
cd PocketMine-MP
../bin/php7/bin/php /usr/bin/composer install
../bin/php7/bin/php src/PocketMine.php --no-wizard --xbox-auth=0 --settings.enable-dev-builds=1 --anonymous-statistics.enabled=0 --disable-readline --debug.level=2

View file

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