Compare commits

..

71 Commits

Author SHA1 Message Date
66a4c4c88b Release 4.18.0-ALPHA2 2023-03-21 00:26:19 +00:00
1a9322c00a ItemStackRequestExecutor: added some missing @throws 2023-03-21 00:23:31 +00:00
c8d9477da1 ItemStackRequestExecutor: make non-final, and make some stuff protected
this allows for plugin extension, for example to implement anvils.
2023-03-21 00:22:21 +00:00
08e8ef275f remove comment 2023-03-21 00:17:24 +00:00
e57fbff28c ItemStackRequestExecutor: added a sanity check for recipe repetitions 2023-03-21 00:16:03 +00:00
f90315c4a2 ItemStackRequestExecutor: harden against invalid item counts
these cases should all be impossible, but that's assuming that the core code doesn't start using them for a different purpose in the future.
2023-03-21 00:13:21 +00:00
955f7944bb ItemStackRequestExecutor: fixed another possible crash condition 2023-03-21 00:06:33 +00:00
ccd288d7fa Avoid repeated calls to getItemInHand() in drop item handler 2023-03-21 00:04:29 +00:00
097632902a InGamePacketHandler: fixed crash condition in drop item handler 2023-03-21 00:02:32 +00:00
e7771d76f2 Cover buffered inventory sync in timings 2023-03-20 23:29:02 +00:00
ecc830a689 InventoryManager: avoid calling TypeConverter::getInstance() in a loop 2023-03-20 23:24:52 +00:00
ee72e80fbb ItemStackResponseBuilder: removed incorrect code
the client expects that all itemstacks must be acked by ItemStackResponse, regardless of whether the server changed them to some other item.
We'll overwrite the item to the correct thing at the end of the tick anyway.
2023-03-20 23:21:24 +00:00
63310cf764 Do not cache ItemStacks for every item
this is very memory inefficient, and only provides a performance advantage in cold code anyway.
2023-03-20 23:18:43 +00:00
1992d3b6db InventoryManager: avoid useless work in trackItemStack()
this attempts to accommodate slots being set to themselves, which is a rare enough occurrence (only plugins will cause it) that it doesn't make sense to penalize every inventory update this way.
attempting to avoid changing the itemstackID in this way is detrimental to performance, and it doesn't actually matter if we set a new itemstackID anyway.
2023-03-20 23:08:17 +00:00
035a0a4e9d InventoryManager: specialize trackItemStack() to avoid useless lookups 2023-03-20 22:57:58 +00:00
23ea721164 Reduce packets-per-batch limit to 100
this should be well in excess of requirements with the ItemStackRequest system in use.
2023-03-20 22:15:02 +00:00
8408da8534 Merge branch 'item-stack-request' into minor-next 2023-03-20 22:05:50 +00:00
c9601ae67d Fixed crash when opening crafting table and other 'UI' inventories 2023-03-20 22:00:38 +00:00
758b5ee500 InventoryManager: fixed armor slots hack
the correct condition for this should be an unsynced armor slot changed during a transaction, but conveying this information to syncSlot() is a bit of a hassle, so this will do for now.
2023-03-20 21:27:56 +00:00
ca6d51498f Buffer slot and content syncing until the end of the tick
we may receive multiple requests in one tick (e.g. crafting in a batch)
2023-03-20 19:16:00 +00:00
e8085e22a0 Fixed crash when opening main inventory
the InventoryManagerEntry was getting overwritten, since we don't expect to open the same inventory with two different window IDs.
2023-03-20 18:40:18 +00:00
a83fc85f1e InventoryManagerEntry: fixed missing default 2023-03-20 17:32:44 +00:00
3d70a169e1 Reduce chaos in InventoryManager
the information in these arrays is usually needed all at the same time, so it doesn't make sense to force multiple array lookups for it.

in addition, this (obviously) cleans up the code quite a lot.
2023-03-20 17:31:54 +00:00
6ccb8f7373 git 2023-03-20 16:57:38 +00:00
59bae9b077 Give InventoryManager internals clearer names
and stop mixing 'window' and 'inventory' terminology...
2023-03-20 16:53:57 +00:00
2751e1ec02 replacing new Vector3(0, 0, 0) with Vector3::zero() (#5640) 2023-03-20 12:54:28 +00:00
c91168db66 ... 2023-03-20 01:35:15 +00:00
4e55433ed8 Fixed request rejecting 2023-03-20 01:35:03 +00:00
eece6c4433 InGamePacketHandler: remove dead code 2023-03-20 01:28:18 +00:00
67b7b60d18 .............. 2023-03-20 01:19:07 +00:00
804feedb67 Added some dumb limits 2023-03-20 00:54:33 +00:00
d57aca1367 CS 2023-03-20 00:53:00 +00:00
7b0816e42f Properly handle transaction building errors instead of kicking the player 2023-03-20 00:52:26 +00:00
4864444440 Added CraftingManager::getCraftingRecipeFromIndex() 2023-03-19 22:14:23 +00:00
f696a5881b Merge remote-tracking branch 'origin/stable' into minor-next 2023-03-19 16:23:09 +00:00
419962d3a2 Added timer for player-specific movement code
players use an entirely different pathway for movement processing, which could be costly.
2023-03-19 16:12:47 +00:00
054c06fab9 Add specialized entityBaseTick timer for item entities
since item merging is a potential hotspot, we want to know if this code section is a performance problem.
Current timers only tell us whether overall ticking of a particular entity is slow, but that includes movement and therefore isn't particularly helpful.
2023-03-19 15:59:06 +00:00
7bc5d8c824 Rename more timers 2023-03-19 15:57:36 +00:00
607bdfa42f Timings: added new timers for entity move collision checks and projectile move ray tracing
projectiles get their own distinct sub-timer, since the logic is completely different from regular entities.
2023-03-19 15:49:35 +00:00
eec53f9ae0 Timings: clean up timer names 2023-03-19 15:39:44 +00:00
3d56bd267c Timings: fixup network timer inheritance 2023-03-18 23:13:25 +00:00
9a969e21c7 ÂNetworkSession: ensure onResolve handler for CompressBatchPromise is covered by network send timings 2023-03-18 22:49:52 +00:00
195bc3b623 NetworkSession: prevent dev client asserts from missing ability flags 2023-03-18 21:53:17 +00:00
2177d8d352 Push Docker image tags to ghcr.io 2023-03-17 16:32:28 +00:00
471625e697 readme: remove docker hub shield
sadly there isn't any ghcr replacement right now.

[ci skip]
2023-03-17 16:24:13 +00:00
2135776c19 readme: goodbye docker hub, won't miss you
[ci skip]
2023-03-17 16:21:57 +00:00
765aef0810 4.18.0-ALPHA2 is next 2023-03-16 21:45:21 +00:00
34ced382db Eliminate final remaining usage of TypeConverter::netItemStackToCore()
instead, we can verify that the held items match by comparing the received ItemStack with the one cached in InventoryManager, which is more cost effective and closes off internal item deserializers to external attacks.
2023-03-14 22:56:11 +00:00
a573a279fa Merge branch 'minor-next' into item-stack-request 2023-03-14 22:25:49 +00:00
faaec12aaf Update BedrockProtocol 2023-01-06 22:16:29 +00:00
1123a5aa23 InventoryManager: Track predictions using ItemStack directly, instead of internal Item
this removes the need for deserializing network itemstacks to core items, thereby eliminating a whole bunch of potential security issues.
2023-01-06 20:45:08 +00:00
8633804f15 InventoryManager: disentangle slot tracking from slot syncing 2023-01-06 20:26:19 +00:00
d3cea2ca7c CS 2023-01-06 02:07:31 +00:00
5d6dba96af Merge branch 'stable' into item-stack-request 2023-01-06 01:47:27 +00:00
b24eb153f9 Constrain inventory transaction predictions
these are now only used for actions done with a closed inventory window. This means that they can only predict the slots of inventory, offhand and armor (total 41 slots) and perhaps include some DropItem actions.
2023-01-05 21:18:30 +00:00
36525d9055 Fixed multi-output recipe handling 2023-01-05 20:41:44 +00:00
3d6baa8a55 Working creative inventory, with a few more hacks than I'd like 2023-01-05 18:09:57 +00:00
30d3869eea Remove dead code 2023-01-05 17:26:58 +00:00
81697111b9 Merge branch 'item-stack-request' of github.com:pmmp/PocketMine-MP into item-stack-request 2023-01-05 17:24:15 +00:00
eedc943766 Confine legacy transaction handling to dropping items only 2023-01-05 17:23:50 +00:00
c6e11a8453 Remove unnecessary ternary operator (#5493) 2023-01-04 23:22:31 +00:00
2e9a3f9160 Working crafting :woohoo: 2023-01-04 22:29:29 +00:00
3d4ed5308e Merge branch 'stable' into item-stack-request 2023-01-04 01:28:42 +00:00
58dd4a44e3 Merge branch 'stable' into item-stack-request 2023-01-04 00:41:58 +00:00
5fdbb19852 Fixed a whole bunch of issues with legacy transactions 2023-01-04 00:13:51 +00:00
6b2156151f Merge branch 'stable' into item-stack-request 2023-01-03 23:51:37 +00:00
d8d236842f Fixed merge error 2023-01-03 19:54:41 +00:00
f51717323b Merge branch 'stable' into item-stack-request 2023-01-03 19:53:25 +00:00
0039af984d Merge branch 'next-minor' into item-stack-request 2022-10-16 16:56:26 +01:00
3235d128e5 Fixed handling of fake requests during block placement and other actions 2022-08-18 18:25:49 +01:00
2b7510945a First look at ItemStackRequest usage (very unstable) 2022-08-18 17:38:57 +01:00
30 changed files with 1339 additions and 404 deletions

View File

@ -52,7 +52,7 @@ jobs:
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 }}
@ -65,7 +65,7 @@ jobs:
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 }}
@ -78,7 +78,7 @@ jobs:
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 }}
@ -91,7 +91,7 @@ jobs:
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 }}

View File

@ -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

View File

@ -43,3 +43,49 @@ Released 16th March 2023.
- `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.

View File

@ -37,7 +37,7 @@
"pocketmine/bedrock-block-upgrade-schema": "~1.1.1+bedrock-1.19.70",
"pocketmine/bedrock-data": "~2.1.1+bedrock-1.19.70",
"pocketmine/bedrock-item-upgrade-schema": "~1.1.0+bedrock-1.19.70",
"pocketmine/bedrock-protocol": "~20.0.0+bedrock-1.19.70",
"pocketmine/bedrock-protocol": "~20.1.0+bedrock-1.19.70",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
"pocketmine/classloader": "^0.2.0",

16
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": "1d0c1d2fe668d85ae87110a1e3cfac05",
"content-hash": "01afa65b40f95ad9378c8cd999e6098d",
"packages": [
{
"name": "adhocore/json-comment",
@ -328,16 +328,16 @@
},
{
"name": "pocketmine/bedrock-protocol",
"version": "20.0.0+bedrock-1.19.70",
"version": "20.1.0+bedrock-1.19.70",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "4892a5020187da805d7b46ab522d8185b0283726"
"reference": "91d67c8b1bced3c82d0841b1041c0c1f4e93eb68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/4892a5020187da805d7b46ab522d8185b0283726",
"reference": "4892a5020187da805d7b46ab522d8185b0283726",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/91d67c8b1bced3c82d0841b1041c0c1f4e93eb68",
"reference": "91d67c8b1bced3c82d0841b1041c0c1f4e93eb68",
"shasum": ""
},
"require": {
@ -351,7 +351,7 @@
"ramsey/uuid": "^4.1"
},
"require-dev": {
"phpstan/phpstan": "1.10.1",
"phpstan/phpstan": "1.10.7",
"phpstan/phpstan-phpunit": "^1.0.0",
"phpstan/phpstan-strict-rules": "^1.0.0",
"phpunit/phpunit": "^9.5"
@ -369,9 +369,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/20.0.0+bedrock-1.19.70"
"source": "https://github.com/pmmp/BedrockProtocol/tree/20.1.0+bedrock-1.19.70"
},
"time": "2023-03-14T17:06:38+00:00"
"time": "2023-03-20T01:17:00+00:00"
},
{
"name": "pocketmine/binaryutils",

View File

@ -31,7 +31,7 @@ use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "4.18.0-ALPHA1";
public const BASE_VERSION = "4.18.0-ALPHA2";
public const IS_DEVELOPMENT_BUILD = false;
public const BUILD_CHANNEL = "alpha";

View File

@ -45,6 +45,12 @@ class CraftingManager{
*/
protected $shapelessRecipes = [];
/**
* @var CraftingRecipe[]
* @phpstan-var array<int, CraftingRecipe>
*/
private array $craftingRecipeIndex = [];
/**
* @var FurnaceRecipeManager[]
* @phpstan-var array<int, FurnaceRecipeManager>
@ -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();

View File

@ -246,7 +246,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();
@ -1139,6 +1139,7 @@ abstract class Entity{
$this->blocksAround = null;
Timings::$entityMove->startTiming();
Timings::$entityMoveCollision->startTiming();
$wantedX = $dx;
$wantedY = $dy;
@ -1223,6 +1224,7 @@ abstract class Entity{
$this->boundingBox = $moveBB;
}
Timings::$entityMoveCollision->stopTiming();
$this->location = new Location(
($this->boundingBox->minX + $this->boundingBox->maxX) / 2,

View File

@ -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>");

View File

@ -41,6 +41,7 @@ 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{
@ -113,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;
}
/**

View File

@ -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();
}
/**

View File

@ -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;
}

View File

@ -348,7 +348,7 @@ abstract class BaseInventory implements Inventory{
if($invManager === null){
continue;
}
$invManager->syncSlot($this, $index);
$invManager->onSlotChange($this, $index);
}
}

View File

@ -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);
}
}

View File

@ -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])){

View File

@ -25,7 +25,7 @@ namespace pocketmine\network\mcpe;
use pocketmine\inventory\Inventory;
final class ComplexWindowMapEntry{
final class ComplexInventoryMapEntry{
/**
* @var int[]

View File

@ -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,33 @@ 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;
}
$coreSlotId = $entry->mapNetToCore($netSlotId);
return $coreSlotId !== null ? [$entry->getInventory(), $coreSlotId] : null;
}
if(isset($this->windowMap[$windowId])){
return [$this->windowMap[$windowId], $netSlotId];
if(isset($this->networkIdToInventoryMap[$windowId])){
return [$this->networkIdToInventoryMap[$windowId], $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 +232,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 +303,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 +360,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 +372,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 +384,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 +406,149 @@ 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{
$currentItem = TypeConverter::getInstance()->coreItemStackToNet($inventory->getItem($slot));
$inventoryEntry = $this->inventories[spl_object_id($inventory)];
$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)];
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 +563,17 @@ class InventoryManager{
}
public function syncSelectedHotbarSlot() : void{
$selected = $this->player->getInventory()->getHeldItemIndex();
$playerInventory = $this->player->getInventory();
$selected = $playerInventory->getHeldItemIndex();
if($selected !== $this->clientSelectedHotbarSlot){
$itemStackInfo = $this->getItemStackInfo($playerInventory, $selected);
if($itemStackInfo === null){
throw new AssumptionFailedError("Player inventory slots should always be tracked");
}
$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 +585,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;
}
}

View 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 int[]
* @phpstan-var array<int, ItemStack>
*/
public array $pendingSyncs = [];
public function __construct(
public Inventory $inventory,
public ?ComplexInventoryMapEntry $complexSlotMap = null
){}
}

View 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; }
}

View File

@ -376,7 +376,7 @@ class NetworkSession{
$stream = new BinaryStream($decompressed);
$count = 0;
foreach(PacketBatch::decodeRaw($stream) as $buffer){
if(++$count > 1300){
if(++$count > 100){
throw new PacketHandlingException("Too many packets in batch");
}
$packet = $this->packetPool->getPacket($buffer);
@ -561,20 +561,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();
}
}
});
@ -889,6 +894,7 @@ 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,
];
$this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData(
@ -1140,6 +1146,12 @@ class NetworkSession{
$attribute->markSynchronized();
}
}
Timings::$playerNetworkSendInventorySync->startTiming();
try{
$this->invManager?->flushPendingUpdates();
}finally{
Timings::$playerNetworkSendInventorySync->stopTiming();
}
$this->flushSendBuffer();
}

View File

@ -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
}
}

View File

@ -24,11 +24,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
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 +32,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;
@ -261,60 +251,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");
}
}
}

View File

@ -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 */
@ -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,18 @@ 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");
}
$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 +346,81 @@ 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();
$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 only 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 extremely strict about the validation criteria.
if(count($data->getActions()) > 2){
throw new PacketHandlingException("Expected exactly 2 actions for dropping an item");
}
foreach($data->getActions() as $networkInventoryAction){
if($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_WORLD && $networkInventoryAction->inventorySlot == NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){
//drop item - we don't need to validate this, we only care about the count
//if the resulting actions don't match the client for some reason, it will trigger an automatic
//prediction rollback anyway.
//it's technically possible to see this more than once, but a normal client should never do that.
$droppedItemStack = $networkInventoryAction->newItem->getItemStack();
if($droppedItemStack->getCount() <= 0){
throw new PacketHandlingException("Expected positive count for dropped item");
}
$inventory = $this->player->getInventory();
$heldItem = $inventory->getItemInHand();
$heldItemStack = TypeConverter::getInstance()->coreItemStackToNet($heldItem);
//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($heldItemStack->getCount() < $droppedItemStack->getCount() || !$heldItemStack->equalsWithoutCount($droppedItemStack)){
return false;
}
//this modifies $heldItem
$droppedItem = $heldItem->pop($droppedItemStack->getCount());
$builder = new TransactionBuilder();
$builder->getInventory($inventory)->setItem($inventory->getHeldItemIndex(), $heldItem);
$builder->addAction(new DropItemAction($droppedItem));
$transaction = new InventoryTransaction($this->player, $builder->generateActions());
return $this->executeInventoryTransaction($transaction, $itemStackRequestId);
}
}
throw new PacketHandlingException("Legacy 'normal' transactions should only be used for dropping items");
}
private function handleUseItemTransaction(UseItemTransactionData $data) : bool{
$this->player->selectHotbarSlot($data->getHotbarSlot());
@ -537,6 +532,43 @@ class InGamePacketHandler extends PacketHandler{
return false;
}
private function handleSingleItemStackRequest(ItemStackRequest $request) : ItemStackResponse{
if(count($request->getActions()) > 20){
//TODO: we can probably lower this limit, but this will do for now
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

View File

@ -0,0 +1,90 @@
<?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
}
public static function translate(int $containerInterfaceId, int $currentWindowId) : int{
return match($containerInterfaceId){
ContainerUIIds::ARMOR => ContainerIds::ARMOR,
ContainerUIIds::HOTBAR,
ContainerUIIds::INVENTORY,
ContainerUIIds::COMBINED_HOTBAR_AND_INVENTORY => ContainerIds::INVENTORY,
ContainerUIIds::OFFHAND => ContainerIds::OFFHAND,
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,
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,
//all preview slots are ignored, since the client shouldn't be modifying those directly
default => throw new PacketHandlingException("Unexpected container UI ID $containerInterfaceId")
};
}
}

View File

@ -0,0 +1,385 @@
<?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\crafting\CraftingGrid;
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 = ItemStackContainerIdTranslator::translate($info->getContainerId(), $this->inventoryManager->getCurrentWindowId());
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $info->getSlotId());
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{
$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);
}
/**
* @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);
$currentWindow = $this->player->getCurrentWindow();
if($currentWindow !== null && !($currentWindow instanceof CraftingGrid)){
throw new ItemStackRequestProcessException("Player's current window is not a crafting grid");
}
$craftingGrid = $currentWindow ?? $this->player->getCraftingGrid();
$craftingResults = $recipe->getResultsFor($craftingGrid);
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(ItemStackRequestSlotInfo $destination, int $count) : void{
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;
$this->addItemToSlot($destination, $createdItem, $count);
if(!$this->createdItemFromCreativeInventory && $this->createdItemsTakenCount >= $createdItem->getCount()){
$this->setNextCreatedItem(null);
}
}
/**
* @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
){
$source = $action->getSource();
$destination = $action->getDestination();
if($source->getContainerId() === ContainerUIIds::CREATED_OUTPUT && $source->getSlotId() === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
$this->takeCreatedItem($destination, $action->getCount());
}else{
$this->transferItems($source, $destination, $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();
}
}

View File

@ -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{
}

View File

@ -0,0 +1,106 @@
<?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\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 = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId());
$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(),
0
);
}
}
$responseContainerInfos = [];
foreach($responseInfosByContainer as $containerInterfaceId => $responseInfos){
$responseContainerInfos[] = new ItemStackResponseContainerInfo($containerInterfaceId, $responseInfos);
}
return new ItemStackResponse(ItemStackResponse::RESULT_OK, $this->requestId, $responseContainerInfos);
}
}

View File

@ -98,7 +98,7 @@ class PreSpawnPacketHandler extends PacketHandler{
0,
0,
"",
false,
true,
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
Uuid::fromString(Uuid::NIL),
false,

View File

@ -1212,6 +1212,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @param Vector3 $newPos Coordinates of the player's feet, centered horizontally at the base of their bounding box.
*/
public function handleMovement(Vector3 $newPos) : void{
Timings::$playerMove->startTiming();
try{
$this->actuallyHandleMovement($newPos);
}finally{
Timings::$playerMove->stopTiming();
}
}
private function actuallyHandleMovement(Vector3 $newPos) : void{
$this->moveRateLimit--;
if($this->moveRateLimit < 0){
return;
@ -1365,13 +1374,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->timings->startTiming();
if($this->spawned){
Timings::$playerMove->startTiming();
$this->processMostRecentMovements();
$this->motion = new Vector3(0, 0, 0); //TODO: HACK! (Fixes player knockback being messed up)
$this->motion = Vector3::zero(); //TODO: HACK! (Fixes player knockback being messed up)
if($this->onGround){
$this->inAirTicks = 0;
}else{
$this->inAirTicks += $tickDiff;
}
Timings::$playerMove->stopTiming();
Timings::$entityBaseTick->startTiming();
$this->entityBaseTick($tickDiff);

View File

@ -27,9 +27,7 @@ use pocketmine\block\tile\Tile;
use pocketmine\entity\Entity;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\ServerboundPacket;
use pocketmine\player\Player;
use pocketmine\scheduler\TaskHandler;
use function dechex;
abstract class Timings{
public const INCLUDED_BY_OTHER_TIMINGS_PREFIX = "** ";
@ -58,6 +56,9 @@ abstract class Timings{
/** @var TimingsHandler */
public static $playerNetworkSendEncrypt;
public static TimingsHandler $playerNetworkSendInventorySync;
/** @var TimingsHandler */
public static $playerNetworkReceive;
/** @var TimingsHandler */
@ -91,6 +92,12 @@ abstract class Timings{
/** @var TimingsHandler */
public static $entityMove;
public static TimingsHandler $entityMoveCollision;
public static TimingsHandler $projectileMove;
public static TimingsHandler $projectileMoveRayTrace;
/** @var TimingsHandler */
public static $playerCheckNearEntities;
/** @var TimingsHandler */
@ -103,6 +110,8 @@ abstract class Timings{
/** @var TimingsHandler */
public static $livingEntityBaseTick;
public static TimingsHandler $itemEntityBaseTick;
/** @var TimingsHandler */
public static $schedulerSync;
/** @var TimingsHandler */
@ -142,6 +151,8 @@ abstract class Timings{
/** @var TimingsHandler */
public static $broadcastPackets;
public static TimingsHandler $playerMove;
public static function init() : void{
if(self::$initialized){
return;
@ -155,21 +166,24 @@ abstract class Timings{
self::$garbageCollector = new TimingsHandler("Garbage Collector", self::$memoryManager);
self::$titleTick = new TimingsHandler("Console Title Tick");
self::$playerNetworkSend = new TimingsHandler("Player Network Send");
self::$connection = new TimingsHandler("Connection Handler");
self::$playerNetworkSend = new TimingsHandler("Player Network Send", self::$connection);
self::$playerNetworkSendCompress = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Compression", self::$playerNetworkSend);
self::$playerNetworkSendCompressBroadcast = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Compression (Broadcast)", self::$playerNetworkSendCompress);
self::$playerNetworkSendCompressSessionBuffer = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Compression (Session Buffer)", self::$playerNetworkSendCompress);
self::$playerNetworkSendEncrypt = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Encryption", self::$playerNetworkSend);
self::$playerNetworkSendInventorySync = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Inventory Sync", self::$playerNetworkSend);
self::$playerNetworkReceive = new TimingsHandler("Player Network Receive");
self::$playerNetworkReceive = new TimingsHandler("Player Network Receive", self::$connection);
self::$playerNetworkReceiveDecompress = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Receive - Decompression", self::$playerNetworkReceive);
self::$playerNetworkReceiveDecrypt = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Receive - Decryption", self::$playerNetworkReceive);
self::$broadcastPackets = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Broadcast Packets", self::$playerNetworkSend);
self::$playerMove = new TimingsHandler("Player Movement");
self::$playerChunkOrder = new TimingsHandler("Player Order Chunks");
self::$playerChunkSend = new TimingsHandler("Player Send Chunks");
self::$connection = new TimingsHandler("Connection Handler");
self::$playerChunkSend = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Chunks", self::$playerNetworkSend);
self::$scheduler = new TimingsHandler("Scheduler");
self::$serverCommand = new TimingsHandler("Server Command");
self::$worldLoad = new TimingsHandler("World Load");
@ -183,19 +197,25 @@ abstract class Timings{
self::$syncPlayerDataLoad = new TimingsHandler("Player Data Load");
self::$syncPlayerDataSave = new TimingsHandler("Player Data Save");
self::$entityMove = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "entityMove");
self::$playerCheckNearEntities = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "checkNearEntities");
self::$tickEntity = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickEntity");
self::$tickTileEntity = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickTileEntity");
self::$entityMove = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Movement");
self::$entityMoveCollision = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Movement - Collision Checks", self::$entityMove);
self::$entityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "entityBaseTick");
self::$livingEntityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "livingEntityBaseTick");
self::$projectileMove = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Projectile Movement", self::$entityMove);
self::$projectileMoveRayTrace = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Projectile Movement - Ray Tracing", self::$projectileMove);
self::$playerCheckNearEntities = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "checkNearEntities");
self::$tickEntity = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Tick");
self::$tickTileEntity = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Block Entity Tick");
self::$entityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Base Tick");
self::$livingEntityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Base Tick - Living");
self::$itemEntityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Base Tick - ItemEntity");
self::$schedulerSync = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Scheduler - Sync Tasks");
self::$schedulerAsync = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Scheduler - Async Tasks");
self::$playerCommand = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "playerCommand");
self::$craftingDataCacheRebuild = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "craftingDataCacheRebuild");
self::$playerCommand = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Command");
self::$craftingDataCacheRebuild = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Build CraftingDataPacket Cache");
}
@ -218,11 +238,7 @@ abstract class Timings{
public static function getEntityTimings(Entity $entity) : TimingsHandler{
$entityType = (new \ReflectionClass($entity))->getShortName();
if(!isset(self::$entityTypeTimingMap[$entityType])){
if($entity instanceof Player){
self::$entityTypeTimingMap[$entityType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickEntity - EntityPlayer", self::$tickEntity);
}else{
self::$entityTypeTimingMap[$entityType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickEntity - " . $entityType, self::$tickEntity);
}
self::$entityTypeTimingMap[$entityType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Tick - " . $entityType, self::$tickEntity);
}
return self::$entityTypeTimingMap[$entityType];
@ -231,7 +247,7 @@ abstract class Timings{
public static function getTileEntityTimings(Tile $tile) : TimingsHandler{
$tileType = (new \ReflectionClass($tile))->getShortName();
if(!isset(self::$tileEntityTypeTimingMap[$tileType])){
self::$tileEntityTypeTimingMap[$tileType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "tickTileEntity - " . $tileType, self::$tickTileEntity);
self::$tileEntityTypeTimingMap[$tileType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Block Entity Tick - " . $tileType, self::$tickTileEntity);
}
return self::$tileEntityTypeTimingMap[$tileType];
@ -240,7 +256,7 @@ abstract class Timings{
public static function getReceiveDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{
$pid = $pk->pid();
if(!isset(self::$packetReceiveTimingMap[$pid])){
self::$packetReceiveTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "receivePacket - " . $pk->getName() . " [0x" . dechex($pid) . "]", self::$playerNetworkReceive);
self::$packetReceiveTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Receive - " . $pk->getName(), self::$playerNetworkReceive);
}
return self::$packetReceiveTimingMap[$pid];
@ -249,7 +265,7 @@ abstract class Timings{
public static function getDecodeDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{
$pid = $pk->pid();
return self::$packetDecodeTimingMap[$pid] ??= new TimingsHandler(
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Decode - " . $pk->getName() . " [0x" . dechex($pid) . "]",
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Decode - " . $pk->getName(),
self::getReceiveDataPacketTimings($pk)
);
}
@ -257,7 +273,7 @@ abstract class Timings{
public static function getHandleDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{
$pid = $pk->pid();
return self::$packetHandleTimingMap[$pid] ??= new TimingsHandler(
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Handler - " . $pk->getName() . " [0x" . dechex($pid) . "]",
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Handler - " . $pk->getName(),
self::getReceiveDataPacketTimings($pk)
);
}
@ -265,7 +281,7 @@ abstract class Timings{
public static function getEncodeDataPacketTimings(ClientboundPacket $pk) : TimingsHandler{
$pid = $pk->pid();
return self::$packetEncodeTimingMap[$pid] ??= new TimingsHandler(
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Encode - " . $pk->getName() . " [0x" . dechex($pid) . "]",
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Encode - " . $pk->getName(),
self::getSendDataPacketTimings($pk)
);
}
@ -273,7 +289,7 @@ abstract class Timings{
public static function getSendDataPacketTimings(ClientboundPacket $pk) : TimingsHandler{
$pid = $pk->pid();
if(!isset(self::$packetSendTimingMap[$pid])){
self::$packetSendTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "sendPacket - " . $pk->getName() . " [0x" . dechex($pid) . "]", self::$playerNetworkSend);
self::$packetSendTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Send - " . $pk->getName(), self::$playerNetworkSend);
}
return self::$packetSendTimingMap[$pid];