Compare commits

..

2 Commits

Author SHA1 Message Date
9946ea9e8a get rid of the old hack 2025-05-28 23:07:02 +01:00
14b70c04ad NetworkSession: add support for discarding repeated packets before they're decoded
this dramatically reduces the server workload dealing with spammy packets like right-click interactions trigger.
It also solves the problem of players getting kicked for right-clicking for too long.
2025-05-28 23:00:46 +01:00
21 changed files with 208 additions and 342 deletions

View File

@ -53,7 +53,7 @@ jobs:
run: echo NAME=$(echo "${GITHUB_REPOSITORY,,}") >> $GITHUB_OUTPUT
- name: Build image for tag
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v6.16.0
with:
push: true
context: ./pocketmine-mp
@ -66,7 +66,7 @@ jobs:
- name: Build image for major tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v6.16.0
with:
push: true
context: ./pocketmine-mp
@ -79,7 +79,7 @@ jobs:
- name: Build image for minor tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v6.16.0
with:
push: true
context: ./pocketmine-mp
@ -92,7 +92,7 @@ jobs:
- name: Build image for latest tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v6.16.0
with:
push: true
context: ./pocketmine-mp

View File

@ -188,4 +188,4 @@ jobs:
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v3
with:
message: "${{ vars.DRAFT_RELEASE_NOTIFICATION_MENTION }} [Draft release ${{ steps.get-pm-version.outputs.PM_VERSION }}](${{ steps.create-draft.outputs.html_url }}) has been created for commit ${{ github.sha }}. Please review and publish it."
message: "[Draft release ${{ steps.get-pm-version.outputs.PM_VERSION }}](${{ steps.create-draft.outputs.html_url }}) has been created for commit ${{ github.sha }}. Please review and publish it."

View File

@ -1,25 +0,0 @@
# 5.29.0
Released 18th June 2025.
This is a support release for Minecraft: Bedrock Edition 1.21.90.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` 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.
## General
- Added support for Minecraft: Bedrock Edition 1.21.90.
- Removed support for earlier versions.
## Fixes
- Fixed thread crashes sometimes not reporting proper cause information in crashdumps.
- Fixed crash when a plugin replaced a player's held tool with a different tool with a damage exceeding the old tool's max damage during an action.
- Fixed performance issue of `PlayerAuthInputPacket` input flags handling (broken change detection).
- Fixed `BaseInventory->addItem()` triggering updates on empty slots when no items were added.
- Fixed slow check in `SubChunk` block layer garbage collection.
## Internals
- `LoginPacketHandler->processLogin()` signature has changed. This will break any plugins overriding `LoginPacketHandler`. As noted above, this is _not_ covered by the API version guarantee.
- Automated branch sync for `minor-next` and `major-next` is now triggered by `repository_dispatch` from a cron job in this repository instead of `RestrictedActions`. The `RestrictedActions` cron job was getting automatically disabled by GitHub due to repo inactivity.

View File

@ -1,55 +0,0 @@
# 5.30.0
Released 18th June 2025.
This is a minor feature release containing API additions, internals cleanup and user experience improvements.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` 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.
## General
- Significantly reduced log spam when unknown blocks, tiles and entities are found in saved worlds.
- The file name structure for crashdumps has been changed to improve sorting order in file browsers.
- Buffering is now skipped on the RakLib layer. In theory this could reduce player network latency by 10 ms (YMMV).
## Gameplay
### Blocks
- Many blocks have had their hardness and blast resistance updated to match vanilla.
- Implemented Respawn Anchor.
- Melon Stem and Pumpkin Stem drop amounts should now match vanilla (using binomial distribution).
## API
## General
- Verification of save registration has been added for blocks, entities and tiles. This is intended to make it easier to find mistakes when registering custom things, which previously would produce obscure core crashes.
### `pocketmine\event\entity`
- The following classes have been added:
- `EntityExtinguishEvent` - called when a burning entity is extinguished by water or other sources
- `EntityFrostWalkerEvent` - called every tick upon which an entity wearing Frost Walker boots moves; this can be used to customise or cancel the behaviour of the Frost Walker enchantment
### `pocketmine\entity`
- The following methods have been added:
- `public Entity->getStepHeight() : float`
- `public Entity->setStepHeight(float $stepHeight) : void`
### `pocketmine\world\generator`
- Generator execution has been decoupled from `PopulationTask` and async tasks in general. The following classes have been added:
- `executor\GeneratorExecutor`
- `executor\SyncGeneratorExecutor` - runs a generator on the main thread (used for flat world generation, which doesn't need threads)
- `executor\AsyncGeneratorExecutor` - runs a generator inside an async task, as before
- `PopulationUtils` - contains population business logic previously baked into `PopulationTask` - this permits the reuse of that logic outside async tasks
- The following methods have signature changes:
- `GeneratorManager->addGenerator()` now accepts an optional `bool $fast` parameter, defaulting to `false`; setting this to `true` will cause your generator to run on the main thread
- The following methods have been added:
- `public GeneratorManagerEntry->isFast() : bool` - returns whether this generator should run on the main thread
- `PopulationTask` has been marked as `@internal`. In the next major version, it will move to the `generator\executor` namespace; however, for now it stays put because plugins currently have no other way to regenerate chunks.
## Internals
- World data version numbers have been consolidated in `pocketmine\data\bedrock\WorldDataVersions`. This removes the need to modify several different files to support new world versions, and reduces the chances of things getting missed.
- Block hardness and blast resistance is now unit-tested against `block_properties_table.json` in `BedrockData`. This file comes from vanilla BDS, so we can use it to verify compliance.
- Protocol-layer "server auth block breaking" has been enabled. Functionally, this is no different from the previous system, it just works differently on the network layer.
- Various internal classes in the `pocketmine\world\generator` namespace have been moved to the `generator\executor` namespace.
- Removed `World->registerGenerator()` and `World->unregisterGenerator()`.
- Removed redundant calls to `curl_close()` (obsolete since PHP 8.0).

View File

@ -34,9 +34,9 @@
"adhocore/json-comment": "~1.2.0",
"netresearch/jsonmapper": "~v5.0.0",
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
"pocketmine/bedrock-data": "~5.1.0+bedrock-1.21.90",
"pocketmine/bedrock-data": "~5.0.0+bedrock-1.21.80",
"pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50",
"pocketmine/bedrock-protocol": "~39.0.0+bedrock-1.21.90",
"pocketmine/bedrock-protocol": "~38.0.0+bedrock-1.21.80",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
"pocketmine/color": "^0.3.0",
@ -45,14 +45,14 @@
"pocketmine/log": "^0.4.0",
"pocketmine/math": "~1.0.0",
"pocketmine/nbt": "~1.1.0",
"pocketmine/raklib": "~1.2.0",
"pocketmine/raklib": "~1.1.2",
"pocketmine/raklib-ipc": "~1.0.0",
"pocketmine/snooze": "^0.5.0",
"ramsey/uuid": "~4.8.0",
"ramsey/uuid": "~4.7.0",
"symfony/filesystem": "~6.4.0"
},
"require-dev": {
"phpstan/phpstan": "2.1.17",
"phpstan/phpstan": "2.1.16",
"phpstan/phpstan-phpunit": "^2.0.0",
"phpstan/phpstan-strict-rules": "^2.0.0",
"phpunit/phpunit": "^10.5.24"

129
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9bf4984c23f688264d3ce6a729a6ec17",
"content-hash": "b106b34fbd6c8abdfd45931bcb18bb69",
"packages": [
{
"name": "adhocore/json-comment",
@ -67,16 +67,16 @@
},
{
"name": "brick/math",
"version": "0.13.1",
"version": "0.12.3",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04"
"reference": "866551da34e9a618e64a819ee1e01c20d8a588ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04",
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04",
"url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba",
"reference": "866551da34e9a618e64a819ee1e01c20d8a588ba",
"shasum": ""
},
"require": {
@ -115,7 +115,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.13.1"
"source": "https://github.com/brick/math/tree/0.12.3"
},
"funding": [
{
@ -123,7 +123,7 @@
"type": "github"
}
],
"time": "2025-03-29T13:50:30+00:00"
"time": "2025-02-28T13:11:00+00:00"
},
{
"name": "netresearch/jsonmapper",
@ -204,16 +204,16 @@
},
{
"name": "pocketmine/bedrock-data",
"version": "5.1.0+bedrock-1.21.90",
"version": "5.0.0+bedrock-1.21.80",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockData.git",
"reference": "89ed34957aeccc63e517aa849af593adae958e98"
"reference": "e38d5ea19f794ec5216e5f96742237e8c4e7f080"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/89ed34957aeccc63e517aa849af593adae958e98",
"reference": "89ed34957aeccc63e517aa849af593adae958e98",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/e38d5ea19f794ec5216e5f96742237e8c4e7f080",
"reference": "e38d5ea19f794ec5216e5f96742237e8c4e7f080",
"shasum": ""
},
"type": "library",
@ -224,9 +224,9 @@
"description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/BedrockData/issues",
"source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.90"
"source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.80"
},
"time": "2025-06-17T23:44:21+00:00"
"time": "2025-05-09T14:15:18+00:00"
},
{
"name": "pocketmine/bedrock-item-upgrade-schema",
@ -256,16 +256,16 @@
},
{
"name": "pocketmine/bedrock-protocol",
"version": "39.0.0+bedrock-1.21.90",
"version": "38.0.1+bedrock-1.21.80",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "2b088183d12fc003523400867ee398e3893899ed"
"reference": "0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2b088183d12fc003523400867ee398e3893899ed",
"reference": "2b088183d12fc003523400867ee398e3893899ed",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f",
"reference": "0c1c13e970a2e1ded1609d0b442b4fcfd24cd21f",
"shasum": ""
},
"require": {
@ -296,9 +296,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/39.0.0+bedrock-1.21.90"
"source": "https://github.com/pmmp/BedrockProtocol/tree/38.0.1+bedrock-1.21.80"
},
"time": "2025-06-17T23:46:38+00:00"
"time": "2025-05-17T11:56:33+00:00"
},
{
"name": "pocketmine/binaryutils",
@ -618,16 +618,16 @@
},
{
"name": "pocketmine/raklib",
"version": "1.2.0",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/pmmp/RakLib.git",
"reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b"
"reference": "4145a31cd812fe8931c3c9c691fcd2ded2f47e7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/RakLib/zipball/a28d05216d34dbd00e8aed827a58df6b4c11510b",
"reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b",
"url": "https://api.github.com/repos/pmmp/RakLib/zipball/4145a31cd812fe8931c3c9c691fcd2ded2f47e7f",
"reference": "4145a31cd812fe8931c3c9c691fcd2ded2f47e7f",
"shasum": ""
},
"require": {
@ -655,9 +655,9 @@
"description": "A RakNet server implementation written in PHP",
"support": {
"issues": "https://github.com/pmmp/RakLib/issues",
"source": "https://github.com/pmmp/RakLib/tree/1.2.0"
"source": "https://github.com/pmmp/RakLib/tree/1.1.2"
},
"time": "2025-06-08T17:36:06+00:00"
"time": "2025-04-06T03:38:21+00:00"
},
{
"name": "pocketmine/raklib-ipc",
@ -818,20 +818,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.8.1",
"version": "4.7.6",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28"
"reference": "91039bc1faa45ba123c4328958e620d382ec7088"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28",
"reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088",
"reference": "91039bc1faa45ba123c4328958e620d382ec7088",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12",
"ext-json": "*",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
@ -840,23 +840,26 @@
"rhumsaa/uuid": "self.version"
},
"require-dev": {
"captainhook/captainhook": "^5.25",
"captainhook/captainhook": "^5.10",
"captainhook/plugin-composer": "^5.3",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"ergebnis/composer-normalize": "^2.47",
"mockery/mockery": "^1.6",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"doctrine/annotations": "^1.8",
"ergebnis/composer-normalize": "^2.15",
"mockery/mockery": "^1.3",
"paragonie/random-lib": "^2",
"php-mock/php-mock": "^2.6",
"php-mock/php-mock-mockery": "^1.5",
"php-parallel-lint/php-parallel-lint": "^1.4.0",
"phpbench/phpbench": "^1.2.14",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-mockery": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6",
"slevomat/coding-standard": "^8.18",
"squizlabs/php_codesniffer": "^3.13"
"php-mock/php-mock": "^2.2",
"php-mock/php-mock-mockery": "^1.3",
"php-parallel-lint/php-parallel-lint": "^1.1",
"phpbench/phpbench": "^1.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-mockery": "^1.1",
"phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^8.5 || ^9",
"ramsey/composer-repl": "^1.4",
"slevomat/coding-standard": "^8.4",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.9"
},
"suggest": {
"ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
@ -891,9 +894,19 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.8.1"
"source": "https://github.com/ramsey/uuid/tree/4.7.6"
},
"time": "2025-06-01T06:28:46+00:00"
"funding": [
{
"url": "https://github.com/ramsey",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
"type": "tidelift"
}
],
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "symfony/filesystem",
@ -1025,16 +1038,16 @@
},
{
"name": "nikic/php-parser",
"version": "v5.5.0",
"version": "v5.4.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
"reference": "447a020a1f875a434d62f2a401f53b82a396e494"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"shasum": ""
},
"require": {
@ -1077,9 +1090,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
},
"time": "2025-05-31T08:24:38+00:00"
"time": "2024-12-30T11:07:19+00:00"
},
{
"name": "phar-io/manifest",
@ -1201,16 +1214,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.17",
"version": "2.1.16",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
"reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9",
"reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9",
"shasum": ""
},
"require": {
@ -1255,7 +1268,7 @@
"type": "github"
}
],
"time": "2025-05-21T20:55:28+00:00"
"time": "2025-05-16T09:40:10+00:00"
},
{
"name": "phpstan/phpstan-phpunit",

View File

@ -1618,7 +1618,7 @@ class Server{
if(!is_dir($crashFolder)){
mkdir($crashFolder);
}
$crashDumpPath = Path::join($crashFolder, date("Y-m-d_H.i.s_T", (int) $dump->getData()->time) . ".log");
$crashDumpPath = Path::join($crashFolder, date("D_M_j-H.i.s-T_Y", (int) $dump->getData()->time) . ".log");
$fp = @fopen($crashDumpPath, "wb");
if(!is_resource($fp)){

View File

@ -31,8 +31,8 @@ use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.30.0";
public const IS_DEVELOPMENT_BUILD = false;
public const BASE_VERSION = "5.28.3";
public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "stable";
/**

View File

@ -54,12 +54,12 @@ final class WorldDataVersions{
* This may be lower than the current protocol version if PocketMine-MP does not yet support features of the newer
* version. This allows the protocol to be updated independently of world format support.
*/
public const NETWORK = 818;
public const NETWORK = 800;
public const LAST_OPENED_IN = [
1, //major
21, //minor
90, //patch
80, //patch
3, //revision
0 //is beta
];

View File

@ -380,7 +380,6 @@ final class ItemTypeNames{
public const MUSIC_DISC_RELIC = "minecraft:music_disc_relic";
public const MUSIC_DISC_STAL = "minecraft:music_disc_stal";
public const MUSIC_DISC_STRAD = "minecraft:music_disc_strad";
public const MUSIC_DISC_TEARS = "minecraft:music_disc_tears";
public const MUSIC_DISC_WAIT = "minecraft:music_disc_wait";
public const MUSIC_DISC_WARD = "minecraft:music_disc_ward";
public const MUTTON = "minecraft:mutton";

View File

@ -256,7 +256,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
$slotItem->setCount($slotItem->getCount() + $amount);
$this->setItem($i, $slotItem);
if($newItem->getCount() <= 0){
return $newItem;
break;
}
}
}
@ -270,7 +270,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
$slotItem->setCount($amount);
$this->setItem($slotIndex, $slotItem);
if($newItem->getCount() <= 0){
return $newItem;
break;
}
}
}

View File

@ -41,7 +41,6 @@ use pocketmine\inventory\transaction\action\SlotChangeAction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\item\enchantment\EnchantingOption;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\Item;
use pocketmine\network\mcpe\cache\CreativeInventoryCache;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
@ -229,25 +228,17 @@ class InventoryManager{
return null;
}
private function addPredictedSlotChangeInternal(Inventory $inventory, int $slot, ItemStack $item) : void{
private function addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $item) : void{
$this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item;
}
public function addPredictedSlotChange(Inventory $inventory, int $slot, Item $item) : void{
$typeConverter = $this->session->getTypeConverter();
$itemStack = $typeConverter->coreItemStackToNet($item);
$this->addPredictedSlotChangeInternal($inventory, $slot, $itemStack);
}
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
$typeConverter = $this->session->getTypeConverter();
foreach($tx->getActions() as $action){
if($action instanceof SlotChangeAction){
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
$this->addPredictedSlotChange(
$action->getInventory(),
$action->getSlot(),
$action->getTargetItem()
);
$itemStack = $typeConverter->coreItemStackToNet($action->getTargetItem());
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack);
}
}
}
@ -276,7 +267,7 @@ class InventoryManager{
}
[$inventory, $slot] = $info;
$this->addPredictedSlotChangeInternal($inventory, $slot, $action->newItem->getItemStack());
$this->addPredictedSlotChange($inventory, $slot, $action->newItem->getItemStack());
}
}

View File

@ -58,6 +58,7 @@ use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket;
use pocketmine\network\mcpe\protocol\ClientboundCloseFormPacket;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\DisconnectPacket;
use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
use pocketmine\network\mcpe\protocol\ModalFormRequestPacket;
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
use pocketmine\network\mcpe\protocol\NetworkChunkPublisherUpdatePacket;
@ -109,6 +110,7 @@ use pocketmine\promise\PromiseResolver;
use pocketmine\Server;
use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\ObjectSet;
@ -194,6 +196,17 @@ class NetworkSession{
*/
private ObjectSet $disposeHooks;
/**
* @var string[]
* @phpstan-var array<int, string>
*/
private array $repeatedPacketFilters = [];
/**
* @var int[]
* @phpstan-var array<int, int>
*/
private array $repeatedPacketFilterStats = [];
public function __construct(
private Server $server,
private NetworkSessionManager $manager,
@ -221,6 +234,8 @@ class NetworkSession{
$this->onSessionStartSuccess(...)
));
$this->addRepeatedPacketFilter(InventoryTransactionPacket::NETWORK_ID);
$this->manager->add($this);
$this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_open()));
}
@ -350,6 +365,44 @@ class NetworkSession{
}
}
public function addRepeatedPacketFilter(int $packetId) : void{
$this->repeatedPacketFilters[$packetId] = "";
$this->repeatedPacketFilterStats[$packetId] = 0;
}
public function removeRepeatedPacketFilter(int $packetId) : void{
unset($this->repeatedPacketFilters[$packetId]);
unset($this->repeatedPacketFilterStats[$packetId]);
}
/**
* Returns the stats for repeated packet filters, indexed by packet ID.
* The value is the number of times a packet was dropped due to being repeated.
*
* @return int[]
* @phpstan-return array<int, int>
*/
public function getRepeatedPacketFilterStats() : array{
return $this->repeatedPacketFilterStats;
}
private function checkRepeatedPacketFilter(string $buffer) : bool{
//TODO: would be great if we didn't repeat reading the ID inside PacketPool
$dummy = 0;
$packetId = Binary::readUnsignedVarInt($buffer, $dummy);
if(isset($this->repeatedPacketFilters[$packetId])){
if($buffer === $this->repeatedPacketFilters[$packetId]){
$this->repeatedPacketFilterStats[$packetId]++;
return true;
}
$this->repeatedPacketFilters[$packetId] = $buffer;
}
return false;
}
/**
* @throws PacketHandlingException
*/
@ -403,6 +456,10 @@ class NetworkSession{
try{
$stream = new BinaryStream($decompressed);
foreach(PacketBatch::decodeRaw($stream) as $buffer){
if($this->checkRepeatedPacketFilter($buffer)){
continue;
}
$this->gamePacketLimiter->decrement();
$packet = $this->packetPool->getPacket($buffer);
if($packet === null){

View File

@ -116,7 +116,6 @@ use function is_nan;
use function json_decode;
use function max;
use function mb_strlen;
use function microtime;
use function sprintf;
use function str_starts_with;
use function strlen;
@ -128,16 +127,11 @@ 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
protected float $lastRightClickTime = 0.0;
protected ?UseItemTransactionData $lastRightClickData = null;
protected ?Vector3 $lastPlayerAuthInputPosition = null;
protected ?float $lastPlayerAuthInputYaw = null;
protected ?float $lastPlayerAuthInputPitch = null;
protected ?BitSet $lastPlayerAuthInputFlags = null;
protected ?BlockPosition $lastBlockAttacked = null;
public bool $forceMoveSync = false;
protected ?string $lastRequestedFullSkinId = null;
@ -213,7 +207,7 @@ class InGamePacketHandler extends PacketHandler{
}
$inputFlags = $packet->getInputFlags();
if($this->lastPlayerAuthInputFlags === null || !$inputFlags->equals($this->lastPlayerAuthInputFlags)){
if($inputFlags !== $this->lastPlayerAuthInputFlags){
$this->lastPlayerAuthInputFlags = $inputFlags;
$sneaking = $inputFlags->get(PlayerAuthInputFlags::SNEAKING);
@ -250,28 +244,6 @@ class InGamePacketHandler extends PacketHandler{
$packetHandled = true;
$useItemTransaction = $packet->getItemInteractionData();
if($useItemTransaction !== null){
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
throw new PacketHandlingException("Too many actions in item use transaction");
}
$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();
$itemStackResponseBuilder = $itemStackRequest !== null ? $this->handleSingleItemStackRequest($itemStackRequest) : null;
//itemstack request or transaction may set predictions for the outcome of these actions, so these need to be
//processed last
$blockActions = $packet->getBlockActions();
if($blockActions !== null){
if(count($blockActions) > 100){
@ -292,9 +264,27 @@ class InGamePacketHandler extends PacketHandler{
}
}
$useItemTransaction = $packet->getItemInteractionData();
if($useItemTransaction !== null){
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
throw new PacketHandlingException("Too many actions in item use transaction");
}
$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){
$itemStackResponse = $itemStackResponseBuilder?->build() ?? new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $itemStackRequest->getRequestId());
$this->session->sendDataPacket(ItemStackResponsePacket::create([$itemStackResponse]));
$result = $this->handleSingleItemStackRequest($itemStackRequest);
$this->session->sendDataPacket(ItemStackResponsePacket::create([$result]));
}
return $packetHandled;
@ -477,33 +467,24 @@ class InGamePacketHandler extends PacketHandler{
switch($data->getActionType()){
case UseItemTransactionData::ACTION_CLICK_BLOCK:
//TODO: start hack for client spam bug
$clickPos = $data->getClickPosition();
$spamBug = ($this->lastRightClickData !== null &&
microtime(true) - $this->lastRightClickTime < 0.1 && //100ms
$this->lastRightClickData->getPlayerPosition()->distanceSquared($data->getPlayerPosition()) < 0.00001 &&
$this->lastRightClickData->getBlockPosition()->equals($data->getBlockPosition()) &&
$this->lastRightClickData->getClickPosition()->distanceSquared($clickPos) < 0.00001 //signature spam bug has 0 distance, but allow some error
);
//get rid of continued spam if the player clicks and holds right-click
$this->lastRightClickData = $data;
$this->lastRightClickTime = microtime(true);
if($spamBug){
return true;
}
//TODO: end hack for client spam bug
self::validateFacing($data->getFace());
$blockPos = $data->getBlockPosition();
$vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
$this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos);
$this->player->interactBlock($vBlockPos, $data->getFace(), $data->getClickPosition());
//always sync this in case plugins caused a different result than the client expected
//we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating
//more information up the stack. For now I think this is good enough.
//if only the client would tell us what blocks it thinks changed...
$this->syncBlocksNearby($vBlockPos, $data->getFace());
return true;
case UseItemTransactionData::ACTION_BREAK_BLOCK:
$blockPos = $data->getBlockPosition();
$vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
if(!$this->player->breakBlock($vBlockPos)){
$this->syncBlocksNearby($vBlockPos, null);
}
return true;
case UseItemTransactionData::ACTION_CLICK_AIR:
if($this->player->isUsingItem()){
if(!$this->player->consumeHeldItem()){
@ -579,7 +560,7 @@ class InGamePacketHandler extends PacketHandler{
return false;
}
private function handleSingleItemStackRequest(ItemStackRequest $request) : ?ItemStackResponseBuilder{
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
@ -596,11 +577,7 @@ class InGamePacketHandler extends PacketHandler{
$executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $request);
try{
$transaction = $executor->generateInventoryTransaction();
if($transaction !== null){
$result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
}else{
$result = true; //predictions only, just send responses
}
$result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
}catch(ItemStackRequestProcessException $e){
$result = false;
$this->session->getLogger()->debug("ItemStackRequest #" . $request->getRequestId() . " failed: " . $e->getMessage());
@ -608,7 +585,10 @@ class InGamePacketHandler extends PacketHandler{
$this->inventoryManager->requestSyncAll();
}
return $result ? $executor->getItemStackResponseBuilder() : null;
if(!$result){
return new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId());
}
return $executor->buildItemStackResponse();
}
public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{
@ -618,7 +598,7 @@ class InGamePacketHandler extends PacketHandler{
throw new PacketHandlingException("Too many requests in ItemStackRequestPacket");
}
foreach($packet->getRequests() as $request){
$responses[] = $this->handleSingleItemStackRequest($request)?->build() ?? new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId());
$responses[] = $this->handleSingleItemStackRequest($request);
}
$this->session->sendDataPacket(ItemStackResponsePacket::create($responses));
@ -681,27 +661,16 @@ class InGamePacketHandler extends PacketHandler{
switch($action){
case PlayerAction::START_BREAK:
case PlayerAction::CONTINUE_DESTROY_BLOCK: //destroy the next block while holding down left click
self::validateFacing($face);
if($this->lastBlockAttacked !== null && $blockPosition->equals($this->lastBlockAttacked)){
//the client will send CONTINUE_DESTROY_BLOCK for the currently targeted block directly before it
//sends PREDICT_DESTROY_BLOCK, but also when it starts to break the block
//this seems like a bug in the client and would cause spurious left-click events if we allowed it to
//be delivered to the player
$this->session->getLogger()->debug("Ignoring PlayerAction $action on $pos because we were already destroying this block");
break;
}
if(!$this->player->attackBlock($pos, $face)){
$this->syncBlocksNearby($pos, $face);
}
$this->lastBlockAttacked = $blockPosition;
break;
case PlayerAction::ABORT_BREAK:
case PlayerAction::STOP_BREAK:
$this->player->stopBreakBlock($pos);
$this->lastBlockAttacked = null;
break;
case PlayerAction::START_SLEEPING:
//unused
@ -712,17 +681,11 @@ class InGamePacketHandler extends PacketHandler{
case PlayerAction::CRACK_BREAK:
self::validateFacing($face);
$this->player->continueBreakBlock($pos, $face);
$this->lastBlockAttacked = $blockPosition;
break;
case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now)
break;
case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK:
//TODO: do we need to handle this?
case PlayerAction::PREDICT_DESTROY_BLOCK:
if(!$this->player->breakBlock($pos)){
$this->syncBlocksNearby($pos, $face);
}
$this->lastBlockAttacked = null;
break;
case PlayerAction::START_ITEM_USE_ON:
case PlayerAction::STOP_ITEM_USE_ON:

View File

@ -33,11 +33,9 @@ use pocketmine\inventory\transaction\EnchantingTransaction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\inventory\transaction\TransactionBuilder;
use pocketmine\inventory\transaction\TransactionBuilderInventory;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
use pocketmine\network\mcpe\protocol\types\inventory\FullContainerName;
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;
@ -49,7 +47,6 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DropStackReque
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\MineBlockStackRequestAction;
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;
@ -365,16 +362,6 @@ class ItemStackRequestExecutor{
$this->setNextCreatedItem($nextResultItem);
}elseif($action instanceof DeprecatedCraftingResultsStackRequestAction){
//no obvious use
}elseif($action instanceof MineBlockStackRequestAction){
$slot = $action->getHotbarSlot();
$this->requestSlotInfos[] = new ItemStackRequestSlotInfo(new FullContainerName(ContainerUIIds::HOTBAR), $slot, $action->getStackId());
$inventory = $this->player->getInventory();
$usedItem = $inventory->slotExists($slot) ? $inventory->getItem($slot) : null;
$predictedDamage = $action->getPredictedDurability();
if($usedItem instanceof Durable && $predictedDamage >= 0 && $predictedDamage <= $usedItem->getMaxDurability()){
$usedItem->setDamage($predictedDamage);
$this->inventoryManager->addPredictedSlotChange($inventory, $slot, $usedItem);
}
}else{
throw new ItemStackRequestProcessException("Unhandled item stack request action");
}
@ -383,7 +370,7 @@ class ItemStackRequestExecutor{
/**
* @throws ItemStackRequestProcessException
*/
public function generateInventoryTransaction() : ?InventoryTransaction{
public function generateInventoryTransaction() : InventoryTransaction{
foreach(Utils::promoteKeys($this->request->getActions()) as $k => $action){
try{
$this->processItemStackRequestAction($action);
@ -393,9 +380,6 @@ class ItemStackRequestExecutor{
}
$this->setNextCreatedItem(null);
$inventoryActions = $this->builder->generateActions();
if(count($inventoryActions) === 0){
return null;
}
$transaction = $this->specialTransaction ?? new InventoryTransaction($this->player);
foreach($inventoryActions as $action){
@ -405,16 +389,12 @@ class ItemStackRequestExecutor{
return $transaction;
}
public function getItemStackResponseBuilder() : ItemStackResponseBuilder{
public function buildItemStackResponse() : ItemStackResponse{
$builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager);
foreach($this->requestSlotInfos as $requestInfo){
$builder->addSlot($requestInfo->getContainerName()->getContainerId(), $requestInfo->getSlotId());
}
return $builder;
}
public function buildItemStackResponse() : ItemStackResponse{
return $this->getItemStackResponseBuilder()->build();
return $builder->build();
}
}

View File

@ -33,8 +33,6 @@ use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
@ -44,11 +42,7 @@ use pocketmine\player\PlayerInfo;
use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\Server;
use Ramsey\Uuid\Uuid;
use function gettype;
use function is_array;
use function is_object;
use function json_decode;
use const JSON_THROW_ON_ERROR;
/**
* Handles the initial login phase of the session. This handler is used as the initial state.
@ -66,9 +60,7 @@ class LoginPacketHandler extends PacketHandler{
){}
public function handleLogin(LoginPacket $packet) : bool{
$authInfo = $this->parseAuthInfo($packet->authInfoJson);
$jwtChain = $this->parseJwtChain($authInfo->Certificate);
$extraData = $this->fetchAuthData($jwtChain);
$extraData = $this->fetchAuthData($packet->chainDataJwt);
if(!Player::isValidUserName($extraData->displayName)){
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
@ -147,61 +139,11 @@ class LoginPacketHandler extends PacketHandler{
return true;
}
$this->processLogin($authInfo->Token, AuthenticationType::from($authInfo->AuthenticationType), $jwtChain->chain, $packet->clientDataJwt, $ev->isAuthRequired());
$this->processLogin($packet, $ev->isAuthRequired());
return true;
}
/**
* @throws PacketHandlingException
*/
protected function parseAuthInfo(string $authInfo) : AuthenticationInfo{
try{
$authInfoJson = json_decode($authInfo, associative: false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e);
}
if(!is_object($authInfoJson)){
throw new \RuntimeException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{
$clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
return $clientData;
}
/**
* @throws PacketHandlingException
*/
protected function parseJwtChain(string $chainDataJwt) : JwtChain{
try{
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e);
}
if(!is_object($jwtChainJson)){
throw new \RuntimeException("Unexpected type for JWT chain data: " . gettype($jwtChainJson) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{
$clientData = $mapper->map($jwtChainJson, new JwtChain());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
return $clientData;
}
/**
* @throws PacketHandlingException
*/
@ -269,15 +211,10 @@ class LoginPacketHandler extends PacketHandler{
* TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication.
* In the future this won't be necessary.
*
* @param null|string[] $legacyCertificate
*
* @throws \InvalidArgumentException
*/
protected function processLogin(string $token, AuthenticationType $authType, ?array $legacyCertificate, string $clientData, bool $authRequired) : void{
if($legacyCertificate === null){
throw new PacketHandlingException("Legacy certificate cannot be null");
}
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($legacyCertificate, $clientData, $authRequired, $this->authCallback));
protected function processLogin(LoginPacket $packet, bool $authRequired) : void{
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet->chainDataJwt->chain, $packet->clientDataJwt, $authRequired, $this->authCallback));
$this->session->setHandler(null); //drop packets received during login verification
}
}

View File

@ -40,6 +40,7 @@ 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\ServerAuthMovementMode;
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
use pocketmine\player\Player;
use pocketmine\Server;
@ -98,7 +99,7 @@ class PreSpawnPacketHandler extends PacketHandler{
$this->server->getMotd(),
"",
false,
new PlayerMovementSettings(0, true),
new PlayerMovementSettings(ServerAuthMovementMode::SERVER_AUTHORITATIVE_V2, 0, false),
0,
0,
"",

View File

@ -120,8 +120,7 @@ class ResourcePacksPacketHandler extends PacketHandler{
hasAddons: false,
hasScripts: false,
worldTemplateId: Uuid::fromString(Uuid::NIL),
worldTemplateVersion: "",
forceDisableVibrantVisuals: true,
worldTemplateVersion: ""
));
$this->session->getLogger()->debug("Waiting for client to accept resource packs");
}

View File

@ -1644,10 +1644,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$newReplica = clone $oldHeldItem;
$newReplica->setCount($newHeldItem->getCount());
if($newReplica instanceof Durable && $newHeldItem instanceof Durable){
$newDamage = $newHeldItem->getDamage();
if($newDamage >= 0 && $newDamage <= $newReplica->getMaxDurability()){
$newReplica->setDamage($newDamage);
}
$newReplica->setDamage($newHeldItem->getDamage());
}
$damagedOrDeducted = $newReplica->equalsExact($newHeldItem);

View File

@ -134,8 +134,11 @@ class SubChunk{
foreach($this->blockLayers as $layer){
$layer->collectGarbage();
if($layer->getBitsPerBlock() !== 0 || $layer->get(0, 0, 0) !== $this->emptyBlockId){
$cleanedLayers[] = $layer;
foreach($layer->getPalette() as $p){
if($p !== $this->emptyBlockId){
$cleanedLayers[] = $layer;
continue 2;
}
}
}
$this->blockLayers = $cleanedLayers;

View File

@ -702,6 +702,12 @@ parameters:
count: 1
path: ../../../src/inventory/transaction/InventoryTransaction.php
-
message: '#^Cannot cast mixed to int\.$#'
identifier: cast.int
count: 2
path: ../../../src/item/Item.php
-
message: '#^Parameter \#1 \$buffer of method pocketmine\\nbt\\BaseNbtSerializer\:\:read\(\) expects string, mixed given\.$#'
identifier: argument.type