idInfo = $idInfo; $this->fallbackName = $name; $this->typeInfo = $typeInfo; $this->position = new Position(0, 0, 0, null); $calculator = new RuntimeDataSizeCalculator(); $this->describeBlockItemState($calculator); $this->requiredBlockItemStateDataBits = $calculator->getBitsUsed(); $calculator = new RuntimeDataSizeCalculator(); $this->describeBlockOnlyState($calculator); $this->requiredBlockOnlyStateDataBits = $calculator->getBitsUsed(); $this->stateIdXorMask = self::computeStateIdXorMask($idInfo->getBlockTypeId()); //this must be done last, otherwise the defaultState could have uninitialized fields $defaultState = clone $this; $this->defaultState = $defaultState; $defaultState->defaultState = $defaultState; } public function __clone(){ $this->position = clone $this->position; } /** * Returns an object containing information about how to identify and store this block type, such as type ID and * tile type (if any). */ public function getIdInfo() : BlockIdentifier{ return $this->idInfo; } /** * Returns the printable English name of the block. */ public function getName() : string{ return $this->fallbackName; } /** * Returns a type ID that identifies this type of block. This allows comparing basic block types, e.g. wool, stone, * glass, etc. Type ID will not change for a given block type. * * Information such as colour, powered, open/closed, etc. is **not** included in this ID. * If you want to get a state ID that includes this information, use {@link Block::getStateId()} instead. * * @see BlockTypeIds */ public function getTypeId() : int{ return $this->idInfo->getBlockTypeId(); } /** * @internal * * Returns the full blockstate ID of this block. This is a compact way of representing a blockstate used to store * blocks in chunks at runtime. * * This usually encodes all properties of the block, such as facing, open/closed, powered/unpowered, colour, etc. * State ID may change depending on the properties of the block (e.g. a torch facing east will have a different * state ID to one facing west). * * Some blocks (such as signs and chests) may store additional properties in an associated "tile" if they * have too many possible values to be encoded into the state ID. These extra properties are **NOT** included in * this function's result. * * This ID can be used to later obtain a copy of the block with the same state properties by using * {@link RuntimeBlockStateRegistry::fromStateId()}. */ public function getStateId() : int{ return $this->encodeFullState() ^ $this->stateIdXorMask; } /** * Returns whether the given block has the same type ID as this one. */ public function hasSameTypeId(Block $other) : bool{ return $this->getTypeId() === $other->getTypeId(); } /** * Returns whether the given block has the same type and properties as this block. * * Note: Tile data (e.g. sign text, chest contents) are not compared here. */ public function isSameState(Block $other) : bool{ return $this->getStateId() === $other->getStateId(); } /** * @return string[] */ public function getTypeTags() : array{ return $this->typeInfo->getTypeTags(); } /** * Returns whether this block type has the given type tag. Type tags are used as a dynamic way to tag blocks as * having certain properties, allowing type checks which are more dynamic than hardcoding a bunch of IDs or a bunch * of instanceof checks. * * For example, grass blocks, dirt, farmland, podzol and mycelium are all dirt-like blocks, and support the * placement of blocks like flowers, so they have a common tag which allows them to be identified as such. */ public function hasTypeTag(string $tag) : bool{ return $this->typeInfo->hasTypeTag($tag); } /** * Returns the block as an item. * Block-only state such as facing, powered/unpowered, open/closed, etc., is discarded. * Block-item state such as colour, wood type, etc. is preserved. * Complex state properties stored in the tile data (e.g. inventory) are discarded. */ public function asItem() : Item{ $normalized = clone $this->defaultState; $normalized->decodeBlockItemState($this->encodeBlockItemState()); return new ItemBlock($normalized); } private function decodeBlockItemState(int $data) : void{ $reader = new RuntimeDataReader($this->requiredBlockItemStateDataBits, $data); $this->describeBlockItemState($reader); $readBits = $reader->getOffset(); if($this->requiredBlockItemStateDataBits !== $readBits){ throw new \LogicException(get_class($this) . ": Exactly $this->requiredBlockItemStateDataBits bits of block-item state data were provided, but $readBits were read"); } } private function decodeBlockOnlyState(int $data) : void{ $reader = new RuntimeDataReader($this->requiredBlockOnlyStateDataBits, $data); $this->describeBlockOnlyState($reader); $readBits = $reader->getOffset(); if($this->requiredBlockOnlyStateDataBits !== $readBits){ throw new \LogicException(get_class($this) . ": Exactly $this->requiredBlockOnlyStateDataBits bits of block-only state data were provided, but $readBits were read"); } } private function encodeBlockItemState() : int{ $writer = new RuntimeDataWriter($this->requiredBlockItemStateDataBits); $this->describeBlockItemState($writer); $writtenBits = $writer->getOffset(); if($this->requiredBlockItemStateDataBits !== $writtenBits){ throw new \LogicException(get_class($this) . ": Exactly $this->requiredBlockItemStateDataBits bits of block-item state data were expected, but $writtenBits were written"); } return $writer->getValue(); } private function encodeBlockOnlyState() : int{ $writer = new RuntimeDataWriter($this->requiredBlockOnlyStateDataBits); $this->describeBlockOnlyState($writer); $writtenBits = $writer->getOffset(); if($this->requiredBlockOnlyStateDataBits !== $writtenBits){ throw new \LogicException(get_class($this) . ": Exactly $this->requiredBlockOnlyStateDataBits bits of block-only state data were expected, but $writtenBits were written"); } return $writer->getValue(); } private function encodeFullState() : int{ $blockItemBits = $this->requiredBlockItemStateDataBits; $blockOnlyBits = $this->requiredBlockOnlyStateDataBits; if($blockOnlyBits === 0 && $blockItemBits === 0){ return 0; } $result = 0; if($blockItemBits > 0){ $result |= $this->encodeBlockItemState(); } if($blockOnlyBits > 0){ $result |= $this->encodeBlockOnlyState() << $blockItemBits; } return $result; } /** * Describes properties of this block which apply to both the block and item form of the block. * Examples of suitable properties include colour, skull type, and any other information which **IS** kept when the * block is mined or block-picked. * * The method implementation must NOT use conditional logic to determine which properties are written. It must * always write the same properties in the same order, regardless of the current state of the block. */ public function describeBlockItemState(RuntimeDataDescriber $w) : void{ //NOOP } /** * Describes properties of this block which apply only to the block form of the block. * Examples of suitable properties include facing, open/closed, powered/unpowered, on/off, and any other information * which **IS NOT** kept when the block is mined or block-picked. * * The method implementation must NOT use conditional logic to determine which properties are written. It must * always write the same properties in the same order, regardless of the current state of the block. */ protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ //NOOP } /** * Generates copies of this Block in all possible state permutations. * Every possible combination of known properties (e.g. facing, open/closed, powered/unpowered, on/off) will be * generated. * * @phpstan-return \Generator */ public function generateStatePermutations() : \Generator{ //TODO: this bruteforce approach to discovering all valid states is very inefficient for larger state data sizes //at some point we'll need to find a better way to do this $bits = $this->requiredBlockItemStateDataBits + $this->requiredBlockOnlyStateDataBits; if($bits > Block::INTERNAL_STATE_DATA_BITS){ throw new \LogicException("Block state data cannot use more than " . Block::INTERNAL_STATE_DATA_BITS . " bits"); } for($blockItemStateData = 0; $blockItemStateData < (1 << $this->requiredBlockItemStateDataBits); ++$blockItemStateData){ $withType = clone $this; try{ $withType->decodeBlockItemState($blockItemStateData); $encoded = $withType->encodeBlockItemState(); if($encoded !== $blockItemStateData){ throw new \LogicException(static::class . "::decodeBlockItemState() accepts invalid inputs (returned $encoded for input $blockItemStateData)"); } }catch(InvalidSerializedRuntimeDataException){ //invalid property combination, leave it continue; } for($blockOnlyStateData = 0; $blockOnlyStateData < (1 << $this->requiredBlockOnlyStateDataBits); ++$blockOnlyStateData){ $withState = clone $withType; try{ $withState->decodeBlockOnlyState($blockOnlyStateData); $encoded = $withState->encodeBlockOnlyState(); if($encoded !== $blockOnlyStateData){ throw new \LogicException(static::class . "::decodeBlockOnlyState() accepts invalid inputs (returned $encoded for input $blockOnlyStateData)"); } }catch(InvalidSerializedRuntimeDataException){ //invalid property combination, leave it continue; } yield $withState; } } } /** * Called when this block is created, set, or has a neighbouring block update, to re-detect dynamic properties which * are not saved in the blockstate ID. * If any such properties are updated, don't forget to clear things like AABB caches if necessary. * * A replacement block may be returned. This is useful if the block type changed due to reading of world data (e.g. * data from a block entity). */ public function readStateFromWorld() : Block{ return $this; } /** * Writes information about the block into the world. This writes the blockstate ID into the chunk, and creates * and/or removes tiles as necessary. * * Note: Do not call this directly. Pass the block to {@link World::setBlock()} instead. */ public function writeStateToWorld() : void{ $world = $this->position->getWorld(); $chunk = $world->getOrLoadChunkAtPosition($this->position); if($chunk === null){ throw new AssumptionFailedError("World::setBlock() should have loaded the chunk before calling this method"); } $chunk->setBlockStateId($this->position->x & Chunk::COORD_MASK, $this->position->y, $this->position->z & Chunk::COORD_MASK, $this->getStateId()); $tileType = $this->idInfo->getTileClass(); $oldTile = $world->getTile($this->position); if($oldTile !== null){ if($tileType === null || !($oldTile instanceof $tileType)){ $oldTile->close(); $oldTile = null; }elseif($oldTile instanceof Spawnable){ $oldTile->clearSpawnCompoundCache(); //destroy old network cache } } if($oldTile === null && $tileType !== null){ /** * @var Tile $tile * @see Tile::__construct() */ $tile = new $tileType($world, $this->position->asVector3()); $world->addTile($tile); } } /** * AKA: Block->isPlaceable */ public function canBePlaced() : bool{ return true; } /** * Returns whether this block can be replaced by another block placed in the same position. */ public function canBeReplaced() : bool{ return false; } /** * Returns whether this block can replace the given block in the given placement conditions. * This is used to allow slabs of the same type to combine into double slabs. */ public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{ return $blockReplace->canBeReplaced(); } /** * Generates a block transaction to set all blocks affected by placing this block. Usually this is just the block * itself, but may be multiple blocks in some cases (such as doors). * * @param BlockTransaction $tx Blocks to be set should be added to this transaction (do not modify thr world directly) * @param Item $item Item used to place the block * @param Block $blockReplace Block expected to be replaced * @param Block $blockClicked Block that was clicked using the item * @param int $face Face of the clicked block which was clicked * @param Vector3 $clickVector Exact position inside the clicked block where the click occurred, relative to the block's position * @param Player|null $player Player who placed the block, or null if it was not a player * * @return bool whether the placement should go ahead */ public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ $tx->addBlock($blockReplace->position, $this); return true; } /** * Called immediately after the block has been placed in the world. Since placement uses a block transaction, some * things may not be possible until after the transaction has been executed. */ public function onPostPlace() : void{ } /** * Returns an object containing information about the destruction requirements of this block. */ public function getBreakInfo() : BlockBreakInfo{ return $this->typeInfo->getBreakInfo(); } /** * Returns tags that represent the type of item being enchanted and are used to determine * what enchantments can be applied to the item of this block during in-game enchanting (enchanting table, anvil, fishing, etc.). * @see ItemEnchantmentTags * @see ItemEnchantmentTagRegistry * @see AvailableEnchantmentRegistry * * @return string[] */ public function getEnchantmentTags() : array{ return $this->typeInfo->getEnchantmentTags(); } /** * Do the actions needed so the block is broken with the Item * * @param Item[] &$returnedItems Items to be added to the target's inventory (or dropped, if full) */ public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{ $world = $this->position->getWorld(); if(($t = $world->getTile($this->position)) !== null){ $t->onBlockDestroyed(); } $world->setBlock($this->position, VanillaBlocks::AIR()); return true; } /** * Called when this block or a block immediately adjacent to it changes state. */ public function onNearbyBlockChange() : void{ } /** * Returns whether random block updates will be done on this block. */ public function ticksRandomly() : bool{ return false; } /** * Called when this block is randomly updated due to chunk ticking. * WARNING: This will not be called if {@link Block::ticksRandomly()} does not return true! */ public function onRandomTick() : void{ } /** * Called when this block is updated by the delayed blockupdate scheduler in the world. */ public function onScheduledUpdate() : void{ } /** * Do actions when interacted by Item. Returns if it has done anything * * @param Vector3 $clickVector Exact position where the click occurred, relative to the block's integer position * @param Item[] &$returnedItems Items to be added to the target's inventory (or dropped, if the inventory is full) */ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ return false; } /** * Called when this block is attacked (left-clicked) by a player attempting to start breaking it in survival. * * @return bool if an action took place, prevents starting to break the block if true. */ public function onAttack(Item $item, int $face, ?Player $player = null) : bool{ return false; } /** * Returns a multiplier applied to the velocity of entities moving on top of this block. A higher value will make * the block more slippery (like ice). * * @return float 0.0-1.0 */ public function getFrictionFactor() : float{ return 0.6; } /** * Returns the amount of light emitted by this block. * * @return int 0-15 */ public function getLightLevel() : int{ return 0; } /** * Returns the amount of light this block will filter out when light passes through this block. * This value is used in light spread calculation. * * @return int 0-15 */ public function getLightFilter() : int{ return $this->isTransparent() ? 0 : 15; } /** * Returns whether this block blocks direct sky light from passing through it. This is independent from the light * filter value, which is used during propagation. * * In most cases, this is the same as isTransparent(); however, some special cases exist such as leaves and cobwebs, * which don't have any additional effect on light propagation, but don't allow direct sky light to pass through. */ public function blocksDirectSkyLight() : bool{ return $this->getLightFilter() > 0; } public function isTransparent() : bool{ return false; } public function isSolid() : bool{ return true; } /** * AKA: Block->isFlowable */ public function canBeFlowedInto() : bool{ return false; } /** * Returns whether entities can climb up this block. */ public function canClimb() : bool{ return false; } final public function getPosition() : Position{ return $this->position; } /** * @internal */ final public function position(World $world, int $x, int $y, int $z) : void{ $this->position = new Position($x, $y, $z, $world); $this->collisionBoxes = null; } /** * Returns an array of Item objects to be dropped * * @return Item[] */ public function getDrops(Item $item) : array{ if($this->getBreakInfo()->isToolCompatible($item)){ if($this->isAffectedBySilkTouch() && $item->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){ return $this->getSilkTouchDrops($item); } return $this->getDropsForCompatibleTool($item); } return $this->getDropsForIncompatibleTool($item); } /** * Returns an array of Items to be dropped when the block is broken using the correct tool type. * * @return Item[] */ public function getDropsForCompatibleTool(Item $item) : array{ return [$this->asItem()]; } /** * Returns the items dropped by this block when broken with an incorrect tool type (or tool with a too-low tier). * * @return Item[] */ public function getDropsForIncompatibleTool(Item $item) : array{ return []; } /** * Returns an array of Items to be dropped when the block is broken using a compatible Silk Touch-enchanted tool. * * @return Item[] */ public function getSilkTouchDrops(Item $item) : array{ return [$this->asItem()]; } /** * Returns how much XP will be dropped by breaking this block with the given item. */ public function getXpDropForTool(Item $item) : int{ if($item->hasEnchantment(VanillaEnchantments::SILK_TOUCH()) || !$this->getBreakInfo()->isToolCompatible($item)){ return 0; } return $this->getXpDropAmount(); } /** * Returns how much XP this block will drop when broken with an appropriate tool. */ protected function getXpDropAmount() : int{ return 0; } /** * Returns whether Silk Touch enchanted tools will cause this block to drop as itself. */ public function isAffectedBySilkTouch() : bool{ return false; } /** * Returns the item that players will equip when middle-clicking on this block. * If addUserData is true, additional data may be added, such as banner patterns, chest contents, etc. */ public function getPickedItem(bool $addUserData = false) : Item{ $item = $this->asItem(); if($addUserData){ $tile = $this->position->getWorld()->getTile($this->position); if($tile instanceof Tile){ $nbt = $tile->getCleanedNBT(); if($nbt instanceof CompoundTag){ $item->setCustomBlockData($nbt); $item->setLore(["+(DATA)"]); } } } return $item; } /** * Returns the time in ticks which the block will fuel a furnace for. */ public function getFuelTime() : int{ return 0; } /** * Returns the maximum number of this block that can fit into a single item stack. */ public function getMaxStackSize() : int{ return 64; } public function isFireProofAsItem() : bool{ return false; } /** * Returns the chance that the block will catch fire from nearby fire sources. Higher values lead to faster catching * fire. */ public function getFlameEncouragement() : int{ return 0; } /** * Returns the base flammability of this block. Higher values lead to the block burning away more quickly. */ public function getFlammability() : int{ return 0; } /** * Returns whether fire lit on this block will burn indefinitely. */ public function burnsForever() : bool{ return false; } /** * Returns whether this block can catch fire. */ public function isFlammable() : bool{ return $this->getFlammability() > 0; } /** * Called when this block is burned away by being on fire. */ public function onIncinerate() : void{ } /** * Returns the Block on the side $side, works like Vector3::getSide() * * @return Block */ public function getSide(int $side, int $step = 1){ $position = $this->position; if($position->isValid()){ [$dx, $dy, $dz] = Facing::OFFSET[$side] ?? [0, 0, 0]; return $position->getWorld()->getBlockAt( $position->x + ($dx * $step), $position->y + ($dy * $step), $position->z + ($dz * $step) ); } throw new \LogicException("Block does not have a valid world"); } /** * Returns the 4 blocks on the horizontal axes around the block (north, south, east, west) * * @return Block[]|\Generator * @phpstan-return \Generator */ public function getHorizontalSides() : \Generator{ $world = $this->position->getWorld(); foreach(Facing::HORIZONTAL as $facing){ [$dx, $dy, $dz] = Facing::OFFSET[$facing]; //TODO: yield Facing as the key? yield $world->getBlockAt( $this->position->x + $dx, $this->position->y + $dy, $this->position->z + $dz ); } } /** * Returns the six blocks around this block. * * @return Block[]|\Generator * @phpstan-return \Generator */ public function getAllSides() : \Generator{ $world = $this->position->getWorld(); foreach(Facing::OFFSET as [$dx, $dy, $dz]){ //TODO: yield Facing as the key? yield $world->getBlockAt( $this->position->x + $dx, $this->position->y + $dy, $this->position->z + $dz ); } } /** * Returns a list of blocks that this block is part of. In most cases, only contains the block itself, but in cases * such as double plants, beds and doors, will contain both halves. * * @return Block[] */ public function getAffectedBlocks() : array{ return [$this]; } /** * @return string */ public function __toString(){ return "Block[" . $this->getName() . "] (" . $this->getTypeId() . ":" . $this->encodeFullState() . ")"; } /** * Returns whether any of the block's collision boxes intersect with the given AxisAlignedBB. */ public function collidesWithBB(AxisAlignedBB $bb) : bool{ foreach($this->getCollisionBoxes() as $bb2){ if($bb->intersectsWith($bb2)){ return true; } } return false; } /** * Returns whether the block has actions to be executed when an entity enters its cell (full cube space). * * @see Block::onEntityInside() */ public function hasEntityCollision() : bool{ return false; } /** * Called when an entity's bounding box clips inside this block's cell. Note that the entity may not be intersecting * with the collision box or bounding box. * * WARNING: This will not be called if {@link Block::hasEntityCollision()} returns false. * * @return bool Whether the block is still the same after the intersection. If it changed (e.g. due to an explosive * being ignited), this should return false. */ public function onEntityInside(Entity $entity) : bool{ return true; } /** * Returns a direction vector describing which way an entity intersecting this block should be pushed. * This is used by liquids to push entities in liquid currents. * * The returned vector is summed with vectors from every other block the entity is intersecting, and normalized to * produce a final direction vector. * * WARNING: This will not be called if {@link Block::hasEntityCollision()} does not return true! */ public function addVelocityToEntity(Entity $entity) : ?Vector3{ return null; } /** * Called when an entity lands on this block (usually due to falling). * @return float|null The new vertical velocity of the entity, or null if unchanged. */ public function onEntityLand(Entity $entity) : ?float{ return null; } /** * Called when a projectile collides with one of this block's collision boxes. */ public function onProjectileHit(Projectile $projectile, RayTraceResult $hitResult) : void{ //NOOP } /** * Returns an array of collision bounding boxes for this block. * These are used for: * - entity movement collision checks (to ensure entities can't clip through blocks) * - projectile flight paths * - block placement (to ensure the player can't place blocks inside itself or another entity) * - anti-cheat checks in plugins * * @return AxisAlignedBB[] */ final public function getCollisionBoxes() : array{ if($this->collisionBoxes === null){ $this->collisionBoxes = $this->recalculateCollisionBoxes(); $extraOffset = $this->getModelPositionOffset(); $offset = $extraOffset !== null ? $this->position->addVector($extraOffset) : $this->position; foreach($this->collisionBoxes as $bb){ $bb->offset($offset->x, $offset->y, $offset->z); } } return $this->collisionBoxes; } /** * Returns an additional fractional vector to shift the block model's position by based on the current position. * Used to randomize position of things like bamboo canes and tall grass. */ public function getModelPositionOffset() : ?Vector3{ return null; } /** * @return AxisAlignedBB[] */ protected function recalculateCollisionBoxes() : array{ return [AxisAlignedBB::one()]; } /** * Returns the type of support that the block can provide on the given face. This is used to determine whether * blocks placed on the given face can be supported by this block. */ public function getSupportType(int $facing) : SupportType{ return SupportType::FULL; } protected function getAdjacentSupportType(int $facing) : SupportType{ return $this->getSide($facing)->getSupportType(Facing::opposite($facing)); } public function isFullCube() : bool{ $bb = $this->getCollisionBoxes(); return count($bb) === 1 && $bb[0]->getAverageEdgeLength() >= 1 && $bb[0]->isCube(); } /** * Performs a ray trace along the line between the two positions using the block's collision boxes. * Returns the intersection point closest to pos1, or null if no intersection occurred. */ public function calculateIntercept(Vector3 $pos1, Vector3 $pos2) : ?RayTraceResult{ $bbs = $this->getCollisionBoxes(); if(count($bbs) === 0){ return null; } /** @var RayTraceResult|null $currentHit */ $currentHit = null; /** @var int|float $currentDistance */ $currentDistance = PHP_INT_MAX; foreach($bbs as $bb){ $nextHit = $bb->calculateIntercept($pos1, $pos2); if($nextHit === null){ continue; } $nextDistance = $nextHit->hitVector->distanceSquared($pos1); if($nextDistance < $currentDistance){ $currentHit = $nextHit; $currentDistance = $nextDistance; } } return $currentHit; } }