├── .poggit.yml ├── README.md ├── plugin.yml └── src ├── Loader.php ├── block ├── Allow.php ├── Azalea.php ├── AzaleaLeaves.php ├── AzaleaLeavesFlowered.php ├── BaseCampfire.php ├── BaseCaveVines.php ├── Border.php ├── Camera.php ├── Campfire.php ├── CaveVines.php ├── CaveVinesBodyWithBerries.php ├── CaveVinesHeadWithBerries.php ├── Composter.php ├── Deny.php ├── EndGateway.php ├── EndPortal.php ├── EndPortalFrame.php ├── ExtraVanillaBlocks.php ├── FloweringAzalea.php ├── Honey.php ├── Kelp.php ├── MangroveLeaves.php ├── MangrovePropagule.php ├── Moss.php ├── MossCarpet.php ├── Sculk.php ├── SculkCatalyst.php ├── SculkSensor.php ├── SculkShrieker.php ├── SoulCampfire.php ├── Target.php ├── tile │ ├── BaseCampfireTile.php │ ├── ContainerTrait.php │ ├── RegularCampfireTile.php │ └── SoulCampfireTile.php └── utils │ ├── BlockAgeTrait.php │ ├── BlockIngredientsHelper.php │ ├── BlockTypeIdTrait.php │ └── IBlockState.php ├── entity └── projectile │ └── IceBomb.php ├── expansion ├── BlockExpansion.php ├── EntityExpansion.php ├── IExpansion.php └── ItemExpansion.php ├── item ├── Camera.php ├── Campfire.php ├── EnderEye.php ├── ExtraVanillaItems.php ├── GlowBerries.php ├── IceBomb.php ├── Kelp.php ├── SoulCampfire.php └── utils │ └── ItemTypeIdTrait.php └── world ├── generator └── object │ └── AzaleaTree.php ├── particle └── BoneMealParticle.php └── sound ├── CaveVinesPickBerriesSound.php ├── ComposterEmptySound.php ├── ComposterFillSound.php ├── ComposterFillSuccessSound.php ├── ComposterReadySound.php ├── EndPortalFrameFillSound.php └── EndPortalFrameSpawnSound.php /.poggit.yml: -------------------------------------------------------------------------------- 1 | --- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/skh6075/PMExpansion 2 | build-by-default: true 3 | branches: 4 | - pm5 5 | projects: 6 | PMExpansion: 7 | path: "" 8 | ... 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PMExpansion 2 | A plugin that make the PocketMine-MP environment similar to vanilla. 3 | 4 | # Extended System 5 | This plugin is a plugin that adds features that are not **not implemented at all** in **PocketMine-MP** software. 6 | 7 | ## Extended Blocks 8 | - [x] Azalea (Leaves, LeavesFlowered, FloweringAzalea) 9 | - [x] Sculk (Catalyst, Sensor, Shrieker) 10 | - [x] EndPortal (EndPortalFrame, EndPortal, EndGateway) 11 | - [x] Moss (Carpet) 12 | - [x] Education (Allow, Deny, Border) 13 | - [x] CaveVines GlowBerries (Body, Head) 14 | - [x] Target 15 | - [x] Kelp 16 | - [x] CaveVines GlowBerries 17 | - [x] Camera 18 | 19 | ## Extended Items 20 | - [x] IceBomb 21 | - [x] EnderEye (Eye of Ender) 22 | - [x] Camera 23 | - [x] GlowBerries 24 | - [x] Kelp 25 | 26 | ## Extended Objects 27 | - [x] AzaleaTree 28 | 29 | ## Released 1.0.5 30 | **Blocks** 31 | - Added Honey Block 32 | - Added Campfire & Soul Campfire (Cooking interaction too) 33 | - Added Composter Block (Recycle interaction too) 34 | 35 | **Refactor** 36 | - Move the sound path (world/sound/block -> world/sound) 37 | - The Chain block is implemented in core and is no longer supported by PMExpansion. [here](https://github.com/pmmp/PocketMine-MP/commit/b3473960b49f397f64ce6cbcc994bf413b2ab4ac) 38 | 39 | ![Campfire](https://user-images.githubusercontent.com/44698603/209446243-26cfba0b-3d20-42d0-aea1-24e96eeb95f0.png) 40 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: PMExpansion 2 | main: skh6075\pmexpansion\Loader 3 | author: skh6075 4 | version: 1.0.5 5 | api: 5.0.0 6 | 7 | src-namespace-prefix: skh6075\pmexpansion -------------------------------------------------------------------------------- /src/Loader.php: -------------------------------------------------------------------------------- 1 | synchronizeExpansions(); 16 | } 17 | 18 | private function synchronizeExpansions(): void{ 19 | EntityExpansion::synchronize(); 20 | BlockExpansion::synchronize(); 21 | ItemExpansion::synchronize(); 22 | 23 | $this->getServer()->getAsyncPool()->addWorkerStartHook(function(int $worker): void{ 24 | $this->getServer()->getAsyncPool()->submitTaskToWorker(new class extends AsyncTask{ 25 | public function onRun() : void{ 26 | BlockExpansion::synchronize(); 27 | ItemExpansion::synchronize(); 28 | } 29 | }, $worker); 30 | }); 31 | } 32 | } -------------------------------------------------------------------------------- /src/block/Allow.php: -------------------------------------------------------------------------------- 1 | getSide(Facing::DOWN)->isSameType(VanillaBlocks::AIR())){ 32 | $this->position->getWorld()->useBreakOn($this->position); 33 | } 34 | } 35 | 36 | public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ 37 | if(!$this->getSide(Facing::DOWN)->isTransparent()){ 38 | return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); 39 | } 40 | return false; 41 | } 42 | 43 | public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ 44 | if($item instanceof Fertilizer){ 45 | $this->grow(); 46 | $this->position->world->addParticle($this->position, new BoneMealParticle()); 47 | 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | private function grow(): void{ 55 | (new AzaleaTree())->generate($this->position->getWorld(), new Random(), $this->position); 56 | } 57 | } -------------------------------------------------------------------------------- /src/block/AzaleaLeaves.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::AZALEA_LEAVES) 30 | ->writeBool(BlockStateNames::PERSISTENT_BIT, $block->isPersistentBit()) 31 | ->writeBool(BlockStateNames::UPDATE_BIT, $block->isUpdateBit()); 32 | } 33 | 34 | public function getStateDeserialize() : ?Closure{ 35 | return static fn(BlockStateReader $in) : AzaleaLeaves => ExtraVanillaBlocks::AZALEA_LEAVES() 36 | ->setPersistentBit($in->readBool(BlockStateNames::PERSISTENT_BIT)) 37 | ->setUpdateBit($in->readBool(BlockStateNames::UPDATE_BIT)); 38 | } 39 | 40 | public function getRequiredStateDataBits() : int{ return 2; } 41 | 42 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 43 | $w->bool($this->persistentBit); 44 | $w->bool($this->updateBit); 45 | } 46 | 47 | public function isPersistentBit(): bool{ return $this->persistentBit; } 48 | 49 | public function setPersistentBit(bool $state): self{ 50 | $this->persistentBit = $state; 51 | return $this; 52 | } 53 | 54 | public function isUpdateBit(): bool{ return $this->updateBit; } 55 | 56 | public function setUpdateBit(bool $state): self{ 57 | $this->updateBit = $state; 58 | return $this; 59 | } 60 | 61 | public function getDropsForCompatibleTool(Item $item) : array{ 62 | if(($item->getBlockToolType() & BlockToolType::SHEARS) !== 0){ 63 | return parent::getDropsForCompatibleTool($item); 64 | } 65 | 66 | $drops = []; 67 | if(random_int(1, 256) === 1){ 68 | $drops[] = ExtraVanillaBlocks::AZALEA()->asItem(); 69 | } 70 | if(random_int(1, 50) === 1){ 71 | $drops[] = VanillaItems::STICK(); 72 | } 73 | 74 | return $drops; 75 | } 76 | } -------------------------------------------------------------------------------- /src/block/AzaleaLeavesFlowered.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::AZALEA_LEAVES_FLOWERED) 29 | ->writeBool(BlockStateNames::PERSISTENT_BIT, $block->isPersistentBit()) 30 | ->writeBool(BlockStateNames::UPDATE_BIT, $block->isUpdateBit()); 31 | } 32 | 33 | public function getStateDeserialize() : ?Closure{ 34 | return static fn(BlockStateReader $in) : AzaleaLeavesFlowered => ExtraVanillaBlocks::AZALEA_LEAVES_FLOWERED() 35 | ->setPersistentBit($in->readBool(BlockStateNames::PERSISTENT_BIT)) 36 | ->setUpdateBit($in->readBool(BlockStateNames::UPDATE_BIT)); 37 | } 38 | 39 | public function getRequiredStateDataBits() : int{ return 2; } 40 | 41 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 42 | $w->bool($this->persistentBit); 43 | $w->bool($this->updateBit); 44 | } 45 | 46 | public function isPersistentBit(): bool{ return $this->persistentBit; } 47 | 48 | public function setPersistentBit(bool $state): self{ 49 | $this->persistentBit = $state; 50 | return $this; 51 | } 52 | 53 | public function isUpdateBit(): bool{ return $this->updateBit; } 54 | 55 | public function setUpdateBit(bool $state): self{ 56 | $this->updateBit = $state; 57 | return $this; 58 | } 59 | 60 | public function getDropsForCompatibleTool(Item $item) : array{ 61 | if(($item->getBlockToolType() & BlockToolType::SHEARS) !== 0){ 62 | return parent::getDropsForCompatibleTool($item); 63 | } 64 | 65 | $drops = []; 66 | if(random_int(1, 256) === 1){ 67 | $drops[] = ExtraVanillaBlocks::AZALEA()->asItem(); 68 | } 69 | if(random_int(1, 50) === 1){ 70 | $drops[] = VanillaItems::STICK(); 71 | } 72 | 73 | return $drops; 74 | } 75 | } -------------------------------------------------------------------------------- /src/block/BaseCampfire.php: -------------------------------------------------------------------------------- 1 | extinguish; } 36 | 37 | public function setExtinguish(bool $state) : self{ 38 | $this->extinguish = $state; 39 | return $this; 40 | } 41 | 42 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 43 | $w->horizontalFacing($this->facing); 44 | $w->bool($this->extinguish); 45 | } 46 | 47 | protected function recalculateCollisionBoxes() : array{ 48 | return [AxisAlignedBB::one()->trim(Facing::UP, 0.5)]; 49 | } 50 | 51 | public function getRequiredStateDataBits() : int{ return 3; } 52 | 53 | public function hasEntityCollision() : bool{ return true; } 54 | 55 | public function isAffectedBySilkTouch() : bool{ return true; } 56 | 57 | public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ 58 | if(!$this->extinguish){ 59 | if($face === Facing::UP && $item instanceof Shovel){ 60 | $block = clone $this; 61 | $block->extinguish = true; 62 | $this->position->getWorld()->setBlock($this->position, $block); 63 | $this->position->getWorld()->addSound($this->position, new FizzSound()); 64 | 65 | return true; 66 | } 67 | 68 | if($player !== null){ 69 | $tile = $this->position->getWorld()->getTile($this->position); 70 | if($tile instanceof CampfireTile && $tile->addItem($item)){ 71 | $item->pop(); 72 | $this->position->getWorld()->setBlock($this->position, $this); 73 | $this->position->getWorld()->addSound($this->position, new ItemFrameAddItemSound()); //lol 74 | } 75 | 76 | return true; 77 | } 78 | }elseif($item->getTypeId() === ItemTypeIds::FLINT_AND_STEEL){ 79 | $block = clone $this; 80 | $block->extinguish = false; 81 | $this->position->getWorld()->setBlock($this->position, $block); 82 | $this->position->getWorld()->addSound($this->position, new FlintSteelSound()); 83 | 84 | return true; 85 | } 86 | 87 | return false; 88 | } 89 | 90 | public function onEntityInside(Entity $entity) : bool{ 91 | if($this->extinguish || ($entity instanceof Player && !$entity->hasFiniteResources()) || $entity->isOnFire()){ 92 | return false; 93 | } 94 | 95 | $entity->setOnFire(8); 96 | $entity->attack(new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_FIRE, 1)); 97 | 98 | return true; 99 | } 100 | 101 | public function onScheduledUpdate() : void{ 102 | if($this->extinguish){ 103 | return; 104 | } 105 | 106 | $tile = $this->position->getWorld()->getTile($this->position); 107 | if(!$tile instanceof CampfireTile || $tile->isClosed()){ 108 | return; 109 | } 110 | 111 | $canChange = false; 112 | foreach($tile->getContents() as $slot => $item){ 113 | $tile->increaseSlotTime($slot); 114 | if($tile->getItemTime($slot) < 30){ 115 | continue; 116 | } 117 | 118 | $tile->setItem(VanillaItems::AIR(), $slot); 119 | $tile->setSlotTime($slot, 0); 120 | 121 | $this->position->getWorld()->dropItem( 122 | source: $this->position->add(0, 1, 0), 123 | item: $tile->getCookResultItem($item) 124 | ); 125 | $canChange = true; 126 | } 127 | 128 | if($canChange){ 129 | $this->position->getWorld()->setBlock($this->position, $this); 130 | } 131 | 132 | $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 20); 133 | } 134 | 135 | public function getDropsForCompatibleTool(Item $item) : array{ 136 | return [VanillaItems::CHARCOAL()->setCount(4)]; 137 | } 138 | } -------------------------------------------------------------------------------- /src/block/BaseCaveVines.php: -------------------------------------------------------------------------------- 1 | boundedInt(5, 0, $this->getMaxAge(), $this->age); 24 | } 25 | 26 | public function onNearbyBlockChange() : void{ 27 | $up = $this->getSide(Facing::UP); 28 | if(!$up->isSameType($this) && !$up->isSolid()){ 29 | $this->position->world->useBreakOn($this->position); 30 | } 31 | } 32 | 33 | public function getDrops(Item $item) : array{ return []; } 34 | } -------------------------------------------------------------------------------- /src/block/Border.php: -------------------------------------------------------------------------------- 1 | BlockStateSerializerHelper::encodeWall($block, new BlockStateWriter(BlockTypeNames::BORDER_BLOCK)); 22 | } 23 | 24 | public function getStateDeserialize() : ?Closure{ 25 | return static fn(BlockStateReader $in) : Border|Wall => BlockStateDeserializerHelper::decodeWall(ExtraVanillaBlocks::BORDER(), $in); 26 | } 27 | } -------------------------------------------------------------------------------- /src/block/Camera.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::CAMERA) 25 | ->writeHorizontalFacing($block->getFacing()); 26 | } 27 | 28 | public function getStateDeserialize() : ?Closure{ 29 | return static fn(BlockStateReader $in): Camera|Block => ExtraVanillaBlocks::CAMERA() 30 | ->setFacing($in->readHorizontalFacing()); 31 | } 32 | } -------------------------------------------------------------------------------- /src/block/Campfire.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::CAMPFIRE) 18 | ->writeLegacyHorizontalFacing($block->getFacing()) 19 | ->writeBool(BlockStateNames::EXTINGUISHED, $block->isExtinguished()); 20 | } 21 | 22 | public function getStateDeserialize() : ?Closure{ 23 | return fn(BlockStateReader $in): Campfire => ExtraVanillaBlocks::CAMPFIRE() 24 | ->setFacing($in->readLegacyHorizontalFacing()) 25 | ->setExtinguish($in->readBool(BlockStateNames::EXTINGUISHED)); 26 | } 27 | 28 | public function asItem() : Item{ 29 | return ExtraVanillaItems::CAMPFIRE(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/block/CaveVines.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::CAVE_VINES) 29 | ->writeInt(BlockStateNames::GROWING_PLANT_AGE, $block->getAge()); 30 | } 31 | 32 | public function getStateDeserialize() : ?Closure{ 33 | return fn(BlockStateReader $in) : CaveVines => ExtraVanillaBlocks::CAVE_VINES() 34 | ->setAge($in->readBoundedInt(BlockStateNames::GROWING_PLANT_AGE, 0, $this->getMaxAge())); 35 | } 36 | 37 | public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ 38 | if(Facing::axis($face) === Axis::Y){ 39 | return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); 40 | } 41 | 42 | return false; 43 | } 44 | 45 | public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ 46 | if($item instanceof Fertilizer){ 47 | $this->grow(); 48 | $this->position->world->addParticle($this->position, new BoneMealParticle()); 49 | $item->pop(); 50 | 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | public function ticksRandomly() : bool{ return true; } 58 | 59 | public function onRandomTick() : void{ 60 | if($this->age === $this->getMaxAge()){ 61 | $this->grow(); 62 | }else{ 63 | ++$this->age; 64 | $this->position->world->setBlock($this->position, $this); 65 | } 66 | } 67 | 68 | private function grow(): void{ 69 | $newState = ExtraVanillaBlocks::CAVE_VINES_BODY_WITH_BERRIES(); 70 | if($this->isVinesHead()){ 71 | $newState = ExtraVanillaBlocks::CAVE_VINES_HEAD_WITH_BERRIES(); 72 | } 73 | 74 | $ev = new BlockGrowEvent($this, $newState); 75 | $ev->call(); 76 | if(!$ev->isCancelled()){ 77 | $this->position->world->setBlock($this->position, $ev->getNewState()); 78 | } 79 | } 80 | 81 | private function isVinesHead(): bool{ 82 | $down = $this->getSide(Facing::DOWN); 83 | return !$down->isSameType($this) && !$down->isSameType(ExtraVanillaBlocks::CAVE_VINES_HEAD_WITH_BERRIES()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/block/CaveVinesBodyWithBerries.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::CAVE_VINES_BODY_WITH_BERRIES) 24 | ->writeInt(BlockStateNames::GROWING_PLANT_AGE, $block->getAge()); 25 | } 26 | 27 | public function getStateDeserialize() : ?Closure{ 28 | return fn(BlockStateReader $in) : CaveVinesBodyWithBerries => ExtraVanillaBlocks::CAVE_VINES_BODY_WITH_BERRIES() 29 | ->setAge($in->readBoundedInt(BlockStateNames::GROWING_PLANT_AGE, 0, $this->getMaxAge())); 30 | } 31 | 32 | public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ 33 | if($player !== null){ 34 | $world = $this->position->world; 35 | $world->dropItem($this->position->add(0.5, 0.5, 0.5), ExtraVanillaItems::GLOW_BERRIES()); 36 | $world->addSound($this->position, new CaveVinesPickBerriesSound()); 37 | $world->setBlock($this->position, ExtraVanillaBlocks::CAVE_VINES()); 38 | 39 | return true; 40 | } 41 | 42 | return false; 43 | } 44 | 45 | public function getLightLevel() : int{ return 14; } 46 | 47 | public function getDropsForCompatibleTool(Item $item) : array{ return [ExtraVanillaItems::GLOW_BERRIES()]; } 48 | 49 | public function getDrops(Item $item) : array{ return [$this->asItem()]; } 50 | } 51 | -------------------------------------------------------------------------------- /src/block/CaveVinesHeadWithBerries.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::CAVE_VINES_HEAD_WITH_BERRIES) 24 | ->writeInt(BlockStateNames::GROWING_PLANT_AGE, $block->getAge()); 25 | } 26 | 27 | public function getStateDeserialize() : ?Closure{ 28 | return fn(BlockStateReader $in) : CaveVinesHeadWithBerries => ExtraVanillaBlocks::CAVE_VINES_HEAD_WITH_BERRIES() 29 | ->setAge($in->readBoundedInt(BlockStateNames::GROWING_PLANT_AGE, 0, $this->getMaxAge())); 30 | } 31 | 32 | public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ 33 | if($player !== null){ 34 | $world = $this->position->world; 35 | $world->dropItem($this->position->add(0.5, 0.5, 0.5), ExtraVanillaItems::GLOW_BERRIES()); 36 | $world->addSound($this->position, new CaveVinesPickBerriesSound()); 37 | $world->setBlock($this->position, ExtraVanillaBlocks::CAVE_VINES()); 38 | 39 | return true; 40 | } 41 | 42 | return false; 43 | } 44 | 45 | public function getLightLevel() : int{ return 14; } 46 | 47 | public function getDropsForCompatibleTool(Item $item) : array{ return [ExtraVanillaItems::GLOW_BERRIES()]; } 48 | 49 | public function getDrops(Item $item) : array{ return [$this->asItem()]; } 50 | } -------------------------------------------------------------------------------- /src/block/Composter.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::COMPOSTER) 37 | ->writeInt(BlockStateNames::COMPOSTER_FILL_LEVEL, $block->fill_level); 38 | } 39 | 40 | public function getStateDeserialize() : ?Closure{ 41 | return static fn(BlockStateReader $in): Composter => ExtraVanillaBlocks::COMPOSTER() 42 | ->setFillLevel($in->readInt(BlockStateNames::COMPOSTER_FILL_LEVEL)); 43 | } 44 | 45 | public function getRequiredStateDataBits(): int{ return 4; } 46 | 47 | protected function describeState(RuntimeDataReader|RuntimeDataWriter $w) : void{ 48 | $w->int(4, $this->fill_level); 49 | } 50 | 51 | public function getFillLevel(): int{ return $this->fill_level; } 52 | 53 | public function setFillLevel(int $state): self{ 54 | $this->fill_level = $state; 55 | return $this; 56 | } 57 | 58 | public function getFuelTime() : int{ return 300; } 59 | 60 | public function isFullFilled(): bool{ return $this->fill_level >= self::FULL_LEVEL; } 61 | 62 | private function canPutRecycledFuel(): bool{ 63 | if($this->fill_level >= self::READY_LEVEL){ 64 | return false; 65 | } 66 | 67 | if(++$this->fill_level >= self::READY_LEVEL){ 68 | $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 20); 69 | }else{ 70 | $this->position->getWorld()->setBlock($this->position, $this); 71 | } 72 | 73 | $this->position->getWorld()->addSound($this->position, new ComposterFillSuccessSound()); 74 | return true; 75 | } 76 | 77 | public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ 78 | if($player === null){ 79 | return false; 80 | } 81 | 82 | if($this->isFullFilled()){ 83 | $this->fill_level = 0; 84 | $this->position->getWorld()->setBlock($this->position, $this); 85 | $this->position->getWorld()->addSound($this->position, new ComposterReadySound()); 86 | $this->position->getWorld()->dropItem($this->position->add(0.5, 1.1, 0.5), VanillaItems::BONE_MEAL()); 87 | }elseif($this->fill_level < self::READY_LEVEL && ($chance = BlockIngredientsHelper::composter($item)) > 0){ 88 | $item->pop(); 89 | $this->position->world->addParticle($this->position, new BoneMealParticle()); 90 | if($this->fill_level > 0 && random_int(0, 100) <= $chance){ 91 | $this->canPutRecycledFuel(); 92 | }elseif($this->fill_level === 0){ 93 | $this->canPutRecycledFuel(); 94 | } 95 | 96 | $this->position->getWorld()->addSound($this->position, new ComposterFillSound()); 97 | } 98 | 99 | return true; 100 | } 101 | 102 | public function onScheduledUpdate() : void{ 103 | if($this->fill_level !== self::READY_LEVEL){ 104 | return; 105 | } 106 | 107 | ++$this->fill_level; 108 | $this->position->getWorld()->setBlock($this->position, $this); 109 | $this->position->getWorld()->addSound($this->position, new ComposterReadySound()); 110 | } 111 | } -------------------------------------------------------------------------------- /src/block/Deny.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::END_PORTAL_FRAME) 27 | ->writeBool(BlockStateNames::END_PORTAL_EYE_BIT, $block->hasEye()) 28 | ->writeLegacyHorizontalFacing($block->getFacing()); 29 | } 30 | 31 | public function getStateDeserialize() : ?Closure{ 32 | return static fn(BlockStateReader $in): EndPortalFrame|Block => ExtraVanillaBlocks::END_PORTAL_FRAME() 33 | ->setEye($in->readBool(BlockStateNames::END_PORTAL_EYE_BIT)) 34 | ->setFacing($in->readLegacyHorizontalFacing()); 35 | } 36 | 37 | public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ 38 | if($item instanceof EnderEye && !$this->hasEye()){ 39 | $this->position->getWorld()->setBlock($this->position, $this->setEye(true)); 40 | $this->position->getWorld()->addSound($this->position, new EndPortalFrameFillSound()); 41 | 42 | $this->createEndPortal(); 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | private function createEndPortal(): void{ 50 | $center = $this->searchCenter([]); 51 | $world = $this->position->getWorld(); 52 | for($x = -2; $x <= 2; $x ++){ 53 | for($z = -2; $z <= 2; $z ++){ 54 | if(($x === -2 || $x === 2) && ($z === -2 || $z === 2)){ 55 | continue; 56 | } 57 | if($x === -2 || $x === 2 || $z === -2 || $z === 2){ 58 | if(!$this->isEndPortalFrameBlock($world->getBlock($center->add($x, 0, $z), true))){ 59 | return; 60 | } 61 | } 62 | } 63 | } 64 | 65 | for($x = -1; $x <= 1; $x ++){ 66 | for($z = -1; $z <= 1; $z ++){ 67 | $world->setBlock($center->add($x, 0, $z), ExtraVanillaBlocks::END_PORTAL()); 68 | } 69 | } 70 | 71 | $world->addSound($center, new EndPortalFrameSpawnSound()); 72 | } 73 | 74 | private function searchCenter(array $visited): Vector3{ 75 | static $blockHash = null; 76 | if($blockHash === null){ 77 | $blockHash = fn(Block $block): string => $block->getPosition()->getFloorX().':'.$block->getPosition()->getFloorZ(); 78 | } 79 | 80 | $world = $this->position->getWorld(); 81 | for($x = -2; $x <= 2; $x ++){ 82 | if($x === 0){ 83 | continue; 84 | } 85 | 86 | $block = $world->getBlock($this->position->add($x, 0, 0)); 87 | $iBlock = $world->getBlock($this->position->add($x * 2, 0, 0)); 88 | if($this->isEndPortalFrameBlock($block) && !isset($visited[$hash = $blockHash($block)])){ 89 | $visited[$hash] = true; 90 | if(($x === -1 || $x === 1) && $this->isEndPortalFrameBlock($iBlock)){ 91 | return $this->searchCenter($visited); 92 | } 93 | 94 | for($z = -4; $z <= 4; $z ++){ 95 | if($z === 0){ 96 | continue; 97 | } 98 | 99 | $block = $world->getBlock($this->position->add($x, 0, $z)); 100 | if($this->isEndPortalFrameBlock($block)){ 101 | return $this->position->add($x / 2, 0, $z / 2); 102 | } 103 | } 104 | } 105 | } 106 | 107 | for($z = -2; $z <= 2; $z ++){ 108 | if($z === 0){ 109 | continue; 110 | } 111 | 112 | $block = $world->getBlock($this->position->add(0, 0, $z)); 113 | $iBlock = $world->getBlock($this->position->add(0, 0, $z * 2)); 114 | if($this->isEndPortalFrameBlock($block) && !isset($visited[$hash = $blockHash($block)])){ 115 | $visited[$hash] = true; 116 | if(($z === -1 || $z === 1) && $this->isEndPortalFrameBlock($iBlock)){ 117 | return $this->searchCenter($visited); 118 | } 119 | 120 | for($x = -4; $x <= 4; $x ++){ 121 | if($x === 0){ 122 | continue; 123 | } 124 | 125 | $block = $world->getBlock($this->position->add($x, 0, $z)); 126 | if($this->isEndPortalFrameBlock($block)){ 127 | return $this->position->add($x / 2, 0, $z / 2); 128 | } 129 | } 130 | } 131 | } 132 | 133 | throw new AssumptionFailedError("No center point was found."); 134 | } 135 | 136 | private function isEndPortalFrameBlock(Block $block, bool $hasEye = false): bool{ 137 | $pos = $block->getPosition(); 138 | $block = $this->position->getWorld()->getBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()); 139 | if(!$block instanceof EndPortalFrame){ 140 | return false; 141 | } 142 | return !$hasEye || $block->hasEye(); 143 | } 144 | } -------------------------------------------------------------------------------- /src/block/ExtraVanillaBlocks.php: -------------------------------------------------------------------------------- 1 | 62 | */ 63 | public static function getAll(): array{ 64 | return self::_registryGetAll(); 65 | } 66 | 67 | protected static function setup() : void{ 68 | $instantBlockInfo = new Info(BlockBreakInfo::instant()); 69 | $indestructibleInfo = new Info(BlockBreakInfo::indestructible()); 70 | 71 | $leavesBreakInfo = new Info(new class(0.2, BlockToolType::HOE) extends BlockBreakInfo{ 72 | public function getBreakTime(Item $item) : float{ 73 | if($item->getBlockToolType() === BlockToolType::SHEARS){ 74 | return 0.0; 75 | } 76 | return parent::getBreakTime($item); 77 | } 78 | }); 79 | self::register('azalea', new Azalea(new BID(Azalea::getFixedTypeId()), 'Azalea', $instantBlockInfo)); 80 | self::register('azalea_leaves', new AzaleaLeaves(new BID(AzaleaLeaves::getFixedTypeId()), 'Azalea Leaves', $leavesBreakInfo)); 81 | self::register('azalea_leaves_flowered', new AzaleaLeavesFlowered(new BID(AzaleaLeavesFlowered::getFixedTypeId()), 'Azalea Leaves Flowered', $leavesBreakInfo)); 82 | self::register('flowering_azalea', new FloweringAzalea(new BID(FloweringAzalea::getFixedTypeId()), 'Flowering Azalea', $instantBlockInfo)); 83 | 84 | $sculkInfo = new Info(BlockBreakInfo::tier(3.0, BlockToolType::HOE, ToolTier::WOOD())); 85 | self::register('sculk', new Sculk(new BID(Sculk::getFixedTypeId()), 'Sculk', $sculkInfo)); 86 | self::register('sculk_catalyst', new SculkCatalyst(new BID(SculkCatalyst::getFixedTypeId()), 'Sculk Catalyst', $sculkInfo)); 87 | self::register('sculk_sensor', new SculkSensor(new BID(SculkSensor::getFixedTypeId()), 'Sculk Sensor', $sculkInfo)); 88 | self::register('sculk_shrieker', new SculkShrieker(new BID(SculkShrieker::getFixedTypeId()), 'Sculk Shrieker', $sculkInfo)); 89 | 90 | self::register('end_portal_frame', new EndPortalFrame(new BID(BlockTypeIds::END_PORTAL_FRAME), "End Portal Frame", $indestructibleInfo)); 91 | self::register('end_portal', new EndPortal(new BID(EndPortal::getFixedTypeId()), 'End Portal', $indestructibleInfo)); 92 | self::register('end_gateway', new EndGateway(new BID(EndGateway::getFixedTypeId()), 'End Gateway', $indestructibleInfo)); 93 | 94 | self::register('moss', new Moss(new BID(Moss::getFixedTypeId()), 'Moss Block', new Info(BlockBreakInfo::instant(), [BlockTypeTags::DIRT]))); 95 | self::register('moss_carpet', new MossCarpet(new BID(MossCarpet::getFixedTypeId()), 'Moss Carpet', $instantBlockInfo)); 96 | 97 | self::register('border', new Border(new BID(Border::getFixedTypeId()), 'Border Block', $indestructibleInfo)); 98 | self::register('allow', new Allow(new BID(Allow::getFixedTypeId()), 'Allow', $indestructibleInfo)); 99 | self::register('deny', new Deny(new BID(Deny::getFixedTypeId()), 'Deny', $indestructibleInfo)); 100 | 101 | self::register('cave_vines', new CaveVines(new BID(CaveVines::getFixedTypeId()), 'Cave Vines', $instantBlockInfo)); 102 | self::register('cave_vines_body_with_berries', new CaveVinesBodyWithBerries(new BID(CaveVinesBodyWithBerries::getFixedTypeId()), 'Cave Vines Body With Berries', $instantBlockInfo)); 103 | self::register('cave_vines_head_with_berries', new CaveVinesHeadWithBerries(new BID(CaveVinesHeadWithBerries::getFixedTypeId()), 'Cave Vines Head With Berries', $instantBlockInfo)); 104 | 105 | self::register('target', new Target(new BID(Target::getFixedTypeId()), 'Target', $instantBlockInfo)); 106 | self::register('kelp', new Kelp(new BID(Kelp::getFixedTypeId()), 'Kelp', $instantBlockInfo)); 107 | self::register('camera', new Camera(new BID(Camera::getFixedTypeId()), 'Camera', $instantBlockInfo)); 108 | 109 | self::register('mangrove_propagule', new MangrovePropagule(new BID(MangrovePropagule::getFixedTypeId()), 'Mangrove Propagule', $instantBlockInfo)); 110 | self::register('mangrove_leaves', new MangroveLeaves(new BID(MangroveLeaves::getFixedTypeId()), 'Mangrove Leaves', $leavesBreakInfo)); 111 | 112 | self::register('honey_block', new Honey(new BID(Honey::getFixedTypeId()), "Honey Block", $instantBlockInfo)); 113 | 114 | $campfireBreakInfo = new BlockTypeInfo(new BlockBreakInfo(2, BlockToolType::AXE)); 115 | self::register('campfire', new Campfire(new BID(BlockTypeIds::newId(), RegularCampfireTile::class), "Campfire", $campfireBreakInfo)); 116 | self::register('soul_campfire', new SoulCampfire(new BID(BlockTypeIds::newId(), SoulCampfireTile::class), "Soul Campfire", $campfireBreakInfo)); 117 | 118 | self::register('composter', new Composter(new BID(Composter::getFixedTypeId()), 'Composter', new BlockTypeInfo(new BlockBreakInfo(0.6, BlockToolType::AXE)))); 119 | } 120 | } -------------------------------------------------------------------------------- /src/block/FloweringAzalea.php: -------------------------------------------------------------------------------- 1 | fallDistance *= 0.2; 29 | return null; 30 | } 31 | } -------------------------------------------------------------------------------- /src/block/Kelp.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::KELP) 31 | ->writeInt(BlockStateNames::KELP_AGE, $block->getAge()); 32 | } 33 | 34 | public function getStateDeserialize() : ?Closure{ 35 | return fn(BlockStateReader $in) : Kelp => ExtraVanillaBlocks::KELP() 36 | ->setAge($in->readBoundedInt(BlockStateNames::KELP_AGE, 0, $this->getMaxAge())); 37 | } 38 | 39 | public function getRequiredStateDataBits() : int{ return 5; } 40 | 41 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 42 | $w->boundedInt(5, 0, $this->getMaxAge(), $this->age); 43 | } 44 | 45 | protected function recalculateCollisionBoxes() : array{ return []; } 46 | 47 | public function getDrops(Item $item) : array{ 48 | return [ExtraVanillaItems::KELP()]; 49 | } 50 | 51 | public function onNearbyBlockChange() : void{ 52 | $world = $this->position->getWorld(); 53 | if(!$this->canBeSupportedBy($this->getSide(Facing::DOWN))){ 54 | $world->useBreakOn($this->position); 55 | } 56 | } 57 | 58 | private function canBeSupportedBy(Block $block) : bool{ 59 | return $block->getSupportType(Facing::UP)->hasCenterSupport(); 60 | } 61 | } -------------------------------------------------------------------------------- /src/block/MangroveLeaves.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::MANGROVE_LEAVES) 29 | ->writeBool(BlockStateNames::PERSISTENT_BIT, $block->isPersistentBit()) 30 | ->writeBool(BlockStateNames::UPDATE_BIT, $block->isUpdateBit()); 31 | } 32 | 33 | public function getStateDeserialize() : ?Closure{ 34 | return static fn(BlockStateReader $in) : MangroveLeaves => ExtraVanillaBlocks::MANGROVE_LEAVES() 35 | ->setPersistentBit($in->readBool(BlockStateNames::PERSISTENT_BIT)) 36 | ->setUpdateBit($in->readBool(BlockStateNames::UPDATE_BIT)); 37 | } 38 | 39 | public function getRequiredStateDataBits() : int{ return 2; } 40 | 41 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 42 | $w->bool($this->persistentBit); 43 | $w->bool($this->updateBit); 44 | } 45 | 46 | public function isPersistentBit(): bool{ return $this->persistentBit; } 47 | 48 | public function setPersistentBit(bool $state): self{ 49 | $this->persistentBit = $state; 50 | return $this; 51 | } 52 | 53 | public function isUpdateBit(): bool{ return $this->updateBit; } 54 | 55 | public function setUpdateBit(bool $state): self{ 56 | $this->updateBit = $state; 57 | return $this; 58 | } 59 | 60 | public function getDropsForCompatibleTool(Item $item) : array{ 61 | if(($item->getBlockToolType() & BlockToolType::SHEARS) !== 0){ 62 | return parent::getDropsForCompatibleTool($item); 63 | } 64 | 65 | $drops = []; 66 | if(random_int(1, 30) === 1){ 67 | $drops[] = VanillaItems::STICK(); 68 | } 69 | 70 | return $drops; 71 | } 72 | } -------------------------------------------------------------------------------- /src/block/MangrovePropagule.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::MANGROVE_PROPAGULE) 33 | ->writeBool(BlockStateNames::HANGING, $block->isHanging()) 34 | ->writeInt(BlockStateNames::PROPAGULE_STAGE, $block->getPropaguleStage()); 35 | } 36 | 37 | public function getStateDeserialize() : ?Closure{ 38 | return static fn(BlockStateReader $in): MangrovePropagule|Block => ExtraVanillaBlocks::MANGROVE_PROPAGULE() 39 | ->setHanging($in->readBool(BlockStateNames::HANGING)) 40 | ->setPropaguleStage($in->readInt(BlockStateNames::PROPAGULE_STAGE)); 41 | } 42 | 43 | public function getRequiredStateDataBits() : int{ return 3; } 44 | 45 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 46 | $w->bool($this->hanging); 47 | $w->boundedInt(2, 0, 4, $this->propagule_stage); 48 | } 49 | 50 | public function isHanging(): bool{ return $this->hanging; } 51 | 52 | public function setHanging(bool $state): self{ 53 | $this->hanging = $state; 54 | return $this; 55 | } 56 | 57 | public function getPropaguleStage(): int{ return $this->propagule_stage; } 58 | 59 | public function setPropaguleStage(int $stage): self{ 60 | if($stage < 0 || $stage > 4){ 61 | throw new \InvalidArgumentException("PropaguleStage must be in range 0 ... 4"); 62 | } 63 | $this->propagule_stage = $stage; 64 | return $this; 65 | } 66 | 67 | public function onNearbyBlockChange() : void{ 68 | if(!$this->hanging){ 69 | $down = $this->getSide(Facing::DOWN); 70 | if($down->isSameType(VanillaBlocks::AIR())){ 71 | $this->position->getWorld()->useBreakOn($this->position); 72 | } 73 | }else{ 74 | $up = $this->getSide(Facing::UP); 75 | if(!$up->isSolid()){ 76 | $this->position->getWorld()->useBreakOn($this->position); 77 | } 78 | } 79 | } 80 | 81 | public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ 82 | if(Facing::opposite($face) > 1){ 83 | return false; 84 | } 85 | 86 | $this->hanging = Facing::opposite($face) === 1; 87 | 88 | return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); 89 | } 90 | } -------------------------------------------------------------------------------- /src/block/Moss.php: -------------------------------------------------------------------------------- 1 | generateMossBlock(); 32 | $this->populatePlantsRegion(); 33 | $this->position->world->addParticle($this->position, new BoneMealParticle()); 34 | $item->pop(); 35 | 36 | return true; 37 | } 38 | 39 | return false; 40 | } 41 | 42 | private function canGenerateMoss(Block $block): bool{ 43 | static $blocks = [ 44 | BlockTypeIds::GRASS => true, 45 | BlockTypeIds::DIRT => true, 46 | BlockTypeIds::STONE => true, 47 | BlockTypeIds::MYCELIUM => true, 48 | BlockTypeIds::DEEPSLATE => true, 49 | BlockTypeIds::TUFF => true 50 | ]; 51 | return isset($blocks[$block->getTypeId()]); 52 | } 53 | 54 | private function canGrowPlant(Position $pos): bool{ 55 | return $this->canGenerateMoss($this->position->getWorld()->getBlock($pos->down())); 56 | } 57 | 58 | private function canBePopulated(Position $pos): bool{ 59 | return ($down = $this->position->getWorld()->getBlock($pos->down()))->isSolid() 60 | && $down->getTypeId() !== MossCarpet::getFixedTypeId() 61 | && $this->position->getWorld()->getBlock($pos)->getTypeId() === BlockTypeIds::AIR; 62 | } 63 | 64 | private function canBePopulatedToBlockAir(Position $pos): bool{ 65 | return $this->canBePopulated($pos) && $this->position->getWorld()->getBlock($pos->up())->getTypeId() === BlockTypeIds::AIR; 66 | } 67 | 68 | private function generateMossBlock(): void{ 69 | $random = new Random(); 70 | $center = $this->position; 71 | $world = $center->world; 72 | for($x = $center->getFloorX() - 3; $x <= $center->getFloorX() + 3; $x ++){ 73 | for($z = $center->getFloorZ() - 3; $z <= $center->getFloorZ() + 3; $z ++){ 74 | for($y = $center->getFloorY() + 5; $y >= $center->getFloorY() - 5; $y --){ 75 | $block = $world->getBlockAt($x, $y, $z); 76 | if($this->canGenerateMoss($block) && ($random->nextFloat() < 0.6 || abs($x - $center->getFloorX()) < 3 && abs($z - $center->getFloorZ()) < 3)){ 77 | $world->setBlock($block->getPosition(), ExtraVanillaBlocks::MOSS()); 78 | break; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | private function populatePlantsRegion(): void{ 86 | $random = new Random(); 87 | $center = $this->position; 88 | $world = $center->world; 89 | for($x = $center->getFloorX() - 3; $x <= $center->getFloorX() + 3; $x ++){ 90 | for($z = $center->getFloorZ() - 3; $z <= $center->getFloorZ() + 3; $z ++){ 91 | for($y = $center->getFloorY() + 5; $y >= $center->getFloorY() - 5; $y--){ 92 | $block = $world->getBlockAt($x, $y, $z); 93 | $position = $block->getPosition(); 94 | if($this->canBePopulated($position)){ 95 | if(!$this->canGrowPlant($position)){ 96 | break; 97 | } 98 | $randomDouble = $random->nextFloat(); 99 | if($randomDouble >= 0 && $randomDouble < 0.3125){ 100 | $world->setBlock($position, VanillaBlocks::TALL_GRASS()); 101 | } 102 | if($randomDouble >= 0.3125 && $randomDouble < 0.46875){ 103 | $world->setBlock($position, ExtraVanillaBlocks::MOSS_CARPET()); 104 | } 105 | if($randomDouble >= 0.46875 && $randomDouble < 0.53125){ 106 | if($this->canBePopulatedToBlockAir($position)){ 107 | $world->setBlock($position, VanillaBlocks::DOUBLE_TALLGRASS()->setTop(false)); 108 | $world->setBlock($position->up(), VanillaBlocks::DOUBLE_TALLGRASS()->setTop(true)); 109 | }else{ 110 | $world->setBlock($position, VanillaBlocks::TALL_GRASS()); 111 | } 112 | } 113 | if($randomDouble >= 0.53125 && $randomDouble < 0.575){ 114 | $world->setBlock($position, ExtraVanillaBlocks::AZALEA()); 115 | } 116 | if($randomDouble >= 0.575 && $randomDouble < 0.6){ 117 | $world->setBlock($position, ExtraVanillaBlocks::FLOWERING_AZALEA()); 118 | } 119 | if($randomDouble >= 0.6 && $randomDouble < 1){ 120 | $world->setBlock($position, VanillaBlocks::AIR()); 121 | } 122 | break; 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /src/block/MossCarpet.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::SCULK_CATALYST) 28 | ->writeBool(BlockStateNames::BLOOM, $block->isBloom()); 29 | } 30 | 31 | public function getStateDeserialize() : ?Closure{ 32 | return static fn(BlockStateReader $in): SculkCatalyst => ExtraVanillaBlocks::SCULK_CATALYST() 33 | ->setBloom($in->readBool(BlockStateNames::BLOOM)); 34 | } 35 | 36 | public function getRequiredStateDataBits() : int{ return 1; } 37 | 38 | protected function describeState(RuntimeDataReader|RuntimeDataWriter $w) : void{ 39 | $w->bool($this->bloom); 40 | } 41 | 42 | public function isBloom(): bool{ return $this->bloom; } 43 | 44 | public function setBloom(bool $state): self{ 45 | $this->bloom = $state; 46 | return $this; 47 | } 48 | 49 | public function getXpDropForTool(Item $item) : int{ 50 | if($item instanceof Hoe){ 51 | return 5; 52 | } 53 | return parent::getXpDropForTool($item); 54 | } 55 | 56 | public function getLightLevel() : int{ return 6; } 57 | } -------------------------------------------------------------------------------- /src/block/SculkSensor.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::SCULK_SENSOR) 27 | ->writeBool(BlockStateNames::POWERED_BIT, $block->isPoweredBit()); 28 | } 29 | 30 | public function getStateDeserialize() : ?Closure{ 31 | return static fn(BlockStateReader $in): SculkSensor => ExtraVanillaBlocks::SCULK_SENSOR() 32 | ->setPoweredBit($in->readBool(BlockStateNames::POWERED_BIT)); 33 | } 34 | 35 | public function getRequiredStateDataBits() : int{ return 1; } 36 | 37 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 38 | $w->bool($this->poweredBit); 39 | } 40 | 41 | public function isPoweredBit(): bool{ return $this->poweredBit; } 42 | 43 | public function setPoweredBit(bool $state): self{ 44 | $this->poweredBit = $state; 45 | return $this; 46 | } 47 | 48 | public function getXpDropForTool(Item $item) : int{ 49 | if($item instanceof Hoe){ 50 | return 5; 51 | } 52 | return parent::getXpDropForTool($item); 53 | } 54 | } -------------------------------------------------------------------------------- /src/block/SculkShrieker.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::SCULK_SHRIEKER) 28 | ->writeBool(BlockStateNames::ACTIVE, $block->isActive()) 29 | ->writeBool(BlockStateNames::CAN_SUMMON, $block->canSummon()); 30 | } 31 | 32 | public function getStateDeserialize() : ?Closure{ 33 | return static fn(BlockStateReader $in): SculkShrieker => ExtraVanillaBlocks::SCULK_SHRIEKER() 34 | ->setActive($in->readBool(BlockStateNames::ACTIVE)) 35 | ->setSummon($in->readBool(BlockStateNames::CAN_SUMMON)); 36 | } 37 | 38 | public function getRequiredStateDataBits() : int{ return 2; } 39 | 40 | protected function describeState(RuntimeDataWriter|RuntimeDataReader $w) : void{ 41 | $w->bool($this->active); 42 | $w->bool($this->canSummon); 43 | } 44 | 45 | public function isActive(): bool{ return $this->active; } 46 | 47 | public function setActive(bool $state): self{ 48 | $this->active = $state; 49 | return $this; 50 | } 51 | 52 | public function canSummon(): bool{ 53 | return $this->canSummon; 54 | } 55 | 56 | public function setSummon(bool $state): self{ 57 | $this->canSummon = $state; 58 | return $this; 59 | } 60 | 61 | public function getXpDropForTool(Item $item) : int{ 62 | if($item instanceof Hoe){ 63 | return 5; 64 | } 65 | return parent::getXpDropForTool($item); 66 | } 67 | } -------------------------------------------------------------------------------- /src/block/SoulCampfire.php: -------------------------------------------------------------------------------- 1 | BlockStateWriter::create(BlockTypeNames::SOUL_CAMPFIRE) 18 | ->writeLegacyHorizontalFacing($block->getFacing()) 19 | ->writeBool(BlockStateNames::EXTINGUISHED, $block->isExtinguished()); 20 | } 21 | 22 | public function getStateDeserialize() : ?Closure{ 23 | return fn(BlockStateReader $in): SoulCampfire => ExtraVanillaBlocks::SOUL_CAMPFIRE() 24 | ->setFacing($in->readLegacyHorizontalFacing()) 25 | ->setExtinguish($in->readBool(BlockStateNames::EXTINGUISHED)); 26 | } 27 | 28 | public function asItem() : Item{ 29 | return ExtraVanillaItems::SOUL_CAMPFIRE(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/block/Target.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private array $recipe = []; 27 | 28 | private const TAG_ITEM_TIME = "ItemTimes"; 29 | 30 | /** 31 | * @var Item[] 32 | * @phpstan-var array 33 | */ 34 | private array $items = []; 35 | 36 | /** 37 | * @var int[] 38 | * @phpstan-var array 39 | */ 40 | private array $itemTime = []; 41 | 42 | public function __construct(World $world, Vector3 $pos){ 43 | parent::__construct($world, $pos); 44 | 45 | $this->recipe[ItemTypeIds::RAW_BEEF] = VanillaItems::STEAK(); 46 | $this->recipe[ItemTypeIds::RAW_CHICKEN] = VanillaItems::COOKED_CHICKEN(); 47 | $this->recipe[ItemTypeIds::RAW_PORKCHOP] = VanillaItems::COOKED_PORKCHOP(); 48 | $this->recipe[ItemTypeIds::RAW_MUTTON] = VanillaItems::RAW_MUTTON(); 49 | $this->recipe[ItemTypeIds::RAW_FISH] = VanillaItems::COOKED_FISH(); 50 | $this->recipe[ItemTypeIds::RAW_SALMON] = VanillaItems::COOKED_SALMON(); 51 | $this->recipe[ItemTypeIds::POTATO] = VanillaItems::BAKED_POTATO(); 52 | $this->recipe[Kelp::getFixedTypeId()] = VanillaItems::DRIED_KELP(); 53 | } 54 | 55 | public function close(): void{ 56 | foreach($this->items as $item){ 57 | $this->position->getWorld()->dropItem($this->position->add(0, 1, 0), $item); 58 | } 59 | $this->items = []; 60 | parent::close(); 61 | } 62 | 63 | public function setItem(Item $item, ?int $slot = null): void{ 64 | if($slot === null){ 65 | $slot = count($this->items) + 1; 66 | } 67 | if($slot === null || $slot > 4 || $slot < 1){ 68 | throw new InvalidArgumentException("Out of range campfire slot, received $slot expected 1, 2, 3 or 4"); 69 | } 70 | if($item->isNull()){ 71 | if(isset($this->items[$slot])){ 72 | unset($this->items[$slot]); 73 | } 74 | }else{ 75 | $this->items[$slot] = $item; 76 | } 77 | } 78 | 79 | public function addItem(Item $item): bool{ 80 | if(!$this->canAddItem($item)){ 81 | return false; 82 | } 83 | 84 | $this->setItem((clone $item)->setCount(1)); 85 | 86 | return true; 87 | } 88 | 89 | public function canCook(Item $item): bool{ 90 | return isset($this->recipe[$item->getTypeId()]); 91 | } 92 | 93 | public function getCookResultItem(Item $item): Item{ 94 | return $this->recipe[$item->getTypeId()]; 95 | } 96 | 97 | public function canAddItem(Item $item): bool{ 98 | if(count($this->items) >= 4){ 99 | return false; 100 | } 101 | 102 | return $this->canCook($item); 103 | } 104 | 105 | public function setSlotTime(int $slot, int $time): void{ 106 | $this->itemTime[$slot] = $time; 107 | } 108 | 109 | public function increaseSlotTime(int $slot): void{ 110 | $this->setSlotTime($slot, $this->getItemTime($slot) + 1); 111 | } 112 | 113 | public function getItemTime(int $slot): int{ 114 | return $this->itemTime[$slot] ?? 0; 115 | } 116 | 117 | /** 118 | * @param bool $includeEmpty 119 | * @return Item[] 120 | */ 121 | public function getContents(bool $includeEmpty = false): array{ 122 | return $this->items; 123 | } 124 | 125 | public function readSaveData(CompoundTag $nbt): void{ 126 | $this->loadItems($nbt); 127 | 128 | if(($tag = $nbt->getTag(self::TAG_ITEM_TIME)) !== null){ 129 | /** @var IntTag $time */ 130 | foreach($tag->getValue() as $slot => $time){ 131 | $this->itemTime[$slot + 1] = $time->getValue(); 132 | } 133 | } 134 | } 135 | 136 | protected function writeSaveData(CompoundTag $nbt): void{ 137 | $this->saveItems($nbt); 138 | 139 | $times = []; 140 | foreach($this->itemTime as $time){ 141 | $times[] = new IntTag($time); 142 | } 143 | $nbt->setTag(self::TAG_ITEM_TIME, new ListTag($times)); 144 | } 145 | 146 | protected function addAdditionalSpawnData(CompoundTag $nbt): void{ 147 | foreach($this->items as $slot => $item){ 148 | $nbt->setTag("Item" . $slot, $item->nbtSerialize()); 149 | $nbt->setInt("ItemTime" . $slot, $this->getItemTime($slot)); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/block/tile/ContainerTrait.php: -------------------------------------------------------------------------------- 1 | getTag($this->getContainerSaveName())) !== null){ 18 | $inventoryTag = $tag->getValue(); 19 | 20 | /** @var CompoundTag $itemNBT */ 21 | foreach($inventoryTag as $itemNBT){ 22 | $this->setItem(Item::nbtDeserialize($itemNBT), $itemNBT->getByte("Slot")); 23 | } 24 | } 25 | } 26 | 27 | protected function saveItems(CompoundTag $tag) : void{ 28 | $items = []; 29 | foreach($this->getContents() as $slot => $item){ 30 | $items[] = $item->nbtSerialize($slot); 31 | } 32 | $tag->setTag($this->getContainerSaveName(), new ListTag($items, NBT::TAG_Compound)); 33 | } 34 | 35 | abstract public function setItem(Item $item, int $slot = 0) : void; 36 | 37 | /** 38 | * @param bool $includeEmpty 39 | * 40 | * @return Item[] 41 | */ 42 | abstract public function getContents(bool $includeEmpty = false) : array; 43 | } -------------------------------------------------------------------------------- /src/block/tile/RegularCampfireTile.php: -------------------------------------------------------------------------------- 1 | age; } 13 | 14 | public function setAge(int $age): self{ 15 | if($age < 0 || $age > $this->getMaxAge()){ 16 | throw new \InvalidArgumentException("Age must be in range 0 ... " . $this->getMaxAge()); 17 | } 18 | $this->age = $age; 19 | return $this; 20 | } 21 | } -------------------------------------------------------------------------------- /src/block/utils/BlockIngredientsHelper.php: -------------------------------------------------------------------------------- 1 | namespace => chance 18 | */ 19 | private static array $composterIngredients; 20 | 21 | public static function composter(Item $item): int{ 22 | if(empty(self::$composterIngredients)){ 23 | self::$composterIngredients = [ 24 | //30% 25 | ItemTypeNames::BEETROOT_SEEDS => 30, 26 | ItemTypeNames::DRIED_KELP => 30, 27 | ItemTypeNames::GLOW_BERRIES => 30, 28 | // BlockTypeNames::TALLGRASS => 30, //small grass 29 | BlockTypeNames::GRASS => 30, //Only Bedrock Edition 30 | BlockTypeNames::HANGING_ROOTS => 30, 31 | BlockTypeNames::MANGROVE_ROOTS => 30, 32 | ItemTypeNames::KELP => 30, 33 | BlockTypeNames::LEAVES => 30, 34 | ItemTypeNames::MELON_SEEDS => 30, 35 | ItemTypeNames::PUMPKIN_SEEDS => 30, 36 | BlockTypeNames::SAPLING => 30, 37 | BlockTypeNames::SEAGRASS => 30, 38 | BlockTypeNames::SMALL_DRIPLEAF_BLOCK => 30, 39 | ItemTypeNames::SWEET_BERRIES => 30, 40 | ItemTypeNames::WHEAT_SEEDS => 30, 41 | 42 | //50% 43 | BlockTypeNames::CACTUS => 50, 44 | BlockTypeNames::DRIED_KELP_BLOCK => 50, 45 | BlockTypeNames::AZALEA_LEAVES_FLOWERED => 50, 46 | BlockTypeNames::GLOW_LICHEN => 50, 47 | ItemTypeNames::MELON_SLICE => 50, 48 | BlockTypeNames::NETHER_SPROUTS => 50, 49 | ItemTypeNames::SUGAR_CANE => 50, 50 | // BlockTypeNames::DOUBLE_PLANT => 50, //tall grass 51 | BlockTypeNames::TWISTING_VINES => 50, 52 | BlockTypeNames::VINE => 50, 53 | BlockTypeNames::WEEPING_VINES => 50, 54 | 55 | //65% 56 | ItemTypeNames::APPLE => 65, 57 | BlockTypeNames::AZALEA => 65, 58 | ItemTypeNames::BEETROOT => 65, 59 | BlockTypeNames::BIG_DRIPLEAF => 65, 60 | ItemTypeNames::CARROT => 65, 61 | ItemTypeNames::COCOA_BEANS => 65, 62 | // TODO.. ferns 63 | // TODO.. flowers 64 | // TODO.. fungus 65 | BlockTypeNames::WATERLILY => 65, 66 | BlockTypeNames::MELON_BLOCK => 65, 67 | BlockTypeNames::MOSS_BLOCK => 65, 68 | BlockTypeNames::BROWN_MUSHROOM => 65, 69 | BlockTypeNames::RED_MUSHROOM => 65, 70 | // TODO.. mushroom steam 71 | ItemTypeNames::NETHER_WART => 65, 72 | ItemTypeNames::POTATO => 65, 73 | BlockTypeNames::PUMPKIN => 65, 74 | BlockTypeNames::CARVED_PUMPKIN => 65, 75 | BlockTypeNames::CRIMSON_ROOTS => 65, 76 | BlockTypeNames::WARPED_ROOTS => 65, 77 | BlockTypeNames::SEA_PICKLE => 65, 78 | BlockTypeNames::SHROOMLIGHT => 65, 79 | BlockTypeNames::SPORE_BLOSSOM => 65, 80 | ItemTypeNames::WHEAT => 65, 81 | 82 | //85% 83 | ItemTypeNames::BAKED_POTATO => 85, 84 | ItemTypeNames::BREAD => 85, 85 | ItemTypeNames::COOKIE => 85, 86 | BlockTypeNames::FLOWERING_AZALEA => 85, 87 | BlockTypeNames::HAY_BLOCK => 85, 88 | // TODO.. mushroom blocks 89 | BlockTypeNames::NETHER_WART_BLOCK => 85, 90 | BlockTypeNames::WARPED_WART_BLOCK => 85, 91 | 92 | //100% 93 | BlockTypeNames::CAKE => 100, 94 | ItemTypeNames::PUMPKIN_PIE => 100 95 | ]; 96 | } 97 | 98 | return self::$composterIngredients[GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName()] ?? -1; 99 | } 100 | } -------------------------------------------------------------------------------- /src/block/utils/BlockTypeIdTrait.php: -------------------------------------------------------------------------------- 1 | closed){ 19 | return false; 20 | } 21 | 22 | $hasUpdate = parent::entityBaseTick($tickDiff); 23 | if($this->isUnderwater()){ 24 | $this->onNearbyWaterFreeze(); 25 | $this->flagForDespawn(); 26 | $hasUpdate = true; 27 | } 28 | return $hasUpdate; 29 | } 30 | 31 | private function onNearbyWaterFreeze() : void{ 32 | $center = $this->getPosition(); 33 | 34 | for($x = -1; $x <= 1; ++$x){ 35 | for($z = -1; $z <= 1; ++$z){ 36 | $blockPos = $center->add($x, 0, $z); 37 | $block = $this->getWorld()->getBlock($blockPos); 38 | if($block instanceof Water || $block instanceof WaterLily){ 39 | $this->getWorld()->setBlock($blockPos, VanillaBlocks::ICE()); 40 | } 41 | } 42 | } 43 | $center->getWorld()->addSound($center, new BlockBreakSound(VanillaBlocks::ICE())); 44 | } 45 | } -------------------------------------------------------------------------------- /src/expansion/BlockExpansion.php: -------------------------------------------------------------------------------- 1 | register(RegularCampfireTile::class, ["Campfire", "minecraft:campfire"]); 32 | $tileFactory->register(SoulCampfireTile::class, ["SoulCampfire", "minecraft:soul_campfire"]); 33 | } 34 | 35 | private static function register( 36 | Block|IBlockState $block, 37 | ?Closure $serialize = null, 38 | ?Closure $deserialize = null 39 | ): void{ 40 | $namespace = self::reprocess($block->getName()); 41 | 42 | $serialize ??= $block->getStateSerialize() ?? static fn(): BlockStateWriter => BlockStateWriter::create($namespace); 43 | $deserialize ??= $block->getStateDeserialize() ?? static fn(): Block => clone $block; 44 | 45 | Closure::bind( 46 | closure: fn(BlockStateToObjectDeserializer $deserializer) => $deserializer->deserializeFuncs[$namespace] = $deserialize, 47 | newThis: null, 48 | newScope: BlockStateToObjectDeserializer::class 49 | )(GlobalBlockStateHandlers::getDeserializer()); 50 | 51 | Closure::bind( 52 | closure: fn(BlockObjectToStateSerializer $serializer) => $serializer->serializers[$block->getTypeId()][get_class($block)] = $serialize, 53 | newThis: null, 54 | newScope: BlockObjectToStateSerializer::class 55 | )(GlobalBlockStateHandlers::getSerializer()); 56 | 57 | StringToItemParser::getInstance()->override($namespace, fn() => clone $block->asItem()); 58 | BlockFactory::getInstance()->register($block, true); 59 | } 60 | 61 | private static function reprocess(string $name): string{ 62 | return "minecraft:" . strtolower(str_replace(' ', '_', $name)); 63 | } 64 | } -------------------------------------------------------------------------------- /src/expansion/EntityExpansion.php: -------------------------------------------------------------------------------- 1 | register($entityClass, $closure, $savedEntityNames); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/expansion/IExpansion.php: -------------------------------------------------------------------------------- 1 | getName()); 36 | 37 | $runtimeId ??= self::getRuntimeIdByName($namespace); 38 | $serialize ??= static fn() => new SavedItemData($namespace); 39 | $deserialize ??= static fn(SavedItemData $_) => clone $item; 40 | 41 | Closure::bind( 42 | closure: fn(ItemSerializer $serializer) => $serializer->itemSerializers[$item->getTypeId()][get_class($item)] = $serialize, 43 | newThis: null, 44 | newScope: ItemSerializer::class 45 | )(GlobalItemDataHandlers::getSerializer()); 46 | 47 | Closure::bind( 48 | closure: fn(ItemDeserializer $deserializer) => $deserializer->deserializers[$namespace] = $deserialize, 49 | newThis: null, 50 | newScope: ItemDeserializer::class 51 | )(GlobalItemDataHandlers::getDeserializer()); 52 | 53 | Closure::bind( 54 | closure: function(ItemTypeDictionary $dictionary) use ($item, $runtimeId, $namespace): void{ 55 | $dictionary->stringToIntMap[$namespace] = $runtimeId; 56 | $dictionary->intToStringIdMap[$runtimeId] = $namespace; 57 | $dictionary->itemTypes[] = new ItemTypeEntry($namespace, $runtimeId, true); 58 | }, 59 | newThis: null, 60 | newScope: ItemTypeDictionary::class 61 | )(GlobalItemTypeDictionary::getInstance()->getDictionary()); 62 | 63 | StringToItemParser::getInstance()->override($namespace, fn() => clone $item); 64 | } 65 | 66 | private static function reprocess(string $name): string{ 67 | return "minecraft:" . strtolower(str_replace(' ', '_', $name)); 68 | } 69 | 70 | private static function getRuntimeIdByName(string $name): int{ 71 | static $mappedJson = []; 72 | if($mappedJson === []){ 73 | $mappedJson = self::reprocessKeys(json_decode(file_get_contents(Path::join(BEDROCK_DATA_PATH, "required_item_list.json")), true)); 74 | } 75 | return $mappedJson[$name]["runtime_id"] ?? throw new InvalidArgumentException("Not Found $name Runtime Id"); 76 | } 77 | 78 | private static function reprocessKeys(array $data) : array{ 79 | $new = []; 80 | foreach($data as $key => $value){ 81 | $new[$key] = $value; 82 | } 83 | return $new; 84 | } 85 | } -------------------------------------------------------------------------------- /src/item/Camera.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | public static function getAll(): array{ 33 | return self::_registryGetAll(); 34 | } 35 | 36 | protected static function setup() : void{ 37 | self::register('ice_bomb', new IceBomb(new IID(IceBomb::getFixedTypeId()), "Ice Bomb")); 38 | self::register('ender_eye', new EnderEye(new IID(EnderEye::getFixedTypeId()), "Ender Eye")); 39 | self::register('glow_berries', new GlowBerries(new IID(GlowBerries::getFixedTypeId()), "Glow Berries")); 40 | self::register('kelp', new Kelp(new IID(Kelp::getFixedTypeId()), "Kelp")); 41 | self::register('camera', new Camera(new IID(Camera::getFixedTypeId()), 'Camera')); 42 | self::register('campfire', new Campfire(new IID(Campfire::getFixedTypeId()), 'Campfire')); 43 | self::register('soul_campfire', new SoulCampfire(new IID(SoulCampfire::getFixedTypeId()), 'Soul Campfire')); 44 | } 45 | } -------------------------------------------------------------------------------- /src/item/GlowBerries.php: -------------------------------------------------------------------------------- 1 | getBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()); 20 | if(!$block->isSameType(VanillaBlocks::DIRT()) || !$block->getDirtType()->equals(DirtType::ROOTED())){ 21 | $world->setBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ(), VanillaBlocks::DIRT()->setDirtType(DirtType::ROOTED())); 22 | } 23 | } 24 | 25 | private function generateLog(ChunkManager $world, Vector3 $pos): void{ 26 | $block = $world->getBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()); 27 | if( 28 | $block->isSameType(VanillaBlocks::AIR()) || 29 | $block->isSameType(ExtraVanillaBlocks::AZALEA()) || 30 | $block->isSameType(ExtraVanillaBlocks::AZALEA_LEAVES()) || 31 | $block->isSameType(ExtraVanillaBlocks::AZALEA_LEAVES_FLOWERED()) 32 | ){ 33 | $world->setBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ(), VanillaBlocks::OAK_LOG()); 34 | } 35 | } 36 | 37 | private function generateLeave(ChunkManager $world, Vector3 $pos, Random $random): void{ 38 | $block = $world->getBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()); 39 | if($block->isSameType(VanillaBlocks::AIR())){ 40 | if($random->nextBoundedInt(3) === 1){ 41 | $world->setBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ(), ExtraVanillaBlocks::AZALEA_LEAVES_FLOWERED()); 42 | }else{ 43 | $world->setBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ(), ExtraVanillaBlocks::AZALEA_LEAVES()); 44 | } 45 | } 46 | } 47 | 48 | public function generate(ChunkManager $world, Random $random, Vector3 $pos): void{ 49 | $i = $random->nextBoundedInt(2) + 2; 50 | $j = $pos->getFloorX(); 51 | $k = $pos->getFloorY(); 52 | $l = $pos->getFloorZ(); 53 | 54 | $i2 = $k + $i; 55 | 56 | if($k >= -63 && $k + $i + 2 < 320){ 57 | $down = $pos->down(); 58 | for($il = 0; $il < $i + 1; $il ++){ 59 | $this->generateLog($world, new Vector3($j, $il + $k, $l)); 60 | } 61 | $this->generateRootedDirt($world, $down); 62 | 63 | for($i3 = -2; $i3 <= 1; ++ $i3){ 64 | for($l3 = -2; $l3 <= 1; ++ $l3){ 65 | $k4 = 1; 66 | $offsetX = $random->nextRange(0, 1); 67 | $offsetY = $random->nextRange(0, 1); 68 | $offsetZ = $random->nextRange(0, 1); 69 | $this->generateLeave($world, new Vector3($j + $i3 + $offsetX, $i2 + $k4 + $offsetY, $l + $l3 + $offsetZ), $random); 70 | $this->generateLeave($world, new Vector3($j - $i3 + $offsetX, $i2 + $k4 + $offsetY, $l + $l3 + $offsetZ), $random); 71 | $this->generateLeave($world, new Vector3($j + $i3 + $offsetX, $i2 + $k4 + $offsetY, $l - $l3 + $offsetZ), $random); 72 | $this->generateLeave($world, new Vector3($j - $i3 + $offsetX, $i2 + $k4 + $offsetY, $l - $l3 + $offsetZ), $random); 73 | 74 | for($d = 0; $d < 2; $d ++){ 75 | $this->generateLeave($world, new Vector3($j + $i3, $i2 + $d, $l + $l3), $random); 76 | $this->generateLeave($world, new Vector3($j - $i3, $i2 + $d, $l + $l3), $random); 77 | $this->generateLeave($world, new Vector3($j + $i3, $i2 + $d, $l - $l3), $random); 78 | $this->generateLeave($world, new Vector3($j - $i3, $i2 + $d, $l - $l3), $random); 79 | } 80 | 81 | $k4 = 2; 82 | $offsetX = $random->nextRange(-1, 0); 83 | $offsetY = $random->nextRange(-1, 0); 84 | $offsetZ = $random->nextRange(-1, 0); 85 | 86 | $this->generateLeave($world, new Vector3($j + $i3 + $offsetX, $i2 + $k4 + $offsetY, $l + $l3 + $offsetZ), $random); 87 | $this->generateLeave($world, new Vector3($j - $i3 + $offsetX, $i2 + $k4 + $offsetY, $l + $l3 + $offsetZ), $random); 88 | $this->generateLeave($world, new Vector3($j + $i3 + $offsetX, $i2 + $k4 + $offsetY, $l - $l3 + $offsetZ), $random); 89 | $this->generateLeave($world, new Vector3($j - $i3 + $offsetX, $i2 + $k4 + $offsetY, $l - $l3 + $offsetZ), $random); 90 | } 91 | } 92 | } 93 | 94 | $this->generateLog($world, $pos); 95 | } 96 | } -------------------------------------------------------------------------------- /src/world/particle/BoneMealParticle.php: -------------------------------------------------------------------------------- 1 |