Compare commits
479 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa6acab0f3 |
||
|
|
b48518a6e7 |
||
|
|
407756b938 |
||
|
|
0b9c49fedc |
||
|
|
6b1474d2c6 |
||
|
|
6c659feb5d |
||
|
|
06fb3de3a0 |
||
|
|
6f06a8996e |
||
|
|
14daa2d95a |
||
|
|
80751d58a7 |
||
|
|
c66cdd3d62 |
||
|
|
e503c47c79 |
||
|
|
29ba39343a |
||
|
|
e71fd513dd |
||
|
|
b6b0bcdd70 |
||
|
|
d88309507d |
||
|
|
065f41db8c |
||
|
|
9f11f21991 |
||
|
|
47f342ca95 |
||
|
|
328785d8af |
||
|
|
d4a9faf153 |
||
|
|
eeb5e47e35 |
||
|
|
2d7d32dfd1 |
||
|
|
0ed8e32be8 |
||
|
|
75fa085e86 |
||
|
|
8e728255c7 |
||
|
|
cd9a31a435 |
||
|
|
38dc5a2561 |
||
|
|
a79be99c0f |
||
|
|
a37bc5054e |
||
|
|
e7bb990aea | ||
|
|
dfcf5b2f1b | ||
|
|
aa3dafa5f4 |
||
|
|
24d3200181 |
||
|
|
347e303ce4 |
||
|
|
f48b50849a |
||
|
|
1c0836bff0 |
||
|
|
b4adf04845 |
||
|
|
52358d7277 |
||
|
|
29c7b92d96 |
||
|
|
911e0e890f |
||
|
|
fa730786d5 |
||
|
|
dd5c4de4f2 |
||
|
|
a4023b987b |
||
|
|
cd937a45d0 |
||
|
|
391ea9814c |
||
|
|
fc30c96135 |
||
|
|
cb530c8b45 |
||
|
|
418efa1da6 |
||
|
|
3be55777fa |
||
|
|
4c3f62567e |
||
|
|
a797201ecb |
||
|
|
5d3986924d |
||
|
|
c4593aa355 |
||
|
|
84c5231b92 |
||
|
|
1fee54f517 |
||
|
|
5b2d78792c |
||
|
|
19ade45ea5 |
||
|
|
16e15d80a5 |
||
|
|
1ddc525169 |
||
|
|
a4346f9638 |
||
|
|
bd32aa8d04 |
||
|
|
c65fea2916 |
||
|
|
3e5158bebe |
||
|
|
a7ae528c16 |
||
|
|
184339b9d4 |
||
|
|
d8e707112a |
||
|
|
5ee5744ec9 |
||
|
|
914ed6840b |
||
|
|
ab93d0d082 |
||
|
|
0132302e53 |
||
|
|
c278a03f95 |
||
|
|
d3161badc6 |
||
|
|
be6f0cde9f |
||
|
|
842e66266f |
||
|
|
54840f818c |
||
|
|
7b74cbf712 |
||
|
|
fd165b8908 |
||
|
|
d53211c6a1 |
||
|
|
2ecf01d63e |
||
|
|
d66dc1b237 |
||
|
|
63eb673c1f |
||
|
|
50911d2463 |
||
|
|
689658c4ab |
||
|
|
755ba31ea7 |
||
|
|
22502b90fd |
||
|
|
f92db61c89 |
||
|
|
8f3b6c5aec |
||
|
|
4c7bb65ad0 |
||
|
|
f0f1351d40 |
||
|
|
2c00402a9e |
||
|
|
50cd489f6e |
||
|
|
1414420574 |
||
|
|
010d57e78a |
||
|
|
d063a6427a |
||
|
|
46f11204ba |
||
|
|
fe3c3b01fc |
||
|
|
b2c141c25f |
||
|
|
aa0b5743b5 |
||
|
|
d2c4430833 |
||
|
|
d31b2b2aec |
||
|
|
0b7f3e037d |
||
|
|
6d708339aa |
||
|
|
97b8140fdd |
||
|
|
79a6391709 |
||
|
|
cd0ddc16e1 |
||
|
|
2cea7d6912 |
||
|
|
3b59446780 |
||
|
|
17d5b15bac |
||
|
|
32e0c66997 |
||
|
|
8507286895 |
||
|
|
a8f1349c83 |
||
|
|
d96f1a1a69 |
||
|
|
d4226e0ffd |
||
|
|
55cac72af1 |
||
|
|
3c23add812 |
||
|
|
9cda78f9b5 |
||
|
|
92b5e42bb5 |
||
|
|
e5b8cc8bde |
||
|
|
27e85867a6 |
||
|
|
830a58144a |
||
|
|
75c1447bb6 |
||
|
|
7a365aa764 |
||
|
|
9848398714 |
||
|
|
7a05b0a33e |
||
|
|
d631ed827f |
||
|
|
3fd70f97c9 |
||
|
|
c1f008a3c9 |
||
|
|
ff658b2234 |
||
|
|
1a37b4dac7 |
||
|
|
2c78372933 |
||
|
|
359a008b0c |
||
|
|
fada3aa12a |
||
|
|
5e395c5aa4 |
||
|
|
beca68ee3e |
||
|
|
d06386e0f3 |
||
|
|
d7345ed732 |
||
|
|
8c9e776a1a |
||
|
|
74f2efa7a4 |
||
|
|
d66094a4fd |
||
|
|
6737ab09d0 | ||
|
|
b930840bec |
||
|
|
a326d42a10 |
||
|
|
e7f8357a36 |
||
|
|
58724960ba |
||
|
|
20da9446f0 |
||
|
|
616c587f14 |
||
|
|
c362968c5b |
||
|
|
9674fc0126 |
||
|
|
169e305c42 |
||
|
|
9724ff41ed |
||
|
|
c51b55d4c7 |
||
|
|
3b51ebb6ee |
||
|
|
09a17c4ffb |
||
|
|
131733fb5a |
||
|
|
3295c0ddd9 |
||
|
|
7c716b9232 |
||
|
|
72dd749717 |
||
|
|
dbf15c4feb |
||
|
|
f88c8d0bc4 |
||
|
|
30c583fcf3 |
||
|
|
99153c2de2 |
||
|
|
d3a085a260 |
||
|
|
bf2de1dcf4 |
||
|
|
9d717744b6 |
||
|
|
ffe0fb42b5 |
||
|
|
8913d48f42 |
||
|
|
7264cbe312 |
||
|
|
262a4eee00 |
||
|
|
f5c91ba1c9 |
||
|
|
c395f0b05b |
||
|
|
ce06762dce |
||
|
|
3a0071f0ef |
||
|
|
e10c6133ec |
||
|
|
0c77d8e252 |
||
|
|
d1ba619788 |
||
|
|
b36c55e112 |
||
|
|
d221da7613 |
||
|
|
370ca96f2e |
||
|
|
1542ab63d1 |
||
|
|
87a958e4ab | ||
|
|
7263714562 |
||
|
|
149b4fe182 |
||
|
|
bdaf21b8ed |
||
|
|
19bc2519e7 |
||
|
|
6e73a75138 |
||
|
|
c7b825820e |
||
|
|
9fac48e948 |
||
|
|
b6d0ab3765 |
||
|
|
aacd4b4256 |
||
|
|
df31b5d63a |
||
|
|
2d7c210acc |
||
|
|
f7c556ec94 |
||
|
|
f4bd49dca0 |
||
|
|
63d11c24bb |
||
|
|
fbe7ff79e8 |
||
|
|
52156a0024 |
||
|
|
f2d39a071b |
||
|
|
20c53c49d7 |
||
|
|
dc3fb5629e |
||
|
|
930d90ea30 |
||
|
|
5f90777bd7 |
||
|
|
9a4c7f03ea |
||
|
|
cde600d51e |
||
|
|
dfff13867d |
||
|
|
0dc586db9c |
||
|
|
72c07bb7a0 |
||
|
|
92785a1167 |
||
|
|
550b1a19b4 |
||
|
|
817fd918ce |
||
|
|
330c819e7f |
||
|
|
957c83995a |
||
|
|
14af5fe04f |
||
|
|
2aa3b9826a |
||
|
|
eb5ebc6650 |
||
|
|
058c280b66 | ||
|
|
abc1444929 | ||
|
|
a1698d712f | ||
|
|
49fd2b69ee | ||
|
|
7acacc1a13 |
||
|
|
bb2179a8cb |
||
|
|
e3e553bf91 |
||
|
|
572b83047c | ||
|
|
1a1fa618e4 |
||
|
|
b0856e0cc7 |
||
|
|
1b422ac4ce |
||
|
|
214c34c44b |
||
|
|
389a68b98f |
||
|
|
75bf22d619 |
||
|
|
8569b9823f |
||
|
|
adfa248e2d |
||
|
|
ac2e9852be |
||
|
|
10feeea4ca |
||
|
|
72a4743993 |
||
|
|
f8ea6c01f4 |
||
|
|
1f7e94e5db |
||
|
|
2056b7e001 |
||
|
|
c5858d5add |
||
|
|
3267bbfca1 |
||
|
|
2d42caafb9 |
||
|
|
2f941335f6 |
||
|
|
72850b7f60 |
||
|
|
b3edee0b4a |
||
|
|
37244db6dc |
||
|
|
8845621e41 |
||
|
|
254dbefcd4 |
||
|
|
a1fe4802e6 | ||
|
|
0437e83fe9 |
||
|
|
3b96765f45 |
||
|
|
0ab783e1e6 |
||
|
|
f530677245 |
||
|
|
987bf43987 |
||
|
|
1cdb0e4c55 | ||
|
|
b546cda899 | ||
|
|
3f5c4ad12e | ||
|
|
565ae4583d | ||
|
|
90d12fa696 |
||
|
|
6a03c9813c | ||
|
|
5f0adfab76 |
||
|
|
d02eb6c041 |
||
|
|
e16e8381e5 |
||
|
|
9aba6b436d |
||
|
|
9113b0967e |
||
|
|
700ee08e11 | ||
|
|
187e74b823 | ||
|
|
69b7c5c9f4 | ||
|
|
ab70115bd8 | ||
|
|
6da8a9b2fc | ||
|
|
0f0d064b13 | ||
|
|
01575ecd6f | ||
|
|
d981515a75 | ||
|
|
e9098aa744 | ||
|
|
5af828bcb7 | ||
|
|
6a7ad6ab98 | ||
|
|
025c2cfac6 | ||
|
|
9cb4a888f4 |
||
|
|
f0fbf4f859 |
||
|
|
76febb29f1 |
||
|
|
f644595b4b |
||
|
|
879a4c21ba | ||
|
|
7d532e8c7e | ||
|
|
b8f6ab4ed3 | ||
|
|
3a4335a2ae |
||
|
|
bb88115e5e |
||
|
|
8de044aafa | ||
|
|
a8188c07ef | ||
|
|
ab8b6ddc0c |
||
|
|
c9c162bc16 | ||
|
|
faf7373dfd | ||
|
|
cbfd9eac1d |
||
|
|
cf006ab12d |
||
|
|
39659cf48b |
||
|
|
7c6439b301 |
||
|
|
b60fd53ad5 |
||
|
|
5ea8056e04 | ||
|
|
1538c3fc03 | ||
|
|
7263791581 | ||
|
|
999bfd3569 |
||
|
|
d3723ef42a |
||
|
|
d8ff48258c |
||
|
|
0bf6b8bded |
||
|
|
8663247a2a | ||
|
|
aec81efba8 | ||
|
|
fc39d69798 | ||
|
|
21a0dcd393 |
||
|
|
5499f956b9 |
||
|
|
880b052093 |
||
|
|
ed2dbcc568 | ||
|
|
a9ad14008e | ||
|
|
41b9f7b383 | ||
|
|
2aade94033 | ||
|
|
01f19164ba | ||
|
|
7a830317ea | ||
|
|
f4c3d900f1 | ||
|
|
32d52e9878 | ||
|
|
d2c0d3c386 | ||
|
|
88f88559af | ||
|
|
b79e4a65e4 | ||
|
|
f5321dd277 | ||
|
|
a55eaddc98 |
||
|
|
47c76ee597 | ||
|
|
3369fc2790 | ||
|
|
0bdd071876 | ||
|
|
be98fc6cf8 | ||
|
|
43ef9c9430 | ||
|
|
3f5b82f0f4 | ||
|
|
bb9b94fa02 | ||
|
|
e22dfea599 | ||
|
|
cf6471f6eb | ||
|
|
184de537f5 | ||
|
|
b079ebc676 | ||
|
|
edcc370426 |
||
|
|
6803d742e6 | ||
|
|
b569cb6353 | ||
|
|
597453100f | ||
|
|
514a84fd19 | ||
|
|
6b7138695e | ||
|
|
37f7d3de99 | ||
|
|
9c36b71bf6 | ||
|
|
e6cc00ef16 |
||
|
|
458136d877 |
||
|
|
bd97a8e1b7 |
||
|
|
99cce3b371 |
||
|
|
58e011e06d |
||
|
|
ff787ba8ad |
||
|
|
66ba0c8a5c |
||
|
|
1c582acdb5 |
||
|
|
df8612e355 |
||
|
|
3ffcf841ea |
||
|
|
2905d39dd4 | ||
|
|
8a6158bc2d |
||
|
|
667030370e | ||
|
|
9e1500ec77 | ||
|
|
86dcbc1f49 | ||
|
|
f00357eca6 | ||
|
|
863d51c56a | ||
|
|
b64a22a8fc | ||
|
|
c7823ab71a | ||
|
|
9bd8f4c2bb | ||
|
|
93ab3064ab | ||
|
|
4035295cdd | ||
|
|
f3604fa9b5 | ||
|
|
57290cd180 | ||
|
|
8c40910809 | ||
|
|
0ed5a8d061 | ||
|
|
8db7305eed | ||
|
|
fe05a3d645 | ||
|
|
4e3c5f8e31 | ||
|
|
b6f8fff01e | ||
|
|
7b00432c7b | ||
|
|
38840e2c15 | ||
|
|
b2a8d6fa2d | ||
|
|
233f0d33a6 | ||
|
|
49cff40097 | ||
|
|
60e57fad44 | ||
|
|
1a7205b3d1 | ||
|
|
51214fcf29 | ||
|
|
da85b80c59 | ||
|
|
73d4dd1325 | ||
|
|
9c171aa0da | ||
|
|
51c1942153 | ||
|
|
d92a38d3cd | ||
|
|
040887e9df | ||
|
|
d578eae98f | ||
|
|
902a57c722 | ||
|
|
fef514b84d |
||
|
|
db08f34619 |
||
|
|
93b4da32da |
||
|
|
595cfc29d4 | ||
|
|
2fa2d2b929 |
||
|
|
c75a661a48 | ||
|
|
06c77c3668 | ||
|
|
b7e39dec84 | ||
|
|
9c4dec74ca |
||
|
|
7038f7a0f0 |
||
|
|
894f2c949e | ||
|
|
bb13b9c719 | ||
|
|
8d735acbf9 | ||
|
|
7c17673f3f | ||
|
|
acafc8df1b | ||
|
|
d61ddd7d20 | ||
|
|
a244ed62df | ||
|
|
166960e257 | ||
|
|
a187f0a9be | ||
|
|
c3671edc01 | ||
|
|
dd43932720 | ||
|
|
5c6042b61f | ||
|
|
239acc73b4 | ||
|
|
e48f146528 | ||
|
|
ae22c37c20 | ||
|
|
82720980f8 | ||
|
|
24ef9c5da1 | ||
|
|
1dae81fcb2 | ||
|
|
151be30ca5 | ||
|
|
0c1bf07d0c | ||
|
|
0ed2e7b61c | ||
|
|
74a3e6db45 | ||
|
|
f432468de3 | ||
|
|
d478b66324 | ||
|
|
710f4372e6 | ||
|
|
0094941fd5 | ||
|
|
0af8627d23 | ||
|
|
559942de75 | ||
|
|
6332fe5072 | ||
|
|
332d0a5e49 | ||
|
|
2737203101 | ||
|
|
8e920e8399 | ||
|
|
7739d5c810 | ||
|
|
89a63bb137 | ||
|
|
a1b4c6b02b | ||
|
|
ed6870f8d5 | ||
|
|
44b262294d | ||
|
|
d910a96ee3 | ||
|
|
c7d9191420 | ||
|
|
5f42967845 | ||
|
|
b4ed008d64 | ||
|
|
5b1f1ab162 | ||
|
|
82fba41ae1 | ||
|
|
947a5e0872 | ||
|
|
78cfd29aab | ||
|
|
d8ee61b7db | ||
|
|
315e32d634 | ||
|
|
d040667bc0 |
||
|
|
66111dad54 |
||
|
|
2e1e3f8bb2 |
||
|
|
2c6ee76152 |
||
|
|
b86eb6a80f |
||
|
|
6478e625ec |
||
|
|
7bf93ae68b |
||
|
|
549f8dc084 |
||
|
|
3dfc5134b6 |
||
|
|
7cf474a49f |
||
|
|
d22a813a23 |
||
|
|
709a456ecd | ||
|
|
26dca46471 | ||
|
|
54c371dd62 | ||
|
|
1dc00237fa | ||
|
|
8c06ca5a38 |
||
|
|
6783fa43cf | ||
|
|
97c8b7d0aa | ||
|
|
8c30673c6c | ||
|
|
f07c69176e | ||
|
|
712d44bcad | ||
|
|
5b9ec7a1b6 | ||
|
|
39bf3d6aba | ||
|
|
de05863815 | ||
|
|
8109dbcdd8 | ||
|
|
7ea94c355d | ||
|
|
8ba1bf0da0 | ||
|
|
eb5a364274 | ||
|
|
01bf25e837 | ||
|
|
91e246cecc | ||
|
|
6fd8b5d35c | ||
|
|
19e6a1aa12 | ||
|
|
044c2204d6 | ||
|
|
3006fd44bf | ||
|
|
8a524f8856 | ||
|
|
375137b69f |
||
|
|
75d3cd4595 |
74 changed files with 5342 additions and 2286 deletions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
github: PrismarineJS
|
||||
open_collective: prismarinejs
|
||||
custom: https://rysolv.com/repos/detail/74691b23-938d-4b2f-b65a-5c47bf5b3f0f
|
||||
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: raknet-native
|
||||
versions:
|
||||
- 0.2.0
|
||||
- 1.0.0
|
||||
- dependency-name: bedrock-provider
|
||||
versions:
|
||||
- 1.0.0
|
||||
- dependency-name: mocha
|
||||
versions:
|
||||
- 8.3.1
|
||||
139
.github/helper-bot/index.js
vendored
Normal file
139
.github/helper-bot/index.js
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Automatic version update checker for Minecraft bedrock edition.
|
||||
const fs = require('fs')
|
||||
const cp = require('child_process')
|
||||
const helper = require('gh-helpers')()
|
||||
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'
|
||||
|
||||
// Relevant infomation for us is:
|
||||
// "version": "1.17.10",
|
||||
// "currentVersionReleaseDate": "2021-07-13T15:35:49Z",
|
||||
// "releaseNotes": "What's new in 1.17.10:\nVarious bug fixes",
|
||||
|
||||
function buildFirstIssue (title, result, externalPatches) {
|
||||
let commitData = ''
|
||||
let protocolVersion = '?'
|
||||
const date = new Date(result.currentVersionReleaseDate).toUTCString()
|
||||
|
||||
for (const name in externalPatches) {
|
||||
const [patches, diff] = externalPatches[name]
|
||||
commitData += '### ' + name + '\n'
|
||||
for (const [name, url] of patches) {
|
||||
commitData += `<a href="${url}">${name}</a>\n`
|
||||
}
|
||||
if (diff) commitData += `\n**[See the diff between *${result.currentVersionReleaseDate}* and now](${diff})**\n`
|
||||
else commitData += '\n(No changes so far)\n'
|
||||
}
|
||||
try { protocolVersion = getProtocolVersion() } catch (e) { console.log(e) }
|
||||
|
||||
return {
|
||||
title,
|
||||
body: `
|
||||
A new Minecraft Bedrock version is available (as of ${date}), version **${result.version}**
|
||||
|
||||
## Official Changelog
|
||||
* ${result.releaseNotes} *(via App Store)*
|
||||
* ${changelogURL}
|
||||
|
||||
## 3rd party protocol patches
|
||||
${commitData}
|
||||
|
||||
## Protocol Details
|
||||
(I will close this issue automatically if "${result.version}" is added to index.d.ts on "master" and there are no X's below)
|
||||
<table>
|
||||
<tr><td><b>Name</b></td><td>${result.version}</td>
|
||||
<tr><td><b>Protocol ID</b></td><td>${protocolVersion}</td>
|
||||
<!-- TODO ... automatically fetch server, test and grab relevant information and dump
|
||||
<tr><td><b>Partly Already Compatible</b></td><td></td>
|
||||
<tr><td><b>Protocol Dumpers Work</b></td><td></td>
|
||||
-->
|
||||
</table>
|
||||
|
||||
-----
|
||||
|
||||
🤖 I am a bot, I check for updates every 2 hours without a trigger. You can close this issue to prevent any further updates.
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
function getCommitsInRepo (repo, containing, since) {
|
||||
const endpoint = `https://api.github.com/repos/${repo}/commits`
|
||||
console.log('Getting', endpoint)
|
||||
cp.execSync(`curl -L ${endpoint} -o commits.json`, { stdio: 'inherit', shell: true })
|
||||
const commits = JSON.parse(fs.readFileSync('./commits.json', 'utf-8'))
|
||||
const relevant = []
|
||||
for (const commit of commits) {
|
||||
if (commit.commit.message.includes(containing)) {
|
||||
console.log('commit url', commit.html_url)
|
||||
relevant.push([commit.commit.message, commit.html_url])
|
||||
}
|
||||
}
|
||||
if (since) {
|
||||
cp.execSync(`curl -L ${endpoint}?since=${since} -o commits.json`, { stdio: 'inherit', shell: true })
|
||||
const commits = JSON.parse(fs.readFileSync('./commits.json', 'utf-8'))
|
||||
if (commits.length) {
|
||||
const head = commits[0].sha
|
||||
const tail = commits[commits.length - 1].sha
|
||||
return [relevant, `https://github.com/${repo}/compare/${tail}..${head}`]
|
||||
}
|
||||
}
|
||||
return [relevant]
|
||||
}
|
||||
|
||||
function getProtocolVersion () {
|
||||
if (!fs.existsSync('./ProtocolInfo.php')) cp.execSync('curl -LO https://raw.githubusercontent.com/pmmp/PocketMine-MP/stable/src/pocketmine/network/mcpe/protocol/ProtocolInfo.php', { stdio: 'inherit', shell: true })
|
||||
const currentApi = fs.readFileSync('./ProtocolInfo.php', 'utf-8')
|
||||
const [, latestProtocolVersion] = currentApi.match(/public const CURRENT_PROTOCOL = (\d+);/)
|
||||
return latestProtocolVersion
|
||||
}
|
||||
|
||||
async function fetchLatest () {
|
||||
if (!fs.existsSync('./results.json')) cp.execSync(`curl -L "${latestVesionEndpoint}" -o results.json`, { stdio: 'inherit', shell: true })
|
||||
const json = require('./results.json')
|
||||
const result = json.results[0]
|
||||
// console.log(json)
|
||||
|
||||
if (!fs.existsSync('./index.d.ts')) cp.execSync('curl -LO https://raw.githubusercontent.com/PrismarineJS/bedrock-protocol/master/index.d.ts', { stdio: 'inherit', shell: true })
|
||||
const currentApi = fs.readFileSync('./index.d.ts', 'utf-8')
|
||||
const supportedVersions = currentApi.match(/type Version = ([^\n]+)/)[1].replace(/\||'/g, ' ').split(' ').map(k => k.trim()).filter(k => k.length)
|
||||
console.log(supportedVersions)
|
||||
|
||||
let { version, currentVersionReleaseDate, releaseNotes } = result
|
||||
console.log(version, currentVersionReleaseDate, releaseNotes)
|
||||
|
||||
const title = `Support Minecraft ${result.version}`
|
||||
const issueStatus = await helper.findIssue({ titleIncludes: title }) || {}
|
||||
|
||||
if (supportedVersions.includes(version)) {
|
||||
if (issueStatus.isOpen) {
|
||||
helper.close(issueStatus.id, `Closing as ${version} is now supported`)
|
||||
}
|
||||
console.log('Latest version is supported.')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (issueStatus.isClosed) {
|
||||
// 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')
|
||||
return
|
||||
}
|
||||
|
||||
version = version.replace('.0', '')
|
||||
const issuePayload = buildFirstIssue(title, result, {
|
||||
PocketMine: getCommitsInRepo('pmmp/PocketMine-MP', version, currentVersionReleaseDate),
|
||||
gophertunnel: getCommitsInRepo('Sandertv/gophertunnel', version, currentVersionReleaseDate),
|
||||
CloudburstMC: getCommitsInRepo('CloudburstMC/Protocol', version, currentVersionReleaseDate)
|
||||
})
|
||||
|
||||
if (issueStatus.isOpen) {
|
||||
helper.updateIssue(issueStatus.id, issuePayload)
|
||||
} else {
|
||||
helper.createIssue(issuePayload)
|
||||
}
|
||||
|
||||
fs.writeFileSync('./issue.md', issuePayload.body)
|
||||
console.log('OK, wrote to ./issue.md', issuePayload)
|
||||
}
|
||||
|
||||
fetchLatest()
|
||||
36
.github/workflows/ci.yml
vendored
Normal file
36
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ '*', '!gh-pages' ]
|
||||
pull_request:
|
||||
branches: [ '*', '!gh-pages' ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
via:
|
||||
description: 'trigger origin'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
node-version: [22.x]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
# Old versions of bedrock use old libssl that Ubuntu no longer ships with; need manual install
|
||||
- name: (Linux) Install libssl 1.1
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb
|
||||
sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
22
.github/workflows/commands.yml
vendored
Normal file
22
.github/workflows/commands.yml
vendored
Normal 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
|
||||
33
.github/workflows/publish.yml
vendored
Normal file
33
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: npm-publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master # Change this to your default branch
|
||||
jobs:
|
||||
npm-publish:
|
||||
name: npm-publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 14.0.0
|
||||
- run: npm install
|
||||
- id: publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
- name: Create Release
|
||||
if: steps.publish.outputs.type != 'none'
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.publish.outputs.version }}
|
||||
release_name: Release ${{ steps.publish.outputs.version }}
|
||||
body: ${{ steps.publish.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
24
.github/workflows/update-helper.yml
vendored
Normal file
24
.github/workflows/update-helper.yml
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
name: Update Helper
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 */2 * * *"
|
||||
|
||||
jobs:
|
||||
helper:
|
||||
name: update-checker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 22.0.0
|
||||
- name: Install Github Actions helper
|
||||
run: npm i gh-helpers
|
||||
# The env vars contain the relevant trigger information, so we don't need to pass it
|
||||
- name: Runs helper
|
||||
run: cd .github/helper-bot && node index.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,2 +1,8 @@
|
|||
node_modules/
|
||||
npm-debug.log
|
||||
npm-debug.log
|
||||
package-lock.json
|
||||
__*
|
||||
# Runtime generated data
|
||||
data/
|
||||
tools/bds*
|
||||
tools/pmmp*
|
||||
4
.gitpod
Normal file
4
.gitpod
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
image:
|
||||
file: .gitpod.DockerFile
|
||||
tasks:
|
||||
- command: npm install
|
||||
8
.gitpod.DockerFile
Normal file
8
.gitpod.DockerFile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
FROM gitpod/workspace-full:latest
|
||||
|
||||
RUN bash -c ". .nvm/nvm.sh \
|
||||
&& nvm install 14 \
|
||||
&& nvm use 14 \
|
||||
&& nvm alias default 14"
|
||||
|
||||
RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix
|
||||
10
.npmignore
Normal file
10
.npmignore
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
node_modules/
|
||||
npm-debug.log
|
||||
__*
|
||||
# Runtime generated data
|
||||
data/
|
||||
tools/bds*
|
||||
# Extra data
|
||||
examples
|
||||
test
|
||||
.github
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
||||
278
HISTORY.md
278
HISTORY.md
|
|
@ -1,3 +1,279 @@
|
|||
## 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
|
||||
* Fix `raknetBackend` option not being applied correctly
|
||||
|
||||
## 3.27.0
|
||||
* Corrections to types (@stevarino)
|
||||
* Expose ServerAdvertisement class (#368) @hvlxh
|
||||
* Update mc-data links
|
||||
|
||||
## 3.26.0
|
||||
* 1.19.70 support (@CreeperG16)
|
||||
* types: add some type hints (#354) @hvlxh
|
||||
|
||||
## 3.25.0
|
||||
* 1.19.63 support (@stevarino)
|
||||
* Add close packet in server player API doc (#347) @hvlxh
|
||||
|
||||
## 3.24.0
|
||||
* 1.19.62 support (@CreeperG16)
|
||||
|
||||
## 3.23.0
|
||||
* 1.19.60 support (@CreeperG16)
|
||||
* added onMsaCode, profilesFolder to ClientOptions (@jarco-dev)
|
||||
|
||||
## 3.22.0
|
||||
* 1.19.50 support (@WillQizza)
|
||||
|
||||
## 3.21.0
|
||||
* 1.19.40 support (#314)
|
||||
* types: Fix missing field in ServerAdvertisement (#313) (@minerj101)
|
||||
|
||||
## 3.20.1
|
||||
* Fix buffer length calculation in ServerAdvertisement (#292) (thanks @KurtThiemann)
|
||||
* Handle Relay serialization errors by kicking (#290)
|
||||
|
||||
## 3.20.0
|
||||
* Preliminary 1.19.30 support, improve error handling and server pong data (#284)
|
||||
|
||||
## 3.19.0
|
||||
* Add option for port redirection, fix Realm handling (#282)
|
||||
* Add Port Redirect Functionality (#278) @stevarino
|
||||
* Add Get-AppxPackage command to FAQ.md (#276) @stevarino
|
||||
* Remove viewer example
|
||||
|
||||
## 3.18.0
|
||||
* 1.19.21 support (#266)
|
||||
|
||||
## 3.17.0
|
||||
* relay: Add multi-user login support (#258)
|
||||
* Add fields from 1.19.20 to login chain data (#259) @CleSucre
|
||||
* Fix nbt encoding size on single null tag NBT (#264)
|
||||
* test: Add -u flag unzipping vanilla server (#262)
|
||||
|
||||
## 3.16.0
|
||||
* 1.19.20 support (#251)
|
||||
* Add new raknet library option (raknet-node) (#211) @b23r0
|
||||
|
||||
## 3.15.0
|
||||
* 1.19.10 support
|
||||
* Remove Realm fetch when joining via invite (#228) @LucienHH
|
||||
* Add Realm support to Relay (#226) @ATXLtheAxolotl
|
||||
|
||||
## 3.14.0
|
||||
* 1.19 support
|
||||
* Better handle ping timeout, update documentation (#218) @stevarino
|
||||
|
||||
## 3.13.0
|
||||
* Update API documentation
|
||||
* Emit generic 'packet' event for server clients (#205) @ATXLtheAxolotl
|
||||
* Add XUID field for client offline mode client chain (#203)
|
||||
|
||||
## 3.12.0
|
||||
* 1.18.30 support
|
||||
|
||||
## 3.11.1
|
||||
* Bump minecraft-data version
|
||||
|
||||
## 3.11.0
|
||||
* Implement Realm joining (#193) @LucienHH
|
||||
* Refactor client connection sequence (#189) @extremeheat
|
||||
* Add profilesFolder to Relay (#192) @CreeperG16
|
||||
* Emit error from relay when server can't be pinged (#191)
|
||||
* Pass relay onMsaCode to client (#190) @Heath123
|
||||
* Mark raknet-native as required dependency (#188)
|
||||
* Ignore unconnected packets, remove babel (#185)
|
||||
|
||||
## 3.10.0
|
||||
* Support 1.18.11 (#179) @extremeheat
|
||||
* Switch to sync zlib with 512k chunks, adjustable compression level (#174) @extremeheat
|
||||
|
||||
## 3.9.0
|
||||
* Proxy fixes, logging and doc updates [#169](https://github.com/PrismarineJS/bedrock-protocol/pull/169)
|
||||
|
||||
## 3.8.0
|
||||
* 1.18.0 support
|
||||
|
||||
## 3.7.0
|
||||
* 1.17.40 support
|
||||
|
||||
## 3.6.0
|
||||
* 1.17.30 support
|
||||
* minecraft-data used for protocol data
|
||||
|
||||
## 3.5.1
|
||||
* Fix 1.17.10 npc packet serialization (#119)
|
||||
|
||||
## 3.5.0
|
||||
* Add 1.17.10 support [#109](https://github.com/PrismarineJS/bedrock-protocol/pull/109)
|
||||
* You can switch to the JS implementation of raknet by setting `useNativeRaknet: false` in options.
|
||||
|
||||
## 3.4.0
|
||||
* Initial 1.17 support [#99](https://github.com/PrismarineJS/bedrock-protocol/pull/99)
|
||||
* update connect version based on ping response & fix typings (u9g) [#101](https://github.com/PrismarineJS/bedrock-protocol/pull/101)
|
||||
* fix: ping types. (JammSpread) [#100](https://github.com/PrismarineJS/bedrock-protocol/pull/100)
|
||||
|
||||
## 3.3.0
|
||||
* Protocol updates for 1.16, with some minor breaking changes to protocol fields [#95](https://github.com/PrismarineJS/bedrock-protocol/pull/95)
|
||||
* Fix npm install issues
|
||||
|
||||
## 3.2.1
|
||||
* Add `authTitle` option to Relay proxy [#92](https://github.com/PrismarineJS/bedrock-protocol/pull/92)
|
||||
* Protocol, type definition fixes
|
||||
|
||||
## 3.2.0
|
||||
|
||||
* Fix empty chunks on proxy spawn [#89](https://github.com/PrismarineJS/bedrock-protocol/pull/89)
|
||||
* Send skin data to server [#88](https://github.com/PrismarineJS/bedrock-protocol/pull/88)
|
||||
* Support xbox title + live.com auth [#86](https://github.com/PrismarineJS/bedrock-protocol/pull/86)
|
||||
* Protocol updates and fixes
|
||||
* Fix third party servers, optional client encryption [#83](https://github.com/PrismarineJS/bedrock-protocol/pull/83)
|
||||
|
||||
## 3.1.0
|
||||
* Add support for 1.16
|
||||
* New docs and examples
|
||||
* Ping support
|
||||
* Add microsoft authentication
|
||||
* Codebase refactor
|
||||
|
||||
## 2.4.0
|
||||
* Update to version 1.12.0
|
||||
* Add option to provide protocol.json
|
||||
|
||||
## 2.2.3
|
||||
* fix the use item packet
|
||||
|
||||
## 2.2.2
|
||||
* fix the block update packet, for real this time
|
||||
|
||||
## 2.1.1
|
||||
* fix the block update packet
|
||||
|
||||
## 2.1.0
|
||||
* normalize names of packet fields
|
||||
* update to version 0.14.2
|
||||
|
||||
## 2.0.1
|
||||
* player list is now an array
|
||||
* reconnecting has been fixed
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* lot of raknet update that fix bugs
|
||||
|
|
@ -14,4 +290,4 @@
|
|||
|
||||
## 1.0.0
|
||||
|
||||
* first version, protocol definition is there but nothing really works
|
||||
* first version, protocol definition is there but nothing really works
|
||||
|
|
|
|||
144
README.md
144
README.md
|
|
@ -1,27 +1,139 @@
|
|||
pocket-minecraft-protocol
|
||||
=========================
|
||||
# bedrock-protocol
|
||||
[](http://npmjs.com/package/bedrock-protocol)
|
||||
[](https://github.com/PrismarineJS/bedrock-protocol/actions?query=workflow%3A%22CI%22)
|
||||
[](https://gitpod.io/#https://github.com/PrismarineJS/bedrock-protocol)
|
||||
|
||||
[](http://npmjs.com/package/pocket-minecraft-protocol)
|
||||
[](https://gitter.im/mhsjlw/pocket-minecraft-protocol?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://discord.gg/GsEFRM8)
|
||||
|
||||
Parse and serialize Minecraft PE packets.
|
||||
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)
|
||||
|
||||
## Features
|
||||
|
||||
* Supports Minecraft PE `0.14.1`
|
||||
* Pure JavaScript
|
||||
* Easily send and listen for any packet
|
||||
* RakNet support through [node-raknet](https://github.com/mhsjlw/node-raknet)
|
||||
- Supports Minecraft Bedrock version 1.16.201, 1.16.210, 1.16.220, 1.17.0, 1.17.10, 1.17.30, 1.17.40, 1.18.0, 1.18.11, 1.18.30, 1.19.1, 1.19.10, 1.19.20, 1.19.21, 1.19.30, 1.19.40, 1.19.41, 1.19.50, 1.19.60, 1.19.62, 1.19.63, 1.19.70, 1.19.80, 1.20.0, 1.20.10, 1.20.30, 1.20.40, 1.20.50, 1.20.61, 1.20.71, 1.20.80, 1.21.0, 1.21.2, 1.21.21, 1.21.30, 1.21.42, 1.21.50, 1.21.60, 1.21.70, 1.21.80, 1.21.90, 1.21.93, 1.21.100, 1.21.111
|
||||
- Parse and serialize packets as JavaScript objects
|
||||
- Automatically respond to keep-alive packets
|
||||
- [Proxy and mitm connections](docs/API.md#proxy-docs)
|
||||
- Client
|
||||
- Authentication
|
||||
- Encryption
|
||||
- [Ping a server for status](docs/API.md#beping-host-port---serveradvertisement)
|
||||
- Server
|
||||
- Autheticate clients with Xbox Live
|
||||
- Ping status
|
||||
|
||||
* Robust test coverage.
|
||||
* Easily extend with many other PrismarineJS projects, world providers, and more
|
||||
* Optimized for rapidly staying up to date with Minecraft protocol updates.
|
||||
|
||||
|
||||
Want to contribute on something important for PrismarineJS ? go to https://github.com/PrismarineJS/mineflayer/wiki/Big-Prismarine-projects
|
||||
|
||||
## Installation
|
||||
Simply run
|
||||
|
||||
npm install pocket-minecraft-protcol
|
||||
`npm install bedrock-protocol`
|
||||
|
||||
Then view our `examples` for inspiration!
|
||||
To update bedrock-protocol (or any Node.js package) and its dependencies after a previous install, you must run `npm update --depth 9999`
|
||||
|
||||
## Contributors
|
||||
This project is run by these guys:
|
||||
## Usage
|
||||
|
||||
- [mhsjlw](https://github.com/mhsjlw)
|
||||
- [rom1504](https://github.com/rom1504)
|
||||
### Client example
|
||||
|
||||
Example to connect to a server in offline mode, and relay chat messages back:
|
||||
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
host: 'localhost', // optional
|
||||
port: 19132, // optional, default 19132
|
||||
username: 'Notch', // the username you want to join as, optional if online mode
|
||||
offline: true // optional, default false. if true, do not login with Xbox Live. You will not be asked to sign-in if set to true.
|
||||
})
|
||||
|
||||
client.on('text', (packet) => { // Listen for chat messages from the server and echo them back.
|
||||
if (packet.source_name != client.username) {
|
||||
client.queue('text', {
|
||||
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()}`
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Client example joining a Realm
|
||||
|
||||
Example to connect to a Realm that the authenticating account is owner of or has been invited to:
|
||||
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
realms: {
|
||||
pickRealm: (realms) => realms[0] // Function which recieves an array of joined/owned Realms and must return a single Realm. Can be async
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Server example
|
||||
|
||||
*Can't connect locally on Windows? See the [faq](docs/FAQ.md)*
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const server = bedrock.createServer({
|
||||
host: '0.0.0.0', // optional. host to bind as.
|
||||
port: 19132, // optional
|
||||
version: '1.17.10', // optional. The server version, latest if not specified.
|
||||
})
|
||||
|
||||
server.on('connect', client => {
|
||||
client.on('join', () => { // The client has joined the server.
|
||||
const d = 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()} !`)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Ping example
|
||||
|
||||
```js
|
||||
const { ping } = require('bedrock-protocol')
|
||||
ping({ host: 'play.cubecraft.net', port: 19132 }).then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For documentation on the protocol, and packets/fields see the [protocol documentation](https://prismarinejs.github.io/minecraft-data/protocol).
|
||||
|
||||
* See [API documentation](docs/API.md)
|
||||
|
||||
* See [frequently asked questions and answers](docs/FAQ.md)
|
||||
|
||||
<!-- ## Projects Using bedrock-protocol
|
||||
|
||||
* [mineflayer](https://github.com/PrismarineJS/mineflayer/) - create bots with a stable, high level API.
|
||||
* [pakkit](https://github.com/Heath123/pakkit) To monitor your packets
|
||||
* [flying-squid](https://github.com/PrismarineJS/flying-squid/) - create minecraft bots with a stable, high level API. -->
|
||||
|
||||
## Testing
|
||||
|
||||
```npm test```
|
||||
|
||||
## Debugging
|
||||
|
||||
You can enable some protocol debugging output using `DEBUG` environment variable.
|
||||
|
||||
Through node.js, add `process.env.DEBUG = 'minecraft-protocol'` at the top of your script.
|
||||
|
||||
## Contribute
|
||||
|
||||
Please read [CONTRIBUTING.md](docs/CONTRIBUTING.md) and https://github.com/PrismarineJS/prismarine-contribute
|
||||
|
||||
## History
|
||||
|
||||
See [history](HISTORY.md)
|
||||
|
||||
<!-- ## Related
|
||||
|
||||
* [map-colors](https://github.com/AresRPG/aresrpg-map-colors) can be used to convert any image into a buffer of minecraft compatible colors -->
|
||||
|
|
|
|||
1902
data/protocol.json
1902
data/protocol.json
File diff suppressed because it is too large
Load diff
229
docs/API.md
Normal file
229
docs/API.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Documentation
|
||||
|
||||
## be.createClient(options) : Client
|
||||
|
||||
Returns a `Client` instance and connects to the server.
|
||||
|
||||
`options` is an object containing the properties :
|
||||
|
||||
| Parameter | Optionality | Description |
|
||||
| ----------- | ----------- |-|
|
||||
| host | Conditional | Not required if `realms` is set. host to connect to, for example `127.0.0.1`. |
|
||||
| port | *optional* | port to connect to, default to **19132** |
|
||||
| version | *optional* | Version to connect as. If not specified, automatically match server version. |
|
||||
| offline | *optional* | default to **false**. Set this to true to disable Microsoft/Xbox auth. |
|
||||
| username | Required | The profile name to connect to the server as. If `offline` set to true, the username that will appear on join, that would normally be the Xbox Gamer Tag. |
|
||||
| connectTimeout | *optional* | default to **9000ms**. How long to wait in milliseconds while trying to connect to server. |
|
||||
| onMsaCode | *optional* | Callback called when signing in with a microsoft account with device code auth, `data` is an object documented [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-response) |
|
||||
| profilesFolder | *optional* | Where to store cached authentication tokens. Defaults to .minecraft, or the node_modules folder if not found. |
|
||||
| skipPing | *optional* | Whether pinging the server to check its version should be skipped. |
|
||||
| followPort | *optional* | Update the options' port parameter to match the port broadcast on the server's ping data (default to true if `realms` not specified) |
|
||||
| autoInitPlayer | *optional* | default to true, If we should send SetPlayerInitialized to the server after getting play_status spawn. |
|
||||
| conLog | *optional* | Where to log connection information (server join, kick messages to). Defaults to console.log, set to `null` to not log anywhere. |
|
||||
| raknetBackend | *optional* | Specifies the raknet implementation to use. Possible options are 'raknet-native' (default, original C++ implementation), 'jsp-raknet' (JS port), and 'raknet-node' (Rust port). Please note when using the non-JS implementation you may the need approporate build tools on your system (for example a C++ or Rust compiler). |
|
||||
| compressionLevel | *optional* | What zlib compression level to use, default to **7** |
|
||||
| batchingInterval | *optional* | How frequently, in milliseconds to flush and write the packet queue (default: 20ms) |
|
||||
| realms | *optional* | An object which should contain one of the following properties: `realmId`, `realmInvite`, `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** |
|
||||
| realms.realmId | *optional* | The id of the Realm to join. |
|
||||
| realms.realmInvite | *optional* | The invite link/code of the Realm to join. |
|
||||
| realms.pickRealm | *optional* | A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. |
|
||||
|
||||
*`useNativeRaknet` is deprecated. Setting to true will use 'raknet-native' for `raknetBackend` and setting it to false will use a JavaScript implemenation (jsp-raknet)*
|
||||
|
||||
The following special events are emitted by the client on top of protocol packets:
|
||||
* 'status' - When the client's login sequence status has changed
|
||||
* 'join' - When the client has joined the server after authenticating
|
||||
* 'spawn' - When the client has spawned into the game world, as it is getting chunks
|
||||
* 'kick' - The server has kicked the client
|
||||
* 'close' - The server has closed the connection
|
||||
* 'error' - An recoverable exception has happened. Not catching will throw an exception
|
||||
* 'connect_allowed' - Emitted after the client has pinged the server and gets version information.
|
||||
* 'heartbeat' - Emitted after two successful tick_sync (keepalive) packets have been sent bidirectionally
|
||||
* 'packet' - Emitted for all packets received by client
|
||||
* 'session' - When the client has finished authenticating and connecting
|
||||
|
||||
## be.createServer(options) : Server
|
||||
|
||||
Returns a `Server` instance and starts listening for clients. All clients will be
|
||||
authenticated unless offline is set to true.
|
||||
|
||||
`options` is an object containing the properties :
|
||||
|
||||
| Parameter | Optionality | Description |
|
||||
| ----------- | ----------- |-|
|
||||
| host | **Required** | The host to bind to. use `0.0.0.0` to bind all IPv4 addresses. |
|
||||
| port | *optional* | the port to bind to, default **19132** |
|
||||
| version | *optional* | Version to run server as. Clients below this version will be kicked, clients above will still be permitted. |
|
||||
| offline | *optional* | default to **false**. Set this to true to disable Microsoft/Xbox auth enforcement. |
|
||||
| maxPlayers | *optional* | default to **3**. Set this to change the maximum number of players connected. |
|
||||
| kickTimeout | *[Future][1]* | How long to wait before kicking a unresponsive client. |
|
||||
| motd | *optional* | The "message of the day" for the server, the message shown to players in the server list. See usage below. |
|
||||
| advertisementFn | *optional* | optional. Custom function to call that should return a ServerAdvertisement, used for setting the RakNet server PONG data. Overrides `motd`. |
|
||||
| conLog | *optional* | Where to log connection information (server join, kick messages to). Default to log only in DEBUG mode. |
|
||||
| raknetBackend | *optional* | Specifies the raknet implementation to use. Possible options are 'raknet-native' (default, original C++ implementation), 'jsp-raknet' (JS port), and 'raknet-node' (Rust port). Please note when using the non-JS implementation you may the need approporate build tools on your system (for example a C++ or Rust compiler). |
|
||||
|
||||
*`useNativeRaknet` is deprecated. Setting to true will use 'raknet-native' for `raknetBackend` and setting it to false will use a JavaScript implemenation (jsp-raknet)*
|
||||
|
||||
## be.ping({ host, port }) : ServerAdvertisement
|
||||
|
||||
Ping a server and get the response. See type definitions for the structure.
|
||||
|
||||
## Server usage
|
||||
|
||||
You can create a server as such:
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const server = bedrock.createServer({
|
||||
host: '0.0.0.0', // the host to bind to, use '0.0.0.0' to bind all hosts
|
||||
port: 19132, // optional, port to bind to, default 19132
|
||||
offline: false, // default false. verify connections with XBL
|
||||
motd: {
|
||||
motd: 'Funtime Server', // Top level message shown in server list
|
||||
levelName: 'Wonderland' // Sub-level header
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Then you can listen for clients and their events:
|
||||
```js
|
||||
// The 'connect' event is emitted after a new client has started a connection with the server and is handshaking.
|
||||
// Its one paramater is the ServerPlayer class instance which handles this players' session from here on out.
|
||||
server.on('connect', (client) => {
|
||||
// 'join' is emitted after the client has authenticated & connection is now encrypted.
|
||||
client.on('join', () => {
|
||||
// Then we can continue with the server spawning sequence. See examples/serverTest.js for an example spawn sequence.
|
||||
// ...
|
||||
// Here's an example of sending a "text" packet, https://prismarinejs.github.io/minecraft-data/?v=bedrock_1.19.60&d=protocol#packet_text
|
||||
client.queue('text', { type: 'system', message: client.profile.name + ' just joined the server!' })
|
||||
})
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
Server event emissions:
|
||||
* 'connect' - emitted by `Server` after a client first joins the server. Second paramater is a `ServerPlayer` instance.
|
||||
|
||||
'error' event is emitted when a catchable exception happens with a client (for example receiving a bad encrypted packet).
|
||||
|
||||
A ServerPlayer instance also emits the following special events:
|
||||
* 'join' - the client is ready to recieve game packets after successful server-client handshake/encryption
|
||||
* 'close' - emitted when client quit the server
|
||||
* 'login' - emitted by client after the client has been authenticated by the server
|
||||
* 'spawn' - emitted after the client lets the server know that it has successfully spawned
|
||||
* 'packet' - Emitted for all packets received by client
|
||||
|
||||
## Client usage
|
||||
|
||||
You can create a client like below:
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
host: '127.0.0.1', // the host to bind to, use '0.0.0.0' to bind all hosts
|
||||
port: 19132, // optional, port to bind to, default 19132
|
||||
username: 'Notch' // Any profile name, only used internally for account caching when in online mode. In offline mode, the username to connect with.
|
||||
})
|
||||
```
|
||||
|
||||
```js
|
||||
// The 'join' event is emitted after the player has authenticated
|
||||
// and is ready to recieve chunks and start game packets
|
||||
client.on('join', client => console.log('Player has joined!'))
|
||||
|
||||
// The 'spawn' event is emitted. The chunks have been sent and all is well.
|
||||
client.on('spawn', client => console.log('Player has spawned!'))
|
||||
|
||||
// We can listen for text packets. See proto.yml for documentation.
|
||||
client.on('text', (packet) => {
|
||||
console.log('Client got text packet', packet)
|
||||
})
|
||||
|
||||
// For example, we can listen to https://prismarinejs.github.io/minecraft-data/?v=bedrock_1.19.60&d=protocol#packet_add_player
|
||||
// and send them a chat message when a player joins saying hello. Note the lack of the `packet` prefix, and that the packet
|
||||
// names and as explained in the "Protocol doc" section below, fields are all case sensitive!
|
||||
client.on('add_player', (packet) => {
|
||||
client.queue('text', {
|
||||
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '',
|
||||
message: `Hey, ${packet.username} just joined!`
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Order of client event emissions:
|
||||
* 'connect' - emitted after a client first joins the server
|
||||
* 'login' - emitted after the client has been authenticated by the server
|
||||
* 'join' - the client is ready to recieve game packets after successful server-client handshake
|
||||
* 'spawn' - emitted after the client has permission from the server to spawn
|
||||
|
||||
## Methods
|
||||
|
||||
[See the type defintions for this library for more information on methods.](../index.d.ts)
|
||||
|
||||
Both Client and ServerPlayer classes have `write(name, params)` and `queue(name, params)` methods. The former sends a packet immediately, and the latter queues them to be sent in the next packet batch. Prefer the latter for better performance and less blocking.
|
||||
|
||||
You can use `.close()` to terminate a connection, and `.disconnect(reason)` to gracefully kick a connected client.
|
||||
|
||||
### Protocol docs
|
||||
|
||||
For documentation on the protocol, and packets/fields see the [the protocol doc](https://prismarinejs.github.io/minecraft-data/?v=bedrock_1.18.0&d=protocol) (the emitted event names are the Packet types in lower case without the "packet_" prefix). More information on syntax can be found in CONTRIBUTING.md. When sending a packet, you must fill out all of the required fields.
|
||||
|
||||
### Realm docs
|
||||
|
||||
To make joining a Realm easier we've added an optional `realm` property to the client. It accepts the following options `realmId`, `realmInvite`, and `pickRealm`, supplying one of these will fetch host/port information for the specified Realm and then attempt to connect the bot.
|
||||
- `realmId` - The id of the Realm to join.
|
||||
- `realmInvite` - The invite code/link of the Realm to join.
|
||||
- `pickRealm` - A function that will be called with a list of Realms to pick from. The function should return the Realm to join.
|
||||
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
realms: {
|
||||
pickRealm: (realms) => realms[0] // Function which recieves an array of joined/owned Realms and must return a single Realm. Can be async
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### Proxy docs
|
||||
|
||||
You can create a proxy ("Relay") to create a machine-in-the-middle (MITM) connection to a server. You can observe and intercept packets as they go through. The Relay is a server+client combo with some special packet handling and forwarding that takes care of the authentication and encryption on the server side. Clients will be asked to login if `offline` is not specified on connection.
|
||||
|
||||
```js
|
||||
const { Relay } = require('bedrock-protocol')
|
||||
const relay = new Relay({
|
||||
version: '1.16.220', // The version
|
||||
/* host and port to listen for clients on */
|
||||
host: '0.0.0.0',
|
||||
port: 19132,
|
||||
/* Where to send upstream packets to */
|
||||
destination: {
|
||||
host: '127.0.0.1',
|
||||
port: 19131
|
||||
}
|
||||
})
|
||||
relay.listen() // Tell the server to start listening.
|
||||
|
||||
relay.on('connect', player => {
|
||||
console.log('New connection', player.connection.address)
|
||||
|
||||
// Server is sending a message to the client.
|
||||
player.on('clientbound', ({ name, params }, des) => {
|
||||
if (name === 'disconnect') { // Intercept kick
|
||||
params.message = 'Intercepted' // Change kick message to "Intercepted"
|
||||
}
|
||||
})
|
||||
// Client is sending a message to the server
|
||||
player.on('serverbound', ({ name, params }, des) => {
|
||||
if (name === 'text') { // Intercept chat message to server and append time.
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
'Relay' emits 'clientbound' and 'serverbound' events, along with the data for the outgoing packet that can be modified. You can send a packet to the client with `player.queue()` or to the backend server with `player.upstream.queue()`.
|
||||
3
docs/AUTHORS.md
Normal file
3
docs/AUTHORS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mhsjlw <mhsjlw@aol.com> @mhsjlw
|
||||
Romain Beaumont <romain.rom1@gmail.com> @rom1504
|
||||
Filiph Sandström <filiph.sandstrom@filfatstudios.com> @filfat
|
||||
172
docs/CONTRIBUTING.md
Normal file
172
docs/CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
CONTRIBUTING.md
|
||||
|
||||
Contributions are always welcome :). If you have any questions, please discuss on the Discord or in a Discussion.
|
||||
|
||||
## Updating
|
||||
|
||||
Good sources for the Minecraft bedrock protocol are [gophertunnel](https://github.com/Sandertv/gophertunnel/tree/master/minecraft/protocol/packet), [ClouburstMC's protocol library](https://github.com/CloudburstMC/Protocol) and [PocketMine](https://github.com/pmmp/PocketMine-MP/tree/stable/src/pocketmine/network/mcpe/protocol).
|
||||
|
||||
Protocol updates need to happen in two places: in minecraft-data to update the protocol schema (the actual data structures for the packets) and here in the protocol library side. If no changes to the underlying protocol are made aside from packet structure changes (add, remove, modify packets) then the only change needed in bedrock-protocol is to update the README documentation and some constants in `src/options.js` (update the CURRENT_VERSION).
|
||||
|
||||
Steps to update:
|
||||
* Update the protocol data in minecraft-data : see the instructions [here](https://github.com/PrismarineJS/minecraft-data/blob/master/doc/bedrock.md).
|
||||
* Find the relevant changes to the protocol for the current version
|
||||
* Update the [.YML files](https://github.com/PrismarineJS/minecraft-data/tree/master/data/bedrock/latest) in minecraft-data accordingly (see the [Packet serialization](#Packet_serialization) notes at the bottom here for info on syntax)
|
||||
* Then follow the steps to build the protocol .YML files into JSON
|
||||
* Do a release of the minecraft-data package
|
||||
* Add the version to `src/options.js` here
|
||||
* Run `npm run build` and `npm test` to test that everything is OK
|
||||
|
||||
### Development
|
||||
|
||||
For development purposes, you can easily alter the protocol locally without a remote minecraft-data release :
|
||||
* Run `npm install` on the root of this repo after git cloning
|
||||
* Open `node_modules/minecraft-data/minecraft-data/data/bedrock/latest/` and update the .YML files as you need, following the schema at the bottom (make sure to update '!version' if you are changing version)
|
||||
* Go back to the root of this repo and run `npm run build`.
|
||||
* Then `npm test` ; the protocol changes should be automatically applied
|
||||
|
||||
For example, [here](https://github.com/PrismarineJS/minecraft-data/pull/467/files) is a PR for the update to 1.17.30 in minecraft-data - [here](https://github.com/PrismarineJS/bedrock-protocol/pull/150/files) is an accompanying change for bedrock-protocol.
|
||||
|
||||
## Code structure
|
||||
|
||||
The code structure is similar to node-minecraft-protocol. For raknet, raknet-native is used for Raknet communication.
|
||||
|
||||
## Packet serialization
|
||||
|
||||
This project uses ProtoDef to serialize and deserialize Minecraft packets. See the documentation [here](https://github.com/ProtoDef-io/node-protodef).
|
||||
The ProtoDef schema is JSON can be found [here](https://github.com/PrismarineJS/bedrock-protocol/blob/4169453835790de7eeaa8fb6f5a6b4344f71036b/data/1.16.210/protocol.json) for use in other languages.
|
||||
|
||||
In bedrock-protocol, JavaScript code is generated from the JSON through the node-protodef compiler.
|
||||
|
||||
#### YAML syntax
|
||||
|
||||
For easier maintainability, the JSON is generated from a more human readable YAML format. You can read more [here](https://github.com/extremeheat/protodef-yaml).
|
||||
Some documentation is below.
|
||||
|
||||
Packets should go in proto.yml and extra types should go in types.yml.
|
||||
|
||||
```yml
|
||||
# This defines a new data structure, a ProtoDef container.
|
||||
Position:
|
||||
# Variable `x` in this struct has a type of `li32`, a little-endian 32-bit integer
|
||||
x: li32
|
||||
# `z` is a 32-bit LE *unsigned* integer
|
||||
z: lu32
|
||||
# `b` is a 32-bit LE floating point
|
||||
y: lf32
|
||||
|
||||
# Fields starting with `packet_` are structs representing Minecraft packets
|
||||
packet_player_position:
|
||||
# Fields starting with ! are ignored by the parser. '!id' is used by the parser when generating the packet map
|
||||
!id: 0x29 # This packet is ID #0x29
|
||||
!bound: client # `client` or `server` bound, just for documentation purposes. This has no other effect.
|
||||
|
||||
# Read `on_ground` as a boolean
|
||||
on_ground: bool
|
||||
# Read `position` as custom data type `Position` defined above.
|
||||
position: Position
|
||||
|
||||
# Reads a 8-bit unsigned integer, then maps it to a string
|
||||
movement_reason: u8 =>
|
||||
0: player_jump
|
||||
1: player_autojump
|
||||
2: player_sneak
|
||||
3: player_sprint
|
||||
4: player_fall
|
||||
|
||||
# A `_` as a field name declares an anonymous data structure which will be inlined. Adding a '?' at the end will start a `switch` statement
|
||||
_: movement_reason ?
|
||||
# if the condition matches to the string "player_jump" or "player_autojump", there is a data struct that needs to be read
|
||||
if player_jump or player_autojump:
|
||||
# read `original_position` as a `Position`
|
||||
original_position: Position
|
||||
jump_tick: li64
|
||||
# if the condition matches "player_fall", read the containing field
|
||||
if player_fall:
|
||||
original_position: Position
|
||||
default: void
|
||||
|
||||
# Another way to declare a switch, without an anonymous structure. `player_hunger` will be read as a 8-bit int if movement_reason == "player_sprint"
|
||||
player_hunger: movement_reason ?
|
||||
if player_sprint: u8
|
||||
# The default statement as in a switch statement
|
||||
default: void
|
||||
|
||||
# Square brackets notate an array. At the left is the type of the array values, at the right is the type of
|
||||
# the length prefix. If no type at the left is specified, the type is defined below.
|
||||
|
||||
# Reads an array of `Position`, length-prefixed with a ProtoBuf-type unsigned variable length integer (VarInt)
|
||||
last_positions: Position[]varint
|
||||
|
||||
# Reads an array, length-prefixed with a zigzag-encoded signed VarInt
|
||||
# The data structure for the array is defined underneath
|
||||
keys_down: []zigzag32
|
||||
up: bool
|
||||
down: bool
|
||||
shift: bool
|
||||
```
|
||||
|
||||
The above roughly translates to the following JavaScript code to read a packet:
|
||||
```js
|
||||
function read_position(stream) {
|
||||
const ret = {}
|
||||
ret.x = stream.readLI32()
|
||||
ret.z = stream.readLU32()
|
||||
ret.y = stream.readLF32()
|
||||
return ret
|
||||
}
|
||||
|
||||
function read_player_position(stream) {
|
||||
const ret = {}
|
||||
ret.on_ground = Boolean(stream.readU8())
|
||||
ret.position = read_position(stream)
|
||||
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]
|
||||
switch (movement_reason) {
|
||||
case 'player_jump':
|
||||
case 'player_autojump':
|
||||
ret.original_position = read_position(stream)
|
||||
ret.jump_tick = stream.readLI64()
|
||||
break
|
||||
case 'player_fall':
|
||||
ret.original_position = read_position(stream)
|
||||
break
|
||||
default: break
|
||||
}
|
||||
ret.player_hunger = undefined
|
||||
if (movement_reason == 'player_sprint') ret.player_hunger = stream.readU8()
|
||||
ret.last_positions = []
|
||||
let __latest_positions_len = stream.readUnsignedVarInt()
|
||||
for (let i = 0; i < __latest_positions_len; i++) {
|
||||
ret.last_positions.push(read_player_position(stream))
|
||||
}
|
||||
ret.keys_down = []
|
||||
let __keys_down_len = stream.readZigZagVarInt()
|
||||
for (let i = 0; i < __keys_down_len; i++) {
|
||||
const ret1 = {}
|
||||
ret1.up = Boolean(stream.readU8())
|
||||
ret1.down = Boolean(stream.readU8())
|
||||
ret1.shift = Boolean(stream.readU8())
|
||||
ret.keys_down.push(ret1)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
```
|
||||
|
||||
and the results in the following JSON for the packet:
|
||||
```json
|
||||
{
|
||||
"on_ground": false,
|
||||
"position": { "x": 0, "y": 2, "z": 0 },
|
||||
"movement_reason": "player_jump",
|
||||
"original_position": { "x": 0, "y": 0, "z": 0 },
|
||||
"jump_tick": 494894984,
|
||||
"last_positions": [{ "x": 0, "y": 1, "z": 0 }],
|
||||
"keys_down": []
|
||||
}
|
||||
```
|
||||
|
||||
Custom ProtoDef types can be inlined as JSON:
|
||||
```yml
|
||||
string: ["pstring",{"countType":"varint"}]
|
||||
```
|
||||
42
docs/FAQ.md
Normal file
42
docs/FAQ.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
## Can’t connect to localhost Win10 server with Minecraft Win10 Edition
|
||||
|
||||
This issue occurs due to loopback restrictions on Windows 10 UWP apps. To lift this restriction, launch Windows PowerShell as an administrator and run the following:
|
||||
|
||||
```ps
|
||||
CheckNetIsolation LoopbackExempt -a -n="Microsoft.MinecraftUWP_8wekyb3d8bbwe"
|
||||
```
|
||||
|
||||
If you are running a preview or beta release, you can run the following command to unlock that version:
|
||||
|
||||
```ps
|
||||
CheckNetIsolation LoopbackExempt -a -n="Microsoft.MinecraftWindowsBeta"
|
||||
```
|
||||
|
||||
If that still doesn't work, you can inspect what Minecraft versions are available on your system with:
|
||||
|
||||
```ps
|
||||
Get-AppxPackage -AllUsers | Where Name -Match ".*Minecraft.*" | Select Name,InstallLocation,PackageFullName
|
||||
```
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
## Server clients kicked due to "jwt not active"
|
||||
|
||||
The system time is incorrect and needs to be corrected.
|
||||
BIN
examples/chunk
BIN
examples/chunk
Binary file not shown.
|
|
@ -1,24 +0,0 @@
|
|||
var pmp = require('../');
|
||||
|
||||
if(process.argv.length !=5) {
|
||||
console.log("Usage: node client.js <host> <port> <username>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var client = pmp.createClient({
|
||||
host: process.argv[2],
|
||||
port: parseInt(process.argv[3]),
|
||||
username:process.argv[4]
|
||||
});
|
||||
|
||||
client.on('mcpe',packet => console.log(packet));
|
||||
|
||||
client.on('set_spawn_position',() => {
|
||||
client.writeMCPE('request_chunk_radius',{
|
||||
chunk_radius:8
|
||||
})
|
||||
});
|
||||
|
||||
client.on('error',function(err){
|
||||
console.log(err);
|
||||
});
|
||||
17
examples/client/client.js
Normal file
17
examples/client/client.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* eslint-disable */
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
host: 'localhost', // optional
|
||||
port: 19132, // optional, default 19132
|
||||
username: 'Notch', // the username you want to join as, optional if online mode
|
||||
offline: false // optional, default false. if true, do not login with Xbox Live. You will not be asked to sign-in if set to true.
|
||||
})
|
||||
|
||||
client.on('text', (packet) => { // Listen for chat messages and echo them back.
|
||||
if (packet.source_name != client.username) {
|
||||
client.queue('text', {
|
||||
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()}`
|
||||
})
|
||||
}
|
||||
})
|
||||
47
examples/client/clientInternal.js
Normal file
47
examples/client/clientInternal.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Do not use this example unless you need to change the login procedure, instead see `client.js`.
|
||||
*/
|
||||
process.env.DEBUG = 'minecraft-protocol raknet'
|
||||
const { Client } = require('bedrock-protocol')
|
||||
const ChunkColumn = require('bedrock-provider').chunk('bedrock_1.17.10')
|
||||
|
||||
async function test () {
|
||||
const client = new Client({
|
||||
host: '127.0.0.1',
|
||||
port: 19132
|
||||
// You can specify version by adding :
|
||||
// version: '1.16.210'
|
||||
})
|
||||
client.connect()
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
|
||||
client.on('level_chunk', async packet => {
|
||||
const cc = new ChunkColumn(packet.x, packet.z)
|
||||
await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count)
|
||||
const blocks = []
|
||||
for (let x = 0; x < 16; x++) {
|
||||
for (let z = 0; z < 16; z++) {
|
||||
blocks.push(cc.getBlock(x, 0, z)) // Read some blocks in this chunk
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test()
|
||||
9
examples/client/createClient.js
Normal file
9
examples/client/createClient.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const { createClient } = require('bedrock-protocol')
|
||||
|
||||
const client = createClient({ host: '127.0.0.1' })
|
||||
|
||||
let ix = 0
|
||||
client.on('packet', (args) => {
|
||||
console.log(`Packet ${ix} recieved`)
|
||||
ix++
|
||||
})
|
||||
13
examples/client/realm.js
Normal file
13
examples/client/realm.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable */
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
realms: {
|
||||
// realmId: '1234567', // Connect the client to a Realm using the Realms ID
|
||||
// realmInvite: 'https://realms.gg/AB1CD2EFA3B', // Connect the client to a Realm using the Realms invite URL or code
|
||||
pickRealm: (realms) => realms.find(e => e.name === 'Realm Name') // Connect the client to a Realm using a function that returns a Realm
|
||||
}
|
||||
})
|
||||
|
||||
client.on('text', (packet) => { // Listen for chat messages
|
||||
console.log('Received Text:', packet)
|
||||
})
|
||||
22
examples/createRelay.js
Normal file
22
examples/createRelay.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const { Relay } = require('bedrock-protocol')
|
||||
|
||||
function createRelay () {
|
||||
console.log('Creating relay')
|
||||
/* Example to create a non-transparent proxy (or 'Relay') connection to destination server */
|
||||
const relay = new Relay({
|
||||
/* host and port for clients to listen to */
|
||||
host: '0.0.0.0',
|
||||
port: 19130,
|
||||
offline: false,
|
||||
/* Where to send upstream packets to */
|
||||
destination: {
|
||||
host: '127.0.0.1',
|
||||
port: 19132,
|
||||
offline: false
|
||||
}
|
||||
})
|
||||
relay.conLog = console.debug
|
||||
relay.listen()
|
||||
}
|
||||
|
||||
createRelay()
|
||||
5
examples/ping.js
Normal file
5
examples/ping.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const { ping } = require('bedrock-protocol')
|
||||
|
||||
ping({ host: 'play.cubecraft.net', port: 19132 }).then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
29
examples/realmRelay.js
Normal file
29
examples/realmRelay.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
const { Relay } = require('bedrock-protocol')
|
||||
|
||||
function createRelay () {
|
||||
console.log('Creating relay')
|
||||
/* Example to create a non-transparent proxy (or 'Relay') connection to destination server */
|
||||
const relay = new Relay({
|
||||
/* host and port for clients to listen to */
|
||||
host: '0.0.0.0',
|
||||
port: 19130,
|
||||
offline: false,
|
||||
/* Where to send upstream packets to */
|
||||
destination: {
|
||||
realms: {
|
||||
pickRealm: (realms) => realms.find(e => e.name === 'Realm Name')
|
||||
},
|
||||
offline: false
|
||||
}
|
||||
})
|
||||
relay.conLog = console.debug
|
||||
relay.listen()
|
||||
relay.on('connect', player => {
|
||||
// Server is sending a message to the client.
|
||||
player.on('clientbound', ({ name, params }) => {
|
||||
if (name === 'text') console.log(params)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createRelay()
|
||||
37
examples/relay.js
Normal file
37
examples/relay.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
const { Relay } = require('bedrock-protocol')
|
||||
|
||||
// Start your server first on port 19131.
|
||||
|
||||
// Start the proxy server
|
||||
const relay = new Relay({
|
||||
version: '1.16.220', // The version
|
||||
/* host and port to listen for clients on */
|
||||
host: '0.0.0.0',
|
||||
port: 19132,
|
||||
/* Where to send upstream packets to */
|
||||
destination: {
|
||||
host: '127.0.0.1',
|
||||
port: 19131
|
||||
}
|
||||
})
|
||||
relay.conLog = console.debug
|
||||
relay.listen() // Tell the server to start listening.
|
||||
|
||||
relay.on('connect', player => {
|
||||
console.log('New connection', player.connection.address)
|
||||
|
||||
// Server is sending a message to the client.
|
||||
player.on('clientbound', ({ name, params }) => {
|
||||
if (name === 'disconnect') { // Intercept kick
|
||||
params.message = 'Intercepted' // Change kick message to "Intercepted"
|
||||
}
|
||||
})
|
||||
// Client is sending a message to the server
|
||||
player.on('serverbound', ({ name, params }) => {
|
||||
if (name === 'text') { // Intercept chat message to server and append time.
|
||||
params.message += `, on ${new Date().toLocaleString()}`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Now clients can connect to your proxy
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
var mcpe = require('../');
|
||||
|
||||
var parser = mcpe.createDeserializer();
|
||||
var serializer = mcpe.createSerializer();
|
||||
|
||||
serializer.write({
|
||||
name: 'player_status',
|
||||
"params": {
|
||||
status: 1
|
||||
}
|
||||
});
|
||||
|
||||
serializer.pipe(parser);
|
||||
|
||||
parser.on('error', function(err) {
|
||||
console.log(err.stack);
|
||||
})
|
||||
|
||||
parser.on('data', function(chunk) {
|
||||
console.log(JSON.stringify(chunk, null, 2));
|
||||
});
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var pmp = require('../');
|
||||
var fs = require("fs");
|
||||
|
||||
if(process.argv.length !=4) {
|
||||
console.log("Usage: node server.js <host> <port>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var server = pmp.createServer({
|
||||
host: process.argv[2],
|
||||
port: parseInt(process.argv[3]),
|
||||
name: 'MCPE;Minecraft: PE Server;45 45;0.14.1;0;20'
|
||||
});
|
||||
|
||||
server.on('connection', function(client) {
|
||||
|
||||
|
||||
client.on("mcpe",packet => console.log(packet));
|
||||
|
||||
|
||||
client.on("game_login",packet => {
|
||||
client.writeMCPE("player_status",{
|
||||
status:0
|
||||
});
|
||||
client.writeMCPE('move_player', {
|
||||
entity_id: [0,0],
|
||||
x: 1,
|
||||
y: 64 + 1.62,
|
||||
z: 1,
|
||||
yaw: 0,
|
||||
head_yaw: 0,
|
||||
pitch: 0,
|
||||
mode: 0,
|
||||
on_ground: 1
|
||||
});
|
||||
|
||||
client.writeMCPE("start_game",{
|
||||
seed:-1,
|
||||
dimension:0,
|
||||
generator:1,
|
||||
gamemode:1,
|
||||
entity_id:[0,0],
|
||||
spawn_x:1,
|
||||
spawn_y:1,
|
||||
spawn_z:1,
|
||||
x:0,
|
||||
y:1+1.62,
|
||||
z:0,
|
||||
unknown:0
|
||||
});
|
||||
|
||||
client.writeMCPE('set_spawn_position', {
|
||||
x: 1,
|
||||
y: 64,
|
||||
z: 1
|
||||
});
|
||||
client.writeMCPE("set_time",{
|
||||
time:0,
|
||||
started:1
|
||||
});
|
||||
|
||||
client.writeMCPE('respawn', {
|
||||
x: 1,
|
||||
y: 64,
|
||||
z: 1
|
||||
});
|
||||
});
|
||||
|
||||
client.on("request_chunk_radius",() => {
|
||||
client.writeMCPE('chunk_radius_update',{
|
||||
chunk_radius:1
|
||||
});
|
||||
|
||||
for (let x = -1; x <=1; x++) {
|
||||
for (let z = -1; z <=1; z++) {
|
||||
client.writeBatch([{"name":"mcpe","params":{name:"full_chunk_data",params:{
|
||||
chunk_x: x,
|
||||
chunk_z: z,
|
||||
order: 1,
|
||||
chunk_data:fs.readFileSync(__dirname+"/chunk")
|
||||
}}}]);
|
||||
}
|
||||
}
|
||||
|
||||
client.writeMCPE('player_status', {
|
||||
status: 3
|
||||
});
|
||||
|
||||
client.writeMCPE('set_time', {
|
||||
time: 0,
|
||||
started: 1
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
client.on('error', function(err) {
|
||||
console.log(err.stack);
|
||||
});
|
||||
});
|
||||
18
examples/server/basicServer.js
Normal file
18
examples/server/basicServer.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* eslint-disable */
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const server = bedrock.createServer({
|
||||
host: '0.0.0.0', // optional
|
||||
port: 19132, // optional
|
||||
version: '1.19.80', // The server version
|
||||
motd: { // The message of the day
|
||||
motd: 'Funtime Server',
|
||||
levelName: 'Wonderland'
|
||||
}
|
||||
})
|
||||
|
||||
server.on('connect', client => {
|
||||
client.on('join', () => { // The client has joined the server.
|
||||
const date = new Date() // Once client is in the server, send a colorful kick message
|
||||
client.disconnect(`Good ${date.getHours() < 12 ? '§emorning§r' : '§3afternoon§r'}\n\nMy time is ${date.toLocaleString()} !`)
|
||||
})
|
||||
})
|
||||
150
examples/server/server.js
Normal file
150
examples/server/server.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* This example spawns a client. For a basic server that disconnects users, see "basicServer.js".
|
||||
*
|
||||
* bedrock-protocol server example; to run this example you need to clone this repo from git.
|
||||
* first need to dump some packets from the vanilla server as there is alot of boilerplate
|
||||
* to send to clients. The `serverChunks.js` contains the chunk loading code.
|
||||
*
|
||||
* In your server implementation, you need to implement each of the following packets to
|
||||
* get a client to spawn like vanilla. You can look at the dumped packets in `data/1.16.10/sample`
|
||||
*
|
||||
* First, dump packets for version 1.16.210 by running `npm run dumpPackets`.
|
||||
* Then you can run `node server.js <version>` to start this script.
|
||||
*/
|
||||
process.env.DEBUG = 'minecraft-protocol' // packet logging
|
||||
// const fs = require('fs')
|
||||
const { Server } = require('bedrock-protocol')
|
||||
|
||||
const { hasDumps } = require('../../tools/genPacketDumps')
|
||||
const { waitFor } = require('../../src/datatypes/util')
|
||||
const { loadWorld } = require('./serverChunks')
|
||||
const { join } = require('path')
|
||||
|
||||
async function startServer (version = '1.17.10', ok) {
|
||||
if (!hasDumps(version)) {
|
||||
throw Error('You need to dump some packets first. Run tools/genPacketDumps.js')
|
||||
}
|
||||
|
||||
const Item = require('../../types/Item')(version)
|
||||
const port = 19132
|
||||
const server = new Server({ host: '0.0.0.0', port, version })
|
||||
let loop
|
||||
|
||||
const getPath = (packetPath) => join(__dirname, `../data/${server.options.version}/${packetPath}`)
|
||||
const get = (packetName) => require(getPath(`sample/packets/${packetName}.json`))
|
||||
|
||||
server.listen()
|
||||
console.log('Started server')
|
||||
|
||||
// Find the center position from the dumped packets
|
||||
const respawnPacket = get('respawn')
|
||||
const world = await loadWorld(version)
|
||||
const chunks = await world.requestChunks(respawnPacket.x, respawnPacket.z, 2)
|
||||
|
||||
// Connect is emitted when a client first joins our server, before authing them
|
||||
server.on('connect', client => {
|
||||
// Join is emitted after the client has been authenticated and encryption has started
|
||||
client.on('join', () => {
|
||||
console.log('Client joined', client.getUserData())
|
||||
|
||||
// ResourcePacksInfo is sent by the server to inform the client on what resource packs the server has. It
|
||||
// sends a list of the resource packs it has and basic information on them like the version and description.
|
||||
client.write('resource_packs_info', {
|
||||
must_accept: false,
|
||||
has_scripts: false,
|
||||
behaviour_packs: [],
|
||||
texture_packs: [],
|
||||
resource_pack_links: []
|
||||
})
|
||||
|
||||
// ResourcePackStack is sent by the server to send the order in which resource packs and behaviour packs
|
||||
// should be applied (and downloaded) by the client.
|
||||
client.write('resource_pack_stack', {
|
||||
must_accept: false,
|
||||
behavior_packs: [],
|
||||
resource_packs: [],
|
||||
game_version: '',
|
||||
experiments: [],
|
||||
experiments_previously_used: false
|
||||
})
|
||||
|
||||
client.once('resource_pack_client_response', async rp => {
|
||||
// Tell the server we will compress everything (>=1 byte)
|
||||
client.write('network_settings', { compression_threshold: 1 })
|
||||
// Send some inventory slots
|
||||
for (let i = 0; i < 3; i++) {
|
||||
client.queue('inventory_slot', { window_id: 120, slot: 0, item: new Item().toBedrock() })
|
||||
}
|
||||
|
||||
client.queue('player_list', get('player_list'))
|
||||
client.queue('start_game', get('start_game'))
|
||||
client.queue('item_component', { entries: [] })
|
||||
client.queue('set_spawn_position', get('set_spawn_position'))
|
||||
client.queue('set_time', { time: 5433771 })
|
||||
client.queue('set_difficulty', { difficulty: 1 })
|
||||
client.queue('set_commands_enabled', { enabled: true })
|
||||
client.queue('adventure_settings', get('adventure_settings'))
|
||||
client.queue('biome_definition_list', get('biome_definition_list'))
|
||||
client.queue('available_entity_identifiers', get('available_entity_identifiers'))
|
||||
client.queue('update_attributes', get('update_attributes'))
|
||||
client.queue('creative_content', get('creative_content'))
|
||||
client.queue('inventory_content', get('inventory_content'))
|
||||
client.queue('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true })
|
||||
client.queue('crafting_data', get('crafting_data'))
|
||||
client.queue('available_commands', get('available_commands'))
|
||||
client.queue('chunk_radius_update', { chunk_radius: 1 })
|
||||
client.queue('game_rules_changed', get('game_rules_changed'))
|
||||
client.queue('respawn', get('respawn'))
|
||||
|
||||
for (const chunk of chunks) {
|
||||
client.queue('level_chunk', chunk)
|
||||
}
|
||||
|
||||
// Uncomment below and comment above to send dumped chunks. We use bedrock-provider in this example which is still a WIP, some blocks may be broken.
|
||||
// for (const file of fs.readdirSync(`../data/${server.options.version}/sample/chunks`)) {
|
||||
// const buffer = fs.readFileSync(`../data/${server.options.version}/sample/chunks/` + file)
|
||||
// // console.log('Sending chunk', buffer)
|
||||
// client.sendBuffer(buffer)
|
||||
// }
|
||||
|
||||
// Constantly send this packet to the client to tell it the center position for chunks. The client should then request these
|
||||
// missing chunks from the us if it's missing any within the radius. `radius` is in blocks.
|
||||
loop = setInterval(() => {
|
||||
client.write('network_chunk_publisher_update', { coordinates: { x: respawnPacket.x, y: 130, z: respawnPacket.z }, radius: 80 })
|
||||
}, 4500)
|
||||
|
||||
// Wait some time to allow for the client to recieve and load all the chunks
|
||||
setTimeout(() => {
|
||||
// Allow the client to spawn
|
||||
client.write('play_status', { status: 'player_spawn' })
|
||||
}, 6000)
|
||||
|
||||
// Respond to tick synchronization packets
|
||||
client.on('tick_sync', (packet) => {
|
||||
client.queue('tick_sync', {
|
||||
request_time: packet.request_time,
|
||||
response_time: BigInt(Date.now())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ok()
|
||||
|
||||
return {
|
||||
kill: () => {
|
||||
clearInterval(loop)
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let server
|
||||
waitFor((res) => {
|
||||
server = startServer(process.argv[2], res)
|
||||
}, 1000 * 60 /* Wait 60 seconds for the server to start */, function onTimeout () {
|
||||
console.error('Server did not start in time')
|
||||
server?.close()
|
||||
process.exit(1)
|
||||
})
|
||||
44
examples/server/serverChunks.js
Normal file
44
examples/server/serverChunks.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// CHUNKS
|
||||
const { WorldProvider } = require('bedrock-provider')
|
||||
const { LevelDB } = require('leveldb-zlib')
|
||||
const { join } = require('path')
|
||||
|
||||
async function loadWorld (version) {
|
||||
const path = join(__dirname, `../../tools/bds-${version}/worlds/Bedrock level/db`)
|
||||
console.log('Loading world at path', path) // Load world from testing server
|
||||
const db = new LevelDB(path, { createIfMissing: false })
|
||||
await db.open()
|
||||
const wp = new WorldProvider(db, { dimension: 0 })
|
||||
|
||||
async function requestChunks (x, z, radius) {
|
||||
const chunks = []
|
||||
const cxStart = (x >> 4) - radius
|
||||
const cxEnd = (x >> 4) + radius
|
||||
const czStart = (z >> 4) - radius
|
||||
const czEnd = (z >> 4) + radius
|
||||
|
||||
for (let cx = cxStart; cx < cxEnd; cx++) {
|
||||
for (let cz = czStart; cz < czEnd; cz++) {
|
||||
const cc = await wp.load(cx, cz, true)
|
||||
if (!cc) {
|
||||
continue
|
||||
}
|
||||
const cbuf = await cc.networkEncodeNoCache()
|
||||
chunks.push({
|
||||
x: cx,
|
||||
z: cz,
|
||||
sub_chunk_count: cc.sectionsLen,
|
||||
cache_enabled: false,
|
||||
blobs: [],
|
||||
payload: cbuf
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
return { requestChunks }
|
||||
}
|
||||
|
||||
module.exports = { loadWorld }
|
||||
240
index.d.ts
vendored
Normal file
240
index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import EventEmitter from 'events'
|
||||
import { Realm } from 'prismarine-realms'
|
||||
import { ServerDeviceCodeResponse } from 'prismarine-auth'
|
||||
|
||||
declare module 'bedrock-protocol' {
|
||||
type Version = '1.21.93' | '1.21.90' | '1.21.80' | '1.21.70' | '1.21.60' | '1.21.50' | '1.21.42' | '1.21.30' | '1.21.2' | '1.21.0' | '1.20.80' | '1.20.71' | '1.20.61' | '1.20.50' | '1.20.40' | '1.20.30' | '1.20.10' | '1.20.0' | '1.19.80' | '1.19.70' | '1.19.63' | '1.19.62' | '1.19.60' | '1.19.51' | '1.19.50' | '1.19.41' | '1.19.40' | '1.19.31' | '1.19.30' | '1.19.22' | '1.19.21' | '1.19.20' | '1.19.11' | '1.19.10' | '1.19.2' | '1.19.1' | '1.18.31' | '1.18.30' | '1.18.12' | '1.18.11' | '1.18.10' | '1.18.2' | '1.18.1' | '1.18.0' | '1.17.41' | '1.17.40' | '1.17.34' | '1.17.30' | '1.17.11' | '1.17.10' | '1.17.0' | '1.16.220' | '1.16.210' | '1.16.201'
|
||||
|
||||
export interface Options {
|
||||
// The string version to start the client or server as
|
||||
version?: Version
|
||||
// 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)
|
||||
host: string
|
||||
// The port to connect or bind to, default: 19132
|
||||
port: number
|
||||
// For the client, if we should login with Microsoft/Xbox Live.
|
||||
// For the server, if we should verify client's authentication with Xbox Live.
|
||||
offline?: boolean
|
||||
|
||||
// Which raknet backend to use
|
||||
raknetBackend?: 'jsp-raknet' | 'raknet-native' | 'raknet-node'
|
||||
// If using JS implementation of RakNet, should we use workers? (This only affects the client)
|
||||
useRaknetWorker?: boolean
|
||||
// Compression level for zlib, default to 7
|
||||
compressionLevel?: number
|
||||
// How frequently the packet queue should be flushed in milliseconds, defaults to 20ms
|
||||
batchingInterval?: number
|
||||
}
|
||||
|
||||
export interface ClientOptions extends Options {
|
||||
// The username to connect to the server as
|
||||
username: string
|
||||
// The view distance in chunks
|
||||
viewDistance?: number
|
||||
// Specifies which game edition to sign in as. Optional, but some servers verify this.
|
||||
authTitle?: string
|
||||
// How long to wait in milliseconds while trying to connect to the server.
|
||||
connectTimeout?: number
|
||||
// whether to skip initial ping and immediately connect
|
||||
skipPing?: boolean
|
||||
// Update the options' port parameter to match the port broadcast on the server's ping data (default to true if `realms` not specified)
|
||||
followPort?: boolean
|
||||
// where to log connection information to (default to console.log)
|
||||
conLog?: any
|
||||
// used to join a Realm instead of supplying a host/port
|
||||
realms?: RealmsOptions
|
||||
// the path to store authentication caches, defaults to .minecraft
|
||||
profilesFolder?: string | false
|
||||
// Called when microsoft authorization is needed when not provided it will the information log to the console instead
|
||||
onMsaCode?: (data: ServerDeviceCodeResponse) => void
|
||||
}
|
||||
|
||||
export interface ServerOptions extends Options {
|
||||
// The maximum number of players allowed on the server at any time.
|
||||
maxPlayers?: number
|
||||
motd?: {
|
||||
// The header for the MOTD shown in the server list.
|
||||
motd: string
|
||||
// The sub-header for the MOTD shown in the server list.
|
||||
levelName?: string
|
||||
}
|
||||
advertisementFn?: () => ServerAdvertisement
|
||||
}
|
||||
|
||||
enum ClientStatus {
|
||||
Disconnected,
|
||||
Authenticating,
|
||||
Initializing,
|
||||
Initialized
|
||||
}
|
||||
|
||||
export class Connection extends EventEmitter {
|
||||
readonly status: ClientStatus
|
||||
|
||||
// Check if the passed version is less than or greater than the current connected client version.
|
||||
versionLessThan(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
|
||||
write(name: string, params: object): void
|
||||
// Adds a Minecraft bedrock packet to be sent in the next outgoing batch
|
||||
queue(name: string, params: object): void
|
||||
// Writes a MCPE buffer to the connection and skips Protodef serialization. `immediate` if skip queue.
|
||||
sendBuffer(buffer: Buffer, immediate?: boolean): void
|
||||
}
|
||||
|
||||
type PlayStatus =
|
||||
| 'login_success'
|
||||
// # Displays "Could not connect: Outdated client!"
|
||||
| 'failed_client'
|
||||
// # Displays "Could not connect: Outdated server!"
|
||||
| 'failed_spawn'
|
||||
// # Sent after world data to spawn the player
|
||||
| 'player_spawn'
|
||||
// # Displays "Unable to connect to world. Your school does not have access to this server."
|
||||
| 'failed_invalid_tenant'
|
||||
// # Displays "The server is not running Minecraft: Education Edition. Failed to connect."
|
||||
| 'failed_vanilla_edu'
|
||||
// # Displays "The server is running an incompatible edition of Minecraft. Failed to connect."
|
||||
| 'failed_edu_vanilla'
|
||||
// # Displays "Wow this server is popular! Check back later to see if space opens up. Server Full"
|
||||
| 'failed_server_full'
|
||||
|
||||
export class Client extends Connection {
|
||||
constructor(options: Options)
|
||||
// The client's EntityID returned by the server
|
||||
readonly entityId: BigInt
|
||||
|
||||
/**
|
||||
* Close the connection, leave the server.
|
||||
*/
|
||||
close(reason?: string): void
|
||||
|
||||
/**
|
||||
* Send a disconnect packet and close the connection
|
||||
*/
|
||||
disconnect(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* `Player` represents a player connected to the server.
|
||||
*/
|
||||
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.
|
||||
* @param {string} playStatus
|
||||
*/
|
||||
sendDisconnectStatus(playStatus: PlayStatus): void
|
||||
|
||||
/**
|
||||
* Disconnects a client
|
||||
* @param reason The message to be shown to the user on disconnect
|
||||
* @param hide Don't show the client the reason for the disconnect
|
||||
*/
|
||||
disconnect(reason: string, hide?: boolean): void
|
||||
|
||||
/**
|
||||
* Close the connection. Already called by disconnect. Call this to manually close RakNet connection.
|
||||
*/
|
||||
close(): void
|
||||
|
||||
on(event: 'login', cb: () => void): any
|
||||
on(event: 'join', cb: () => 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 {
|
||||
clients: Map<string, Player>
|
||||
conLog: Function
|
||||
|
||||
constructor(options: Options)
|
||||
|
||||
listen(): Promise<void>
|
||||
close(disconnectReason?: string): Promise<void>
|
||||
|
||||
on(event: 'connect', cb: (client: Player) => void): any
|
||||
}
|
||||
|
||||
type RelayOptions = Options & {
|
||||
// Toggle packet logging.
|
||||
logging?: boolean
|
||||
// Skip authentication for connecting clients?
|
||||
offline?: false
|
||||
// Specifies which game edition to sign in as to the destination server. Optional, but some servers verify this.
|
||||
authTitle?: string
|
||||
// Where to proxy requests to.
|
||||
destination: {
|
||||
realms?: RealmsOptions
|
||||
host: string
|
||||
port: number
|
||||
// Skip authentication connecting to the remote server?
|
||||
offline?: boolean
|
||||
}
|
||||
// Whether to enable chunk caching (default: false)
|
||||
enableChunkCaching?: boolean
|
||||
|
||||
// Only allow one client to connect at a time (default: false)
|
||||
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
|
||||
// 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.
|
||||
onMsaCode?(data: ServerDeviceCodeResponse, client: Client): any
|
||||
// prismarine-auth configuration
|
||||
flow?: string,
|
||||
deviceType?: string
|
||||
}
|
||||
|
||||
export class Relay extends Server {
|
||||
constructor(options: RelayOptions)
|
||||
}
|
||||
|
||||
export class ServerAdvertisement {
|
||||
motd: string
|
||||
name: string
|
||||
protocol: number
|
||||
version: string
|
||||
playersOnline: number
|
||||
playersMax: number
|
||||
serverId: string
|
||||
levelName: string
|
||||
gamemodeId: number
|
||||
portV4: number
|
||||
portV6: number
|
||||
|
||||
constructor(obj: object, port: number, version: string)
|
||||
}
|
||||
|
||||
export interface RealmsOptions {
|
||||
realmId?: string
|
||||
realmInvite?: string
|
||||
pickRealm?: (realms: Realm[]) => Realm
|
||||
}
|
||||
|
||||
export function createClient(options: ClientOptions): Client
|
||||
export function createServer(options: ServerOptions): Server
|
||||
|
||||
export function ping({
|
||||
host,
|
||||
port
|
||||
}: {
|
||||
host: string
|
||||
port: number
|
||||
}): Promise<ServerAdvertisement>
|
||||
}
|
||||
25
index.js
25
index.js
|
|
@ -1 +1,24 @@
|
|||
module.exports = require('./src/index.js');
|
||||
if (typeof process !== 'undefined' && parseInt(process.versions.node.split('.')[0]) < 14) {
|
||||
console.error('Your node version is currently', process.versions.node)
|
||||
console.error('Please update it to a version >= 14.x.x from https://nodejs.org/')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const { Client } = require('./src/client')
|
||||
const { Server } = require('./src/server')
|
||||
const { Relay } = require('./src/relay')
|
||||
const { createClient, ping } = require('./src/createClient')
|
||||
const { createServer } = require('./src/createServer')
|
||||
const { Titles } = require('prismarine-auth')
|
||||
const { ServerAdvertisement } = require('./src/server/advertisement')
|
||||
|
||||
module.exports = {
|
||||
Client,
|
||||
Server,
|
||||
Relay,
|
||||
createClient,
|
||||
ping,
|
||||
createServer,
|
||||
title: Titles,
|
||||
ServerAdvertisement
|
||||
}
|
||||
|
|
|
|||
54
package.json
54
package.json
|
|
@ -1,34 +1,56 @@
|
|||
{
|
||||
"name": "pocket-minecraft-protocol",
|
||||
"version": "2.0.0",
|
||||
"description": "Parse and serialize Minecraft PE packets",
|
||||
"name": "bedrock-protocol",
|
||||
"version": "3.49.0",
|
||||
"description": "Minecraft Bedrock Edition protocol library",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"build": "cd tools && node compileProtocol.js",
|
||||
"test": "mocha --retries 2 --bail --exit",
|
||||
"pretest": "npm run lint",
|
||||
"lint": "standard",
|
||||
"vanillaServer": "minecraft-bedrock-server --root tools --version",
|
||||
"dumpPackets": "node tools/genPacketDumps.js",
|
||||
"fix": "standard --fix"
|
||||
},
|
||||
"keywords": [
|
||||
"minecraft",
|
||||
"bedrock",
|
||||
"pocket-edition",
|
||||
"protocol"
|
||||
],
|
||||
"author": "mhsjlw <mhsjlw@aol.com>",
|
||||
"contributors": [
|
||||
"rom1504"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prismarine-nbt": "^1.0.0",
|
||||
"protodef": "^1.2.0",
|
||||
"raknet": "^1.6.7",
|
||||
"uuid-1345": "^0.99.6"
|
||||
"debug": "^4.3.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsp-raknet": "^2.1.3",
|
||||
"minecraft-data": "^3.0.0",
|
||||
"minecraft-folder-path": "^1.2.0",
|
||||
"prismarine-auth": "^2.0.0",
|
||||
"prismarine-nbt": "^2.0.0",
|
||||
"prismarine-realms": "^1.1.0",
|
||||
"protodef": "^1.14.0",
|
||||
"raknet-native": "^1.0.3",
|
||||
"uuid-1345": "^1.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"raknet-node": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bedrock-protocol": "file:.",
|
||||
"bedrock-provider": "^2.0.0",
|
||||
"leveldb-zlib": "^1.0.1",
|
||||
"minecraft-bedrock-server": "^1.4.2",
|
||||
"mocha": "^11.0.1",
|
||||
"protodef-yaml": "^1.1.0",
|
||||
"standard": "^17.0.0-2"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mhsjlw/pocket-minecraft-protocol.git"
|
||||
"url": "git+https://github.com/PrismarineJS/bedrock-protocol.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/mhsjlw/pocket-minecraft-protocol/issues"
|
||||
"url": "https://github.com/PrismarineJS/bedrock-protocol/issues"
|
||||
},
|
||||
"homepage": "https://github.com/mhsjlw/pocket-minecraft-protocol#readme"
|
||||
"homepage": "https://github.com/PrismarineJS/bedrock-protocol#readme"
|
||||
}
|
||||
|
|
|
|||
284
src/client.js
Normal file
284
src/client.js
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
const { ClientStatus, Connection } = require('./connection')
|
||||
const { createDeserializer, createSerializer } = require('./transforms/serializer')
|
||||
const { serialize, isDebug } = require('./datatypes/util')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const Options = require('./options')
|
||||
const auth = require('./client/auth')
|
||||
const initRaknet = require('./rak')
|
||||
const { KeyExchange } = require('./handshake/keyExchange')
|
||||
const Login = require('./handshake/login')
|
||||
const LoginVerify = require('./handshake/loginVerify')
|
||||
|
||||
const debugging = false
|
||||
|
||||
class Client extends Connection {
|
||||
// The RakNet connection
|
||||
connection
|
||||
|
||||
/** @param {{ version: number, host: string, port: number }} options */
|
||||
constructor (options) {
|
||||
super()
|
||||
this.options = { ...Options.defaultOptions, ...options }
|
||||
|
||||
this.startGameData = {}
|
||||
this.clientRuntimeId = null
|
||||
// Start off without compression on 1.19.30, zlib on below
|
||||
this.compressionAlgorithm = this.versionGreaterThanOrEqualTo('1.19.30') ? 'none' : 'deflate'
|
||||
this.compressionThreshold = 512
|
||||
this.compressionLevel = this.options.compressionLevel
|
||||
this.batchHeader = 0xfe
|
||||
|
||||
if (isDebug) {
|
||||
this.inLog = (...args) => debug('C ->', ...args)
|
||||
this.outLog = (...args) => debug('C <-', ...args)
|
||||
}
|
||||
this.conLog = this.options.conLog === undefined ? console.log : this.options.conLog
|
||||
|
||||
if (!options.delayedInit) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
|
||||
init () {
|
||||
this.validateOptions()
|
||||
this.serializer = createSerializer(this.options.version)
|
||||
this.deserializer = createDeserializer(this.options.version)
|
||||
this._loadFeatures()
|
||||
|
||||
KeyExchange(this, null, this.options)
|
||||
Login(this, null, this.options)
|
||||
LoginVerify(this, null, this.options)
|
||||
|
||||
const { RakClient } = initRaknet(this.options.raknetBackend)
|
||||
const host = this.options.host
|
||||
const port = this.options.port
|
||||
this.connection = new RakClient({ useWorkers: this.options.useRaknetWorkers, host, port }, this)
|
||||
|
||||
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 () {
|
||||
if (!this.connection) throw new Error('Connect not currently allowed') // must wait for `connect_allowed`, or use `createClient`
|
||||
this.on('session', this._connect)
|
||||
|
||||
if (this.options.offline) {
|
||||
debug('offline mode, not authenticating', this.options)
|
||||
auth.createOfflineSession(this, this.options)
|
||||
} else {
|
||||
auth.authenticate(this, this.options)
|
||||
}
|
||||
|
||||
this.startQueue()
|
||||
}
|
||||
|
||||
validateOptions () {
|
||||
if (!this.options.host || this.options.port == null) throw Error('Invalid host/port')
|
||||
Options.validateOptions(this.options)
|
||||
}
|
||||
|
||||
get entityId () {
|
||||
return this.startGameData.runtime_entity_id
|
||||
}
|
||||
|
||||
onEncapsulated = (encapsulated, inetAddr) => {
|
||||
const buffer = Buffer.from(encapsulated.buffer)
|
||||
process.nextTick(() => this.handle(buffer))
|
||||
}
|
||||
|
||||
async ping () {
|
||||
try {
|
||||
return await this.connection.ping(this.options.connectTimeout)
|
||||
} catch (e) {
|
||||
this.conLog?.(`Unable to connect to [${this.options.host}]/${this.options.port}. Is the server running?`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
_connect = async (sessionData) => {
|
||||
debug('[client] connecting to', this.options.host, this.options.port, sessionData, this.connection)
|
||||
this.connection.onConnected = () => {
|
||||
this.status = ClientStatus.Connecting
|
||||
if (this.versionGreaterThanOrEqualTo('1.19.30')) {
|
||||
this.queue('request_network_settings', { client_protocol: this.options.protocolVersion })
|
||||
} else {
|
||||
this.sendLogin()
|
||||
}
|
||||
}
|
||||
this.connection.onCloseConnection = (reason) => {
|
||||
if (this.status === ClientStatus.Disconnected) this.conLog?.(`Server closed connection: ${reason}`)
|
||||
this.close()
|
||||
}
|
||||
this.connection.onEncapsulated = this.onEncapsulated
|
||||
this.connection.connect()
|
||||
|
||||
this.connectTimeout = setTimeout(() => {
|
||||
if (this.status === ClientStatus.Disconnected) {
|
||||
this.connection.close()
|
||||
this.emit('error', Error('Connect timed out'))
|
||||
}
|
||||
}, this.options.connectTimeout || 9000)
|
||||
}
|
||||
|
||||
updateCompressorSettings (packet) {
|
||||
this.compressionAlgorithm = packet.compression_algorithm || 'deflate'
|
||||
this.compressionThreshold = packet.compression_threshold
|
||||
this.compressionReady = true
|
||||
}
|
||||
|
||||
sendLogin () {
|
||||
this.status = ClientStatus.Authenticating
|
||||
this.createClientChain(null, this.options.offline)
|
||||
|
||||
const chain = [
|
||||
this.clientIdentityChain, // JWT we generated for auth
|
||||
...this.accessToken // Mojang + Xbox JWT from auth
|
||||
]
|
||||
|
||||
let encodedChain
|
||||
if (this.features.newLoginIdentityFields) { // 1.21.90+
|
||||
encodedChain = JSON.stringify({
|
||||
Certificate: JSON.stringify({ chain }),
|
||||
// 0 = normal, 1 = ss, 2 = offline
|
||||
AuthenticationType: this.options.offline ? 2 : 0,
|
||||
Token: ''
|
||||
})
|
||||
} else {
|
||||
encodedChain = JSON.stringify({ chain })
|
||||
}
|
||||
debug('Auth chain', encodedChain)
|
||||
|
||||
this.write('login', {
|
||||
protocol_version: this.options.protocolVersion,
|
||||
tokens: {
|
||||
identity: encodedChain,
|
||||
client: this.clientUserChain
|
||||
}
|
||||
})
|
||||
this.emit('loggingIn')
|
||||
}
|
||||
|
||||
onDisconnectRequest (packet) {
|
||||
this.conLog?.(`Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`)
|
||||
this.emit('kick', packet)
|
||||
this.close()
|
||||
}
|
||||
|
||||
onPlayStatus (statusPacket) {
|
||||
if (this.status === ClientStatus.Initializing && this.options.autoInitPlayer === true) {
|
||||
if (statusPacket.status === 'player_spawn') {
|
||||
this.status = ClientStatus.Initialized
|
||||
if (!this.entityId) {
|
||||
// We need to wait for start_game in the rare event we get a player_spawn before start_game race condition
|
||||
this.on('start_game', () => this.write('set_local_player_as_initialized', { runtime_entity_id: this.entityId }))
|
||||
} else {
|
||||
this.write('set_local_player_as_initialized', { runtime_entity_id: this.entityId })
|
||||
}
|
||||
this.emit('spawn')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect (reason = 'Client leaving', hide = false) {
|
||||
if (this.status === ClientStatus.Disconnected) return
|
||||
this.write('disconnect', {
|
||||
hide_disconnect_screen: hide,
|
||||
message: reason,
|
||||
filtered_message: ''
|
||||
})
|
||||
this.close(reason)
|
||||
}
|
||||
|
||||
close () {
|
||||
if (this.status !== ClientStatus.Disconnected) {
|
||||
this.emit('close') // Emit close once
|
||||
debug('Client closed!')
|
||||
}
|
||||
clearInterval(this.loop)
|
||||
clearTimeout(this.connectTimeout)
|
||||
this.q = []
|
||||
this.q2 = []
|
||||
this.connection?.close()
|
||||
this.removeAllListeners()
|
||||
this.status = ClientStatus.Disconnected
|
||||
}
|
||||
|
||||
readPacket (packet) {
|
||||
try {
|
||||
var des = this.deserializer.parsePacketBuffer(packet) // eslint-disable-line
|
||||
} catch (e) {
|
||||
// Dump information about the packet only if user is not handling error event.
|
||||
if (this.listenerCount('error') === 0) this.deserializer.dumpFailedBuffer(packet)
|
||||
this.emit('error', e)
|
||||
return
|
||||
}
|
||||
const pakData = { name: des.data.name, params: des.data.params }
|
||||
this.inLog?.('-> C', pakData.name, this.options.logging ? serialize(pakData.params) : '')
|
||||
this.emit('packet', des)
|
||||
|
||||
if (debugging) {
|
||||
// Packet verifying (decode + re-encode + match test)
|
||||
if (pakData.name) {
|
||||
this.deserializer.verify(packet, this.serializer)
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract some boilerplate before sending to listeners
|
||||
switch (des.data.name) {
|
||||
case 'server_to_client_handshake':
|
||||
this.emit('client.server_handshake', des.data.params)
|
||||
break
|
||||
case 'network_settings':
|
||||
this.updateCompressorSettings(des.data.params)
|
||||
if (this.status === ClientStatus.Connecting) {
|
||||
this.sendLogin()
|
||||
}
|
||||
break
|
||||
case 'disconnect': // Client kicked
|
||||
this.emit(des.data.name, des.data.params) // Emit before we kill all listeners.
|
||||
this.onDisconnectRequest(des.data.params)
|
||||
break
|
||||
case 'start_game':
|
||||
this.startGameData = pakData.params
|
||||
// fallsthrough
|
||||
case 'item_registry': // 1.21.60+ send itemstates in item_registry packet
|
||||
pakData.params.itemstates?.forEach(state => {
|
||||
if (state.name === 'minecraft:shield') {
|
||||
this.serializer.proto.setVariable('ShieldItemID', state.runtime_id)
|
||||
this.deserializer.proto.setVariable('ShieldItemID', state.runtime_id)
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'play_status':
|
||||
if (this.status === ClientStatus.Authenticating) {
|
||||
this.inLog?.('Server wants to skip encryption')
|
||||
this.emit('join')
|
||||
this.status = ClientStatus.Initializing
|
||||
}
|
||||
this.onPlayStatus(pakData.params)
|
||||
break
|
||||
default:
|
||||
if (this.status !== ClientStatus.Initializing && this.status !== ClientStatus.Initialized) {
|
||||
this.inLog?.(`Can't accept ${des.data.name}, client not yet authenticated : ${this.status}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Emit packet
|
||||
this.emit(des.data.name, des.data.params)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Client }
|
||||
119
src/client/auth.js
Normal file
119
src/client/auth.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
const path = require('path')
|
||||
const { Authflow: PrismarineAuth, Titles } = require('prismarine-auth')
|
||||
const minecraftFolderPath = require('minecraft-folder-path')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const { uuidFrom } = require('../datatypes/util')
|
||||
const { RealmAPI } = require('prismarine-realms')
|
||||
|
||||
function validateOptions (options) {
|
||||
if (!options.profilesFolder) {
|
||||
options.profilesFolder = path.join(minecraftFolderPath, 'nmp-cache')
|
||||
}
|
||||
if (options.authTitle === undefined) {
|
||||
options.authTitle = Titles.MinecraftNintendoSwitch
|
||||
options.deviceType = 'Nintendo'
|
||||
options.flow = 'live'
|
||||
}
|
||||
}
|
||||
|
||||
async function realmAuthenticate (options) {
|
||||
validateOptions(options)
|
||||
|
||||
options.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)
|
||||
|
||||
const api = RealmAPI.from(options.authflow, 'bedrock')
|
||||
|
||||
const getRealms = async () => {
|
||||
const realms = await api.getRealms()
|
||||
debug('realms', realms)
|
||||
if (!realms.length) throw Error('Couldn\'t find any Realms for the authenticated account')
|
||||
return realms
|
||||
}
|
||||
|
||||
let realm
|
||||
|
||||
if (options.realms.realmId) {
|
||||
const realms = await getRealms()
|
||||
realm = realms.find(e => e.id === Number(options.realms.realmId))
|
||||
} else if (options.realms.realmInvite) {
|
||||
realm = await api.getRealmFromInvite(options.realms.realmInvite)
|
||||
} else if (options.realms.pickRealm) {
|
||||
if (typeof options.realms.pickRealm !== 'function') throw Error('realms.pickRealm must be a function')
|
||||
const realms = await getRealms()
|
||||
realm = await options.realms.pickRealm(realms)
|
||||
}
|
||||
|
||||
if (!realm) throw Error('Couldn\'t find a Realm to connect to. Authenticated account must be the owner or has been invited to the Realm.')
|
||||
|
||||
const { host, port } = await realm.getAddress()
|
||||
|
||||
debug('realms connection', { host, port })
|
||||
|
||||
options.host = host
|
||||
options.port = port
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates to Minecraft via device code based Microsoft auth,
|
||||
* then connects to the specified server in Client Options
|
||||
*
|
||||
* @function
|
||||
* @param {object} client - The client passed to protocol
|
||||
* @param {object} options - Client Options
|
||||
*/
|
||||
async function authenticate (client, options) {
|
||||
validateOptions(options)
|
||||
try {
|
||||
const authflow = options.authflow || new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)
|
||||
const chains = await authflow.getMinecraftBedrockToken(client.clientX509).catch(e => {
|
||||
if (options.password) console.warn('Sign in failed, try removing the password field')
|
||||
throw e
|
||||
})
|
||||
|
||||
debug('chains', chains)
|
||||
|
||||
// First chain is Mojang stuff, second is Xbox profile data used by mc
|
||||
const jwt = chains[1]
|
||||
const [header, payload, signature] = jwt.split('.').map(k => Buffer.from(k, 'base64')) // eslint-disable-line
|
||||
const xboxProfile = JSON.parse(String(payload))
|
||||
|
||||
debug('got xbox profile', xboxProfile)
|
||||
|
||||
const profile = {
|
||||
name: xboxProfile?.extraData?.displayName || 'Player',
|
||||
uuid: xboxProfile?.extraData?.identity || 'adfcf5ca-206c-404a-aec4-f59fff264c9b', // random
|
||||
xuid: xboxProfile?.extraData?.XUID || 0
|
||||
}
|
||||
|
||||
return postAuthenticate(client, profile, chains)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
client.emit('error', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an offline session for the client
|
||||
*/
|
||||
function createOfflineSession (client, options) {
|
||||
if (!options.username) throw Error('Must specify a valid username')
|
||||
const profile = {
|
||||
name: options.username,
|
||||
uuid: uuidFrom(options.username), // random
|
||||
xuid: 0
|
||||
}
|
||||
return postAuthenticate(client, profile, []) // No extra JWTs, only send 1 client signed chain with all the data
|
||||
}
|
||||
|
||||
function postAuthenticate (client, profile, chains) {
|
||||
client.profile = profile
|
||||
client.username = profile.name
|
||||
client.accessToken = chains
|
||||
client.emit('session', profile)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createOfflineSession,
|
||||
authenticate,
|
||||
realmAuthenticate
|
||||
}
|
||||
195
src/connection.js
Normal file
195
src/connection.js
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
const cipher = require('./transforms/encryption')
|
||||
const { EventEmitter } = require('events')
|
||||
const { Versions } = require('./options')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const { Framer } = require('./transforms/framer')
|
||||
|
||||
const ClientStatus = {
|
||||
Disconnected: 0,
|
||||
Connecting: 1,
|
||||
Authenticating: 2, // Handshaking
|
||||
Initializing: 3, // Authed, need to spawn
|
||||
Initialized: 4 // play_status spawn sent by server, client responded with SetPlayerInit packet
|
||||
}
|
||||
|
||||
class Connection extends EventEmitter {
|
||||
#status = ClientStatus.Disconnected
|
||||
sendQ = []
|
||||
sendIds = []
|
||||
|
||||
get status () {
|
||||
return this.#status
|
||||
}
|
||||
|
||||
set status (val) {
|
||||
debug('* new status', val)
|
||||
this.emit('status', val)
|
||||
this.#status = val
|
||||
}
|
||||
|
||||
versionLessThan (version) {
|
||||
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
|
||||
return this.options.protocolVersion < (typeof version === 'string' ? Versions[version] : version)
|
||||
}
|
||||
|
||||
versionGreaterThan (version) {
|
||||
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
|
||||
return this.options.protocolVersion > (typeof version === 'string' ? Versions[version] : version)
|
||||
}
|
||||
|
||||
versionGreaterThanOrEqualTo (version) {
|
||||
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
|
||||
return this.options.protocolVersion >= (typeof version === 'string' ? Versions[version] : version)
|
||||
}
|
||||
|
||||
versionLessThanOrEqualTo (version) {
|
||||
if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version)
|
||||
return this.options.protocolVersion <= (typeof version === 'string' ? Versions[version] : version)
|
||||
}
|
||||
|
||||
startEncryption (iv) {
|
||||
this.encryptionEnabled = true
|
||||
this.inLog?.('Started encryption', this.sharedSecret, iv)
|
||||
this.decrypt = cipher.createDecryptor(this, iv)
|
||||
this.encrypt = cipher.createEncryptor(this, iv)
|
||||
}
|
||||
|
||||
updateItemPalette (palette) {
|
||||
// In the future, we can send down the whole item palette if we need
|
||||
// but since it's only one item, we can just make a single variable.
|
||||
let shieldItemID
|
||||
for (const state of palette) {
|
||||
if (state.name === 'minecraft:shield') {
|
||||
shieldItemID = state.runtime_id
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shieldItemID) {
|
||||
this.serializer.proto.setVariable('ShieldItemID', shieldItemID)
|
||||
this.deserializer.proto.setVariable('ShieldItemID', shieldItemID)
|
||||
}
|
||||
}
|
||||
|
||||
_processOutbound (name, params) {
|
||||
if (name === 'item_registry') {
|
||||
this.updateItemPalette(params.itemstates)
|
||||
} else if (name === 'start_game' && params.itemstates) {
|
||||
this.updateItemPalette(params.itemstates)
|
||||
}
|
||||
}
|
||||
|
||||
write (name, params) {
|
||||
this.outLog?.(name, params)
|
||||
this._processOutbound(name, params)
|
||||
const batch = new Framer(this)
|
||||
const packet = this.serializer.createPacketBuffer({ name, params })
|
||||
batch.addEncodedPacket(packet)
|
||||
|
||||
if (this.encryptionEnabled) {
|
||||
this.sendEncryptedBatch(batch)
|
||||
} else {
|
||||
this.sendDecryptedBatch(batch)
|
||||
}
|
||||
}
|
||||
|
||||
queue (name, params) {
|
||||
this.outLog?.('Q <- ', name, params)
|
||||
this._processOutbound(name, params)
|
||||
const packet = this.serializer.createPacketBuffer({ name, params })
|
||||
if (name === 'level_chunk') {
|
||||
// Skip queue, send ASAP
|
||||
this.sendBuffer(packet)
|
||||
return
|
||||
}
|
||||
this.sendQ.push(packet)
|
||||
this.sendIds.push(name)
|
||||
}
|
||||
|
||||
_tick () {
|
||||
if (this.sendQ.length) {
|
||||
const batch = new Framer(this)
|
||||
batch.addEncodedPackets(this.sendQ)
|
||||
this.sendQ = []
|
||||
this.sendIds = []
|
||||
if (this.encryptionEnabled) {
|
||||
this.sendEncryptedBatch(batch)
|
||||
} else {
|
||||
this.sendDecryptedBatch(batch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTick = this._tick.bind(this)
|
||||
|
||||
startQueue () {
|
||||
this.sendQ = []
|
||||
this.loop = setInterval(this.onTick, this.options.batchingInterval || 20)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MCPE packet buffer
|
||||
*/
|
||||
sendBuffer (buffer, immediate = false) {
|
||||
if (immediate) {
|
||||
const batch = new Framer(this)
|
||||
batch.addEncodedPacket(buffer)
|
||||
if (this.encryptionEnabled) {
|
||||
this.sendEncryptedBatch(batch)
|
||||
} else {
|
||||
this.sendDecryptedBatch(batch)
|
||||
}
|
||||
} else {
|
||||
this.sendQ.push(buffer)
|
||||
this.sendIds.push('rawBuffer')
|
||||
}
|
||||
}
|
||||
|
||||
sendDecryptedBatch (batch) {
|
||||
// send to raknet
|
||||
this.sendMCPE(batch.encode(), true)
|
||||
}
|
||||
|
||||
sendEncryptedBatch (batch) {
|
||||
const buf = batch.getBuffer()
|
||||
this.encrypt(buf)
|
||||
}
|
||||
|
||||
sendMCPE (buffer, immediate) {
|
||||
if (this.connection.connected === false || this.status === ClientStatus.Disconnected) return
|
||||
try {
|
||||
this.connection.sendReliable(buffer, immediate)
|
||||
} catch (e) {
|
||||
debug('while sending to', this.connection, e)
|
||||
}
|
||||
}
|
||||
|
||||
// These are callbacks called from encryption.js
|
||||
onEncryptedPacket = (buf) => {
|
||||
const packet = this.batchHeader ? Buffer.concat([Buffer.from([this.batchHeader]), buf]) : buf
|
||||
this.sendMCPE(packet)
|
||||
}
|
||||
|
||||
onDecryptedPacket = (buf) => {
|
||||
const packets = Framer.getPackets(buf)
|
||||
for (const packet of packets) {
|
||||
this.readPacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
handle (buffer) { // handle encapsulated
|
||||
if (!this.batchHeader || buffer[0] === this.batchHeader) { // wrapper
|
||||
if (this.encryptionEnabled) {
|
||||
this.decrypt(buffer.slice(1))
|
||||
} else {
|
||||
const packets = Framer.decode(this, buffer)
|
||||
for (const packet of packets) {
|
||||
this.readPacket(packet)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw Error('Bad packet header ' + buffer[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ClientStatus, Connection }
|
||||
|
|
@ -1,61 +1,101 @@
|
|||
'use strict';
|
||||
const assert = require('assert');
|
||||
const raknet = require('raknet');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
const ProtoDef = require('protodef').ProtoDef;
|
||||
const batchProto=new ProtoDef();
|
||||
batchProto.addTypes(require("./datatypes/minecraft"));
|
||||
batchProto.addType("insideBatch",["endOfArray",{"type":["buffer",{"countType":"i32"}]}]);
|
||||
const { Client } = require('./client')
|
||||
const { RakClient } = require('./rak')('raknet-native')
|
||||
const { sleep } = require('./datatypes/util')
|
||||
const assert = require('assert')
|
||||
const Options = require('./options')
|
||||
const advertisement = require('./server/advertisement')
|
||||
const auth = require('./client/auth')
|
||||
|
||||
function createClient(options) {
|
||||
assert.ok(options, 'options is required');
|
||||
var port = options.port || 19132;
|
||||
var host = options.host || 'localhost';
|
||||
/** @param {{ version?: number, host: string, port?: number, connectTimeout?: number, skipPing?: boolean }} options */
|
||||
function createClient (options) {
|
||||
assert(options)
|
||||
const client = new Client({ port: 19132, followPort: !options.realms, ...options, delayedInit: true })
|
||||
|
||||
assert.ok(options.username, 'username is required');
|
||||
function onServerInfo () {
|
||||
client.on('connect_allowed', () => connect(client))
|
||||
if (options.skipPing) {
|
||||
client.init()
|
||||
} else {
|
||||
ping(client.options).then(ad => {
|
||||
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)
|
||||
|
||||
options.customPackets=require('../data/protocol');
|
||||
options.customTypes=require('./datatypes/minecraft');
|
||||
|
||||
var client=raknet.createClient(options);
|
||||
client.username = options.username;
|
||||
client.on('mcpe',packet => client.emit(packet.name,packet.params))
|
||||
client.writeMCPE=(name,packet) => {
|
||||
client.writeEncapsulated('mcpe',{
|
||||
name:name,
|
||||
params:packet
|
||||
});
|
||||
};
|
||||
|
||||
client.on('login', function() {
|
||||
client.writeMCPE('game_login',
|
||||
{
|
||||
username: client.username,
|
||||
protocol: 46,
|
||||
protocol2: 46,
|
||||
client_id: [ -1, -697896776 ],
|
||||
client_uuid: '86372ed8-d055-b23a-9171-5e3ac594d766',
|
||||
server_address: client.host+":"+client.port,
|
||||
client_secret: new Buffer('e8 88 db 7b 9f f2 f0 44 a3 51 08 18 4e 8c 7f 9a'.replace(/ /g,''),'hex'),
|
||||
skin:
|
||||
{
|
||||
skinType: 'Standard_Steve',
|
||||
texture: fs.readFileSync(path.join(__dirname,'texture'))
|
||||
if (ad.portV4 && client.options.followPort) {
|
||||
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.init()
|
||||
}).catch(e => client.emit('error', e))
|
||||
}
|
||||
}
|
||||
|
||||
client.on('batch', function(packet) {
|
||||
var buf = zlib.inflateSync(packet.payload);
|
||||
var packets=batchProto.parsePacketBuffer("insideBatch",buf).data;
|
||||
packets.forEach(packet => client.readEncapsulatedPacket(Buffer.concat([new Buffer([0x8e]),packet])));
|
||||
});
|
||||
|
||||
return client;
|
||||
if (options.realms) {
|
||||
auth.realmAuthenticate(client.options).then(onServerInfo).catch(e => client.emit('error', e))
|
||||
} else {
|
||||
onServerInfo()
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
module.exports = createClient;
|
||||
function connect (client) {
|
||||
// Actually connect
|
||||
client.connect()
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
|
||||
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 }))
|
||||
})
|
||||
|
||||
if (client.versionLessThanOrEqualTo('1.20.80')) {
|
||||
const keepAliveInterval = 10
|
||||
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 => {
|
||||
client.emit('heartbeat', packet.response_time)
|
||||
client.tick = packet.response_time
|
||||
})
|
||||
})
|
||||
|
||||
client.once('close', () => {
|
||||
clearInterval(keepalive)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function ping ({ host, port }) {
|
||||
const con = new RakClient({ host, port })
|
||||
|
||||
try {
|
||||
return advertisement.fromServerName(await con.ping())
|
||||
} finally {
|
||||
con.close()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createClient, ping }
|
||||
|
|
|
|||
|
|
@ -1,47 +1,10 @@
|
|||
const raknet = require('raknet');
|
||||
const zlib = require('zlib');
|
||||
const ProtoDef = require('protodef').ProtoDef;
|
||||
const batchProto=new ProtoDef();
|
||||
batchProto.addTypes(require("./datatypes/minecraft"));
|
||||
batchProto.addType("insideBatch",["endOfArray",{"type":["buffer",{"countType":"i32"}]}]);
|
||||
const { Server } = require('./server')
|
||||
|
||||
function createServer(options) {
|
||||
options = options || {};
|
||||
var port = options.port != null ?
|
||||
options.port :
|
||||
options['server-port'] != null ?
|
||||
options['server-port'] :
|
||||
19132;
|
||||
var host = options.host || '0.0.0.0';
|
||||
|
||||
options.customPackets=require("../data/protocol");
|
||||
options.customTypes=require("./datatypes/minecraft");
|
||||
var server = raknet.createServer(options);
|
||||
|
||||
server.name = options.name || "Minecraft Server";
|
||||
server.motd = options.motd || "A Minecraft server";
|
||||
server.maxPlayers = options['max-players'] || 20;
|
||||
server.playerCount = 0;
|
||||
|
||||
server.on("connection", function (client) {
|
||||
client.on("mcpe",packet => client.emit(packet.name,packet.params));
|
||||
|
||||
client.writeMCPE=(name,packet) => {
|
||||
client.writeEncapsulated("mcpe",{
|
||||
name:name,
|
||||
params:packet
|
||||
});
|
||||
};
|
||||
client.writeBatch=function(packets) {
|
||||
const payload=zlib.deflateSync(batchProto.createPacketBuffer("insideBatch",
|
||||
packets.map(packet =>
|
||||
client.encapsulatedPacketSerializer.createPacketBuffer(packet).slice(1))));
|
||||
client.writeMCPE("batch",{
|
||||
payload:payload
|
||||
});
|
||||
}
|
||||
});
|
||||
return server;
|
||||
function createServer (options) {
|
||||
if (!options.port) options.port = 19132
|
||||
const server = new Server(options)
|
||||
server.listen()
|
||||
return server
|
||||
}
|
||||
|
||||
module.exports = createServer;
|
||||
module.exports = { createServer }
|
||||
|
|
|
|||
155
src/datatypes/compiler-minecraft.js
Normal file
155
src/datatypes/compiler-minecraft.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/* eslint-disable */
|
||||
const UUID = require('uuid-1345')
|
||||
const minecraft = require('./minecraft')
|
||||
const [Read, Write, SizeOf] = [{}, {}, {}]
|
||||
|
||||
/**
|
||||
* UUIDs
|
||||
*/
|
||||
Read.uuid = ['native', (buffer, offset) => {
|
||||
return {
|
||||
value: UUID.stringify(buffer.slice(offset, 16 + offset)),
|
||||
size: 16
|
||||
}
|
||||
}]
|
||||
Write.uuid = ['native', (value, buffer, offset) => {
|
||||
const buf = UUID.parse(value)
|
||||
buf.copy(buffer, offset)
|
||||
return offset + 16
|
||||
}]
|
||||
SizeOf.uuid = ['native', 16]
|
||||
|
||||
/**
|
||||
* Rest of buffer
|
||||
*/
|
||||
Read.restBuffer = ['native', (buffer, offset) => {
|
||||
return {
|
||||
value: buffer.slice(offset),
|
||||
size: buffer.length - offset
|
||||
}
|
||||
}]
|
||||
Write.restBuffer = ['native', (value, buffer, offset) => {
|
||||
value.copy(buffer, offset)
|
||||
return offset + value.length
|
||||
}]
|
||||
SizeOf.restBuffer = ['native', (value) => {
|
||||
return value.length
|
||||
}]
|
||||
|
||||
/**
|
||||
* Encapsulated data with length prefix
|
||||
*/
|
||||
Read.encapsulated = ['parametrizable', (compiler, { lengthType, type }) => {
|
||||
return compiler.wrapCode(`
|
||||
const payloadSize = ${compiler.callType(lengthType, 'offset')}
|
||||
const { value, size } = ctx.${type}(buffer, offset + payloadSize.size)
|
||||
return { value, size: size + payloadSize.size }
|
||||
`.trim())
|
||||
}]
|
||||
Write.encapsulated = ['parametrizable', (compiler, { lengthType, type }) => {
|
||||
return compiler.wrapCode(`
|
||||
const buf = Buffer.allocUnsafe(buffer.length - offset)
|
||||
const payloadSize = (ctx.${type})(value, buf, 0)
|
||||
let size = (ctx.${lengthType})(payloadSize, buffer, offset)
|
||||
size += buf.copy(buffer, size, 0, payloadSize)
|
||||
return size
|
||||
`.trim())
|
||||
}]
|
||||
SizeOf.encapsulated = ['parametrizable', (compiler, { lengthType, type }) => {
|
||||
return compiler.wrapCode(`
|
||||
const payloadSize = (ctx.${type})(value)
|
||||
return (ctx.${lengthType})(payloadSize) + payloadSize
|
||||
`.trim())
|
||||
}]
|
||||
|
||||
/**
|
||||
* Read NBT until end of buffer or \0
|
||||
*/
|
||||
Read.nbtLoop = ['context', (buffer, offset) => {
|
||||
const values = []
|
||||
while (buffer[offset] != 0) {
|
||||
const n = ctx.nbt(buffer, offset)
|
||||
values.push(n.value)
|
||||
offset += n.size
|
||||
}
|
||||
return { value: values, size: buffer.length - offset }
|
||||
}]
|
||||
Write.nbtLoop = ['context', (value, buffer, offset) => {
|
||||
for (const val of value) {
|
||||
offset = ctx.nbt(val, buffer, offset)
|
||||
}
|
||||
buffer.writeUint8(0, offset)
|
||||
return offset + 1
|
||||
}]
|
||||
SizeOf.nbtLoop = ['context', (value, buffer, offset) => {
|
||||
let size = 1
|
||||
for (const val of value) {
|
||||
size += ctx.nbt(val, buffer, offset)
|
||||
}
|
||||
return size
|
||||
}]
|
||||
|
||||
/**
|
||||
* Read rotation float encoded as a byte
|
||||
*/
|
||||
Read.byterot = ['context', (buffer, offset) => {
|
||||
const val = buffer.readUint8(offset)
|
||||
return { value: (val * (360 / 256)), size: 1 }
|
||||
}]
|
||||
Write.byterot = ['context', (value, buffer, offset) => {
|
||||
const val = (value / (360 / 256))
|
||||
buffer.writeUint8(val, offset)
|
||||
return offset + 1
|
||||
}]
|
||||
SizeOf.byterot = ['context', (value, buffer, offset) => {
|
||||
return 1
|
||||
}]
|
||||
|
||||
/**
|
||||
* NBT
|
||||
*/
|
||||
Read.nbt = ['native', minecraft.nbt[0]]
|
||||
Write.nbt = ['native', minecraft.nbt[1]]
|
||||
SizeOf.nbt = ['native', minecraft.nbt[2]]
|
||||
|
||||
Read.lnbt = ['native', minecraft.lnbt[0]]
|
||||
Write.lnbt = ['native', minecraft.lnbt[1]]
|
||||
SizeOf.lnbt = ['native', minecraft.lnbt[2]]
|
||||
|
||||
/**
|
||||
* Command Packet
|
||||
* - used for determining the size of the following enum
|
||||
*/
|
||||
Read.enum_size_based_on_values_len = ['parametrizable', (compiler) => {
|
||||
return compiler.wrapCode(js(() => {
|
||||
if (values_len <= 0xff) return { value: 'byte', size: 0 }
|
||||
if (values_len <= 0xffff) return { value: 'short', size: 0 }
|
||||
if (values_len <= 0xffffff) return { value: 'int', size: 0 }
|
||||
}))
|
||||
}]
|
||||
Write.enum_size_based_on_values_len = ['parametrizable', (compiler) => {
|
||||
return str(() => {
|
||||
if (value.values_len <= 0xff) _enum_type = 'byte'
|
||||
else if (value.values_len <= 0xffff) _enum_type = 'short'
|
||||
else if (value.values_len <= 0xffffff) _enum_type = 'int'
|
||||
return offset
|
||||
})
|
||||
}]
|
||||
SizeOf.enum_size_based_on_values_len = ['parametrizable', (compiler) => {
|
||||
return str(() => {
|
||||
if (value.values_len <= 0xff) _enum_type = 'byte'
|
||||
else if (value.values_len <= 0xffff) _enum_type = 'short'
|
||||
else if (value.values_len <= 0xffffff) _enum_type = 'int'
|
||||
return 0
|
||||
})
|
||||
}]
|
||||
|
||||
function js (fn) {
|
||||
return fn.toString().split('\n').slice(1, -1).join('\n').trim()
|
||||
}
|
||||
|
||||
function str (fn) {
|
||||
return fn.toString() + ')();(()=>{}'
|
||||
}
|
||||
|
||||
module.exports = { Read, Write, SizeOf }
|
||||
|
|
@ -1,135 +1,160 @@
|
|||
'use strict';
|
||||
const nbt = require('prismarine-nbt')
|
||||
const UUID = require('uuid-1345')
|
||||
|
||||
var nbt = require('prismarine-nbt');
|
||||
const UUID = require('uuid-1345');
|
||||
const protoLE = nbt.protos.little
|
||||
const protoLEV = nbt.protos.littleVarint
|
||||
|
||||
function readUUID(buffer, offset) {
|
||||
if(offset+16>buffer.length)
|
||||
throw new PartialReadError();
|
||||
function readUUID (buffer, offset) {
|
||||
if (offset + 16 > buffer.length) { throw new Error('Reached end of buffer') }
|
||||
return {
|
||||
value: UUID.stringify(buffer.slice(offset,16+offset)),
|
||||
value: UUID.stringify(buffer.slice(offset, 16 + offset)),
|
||||
size: 16
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function writeUUID(value, buffer, offset) {
|
||||
const buf=UUID.parse(value);
|
||||
buf.copy(buffer,offset);
|
||||
return offset + 16;
|
||||
function writeUUID (value, buffer, offset) {
|
||||
const buf = UUID.parse(value)
|
||||
buf.copy(buffer, offset)
|
||||
return offset + 16
|
||||
}
|
||||
|
||||
function readNbt(buffer, offset) {
|
||||
return nbt.protoLE.read(buffer,offset,"nbt");
|
||||
// Little Endian + varints
|
||||
|
||||
function readNbt (buffer, offset) {
|
||||
return protoLEV.read(buffer, offset, 'nbt')
|
||||
}
|
||||
|
||||
function writeNbt(value, buffer, offset) {
|
||||
return nbt.protoLE.write(value,buffer,offset,"nbt");
|
||||
function writeNbt (value, buffer, offset) {
|
||||
return protoLEV.write(value, buffer, offset, 'nbt')
|
||||
}
|
||||
|
||||
function sizeOfNbt(value) {
|
||||
return nbt.protoLE.sizeOf(value,"nbt");
|
||||
function sizeOfNbt (value) {
|
||||
return protoLEV.sizeOf(value, 'nbt')
|
||||
}
|
||||
|
||||
function readEntityMetadata(buffer, offset, _ref) {
|
||||
var type = _ref.type;
|
||||
var endVal = _ref.endVal;
|
||||
// Little Endian
|
||||
|
||||
var cursor = offset;
|
||||
var metadata = [];
|
||||
var item = undefined;
|
||||
function readNbtLE (buffer, offset) {
|
||||
const r = protoLE.read(buffer, offset, 'nbt')
|
||||
// End size is 3 for some reason
|
||||
if (r.value.type === 'end') return { value: r.value, size: 1 }
|
||||
return r
|
||||
}
|
||||
|
||||
function writeNbtLE (value, buffer, offset) {
|
||||
if (value.type === 'end') {
|
||||
buffer.writeInt8(0, offset)
|
||||
return offset + 1
|
||||
}
|
||||
return protoLE.write(value, buffer, offset, 'nbt')
|
||||
}
|
||||
|
||||
function sizeOfNbtLE (value) {
|
||||
if (value.type === 'end') return 1
|
||||
return protoLE.sizeOf(value, 'nbt')
|
||||
}
|
||||
|
||||
function readEntityMetadata (buffer, offset, _ref) {
|
||||
const type = _ref.type
|
||||
const endVal = _ref.endVal
|
||||
|
||||
let cursor = offset
|
||||
const metadata = []
|
||||
let item
|
||||
while (true) {
|
||||
if (offset + 1 > buffer.length) throw new PartialReadError();
|
||||
item = buffer.readUInt8(cursor);
|
||||
if (offset + 1 > buffer.length) throw new Error('Reached end of buffer')
|
||||
item = buffer.readUInt8(cursor)
|
||||
if (item === endVal) {
|
||||
return {
|
||||
value: metadata,
|
||||
size: cursor + 1 - offset
|
||||
};
|
||||
}
|
||||
}
|
||||
var results = this.read(buffer, cursor, type, {});
|
||||
metadata.push(results.value);
|
||||
cursor += results.size;
|
||||
const results = this.read(buffer, cursor, type, {})
|
||||
metadata.push(results.value)
|
||||
cursor += results.size
|
||||
}
|
||||
}
|
||||
|
||||
function writeEntityMetadata(value, buffer, offset, _ref2) {
|
||||
var type = _ref2.type;
|
||||
var endVal = _ref2.endVal;
|
||||
function writeEntityMetadata (value, buffer, offset, _ref2) {
|
||||
const type = _ref2.type
|
||||
const endVal = _ref2.endVal
|
||||
|
||||
var self = this;
|
||||
const self = this
|
||||
value.forEach(function (item) {
|
||||
offset = self.write(item, buffer, offset, type, {});
|
||||
});
|
||||
buffer.writeUInt8(endVal, offset);
|
||||
return offset + 1;
|
||||
offset = self.write(item, buffer, offset, type, {})
|
||||
})
|
||||
buffer.writeUInt8(endVal, offset)
|
||||
return offset + 1
|
||||
}
|
||||
|
||||
function sizeOfEntityMetadata(value, _ref3) {
|
||||
var type = _ref3.type;
|
||||
function sizeOfEntityMetadata (value, _ref3) {
|
||||
const type = _ref3.type
|
||||
|
||||
var size = 1;
|
||||
for (var i = 0; i < value.length; ++i) {
|
||||
size += this.sizeOf(value[i], type, {});
|
||||
let size = 1
|
||||
for (let i = 0; i < value.length; ++i) {
|
||||
size += this.sizeOf(value[i], type, {})
|
||||
}
|
||||
return size;
|
||||
return size
|
||||
}
|
||||
|
||||
function readIpAddress(buffer, offset) {
|
||||
var address = buffer[offset] + '.' + buffer[offset+1] + '.' + buffer[offset+2] + '.' + buffer[offset+3];
|
||||
function readIpAddress (buffer, offset) {
|
||||
const address = buffer[offset] + '.' + buffer[offset + 1] + '.' + buffer[offset + 2] + '.' + buffer[offset + 3]
|
||||
return {
|
||||
size: 4,
|
||||
value: address
|
||||
}
|
||||
}
|
||||
|
||||
function writeIpAddress(value, buffer, offset) {
|
||||
var address = value.split('.');
|
||||
function writeIpAddress (value, buffer, offset) {
|
||||
const address = value.split('.')
|
||||
|
||||
address.forEach(function(b) {
|
||||
buffer[offset] = parseInt(b);
|
||||
offset++;
|
||||
});
|
||||
address.forEach(function (b) {
|
||||
buffer[offset] = parseInt(b)
|
||||
offset++
|
||||
})
|
||||
|
||||
return offset;
|
||||
return offset
|
||||
}
|
||||
|
||||
function readEndOfArray(buffer, offset, typeArgs) {
|
||||
var type=typeArgs.type;
|
||||
var cursor = offset;
|
||||
var elements = [];
|
||||
while(cursor<buffer.length) {
|
||||
var results = this.read(buffer, cursor, type, {});
|
||||
elements.push(results.value);
|
||||
cursor += results.size;
|
||||
function readEndOfArray (buffer, offset, typeArgs) {
|
||||
const type = typeArgs.type
|
||||
let cursor = offset
|
||||
const elements = []
|
||||
while (cursor < buffer.length) {
|
||||
const results = this.read(buffer, cursor, type, {})
|
||||
elements.push(results.value)
|
||||
cursor += results.size
|
||||
}
|
||||
return {
|
||||
value: elements,
|
||||
size: cursor - offset
|
||||
};
|
||||
}
|
||||
|
||||
function writeEndOfArray(value, buffer, offset,typeArgs) {
|
||||
var type=typeArgs.type;
|
||||
var self = this;
|
||||
value.forEach(function(item) {
|
||||
offset = self.write(item, buffer, offset, type, {});
|
||||
});
|
||||
return offset;
|
||||
}
|
||||
|
||||
function sizeOfEndOfArray(value, typeArgs) {
|
||||
var type=typeArgs.type;
|
||||
var size = 0;
|
||||
for(var i = 0; i < value.length; ++i) {
|
||||
size += this.sizeOf(value[i], type, {});
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
function writeEndOfArray (value, buffer, offset, typeArgs) {
|
||||
const type = typeArgs.type
|
||||
const self = this
|
||||
value.forEach(function (item) {
|
||||
offset = self.write(item, buffer, offset, type, {})
|
||||
})
|
||||
return offset
|
||||
}
|
||||
|
||||
function sizeOfEndOfArray (value, typeArgs) {
|
||||
const type = typeArgs.type
|
||||
let size = 0
|
||||
for (let i = 0; i < value.length; ++i) {
|
||||
size += this.sizeOf(value[i], type, {})
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
'uuid': [readUUID, writeUUID, 16],
|
||||
'nbt': [readNbt, writeNbt, sizeOfNbt],
|
||||
'entityMetadataLoop': [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata],
|
||||
'ipAddress': [readIpAddress, writeIpAddress, 4],
|
||||
'endOfArray':[readEndOfArray,writeEndOfArray,sizeOfEndOfArray]
|
||||
};
|
||||
uuid: [readUUID, writeUUID, 16],
|
||||
nbt: [readNbt, writeNbt, sizeOfNbt],
|
||||
lnbt: [readNbtLE, writeNbtLE, sizeOfNbtLE],
|
||||
entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata],
|
||||
ipAddress: [readIpAddress, writeIpAddress, 4],
|
||||
endOfArray: [readEndOfArray, writeEndOfArray, sizeOfEndOfArray]
|
||||
}
|
||||
|
|
|
|||
48
src/datatypes/util.js
Normal file
48
src/datatypes/util.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
const fs = require('fs')
|
||||
const UUID = require('uuid-1345')
|
||||
|
||||
function getFiles (dir) {
|
||||
let results = []
|
||||
const list = fs.readdirSync(dir)
|
||||
list.forEach((file) => {
|
||||
file = dir + '/' + file
|
||||
const stat = fs.statSync(file)
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(getFiles(file))
|
||||
} else {
|
||||
results.push(file)
|
||||
}
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
function sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function waitFor (cb, withTimeout, onTimeout) {
|
||||
let t
|
||||
const ret = await Promise.race([
|
||||
new Promise((resolve, reject) => cb(resolve, reject)),
|
||||
new Promise(resolve => { t = setTimeout(() => resolve('timeout'), withTimeout) })
|
||||
])
|
||||
clearTimeout(t)
|
||||
if (ret === 'timeout') await onTimeout()
|
||||
return ret
|
||||
}
|
||||
|
||||
function serialize (obj = {}, fmt) {
|
||||
return JSON.stringify(obj, (k, v) => typeof v === 'bigint' ? v.toString() : v, fmt)
|
||||
}
|
||||
|
||||
function uuidFrom (string) {
|
||||
return UUID.v3({ namespace: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', name: string })
|
||||
}
|
||||
|
||||
function nextUUID () {
|
||||
return uuidFrom(Date.now().toString())
|
||||
}
|
||||
|
||||
const isDebug = process.env.DEBUG?.includes('minecraft-protocol')
|
||||
|
||||
module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom, nextUUID, isDebug }
|
||||
3
src/handshake/constants.js
Normal file
3
src/handshake/constants.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
PUBLIC_KEY: 'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp'
|
||||
}
|
||||
93
src/handshake/keyExchange.js
Normal file
93
src/handshake/keyExchange.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
const { ClientStatus } = require('../connection')
|
||||
const JWT = require('jsonwebtoken')
|
||||
const crypto = require('crypto')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
const SALT = '🧂'
|
||||
const curve = 'secp384r1'
|
||||
const pem = { format: 'pem', type: 'sec1' }
|
||||
const der = { format: 'der', type: 'spki' }
|
||||
|
||||
function KeyExchange (client, server, options) {
|
||||
// Generate a key pair at program start up
|
||||
client.ecdhKeyPair = crypto.generateKeyPairSync('ec', { namedCurve: curve })
|
||||
client.publicKeyDER = client.ecdhKeyPair.publicKey.export(der)
|
||||
client.privateKeyPEM = client.ecdhKeyPair.privateKey.export(pem)
|
||||
client.clientX509 = client.publicKeyDER.toString('base64')
|
||||
|
||||
function startClientboundEncryption (publicKey) {
|
||||
debug('[encrypt] Client pub key base64: ', publicKey)
|
||||
|
||||
const pubKeyDer = crypto.createPublicKey({ key: Buffer.from(publicKey.key, 'base64'), ...der })
|
||||
// Shared secret from the client's public key + our private key
|
||||
client.sharedSecret = crypto.diffieHellman({ privateKey: client.ecdhKeyPair.privateKey, publicKey: pubKeyDer })
|
||||
|
||||
// Secret hash we use for packet encryption:
|
||||
// From the public key of the remote and the private key
|
||||
// of the local, a shared secret is generated using ECDH.
|
||||
// The secret key bytes are then computed as
|
||||
// sha256(server_token + shared_secret). These secret key
|
||||
// bytes are 32 bytes long.
|
||||
const secretHash = crypto.createHash('sha256')
|
||||
secretHash.update(SALT)
|
||||
secretHash.update(client.sharedSecret)
|
||||
|
||||
client.secretKeyBytes = secretHash.digest()
|
||||
|
||||
const token = JWT.sign({
|
||||
salt: toBase64(SALT),
|
||||
signedToken: client.clientX509
|
||||
}, client.ecdhKeyPair.privateKey, { algorithm: 'ES384', header: { x5u: client.clientX509 } })
|
||||
|
||||
client.write('server_to_client_handshake', { token })
|
||||
|
||||
// The encryption scheme is AES/CFB8/NoPadding with the
|
||||
// secret key being the result of the sha256 above and
|
||||
// the IV being the first 16 bytes of this secret key.
|
||||
const initial = client.secretKeyBytes.slice(0, 16)
|
||||
client.startEncryption(initial)
|
||||
}
|
||||
|
||||
function startServerboundEncryption (token) {
|
||||
debug('[encrypt] Starting serverbound encryption', token)
|
||||
const jwt = token?.token
|
||||
if (!jwt) {
|
||||
throw Error('Server did not return a valid JWT, cannot start encryption')
|
||||
}
|
||||
|
||||
// No verification here, not needed
|
||||
|
||||
const [header, payload] = jwt.split('.').map(k => Buffer.from(k, 'base64'))
|
||||
const head = JSON.parse(String(header))
|
||||
const body = JSON.parse(String(payload))
|
||||
|
||||
const pubKeyDer = crypto.createPublicKey({ key: Buffer.from(head.x5u, 'base64'), ...der })
|
||||
|
||||
// Shared secret from the client's public key + our private key
|
||||
client.sharedSecret = crypto.diffieHellman({ privateKey: client.ecdhKeyPair.privateKey, publicKey: pubKeyDer })
|
||||
|
||||
const salt = Buffer.from(body.salt, 'base64')
|
||||
const secretHash = crypto.createHash('sha256')
|
||||
secretHash.update(salt)
|
||||
secretHash.update(client.sharedSecret)
|
||||
|
||||
client.secretKeyBytes = secretHash.digest()
|
||||
const iv = client.secretKeyBytes.slice(0, 16)
|
||||
client.startEncryption(iv)
|
||||
|
||||
// It works! First encrypted packet :)
|
||||
|
||||
client.write('client_to_server_handshake', {})
|
||||
this.emit('join')
|
||||
client.status = ClientStatus.Initializing
|
||||
}
|
||||
|
||||
client.on('server.client_handshake', startClientboundEncryption)
|
||||
client.on('client.server_handshake', startServerboundEncryption)
|
||||
}
|
||||
|
||||
function toBase64 (string) {
|
||||
return Buffer.from(string).toString('base64')
|
||||
}
|
||||
|
||||
module.exports = { KeyExchange }
|
||||
80
src/handshake/login.js
Normal file
80
src/handshake/login.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
const JWT = require('jsonwebtoken')
|
||||
const { nextUUID } = require('../datatypes/util')
|
||||
const { PUBLIC_KEY } = require('./constants')
|
||||
const algorithm = 'ES384'
|
||||
|
||||
module.exports = (client, server, options) => {
|
||||
const skinData = require('minecraft-data')('bedrock_' + options.version).defaultSkin
|
||||
|
||||
client.createClientChain = (mojangKey, offline) => {
|
||||
const privateKey = client.ecdhKeyPair.privateKey
|
||||
|
||||
let token
|
||||
if (offline) {
|
||||
const payload = {
|
||||
extraData: {
|
||||
displayName: client.username,
|
||||
identity: client.profile.uuid,
|
||||
titleId: '89692877',
|
||||
XUID: '0'
|
||||
},
|
||||
certificateAuthority: true,
|
||||
identityPublicKey: client.clientX509
|
||||
}
|
||||
token = JWT.sign(payload, privateKey, { algorithm, notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509, typ: undefined } })
|
||||
} else {
|
||||
token = JWT.sign({
|
||||
identityPublicKey: mojangKey || PUBLIC_KEY,
|
||||
certificateAuthority: true
|
||||
}, privateKey, { algorithm, header: { x5u: client.clientX509, typ: undefined } })
|
||||
}
|
||||
|
||||
client.clientIdentityChain = token
|
||||
client.createClientUserChain(privateKey)
|
||||
}
|
||||
|
||||
client.createClientUserChain = (privateKey) => {
|
||||
let payload = {
|
||||
...skinData,
|
||||
|
||||
ClientRandomId: Date.now(),
|
||||
CurrentInputMode: 1,
|
||||
DefaultInputMode: 1,
|
||||
DeviceId: nextUUID(),
|
||||
DeviceModel: 'PrismarineJS',
|
||||
DeviceOS: client.session?.deviceOS || 7,
|
||||
GameVersion: options.version || '1.16.201',
|
||||
GuiScale: -1,
|
||||
LanguageCode: 'en_GB', // TODO locale
|
||||
GraphicsMode: 1, // 1:simple, 2:fancy, 3:advanced, 4:ray_traced
|
||||
|
||||
PlatformOfflineId: '',
|
||||
PlatformOnlineId: '', // chat
|
||||
// PlayFabID is the PlayFab ID produced for the skin. PlayFab is the company that hosts the Marketplace,
|
||||
// skins and other related features from the game. This ID is the ID of the skin used to store the skin
|
||||
// inside of PlayFab.The playfab ID is always lowercased.
|
||||
PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16).toLowerCase(), // 1.16.210
|
||||
|
||||
SelfSignedId: nextUUID(),
|
||||
ServerAddress: `${options.host}:${options.port}`,
|
||||
|
||||
ThirdPartyName: client.profile.name, // Gamertag
|
||||
ThirdPartyNameOnly: client.versionGreaterThanOrEqualTo('1.21.90') ? undefined : false,
|
||||
UIProfile: 0,
|
||||
|
||||
IsEditorMode: false,
|
||||
TrustedSkin: client.versionGreaterThanOrEqualTo('1.19.20') ? false : undefined,
|
||||
OverrideSkin: client.versionGreaterThanOrEqualTo('1.19.62') ? false : undefined,
|
||||
CompatibleWithClientSideChunkGen: client.versionGreaterThanOrEqualTo('1.19.80') ? false : undefined,
|
||||
|
||||
MaxViewDistance: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined,
|
||||
MemoryTier: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined,
|
||||
PlatformType: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined
|
||||
}
|
||||
const customPayload = options.skinData || {}
|
||||
payload = { ...payload, ...customPayload }
|
||||
payload.ServerAddress = `${options.host}:${options.port}`
|
||||
|
||||
client.clientUserChain = JWT.sign(payload, privateKey, { algorithm, header: { x5u: client.clientX509, typ: undefined }, noTimestamp: true /* pocketmine.. */ })
|
||||
}
|
||||
}
|
||||
74
src/handshake/loginVerify.js
Normal file
74
src/handshake/loginVerify.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
const JWT = require('jsonwebtoken')
|
||||
const constants = require('./constants')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const crypto = require('crypto')
|
||||
|
||||
module.exports = (client, server, options) => {
|
||||
// Refer to the docs:
|
||||
// https://web.archive.org/web/20180917171505if_/https://confluence.yawk.at/display/PEPROTOCOL/Game+Packets#GamePackets-Login
|
||||
|
||||
const getDER = b64 => crypto.createPublicKey({ key: Buffer.from(b64, 'base64'), format: 'der', type: 'spki' })
|
||||
|
||||
function verifyAuth (chain) {
|
||||
let data = {}
|
||||
|
||||
// There are three JWT tokens sent to us, one signed by the client
|
||||
// one signed by Mojang with the Mojang token we have and another one
|
||||
// from Xbox with addition user profile data
|
||||
// We verify that at least one of the tokens in the chain has been properly
|
||||
// signed by Mojang by checking the x509 public key in the JWT headers
|
||||
let didVerify = false
|
||||
|
||||
let pubKey = getDER(getX5U(chain[0])) // the first one is client signed, allow it
|
||||
let finalKey = null
|
||||
|
||||
for (const token of chain) {
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] })
|
||||
|
||||
// Check if signed by Mojang key
|
||||
const x5u = getX5U(token)
|
||||
if (x5u === constants.PUBLIC_KEY && !data.extraData?.XUID) {
|
||||
didVerify = true
|
||||
debug('Verified client with mojang key', x5u)
|
||||
}
|
||||
|
||||
pubKey = decoded.identityPublicKey ? getDER(decoded.identityPublicKey) : x5u
|
||||
finalKey = decoded.identityPublicKey || finalKey // non pem
|
||||
data = { ...data, ...decoded }
|
||||
}
|
||||
|
||||
if (!didVerify && !options.offline) {
|
||||
client.disconnect('disconnectionScreen.notAuthenticated')
|
||||
}
|
||||
|
||||
return { key: finalKey, data }
|
||||
}
|
||||
|
||||
function verifySkin (publicKey, token) {
|
||||
const pubKey = getDER(publicKey)
|
||||
const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] })
|
||||
return decoded
|
||||
}
|
||||
|
||||
client.decodeLoginJWT = (authTokens, skinTokens) => {
|
||||
const { key, data } = verifyAuth(authTokens)
|
||||
const skinData = verifySkin(key, skinTokens)
|
||||
return { key, userData: data, skinData }
|
||||
}
|
||||
|
||||
client.encodeLoginJWT = (localChain, mojangChain) => {
|
||||
const chains = []
|
||||
chains.push(localChain)
|
||||
for (const chain of mojangChain) {
|
||||
chains.push(chain)
|
||||
}
|
||||
return chains
|
||||
}
|
||||
}
|
||||
|
||||
function getX5U (token) {
|
||||
const [header] = token.split('.')
|
||||
const hdec = Buffer.from(header, 'base64').toString('utf-8')
|
||||
const hjson = JSON.parse(hdec)
|
||||
return hjson.x5u
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
createSerializer: require("./transforms/serializer").createSerializer,
|
||||
createDeserializer: require("./transforms/serializer").createDeserializer,
|
||||
createServer: require("./createServer"),
|
||||
createClient: require("./createClient")
|
||||
};
|
||||
52
src/options.js
Normal file
52
src/options.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
const mcData = require('minecraft-data')
|
||||
|
||||
// Minimum supported version (< will be kicked)
|
||||
const MIN_VERSION = '1.16.201'
|
||||
// Currently supported verson. Note, clients with newer versions can still connect as long as data is in minecraft-data
|
||||
const CURRENT_VERSION = '1.21.111'
|
||||
|
||||
const Versions = Object.fromEntries(mcData.versions.bedrock.filter(e => e.releaseType === 'release').map(e => [e.minecraftVersion, e.version]))
|
||||
|
||||
// Skip some low priority versions (middle major) on Github Actions to allow faster CI
|
||||
const skippedVersionsOnGithubCI = ['1.16.210', '1.17.10', '1.17.30', '1.18.11', '1.19.10', '1.19.20', '1.19.30', '1.19.40', '1.19.50', '1.19.60', '1.19.63', '1.19.70', '1.20.10', '1.20.15', '1.20.30', '1.20.40', '1.20.50', '1.20.61', '1.20.71', '1.21.2', '1.21.20', '1.21.30', '1.21.42', '1.21.50', '1.21.60', '1.21.70', '1.21.80', '1.21.90']
|
||||
const testedVersions = process.env.CI ? Object.keys(Versions).filter(v => !skippedVersionsOnGithubCI.includes(v)) : Object.keys(Versions)
|
||||
|
||||
const defaultOptions = {
|
||||
// https://minecraft.wiki/w/Protocol_version#Bedrock_Edition_2
|
||||
version: CURRENT_VERSION,
|
||||
// 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
|
||||
// client.status to ClientStatus.Initialized after sending the init packet.
|
||||
autoInitPlayer: true,
|
||||
// If true, do not authenticate with Xbox Live
|
||||
offline: false,
|
||||
// Milliseconds to wait before aborting connection attempt
|
||||
connectTimeout: 9000,
|
||||
// Specifies the raknet implementation to use
|
||||
raknetBackend: 'raknet-native',
|
||||
// If using JS implementation of RakNet, should we use workers? (This only affects the client)
|
||||
useRaknetWorkers: true,
|
||||
|
||||
// server: What compression algorithm to use by default, either `none`, `deflate` or `snappy`
|
||||
compressionAlgorithm: 'deflate',
|
||||
// server and client: On Deflate, what compression level to use, between 1 and 9
|
||||
compressionLevel: 7,
|
||||
// server: If true, only compress if a payload is larger than compressionThreshold
|
||||
compressionThreshold: 512
|
||||
}
|
||||
|
||||
function validateOptions (options) {
|
||||
if (!Versions[options.version]) {
|
||||
console.warn('Supported versions', Versions)
|
||||
throw Error(`Unsupported version ${options.version}`)
|
||||
}
|
||||
|
||||
options.protocolVersion = Versions[options.version]
|
||||
if (options.protocolVersion < MIN_VERSION) {
|
||||
throw new Error(`Protocol version < ${MIN_VERSION} : ${options.protocolVersion}, too old`)
|
||||
}
|
||||
if (options.useNativeRaknet === true) options.raknetBackend = 'raknet-native'
|
||||
if (options.useNativeRaknet === false) options.raknetBackend = 'jsp-raknet'
|
||||
}
|
||||
|
||||
module.exports = { defaultOptions, MIN_VERSION, CURRENT_VERSION, Versions, validateOptions, testedVersions }
|
||||
270
src/rak.js
Normal file
270
src/rak.js
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
const { EventEmitter } = require('events')
|
||||
const ConnWorker = require('./rakWorker')
|
||||
const { waitFor } = require('./datatypes/util')
|
||||
|
||||
let Client, Server, PacketPriority, EncapsulatedPacket, PacketReliability, Reliability
|
||||
class RakTimeout extends Error {};
|
||||
|
||||
function setBackend (backend) {
|
||||
// We have to explicitly require the backend for bundlers
|
||||
switch (backend) {
|
||||
case 'raknet-node':
|
||||
({ Client, Server, PacketPriority, PacketReliability } = require('raknet-node'))
|
||||
return { RakServer: RakNativeServer, RakClient: RakNativeClient, RakTimeout }
|
||||
case 'raknet-native':
|
||||
({ Client, Server, PacketPriority, PacketReliability } = require('raknet-native'))
|
||||
return { RakServer: RakNativeServer, RakClient: RakNativeClient, RakTimeout }
|
||||
case 'jsp-raknet':
|
||||
({ Client, Server, EncapsulatedPacket, Reliability } = require('jsp-raknet'))
|
||||
return { RakServer: RakJsServer, RakClient: RakJsClient, RakTimeout }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (backend) => {
|
||||
if (backend) {
|
||||
return setBackend(backend)
|
||||
} else {
|
||||
try {
|
||||
return setBackend('raknet-native')
|
||||
} catch (e) {
|
||||
console.debug(`[raknet] ${backend} library not found, defaulting to jsp-raknet. Correct the "raknetBackend" option to avoid this error.`, e)
|
||||
return setBackend('jsp-raknet')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RakNativeClient extends EventEmitter {
|
||||
constructor (options, client) {
|
||||
super()
|
||||
this.connected = false
|
||||
this.onConnected = () => { }
|
||||
this.onCloseConnection = () => { }
|
||||
this.onEncapsulated = () => { }
|
||||
|
||||
const protocolVersion = client?.versionGreaterThanOrEqualTo('1.19.30') ? 11 : 10
|
||||
this.raknet = new Client(options.host, options.port, { protocolVersion })
|
||||
this.raknet.on('encapsulated', ({ buffer, address }) => {
|
||||
if (this.connected) { // Discard packets that are queued to be sent to us after close
|
||||
this.onEncapsulated(buffer, address)
|
||||
}
|
||||
})
|
||||
|
||||
this.raknet.on('connect', () => {
|
||||
this.connected = true
|
||||
this.onConnected()
|
||||
})
|
||||
|
||||
this.raknet.on('disconnect', ({ reason }) => {
|
||||
this.connected = false
|
||||
this.onCloseConnection(reason)
|
||||
})
|
||||
}
|
||||
|
||||
async ping (timeout = 1000) {
|
||||
this.raknet.ping()
|
||||
return waitFor((done) => {
|
||||
this.raknet.on('pong', (ret) => {
|
||||
if (ret.extra) {
|
||||
done(ret.extra.toString())
|
||||
}
|
||||
})
|
||||
}, 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 () {
|
||||
this.raknet.connect()
|
||||
}
|
||||
|
||||
close () {
|
||||
this.connected = false
|
||||
setTimeout(() => {
|
||||
this.raknet.close()
|
||||
}, 40)
|
||||
}
|
||||
|
||||
sendReliable (buffer, immediate) {
|
||||
if (!this.connected) return
|
||||
const priority = immediate ? PacketPriority.IMMEDIATE_PRIORITY : PacketPriority.MEDIUM_PRIORITY
|
||||
return this.raknet.send(buffer, priority, PacketReliability.RELIABLE_ORDERED, 0)
|
||||
}
|
||||
}
|
||||
|
||||
class RakNativeServer extends EventEmitter {
|
||||
constructor (options = {}, server) {
|
||||
super()
|
||||
this.onOpenConnection = () => { }
|
||||
this.onCloseConnection = () => { }
|
||||
this.onEncapsulated = () => { }
|
||||
this.raknet = new Server(options.host, options.port, {
|
||||
maxConnections: options.maxPlayers || 3,
|
||||
protocolVersion: server.versionLessThan('1.19.30') ? 10 : 11,
|
||||
message: server.getAdvertisement().toBuffer()
|
||||
})
|
||||
this.onClose = () => {}
|
||||
|
||||
this.updateAdvertisement = () => {
|
||||
this.raknet.setOfflineMessage(server.getAdvertisement().toBuffer())
|
||||
}
|
||||
|
||||
this.raknet.on('openConnection', (client) => {
|
||||
client.sendReliable = function (buffer, immediate) {
|
||||
const priority = immediate ? PacketPriority.IMMEDIATE_PRIORITY : PacketPriority.MEDIUM_PRIORITY
|
||||
return this.send(buffer, priority, PacketReliability.RELIABLE_ORDERED, 0)
|
||||
}
|
||||
this.onOpenConnection(client)
|
||||
})
|
||||
|
||||
this.raknet.on('closeConnection', (client) => {
|
||||
this.onCloseConnection(client)
|
||||
})
|
||||
|
||||
this.raknet.on('encapsulated', ({ buffer, address }) => {
|
||||
this.onEncapsulated(buffer, address)
|
||||
})
|
||||
|
||||
this.raknet.on('close', (reason) => this.onClose(reason))
|
||||
}
|
||||
|
||||
listen () {
|
||||
this.raknet.listen()
|
||||
}
|
||||
|
||||
close () {
|
||||
this.raknet.close()
|
||||
}
|
||||
}
|
||||
|
||||
class RakJsClient extends EventEmitter {
|
||||
constructor (options = {}) {
|
||||
super()
|
||||
this.options = options
|
||||
this.onConnected = () => { }
|
||||
this.onCloseConnection = () => { }
|
||||
this.onEncapsulated = () => { }
|
||||
if (options.useWorkers) {
|
||||
this.connect = this.workerConnect
|
||||
this.close = reason => this.worker?.postMessage({ type: 'close', reason })
|
||||
this.sendReliable = this.workerSendReliable
|
||||
} else {
|
||||
this.connect = this.plainConnect
|
||||
this.close = reason => this.raknet.close(reason)
|
||||
this.sendReliable = this.plainSendReliable
|
||||
}
|
||||
this.pongCb = null
|
||||
}
|
||||
|
||||
workerConnect (host = this.options.host, port = this.options.port) {
|
||||
this.worker = ConnWorker.connect(host, port)
|
||||
|
||||
this.worker.on('message', (evt) => {
|
||||
switch (evt.type) {
|
||||
case 'connected': {
|
||||
this.onConnected()
|
||||
break
|
||||
}
|
||||
case 'encapsulated': {
|
||||
const [ecapsulated, address] = evt.args
|
||||
this.onEncapsulated(ecapsulated, address.hash)
|
||||
break
|
||||
}
|
||||
case 'pong':
|
||||
this.pongCb?.(evt.args)
|
||||
break
|
||||
case 'disconnect':
|
||||
this.onCloseConnection()
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async plainConnect (host = this.options.host, port = this.options.port) {
|
||||
this.raknet = new Client(host, port)
|
||||
await this.raknet.connect()
|
||||
|
||||
this.raknet.on('connecting', () => {
|
||||
console.log(`[client] connecting to ${host}/${port}`)
|
||||
})
|
||||
|
||||
this.raknet.on('connected', this.onConnected)
|
||||
this.raknet.on('encapsulated', (encapsulated, addr) => this.onEncapsulated(encapsulated, addr.hash))
|
||||
this.raknet.on('disconnect', (reason) => this.onCloseConnection(reason))
|
||||
}
|
||||
|
||||
workerSendReliable (buffer, immediate) {
|
||||
this.worker.postMessage({ type: 'queueEncapsulated', packet: buffer, immediate })
|
||||
}
|
||||
|
||||
plainSendReliable (buffer, immediate) {
|
||||
const sendPacket = new EncapsulatedPacket()
|
||||
sendPacket.reliability = Reliability.ReliableOrdered
|
||||
sendPacket.buffer = buffer
|
||||
this.raknet.connection.addEncapsulatedToQueue(sendPacket)
|
||||
if (immediate) this.raknet.connection.sendQueue()
|
||||
}
|
||||
|
||||
async ping (timeout = 1000) {
|
||||
if (this.worker) {
|
||||
this.worker.postMessage({ type: 'ping' })
|
||||
return waitFor(res => {
|
||||
this.pongCb = data => res(data)
|
||||
}, timeout, () => { throw new RakTimeout('Ping timed out') })
|
||||
} else {
|
||||
if (!this.raknet) this.raknet = new Client(this.options.host, this.options.port)
|
||||
return waitFor(res => {
|
||||
this.raknet.ping(data => {
|
||||
this.raknet.close()
|
||||
res(data)
|
||||
})
|
||||
}, timeout, () => { throw new RakTimeout('Ping timed out') })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RakJsServer extends EventEmitter {
|
||||
constructor (options = {}, server) {
|
||||
super()
|
||||
this.options = options
|
||||
this.server = server
|
||||
this.onOpenConnection = () => { }
|
||||
this.onCloseConnection = () => { }
|
||||
this.onEncapsulated = (packet, address) => server.onEncapsulated(packet.buffer, address)
|
||||
this.onClose = () => {}
|
||||
this.updateAdvertisement = () => {
|
||||
this.raknet.setPongAdvertisement(server.getAdvertisement())
|
||||
}
|
||||
if (options.useWorkers) {
|
||||
throw Error('nyi')
|
||||
} else {
|
||||
this.listen = this.plainListen
|
||||
}
|
||||
}
|
||||
|
||||
async plainListen () {
|
||||
this.raknet = new Server(this.options.host, this.options.port, this.server.getAdvertisement())
|
||||
await this.raknet.listen(this.options.host, this.options.port)
|
||||
this.raknet.on('openConnection', (conn) => {
|
||||
conn.sendReliable = (buffer, immediate) => {
|
||||
const sendPacket = new EncapsulatedPacket()
|
||||
sendPacket.reliability = Reliability.ReliableOrdered
|
||||
sendPacket.buffer = buffer
|
||||
conn.addEncapsulatedToQueue(sendPacket, immediate ? 1 : 0)
|
||||
}
|
||||
this.onOpenConnection(conn)
|
||||
})
|
||||
this.raknet.on('closeConnection', this.onCloseConnection)
|
||||
this.raknet.on('encapsulated', this.onEncapsulated)
|
||||
this.raknet.on('close', this.onClose)
|
||||
}
|
||||
|
||||
close () {
|
||||
// Allow some time for the final packets to come in/out
|
||||
setTimeout(() => {
|
||||
this.raknet.close()
|
||||
}, 40)
|
||||
}
|
||||
}
|
||||
69
src/rakWorker.js
Normal file
69
src/rakWorker.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
const { Worker, isMainThread, parentPort } = require('worker_threads')
|
||||
const { Client, EncapsulatedPacket, Reliability } = require('jsp-raknet')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
function connect (host, port) {
|
||||
if (isMainThread) {
|
||||
const worker = new Worker(__filename)
|
||||
worker.postMessage({ type: 'connect', host, port })
|
||||
return worker
|
||||
}
|
||||
}
|
||||
|
||||
let raknet
|
||||
|
||||
function main () {
|
||||
parentPort.on('message', (evt) => {
|
||||
if (evt.type === 'connect') {
|
||||
const { host, port } = evt
|
||||
raknet = new Client(host, port)
|
||||
|
||||
raknet.connect().then(() => {
|
||||
debug('Raknet Connected!')
|
||||
})
|
||||
|
||||
raknet.on('connecting', () => {
|
||||
debug(`[client] connecting to ${host}/${port}`)
|
||||
parentPort.postMessage('message', { type: 'connecting' })
|
||||
})
|
||||
|
||||
raknet.once('connected', (connection) => {
|
||||
debug('[worker] connected!')
|
||||
globalThis.raknetConnection = connection
|
||||
parentPort.postMessage({ type: 'connected' })
|
||||
})
|
||||
|
||||
raknet.on('encapsulated', (...args) => {
|
||||
parentPort.postMessage({ type: 'encapsulated', args })
|
||||
})
|
||||
|
||||
raknet.on('disconnect', (reason) => {
|
||||
debug('[worker] disconnected!')
|
||||
parentPort.postMessage({ type: 'disconnect', reason })
|
||||
})
|
||||
|
||||
raknet.on('raw', (buffer, inetAddr) => {
|
||||
debug('Raw packet', buffer, inetAddr)
|
||||
})
|
||||
} else if (evt.type === 'queueEncapsulated') {
|
||||
const sendPacket = new EncapsulatedPacket()
|
||||
sendPacket.reliability = Reliability.ReliableOrdered
|
||||
sendPacket.buffer = evt.packet
|
||||
|
||||
globalThis.raknetConnection?.addEncapsulatedToQueue(sendPacket)
|
||||
if (evt.immediate) {
|
||||
globalThis.raknetConnection?.sendQueue()
|
||||
}
|
||||
} else if (evt.type === 'close') {
|
||||
raknet.close()
|
||||
process.exit(0)
|
||||
} else if (evt.type === 'ping') {
|
||||
raknet.ping((args) => {
|
||||
parentPort.postMessage({ type: 'pong', args })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!isMainThread) main()
|
||||
module.exports = { connect }
|
||||
292
src/relay.js
Normal file
292
src/relay.js
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
const { Client } = require('./client')
|
||||
const { Server } = require('./server')
|
||||
const { Player } = require('./serverPlayer')
|
||||
const { realmAuthenticate } = require('./client/auth')
|
||||
const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol')
|
||||
|
||||
const debugging = false // Do re-encoding tests
|
||||
|
||||
class RelayPlayer extends Player {
|
||||
constructor (server, conn) {
|
||||
super(server, conn)
|
||||
|
||||
this.startRelaying = false
|
||||
this.once('join', () => { // The client has joined our proxy
|
||||
this.flushDownQueue() // Send queued packets from the upstream backend
|
||||
this.startRelaying = true
|
||||
})
|
||||
this.downQ = []
|
||||
this.upQ = []
|
||||
this.upInLog = (...msg) => console.debug('* Backend -> Proxy', ...msg)
|
||||
this.upOutLog = (...msg) => console.debug('* Proxy -> Backend', ...msg)
|
||||
this.downInLog = (...msg) => console.debug('* Client -> Proxy', ...msg)
|
||||
this.downOutLog = (...msg) => console.debug('* Proxy -> Client', ...msg)
|
||||
|
||||
if (!server.options.logging) {
|
||||
this.upInLog = () => { }
|
||||
this.upOutLog = () => { }
|
||||
this.downInLog = () => { }
|
||||
this.downOutLog = () => { }
|
||||
}
|
||||
|
||||
this.outLog = this.downOutLog
|
||||
this.inLog = this.downInLog
|
||||
this.chunkSendCache = []
|
||||
this.sentStartGame = false
|
||||
this.respawnPacket = []
|
||||
}
|
||||
|
||||
// Called when we get a packet from backend server (Backend -> PROXY -> Client)
|
||||
readUpstream (packet) {
|
||||
if (!this.startRelaying) {
|
||||
this.upInLog('Client not ready, queueing packet until join')
|
||||
this.downQ.push(packet)
|
||||
return
|
||||
}
|
||||
let des
|
||||
try {
|
||||
des = this.server.deserializer.parsePacketBuffer(packet)
|
||||
} catch (e) {
|
||||
this.server.deserializer.dumpFailedBuffer(packet, this.connection.address)
|
||||
console.error(this.connection.address, e)
|
||||
|
||||
if (!this.options.omitParseErrors) {
|
||||
this.disconnect('Server packet parse error')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
const name = des.data.name
|
||||
const params = des.data.params
|
||||
this.upInLog('->', name, params)
|
||||
|
||||
if (name === 'play_status' && params.status === 'login_success') return // Already sent this, this needs to be sent ASAP or client will disconnect
|
||||
|
||||
if (debugging) { // some packet encode/decode testing stuff
|
||||
this.server.deserializer.verify(des, this.server.serializer)
|
||||
}
|
||||
|
||||
this.emit('clientbound', des.data, des)
|
||||
|
||||
if (!des.canceled) {
|
||||
if (name === 'start_game') {
|
||||
setTimeout(() => {
|
||||
this.sentStartGame = true
|
||||
}, 500)
|
||||
} else if (name === 'level_chunk' && !this.sentStartGame) {
|
||||
this.chunkSendCache.push(params)
|
||||
return
|
||||
}
|
||||
|
||||
this.queue(name, params)
|
||||
}
|
||||
|
||||
if (this.chunkSendCache.length > 0 && this.sentStartGame) {
|
||||
for (const entry of this.chunkSendCache) {
|
||||
this.queue('level_chunk', entry)
|
||||
}
|
||||
this.chunkSendCache = []
|
||||
}
|
||||
}
|
||||
|
||||
// Send queued packets to the connected client
|
||||
flushDownQueue () {
|
||||
this.downOutLog('Flushing downstream queue')
|
||||
for (const packet of this.downQ) {
|
||||
const des = this.server.deserializer.parsePacketBuffer(packet)
|
||||
this.write(des.data.name, des.data.params)
|
||||
}
|
||||
this.downQ = []
|
||||
}
|
||||
|
||||
// Send queued packets to the backend upstream server from the client
|
||||
flushUpQueue () {
|
||||
this.upOutLog('Flushing upstream queue')
|
||||
for (const e of this.upQ) { // Send the queue
|
||||
const des = this.server.deserializer.parsePacketBuffer(e)
|
||||
if (des.data.name === 'client_cache_status') {
|
||||
// Currently not working, force off the chunk cache
|
||||
} else {
|
||||
this.upstream.write(des.data.name, des.data.params)
|
||||
}
|
||||
}
|
||||
this.upQ = []
|
||||
}
|
||||
|
||||
// Called when the server gets a packet from the downstream player (Client -> PROXY -> Backend)
|
||||
readPacket (packet) {
|
||||
// The downstream client conn is established & we got a packet to send to upstream server
|
||||
if (this.startRelaying) {
|
||||
// Upstream is still connecting/handshaking
|
||||
if (!this.upstream) {
|
||||
const des = this.server.deserializer.parsePacketBuffer(packet)
|
||||
this.downInLog('Got downstream connected packet but upstream is not connected yet, added to q', des)
|
||||
this.upQ.push(packet) // Put into a queue
|
||||
return
|
||||
}
|
||||
|
||||
// Send queued packets
|
||||
this.flushUpQueue()
|
||||
this.downInLog('recv', packet)
|
||||
|
||||
// TODO: If we fail to parse a packet, proxy it raw and log an error
|
||||
const des = this.server.deserializer.parsePacketBuffer(packet)
|
||||
|
||||
if (debugging) { // some packet encode/decode testing stuff
|
||||
this.server.deserializer.verify(des, this.server.serializer)
|
||||
}
|
||||
|
||||
this.emit('serverbound', des.data, des)
|
||||
if (des.canceled) return
|
||||
|
||||
switch (des.data.name) {
|
||||
case 'client_cache_status':
|
||||
// Force the chunk cache off.
|
||||
this.upstream.queue('client_cache_status', { enabled: this.enableChunkCaching })
|
||||
break
|
||||
case 'set_local_player_as_initialized':
|
||||
this.status = 3
|
||||
// falls through
|
||||
default:
|
||||
// Emit the packet as-is back to the upstream server
|
||||
this.downInLog('Relaying', des.data)
|
||||
this.upstream.queue(des.data.name, des.data.params)
|
||||
}
|
||||
} else {
|
||||
super.readPacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
close (reason) {
|
||||
this.upstream?.close(reason)
|
||||
super.close(reason)
|
||||
}
|
||||
}
|
||||
|
||||
class Relay extends Server {
|
||||
/**
|
||||
* Creates a new non-transparent proxy connection to a destination server
|
||||
* @param {Options} options
|
||||
*/
|
||||
constructor (options) {
|
||||
super(options)
|
||||
this.RelayPlayer = options.relayPlayer || RelayPlayer
|
||||
this.forceSingle = options.forceSingle
|
||||
this.upstreams = new Map()
|
||||
this.conLog = debug
|
||||
this.enableChunkCaching = options.enableChunkCaching
|
||||
}
|
||||
|
||||
// Called after a new player joins our proxy. We first create a new Client to connect to
|
||||
// the remote server. Then we listen to some events and proxy them over. The queue and
|
||||
// flushing logic is more of an accessory to make sure the server or client recieves
|
||||
// a packet, no matter what state it's in. For example, if the client wants to send a
|
||||
// packet to the server but it's not connected, it will add to the queue and send as soon
|
||||
// as a connection with the server is established.
|
||||
async openUpstreamConnection (ds, clientAddr) {
|
||||
const options = {
|
||||
authTitle: this.options.authTitle,
|
||||
flow: this.options.flow,
|
||||
deviceType: this.options.deviceType,
|
||||
offline: this.options.destination.offline ?? this.options.offline,
|
||||
username: this.options.offline ? ds.profile.name : ds.profile.xuid,
|
||||
version: this.options.version,
|
||||
realms: this.options.destination.realms,
|
||||
host: this.options.destination.host,
|
||||
port: this.options.destination.port,
|
||||
batchingInterval: this.options.batchingInterval,
|
||||
onMsaCode: (code) => {
|
||||
if (this.options.onMsaCode) {
|
||||
this.options.onMsaCode(code, ds)
|
||||
} else {
|
||||
ds.disconnect("It's your first time joining. Please sign in and reconnect to join this server:\n\n" + code.message)
|
||||
}
|
||||
},
|
||||
profilesFolder: this.options.profilesFolder,
|
||||
backend: this.options.backend,
|
||||
autoInitPlayer: false
|
||||
}
|
||||
|
||||
if (this.options.destination.realms) {
|
||||
await realmAuthenticate(options)
|
||||
}
|
||||
|
||||
const client = new Client(options)
|
||||
// Set the login payload unless `noLoginForward` option
|
||||
if (!client.noLoginForward) client.options.skinData = ds.skinData
|
||||
client.ping().then(pongData => {
|
||||
client.connect()
|
||||
}).catch(err => {
|
||||
this.emit('error', err)
|
||||
})
|
||||
this.conLog('Connecting to', options.host, options.port)
|
||||
client.outLog = ds.upOutLog
|
||||
client.inLog = ds.upInLog
|
||||
client.once('join', () => {
|
||||
// Tell the server to disable chunk cache for this connection as a client.
|
||||
// Wait a bit for the server to ack and process, the continue with proxying
|
||||
// otherwise the player can get stuck in an empty world.
|
||||
client.write('client_cache_status', { enabled: this.enableChunkCaching })
|
||||
ds.upstream = client
|
||||
ds.flushUpQueue()
|
||||
this.conLog('Connected to upstream server')
|
||||
client.readPacket = (packet) => ds.readUpstream(packet)
|
||||
|
||||
this.emit('join', /* client connected to proxy */ ds, /* backend server */ client)
|
||||
})
|
||||
client.on('error', (err) => {
|
||||
ds.disconnect('Server error: ' + err.message)
|
||||
debug(clientAddr, 'was disconnected because of error', err)
|
||||
this.upstreams.delete(clientAddr.hash)
|
||||
})
|
||||
client.on('close', (reason) => {
|
||||
ds.disconnect('Backend server closed connection')
|
||||
this.upstreams.delete(clientAddr.hash)
|
||||
})
|
||||
|
||||
this.upstreams.set(clientAddr.hash, client)
|
||||
}
|
||||
|
||||
// Close a connection to a remote backend server.
|
||||
closeUpstreamConnection (clientAddr) {
|
||||
const up = this.upstreams.get(clientAddr.hash)
|
||||
if (!up) throw Error(`unable to close non-open connection ${clientAddr.hash}`)
|
||||
up.close()
|
||||
this.upstreams.delete(clientAddr.hash)
|
||||
this.conLog('closed upstream connection', clientAddr)
|
||||
}
|
||||
|
||||
// Called when a new player connects to our proxy server. Once the player has authenticated,
|
||||
// we can open an upstream connection to the backend server.
|
||||
onOpenConnection = (conn) => {
|
||||
if (this.forceSingle && this.clientCount > 0) {
|
||||
this.conLog('dropping connection as single client relay', conn)
|
||||
conn.close()
|
||||
} else {
|
||||
this.clientCount++
|
||||
const player = new this.RelayPlayer(this, conn)
|
||||
this.conLog('New connection from', conn.address)
|
||||
this.clients[conn.address] = player
|
||||
this.emit('connect', player)
|
||||
player.on('login', () => {
|
||||
this.openUpstreamConnection(player, conn.address)
|
||||
})
|
||||
player.on('close', (reason) => {
|
||||
this.conLog('player disconnected', conn.address, reason)
|
||||
this.clientCount--
|
||||
delete this.clients[conn.address]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// When our server is closed, make sure to kick all of the connected clients and run emitters.
|
||||
close (...a) {
|
||||
for (const [, v] of this.upstreams) {
|
||||
v.close(...a)
|
||||
}
|
||||
super.close(...a)
|
||||
}
|
||||
}
|
||||
|
||||
// Too many things called 'Proxy' ;)
|
||||
module.exports = { Relay }
|
||||
160
src/server.js
Normal file
160
src/server.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
const { EventEmitter } = require('events')
|
||||
const { createDeserializer, createSerializer } = require('./transforms/serializer')
|
||||
const { Player } = require('./serverPlayer')
|
||||
const { sleep } = require('./datatypes/util')
|
||||
const { ServerAdvertisement } = require('./server/advertisement')
|
||||
const Options = require('./options')
|
||||
const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol')
|
||||
|
||||
class Server extends EventEmitter {
|
||||
constructor (options) {
|
||||
super()
|
||||
|
||||
this.options = { ...Options.defaultOptions, ...options }
|
||||
this.validateOptions()
|
||||
|
||||
this.RakServer = require('./rak')(this.options.raknetBackend).RakServer
|
||||
|
||||
this._loadFeatures(this.options.version)
|
||||
this.serializer = createSerializer(this.options.version)
|
||||
this.deserializer = createDeserializer(this.options.version)
|
||||
this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version)
|
||||
this.advertisement.playersMax = options.maxPlayers ?? 3
|
||||
/** @type {Object<string, Player>} */
|
||||
this.clients = {}
|
||||
this.clientCount = 0
|
||||
this.conLog = debug
|
||||
this.batchHeader = 0xfe
|
||||
|
||||
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) {
|
||||
switch (algorithm) {
|
||||
case 'none':
|
||||
this.compressionAlgorithm = 'none'
|
||||
this.compressionLevel = 0
|
||||
this.compressionHeader = 255
|
||||
break
|
||||
case 'deflate':
|
||||
this.compressionAlgorithm = 'deflate'
|
||||
this.compressionLevel = level
|
||||
this.compressionThreshold = threshold
|
||||
this.compressionHeader = 0
|
||||
break
|
||||
case 'snappy':
|
||||
this.compressionAlgorithm = 'snappy'
|
||||
this.compressionLevel = level
|
||||
this.compressionThreshold = threshold
|
||||
this.compressionHeader = 1
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown compression algorithm: ${algorithm}`)
|
||||
}
|
||||
}
|
||||
|
||||
validateOptions () {
|
||||
Options.validateOptions(this.options)
|
||||
}
|
||||
|
||||
versionLessThan (version) {
|
||||
return this.options.protocolVersion < (typeof version === 'string' ? Options.Versions[version] : version)
|
||||
}
|
||||
|
||||
versionGreaterThan (version) {
|
||||
return this.options.protocolVersion > (typeof version === 'string' ? Options.Versions[version] : version)
|
||||
}
|
||||
|
||||
versionGreaterThanOrEqualTo (version) {
|
||||
return this.options.protocolVersion >= (typeof version === 'string' ? Options.Versions[version] : version)
|
||||
}
|
||||
|
||||
onOpenConnection = (conn) => {
|
||||
this.conLog('New connection: ', conn?.address)
|
||||
|
||||
const player = new Player(this, conn)
|
||||
this.clients[conn.address] = player
|
||||
this.clientCount++
|
||||
this.emit('connect', player)
|
||||
}
|
||||
|
||||
onCloseConnection = (conn, reason) => {
|
||||
this.conLog('Connection closed: ', conn.address, reason)
|
||||
this.clients[conn.address]?.close()
|
||||
delete this.clients[conn.address]
|
||||
this.clientCount--
|
||||
}
|
||||
|
||||
onEncapsulated = (buffer, address) => {
|
||||
const client = this.clients[address]
|
||||
if (!client) {
|
||||
// Ignore packets from clients that are not connected.
|
||||
debug(`Ignoring packet from unknown inet address: ${address}`)
|
||||
return
|
||||
}
|
||||
|
||||
process.nextTick(() => client.handle(buffer))
|
||||
}
|
||||
|
||||
getAdvertisement () {
|
||||
if (this.options.advertisementFn) {
|
||||
return this.options.advertisementFn()
|
||||
}
|
||||
|
||||
this.advertisement.playersOnline = this.clientCount
|
||||
return this.advertisement
|
||||
}
|
||||
|
||||
async listen () {
|
||||
const { host, port, maxPlayers } = this.options
|
||||
this.raknet = new this.RakServer({ host, port, maxPlayers }, this)
|
||||
|
||||
try {
|
||||
await this.raknet.listen()
|
||||
} catch (e) {
|
||||
console.warn(`Failed to bind server on [${this.options.host}]/${this.options.port}, is the port free?`)
|
||||
throw e
|
||||
}
|
||||
|
||||
this.conLog('Listening on', host, port, this.options.version)
|
||||
this.raknet.onOpenConnection = this.onOpenConnection
|
||||
this.raknet.onCloseConnection = this.onCloseConnection
|
||||
this.raknet.onEncapsulated = this.onEncapsulated
|
||||
this.raknet.onClose = (reason) => this.close(reason || 'Raknet closed')
|
||||
|
||||
this.serverTimer = setInterval(() => {
|
||||
this.raknet.updateAdvertisement()
|
||||
}, 1000)
|
||||
|
||||
return { host, port }
|
||||
}
|
||||
|
||||
async close (disconnectReason = 'Server closed') {
|
||||
for (const caddr in this.clients) {
|
||||
const client = this.clients[caddr]
|
||||
client.disconnect(disconnectReason)
|
||||
}
|
||||
|
||||
clearInterval(this.serverTimer)
|
||||
this.clients = {}
|
||||
this.clientCount = 0
|
||||
|
||||
// Allow some time for client to get disconnect before closing connection.
|
||||
await sleep(60)
|
||||
this.raknet.close()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Server }
|
||||
70
src/server/advertisement.js
Normal file
70
src/server/advertisement.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
const { Versions, CURRENT_VERSION } = require('../options')
|
||||
|
||||
class ServerAdvertisement {
|
||||
motd = 'Bedrock Protocol Server'
|
||||
levelName = 'bedrock-protocol'
|
||||
playersOnline = 0
|
||||
playersMax = 5
|
||||
gamemode = 'Creative'
|
||||
serverId = Date.now().toString()
|
||||
gamemodeId = 1
|
||||
portV4 = undefined
|
||||
portV6 = undefined
|
||||
|
||||
constructor (obj, port, version = CURRENT_VERSION) {
|
||||
if (obj?.name) obj.motd = obj.name
|
||||
this.protocol = Versions[version]
|
||||
this.version = version
|
||||
this.portV4 = port
|
||||
this.portV6 = port
|
||||
Object.assign(this, obj)
|
||||
}
|
||||
|
||||
fromString (str) {
|
||||
const [header, motd, protocol, version, playersOnline, playersMax, serverId, levelName, gamemode, gamemodeId, portV4, portV6] = str.split(';')
|
||||
Object.assign(this, { header, motd, protocol, version, playersOnline, playersMax, serverId, levelName, gamemode, gamemodeId, portV4, portV6 })
|
||||
for (const numeric of ['playersOnline', 'playersMax', 'gamemodeId', 'portV4', 'portV6']) {
|
||||
if (this[numeric] !== undefined) {
|
||||
this[numeric] = this[numeric] ? parseInt(this[numeric]) : null
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
toString () {
|
||||
return [
|
||||
'MCPE',
|
||||
this.motd,
|
||||
this.protocol,
|
||||
this.version,
|
||||
this.playersOnline,
|
||||
this.playersMax,
|
||||
this.serverId,
|
||||
this.levelName,
|
||||
this.gamemode,
|
||||
this.gamemodeId,
|
||||
this.portV4,
|
||||
this.portV6,
|
||||
'0'
|
||||
].join(';') + ';'
|
||||
}
|
||||
|
||||
toBuffer (version) {
|
||||
const str = this.toString(version)
|
||||
const length = Buffer.byteLength(str)
|
||||
const buf = Buffer.alloc(2 + length)
|
||||
buf.writeUInt16BE(length, 0)
|
||||
buf.write(str, 2)
|
||||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ServerAdvertisement,
|
||||
getServerName (client) {
|
||||
return new ServerAdvertisement().toBuffer()
|
||||
},
|
||||
fromServerName (string) {
|
||||
return new ServerAdvertisement().fromString(string)
|
||||
}
|
||||
}
|
||||
203
src/serverPlayer.js
Normal file
203
src/serverPlayer.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
const { ClientStatus, Connection } = require('./connection')
|
||||
const Options = require('./options')
|
||||
const { serialize, isDebug } = require('./datatypes/util')
|
||||
const { KeyExchange } = require('./handshake/keyExchange')
|
||||
const Login = require('./handshake/login')
|
||||
const LoginVerify = require('./handshake/loginVerify')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
||||
class Player extends Connection {
|
||||
constructor (server, connection) {
|
||||
super()
|
||||
this.server = server
|
||||
this.features = server.features
|
||||
this.serializer = server.serializer
|
||||
this.deserializer = server.deserializer
|
||||
this.connection = connection
|
||||
this.options = server.options
|
||||
|
||||
KeyExchange(this, server, server.options)
|
||||
Login(this, server, server.options)
|
||||
LoginVerify(this, server, server.options)
|
||||
|
||||
this.startQueue()
|
||||
this.status = ClientStatus.Authenticating
|
||||
|
||||
if (isDebug) {
|
||||
this.inLog = (...args) => debug('-> S', ...args)
|
||||
this.outLog = (...args) => debug('<- S', ...args)
|
||||
}
|
||||
|
||||
this.batchHeader = this.server.batchHeader
|
||||
// Compression is server-wide
|
||||
this.compressionAlgorithm = this.server.compressionAlgorithm
|
||||
this.compressionLevel = this.server.compressionLevel
|
||||
this.compressionThreshold = this.server.compressionThreshold
|
||||
this.compressionHeader = this.server.compressionHeader
|
||||
|
||||
this._sentNetworkSettings = false // 1.19.30+
|
||||
}
|
||||
|
||||
getUserData () {
|
||||
return this.userData
|
||||
}
|
||||
|
||||
sendNetworkSettings () {
|
||||
this.write('network_settings', {
|
||||
compression_threshold: this.server.compressionThreshold,
|
||||
compression_algorithm: this.server.compressionAlgorithm,
|
||||
client_throttle: false,
|
||||
client_throttle_threshold: 0,
|
||||
client_throttle_scalar: 0
|
||||
})
|
||||
this._sentNetworkSettings = true
|
||||
this.compressionReady = true
|
||||
}
|
||||
|
||||
handleClientProtocolVersion (clientVersion) {
|
||||
if (this.server.options.protocolVersion) {
|
||||
if (this.server.options.protocolVersion < clientVersion) {
|
||||
this.sendDisconnectStatus('failed_spawn') // client too new
|
||||
return false
|
||||
}
|
||||
} else if (clientVersion < Options.MIN_VERSION) {
|
||||
this.sendDisconnectStatus('failed_client') // client too old
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
onLogin (packet) {
|
||||
const body = packet.data
|
||||
this.emit('loggingIn', body)
|
||||
|
||||
const clientVer = body.params.protocol_version
|
||||
if (!this.handleClientProtocolVersion(clientVer)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse login data
|
||||
const tokens = body.params.tokens
|
||||
try {
|
||||
const skinChain = tokens.client
|
||||
const authChain = JSON.parse(tokens.identity)
|
||||
let chain
|
||||
if (authChain.Certificate) { // 1.21.90+
|
||||
chain = JSON.parse(authChain.Certificate).chain
|
||||
} else if (authChain.chain) {
|
||||
chain = authChain.chain
|
||||
} else {
|
||||
throw new Error('Invalid login packet: missing chain or Certificate')
|
||||
}
|
||||
var { key, userData, skinData } = this.decodeLoginJWT(chain, skinChain) // eslint-disable-line
|
||||
} catch (e) {
|
||||
debug(this.address, e)
|
||||
this.disconnect('Server authentication error')
|
||||
return
|
||||
}
|
||||
|
||||
this.emit('server.client_handshake', { key }) // internal so we start encryption
|
||||
|
||||
this.userData = userData.extraData
|
||||
this.skinData = skinData
|
||||
this.profile = {
|
||||
name: userData.extraData?.displayName,
|
||||
uuid: userData.extraData?.identity,
|
||||
xuid: userData.extraData?.xuid || userData.extraData?.XUID
|
||||
}
|
||||
this.version = clientVer
|
||||
this.emit('login', { user: userData.extraData }) // emit events for user
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects a client before it has joined
|
||||
* @param {string} playStatus
|
||||
*/
|
||||
sendDisconnectStatus (playStatus) {
|
||||
if (this.status === ClientStatus.Disconnected) return
|
||||
this.write('play_status', { status: playStatus })
|
||||
this.close('kick')
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects a client
|
||||
*/
|
||||
disconnect (reason = 'Server closed', hide = false) {
|
||||
if (this.status === ClientStatus.Disconnected) return
|
||||
this.write('disconnect', {
|
||||
hide_disconnect_screen: hide,
|
||||
message: reason,
|
||||
filtered_message: ''
|
||||
})
|
||||
this.server.conLog('Kicked ', this.connection?.address, reason)
|
||||
setTimeout(() => this.close('kick'), 100) // Allow time for message to be recieved.
|
||||
}
|
||||
|
||||
// After sending Server to Client Handshake, this handles the client's
|
||||
// Client to Server handshake response. This indicates successful encryption
|
||||
onHandshake () {
|
||||
// https://wiki.vg/Bedrock_Protocol#Play_Status
|
||||
this.write('play_status', { status: 'login_success' })
|
||||
this.status = ClientStatus.Initializing
|
||||
this.emit('join')
|
||||
}
|
||||
|
||||
close (reason) {
|
||||
if (this.status !== ClientStatus.Disconnected) {
|
||||
this.emit('close') // Emit close once
|
||||
if (!reason) this.inLog?.('Client closed connection', this.connection?.address)
|
||||
}
|
||||
this.q = []
|
||||
this.q2 = []
|
||||
clearInterval(this.loop)
|
||||
this.connection?.close()
|
||||
this.removeAllListeners()
|
||||
this.status = ClientStatus.Disconnected
|
||||
}
|
||||
|
||||
readPacket (packet) {
|
||||
try {
|
||||
var des = this.server.deserializer.parsePacketBuffer(packet) // eslint-disable-line
|
||||
} catch (e) {
|
||||
this.disconnect('Server error')
|
||||
debug('Dropping packet from', this.connection.address, e)
|
||||
return
|
||||
}
|
||||
|
||||
this.inLog?.(des.data.name, serialize(des.data.params))
|
||||
|
||||
switch (des.data.name) {
|
||||
// This is the first packet on 1.19.30 & above
|
||||
case 'request_network_settings':
|
||||
if (this.handleClientProtocolVersion(des.data.params.client_protocol)) {
|
||||
this.sendNetworkSettings()
|
||||
this.compressionLevel = this.server.compressionLevel
|
||||
}
|
||||
return
|
||||
// Below 1.19.30, this is the first packet.
|
||||
case 'login':
|
||||
this.onLogin(des)
|
||||
if (!this._sentNetworkSettings) this.sendNetworkSettings()
|
||||
return
|
||||
case 'client_to_server_handshake':
|
||||
// Emit the 'join' event
|
||||
this.onHandshake()
|
||||
break
|
||||
case 'set_local_player_as_initialized':
|
||||
this.status = ClientStatus.Initialized
|
||||
this.inLog?.('Server client spawned')
|
||||
// Emit the 'spawn' event
|
||||
this.emit('spawn')
|
||||
break
|
||||
default:
|
||||
if (this.status === ClientStatus.Disconnected || this.status === ClientStatus.Authenticating) {
|
||||
this.inLog?.('ignoring', des.data.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.emit(des.data.name, des.data.params)
|
||||
this.emit('packet', des)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Player }
|
||||
BIN
src/texture
BIN
src/texture
Binary file not shown.
104
src/transforms/encryption.js
Normal file
104
src/transforms/encryption.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
const crypto = require('crypto')
|
||||
const Zlib = require('zlib')
|
||||
|
||||
function createCipher (secret, initialValue, cipherAlgorithm) {
|
||||
if (crypto.getCiphers().includes(cipherAlgorithm)) {
|
||||
return crypto.createCipheriv(cipherAlgorithm, secret, initialValue)
|
||||
}
|
||||
}
|
||||
|
||||
function createDecipher (secret, initialValue, cipherAlgorithm) {
|
||||
if (crypto.getCiphers().includes(cipherAlgorithm)) {
|
||||
return crypto.createDecipheriv(cipherAlgorithm, secret, initialValue)
|
||||
}
|
||||
}
|
||||
|
||||
function computeCheckSum (packetPlaintext, sendCounter, secretKeyBytes) {
|
||||
const digest = crypto.createHash('sha256')
|
||||
const counter = Buffer.alloc(8)
|
||||
counter.writeBigInt64LE(sendCounter, 0)
|
||||
digest.update(counter)
|
||||
digest.update(packetPlaintext)
|
||||
digest.update(secretKeyBytes)
|
||||
const hash = digest.digest()
|
||||
return hash.slice(0, 8)
|
||||
}
|
||||
|
||||
function createEncryptor (client, iv) {
|
||||
if (client.versionLessThan('1.16.220')) {
|
||||
client.cipher = createCipher(client.secretKeyBytes, iv, 'aes-256-cfb8')
|
||||
} else {
|
||||
client.cipher = createCipher(client.secretKeyBytes, iv.slice(0, 12), 'aes-256-gcm')
|
||||
}
|
||||
client.sendCounter = client.sendCounter || 0n
|
||||
|
||||
// A packet is encrypted via AES256(plaintext + SHA256(send_counter + plaintext + secret_key)[0:8]).
|
||||
// The send counter is represented as a little-endian 64-bit long and incremented after each packet.
|
||||
|
||||
function process (chunk) {
|
||||
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)])
|
||||
client.sendCounter++
|
||||
client.cipher.write(packet)
|
||||
}
|
||||
|
||||
client.cipher.on('data', client.onEncryptedPacket)
|
||||
|
||||
return (blob) => {
|
||||
process(blob)
|
||||
}
|
||||
}
|
||||
|
||||
function createDecryptor (client, iv) {
|
||||
if (client.versionLessThan('1.16.220')) {
|
||||
client.decipher = createDecipher(client.secretKeyBytes, iv, 'aes-256-cfb8')
|
||||
} else {
|
||||
client.decipher = createDecipher(client.secretKeyBytes, iv.slice(0, 12), 'aes-256-gcm')
|
||||
}
|
||||
|
||||
client.receiveCounter = client.receiveCounter || 0n
|
||||
|
||||
function verify (chunk) {
|
||||
const packet = chunk.slice(0, chunk.length - 8)
|
||||
const checksum = chunk.slice(chunk.length - 8, chunk.length)
|
||||
const computedCheckSum = computeCheckSum(packet, client.receiveCounter, client.secretKeyBytes)
|
||||
client.receiveCounter++
|
||||
|
||||
if (!checksum.equals(computedCheckSum)) {
|
||||
client.emit('error', Error(`Checksum mismatch ${checksum.toString('hex')} != ${computedCheckSum.toString('hex')}`))
|
||||
client.disconnect('disconnectionScreen.badPacket')
|
||||
return
|
||||
}
|
||||
|
||||
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.decipher.on('data', verify)
|
||||
|
||||
return (blob) => {
|
||||
client.decipher.write(blob)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createCipher, createDecipher, createEncryptor, createDecryptor
|
||||
}
|
||||
111
src/transforms/framer.js
Normal file
111
src/transforms/framer.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
const [readVarInt, writeVarInt, sizeOfVarInt] = require('protodef').types.varint
|
||||
const zlib = require('zlib')
|
||||
|
||||
// Concatenates packets into one batch packet, and adds length prefixs.
|
||||
class Framer {
|
||||
constructor (client) {
|
||||
// Encoding
|
||||
this.packets = []
|
||||
this.batchHeader = client.batchHeader
|
||||
this.compressor = client.compressionAlgorithm || 'none'
|
||||
this.compressionLevel = client.compressionLevel
|
||||
this.compressionThreshold = client.compressionThreshold
|
||||
this.compressionHeader = client.compressionHeader || 0
|
||||
this.writeCompressor = client.features.compressorInHeader && client.compressionReady
|
||||
}
|
||||
|
||||
// No compression in base class
|
||||
compress (buffer) {
|
||||
switch (this.compressor) {
|
||||
case 'deflate': return zlib.deflateRawSync(buffer, { level: this.compressionLevel })
|
||||
case 'snappy': throw Error('Snappy compression not implemented')
|
||||
case 'none': return buffer
|
||||
}
|
||||
}
|
||||
|
||||
static decompress (algorithm, buffer) {
|
||||
switch (algorithm) {
|
||||
case 0:
|
||||
case 'deflate':
|
||||
return zlib.inflateRawSync(buffer, { chunkSize: 512000 })
|
||||
case 1:
|
||||
case 'snappy':
|
||||
throw Error('Snappy compression not implemented')
|
||||
case 'none':
|
||||
case 255:
|
||||
return buffer
|
||||
default: throw Error('Unknown compression type ' + algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
static decode (client, buf) {
|
||||
// Read header
|
||||
if (this.batchHeader && buf[0] !== this.batchHeader) throw Error(`bad batch packet header, received: ${buf[0]}, expected: ${this.batchHeader}`)
|
||||
const buffer = buf.slice(1)
|
||||
// Decompress
|
||||
let decompressed
|
||||
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)
|
||||
}
|
||||
|
||||
encode () {
|
||||
const buf = Buffer.concat(this.packets)
|
||||
const shouldCompress = buf.length > this.compressionThreshold
|
||||
const header = this.batchHeader ? [this.batchHeader] : []
|
||||
if (this.writeCompressor) header.push(shouldCompress ? this.compressionHeader : 255)
|
||||
return Buffer.concat([Buffer.from(header), shouldCompress ? this.compress(buf) : buf])
|
||||
}
|
||||
|
||||
addEncodedPacket (chunk) {
|
||||
const varIntSize = sizeOfVarInt(chunk.byteLength)
|
||||
const buffer = Buffer.allocUnsafe(varIntSize + chunk.byteLength)
|
||||
writeVarInt(chunk.length, buffer, 0)
|
||||
chunk.copy(buffer, varIntSize)
|
||||
this.packets.push(buffer)
|
||||
}
|
||||
|
||||
addEncodedPackets (packets) {
|
||||
let allocSize = 0
|
||||
for (const packet of packets) {
|
||||
allocSize += sizeOfVarInt(packet.byteLength)
|
||||
allocSize += packet.byteLength
|
||||
}
|
||||
const buffer = Buffer.allocUnsafe(allocSize)
|
||||
let offset = 0
|
||||
for (const chunk of packets) {
|
||||
offset = writeVarInt(chunk.length, buffer, offset)
|
||||
offset += chunk.copy(buffer, offset, 0)
|
||||
}
|
||||
|
||||
this.packets.push(buffer)
|
||||
}
|
||||
|
||||
getBuffer () {
|
||||
return Buffer.concat(this.packets)
|
||||
}
|
||||
|
||||
static getPackets (buffer) {
|
||||
const packets = []
|
||||
let offset = 0
|
||||
while (offset < buffer.byteLength) {
|
||||
const { value, size } = readVarInt(buffer, offset)
|
||||
const dec = Buffer.allocUnsafe(value)
|
||||
offset += size
|
||||
offset += buffer.copy(dec, 0, offset, offset + value)
|
||||
packets.push(dec)
|
||||
}
|
||||
return packets
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Framer }
|
||||
|
|
@ -1,28 +1,75 @@
|
|||
var ProtoDef = require('protodef').ProtoDef;
|
||||
var Serializer = require('protodef').Serializer;
|
||||
var Parser = require('protodef').Parser;
|
||||
const { ProtoDefCompiler, CompiledProtodef } = require('protodef').Compiler
|
||||
const { FullPacketParser, Serializer } = require('protodef')
|
||||
const { join } = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
var protocol = require('../../data/protocol.json').types;
|
||||
class Parser extends FullPacketParser {
|
||||
dumpFailedBuffer (packet, prefix = '') {
|
||||
if (packet.length > 1000) {
|
||||
const now = Date.now()
|
||||
fs.writeFileSync(now + '_packetReadError.txt', packet.toString('hex'))
|
||||
console.log(prefix, `Deserialization failure for packet 0x${packet.slice(0, 1).toString('hex')}. Packet buffer saved in ./${now}_packetReadError.txt as buffer was too large (${packet.length} bytes).`)
|
||||
} else {
|
||||
console.log(prefix, 'Read failure for 0x' + packet.slice(0, 1).toString('hex'), packet.slice(0, 1000))
|
||||
}
|
||||
}
|
||||
|
||||
function createProtocol(packets) {
|
||||
var proto = new ProtoDef();
|
||||
proto.addTypes(require('../datatypes/minecraft'));
|
||||
proto.addTypes(protocol);
|
||||
|
||||
return proto;
|
||||
verify (deserialized, serializer) {
|
||||
const { name, params } = deserialized.data
|
||||
const oldBuffer = deserialized.fullBuffer
|
||||
const newBuffer = serializer.createPacketBuffer({ name, params })
|
||||
if (!newBuffer.equals(oldBuffer)) {
|
||||
const fs = require('fs')
|
||||
fs.writeFileSync('new.bin', newBuffer)
|
||||
fs.writeFileSync('old.bin', oldBuffer)
|
||||
fs.writeFileSync('failed.json', JSON.stringify(params, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2))
|
||||
console.warn('Failed to re-encode', name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var proto = createProtocol(protocol);
|
||||
// Compiles the ProtoDef schema at runtime
|
||||
function createProtocol (version) {
|
||||
// Try and load from .js if available
|
||||
try { require(`../../data/${version}/size.js`); return getProtocol(version) } catch {}
|
||||
|
||||
function createSerializer() {
|
||||
return new Serializer(proto, 'packet');
|
||||
const protocol = require('minecraft-data')('bedrock_' + version).protocol
|
||||
const compiler = new ProtoDefCompiler()
|
||||
compiler.addTypesToCompile(protocol.types)
|
||||
compiler.addTypes(require('../datatypes/compiler-minecraft'))
|
||||
|
||||
const compiledProto = compiler.compileProtoDefSync()
|
||||
return compiledProto
|
||||
}
|
||||
|
||||
function createDeserializer() {
|
||||
return new Parser(proto, 'packet');
|
||||
// Loads already generated read/write/sizeof code
|
||||
function getProtocol (version) {
|
||||
const compiler = new ProtoDefCompiler()
|
||||
compiler.addTypes(require(join(__dirname, '../datatypes/compiler-minecraft')))
|
||||
|
||||
global.PartialReadError = require('protodef/src/utils').PartialReadError
|
||||
const compile = (compiler, file) => require(file)(compiler.native)
|
||||
|
||||
return new CompiledProtodef(
|
||||
compile(compiler.sizeOfCompiler, join(__dirname, `../../data/${version}/size.js`)),
|
||||
compile(compiler.writeCompiler, join(__dirname, `../../data/${version}/write.js`)),
|
||||
compile(compiler.readCompiler, join(__dirname, `../../data/${version}/read.js`))
|
||||
)
|
||||
}
|
||||
|
||||
function createSerializer (version) {
|
||||
const proto = createProtocol(version)
|
||||
return new Serializer(proto, 'mcpe_packet')
|
||||
}
|
||||
|
||||
function createDeserializer (version) {
|
||||
const proto = createProtocol(version)
|
||||
return new Parser(proto, 'mcpe_packet')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDeserializer: createDeserializer,
|
||||
createSerializer: createSerializer
|
||||
};
|
||||
createDeserializer,
|
||||
createSerializer,
|
||||
createProtocol,
|
||||
getProtocol
|
||||
}
|
||||
|
|
|
|||
221
test/internal.js
Normal file
221
test/internal.js
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
const { Server, Client } = require('../')
|
||||
const { dumpPackets } = require('../tools/genPacketDumps')
|
||||
const { ping } = require('../src/createClient')
|
||||
const { CURRENT_VERSION } = require('../src/options')
|
||||
const { join } = require('path')
|
||||
const { waitFor } = require('../src/datatypes/util')
|
||||
const { getPort } = require('./util')
|
||||
|
||||
// First we need to dump some packets that a vanilla server would send a vanilla
|
||||
// client. Then we can replay those back in our custom server.
|
||||
function prepare (version) {
|
||||
return dumpPackets(version)
|
||||
}
|
||||
|
||||
async function startTest (version = CURRENT_VERSION, ok) {
|
||||
await prepare(version)
|
||||
// const Item = require('../types/Item')(version)
|
||||
const port = await getPort()
|
||||
const server = new Server({ host: '0.0.0.0', port, version, offline: true })
|
||||
|
||||
function getPath (packetPath) {
|
||||
return join(__dirname, `../data/${server.options.version}/${packetPath}`)
|
||||
}
|
||||
|
||||
function get (packetPath) {
|
||||
return require(getPath('sample/' + packetPath))
|
||||
}
|
||||
|
||||
console.log('Starting internal server')
|
||||
server.listen()
|
||||
console.log('Started server')
|
||||
|
||||
const pongData = await ping({ host: '127.0.0.1', port })
|
||||
console.assert(pongData, 'did not get valid pong data from server')
|
||||
|
||||
const respawnPacket = get('packets/respawn.json')
|
||||
const chunks = await requestChunks(version, respawnPacket.x, respawnPacket.z, 1)
|
||||
|
||||
let loop
|
||||
|
||||
// server logic
|
||||
server.on('connect', client => {
|
||||
client.on('join', () => {
|
||||
console.log('Client joined server', client.getUserData())
|
||||
|
||||
client.write('resource_packs_info', {
|
||||
must_accept: false,
|
||||
has_scripts: false,
|
||||
behaviour_packs: [],
|
||||
world_template: { uuid: '550e8400-e29b-41d4-a716-446655440000', version: '' }, // 1.21.50
|
||||
texture_packs: [],
|
||||
resource_pack_links: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_client_response', async rp => {
|
||||
// Tell the server we will compress everything (>=1 byte)
|
||||
client.write('network_settings', { compression_threshold: 1 })
|
||||
// Send some inventory slots
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// client.queue('inventory_slot', { window_id: 'armor', slot: 0, item: new Item().toBedrock() })
|
||||
}
|
||||
|
||||
// client.queue('inventory_transaction', get('packets/inventory_transaction.json'))
|
||||
client.queue('player_list', get('packets/player_list.json'))
|
||||
client.queue('start_game', get('packets/start_game.json'))
|
||||
if (client.versionLessThan('1.21.60')) {
|
||||
client.queue('item_component', { entries: [] })
|
||||
} else {
|
||||
client.queue('item_registry', get('packets/item_registry.json'))
|
||||
}
|
||||
client.queue('set_spawn_position', get('packets/set_spawn_position.json'))
|
||||
client.queue('set_time', { time: 5433771 })
|
||||
client.queue('set_difficulty', { difficulty: 1 })
|
||||
client.queue('set_commands_enabled', { enabled: true })
|
||||
|
||||
if (client.versionLessThan('1.19.10')) {
|
||||
client.queue('adventure_settings', get('packets/adventure_settings.json'))
|
||||
}
|
||||
|
||||
client.queue('biome_definition_list', get('packets/biome_definition_list.json'))
|
||||
client.queue('available_entity_identifiers', get('packets/available_entity_identifiers.json'))
|
||||
|
||||
client.queue('update_attributes', get('packets/update_attributes.json'))
|
||||
client.queue('creative_content', get('packets/creative_content.json'))
|
||||
client.queue('inventory_content', get('packets/inventory_content.json'))
|
||||
|
||||
client.queue('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true })
|
||||
|
||||
client.queue('crafting_data', get('packets/crafting_data.json'))
|
||||
client.queue('available_commands', get('packets/available_commands.json'))
|
||||
client.queue('chunk_radius_update', { chunk_radius: 5 })
|
||||
|
||||
// client.queue('set_entity_data', get('packets/set_entity_data.json'))
|
||||
|
||||
client.queue('game_rules_changed', get('packets/game_rules_changed.json'))
|
||||
client.queue('respawn', get('packets/respawn.json'))
|
||||
|
||||
for (const chunk of chunks) {
|
||||
client.queue('level_chunk', chunk)
|
||||
}
|
||||
|
||||
loop = setInterval(() => {
|
||||
client.write('network_chunk_publisher_update', { coordinates: { x: 646, y: 130, z: 77 }, radius: 64 })
|
||||
}, 6500)
|
||||
|
||||
setTimeout(() => {
|
||||
client.write('play_status', { status: 'player_spawn' })
|
||||
}, 3000)
|
||||
|
||||
// Respond to tick synchronization packets
|
||||
client.on('tick_sync', (packet) => {
|
||||
client.queue('tick_sync', {
|
||||
request_time: packet.request_time,
|
||||
response_time: BigInt(Date.now())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// client logic
|
||||
const client = new Client({
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
username: 'Notch',
|
||||
version,
|
||||
offline: true
|
||||
})
|
||||
|
||||
console.log('Started client')
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
|
||||
client.once('spawn', () => {
|
||||
console.info('Client spawend!')
|
||||
setTimeout(() => {
|
||||
client.close()
|
||||
|
||||
server.close().then(() => {
|
||||
ok?.()
|
||||
})
|
||||
}, 500)
|
||||
clearInterval(loop)
|
||||
})
|
||||
|
||||
client.connect()
|
||||
}
|
||||
|
||||
async function requestChunks (version, x, z, radius) {
|
||||
const ChunkColumn = require('bedrock-provider').chunk('bedrock_1.17.10')
|
||||
// const mcData = require('minecraft-data')('1.16')
|
||||
|
||||
const cxStart = (x >> 4) - radius
|
||||
const cxEnd = (x >> 4) + radius
|
||||
const czStart = (z >> 4) - radius
|
||||
const czEnd = (z >> 4) + radius
|
||||
|
||||
// const stone = mcData.blocksByName.stone
|
||||
const chunks = []
|
||||
|
||||
for (let cx = cxStart; cx < cxEnd; cx++) {
|
||||
for (let cz = czStart; cz < czEnd; cz++) {
|
||||
console.log('reading chunk at ', cx, cz)
|
||||
const cc = new ChunkColumn(x, z)
|
||||
|
||||
// Temporarily disable until 1.18 PR in bedrock-provider goes through
|
||||
// for (let x = 0; x < 16; x++) {
|
||||
// for (let y = 0; y < 60; y++) {
|
||||
// for (let z = 0; z < 16; z++) {
|
||||
// cc.setBlock({ x, y, z }, stone)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!cc) {
|
||||
console.log('no chunk')
|
||||
continue
|
||||
}
|
||||
const cbuf = await cc.networkEncodeNoCache()
|
||||
chunks.push({
|
||||
x: cx,
|
||||
z: cz,
|
||||
sub_chunk_count: cc.sectionsLen,
|
||||
cache_enabled: false,
|
||||
blobs: [],
|
||||
payload: cbuf
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
async function timedTest (version, timeout = 1000 * 220) {
|
||||
await waitFor((resolve, reject) => {
|
||||
// mocha eats up stack traces...
|
||||
startTest(version, resolve).catch(reject)
|
||||
}, timeout, () => {
|
||||
throw Error('timed out')
|
||||
})
|
||||
console.info('✔ ok')
|
||||
}
|
||||
|
||||
// if (!module.parent) timedTest('1.19.10')
|
||||
module.exports = { startTest, timedTest, requestChunks }
|
||||
19
test/internal.test.js
Normal file
19
test/internal.test.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
const { timedTest } = require('./internal')
|
||||
const { testedVersions } = require('../src/options')
|
||||
const { sleep } = require('../src/datatypes/util')
|
||||
require('events').captureRejections = true
|
||||
|
||||
describe('internal client/server test', function () {
|
||||
const vcount = testedVersions.length
|
||||
this.timeout(vcount * 80 * 1000)
|
||||
|
||||
for (const version of testedVersions) {
|
||||
it('connects ' + version, async () => {
|
||||
console.debug(version)
|
||||
await timedTest(version)
|
||||
await sleep(100)
|
||||
})
|
||||
}
|
||||
})
|
||||
68
test/proxy.js
Normal file
68
test/proxy.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
const { createClient, Server, Relay } = require('bedrock-protocol')
|
||||
const { sleep, waitFor } = require('../src/datatypes/util')
|
||||
const { getPort } = require('./util')
|
||||
|
||||
function proxyTest (version, raknetBackend = 'raknet-native', timeout = 1000 * 40) {
|
||||
console.log('with raknet backend', raknetBackend)
|
||||
return waitFor(async res => {
|
||||
const SERVER_PORT = await getPort()
|
||||
const CLIENT_PORT = await getPort()
|
||||
const server = new Server({
|
||||
host: '0.0.0.0', // optional
|
||||
port: SERVER_PORT, // optional
|
||||
offline: true,
|
||||
raknetBackend,
|
||||
version // The server version
|
||||
})
|
||||
await server.listen()
|
||||
|
||||
server.on('connect', client => {
|
||||
console.debug('Client has connected')
|
||||
client.on('join', () => { // The client has joined the server.
|
||||
console.debug('Client has authenticated')
|
||||
setTimeout(() => {
|
||||
client.disconnect('Hello world !')
|
||||
}, 500) // allow some time for client to connect
|
||||
})
|
||||
})
|
||||
|
||||
console.debug('Server started', server.options.version)
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const relay = new Relay({
|
||||
version,
|
||||
offline: true,
|
||||
/* host and port for clients to listen to */
|
||||
host: '0.0.0.0',
|
||||
port: CLIENT_PORT,
|
||||
/* Where to send upstream packets to */
|
||||
destination: {
|
||||
host: '127.0.0.1',
|
||||
port: SERVER_PORT
|
||||
},
|
||||
raknetBackend
|
||||
})
|
||||
relay.conLog = console.debug
|
||||
await relay.listen()
|
||||
|
||||
console.debug('Proxy started', server.options.version)
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const client = createClient({ host: '127.0.0.1', port: CLIENT_PORT, version, username: 'Boat', offline: true, raknetBackend, skipPing: true })
|
||||
console.debug('Client started')
|
||||
client.on('error', console.log)
|
||||
client.on('packet', console.log)
|
||||
client.on('disconnect', packet => {
|
||||
console.assert(packet.message === 'Hello world !')
|
||||
|
||||
server.close()
|
||||
relay.close()
|
||||
console.log('✔ OK')
|
||||
sleep(200).then(res)
|
||||
})
|
||||
}, timeout, () => { throw Error('timed out') })
|
||||
}
|
||||
|
||||
// if (!module.parent) { proxyTest('1.16.220', 'raknet-native') }
|
||||
|
||||
module.exports = { proxyTest }
|
||||
18
test/proxy.test.js
Normal file
18
test/proxy.test.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* eslint-env jest */
|
||||
const { proxyTest } = require('./proxy')
|
||||
const { testedVersions } = require('../src/options')
|
||||
const { sleep } = require('../src/datatypes/util')
|
||||
|
||||
describe('proxies client/server', function () {
|
||||
const vcount = testedVersions.length
|
||||
this.timeout(vcount * 30 * 1000)
|
||||
|
||||
for (const version of testedVersions) {
|
||||
it('proxies ' + version, async () => {
|
||||
console.debug(version)
|
||||
await proxyTest(version)
|
||||
await sleep(100)
|
||||
console.debug('Done', version)
|
||||
})
|
||||
}
|
||||
})
|
||||
17
test/util.js
Normal file
17
test/util.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
const net = require('net')
|
||||
|
||||
const getPort = () => new Promise(resolve => {
|
||||
const server = net.createServer()
|
||||
server.listen(0, '127.0.0.1')
|
||||
server.on('listening', () => {
|
||||
const { port } = server.address()
|
||||
server.close(() => {
|
||||
// Wait a bit for port to free as we try to bind right after freeing it
|
||||
setTimeout(() => {
|
||||
resolve(port)
|
||||
}, 200)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = { getPort }
|
||||
74
test/vanilla.js
Normal file
74
test/vanilla.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// process.env.DEBUG = 'minecraft-protocol raknet'
|
||||
const vanillaServer = require('../tools/startVanillaServer')
|
||||
const { Client } = require('../src/client')
|
||||
const { waitFor } = require('../src/datatypes/util')
|
||||
const { getPort } = require('./util')
|
||||
|
||||
async function test (version) {
|
||||
// const ChunkColumn = require('bedrock-provider').chunk('bedrock_' + (version.includes('1.19') ? '1.18.30' : version)) // TODO: Fix prismarine-chunk
|
||||
|
||||
// Start the server, wait for it to accept clients, throws on timeout
|
||||
const [port, v6] = [await getPort(), await getPort()]
|
||||
console.log('Starting vanilla server', version, 'on port', port, v6)
|
||||
const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port, 'server-portv6': v6 })
|
||||
console.log('Started server')
|
||||
|
||||
const client = new Client({
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
username: 'Notch',
|
||||
version,
|
||||
raknetBackend: 'raknet-native',
|
||||
offline: true
|
||||
})
|
||||
|
||||
console.log('Started client')
|
||||
client.connect()
|
||||
|
||||
let loop
|
||||
|
||||
await waitFor((res) => {
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
|
||||
clearInterval(loop)
|
||||
loop = setInterval(() => {
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
|
||||
}, 200)
|
||||
|
||||
// client.on('level_chunk', async packet => { // Chunk read test
|
||||
// const cc = new ChunkColumn(packet.x, packet.z)
|
||||
// await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count)
|
||||
// })
|
||||
|
||||
console.log('Awaiting join')
|
||||
|
||||
client.on('spawn', () => {
|
||||
console.log('✔ Client has spawned')
|
||||
client.close()
|
||||
handle.kill()
|
||||
res()
|
||||
})
|
||||
})
|
||||
}, 1000 * 60, () => {
|
||||
client.close()
|
||||
handle.kill()
|
||||
throw Error('❌ client timed out ')
|
||||
})
|
||||
clearInterval(loop)
|
||||
}
|
||||
|
||||
module.exports = { clientTest: test }
|
||||
17
test/vanilla.test.js
Normal file
17
test/vanilla.test.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
const { clientTest } = require('./vanilla')
|
||||
const { testedVersions } = require('../src/options')
|
||||
const { sleep } = require('../src/datatypes/util')
|
||||
|
||||
describe('vanilla server test', function () {
|
||||
const vcount = testedVersions.length
|
||||
this.timeout(vcount * 80 * 1000)
|
||||
|
||||
for (const version of testedVersions) {
|
||||
it('client spawns ' + version, async () => {
|
||||
await clientTest(version)
|
||||
await sleep(100)
|
||||
})
|
||||
}
|
||||
})
|
||||
47
tools/compileProtocol.js
Normal file
47
tools/compileProtocol.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Pre-compiles JS code from the schema for easier development.
|
||||
* You can run this with `npm run build`
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const { ProtoDefCompiler } = require('protodef').Compiler
|
||||
const { convert } = require('minecraft-data/minecraft-data/tools/js/compileProtocol')
|
||||
const mcData = require('minecraft-data')
|
||||
const { join } = require('path')
|
||||
// Filter versions we support
|
||||
const versions = mcData.versions.bedrock.filter(e => e.releaseType === 'release').map(e => e.minecraftVersion)
|
||||
|
||||
// Compile the ProtoDef JSON into JS
|
||||
function createProtocol (version) {
|
||||
const compiler = new ProtoDefCompiler()
|
||||
const protocol = mcData('bedrock_' + version).protocol.types
|
||||
compiler.addTypes(require('../src/datatypes/compiler-minecraft'))
|
||||
compiler.addTypesToCompile(protocol)
|
||||
|
||||
fs.writeFileSync('./read.js', 'module.exports = ' + compiler.readCompiler.generate().replace('() =>', 'native =>'))
|
||||
fs.writeFileSync('./write.js', 'module.exports = ' + compiler.writeCompiler.generate().replace('() =>', 'native =>'))
|
||||
fs.writeFileSync('./size.js', 'module.exports = ' + compiler.sizeOfCompiler.generate().replace('() =>', 'native =>'))
|
||||
|
||||
const compiledProto = compiler.compileProtoDefSync()
|
||||
return compiledProto
|
||||
}
|
||||
|
||||
function main (ver = 'latest') {
|
||||
// Put the .js files into the data/ dir, we also use the data dir when dumping packets for tests
|
||||
const dir = join(__dirname, '/../data/', ver)
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
||||
process.chdir(dir)
|
||||
console.log('Generating JS...', ver)
|
||||
createProtocol(ver)
|
||||
}
|
||||
|
||||
require('minecraft-data/bin/generate_data')
|
||||
|
||||
// If no argument, build everything
|
||||
if (!process.argv[2]) {
|
||||
convert('bedrock', 'latest')
|
||||
for (const version of versions) {
|
||||
main(version)
|
||||
}
|
||||
} else { // build the specified version
|
||||
main(process.argv[2])
|
||||
}
|
||||
165
tools/dumpPackets.js
Normal file
165
tools/dumpPackets.js
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// dumps (up to 5 of each) packet encountered until 'spawn' event
|
||||
// uses the same format as prismarine-packet-dumper
|
||||
const assert = require('assert')
|
||||
const fs = require('fs')
|
||||
const vanillaServer = require('../tools/startVanillaServer')
|
||||
const { Client } = require('../src/client')
|
||||
const { serialize, waitFor } = require('../src/datatypes/util')
|
||||
const { CURRENT_VERSION } = require('../src/options')
|
||||
const path = require('path')
|
||||
|
||||
const output = path.resolve(process.argv[3] ?? 'output')
|
||||
|
||||
let loop
|
||||
|
||||
async function dump (version) {
|
||||
const random = ((Math.random() * 100) | 0)
|
||||
const port = 19130 + random
|
||||
|
||||
const handle = await vanillaServer.startServerAndWait(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port })
|
||||
|
||||
console.log('Started server')
|
||||
const client = new Client({
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
username: 'dumpBot',
|
||||
offline: true
|
||||
})
|
||||
|
||||
return waitFor(async res => {
|
||||
await fs.promises.mkdir(output)
|
||||
await fs.promises.mkdir(path.join(output, 'from-server'))
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
|
||||
clearInterval(loop)
|
||||
loop = setInterval(() => {
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
|
||||
}, 200)
|
||||
})
|
||||
|
||||
const kindCounter = {}
|
||||
const MAX_PACKETS_PER_TYPE = 5
|
||||
client.on('packet', async packet => { // Packet dumping
|
||||
const { fullBuffer, data: { name, params } } = packet
|
||||
if (!packet.data.name) return
|
||||
if (!kindCounter[packet.name]) {
|
||||
await fs.promises.mkdir(path.join(output, 'from-server', name), { recursive: true })
|
||||
kindCounter[name] = 0
|
||||
}
|
||||
if (kindCounter[name] === MAX_PACKETS_PER_TYPE) return
|
||||
kindCounter[name]++
|
||||
|
||||
await fs.promises.writeFile(path.join(output, 'from-server', name, `${kindCounter[name]}.bin`), fullBuffer)
|
||||
|
||||
try {
|
||||
fs.writeFileSync(path.join(output, 'from-server', name, `${kindCounter[name]}.json`), serialize(params, 2))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Awaiting join...')
|
||||
|
||||
client.on('spawn', () => {
|
||||
console.log('Spawned!')
|
||||
clearInterval(loop)
|
||||
client.close()
|
||||
handle.kill()
|
||||
res(kindCounter)
|
||||
})
|
||||
}, 1000 * 60, () => {
|
||||
clearInterval(loop)
|
||||
handle.kill()
|
||||
throw Error('timed out')
|
||||
})
|
||||
}
|
||||
|
||||
const makeDropdownStart = (name, arr) => {
|
||||
arr.push(`<details><summary>${name}</summary>`)
|
||||
arr.push('<p>')
|
||||
arr.push('')
|
||||
}
|
||||
const makeDropdownEnd = (arr) => {
|
||||
arr.push('')
|
||||
arr.push('</p>')
|
||||
arr.push('</details>')
|
||||
}
|
||||
|
||||
function makeMarkdown (data) {
|
||||
const str = []
|
||||
const { collected, missing } = data
|
||||
|
||||
makeDropdownStart(`Collected (${collected.length})`, str)
|
||||
str.push('| Packet |')
|
||||
str.push('| --- |')
|
||||
collected.forEach(elem => {
|
||||
str.push(`| ${elem} |`)
|
||||
})
|
||||
makeDropdownEnd(str)
|
||||
|
||||
makeDropdownStart(`Missing (${missing.length})`, str)
|
||||
str.push('| Packet |')
|
||||
str.push('| --- |')
|
||||
missing.forEach(elem => {
|
||||
str.push(`| ${elem} |`)
|
||||
})
|
||||
makeDropdownEnd(str)
|
||||
|
||||
return str.join('\n')
|
||||
}
|
||||
|
||||
function parsePacketCounter (version, kindCounter) {
|
||||
const protocol = require(`../data/${version}/protocol.json`)
|
||||
// record packets
|
||||
return {
|
||||
collectedPackets: Object.keys(kindCounter),
|
||||
allPackets: Object.keys(protocol)
|
||||
.filter(o => o.startsWith('packet_'))
|
||||
.map(o => o.replace('packet_', ''))
|
||||
}
|
||||
}
|
||||
|
||||
async function makeStats (kindCounter, version) {
|
||||
const { collectedPackets, allPackets } = parsePacketCounter(version, kindCounter)
|
||||
// write packet data
|
||||
const data = {
|
||||
collected: collectedPackets,
|
||||
missing: allPackets.filter(o => !collectedPackets.includes(o))
|
||||
}
|
||||
const metadataFolder = path.join(output, 'metadata')
|
||||
|
||||
await fs.promises.writeFile(path.join(output, 'README.md'), makeMarkdown(data))
|
||||
await fs.promises.mkdir(metadataFolder)
|
||||
await fs.promises.writeFile(path.join(metadataFolder, 'packets_info.json'), JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const version = process.argv[2]
|
||||
if (!version) {
|
||||
console.error('Usage: node dumpPackets.js <version> [outputPath]')
|
||||
}
|
||||
const vers = Object.keys(require('../src/options').Versions)
|
||||
assert(vers.includes(version), 'Version not supported')
|
||||
if (fs.existsSync(output)) fs.promises.rm(output, { force: true, recursive: true })
|
||||
const kindCounter = await dump(version)
|
||||
await fs.promises.rm(path.join(output, '..', `bds-${version}`), { recursive: true })
|
||||
await makeStats(kindCounter, version)
|
||||
console.log('Successfully dumped packets')
|
||||
}
|
||||
|
||||
main()
|
||||
107
tools/genPacketDumps.js
Normal file
107
tools/genPacketDumps.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Collect sample packets needed for `serverTest.js`
|
||||
// process.env.DEBUG = 'minecraft-protocol'
|
||||
const fs = require('fs')
|
||||
const vanillaServer = require('../tools/startVanillaServer')
|
||||
const { Client } = require('../src/client')
|
||||
const { serialize, waitFor, getFiles } = require('../src/datatypes/util')
|
||||
const { CURRENT_VERSION } = require('../src/options')
|
||||
const { join } = require('path')
|
||||
const { getPort } = require('../test/util')
|
||||
|
||||
function hasDumps (version) {
|
||||
const root = join(__dirname, `../data/${version}/sample/packets/`)
|
||||
|
||||
if (!fs.existsSync(root) || getFiles(root).length < 10) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
let loop
|
||||
|
||||
async function dump (version, force = true) {
|
||||
const random = (Math.random() * 1000) | 0
|
||||
const [port, v6] = [await getPort(), await getPort()]
|
||||
|
||||
console.log('Starting dump server', version, 'on port', port, v6)
|
||||
const handle = await vanillaServer.startServerAndWait2(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port, 'server-portv6': v6 })
|
||||
|
||||
console.log('Started dump server', version)
|
||||
const client = new Client({
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
version,
|
||||
username: 'Packet' + random,
|
||||
offline: true
|
||||
})
|
||||
client.connect()
|
||||
return waitFor(async res => {
|
||||
const root = join(__dirname, `../data/${client.options.version}/sample/`)
|
||||
|
||||
fs.mkdirSync(root + 'packets', { recursive: true })
|
||||
fs.mkdirSync(root + 'chunks', { recursive: true })
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
|
||||
clearInterval(loop)
|
||||
|
||||
loop = setInterval(() => {
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
|
||||
}, 200)
|
||||
})
|
||||
|
||||
let i = 0
|
||||
|
||||
client.on('packet', async packet => { // Packet dumping
|
||||
const { name, params } = packet.data
|
||||
|
||||
if (name === 'level_chunk') {
|
||||
fs.writeFileSync(root + `chunks/${name}-${i++}.bin`, packet.buffer)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(root + `packets/${name}.json`) || force) {
|
||||
fs.writeFileSync(root + `packets/${name}.json`, serialize(params, 2))
|
||||
}
|
||||
} catch (e) { console.log(e) }
|
||||
})
|
||||
|
||||
console.log('Awaiting join...')
|
||||
|
||||
client.on('spawn', () => {
|
||||
console.log('Spawned!')
|
||||
|
||||
clearInterval(loop)
|
||||
client.close()
|
||||
handle.kill()
|
||||
res()
|
||||
})
|
||||
}, 1000 * 60, () => {
|
||||
clearInterval(loop)
|
||||
handle.kill()
|
||||
throw Error('Timed out')
|
||||
})
|
||||
}
|
||||
|
||||
if (!module.parent) {
|
||||
dump(null, true).then(() => {
|
||||
console.log('Successfully dumped packets')
|
||||
})
|
||||
}
|
||||
module.exports = { dumpPackets: dump, hasDumps }
|
||||
8
tools/installPMMP.sh
Normal file
8
tools/installPMMP.sh
Normal 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
|
||||
11
tools/startVanillaServer.js
Normal file
11
tools/startVanillaServer.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const bedrockServer = require('minecraft-bedrock-server')
|
||||
|
||||
module.exports = {
|
||||
...bedrockServer,
|
||||
startServerAndWait (version, withTimeout, options) {
|
||||
return bedrockServer.startServerAndWait(version, withTimeout, { ...options, root: __dirname })
|
||||
},
|
||||
startServerAndWait2 (version, withTimeout, options) {
|
||||
return bedrockServer.startServerAndWait2(version, withTimeout, { ...options, root: __dirname })
|
||||
}
|
||||
}
|
||||
67
types/Item.js
Normal file
67
types/Item.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const { Versions } = require('../src/options')
|
||||
|
||||
module.exports = (version) =>
|
||||
class Item {
|
||||
nbt
|
||||
constructor (obj) {
|
||||
this.networkId = 0
|
||||
this.runtimeId = 0
|
||||
this.count = 0
|
||||
this.metadata = 0
|
||||
Object.assign(this, obj)
|
||||
this.version = version
|
||||
}
|
||||
|
||||
static fromBedrock (obj) {
|
||||
if (Versions[version] >= Versions['1.16.220']) {
|
||||
return new Item({
|
||||
networkId: obj.network_id,
|
||||
stackId: obj.stack_id,
|
||||
blockRuntimeId: obj.block_runtime_id,
|
||||
count: obj.count,
|
||||
metadata: obj.metadata,
|
||||
nbt: obj.extra.nbt
|
||||
})
|
||||
} else {
|
||||
return new Item({
|
||||
networkId: obj.runtime_id,
|
||||
sackId: obj.item?.network_id,
|
||||
count: obj.item?.auxiliary_value & 0xff,
|
||||
metadata: obj.item?.auxiliary_value >> 8,
|
||||
nbt: obj.item?.nbt?.nbt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toBedrock () {
|
||||
if (Versions[version] >= Versions['1.16.220']) {
|
||||
return {
|
||||
network_id: this.networkId,
|
||||
count: this.count,
|
||||
metadata: this.metadata,
|
||||
has_stack_id: this.stackId,
|
||||
stack_id: this.stackId,
|
||||
extra: {
|
||||
has_nbt: !!this.nbt,
|
||||
nbt: { version: 1, nbt: this.nbt },
|
||||
can_place_on: [],
|
||||
can_destroy: [],
|
||||
blocking_tick: 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
runtime_id: this.runtimeId,
|
||||
item: {
|
||||
network_id: this.networkId,
|
||||
auxiliary_value: (this.metadata << 8) | (this.count & 0xff),
|
||||
has_nbt: !!this.nbt,
|
||||
nbt: { version: 1, nbt: this.nbt },
|
||||
can_place_on: [],
|
||||
can_destroy: [],
|
||||
blocking_tick: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue