diff --git a/src/block/BlockTypeIds.php b/src/block/BlockTypeIds.php index 27fc92bd5..69c4d6ee6 100644 --- a/src/block/BlockTypeIds.php +++ b/src/block/BlockTypeIds.php @@ -694,6 +694,11 @@ final class BlockTypeIds{ public const SMITHING_TABLE = 10667; public const NETHERITE = 10668; public const SPORE_BLOSSOM = 10669; + public const CAULDRON = 10670; + public const WATER_CAULDRON = 10671; + public const LAVA_CAULDRON = 10672; + public const POTION_CAULDRON = 10673; + public const POWDER_SNOW_CAULDRON = 10674; - public const FIRST_UNUSED_BLOCK_ID = 10670; + public const FIRST_UNUSED_BLOCK_ID = 10675; } diff --git a/src/block/Cauldron.php b/src/block/Cauldron.php new file mode 100644 index 000000000..da1a938b2 --- /dev/null +++ b/src/block/Cauldron.php @@ -0,0 +1,104 @@ +position->getWorld()->getTile($this->position); + assert($tile instanceof TileCauldron); + + //empty cauldrons don't use this information + $tile->setCustomWaterColor(null); + $tile->setPotionItem(null); + } + + protected function recalculateCollisionBoxes() : array{ + $result = [ + AxisAlignedBB::one()->trim(Facing::UP, 11 / 16) //bottom of the cauldron + ]; + + foreach(Facing::HORIZONTAL as $f){ //add the frame parts around the bowl + $result[] = AxisAlignedBB::one()->trim($f, 14 / 16); + } + return $result; + } + + public function getSupportType(int $facing) : SupportType{ + return $facing === Facing::UP ? SupportType::EDGE() : SupportType::NONE(); + } + + /** + * @param Item[] &$returnedItems + */ + private function fill(int $amount, FillableCauldron $result, Item $usedItem, Item $returnedItem, array &$returnedItems) : void{ + $this->position->getWorld()->setBlock($this->position, $result->setFillLevel($amount)); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), $result->getFillSound()); + + $usedItem->pop(); + $returnedItems[] = $returnedItem; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + if($item->getTypeId() === ItemTypeIds::WATER_BUCKET){ + $this->fill(FillableCauldron::MAX_FILL_LEVEL, VanillaBlocks::WATER_CAULDRON(), $item, VanillaItems::BUCKET(), $returnedItems); + }elseif($item->getTypeId() === ItemTypeIds::LAVA_BUCKET){ + $this->fill(FillableCauldron::MAX_FILL_LEVEL, VanillaBlocks::LAVA_CAULDRON(), $item, VanillaItems::BUCKET(), $returnedItems); + }elseif($item->getTypeId() === ItemTypeIds::POWDER_SNOW_BUCKET){ + //TODO: powder snow cauldron + }elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion + if($item->getType()->equals(PotionType::WATER())){ + $this->fill(WaterCauldron::WATER_BOTTLE_FILL_AMOUNT, VanillaBlocks::WATER_CAULDRON(), $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); + }else{ + $this->fill(PotionCauldron::POTION_FILL_AMOUNT, VanillaBlocks::POTION_CAULDRON()->setPotionItem($item), $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); + } + } + + return true; + } + + public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); + if($world->getBlock($this->position->up())->getTypeId() === BlockTypeIds::WATER){ + $cauldron = VanillaBlocks::WATER_CAULDRON()->setFillLevel(FillableCauldron::MAX_FILL_LEVEL); + $world->setBlock($this->position, $cauldron); + $world->addSound($this->position->add(0.5, 0.5, 0.5), $cauldron->getFillSound()); + } + } +} diff --git a/src/block/FillableCauldron.php b/src/block/FillableCauldron.php new file mode 100644 index 000000000..acc16e575 --- /dev/null +++ b/src/block/FillableCauldron.php @@ -0,0 +1,132 @@ +boundedInt(3, self::MIN_FILL_LEVEL, self::MAX_FILL_LEVEL, $this->fillLevel); + } + + public function getFillLevel() : int{ return $this->fillLevel; } + + /** @return $this */ + public function setFillLevel(int $fillLevel) : self{ + if($fillLevel < self::MIN_FILL_LEVEL || $fillLevel > self::MAX_FILL_LEVEL){ + throw new \InvalidArgumentException("Fill level must be in range " . self::MIN_FILL_LEVEL . " ... " . self::MAX_FILL_LEVEL); + } + $this->fillLevel = $fillLevel; + return $this; + } + + protected function recalculateCollisionBoxes() : array{ + $result = [ + AxisAlignedBB::one()->trim(Facing::UP, 11 / 16) //bottom of the cauldron + ]; + + foreach(Facing::HORIZONTAL as $f){ //add the frame parts around the bowl + $result[] = AxisAlignedBB::one()->trim($f, 14 / 16); + } + return $result; + } + + public function getSupportType(int $facing) : SupportType{ + return $facing === Facing::UP ? SupportType::EDGE() : SupportType::NONE(); + } + + protected function withFillLevel(int $fillLevel) : Block{ + return $fillLevel === 0 ? VanillaBlocks::CAULDRON() : $this->setFillLevel(min(self::MAX_FILL_LEVEL, $fillLevel)); + } + + /** + * @param Item[] &$returnedItems + */ + protected function addFillLevels(int $amount, Item $usedItem, Item $returnedItem, array &$returnedItems) : void{ + if($this->fillLevel >= self::MAX_FILL_LEVEL){ + return; + } + $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->fillLevel + $amount)); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), $this->getFillSound()); + + $usedItem->pop(); + $returnedItems[] = $returnedItem; + } + + /** + * @param Item[] &$returnedItems + */ + protected function removeFillLevels(int $amount, Item $usedItem, Item $returnedItem, array &$returnedItems) : void{ + if($this->fillLevel < $amount){ + return; + } + + $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->fillLevel - $amount)); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), $this->getEmptySound()); + + $usedItem->pop(); + $returnedItems[] = $returnedItem; + } + + /** + * Returns the sound played when adding levels to the cauldron liquid. + */ + abstract public function getFillSound() : Sound; + + /** + * Returns the sound played when removing levels from the cauldron liquid. + */ + abstract public function getEmptySound() : Sound; + + /** + * @param Item[] &$returnedItems + */ + protected function mix(Item $usedItem, Item $returnedItem, array &$returnedItems) : void{ + $this->position->getWorld()->setBlock($this->position, VanillaBlocks::CAULDRON()); + //TODO: sounds and particles + + $usedItem->pop(); + $returnedItems[] = $returnedItem; + } + + public function asItem() : Item{ + return VanillaBlocks::CAULDRON()->asItem(); + } +} diff --git a/src/block/LavaCauldron.php b/src/block/LavaCauldron.php new file mode 100644 index 000000000..3df903e22 --- /dev/null +++ b/src/block/LavaCauldron.php @@ -0,0 +1,88 @@ +position->getWorld()->getTile($this->position); + assert($tile instanceof TileCauldron); + + $tile->setCustomWaterColor(null); + $tile->setPotionItem(null); + } + + public function getLightLevel() : int{ + return 15; + } + + public function getFillSound() : Sound{ + return new CauldronFillLavaSound(); + } + + public function getEmptySound() : Sound{ + return new CauldronEmptyLavaSound(); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + match($item->getTypeId()){ + ItemTypeIds::BUCKET => $this->removeFillLevels(self::MAX_FILL_LEVEL, $item, VanillaItems::LAVA_BUCKET(), $returnedItems), + ItemTypeIds::POWDER_SNOW_BUCKET, ItemTypeIds::WATER_BUCKET => $this->mix($item, VanillaItems::BUCKET(), $returnedItems), + ItemTypeIds::LINGERING_POTION, ItemTypeIds::POTION, ItemTypeIds::SPLASH_POTION => $this->mix($item, VanillaItems::GLASS_BOTTLE(), $returnedItems), + default => null + }; + return true; + } + + public function hasEntityCollision() : bool{ return true; } + + public function onEntityInside(Entity $entity) : bool{ + $ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_LAVA, 4); + $entity->attack($ev); + + $ev = new EntityCombustByBlockEvent($this, $entity, 8); + $ev->call(); + if(!$ev->isCancelled()){ + $entity->setOnFire($ev->getDuration()); + } + + return true; + } +} diff --git a/src/block/PotionCauldron.php b/src/block/PotionCauldron.php new file mode 100644 index 000000000..d0bcf2af5 --- /dev/null +++ b/src/block/PotionCauldron.php @@ -0,0 +1,108 @@ +position->getWorld()->getTile($this->position); + $this->potionItem = $tile instanceof TileCauldron ? $tile->getPotionItem() : null; + + return $this; + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileCauldron); + $tile->setCustomWaterColor(null); + $tile->setPotionItem($this->potionItem); + } + + public function getPotionItem() : ?Item{ return $this->potionItem === null ? null : clone $this->potionItem; } + + /** @return $this */ + public function setPotionItem(?Item $potionItem) : self{ + $this->potionItem = $potionItem !== null ? (clone $potionItem)->setCount(1) : null; + return $this; + } + + public function getFillSound() : Sound{ + return new CauldronFillPotionSound(); + } + + public function getEmptySound() : Sound{ + return new CauldronEmptyPotionSound(); + } + + /** + * @param Item[] &$returnedItems + */ + protected function addFillLevelsOrMix(int $amount, Item $usedItem, Item $returnedItem, array &$returnedItems) : void{ + if($this->potionItem !== null && !$usedItem->equals($this->potionItem, true, false)){ + $this->mix($usedItem, $returnedItem, $returnedItems); + }else{ + $this->addFillLevels($amount, $usedItem, $returnedItem, $returnedItems); + } + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + match($item->getTypeId()){ + ItemTypeIds::LINGERING_POTION, ItemTypeIds::POTION, ItemTypeIds::SPLASH_POTION => $this->addFillLevelsOrMix(self::POTION_FILL_AMOUNT, $item, VanillaItems::GLASS_BOTTLE(), $returnedItems), + ItemTypeIds::GLASS_BOTTLE => $this->potionItem === null ? null : $this->removeFillLevels(self::POTION_FILL_AMOUNT, $item, clone $this->potionItem, $returnedItems), + ItemTypeIds::LAVA_BUCKET, ItemTypeIds::POWDER_SNOW_BUCKET, ItemTypeIds::WATER_BUCKET => $this->mix($item, VanillaItems::BUCKET(), $returnedItems), + //TODO: tipped arrows + default => null + }; + return true; + } + + public function onNearbyBlockChange() : void{ + $world = $this->position->getWorld(); + if($world->getBlock($this->position->up())->getTypeId() === BlockTypeIds::WATER){ + $cauldron = VanillaBlocks::WATER_CAULDRON()->setFillLevel(FillableCauldron::MAX_FILL_LEVEL); + $world->setBlock($this->position, $cauldron); + $world->addSound($this->position->add(0.5, 0.5, 0.5), $cauldron->getFillSound()); + } + } +} diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index 679e51553..a900cabe5 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -34,6 +34,7 @@ use pocketmine\block\tile\Bed as TileBed; use pocketmine\block\tile\Bell as TileBell; use pocketmine\block\tile\BlastFurnace as TileBlastFurnace; use pocketmine\block\tile\BrewingStand as TileBrewingStand; +use pocketmine\block\tile\Cauldron as TileCauldron; use pocketmine\block\tile\Chest as TileChest; use pocketmine\block\tile\Comparator as TileComparator; use pocketmine\block\tile\DaylightSensor as TileDaylightSensor; @@ -143,6 +144,7 @@ use function mb_strtolower; * @method static Carrot CARROTS() * @method static CartographyTable CARTOGRAPHY_TABLE() * @method static CarvedPumpkin CARVED_PUMPKIN() + * @method static Cauldron CAULDRON() * @method static ChemicalHeat CHEMICAL_HEAT() * @method static Chest CHEST() * @method static Opaque CHISELED_DEEPSLATE() @@ -453,6 +455,7 @@ use function mb_strtolower; * @method static LapisOre LAPIS_LAZULI_ORE() * @method static DoubleTallGrass LARGE_FERN() * @method static Lava LAVA() + * @method static LavaCauldron LAVA_CAULDRON() * @method static Lectern LECTERN() * @method static Opaque LEGACY_STONECUTTER() * @method static Lever LEVER() @@ -558,6 +561,7 @@ use function mb_strtolower; * @method static Stair POLISHED_GRANITE_STAIRS() * @method static Flower POPPY() * @method static Potato POTATOES() + * @method static PotionCauldron POTION_CAULDRON() * @method static PoweredRail POWERED_RAIL() * @method static Opaque PRISMARINE() * @method static Opaque PRISMARINE_BRICKS() @@ -697,6 +701,7 @@ use function mb_strtolower; * @method static WallSign WARPED_WALL_SIGN() * @method static Opaque WARPED_WART_BLOCK() * @method static Water WATER() + * @method static WaterCauldron WATER_CAULDRON() * @method static WeightedPressurePlateHeavy WEIGHTED_PRESSURE_PLATE_HEAVY() * @method static WeightedPressurePlateLight WEIGHTED_PRESSURE_PLATE_LIGHT() * @method static Wheat WHEAT() @@ -1166,6 +1171,7 @@ final class VanillaBlocks{ self::registerCraftingTables(); self::registerOres(); self::registerWoodenBlocks(); + self::registerCauldronBlocks(); } private static function registerWoodenBlocks() : void{ @@ -1513,4 +1519,12 @@ final class VanillaBlocks{ self::register("mud_brick_wall", new Wall(new BID(Ids::MUD_BRICK_WALL), "Mud Brick Wall", $mudBricksBreakInfo)); } + private static function registerCauldronBlocks() : void{ + $cauldronBreakInfo = new BreakInfo(2, ToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel()); + + self::register("cauldron", new Cauldron(new BID(Ids::CAULDRON, TileCauldron::class), "Cauldron", $cauldronBreakInfo)); + self::register("water_cauldron", new WaterCauldron(new BID(Ids::WATER_CAULDRON, TileCauldron::class), "Water Cauldron", $cauldronBreakInfo)); + self::register("lava_cauldron", new LavaCauldron(new BID(Ids::LAVA_CAULDRON, TileCauldron::class), "Lava Cauldron", $cauldronBreakInfo)); + self::register("potion_cauldron", new PotionCauldron(new BID(Ids::POTION_CAULDRON, TileCauldron::class), "Potion Cauldron", $cauldronBreakInfo)); + } } diff --git a/src/block/WaterCauldron.php b/src/block/WaterCauldron.php new file mode 100644 index 000000000..f346ac4a0 --- /dev/null +++ b/src/block/WaterCauldron.php @@ -0,0 +1,209 @@ +position->getWorld()->getTile($this->position); + + $potionItem = $tile instanceof TileCauldron ? $tile->getPotionItem() : null; + if($potionItem !== null){ + //TODO: HACK! we keep potion cauldrons as a separate block type due to different behaviour, but in the + //blockstate they are typically indistinguishable from water cauldrons. This hack converts cauldrons into + //their appropriate type. + return VanillaBlocks::POTION_CAULDRON()->setFillLevel($this->getFillLevel())->setPotionItem($potionItem); + } + + $this->customWaterColor = $tile instanceof TileCauldron ? $tile->getCustomWaterColor() : null; + + return $this; + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileCauldron); + $tile->setCustomWaterColor($this->customWaterColor); + $tile->setPotionItem(null); + } + + /** @return Color|null */ + public function getCustomWaterColor() : ?Color{ return $this->customWaterColor; } + + /** @return $this */ + public function setCustomWaterColor(?Color $customWaterColor) : self{ + $this->customWaterColor = $customWaterColor; + return $this; + } + + public function getFillSound() : Sound{ + return new CauldronFillWaterSound(); + } + + public function getEmptySound() : Sound{ + return new CauldronEmptyWaterSound(); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + if(($newColor = match($item->getTypeId()){ + ItemTypeIds::LAPIS_LAZULI => DyeColor::BLUE()->getRgbValue(), + ItemTypeIds::INK_SAC => DyeColor::BLACK()->getRgbValue(), + ItemTypeIds::COCOA_BEANS => DyeColor::BROWN()->getRgbValue(), + ItemTypeIds::BONE_MEAL => DyeColor::WHITE()->getRgbValue(), + ItemTypeIds::DYE => $item instanceof Dye ? $item->getColor()->getRgbValue() : null, + default => null + }) !== null && $newColor->toRGBA() !== $this->customWaterColor?->toRGBA() + ){ + $this->position->getWorld()->setBlock($this->position, $this->setCustomWaterColor($this->customWaterColor === null ? $newColor : Color::mix($this->customWaterColor, $newColor))); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronAddDyeSound()); + + $item->pop(); + }elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion + if($item->getType()->equals(PotionType::WATER())){ + $this->addFillLevels(self::WATER_BOTTLE_FILL_AMOUNT, $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); + }else{ + $this->mix($item, VanillaItems::GLASS_BOTTLE(), $returnedItems); + } + }elseif($item instanceof Armor){ + if($this->customWaterColor !== null){ + if(match($item->getTypeId()){ //TODO: a DyeableArmor class would probably be a better idea, since not all types of armor are dyeable + ItemTypeIds::LEATHER_CAP, + ItemTypeIds::LEATHER_TUNIC, + ItemTypeIds::LEATHER_PANTS, + ItemTypeIds::LEATHER_BOOTS => true, + default => false + } && $item->getCustomColor()?->toRGBA() !== $this->customWaterColor->toRGBA()){ + $item->setCustomColor($this->customWaterColor); + $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->getFillLevel() - self::DYE_ARMOR_USE_AMOUNT)); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronDyeItemSound()); + } + }elseif($item->getCustomColor() !== null){ + $item->clearCustomColor(); + $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->getFillLevel() - self::CLEAN_ARMOR_USE_AMOUNT)); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronCleanItemSound()); + } + }elseif($item instanceof Banner){ + $patterns = $item->getPatterns(); + if(count($patterns) > 0 && $this->customWaterColor === null){ + array_pop($patterns); + $item->setPatterns($patterns); + + $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->getFillLevel() - self::CLEAN_BANNER_USE_AMOUNT)); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronCleanItemSound()); + } + }elseif($item instanceof ItemBlock && $item->getBlock()->getTypeId() === BlockTypeIds::DYED_SHULKER_BOX){ + if($this->customWaterColor === null){ + $newItem = VanillaBlocks::SHULKER_BOX()->asItem(); + $newItem->setNamedTag($item->getNamedTag()); + + $item->pop(); + $returnedItems[] = $newItem; + + $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->getFillLevel() - self::CLEAN_SHULKER_BOX_USE_AMOUNT)); + $this->position->getWorld()->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronCleanItemSound()); + } + }else{ + match($item->getTypeId()){ + ItemTypeIds::WATER_BUCKET => $this->addFillLevels(self::MAX_FILL_LEVEL, $item, VanillaItems::BUCKET(), $returnedItems), + ItemTypeIds::BUCKET => $this->removeFillLevels(self::MAX_FILL_LEVEL, $item, VanillaItems::WATER_BUCKET(), $returnedItems), + ItemTypeIds::GLASS_BOTTLE => $this->removeFillLevels(self::WATER_BOTTLE_FILL_AMOUNT, $item, VanillaItems::POTION()->setType(PotionType::WATER()), $returnedItems), + ItemTypeIds::LAVA_BUCKET, ItemTypeIds::POWDER_SNOW_BUCKET => $this->mix($item, VanillaItems::BUCKET(), $returnedItems), + default => null + }; + } + + return true; + } + + public function hasEntityCollision() : bool{ return true; } + + public function onEntityInside(Entity $entity) : bool{ + if($entity->isOnFire()){ + $entity->extinguish(); + //TODO: particles + + $this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->getFillLevel() - self::ENTITY_EXTINGUISH_USE_AMOUNT)); + } + + return true; + } + + public function onNearbyBlockChange() : void{ + $hasCustomWaterColor = $this->customWaterColor !== null; + if($this->getFillLevel() < self::MAX_FILL_LEVEL || $hasCustomWaterColor){ + $world = $this->position->getWorld(); + if($world->getBlock($this->position->up())->getTypeId() === BlockTypeIds::WATER){ + if($hasCustomWaterColor){ + //TODO: particles + } + $world->setBlock($this->position, $this->setCustomWaterColor(null)->setFillLevel(FillableCauldron::MAX_FILL_LEVEL)); + $world->addSound($this->position->add(0.5, 0.5, 0.5), $this->getFillSound()); + } + } + } +} diff --git a/src/block/tile/Cauldron.php b/src/block/tile/Cauldron.php new file mode 100644 index 000000000..d10f97e14 --- /dev/null +++ b/src/block/tile/Cauldron.php @@ -0,0 +1,135 @@ +potionItem; } + + public function setPotionItem(?Item $potionItem) : void{ + $this->potionItem = $potionItem; + } + + public function getCustomWaterColor() : ?Color{ return $this->customWaterColor; } + + public function setCustomWaterColor(?Color $customWaterColor) : void{ + $this->customWaterColor = $customWaterColor; + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + $nbt->setShort(self::TAG_POTION_CONTAINER_TYPE, match($this->potionItem?->getTypeId()){ + ItemTypeIds::POTION => self::POTION_CONTAINER_TYPE_NORMAL, + ItemTypeIds::SPLASH_POTION => self::POTION_CONTAINER_TYPE_SPLASH, + ItemTypeIds::LINGERING_POTION => self::POTION_CONTAINER_TYPE_LINGERING, + null => self::POTION_CONTAINER_TYPE_NONE, + default => throw new AssumptionFailedError("Unexpected potion item type") + }); + + //TODO: lingering potion + $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; + $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); + + if($this->customWaterColor !== null){ + $nbt->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->customWaterColor->toARGB())); + } + } + + public function readSaveData(CompoundTag $nbt) : void{ + $containerType = $nbt->getShort(self::TAG_POTION_CONTAINER_TYPE, self::POTION_CONTAINER_TYPE_NONE); + $potionId = $nbt->getShort(self::TAG_POTION_ID, self::POTION_ID_NONE); + if($containerType !== self::POTION_CONTAINER_TYPE_NONE && $potionId !== self::POTION_ID_NONE){ + $potionType = PotionTypeIdMap::getInstance()->fromId($potionId); + if($potionType === null){ + throw new SavedDataLoadingException("Unknown potion type ID $potionId"); + } + $this->potionItem = match($containerType){ + self::POTION_CONTAINER_TYPE_NORMAL => VanillaItems::POTION()->setType($potionType), + self::POTION_CONTAINER_TYPE_SPLASH => VanillaItems::SPLASH_POTION()->setType($potionType), + self::POTION_CONTAINER_TYPE_LINGERING => throw new SavedDataLoadingException("Not implemented"), + default => throw new SavedDataLoadingException("Invalid potion container type ID $containerType") + }; + }else{ + $this->potionItem = null; + } + + $this->customWaterColor = ($customColorTag = $nbt->getTag(self::TAG_CUSTOM_COLOR)) instanceof IntTag ? Color::fromARGB(Binary::unsignInt($customColorTag->getValue())) : null; + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setShort(self::TAG_POTION_CONTAINER_TYPE, match($this->potionItem?->getTypeId()){ + ItemTypeIds::POTION => self::POTION_CONTAINER_TYPE_NORMAL, + ItemTypeIds::SPLASH_POTION => self::POTION_CONTAINER_TYPE_SPLASH, + ItemTypeIds::LINGERING_POTION => self::POTION_CONTAINER_TYPE_LINGERING, + null => self::POTION_CONTAINER_TYPE_NONE, + default => throw new AssumptionFailedError("Unexpected potion item type") + }); + + //TODO: lingering potion + $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; + $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); + + if($this->customWaterColor !== null){ + $nbt->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->customWaterColor->toARGB())); + } + } + + public function getRenderUpdateBugWorkaroundStateProperties(Block $block) : array{ + if($block instanceof FillableCauldron){ + $realFillLevel = $block->getFillLevel(); + return [BlockStateNames::FILL_LEVEL => new IntTag($realFillLevel === FillableCauldron::MAX_FILL_LEVEL ? FillableCauldron::MIN_FILL_LEVEL : $realFillLevel + 1)]; + } + + return []; + } +} diff --git a/src/block/tile/TileFactory.php b/src/block/tile/TileFactory.php index d321c4754..4a9c73872 100644 --- a/src/block/tile/TileFactory.php +++ b/src/block/tile/TileFactory.php @@ -57,6 +57,7 @@ final class TileFactory{ $this->register(Bell::class, ["Bell", "minecraft:bell"]); $this->register(BlastFurnace::class, ["BlastFurnace", "minecraft:blast_furnace"]); $this->register(BrewingStand::class, ["BrewingStand", "minecraft:brewing_stand"]); + $this->register(Cauldron::class, ["Cauldron", "minecraft:cauldron"]); $this->register(Chest::class, ["Chest", "minecraft:chest"]); $this->register(Comparator::class, ["Comparator", "minecraft:comparator"]); $this->register(DaylightSensor::class, ["DaylightDetector", "minecraft:daylight_detector"]); diff --git a/src/data/bedrock/block/convert/BlockObjectToBlockStateSerializer.php b/src/data/bedrock/block/convert/BlockObjectToBlockStateSerializer.php index 27b620ee0..cba222dab 100644 --- a/src/data/bedrock/block/convert/BlockObjectToBlockStateSerializer.php +++ b/src/data/bedrock/block/convert/BlockObjectToBlockStateSerializer.php @@ -68,6 +68,7 @@ use pocketmine\block\EndPortalFrame; use pocketmine\block\EndRod; use pocketmine\block\Farmland; use pocketmine\block\FenceGate; +use pocketmine\block\FillableCauldron; use pocketmine\block\Fire; use pocketmine\block\FloorBanner; use pocketmine\block\FloorCoralFan; @@ -176,6 +177,7 @@ final class BlockObjectToBlockStateSerializer implements BlockStateSerializer{ public function __construct(){ $this->registerCandleSerializers(); + $this->registerCauldronSerializers(); $this->registerSimpleSerializers(); $this->registerSerializers(); } @@ -292,6 +294,14 @@ final class BlockObjectToBlockStateSerializer implements BlockStateSerializer{ })->writeBool(StateNames::LIT, $block->isLit())); } + private function registerCauldronSerializers() : void{ + $this->map(Blocks::CAULDRON(), fn() => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_WATER, 0, new Writer(Ids::CAULDRON))); + $this->map(Blocks::LAVA_CAULDRON(), fn(FillableCauldron $b) => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_LAVA, $b->getFillLevel(), new Writer(Ids::LAVA_CAULDRON))); + //potion cauldrons store their real information in the block actor data + $this->map(Blocks::POTION_CAULDRON(), fn(FillableCauldron $b) => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_WATER, $b->getFillLevel(), new Writer(Ids::CAULDRON))); + $this->map(Blocks::WATER_CAULDRON(), fn(FillableCauldron $b) => Helper::encodeCauldron(StringValues::CAULDRON_LIQUID_WATER, $b->getFillLevel(), new Writer(Ids::CAULDRON))); + } + private function registerSimpleSerializers() : void{ $this->mapSimple(Blocks::AIR(), Ids::AIR); $this->mapSimple(Blocks::AMETHYST(), Ids::AMETHYST_BLOCK); diff --git a/src/data/bedrock/block/convert/BlockStateSerializerHelper.php b/src/data/bedrock/block/convert/BlockStateSerializerHelper.php index cd16b45b7..c87b64788 100644 --- a/src/data/bedrock/block/convert/BlockStateSerializerHelper.php +++ b/src/data/bedrock/block/convert/BlockStateSerializerHelper.php @@ -91,6 +91,12 @@ final class BlockStateSerializerHelper{ ->writeTorchFacing($block->getFacing()); } + public static function encodeCauldron(string $liquid, int $fillLevel, BlockStateWriter $out) : BlockStateWriter{ + return $out + ->writeString(BlockStateNames::CAULDRON_LIQUID, $liquid) + ->writeInt(BlockStateNames::FILL_LEVEL, $fillLevel); + } + public static function selectCopperId(CopperOxidation $oxidation, string $noneId, string $exposedId, string $weatheredId, string $oxidizedId) : string{ return match($oxidation){ CopperOxidation::NONE() => $noneId, diff --git a/src/data/bedrock/block/convert/BlockStateToBlockObjectDeserializer.php b/src/data/bedrock/block/convert/BlockStateToBlockObjectDeserializer.php index fb689c294..765a5ae1d 100644 --- a/src/data/bedrock/block/convert/BlockStateToBlockObjectDeserializer.php +++ b/src/data/bedrock/block/convert/BlockStateToBlockObjectDeserializer.php @@ -60,6 +60,7 @@ final class BlockStateToBlockObjectDeserializer implements BlockStateDeserialize public function __construct(){ $this->registerCandleDeserializers(); + $this->registerCauldronDeserializers(); $this->registerSimpleDeserializers(); $this->registerDeserializers(); } @@ -136,6 +137,25 @@ final class BlockStateToBlockObjectDeserializer implements BlockStateDeserialize $this->map(Ids::YELLOW_CANDLE_CAKE, $cakeWithDyedCandleDeserializer(DyeColor::YELLOW())); } + private function registerCauldronDeserializers() : void{ + $deserializer = function(Reader $in) : Block{ + $level = $in->readBoundedInt(StateNames::FILL_LEVEL, 0, 6); + if($level === 0){ + $in->ignored(StateNames::CAULDRON_LIQUID); + return Blocks::CAULDRON(); + } + + return (match($liquid = $in->readString(StateNames::CAULDRON_LIQUID)){ + StringValues::CAULDRON_LIQUID_WATER => Blocks::WATER_CAULDRON(), + StringValues::CAULDRON_LIQUID_LAVA => Blocks::LAVA_CAULDRON(), + StringValues::CAULDRON_LIQUID_POWDER_SNOW => throw new UnsupportedBlockStateException("Powder snow is not supported yet"), + default => throw $in->badValueException(StateNames::CAULDRON_LIQUID, $liquid) + })->setFillLevel($level); + }; + $this->map(Ids::CAULDRON, $deserializer); + $this->map(Ids::LAVA_CAULDRON, $deserializer); + } + private function registerSimpleDeserializers() : void{ $this->map(Ids::AIR, fn() => Blocks::AIR()); $this->map(Ids::AMETHYST_BLOCK, fn() => Blocks::AMETHYST()); diff --git a/src/data/bedrock/item/ItemDeserializer.php b/src/data/bedrock/item/ItemDeserializer.php index dfa463bc8..85d415d6a 100644 --- a/src/data/bedrock/item/ItemDeserializer.php +++ b/src/data/bedrock/item/ItemDeserializer.php @@ -198,7 +198,7 @@ final class ItemDeserializer{ $this->map(Ids::CARROT, fn() => Items::CARROT()); //TODO: minecraft:carrot_on_a_stick //TODO: minecraft:cat_spawn_egg - //TODO: minecraft:cauldron + $this->map(Ids::CAULDRON, fn() => Blocks::CAULDRON()->asItem()); //TODO: minecraft:cave_spider_spawn_egg //TODO: minecraft:chain $this->map(Ids::CHAINMAIL_BOOTS, fn() => Items::CHAINMAIL_BOOTS()); diff --git a/src/data/bedrock/item/ItemSerializer.php b/src/data/bedrock/item/ItemSerializer.php index b652d3a3a..0c993f2e6 100644 --- a/src/data/bedrock/item/ItemSerializer.php +++ b/src/data/bedrock/item/ItemSerializer.php @@ -229,6 +229,7 @@ final class ItemSerializer{ $this->mapBlock(Blocks::BIRCH_DOOR(), self::id(Ids::BIRCH_DOOR)); $this->mapBlock(Blocks::BREWING_STAND(), self::id(Ids::BREWING_STAND)); $this->mapBlock(Blocks::CAKE(), self::id(Ids::CAKE)); + $this->mapBlock(Blocks::CAULDRON(), self::id(Ids::CAULDRON)); $this->mapBlock(Blocks::CRIMSON_DOOR(), self::id(Ids::CRIMSON_DOOR)); $this->mapBlock(Blocks::DARK_OAK_DOOR(), self::id(Ids::DARK_OAK_DOOR)); $this->mapBlock(Blocks::FLOWER_POT(), self::id(Ids::FLOWER_POT)); diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index ff6ef0a04..d82f23642 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -294,6 +294,8 @@ final class ItemTypeIds{ public const RAW_GOLD = 20255; public const SPYGLASS = 20256; public const NETHERITE_SCRAP = 20257; + public const POWDER_SNOW_BUCKET = 20258; + public const LINGERING_POTION = 20259; - public const FIRST_UNUSED_ITEM_ID = 20258; + public const FIRST_UNUSED_ITEM_ID = 20260; } diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 2a8b9d107..0b41a7826 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -197,6 +197,7 @@ final class StringToItemParser extends StringToTParser{ $result->registerBlock("carrot_block", fn() => Blocks::CARROTS()); $result->registerBlock("carrots", fn() => Blocks::CARROTS()); $result->registerBlock("carved_pumpkin", fn() => Blocks::CARVED_PUMPKIN()); + $result->registerBlock("cauldron", fn() => Blocks::CAULDRON()); $result->registerBlock("chemical_heat", fn() => Blocks::CHEMICAL_HEAT()); $result->registerBlock("chemistry_table", fn() => Blocks::COMPOUND_CREATOR()); $result->registerBlock("chest", fn() => Blocks::CHEST()); diff --git a/src/world/sound/CauldronAddDyeSound.php b/src/world/sound/CauldronAddDyeSound.php new file mode 100644 index 000000000..2a427959e --- /dev/null +++ b/src/world/sound/CauldronAddDyeSound.php @@ -0,0 +1,35 @@ +getMessage()); } + if($block->getTypeId() === BlockTypeIds::POTION_CAULDRON){ + //this pretends to be a water cauldron in the blockstate, and stores its actual data in the blockentity + continue; + } + //The following are workarounds for differences in blockstate representation in Bedrock vs PM //In these cases, some properties are not stored in the blockstate (but rather in the block entity NBT), but //they do form part of the internal blockstate hash in PM. This leads to inconsistencies when serializing