woodType = $woodType; parent::__construct($idInfo, $name, $typeInfo); $this->text = new SignText(); $this->backText = new SignText(); $this->asItemCallback = $asItemCallback; } public function readStateFromWorld() : Block{ parent::readStateFromWorld(); $tile = $this->position->getWorld()->getTile($this->position); if($tile instanceof TileSign){ $this->text = $tile->getText(); $this->backText = $tile->getBackText(); $this->waxed = $tile->isWaxed(); $this->editorEntityRuntimeId = $tile->getEditorEntityRuntimeId(); } return $this; } public function writeStateToWorld() : void{ parent::writeStateToWorld(); $tile = $this->position->getWorld()->getTile($this->position); assert($tile instanceof TileSign); $tile->setText($this->text); $tile->setBackText($this->backText); $tile->setWaxed($this->waxed); $tile->setEditorEntityRuntimeId($this->editorEntityRuntimeId); } public function isSolid() : bool{ return false; } public function getMaxStackSize() : int{ return 16; } protected function recalculateCollisionBoxes() : array{ return []; } public function getSupportType(Facing $facing) : SupportType{ return SupportType::NONE; } abstract protected function getSupportingFace() : Facing; public function onNearbyBlockChange() : void{ if($this->getSide($this->getSupportingFace())->getTypeId() === BlockTypeIds::AIR){ $this->position->getWorld()->useBreakOn($this->position); } } public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, Facing $face, Vector3 $clickVector, ?Player $player = null) : bool{ if($player !== null){ $this->editorEntityRuntimeId = $player->getId(); } return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); } public function onPostPlace() : void{ $player = $this->editorEntityRuntimeId !== null ? $this->position->getWorld()->getEntity($this->editorEntityRuntimeId) : null; if($player instanceof Player){ $player->openSignEditor($this->position); } } private function doSignChange(SignText $newText, Player $player, Item $item, bool $frontFace) : bool{ $ev = new SignChangeEvent($this, $player, $newText, $frontFace); $ev->call(); if(!$ev->isCancelled()){ $this->setFaceText($frontFace, $ev->getNewText()); $this->position->getWorld()->setBlock($this->position, $this); $item->pop(); return true; } return false; } private function changeSignGlowingState(bool $glowing, Player $player, Item $item, bool $frontFace) : bool{ $text = $this->getFaceText($frontFace); if($text->isGlowing() !== $glowing && $this->doSignChange(new SignText($text->getLines(), $text->getBaseColor(), $glowing), $player, $item, $frontFace)){ $this->position->getWorld()->addSound($this->position, new InkSacUseSound()); return true; } return false; } private function wax(Player $player, Item $item) : bool{ if($this->waxed){ return false; } $this->waxed = true; $this->position->getWorld()->setBlock($this->position, $this); $item->pop(); return true; } public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player === null){ return false; } if($this->waxed){ return true; } $frontFace = $this->interactsFront($this->getHitboxCenter(), $player->getPosition(), $this->getFacingDegrees()); $dyeColor = $item instanceof Dye ? $item->getColor() : match($item->getTypeId()){ ItemTypeIds::BONE_MEAL => DyeColor::WHITE, ItemTypeIds::LAPIS_LAZULI => DyeColor::BLUE, ItemTypeIds::COCOA_BEANS => DyeColor::BROWN, default => null }; if($dyeColor !== null){ $color = $dyeColor === DyeColor::BLACK ? new Color(0, 0, 0) : $dyeColor->getRgbValue(); $text = $this->getFaceText($frontFace); if( $color->toARGB() !== $text->getBaseColor()->toARGB() && $this->doSignChange(new SignText($text->getLines(), $color, $text->isGlowing()), $player, $item, $frontFace) ){ $this->position->getWorld()->addSound($this->position, new DyeUseSound()); return true; } }elseif(match($item->getTypeId()){ ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item, $frontFace), ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item, $frontFace), ItemTypeIds::HONEYCOMB => $this->wax($player, $item), default => false }){ return true; } $player->openSignEditor($this->position, $frontFace); return true; } private function interactsFront(Vector3 $hitboxCenter, Vector3 $playerPosition, float $signFacingDegrees) : bool{ $playerCenterDiffX = $playerPosition->x - $hitboxCenter->x; $playerCenterDiffZ = $playerPosition->z - $hitboxCenter->z; $f1 = rad2deg(atan2($playerCenterDiffZ, $playerCenterDiffX)) - 90.0; $rotationDiff = $signFacingDegrees - $f1; $rotation = fmod($rotationDiff + 180.0, 360.0) - 180.0; // Normalize to [-180, 180] return abs($rotation) <= 90.0; } /** * Returns the center of the sign's hitbox. Used to decide which face of the sign to open when a player interacts. */ protected function getHitboxCenter() : Vector3{ return $this->position->add(0.5, 0.5, 0.5); } /** * TODO: make this abstract (BC break) */ protected function getFacingDegrees() : float{ return 0; } /** * Returns an object containing information about the sign text. * @deprecated * @see self::getFaceText() */ public function getText() : SignText{ return $this->text; } /** * @deprecated * @see self::setFaceText() * @return $this */ public function setText(SignText $text) : self{ $this->text = $text; return $this; } public function getFaceText(bool $frontFace) : SignText{ return $frontFace ? $this->text : $this->backText; } /** @return $this */ public function setFaceText(bool $frontFace, SignText $text) : self{ $frontFace ? $this->text = $text : $this->backText = $text; return $this; } /** * Returns whether the sign has been waxed using a honeycomb. If true, the sign cannot be edited by a player. */ public function isWaxed() : bool{ return $this->waxed; } /** @return $this */ public function setWaxed(bool $waxed) : self{ $this->waxed = $waxed; return $this; } /** * Sets the runtime entity ID of the player editing this sign. Only this player will be able to edit the sign. * This is used to prevent multiple players from editing the same sign at the same time, and to prevent players * from editing signs they didn't place. */ public function getEditorEntityRuntimeId() : ?int{ return $this->editorEntityRuntimeId; } /** @return $this */ public function setEditorEntityRuntimeId(?int $editorEntityRuntimeId) : self{ $this->editorEntityRuntimeId = $editorEntityRuntimeId; return $this; } /** * @deprecated * @see self::updateFaceText() */ public function updateText(Player $author, SignText $text) : bool{ return $this->updateFaceText($author, true, $text); } /** * Called by the player controller (network session) to update the sign text, firing events as appropriate. * * @return bool if the sign update was successful. * @throws \UnexpectedValueException if the text payload is too large */ public function updateFaceText(Player $author, bool $frontFace, SignText $text) : bool{ $size = 0; foreach($text->getLines() as $line){ $size += strlen($line); } if($size > 1000){ throw new \UnexpectedValueException($author->getName() . " tried to write $size bytes of text onto a sign (bigger than max 1000)"); } $oldText = $this->getFaceText($frontFace); $ev = new SignChangeEvent($this, $author, new SignText(array_map(function(string $line) : string{ return TextFormat::clean($line, false); }, $text->getLines()), $oldText->getBaseColor(), $oldText->isGlowing()), $frontFace); if($this->waxed || $this->editorEntityRuntimeId !== $author->getId()){ $ev->cancel(); } $ev->call(); if(!$ev->isCancelled()){ $this->setFaceText($frontFace, $ev->getNewText()); $this->setEditorEntityRuntimeId(null); $this->position->getWorld()->setBlock($this->position, $this); return true; } return false; } public function asItem() : Item{ return ($this->asItemCallback)(); } public function getFuelTime() : int{ return $this->woodType->isFlammable() ? 200 : 0; } }