diff --git a/src/block/MonsterSpawner.php b/src/block/MonsterSpawner.php index 5cbe80e0a81..6d603fc916f 100644 --- a/src/block/MonsterSpawner.php +++ b/src/block/MonsterSpawner.php @@ -23,12 +23,102 @@ namespace pocketmine\block; +use pocketmine\block\tile\MonsterSpawner as TileSpawner; +use pocketmine\block\tile\SpawnerSpawnRangeRegistry; use pocketmine\block\utils\SupportType; +use pocketmine\entity\Entity; +use pocketmine\entity\EntityFactory; +use pocketmine\entity\Location; +use pocketmine\event\block\SpawnerAttemptSpawnEvent; use pocketmine\item\Item; +use pocketmine\item\SpawnEgg; +use pocketmine\item\SpawnEggEntityRegistry; +use pocketmine\math\AxisAlignedBB; +use pocketmine\math\Vector3; +use pocketmine\nbt\tag\CompoundTag; +use pocketmine\nbt\tag\DoubleTag; +use pocketmine\nbt\tag\FloatTag; +use pocketmine\nbt\tag\ListTag; +use pocketmine\player\Player; +use pocketmine\world\particle\MobSpawnParticle; +use function assert; use function mt_rand; class MonsterSpawner extends Transparent{ + /** TODO: replace this with a cached entity or something of that nature */ + private string $entityTypeId = TileSpawner::DEFAULT_ENTITY_TYPE_ID; + /** TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) */ + private ?ListTag $spawnPotentials = null; + /** TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) */ + private ?CompoundTag $spawnData = null; + + private float $displayEntityWidth = 1.0; + private float $displayEntityHeight = 1.0; + private float $displayEntityScale = 1.0; + + private int $spawnDelay = TileSpawner::DEFAULT_MIN_SPAWN_DELAY; + private int $minSpawnDelay = TileSpawner::DEFAULT_MIN_SPAWN_DELAY; + private int $maxSpawnDelay = TileSpawner::DEFAULT_MAX_SPAWN_DELAY; + private int $spawnPerAttempt = 1; + private int $maxNearbyEntities = TileSpawner::DEFAULT_MAX_NEARBY_ENTITIES; + private int $spawnRange = TileSpawner::DEFAULT_SPAWN_RANGE; + private int $requiredPlayerRange = TileSpawner::DEFAULT_REQUIRED_PLAYER_RANGE; + + public function readStateFromWorld() : Block{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileSpawner){ + $this->entityTypeId = $tile->getEntityTypeId(); + $this->spawnPotentials = $tile->getSpawnPotentials(); + $this->spawnData = $tile->getSpawnData(); + $this->displayEntityWidth = $tile->getDisplayEntityWidth(); + $this->displayEntityHeight = $tile->getDisplayEntityHeight(); + $this->displayEntityScale = $tile->getDisplayEntityScale(); + $this->spawnDelay = $tile->getSpawnDelay(); + $this->minSpawnDelay = $tile->getMinSpawnDelay(); + $this->maxSpawnDelay = $tile->getMaxSpawnDelay(); + $this->spawnPerAttempt = $tile->getSpawnPerAttempt(); + $this->maxNearbyEntities = $tile->getMaxNearbyEntities(); + $this->spawnRange = $tile->getSpawnRange(); + $this->requiredPlayerRange = $tile->getRequiredPlayerRange(); + }else{ + $this->entityTypeId = TileSpawner::DEFAULT_ENTITY_TYPE_ID; + $this->spawnPotentials = null; + $this->spawnData = null; + $this->displayEntityWidth = 1.0; + $this->displayEntityHeight = 1.0; + $this->displayEntityScale = 1.0; + $this->spawnDelay = TileSpawner::DEFAULT_MIN_SPAWN_DELAY; + $this->minSpawnDelay = TileSpawner::DEFAULT_MIN_SPAWN_DELAY; + $this->maxSpawnDelay = TileSpawner::DEFAULT_MAX_SPAWN_DELAY; + $this->spawnPerAttempt = 1; + $this->maxNearbyEntities = TileSpawner::DEFAULT_MAX_NEARBY_ENTITIES; + $this->spawnRange = TileSpawner::DEFAULT_SPAWN_RANGE; + $this->requiredPlayerRange = TileSpawner::DEFAULT_REQUIRED_PLAYER_RANGE; + } + return $this; + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + assert($tile instanceof TileSpawner); + $tile->setEntityTypeId($this->entityTypeId); + $tile->setSpawnPotentials($this->spawnPotentials); + $tile->setSpawnData($this->spawnData); + $tile->setDisplayEntityWidth($this->displayEntityWidth); + $tile->setDisplayEntityHeight($this->displayEntityHeight); + $tile->setDisplayEntityScale($this->displayEntityScale); + $tile->setSpawnDelay($this->spawnDelay); + $tile->setMinSpawnDelay($this->minSpawnDelay); + $tile->setMaxSpawnDelay($this->maxSpawnDelay); + $tile->setSpawnPerAttempt($this->spawnPerAttempt); + $tile->setMaxNearbyEntities($this->maxNearbyEntities); + $tile->setSpawnRange($this->spawnRange); + $tile->setRequiredPlayerRange($this->requiredPlayerRange); + } + public function getDropsForCompatibleTool(Item $item) : array{ return []; } @@ -38,10 +128,199 @@ protected function getXpDropAmount() : int{ } public function onScheduledUpdate() : void{ - //TODO + if($this->onUpdate()){ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); + } } public function getSupportType(int $facing) : SupportType{ return SupportType::NONE; } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + if($item instanceof SpawnEgg){ + $entityId = SpawnEggEntityRegistry::getInstance()->getEntityId($item); + if($entityId === null){ + return false; + } + $spawner = $this->position->getWorld()->getTile($this->position); + if($spawner instanceof TileSpawner){ + $spawner->setEntityTypeId($entityId); + $this->position->getWorld()->setBlock($this->position, $this); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); + return true; + } + } + return parent::onInteract($item, $face, $clickVector, $player, $returnedItems); + } + + public function onUpdate() : bool{ + $world = $this->position->getWorld(); + $spawnerTile = $world->getTile($this->position); + + if(!$spawnerTile instanceof TileSpawner || $spawnerTile->closed || $this->entityTypeId === TileSpawner::DEFAULT_ENTITY_TYPE_ID){ + return false; + } + if($this->spawnDelay > 0){ + $this->spawnDelay--; + return true; + } + $position = $this->getPosition(); + $world = $position->getWorld(); + if($world->getNearestEntity($position, $this->requiredPlayerRange, Player::class) === null){ + return true; + } + $count = 0; + $spawnRangeBB = SpawnerSpawnRangeRegistry::getInstance()->getSpawnRange($this->entityTypeId) ?? AxisAlignedBB::one()->expand($this->spawnRange * 2 + 1, 8, $this->spawnRange * 2 + 1); + $spawnRangeBB->offset($position->x, $position->y, $position->z); + foreach($world->getNearbyEntities($spawnRangeBB) as $entity){ + if($entity::getNetworkTypeId() === $this->entityTypeId){ + $count++; + if($count >= $this->maxNearbyEntities){ + return true; + } + } + } + $entityTypeId = $this->entityTypeId; + if(SpawnerAttemptSpawnEvent::hasHandlers()){ + $ev = new SpawnerAttemptSpawnEvent($this, $entityTypeId); + $ev->call(); + if($ev->isCancelled()){ + return true; + } + $entityTypeId = $ev->getEntityType(); + } + // TODO: spawn condition check (light level etc.) + for($i = 0; $i < $this->spawnPerAttempt; $i++){ + $spawnLocation = $position->add(mt_rand(-$this->spawnRange, $this->spawnRange), 0, mt_rand(-$this->spawnRange, $this->spawnRange)); + $spawnLocation = Location::fromObject($spawnLocation, $world); + $nbt = CompoundTag::create() + ->setString(EntityFactory::TAG_IDENTIFIER, $entityTypeId) + ->setTag(Entity::TAG_POS, new ListTag([ + new DoubleTag($spawnLocation->x), + new DoubleTag($spawnLocation->y), + new DoubleTag($spawnLocation->z) + ])) + ->setTag(Entity::TAG_ROTATION, new ListTag([ + new FloatTag($spawnLocation->yaw), + new FloatTag($spawnLocation->pitch) + ])); + // TODO: spawnData, spawnPotentials + $entity = EntityFactory::getInstance()->createFromData($world, $nbt); + if($entity !== null){ + $entity->spawnToAll(); + $world->addParticle($spawnLocation, new MobSpawnParticle((int) $entity->getSize()->getWidth(), (int) $entity->getSize()->getHeight())); + $count++; + if($count >= $this->maxNearbyEntities){ + break; + } + } + } + $this->spawnDelay = mt_rand($this->minSpawnDelay, $this->maxSpawnDelay); + return true; + } + + public function getEntityTypeId() : string{ + return $this->entityTypeId; + } + + public function setEntityTypeId(string $entityTypeId) : void{ + $this->entityTypeId = $entityTypeId; + } + + public function getSpawnPotentials() : ?ListTag{ + return $this->spawnPotentials; + } + + public function setSpawnPotentials(?ListTag $spawnPotentials) : void{ + $this->spawnPotentials = $spawnPotentials; + } + + public function getSpawnData() : ?CompoundTag{ + return $this->spawnData; + } + + public function setSpawnData(?CompoundTag $spawnData) : void{ + $this->spawnData = $spawnData; + } + + public function getDisplayEntityHeight() : float{ + return $this->displayEntityHeight; + } + + public function setDisplayEntityHeight(float $displayEntityHeight) : void{ + $this->displayEntityHeight = $displayEntityHeight; + } + + public function getDisplayEntityWidth() : float{ + return $this->displayEntityWidth; + } + + public function setDisplayEntityWidth(float $displayEntityWidth) : void{ + $this->displayEntityWidth = $displayEntityWidth; + } + + public function getDisplayEntityScale() : float{ + return $this->displayEntityScale; + } + + public function setDisplayEntityScale(float $displayEntityScale) : void{ + $this->displayEntityScale = $displayEntityScale; + } + + public function getSpawnDelay() : int{ + return $this->spawnDelay; + } + + public function setSpawnDelay(int $spawnDelay) : void{ + $this->spawnDelay = $spawnDelay; + } + + public function getMinSpawnDelay() : int{ + return $this->minSpawnDelay; + } + + public function setMinSpawnDelay(int $minSpawnDelay) : void{ + $this->minSpawnDelay = $minSpawnDelay; + } + + public function getMaxSpawnDelay() : int{ + return $this->maxSpawnDelay; + } + + public function setMaxSpawnDelay(int $maxSpawnDelay) : void{ + $this->maxSpawnDelay = $maxSpawnDelay; + } + + public function getRequiredPlayerRange() : int{ + return $this->requiredPlayerRange; + } + + public function setRequiredPlayerRange(int $requiredPlayerRange) : void{ + $this->requiredPlayerRange = $requiredPlayerRange; + } + + public function getSpawnRange() : int{ + return $this->spawnRange; + } + + public function setSpawnRange(int $spawnRange) : void{ + $this->spawnRange = $spawnRange; + } + + public function getSpawnPerAttempt() : int{ + return $this->spawnPerAttempt; + } + + public function setSpawnPerAttempt(int $spawnPerAttempt) : void{ + $this->spawnPerAttempt = $spawnPerAttempt; + } + + public function getMaxNearbyEntities() : int{ + return $this->maxNearbyEntities; + } + + public function setMaxNearbyEntities(int $maxNearbyEntities) : void{ + $this->maxNearbyEntities = $maxNearbyEntities; + } } diff --git a/src/block/tile/MonsterSpawner.php b/src/block/tile/MonsterSpawner.php index b09953567c0..69cc97bc86f 100644 --- a/src/block/tile/MonsterSpawner.php +++ b/src/block/tile/MonsterSpawner.php @@ -56,8 +56,11 @@ class MonsterSpawner extends Spawnable{ public const DEFAULT_SPAWN_RANGE = 4; //blocks public const DEFAULT_REQUIRED_PLAYER_RANGE = 16; + public const DEFAULT_ENTITY_TYPE_ID = ""; + public const DEFAULT_LEGACY_ENTITY_TYPE_ID = ":"; + /** TODO: replace this with a cached entity or something of that nature */ - private string $entityTypeId = ":"; + private string $entityTypeId = self::DEFAULT_ENTITY_TYPE_ID; /** TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) */ private ?ListTag $spawnPotentials = null; /** TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) */ @@ -78,11 +81,14 @@ class MonsterSpawner extends Spawnable{ public function readSaveData(CompoundTag $nbt) : void{ if(($legacyIdTag = $nbt->getTag(self::TAG_LEGACY_ENTITY_TYPE_ID)) instanceof IntTag){ //TODO: this will cause unexpected results when there's no mapping for the entity - $this->entityTypeId = LegacyEntityIdToStringIdMap::getInstance()->legacyToString($legacyIdTag->getValue()) ?? ":"; + $this->entityTypeId = LegacyEntityIdToStringIdMap::getInstance()->legacyToString($legacyIdTag->getValue()) ?? self::DEFAULT_ENTITY_TYPE_ID; }elseif(($idTag = $nbt->getTag(self::TAG_ENTITY_TYPE_ID)) instanceof StringTag){ $this->entityTypeId = $idTag->getValue(); + if($this->entityTypeId === self::DEFAULT_LEGACY_ENTITY_TYPE_ID){ + $this->entityTypeId = self::DEFAULT_ENTITY_TYPE_ID; + } }else{ - $this->entityTypeId = ":"; //default - TODO: replace this with a constant + $this->entityTypeId = self::DEFAULT_ENTITY_TYPE_ID; } $this->spawnData = $nbt->getCompoundTag(self::TAG_SPAWN_DATA); @@ -130,4 +136,108 @@ protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ $nbt->setFloat(self::TAG_ENTITY_SCALE, $this->displayEntityScale); } + + public function getEntityTypeId() : string{ + return $this->entityTypeId; + } + + public function setEntityTypeId(string $entityTypeId) : void{ + $this->entityTypeId = $entityTypeId; + } + + public function getSpawnPotentials() : ?ListTag{ + return $this->spawnPotentials; + } + + public function setSpawnPotentials(?ListTag $spawnPotentials) : void{ + $this->spawnPotentials = $spawnPotentials; + } + + public function getSpawnData() : ?CompoundTag{ + return $this->spawnData; + } + + public function setSpawnData(?CompoundTag $spawnData) : void{ + $this->spawnData = $spawnData; + } + + public function getDisplayEntityHeight() : float{ + return $this->displayEntityHeight; + } + + public function setDisplayEntityHeight(float $displayEntityHeight) : void{ + $this->displayEntityHeight = $displayEntityHeight; + } + + public function getDisplayEntityWidth() : float{ + return $this->displayEntityWidth; + } + + public function setDisplayEntityWidth(float $displayEntityWidth) : void{ + $this->displayEntityWidth = $displayEntityWidth; + } + + public function getDisplayEntityScale() : float{ + return $this->displayEntityScale; + } + + public function setDisplayEntityScale(float $displayEntityScale) : void{ + $this->displayEntityScale = $displayEntityScale; + } + + public function getSpawnDelay() : int{ + return $this->spawnDelay; + } + + public function setSpawnDelay(int $spawnDelay) : void{ + $this->spawnDelay = $spawnDelay; + } + + public function getMinSpawnDelay() : int{ + return $this->minSpawnDelay; + } + + public function setMinSpawnDelay(int $minSpawnDelay) : void{ + $this->minSpawnDelay = $minSpawnDelay; + } + + public function getMaxSpawnDelay() : int{ + return $this->maxSpawnDelay; + } + + public function setMaxSpawnDelay(int $maxSpawnDelay) : void{ + $this->maxSpawnDelay = $maxSpawnDelay; + } + + public function getRequiredPlayerRange() : int{ + return $this->requiredPlayerRange; + } + + public function setRequiredPlayerRange(int $requiredPlayerRange) : void{ + $this->requiredPlayerRange = $requiredPlayerRange; + } + + public function getSpawnRange() : int{ + return $this->spawnRange; + } + + public function setSpawnRange(int $spawnRange) : void{ + $this->spawnRange = $spawnRange; + } + + public function getSpawnPerAttempt() : int{ + return $this->spawnPerAttempt; + } + + public function setSpawnPerAttempt(int $spawnPerAttempt) : void{ + $this->spawnPerAttempt = $spawnPerAttempt; + } + + public function getMaxNearbyEntities() : int{ + return $this->maxNearbyEntities; + } + + public function setMaxNearbyEntities(int $maxNearbyEntities) : void{ + $this->maxNearbyEntities = $maxNearbyEntities; + } } diff --git a/src/block/tile/SpawnerSpawnRangeRegistry.php b/src/block/tile/SpawnerSpawnRangeRegistry.php new file mode 100644 index 00000000000..16ab017bc2a --- /dev/null +++ b/src/block/tile/SpawnerSpawnRangeRegistry.php @@ -0,0 +1,49 @@ + + * @var AxisAlignedBB[] + */ + private array $spawnRange = []; + + public function register(string $entitySaveId, AxisAlignedBB $spawnRange) : void{ + if(isset($this->spawnRange[$entitySaveId])){ + throw new \InvalidArgumentException("Spawn range for entity $entitySaveId is already registered"); + } + $this->spawnRange[$entitySaveId] = $spawnRange; + } + + public function getSpawnRange(string $entitySaveId) : ?AxisAlignedBB{ + $spawnRange = $this->spawnRange[$entitySaveId] ?? null; + return $spawnRange === null ? null : clone $spawnRange; + } +} diff --git a/src/event/block/SpawnerAttemptSpawnEvent.php b/src/event/block/SpawnerAttemptSpawnEvent.php new file mode 100644 index 00000000000..9d0e0fffff7 --- /dev/null +++ b/src/event/block/SpawnerAttemptSpawnEvent.php @@ -0,0 +1,44 @@ +entityType; + } + + public function setEntityType(string $entityType) : void{ + $this->entityType = $entityType; + } +} diff --git a/src/item/SpawnEgg.php b/src/item/SpawnEgg.php index 51dcceebd2a..47e2d846194 100644 --- a/src/item/SpawnEgg.php +++ b/src/item/SpawnEgg.php @@ -24,6 +24,7 @@ namespace pocketmine\item; use pocketmine\block\Block; +use pocketmine\block\MonsterSpawner; use pocketmine\entity\Entity; use pocketmine\math\Vector3; use pocketmine\player\Player; @@ -35,6 +36,9 @@ abstract class SpawnEgg extends Item{ abstract protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity; public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, array &$returnedItems) : ItemUseResult{ + if($blockClicked instanceof MonsterSpawner){ + return ItemUseResult::NONE; + } $entity = $this->createEntity($player->getWorld(), $blockReplace->getPosition()->add(0.5, 0, 0.5), lcg_value() * 360, 0); if($this->hasCustomName()){ diff --git a/src/item/SpawnEggEntityRegistry.php b/src/item/SpawnEggEntityRegistry.php new file mode 100644 index 00000000000..1a8fabb9b2f --- /dev/null +++ b/src/item/SpawnEggEntityRegistry.php @@ -0,0 +1,51 @@ + + * @var string[] + */ + private array $entityMap = []; + + private function __construct(){ + $this->register(EntityIds::SQUID, VanillaItems::SQUID_SPAWN_EGG()); + $this->register(EntityIds::VILLAGER, VanillaItems::VILLAGER_SPAWN_EGG()); + $this->register(EntityIds::ZOMBIE, VanillaItems::ZOMBIE_SPAWN_EGG()); + } + + public function register(string $entitySaveId, SpawnEgg $spawnEgg) : void{ + $this->entityMap[$spawnEgg->getTypeId()] = $entitySaveId; + } + + public function getEntityId(SpawnEgg $item) : ?string{ + return $this->entityMap[$item->getTypeId()] ?? null; + } +}