diff --git a/src/pocketmine/entity/Entity.php b/src/pocketmine/entity/Entity.php index a0c7ae2ad..c64bcd2a3 100644 --- a/src/pocketmine/entity/Entity.php +++ b/src/pocketmine/entity/Entity.php @@ -30,6 +30,8 @@ use pocketmine\block\Block; use pocketmine\block\BlockFactory; use pocketmine\block\Water; use pocketmine\entity\object\ExperienceOrb; +use pocketmine\entity\object\Painting; +use pocketmine\entity\object\PaintingMotive; use pocketmine\entity\projectile\Arrow; use pocketmine\entity\projectile\Egg; use pocketmine\entity\projectile\Snowball; @@ -231,6 +233,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{ Entity::registerEntity(ExperienceOrb::class, false, ['XPOrb', 'minecraft:xp_orb']); Entity::registerEntity(FallingSand::class, false, ['FallingSand', 'minecraft:falling_block']); Entity::registerEntity(Item::class, false, ['Item', 'minecraft:item']); + Entity::registerEntity(Painting::class, false, ['Painting', 'minecraft:painting']); Entity::registerEntity(PrimedTNT::class, false, ['PrimedTnt', 'PrimedTNT', 'minecraft:tnt']); Entity::registerEntity(Snowball::class, false, ['Snowball', 'minecraft:snowball']); Entity::registerEntity(Squid::class, false, ['Squid', 'minecraft:squid']); @@ -241,6 +244,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{ Attribute::init(); Effect::init(); + PaintingMotive::init(); } diff --git a/src/pocketmine/entity/object/Painting.php b/src/pocketmine/entity/object/Painting.php new file mode 100644 index 000000000..8e03903c1 --- /dev/null +++ b/src/pocketmine/entity/object/Painting.php @@ -0,0 +1,301 @@ +motive = $nbt->getString("Motive"); + $this->blockIn = new Vector3($nbt->getInt("TileX"), $nbt->getInt("TileY"), $nbt->getInt("TileZ")); + if($nbt->hasTag("Direction", ByteTag::class)){ + $this->direction = $nbt->getByte("Direction"); + }elseif($nbt->hasTag("Facing", ByteTag::class)){ + $this->direction = $nbt->getByte("Facing"); + } + parent::__construct($level, $nbt); + } + + protected function initEntity(){ + $this->setMaxHealth(1); + $this->setHealth(1); + parent::initEntity(); + } + + public function saveNBT(){ + parent::saveNBT(); + $this->namedtag->setInt("TileX", (int) $this->blockIn->x); + $this->namedtag->setInt("TileY", (int) $this->blockIn->y); + $this->namedtag->setInt("TileZ", (int) $this->blockIn->z); + + $this->namedtag->setByte("Facing", (int) $this->direction); + $this->namedtag->setByte("Direction", (int) $this->direction); //Save both for full compatibility + } + + public function entityBaseTick(int $tickDiff = 1) : bool{ + static $directions = [ + 0 => Vector3::SIDE_SOUTH, + 1 => Vector3::SIDE_WEST, + 2 => Vector3::SIDE_NORTH, + 3 => Vector3::SIDE_EAST + ]; + + $hasUpdate = parent::entityBaseTick($tickDiff); + + if($this->checkDestroyedTicker++ > 10){ + /* + * we don't have a way to only update on local block updates yet! since random chunk ticking always updates + * all the things + * ugly hack, but vanilla uses 100 ticks so on there it looks even worse + */ + $this->checkDestroyedTicker = 0; + $face = $directions[$this->direction]; + if(!self::canFit($this->level, $this->blockIn->getSide($face), $face, false, $this->getMotive())){ + $this->kill(); + $hasUpdate = true; + } + } + + return $hasUpdate; //doesn't need to be ticked always + } + + public function kill(){ + parent::kill(); + + $drops = true; + + if($this->lastDamageCause instanceof EntityDamageByEntityEvent){ + $killer = $this->lastDamageCause->getDamager(); + if($killer instanceof Player and $killer->isCreative()){ + $drops = false; + } + } + + if($drops){ + //non-living entities don't have a way to create drops generically yet + $this->level->dropItem($this, ItemFactory::get(Item::PAINTING)); + } + $this->level->addParticle(new DestroyParticle($this->add(0.5, 0.5, 0.5), Item::PAINTING)); + } + + protected function recalculateBoundingBox() : void{ + static $directions = [ + 0 => Vector3::SIDE_SOUTH, + 1 => Vector3::SIDE_WEST, + 2 => Vector3::SIDE_NORTH, + 3 => Vector3::SIDE_EAST + ]; + + $facing = $directions[$this->direction]; + + $this->boundingBox->setBB(self::getPaintingBB($this->blockIn->getSide($facing), $facing, $this->getMotive())); + } + + protected function tryChangeMovement(){ + $this->motionX = $this->motionY = $this->motionZ = 0; + } + + protected function updateMovement(bool $teleport = false){ + + } + + public function canBeCollidedWith() : bool{ + return false; + } + + protected function sendSpawnPacket(Player $player) : void{ + $pk = new AddPaintingPacket(); + $pk->entityRuntimeId = $this->getId(); + $pk->x = $this->blockIn->x; + $pk->y = $this->blockIn->y; + $pk->z = $this->blockIn->z; + $pk->direction = $this->direction; + $pk->title = $this->motive; + + $player->dataPacket($pk); + } + + /** + * Returns the painting motive (which image is displayed on the painting) + * @return PaintingMotive + */ + public function getMotive() : PaintingMotive{ + return PaintingMotive::getMotiveByName($this->motive); + } + + public function getDirection() : int{ + return $this->direction; + } + + /** + * Returns the bounding-box a painting with the specified motive would have at the given position and direction. + * + * @param Vector3 $blockIn + * @param int $facing + * @param PaintingMotive $motive + * + * @return AxisAlignedBB + */ + private static function getPaintingBB(Vector3 $blockIn, int $facing, PaintingMotive $motive) : AxisAlignedBB{ + $width = $motive->getWidth(); + $height = $motive->getHeight(); + + $horizontalStart = (int) (ceil($width / 2) - 1); + $verticalStart = (int) (ceil($height / 2) - 1); + + $thickness = 1 / 16; + + $minX = $maxX = 0; + $minZ = $maxZ = 0; + + $minY = -$verticalStart; + $maxY = $minY + $height; + + switch($facing){ + case Vector3::SIDE_NORTH: + $minZ = 1 - $thickness; + $maxZ = 1; + $maxX = $horizontalStart + 1; + $minX = $maxX - $width; + break; + case Vector3::SIDE_SOUTH: + $minZ = 0; + $maxZ = $thickness; + $minX = -$horizontalStart; + $maxX = $minX + $width; + break; + case Vector3::SIDE_WEST: + $minX = 1 - $thickness; + $maxX = 1; + $minZ = -$horizontalStart; + $maxZ = $minZ + $width; + break; + case Vector3::SIDE_EAST: + $minX = 0; + $maxX = $thickness; + $maxZ = $horizontalStart + 1; + $minZ = $maxZ - $width; + break; + } + + return new AxisAlignedBB( + $blockIn->x + $minX, + $blockIn->y + $minY, + $blockIn->z + $minZ, + $blockIn->x + $maxX, + $blockIn->y + $maxY, + $blockIn->z + $maxZ + ); + } + + /** + * Returns whether a painting with the specified motive can be placed at the given position. + * + * @param Level $level + * @param Vector3 $blockIn + * @param int $facing + * @param bool $checkOverlap + * @param PaintingMotive $motive + * + * @return bool + */ + public static function canFit(Level $level, Vector3 $blockIn, int $facing, bool $checkOverlap, PaintingMotive $motive) : bool{ + $width = $motive->getWidth(); + $height = $motive->getHeight(); + + $horizontalStart = (int) (ceil($width / 2) - 1); + $verticalStart = (int) (ceil($height / 2) - 1); + + switch($facing){ + case Vector3::SIDE_NORTH: + $rotatedFace = Vector3::SIDE_WEST; + break; + case Vector3::SIDE_WEST: + $rotatedFace = Vector3::SIDE_SOUTH; + break; + case Vector3::SIDE_SOUTH: + $rotatedFace = Vector3::SIDE_EAST; + break; + case Vector3::SIDE_EAST: + $rotatedFace = Vector3::SIDE_NORTH; + break; + default: + return false; + } + + $oppositeSide = Vector3::getOppositeSide($facing); + + $startPos = $blockIn->asVector3()->getSide(Vector3::getOppositeSide($rotatedFace), $horizontalStart)->getSide(Vector3::SIDE_DOWN, $verticalStart); + + for($w = 0; $w < $width; ++$w){ + for($h = 0; $h < $height; ++$h){ + $pos = $startPos->getSide($rotatedFace, $w)->getSide(Vector3::SIDE_UP, $h); + + $block = $level->getBlockAt($pos->x, $pos->y, $pos->z); + if($block->isSolid() or !$block->getSide($oppositeSide)->isSolid()){ + return false; + } + } + } + + if($checkOverlap){ + $bb = self::getPaintingBB($blockIn, $facing, $motive); + + foreach($level->getNearbyEntities($bb) as $entity){ + if($entity instanceof self){ + return false; + } + } + } + + return true; + } +} diff --git a/src/pocketmine/entity/object/PaintingMotive.php b/src/pocketmine/entity/object/PaintingMotive.php new file mode 100644 index 000000000..5a9509664 --- /dev/null +++ b/src/pocketmine/entity/object/PaintingMotive.php @@ -0,0 +1,127 @@ +getName()] = $motive; + } + + /** + * @param string $name + * @return PaintingMotive|null + */ + public static function getMotiveByName(string $name) : ?PaintingMotive{ + return self::$motives[$name] ?? null; + } + + /** + * @return PaintingMotive[] + */ + public static function getAll() : array{ + return self::$motives; + } + + /** @var string */ + protected $name; + /** @var int */ + protected $width; + /** @var int */ + protected $height; + + + public function __construct(int $width, int $height, string $name){ + $this->name = $name; + $this->width = $width; + $this->height = $height; + } + + /** + * @return string + */ + public function getName() : string{ + return $this->name; + } + + /** + * @return int + */ + public function getWidth() : int{ + return $this->width; + } + + /** + * @return int + */ + public function getHeight() : int{ + return $this->height; + } + + public function __toString() : string{ + return "PaintingMotive(name: " . $this->getName() . ", height: " . $this->getHeight() . ", width: " . $this->getWidth() . ")"; + } +} diff --git a/src/pocketmine/item/ItemFactory.php b/src/pocketmine/item/ItemFactory.php index 63a03e925..45eabf946 100644 --- a/src/pocketmine/item/ItemFactory.php +++ b/src/pocketmine/item/ItemFactory.php @@ -105,7 +105,7 @@ class ItemFactory{ self::registerItem(new Item(Item::FLINT, 0, "Flint")); self::registerItem(new RawPorkchop()); self::registerItem(new CookedPorkchop()); - self::registerItem(new Painting()); + self::registerItem(new PaintingItem()); self::registerItem(new GoldenApple()); self::registerItem(new Sign()); self::registerItem(new ItemBlock(Block::OAK_DOOR_BLOCK, 0, Item::OAK_DOOR)); diff --git a/src/pocketmine/item/Painting.php b/src/pocketmine/item/Painting.php deleted file mode 100644 index df08dfe0b..000000000 --- a/src/pocketmine/item/Painting.php +++ /dev/null @@ -1,94 +0,0 @@ -isTransparent() === false and $face > 1 and $blockReplace->isSolid() === false){ - $faces = [ - 2 => 1, - 3 => 3, - 4 => 0, - 5 => 2, - - ]; - $motives = [ - // Motive Width Height - ["Kebab", 1, 1], - ["Aztec", 1, 1], - ["Alban", 1, 1], - ["Aztec2", 1, 1], - ["Bomb", 1, 1], - ["Plant", 1, 1], - ["Wasteland", 1, 1], - ["Wanderer", 1, 2], - ["Graham", 1, 2], - ["Pool", 2, 1], - ["Courbet", 2, 1], - ["Sunset", 2, 1], - ["Sea", 2, 1], - ["Creebet", 2, 1], - ["Match", 2, 2], - ["Bust", 2, 2], - ["Stage", 2, 2], - ["Void", 2, 2], - ["SkullAndRoses", 2, 2], - //array("Wither", 2, 2), - ["Fighters", 4, 2], - ["Skeleton", 4, 3], - ["DonkeyKong", 4, 3], - ["Pointer", 4, 4], - ["Pigscene", 4, 4], - ["Flaming Skull", 4, 4], - ]; - $motive = $motives[mt_rand(0, count($motives) - 1)]; - $data = [ - "x" => $blockClicked->x, - "y" => $blockClicked->y, - "z" => $blockClicked->z, - "yaw" => $faces[$face] * 90, - "Motive" => $motive[0], - ]; - //TODO - //$e = $server->api->entity->add($level, ENTITY_OBJECT, OBJECT_PAINTING, $data); - //$e->spawnToAll(); - /*if(($player->gamemode & 0x01) === 0x00){ - $player->removeItem(Item::get($this->getId(), $this->getDamage(), 1)); - }*/ - - return true; - } - - return false; - } - -} diff --git a/src/pocketmine/item/PaintingItem.php b/src/pocketmine/item/PaintingItem.php new file mode 100644 index 000000000..c342502a3 --- /dev/null +++ b/src/pocketmine/item/PaintingItem.php @@ -0,0 +1,105 @@ +isTransparent() and $face > 1 and !$blockReplace->isSolid()){ + /** @var PaintingMotive[] $motives */ + $motives = []; + + $totalDimension = 0; + foreach(PaintingMotive::getAll() as $motive){ + $currentTotalDimension = $motive->getHeight() + $motive->getWidth(); + if($currentTotalDimension < $totalDimension){ + continue; + } + + if(Painting::canFit($player->level, $blockReplace, $face, true, $motive)){ + if($currentTotalDimension > $totalDimension){ + $totalDimension = $currentTotalDimension; + /* + * This drops all motive possibilities smaller than this + * We use the total of height + width to allow equal chance of horizontal/vertical paintings + * when there is an L-shape of space available. + */ + $motives = []; + } + + $motives[] = $motive; + } + } + + if(empty($motives)){ //No space available + return false; + } + + /** @var PaintingMotive $motive */ + $motive = $motives[array_rand($motives)]; + + static $directions = [ + Vector3::SIDE_SOUTH => 0, + Vector3::SIDE_WEST => 1, + Vector3::SIDE_NORTH => 2, + Vector3::SIDE_EAST => 3 + ]; + + $direction = $directions[$face] ?? -1; + if($direction === -1){ + return false; + } + + $nbt = Entity::createBaseNBT($blockReplace, null, $direction * 90, 0); + $nbt->setByte("Direction", $direction); + $nbt->setString("Motive", $motive->getName()); + $nbt->setInt("TileX", $blockClicked->getFloorX()); + $nbt->setInt("TileY", $blockClicked->getFloorY()); + $nbt->setInt("TileZ", $blockClicked->getFloorZ()); + + $entity = Entity::createEntity("Painting", $blockReplace->getLevel(), $nbt); + + if($entity instanceof Entity){ + --$this->count; + $entity->spawnToAll(); + + $player->getLevel()->broadcastLevelEvent($blockReplace->add(0.5, 0.5, 0.5), LevelEventPacket::EVENT_SOUND_ITEMFRAME_PLACE); //item frame and painting have the same sound + return true; + } + } + + return false; + } +}