Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save extremeheat/f1e9e0b8ca884727e6a3abfc7bfdc5a3 to your computer and use it in GitHub Desktop.
Save extremeheat/f1e9e0b8ca884727e6a3abfc7bfdc5a3 to your computer and use it in GitHub Desktop.
Various Minecraft: Windows 10 Edition-related patches for PocketMine-MP. Must be applied consecutively.
From ee6d6a6154ce35a4945add6aa6bd57f2144b185c Mon Sep 17 00:00:00 2001
From: extremeheat <[email protected]>
Date: Sun, 24 Apr 2016 00:46:33 -0400
Subject: [PATCH 1/4] Keep better track of inventory changes on Windows 10
Edition, fix inventory moving issues
---
src/pocketmine/Player.php | 198 ++++++++++++++++++++-
.../inventory/SimpleTransactionGroup.php | 26 ++-
2 files changed, 221 insertions(+), 3 deletions(-)
diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php
index 7cdf04e..d3fcfcb 100644
--- a/src/pocketmine/Player.php
+++ b/src/pocketmine/Player.php
@@ -186,6 +186,9 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
public $creationTime = 0;
+ /** @var Item[] */
+ protected $pickedupItems = [];
+
protected $randomClientId;
protected $lastMovement = 0;
@@ -488,6 +491,57 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
return $this->perm->getEffectivePermissions();
}
+ public function addPickedupItem(Item $item) {
+ if (count($this->getPickedupItems()) > 5 and !$this->isCreative())
+ throw new \OutOfRangeException("Items array size cannot be greater than 5.");
+
+ $appended = false;
+
+ // Check if we already picked up the same item and if so append it onto
+ // existing picked up items.
+ foreach ($this->getPickedupItems() as $i) {
+ if ($i->deepEquals($item) and $i->getCount() < $i->getMaxStackSize()) {
+
+ $amountFree = $i->getMaxStackSize() - $i->getCount();
+
+ if ($amountFree <= 0)
+ continue;
+ else {
+ if ($amountFree < $item->getCount()) {
+ $i->setCount($i->getMaxStackSize());
+ $item->setCount($item->getCount() - $amountFree);
+ } elseif ($amountFree == $item->getCount()) {
+ $i->setCount($i->getMaxStackSize());
+ $appended = true;
+ break;
+ } else {
+ $i->setCount($i->getCount() + $item->getCount());
+ $appended = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!$appended)
+ $this->pickedupItems[] = $item;
+ }
+
+ public function removePickedupItem($index) {
+ unset($this->pickedupItems[$index]);
+ }
+
+ public function setPickedupItems(Array $items) {
+ if (count($items) > 5)
+ throw new \OutOfRangeException("Items array size must be 5.");
+ $this->pickedupItems = $items;
+ }
+
+
+ public function getPickedupItems() {
+ return $this->pickedupItems;
+ }
+
/**
* @param SourceInterface $interface
* @param null $clientID
@@ -1747,6 +1801,141 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->server->onPlayerLogin($this);
}
+ public function processContainerChange(Inventory $inventory, $slot, Item $newItem, Item $oldItem) {
+ if ($newItem->getCount() > $newItem->getMaxStackSize() || $newItem->getCount() < 0)
+ $this->inventory->sendContents($this);
+
+ if ($oldItem != $newItem) {
+ $this->server->getLogger()->debug("Item change!");
+ if ($newItem->getId() == 0 && $oldItem->getId() != 0) {
+ // The entire slot was picked up.
+ $this->server->getLogger()->debug("PICKUP: Full slot of " . $oldItem);
+
+ // Picked up all items in slot
+ $this->addPickedupItem($oldItem);
+
+ // Clear the slot now that we've picked it up
+ $inventory->clear($slot);
+
+ } else if ($newItem->deepEquals($oldItem)) {
+ // We're picking up a portion of the items in a slot
+
+ if ($newItem->getCount() > $oldItem->getCount()) {
+ $this->server->getLogger()->debug("PLACEMENT: " . $newItem);
+
+ // We're placing an item
+ if (count($this->getPickedupItems()) == 0) {
+ // Illegal action: we're placing an item that we haven't picked up.
+ $this->kick("Cannot place an item that does not exist.");
+ return;
+ } else {
+ // Check if we picked up the item
+ foreach ($this->getPickedupItems() as $k => $i) {
+ if ($i->deepEquals($newItem)) {
+ $amountPlaced = $newItem->getCount() - $oldItem->getCount();
+ $remainingPickedup = $i->getCount() - $amountPlaced;
+
+ if (!$i->getCount() >= $newItem->getCount() || $remainingPickedup < 0) {
+ $this->kick("You attempted to place more of an item than you have.");
+ return;
+ }
+
+ if ($remainingPickedup)
+ $i->setCount($remainingPickedup);
+ else
+ $this->removePickedupItem($k);
+
+ $this->server->getLogger()->debug("You picked up and placed: " . $i);
+ $inventory->setItem($slot, $newItem);
+
+ break;
+ }
+ }
+ }
+ } else {
+ $this->server->getLogger()->debug("PICKUP: " . $newItem);
+ // We're picking an item up
+
+ $amountPickedup = $oldItem->getCount() - $newItem->getCount();
+ if ($amountPickedup < 1) {
+ $this->kick("Cannot pickup less than 1 of an item!");
+ return;
+ }
+
+ $itemPickedup = clone $oldItem;
+ $itemPickedup->setCount($amountPickedup);
+
+ $inventory->setItem($slot, $newItem);
+ $this->addPickedupItem($itemPickedup);
+
+ }
+ } else if (($newItem->getId() != 0 && $oldItem->getId() == 0) && count($this->getPickedupItems()) == 0) {
+ // PE sends us this information in reverse order.
+ // This bit of code should never be reached because SimpleTransactionGroup.php handles it
+ } else if ($newItem->getId() != 0 && $oldItem->getId() == 0) {
+ $this->server->getLogger()->debug("PLACEMENT: Full slot of " . $newItem);
+
+ // We're placing some items into a blank slot
+ foreach ($this->getPickedupItems() as $k => $i) {
+ if ($i->deepEquals($newItem)) {
+ $remainingPickedup = $i->getCount() - $newItem->getCount();
+
+ if (!$i->getCount() >= $newItem->getCount() || $remainingPickedup < 0) {
+ $this->kick("You attempted to place more of an item than you have.");
+ return;
+ }
+
+ if ($remainingPickedup)
+ $i->setCount($remainingPickedup);
+ else
+ $this->removePickedupItem($k);
+
+ $this->server->getLogger()->info("You picked up and placed: " . $i);
+ $inventory->setItem($slot, $newItem);
+ break;
+ }
+ }
+ } else if (!$newItem->deepEquals($oldItem)) {
+ if ($oldItem->deepEquals($newItem, false)) // Durability change, already handled by server
+ return;
+
+ // Swapping items
+ $this->server->getLogger()->debug("SWAP: " . $oldItem . " for " . $newItem);
+
+ // Check through our pickedup items to make sure that $newItem was picked up
+ $found = false;
+ foreach ($this->getPickedupItems() as $k => $i) {
+ if ($i->deepEquals($newItem)) {
+ // This checks how much of the item we picked up is actually swapped
+ $remainingPickedup = $i->getCount() - $newItem->getCount();
+
+ if (!$i->getCount() >= $newItem->getCount() || $remainingPickedup < 0) {
+ $this->kick("You attempted to place more of an item than you have.");
+ return;
+ }
+
+ if ($remainingPickedup)
+ $i->setCount($remainingPickedup);
+ else
+ $this->removePickedupItem($k);
+
+ $inventory->setItem($slot, $newItem);
+
+ $found = true;
+ break;
+ }
+ }
+
+ if ($found == false) {
+ $this->kick("You attempted to swap for an item that you don't have.");
+ return;
+ }
+
+ $this->addPickedupItem($oldItem);
+ }
+ }
+ }
+
/**
* Handles a Minecraft packet
* TODO: Separate all of this in handlers
@@ -2541,6 +2730,13 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
}else{
unset($this->windowIndex[$packet->windowid]);
}
+
+ if (!$this->isCreative())
+ foreach ($this->getPickedupItems() as $i)
+ $this->level->dropItem($this, $i);
+
+ $this->setPickedupItems([]);
+
break;
case ProtocolInfo::CRAFTING_EVENT_PACKET:
@@ -2774,7 +2970,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->currentTransaction->addTransaction($transaction);
- if($this->currentTransaction->canExecute()){
+ if($this->currentTransaction->canExecute() || $this->currentTransaction->isWindows10Edition($transaction)){
$achievements = [];
foreach($this->currentTransaction->getTransactions() as $ts){
$inv = $ts->getInventory();
diff --git a/src/pocketmine/inventory/SimpleTransactionGroup.php b/src/pocketmine/inventory/SimpleTransactionGroup.php
index 595cff3..a8781df 100644
--- a/src/pocketmine/inventory/SimpleTransactionGroup.php
+++ b/src/pocketmine/inventory/SimpleTransactionGroup.php
@@ -41,6 +41,9 @@ class SimpleTransactionGroup implements TransactionGroup{
/** @var Transaction[] */
protected $transactions = [];
+ /** @var bool */
+ protected $windows10Edition = false;
+
/**
* @param Player $source
*/
@@ -126,6 +129,22 @@ class SimpleTransactionGroup implements TransactionGroup{
return true;
}
+ public function isWindows10Edition($transaction) {
+ if (count($this->transactions) > 1)
+ return false;
+
+ if (count($this->source->getPickedupItems()) == 0 && // The player dosen't have anything picked up
+ (($transaction->getTargetItem()->getId() != 0 && $transaction->getSourceItem()->getId() == 0) || // An item was placed into an air block
+ ($transaction->getTargetItem()->getCount() > $transaction->getSourceItem()->getCount()))) { // An item was added to an existing slot with a larger quantity
+ // PE
+ $this->windows10Edition = false;
+ return false;
+ }
+ $this->windows10Edition = true;
+ return true;
+ }
+
+
public function canExecute(){
$haveItems = [];
$needItems = [];
@@ -134,7 +153,7 @@ class SimpleTransactionGroup implements TransactionGroup{
}
public function execute(){
- if($this->hasExecuted() or !$this->canExecute()){
+ if($this->hasExecuted() or (!$this->canExecute() && !$this->windows10Edition)){
return false;
}
@@ -151,7 +170,10 @@ class SimpleTransactionGroup implements TransactionGroup{
}
foreach($this->transactions as $transaction){
- $transaction->getInventory()->setItem($transaction->getSlot(), $transaction->getTargetItem());
+ if ($this->windows10Edition)
+ $this->source->processContainerChange($transaction->getInventory(), $transaction->getSlot(), $transaction->getTargetItem(), $transaction->getSourceItem());
+ else
+ $transaction->getInventory()->setItem($transaction->getSlot(), $transaction->getTargetItem());
}
$this->hasExecuted = true;
--
2.8.1.windows.1
From b916b75ae927c55916f64d5345063b201a4f8c8a Mon Sep 17 00:00:00 2001
From: extremeheat <[email protected]>
Date: Sun, 24 Apr 2016 16:32:28 -0400
Subject: [PATCH 2/4] Fix dropping items on Windows 10 Edition
---
src/pocketmine/Player.php | 58 +++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 54 insertions(+), 4 deletions(-)
diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php
index d3fcfcb..2f7bc4a 100644
--- a/src/pocketmine/Player.php
+++ b/src/pocketmine/Player.php
@@ -2670,18 +2670,68 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
if($this->spawned === false or $this->blocked === true or !$this->isAlive()){
break;
}
- $item = $this->inventory->getItemInHand();
- $ev = new PlayerDropItemEvent($this, $item);
+
+ $replacementItem = Item::get(Item::AIR, 0, 1);
+ $droppedItem = null;
+
+ // We can't just assume that we're dropping the item in hand because
+ // on the Windows 10 Edition you can drop items directly from inventories
+ if (count($this->getPickedupItems()) > 0) {
+ foreach ($this->getPickedupItems() as $k => $i) {
+ if ($packet->item->deepEquals($i)) {
+ $remaining = $i->getCount() - $packet->item->getCount();
+ if ($remaining < 0) {
+ $this->kick("Cannot drop more of an item than you have!");
+ break;
+ } else if ($remaining == 0)
+ $this->removePickedupItem($k);
+ else
+ $i->setCount($remaining);
+
+ $droppedItem = $packet->item;
+ // Nothing to replace when dropping from inventory
+ $replacementItem = null;
+ }
+ }
+ } else {
+ $itemInHand = clone $this->inventory->getItemInHand();
+ if (!$itemInHand->deepEquals($packet->item)) {
+ // The block held and the block being dropped is not the same
+ break;
+ } elseif ($itemInHand->getCount() != $packet->item->getCount()) {
+ // The only difference is that the amount dropped differs
+ $remaining = $itemInHand->getCount() - $packet->item->getCount();
+ if ($remaining < 0) {
+ // The client is attempting to drop more of an item than in hand
+ break;
+ } else {
+ $itemInHand->setCount($remaining);
+ $replacementItem = $itemInHand;
+ $droppedItem = $packet->item;
+ }
+ } else {
+ // Dropped everyting in slot
+ $droppedItem = $itemInHand;
+ }
+ }
+
+ if (!$droppedItem)
+ break;
+
+ $ev = new PlayerDropItemEvent($this, $droppedItem);
$this->server->getPluginManager()->callEvent($ev);
if($ev->isCancelled()){
$this->inventory->sendContents($this);
break;
}
- $this->inventory->setItemInHand(Item::get(Item::AIR, 0, 1));
+ if ($replacementItem)
+ $this->inventory->setItemInHand($replacementItem);
+
$motion = $this->getDirectionVector()->multiply(0.4);
- $this->level->dropItem($this->add(0, 1.3, 0), $item, $motion, 40);
+ // TODO: Stack nearby item drops
+ $this->level->dropItem($this->add(0, 1.3, 0), $droppedItem, $motion, 40);
$this->setDataFlag(self::DATA_FLAGS, self::DATA_FLAG_ACTION, false);
break;
--
2.8.1.windows.1
From a28f2262e649881ee07a81b3527b153cb0c80e6f Mon Sep 17 00:00:00 2001
From: extremeheat <[email protected]>
Date: Mon, 25 Apr 2016 15:25:58 -0400
Subject: [PATCH 3/4] Account for hotbar <-> slot changes
---
src/pocketmine/Player.php | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php
index 2f7bc4a..490ba0d 100644
--- a/src/pocketmine/Player.php
+++ b/src/pocketmine/Player.php
@@ -1927,6 +1927,12 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
}
if ($found == false) {
+ // Hotbar <-> slot mapping change
+ if ($this->inventory->getItem($hotbarSlot = $this->inventory->getHotbarSlotIndex($slot))->deepEquals($newItem)) {
+ $this->inventory->setItem($hotbarSlot, $oldItem);
+ $this->inventory->setItem($slot, $newItem);
+ return;
+ }
$this->kick("You attempted to swap for an item that you don't have.");
return;
}
--
2.8.1.windows.1
From 996538338a6f0313f48c11b7eb984560959a719f Mon Sep 17 00:00:00 2001
From: extremeheat <[email protected]>
Date: Tue, 26 Apr 2016 00:21:00 -0400
Subject: [PATCH] Fix crafting in Windows 10 Edition
---
src/pocketmine/Player.php | 80 ++++++++++++++++++++++------
src/pocketmine/inventory/CraftingManager.php | 8 +++
src/pocketmine/inventory/ShapedRecipe.php | 13 +++++
3 files changed, 84 insertions(+), 17 deletions(-)
diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php
index 490ba0d..5bc4b52 100644
--- a/src/pocketmine/Player.php
+++ b/src/pocketmine/Player.php
@@ -186,6 +186,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
public $creationTime = 0;
+ protected $maxPickupableItems = 10; // 9 crafting, 1 held
/** @var Item[] */
protected $pickedupItems = [];
@@ -491,9 +492,9 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
return $this->perm->getEffectivePermissions();
}
- public function addPickedupItem(Item $item) {
- if (count($this->getPickedupItems()) > 5 and !$this->isCreative())
- throw new \OutOfRangeException("Items array size cannot be greater than 5.");
+ public function addPickedupItem(Item $item, bool $ignoreItemCount = false) {
+ if (count($this->getPickedupItems()) >= $this->maxPickupableItems and !$this->isCreative() and !$ignoreItemCount)
+ throw new \OutOfRangeException("Items array size cannot be greater than {$this->maxPickupableItems}.");
$appended = false;
@@ -531,9 +532,9 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
unset($this->pickedupItems[$index]);
}
- public function setPickedupItems(Array $items) {
- if (count($items) > 5)
- throw new \OutOfRangeException("Items array size must be 5.");
+ public function setPickedupItems(Array $items, bool $ignoreItemCount = false) {
+ if (count($items) > $this->maxPickupableItems and !$ignoreItemCount)
+ throw new \OutOfRangeException("Items array size must be {$this->maxPickupableItems}.");
$this->pickedupItems = $items;
}
@@ -2805,27 +2806,67 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->dataPacket($pk);
break;
}
-
$recipe = $this->server->getCraftingManager()->getRecipe($packet->id);
-
if($recipe === null or (($recipe instanceof BigShapelessRecipe or $recipe instanceof BigShapedRecipe) and $this->craftingType === 0)){
$this->inventory->sendContents($this);
break;
}
-
foreach($packet->input as $i => $item){
if($item->getDamage() === -1 or $item->getDamage() === 0xffff){
$item->setDamage(null);
}
-
if($i < 9 and $item->getId() > 0){
$item->setCount(1);
}
}
-
$canCraft = true;
+ $giveItem = true;
+ // Keep a local copy so we can revert changes if needed
+ $pickedupItems = $this->getPickedupItems();
+
+ if (empty($packet->input)) {
+ // Get a list of recipes that can be used to craft the result.
+ $recipes = $this->server->getCraftingManager()->getRecipesByItem($packet->output[0]);
+ // As soon as a recipe whose ingridients can be satisfied is found,
+ // use it. This shouldn't cause any issues for the few items that have
+ // multiple recipes they can be crafted with.
+ $canCraft = false;
+ foreach ($recipes as $r) {
+ $need = $r->getIngredientList();
+ $have = [];
- if($recipe instanceof ShapedRecipe){
+ foreach ($need as $i){
+ foreach ($this->getPickedupItems() as $key => $item) {
+ if ($need == $have) // We already have all the things we need
+ break;
+ if ($item->deepEquals($i, $i->getDamage() !== null, $i->getCompoundTag() !== null)) {
+ $amountRemaining = $item->getCount() - $i->getCount();
+
+ if ($amountRemaining > 0) {
+ $item->setCount($amountRemaining);
+ $have[] = $i;
+ } elseif ($amountRemaining <= 0) {
+ // Relatively safe operation here as items stack
+ $have[] = $item;
+ $this->removePickedupItem($key);
+ }
+ }
+ }
+ }
+ if ($need != $have) {
+ // Can't craft
+ $this->setPickedupItems($pickedupItems);
+ continue;
+ } else {
+ // Reset the recipe to the correct one
+ $recipe = $r;
+ $canCraft = true;
+ $giveItem = false;
+ $this->addPickedupItem(clone $recipe->getResult(), true);
+ break;
+ }
+ }
+ }elseif($recipe instanceof ShapedRecipe){
for($x = 0; $x < 3 and $canCraft; ++$x){
for($y = 0; $y < 3; ++$y){
$item = $packet->input[$y * 3 + $x];
@@ -2878,6 +2919,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
if(!$canCraft or !$recipe->getResult()->deepEquals($result)){
$this->server->getLogger()->debug("Unmatched recipe " . $recipe->getId() . " from player " . $this->getName() . ": expected " . $recipe->getResult() . ", got " . $result . ", using: " . implode(", ", $ingredients));
$this->inventory->sendContents($this);
+ $this->setPickedupItems($pickedupItems);
break;
}
@@ -2902,6 +2944,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
if(!$canCraft){
$this->server->getLogger()->debug("Unmatched recipe " . $recipe->getId() . " from player " . $this->getName() . ": client does not have enough items, using: " . implode(", ", $ingredients));
$this->inventory->sendContents($this);
+ $this->setPickedupItems($pickedupItems);
break;
}
@@ -2909,6 +2952,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
if($ev->isCancelled()){
$this->inventory->sendContents($this);
+ $this->setPickedupItems($pickedupItems);
break;
}
@@ -2929,10 +2973,13 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->inventory->setItem($slot, $newItem);
}
- $extraItem = $this->inventory->addItem($recipe->getResult());
- if(count($extraItem) > 0){
- foreach($extraItem as $item){
- $this->level->dropItem($this, $item);
+ // Avoid item client-sided duplicating in Windows 10 Edition as the item is already created
+ if ($giveItem) {
+ $extraItem = $this->inventory->addItem($recipe->getResult());
+ if(count($extraItem) > 0){
+ foreach($extraItem as $item){
+ $this->level->dropItem($this, $item);
+ }
}
}
@@ -2970,7 +3017,6 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->awardAchievement("diamond");
break;
}
-
break;
case ProtocolInfo::CONTAINER_SET_SLOT_PACKET:
diff --git a/src/pocketmine/inventory/CraftingManager.php b/src/pocketmine/inventory/CraftingManager.php
index 6ff6f1e..876cb30 100644
--- a/src/pocketmine/inventory/CraftingManager.php
+++ b/src/pocketmine/inventory/CraftingManager.php
@@ -126,6 +126,14 @@ class CraftingManager{
}
/**
+ * @param Item $item
+ * @return Recipe[]
+ */
+ public function getRecipesByItem(Item $item) {
+ return @array_values($this->recipeLookup[$item->getId() . ":" . $item->getDamage()]) ?? [];
+ }
+
+ /**
* @return FurnaceRecipe[]
*/
public function getFurnaceRecipes(){
diff --git a/src/pocketmine/inventory/ShapedRecipe.php b/src/pocketmine/inventory/ShapedRecipe.php
index 823e6c4..864a93a 100644
--- a/src/pocketmine/inventory/ShapedRecipe.php
+++ b/src/pocketmine/inventory/ShapedRecipe.php
@@ -139,6 +139,19 @@ class ShapedRecipe implements Recipe{
}
/**
+ * @return Item[][]
+ */
+ public function getIngredientList() {
+ $ingredients = [];
+ for ($x = 0; $x < 3; ++$x)
+ for ($y = 0; $y < 3; ++$y)
+ if (!empty($this->ingredients[$x][$y]))
+ if (!$this->ingredients[$x][$y]->getId() == Item::AIR)
+ $ingredients[] = $this->ingredients[$x][$y];
+ return $ingredients;
+ }
+
+ /**
* @return string[]
*/
public function getShape(){
--
2.8.1.windows.1
Copy link

ghost commented May 10, 2016

????

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment