mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-08 10:53:05 +00:00
Compare commits
366 Commits
5.0.0-ALPH
...
4.22.0
Author | SHA1 | Date | |
---|---|---|---|
b5fc31a781 | |||
441f1f534f | |||
e747478afd | |||
92c45dd7e1 | |||
2538880408 | |||
c715efb18e | |||
6678360c00 | |||
3db45b6a68 | |||
3e87ad281f | |||
b72da777eb | |||
1101f35c17 | |||
3948dc4f75 | |||
20942b37eb | |||
d343db8750 | |||
f2df702c67 | |||
481270e6aa | |||
e7bdaa8579 | |||
76749cbaa7 | |||
a897bdfaa0 | |||
09668a37d6 | |||
ea92a23d0d | |||
691e67018d | |||
fe2140a716 | |||
57330a7186 | |||
9ddac21de0 | |||
c91aa24daa | |||
6186fc0bfe | |||
ef40934d24 | |||
69b668355f | |||
0547383296 | |||
c7dff9ea40 | |||
043350753b | |||
5ad8016b99 | |||
2e5b2eed6e | |||
5a0cde49cc | |||
008a022ec1 | |||
5c85a7c306 | |||
599c4284f5 | |||
9499e2e595 | |||
a4fea1444a | |||
1ba47802a8 | |||
9d111e13f1 | |||
44bc4d8c7c | |||
d317347a9b | |||
077fac84bf | |||
fdb3a5b121 | |||
e3bc36ab5b | |||
283ff28aa9 | |||
c3ceeeace7 | |||
aac4f6c0e1 | |||
bb60a9057f | |||
3b893961e4 | |||
325ffec1be | |||
fa715a074a | |||
4caa2c7690 | |||
d04da9b1d8 | |||
02cf5ed388 | |||
6cad559dbe | |||
84a943bcec | |||
633e77a34c | |||
092d130c96 | |||
c09390d20f | |||
22f8623e17 | |||
f04151dbe6 | |||
5dcd8bf289 | |||
b70ff32548 | |||
73bf5d4b29 | |||
eb136e60c8 | |||
4228880509 | |||
709d874204 | |||
07dc10d6e6 | |||
7f6269c432 | |||
194714b448 | |||
023460db2c | |||
2910ffebf4 | |||
fea820a99e | |||
7c19f14cf5 | |||
5a54d09869 | |||
4def4d52d9 | |||
1d10107024 | |||
54ae4d0ea2 | |||
0d21e591d1 | |||
408616723c | |||
9bfcd39f2a | |||
8102616ff4 | |||
b162d688a3 | |||
eb130f2906 | |||
3b09c3a48a | |||
87781cff4d | |||
db0cf4bb5a | |||
eb4679fefd | |||
a4f2b99ed5 | |||
3ecc980bc4 | |||
107b56154b | |||
f86fde064d | |||
84a16ce69a | |||
d06d3bc871 | |||
11e34b3e5c | |||
f9318bf286 | |||
71b78b02d3 | |||
674b65f789 | |||
a77fc8109f | |||
6102740ee3 | |||
40168a457e | |||
d07acd0013 | |||
9561ae5af7 | |||
56fbd45dd5 | |||
b5dc72b0ee | |||
4ba57f2b03 | |||
84cb070d56 | |||
df0d72bf61 | |||
a534ac759a | |||
5ab954b7a0 | |||
6c6f686f8e | |||
bf7975da57 | |||
7aeedd8220 | |||
3dd1ce2d02 | |||
5a29d07021 | |||
692e1253c6 | |||
ab0c444823 | |||
e48a4aaa55 | |||
6fc4ce0f86 | |||
10243c7b2c | |||
8102586ee0 | |||
18b528f72d | |||
0336394098 | |||
7e92da126d | |||
1569bed37a | |||
ba62e0f9cb | |||
a6e79c5004 | |||
858d4a2ed2 | |||
87d8c1ea11 | |||
89deb0fe18 | |||
ad88490e84 | |||
3490e2b06a | |||
946c2fbacc | |||
1c0eed56f1 | |||
9e9b4db00f | |||
e667b5c7db | |||
f61f72180f | |||
3f82150837 | |||
017fcde6aa | |||
24374297e7 | |||
76ebedff6a | |||
3ea8d27a3b | |||
d6c923b525 | |||
7b55c984bf | |||
f5b4d64668 | |||
1683aa681d | |||
bf84caa02c | |||
734adec90d | |||
4724195791 | |||
f32a853bd4 | |||
61b0ad3e7f | |||
b2f755720d | |||
8ef2780dcd | |||
b19c7212ab | |||
14c1a9550d | |||
9037d5f16b | |||
8b64ea9e65 | |||
2936726bf8 | |||
9cd07f6721 | |||
4bb8daa1a5 | |||
6e8eda4ac1 | |||
73522d06ef | |||
a6a360d179 | |||
199ef7401f | |||
f63d349be4 | |||
02e11b5a60 | |||
4a770e5801 | |||
a2ff9649d5 | |||
a862cf5144 | |||
5ac0d7ae11 | |||
0c47455b24 | |||
821dd8885b | |||
a78ae73119 | |||
17a1266056 | |||
217d7ab4cf | |||
9e8c0a6bea | |||
dc1b5a9285 | |||
c3a16d9b1f | |||
bed218d1dd | |||
5e1f837a73 | |||
b49a9ae81d | |||
4c60e82110 | |||
beb0713a40 | |||
cd603e8266 | |||
af385668c2 | |||
3ee62d8440 | |||
811639f2cd | |||
58974765a6 | |||
eca9fe50b6 | |||
1959d6dc9b | |||
9db7e5f0ca | |||
c1cef19f84 | |||
cebdb95265 | |||
006cdaf6ea | |||
acc8ae87fb | |||
0e8b28716a | |||
7c77233d12 | |||
6f02b83a26 | |||
fbfdf749f2 | |||
289c0b08f4 | |||
dd37b531ad | |||
58d5126ada | |||
f978c1e9a0 | |||
0b8193aeb3 | |||
00286e761c | |||
db59f71130 | |||
b11457d605 | |||
ea386c42d3 | |||
043e81e737 | |||
66a4c4c88b | |||
1a9322c00a | |||
c8d9477da1 | |||
08e8ef275f | |||
e57fbff28c | |||
f90315c4a2 | |||
955f7944bb | |||
ccd288d7fa | |||
097632902a | |||
e7771d76f2 | |||
ecc830a689 | |||
ee72e80fbb | |||
63310cf764 | |||
1992d3b6db | |||
035a0a4e9d | |||
23ea721164 | |||
8408da8534 | |||
c9601ae67d | |||
758b5ee500 | |||
ca6d51498f | |||
e8085e22a0 | |||
a83fc85f1e | |||
3d70a169e1 | |||
6ccb8f7373 | |||
59bae9b077 | |||
2751e1ec02 | |||
c91168db66 | |||
4e55433ed8 | |||
eece6c4433 | |||
67b7b60d18 | |||
804feedb67 | |||
d57aca1367 | |||
7b0816e42f | |||
4864444440 | |||
52ea4feac0 | |||
01d557062a | |||
a619fd2be6 | |||
05d9298958 | |||
f696a5881b | |||
419962d3a2 | |||
054c06fab9 | |||
7bc5d8c824 | |||
607bdfa42f | |||
eec53f9ae0 | |||
3d56bd267c | |||
9a969e21c7 | |||
195bc3b623 | |||
2177d8d352 | |||
471625e697 | |||
2135776c19 | |||
765aef0810 | |||
bd21feffc4 | |||
5b324f695c | |||
9caed10488 | |||
83945ff0a0 | |||
ef45180b80 | |||
941fd03998 | |||
1af8da3c1f | |||
a5985dcf7d | |||
183d1f4038 | |||
08ee825d91 | |||
337a254768 | |||
a31e3331fd | |||
acebbeed16 | |||
e0fdbe6eb1 | |||
cc8660629b | |||
e7e19abe85 | |||
5f9e0081fd | |||
b266f45152 | |||
34ced382db | |||
a573a279fa | |||
14f141fab2 | |||
daff955bc4 | |||
0022d82779 | |||
7cad9be0d2 | |||
2f862a552a | |||
590f6dad08 | |||
9564c81582 | |||
3de7a8c27f | |||
d376399b7f | |||
e2071e59c8 | |||
8e280ebb8b | |||
fa7c38276c | |||
b13e97de3d | |||
328b87fc18 | |||
acaa1a9ce1 | |||
3aec0fa3df | |||
fa131dab12 | |||
bb4a82b1e7 | |||
93d844a281 | |||
616844696e | |||
71e3e36522 | |||
a1b42d419f | |||
ef942a627f | |||
fd8c276bd2 | |||
9783380d1a | |||
a784d93bfd | |||
a05e8b366f | |||
87a2e0460c | |||
4073c3fb39 | |||
e227e6d8bf | |||
3aa40829ae | |||
035d4b7263 | |||
3db1492c18 | |||
a523189149 | |||
f8893efb94 | |||
70f1ee3e97 | |||
eb2f0ed3d0 | |||
14e7d3e143 | |||
6d636fc2c7 | |||
a39f61a33d | |||
aaec21f544 | |||
0edc5f8113 | |||
a382f0fd92 | |||
0fcd2e7894 | |||
369e0855a7 | |||
a6cf39b94e | |||
17afd38274 | |||
8f024cb382 | |||
e7209679fb | |||
da054736b1 | |||
d92173cded | |||
308cdb6863 | |||
ae50b952f1 | |||
f44946cb49 | |||
f704bfb63a | |||
9acb4d64db | |||
8234360c8d | |||
6a64486f55 | |||
6ec778d0af | |||
75bb4f8da6 | |||
efdd7a186d | |||
faaec12aaf | |||
1123a5aa23 | |||
8633804f15 | |||
d3cea2ca7c | |||
5d6dba96af | |||
b24eb153f9 | |||
36525d9055 | |||
3d6baa8a55 | |||
30d3869eea | |||
81697111b9 | |||
eedc943766 | |||
c6e11a8453 | |||
2e9a3f9160 | |||
3d4ed5308e | |||
58dd4a44e3 | |||
5fdbb19852 | |||
6b2156151f | |||
d8d236842f | |||
f51717323b | |||
0039af984d | |||
3235d128e5 | |||
2b7510945a |
33
.github/workflows/build-docker-image.yml
vendored
33
.github/workflows/build-docker-image.yml
vendored
@ -20,6 +20,13 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Clone pmmp/PocketMine-Docker repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
@ -30,68 +37,68 @@ jobs:
|
||||
id: tag-name
|
||||
run: |
|
||||
VERSION=$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{')
|
||||
echo ::set-output name=TAG_NAME::$VERSION
|
||||
echo ::set-output name=MAJOR::$(echo $VERSION | cut -d. -f1)
|
||||
echo ::set-output name=MINOR::$(echo $VERSION | cut -d. -f1-2)
|
||||
echo TAG_NAME=$VERSION >> $GITHUB_OUTPUT
|
||||
echo MAJOR=$(echo $VERSION | cut -d. -f1) >> $GITHUB_OUTPUT
|
||||
echo MINOR=$(echo $VERSION | cut -d. -f1-2) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download new release information
|
||||
run: curl -f -L ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.tag-name.outputs.TAG_NAME }}/build_info.json -o new_build_info.json
|
||||
|
||||
- name: Detect channel
|
||||
id: channel
|
||||
run: echo ::set-output name=CHANNEL::$(jq -r '.channel' new_build_info.json)
|
||||
run: echo CHANNEL=$(jq -r '.channel' new_build_info.json) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get name of Docker repository name
|
||||
id: docker-repo-name
|
||||
run: echo ::set-output name=NAME::$(echo "${GITHUB_REPOSITORY,,}")
|
||||
run: echo NAME=$(echo "${GITHUB_REPOSITORY,,}") >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build image for tag
|
||||
uses: docker/build-push-action@v3.3.0
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
push: true
|
||||
context: ./pocketmine-mp
|
||||
tags: |
|
||||
${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
# ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
build-args: |
|
||||
PMMP_TAG=${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
PMMP_REPO=${{ github.repository }}
|
||||
|
||||
- name: Build image for major tag
|
||||
if: steps.channel.outputs.CHANNEL == 'stable'
|
||||
uses: docker/build-push-action@v3.3.0
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
push: true
|
||||
context: ./pocketmine-mp
|
||||
tags: |
|
||||
${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MAJOR }}
|
||||
# ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MAJOR }}
|
||||
ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MAJOR }}
|
||||
build-args: |
|
||||
PMMP_TAG=${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
PMMP_REPO=${{ github.repository }}
|
||||
|
||||
- name: Build image for minor tag
|
||||
if: steps.channel.outputs.CHANNEL == 'stable'
|
||||
uses: docker/build-push-action@v3.3.0
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
push: true
|
||||
context: ./pocketmine-mp
|
||||
tags: |
|
||||
${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MINOR }}
|
||||
# ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MINOR }}
|
||||
ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:${{ steps.tag-name.outputs.MINOR }}
|
||||
build-args: |
|
||||
PMMP_TAG=${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
PMMP_REPO=${{ github.repository }}
|
||||
|
||||
- name: Build image for latest tag
|
||||
if: steps.channel.outputs.CHANNEL == 'stable'
|
||||
uses: docker/build-push-action@v3.3.0
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
push: true
|
||||
context: ./pocketmine-mp
|
||||
tags: |
|
||||
${{ steps.docker-repo-name.outputs.NAME }}:latest
|
||||
# ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:latest
|
||||
ghcr.io/${{ steps.docker-repo-name.outputs.NAME }}:latest
|
||||
build-args: |
|
||||
PMMP_TAG=${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
PMMP_REPO=${{ github.repository }}
|
||||
|
11
.github/workflows/discord-release-embed.php
vendored
11
.github/workflows/discord-release-embed.php
vendored
@ -18,8 +18,9 @@ require dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
/**
|
||||
* @phpstan-return array<string, mixed>
|
||||
*/
|
||||
function generateDiscordEmbed(string $version, string $channel, string $description, string $detailsUrl, string $sourceUrl, string $pharDownloadUrl, string $buildLogUrl) : array{
|
||||
function generateDiscordEmbed(string $version, string $channel, string $description, string $detailsUrl, string $sourceUrl, string $pharDownloadUrl, string $buildLogUrl, int $newsPingRoleId) : array{
|
||||
return [
|
||||
"content" => "<@&$newsPingRoleId> New PocketMine-MP release: $version ($channel)",
|
||||
"embeds" => [
|
||||
[
|
||||
"title" => "New PocketMine-MP release: $version ($channel)",
|
||||
@ -35,11 +36,11 @@ DESCRIPTION,
|
||||
];
|
||||
}
|
||||
|
||||
if(count($argv) !== 5){
|
||||
fwrite(STDERR, "Required arguments: github repo, version, API token\n");
|
||||
if(count($argv) !== 6){
|
||||
fwrite(STDERR, "Required arguments: github repo, version, API token, webhook URL, ping role ID\n");
|
||||
exit(1);
|
||||
}
|
||||
[, $repo, $tagName, $token, $hookURL] = $argv;
|
||||
[, $repo, $tagName, $token, $hookURL, $newsPingRoleId] = $argv;
|
||||
|
||||
$result = Internet::getURL('https://api.github.com/repos/' . $repo . '/releases/tags/' . $tagName, extraHeaders: [
|
||||
'Authorization: token ' . $token
|
||||
@ -86,7 +87,7 @@ $buildLogUrl = $buildInfoJson["build_log_url"];
|
||||
|
||||
$description = $releaseInfoJson["body"];
|
||||
|
||||
$discordPayload = generateDiscordEmbed($buildInfoJson["base_version"], $buildInfoJson["channel"], $description, $detailsUrl, $sourceUrl, $pharDownloadUrl, $buildLogUrl);
|
||||
$discordPayload = generateDiscordEmbed($buildInfoJson["base_version"], $buildInfoJson["channel"], $description, $detailsUrl, $sourceUrl, $pharDownloadUrl, $buildLogUrl, (int) $newsPingRoleId);
|
||||
|
||||
$response = Internet::postURL(
|
||||
$hookURL,
|
||||
|
8
.github/workflows/discord-release-notify.yml
vendored
8
.github/workflows/discord-release-notify.yml
vendored
@ -13,9 +13,9 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP and tools
|
||||
uses: shivammathur/setup-php@2.24.0
|
||||
uses: shivammathur/setup-php@2.25.2
|
||||
with:
|
||||
php-version: 8.0
|
||||
php-version: 8.1
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Get actual tag name
|
||||
id: tag-name
|
||||
run: echo ::set-output name=TAG_NAME::$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{')
|
||||
run: echo TAG_NAME=$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run webhook post script
|
||||
run: php .github/workflows/discord-release-embed.php ${{ github.repository }} ${{ steps.tag-name.outputs.TAG_NAME }} ${{ github.token }} ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
run: php .github/workflows/discord-release-embed.php ${{ github.repository }} ${{ steps.tag-name.outputs.TAG_NAME }} ${{ github.token }} ${{ secrets.DISCORD_RELEASE_WEBHOOK }} ${{ secrets.DISCORD_NEWS_PING_ROLE_ID }}
|
||||
|
18
.github/workflows/draft-release.yml
vendored
18
.github/workflows/draft-release.yml
vendored
@ -18,9 +18,9 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.24.0
|
||||
uses: shivammathur/setup-php@2.25.2
|
||||
with:
|
||||
php-version: 8.0
|
||||
php-version: 8.1
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
BUILD_NUMBER=$((2000+$GITHUB_RUN_NUMBER)) #to stay above jenkins
|
||||
echo "Build number: $BUILD_NUMBER"
|
||||
echo ::set-output name=BUILD_NUMBER::$BUILD_NUMBER
|
||||
echo BUILD_NUMBER=$BUILD_NUMBER >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Minify BedrockData JSON files
|
||||
run: php vendor/pocketmine/bedrock-data/.minify_json.php
|
||||
@ -51,11 +51,12 @@ jobs:
|
||||
- name: Get PocketMine-MP release version
|
||||
id: get-pm-version
|
||||
run: |
|
||||
echo ::set-output name=PM_VERSION::$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BASE_VERSION;')
|
||||
echo ::set-output name=MCPE_VERSION::$(php -r 'require "vendor/autoload.php"; echo \pocketmine\network\mcpe\protocol\ProtocolInfo::MINECRAFT_VERSION_NETWORK;')
|
||||
echo ::set-output name=PM_VERSION_SHORT::$(php -r 'require "vendor/autoload.php"; $v = explode(".", \pocketmine\VersionInfo::BASE_VERSION); array_pop($v); echo implode(".", $v);')
|
||||
echo ::set-output name=PM_VERSION_MD::$(php -r 'require "vendor/autoload.php"; echo str_replace(".", "", \pocketmine\VersionInfo::BASE_VERSION);')
|
||||
echo ::set-output name=CHANGELOG_SUFFIX::$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BUILD_CHANNEL === "stable" ? "" : "-" . \pocketmine\VersionInfo::BUILD_CHANNEL;')
|
||||
echo PM_VERSION=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BASE_VERSION;') >> $GITHUB_OUTPUT
|
||||
echo MCPE_VERSION=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\network\mcpe\protocol\ProtocolInfo::MINECRAFT_VERSION_NETWORK;') >> $GITHUB_OUTPUT
|
||||
echo PM_VERSION_SHORT=$(php -r 'require "vendor/autoload.php"; $v = explode(".", \pocketmine\VersionInfo::BASE_VERSION); array_pop($v); echo implode(".", $v);') >> $GITHUB_OUTPUT
|
||||
echo PM_VERSION_MD=$(php -r 'require "vendor/autoload.php"; echo str_replace(".", "", \pocketmine\VersionInfo::BASE_VERSION);') >> $GITHUB_OUTPUT
|
||||
echo CHANGELOG_SUFFIX=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BUILD_CHANNEL === "stable" ? "" : "-" . \pocketmine\VersionInfo::BUILD_CHANNEL;') >> $GITHUB_OUTPUT
|
||||
echo PRERELEASE=$(php -r 'require "vendor/autoload.php"; echo \pocketmine\VersionInfo::BUILD_CHANNEL === "stable" ? "false" : "true";') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate build info
|
||||
run: php build/generate-build-info-json.php ${{ github.sha }} ${{ steps.get-pm-version.outputs.PM_VERSION }} ${{ github.repository }} ${{ steps.build-number.outputs.BUILD_NUMBER }} ${{ github.run_id }} > build_info.json
|
||||
@ -75,6 +76,7 @@ jobs:
|
||||
artifacts: ${{ github.workspace }}/PocketMine-MP.phar,${{ github.workspace }}/start.*,${{ github.workspace }}/build_info.json
|
||||
commit: ${{ github.sha }}
|
||||
draft: true
|
||||
prerelease: ${{ steps.get-pm-version.outputs.PRERELEASE }}
|
||||
name: PocketMine-MP ${{ steps.get-pm-version.outputs.PM_VERSION }}
|
||||
tag: ${{ steps.get-pm-version.outputs.PM_VERSION }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
67
.github/workflows/main.yml
vendored
67
.github/workflows/main.yml
vendored
@ -6,48 +6,26 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-php:
|
||||
name: Prepare PHP
|
||||
runs-on: ${{ matrix.image }}
|
||||
concurrency: php-build-${{ matrix.php }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.27, 8.1.14, 8.2.1]
|
||||
|
||||
steps:
|
||||
- name: Build and prepare PHP cache
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
pm-version-major: "4"
|
||||
|
||||
phpstan:
|
||||
name: PHPStan analysis
|
||||
needs: build-php
|
||||
runs-on: ${{ matrix.image }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.27, 8.1.14, 8.2.1]
|
||||
php: ["8.1", "8.2"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@2.0.0
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
pm-version-major: "4"
|
||||
|
||||
- name: Install Composer
|
||||
run: curl -sS https://getcomposer.org/installer | php
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@ -59,34 +37,30 @@ jobs:
|
||||
composer-v2-cache-
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: php composer.phar install --prefer-dist --no-interaction
|
||||
run: composer install --prefer-dist --no-interaction
|
||||
|
||||
- name: Run PHPStan
|
||||
run: ./vendor/bin/phpstan analyze --no-progress --memory-limit=2G
|
||||
|
||||
phpunit:
|
||||
name: PHPUnit tests
|
||||
needs: build-php
|
||||
runs-on: ${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.27, 8.1.14, 8.2.1]
|
||||
php: ["8.1", "8.2"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@2.0.0
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
pm-version-major: "4"
|
||||
|
||||
- name: Install Composer
|
||||
run: curl -sS https://getcomposer.org/installer | php
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@ -98,20 +72,19 @@ jobs:
|
||||
composer-v2-cache-
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: php composer.phar install --prefer-dist --no-interaction
|
||||
run: composer install --prefer-dist --no-interaction
|
||||
|
||||
- name: Run PHPUnit tests
|
||||
run: ./vendor/bin/phpunit --bootstrap vendor/autoload.php --fail-on-warning tests/phpunit
|
||||
|
||||
integration:
|
||||
name: Integration tests
|
||||
needs: build-php
|
||||
runs-on: ${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.27, 8.1.14, 8.2.1]
|
||||
php: ["8.1", "8.2"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@ -119,15 +92,12 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@2.0.0
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
pm-version-major: "4"
|
||||
|
||||
- name: Install Composer
|
||||
run: curl -sS https://getcomposer.org/installer | php
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@ -139,34 +109,30 @@ jobs:
|
||||
composer-v2-cache-
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: php composer.phar install --no-dev --prefer-dist --no-interaction
|
||||
run: composer install --no-dev --prefer-dist --no-interaction
|
||||
|
||||
- name: Run integration tests
|
||||
run: ./tests/travis.sh -t4
|
||||
|
||||
codegen:
|
||||
name: Generated Code consistency checks
|
||||
needs: build-php
|
||||
runs-on: ${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.27, 8.1.14, 8.2.1]
|
||||
php: ["8.1", "8.2"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@2.0.0
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
pm-version-major: "4"
|
||||
|
||||
- name: Install Composer
|
||||
run: curl -sS https://getcomposer.org/installer | php
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@ -178,7 +144,7 @@ jobs:
|
||||
composer-v2-cache-
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: php composer.phar install --no-dev --prefer-dist --no-interaction
|
||||
run: composer install --no-dev --prefer-dist --no-interaction
|
||||
|
||||
- name: Regenerate registry annotations
|
||||
run: php build/generate-registry-annotations.php src
|
||||
@ -186,6 +152,9 @@ jobs:
|
||||
- name: Regenerate KnownTranslation APIs
|
||||
run: php build/generate-known-translation-apis.php
|
||||
|
||||
- name: Regenerate BedrockData available files constants
|
||||
run: php build/generate-bedrockdata-path-consts.php
|
||||
|
||||
- name: Verify code is unchanged
|
||||
run: |
|
||||
git diff
|
||||
@ -201,10 +170,10 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP and tools
|
||||
uses: shivammathur/setup-php@2.24.0
|
||||
uses: shivammathur/setup-php@2.25.2
|
||||
with:
|
||||
php-version: 8.0
|
||||
tools: php-cs-fixer:3.11
|
||||
php-version: 8.1
|
||||
tools: php-cs-fixer:3.16
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
51
.github/workflows/update-php-versions.php
vendored
51
.github/workflows/update-php-versions.php
vendored
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
const VERSIONS = [
|
||||
"8.0",
|
||||
"8.1",
|
||||
"8.2"
|
||||
];
|
||||
|
||||
$workflowFile = file_get_contents(__DIR__ . '/main.yml');
|
||||
$newWorkflowFile = $workflowFile;
|
||||
foreach(VERSIONS as $v){
|
||||
$releaseInfo = file_get_contents("https://secure.php.net/releases?json&version=$v");
|
||||
if($releaseInfo === false){
|
||||
throw new \RuntimeException("Failed to contact php.net API");
|
||||
}
|
||||
$data = json_decode($releaseInfo, true);
|
||||
if(!is_array($data) || !isset($data["version"]) || !is_string($data["version"]) || preg_match('/^\d+\.\d+\.\d+(-[A-Za-z\d]+)?$/', $data["version"]) === 0){
|
||||
throw new \RuntimeException("Invalid data returned by API");
|
||||
}
|
||||
$updated = preg_replace("/$v\.\d+/", $data["version"], $newWorkflowFile);
|
||||
if($updated !== $newWorkflowFile){
|
||||
echo "Updated $v revision to " . $data["version"] . "\n";
|
||||
}
|
||||
$newWorkflowFile = $updated;
|
||||
}
|
||||
|
||||
if($workflowFile !== $newWorkflowFile){
|
||||
echo "Writing modified workflow file\n";
|
||||
file_put_contents(__DIR__ . '/main.yml', $newWorkflowFile);
|
||||
}
|
59
.github/workflows/update-updater-api.yml
vendored
59
.github/workflows/update-updater-api.yml
vendored
@ -15,23 +15,68 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: pmmp/update.pmmp.io
|
||||
repository: ${{ github.repository_owner }}/update.pmmp.io
|
||||
ssh-key: ${{ secrets.UPDATE_PMMP_IO_DEPLOY_KEY }}
|
||||
|
||||
- name: Get actual tag name
|
||||
id: tag-name
|
||||
run: echo ::set-output name=TAG_NAME::$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{')
|
||||
run: echo TAG_NAME=$(echo "${{ github.ref }}" | sed 's{^refs/tags/{{') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download new release information
|
||||
run: curl -f -L ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.tag-name.outputs.TAG_NAME }}/build_info.json -o new_build_info.json
|
||||
|
||||
- name: Detect channel
|
||||
- name: Detect channels
|
||||
id: channel
|
||||
run: echo ::set-output name=CHANNEL::$(jq -r '.channel' new_build_info.json)
|
||||
|
||||
- name: Copy release information
|
||||
run: |
|
||||
cp new_build_info.json channels/${{ steps.channel.outputs.CHANNEL }}.json
|
||||
CHANNEL=$(jq -r '.channel' new_build_info.json)
|
||||
VERSION=${{ steps.tag-name.outputs.TAG_NAME }}
|
||||
echo CHANNEL=$CHANNEL >> $GITHUB_OUTPUT
|
||||
if [ "$CHANNEL" == "stable" ]; then
|
||||
echo MAJOR=$(echo $VERSION | cut -d. -f1) >> $GITHUB_OUTPUT
|
||||
echo MINOR=$(echo $VERSION | cut -d. -f1-2) >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo MAJOR=$(echo $VERSION | cut -d. -f1)-$CHANNEL >> $GITHUB_OUTPUT
|
||||
echo MINOR=$(echo $VERSION | cut -d. -f1-2)-$CHANNEL >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Update channel info
|
||||
run: |
|
||||
function version_id() {
|
||||
major=$(echo $1 | cut -d. -f1)
|
||||
minor=$(echo $1 | cut -d. -f2)
|
||||
patch=$(echo $1 | cut -d. -f3)
|
||||
echo $(((major * 1000000) + (minor * 1000) + patch))
|
||||
}
|
||||
|
||||
function update_channel() {
|
||||
local target_file_name="$1"
|
||||
local new_file_name="$2"
|
||||
|
||||
local old_version_id
|
||||
local new_version_id
|
||||
|
||||
if [ ! -f "$target_file_name" ]; then
|
||||
echo "Creating channel file: $target_file_name"
|
||||
cp "$new_file_name" "$target_file_name"
|
||||
else
|
||||
old_version_id=$(version_id "$(jq -r '.base_version' "$target_file_name")")
|
||||
new_version_id=$(version_id "$(jq -r '.base_version' "$new_file_name")")
|
||||
|
||||
echo "Old version ID: $old_version_id"
|
||||
echo "New version ID: $new_version_id"
|
||||
|
||||
if [ $new_version_id -ge $old_version_id ]; then #suffixed versions will have the same version ID - assume they'll always be newer
|
||||
echo "Updating channel file: $target_file_name ($old_version_id -> $new_version_id)"
|
||||
cp "$new_file_name" "$target_file_name"
|
||||
else
|
||||
echo "Version $new_version_id is less than $old_version_id, not updating channel file: $target_file_name"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
update_channel "channels/${{ steps.channel.outputs.CHANNEL }}.json" "new_build_info.json"
|
||||
update_channel "channels/${{ steps.channel.outputs.MAJOR }}.json" "new_build_info.json"
|
||||
update_channel "channels/${{ steps.channel.outputs.MINOR }}.json" "new_build_info.json"
|
||||
rm new_build_info.json
|
||||
|
||||
- name: Commit changes
|
||||
|
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
@ -30,6 +30,13 @@
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Markdown">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="PHP">
|
||||
<option name="CLASS_BRACE_STYLE" value="1" />
|
||||
<option name="METHOD_BRACE_STYLE" value="1" />
|
||||
|
10
BUILDING.md
10
BUILDING.md
@ -2,13 +2,13 @@
|
||||
## Pre-requisites
|
||||
- A bash shell (git bash is sufficient for Windows)
|
||||
- [`git`](https://git-scm.com) available in your shell
|
||||
- PHP 8.0 or newer available in your shell
|
||||
- PHP 8.1 or newer available in your shell
|
||||
- [`composer`](https://getcomposer.org) available in your shell
|
||||
|
||||
## Custom PHP binaries
|
||||
Because PocketMine-MP requires several non-standard PHP extensions and configuration, PMMP provides scripts to build custom binaries for running PocketMine-MP, as well as prebuilt binaries.
|
||||
|
||||
- [Prebuilt binaries](https://jenkins.pmmp.io/job/PHP-8.0-Aggregate)
|
||||
- [Prebuilt binaries](https://github.com/pmmp/PHP-Binaries/releases)
|
||||
- [Compile scripts](https://github.com/pmmp/php-build-scripts) are provided as a submodule in the path `build/php`
|
||||
|
||||
If you use a custom binary, you'll need to replace `composer` usages in this guide with `path/to/your/php path/to/your/composer.phar`.
|
||||
@ -29,11 +29,5 @@ Run `composer make-server` using your preferred PHP binary. It'll drop a `Pocket
|
||||
|
||||
You can also use the `--out` option to change the output filename.
|
||||
|
||||
There is a bug in PHP that might cause an error which looks like this:
|
||||
```
|
||||
Fatal error: Uncaught BadMethodCallException: unable to create temporary file in PocketMine-MP/build/server-phar.php:119
|
||||
```
|
||||
You can work around it by setting `ulimit -n` to some bigger number, e.g. `8192`, or by updating your PHP version to at least 8.0.3.
|
||||
|
||||
## Running PocketMine-MP from source code
|
||||
Run `src/PocketMine.php` using your preferred PHP binary.
|
||||
|
@ -18,24 +18,49 @@ Larger contributions like feature additions should be preceded by a [Change Prop
|
||||
## Other things you'll need
|
||||
- [git](https://git-scm.com/)
|
||||
|
||||
## List of `pocketmine` namespaces which are in other repos
|
||||
PocketMine-MP has several dependencies which are independent from the main server code. Most of them use the `pocketmine` namespace.
|
||||
Some of these add extra classes to packages which already exist in PocketMine-MP.
|
||||
|
||||
Take a look at the table below if you can't find the class or function you're looking for.
|
||||
|
||||
| Source URL | Namespace, class or function |
|
||||
|:----------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [pmmp/BedrockProtocol](https://github.com/pmmp/BedrockProtocol) | `pocketmine\network\mcpe\protocol` |
|
||||
| [pmmp/BinaryUtils](https://github.com/pmmp/BinaryUtils) | `pocketmine\utils\BinaryDataException`</br>`pocketmine\utils\BinaryStream`</br>`pocketmine\utils\Binary` |
|
||||
| [pmmp/ClassLoader](https://github.com/pmmp/`ClassLoader`) | `BaseClassLoader`</br>`ClassLoader`</br>`DynamicClassLoader` |
|
||||
| [pmmp/Color](https://github.com/pmmp/Color) | `pocketmine\color` |
|
||||
| [pmmp/ErrorHandler](https://github.com/pmmp/ErrorHandler) | `pocketmine\errorhandler` |
|
||||
| [pmmp/LogPthreads](https://github.com/pmmp/LogPthreads) | `ThreadedLoggerAttachment`</br>`ThreadedLogger`</br>`AttachableThreadedLogger` |
|
||||
| [pmmp/Log](https://github.com/pmmp/Log) | `AttachableLogger`</br>`BufferedLogger`</br>`GlobalLogger`</br>`LogLevel`</br>`Logger`</br>`PrefixedLogger`</br>`SimpleLogger` |
|
||||
| [pmmp/Math](https://github.com/pmmp/Math) | `pocketmine\math` |
|
||||
| [pmmp/NBT](https://github.com/pmmp/NBT) | `pocketmine\nbt` |
|
||||
| [pmmp/RakLibIpc](https://github.com/pmmp/RakLibIpc) | `raklib\server\ipc` |
|
||||
| [pmmp/RakLib](https://github.com/pmmp/RakLib) | `raklib` |
|
||||
| [pmmp/Snooze](https://github.com/pmmp/Snooze) | `pocketmine\snooze` |
|
||||
| [pmmp/ext-chunkutils2](https://github.com/pmmp/ext-chunkutils2) | `pocketmine\world\format\LightArray`</br>`pocketmine\world\format\PalettedBlockArray`</br>`pocketmine\world\format\io\SubChunkConverter` |
|
||||
| [pmmp/ext-morton](https://github.com/pmmp/ext-morton) | `morton2d_decode`</br>`morton2d_encode`</br>`morton3d_decode`</br>`morton3d_encode` |
|
||||
| [pmmp/ext-libdeflate](https://github.com/pmmp/ext-libdeflate) | `libdeflate_deflate_compress`</br>`libdeflate_gzip_compress`</br>`libdeflate_zlib_compress` |
|
||||
|
||||
## Choosing a target branch
|
||||
PocketMine-MP has three primary branches of development.
|
||||
|
||||
| Type of change | `stable` | `next-minor` | `next-major` |
|
||||
|:---------------|:--------:|:------------:|:------------:|
|
||||
| Bug fixes | ✔️ | ✔️ | ✔️ |
|
||||
| Improvements to API docs | ✔️ | ✔️ | ✔️ |
|
||||
| Cleaning up code | ❌ | ✔️ | ✔️ |
|
||||
| Changing code formatting or style | ❌ | ✔️ | ✔️ |
|
||||
| Addition of new core features | ❌ | 🟡 Only if non-disruptive | ✔️ |
|
||||
| Changing core behaviour (e.g. making something use threads) | ❌ | ✔️ | ✔️ |
|
||||
| Addition of new configuration options | ❌ | 🟡 Only if optional | ✔️ |
|
||||
| Addition of new API classes, methods or constants | ❌ | ✔️ | ✔️ |
|
||||
| Deprecating API classes, methods or constants | ❌ | ✔️ | ✔️ |
|
||||
| Adding optional parameters to an API method | ❌ | ✔️ | ✔️ |
|
||||
| Changing API behaviour | ❌ | 🟡 Only if backwards-compatible | ✔️ |
|
||||
| Removal of API | ❌ | ❌ | ✔️ |
|
||||
| Backwards-incompatible API change (e.g. renaming a method) | ❌ | ❌ | ✔️ |
|
||||
| Type of change | `stable` | `minor-next` | `major-next` |
|
||||
|:--------------------------------------------------------------------------------------------|:--------:|:-------------------------------:|:------------:|
|
||||
| Bug fixes | ✔️ | ✔️ | ✔️ |
|
||||
| Improvements to API docs | ✔️ | ✔️ | ✔️ |
|
||||
| Cleaning up code | ❌ | ✔️ | ✔️ |
|
||||
| Changing code formatting or style | ❌ | ✔️ | ✔️ |
|
||||
| Addition of new core features | ❌ | 🟡 Only if non-disruptive | ✔️ |
|
||||
| Changing core behaviour (e.g. making something use threads) | ❌ | ✔️ | ✔️ |
|
||||
| Addition of new configuration options | ❌ | 🟡 Only if optional | ✔️ |
|
||||
| Addition of new API classes, methods or constants | ❌ | ✔️ | ✔️ |
|
||||
| Deprecating API classes, methods or constants | ❌ | ✔️ | ✔️ |
|
||||
| Adding optional parameters to an API method | ❌ | ✔️ | ✔️ |
|
||||
| Changing API behaviour | ❌ | 🟡 Only if backwards-compatible | ✔️ |
|
||||
| Removal of API | ❌ | ❌ | ✔️ |
|
||||
| Backwards-incompatible API change (e.g. renaming a method) | ❌ | ❌ | ✔️ |
|
||||
| Backwards-incompatible internals change (e.g. changing things in `pocketmine\network\mcpe`) | ❌ | ✔️ | ✔️ |
|
||||
|
||||
### Notes
|
||||
- **Non-disruptive** means that usage should not be significantly altered by the change.
|
||||
@ -43,6 +68,10 @@ PocketMine-MP has three primary branches of development.
|
||||
- Examples of **disruptive** changes include changing the way the server is run, world format changes (since those require downtime for the user to convert their world).
|
||||
- **API** includes all public and protected classes, functions and constants (unless marked as `@internal`).
|
||||
- Private members are not part of the API, **unless in a trait**.
|
||||
- The `pocketmine\network\mcpe` package is considered implicitly `@internal` in its entirety (see its [README](src/network/mcpe/README.md) for more details).
|
||||
- Minecraft's protocol changes are considered necessary internal changes, and are **not** subject to the same rules.
|
||||
- Protocol changes must always be released in a new minor version, since they disrupt user experience by requiring a client update.
|
||||
- BC-breaking changes to the internal network API are allowed, but only in new minor versions. This ensures that plugins which use the internal network API will not break (though they shouldn't use such API anyway).
|
||||
|
||||
## Making a pull request
|
||||
The basic procedure to create a pull request is:
|
||||
|
@ -14,7 +14,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/pmmp/PocketMine-MP/actions/workflows/main.yml"><img src="https://github.com/pmmp/PocketMine-MP/workflows/CI/badge.svg" alt="CI" /></a>
|
||||
<a href="https://github.com/pmmp/PocketMine-MP/releases/latest"><img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/pmmp/PocketMine-MP?label=release&sort=semver"></a>
|
||||
<a href="https://hub.docker.com/r/pmmp/pocketmine-mp"><img src="https://img.shields.io/docker/v/pmmp/pocketmine-mp?logo=docker&label=image" alt="Docker image version (latest semver)" /></a>
|
||||
<a href="https://discord.gg/bmSAZBG"><img src="https://img.shields.io/discord/373199722573201408?label=discord&color=7289DA&logo=discord" alt="Discord" /></a>
|
||||
<br>
|
||||
<a href="https://github.com/pmmp/PocketMine-MP/releases"><img alt="GitHub all releases" src="https://img.shields.io/github/downloads/pmmp/PocketMine-MP/total?label=downloads%40total"></a>
|
||||
@ -24,7 +23,7 @@
|
||||
## Getting started
|
||||
- [Documentation](http://pmmp.readthedocs.org/)
|
||||
- [Installation instructions](https://pmmp.readthedocs.io/en/rtfd/installation.html)
|
||||
- [Docker image](https://hub.docker.com/r/pmmp/pocketmine-mp)
|
||||
- [Docker image](https://github.com/pmmp/PocketMine-MP/pkgs/container/pocketmine-mp)
|
||||
- [Plugin repository](https://poggit.pmmp.io/plugins)
|
||||
|
||||
## Discussion/Help
|
||||
@ -36,7 +35,7 @@
|
||||
* [Building and running from source](BUILDING.md)
|
||||
* [Developer documentation](https://devdoc.pmmp.io) - General documentation for PocketMine-MP plugin developers
|
||||
* [Latest release API documentation](https://apidoc.pmmp.io) - Doxygen API documentation generated for each release
|
||||
* [Latest bleeding-edge API documentation](https://apidoc-dev.pmmp.io) - Doxygen API documentation generated weekly from `next-major` branch
|
||||
* [Latest bleeding-edge API documentation](https://apidoc-dev.pmmp.io) - Doxygen API documentation generated weekly from `major-next` branch
|
||||
* [DevTools](https://github.com/pmmp/DevTools/) - Development tools plugin for creating plugins
|
||||
* [ExamplePlugin](https://github.com/pmmp/ExamplePlugin/) - Example plugin demonstrating some basic API features
|
||||
* [Contributing Guidelines](CONTRIBUTING.md)
|
||||
|
128
build/generate-bedrockdata-path-consts.php
Normal file
128
build/generate-bedrockdata-path-consts.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\build\generate_bedrockdata_path_consts;
|
||||
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
use function dirname;
|
||||
use function fclose;
|
||||
use function fopen;
|
||||
use function fwrite;
|
||||
use function is_file;
|
||||
use function scandir;
|
||||
use function str_replace;
|
||||
use function strtoupper;
|
||||
use const PHP_EOL;
|
||||
use const pocketmine\BEDROCK_DATA_PATH;
|
||||
use const SCANDIR_SORT_ASCENDING;
|
||||
use const STDERR;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
function constantify(string $permissionName) : string{
|
||||
return strtoupper(str_replace([".", "-"], "_", $permissionName));
|
||||
}
|
||||
|
||||
$files = scandir(BEDROCK_DATA_PATH, SCANDIR_SORT_ASCENDING);
|
||||
if($files === false){
|
||||
fwrite(STDERR, "Couldn't find any files in " . BEDROCK_DATA_PATH . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$consts = [];
|
||||
|
||||
foreach($files as $file){
|
||||
if($file === '.' || $file === '..'){
|
||||
continue;
|
||||
}
|
||||
if($file[0] === '.'){
|
||||
continue;
|
||||
}
|
||||
$path = Path::join(BEDROCK_DATA_PATH, $file);
|
||||
if(!is_file($path)){
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach([
|
||||
'README.md',
|
||||
'LICENSE',
|
||||
'composer.json',
|
||||
] as $ignored){
|
||||
if($file === $ignored){
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$consts[] = $file;
|
||||
}
|
||||
|
||||
$output = fopen(dirname(__DIR__) . '/src/data/bedrock/BedrockDataFiles.php', 'wb');
|
||||
if($output === false){
|
||||
fwrite(STDERR, "Couldn't open output file" . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
fwrite($output, <<<'HEADER'
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\data\bedrock;
|
||||
|
||||
use const pocketmine\BEDROCK_DATA_PATH;
|
||||
|
||||
final class BedrockDataFiles{
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
|
||||
HEADER
|
||||
);
|
||||
|
||||
foreach($consts as $constName => $fileName){
|
||||
fwrite($output, "\tpublic const " . constantify($fileName) . " = BEDROCK_DATA_PATH . '/$fileName';\n");
|
||||
}
|
||||
|
||||
fwrite($output, "}\n");
|
||||
fclose($output);
|
||||
|
||||
echo "Done. Don't forget to run CS fixup after generating code.\n";
|
@ -36,11 +36,12 @@ use function fwrite;
|
||||
use function getopt;
|
||||
use function is_string;
|
||||
use function max;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function sleep;
|
||||
use function sprintf;
|
||||
use function str_pad;
|
||||
use function strlen;
|
||||
use function strtolower;
|
||||
use function system;
|
||||
use const STDERR;
|
||||
use const STDIN;
|
||||
@ -102,22 +103,43 @@ function main() : void{
|
||||
$filteredOpts[$optName] = $optValue;
|
||||
}
|
||||
|
||||
$channel = $filteredOpts["channel"] ?? null;
|
||||
if(isset($filteredOpts["current"])){
|
||||
$currentVer = new VersionString($filteredOpts["current"]);
|
||||
}else{
|
||||
$currentVer = new VersionString(VersionInfo::BASE_VERSION);
|
||||
}
|
||||
if(isset($filteredOpts["next"])){
|
||||
$nextVer = new VersionString($filteredOpts["next"]);
|
||||
|
||||
$nextVer = isset($filteredOpts["next"]) ? new VersionString($filteredOpts["next"]) : null;
|
||||
|
||||
$suffix = $currentVer->getSuffix();
|
||||
if($suffix !== ""){
|
||||
if($channel === "stable"){
|
||||
fwrite(STDERR, "error: cannot release a suffixed build into the stable channel\n");
|
||||
exit(1);
|
||||
}
|
||||
if(preg_match('/^([A-Za-z]+)(\d+)$/', $suffix, $matches) !== 1){
|
||||
echo "error: invalid current version suffix \"$suffix\"; aborting\n";
|
||||
exit(1);
|
||||
}
|
||||
$nextVer ??= new VersionString(sprintf(
|
||||
"%u.%u.%u-%s%u",
|
||||
$currentVer->getMajor(),
|
||||
$currentVer->getMinor(),
|
||||
$currentVer->getPatch(),
|
||||
$matches[1],
|
||||
((int) $matches[2]) + 1
|
||||
));
|
||||
$channel ??= strtolower($matches[1]);
|
||||
}else{
|
||||
$nextVer = new VersionString(sprintf(
|
||||
$nextVer ??= new VersionString(sprintf(
|
||||
"%u.%u.%u",
|
||||
$currentVer->getMajor(),
|
||||
$currentVer->getMinor(),
|
||||
$currentVer->getPatch() + 1
|
||||
));
|
||||
$channel ??= "stable";
|
||||
}
|
||||
$channel = $filteredOpts["channel"] ?? VersionInfo::BUILD_CHANNEL;
|
||||
|
||||
echo "About to tag version $currentVer. Next version will be $nextVer.\n";
|
||||
echo "$currentVer will be published on release channel \"$channel\".\n";
|
||||
@ -137,9 +159,6 @@ function main() : void{
|
||||
replaceVersion($versionInfoPath, $nextVer->getBaseVersion(), true, $channel);
|
||||
systemWrapper('git add "' . $versionInfoPath . '"', "failed to stage changes for post-release commit");
|
||||
systemWrapper('git commit -m "' . $nextVer->getBaseVersion() . ' is next" --include "' . $versionInfoPath . '"', "failed to create post-release commit");
|
||||
echo "pushing changes in 5 seconds\n";
|
||||
sleep(5);
|
||||
systemWrapper('git push origin HEAD ' . $currentVer->getBaseVersion(), "failed to push changes to remote");
|
||||
}
|
||||
|
||||
main();
|
||||
|
Submodule build/php updated: b2207cf70d...f2ece7b30d
@ -20,3 +20,19 @@ Released 21st February 2023.
|
||||
- Fixed dropped items not despawning when in non-ticking chunks.
|
||||
- Fixed dropped items not despawning if an infinite pickup delay is set.
|
||||
- Fixed infinite despawn delay (never despawn) being ignored for dropped items.
|
||||
|
||||
# 4.15.2
|
||||
Released 24th February 2023.
|
||||
|
||||
## General
|
||||
- Accept Minecraft: Bedrock Edition 1.19.63 (identical protocol to 1.19.62, but different version due to Mojang mixup).
|
||||
|
||||
## Fixes
|
||||
- Fixed `World Population` timer sometimes not being stopped, causing strange results in timings reports.
|
||||
|
||||
# 4.15.3
|
||||
Released 7th March 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed `/dumpmemory` crash when any object contained an `INF` or `NaN` float value.
|
||||
- Updated RakLib for security fixes.
|
||||
|
45
changelogs/4.16-beta.md
Normal file
45
changelogs/4.16-beta.md
Normal file
@ -0,0 +1,45 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.62**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the protocol and compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
|
||||
|
||||
# 4.16.0-BETA1
|
||||
Released 4th March 2023.
|
||||
|
||||
## General
|
||||
- Added granular timings for packet encode, similar to the existing timings for packet decode.
|
||||
- Timings now covers several areas of the network system which were previously not counted by network timings, but were counted by total timings. This provides a better insight into the performance of the network system.
|
||||
|
||||
## Performance
|
||||
- Improved performance of packet batch handling by avoiding unnecessary object allocations.
|
||||
- Improved performance of packet broadcasting when the broadcast size is below the batch threshold. Previously, the packets would be encoded once by every recipient, but now they are encoded once and then added to the send buffer of each session in their raw form.
|
||||
- This change mostly affects servers with larger maps, where players are more widely distributed.
|
||||
|
||||
## Build system
|
||||
- Added a new script `build/generate-bedrockdata-path-consts.php`, which must be run whenever BedrockData is updated. This script generates a class of constants with the file paths of all BedrockData files.
|
||||
|
||||
## API
|
||||
### `pocketmine\entity`
|
||||
- The following new API methods have been added:
|
||||
- `public Entity->getGravity() : float` - returns the entity's gravity acceleration in blocks/tick^2
|
||||
- `public Entity->setGravity(float $gravity) : void` - sets the entity's gravity acceleration in blocks/tick^2
|
||||
|
||||
## Internals
|
||||
- Now uses [`pocketmine/bedrock-data` 2.0.0](https://github.com/pmmp/BedrockData/releases/tag/2.0.0+bedrock-1.19.60).
|
||||
- This version is now used by both PM4 and PM5, reducing maintenance burden.
|
||||
- Now uses [`pocketmine/bedrock-protocol` 19.3.0](https://github.com/pmmp/BedrockProtocol/releases/tag/19.3.0+bedrock-1.19.62).
|
||||
- This version provides new APIs for handling packet batches which enabled improving performance and adding new features, such as detailed packet encode timings.
|
||||
- Crafting recipes and creative inventory data are now loaded from `recipes/legacy_recipes.json` and `recipes/legacy_creativeitems.json` respectively. Previously, these were loaded from BedrockData directly, but BedrockData 2.0 now uses a format which can't be supported in 4.x without BC breaks.
|
||||
- Added dependencies on [`pocketmine/bedrock-block-upgrade-schema`](https://github.com/pmmp/BedrockBlockUpgradeSchema) and [`pocketmine/bedrock-item-upgrade-schema`](https://github.com/pmmp/BedrockItemUpgradeSchema). These provide mapping files no longer present in BedrockData 2.0.
|
||||
- Reduced and/or eliminated most usages of `PacketBatch`, since it only appeared as a throwaway object and was therefore wasting performance.
|
||||
- `Compressor` now exposes `getCompressionThreshold()` instead of `willCompress()`, which allows determining whether a batch will be compressed without allocating it.
|
||||
- Added `pocketmine\data\bedrock\BedrockDataFiles`, an auto-generated class of constants with the file paths of all BedrockData files. This makes it easier to locate usages, detect unused files and avoid typos.
|
||||
|
||||
# 4.16.0-BETA2
|
||||
Released 4th March 2023.
|
||||
|
||||
## General
|
||||
- Fixed incorrect release channel for 4.16.0-BETA1.
|
41
changelogs/4.16.md
Normal file
41
changelogs/4.16.md
Normal file
@ -0,0 +1,41 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.62**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the protocol and compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
|
||||
|
||||
# 4.16.0
|
||||
Released 7th March 2023.
|
||||
|
||||
## General
|
||||
- Added granular timings for packet encode, similar to the existing timings for packet decode.
|
||||
- Split `Player Network Send - Compression` timings into two timers, one for `Session Buffer` compression and one for `Broadcast` compression.
|
||||
- Timings now covers several areas of the network system which were previously not counted by network timings, but were counted by total timings. This provides a better insight into the performance of the network system.
|
||||
|
||||
## Performance
|
||||
- Improved performance of packet batch handling by avoiding unnecessary object allocations.
|
||||
- Improved performance of packet broadcasting when the broadcast size is below the batch threshold. Previously, the packets would be encoded once by every recipient, but now they are encoded once and then added to the send buffer of each session in their raw form.
|
||||
- This change mostly affects servers with larger maps, where players are more widely distributed.
|
||||
- Improved performance of packet broadcasting when the broadcast has only one recipient (allow the session to compress the packet with the rest of its buffer).
|
||||
|
||||
## Build system
|
||||
- Added a new script `build/generate-bedrockdata-path-consts.php`, which must be run whenever BedrockData is updated. This script generates a class of constants with the file paths of all BedrockData files.
|
||||
|
||||
## API
|
||||
### `pocketmine\entity`
|
||||
- The following new API methods have been added:
|
||||
- `public Entity->getGravity() : float` - returns the entity's gravity acceleration in blocks/tick^2
|
||||
- `public Entity->setGravity(float $gravity) : void` - sets the entity's gravity acceleration in blocks/tick^2
|
||||
|
||||
## Internals
|
||||
- Now uses [`pocketmine/bedrock-data` 2.0.0](https://github.com/pmmp/BedrockData/releases/tag/2.0.0+bedrock-1.19.60).
|
||||
- This version is now used by both PM4 and PM5, reducing maintenance burden.
|
||||
- Now uses [`pocketmine/bedrock-protocol` 19.3.0](https://github.com/pmmp/BedrockProtocol/releases/tag/19.3.0+bedrock-1.19.62).
|
||||
- This version provides new APIs for handling packet batches which enabled improving performance and adding new features, such as detailed packet encode timings.
|
||||
- Crafting recipes and creative inventory data are now loaded from `recipes/legacy_recipes.json` and `recipes/legacy_creativeitems.json` respectively. Previously, these were loaded from BedrockData directly, but BedrockData 2.0 now uses a format which can't be supported in 4.x without BC breaks.
|
||||
- Added dependencies on [`pocketmine/bedrock-block-upgrade-schema`](https://github.com/pmmp/BedrockBlockUpgradeSchema) and [`pocketmine/bedrock-item-upgrade-schema`](https://github.com/pmmp/BedrockItemUpgradeSchema). These provide mapping files no longer present in BedrockData 2.0.
|
||||
- Reduced and/or eliminated most usages of `PacketBatch`, since it only appeared as a throwaway object and was therefore wasting performance.
|
||||
- `Compressor` now exposes `getCompressionThreshold()` instead of `willCompress()`, which allows determining whether a batch will be compressed without allocating it.
|
||||
- Added `pocketmine\data\bedrock\BedrockDataFiles`, an auto-generated class of constants with the file paths of all BedrockData files. This makes it easier to locate usages, detect unused files and avoid typos.
|
39
changelogs/4.17.md
Normal file
39
changelogs/4.17.md
Normal file
@ -0,0 +1,39 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.70**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the protocol and compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the protocol, you're not shielded by API change constraints.** You should consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you do.
|
||||
|
||||
# 4.17.0
|
||||
Released 14th March 2023.
|
||||
|
||||
## General
|
||||
- Added support for Minecraft: Bedrock Edition 1.19.70.
|
||||
- Removed support for older versions.
|
||||
|
||||
# 4.17.1
|
||||
Released 22nd March 2023.
|
||||
|
||||
## General
|
||||
- Docker images for PocketMine-MP are now published on [GitHub Container Registry](https://github.com/pmmp/PocketMine-MP/pkgs/container/pocketmine-mp). The Docker Hub images will stop being maintained in the future.
|
||||
- Updated translations.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash on empty packets in certain cases.
|
||||
- Fixed mushroom blocks dropping the wrong items when broken with a silk-touch tool.
|
||||
- Fixed mushroom blocks giving the wrong items when block-picked.
|
||||
- Fixed missing ability flag `PRIVILEGED_BUILDER`.
|
||||
|
||||
## Internals
|
||||
- `update-updater-api.yml` workflow now uses `github.repository_owner` to make it easier to test the workflow on forks.
|
||||
- Added version-specific channels to `update.pmmp.io`, such as `4`, `4.18-beta`, `4.17`, etc.
|
||||
- Replaced deprecated `::set-output` commands in GitHub Actions workflows.
|
||||
- `build/make-release.php` no longer automatically pushes changes, to avoid accidents when testing release workflows on forks.
|
||||
|
||||
# 4.17.2
|
||||
Released 29th March 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed players being unable to join due to the appearance of a new `x5t` field in the JWT header of Xbox Live authentication tokens.
|
91
changelogs/4.18-alpha.md
Normal file
91
changelogs/4.18-alpha.md
Normal file
@ -0,0 +1,91 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.70**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace are compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
### Alpha release warning
|
||||
Alpha releases are **experimental**. Features introduced in these releases are subject to change or removal.
|
||||
|
||||
APIs which existed **prior** to this version will continue to work as normal, so plugins which use them will continue to work.
|
||||
|
||||
### Highlights
|
||||
This version makes changes to the internal network system to improve server performance and reduce memory usage.
|
||||
|
||||
While these changes don't affect non-internal API, they are still significant enough to warrant a new minor version, as they may break plugins which use the internal network API (not recommended).
|
||||
|
||||
# 4.18.0-ALPHA1
|
||||
Released 16th March 2023.
|
||||
|
||||
## General
|
||||
- Improved server performance in congested areas of the world (lots of players and/or entities in the same area).
|
||||
|
||||
## API
|
||||
### `pocketmine\event\server`
|
||||
- The following new classes have been added:
|
||||
- `DataPacketDecodeEvent` - called before a packet is decoded by a `NetworkSession`; useful to mitigate DoS attacks if PocketMine-MP hasn't been patched against new bugs yet
|
||||
|
||||
## Internals
|
||||
- Introduced new system for broadcasting entity events to network sessions.
|
||||
- This change improves performance when lots of players and/or entities are in the same area.
|
||||
- New interface `EntityEventBroadcaster` and class `StandardEntityEventBroadcaster` have been added to implement this.
|
||||
- All entity-specific `on*()` and `sync*()` methods have been removed from `NetworkSession` (BC break).
|
||||
- `NetworkSession` now accepts an `EntityEventBroadcaster` instance in its constructor.
|
||||
- `NetworkBroadcastUtils::broadcastEntityEvent()` can be used to efficiently broadcast events to unique broadcasters shared by several network sessions.
|
||||
- All network sessions now share the same `PacketSerializerContext` and `PacketBroadcaster` by default.
|
||||
- Previously, every session had its own context, meaning that broadcast optimisations were not used, causing significant performance losses compared to 3.x.
|
||||
- This change improves performance in congested areas by allowing reuse of previously encoded packet buffers for all sessions sharing the same context.
|
||||
- Packet broadcasts are automatically encoded separately per unique `PacketSerializerContext` instance. This allows, for example, a multi-version fork to have a separate context for each protocol version, to ensure maximum broadcast efficiency while encoding different packets for different versions.
|
||||
- `PacketSerializerContext` is now passed in `NetworkSession::__construct()`, instead of being created by the session.
|
||||
- `StandardPacketBroadcaster` is now locked to a single `PacketSerializer` context, reducing complexity.
|
||||
- Introduced `NetworkBroadcastUtils::broadcastPackets()`, replacing `Server->broadcastPackets()`.
|
||||
- `Server->broadcastPackets()` has been deprecated. It will be removed in a future version.
|
||||
|
||||
# 4.18.0-ALPHA2
|
||||
Released 21st March 2023.
|
||||
|
||||
## General
|
||||
- Included more sections of the network system in Player Network Send timings.
|
||||
- Changed the names of some timings to make them more user-friendly.
|
||||
- Removed packet IDs from `receivePacket` and `sendPacket` timings, as they were not very useful.
|
||||
- Added new specialized timers for the following:
|
||||
- Item entity base ticking (merging)
|
||||
- Player movement processing
|
||||
- Entity movement processing (collision checking section)
|
||||
- Projectile movement (all)
|
||||
- Projectile movement processing (ray tracing section)
|
||||
|
||||
## API
|
||||
### `pocketmine\crafting`
|
||||
- The following new API methods have been added:
|
||||
- `CraftingManager->getCraftingRecipeIndex() : array<int, CraftingRecipe>` - returns a list of all crafting recipes
|
||||
- `CraftingManager->getCraftingRecipeFromIndex(int $index) : ?CraftingRecipe` - returns the crafting recipe at the given index, or null if it doesn't exist
|
||||
|
||||
### `pocketmine\inventory\transaction`
|
||||
- The following API methods have changed signatures:
|
||||
- `CraftingTransaction->__construct()` now accepts additional arguments `?CraftingRecipe $recipe = null, ?int $repetitions = null`
|
||||
- The following new API methods have been added:
|
||||
- `TransactionBuilderInventory->getActualInventory() : Inventory` - returns the actual inventory that this inventory is a proxy for
|
||||
|
||||
## Internals
|
||||
### Network
|
||||
- Introduced support for the `ItemStackRequest` Minecraft: Bedrock network protocol.
|
||||
- This fixes a large number of inventory- and crafting-related bugs.
|
||||
- This also improves server security by closing off many code pathways that might have been used for exploits. `TypeConverter->netItemStackToCore()` is no longer used in server code, and remains for tool usage only.
|
||||
- This system is also significantly more bandwidth-efficient and has lower overhead than the legacy system.
|
||||
- This now opens the gateway to easily implement lots of gameplay features which have been missing for a long time, such as enchanting, anvils, looms, and more.
|
||||
- Significant changes have been made to `pocketmine\network\mcpe\InventoryManager` internals. These shouldn't affect plugins, but may affect plugins which use internal network API.
|
||||
- **No changes have been made to the plugin `InventoryTransaction` API**.
|
||||
- This system has been implemented as a shim for the existing PocketMine-MP transaction system to preserve plugin compatibility. Plugins using `InventoryTransactionEvent` should continue to work seamlessly.
|
||||
- The `InventoryTransaction` API will be redesigned in a future major version to make use of the new information provided by the `ItemStackRequest` system.
|
||||
- `InventoryTransactionPacket` is no longer sent by the client for "regular" inventory actions. However, it is still sent when dropping items, interacting with blocks, and using items.
|
||||
- Inventory slot and content syncing is now buffered until the end of the tick. This reduces outbound network usage when the client performs multiple transactions in a single tick (e.g. crafting a stack of items).
|
||||
- Renamed some `InventoryManager` internal properties to make them easier to understand.
|
||||
- `TypeConverter->createInventoryAction()` has been removed.
|
||||
- Packet batch limit has been lowered to `100` packets. With the introduction of `ItemStackRequest`, this is more than sufficient for normal gameplay.
|
||||
|
||||
### Other
|
||||
- Use `Vector3::zero()` instead of `new Vector3()` in some places.
|
123
changelogs/4.18.md
Normal file
123
changelogs/4.18.md
Normal file
@ -0,0 +1,123 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.70**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace are compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
### Highlights
|
||||
This version significantly improves server performance with many players and/or entities by making changes to the internal network system.
|
||||
|
||||
It also introduces support for the newer `ItemStackRequest` protocol, which fixes many bugs and improves server security.
|
||||
|
||||
While these changes don't affect non-internal API, they are still significant enough to warrant a new minor version, as they may break plugins which use the internal network API (not recommended).
|
||||
|
||||
# 4.18.0
|
||||
Released 25th March 2023.
|
||||
|
||||
## General
|
||||
- Significantly improved server performance in congested areas of the world (lots of players and/or entities in the same area).
|
||||
- Included more sections of the network system in `Player Network Send` performance timings.
|
||||
- Changed the names of some performance timings to make them more user-friendly.
|
||||
- Removed packet IDs from `receivePacket` and `sendPacket` performance timings, as they were not very useful.
|
||||
- Added new specialized performance timings for the following:
|
||||
- Item entity base ticking (merging)
|
||||
- Player movement processing
|
||||
- Entity movement processing (collision checking section)
|
||||
- Projectile movement (all)
|
||||
- Projectile movement processing (ray tracing section)
|
||||
|
||||
## API
|
||||
### `pocketmine\crafting`
|
||||
- The following new API methods have been added:
|
||||
- `CraftingManager->getCraftingRecipeIndex() : array<int, CraftingRecipe>` - returns a list of all crafting recipes
|
||||
- `CraftingManager->getCraftingRecipeFromIndex(int $index) : ?CraftingRecipe` - returns the crafting recipe at the given index, or null if it doesn't exist
|
||||
|
||||
### `pocketmine\event\server`
|
||||
- The following new classes have been added:
|
||||
- `DataPacketDecodeEvent` - called before a packet is decoded by a `NetworkSession`; useful to mitigate DoS attacks if PocketMine-MP hasn't been patched against new bugs yet
|
||||
|
||||
### `pocketmine\inventory\transaction`
|
||||
- The following API methods have changed signatures:
|
||||
- `CraftingTransaction->__construct()` now accepts additional arguments `?CraftingRecipe $recipe = null, ?int $repetitions = null`
|
||||
- The following new API methods have been added:
|
||||
- `TransactionBuilderInventory->getActualInventory() : Inventory` - returns the actual inventory that this inventory is a proxy for
|
||||
|
||||
## Internals
|
||||
### Network
|
||||
- Introduced new system for broadcasting entity events to network sessions.
|
||||
- This change improves performance when lots of players and/or entities are in the same area.
|
||||
- New interface `EntityEventBroadcaster` and class `StandardEntityEventBroadcaster` have been added to implement this.
|
||||
- All entity-specific `on*()` and `sync*()` methods have been removed from `NetworkSession` (internals backwards compatibility break, not covered by API version guarantee).
|
||||
- `NetworkSession` now accepts an `EntityEventBroadcaster` instance in its constructor.
|
||||
- `NetworkBroadcastUtils::broadcastEntityEvent()` can be used to efficiently broadcast events to unique broadcasters shared by several network sessions.
|
||||
- All network sessions now share the same `PacketSerializerContext` and `PacketBroadcaster` by default.
|
||||
- Previously, every session had its own context, meaning that broadcast optimisations were not used, causing significant performance losses compared to 3.x.
|
||||
- This change improves performance in congested areas by allowing reuse of previously encoded packet buffers for all sessions sharing the same context.
|
||||
- Packet broadcasts are automatically encoded separately per unique `PacketSerializerContext` instance. This allows, for example, a multi-version fork to have a separate context for each protocol version, to ensure maximum broadcast efficiency while encoding different packets for different versions.
|
||||
- `PacketSerializerContext` is now passed in `NetworkSession::__construct()`, instead of being created by the session.
|
||||
- `StandardPacketBroadcaster` is now locked to a single `PacketSerializer` context, reducing complexity.
|
||||
- Introduced `NetworkBroadcastUtils::broadcastPackets()`, replacing `Server->broadcastPackets()`.
|
||||
- `Server->broadcastPackets()` has been deprecated. It will be removed in a future version.
|
||||
- Introduced support for the `ItemStackRequest` Minecraft: Bedrock network protocol.
|
||||
- This fixes a large number of inventory- and crafting-related bugs.
|
||||
- This also improves server security by closing off many code pathways that might have been used for exploits. `TypeConverter->netItemStackToCore()` is no longer used in server code, and remains for tool usage only.
|
||||
- This system is also significantly more bandwidth-efficient and has lower overhead than the legacy system.
|
||||
- This now opens the gateway to easily implement lots of gameplay features which have been missing for a long time, such as enchanting, anvils, looms, and more.
|
||||
- Significant changes have been made to `pocketmine\network\mcpe\InventoryManager` internals. These shouldn't affect plugins, but may affect plugins which use internal network API.
|
||||
- **No changes have been made to the plugin `InventoryTransaction` API**.
|
||||
- This system has been implemented as a shim for the existing PocketMine-MP transaction system to preserve plugin compatibility. Plugins using `InventoryTransactionEvent` should continue to work seamlessly.
|
||||
- The `InventoryTransaction` API will be redesigned in a future major version to make use of the new information provided by the `ItemStackRequest` system.
|
||||
- `InventoryTransactionPacket` is no longer sent by the client for "regular" inventory actions. However, it is still sent when dropping items, interacting with blocks, and using items.
|
||||
- Inventory slot and content syncing is now buffered until the end of the tick. This reduces outbound network usage when the client performs multiple transactions in a single tick (e.g. crafting a stack of items).
|
||||
- Renamed some `InventoryManager` internal properties to make them easier to understand.
|
||||
- `TypeConverter->createInventoryAction()` has been removed.
|
||||
- Packet batch limit has been lowered to `100` packets. With the introduction of `ItemStackRequest`, this is more than sufficient for normal gameplay.
|
||||
|
||||
### Other
|
||||
- Use `Vector3::zero()` instead of `new Vector3()` in some places.
|
||||
|
||||
# 4.18.1
|
||||
Released 27th March 2023.
|
||||
|
||||
## General
|
||||
- `RakLibInterface` now logs the name of the currently active session if a crash occurs when processing a packet. This makes it easier to reproduce bugs, which is important to be able to fix them.
|
||||
- Added more detailed debugging information to `InventoryManager->syncSelectedHotbarSlot()`.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash when attempting to drop more of an item from a stack than available in the inventory.
|
||||
- Fixed packet processing errors when editing writable books.
|
||||
- Fixed packet processing errors when shift-clicking on the recipe book to craft recipes which draw from a large number of inventory slots.
|
||||
|
||||
# 4.18.2
|
||||
Released 29th March 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed players being unable to join due to the appearance of a new `x5t` field in the JWT header of Xbox Live authentication tokens.
|
||||
- Fixed items' durability appearing to reset when moving them around in the inventory.
|
||||
|
||||
# 4.18.3
|
||||
Released 5th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed Average Players not being shown on timings reports when custom player classes are used.
|
||||
- Fixed incorrect tick violation calculation in timings reports.
|
||||
- Fixed not being able to add or remove items from the offhand slot.
|
||||
- Fixed creative inventory item count corruption when taking items (some players would see 64x items in the creative inventory after rejoining or changing gamemode).
|
||||
- Fixed not being able to drop items directly from the creative inventory on mobile.
|
||||
- Fixed `DataPacketReceiveEvent` not being called for packets sent by `EntityEventBroadcaster`.
|
||||
- `CreativeInventory::getItem()` and `CreativeInventory::getAll()` now return cloned itemstacks, to prevent accidental modification of the creative inventory.
|
||||
|
||||
# 4.18.4
|
||||
Released 10th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed movement becoming broken when the player moves at high speed (e.g. due to high levels of the Speed effect).
|
||||
- Updated dependencies to get fixes in `pocketmine/nbt` and `pocketmine/bedrock-protocol`.
|
||||
|
||||
## Internals
|
||||
### Network
|
||||
- Game packets are now rate-limited in a similar manner to packet batches. This helps to more effectively mitigate certain types of DoS attacks.
|
||||
- Added a new class `PacketRateLimiter`, implementing functionality previously baked directly into `NetworkSession` in a more generic way to allow reuse.
|
111
changelogs/4.19.md
Normal file
111
changelogs/4.19.md
Normal file
@ -0,0 +1,111 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.70**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace are compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
### Highlights
|
||||
This version introduces support for a new, more advanced version of Timings.
|
||||
This improved system provides more detail than the old system, and supports being displayed in a tree view, making it much easier to see which timers contribute to which other timers.
|
||||
|
||||
In addition, some minor performance improvements have been made, along with a couple of minor API additions.
|
||||
|
||||
# 4.19.0
|
||||
Released 11th April 2023.
|
||||
|
||||
## General
|
||||
- Updated the Timings system.
|
||||
- Timings records now include parent information, allowing them to be displayed in a tree view (e.g. https://timings.pmmp.io/?id=303556).
|
||||
- Timings records now include additional information, such as Peak (max time spent on any single tick), and Ticks (number of ticks the timer was active on).
|
||||
- New timings have been added for every event.
|
||||
- A new timer `Player Network Send - Pre-Spawn Game Data` has been added, and covers most of the time spent handling `ResourcePackClientResponsePacket`, giving a clearer picture of what's happening.
|
||||
- Improved performance of the plugin event system.
|
||||
- By introducing some caching, the event system now has 90% less overhead than in previous versions.
|
||||
- Improved performance of the random chunk ticking system.
|
||||
- The selection of ticked random chunks, and their validation for ticking, is now cached. This significantly reduces the overhead of chunk selection.
|
||||
- Factions servers and other game modes with big maps and sparsely populated areas will see the most benefit from this change.
|
||||
- Real-world performance benefit of this change is anywhere from 0-20%, depending on server type and configuration.
|
||||
- The `timings paste` command now logs a debug message with the server response on failure to paste a timings report.
|
||||
|
||||
## API
|
||||
### `pocketmine\entity\object`
|
||||
- The following API constants have been added:
|
||||
- `ExperienceOrb::DEFAULT_DESPAWN_DELAY` - the default delay in ticks before an experience orb despawns
|
||||
- `ExperienceOrb::NEVER_DESPAWN` - magic value for `setDespawnDelay()` to make an experience orb never despawn
|
||||
- `ExperienceOrb::MAX_DESPAWN_DELAY` - the maximum delay in ticks before an experience orb despawns
|
||||
- The following API methods have been added:
|
||||
- `public ExperienceOrb->getDespawnDelay() : int` - returns the delay in ticks before this experience orb despawns
|
||||
- `public ExperienceOrb->setDespawnDelay(int $despawnDelay) : void` - sets the delay in ticks before this experience orb despawns
|
||||
- The following properties have been deprecated
|
||||
- `ExperienceOrb->age` - superseded by despawn delay methods
|
||||
|
||||
### `pocketmine\event`
|
||||
- The following API methods have been added:
|
||||
- `public HandlerList->getListenerList() : list<RegisteredListener>` - returns an ordered list of handlers to be called for the event
|
||||
|
||||
### `pocketmine\player`
|
||||
- The following API methods have behavioural changes:
|
||||
- `ChunkSelector->selectChunks()` now yields the distance in chunks from the center as the key, instead of an incrementing integer.
|
||||
- The following classes have been deprecated:
|
||||
- `PlayerChunkLoader` (this was technically internal, but never marked as such)
|
||||
|
||||
### `pocketmine\timings`
|
||||
- The following API constants have been deprecated:
|
||||
- `Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX` - this is superseded by timings group support (see `Timings::GROUP_BREAKDOWN`)
|
||||
- The following API constants have been added:
|
||||
- `Timings::GROUP_BREAKDOWN` - this group makes a timer appear in the `Minecraft - Breakdown` section of a timings report
|
||||
- The following API methods have been added:
|
||||
- `public TimingsHandler->getGroup() : string` - returns the name of the table in which this timer will appear in a timings report
|
||||
- The following API methods have changed signatures:
|
||||
- `TimingsHandler->__construct()` now accepts an additional, optional `string $group` parameter, which defaults to `Minecraft`.
|
||||
|
||||
### `pocketmine\world`
|
||||
#### Highlights
|
||||
Ticking chunks is now done using the `ChunkTicker` system, which has a much more fine-grained API than the old `TickingChunkLoader` system, as well as better performance.
|
||||
It works similarly to the `ChunkLoader` system, in that chunks will be ticked as long as at least one `ChunkTicker` is registered for them.
|
||||
|
||||
#### API changes
|
||||
- The following classes have been deprecated:
|
||||
- `TickingChunkLoader` - this has been superseded by the more powerful and performant `ChunkTicker` APIs
|
||||
- The following classes have been added:
|
||||
- `ChunkTicker` - an opaque object used for `registerTickingChunk()` to instruct the `World` that we want a chunk to be ticked
|
||||
- The following API methods have been added:
|
||||
- `public World->registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void` - registers a chunk to be ticked by the given `ChunkTicker`
|
||||
- `public World->unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void` - unregisters a chunk from being ticked by the given `ChunkTicker`
|
||||
|
||||
# 4.19.1
|
||||
Released 14th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed inventory rollbacks when spreading items in ender chests.
|
||||
- Fixed inventory rollbacks when shift-clicking to craft and the outputs would have been split across multiple inventory slots.
|
||||
- Fixed incorrect spawn terrain generation for newly created worlds.
|
||||
- Fixed `chunk-ticking.tick-radius` not disabling chunk ticking when set to `0`.
|
||||
- Fixed chunks not being ticked if they previously left a player's simulation distance without leaving their view distance.
|
||||
- Fixed height of collision boxes for Grass Path and Farmland blocks.
|
||||
|
||||
# 4.19.2
|
||||
Released 14th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed player timings duplication leading to extremely large timings reports when timings runs for a long time with many players.
|
||||
- Packet timings are now indexed by class FQN instead of packet ID. This prevents erroneous timer reuse on packet ID reuse (e.g. multi version servers).
|
||||
- Fixed entity timings being shared by different classes with the same short name. This led to incorrect timings being reported for some entities when custom entities were used.
|
||||
|
||||
# 4.19.3
|
||||
Released 21st April 2023.
|
||||
|
||||
## General
|
||||
- Error IDs for `Packet processing error` disconnects are now split into 4-character chunks to make them easier to type (since they can't be copied from the disconnection screen of a client).
|
||||
|
||||
## Fixes
|
||||
- Fixed entity-block intersections being checked twice per tick. Besides wasting CPU time, this may have caused unexpected behaviour during entity-block interactions with blocks like water or cacti.
|
||||
- Fixed performance issue in network inventory synchronization due item NBT being prepared twice.
|
||||
- Fixed `tools/simulate-chunk-selector.php` argument parsing being completely broken (weird behaviour of PHP `getopt()`).
|
||||
|
||||
## Internals
|
||||
- `TimingsHandler->stopTiming()` now logs an error message if a subtimer wasn't stopped, rather than throwing an exception.
|
||||
- Due to interactions between `try...finally` and unexpected errors, throwing exceptions made it difficult for plugin developers to debug errors in their plugins, since it obscured the original error.
|
79
changelogs/4.20.md
Normal file
79
changelogs/4.20.md
Normal file
@ -0,0 +1,79 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.80**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace are compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
### Interim releases
|
||||
If you're upgrading from 4.17.x directly to 4.20.x, please also read the following changelogs, as the interim releases contain important changes:
|
||||
|
||||
- [4.18.0](https://github.com/pmmp/PocketMine-MP/blob/4.20.0/changelogs/4.18.md#4180) - major performance improvements, internal network changes, minor API additions
|
||||
- [4.19.0](https://github.com/pmmp/PocketMine-MP/blob/4.20.0/changelogs/4.19.md#4190) - minor performance improvements, improved timings system, minor API additions
|
||||
|
||||
# 4.20.0
|
||||
Released 26th April 2023.
|
||||
|
||||
## General
|
||||
- Added support for Minecraft: Bedrock Edition 1.19.80.
|
||||
- Removed support for older versions.
|
||||
|
||||
## Fixes
|
||||
- Fixed packet processing error when attempting to use a stonecutter.
|
||||
- Fixed armor slots containing ghost items when cancelling right-click to equip armor.
|
||||
- Fixed crash in `HandlerList->getListenersByPriority()` when no listeners are registered at the given priority.
|
||||
|
||||
## API
|
||||
### `pocketmine\block`
|
||||
- The following API methods have been added:
|
||||
- `public BaseSign->getEditorEntityRuntimeId() : int` - returns the entity runtime ID of the player currently editing this sign, or `null` if none
|
||||
- `public BaseSign->setEditorEntityRuntimeId(?int $editorEntityRuntimeId) : $this` - sets the entity runtime ID of the player currently editing this sign
|
||||
|
||||
### `pocketmine\player`
|
||||
- The following API methods have been added:
|
||||
- `public Player->openSignEditor(Vector3 $position) : void` - opens the client-side sign editor GUI for the given position
|
||||
|
||||
# 4.20.1
|
||||
Released 27th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash when firing a bow while holding arrows in the offhand slot.
|
||||
|
||||
## Internals
|
||||
- `ItemStackContainerIdTranslator::translate()` now requires an additional `int $slotId` parameter and returns `array{int, int}` (translated window ID, translated slot ID) to be used with `InventoryManager->locateWindowAndSlot()`.
|
||||
- `InventoryManager->locateWindowAndSlot()` now checks if the translated slot actually exists in the requested inventory, and returns `null` if not. Previously, it would return potentially invalid slot IDs without checking them, potentially leading to crashes.
|
||||
|
||||
# 4.20.2
|
||||
Released 4th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed all types of wooden logs appearing as oak in the inventory.
|
||||
- Fixed a performance issue in `BaseInventory->canAddItem()` (missing `continue` causing useless logic to run).
|
||||
|
||||
# 4.20.3
|
||||
Released 6th May 2023.
|
||||
|
||||
## Improvements
|
||||
- Reduced memory usage of `RuntimeBlockMapping` from 25 MB to 9 MB. Since every thread has its own copy of the block map, this saves a substantial amount of memory.
|
||||
|
||||
## Fixes
|
||||
- Fixed players falling through blocks in spectator mode.
|
||||
- Fixed timings reports getting bloated by prolific usage of `PluginManager->registerEvent()`.
|
||||
- This was caused by creating a new timings handler for each call, regardless of whether a timer already existed for the given event and callback.
|
||||
- Fixed `Full Server Tick` and other records being missing from timings reports.
|
||||
- This was caused by timings handler depth not getting reset when timings was disabled and later re-enabled.
|
||||
|
||||
# 4.20.4
|
||||
Released 6th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed players being forced into flight mode in every game mode.
|
||||
- Moral of the story: do not assume anything in Mojang internals does what its name suggests...
|
||||
|
||||
# 4.20.5
|
||||
Released 30th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash due to a bug in upstream dependency [`netresearch/jsonmapper`](https://github.com/cweiske/JsonMapper).
|
77
changelogs/4.21.md
Normal file
77
changelogs/4.21.md
Normal file
@ -0,0 +1,77 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.80**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace, and don't use reflection or any internal methods,
|
||||
will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
# 4.21.0
|
||||
Released 17th May 2023.
|
||||
|
||||
## General
|
||||
- PHP 8.1 is now required. Most plugins should run without changes, but some might need to be updated due to language-level deprecations.
|
||||
- Ticking chunk count is now shown separately from loaded chunk count in the `/status` command, providing useful performance information.
|
||||
- Further improved performance of ticking chunk selection.
|
||||
- Improved performance of some inventory functions.
|
||||
- Reduced server memory footprint in most cases by ~9 MB per thread.
|
||||
- Due to large overhead, async network compression is now only used for packets larger than 10 KB by default.
|
||||
|
||||
## Configuration
|
||||
- Added the following new `pocketmine.yml` configuration options:
|
||||
- `network.async-compression-threshold` - minimum size of packet which will be compressed using `AsyncTask`
|
||||
- Default is 10 KB, which means that very few packets will use async compression in practice. This is because the overhead of compressing async is currently so high that it's not worth it for smaller packets.
|
||||
|
||||
## Timings
|
||||
- Timings reports no longer include the unused metadata fields `Entities` and `LivingEntities`.
|
||||
- Timings reports now correctly calculate the peak time of a timer.
|
||||
- Previously it was incorrectly recorded as the longest time spent in a single tick, rather than the longest time spent in a single activation.
|
||||
- Timings report version has been bumped to `2` to reflect this change.
|
||||
- All world-specific timers now have generic aggregate timings, making it much easier to locate performance patterns across all worlds.
|
||||
|
||||
## Gameplay
|
||||
- Players in spectator mode are no longer able to pick blocks, and now have finite resources similar to survival mode.
|
||||
|
||||
## API
|
||||
### `pocketmine\world`
|
||||
- The following API methods have been added:
|
||||
- `public World->getTickingChunks() : list<int>` - returns a list of chunk position hashes (a la `World::chunkHash()`) which are currently valid for random ticking
|
||||
|
||||
### `pocketmine\inventory`
|
||||
- The following API methods have been added:
|
||||
- `protected BaseInventory->getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int` - returns the number of items in the given stack if the content of the slot matches the test item, or zero otherwise
|
||||
- This should be overridden if directly extending `BaseInventory` to provide a performance-optimised version. A slow default implementation is provided, but it will be removed in the future.
|
||||
|
||||
## Internals
|
||||
### Entity
|
||||
- Unused `NameTag` tag is no longer saved for `Human` entities.
|
||||
|
||||
### Inventory
|
||||
- `BaseInventory` now uses a new internal method `getMatchingItemCount()` to locate items in the inventory without useless cloning. This improves performance of various API methods, such as `addItem()`, `contains()`, and more.
|
||||
- Specialization of `Inventory->isSlotEmpty()` in `BaseInventory` subclasses has been added to improve performance of some API methods.
|
||||
|
||||
### Network
|
||||
- `RuntimeBlockMapping` no longer keeps all block palette NBT data in memory.
|
||||
- This significantly reduces server idle memory footprint.
|
||||
- For multi-version implementations, this will have a significant impact on memory usage, since a different block palette is often required to support each version.
|
||||
- NBT will be lazy-loaded into memory and cached if `getBedrockKnownStates()` is called. However, this is not used by PocketMine-MP under normal circumstances.
|
||||
- Removed unnecessary usage of `Utils::validateCallableSignature()` from some internal network pathways, improving performance.
|
||||
|
||||
### Scheduler
|
||||
- `AsyncPool` no longer double-checks progress updates on completed tasks.
|
||||
|
||||
### World
|
||||
- Ticking chunks are now tracked in `World->validTickingChunks` and `World->recheckTickingChunks`.
|
||||
- This allows avoiding rechecking every ticking chunk for validity during ticking chunk selection, improving performance.
|
||||
- In some cases, this allows bypassing chunk selection entirely, reducing selection cost to zero.
|
||||
- Registered but ineligible ticking chunks are no longer rechecked every tick.
|
||||
- This was causing wasted cycles during async worker backlog.
|
||||
- The internal system must call `markTickingChunkForRecheck()` whenever a ticking chunk's eligibility for ticking has potentially changed, rather than just when it has changed from a yes to a no.
|
||||
|
||||
# 4.21.1
|
||||
Released 30th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash due to a bug in upstream dependency [`netresearch/jsonmapper`](https://github.com/cweiske/JsonMapper).
|
24
changelogs/4.22.md
Normal file
24
changelogs/4.22.md
Normal file
@ -0,0 +1,24 @@
|
||||
# 4.22.0
|
||||
Released 7th June 2023.
|
||||
|
||||
**For Minecraft: Bedrock Edition 1.20.0**
|
||||
|
||||
This is a support release for Minecraft: Bedrock Edition 1.20.0.
|
||||
|
||||
**Plugin compatibility:** Plugins for previous 4.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` namespace.
|
||||
Do not update plugin minimum API versions unless you need new features added in this release.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
## Interim releases
|
||||
If you're upgrading from 4.20.x directly to 4.22.x, please also read the following changelogs, as the interim releases contain important changes:
|
||||
- [4.21.0](https://github.com/pmmp/PocketMine-MP/blob/4.22.0/changelogs/4.21.md#4210) - PHP 8.1 minimum version, minor performance improvements
|
||||
|
||||
## General
|
||||
- Added support for Minecraft: Bedrock Edition 1.20.0.
|
||||
- Removed support for older versions.
|
||||
|
||||
## Fixes
|
||||
- Removed deprecated `ReflectionProperty::setAccessible()` calls.
|
||||
- Fixed jukebox music not stopping when destroyed by an explosion.
|
@ -5,7 +5,7 @@
|
||||
"homepage": "https://pmmp.io",
|
||||
"license": "LGPL-3.0",
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"php": "^8.1",
|
||||
"php-64bit": "*",
|
||||
"ext-chunkutils2": "^0.3.1",
|
||||
"ext-crypto": "^0.3.1",
|
||||
@ -33,17 +33,17 @@
|
||||
"composer-runtime-api": "^2.0",
|
||||
"adhocore/json-comment": "^1.1",
|
||||
"fgrosse/phpasn1": "^2.3",
|
||||
"netresearch/jsonmapper": "^4.0",
|
||||
"pocketmine/bedrock-block-upgrade-schema": "^1.0.0",
|
||||
"pocketmine/bedrock-data": "~1.14.0+bedrock-1.19.60",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "^1.0.0",
|
||||
"pocketmine/bedrock-protocol": "~19.3.0+bedrock-1.19.62",
|
||||
"pocketmine/netresearch-jsonmapper": "~v4.2.999",
|
||||
"pocketmine/bedrock-block-upgrade-schema": "~2.2.0+bedrock-1.20.0",
|
||||
"pocketmine/bedrock-data": "~2.3.0+bedrock-1.20.0",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "~1.3.0+bedrock-1.20.0",
|
||||
"pocketmine/bedrock-protocol": "~22.0.0+bedrock-1.20.0",
|
||||
"pocketmine/binaryutils": "^0.2.1",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/classloader": "^0.2.0",
|
||||
"pocketmine/color": "^0.3.0",
|
||||
"pocketmine/errorhandler": "^0.6.0",
|
||||
"pocketmine/locale-data": "~2.18.0",
|
||||
"pocketmine/locale-data": "~2.19.0",
|
||||
"pocketmine/log": "^0.4.0",
|
||||
"pocketmine/log-pthreads": "^0.4.0",
|
||||
"pocketmine/math": "^0.4.0",
|
||||
@ -56,10 +56,10 @@
|
||||
"webmozart/path-util": "^2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "1.9.18",
|
||||
"phpstan/phpstan": "1.10.15",
|
||||
"phpstan/phpstan-phpunit": "^1.1.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.2.0",
|
||||
"phpunit/phpunit": "^9.2"
|
||||
"phpunit/phpunit": "^10.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@ -77,7 +77,7 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.0.0"
|
||||
"php": "8.1.0"
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
|
1013
composer.lock
generated
1013
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ includes:
|
||||
- tests/phpstan/configs/impossible-generics.neon
|
||||
- tests/phpstan/configs/php-bugs.neon
|
||||
- tests/phpstan/configs/phpstan-bugs.neon
|
||||
- tests/phpstan/configs/runtime-type-checks.neon
|
||||
- tests/phpstan/configs/spl-fixed-array-sucks.neon
|
||||
- vendor/phpstan/phpstan-phpunit/extension.neon
|
||||
- vendor/phpstan/phpstan-phpunit/rules.neon
|
||||
|
@ -85,8 +85,11 @@ network:
|
||||
batch-threshold: 256
|
||||
#Compression level used when sending batched packets. Higher = more CPU, less bandwidth usage
|
||||
compression-level: 6
|
||||
#Use AsyncTasks for compression. Adds half/one tick delay, less CPU load on main thread
|
||||
#Use AsyncTasks for compression during the main game session. Increases latency, but may reduce main thread load
|
||||
async-compression: false
|
||||
#Threshold for async compression, in bytes. Only packets larger than this will be compressed asynchronously
|
||||
#Due to large overhead of AsyncTask, async compression isn't worth it except for large packets
|
||||
async-compression-threshold: 10000
|
||||
#Experimental. Use UPnP to automatically port forward
|
||||
upnp-forwarding: false
|
||||
#Maximum size in bytes of packets sent over the network (default 1492 bytes). Packets larger than this will be
|
||||
|
@ -49,6 +49,7 @@ use function ini_get;
|
||||
use function ini_set;
|
||||
use function intdiv;
|
||||
use function is_array;
|
||||
use function is_float;
|
||||
use function is_object;
|
||||
use function is_resource;
|
||||
use function is_string;
|
||||
@ -315,9 +316,6 @@ class MemoryManager{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!$property->isPublic()){
|
||||
$property->setAccessible(true);
|
||||
}
|
||||
if(!$property->isInitialized()){
|
||||
continue;
|
||||
}
|
||||
@ -441,9 +439,6 @@ class MemoryManager{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(!$property->isPublic()){
|
||||
$property->setAccessible(true);
|
||||
}
|
||||
if(!$property->isInitialized($object)){
|
||||
continue;
|
||||
}
|
||||
@ -519,6 +514,8 @@ class MemoryManager{
|
||||
$data = "(string) len(" . strlen($from) . ") " . substr(Utils::printable($from), 0, $maxStringSize);
|
||||
}elseif(is_resource($from)){
|
||||
$data = "(resource) " . print_r($from, true);
|
||||
}elseif(is_float($from)){
|
||||
$data = "(float) $from";
|
||||
}else{
|
||||
$data = $from;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ namespace pocketmine {
|
||||
|
||||
require_once __DIR__ . '/VersionInfo.php';
|
||||
|
||||
const MIN_PHP_VERSION = "8.0.0";
|
||||
const MIN_PHP_VERSION = "8.1.0";
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
@ -265,9 +265,6 @@ JIT_WARNING
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if(extension_loaded('parallel')){
|
||||
\parallel\bootstrap(\pocketmine\COMPOSER_AUTOLOADER_PATH);
|
||||
}
|
||||
|
||||
ErrorToExceptionHandler::set();
|
||||
|
||||
|
@ -43,7 +43,6 @@ use pocketmine\event\player\PlayerCreationEvent;
|
||||
use pocketmine\event\player\PlayerDataSaveEvent;
|
||||
use pocketmine\event\player\PlayerLoginEvent;
|
||||
use pocketmine\event\server\CommandEvent;
|
||||
use pocketmine\event\server\DataPacketSendEvent;
|
||||
use pocketmine\event\server\QueryRegenerateEvent;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\lang\Language;
|
||||
@ -54,13 +53,19 @@ use pocketmine\network\mcpe\compression\CompressBatchPromise;
|
||||
use pocketmine\network\mcpe\compression\CompressBatchTask;
|
||||
use pocketmine\network\mcpe\compression\Compressor;
|
||||
use pocketmine\network\mcpe\compression\ZlibCompressor;
|
||||
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
|
||||
use pocketmine\network\mcpe\encryption\EncryptionContext;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkBroadcastUtils;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\PacketBroadcaster;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
|
||||
use pocketmine\network\mcpe\raklib\RakLibInterface;
|
||||
use pocketmine\network\mcpe\StandardEntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\StandardPacketBroadcaster;
|
||||
use pocketmine\network\Network;
|
||||
use pocketmine\network\NetworkInterfaceStartException;
|
||||
use pocketmine\network\query\DedicatedQueryNetworkInterface;
|
||||
@ -203,6 +208,8 @@ class Server{
|
||||
private const TICKS_PER_TPS_OVERLOAD_WARNING = 5 * self::TARGET_TICKS_PER_SECOND;
|
||||
private const TICKS_PER_STATS_REPORT = 300 * self::TARGET_TICKS_PER_SECOND;
|
||||
|
||||
private const DEFAULT_ASYNC_COMPRESSION_THRESHOLD = 10_000;
|
||||
|
||||
private static ?Server $instance = null;
|
||||
|
||||
private TimeTrackingSleeperHandler $tickSleeper;
|
||||
@ -261,6 +268,7 @@ class Server{
|
||||
|
||||
private Network $network;
|
||||
private bool $networkCompressionAsync = true;
|
||||
private int $networkCompressionAsyncThreshold = self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD;
|
||||
|
||||
private Language $language;
|
||||
private bool $forceLanguage = false;
|
||||
@ -891,6 +899,9 @@ class Server{
|
||||
if($this->configGroup->getPropertyInt("network.batch-threshold", 256) >= 0){
|
||||
$netCompressionThreshold = $this->configGroup->getPropertyInt("network.batch-threshold", 256);
|
||||
}
|
||||
if($netCompressionThreshold < 0){
|
||||
$netCompressionThreshold = null;
|
||||
}
|
||||
|
||||
$netCompressionLevel = $this->configGroup->getPropertyInt("network.compression-level", 6);
|
||||
if($netCompressionLevel < 1 || $netCompressionLevel > 9){
|
||||
@ -900,6 +911,10 @@ class Server{
|
||||
ZlibCompressor::setInstance(new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE));
|
||||
|
||||
$this->networkCompressionAsync = $this->configGroup->getPropertyBool("network.async-compression", true);
|
||||
$this->networkCompressionAsyncThreshold = max(
|
||||
$this->configGroup->getPropertyInt("network.async-compression-threshold", self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD),
|
||||
$netCompressionThreshold ?? self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD
|
||||
);
|
||||
|
||||
EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool("network.enable-encryption", true);
|
||||
|
||||
@ -1166,10 +1181,18 @@ class Server{
|
||||
return !$anyWorldFailedToLoad;
|
||||
}
|
||||
|
||||
private function startupPrepareConnectableNetworkInterfaces(string $ip, int $port, bool $ipV6, bool $useQuery) : bool{
|
||||
private function startupPrepareConnectableNetworkInterfaces(
|
||||
string $ip,
|
||||
int $port,
|
||||
bool $ipV6,
|
||||
bool $useQuery,
|
||||
PacketBroadcaster $packetBroadcaster,
|
||||
EntityEventBroadcaster $entityEventBroadcaster,
|
||||
PacketSerializerContext $packetSerializerContext
|
||||
) : bool{
|
||||
$prettyIp = $ipV6 ? "[$ip]" : $ip;
|
||||
try{
|
||||
$rakLibRegistered = $this->network->registerInterface(new RakLibInterface($this, $ip, $port, $ipV6));
|
||||
$rakLibRegistered = $this->network->registerInterface(new RakLibInterface($this, $ip, $port, $ipV6, $packetBroadcaster, $entityEventBroadcaster, $packetSerializerContext));
|
||||
}catch(NetworkInterfaceStartException $e){
|
||||
$this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStartFailed(
|
||||
$ip,
|
||||
@ -1195,11 +1218,15 @@ class Server{
|
||||
private function startupPrepareNetworkInterfaces() : bool{
|
||||
$useQuery = $this->configGroup->getConfigBool("enable-query", true);
|
||||
|
||||
$packetSerializerContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary());
|
||||
$packetBroadcaster = new StandardPacketBroadcaster($this, $packetSerializerContext);
|
||||
$entityEventBroadcaster = new StandardEntityEventBroadcaster($packetBroadcaster);
|
||||
|
||||
if(
|
||||
!$this->startupPrepareConnectableNetworkInterfaces($this->getIp(), $this->getPort(), false, $useQuery) ||
|
||||
!$this->startupPrepareConnectableNetworkInterfaces($this->getIp(), $this->getPort(), false, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $packetSerializerContext) ||
|
||||
(
|
||||
$this->configGroup->getConfigBool("enable-ipv6", true) &&
|
||||
!$this->startupPrepareConnectableNetworkInterfaces($this->getIpV6(), $this->getPortV6(), true, $useQuery)
|
||||
!$this->startupPrepareConnectableNetworkInterfaces($this->getIpV6(), $this->getPortV6(), true, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $packetSerializerContext)
|
||||
)
|
||||
){
|
||||
return false;
|
||||
@ -1331,46 +1358,10 @@ class Server{
|
||||
/**
|
||||
* @param Player[] $players
|
||||
* @param ClientboundPacket[] $packets
|
||||
* @deprecated
|
||||
*/
|
||||
public function broadcastPackets(array $players, array $packets) : bool{
|
||||
if(count($packets) === 0){
|
||||
throw new \InvalidArgumentException("Cannot broadcast empty list of packets");
|
||||
}
|
||||
|
||||
return Timings::$broadcastPackets->time(function() use ($players, $packets) : bool{
|
||||
/** @var NetworkSession[] $recipients */
|
||||
$recipients = [];
|
||||
foreach($players as $player){
|
||||
if($player->isConnected()){
|
||||
$recipients[] = $player->getNetworkSession();
|
||||
}
|
||||
}
|
||||
if(count($recipients) === 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
$ev = new DataPacketSendEvent($recipients, $packets);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return false;
|
||||
}
|
||||
$recipients = $ev->getTargets();
|
||||
|
||||
/** @var PacketBroadcaster[] $broadcasters */
|
||||
$broadcasters = [];
|
||||
/** @var NetworkSession[][] $broadcasterTargets */
|
||||
$broadcasterTargets = [];
|
||||
foreach($recipients as $recipient){
|
||||
$broadcaster = $recipient->getBroadcaster();
|
||||
$broadcasters[spl_object_id($broadcaster)] = $broadcaster;
|
||||
$broadcasterTargets[spl_object_id($broadcaster)][] = $recipient;
|
||||
}
|
||||
foreach($broadcasters as $broadcaster){
|
||||
$broadcaster->broadcastPackets($broadcasterTargets[spl_object_id($broadcaster)], $packets);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return NetworkBroadcastUtils::broadcastPackets($players, $packets);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1378,18 +1369,20 @@ class Server{
|
||||
*
|
||||
* @param bool|null $sync Compression on the main thread (true) or workers (false). Default is automatic (null).
|
||||
*/
|
||||
public function prepareBatch(PacketBatch $stream, Compressor $compressor, ?bool $sync = null) : CompressBatchPromise{
|
||||
public function prepareBatch(PacketBatch $stream, Compressor $compressor, ?bool $sync = null, ?TimingsHandler $timings = null) : CompressBatchPromise{
|
||||
$timings ??= Timings::$playerNetworkSendCompress;
|
||||
try{
|
||||
Timings::$playerNetworkSendCompress->startTiming();
|
||||
$timings->startTiming();
|
||||
|
||||
$buffer = $stream->getBuffer();
|
||||
|
||||
if($sync === null){
|
||||
$sync = !($this->networkCompressionAsync && $compressor->willCompress($buffer));
|
||||
$threshold = $compressor->getCompressionThreshold();
|
||||
$sync = !$this->networkCompressionAsync || $threshold === null || strlen($stream->getBuffer()) < $threshold;
|
||||
}
|
||||
|
||||
$promise = new CompressBatchPromise();
|
||||
if(!$sync){
|
||||
if(!$sync && strlen($buffer) >= $this->networkCompressionAsyncThreshold){
|
||||
$task = new CompressBatchTask($buffer, $promise, $compressor);
|
||||
$this->asyncPool->submitTask($task);
|
||||
}else{
|
||||
@ -1398,7 +1391,7 @@ class Server{
|
||||
|
||||
return $promise;
|
||||
}finally{
|
||||
Timings::$playerNetworkSendCompress->stopTiming();
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,8 @@ use function str_repeat;
|
||||
|
||||
final class VersionInfo{
|
||||
public const NAME = "PocketMine-MP";
|
||||
public const BASE_VERSION = "4.15.2";
|
||||
public const IS_DEVELOPMENT_BUILD = true;
|
||||
public const BASE_VERSION = "4.22.0";
|
||||
public const IS_DEVELOPMENT_BUILD = false;
|
||||
public const BUILD_CHANNEL = "stable";
|
||||
|
||||
private function __construct(){
|
||||
|
@ -97,6 +97,15 @@ abstract class BaseSign extends Transparent{
|
||||
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
|
||||
}
|
||||
|
||||
public function onPostPlace() : void{
|
||||
$player = $this->editorEntityRuntimeId !== null ?
|
||||
$this->position->getWorld()->getEntity($this->editorEntityRuntimeId) :
|
||||
null;
|
||||
if($player instanceof Player){
|
||||
$player->openSignEditor($this->position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing information about the sign text.
|
||||
*/
|
||||
@ -110,6 +119,19 @@ abstract class BaseSign extends Transparent{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the runtime entity ID of the player editing this sign. Only this player will be able to edit the sign.
|
||||
* This is used to prevent multiple players from editing the same sign at the same time, and to prevent players
|
||||
* from editing signs they didn't place.
|
||||
*/
|
||||
public function getEditorEntityRuntimeId() : ?int{ return $this->editorEntityRuntimeId; }
|
||||
|
||||
/** @return $this */
|
||||
public function setEditorEntityRuntimeId(?int $editorEntityRuntimeId) : self{
|
||||
$this->editorEntityRuntimeId = $editorEntityRuntimeId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the player controller (network session) to update the sign text, firing events as appropriate.
|
||||
*
|
||||
@ -133,6 +155,7 @@ abstract class BaseSign extends Transparent{
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled()){
|
||||
$this->setText($ev->getNewText());
|
||||
$this->setEditorEntityRuntimeId(null);
|
||||
$this->position->getWorld()->setBlock($this->position, $this);
|
||||
return true;
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class Farmland extends Transparent{
|
||||
* @return AxisAlignedBB[]
|
||||
*/
|
||||
protected function recalculateCollisionBoxes() : array{
|
||||
return [AxisAlignedBB::one()]; //TODO: this should be trimmed at the top by 1/16, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109)
|
||||
return [AxisAlignedBB::one()->trim(Facing::UP, 1 / 16)];
|
||||
}
|
||||
|
||||
public function onNearbyBlockChange() : void{
|
||||
|
@ -33,7 +33,7 @@ class GrassPath extends Transparent{
|
||||
* @return AxisAlignedBB[]
|
||||
*/
|
||||
protected function recalculateCollisionBoxes() : array{
|
||||
return [AxisAlignedBB::one()]; //TODO: this should be trimmed at the top by 1/16, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109)
|
||||
return [AxisAlignedBB::one()->trim(Facing::UP, 1 / 16)];
|
||||
}
|
||||
|
||||
public function onNearbyBlockChange() : void{
|
||||
|
@ -34,7 +34,7 @@ class RedMushroomBlock extends Opaque{
|
||||
protected MushroomBlockType $mushroomBlockType;
|
||||
|
||||
public function __construct(BlockIdentifier $idInfo, string $name, BlockBreakInfo $breakInfo){
|
||||
$this->mushroomBlockType = MushroomBlockType::PORES();
|
||||
$this->mushroomBlockType = MushroomBlockType::ALL_CAP();
|
||||
parent::__construct($idInfo, $name, $breakInfo);
|
||||
}
|
||||
|
||||
@ -42,6 +42,11 @@ class RedMushroomBlock extends Opaque{
|
||||
return MushroomBlockTypeIdMap::getInstance()->toId($this->mushroomBlockType);
|
||||
}
|
||||
|
||||
protected function writeStateToItemMeta() : int{
|
||||
//these blocks always drop as all-cap, but may exist in other forms in the inventory (particularly creative)
|
||||
return BlockLegacyMetadata::MUSHROOM_BLOCK_ALL_CAP;
|
||||
}
|
||||
|
||||
public function readStateFromData(int $id, int $stateMeta) : void{
|
||||
$type = MushroomBlockTypeIdMap::getInstance()->fromId($stateMeta);
|
||||
if($type === null){
|
||||
|
@ -85,6 +85,20 @@ class DoubleChestInventory extends BaseInventory implements BlockInventory, Inve
|
||||
$this->right->setContents($rightContents);
|
||||
}
|
||||
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{
|
||||
$leftSize = $this->left->getSize();
|
||||
return $slot < $leftSize ?
|
||||
$this->left->getMatchingItemCount($slot, $test, $checkDamage, $checkTags) :
|
||||
$this->right->getMatchingItemCount($slot - $leftSize, $test, $checkDamage, $checkTags);
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
$leftSize = $this->left->getSize();
|
||||
return $index < $leftSize ?
|
||||
$this->left->isSlotEmpty($index) :
|
||||
$this->right->isSlotEmpty($index - $leftSize);
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{ return new ChestOpenSound(); }
|
||||
|
||||
protected function getCloseSound() : Sound{ return new ChestCloseSound(); }
|
||||
|
@ -26,6 +26,7 @@ namespace pocketmine\block\tile;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\Record;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\world\sound\RecordStopSound;
|
||||
|
||||
class Jukebox extends Spawnable{
|
||||
private const TAG_RECORD = "RecordItem"; //Item CompoundTag
|
||||
@ -61,4 +62,8 @@ class Jukebox extends Spawnable{
|
||||
$nbt->setTag(self::TAG_RECORD, $this->record->nbtSerialize());
|
||||
}
|
||||
}
|
||||
|
||||
protected function onBlockDestroyedHook() : void{
|
||||
$this->position->getWorld()->addSound($this->position, new RecordStopSound());
|
||||
}
|
||||
}
|
||||
|
@ -45,12 +45,18 @@ class Sign extends Spawnable{
|
||||
public const TAG_TEXT_LINE = "Text%d"; //sprintf()able
|
||||
public const TAG_TEXT_COLOR = "SignTextColor";
|
||||
public const TAG_GLOWING_TEXT = "IgnoreLighting";
|
||||
public const TAG_PERSIST_FORMATTING = "PersistFormatting"; //TAG_Byte
|
||||
/**
|
||||
* This tag is set to indicate that MCPE-117835 has been addressed in whatever version this sign was created.
|
||||
* @see https://bugs.mojang.com/browse/MCPE-117835
|
||||
*/
|
||||
public const TAG_LEGACY_BUG_RESOLVE = "TextIgnoreLegacyBugResolved";
|
||||
|
||||
public const TAG_FRONT_TEXT = "FrontText"; //TAG_Compound
|
||||
public const TAG_BACK_TEXT = "BackText"; //TAG_Compound
|
||||
public const TAG_WAXED = "IsWaxed"; //TAG_Byte
|
||||
public const TAG_LOCKED_FOR_EDITING_BY = "LockedForEditingBy"; //TAG_Long
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
@ -118,12 +124,22 @@ class Sign extends Spawnable{
|
||||
}
|
||||
|
||||
protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
|
||||
$nbt->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines()));
|
||||
|
||||
//the following are not yet used by the server, but needed to roll back any changes to glowing state or colour
|
||||
//if the client uses dye on the sign
|
||||
$nbt->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00));
|
||||
$nbt->setByte(self::TAG_GLOWING_TEXT, 0);
|
||||
$nbt->setByte(self::TAG_LEGACY_BUG_RESOLVE, 1);
|
||||
$textPolyfill = function(CompoundTag $textTag) : CompoundTag{
|
||||
//the following are not yet used by the server, but needed to roll back any changes to glowing state or colour
|
||||
//if the client uses dye on the sign
|
||||
return $textTag
|
||||
->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00))
|
||||
->setByte(self::TAG_GLOWING_TEXT, 0)
|
||||
->setByte(self::TAG_PERSIST_FORMATTING, 1); //TODO: not sure what this is used for
|
||||
};
|
||||
$nbt->setTag(self::TAG_FRONT_TEXT, $textPolyfill(CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines()))
|
||||
));
|
||||
//TODO: this is not yet used by the server, but needed to rollback any client-side changes to the back text
|
||||
$nbt->setTag(self::TAG_BACK_TEXT, $textPolyfill(CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, "")
|
||||
));
|
||||
$nbt->setByte(self::TAG_WAXED, 0);
|
||||
$nbt->setLong(self::TAG_LOCKED_FOR_EDITING_BY, $this->editorEntityRuntimeId ?? -1);
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ abstract class Command{
|
||||
public function setLabel(string $name) : bool{
|
||||
$this->nextLabel = $name;
|
||||
if(!$this->isRegistered()){
|
||||
$this->timings = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Command: " . $name);
|
||||
$this->timings = new TimingsHandler("Command: " . $name, group: Timings::GROUP_BREAKDOWN);
|
||||
$this->label = $name;
|
||||
|
||||
return true;
|
||||
|
@ -110,7 +110,8 @@ class StatusCommand extends VanillaCommand{
|
||||
$worldName = $world->getFolderName() !== $world->getDisplayName() ? " (" . $world->getDisplayName() . ")" : "";
|
||||
$timeColor = $world->getTickRateTime() > 40 ? TextFormat::RED : TextFormat::YELLOW;
|
||||
$sender->sendMessage(TextFormat::GOLD . "World \"{$world->getFolderName()}\"$worldName: " .
|
||||
TextFormat::RED . number_format(count($world->getLoadedChunks())) . TextFormat::GREEN . " chunks, " .
|
||||
TextFormat::RED . number_format(count($world->getLoadedChunks())) . TextFormat::GREEN . " loaded chunks, " .
|
||||
TextFormat::RED . number_format(count($world->getTickingChunks())) . TextFormat::GREEN . " ticking chunks, " .
|
||||
TextFormat::RED . number_format(count($world->getEntities())) . TextFormat::GREEN . " entities. " .
|
||||
"Time $timeColor" . round($world->getTickRateTime(), 2) . "ms"
|
||||
);
|
||||
|
@ -167,6 +167,7 @@ class TimingsCommand extends VanillaCommand{
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead(
|
||||
"https://" . $host . "/?id=" . $response["id"]));
|
||||
}else{
|
||||
$sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody());
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError());
|
||||
}
|
||||
}
|
||||
|
@ -45,11 +45,17 @@ class CraftingManager{
|
||||
*/
|
||||
protected $shapelessRecipes = [];
|
||||
|
||||
/**
|
||||
* @var CraftingRecipe[]
|
||||
* @phpstan-var array<int, CraftingRecipe>
|
||||
*/
|
||||
private array $craftingRecipeIndex = [];
|
||||
|
||||
/**
|
||||
* @var FurnaceRecipeManager[]
|
||||
* @phpstan-var array<int, FurnaceRecipeManager>
|
||||
*/
|
||||
protected $furnaceRecipeManagers;
|
||||
protected $furnaceRecipeManagers = [];
|
||||
|
||||
/**
|
||||
* @var PotionTypeRecipe[][]
|
||||
@ -153,6 +159,18 @@ class CraftingManager{
|
||||
return $this->shapedRecipes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CraftingRecipe[]
|
||||
* @phpstan-return array<int, CraftingRecipe>
|
||||
*/
|
||||
public function getCraftingRecipeIndex() : array{
|
||||
return $this->craftingRecipeIndex;
|
||||
}
|
||||
|
||||
public function getCraftingRecipeFromIndex(int $index) : ?CraftingRecipe{
|
||||
return $this->craftingRecipeIndex[$index] ?? null;
|
||||
}
|
||||
|
||||
public function getFurnaceRecipeManager(FurnaceType $furnaceType) : FurnaceRecipeManager{
|
||||
return $this->furnaceRecipeManagers[$furnaceType->id()];
|
||||
}
|
||||
@ -175,6 +193,7 @@ class CraftingManager{
|
||||
|
||||
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
|
||||
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
||||
$this->craftingRecipeIndex[] = $recipe;
|
||||
|
||||
foreach($this->recipeRegisteredCallbacks as $callback){
|
||||
$callback();
|
||||
@ -183,6 +202,7 @@ class CraftingManager{
|
||||
|
||||
public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{
|
||||
$this->shapelessRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
||||
$this->craftingRecipeIndex[] = $recipe;
|
||||
|
||||
foreach($this->recipeRegisteredCallbacks as $callback){
|
||||
$callback();
|
||||
|
@ -248,7 +248,6 @@ class CrashDump{
|
||||
if(file_exists($filePath)){
|
||||
$reflection = new \ReflectionClass(PluginBase::class);
|
||||
$file = $reflection->getProperty("file");
|
||||
$file->setAccessible(true);
|
||||
foreach($this->server->getPluginManager()->getPlugins() as $plugin){
|
||||
$filePath = Filesystem::cleanPath($file->getValue($plugin));
|
||||
if(str_starts_with($frameCleanPath, $filePath)){
|
||||
|
50
src/data/bedrock/BedrockDataFiles.php
Normal file
50
src/data/bedrock/BedrockDataFiles.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\data\bedrock;
|
||||
|
||||
use const pocketmine\BEDROCK_DATA_PATH;
|
||||
|
||||
final class BedrockDataFiles{
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
public const BANNER_PATTERNS_JSON = BEDROCK_DATA_PATH . '/banner_patterns.json';
|
||||
public const BIOME_DEFINITIONS_NBT = BEDROCK_DATA_PATH . '/biome_definitions.nbt';
|
||||
public const BIOME_DEFINITIONS_FULL_NBT = BEDROCK_DATA_PATH . '/biome_definitions_full.nbt';
|
||||
public const BIOME_ID_MAP_JSON = BEDROCK_DATA_PATH . '/biome_id_map.json';
|
||||
public const BLOCK_ID_TO_ITEM_ID_MAP_JSON = BEDROCK_DATA_PATH . '/block_id_to_item_id_map.json';
|
||||
public const BLOCK_STATE_META_MAP_JSON = BEDROCK_DATA_PATH . '/block_state_meta_map.json';
|
||||
public const CANONICAL_BLOCK_STATES_NBT = BEDROCK_DATA_PATH . '/canonical_block_states.nbt';
|
||||
public const COMMAND_ARG_TYPES_JSON = BEDROCK_DATA_PATH . '/command_arg_types.json';
|
||||
public const CREATIVEITEMS_JSON = BEDROCK_DATA_PATH . '/creativeitems.json';
|
||||
public const ENTITY_ID_MAP_JSON = BEDROCK_DATA_PATH . '/entity_id_map.json';
|
||||
public const ENTITY_IDENTIFIERS_NBT = BEDROCK_DATA_PATH . '/entity_identifiers.nbt';
|
||||
public const ITEM_TAGS_JSON = BEDROCK_DATA_PATH . '/item_tags.json';
|
||||
public const LEVEL_SOUND_ID_MAP_JSON = BEDROCK_DATA_PATH . '/level_sound_id_map.json';
|
||||
public const PARTICLE_ID_MAP_JSON = BEDROCK_DATA_PATH . '/particle_id_map.json';
|
||||
public const R12_TO_CURRENT_BLOCK_MAP_BIN = BEDROCK_DATA_PATH . '/r12_to_current_block_map.bin';
|
||||
public const R16_TO_CURRENT_ITEM_MAP_JSON = BEDROCK_DATA_PATH . '/r16_to_current_item_map.json';
|
||||
public const REQUIRED_ITEM_LIST_JSON = BEDROCK_DATA_PATH . '/required_item_list.json';
|
||||
}
|
@ -24,12 +24,11 @@ declare(strict_types=1);
|
||||
namespace pocketmine\data\bedrock;
|
||||
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
final class LegacyBiomeIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
|
||||
use SingletonTrait;
|
||||
|
||||
public function __construct(){
|
||||
parent::__construct(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'biome_id_map.json'));
|
||||
parent::__construct(BedrockDataFiles::BIOME_ID_MAP_JSON);
|
||||
}
|
||||
}
|
||||
|
@ -24,12 +24,11 @@ declare(strict_types=1);
|
||||
namespace pocketmine\data\bedrock;
|
||||
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
final class LegacyEntityIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
|
||||
use SingletonTrait;
|
||||
|
||||
public function __construct(){
|
||||
parent::__construct(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'entity_id_map.json'));
|
||||
parent::__construct(BedrockDataFiles::ENTITY_ID_MAP_JSON);
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ use pocketmine\nbt\tag\DoubleTag;
|
||||
use pocketmine\nbt\tag\FloatTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkBroadcastUtils;
|
||||
use pocketmine\network\mcpe\protocol\AddActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\MoveActorAbsolutePacket;
|
||||
use pocketmine\network\mcpe\protocol\SetActorMotionPacket;
|
||||
@ -113,6 +115,8 @@ abstract class Entity{
|
||||
/** @var Block[]|null */
|
||||
protected $blocksAround;
|
||||
|
||||
private bool $checkBlockIntersectionsNextTick = true;
|
||||
|
||||
/** @var Location */
|
||||
protected $location;
|
||||
/** @var Location */
|
||||
@ -244,7 +248,7 @@ abstract class Entity{
|
||||
if($nbt !== null){
|
||||
$this->motion = EntityDataHelper::parseVec3($nbt, self::TAG_MOTION, true);
|
||||
}else{
|
||||
$this->motion = new Vector3(0, 0, 0);
|
||||
$this->motion = Vector3::zero();
|
||||
}
|
||||
|
||||
$this->resetLastMovements();
|
||||
@ -647,7 +651,10 @@ abstract class Entity{
|
||||
|
||||
$hasUpdate = false;
|
||||
|
||||
$this->checkBlockIntersections();
|
||||
if($this->checkBlockIntersectionsNextTick){
|
||||
$this->checkBlockIntersections();
|
||||
}
|
||||
$this->checkBlockIntersectionsNextTick = true;
|
||||
|
||||
if($this->location->y <= World::Y_MIN - 16 && $this->isAlive()){
|
||||
$ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_VOID, 10);
|
||||
@ -785,7 +792,7 @@ abstract class Entity{
|
||||
$this->spawnTo($player);
|
||||
}
|
||||
}else{
|
||||
$this->server->broadcastPackets($this->hasSpawned, [MoveActorAbsolutePacket::create(
|
||||
NetworkBroadcastUtils::broadcastPackets($this->hasSpawned, [MoveActorAbsolutePacket::create(
|
||||
$this->id,
|
||||
$this->getOffsetPosition($this->location),
|
||||
$this->location->pitch,
|
||||
@ -800,7 +807,7 @@ abstract class Entity{
|
||||
}
|
||||
|
||||
protected function broadcastMotion() : void{
|
||||
$this->server->broadcastPackets($this->hasSpawned, [SetActorMotionPacket::create($this->id, $this->getMotion())]);
|
||||
NetworkBroadcastUtils::broadcastPackets($this->hasSpawned, [SetActorMotionPacket::create($this->id, $this->getMotion())]);
|
||||
}
|
||||
|
||||
public function getGravity() : float{
|
||||
@ -1137,6 +1144,7 @@ abstract class Entity{
|
||||
$this->blocksAround = null;
|
||||
|
||||
Timings::$entityMove->startTiming();
|
||||
Timings::$entityMoveCollision->startTiming();
|
||||
|
||||
$wantedX = $dx;
|
||||
$wantedY = $dy;
|
||||
@ -1221,6 +1229,7 @@ abstract class Entity{
|
||||
|
||||
$this->boundingBox = $moveBB;
|
||||
}
|
||||
Timings::$entityMoveCollision->stopTiming();
|
||||
|
||||
$this->location = new Location(
|
||||
($this->boundingBox->minX + $this->boundingBox->maxX) / 2,
|
||||
@ -1304,6 +1313,7 @@ abstract class Entity{
|
||||
}
|
||||
|
||||
protected function checkBlockIntersections() : void{
|
||||
$this->checkBlockIntersectionsNextTick = false;
|
||||
$vectors = [];
|
||||
|
||||
foreach($this->getBlocksAroundWithEntityInsideActions() as $block){
|
||||
@ -1315,10 +1325,12 @@ abstract class Entity{
|
||||
}
|
||||
}
|
||||
|
||||
$vector = Vector3::sum(...$vectors);
|
||||
if($vector->lengthSquared() > 0){
|
||||
$d = 0.014;
|
||||
$this->motion = $this->motion->addVector($vector->normalize()->multiply($d));
|
||||
if(count($vectors) > 0){
|
||||
$vector = Vector3::sum(...$vectors);
|
||||
if($vector->lengthSquared() > 0){
|
||||
$d = 0.014;
|
||||
$this->motion = $this->motion->addVector($vector->normalize()->multiply($d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1537,7 +1549,7 @@ abstract class Entity{
|
||||
$id = spl_object_id($player);
|
||||
if(isset($this->hasSpawned[$id])){
|
||||
if($send){
|
||||
$player->getNetworkSession()->onEntityRemoved($this);
|
||||
$player->getNetworkSession()->getEntityEventBroadcaster()->onEntityRemoved([$player->getNetworkSession()], $this);
|
||||
}
|
||||
unset($this->hasSpawned[$id]);
|
||||
}
|
||||
@ -1548,9 +1560,11 @@ abstract class Entity{
|
||||
* player moves, viewers will once again be able to see the entity.
|
||||
*/
|
||||
public function despawnFromAll() : void{
|
||||
foreach($this->hasSpawned as $player){
|
||||
$this->despawnFrom($player);
|
||||
}
|
||||
NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->hasSpawned,
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onEntityRemoved($recipients, $this)
|
||||
);
|
||||
$this->hasSpawned = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1624,9 +1638,7 @@ abstract class Entity{
|
||||
$targets = $targets ?? $this->hasSpawned;
|
||||
$data = $data ?? $this->getAllNetworkData();
|
||||
|
||||
foreach($targets as $p){
|
||||
$p->getNetworkSession()->syncActorData($this, $data);
|
||||
}
|
||||
NetworkBroadcastUtils::broadcastEntityEvent($targets, fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->syncActorData($recipients, $this, $data));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1680,7 +1692,7 @@ abstract class Entity{
|
||||
* @param Player[]|null $targets
|
||||
*/
|
||||
public function broadcastAnimation(Animation $animation, ?array $targets = null) : void{
|
||||
$this->server->broadcastPackets($targets ?? $this->getViewers(), $animation->encode());
|
||||
NetworkBroadcastUtils::broadcastPackets($targets ?? $this->getViewers(), $animation->encode());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1689,7 +1701,7 @@ abstract class Entity{
|
||||
*/
|
||||
public function broadcastSound(Sound $sound, ?array $targets = null) : void{
|
||||
if(!$this->silent){
|
||||
$this->server->broadcastPackets($targets ?? $this->getViewers(), $sound->encode($this->location));
|
||||
NetworkBroadcastUtils::broadcastPackets($targets ?? $this->getViewers(), $sound->encode($this->location));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ final class EntityDataHelper{
|
||||
public static function parseVec3(CompoundTag $nbt, string $tagName, bool $optional) : Vector3{
|
||||
$pos = $nbt->getTag($tagName);
|
||||
if($pos === null && $optional){
|
||||
return new Vector3(0, 0, 0);
|
||||
return Vector3::zero();
|
||||
}
|
||||
if(!($pos instanceof ListTag) || ($pos->getTagType() !== NBT::TAG_Double && $pos->getTagType() !== NBT::TAG_Float)){
|
||||
throw new SavedDataLoadingException("'$tagName' should be a List<Double> or List<Float>");
|
||||
|
@ -47,6 +47,8 @@ use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkBroadcastUtils;
|
||||
use pocketmine\network\mcpe\protocol\AddPlayerPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerListPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
|
||||
@ -90,7 +92,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
private const TAG_XP_PROGRESS = "XpP"; //TAG_Float
|
||||
private const TAG_LIFETIME_XP_TOTAL = "XpTotal"; //TAG_Int
|
||||
private const TAG_XP_SEED = "XpSeed"; //TAG_Int
|
||||
private const TAG_NAME_TAG = "NameTag"; //TAG_String
|
||||
private const TAG_SKIN = "Skin"; //TAG_Compound
|
||||
private const TAG_SKIN_NAME = "Name"; //TAG_String
|
||||
private const TAG_SKIN_DATA = "Data"; //TAG_ByteArray
|
||||
@ -174,7 +175,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
* @param Player[]|null $targets
|
||||
*/
|
||||
public function sendSkin(?array $targets = null) : void{
|
||||
$this->server->broadcastPackets($targets ?? $this->hasSpawned, [
|
||||
NetworkBroadcastUtils::broadcastPackets($targets ?? $this->hasSpawned, [
|
||||
PlayerSkinPacket::create($this->getUniqueId(), "", "", SkinAdapterSingleton::get()->toSkinData($this->skin))
|
||||
]);
|
||||
}
|
||||
@ -189,9 +190,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
}
|
||||
|
||||
public function emote(string $emoteId) : void{
|
||||
foreach($this->getViewers() as $player){
|
||||
$player->getNetworkSession()->onEmote($this, $emoteId);
|
||||
}
|
||||
NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onEmote($recipients, $this, $emoteId)
|
||||
);
|
||||
}
|
||||
|
||||
public function getHungerManager() : HungerManager{
|
||||
@ -242,10 +244,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
* For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT.
|
||||
*/
|
||||
protected function initHumanData(CompoundTag $nbt) : void{
|
||||
if(($nameTagTag = $nbt->getTag(self::TAG_NAME_TAG)) instanceof StringTag){
|
||||
$this->setNameTag($nameTagTag->getValue());
|
||||
}
|
||||
|
||||
//TODO: use of NIL UUID for namespace is a hack; we should provide a proper UUID for the namespace
|
||||
$this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag());
|
||||
}
|
||||
@ -270,11 +268,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
$this->xpManager = new ExperienceManager($this);
|
||||
|
||||
$this->inventory = new PlayerInventory($this);
|
||||
$syncHeldItem = function() : void{
|
||||
foreach($this->getViewers() as $viewer){
|
||||
$viewer->getNetworkSession()->onMobMainHandItemChange($this);
|
||||
}
|
||||
};
|
||||
$syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
|
||||
);
|
||||
$this->inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
function(Inventory $unused, int $slot, Item $unused2) use ($syncHeldItem) : void{
|
||||
if($slot === $this->inventory->getHeldItemIndex()){
|
||||
@ -315,11 +312,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
if($offHand !== null){
|
||||
$this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand));
|
||||
}
|
||||
$this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(function() : void{
|
||||
foreach($this->getViewers() as $viewer){
|
||||
$viewer->getNetworkSession()->onMobOffHandItemChange($this);
|
||||
}
|
||||
}));
|
||||
$this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobOffHandItemChange($recipients, $this)
|
||||
)));
|
||||
|
||||
$enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY);
|
||||
if($enderChestInventoryTag !== null){
|
||||
@ -333,11 +329,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
}
|
||||
|
||||
$this->inventory->setHeldItemIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
|
||||
$this->inventory->getHeldItemIndexChangeListeners()->add(function(int $oldIndex) : void{
|
||||
foreach($this->getViewers() as $viewer){
|
||||
$viewer->getNetworkSession()->onMobMainHandItemChange($this);
|
||||
}
|
||||
});
|
||||
$this->inventory->getHeldItemIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
|
||||
));
|
||||
|
||||
$this->hungerManager->setFood((float) $nbt->getInt(self::TAG_FOOD_LEVEL, (int) $this->hungerManager->getFood()));
|
||||
$this->hungerManager->setExhaustion($nbt->getFloat(self::TAG_FOOD_EXHAUSTION_LEVEL, $this->hungerManager->getExhaustion()));
|
||||
@ -490,11 +485,12 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
}
|
||||
|
||||
protected function sendSpawnPacket(Player $player) : void{
|
||||
$networkSession = $player->getNetworkSession();
|
||||
if(!($this instanceof Player)){
|
||||
$player->getNetworkSession()->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), SkinAdapterSingleton::get()->toSkinData($this->skin))]));
|
||||
$networkSession->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), SkinAdapterSingleton::get()->toSkinData($this->skin))]));
|
||||
}
|
||||
|
||||
$player->getNetworkSession()->sendDataPacket(AddPlayerPacket::create(
|
||||
$networkSession->sendDataPacket(AddPlayerPacket::create(
|
||||
$this->getUniqueId(),
|
||||
$this->getName(),
|
||||
$this->getId(),
|
||||
@ -524,11 +520,12 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
//TODO: Hack for MCPE 1.2.13: DATA_NAMETAG is useless in AddPlayerPacket, so it has to be sent separately
|
||||
$this->sendData([$player], [EntityMetadataProperties::NAMETAG => new StringMetadataProperty($this->getNameTag())]);
|
||||
|
||||
$player->getNetworkSession()->onMobArmorChange($this);
|
||||
$player->getNetworkSession()->onMobOffHandItemChange($this);
|
||||
$entityEventBroadcaster = $networkSession->getEntityEventBroadcaster();
|
||||
$entityEventBroadcaster->onMobArmorChange([$networkSession], $this);
|
||||
$entityEventBroadcaster->onMobOffHandItemChange([$networkSession], $this);
|
||||
|
||||
if(!($this instanceof Player)){
|
||||
$player->getNetworkSession()->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)]));
|
||||
$networkSession->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,8 @@ use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\FloatTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\ShortTag;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkBroadcastUtils;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
|
||||
@ -143,13 +145,10 @@ abstract class Living extends Entity{
|
||||
|
||||
$this->armorInventory = new ArmorInventory($this);
|
||||
//TODO: load/save armor inventory contents
|
||||
$this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(
|
||||
function(Inventory $unused) : void{
|
||||
foreach($this->getViewers() as $viewer){
|
||||
$viewer->getNetworkSession()->onMobArmorChange($this);
|
||||
}
|
||||
}
|
||||
));
|
||||
$this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobArmorChange($recipients, $this)
|
||||
)));
|
||||
|
||||
$health = $this->getMaxHealth();
|
||||
|
||||
@ -850,7 +849,8 @@ abstract class Living extends Entity{
|
||||
protected function sendSpawnPacket(Player $player) : void{
|
||||
parent::sendSpawnPacket($player);
|
||||
|
||||
$player->getNetworkSession()->onMobArmorChange($this);
|
||||
$networkSession = $player->getNetworkSession();
|
||||
$networkSession->getEntityEventBroadcaster()->onMobArmorChange([$networkSession], $this);
|
||||
}
|
||||
|
||||
protected function syncNetworkData(EntityMetadataCollection $properties) : void{
|
||||
|
@ -32,6 +32,7 @@ use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
|
||||
use pocketmine\player\Player;
|
||||
use function max;
|
||||
use function sqrt;
|
||||
|
||||
class ExperienceOrb extends Entity{
|
||||
@ -48,6 +49,10 @@ class ExperienceOrb extends Entity{
|
||||
/** Split sizes used for dropping experience orbs. */
|
||||
public const ORB_SPLIT_SIZES = [2477, 1237, 617, 307, 149, 73, 37, 17, 7, 3, 1]; //This is indexed biggest to smallest so that we can return as soon as we found the biggest value.
|
||||
|
||||
public const DEFAULT_DESPAWN_DELAY = 6000;
|
||||
public const NEVER_DESPAWN = -1;
|
||||
public const MAX_DESPAWN_DELAY = 32767 + self::DEFAULT_DESPAWN_DELAY; //max value storable by mojang NBT :(
|
||||
|
||||
/**
|
||||
* Returns the largest size of normal XP orb that will be spawned for the specified amount of XP. Used to split XP
|
||||
* up into multiple orbs when an amount of XP is dropped.
|
||||
@ -82,7 +87,10 @@ class ExperienceOrb extends Entity{
|
||||
public $gravity = 0.04;
|
||||
public $drag = 0.02;
|
||||
|
||||
/** @var int */
|
||||
/**
|
||||
* @var int
|
||||
* @deprecated
|
||||
*/
|
||||
protected $age = 0;
|
||||
|
||||
/**
|
||||
@ -100,6 +108,8 @@ class ExperienceOrb extends Entity{
|
||||
/** @var int */
|
||||
protected $xpValue;
|
||||
|
||||
private int $despawnDelay = self::DEFAULT_DESPAWN_DELAY;
|
||||
|
||||
public function __construct(Location $location, int $xpValue, ?CompoundTag $nbt = null){
|
||||
$this->xpValue = $xpValue;
|
||||
parent::__construct($location, $nbt);
|
||||
@ -111,12 +121,22 @@ class ExperienceOrb extends Entity{
|
||||
parent::initEntity($nbt);
|
||||
|
||||
$this->age = $nbt->getShort(self::TAG_AGE, 0);
|
||||
if($this->age === -32768){
|
||||
$this->despawnDelay = self::NEVER_DESPAWN;
|
||||
}else{
|
||||
$this->despawnDelay = max(0, self::DEFAULT_DESPAWN_DELAY - $this->age);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveNBT() : CompoundTag{
|
||||
$nbt = parent::saveNBT();
|
||||
|
||||
$nbt->setShort(self::TAG_AGE, $this->age);
|
||||
if($this->despawnDelay === self::NEVER_DESPAWN){
|
||||
$age = -32768;
|
||||
}else{
|
||||
$age = self::DEFAULT_DESPAWN_DELAY - $this->despawnDelay;
|
||||
}
|
||||
$nbt->setShort(self::TAG_AGE, $age);
|
||||
|
||||
$nbt->setShort(self::TAG_VALUE_PC, $this->getXpValue());
|
||||
$nbt->setInt(self::TAG_VALUE_PE, $this->getXpValue());
|
||||
@ -124,6 +144,15 @@ class ExperienceOrb extends Entity{
|
||||
return $nbt;
|
||||
}
|
||||
|
||||
public function getDespawnDelay() : int{ return $this->despawnDelay; }
|
||||
|
||||
public function setDespawnDelay(int $despawnDelay) : void{
|
||||
if(($despawnDelay < 0 || $despawnDelay > self::MAX_DESPAWN_DELAY) && $despawnDelay !== self::NEVER_DESPAWN){
|
||||
throw new \InvalidArgumentException("Despawn ticker must be in range 0 ... " . self::MAX_DESPAWN_DELAY . " or " . self::NEVER_DESPAWN . ", got $despawnDelay");
|
||||
}
|
||||
$this->despawnDelay = $despawnDelay;
|
||||
}
|
||||
|
||||
public function getXpValue() : int{
|
||||
return $this->xpValue;
|
||||
}
|
||||
@ -161,7 +190,8 @@ class ExperienceOrb extends Entity{
|
||||
$hasUpdate = parent::entityBaseTick($tickDiff);
|
||||
|
||||
$this->age += $tickDiff;
|
||||
if($this->age > 6000){
|
||||
$this->despawnDelay -= $tickDiff;
|
||||
if($this->despawnDelay <= 0){
|
||||
$this->flagForDespawn();
|
||||
return true;
|
||||
}
|
||||
|
@ -35,10 +35,13 @@ use pocketmine\item\Item;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkBroadcastUtils;
|
||||
use pocketmine\network\mcpe\protocol\AddItemActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\timings\Timings;
|
||||
use function max;
|
||||
|
||||
class ItemEntity extends Entity{
|
||||
@ -111,57 +114,63 @@ class ItemEntity extends Entity{
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasUpdate = parent::entityBaseTick($tickDiff);
|
||||
Timings::$itemEntityBaseTick->startTiming();
|
||||
try{
|
||||
|
||||
if($this->isFlaggedForDespawn()){
|
||||
return $hasUpdate;
|
||||
}
|
||||
$hasUpdate = parent::entityBaseTick($tickDiff);
|
||||
|
||||
if($this->pickupDelay !== self::NEVER_DESPAWN && $this->pickupDelay > 0){ //Infinite delay
|
||||
$hasUpdate = true;
|
||||
$this->pickupDelay -= $tickDiff;
|
||||
if($this->pickupDelay < 0){
|
||||
$this->pickupDelay = 0;
|
||||
if($this->isFlaggedForDespawn()){
|
||||
return $hasUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
if($this->hasMovementUpdate() && $this->despawnDelay % self::MERGE_CHECK_PERIOD === 0){
|
||||
$mergeable = [$this]; //in case the merge target ends up not being this
|
||||
$mergeTarget = $this;
|
||||
foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(0.5, 0.5, 0.5), $this) as $entity){
|
||||
if(!$entity instanceof ItemEntity || $entity->isFlaggedForDespawn()){
|
||||
continue;
|
||||
if($this->pickupDelay !== self::NEVER_DESPAWN && $this->pickupDelay > 0){ //Infinite delay
|
||||
$hasUpdate = true;
|
||||
$this->pickupDelay -= $tickDiff;
|
||||
if($this->pickupDelay < 0){
|
||||
$this->pickupDelay = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if($entity->isMergeable($this)){
|
||||
$mergeable[] = $entity;
|
||||
if($entity->item->getCount() > $mergeTarget->item->getCount()){
|
||||
$mergeTarget = $entity;
|
||||
if($this->hasMovementUpdate() && $this->despawnDelay % self::MERGE_CHECK_PERIOD === 0){
|
||||
$mergeable = [$this]; //in case the merge target ends up not being this
|
||||
$mergeTarget = $this;
|
||||
foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(0.5, 0.5, 0.5), $this) as $entity){
|
||||
if(!$entity instanceof ItemEntity || $entity->isFlaggedForDespawn()){
|
||||
continue;
|
||||
}
|
||||
|
||||
if($entity->isMergeable($this)){
|
||||
$mergeable[] = $entity;
|
||||
if($entity->item->getCount() > $mergeTarget->item->getCount()){
|
||||
$mergeTarget = $entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach($mergeable as $itemEntity){
|
||||
if($itemEntity !== $mergeTarget){
|
||||
$itemEntity->tryMergeInto($mergeTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach($mergeable as $itemEntity){
|
||||
if($itemEntity !== $mergeTarget){
|
||||
$itemEntity->tryMergeInto($mergeTarget);
|
||||
|
||||
if(!$this->isFlaggedForDespawn() && $this->despawnDelay !== self::NEVER_DESPAWN){
|
||||
$hasUpdate = true;
|
||||
$this->despawnDelay -= $tickDiff;
|
||||
if($this->despawnDelay <= 0){
|
||||
$ev = new ItemDespawnEvent($this);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
$this->despawnDelay = self::DEFAULT_DESPAWN_DELAY;
|
||||
}else{
|
||||
$this->flagForDespawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!$this->isFlaggedForDespawn() && $this->despawnDelay !== self::NEVER_DESPAWN){
|
||||
$hasUpdate = true;
|
||||
$this->despawnDelay -= $tickDiff;
|
||||
if($this->despawnDelay <= 0){
|
||||
$ev = new ItemDespawnEvent($this);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
$this->despawnDelay = self::DEFAULT_DESPAWN_DELAY;
|
||||
}else{
|
||||
$this->flagForDespawn();
|
||||
}
|
||||
}
|
||||
return $hasUpdate;
|
||||
}finally{
|
||||
Timings::$itemEntityBaseTick->stopTiming();
|
||||
}
|
||||
|
||||
return $hasUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,9 +337,10 @@ class ItemEntity extends Entity{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($this->getViewers() as $viewer){
|
||||
$viewer->getNetworkSession()->onPlayerPickUpItem($player, $this);
|
||||
}
|
||||
NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $player, $this)
|
||||
);
|
||||
|
||||
$inventory = $ev->getInventory();
|
||||
if($inventory !== null){
|
||||
|
@ -33,6 +33,8 @@ use pocketmine\event\entity\ProjectileHitEvent;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\math\RayTraceResult;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkBroadcastUtils;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags;
|
||||
@ -196,9 +198,10 @@ class Arrow extends Projectile{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($this->getViewers() as $viewer){
|
||||
$viewer->getNetworkSession()->onPlayerPickUpItem($player, $this);
|
||||
}
|
||||
NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $player, $this)
|
||||
);
|
||||
|
||||
$ev->getInventory()?->addItem($ev->getItem());
|
||||
$this->flagForDespawn();
|
||||
|
@ -175,7 +175,8 @@ abstract class Projectile extends Entity{
|
||||
protected function move(float $dx, float $dy, float $dz) : void{
|
||||
$this->blocksAround = null;
|
||||
|
||||
Timings::$entityMove->startTiming();
|
||||
Timings::$projectileMove->startTiming();
|
||||
Timings::$projectileMoveRayTrace->startTiming();
|
||||
|
||||
$start = $this->location->asVector3();
|
||||
$end = $start->add($dx, $dy, $dz);
|
||||
@ -221,6 +222,8 @@ abstract class Projectile extends Entity{
|
||||
}
|
||||
}
|
||||
|
||||
Timings::$projectileMoveRayTrace->stopTiming();
|
||||
|
||||
$this->location = Location::fromObject(
|
||||
$end,
|
||||
$this->location->world,
|
||||
@ -252,7 +255,7 @@ abstract class Projectile extends Entity{
|
||||
}
|
||||
|
||||
$this->isCollided = $this->onGround = true;
|
||||
$this->motion = new Vector3(0, 0, 0);
|
||||
$this->motion = Vector3::zero();
|
||||
}else{
|
||||
$this->isCollided = $this->onGround = false;
|
||||
$this->blockHit = null;
|
||||
@ -268,7 +271,7 @@ abstract class Projectile extends Entity{
|
||||
$this->getWorld()->onEntityMoved($this);
|
||||
$this->checkBlockIntersections();
|
||||
|
||||
Timings::$entityMove->stopTiming();
|
||||
Timings::$projectileMove->stopTiming();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,6 +26,7 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\timings\Timings;
|
||||
use function get_class;
|
||||
|
||||
abstract class Event{
|
||||
@ -51,22 +52,19 @@ abstract class Event{
|
||||
throw new \RuntimeException("Recursive event call detected (reached max depth of " . self::MAX_EVENT_CALL_DEPTH . " calls)");
|
||||
}
|
||||
|
||||
$timings = Timings::getEventTimings($this);
|
||||
$timings->startTiming();
|
||||
|
||||
$handlerList = HandlerListManager::global()->getListFor(get_class($this));
|
||||
|
||||
++self::$eventCallDepth;
|
||||
try{
|
||||
foreach(EventPriority::ALL as $priority){
|
||||
$currentList = $handlerList;
|
||||
while($currentList !== null){
|
||||
foreach($currentList->getListenersByPriority($priority) as $registration){
|
||||
$registration->callEvent($this);
|
||||
}
|
||||
|
||||
$currentList = $currentList->getParent();
|
||||
}
|
||||
foreach($handlerList->getListenerList() as $registration){
|
||||
$registration->callEvent($this);
|
||||
}
|
||||
}finally{
|
||||
--self::$eventCallDepth;
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ use function mb_strtoupper;
|
||||
* LOWEST -> LOW -> NORMAL -> HIGH -> HIGHEST -> MONITOR
|
||||
*
|
||||
* MONITOR events should not change the event outcome or contents
|
||||
*
|
||||
* WARNING: If these values are changed, handler sorting in HandlerList::getListenerList() may need to be updated.
|
||||
*/
|
||||
final class EventPriority{
|
||||
|
||||
|
@ -24,13 +24,20 @@ declare(strict_types=1);
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use function array_fill_keys;
|
||||
use function array_merge;
|
||||
use function krsort;
|
||||
use function spl_object_id;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
class HandlerList{
|
||||
/** @var RegisteredListener[][] */
|
||||
private array $handlerSlots = [];
|
||||
|
||||
private RegisteredListenerCache $handlerCache;
|
||||
|
||||
/** @var RegisteredListenerCache[] */
|
||||
private array $affectedHandlerCaches = [];
|
||||
|
||||
/**
|
||||
* @phpstan-template TEvent of Event
|
||||
* @phpstan-param class-string<TEvent> $class
|
||||
@ -39,7 +46,10 @@ class HandlerList{
|
||||
private string $class,
|
||||
private ?HandlerList $parentList
|
||||
){
|
||||
$this->handlerSlots = array_fill_keys(EventPriority::ALL, []);
|
||||
$this->handlerCache = new RegisteredListenerCache();
|
||||
for($list = $this; $list !== null; $list = $list->parentList){
|
||||
$list->affectedHandlerCaches[spl_object_id($this->handlerCache)] = $this->handlerCache;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,6 +60,7 @@ class HandlerList{
|
||||
throw new \InvalidArgumentException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}");
|
||||
}
|
||||
$this->handlerSlots[$listener->getPriority()][spl_object_id($listener)] = $listener;
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,6 +70,7 @@ class HandlerList{
|
||||
foreach($listeners as $listener){
|
||||
$this->register($listener);
|
||||
}
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,24 +88,60 @@ class HandlerList{
|
||||
}
|
||||
}
|
||||
}elseif($object instanceof RegisteredListener){
|
||||
if(isset($this->handlerSlots[$object->getPriority()][spl_object_id($object)])){
|
||||
unset($this->handlerSlots[$object->getPriority()][spl_object_id($object)]);
|
||||
}
|
||||
unset($this->handlerSlots[$object->getPriority()][spl_object_id($object)]);
|
||||
}
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
public function clear() : void{
|
||||
$this->handlerSlots = array_fill_keys(EventPriority::ALL, []);
|
||||
$this->handlerSlots = [];
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
*/
|
||||
public function getListenersByPriority(int $priority) : array{
|
||||
return $this->handlerSlots[$priority];
|
||||
return $this->handlerSlots[$priority] ?? [];
|
||||
}
|
||||
|
||||
public function getParent() : ?HandlerList{
|
||||
return $this->parentList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates all known caches which might be affected by this list's contents.
|
||||
*/
|
||||
private function invalidateAffectedCaches() : void{
|
||||
foreach($this->affectedHandlerCaches as $cache){
|
||||
$cache->list = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
* @phpstan-return list<RegisteredListener>
|
||||
*/
|
||||
public function getListenerList() : array{
|
||||
if($this->handlerCache->list !== null){
|
||||
return $this->handlerCache->list;
|
||||
}
|
||||
|
||||
$handlerLists = [];
|
||||
for($currentList = $this; $currentList !== null; $currentList = $currentList->parentList){
|
||||
$handlerLists[] = $currentList;
|
||||
}
|
||||
|
||||
$listenersByPriority = [];
|
||||
foreach($handlerLists as $currentList){
|
||||
foreach($currentList->handlerSlots as $priority => $listeners){
|
||||
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $listeners);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: why on earth do the priorities have higher values for lower priority?
|
||||
krsort($listenersByPriority, SORT_NUMERIC);
|
||||
|
||||
return $this->handlerCache->list = array_merge(...$listenersByPriority);
|
||||
}
|
||||
}
|
||||
|
@ -57,8 +57,11 @@ class RegisteredListener{
|
||||
return;
|
||||
}
|
||||
$this->timings->startTiming();
|
||||
($this->handler)($event);
|
||||
$this->timings->stopTiming();
|
||||
try{
|
||||
($this->handler)($event);
|
||||
}finally{
|
||||
$this->timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
public function isHandlingCancelled() : bool{
|
||||
|
38
src/event/RegisteredListenerCache.php
Normal file
38
src/event/RegisteredListenerCache.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class RegisteredListenerCache{
|
||||
|
||||
/**
|
||||
* List of all handlers that will be called for a particular event, ordered by execution order.
|
||||
*
|
||||
* @var RegisteredListener[]
|
||||
* @phpstan-var list<RegisteredListener>
|
||||
*/
|
||||
public ?array $list = null;
|
||||
}
|
@ -59,7 +59,7 @@ class PlayerInteractEvent extends PlayerEvent implements Cancellable{
|
||||
$this->player = $player;
|
||||
$this->item = $item;
|
||||
$this->blockTouched = $block;
|
||||
$this->touchVector = $touchVector ?? new Vector3(0, 0, 0);
|
||||
$this->touchVector = $touchVector ?? Vector3::zero();
|
||||
$this->blockFace = $face;
|
||||
$this->action = $action;
|
||||
}
|
||||
|
54
src/event/server/DataPacketDecodeEvent.php
Normal file
54
src/event/server/DataPacketDecodeEvent.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\server;
|
||||
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\CancellableTrait;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
|
||||
/**
|
||||
* Called before a packet is decoded and handled by the network session.
|
||||
* Cancelling this event will drop the packet without decoding it, minimizing wasted CPU time.
|
||||
*/
|
||||
class DataPacketDecodeEvent extends ServerEvent implements Cancellable{
|
||||
use CancellableTrait;
|
||||
|
||||
public function __construct(
|
||||
private NetworkSession $origin,
|
||||
private int $packetId,
|
||||
private string $packetBuffer
|
||||
){}
|
||||
|
||||
public function getOrigin() : NetworkSession{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function getPacketId() : int{
|
||||
return $this->packetId;
|
||||
}
|
||||
|
||||
public function getPacketBuffer() : string{
|
||||
return $this->packetBuffer;
|
||||
}
|
||||
}
|
@ -108,13 +108,23 @@ abstract class BaseInventory implements Inventory{
|
||||
$this->onContentChange($oldContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for utility functions which search the inventory.
|
||||
* TODO: make this abstract instead of providing a slow default implementation (BC break)
|
||||
*/
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{
|
||||
$item = $this->getItem($slot);
|
||||
return $item->equals($test, $checkDamage, $checkTags) ? $item->getCount() : 0;
|
||||
}
|
||||
|
||||
public function contains(Item $item) : bool{
|
||||
$count = max(1, $item->getCount());
|
||||
$checkDamage = !$item->hasAnyDamageValue();
|
||||
$checkTags = $item->hasNamedTag();
|
||||
foreach($this->getContents() as $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags)){
|
||||
$count -= $i->getCount();
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
$slotCount = $this->getMatchingItemCount($i, $item, $checkDamage, $checkTags);
|
||||
if($slotCount > 0){
|
||||
$count -= $slotCount;
|
||||
if($count <= 0){
|
||||
return true;
|
||||
}
|
||||
@ -128,9 +138,9 @@ abstract class BaseInventory implements Inventory{
|
||||
$slots = [];
|
||||
$checkDamage = !$item->hasAnyDamageValue();
|
||||
$checkTags = $item->hasNamedTag();
|
||||
foreach($this->getContents() as $index => $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags)){
|
||||
$slots[$index] = $i;
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
if($this->getMatchingItemCount($i, $item, $checkDamage, $checkTags) > 0){
|
||||
$slots[$i] = $this->getItem($i);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,18 +152,9 @@ abstract class BaseInventory implements Inventory{
|
||||
$checkDamage = $exact || !$item->hasAnyDamageValue();
|
||||
$checkTags = $exact || $item->hasNamedTag();
|
||||
|
||||
foreach($this->getContents() as $index => $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags) && ($i->getCount() === $count || (!$exact && $i->getCount() > $count))){
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public function firstEmpty() : int{
|
||||
foreach($this->getContents(true) as $i => $slot){
|
||||
if($slot->isNull()){
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
$slotCount = $this->getMatchingItemCount($i, $item, $checkDamage, $checkTags);
|
||||
if($slotCount > 0 && ($slotCount === $count || (!$exact && $slotCount > $count))){
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
@ -161,6 +162,20 @@ abstract class BaseInventory implements Inventory{
|
||||
return -1;
|
||||
}
|
||||
|
||||
public function firstEmpty() : int{
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
if($this->isSlotEmpty($i)){
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: make this abstract and force implementations to implement it properly (BC break)
|
||||
* This default implementation works, but is slow.
|
||||
*/
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
return $this->getItem($index)->isNull();
|
||||
}
|
||||
@ -171,14 +186,16 @@ abstract class BaseInventory implements Inventory{
|
||||
|
||||
public function getAddableItemQuantity(Item $item) : int{
|
||||
$count = $item->getCount();
|
||||
$maxStackSize = min($this->getMaxStackSize(), $item->getMaxStackSize());
|
||||
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
$slot = $this->getItem($i);
|
||||
if($item->canStackWith($slot)){
|
||||
if(($diff = min($slot->getMaxStackSize(), $item->getMaxStackSize()) - $slot->getCount()) > 0){
|
||||
if($this->isSlotEmpty($i)){
|
||||
$count -= $maxStackSize;
|
||||
}else{
|
||||
$slotCount = $this->getMatchingItemCount($i, $item, true, true);
|
||||
if($slotCount > 0 && ($diff = $maxStackSize - $slotCount) > 0){
|
||||
$count -= $diff;
|
||||
}
|
||||
}elseif($slot->isNull()){
|
||||
$count -= min($this->getMaxStackSize(), $item->getMaxStackSize());
|
||||
}
|
||||
|
||||
if($count <= 0){
|
||||
@ -212,22 +229,29 @@ abstract class BaseInventory implements Inventory{
|
||||
return $returnSlots;
|
||||
}
|
||||
|
||||
private function internalAddItem(Item $slot) : Item{
|
||||
private function internalAddItem(Item $newItem) : Item{
|
||||
$emptySlots = [];
|
||||
|
||||
$maxStackSize = min($this->getMaxStackSize(), $newItem->getMaxStackSize());
|
||||
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
$item = $this->getItem($i);
|
||||
if($item->isNull()){
|
||||
if($this->isSlotEmpty($i)){
|
||||
$emptySlots[] = $i;
|
||||
continue;
|
||||
}
|
||||
$slotCount = $this->getMatchingItemCount($i, $newItem, true, true);
|
||||
if($slotCount === 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
if($slot->canStackWith($item) && $item->getCount() < $item->getMaxStackSize()){
|
||||
$amount = min($item->getMaxStackSize() - $item->getCount(), $slot->getCount(), $this->getMaxStackSize());
|
||||
if($slotCount < $maxStackSize){
|
||||
$amount = min($maxStackSize - $slotCount, $newItem->getCount());
|
||||
if($amount > 0){
|
||||
$slot->setCount($slot->getCount() - $amount);
|
||||
$item->setCount($item->getCount() + $amount);
|
||||
$this->setItem($i, $item);
|
||||
if($slot->getCount() <= 0){
|
||||
$newItem->setCount($newItem->getCount() - $amount);
|
||||
$slotItem = $this->getItem($i);
|
||||
$slotItem->setCount($slotItem->getCount() + $amount);
|
||||
$this->setItem($i, $slotItem);
|
||||
if($newItem->getCount() <= 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -236,65 +260,67 @@ abstract class BaseInventory implements Inventory{
|
||||
|
||||
if(count($emptySlots) > 0){
|
||||
foreach($emptySlots as $slotIndex){
|
||||
$amount = min($slot->getMaxStackSize(), $slot->getCount(), $this->getMaxStackSize());
|
||||
$slot->setCount($slot->getCount() - $amount);
|
||||
$item = clone $slot;
|
||||
$item->setCount($amount);
|
||||
$this->setItem($slotIndex, $item);
|
||||
if($slot->getCount() <= 0){
|
||||
$amount = min($maxStackSize, $newItem->getCount());
|
||||
$newItem->setCount($newItem->getCount() - $amount);
|
||||
$slotItem = clone $newItem;
|
||||
$slotItem->setCount($amount);
|
||||
$this->setItem($slotIndex, $slotItem);
|
||||
if($newItem->getCount() <= 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $slot;
|
||||
return $newItem;
|
||||
}
|
||||
|
||||
public function remove(Item $item) : void{
|
||||
$checkDamage = !$item->hasAnyDamageValue();
|
||||
$checkTags = $item->hasNamedTag();
|
||||
|
||||
foreach($this->getContents() as $index => $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags)){
|
||||
$this->clear($index);
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
if($this->getMatchingItemCount($i, $item, $checkDamage, $checkTags) > 0){
|
||||
$this->clear($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeItem(Item ...$slots) : array{
|
||||
/** @var Item[] $itemSlots */
|
||||
/** @var Item[] $searchItems */
|
||||
/** @var Item[] $slots */
|
||||
$itemSlots = [];
|
||||
$searchItems = [];
|
||||
foreach($slots as $slot){
|
||||
if(!$slot->isNull()){
|
||||
$itemSlots[] = clone $slot;
|
||||
$searchItems[] = clone $slot;
|
||||
}
|
||||
}
|
||||
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
$item = $this->getItem($i);
|
||||
if($item->isNull()){
|
||||
if($this->isSlotEmpty($i)){
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach($itemSlots as $index => $slot){
|
||||
if($slot->equals($item, !$slot->hasAnyDamageValue(), $slot->hasNamedTag())){
|
||||
$amount = min($item->getCount(), $slot->getCount());
|
||||
$slot->setCount($slot->getCount() - $amount);
|
||||
$item->setCount($item->getCount() - $amount);
|
||||
$this->setItem($i, $item);
|
||||
if($slot->getCount() <= 0){
|
||||
unset($itemSlots[$index]);
|
||||
foreach($searchItems as $index => $search){
|
||||
$slotCount = $this->getMatchingItemCount($i, $search, !$search->hasAnyDamageValue(), $search->hasNamedTag());
|
||||
if($slotCount > 0){
|
||||
$amount = min($slotCount, $search->getCount());
|
||||
$search->setCount($search->getCount() - $amount);
|
||||
|
||||
$slotItem = $this->getItem($i);
|
||||
$slotItem->setCount($slotItem->getCount() - $amount);
|
||||
$this->setItem($i, $slotItem);
|
||||
if($search->getCount() <= 0){
|
||||
unset($searchItems[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(count($itemSlots) === 0){
|
||||
if(count($searchItems) === 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $itemSlots;
|
||||
return $searchItems;
|
||||
}
|
||||
|
||||
public function clear(int $index) : void{
|
||||
@ -348,7 +374,7 @@ abstract class BaseInventory implements Inventory{
|
||||
if($invManager === null){
|
||||
continue;
|
||||
}
|
||||
$invManager->syncSlot($this, $index);
|
||||
$invManager->onSlotChange($this, $index);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ use pocketmine\item\Durable;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use pocketmine\utils\Utils;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
use function json_decode;
|
||||
|
||||
@ -60,11 +61,11 @@ final class CreativeInventory{
|
||||
* @return Item[]
|
||||
*/
|
||||
public function getAll() : array{
|
||||
return $this->creative;
|
||||
return Utils::cloneObjectArray($this->creative);
|
||||
}
|
||||
|
||||
public function getItem(int $index) : ?Item{
|
||||
return $this->creative[$index] ?? null;
|
||||
return isset($this->creative[$index]) ? clone $this->creative[$index] : null;
|
||||
}
|
||||
|
||||
public function getItemIndex(Item $item) : int{
|
||||
|
@ -30,6 +30,7 @@ use pocketmine\item\Item;
|
||||
*/
|
||||
class DelegateInventory extends BaseInventory{
|
||||
private InventoryListener $inventoryListener;
|
||||
private bool $backingInventoryChanging = false;
|
||||
|
||||
public function __construct(
|
||||
private Inventory $backingInventory
|
||||
@ -39,12 +40,22 @@ class DelegateInventory extends BaseInventory{
|
||||
$this->backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener(
|
||||
static function(Inventory $unused, int $slot, Item $oldItem) use ($weakThis) : void{
|
||||
if(($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->onSlotChange($slot, $oldItem);
|
||||
$strongThis->backingInventoryChanging = true;
|
||||
try{
|
||||
$strongThis->onSlotChange($slot, $oldItem);
|
||||
}finally{
|
||||
$strongThis->backingInventoryChanging = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
static function(Inventory $unused, array $oldContents) use ($weakThis) : void{
|
||||
if(($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->onContentChange($oldContents);
|
||||
$strongThis->backingInventoryChanging = true;
|
||||
try{
|
||||
$strongThis->onContentChange($oldContents);
|
||||
}finally{
|
||||
$strongThis->backingInventoryChanging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
@ -73,4 +84,20 @@ class DelegateInventory extends BaseInventory{
|
||||
protected function internalSetContents(array $items) : void{
|
||||
$this->backingInventory->setContents($items);
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
return $this->backingInventory->isSlotEmpty($index);
|
||||
}
|
||||
|
||||
protected function onSlotChange(int $index, Item $before) : void{
|
||||
if($this->backingInventoryChanging){
|
||||
parent::onSlotChange($index, $before);
|
||||
}
|
||||
}
|
||||
|
||||
protected function onContentChange(array $itemsBefore) : void{
|
||||
if($this->backingInventoryChanging){
|
||||
parent::onContentChange($itemsBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,4 +83,13 @@ class SimpleInventory extends BaseInventory{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{
|
||||
$slotItem = $this->slots[$slot];
|
||||
return $slotItem !== null && $slotItem->equals($test, $checkDamage, $checkTags) ? $slotItem->getCount() : 0;
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
return $this->slots[$index] === null || $this->slots[$index]->isNull();
|
||||
}
|
||||
}
|
||||
|
@ -60,9 +60,11 @@ class CraftingTransaction extends InventoryTransaction{
|
||||
|
||||
private CraftingManager $craftingManager;
|
||||
|
||||
public function __construct(Player $source, CraftingManager $craftingManager, array $actions = []){
|
||||
public function __construct(Player $source, CraftingManager $craftingManager, array $actions = [], ?CraftingRecipe $recipe = null, ?int $repetitions = null){
|
||||
parent::__construct($source, $actions);
|
||||
$this->craftingManager = $craftingManager;
|
||||
$this->recipe = $recipe;
|
||||
$this->repetitions = $repetitions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,6 +125,18 @@ class CraftingTransaction extends InventoryTransaction{
|
||||
return $iterations;
|
||||
}
|
||||
|
||||
private function validateRecipe(CraftingRecipe $recipe, ?int $expectedRepetitions) : int{
|
||||
//compute number of times recipe was crafted
|
||||
$repetitions = $this->matchRecipeItems($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()), false);
|
||||
if($expectedRepetitions !== null && $repetitions !== $expectedRepetitions){
|
||||
throw new TransactionValidationException("Expected $expectedRepetitions repetitions, got $repetitions");
|
||||
}
|
||||
//assert that $repetitions x recipe ingredients should be consumed
|
||||
$this->matchRecipeItems($this->inputs, $recipe->getIngredientList(), true, $repetitions);
|
||||
|
||||
return $repetitions;
|
||||
}
|
||||
|
||||
public function validate() : void{
|
||||
$this->squashDuplicateSlotChanges();
|
||||
if(count($this->actions) < 1){
|
||||
@ -131,25 +145,24 @@ class CraftingTransaction extends InventoryTransaction{
|
||||
|
||||
$this->matchItems($this->outputs, $this->inputs);
|
||||
|
||||
$failed = 0;
|
||||
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
|
||||
try{
|
||||
//compute number of times recipe was crafted
|
||||
$this->repetitions = $this->matchRecipeItems($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()), false);
|
||||
//assert that $repetitions x recipe ingredients should be consumed
|
||||
$this->matchRecipeItems($this->inputs, $recipe->getIngredientList(), true, $this->repetitions);
|
||||
|
||||
//Success!
|
||||
$this->recipe = $recipe;
|
||||
break;
|
||||
}catch(TransactionValidationException $e){
|
||||
//failed
|
||||
++$failed;
|
||||
}
|
||||
}
|
||||
|
||||
if($this->recipe === null){
|
||||
throw new TransactionValidationException("Unable to match a recipe to transaction (tried to match against $failed recipes)");
|
||||
$failed = 0;
|
||||
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
|
||||
try{
|
||||
$this->repetitions = $this->validateRecipe($recipe, $this->repetitions);
|
||||
$this->recipe = $recipe;
|
||||
break;
|
||||
}catch(TransactionValidationException $e){
|
||||
//failed
|
||||
++$failed;
|
||||
}
|
||||
}
|
||||
|
||||
if($this->recipe === null){
|
||||
throw new TransactionValidationException("Unable to match a recipe to transaction (tried to match against $failed recipes)");
|
||||
}
|
||||
}else{
|
||||
$this->repetitions = $this->validateRecipe($this->recipe, $this->repetitions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,10 @@ final class TransactionBuilderInventory extends BaseInventory{
|
||||
$this->changedSlots = new \SplFixedArray($this->actualInventory->getSize());
|
||||
}
|
||||
|
||||
public function getActualInventory() : Inventory{
|
||||
return $this->actualInventory;
|
||||
}
|
||||
|
||||
protected function internalSetContents(array $items) : void{
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
if(!isset($items[$i])){
|
||||
|
@ -353,30 +353,35 @@ class Item implements \JsonSerializable{
|
||||
}
|
||||
|
||||
protected function serializeCompoundTag(CompoundTag $tag) : void{
|
||||
$display = $tag->getCompoundTag(self::TAG_DISPLAY) ?? new CompoundTag();
|
||||
$display = $tag->getCompoundTag(self::TAG_DISPLAY);
|
||||
|
||||
$this->hasCustomName() ?
|
||||
$display->setString(self::TAG_DISPLAY_NAME, $this->getCustomName()) :
|
||||
$display->removeTag(self::TAG_DISPLAY_NAME);
|
||||
if($this->customName !== ""){
|
||||
$display ??= new CompoundTag();
|
||||
$display->setString(self::TAG_DISPLAY_NAME, $this->customName);
|
||||
}else{
|
||||
$display?->removeTag(self::TAG_DISPLAY_NAME);
|
||||
}
|
||||
|
||||
if(count($this->lore) > 0){
|
||||
$loreTag = new ListTag();
|
||||
foreach($this->lore as $line){
|
||||
$loreTag->push(new StringTag($line));
|
||||
}
|
||||
$display ??= new CompoundTag();
|
||||
$display->setTag(self::TAG_DISPLAY_LORE, $loreTag);
|
||||
}else{
|
||||
$display->removeTag(self::TAG_DISPLAY_LORE);
|
||||
$display?->removeTag(self::TAG_DISPLAY_LORE);
|
||||
}
|
||||
$display->count() > 0 ?
|
||||
$display !== null && $display->count() > 0 ?
|
||||
$tag->setTag(self::TAG_DISPLAY, $display) :
|
||||
$tag->removeTag(self::TAG_DISPLAY);
|
||||
|
||||
if($this->hasEnchantments()){
|
||||
if(count($this->enchantments) > 0){
|
||||
$ench = new ListTag();
|
||||
foreach($this->getEnchantments() as $enchantmentInstance){
|
||||
$enchantmentIdMap = EnchantmentIdMap::getInstance();
|
||||
foreach($this->enchantments as $enchantmentInstance){
|
||||
$ench->push(CompoundTag::create()
|
||||
->setShort(self::TAG_ENCH_ID, EnchantmentIdMap::getInstance()->toId($enchantmentInstance->getType()))
|
||||
->setShort(self::TAG_ENCH_ID, $enchantmentIdMap->toId($enchantmentInstance->getType()))
|
||||
->setShort(self::TAG_ENCH_LVL, $enchantmentInstance->getLevel())
|
||||
);
|
||||
}
|
||||
@ -385,8 +390,8 @@ class Item implements \JsonSerializable{
|
||||
$tag->removeTag(self::TAG_ENCH);
|
||||
}
|
||||
|
||||
($blockData = $this->getCustomBlockData()) !== null ?
|
||||
$tag->setTag(self::TAG_BLOCK_ENTITY_TAG, clone $blockData) :
|
||||
$this->blockEntityTag !== null ?
|
||||
$tag->setTag(self::TAG_BLOCK_ENTITY_TAG, clone $this->blockEntityTag) :
|
||||
$tag->removeTag(self::TAG_BLOCK_ENTITY_TAG);
|
||||
|
||||
if(count($this->canPlaceOn) > 0){
|
||||
|
@ -1606,6 +1606,14 @@ final class KnownTranslationFactory{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_BAN_NOREASON, []);
|
||||
}
|
||||
|
||||
public static function pocketmine_disconnect_clientDisconnect() : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_CLIENTDISCONNECT, []);
|
||||
}
|
||||
|
||||
public static function pocketmine_disconnect_clientReconnect() : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_CLIENTRECONNECT, []);
|
||||
}
|
||||
|
||||
public static function pocketmine_disconnect_error(Translatable|string $error, Translatable|string $errorId) : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR, [
|
||||
"error" => $error,
|
||||
@ -1633,6 +1641,10 @@ final class KnownTranslationFactory{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR_RESPAWN, []);
|
||||
}
|
||||
|
||||
public static function pocketmine_disconnect_error_timeout() : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR_TIMEOUT, []);
|
||||
}
|
||||
|
||||
public static function pocketmine_disconnect_incompatibleProtocol(Translatable|string $param0) : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INCOMPATIBLEPROTOCOL, [
|
||||
0 => $param0,
|
||||
|
@ -349,12 +349,15 @@ final class KnownTranslationKeys{
|
||||
public const POCKETMINE_DISCONNECT_BAN_HARDCORE = "pocketmine.disconnect.ban.hardcore";
|
||||
public const POCKETMINE_DISCONNECT_BAN_IP = "pocketmine.disconnect.ban.ip";
|
||||
public const POCKETMINE_DISCONNECT_BAN_NOREASON = "pocketmine.disconnect.ban.noReason";
|
||||
public const POCKETMINE_DISCONNECT_CLIENTDISCONNECT = "pocketmine.disconnect.clientDisconnect";
|
||||
public const POCKETMINE_DISCONNECT_CLIENTRECONNECT = "pocketmine.disconnect.clientReconnect";
|
||||
public const POCKETMINE_DISCONNECT_ERROR = "pocketmine.disconnect.error";
|
||||
public const POCKETMINE_DISCONNECT_ERROR_AUTHENTICATION = "pocketmine.disconnect.error.authentication";
|
||||
public const POCKETMINE_DISCONNECT_ERROR_BADPACKET = "pocketmine.disconnect.error.badPacket";
|
||||
public const POCKETMINE_DISCONNECT_ERROR_INTERNAL = "pocketmine.disconnect.error.internal";
|
||||
public const POCKETMINE_DISCONNECT_ERROR_LOGINTIMEOUT = "pocketmine.disconnect.error.loginTimeout";
|
||||
public const POCKETMINE_DISCONNECT_ERROR_RESPAWN = "pocketmine.disconnect.error.respawn";
|
||||
public const POCKETMINE_DISCONNECT_ERROR_TIMEOUT = "pocketmine.disconnect.error.timeout";
|
||||
public const POCKETMINE_DISCONNECT_INCOMPATIBLEPROTOCOL = "pocketmine.disconnect.incompatibleProtocol";
|
||||
public const POCKETMINE_DISCONNECT_INVALIDSESSION = "pocketmine.disconnect.invalidSession";
|
||||
public const POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE = "pocketmine.disconnect.invalidSession.badSignature";
|
||||
|
@ -25,7 +25,7 @@ namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
|
||||
final class ComplexWindowMapEntry{
|
||||
final class ComplexInventoryMapEntry{
|
||||
|
||||
/**
|
||||
* @var int[]
|
93
src/network/mcpe/EntityEventBroadcaster.php
Normal file
93
src/network/mcpe/EntityEventBroadcaster.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\entity\Attribute;
|
||||
use pocketmine\entity\effect\EffectInstance;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\MetadataProperty;
|
||||
|
||||
/**
|
||||
* This class allows broadcasting entity events to many viewers on the server network.
|
||||
*/
|
||||
interface EntityEventBroadcaster{
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
* @param Attribute[] $attributes
|
||||
*/
|
||||
public function syncAttributes(array $recipients, Living $entity, array $attributes) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
* @param MetadataProperty[] $properties
|
||||
*
|
||||
* @phpstan-param array<int, MetadataProperty> $properties
|
||||
*/
|
||||
public function syncActorData(array $recipients, Entity $entity, array $properties) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onEntityEffectAdded(array $recipients, Living $entity, EffectInstance $effect, bool $replacesOldEffect) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onEntityEffectRemoved(array $recipients, Living $entity, EffectInstance $effect) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onEntityRemoved(array $recipients, Entity $entity) : void;
|
||||
|
||||
/**
|
||||
* TODO: expand this to more than just humans
|
||||
*
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onMobMainHandItemChange(array $recipients,Human $mob) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onMobOffHandItemChange(array $recipients, Human $mob) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onMobArmorChange(array $recipients, Living $mob) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onPickUpItem(array $recipients, Entity $collector, Entity $pickedUp) : void;
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
public function onEmote(array $recipients, Human $from, string $emoteId) : void;
|
||||
}
|
@ -38,7 +38,6 @@ use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\convert\TypeConversionException;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
|
||||
@ -51,6 +50,7 @@ use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
@ -59,9 +59,11 @@ use pocketmine\network\PacketHandlingException;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use function array_map;
|
||||
use function array_keys;
|
||||
use function array_search;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function implode;
|
||||
use function is_int;
|
||||
use function max;
|
||||
use function spl_object_id;
|
||||
@ -70,26 +72,25 @@ use function spl_object_id;
|
||||
* @phpstan-type ContainerOpenClosure \Closure(int $id, Inventory $inventory) : (list<ClientboundPacket>|null)
|
||||
*/
|
||||
class InventoryManager{
|
||||
/** @var Inventory[] */
|
||||
private array $windowMap = [];
|
||||
/**
|
||||
* @var ComplexWindowMapEntry[]
|
||||
* @phpstan-var array<int, ComplexWindowMapEntry>
|
||||
* @var InventoryManagerEntry[] spl_object_id(Inventory) => InventoryManagerEntry
|
||||
* @phpstan-var array<int, InventoryManagerEntry>
|
||||
*/
|
||||
private array $complexWindows = [];
|
||||
private array $inventories = [];
|
||||
|
||||
/**
|
||||
* @var ComplexWindowMapEntry[]
|
||||
* @phpstan-var array<int, ComplexWindowMapEntry>
|
||||
* @var Inventory[] network window ID => Inventory
|
||||
* @phpstan-var array<int, Inventory>
|
||||
*/
|
||||
private array $complexSlotToWindowMap = [];
|
||||
private array $networkIdToInventoryMap = [];
|
||||
/**
|
||||
* @var ComplexInventoryMapEntry[] net slot ID => ComplexWindowMapEntry
|
||||
* @phpstan-var array<int, ComplexInventoryMapEntry>
|
||||
*/
|
||||
private array $complexSlotToInventoryMap = [];
|
||||
|
||||
private int $lastInventoryNetworkId = ContainerIds::FIRST;
|
||||
|
||||
/**
|
||||
* @var Item[][]
|
||||
* @phpstan-var array<int, array<int, Item>>
|
||||
*/
|
||||
private array $initiatedSlotChanges = [];
|
||||
private int $clientSelectedHotbarSlot = -1;
|
||||
|
||||
/** @phpstan-var ObjectSet<ContainerOpenClosure> */
|
||||
@ -99,6 +100,11 @@ class InventoryManager{
|
||||
/** @phpstan-var \Closure() : void */
|
||||
private ?\Closure $pendingOpenWindowCallback = null;
|
||||
|
||||
private int $nextItemStackId = 1;
|
||||
private ?int $currentItemStackRequestId = null;
|
||||
|
||||
private bool $fullSyncRequested = false;
|
||||
|
||||
public function __construct(
|
||||
private Player $player,
|
||||
private NetworkSession $session
|
||||
@ -117,14 +123,27 @@ class InventoryManager{
|
||||
});
|
||||
}
|
||||
|
||||
private function associateIdWithInventory(int $id, Inventory $inventory) : void{
|
||||
$this->networkIdToInventoryMap[$id] = $inventory;
|
||||
}
|
||||
|
||||
private function getNewWindowId() : int{
|
||||
$this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST);
|
||||
return $this->lastInventoryNetworkId;
|
||||
}
|
||||
|
||||
private function add(int $id, Inventory $inventory) : void{
|
||||
$this->windowMap[$id] = $inventory;
|
||||
if(isset($this->inventories[spl_object_id($inventory)])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
|
||||
}
|
||||
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry($inventory);
|
||||
$this->associateIdWithInventory($id, $inventory);
|
||||
}
|
||||
|
||||
private function addDynamic(Inventory $inventory) : int{
|
||||
$this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST);
|
||||
$this->add($this->lastInventoryNetworkId, $inventory);
|
||||
return $this->lastInventoryNetworkId;
|
||||
$id = $this->getNewWindowId();
|
||||
$this->add($id, $inventory);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,26 +151,45 @@ class InventoryManager{
|
||||
* @phpstan-param array<int, int>|int $slotMap
|
||||
*/
|
||||
private function addComplex(array|int $slotMap, Inventory $inventory) : void{
|
||||
$entry = new ComplexWindowMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
|
||||
$this->complexWindows[spl_object_id($inventory)] = $entry;
|
||||
foreach($entry->getSlotMap() as $netSlot => $coreSlot){
|
||||
$this->complexSlotToWindowMap[$netSlot] = $entry;
|
||||
if(isset($this->inventories[spl_object_id($inventory)])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
|
||||
}
|
||||
$complexSlotMap = new ComplexInventoryMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
|
||||
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry(
|
||||
$inventory,
|
||||
$complexSlotMap
|
||||
);
|
||||
foreach($complexSlotMap->getSlotMap() as $netSlot => $coreSlot){
|
||||
$this->complexSlotToInventoryMap[$netSlot] = $complexSlotMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|int $slotMap
|
||||
* @phpstan-param array<int, int>|int $slotMap
|
||||
*/
|
||||
private function addComplexDynamic(array|int $slotMap, Inventory $inventory) : int{
|
||||
$this->addComplex($slotMap, $inventory);
|
||||
$id = $this->getNewWindowId();
|
||||
$this->associateIdWithInventory($id, $inventory);
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function remove(int $id) : void{
|
||||
$inventory = $this->windowMap[$id];
|
||||
$splObjectId = spl_object_id($inventory);
|
||||
unset($this->windowMap[$id], $this->initiatedSlotChanges[$id], $this->complexWindows[$splObjectId]);
|
||||
foreach($this->complexSlotToWindowMap as $netSlot => $entry){
|
||||
if($entry->getInventory() === $inventory){
|
||||
unset($this->complexSlotToWindowMap[$netSlot]);
|
||||
$inventory = $this->networkIdToInventoryMap[$id];
|
||||
unset($this->networkIdToInventoryMap[$id]);
|
||||
if($this->getWindowId($inventory) === null){
|
||||
unset($this->inventories[spl_object_id($inventory)]);
|
||||
foreach($this->complexSlotToInventoryMap as $netSlot => $entry){
|
||||
if($entry->getInventory() === $inventory){
|
||||
unset($this->complexSlotToInventoryMap[$netSlot]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getWindowId(Inventory $inventory) : ?int{
|
||||
return ($id = array_search($inventory, $this->windowMap, true)) !== false ? $id : null;
|
||||
return ($id = array_search($inventory, $this->networkIdToInventoryMap, true)) !== false ? $id : null;
|
||||
}
|
||||
|
||||
public function getCurrentWindowId() : int{
|
||||
@ -159,28 +197,35 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}
|
||||
* @phpstan-return array{Inventory, int}|null
|
||||
*/
|
||||
public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
|
||||
if($windowId === ContainerIds::UI){
|
||||
$entry = $this->complexSlotToWindowMap[$netSlotId] ?? null;
|
||||
$entry = $this->complexSlotToInventoryMap[$netSlotId] ?? null;
|
||||
if($entry === null){
|
||||
return null;
|
||||
}
|
||||
$inventory = $entry->getInventory();
|
||||
$coreSlotId = $entry->mapNetToCore($netSlotId);
|
||||
return $coreSlotId !== null ? [$entry->getInventory(), $coreSlotId] : null;
|
||||
return $coreSlotId !== null && $inventory->slotExists($coreSlotId) ? [$inventory, $coreSlotId] : null;
|
||||
}
|
||||
if(isset($this->windowMap[$windowId])){
|
||||
return [$this->windowMap[$windowId], $netSlotId];
|
||||
$inventory = $this->networkIdToInventoryMap[$windowId] ?? null;
|
||||
if($inventory !== null && $inventory->slotExists($netSlotId)){
|
||||
return [$inventory, $netSlotId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onTransactionStart(InventoryTransaction $tx) : void{
|
||||
private function addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $item) : void{
|
||||
$this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item;
|
||||
}
|
||||
|
||||
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
|
||||
foreach($tx->getActions() as $action){
|
||||
if($action instanceof SlotChangeAction && ($windowId = $this->getWindowId($action->getInventory())) !== null){
|
||||
//in some cases the inventory might not have a window ID, but still be referenced by a transaction (e.g. crafting grid changes), so we can't unconditionally record the change here or we might leak things
|
||||
$this->initiatedSlotChanges[$windowId][$action->getSlot()] = $action->getTargetItem();
|
||||
if($action instanceof SlotChangeAction){
|
||||
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
|
||||
$itemStack = TypeConverter::getInstance()->coreItemStackToNet($action->getTargetItem());
|
||||
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -189,22 +234,34 @@ class InventoryManager{
|
||||
* @param NetworkInventoryAction[] $networkInventoryActions
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
public function addPredictedSlotChanges(array $networkInventoryActions) : void{
|
||||
public function addRawPredictedSlotChanges(array $networkInventoryActions) : void{
|
||||
foreach($networkInventoryActions as $action){
|
||||
if($action->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && (
|
||||
isset($this->windowMap[$action->windowId]) ||
|
||||
($action->windowId === ContainerIds::UI && isset($this->complexSlotToWindowMap[$action->inventorySlot]))
|
||||
)){
|
||||
try{
|
||||
$item = TypeConverter::getInstance()->netItemStackToCore($action->newItem->getItemStack());
|
||||
}catch(TypeConversionException $e){
|
||||
throw new PacketHandlingException($e->getMessage(), 0, $e);
|
||||
}
|
||||
$this->initiatedSlotChanges[$action->windowId][$action->inventorySlot] = $item;
|
||||
if($action->sourceType !== NetworkInventoryAction::SOURCE_CONTAINER){
|
||||
continue;
|
||||
}
|
||||
|
||||
//legacy transactions should not modify or predict anything other than these inventories, since these are
|
||||
//the only ones accessible when not in-game (ItemStackRequest is used for everything else)
|
||||
if(match($action->windowId){
|
||||
ContainerIds::INVENTORY, ContainerIds::OFFHAND, ContainerIds::ARMOR => false,
|
||||
default => true
|
||||
}){
|
||||
throw new PacketHandlingException("Legacy transactions cannot predict changes to inventory with ID " . $action->windowId);
|
||||
}
|
||||
$info = $this->locateWindowAndSlot($action->windowId, $action->inventorySlot);
|
||||
if($info === null){
|
||||
continue;
|
||||
}
|
||||
|
||||
[$inventory, $slot] = $info;
|
||||
$this->addPredictedSlotChange($inventory, $slot, $action->newItem->getItemStack());
|
||||
}
|
||||
}
|
||||
|
||||
public function setCurrentItemStackRequestId(?int $id) : void{
|
||||
$this->currentItemStackRequestId = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the server initiates a window close, it does so by sending a ContainerClose to the client, which causes the
|
||||
* client to behave as if it initiated the close itself. It responds by sending a ContainerClose back to the server,
|
||||
@ -248,9 +305,10 @@ class InventoryManager{
|
||||
$this->onCurrentWindowRemove();
|
||||
|
||||
$this->openWindowDeferred(function() use ($inventory) : void{
|
||||
$windowId = $this->addDynamic($inventory);
|
||||
if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){
|
||||
$this->addComplex($slotMap, $inventory);
|
||||
$windowId = $this->addComplexDynamic($slotMap, $inventory);
|
||||
}else{
|
||||
$windowId = $this->addDynamic($inventory);
|
||||
}
|
||||
|
||||
foreach($this->containerOpenCallbacks as $callback){
|
||||
@ -304,7 +362,8 @@ class InventoryManager{
|
||||
$this->onCurrentWindowRemove();
|
||||
|
||||
$this->openWindowDeferred(function() : void{
|
||||
$windowId = $this->addDynamic($this->player->getInventory());
|
||||
$windowId = $this->getNewWindowId();
|
||||
$this->associateIdWithInventory($windowId, $this->player->getInventory());
|
||||
|
||||
$this->session->sendDataPacket(ContainerOpenPacket::entityInv(
|
||||
$windowId,
|
||||
@ -315,7 +374,7 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function onCurrentWindowRemove() : void{
|
||||
if(isset($this->windowMap[$this->lastInventoryNetworkId])){
|
||||
if(isset($this->networkIdToInventoryMap[$this->lastInventoryNetworkId])){
|
||||
$this->remove($this->lastInventoryNetworkId);
|
||||
$this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, true));
|
||||
if($this->pendingCloseWindowId !== null){
|
||||
@ -327,7 +386,7 @@ class InventoryManager{
|
||||
|
||||
public function onClientRemoveWindow(int $id) : void{
|
||||
if($id === $this->lastInventoryNetworkId){
|
||||
if(isset($this->windowMap[$id]) && $id !== $this->pendingCloseWindowId){
|
||||
if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){
|
||||
$this->remove($id);
|
||||
$this->player->removeCurrentWindow();
|
||||
}
|
||||
@ -349,96 +408,159 @@ class InventoryManager{
|
||||
}
|
||||
}
|
||||
|
||||
public function syncSlot(Inventory $inventory, int $slot) : void{
|
||||
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
|
||||
if($slotMap !== null){
|
||||
$windowId = ContainerIds::UI;
|
||||
$netSlot = $slotMap->mapCoreToNet($slot) ?? null;
|
||||
public function onSlotChange(Inventory $inventory, int $slot) : void{
|
||||
$inventoryEntry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
if($inventoryEntry === null){
|
||||
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
|
||||
//is cleared before removal.
|
||||
return;
|
||||
}
|
||||
$currentItem = TypeConverter::getInstance()->coreItemStackToNet($inventory->getItem($slot));
|
||||
$clientSideItem = $inventoryEntry->predictions[$slot] ?? null;
|
||||
if($clientSideItem === null || !$clientSideItem->equals($currentItem)){
|
||||
//no prediction or incorrect - do not associate this with the currently active itemstack request
|
||||
$this->trackItemStack($inventoryEntry, $slot, $currentItem, null);
|
||||
$inventoryEntry->pendingSyncs[$slot] = $currentItem;
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory);
|
||||
//correctly predicted - associate the change with the currently active itemstack request
|
||||
$this->trackItemStack($inventoryEntry, $slot, $currentItem, $this->currentItemStackRequestId);
|
||||
}
|
||||
|
||||
unset($inventoryEntry->predictions[$slot]);
|
||||
}
|
||||
|
||||
public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) : void{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
if($entry === null){
|
||||
throw new \LogicException("Cannot sync an untracked inventory");
|
||||
}
|
||||
$itemStackInfo = $entry->itemStackInfos[$slot];
|
||||
if($itemStackInfo === null){
|
||||
throw new \LogicException("Cannot sync an untracked inventory slot");
|
||||
}
|
||||
if($entry->complexSlotMap !== null){
|
||||
$windowId = ContainerIds::UI;
|
||||
$netSlot = $entry->complexSlotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||
$netSlot = $slot;
|
||||
}
|
||||
if($windowId !== null && $netSlot !== null){
|
||||
$currentItem = $inventory->getItem($slot);
|
||||
$clientSideItem = $this->initiatedSlotChanges[$windowId][$netSlot] ?? null;
|
||||
if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){
|
||||
$itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem));
|
||||
if($windowId === ContainerIds::OFFHAND){
|
||||
//TODO: HACK!
|
||||
//The client may sometimes ignore the InventorySlotPacket for the offhand slot.
|
||||
//This can cause a lot of problems (totems, arrows, and more...).
|
||||
//The workaround is to send an InventoryContentPacket instead
|
||||
//BDS (Bedrock Dedicated Server) also seems to work this way.
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
|
||||
}else{
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
|
||||
}
|
||||
|
||||
$itemStackWrapper = new ItemStackWrapper($itemStackInfo->getStackId(), $itemStack);
|
||||
if($windowId === ContainerIds::OFFHAND){
|
||||
//TODO: HACK!
|
||||
//The client may sometimes ignore the InventorySlotPacket for the offhand slot.
|
||||
//This can cause a lot of problems (totems, arrows, and more...).
|
||||
//The workaround is to send an InventoryContentPacket instead
|
||||
//BDS (Bedrock Dedicated Server) also seems to work this way.
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
|
||||
}else{
|
||||
if($windowId === ContainerIds::ARMOR){
|
||||
//TODO: HACK!
|
||||
//When right-clicking to equip armour, the client predicts the content of the armour slot, but
|
||||
//doesn't report it in the transaction packet. The server then sends an InventorySlotPacket to
|
||||
//the client, assuming the slot changed for some other reason, since there is no prediction for
|
||||
//the slot.
|
||||
//However, later requests involving that itemstack will refer to the request ID in which the
|
||||
//armour was equipped, instead of the stack ID provided by the server in the outgoing
|
||||
//InventorySlotPacket. (Perhaps because the item is already the same as the client actually
|
||||
//predicted, but didn't tell us?)
|
||||
//We work around this bug by setting the slot to air and then back to the correct item. In
|
||||
//theory, setting a different count and then back again (or changing any other property) would
|
||||
//also work, but this is simpler.
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, new ItemStackWrapper(0, ItemStack::null())));
|
||||
}
|
||||
unset($this->initiatedSlotChanges[$windowId][$netSlot]);
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
|
||||
}
|
||||
unset($entry->predictions[$slot], $entry->pendingSyncs[$slot]);
|
||||
}
|
||||
|
||||
public function syncContents(Inventory $inventory) : void{
|
||||
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
|
||||
if($slotMap !== null){
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
if($entry === null){
|
||||
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
|
||||
//is cleared before removal.
|
||||
return;
|
||||
}
|
||||
if($entry->complexSlotMap !== null){
|
||||
$windowId = ContainerIds::UI;
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory);
|
||||
}
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
if($windowId !== null){
|
||||
if($slotMap !== null){
|
||||
foreach($inventory->getContents(true) as $slotId => $item){
|
||||
$packetSlot = $slotMap->mapCoreToNet($slotId) ?? null;
|
||||
$entry->predictions = [];
|
||||
$entry->pendingSyncs = [];
|
||||
$contents = [];
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
foreach($inventory->getContents(true) as $slot => $item){
|
||||
$itemStack = $typeConverter->coreItemStackToNet($item);
|
||||
$info = $this->trackItemStack($entry, $slot, $itemStack, null);
|
||||
$contents[] = new ItemStackWrapper($info->getStackId(), $itemStack);
|
||||
}
|
||||
if($entry->complexSlotMap !== null){
|
||||
foreach($contents as $slotId => $info){
|
||||
$packetSlot = $entry->complexSlotMap->mapCoreToNet($slotId) ?? null;
|
||||
if($packetSlot === null){
|
||||
continue;
|
||||
}
|
||||
unset($this->initiatedSlotChanges[$windowId][$packetSlot]);
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create(
|
||||
$windowId,
|
||||
$packetSlot,
|
||||
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem($slotId)))
|
||||
$info
|
||||
));
|
||||
}
|
||||
}else{
|
||||
unset($this->initiatedSlotChanges[$windowId]);
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{
|
||||
return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack));
|
||||
}, $inventory->getContents(true))));
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function syncAll() : void{
|
||||
foreach($this->windowMap as $inventory){
|
||||
$this->syncContents($inventory);
|
||||
}
|
||||
foreach($this->complexWindows as $entry){
|
||||
$this->syncContents($entry->getInventory());
|
||||
foreach($this->inventories as $entry){
|
||||
$this->syncContents($entry->inventory);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncMismatchedPredictedSlotChanges() : void{
|
||||
foreach($this->initiatedSlotChanges as $windowId => $slots){
|
||||
foreach($slots as $netSlot => $expectedItem){
|
||||
$located = $this->locateWindowAndSlot($windowId, $netSlot);
|
||||
if($located === null){
|
||||
continue;
|
||||
}
|
||||
[$inventory, $slot] = $located;
|
||||
public function requestSyncAll() : void{
|
||||
$this->fullSyncRequested = true;
|
||||
}
|
||||
|
||||
if(!$inventory->slotExists($slot)){
|
||||
public function syncMismatchedPredictedSlotChanges() : void{
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
foreach($this->inventories as $entry){
|
||||
$inventory = $entry->inventory;
|
||||
foreach($entry->predictions as $slot => $expectedItem){
|
||||
if(!$inventory->slotExists($slot) || $entry->itemStackInfos[$slot] === null){
|
||||
continue; //TODO: size desync ???
|
||||
}
|
||||
$actualItem = $inventory->getItem($slot);
|
||||
if(!$actualItem->equalsExact($expectedItem)){
|
||||
$this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot");
|
||||
$this->syncSlot($inventory, $slot);
|
||||
|
||||
//any prediction that still exists at this point is a slot that was predicted to change but didn't
|
||||
$this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot");
|
||||
$entry->pendingSyncs[$slot] = $typeConverter->coreItemStackToNet($inventory->getItem($slot));
|
||||
}
|
||||
|
||||
$entry->predictions = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function flushPendingUpdates() : void{
|
||||
if($this->fullSyncRequested){
|
||||
$this->fullSyncRequested = false;
|
||||
$this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->inventories) . " inventories");
|
||||
$this->syncAll();
|
||||
}else{
|
||||
foreach($this->inventories as $entry){
|
||||
if(count($entry->pendingSyncs) === 0){
|
||||
continue;
|
||||
}
|
||||
$inventory = $entry->inventory;
|
||||
$this->session->getLogger()->debug("Syncing slots " . implode(", ", array_keys($entry->pendingSyncs)) . " in inventory " . get_class($inventory) . "#" . spl_object_id($inventory));
|
||||
foreach($entry->pendingSyncs as $slot => $itemStack){
|
||||
$this->syncSlot($inventory, $slot, $itemStack);
|
||||
}
|
||||
$entry->pendingSyncs = [];
|
||||
}
|
||||
}
|
||||
|
||||
$this->initiatedSlotChanges = [];
|
||||
}
|
||||
|
||||
public function syncData(Inventory $inventory, int $propertyId, int $value) : void{
|
||||
@ -453,11 +575,21 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function syncSelectedHotbarSlot() : void{
|
||||
$selected = $this->player->getInventory()->getHeldItemIndex();
|
||||
$playerInventory = $this->player->getInventory();
|
||||
$selected = $playerInventory->getHeldItemIndex();
|
||||
if($selected !== $this->clientSelectedHotbarSlot){
|
||||
$inventoryEntry = $this->inventories[spl_object_id($playerInventory)] ?? null;
|
||||
if($inventoryEntry === null){
|
||||
throw new AssumptionFailedError("Player inventory should always be tracked");
|
||||
}
|
||||
$itemStackInfo = $inventoryEntry->itemStackInfos[$selected] ?? null;
|
||||
if($itemStackInfo === null){
|
||||
throw new AssumptionFailedError("Untracked player inventory slot $selected");
|
||||
}
|
||||
|
||||
$this->session->sendDataPacket(MobEquipmentPacket::create(
|
||||
$this->player->getId(),
|
||||
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->player->getInventory()->getItemInHand())),
|
||||
new ItemStackWrapper($itemStackInfo->getStackId(), TypeConverter::getInstance()->coreItemStackToNet($playerInventory->getItemInHand())),
|
||||
$selected,
|
||||
$selected,
|
||||
ContainerIds::INVENTORY
|
||||
@ -469,9 +601,28 @@ class InventoryManager{
|
||||
public function syncCreative() : void{
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
|
||||
$nextEntryId = 1;
|
||||
$this->session->sendDataPacket(CreativeContentPacket::create(array_map(function(Item $item) use($typeConverter, &$nextEntryId) : CreativeContentEntry{
|
||||
return new CreativeContentEntry($nextEntryId++, $typeConverter->coreItemStackToNet($item));
|
||||
}, $this->player->isSpectator() ? [] : CreativeInventory::getInstance()->getAll())));
|
||||
$entries = [];
|
||||
if(!$this->player->isSpectator()){
|
||||
//creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent
|
||||
foreach(CreativeInventory::getInstance()->getAll() as $k => $item){
|
||||
$entries[] = new CreativeContentEntry($k, $typeConverter->coreItemStackToNet($item));
|
||||
}
|
||||
}
|
||||
$this->session->sendDataPacket(CreativeContentPacket::create($entries));
|
||||
}
|
||||
|
||||
private function newItemStackId() : int{
|
||||
return $this->nextItemStackId++;
|
||||
}
|
||||
|
||||
public function getItemStackInfo(Inventory $inventory, int $slot) : ?ItemStackInfo{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
return $entry?->itemStackInfos[$slot] ?? null;
|
||||
}
|
||||
|
||||
private function trackItemStack(InventoryManagerEntry $entry, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{
|
||||
//TODO: ItemStack->isNull() would be nice to have here
|
||||
$info = new ItemStackInfo($itemStackRequestId, $itemStack->getId() === 0 ? 0 : $this->newItemStackId());
|
||||
return $entry->itemStackInfos[$slotId] = $info;
|
||||
}
|
||||
}
|
||||
|
52
src/network/mcpe/InventoryManagerEntry.php
Normal file
52
src/network/mcpe/InventoryManagerEntry.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
|
||||
final class InventoryManagerEntry{
|
||||
/**
|
||||
* @var ItemStack[]
|
||||
* @phpstan-var array<int, ItemStack>
|
||||
*/
|
||||
public array $predictions = [];
|
||||
|
||||
/**
|
||||
* @var ItemStackInfo[]
|
||||
* @phpstan-var array<int, ItemStackInfo>
|
||||
*/
|
||||
public array $itemStackInfos = [];
|
||||
|
||||
/**
|
||||
* @var ItemStack[]
|
||||
* @phpstan-var array<int, ItemStack>
|
||||
*/
|
||||
public array $pendingSyncs = [];
|
||||
|
||||
public function __construct(
|
||||
public Inventory $inventory,
|
||||
public ?ComplexInventoryMapEntry $complexSlotMap = null
|
||||
){}
|
||||
}
|
36
src/network/mcpe/ItemStackInfo.php
Normal file
36
src/network/mcpe/ItemStackInfo.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
final class ItemStackInfo{
|
||||
|
||||
public function __construct(
|
||||
private ?int $requestId,
|
||||
private int $stackId
|
||||
){}
|
||||
|
||||
public function getRequestId() : ?int{ return $this->requestId; }
|
||||
|
||||
public function getStackId() : int{ return $this->stackId; }
|
||||
}
|
95
src/network/mcpe/NetworkBroadcastUtils.php
Normal file
95
src/network/mcpe/NetworkBroadcastUtils.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\timings\Timings;
|
||||
use function count;
|
||||
use function spl_object_id;
|
||||
|
||||
final class NetworkBroadcastUtils{
|
||||
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Player[] $recipients
|
||||
* @param ClientboundPacket[] $packets
|
||||
*/
|
||||
public static function broadcastPackets(array $recipients, array $packets) : bool{
|
||||
if(count($packets) === 0){
|
||||
throw new \InvalidArgumentException("Cannot broadcast empty list of packets");
|
||||
}
|
||||
|
||||
return Timings::$broadcastPackets->time(function() use ($recipients, $packets) : bool{
|
||||
/** @var NetworkSession[] $sessions */
|
||||
$sessions = [];
|
||||
foreach($recipients as $player){
|
||||
if($player->isConnected()){
|
||||
$sessions[] = $player->getNetworkSession();
|
||||
}
|
||||
}
|
||||
if(count($sessions) === 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var PacketBroadcaster[] $uniqueBroadcasters */
|
||||
$uniqueBroadcasters = [];
|
||||
/** @var NetworkSession[][] $broadcasterTargets */
|
||||
$broadcasterTargets = [];
|
||||
foreach($sessions as $recipient){
|
||||
$broadcaster = $recipient->getBroadcaster();
|
||||
$uniqueBroadcasters[spl_object_id($broadcaster)] = $broadcaster;
|
||||
$broadcasterTargets[spl_object_id($broadcaster)][spl_object_id($recipient)] = $recipient;
|
||||
}
|
||||
foreach($uniqueBroadcasters as $broadcaster){
|
||||
$broadcaster->broadcastPackets($broadcasterTargets[spl_object_id($broadcaster)], $packets);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Player[] $recipients
|
||||
* @phpstan-param \Closure(EntityEventBroadcaster, array<int, NetworkSession>) : void $callback
|
||||
*/
|
||||
public static function broadcastEntityEvent(array $recipients, \Closure $callback) : void{
|
||||
$uniqueBroadcasters = [];
|
||||
$broadcasterTargets = [];
|
||||
|
||||
foreach($recipients as $recipient){
|
||||
$session = $recipient->getNetworkSession();
|
||||
$broadcaster = $session->getEntityEventBroadcaster();
|
||||
$uniqueBroadcasters[spl_object_id($broadcaster)] = $broadcaster;
|
||||
$broadcasterTargets[spl_object_id($broadcaster)][spl_object_id($session)] = $session;
|
||||
}
|
||||
|
||||
foreach($uniqueBroadcasters as $k => $broadcaster){
|
||||
$callback($broadcaster, $broadcasterTargets[$k]);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,13 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\data\bedrock\EffectIdMap;
|
||||
use pocketmine\entity\Attribute;
|
||||
use pocketmine\entity\effect\EffectInstance;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\event\player\PlayerDuplicateLoginEvent;
|
||||
use pocketmine\event\server\DataPacketDecodeEvent;
|
||||
use pocketmine\event\server\DataPacketReceiveEvent;
|
||||
use pocketmine\event\server\DataPacketSendEvent;
|
||||
use pocketmine\form\Form;
|
||||
@ -43,7 +39,6 @@ use pocketmine\network\mcpe\cache\ChunkCache;
|
||||
use pocketmine\network\mcpe\compression\CompressBatchPromise;
|
||||
use pocketmine\network\mcpe\compression\Compressor;
|
||||
use pocketmine\network\mcpe\compression\DecompressionException;
|
||||
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
|
||||
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\encryption\DecryptionException;
|
||||
@ -62,31 +57,25 @@ use pocketmine\network\mcpe\protocol\AvailableCommandsPacket;
|
||||
use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\DisconnectPacket;
|
||||
use pocketmine\network\mcpe\protocol\EmotePacket;
|
||||
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobEffectPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\ModalFormRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
|
||||
use pocketmine\network\mcpe\protocol\NetworkChunkPublisherUpdatePacket;
|
||||
use pocketmine\network\mcpe\protocol\OpenSignPacket;
|
||||
use pocketmine\network\mcpe\protocol\Packet;
|
||||
use pocketmine\network\mcpe\protocol\PacketDecodeException;
|
||||
use pocketmine\network\mcpe\protocol\PacketPool;
|
||||
use pocketmine\network\mcpe\protocol\PlayerListPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
|
||||
use pocketmine\network\mcpe\protocol\RemoveActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
|
||||
use pocketmine\network\mcpe\protocol\ServerboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ServerToClientHandshakePacket;
|
||||
use pocketmine\network\mcpe\protocol\SetActorDataPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetDifficultyPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetPlayerGameTypePacket;
|
||||
use pocketmine\network\mcpe\protocol\SetSpawnPositionPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetTimePacket;
|
||||
use pocketmine\network\mcpe\protocol\SetTitlePacket;
|
||||
use pocketmine\network\mcpe\protocol\TakeItemActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\TextPacket;
|
||||
use pocketmine\network\mcpe\protocol\ToastRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\TransferPacket;
|
||||
@ -98,16 +87,10 @@ use pocketmine\network\mcpe\protocol\types\command\CommandEnum;
|
||||
use pocketmine\network\mcpe\protocol\types\command\CommandParameter;
|
||||
use pocketmine\network\mcpe\protocol\types\command\CommandPermissions;
|
||||
use pocketmine\network\mcpe\protocol\types\DimensionIds;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\Attribute as NetworkAttribute;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\MetadataProperty;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\PropertySyncData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerListEntry;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerPermissions;
|
||||
use pocketmine\network\mcpe\protocol\UpdateAbilitiesPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateAdventureSettingsPacket;
|
||||
use pocketmine\network\mcpe\protocol\UpdateAttributesPacket;
|
||||
use pocketmine\network\NetworkSessionManager;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
use pocketmine\permission\DefaultPermissionNames;
|
||||
@ -120,10 +103,10 @@ use pocketmine\player\XboxLivePlayerInfo;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\BinaryDataException;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\Position;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
@ -131,12 +114,8 @@ use function base64_encode;
|
||||
use function bin2hex;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function hrtime;
|
||||
use function in_array;
|
||||
use function intdiv;
|
||||
use function json_encode;
|
||||
use function ksort;
|
||||
use function min;
|
||||
use function strcasecmp;
|
||||
use function strlen;
|
||||
use function strtolower;
|
||||
@ -144,22 +123,16 @@ use function substr;
|
||||
use function time;
|
||||
use function ucfirst;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
class NetworkSession{
|
||||
private const INCOMING_PACKET_BATCH_PER_TICK = 2; //usually max 1 per tick, but transactions may arrive separately
|
||||
private const INCOMING_PACKET_BATCH_MAX_BUDGET = 100 * self::INCOMING_PACKET_BATCH_PER_TICK; //enough to account for a 5-second lag spike
|
||||
private const INCOMING_PACKET_BATCH_PER_TICK = 2; //usually max 1 per tick, but transactions arrive separately
|
||||
private const INCOMING_PACKET_BATCH_BUFFER_TICKS = 100; //enough to account for a 5-second lag spike
|
||||
|
||||
/**
|
||||
* At most this many more packets can be received. If this reaches zero, any additional packets received will cause
|
||||
* the player to be kicked from the server.
|
||||
* This number is increased every tick up to a maximum limit.
|
||||
*
|
||||
* @see self::INCOMING_PACKET_BATCH_PER_TICK
|
||||
* @see self::INCOMING_PACKET_BATCH_MAX_BUDGET
|
||||
*/
|
||||
private int $incomingPacketBatchBudget = self::INCOMING_PACKET_BATCH_MAX_BUDGET;
|
||||
private int $lastPacketBudgetUpdateTimeNs;
|
||||
private const INCOMING_GAME_PACKETS_PER_TICK = 2;
|
||||
private const INCOMING_GAME_PACKETS_BUFFER_TICKS = 100;
|
||||
|
||||
private PacketRateLimiter $packetBatchLimiter;
|
||||
private PacketRateLimiter $gamePacketLimiter;
|
||||
|
||||
private \PrefixedLogger $logger;
|
||||
private ?Player $player = null;
|
||||
@ -177,7 +150,7 @@ class NetworkSession{
|
||||
|
||||
private ?EncryptionContext $cipher = null;
|
||||
|
||||
/** @var ClientboundPacket[] */
|
||||
/** @var string[] */
|
||||
private array $sendBuffer = [];
|
||||
|
||||
/**
|
||||
@ -188,8 +161,6 @@ class NetworkSession{
|
||||
private bool $forceAsyncCompression = true;
|
||||
private bool $enableCompression = false; //disabled until handshake completed
|
||||
|
||||
private PacketSerializerContext $packetSerializerContext;
|
||||
|
||||
private ?InventoryManager $invManager = null;
|
||||
|
||||
/**
|
||||
@ -202,8 +173,10 @@ class NetworkSession{
|
||||
private Server $server,
|
||||
private NetworkSessionManager $manager,
|
||||
private PacketPool $packetPool,
|
||||
private PacketSerializerContext $packetSerializerContext,
|
||||
private PacketSender $sender,
|
||||
private PacketBroadcaster $broadcaster,
|
||||
private EntityEventBroadcaster $entityEventBroadcaster,
|
||||
private Compressor $compressor,
|
||||
private string $ip,
|
||||
private int $port
|
||||
@ -212,13 +185,11 @@ class NetworkSession{
|
||||
|
||||
$this->compressedQueue = new \SplQueue();
|
||||
|
||||
//TODO: allow this to be injected
|
||||
$this->packetSerializerContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary());
|
||||
|
||||
$this->disposeHooks = new ObjectSet();
|
||||
|
||||
$this->connectTime = time();
|
||||
$this->lastPacketBudgetUpdateTimeNs = hrtime(true);
|
||||
$this->packetBatchLimiter = new PacketRateLimiter("Packet Batches", self::INCOMING_PACKET_BATCH_PER_TICK, self::INCOMING_PACKET_BATCH_BUFFER_TICKS);
|
||||
$this->gamePacketLimiter = new PacketRateLimiter("Game Packets", self::INCOMING_GAME_PACKETS_PER_TICK, self::INCOMING_GAME_PACKETS_BUFFER_TICKS);
|
||||
|
||||
$this->setHandler(new SessionStartPacketHandler(
|
||||
$this->server,
|
||||
@ -264,23 +235,6 @@ class NetworkSession{
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ClientboundPacket[] $packets
|
||||
*/
|
||||
public static function encodePacketBatchTimed(BinaryStream $stream, PacketSerializerContext $context, array $packets) : void{
|
||||
PacketBatch::encodeRaw($stream, array_map(function(ClientboundPacket $packet) use ($context) : string{
|
||||
$timings = Timings::getEncodeDataPacketTimings($packet);
|
||||
$timings->startTiming();
|
||||
try{
|
||||
$stream = PacketSerializer::encoder($context);
|
||||
$packet->encode($stream);
|
||||
return $stream->getBuffer();
|
||||
}finally{
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}, $packets));
|
||||
}
|
||||
|
||||
private function onPlayerCreated(Player $player) : void{
|
||||
if(!$this->isConnected()){
|
||||
//the remote player might have disconnected before spawn terrain generation was finished
|
||||
@ -295,10 +249,10 @@ class NetworkSession{
|
||||
|
||||
$effectManager = $this->player->getEffects();
|
||||
$effectManager->getEffectAddHooks()->add($effectAddHook = function(EffectInstance $effect, bool $replacesOldEffect) : void{
|
||||
$this->onEntityEffectAdded($this->player, $effect, $replacesOldEffect);
|
||||
$this->entityEventBroadcaster->onEntityEffectAdded([$this], $this->player, $effect, $replacesOldEffect);
|
||||
});
|
||||
$effectManager->getEffectRemoveHooks()->add($effectRemoveHook = function(EffectInstance $effect) : void{
|
||||
$this->onEntityEffectRemoved($this->player, $effect);
|
||||
$this->entityEventBroadcaster->onEntityEffectRemoved([$this], $this->player, $effect);
|
||||
});
|
||||
$this->disposeHooks->add(static function() use ($effectManager, $effectAddHook, $effectRemoveHook) : void{
|
||||
$effectManager->getEffectAddHooks()->remove($effectAddHook);
|
||||
@ -378,13 +332,7 @@ class NetworkSession{
|
||||
|
||||
Timings::$playerNetworkReceive->startTiming();
|
||||
try{
|
||||
if($this->incomingPacketBatchBudget <= 0){
|
||||
$this->updatePacketBudget();
|
||||
if($this->incomingPacketBatchBudget <= 0){
|
||||
throw new PacketHandlingException("Receiving packets too fast");
|
||||
}
|
||||
}
|
||||
$this->incomingPacketBatchBudget--;
|
||||
$this->packetBatchLimiter->decrement();
|
||||
|
||||
if($this->cipher !== null){
|
||||
Timings::$playerNetworkReceiveDecrypt->startTiming();
|
||||
@ -416,7 +364,8 @@ class NetworkSession{
|
||||
$stream = new BinaryStream($decompressed);
|
||||
$count = 0;
|
||||
foreach(PacketBatch::decodeRaw($stream) as $buffer){
|
||||
if(++$count > 1300){
|
||||
$this->gamePacketLimiter->decrement();
|
||||
if(++$count > 100){
|
||||
throw new PacketHandlingException("Too many packets in batch");
|
||||
}
|
||||
$packet = $this->packetPool->getPacket($buffer);
|
||||
@ -431,7 +380,7 @@ class NetworkSession{
|
||||
throw PacketHandlingException::wrap($e, "Error processing " . $packet->getName());
|
||||
}
|
||||
}
|
||||
}catch(PacketDecodeException $e){
|
||||
}catch(PacketDecodeException|BinaryDataException $e){
|
||||
$this->logger->logException($e);
|
||||
throw PacketHandlingException::wrap($e, "Packet batch decode error");
|
||||
}
|
||||
@ -448,32 +397,45 @@ class NetworkSession{
|
||||
throw new PacketHandlingException("Unexpected non-serverbound packet");
|
||||
}
|
||||
|
||||
$timings = Timings::getDecodeDataPacketTimings($packet);
|
||||
$timings = Timings::getReceiveDataPacketTimings($packet);
|
||||
$timings->startTiming();
|
||||
try{
|
||||
$stream = PacketSerializer::decoder($buffer, 0, $this->packetSerializerContext);
|
||||
try{
|
||||
$packet->decode($stream);
|
||||
}catch(PacketDecodeException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(!$stream->feof()){
|
||||
$remains = substr($stream->getBuffer(), $stream->getOffset());
|
||||
$this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
|
||||
}
|
||||
}finally{
|
||||
$timings->stopTiming();
|
||||
}
|
||||
|
||||
$timings = Timings::getHandleDataPacketTimings($packet);
|
||||
$timings->startTiming();
|
||||
try{
|
||||
//TODO: I'm not sure DataPacketReceiveEvent should be included in the handler timings, but it needs to be
|
||||
//included for now to ensure the receivePacket timings are counted the way they were before
|
||||
$ev = new DataPacketDecodeEvent($this, $packet->pid(), $buffer);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return;
|
||||
}
|
||||
|
||||
$decodeTimings = Timings::getDecodeDataPacketTimings($packet);
|
||||
$decodeTimings->startTiming();
|
||||
try{
|
||||
$stream = PacketSerializer::decoder($buffer, 0, $this->packetSerializerContext);
|
||||
try{
|
||||
$packet->decode($stream);
|
||||
}catch(PacketDecodeException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(!$stream->feof()){
|
||||
$remains = substr($stream->getBuffer(), $stream->getOffset());
|
||||
$this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
|
||||
}
|
||||
}finally{
|
||||
$decodeTimings->stopTiming();
|
||||
}
|
||||
|
||||
$ev = new DataPacketReceiveEvent($this, $packet);
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled() && ($this->handler === null || !$packet->handle($this->handler))){
|
||||
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
|
||||
if(!$ev->isCancelled()){
|
||||
$handlerTimings = Timings::getHandleDataPacketTimings($packet);
|
||||
$handlerTimings->startTiming();
|
||||
try{
|
||||
if($this->handler === null || !$packet->handle($this->handler)){
|
||||
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
|
||||
}
|
||||
}finally{
|
||||
$handlerTimings->stopTiming();
|
||||
}
|
||||
}
|
||||
}finally{
|
||||
$timings->stopTiming();
|
||||
@ -498,7 +460,7 @@ class NetworkSession{
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->addToSendBuffer($packet);
|
||||
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder($this->packetSerializerContext), $packet));
|
||||
if($immediate){
|
||||
$this->flushSendBuffer(true);
|
||||
}
|
||||
@ -512,16 +474,24 @@ class NetworkSession{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function addToSendBuffer(ClientboundPacket $packet) : void{
|
||||
$timings = Timings::getSendDataPacketTimings($packet);
|
||||
public static function encodePacketTimed(PacketSerializer $serializer, ClientboundPacket $packet) : string{
|
||||
$timings = Timings::getEncodeDataPacketTimings($packet);
|
||||
$timings->startTiming();
|
||||
try{
|
||||
$this->sendBuffer[] = $packet;
|
||||
$packet->encode($serializer);
|
||||
return $serializer->getBuffer();
|
||||
}finally{
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function addToSendBuffer(string $buffer) : void{
|
||||
$this->sendBuffer[] = $buffer;
|
||||
}
|
||||
|
||||
private function flushSendBuffer(bool $immediate = false) : void{
|
||||
if(count($this->sendBuffer) > 0){
|
||||
Timings::$playerNetworkSend->startTiming();
|
||||
@ -534,10 +504,10 @@ class NetworkSession{
|
||||
}
|
||||
|
||||
$stream = new BinaryStream();
|
||||
self::encodePacketBatchTimed($stream, $this->packetSerializerContext, $this->sendBuffer);
|
||||
PacketBatch::encodeRaw($stream, $this->sendBuffer);
|
||||
|
||||
if($this->enableCompression){
|
||||
$promise = $this->server->prepareBatch(new PacketBatch($stream->getBuffer()), $this->compressor, $syncMode);
|
||||
$promise = $this->server->prepareBatch(new PacketBatch($stream->getBuffer()), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
|
||||
}else{
|
||||
$promise = new CompressBatchPromise();
|
||||
$promise->resolve($stream->getBuffer());
|
||||
@ -554,6 +524,8 @@ class NetworkSession{
|
||||
|
||||
public function getBroadcaster() : PacketBroadcaster{ return $this->broadcaster; }
|
||||
|
||||
public function getEntityEventBroadcaster() : EntityEventBroadcaster{ return $this->entityEventBroadcaster; }
|
||||
|
||||
public function getCompressor() : Compressor{
|
||||
return $this->compressor;
|
||||
}
|
||||
@ -578,20 +550,25 @@ class NetworkSession{
|
||||
$this->compressedQueue->enqueue($payload);
|
||||
$payload->onResolve(function(CompressBatchPromise $payload) : void{
|
||||
if($this->connected && $this->compressedQueue->bottom() === $payload){
|
||||
$this->compressedQueue->dequeue(); //result unused
|
||||
$this->sendEncoded($payload->getResult());
|
||||
Timings::$playerNetworkSend->startTiming();
|
||||
try{
|
||||
$this->compressedQueue->dequeue(); //result unused
|
||||
$this->sendEncoded($payload->getResult());
|
||||
|
||||
while(!$this->compressedQueue->isEmpty()){
|
||||
/** @var CompressBatchPromise $current */
|
||||
$current = $this->compressedQueue->bottom();
|
||||
if($current->hasResult()){
|
||||
$this->compressedQueue->dequeue();
|
||||
while(!$this->compressedQueue->isEmpty()){
|
||||
/** @var CompressBatchPromise $current */
|
||||
$current = $this->compressedQueue->bottom();
|
||||
if($current->hasResult()){
|
||||
$this->compressedQueue->dequeue();
|
||||
|
||||
$this->sendEncoded($current->getResult());
|
||||
}else{
|
||||
//can't send any more queued until this one is ready
|
||||
break;
|
||||
$this->sendEncoded($current->getResult());
|
||||
}else{
|
||||
//can't send any more queued until this one is ready
|
||||
break;
|
||||
}
|
||||
}
|
||||
}finally{
|
||||
Timings::$playerNetworkSend->stopTiming();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -824,7 +801,7 @@ class NetworkSession{
|
||||
}
|
||||
|
||||
public function onServerRespawn() : void{
|
||||
$this->syncAttributes($this->player, $this->player->getAttributeMap()->getAll());
|
||||
$this->entityEventBroadcaster->syncAttributes([$this], $this->player, $this->player->getAttributeMap()->getAll());
|
||||
$this->player->sendData(null);
|
||||
|
||||
$this->syncAbilities($this->player);
|
||||
@ -906,16 +883,29 @@ class NetworkSession{
|
||||
AbilitiesLayer::ABILITY_OPEN_CONTAINERS => !$for->isSpectator(),
|
||||
AbilitiesLayer::ABILITY_ATTACK_PLAYERS => !$for->isSpectator(),
|
||||
AbilitiesLayer::ABILITY_ATTACK_MOBS => !$for->isSpectator(),
|
||||
AbilitiesLayer::ABILITY_PRIVILEGED_BUILDER => false,
|
||||
];
|
||||
|
||||
$layers = [
|
||||
//TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!!
|
||||
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
|
||||
];
|
||||
if(!$for->hasBlockCollision()){
|
||||
//TODO: HACK! In 1.19.80, the client starts falling in our faux spectator mode when it clips into a
|
||||
//block. We can't seem to prevent this short of forcing the player to always fly when block collision is
|
||||
//disabled. Also, for some reason the client always reads flight state from this layer if present, even
|
||||
//though the player isn't in spectator mode.
|
||||
|
||||
$layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [
|
||||
AbilitiesLayer::ABILITY_FLYING => true,
|
||||
], null, null);
|
||||
}
|
||||
|
||||
$this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData(
|
||||
$isOp ? CommandPermissions::OPERATOR : CommandPermissions::NORMAL,
|
||||
$isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
|
||||
$for->getId(),
|
||||
[
|
||||
//TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!!
|
||||
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
|
||||
]
|
||||
$layers
|
||||
)));
|
||||
}
|
||||
|
||||
@ -933,41 +923,6 @@ class NetworkSession{
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Attribute[] $attributes
|
||||
*/
|
||||
public function syncAttributes(Living $entity, array $attributes) : void{
|
||||
if(count($attributes) > 0){
|
||||
$this->sendDataPacket(UpdateAttributesPacket::create($entity->getId(), array_map(function(Attribute $attr) : NetworkAttribute{
|
||||
return new NetworkAttribute($attr->getId(), $attr->getMinValue(), $attr->getMaxValue(), $attr->getValue(), $attr->getDefaultValue(), []);
|
||||
}, $attributes), 0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MetadataProperty[] $properties
|
||||
* @phpstan-param array<int, MetadataProperty> $properties
|
||||
*/
|
||||
public function syncActorData(Entity $entity, array $properties) : void{
|
||||
//TODO: HACK! as of 1.18.10, the client responds differently to the same data ordered in different orders - for
|
||||
//example, sending HEIGHT in the list before FLAGS when unsetting the SWIMMING flag results in a hitbox glitch
|
||||
ksort($properties, SORT_NUMERIC);
|
||||
$this->sendDataPacket(SetActorDataPacket::create($entity->getId(), $properties, new PropertySyncData([], []), 0));
|
||||
}
|
||||
|
||||
public function onEntityEffectAdded(Living $entity, EffectInstance $effect, bool $replacesOldEffect) : void{
|
||||
//TODO: we may need yet another effect <=> ID map in the future depending on protocol changes
|
||||
$this->sendDataPacket(MobEffectPacket::add($entity->getId(), $replacesOldEffect, EffectIdMap::getInstance()->toId($effect->getType()), $effect->getAmplifier(), $effect->isVisible(), $effect->getDuration()));
|
||||
}
|
||||
|
||||
public function onEntityEffectRemoved(Living $entity, EffectInstance $effect) : void{
|
||||
$this->sendDataPacket(MobEffectPacket::remove($entity->getId(), EffectIdMap::getInstance()->toId($effect->getType())));
|
||||
}
|
||||
|
||||
public function onEntityRemoved(Entity $entity) : void{
|
||||
$this->sendDataPacket(RemoveActorPacket::create($entity->getId()));
|
||||
}
|
||||
|
||||
public function syncAvailableCommands() : void{
|
||||
$commandData = [];
|
||||
foreach($this->server->getCommandMap()->getCommands() as $name => $command){
|
||||
@ -1044,8 +999,6 @@ class NetworkSession{
|
||||
* @phpstan-param \Closure() : void $onCompletion
|
||||
*/
|
||||
public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{
|
||||
Utils::validateCallableSignature(function() : void{}, $onCompletion);
|
||||
|
||||
$world = $this->player->getLocation()->getWorld();
|
||||
ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ)->onResolve(
|
||||
|
||||
@ -1103,36 +1056,6 @@ class NetworkSession{
|
||||
return $this->invManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: expand this to more than just humans
|
||||
*/
|
||||
public function onMobMainHandItemChange(Human $mob) : void{
|
||||
//TODO: we could send zero for slot here because remote players don't need to know which slot was selected
|
||||
$inv = $mob->getInventory();
|
||||
$this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItemInHand())), $inv->getHeldItemIndex(), $inv->getHeldItemIndex(), ContainerIds::INVENTORY));
|
||||
}
|
||||
|
||||
public function onMobOffHandItemChange(Human $mob) : void{
|
||||
$inv = $mob->getOffHandInventory();
|
||||
$this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItem(0))), 0, 0, ContainerIds::OFFHAND));
|
||||
}
|
||||
|
||||
public function onMobArmorChange(Living $mob) : void{
|
||||
$inv = $mob->getArmorInventory();
|
||||
$converter = TypeConverter::getInstance();
|
||||
$this->sendDataPacket(MobArmorEquipmentPacket::create(
|
||||
$mob->getId(),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getHelmet())),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getChestplate())),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getLeggings())),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getBoots()))
|
||||
));
|
||||
}
|
||||
|
||||
public function onPlayerPickUpItem(Player $collector, Entity $pickedUp) : void{
|
||||
$this->sendDataPacket(TakeItemActorPacket::create($collector->getId(), $pickedUp->getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Player[] $players
|
||||
*/
|
||||
@ -1176,29 +1099,12 @@ class NetworkSession{
|
||||
$this->sendDataPacket(SetTitlePacket::setAnimationTimes($fadeIn, $stay, $fadeOut));
|
||||
}
|
||||
|
||||
public function onEmote(Human $from, string $emoteId) : void{
|
||||
$this->sendDataPacket(EmotePacket::create($from->getId(), $emoteId, EmotePacket::FLAG_SERVER));
|
||||
}
|
||||
|
||||
public function onToastNotification(string $title, string $body) : void{
|
||||
$this->sendDataPacket(ToastRequestPacket::create($title, $body));
|
||||
}
|
||||
|
||||
private function updatePacketBudget() : void{
|
||||
$nowNs = hrtime(true);
|
||||
$timeSinceLastUpdateNs = $nowNs - $this->lastPacketBudgetUpdateTimeNs;
|
||||
if($timeSinceLastUpdateNs > 50_000_000){
|
||||
$ticksSinceLastUpdate = intdiv($timeSinceLastUpdateNs, 50_000_000);
|
||||
/*
|
||||
* If the server takes an abnormally long time to process a tick, add the budget for time difference to
|
||||
* compensate. This extra budget may be very large, but it will disappear the next time a normal update
|
||||
* occurs. This ensures that backlogs during a large lag spike don't cause everyone to get kicked.
|
||||
* As long as all the backlogged packets are processed before the next tick, everything should be OK for
|
||||
* clients behaving normally.
|
||||
*/
|
||||
$this->incomingPacketBatchBudget = min($this->incomingPacketBatchBudget, self::INCOMING_PACKET_BATCH_MAX_BUDGET) + (self::INCOMING_PACKET_BATCH_PER_TICK * 2 * $ticksSinceLastUpdate);
|
||||
$this->lastPacketBudgetUpdateTimeNs = $nowNs;
|
||||
}
|
||||
public function onOpenSignEditor(Vector3 $signPosition, bool $frontSide) : void{
|
||||
$this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide));
|
||||
}
|
||||
|
||||
public function tick() : void{
|
||||
@ -1219,13 +1125,19 @@ class NetworkSession{
|
||||
$this->player->doChunkRequests();
|
||||
|
||||
$dirtyAttributes = $this->player->getAttributeMap()->needSend();
|
||||
$this->syncAttributes($this->player, $dirtyAttributes);
|
||||
$this->entityEventBroadcaster->syncAttributes([$this], $this->player, $dirtyAttributes);
|
||||
foreach($dirtyAttributes as $attribute){
|
||||
//TODO: we might need to send these to other players in the future
|
||||
//if that happens, this will need to become more complex than a flag on the attribute itself
|
||||
$attribute->markSynchronized();
|
||||
}
|
||||
}
|
||||
Timings::$playerNetworkSendInventorySync->startTiming();
|
||||
try{
|
||||
$this->invManager?->flushPendingUpdates();
|
||||
}finally{
|
||||
Timings::$playerNetworkSendInventorySync->stopTiming();
|
||||
}
|
||||
|
||||
$this->flushSendBuffer();
|
||||
}
|
||||
|
81
src/network/mcpe/PacketRateLimiter.php
Normal file
81
src/network/mcpe/PacketRateLimiter.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
use function hrtime;
|
||||
use function intdiv;
|
||||
use function min;
|
||||
|
||||
final class PacketRateLimiter{
|
||||
/**
|
||||
* At most this many more packets can be received. If this reaches zero, any additional packets received will cause
|
||||
* the player to be kicked from the server.
|
||||
* This number is increased every tick up to a maximum limit, and decreased by one every time a packet is received.
|
||||
*/
|
||||
private int $budget;
|
||||
private int $lastUpdateTimeNs;
|
||||
private int $maxBudget;
|
||||
|
||||
public function __construct(
|
||||
private string $name,
|
||||
private int $averagePerTick,
|
||||
int $maxBufferTicks,
|
||||
private int $updateFrequencyNs = 50_000_000,
|
||||
){
|
||||
$this->maxBudget = $this->averagePerTick * $maxBufferTicks;
|
||||
$this->budget = $this->maxBudget;
|
||||
$this->lastUpdateTimeNs = hrtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PacketHandlingException if the rate limit has been exceeded
|
||||
*/
|
||||
public function decrement(int $amount = 1) : void{
|
||||
if($this->budget <= 0){
|
||||
$this->update();
|
||||
if($this->budget <= 0){
|
||||
throw new PacketHandlingException("Exceeded rate limit for \"$this->name\"");
|
||||
}
|
||||
}
|
||||
$this->budget -= $amount;
|
||||
}
|
||||
|
||||
public function update() : void{
|
||||
$nowNs = hrtime(true);
|
||||
$timeSinceLastUpdateNs = $nowNs - $this->lastUpdateTimeNs;
|
||||
if($timeSinceLastUpdateNs > $this->updateFrequencyNs){
|
||||
$ticksSinceLastUpdate = intdiv($timeSinceLastUpdateNs, $this->updateFrequencyNs);
|
||||
/*
|
||||
* If the server takes an abnormally long time to process a tick, add the budget for time difference to
|
||||
* compensate. This extra budget may be very large, but it will disappear the next time a normal update
|
||||
* occurs. This ensures that backlogs during a large lag spike don't cause everyone to get kicked.
|
||||
* As long as all the backlogged packets are processed before the next tick, everything should be OK for
|
||||
* clients behaving normally.
|
||||
*/
|
||||
$this->budget = min($this->budget, $this->maxBudget) + ($this->averagePerTick * 2 * $ticksSinceLastUpdate);
|
||||
$this->lastUpdateTimeNs = $nowNs;
|
||||
}
|
||||
}
|
||||
}
|
143
src/network/mcpe/StandardEntityEventBroadcaster.php
Normal file
143
src/network/mcpe/StandardEntityEventBroadcaster.php
Normal file
@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\data\bedrock\EffectIdMap;
|
||||
use pocketmine\entity\Attribute;
|
||||
use pocketmine\entity\effect\EffectInstance;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\EmotePacket;
|
||||
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobEffectPacket;
|
||||
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\RemoveActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetActorDataPacket;
|
||||
use pocketmine\network\mcpe\protocol\TakeItemActorPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\Attribute as NetworkAttribute;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\PropertySyncData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use pocketmine\network\mcpe\protocol\UpdateAttributesPacket;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function ksort;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
|
||||
|
||||
public function __construct(
|
||||
private StandardPacketBroadcaster $broadcaster
|
||||
){}
|
||||
|
||||
/**
|
||||
* @param NetworkSession[] $recipients
|
||||
*/
|
||||
private function sendDataPacket(array $recipients, ClientboundPacket $packet) : void{
|
||||
$this->broadcaster->broadcastPackets($recipients, [$packet]);
|
||||
}
|
||||
|
||||
public function syncAttributes(array $recipients, Living $entity, array $attributes) : void{
|
||||
if(count($attributes) > 0){
|
||||
$this->sendDataPacket($recipients, UpdateAttributesPacket::create(
|
||||
$entity->getId(),
|
||||
array_map(fn(Attribute $attr) => new NetworkAttribute($attr->getId(), $attr->getMinValue(), $attr->getMaxValue(), $attr->getValue(), $attr->getDefaultValue(), []), $attributes),
|
||||
0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function syncActorData(array $recipients, Entity $entity, array $properties) : void{
|
||||
//TODO: HACK! as of 1.18.10, the client responds differently to the same data ordered in different orders - for
|
||||
//example, sending HEIGHT in the list before FLAGS when unsetting the SWIMMING flag results in a hitbox glitch
|
||||
ksort($properties, SORT_NUMERIC);
|
||||
$this->sendDataPacket($recipients, SetActorDataPacket::create($entity->getId(), $properties, new PropertySyncData([], []), 0));
|
||||
}
|
||||
|
||||
public function onEntityEffectAdded(array $recipients, Living $entity, EffectInstance $effect, bool $replacesOldEffect) : void{
|
||||
//TODO: we may need yet another effect <=> ID map in the future depending on protocol changes
|
||||
$this->sendDataPacket($recipients, MobEffectPacket::add(
|
||||
$entity->getId(),
|
||||
$replacesOldEffect,
|
||||
EffectIdMap::getInstance()->toId($effect->getType()),
|
||||
$effect->getAmplifier(),
|
||||
$effect->isVisible(),
|
||||
$effect->getDuration()
|
||||
));
|
||||
}
|
||||
|
||||
public function onEntityEffectRemoved(array $recipients, Living $entity, EffectInstance $effect) : void{
|
||||
$this->sendDataPacket($recipients, MobEffectPacket::remove($entity->getId(), EffectIdMap::getInstance()->toId($effect->getType())));
|
||||
}
|
||||
|
||||
public function onEntityRemoved(array $recipients, Entity $entity) : void{
|
||||
$this->sendDataPacket($recipients, RemoveActorPacket::create($entity->getId()));
|
||||
}
|
||||
|
||||
public function onMobMainHandItemChange(array $recipients, Human $mob) : void{
|
||||
//TODO: we could send zero for slot here because remote players don't need to know which slot was selected
|
||||
$inv = $mob->getInventory();
|
||||
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
|
||||
$mob->getId(),
|
||||
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItemInHand())),
|
||||
$inv->getHeldItemIndex(),
|
||||
$inv->getHeldItemIndex(),
|
||||
ContainerIds::INVENTORY
|
||||
));
|
||||
}
|
||||
|
||||
public function onMobOffHandItemChange(array $recipients, Human $mob) : void{
|
||||
$inv = $mob->getOffHandInventory();
|
||||
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
|
||||
$mob->getId(),
|
||||
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItem(0))),
|
||||
0,
|
||||
0,
|
||||
ContainerIds::OFFHAND
|
||||
));
|
||||
}
|
||||
|
||||
public function onMobArmorChange(array $recipients, Living $mob) : void{
|
||||
$inv = $mob->getArmorInventory();
|
||||
$converter = TypeConverter::getInstance();
|
||||
$this->sendDataPacket($recipients, MobArmorEquipmentPacket::create(
|
||||
$mob->getId(),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getHelmet())),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getChestplate())),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getLeggings())),
|
||||
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getBoots()))
|
||||
));
|
||||
}
|
||||
|
||||
public function onPickUpItem(array $recipients, Entity $collector, Entity $pickedUp) : void{
|
||||
$this->sendDataPacket($recipients, TakeItemActorPacket::create($collector->getId(), $pickedUp->getId()));
|
||||
}
|
||||
|
||||
public function onEmote(array $recipients, Human $from, string $emoteId) : void{
|
||||
$this->sendDataPacket($recipients, EmotePacket::create($from->getId(), $emoteId, "", "", EmotePacket::FLAG_SERVER));
|
||||
}
|
||||
}
|
@ -23,48 +23,77 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\event\server\DataPacketSendEvent;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use function count;
|
||||
use function log;
|
||||
use function spl_object_id;
|
||||
use function strlen;
|
||||
|
||||
final class StandardPacketBroadcaster implements PacketBroadcaster{
|
||||
public function __construct(private Server $server){}
|
||||
public function __construct(
|
||||
private Server $server,
|
||||
private PacketSerializerContext $protocolContext
|
||||
){}
|
||||
|
||||
public function broadcastPackets(array $recipients, array $packets) : void{
|
||||
$buffers = [];
|
||||
//TODO: this shouldn't really be called here, since the broadcaster might be replaced by an alternative
|
||||
//implementation that doesn't fire events
|
||||
$ev = new DataPacketSendEvent($recipients, $packets);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return;
|
||||
}
|
||||
$packets = $ev->getPackets();
|
||||
|
||||
$compressors = [];
|
||||
$targetMap = [];
|
||||
|
||||
/** @var NetworkSession[][] $targetsByCompressor */
|
||||
$targetsByCompressor = [];
|
||||
foreach($recipients as $recipient){
|
||||
$serializerContext = $recipient->getPacketSerializerContext();
|
||||
$bufferId = spl_object_id($serializerContext);
|
||||
if(!isset($buffers[$bufferId])){
|
||||
$stream = new BinaryStream();
|
||||
NetworkSession::encodePacketBatchTimed($stream, $serializerContext, $packets);
|
||||
$buffers[$bufferId] = $stream->getBuffer();
|
||||
if($recipient->getPacketSerializerContext() !== $this->protocolContext){
|
||||
throw new \InvalidArgumentException("Only recipients with the same protocol context as the broadcaster can be broadcast to by this broadcaster");
|
||||
}
|
||||
|
||||
//TODO: different compressors might be compatible, it might not be necessary to split them up by object
|
||||
$compressor = $recipient->getCompressor();
|
||||
$compressors[spl_object_id($compressor)] = $compressor;
|
||||
|
||||
$targetMap[$bufferId][spl_object_id($compressor)][] = $recipient;
|
||||
$targetsByCompressor[spl_object_id($compressor)][] = $recipient;
|
||||
}
|
||||
|
||||
foreach($targetMap as $bufferId => $compressorMap){
|
||||
$buffer = $buffers[$bufferId];
|
||||
foreach($compressorMap as $compressorId => $compressorTargets){
|
||||
$compressor = $compressors[$compressorId];
|
||||
if(!$compressor->willCompress($buffer)){
|
||||
foreach($compressorTargets as $target){
|
||||
foreach($packets as $pk){
|
||||
$target->addToSendBuffer($pk);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
$promise = $this->server->prepareBatch(new PacketBatch($buffer), $compressor);
|
||||
foreach($compressorTargets as $target){
|
||||
$target->queueCompressed($promise);
|
||||
$totalLength = 0;
|
||||
$packetBuffers = [];
|
||||
foreach($packets as $packet){
|
||||
$buffer = NetworkSession::encodePacketTimed(PacketSerializer::encoder($this->protocolContext), $packet);
|
||||
//varint length prefix + packet buffer
|
||||
$totalLength += (((int) log(strlen($buffer), 128)) + 1) + strlen($buffer);
|
||||
$packetBuffers[] = $buffer;
|
||||
}
|
||||
|
||||
foreach($targetsByCompressor as $compressorId => $compressorTargets){
|
||||
$compressor = $compressors[$compressorId];
|
||||
|
||||
$threshold = $compressor->getCompressionThreshold();
|
||||
if(count($compressorTargets) > 1 && $threshold !== null && $totalLength >= $threshold){
|
||||
//do not prepare shared batch unless we're sure it will be compressed
|
||||
$stream = new BinaryStream();
|
||||
PacketBatch::encodeRaw($stream, $packetBuffers);
|
||||
$batchBuffer = $stream->getBuffer();
|
||||
|
||||
$promise = $this->server->prepareBatch(new PacketBatch($batchBuffer), $compressor, timings: Timings::$playerNetworkSendCompressBroadcast);
|
||||
foreach($compressorTargets as $target){
|
||||
$target->queueCompressed($promise);
|
||||
}
|
||||
}else{
|
||||
foreach($compressorTargets as $target){
|
||||
foreach($packetBuffers as $packetBuffer){
|
||||
$target->addToSendBuffer($packetBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,10 @@ use function time;
|
||||
class ProcessLoginTask extends AsyncTask{
|
||||
private const TLS_KEY_ON_COMPLETION = "completion";
|
||||
|
||||
public const MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V";
|
||||
public const MOJANG_OLD_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V";
|
||||
public const MOJANG_OLD_KEY_EXPIRY = 1688169600; //2023-07-01 00:00:00 UTC - there is no official date for the changeover to the new key, so this is a guess
|
||||
|
||||
public const MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
|
||||
|
||||
private const CLOCK_DRIFT_MAX = 60;
|
||||
|
||||
@ -151,7 +154,7 @@ class ProcessLoginTask extends AsyncTask{
|
||||
throw new VerifyLoginException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
|
||||
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY || (time() < self::MOJANG_OLD_KEY_EXPIRY && $headers->x5u === self::MOJANG_OLD_ROOT_PUBLIC_KEY)){
|
||||
$this->authenticated = true; //we're signed into xbox live
|
||||
}
|
||||
|
||||
|
23
src/network/mcpe/cache/CraftingDataCache.php
vendored
23
src/network/mcpe/cache/CraftingDataCache.php
vendored
@ -25,6 +25,8 @@ namespace pocketmine\network\mcpe\cache;
|
||||
|
||||
use pocketmine\crafting\CraftingManager;
|
||||
use pocketmine\crafting\FurnaceType;
|
||||
use pocketmine\crafting\ShapedRecipe;
|
||||
use pocketmine\crafting\ShapelessRecipe;
|
||||
use pocketmine\crafting\ShapelessRecipeType;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\convert\ItemTranslator;
|
||||
@ -76,12 +78,12 @@ final class CraftingDataCache{
|
||||
private function buildCraftingDataCache(CraftingManager $manager) : CraftingDataPacket{
|
||||
Timings::$craftingDataCacheRebuild->startTiming();
|
||||
|
||||
$counter = 0;
|
||||
$nullUUID = Uuid::fromString(Uuid::NIL);
|
||||
$converter = TypeConverter::getInstance();
|
||||
$recipesWithTypeIds = [];
|
||||
foreach($manager->getShapelessRecipes() as $list){
|
||||
foreach($list as $recipe){
|
||||
|
||||
foreach($manager->getCraftingRecipeIndex() as $index => $recipe){
|
||||
if($recipe instanceof ShapelessRecipe){
|
||||
$typeTag = match($recipe->getType()->id()){
|
||||
ShapelessRecipeType::CRAFTING()->id() => CraftingRecipeBlockName::CRAFTING_TABLE,
|
||||
ShapelessRecipeType::STONECUTTER()->id() => CraftingRecipeBlockName::STONECUTTER,
|
||||
@ -89,7 +91,7 @@ final class CraftingDataCache{
|
||||
};
|
||||
$recipesWithTypeIds[] = new ProtocolShapelessRecipe(
|
||||
CraftingDataPacket::ENTRY_SHAPELESS,
|
||||
Binary::writeInt(++$counter),
|
||||
Binary::writeInt($index),
|
||||
array_map(function(Item $item) use ($converter) : RecipeIngredient{
|
||||
return $converter->coreItemStackToRecipeIngredient($item);
|
||||
}, $recipe->getIngredientList()),
|
||||
@ -99,12 +101,9 @@ final class CraftingDataCache{
|
||||
$nullUUID,
|
||||
$typeTag,
|
||||
50,
|
||||
$counter
|
||||
$index
|
||||
);
|
||||
}
|
||||
}
|
||||
foreach($manager->getShapedRecipes() as $list){
|
||||
foreach($list as $recipe){
|
||||
}elseif($recipe instanceof ShapedRecipe){
|
||||
$inputs = [];
|
||||
|
||||
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
|
||||
@ -114,7 +113,7 @@ final class CraftingDataCache{
|
||||
}
|
||||
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
|
||||
CraftingDataPacket::ENTRY_SHAPED,
|
||||
Binary::writeInt(++$counter),
|
||||
Binary::writeInt($index),
|
||||
$inputs,
|
||||
array_map(function(Item $item) use ($converter) : ItemStack{
|
||||
return $converter->coreItemStackToNet($item);
|
||||
@ -122,8 +121,10 @@ final class CraftingDataCache{
|
||||
$nullUUID,
|
||||
CraftingRecipeBlockName::CRAFTING_TABLE,
|
||||
50,
|
||||
$counter
|
||||
$index
|
||||
);
|
||||
}else{
|
||||
//TODO: probably special recipe types
|
||||
}
|
||||
}
|
||||
|
||||
|
6
src/network/mcpe/cache/StaticPacketCache.php
vendored
6
src/network/mcpe/cache/StaticPacketCache.php
vendored
@ -23,13 +23,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\cache;
|
||||
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket;
|
||||
use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket;
|
||||
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
|
||||
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
class StaticPacketCache{
|
||||
use SingletonTrait;
|
||||
@ -43,8 +43,8 @@ class StaticPacketCache{
|
||||
|
||||
private static function make() : self{
|
||||
return new self(
|
||||
BiomeDefinitionListPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'biome_definitions.nbt'))),
|
||||
AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'entity_identifiers.nbt')))
|
||||
BiomeDefinitionListPacket::create(self::loadCompoundFromFile(BedrockDataFiles::BIOME_DEFINITIONS_NBT)),
|
||||
AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(BedrockDataFiles::ENTITY_IDENTIFIERS_NBT))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\compression;
|
||||
|
||||
use pocketmine\utils\Utils;
|
||||
use function array_push;
|
||||
|
||||
class CompressBatchPromise{
|
||||
@ -42,9 +41,6 @@ class CompressBatchPromise{
|
||||
*/
|
||||
public function onResolve(\Closure ...$callbacks) : void{
|
||||
$this->checkCancelled();
|
||||
foreach($callbacks as $callback){
|
||||
Utils::validateCallableSignature(function(CompressBatchPromise $promise) : void{}, $callback);
|
||||
}
|
||||
if($this->result !== null){
|
||||
foreach($callbacks as $callback){
|
||||
$callback($this);
|
||||
|
@ -24,13 +24,20 @@ declare(strict_types=1);
|
||||
namespace pocketmine\network\mcpe\compression;
|
||||
|
||||
interface Compressor{
|
||||
|
||||
public function willCompress(string $data) : bool;
|
||||
|
||||
/**
|
||||
* @throws DecompressionException
|
||||
*/
|
||||
public function decompress(string $payload) : string;
|
||||
|
||||
public function compress(string $payload) : string;
|
||||
|
||||
/**
|
||||
* Returns the minimum size of packet batch that the compressor will attempt to compress.
|
||||
*
|
||||
* The compressor's output **MUST** still be valid input for the decompressor even if the compressor input is
|
||||
* below this threshold.
|
||||
* However, it may choose to use a cheaper compression option (e.g. zlib level 0, which simply wraps the data and
|
||||
* doesn't attempt to compress it) to avoid wasting CPU time.
|
||||
*/
|
||||
public function getCompressionThreshold() : ?int;
|
||||
}
|
||||
|
@ -48,12 +48,12 @@ final class ZlibCompressor implements Compressor{
|
||||
|
||||
public function __construct(
|
||||
private int $level,
|
||||
private int $minCompressionSize,
|
||||
private ?int $minCompressionSize,
|
||||
private int $maxDecompressionSize
|
||||
){}
|
||||
|
||||
public function willCompress(string $data) : bool{
|
||||
return $this->minCompressionSize > -1 && strlen($data) >= $this->minCompressionSize;
|
||||
public function getCompressionThreshold() : ?int{
|
||||
return $this->minCompressionSize;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,11 +72,12 @@ final class ZlibCompressor implements Compressor{
|
||||
}
|
||||
|
||||
public function compress(string $payload) : string{
|
||||
$compressible = $this->minCompressionSize !== null && strlen($payload) >= $this->minCompressionSize;
|
||||
if(function_exists('libdeflate_deflate_compress')){
|
||||
return $this->willCompress($payload) ?
|
||||
return $compressible ?
|
||||
libdeflate_deflate_compress($payload, $this->level) :
|
||||
self::zlib_encode($payload, 0);
|
||||
}
|
||||
return self::zlib_encode($payload, $this->willCompress($payload) ? $this->level : 0);
|
||||
return self::zlib_encode($payload, $compressible ? $this->level : 0);
|
||||
}
|
||||
}
|
||||
|
@ -23,12 +23,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
|
||||
use pocketmine\network\mcpe\protocol\types\ItemTypeEntry;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_int;
|
||||
@ -39,7 +39,7 @@ final class GlobalItemTypeDictionary{
|
||||
use SingletonTrait;
|
||||
|
||||
private static function make() : self{
|
||||
$data = Filesystem::fileGetContents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'required_item_list.json'));
|
||||
$data = Filesystem::fileGetContents(BedrockDataFiles::REQUIRED_ITEM_LIST_JSON);
|
||||
$table = json_decode($data, true);
|
||||
if(!is_array($table)){
|
||||
throw new AssumptionFailedError("Invalid item list format");
|
||||
|
@ -23,13 +23,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\data\bedrock\LegacyItemIdToStringIdMap;
|
||||
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use pocketmine\utils\Utils;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use function is_numeric;
|
||||
@ -67,7 +67,7 @@ final class ItemTranslator{
|
||||
private array $complexNetToCoreMapping = [];
|
||||
|
||||
private static function make() : self{
|
||||
$data = Filesystem::fileGetContents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'r16_to_current_item_map.json'));
|
||||
$data = Filesystem::fileGetContents(BedrockDataFiles::R16_TO_CURRENT_ITEM_MAP_JSON);
|
||||
$json = json_decode($data, true);
|
||||
if(!is_array($json) || !isset($json["simple"], $json["complex"]) || !is_array($json["simple"]) || !is_array($json["complex"])){
|
||||
throw new AssumptionFailedError("Invalid item table format");
|
||||
@ -110,6 +110,12 @@ final class ItemTranslator{
|
||||
//new item without a fixed legacy ID - we can't handle this right now
|
||||
continue;
|
||||
}
|
||||
if(isset($complexMappings[$newId]) && $complexMappings[$newId][0] === $intId && $complexMappings[$newId][1] <= $meta){
|
||||
//TODO: HACK! Multiple legacy ID/meta pairs can be mapped to the same new ID (see minecraft:log)
|
||||
//Assume that the first one is the most relevant for now
|
||||
//However, this could catch fire in the future if this assumption is broken
|
||||
continue;
|
||||
}
|
||||
$complexMappings[$newId] = [$intId, (int) $meta];
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +25,18 @@ namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap;
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\nbt\tag\ByteTag;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -44,43 +48,57 @@ final class RuntimeBlockMapping{
|
||||
private array $legacyToRuntimeMap = [];
|
||||
/** @var int[] */
|
||||
private array $runtimeToLegacyMap = [];
|
||||
/** @var CompoundTag[] */
|
||||
private array $bedrockKnownStates;
|
||||
/** @var CompoundTag[]|null */
|
||||
private ?array $bedrockKnownStates = null;
|
||||
|
||||
private static function make() : self{
|
||||
return new self(
|
||||
Path::join(\pocketmine\BEDROCK_DATA_PATH, "canonical_block_states.nbt"),
|
||||
Path::join(\pocketmine\BEDROCK_DATA_PATH, "r12_to_current_block_map.bin")
|
||||
BedrockDataFiles::CANONICAL_BLOCK_STATES_NBT,
|
||||
BedrockDataFiles::R12_TO_CURRENT_BLOCK_MAP_BIN
|
||||
);
|
||||
}
|
||||
|
||||
public function __construct(string $canonicalBlockStatesFile, string $r12ToCurrentBlockMapFile){
|
||||
$stream = PacketSerializer::decoder(
|
||||
Filesystem::fileGetContents($canonicalBlockStatesFile),
|
||||
0,
|
||||
new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
|
||||
);
|
||||
$list = [];
|
||||
while(!$stream->feof()){
|
||||
$list[] = $stream->getNbtCompoundRoot();
|
||||
/**
|
||||
* @param string[] $keyIndex
|
||||
* @param (ByteTag|StringTag|IntTag)[][] $valueIndex
|
||||
* @phpstan-param array<string, string> $keyIndex
|
||||
* @phpstan-param array<int, array<int|string, ByteTag|IntTag|StringTag>> $valueIndex
|
||||
*/
|
||||
private static function deduplicateCompound(CompoundTag $tag, array &$keyIndex, array &$valueIndex) : CompoundTag{
|
||||
if($tag->count() === 0){
|
||||
return $tag;
|
||||
}
|
||||
$this->bedrockKnownStates = $list;
|
||||
|
||||
$this->setupLegacyMappings($r12ToCurrentBlockMapFile);
|
||||
$newTag = CompoundTag::create();
|
||||
foreach($tag as $key => $value){
|
||||
$key = $keyIndex[$key] ??= $key;
|
||||
|
||||
if($value instanceof CompoundTag){
|
||||
$value = $valueIndex[$value->getType()][(new LittleEndianNbtSerializer())->write(new TreeRoot($value))] ??= self::deduplicateCompound($value, $keyIndex, $valueIndex);
|
||||
}elseif($value instanceof ByteTag || $value instanceof IntTag || $value instanceof StringTag){
|
||||
$value = $valueIndex[$value->getType()][$value->getValue()] ??= $value;
|
||||
}
|
||||
|
||||
$newTag->setTag($key, $value);
|
||||
}
|
||||
|
||||
return $newTag;
|
||||
}
|
||||
|
||||
private function setupLegacyMappings(string $r12ToCurrentBlockMapFile) : void{
|
||||
public function __construct(
|
||||
private string $canonicalBlockStatesFile,
|
||||
string $r12ToCurrentBlockMapFile
|
||||
){
|
||||
//do not cache this - we only need it to set up mappings under normal circumstances
|
||||
$bedrockKnownStates = $this->loadBedrockKnownStates();
|
||||
|
||||
$legacyIdMap = LegacyBlockIdToStringIdMap::getInstance();
|
||||
/** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */
|
||||
$legacyStateMap = [];
|
||||
$legacyStateMapReader = PacketSerializer::decoder(
|
||||
Filesystem::fileGetContents($r12ToCurrentBlockMapFile),
|
||||
0,
|
||||
new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
|
||||
);
|
||||
$legacyStateMapReader = new BinaryStream(Filesystem::fileGetContents($r12ToCurrentBlockMapFile));
|
||||
$nbtReader = new NetworkNbtSerializer();
|
||||
while(!$legacyStateMapReader->feof()){
|
||||
$id = $legacyStateMapReader->getString();
|
||||
$id = $legacyStateMapReader->get($legacyStateMapReader->getUnsignedVarInt());
|
||||
$meta = $legacyStateMapReader->getLShort();
|
||||
|
||||
$offset = $legacyStateMapReader->getOffset();
|
||||
@ -93,7 +111,7 @@ final class RuntimeBlockMapping{
|
||||
* @var int[][] $idToStatesMap string id -> int[] list of candidate state indices
|
||||
*/
|
||||
$idToStatesMap = [];
|
||||
foreach($this->bedrockKnownStates as $k => $state){
|
||||
foreach($bedrockKnownStates as $k => $state){
|
||||
$idToStatesMap[$state->getString("name")][] = $k;
|
||||
}
|
||||
foreach($legacyStateMap as $pair){
|
||||
@ -112,7 +130,7 @@ final class RuntimeBlockMapping{
|
||||
throw new \RuntimeException("Mapped new state does not appear in network table");
|
||||
}
|
||||
foreach($idToStatesMap[$mappedName] as $k){
|
||||
$networkState = $this->bedrockKnownStates[$k];
|
||||
$networkState = $bedrockKnownStates[$k];
|
||||
if($mappedState->equals($networkState)){
|
||||
$this->registerMapping($k, $id, $data);
|
||||
continue 2;
|
||||
@ -122,6 +140,25 @@ final class RuntimeBlockMapping{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CompoundTag[]
|
||||
*/
|
||||
private function loadBedrockKnownStates() : array{
|
||||
$stream = new BinaryStream(Filesystem::fileGetContents($this->canonicalBlockStatesFile));
|
||||
$list = [];
|
||||
$nbtReader = new NetworkNbtSerializer();
|
||||
|
||||
$keyIndex = [];
|
||||
$valueIndex = [];
|
||||
while(!$stream->feof()){
|
||||
$offset = $stream->getOffset();
|
||||
$blockState = $nbtReader->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
|
||||
$stream->setOffset($offset);
|
||||
$list[] = self::deduplicateCompound($blockState, $keyIndex, $valueIndex);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function toRuntimeId(int $internalStateId) : int{
|
||||
return $this->legacyToRuntimeMap[$internalStateId] ?? $this->legacyToRuntimeMap[BlockLegacyIds::INFO_UPDATE << Block::INTERNAL_METADATA_BITS];
|
||||
}
|
||||
@ -136,9 +173,14 @@ final class RuntimeBlockMapping{
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: This method may load the palette from disk, which is a slow operation.
|
||||
* Afterwards, it will cache the palette in memory, which requires (in some cases) tens of MB of memory.
|
||||
* Avoid using this where possible.
|
||||
*
|
||||
* @deprecated
|
||||
* @return CompoundTag[]
|
||||
*/
|
||||
public function getBedrockKnownStates() : array{
|
||||
return $this->bedrockKnownStates;
|
||||
return $this->bedrockKnownStates ??= $this->loadBedrockKnownStates();
|
||||
}
|
||||
}
|
||||
|
@ -23,12 +23,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\inventory\transaction\action\CreateItemAction;
|
||||
use pocketmine\inventory\transaction\action\DestroyItemAction;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\action\InventoryAction;
|
||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\item\Durable;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\ItemFactory;
|
||||
@ -37,17 +33,12 @@ use pocketmine\item\VanillaItems;
|
||||
use pocketmine\nbt\NbtException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor;
|
||||
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
|
||||
use pocketmine\network\mcpe\protocol\types\recipe\StringIdMetaItemDescriptor;
|
||||
use pocketmine\player\GameMode;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
|
||||
@ -145,14 +136,16 @@ class TypeConverter{
|
||||
if($itemStack->isNull()){
|
||||
return ItemStack::null();
|
||||
}
|
||||
$nbt = null;
|
||||
if($itemStack->hasNamedTag()){
|
||||
$nbt = clone $itemStack->getNamedTag();
|
||||
$nbt = $itemStack->getNamedTag();
|
||||
if($nbt->count() === 0){
|
||||
$nbt = null;
|
||||
}else{
|
||||
$nbt = clone $nbt;
|
||||
}
|
||||
|
||||
$isBlockItem = $itemStack->getId() < 256;
|
||||
|
||||
$idMeta = ItemTranslator::getInstance()->toNetworkIdQuiet($itemStack->getId(), $itemStack->getMeta());
|
||||
$internalId = $itemStack->getId();
|
||||
$internalMeta = $itemStack->getMeta();
|
||||
$idMeta = ItemTranslator::getInstance()->toNetworkIdQuiet($internalId, $internalMeta);
|
||||
if($idMeta === null){
|
||||
//Display unmapped items as INFO_UPDATE, but stick something in their NBT to make sure they don't stack with
|
||||
//other unmapped items.
|
||||
@ -160,8 +153,8 @@ class TypeConverter{
|
||||
if($nbt === null){
|
||||
$nbt = new CompoundTag();
|
||||
}
|
||||
$nbt->setInt(self::PM_ID_TAG, $itemStack->getId());
|
||||
$nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
|
||||
$nbt->setInt(self::PM_ID_TAG, $internalId);
|
||||
$nbt->setInt(self::PM_META_TAG, $internalMeta);
|
||||
}else{
|
||||
[$id, $meta] = $idMeta;
|
||||
|
||||
@ -176,23 +169,15 @@ class TypeConverter{
|
||||
}
|
||||
$nbt->setInt(self::DAMAGE_TAG, $itemStack->getDamage());
|
||||
$meta = 0;
|
||||
}elseif($isBlockItem && $itemStack->getMeta() !== 0){
|
||||
//TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
|
||||
//client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
|
||||
//client-side. Aside from being very annoying, this also breaks various server-side behaviours.
|
||||
if($nbt === null){
|
||||
$nbt = new CompoundTag();
|
||||
}
|
||||
$nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
|
||||
$meta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$blockRuntimeId = 0;
|
||||
if($isBlockItem){
|
||||
if($internalId < 256){
|
||||
$block = $itemStack->getBlock();
|
||||
if($block->getId() !== BlockLegacyIds::AIR){
|
||||
$blockRuntimeId = RuntimeBlockMapping::getInstance()->toRuntimeId($block->getFullId());
|
||||
$meta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,6 +203,11 @@ class TypeConverter{
|
||||
$compound = $itemStack->getNbt();
|
||||
|
||||
[$id, $meta] = ItemTranslator::getInstance()->fromNetworkId($itemStack->getId(), $itemStack->getMeta());
|
||||
if($itemStack->getBlockRuntimeId() !== 0){
|
||||
//blockitem meta is zeroed out by the client, so we have to infer it from the block runtime ID
|
||||
$blockFullId = RuntimeBlockMapping::getInstance()->fromRuntimeId($itemStack->getBlockRuntimeId());
|
||||
$meta = $blockFullId & Block::INTERNAL_METADATA_MASK;
|
||||
}
|
||||
|
||||
if($compound !== null){
|
||||
$compound = clone $compound;
|
||||
@ -232,12 +222,6 @@ class TypeConverter{
|
||||
$compound->removeTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION);
|
||||
$compound->setTag(self::DAMAGE_TAG, $conflicted);
|
||||
}
|
||||
}elseif(($metaTag = $compound->getTag(self::PM_META_TAG)) instanceof IntTag){
|
||||
//TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
|
||||
//client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
|
||||
//client-side. Aside from being very annoying, this also breaks various server-side behaviours.
|
||||
$meta = $metaTag->getValue();
|
||||
$compound->removeTag(self::PM_META_TAG);
|
||||
}
|
||||
if($compound->count() === 0){
|
||||
$compound = null;
|
||||
@ -261,60 +245,4 @@ class TypeConverter{
|
||||
throw TypeConversionException::wrap($e, "Bad itemstack NBT data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TypeConversionException
|
||||
*/
|
||||
public function createInventoryAction(NetworkInventoryAction $action, Player $player, InventoryManager $inventoryManager) : ?InventoryAction{
|
||||
if($action->oldItem->getItemStack()->equals($action->newItem->getItemStack())){
|
||||
//filter out useless noise in 1.13
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
$old = $this->netItemStackToCore($action->oldItem->getItemStack());
|
||||
}catch(TypeConversionException $e){
|
||||
throw TypeConversionException::wrap($e, "Inventory action: oldItem");
|
||||
}
|
||||
try{
|
||||
$new = $this->netItemStackToCore($action->newItem->getItemStack());
|
||||
}catch(TypeConversionException $e){
|
||||
throw TypeConversionException::wrap($e, "Inventory action: newItem");
|
||||
}
|
||||
switch($action->sourceType){
|
||||
case NetworkInventoryAction::SOURCE_CONTAINER:
|
||||
if($action->windowId === ContainerIds::UI && $action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
|
||||
return null; //useless noise
|
||||
}
|
||||
$located = $inventoryManager->locateWindowAndSlot($action->windowId, $action->inventorySlot);
|
||||
if($located !== null){
|
||||
[$window, $slot] = $located;
|
||||
return new SlotChangeAction($window, $slot, $old, $new);
|
||||
}
|
||||
|
||||
throw new TypeConversionException("No open container with window ID $action->windowId");
|
||||
case NetworkInventoryAction::SOURCE_WORLD:
|
||||
if($action->inventorySlot !== NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){
|
||||
throw new TypeConversionException("Only expecting drop-item world actions from the client!");
|
||||
}
|
||||
|
||||
return new DropItemAction($new);
|
||||
case NetworkInventoryAction::SOURCE_CREATIVE:
|
||||
switch($action->inventorySlot){
|
||||
case NetworkInventoryAction::ACTION_MAGIC_SLOT_CREATIVE_DELETE_ITEM:
|
||||
return new DestroyItemAction($new);
|
||||
case NetworkInventoryAction::ACTION_MAGIC_SLOT_CREATIVE_CREATE_ITEM:
|
||||
return new CreateItemAction($old);
|
||||
default:
|
||||
throw new TypeConversionException("Unexpected creative action type $action->inventorySlot");
|
||||
|
||||
}
|
||||
case NetworkInventoryAction::SOURCE_TODO:
|
||||
//These are used to balance a transaction that involves special actions, like crafting, enchanting, etc.
|
||||
//The vanilla server just accepted these without verifying them. We don't need to care about them since
|
||||
//we verify crafting by checking for imbalances anyway.
|
||||
return null;
|
||||
default:
|
||||
throw new TypeConversionException("Unknown inventory source type $action->sourceType");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,10 +32,10 @@ use pocketmine\entity\animation\ConsumingItemAnimation;
|
||||
use pocketmine\entity\Attribute;
|
||||
use pocketmine\entity\InvalidSkinException;
|
||||
use pocketmine\event\player\PlayerEditBookEvent;
|
||||
use pocketmine\inventory\transaction\action\InventoryAction;
|
||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\inventory\transaction\TransactionException;
|
||||
use pocketmine\inventory\transaction\TransactionBuilder;
|
||||
use pocketmine\inventory\transaction\TransactionCancelledException;
|
||||
use pocketmine\inventory\transaction\TransactionValidationException;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\item\WritableBook;
|
||||
@ -46,7 +46,6 @@ use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
|
||||
use pocketmine\network\mcpe\convert\TypeConversionException;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
@ -65,6 +64,8 @@ use pocketmine\network\mcpe\protocol\EmotePacket;
|
||||
use pocketmine\network\mcpe\protocol\InteractPacket;
|
||||
use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemFrameDropItemPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemStackRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemStackResponsePacket;
|
||||
use pocketmine\network\mcpe\protocol\LabTablePacket;
|
||||
use pocketmine\network\mcpe\protocol\LecternUpdatePacket;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
@ -96,7 +97,8 @@ use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UseItemOnEntityTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UseItemTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerAction;
|
||||
@ -108,17 +110,18 @@ use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Limits;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use function array_push;
|
||||
use function base64_encode;
|
||||
use function count;
|
||||
use function fmod;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function is_bool;
|
||||
use function is_infinite;
|
||||
use function is_nan;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function max;
|
||||
use function mb_strlen;
|
||||
use function microtime;
|
||||
@ -133,9 +136,6 @@ use const JSON_THROW_ON_ERROR;
|
||||
class InGamePacketHandler extends PacketHandler{
|
||||
private const MAX_FORM_RESPONSE_DEPTH = 2; //modal/simple will be 1, custom forms 2 - they will never contain anything other than string|int|float|bool|null
|
||||
|
||||
/** @var CraftingTransaction|null */
|
||||
protected $craftingTransaction = null;
|
||||
|
||||
/** @var float */
|
||||
protected $lastRightClickTime = 0.0;
|
||||
/** @var UseItemTransactionData|null */
|
||||
@ -214,7 +214,7 @@ class InGamePacketHandler extends PacketHandler{
|
||||
if($newPos->distanceSquared($curPos) > 1){ //Tolerate up to 1 block to avoid problems with client-sided physics when spawning in blocks
|
||||
$this->session->getLogger()->debug("Got outdated pre-teleport movement, received " . $newPos . ", expected " . $curPos);
|
||||
//Still getting movements from before teleport, ignore them
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Once we get a movement within a reasonable distance, treat it as a teleport ACK and remove position lock
|
||||
@ -276,13 +276,22 @@ class InGamePacketHandler extends PacketHandler{
|
||||
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
|
||||
throw new PacketHandlingException("Too many actions in item use transaction");
|
||||
}
|
||||
$this->inventoryManager->addPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions());
|
||||
|
||||
$this->inventoryManager->setCurrentItemStackRequestId($useItemTransaction->getRequestId());
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions());
|
||||
if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){
|
||||
$packetHandled = false;
|
||||
$this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")");
|
||||
}else{
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
}
|
||||
$this->inventoryManager->setCurrentItemStackRequestId(null);
|
||||
}
|
||||
|
||||
$itemStackRequest = $packet->getItemStackRequest();
|
||||
if($itemStackRequest !== null){
|
||||
$result = $this->handleSingleItemStackRequest($itemStackRequest);
|
||||
$this->session->sendDataPacket(ItemStackResponsePacket::create([$result]));
|
||||
}
|
||||
|
||||
return $packetHandled;
|
||||
@ -316,17 +325,21 @@ class InGamePacketHandler extends PacketHandler{
|
||||
public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{
|
||||
$result = true;
|
||||
|
||||
if(count($packet->trData->getActions()) > 100){
|
||||
if(count($packet->trData->getActions()) > 50){
|
||||
throw new PacketHandlingException("Too many actions in inventory transaction");
|
||||
}
|
||||
if(count($packet->requestChangedSlots) > 10){
|
||||
throw new PacketHandlingException("Too many slot sync requests in inventory transaction");
|
||||
}
|
||||
|
||||
$this->inventoryManager->addPredictedSlotChanges($packet->trData->getActions());
|
||||
$this->inventoryManager->setCurrentItemStackRequestId($packet->requestId);
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($packet->trData->getActions());
|
||||
|
||||
if($packet->trData instanceof NormalTransactionData){
|
||||
$result = $this->handleNormalTransaction($packet->trData);
|
||||
$result = $this->handleNormalTransaction($packet->trData, $packet->requestId);
|
||||
}elseif($packet->trData instanceof MismatchTransactionData){
|
||||
$this->session->getLogger()->debug("Mismatch transaction received");
|
||||
$this->inventoryManager->syncAll();
|
||||
$this->inventoryManager->requestSyncAll();
|
||||
$result = true;
|
||||
}elseif($packet->trData instanceof UseItemTransactionData){
|
||||
$result = $this->handleUseItemTransaction($packet->trData);
|
||||
@ -336,96 +349,121 @@ class InGamePacketHandler extends PacketHandler{
|
||||
$result = $this->handleReleaseItemTransaction($packet->trData);
|
||||
}
|
||||
|
||||
if($this->craftingTransaction === null){ //don't sync if we're waiting to complete a crafting transaction
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
|
||||
//requestChangedSlots asks the server to always send out the contents of the specified slots, even if they
|
||||
//haven't changed. Handling these is necessary to ensure the client inventory stays in sync if the server
|
||||
//rejects the transaction. The most common example of this is equipping armor by right-click, which doesn't send
|
||||
//a legacy prediction action for the destination armor slot.
|
||||
foreach($packet->requestChangedSlots as $containerInfo){
|
||||
foreach($containerInfo->getChangedSlotIndexes() as $netSlot){
|
||||
[$windowId, $slot] = ItemStackContainerIdTranslator::translate($containerInfo->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $netSlot);
|
||||
$inventoryAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slot);
|
||||
if($inventoryAndSlot !== null){ //trigger the normal slot sync logic
|
||||
$this->inventoryManager->onSlotChange($inventoryAndSlot[0], $inventoryAndSlot[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->inventoryManager->setCurrentItemStackRequestId(null);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function handleNormalTransaction(NormalTransactionData $data) : bool{
|
||||
/** @var InventoryAction[] $actions */
|
||||
$actions = [];
|
||||
private function executeInventoryTransaction(InventoryTransaction $transaction, int $requestId) : bool{
|
||||
$this->player->setUsingItem(false);
|
||||
|
||||
$isCraftingPart = false;
|
||||
$converter = TypeConverter::getInstance();
|
||||
foreach($data->getActions() as $networkInventoryAction){
|
||||
if(
|
||||
$networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO || (
|
||||
$this->craftingTransaction !== null &&
|
||||
!$networkInventoryAction->oldItem->getItemStack()->equals($networkInventoryAction->newItem->getItemStack()) &&
|
||||
$networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER &&
|
||||
$networkInventoryAction->windowId === ContainerIds::UI &&
|
||||
$networkInventoryAction->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT
|
||||
)
|
||||
){
|
||||
$isCraftingPart = true;
|
||||
}
|
||||
$this->inventoryManager->setCurrentItemStackRequestId($requestId);
|
||||
$this->inventoryManager->addTransactionPredictedSlotChanges($transaction);
|
||||
try{
|
||||
$transaction->execute();
|
||||
}catch(TransactionValidationException $e){
|
||||
$this->inventoryManager->requestSyncAll();
|
||||
$logger = $this->session->getLogger();
|
||||
$logger->debug("Invalid inventory transaction $requestId: " . $e->getMessage());
|
||||
|
||||
try{
|
||||
$action = $converter->createInventoryAction($networkInventoryAction, $this->player, $this->inventoryManager);
|
||||
if($action !== null){
|
||||
$actions[] = $action;
|
||||
}
|
||||
}catch(TypeConversionException $e){
|
||||
$this->session->getLogger()->debug("Error unpacking inventory action: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}catch(TransactionCancelledException){
|
||||
$this->session->getLogger()->debug("Inventory transaction $requestId cancelled by a plugin");
|
||||
|
||||
if($isCraftingPart){
|
||||
if($this->craftingTransaction === null){
|
||||
//TODO: this might not be crafting if there is a special inventory open (anvil, enchanting, loom etc)
|
||||
$this->craftingTransaction = new CraftingTransaction($this->player, $this->player->getServer()->getCraftingManager(), $actions);
|
||||
}else{
|
||||
foreach($actions as $action){
|
||||
$this->craftingTransaction->addAction($action);
|
||||
}
|
||||
}
|
||||
|
||||
try{
|
||||
$this->craftingTransaction->validate();
|
||||
}catch(TransactionValidationException $e){
|
||||
//transaction is incomplete - crafting transaction comes in lots of little bits, so we have to collect
|
||||
//all of the parts before we can execute it
|
||||
return true;
|
||||
}
|
||||
$this->player->setUsingItem(false);
|
||||
try{
|
||||
$this->craftingTransaction->execute();
|
||||
}catch(TransactionException $e){
|
||||
$this->session->getLogger()->debug("Failed to execute crafting transaction: " . $e->getMessage());
|
||||
return false;
|
||||
}finally{
|
||||
$this->craftingTransaction = null;
|
||||
}
|
||||
}else{
|
||||
//normal transaction fallthru
|
||||
if($this->craftingTransaction !== null){
|
||||
$this->session->getLogger()->debug("Got unexpected normal inventory action with incomplete crafting transaction, refusing to execute crafting");
|
||||
$this->craftingTransaction = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(count($actions) === 0){
|
||||
//TODO: 1.13+ often sends transactions with nothing but useless crap in them, no need for the debug noise
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->player->setUsingItem(false);
|
||||
$transaction = new InventoryTransaction($this->player, $actions);
|
||||
try{
|
||||
$transaction->execute();
|
||||
}catch(TransactionException $e){
|
||||
$logger = $this->session->getLogger();
|
||||
$logger->debug("Failed to execute inventory transaction: " . $e->getMessage());
|
||||
$logger->debug("Actions: " . json_encode($data->getActions()));
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}finally{
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
$this->inventoryManager->setCurrentItemStackRequestId(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function handleNormalTransaction(NormalTransactionData $data, int $itemStackRequestId) : bool{
|
||||
//When the ItemStackRequest system is used, this transaction type is used for dropping items by pressing Q.
|
||||
//I don't know why they don't just use ItemStackRequest for that too, which already supports dropping items by
|
||||
//clicking them outside an open inventory menu, but for now it is what it is.
|
||||
//Fortunately, this means we can be much stricter about the validation criteria.
|
||||
|
||||
$actionCount = count($data->getActions());
|
||||
if($actionCount > 2){
|
||||
if($actionCount > 5){
|
||||
throw new PacketHandlingException("Too many actions ($actionCount) in normal inventory transaction");
|
||||
}
|
||||
|
||||
//Due to a bug in the game, this transaction type is still sent when a player edits a book. We don't need
|
||||
//these transactions for editing books, since we have BookEditPacket, so we can just ignore them.
|
||||
$this->session->getLogger()->debug("Ignoring normal inventory transaction with $actionCount actions (drop-item should have exactly 2 actions)");
|
||||
return false;
|
||||
}
|
||||
|
||||
$sourceSlot = null;
|
||||
$clientItemStack = null;
|
||||
$droppedCount = null;
|
||||
|
||||
foreach($data->getActions() as $networkInventoryAction){
|
||||
if($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_WORLD && $networkInventoryAction->inventorySlot == NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){
|
||||
$droppedCount = $networkInventoryAction->newItem->getItemStack()->getCount();
|
||||
if($droppedCount <= 0){
|
||||
throw new PacketHandlingException("Expected positive count for dropped item");
|
||||
}
|
||||
}elseif($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && $networkInventoryAction->windowId === ContainerIds::INVENTORY){
|
||||
//mobile players can drop an item from a non-selected hotbar slot
|
||||
$sourceSlot = $networkInventoryAction->inventorySlot;
|
||||
$clientItemStack = $networkInventoryAction->oldItem->getItemStack();
|
||||
}else{
|
||||
$this->session->getLogger()->debug("Unexpected inventory action type $networkInventoryAction->sourceType in drop item transaction");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if($sourceSlot === null || $clientItemStack === null || $droppedCount === null){
|
||||
$this->session->getLogger()->debug("Missing information in drop item transaction, need source slot, client item stack and dropped count");
|
||||
return false;
|
||||
}
|
||||
|
||||
$inventory = $this->player->getInventory();
|
||||
|
||||
if(!$inventory->slotExists($sourceSlot)){
|
||||
return false; //TODO: size desync??
|
||||
}
|
||||
|
||||
$sourceSlotItem = $inventory->getItem($sourceSlot);
|
||||
if($sourceSlotItem->getCount() < $droppedCount){
|
||||
return false;
|
||||
}
|
||||
$serverItemStack = TypeConverter::getInstance()->coreItemStackToNet($sourceSlotItem);
|
||||
//because the client doesn't tell us the expected itemstack ID, we have to deep-compare our known
|
||||
//itemstack info with the one the client sent. This is costly, but we don't have any other option :(
|
||||
if(!$serverItemStack->equals($clientItemStack)){
|
||||
return false;
|
||||
}
|
||||
|
||||
//this modifies $sourceSlotItem
|
||||
$droppedItem = $sourceSlotItem->pop($droppedCount);
|
||||
|
||||
$builder = new TransactionBuilder();
|
||||
$builder->getInventory($inventory)->setItem($sourceSlot, $sourceSlotItem);
|
||||
$builder->addAction(new DropItemAction($droppedItem));
|
||||
|
||||
$transaction = new InventoryTransaction($this->player, $builder->generateActions());
|
||||
return $this->executeInventoryTransaction($transaction, $itemStackRequestId);
|
||||
}
|
||||
|
||||
private function handleUseItemTransaction(UseItemTransactionData $data) : bool{
|
||||
$this->player->selectHotbarSlot($data->getHotbarSlot());
|
||||
|
||||
@ -537,6 +575,52 @@ class InGamePacketHandler extends PacketHandler{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function handleSingleItemStackRequest(ItemStackRequest $request) : ItemStackResponse{
|
||||
if(count($request->getActions()) > 60){
|
||||
//recipe book auto crafting can affect all slots of the inventory when consuming inputs or producing outputs
|
||||
//this means there could be as many as 50 CraftingConsumeInput actions or Place (taking the result) actions
|
||||
//in a single request (there are certain ways items can be arranged which will result in the same stack
|
||||
//being taken from multiple times, but this is behaviour with a calculable limit)
|
||||
//this means there SHOULD be AT MOST 53 actions in a single request, but 60 is a nice round number.
|
||||
//n64Stacks = ?
|
||||
//n1Stacks = 45 - n64Stacks
|
||||
//nItemsRequiredFor1Craft = 9
|
||||
//nResults = floor((n1Stacks + (n64Stacks * 64)) / nItemsRequiredFor1Craft)
|
||||
//nTakeActionsTotal = floor(64 / nResults) + max(1, 64 % nResults) + ((nResults * nItemsRequiredFor1Craft) - (n64Stacks * 64))
|
||||
throw new PacketHandlingException("Too many actions in ItemStackRequest");
|
||||
}
|
||||
$executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $request);
|
||||
try{
|
||||
$transaction = $executor->generateInventoryTransaction();
|
||||
$result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
|
||||
}catch(ItemStackRequestProcessException $e){
|
||||
$result = false;
|
||||
$this->session->getLogger()->debug("ItemStackRequest #" . $request->getRequestId() . " failed: " . $e->getMessage());
|
||||
$this->session->getLogger()->debug(implode("\n", Utils::printableExceptionInfo($e)));
|
||||
$this->inventoryManager->requestSyncAll();
|
||||
}
|
||||
|
||||
if(!$result){
|
||||
return new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId());
|
||||
}
|
||||
return $executor->buildItemStackResponse();
|
||||
}
|
||||
|
||||
public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{
|
||||
$responses = [];
|
||||
if(count($packet->getRequests()) > 80){
|
||||
//TODO: we can probably lower this limit, but this will do for now
|
||||
throw new PacketHandlingException("Too many requests in ItemStackRequestPacket");
|
||||
}
|
||||
foreach($packet->getRequests() as $request){
|
||||
$responses[] = $this->handleSingleItemStackRequest($request);
|
||||
}
|
||||
|
||||
$this->session->sendDataPacket(ItemStackResponsePacket::create($responses));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleMobEquipment(MobEquipmentPacket $packet) : bool{
|
||||
if($packet->windowId === ContainerIds::OFFHAND){
|
||||
return true; //this happens when we put an item into the offhand
|
||||
@ -664,7 +748,7 @@ class InGamePacketHandler extends PacketHandler{
|
||||
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
|
||||
|
||||
if($block instanceof BaseSign){
|
||||
if(($textBlobTag = $nbt->getTag(Sign::TAG_TEXT_BLOB)) instanceof StringTag){
|
||||
if(($textBlobTag = $nbt->getCompoundTag(Sign::TAG_FRONT_TEXT)?->getTag(Sign::TAG_TEXT_BLOB)) instanceof StringTag){
|
||||
try{
|
||||
$text = SignText::fromBlob($textBlobTag->getValue());
|
||||
}catch(\InvalidArgumentException $e){
|
||||
|
96
src/network/mcpe/handler/ItemStackContainerIdTranslator.php
Normal file
96
src/network/mcpe/handler/ItemStackContainerIdTranslator.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\handler;
|
||||
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
|
||||
final class ItemStackContainerIdTranslator{
|
||||
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
* @phpstan-return array{int, int}
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
public static function translate(int $containerInterfaceId, int $currentWindowId, int $slotId) : array{
|
||||
return match($containerInterfaceId){
|
||||
ContainerUIIds::ARMOR => [ContainerIds::ARMOR, $slotId],
|
||||
|
||||
ContainerUIIds::HOTBAR,
|
||||
ContainerUIIds::INVENTORY,
|
||||
ContainerUIIds::COMBINED_HOTBAR_AND_INVENTORY => [ContainerIds::INVENTORY, $slotId],
|
||||
|
||||
//TODO: HACK! The client sends an incorrect slot ID for the offhand as of 1.19.70 (though this doesn't really matter since the offhand has only 1 slot anyway)
|
||||
ContainerUIIds::OFFHAND => [ContainerIds::OFFHAND, 0],
|
||||
|
||||
ContainerUIIds::ANVIL_INPUT,
|
||||
ContainerUIIds::ANVIL_MATERIAL,
|
||||
ContainerUIIds::BEACON_PAYMENT,
|
||||
ContainerUIIds::CARTOGRAPHY_ADDITIONAL,
|
||||
ContainerUIIds::CARTOGRAPHY_INPUT,
|
||||
ContainerUIIds::COMPOUND_CREATOR_INPUT,
|
||||
ContainerUIIds::CRAFTING_INPUT,
|
||||
ContainerUIIds::CREATED_OUTPUT,
|
||||
ContainerUIIds::CURSOR,
|
||||
ContainerUIIds::ENCHANTING_INPUT,
|
||||
ContainerUIIds::ENCHANTING_MATERIAL,
|
||||
ContainerUIIds::GRINDSTONE_ADDITIONAL,
|
||||
ContainerUIIds::GRINDSTONE_INPUT,
|
||||
ContainerUIIds::LAB_TABLE_INPUT,
|
||||
ContainerUIIds::LOOM_DYE,
|
||||
ContainerUIIds::LOOM_INPUT,
|
||||
ContainerUIIds::LOOM_MATERIAL,
|
||||
ContainerUIIds::MATERIAL_REDUCER_INPUT,
|
||||
ContainerUIIds::MATERIAL_REDUCER_OUTPUT,
|
||||
ContainerUIIds::SMITHING_TABLE_INPUT,
|
||||
ContainerUIIds::SMITHING_TABLE_MATERIAL,
|
||||
ContainerUIIds::STONECUTTER_INPUT,
|
||||
ContainerUIIds::TRADE2_INGREDIENT1,
|
||||
ContainerUIIds::TRADE2_INGREDIENT2,
|
||||
ContainerUIIds::TRADE_INGREDIENT1,
|
||||
ContainerUIIds::TRADE_INGREDIENT2 => [ContainerIds::UI, $slotId],
|
||||
|
||||
ContainerUIIds::BARREL,
|
||||
ContainerUIIds::BLAST_FURNACE_INGREDIENT,
|
||||
ContainerUIIds::BREWING_STAND_FUEL,
|
||||
ContainerUIIds::BREWING_STAND_INPUT,
|
||||
ContainerUIIds::BREWING_STAND_RESULT,
|
||||
ContainerUIIds::FURNACE_FUEL,
|
||||
ContainerUIIds::FURNACE_INGREDIENT,
|
||||
ContainerUIIds::FURNACE_RESULT,
|
||||
ContainerUIIds::LEVEL_ENTITY, //chest
|
||||
ContainerUIIds::SHULKER_BOX,
|
||||
ContainerUIIds::SMOKER_INGREDIENT => [$currentWindowId, $slotId],
|
||||
|
||||
//all preview slots are ignored, since the client shouldn't be modifying those directly
|
||||
|
||||
default => throw new PacketHandlingException("Unexpected container UI ID $containerInterfaceId")
|
||||
};
|
||||
}
|
||||
}
|
389
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
389
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\handler;
|
||||
|
||||
use pocketmine\inventory\CreativeInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\CreateItemAction;
|
||||
use pocketmine\inventory\transaction\action\DestroyItemAction;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\inventory\transaction\TransactionBuilder;
|
||||
use pocketmine\inventory\transaction\TransactionBuilderInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingConsumeInputStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingCreateSpecificResultStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeAutoStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CreativeCreateStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DeprecatedCraftingResultsStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DestroyStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DropStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestSlotInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\PlaceStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\SwapStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\TakeStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function array_key_first;
|
||||
use function count;
|
||||
use function spl_object_id;
|
||||
|
||||
class ItemStackRequestExecutor{
|
||||
private TransactionBuilder $builder;
|
||||
|
||||
/** @var ItemStackRequestSlotInfo[] */
|
||||
private array $requestSlotInfos = [];
|
||||
|
||||
private ?InventoryTransaction $specialTransaction = null;
|
||||
|
||||
/** @var Item[] */
|
||||
private array $craftingResults = [];
|
||||
|
||||
private ?Item $nextCreatedItem = null;
|
||||
private bool $createdItemFromCreativeInventory = false;
|
||||
private int $createdItemsTakenCount = 0;
|
||||
|
||||
public function __construct(
|
||||
private Player $player,
|
||||
private InventoryManager $inventoryManager,
|
||||
private ItemStackRequest $request
|
||||
){
|
||||
$this->builder = new TransactionBuilder();
|
||||
}
|
||||
|
||||
protected function prettyInventoryAndSlot(Inventory $inventory, int $slot) : string{
|
||||
if($inventory instanceof TransactionBuilderInventory){
|
||||
$inventory = $inventory->getActualInventory();
|
||||
}
|
||||
return (new \ReflectionClass($inventory))->getShortName() . "#" . spl_object_id($inventory) . ", slot: $slot";
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : void{
|
||||
$info = $this->inventoryManager->getItemStackInfo($inventory, $slotId);
|
||||
if($info === null){
|
||||
throw new AssumptionFailedError("The inventory is tracked and the slot is valid, so this should not be null");
|
||||
}
|
||||
|
||||
if(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){
|
||||
throw new ItemStackRequestProcessException(
|
||||
$this->prettyInventoryAndSlot($inventory, $slotId) . ": " .
|
||||
"Mismatched expected itemstack, " .
|
||||
"client expected: $clientItemStackId, server actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{TransactionBuilderInventory, int}
|
||||
*
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
|
||||
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($info->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $info->getSlotId());
|
||||
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
|
||||
if($windowAndSlot === null){
|
||||
throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerId() . ", slot ID: " . $info->getSlotId());
|
||||
}
|
||||
[$inventory, $slot] = $windowAndSlot;
|
||||
if(!$inventory->slotExists($slot)){
|
||||
throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyInventoryAndSlot($inventory, $slot));
|
||||
}
|
||||
|
||||
if($info->getStackId() !== $this->request->getRequestId()){ //the itemstack may have been modified by the current request
|
||||
$this->matchItemStack($inventory, $slot, $info->getStackId());
|
||||
}
|
||||
|
||||
return [$this->builder->getInventory($inventory), $slot];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function transferItems(ItemStackRequestSlotInfo $source, ItemStackRequestSlotInfo $destination, int $count) : void{
|
||||
$removed = $this->removeItemFromSlot($source, $count);
|
||||
$this->addItemToSlot($destination, $removed, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deducts items from an inventory slot, returning a stack containing the removed items.
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function removeItemFromSlot(ItemStackRequestSlotInfo $slotInfo, int $count) : Item{
|
||||
if($slotInfo->getContainerId() === ContainerUIIds::CREATED_OUTPUT && $slotInfo->getSlotId() === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
|
||||
//special case for the "created item" output slot
|
||||
//TODO: do we need to send a response for this slot info?
|
||||
return $this->takeCreatedItem($count);
|
||||
}
|
||||
$this->requestSlotInfos[] = $slotInfo;
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
|
||||
if($count < 1){
|
||||
//this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
|
||||
}
|
||||
|
||||
$existingItem = $inventory->getItem($slot);
|
||||
if($existingItem->getCount() < $count){
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
|
||||
}
|
||||
|
||||
$removed = $existingItem->pop($count);
|
||||
$inventory->setItem($slot, $existingItem);
|
||||
|
||||
return $removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the target slot, if they are stackable.
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function addItemToSlot(ItemStackRequestSlotInfo $slotInfo, Item $item, int $count) : void{
|
||||
$this->requestSlotInfos[] = $slotInfo;
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
|
||||
if($count < 1){
|
||||
//this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
|
||||
}
|
||||
|
||||
$existingItem = $inventory->getItem($slot);
|
||||
if(!$existingItem->isNull() && !$existingItem->canStackWith($item)){
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
|
||||
}
|
||||
|
||||
//we can't use the existing item here; it may be an empty stack
|
||||
$newItem = clone $item;
|
||||
$newItem->setCount($existingItem->getCount() + $count);
|
||||
$inventory->setItem($slot, $newItem);
|
||||
}
|
||||
|
||||
protected function dropItem(Item $item, int $count) : void{
|
||||
if($count < 1){
|
||||
throw new ItemStackRequestProcessException("Cannot drop less than 1 of an item");
|
||||
}
|
||||
$this->builder->addAction(new DropItemAction((clone $item)->setCount($count)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function setNextCreatedItem(?Item $item, bool $creative = false) : void{
|
||||
if($item !== null && $item->isNull()){
|
||||
$item = null;
|
||||
}
|
||||
if($this->nextCreatedItem !== null){
|
||||
//while this is more complicated than simply adding the action when the item is taken, this ensures that
|
||||
//plugins can tell the difference between 1 item that got split into 2 slots, vs 2 separate items.
|
||||
if($this->createdItemFromCreativeInventory && $this->createdItemsTakenCount > 0){
|
||||
$this->nextCreatedItem->setCount($this->createdItemsTakenCount);
|
||||
$this->builder->addAction(new CreateItemAction($this->nextCreatedItem));
|
||||
}elseif($this->createdItemsTakenCount < $this->nextCreatedItem->getCount()){
|
||||
throw new ItemStackRequestProcessException("Not all of the previous created item was taken");
|
||||
}
|
||||
}
|
||||
$this->nextCreatedItem = $item;
|
||||
$this->createdItemFromCreativeInventory = $creative;
|
||||
$this->createdItemsTakenCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function beginCrafting(int $recipeId, int $repetitions) : void{
|
||||
if($this->specialTransaction !== null){
|
||||
throw new ItemStackRequestProcessException("Another special transaction is already in progress");
|
||||
}
|
||||
if($repetitions < 1){
|
||||
throw new ItemStackRequestProcessException("Cannot craft a recipe less than 1 time");
|
||||
}
|
||||
if($repetitions > 256){
|
||||
//TODO: we can probably lower this limit to 64, but I'm unsure if there are cases where the client may
|
||||
//request more than 64 repetitions of a recipe.
|
||||
//It's already hard-limited to 256 repetitions in the protocol, so this is just a sanity check.
|
||||
throw new ItemStackRequestProcessException("Cannot craft a recipe more than 256 times");
|
||||
}
|
||||
$craftingManager = $this->player->getServer()->getCraftingManager();
|
||||
$recipe = $craftingManager->getCraftingRecipeFromIndex($recipeId);
|
||||
if($recipe === null){
|
||||
throw new ItemStackRequestProcessException("No such crafting recipe index: $recipeId");
|
||||
}
|
||||
|
||||
$this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions);
|
||||
|
||||
//TODO: Since the system assumes that crafting can only be done in the crafting grid, we have to give it a
|
||||
//crafting grid to make the API happy. No implementation of getResultsFor() actually uses the crafting grid
|
||||
//right now, so this will work, but this will become a problem in the future for things like shulker boxes and
|
||||
//custom crafting recipes.
|
||||
$craftingResults = $recipe->getResultsFor($this->player->getCraftingGrid());
|
||||
foreach($craftingResults as $k => $craftingResult){
|
||||
$craftingResult->setCount($craftingResult->getCount() * $repetitions);
|
||||
$this->craftingResults[$k] = $craftingResult;
|
||||
}
|
||||
if(count($this->craftingResults) === 1){
|
||||
//for multi-output recipes, later actions will tell us which result to create and when
|
||||
$this->setNextCreatedItem($this->craftingResults[array_key_first($this->craftingResults)]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function takeCreatedItem(int $count) : Item{
|
||||
if($count < 1){
|
||||
//this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
|
||||
throw new ItemStackRequestProcessException("Cannot take less than 1 created item");
|
||||
}
|
||||
$createdItem = $this->nextCreatedItem;
|
||||
if($createdItem === null){
|
||||
throw new ItemStackRequestProcessException("No created item is waiting to be taken");
|
||||
}
|
||||
|
||||
if(!$this->createdItemFromCreativeInventory){
|
||||
$availableCount = $createdItem->getCount() - $this->createdItemsTakenCount;
|
||||
if($count > $availableCount){
|
||||
throw new ItemStackRequestProcessException("Not enough created items available to be taken (have $availableCount, tried to take $count)");
|
||||
}
|
||||
}
|
||||
|
||||
$this->createdItemsTakenCount += $count;
|
||||
$takenItem = clone $createdItem;
|
||||
$takenItem->setCount($count);
|
||||
if(!$this->createdItemFromCreativeInventory && $this->createdItemsTakenCount >= $createdItem->getCount()){
|
||||
$this->setNextCreatedItem(null);
|
||||
}
|
||||
return $takenItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function assertDoingCrafting() : void{
|
||||
if(!$this->specialTransaction instanceof CraftingTransaction){
|
||||
if($this->specialTransaction === null){
|
||||
throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action");
|
||||
}else{
|
||||
throw new ItemStackRequestProcessException("A different special transaction is already in progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function processItemStackRequestAction(ItemStackRequestAction $action) : void{
|
||||
if(
|
||||
$action instanceof TakeStackRequestAction ||
|
||||
$action instanceof PlaceStackRequestAction
|
||||
){
|
||||
$this->transferItems($action->getSource(), $action->getDestination(), $action->getCount());
|
||||
}elseif($action instanceof SwapStackRequestAction){
|
||||
$this->requestSlotInfos[] = $action->getSlot1();
|
||||
$this->requestSlotInfos[] = $action->getSlot2();
|
||||
|
||||
[$inventory1, $slot1] = $this->getBuilderInventoryAndSlot($action->getSlot1());
|
||||
[$inventory2, $slot2] = $this->getBuilderInventoryAndSlot($action->getSlot2());
|
||||
|
||||
$item1 = $inventory1->getItem($slot1);
|
||||
$item2 = $inventory2->getItem($slot2);
|
||||
$inventory1->setItem($slot1, $item2);
|
||||
$inventory2->setItem($slot2, $item1);
|
||||
}elseif($action instanceof DropStackRequestAction){
|
||||
//TODO: this action has a "randomly" field, I have no idea what it's used for
|
||||
$dropped = $this->removeItemFromSlot($action->getSource(), $action->getCount());
|
||||
$this->builder->addAction(new DropItemAction($dropped));
|
||||
|
||||
}elseif($action instanceof DestroyStackRequestAction){
|
||||
$destroyed = $this->removeItemFromSlot($action->getSource(), $action->getCount());
|
||||
$this->builder->addAction(new DestroyItemAction($destroyed));
|
||||
|
||||
}elseif($action instanceof CreativeCreateStackRequestAction){
|
||||
$item = CreativeInventory::getInstance()->getItem($action->getCreativeItemId());
|
||||
if($item === null){
|
||||
throw new ItemStackRequestProcessException("No such creative item index: " . $action->getCreativeItemId());
|
||||
}
|
||||
|
||||
$this->setNextCreatedItem($item, true);
|
||||
}elseif($action instanceof CraftRecipeStackRequestAction){
|
||||
$this->beginCrafting($action->getRecipeId(), 1);
|
||||
}elseif($action instanceof CraftRecipeAutoStackRequestAction){
|
||||
$this->beginCrafting($action->getRecipeId(), $action->getRepetitions());
|
||||
}elseif($action instanceof CraftingConsumeInputStackRequestAction){
|
||||
$this->assertDoingCrafting();
|
||||
$this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance
|
||||
|
||||
}elseif($action instanceof CraftingCreateSpecificResultStackRequestAction){
|
||||
$this->assertDoingCrafting();
|
||||
|
||||
$nextResultItem = $this->craftingResults[$action->getResultIndex()] ?? null;
|
||||
if($nextResultItem === null){
|
||||
throw new ItemStackRequestProcessException("No such crafting result index: " . $action->getResultIndex());
|
||||
}
|
||||
$this->setNextCreatedItem($nextResultItem);
|
||||
}elseif($action instanceof DeprecatedCraftingResultsStackRequestAction){
|
||||
//no obvious use
|
||||
}else{
|
||||
throw new ItemStackRequestProcessException("Unhandled item stack request action");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
public function generateInventoryTransaction() : InventoryTransaction{
|
||||
foreach($this->request->getActions() as $k => $action){
|
||||
try{
|
||||
$this->processItemStackRequestAction($action);
|
||||
}catch(ItemStackRequestProcessException $e){
|
||||
throw new ItemStackRequestProcessException("Error processing action $k (" . (new \ReflectionClass($action))->getShortName() . "): " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
$this->setNextCreatedItem(null);
|
||||
$inventoryActions = $this->builder->generateActions();
|
||||
|
||||
$transaction = $this->specialTransaction ?? new InventoryTransaction($this->player);
|
||||
foreach($inventoryActions as $action){
|
||||
$transaction->addAction($action);
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
|
||||
public function buildItemStackResponse() : ItemStackResponse{
|
||||
$builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager);
|
||||
foreach($this->requestSlotInfos as $requestInfo){
|
||||
$builder->addSlot($requestInfo->getContainerId(), $requestInfo->getSlotId());
|
||||
}
|
||||
|
||||
return $builder->build();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\handler;
|
||||
|
||||
/**
|
||||
* Thrown when an error occurs during processing of an ItemStackRequest.
|
||||
*/
|
||||
final class ItemStackRequestProcessException extends \RuntimeException{
|
||||
|
||||
}
|
107
src/network/mcpe/handler/ItemStackResponseBuilder.php
Normal file
107
src/network/mcpe/handler/ItemStackResponseBuilder.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\handler;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\item\Durable;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseContainerInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseSlotInfo;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
|
||||
final class ItemStackResponseBuilder{
|
||||
|
||||
/**
|
||||
* @var int[][]
|
||||
* @phpstan-var array<int, array<int, int>>
|
||||
*/
|
||||
private array $changedSlots = [];
|
||||
|
||||
public function __construct(
|
||||
private int $requestId,
|
||||
private InventoryManager $inventoryManager
|
||||
){}
|
||||
|
||||
public function addSlot(int $containerInterfaceId, int $slotId) : void{
|
||||
$this->changedSlots[$containerInterfaceId][$slotId] = $slotId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}
|
||||
*/
|
||||
private function getInventoryAndSlot(int $containerInterfaceId, int $slotId) : ?array{
|
||||
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId(), $slotId);
|
||||
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
|
||||
if($windowAndSlot === null){
|
||||
return null;
|
||||
}
|
||||
[$inventory, $slot] = $windowAndSlot;
|
||||
if(!$inventory->slotExists($slot)){
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$inventory, $slot];
|
||||
}
|
||||
|
||||
public function build() : ItemStackResponse{
|
||||
$responseInfosByContainer = [];
|
||||
foreach($this->changedSlots as $containerInterfaceId => $slotIds){
|
||||
if($containerInterfaceId === ContainerUIIds::CREATED_OUTPUT){
|
||||
continue;
|
||||
}
|
||||
foreach($slotIds as $slotId){
|
||||
$inventoryAndSlot = $this->getInventoryAndSlot($containerInterfaceId, $slotId);
|
||||
if($inventoryAndSlot === null){
|
||||
//a plugin may have closed the inventory during an event, or the slot may have been invalid
|
||||
continue;
|
||||
}
|
||||
[$inventory, $slot] = $inventoryAndSlot;
|
||||
|
||||
$itemStackInfo = $this->inventoryManager->getItemStackInfo($inventory, $slot);
|
||||
if($itemStackInfo === null){
|
||||
throw new AssumptionFailedError("ItemStackInfo should never be null for an open inventory");
|
||||
}
|
||||
$item = $inventory->getItem($slot);
|
||||
|
||||
$responseInfosByContainer[$containerInterfaceId][] = new ItemStackResponseSlotInfo(
|
||||
$slotId,
|
||||
$slotId,
|
||||
$item->getCount(),
|
||||
$itemStackInfo->getStackId(),
|
||||
$item->getCustomName(),
|
||||
$item instanceof Durable ? $item->getDamage() : 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$responseContainerInfos = [];
|
||||
foreach($responseInfosByContainer as $containerInterfaceId => $responseInfos){
|
||||
$responseContainerInfos[] = new ItemStackResponseContainerInfo($containerInterfaceId, $responseInfos);
|
||||
}
|
||||
|
||||
return new ItemStackResponse(ItemStackResponse::RESULT_OK, $this->requestId, $responseContainerInfos);
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ namespace pocketmine\network\mcpe\handler;
|
||||
|
||||
use pocketmine\entity\InvalidSkinException;
|
||||
use pocketmine\event\player\PlayerPreLoginEvent;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\lang\KnownTranslationKeys;
|
||||
use pocketmine\network\mcpe\auth\ProcessLoginTask;
|
||||
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
|
||||
@ -33,8 +32,6 @@ use pocketmine\network\mcpe\JwtException;
|
||||
use pocketmine\network\mcpe\JwtUtils;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\LoginPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
|
||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\ClientData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
|
||||
@ -46,7 +43,6 @@ use pocketmine\player\XboxLivePlayerInfo;
|
||||
use pocketmine\Server;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use function is_array;
|
||||
use function preg_match;
|
||||
|
||||
/**
|
||||
* Handles the initial login phase of the session. This handler is used as the initial state.
|
||||
@ -64,18 +60,6 @@ class LoginPacketHandler extends PacketHandler{
|
||||
){}
|
||||
|
||||
public function handleLogin(LoginPacket $packet) : bool{
|
||||
if(!$this->isCompatibleProtocol($packet->protocol)){
|
||||
$this->session->sendDataPacket(PlayStatusPacket::create($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);
|
||||
|
||||
//This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client)
|
||||
$this->session->disconnect(
|
||||
$this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((string) $packet->protocol)),
|
||||
false
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$extraData = $this->fetchAuthData($packet->chainDataJwt);
|
||||
|
||||
if(!Player::isValidUserName($extraData->displayName)){
|
||||
@ -86,26 +70,6 @@ class LoginPacketHandler extends PacketHandler{
|
||||
|
||||
$clientData = $this->parseClientData($packet->clientDataJwt);
|
||||
|
||||
//TODO: REMOVE THIS
|
||||
//Mojang forgot to bump the protocol version when they changed protocol in 1.19.62. Check the game version instead.
|
||||
if(preg_match('/^(\d+)\.(\d+)\.(\d+)/', $clientData->GameVersion, $matches) !== 1){
|
||||
throw new PacketHandlingException("Invalid game version format, expected at least 3 digits");
|
||||
}
|
||||
$major = (int) $matches[1];
|
||||
$minor = (int) $matches[2];
|
||||
$patch = (int) $matches[3];
|
||||
if($major === 1 && $minor === 19 && $patch < 62){
|
||||
$this->session->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::LOGIN_FAILED_CLIENT), true);
|
||||
|
||||
//This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client)
|
||||
$this->session->disconnect(
|
||||
$this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol("$packet->protocol (< v1.19.62)")),
|
||||
false
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try{
|
||||
$skin = SkinAdapterSingleton::get()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData));
|
||||
}catch(\InvalidArgumentException | InvalidSkinException $e){
|
||||
@ -237,8 +201,4 @@ class LoginPacketHandler extends PacketHandler{
|
||||
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet->chainDataJwt->chain, $packet->clientDataJwt, $authRequired, $this->authCallback));
|
||||
$this->session->setHandler(null); //drop packets received during login verification
|
||||
}
|
||||
|
||||
protected function isCompatibleProtocol(int $protocolVersion) : bool{
|
||||
return $protocolVersion === ProtocolInfo::CURRENT_PROTOCOL;
|
||||
}
|
||||
}
|
||||
|
@ -39,11 +39,13 @@ use pocketmine\network\mcpe\protocol\types\CacheableNbt;
|
||||
use pocketmine\network\mcpe\protocol\types\DimensionIds;
|
||||
use pocketmine\network\mcpe\protocol\types\Experiments;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSettings;
|
||||
use pocketmine\network\mcpe\protocol\types\NetworkPermissions;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerMovementSettings;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerMovementType;
|
||||
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\VersionInfo;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use function sprintf;
|
||||
@ -60,89 +62,96 @@ class PreSpawnPacketHandler extends PacketHandler{
|
||||
){}
|
||||
|
||||
public function setUp() : void{
|
||||
$location = $this->player->getLocation();
|
||||
$world = $location->getWorld();
|
||||
Timings::$playerNetworkSendPreSpawnGameData->startTiming();
|
||||
try{
|
||||
$location = $this->player->getLocation();
|
||||
$world = $location->getWorld();
|
||||
|
||||
$this->session->getLogger()->debug("Preparing StartGamePacket");
|
||||
$levelSettings = new LevelSettings();
|
||||
$levelSettings->seed = -1;
|
||||
$levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
|
||||
$levelSettings->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode());
|
||||
$levelSettings->difficulty = $world->getDifficulty();
|
||||
$levelSettings->spawnPosition = BlockPosition::fromVector3($world->getSpawnLocation());
|
||||
$levelSettings->hasAchievementsDisabled = true;
|
||||
$levelSettings->time = $world->getTime();
|
||||
$levelSettings->eduEditionOffer = 0;
|
||||
$levelSettings->rainLevel = 0; //TODO: implement these properly
|
||||
$levelSettings->lightningLevel = 0;
|
||||
$levelSettings->commandsEnabled = true;
|
||||
$levelSettings->gameRules = [
|
||||
"naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration
|
||||
];
|
||||
$levelSettings->experiments = new Experiments([], false);
|
||||
$this->session->getLogger()->debug("Preparing StartGamePacket");
|
||||
$levelSettings = new LevelSettings();
|
||||
$levelSettings->seed = -1;
|
||||
$levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
|
||||
$levelSettings->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode());
|
||||
$levelSettings->difficulty = $world->getDifficulty();
|
||||
$levelSettings->spawnPosition = BlockPosition::fromVector3($world->getSpawnLocation());
|
||||
$levelSettings->hasAchievementsDisabled = true;
|
||||
$levelSettings->time = $world->getTime();
|
||||
$levelSettings->eduEditionOffer = 0;
|
||||
$levelSettings->rainLevel = 0; //TODO: implement these properly
|
||||
$levelSettings->lightningLevel = 0;
|
||||
$levelSettings->commandsEnabled = true;
|
||||
$levelSettings->gameRules = [
|
||||
"naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration
|
||||
];
|
||||
$levelSettings->experiments = new Experiments([], false);
|
||||
|
||||
$this->session->sendDataPacket(StartGamePacket::create(
|
||||
$this->player->getId(),
|
||||
$this->player->getId(),
|
||||
TypeConverter::getInstance()->coreGameModeToProtocol($this->player->getGamemode()),
|
||||
$this->player->getOffsetPosition($location),
|
||||
$location->pitch,
|
||||
$location->yaw,
|
||||
new CacheableNbt(CompoundTag::create()), //TODO: we don't care about this right now
|
||||
$levelSettings,
|
||||
"",
|
||||
$this->server->getMotd(),
|
||||
"",
|
||||
false,
|
||||
new PlayerMovementSettings(PlayerMovementType::SERVER_AUTHORITATIVE_V1, 0, false),
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
false,
|
||||
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
|
||||
Uuid::fromString(Uuid::NIL),
|
||||
false,
|
||||
[],
|
||||
0,
|
||||
GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries(),
|
||||
));
|
||||
$this->session->sendDataPacket(StartGamePacket::create(
|
||||
$this->player->getId(),
|
||||
$this->player->getId(),
|
||||
TypeConverter::getInstance()->coreGameModeToProtocol($this->player->getGamemode()),
|
||||
$this->player->getOffsetPosition($location),
|
||||
$location->pitch,
|
||||
$location->yaw,
|
||||
new CacheableNbt(CompoundTag::create()), //TODO: we don't care about this right now
|
||||
$levelSettings,
|
||||
"",
|
||||
$this->server->getMotd(),
|
||||
"",
|
||||
false,
|
||||
new PlayerMovementSettings(PlayerMovementType::SERVER_AUTHORITATIVE_V1, 0, false),
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
true,
|
||||
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
|
||||
Uuid::fromString(Uuid::NIL),
|
||||
false,
|
||||
false,
|
||||
new NetworkPermissions(disableClientSounds: true),
|
||||
[],
|
||||
0,
|
||||
GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries(),
|
||||
));
|
||||
|
||||
$this->session->getLogger()->debug("Sending actor identifiers");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers());
|
||||
$this->session->getLogger()->debug("Sending actor identifiers");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers());
|
||||
|
||||
$this->session->getLogger()->debug("Sending biome definitions");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getBiomeDefs());
|
||||
$this->session->getLogger()->debug("Sending biome definitions");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getBiomeDefs());
|
||||
|
||||
$this->session->getLogger()->debug("Sending attributes");
|
||||
$this->session->syncAttributes($this->player, $this->player->getAttributeMap()->getAll());
|
||||
$this->session->getLogger()->debug("Sending attributes");
|
||||
$this->session->getEntityEventBroadcaster()->syncAttributes([$this->session], $this->player, $this->player->getAttributeMap()->getAll());
|
||||
|
||||
$this->session->getLogger()->debug("Sending available commands");
|
||||
$this->session->syncAvailableCommands();
|
||||
$this->session->getLogger()->debug("Sending available commands");
|
||||
$this->session->syncAvailableCommands();
|
||||
|
||||
$this->session->getLogger()->debug("Sending abilities");
|
||||
$this->session->syncAbilities($this->player);
|
||||
$this->session->syncAdventureSettings();
|
||||
$this->session->getLogger()->debug("Sending abilities");
|
||||
$this->session->syncAbilities($this->player);
|
||||
$this->session->syncAdventureSettings();
|
||||
|
||||
$this->session->getLogger()->debug("Sending effects");
|
||||
foreach($this->player->getEffects()->all() as $effect){
|
||||
$this->session->onEntityEffectAdded($this->player, $effect, false);
|
||||
$this->session->getLogger()->debug("Sending effects");
|
||||
foreach($this->player->getEffects()->all() as $effect){
|
||||
$this->session->getEntityEventBroadcaster()->onEntityEffectAdded([$this->session], $this->player, $effect, false);
|
||||
}
|
||||
|
||||
$this->session->getLogger()->debug("Sending actor metadata");
|
||||
$this->player->sendData([$this->player]);
|
||||
|
||||
$this->session->getLogger()->debug("Sending inventory");
|
||||
$this->inventoryManager->syncAll();
|
||||
$this->inventoryManager->syncSelectedHotbarSlot();
|
||||
|
||||
$this->session->getLogger()->debug("Sending creative inventory data");
|
||||
$this->inventoryManager->syncCreative();
|
||||
|
||||
$this->session->getLogger()->debug("Sending crafting data");
|
||||
$this->session->sendDataPacket(CraftingDataCache::getInstance()->getCache($this->server->getCraftingManager()));
|
||||
|
||||
$this->session->getLogger()->debug("Sending player list");
|
||||
$this->session->syncPlayerList($this->server->getOnlinePlayers());
|
||||
}finally{
|
||||
Timings::$playerNetworkSendPreSpawnGameData->stopTiming();
|
||||
}
|
||||
|
||||
$this->session->getLogger()->debug("Sending actor metadata");
|
||||
$this->player->sendData([$this->player]);
|
||||
|
||||
$this->session->getLogger()->debug("Sending inventory");
|
||||
$this->inventoryManager->syncAll();
|
||||
$this->inventoryManager->syncSelectedHotbarSlot();
|
||||
|
||||
$this->session->getLogger()->debug("Sending creative inventory data");
|
||||
$this->inventoryManager->syncCreative();
|
||||
|
||||
$this->session->getLogger()->debug("Sending crafting data");
|
||||
$this->session->sendDataPacket(CraftingDataCache::getInstance()->getCache($this->server->getCraftingManager()));
|
||||
|
||||
$this->session->getLogger()->debug("Sending player list");
|
||||
$this->session->syncPlayerList($this->server->getOnlinePlayers());
|
||||
}
|
||||
|
||||
public function handleRequestChunkRadius(RequestChunkRadiusPacket $packet) : bool{
|
||||
|
@ -26,11 +26,12 @@ namespace pocketmine\network\mcpe\raklib;
|
||||
use pocketmine\network\AdvancedNetworkInterface;
|
||||
use pocketmine\network\mcpe\compression\ZlibCompressor;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\PacketBroadcaster;
|
||||
use pocketmine\network\mcpe\protocol\PacketPool;
|
||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\network\mcpe\StandardPacketBroadcaster;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
|
||||
use pocketmine\network\Network;
|
||||
use pocketmine\network\NetworkInterfaceStartException;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
@ -52,6 +53,7 @@ use function implode;
|
||||
use function mt_rand;
|
||||
use function random_bytes;
|
||||
use function rtrim;
|
||||
use function str_split;
|
||||
use function substr;
|
||||
use const PHP_INT_MAX;
|
||||
|
||||
@ -78,10 +80,16 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
|
||||
|
||||
private SleeperNotifier $sleeper;
|
||||
|
||||
private PacketBroadcaster $broadcaster;
|
||||
private PacketBroadcaster $packetBroadcaster;
|
||||
private EntityEventBroadcaster $entityEventBroadcaster;
|
||||
private PacketSerializerContext $packetSerializerContext;
|
||||
|
||||
public function __construct(Server $server, string $ip, int $port, bool $ipV6){
|
||||
public function __construct(Server $server, string $ip, int $port, bool $ipV6, PacketBroadcaster $packetBroadcaster, EntityEventBroadcaster $entityEventBroadcaster, PacketSerializerContext $packetSerializerContext){
|
||||
$this->server = $server;
|
||||
$this->packetBroadcaster = $packetBroadcaster;
|
||||
$this->packetSerializerContext = $packetSerializerContext;
|
||||
$this->entityEventBroadcaster = $entityEventBroadcaster;
|
||||
|
||||
$this->rakServerId = mt_rand(0, PHP_INT_MAX);
|
||||
|
||||
$this->sleeper = new SleeperNotifier();
|
||||
@ -105,8 +113,6 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
|
||||
$this->interface = new UserToRakLibThreadMessageSender(
|
||||
new PthreadsChannelWriter($mainToThreadBuffer)
|
||||
);
|
||||
|
||||
$this->broadcaster = new StandardPacketBroadcaster($this->server);
|
||||
}
|
||||
|
||||
public function start() : void{
|
||||
@ -166,8 +172,10 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
|
||||
$this->server,
|
||||
$this->network->getSessionManager(),
|
||||
PacketPool::getInstance(),
|
||||
$this->packetSerializerContext,
|
||||
new RakLibPacketSender($sessionId, $this),
|
||||
$this->broadcaster,
|
||||
$this->packetBroadcaster,
|
||||
$this->entityEventBroadcaster,
|
||||
ZlibCompressor::getInstance(), //TODO: this shouldn't be hardcoded, but we might need the RakNet protocol version to select it
|
||||
$address,
|
||||
$port
|
||||
@ -185,10 +193,11 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
|
||||
$session = $this->sessions[$sessionId];
|
||||
$address = $session->getIp();
|
||||
$buf = substr($packet, 1);
|
||||
$name = $session->getDisplayName();
|
||||
try{
|
||||
$session->handleEncoded($buf);
|
||||
}catch(PacketHandlingException $e){
|
||||
$errorId = bin2hex(random_bytes(6));
|
||||
$errorId = implode("-", str_split(bin2hex(random_bytes(6)), 4));
|
||||
|
||||
$logger = $session->getLogger();
|
||||
$logger->error("Bad packet (error ID $errorId): " . $e->getMessage());
|
||||
@ -197,6 +206,10 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
|
||||
$logger->debug(implode("\n", Utils::printableExceptionInfo($e)));
|
||||
$session->disconnect("Packet processing error (Error ID: $errorId)");
|
||||
$this->interface->blockAddress($address, 5);
|
||||
}catch(\Throwable $e){
|
||||
//record the name of the player who caused the crash, to make it easier to find the reproducing steps
|
||||
$this->server->getLogger()->emergency("Crash occurred while handling a packet from session: $name");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,23 +54,23 @@ final class ChunkSelector{
|
||||
//If the chunk is in the radius, others at the same offsets in different quadrants are also guaranteed to be.
|
||||
|
||||
/* Top right quadrant */
|
||||
yield World::chunkHash($centerX + $x, $centerZ + $z);
|
||||
yield $subRadius => World::chunkHash($centerX + $x, $centerZ + $z);
|
||||
/* Top left quadrant */
|
||||
yield World::chunkHash($centerX - $x - 1, $centerZ + $z);
|
||||
yield $subRadius => World::chunkHash($centerX - $x - 1, $centerZ + $z);
|
||||
/* Bottom right quadrant */
|
||||
yield World::chunkHash($centerX + $x, $centerZ - $z - 1);
|
||||
yield $subRadius => World::chunkHash($centerX + $x, $centerZ - $z - 1);
|
||||
/* Bottom left quadrant */
|
||||
yield World::chunkHash($centerX - $x - 1, $centerZ - $z - 1);
|
||||
yield $subRadius => World::chunkHash($centerX - $x - 1, $centerZ - $z - 1);
|
||||
|
||||
if($x !== $z){
|
||||
/* Top right quadrant mirror */
|
||||
yield World::chunkHash($centerX + $z, $centerZ + $x);
|
||||
yield $subRadius => World::chunkHash($centerX + $z, $centerZ + $x);
|
||||
/* Top left quadrant mirror */
|
||||
yield World::chunkHash($centerX - $z - 1, $centerZ + $x);
|
||||
yield $subRadius => World::chunkHash($centerX - $z - 1, $centerZ + $x);
|
||||
/* Bottom right quadrant mirror */
|
||||
yield World::chunkHash($centerX + $z, $centerZ - $x - 1);
|
||||
yield $subRadius => World::chunkHash($centerX + $z, $centerZ - $x - 1);
|
||||
/* Bottom left quadrant mirror */
|
||||
yield World::chunkHash($centerX - $z - 1, $centerZ - $x - 1);
|
||||
yield $subRadius => World::chunkHash($centerX - $z - 1, $centerZ - $x - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user