diff --git a/src/block/BrewingStand.php b/src/block/BrewingStand.php index e0366b7d3..fd763a3f0 100644 --- a/src/block/BrewingStand.php +++ b/src/block/BrewingStand.php @@ -125,6 +125,24 @@ class BrewingStand extends Transparent{ } public function onScheduledUpdate() : void{ - //TODO + $brewing = $this->position->getWorld()->getTile($this->position); + if($brewing instanceof TileBrewingStand){ + if($brewing->onUpdate()){ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); + } + + $changed = false; + foreach(BrewingStandSlot::getAll() as $slot){ + $occupied = !$brewing->getInventory()->isSlotEmpty($slot->getSlotNumber()); + if($occupied !== $this->hasSlot($slot)){ + $this->setSlot($slot, $occupied); + $changed = true; + } + } + + if($changed){ + $this->position->getWorld()->setBlock($this->position, $this); + } + } } } diff --git a/src/block/tile/BrewingStand.php b/src/block/tile/BrewingStand.php index 450a52125..e008c7fda 100644 --- a/src/block/tile/BrewingStand.php +++ b/src/block/tile/BrewingStand.php @@ -24,16 +24,29 @@ declare(strict_types=1); namespace pocketmine\block\tile; use pocketmine\block\inventory\BrewingStandInventory; +use pocketmine\crafting\BrewingRecipe; +use pocketmine\event\block\BrewingFuelUseEvent; +use pocketmine\event\block\BrewItemEvent; use pocketmine\inventory\CallbackInventoryListener; use pocketmine\inventory\Inventory; +use pocketmine\item\Item; +use pocketmine\item\VanillaItems; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\network\mcpe\protocol\ContainerSetDataPacket; +use pocketmine\player\Player; +use pocketmine\world\sound\PotionFinishBrewingSound; use pocketmine\world\World; +use function array_map; +use function count; class BrewingStand extends Spawnable implements Container, Nameable{ - + use NameableTrait { + addAdditionalSpawnData as addNameSpawnData; + } use ContainerTrait; - use NameableTrait; + + public const BREW_TIME_TICKS = 400; // Brew time in ticks private const TAG_BREW_TIME = "BrewTime"; //TAG_Short private const TAG_BREW_TIME_PE = "CookTime"; //TAG_Short @@ -41,15 +54,11 @@ class BrewingStand extends Spawnable implements Container, Nameable{ private const TAG_REMAINING_FUEL_TIME = "Fuel"; //TAG_Byte private const TAG_REMAINING_FUEL_TIME_PE = "FuelAmount"; //TAG_Short - /** @var BrewingStandInventory */ - private $inventory; + private BrewingStandInventory $inventory; - /** @var int */ - private $brewTime = 0; - /** @var int */ - private $maxFuelTime = 0; - /** @var int */ - private $remainingFuelTime = 0; + private int $brewTime = 0; + private int $maxFuelTime = 0; + private int $remainingFuelTime = 0; public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); @@ -83,6 +92,14 @@ class BrewingStand extends Spawnable implements Container, Nameable{ $nbt->setShort(self::TAG_REMAINING_FUEL_TIME_PE, $this->remainingFuelTime); } + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $this->addNameSpawnData($nbt); + + $nbt->setShort(self::TAG_BREW_TIME_PE, $this->brewTime); + $nbt->setShort(self::TAG_MAX_FUEL_TIME, $this->maxFuelTime); + $nbt->setShort(self::TAG_REMAINING_FUEL_TIME_PE, $this->remainingFuelTime); + } + public function getDefaultName() : string{ return "Brewing Stand"; } @@ -108,4 +125,135 @@ class BrewingStand extends Spawnable implements Container, Nameable{ public function getRealInventory(){ return $this->inventory; } + + private function checkFuel(Item $item) : void{ + $ev = new BrewingFuelUseEvent($this); + if(!$item->equals(VanillaItems::BLAZE_POWDER(), true, false)){ + $ev->cancel(); + } + + $ev->call(); + if($ev->isCancelled()){ + return; + } + + $item->pop(); + $this->inventory->setItem(BrewingStandInventory::SLOT_FUEL, $item); + + $this->maxFuelTime = $this->remainingFuelTime = $ev->getFuelTime(); + } + + /** + * @return BrewingRecipe[] + * @phpstan-return array + */ + private function getBrewableRecipes() : array{ + if($this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT)->isNull()){ + return []; + } + + $recipes = []; + foreach([BrewingStandInventory::SLOT_BOTTLE_LEFT, BrewingStandInventory::SLOT_BOTTLE_MIDDLE, BrewingStandInventory::SLOT_BOTTLE_RIGHT] as $slot){ + $input = $this->inventory->getItem($slot); + if($input->isNull()){ + continue; + } + + if(($recipe = $this->position->getWorld()->getServer()->getCraftingManager()->matchBrewingRecipe($input, $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT))) !== null){ + $recipes[$slot] = $recipe; + } + } + + return $recipes; + } + + public function onUpdate() : bool{ + if($this->closed){ + return false; + } + + $this->timings->startTiming(); + + $prevBrewTime = $this->brewTime; + $prevRemainingFuelTime = $this->remainingFuelTime; + $prevMaxFuelTime = $this->maxFuelTime; + + $ret = false; + + $fuel = $this->inventory->getItem(BrewingStandInventory::SLOT_FUEL); + $ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT); + + $recipes = $this->getBrewableRecipes(); + $canBrew = count($recipes) !== 0; + + if($this->remainingFuelTime <= 0 && $canBrew){ + $this->checkFuel($fuel); + } + + if($this->remainingFuelTime > 0){ + if($canBrew){ + if($this->brewTime === 0){ + $this->brewTime = self::BREW_TIME_TICKS; + --$this->remainingFuelTime; + } + + --$this->brewTime; + + if($this->brewTime <= 0){ + $anythingBrewed = false; + foreach($recipes as $slot => $recipe){ + $input = $this->inventory->getItem($slot); + $output = $recipe->getResultFor($input); + if($output === null){ + continue; + } + + $ev = new BrewItemEvent($this, $slot, $input, $output, $recipe); + $ev->call(); + if($ev->isCancelled()){ + continue; + } + + $this->inventory->setItem($slot, $ev->getResult()); + $anythingBrewed = true; + } + + if($anythingBrewed){ + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new PotionFinishBrewingSound()); + } + + $ingredient->pop(); + $this->inventory->setItem(BrewingStandInventory::SLOT_INGREDIENT, $ingredient); + + $this->brewTime = 0; + }else{ + $ret = true; + } + }else{ + $this->brewTime = 0; + } + }else{ + $this->brewTime = $this->remainingFuelTime = $this->maxFuelTime = 0; + } + + $viewers = array_map(fn(Player $p) => $p->getNetworkSession()->getInvManager(), $this->inventory->getViewers()); + foreach($viewers as $v){ + if($v === null){ + continue; + } + if($prevBrewTime !== $this->brewTime){ + $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_BREWING_STAND_BREW_TIME, $this->brewTime); + } + if($prevRemainingFuelTime !== $this->remainingFuelTime){ + $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_BREWING_STAND_FUEL_AMOUNT, $this->remainingFuelTime); + } + if($prevMaxFuelTime !== $this->maxFuelTime){ + $v->syncData($this->inventory, ContainerSetDataPacket::PROPERTY_BREWING_STAND_FUEL_TOTAL, $this->maxFuelTime); + } + } + + $this->timings->stopTiming(); + + return $ret; + } } diff --git a/src/block/utils/BrewingStandSlot.php b/src/block/utils/BrewingStandSlot.php index c5b14af1b..37182693e 100644 --- a/src/block/utils/BrewingStandSlot.php +++ b/src/block/utils/BrewingStandSlot.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\block\utils; +use pocketmine\block\inventory\BrewingStandInventory; use pocketmine\utils\EnumTrait; /** @@ -36,13 +37,24 @@ use pocketmine\utils\EnumTrait; * @method static BrewingStandSlot SOUTHWEST() */ final class BrewingStandSlot{ - use EnumTrait; + use EnumTrait { + __construct as Enum___construct; + } protected static function setup() : void{ self::registerAll( - new self("east"), - new self("northwest"), - new self("southwest") + new self("east", BrewingStandInventory::SLOT_BOTTLE_LEFT), + new self("northwest", BrewingStandInventory::SLOT_BOTTLE_MIDDLE), + new self("southwest", BrewingStandInventory::SLOT_BOTTLE_RIGHT) ); } + + private function __construct(string $enumName, private int $slotNumber){ + $this->Enum___construct($enumName); + } + + /** + * Returns the brewing stand inventory slot number associated with this visual slot. + */ + public function getSlotNumber() : int{ return $this->slotNumber; } } diff --git a/src/crafting/BrewingRecipe.php b/src/crafting/BrewingRecipe.php new file mode 100644 index 000000000..e903f0c3c --- /dev/null +++ b/src/crafting/BrewingRecipe.php @@ -0,0 +1,30 @@ +> + */ + protected $potionTypeRecipes = []; + + /** + * @var PotionContainerChangeRecipe[][] + * @phpstan-var array> + */ + protected $potionContainerChangeRecipes = []; + /** * @var ObjectSet * @phpstan-var ObjectSet<\Closure() : void> @@ -140,6 +152,22 @@ class CraftingManager{ return $this->furnaceRecipeManagers[$furnaceType->id()]; } + /** + * @return PotionTypeRecipe[][] + * @phpstan-return array> + */ + public function getPotionTypeRecipes() : array{ + return $this->potionTypeRecipes; + } + + /** + * @return PotionContainerChangeRecipe[][] + * @phpstan-return array> + */ + public function getPotionContainerChangeRecipes() : array{ + return $this->potionContainerChangeRecipes; + } + public function registerShapedRecipe(ShapedRecipe $recipe) : void{ $this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe; @@ -156,6 +184,25 @@ class CraftingManager{ } } + public function registerPotionTypeRecipe(PotionTypeRecipe $recipe) : void{ + $input = $recipe->getInput(); + $ingredient = $recipe->getIngredient(); + $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . ($ingredient->hasAnyDamageValue() ? "?" : $ingredient->getMeta())] = $recipe; + + foreach($this->recipeRegisteredCallbacks as $callback){ + $callback(); + } + } + + public function registerPotionContainerChangeRecipe(PotionContainerChangeRecipe $recipe) : void{ + $ingredient = $recipe->getIngredient(); + $this->potionContainerChangeRecipes[$recipe->getInputItemId()][$ingredient->getId() . ":" . ($ingredient->hasAnyDamageValue() ? "?" : $ingredient->getMeta())] = $recipe; + + foreach($this->recipeRegisteredCallbacks as $callback){ + $callback(); + } + } + /** * @param Item[] $outputs */ @@ -206,4 +253,11 @@ class CraftingManager{ } } } + + public function matchBrewingRecipe(Item $input, Item $ingredient) : ?BrewingRecipe{ + return $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . $ingredient->getMeta()] ?? + $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":?"] ?? + $this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":" . $ingredient->getMeta()] ?? + $this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":?"] ?? null; + } } diff --git a/src/crafting/CraftingManagerFromDataHelper.php b/src/crafting/CraftingManagerFromDataHelper.php index bd8465b1b..b5c485ab2 100644 --- a/src/crafting/CraftingManagerFromDataHelper.php +++ b/src/crafting/CraftingManagerFromDataHelper.php @@ -77,6 +77,20 @@ final class CraftingManagerFromDataHelper{ Item::jsonDeserialize($recipe["input"])) ); } + foreach($recipes["potion_type"] as $recipe){ + $result->registerPotionTypeRecipe(new PotionTypeRecipe( + Item::jsonDeserialize($recipe["input"]), + Item::jsonDeserialize($recipe["ingredient"]), + Item::jsonDeserialize($recipe["output"]) + )); + } + foreach($recipes["potion_container_change"] as $recipe){ + $result->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe( + $recipe["input_item_id"], + Item::jsonDeserialize($recipe["ingredient"]), + $recipe["output_item_id"] + )); + } return $result; } diff --git a/src/crafting/PotionContainerChangeRecipe.php b/src/crafting/PotionContainerChangeRecipe.php new file mode 100644 index 000000000..4f4ec2cde --- /dev/null +++ b/src/crafting/PotionContainerChangeRecipe.php @@ -0,0 +1,54 @@ +ingredient = clone $ingredient; + } + + public function getInputItemId() : int{ + return $this->inputItemId; + } + + public function getIngredient() : Item{ + return clone $this->ingredient; + } + + public function getOutputItemId() : int{ + return $this->outputItemId; + } + + public function getResultFor(Item $input) : ?Item{ + return $input->getId() === $this->getInputItemId() ? ItemFactory::getInstance()->get($this->getOutputItemId(), $input->getMeta()) : null; + } +} diff --git a/src/crafting/PotionTypeRecipe.php b/src/crafting/PotionTypeRecipe.php new file mode 100644 index 000000000..aa1604d6f --- /dev/null +++ b/src/crafting/PotionTypeRecipe.php @@ -0,0 +1,55 @@ +input = clone $input; + $this->ingredient = clone $ingredient; + $this->output = clone $output; + } + + public function getInput() : Item{ + return clone $this->input; + } + + public function getIngredient() : Item{ + return clone $this->ingredient; + } + + public function getOutput() : Item{ + return clone $this->output; + } + + public function getResultFor(Item $input) : ?Item{ + return $input->equals($this->input, true, false) ? $this->getOutput() : null; + } +} diff --git a/src/event/block/BrewItemEvent.php b/src/event/block/BrewItemEvent.php new file mode 100644 index 000000000..44df0047a --- /dev/null +++ b/src/event/block/BrewItemEvent.php @@ -0,0 +1,71 @@ +getBlock()); + } + + public function getBrewingStand() : BrewingStand{ + return $this->brewingStand; + } + + /** + * Returns which slot of the brewing stand's inventory the potion is in. + */ + public function getSlot() : int{ + return $this->slot; + } + + public function getInput() : Item{ + return clone $this->input; + } + + public function getResult() : Item{ + return clone $this->result; + } + + public function setResult(Item $result) : void{ + $this->result = clone $result; + } + + public function getRecipe() : BrewingRecipe{ + return $this->recipe; + } +} diff --git a/src/event/block/BrewingFuelUseEvent.php b/src/event/block/BrewingFuelUseEvent.php new file mode 100644 index 000000000..25b2c105c --- /dev/null +++ b/src/event/block/BrewingFuelUseEvent.php @@ -0,0 +1,64 @@ +getBlock()); + } + + public function getBrewingStand() : BrewingStand{ + return $this->brewingStand; + } + + /** + * Returns how many times the fuel can be used for potion brewing before it runs out. + */ + public function getFuelTime() : int{ + return $this->fuelTime; + } + + /** + * Sets how many times the fuel can be used for potion brewing before it runs out. + */ + public function setFuelTime(int $fuelTime) : void{ + if($fuelTime <= 0){ + throw new \InvalidArgumentException("Fuel time must be positive"); + } + $this->fuelTime = $fuelTime; + } +} diff --git a/src/network/mcpe/cache/CraftingDataCache.php b/src/network/mcpe/cache/CraftingDataCache.php index 369a45039..2b2d1968e 100644 --- a/src/network/mcpe/cache/CraftingDataCache.php +++ b/src/network/mcpe/cache/CraftingDataCache.php @@ -26,12 +26,15 @@ namespace pocketmine\network\mcpe\cache; use pocketmine\crafting\CraftingManager; use pocketmine\crafting\FurnaceType; use pocketmine\item\Item; +use pocketmine\network\mcpe\convert\ItemTranslator; use pocketmine\network\mcpe\convert\TypeConverter; use pocketmine\network\mcpe\protocol\CraftingDataPacket; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\recipe\CraftingRecipeBlockName; use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe as ProtocolFurnaceRecipe; use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipeBlockName; +use pocketmine\network\mcpe\protocol\types\recipe\PotionContainerChangeRecipe as ProtocolPotionContainerChangeRecipe; +use pocketmine\network\mcpe\protocol\types\recipe\PotionTypeRecipe as ProtocolPotionTypeRecipe; use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient; use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShapedRecipe; use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe; @@ -137,7 +140,39 @@ final class CraftingDataCache{ } } + $potionTypeRecipes = []; + foreach($manager->getPotionTypeRecipes() as $recipes){ + foreach($recipes as $recipe){ + $input = $converter->coreItemStackToNet($recipe->getInput()); + $ingredient = $converter->coreItemStackToNet($recipe->getIngredient()); + $output = $converter->coreItemStackToNet($recipe->getOutput()); + $potionTypeRecipes[] = new ProtocolPotionTypeRecipe( + $input->getId(), + $input->getMeta(), + $ingredient->getId(), + $ingredient->getMeta(), + $output->getId(), + $output->getMeta() + ); + } + } + + $potionContainerChangeRecipes = []; + $itemTranslator = ItemTranslator::getInstance(); + foreach($manager->getPotionContainerChangeRecipes() as $recipes){ + foreach($recipes as $recipe){ + $input = $itemTranslator->toNetworkId($recipe->getInputItemId(), 0); + $ingredient = $itemTranslator->toNetworkId($recipe->getIngredient()->getId(), 0); + $output = $itemTranslator->toNetworkId($recipe->getOutputItemId(), 0); + $potionContainerChangeRecipes[] = new ProtocolPotionContainerChangeRecipe( + $input[0], + $ingredient[0], + $output[0] + ); + } + } + Timings::$craftingDataCacheRebuild->stopTiming(); - return CraftingDataPacket::create($recipesWithTypeIds, [], [], [], true); + return CraftingDataPacket::create($recipesWithTypeIds, $potionTypeRecipes, $potionContainerChangeRecipes, [], true); } } diff --git a/src/world/sound/PotionFinishBrewingSound.php b/src/world/sound/PotionFinishBrewingSound.php new file mode 100644 index 000000000..5f292687b --- /dev/null +++ b/src/world/sound/PotionFinishBrewingSound.php @@ -0,0 +1,35 @@ +