creator function * @phpstan-var array, \Closure(World, CompoundTag) : Entity> */ private $creationFuncs = []; /** * @var string[] * @phpstan-var array> */ private $knownEntities = []; /** * @var string[][] * @phpstan-var array, list> */ private $saveNames = []; public function __construct(){ //define legacy save IDs first - use them for saving for maximum compatibility with Minecraft PC //TODO: index them by version to allow proper multi-save compatibility $this->register(Arrow::class, function(World $world, CompoundTag $nbt) : Arrow{ return new Arrow(self::parseLocation($nbt, $world), null, false, $nbt); //TODO: missing critical flag }, ['Arrow', 'minecraft:arrow'], EntityLegacyIds::ARROW); $this->register(Egg::class, function(World $world, CompoundTag $nbt) : Egg{ return new Egg(self::parseLocation($nbt, $world), null, $nbt); }, ['Egg', 'minecraft:egg'], EntityLegacyIds::EGG); $this->register(EnderPearl::class, function(World $world, CompoundTag $nbt) : EnderPearl{ return new EnderPearl(self::parseLocation($nbt, $world), null, $nbt); }, ['ThrownEnderpearl', 'minecraft:ender_pearl'], EntityLegacyIds::ENDER_PEARL); $this->register(ExperienceBottle::class, function(World $world, CompoundTag $nbt) : ExperienceBottle{ return new ExperienceBottle(self::parseLocation($nbt, $world), null, $nbt); }, ['ThrownExpBottle', 'minecraft:xp_bottle'], EntityLegacyIds::XP_BOTTLE); $this->register(ExperienceOrb::class, function(World $world, CompoundTag $nbt) : ExperienceOrb{ return new ExperienceOrb(self::parseLocation($nbt, $world), $nbt); }, ['XPOrb', 'minecraft:xp_orb'], EntityLegacyIds::XP_ORB); $this->register(FallingBlock::class, function(World $world, CompoundTag $nbt) : FallingBlock{ return new FallingBlock(self::parseLocation($nbt, $world), FallingBlock::parseBlockNBT(BlockFactory::getInstance(), $nbt), $nbt); }, ['FallingSand', 'minecraft:falling_block'], EntityLegacyIds::FALLING_BLOCK); $this->register(ItemEntity::class, function(World $world, CompoundTag $nbt) : ItemEntity{ $itemTag = $nbt->getCompoundTag("Item"); if($itemTag === null){ throw new \UnexpectedValueException("Expected \"Item\" NBT tag not found"); } $item = Item::nbtDeserialize($itemTag); if($item->isNull()){ throw new \UnexpectedValueException("Item is invalid"); } return new ItemEntity(self::parseLocation($nbt, $world), $item, $nbt); }, ['Item', 'minecraft:item'], EntityLegacyIds::ITEM); $this->register(Painting::class, function(World $world, CompoundTag $nbt) : Painting{ $motive = PaintingMotive::getMotiveByName($nbt->getString("Motive")); if($motive === null){ throw new \UnexpectedValueException("Unknown painting motive"); } $blockIn = new Vector3($nbt->getInt("TileX"), $nbt->getInt("TileY"), $nbt->getInt("TileZ")); if($nbt->hasTag("Direction", ByteTag::class)){ $facing = Painting::DATA_TO_FACING[$nbt->getByte("Direction")] ?? Facing::NORTH; }elseif($nbt->hasTag("Facing", ByteTag::class)){ $facing = Painting::DATA_TO_FACING[$nbt->getByte("Facing")] ?? Facing::NORTH; }else{ throw new \UnexpectedValueException("Missing facing info"); } return new Painting(self::parseLocation($nbt, $world), $blockIn, $facing, $motive, $nbt); }, ['Painting', 'minecraft:painting'], EntityLegacyIds::PAINTING); $this->register(PrimedTNT::class, function(World $world, CompoundTag $nbt) : PrimedTNT{ return new PrimedTNT(self::parseLocation($nbt, $world), $nbt); }, ['PrimedTnt', 'PrimedTNT', 'minecraft:tnt'], EntityLegacyIds::TNT); $this->register(Snowball::class, function(World $world, CompoundTag $nbt) : Snowball{ return new Snowball(self::parseLocation($nbt, $world), null, $nbt); }, ['Snowball', 'minecraft:snowball'], EntityLegacyIds::SNOWBALL); $this->register(SplashPotion::class, function(World $world, CompoundTag $nbt) : SplashPotion{ return new SplashPotion(self::parseLocation($nbt, $world), null, $nbt); }, ['ThrownPotion', 'minecraft:potion', 'thrownpotion'], EntityLegacyIds::SPLASH_POTION); $this->register(Squid::class, function(World $world, CompoundTag $nbt) : Squid{ return new Squid(self::parseLocation($nbt, $world), $nbt); }, ['Squid', 'minecraft:squid'], EntityLegacyIds::SQUID); $this->register(Villager::class, function(World $world, CompoundTag $nbt) : Villager{ return new Villager(self::parseLocation($nbt, $world), $nbt); }, ['Villager', 'minecraft:villager'], EntityLegacyIds::VILLAGER); $this->register(Zombie::class, function(World $world, CompoundTag $nbt) : Zombie{ return new Zombie(self::parseLocation($nbt, $world), $nbt); }, ['Zombie', 'minecraft:zombie'], EntityLegacyIds::ZOMBIE); $this->register(Human::class, function(World $world, CompoundTag $nbt) : Human{ return new Human(self::parseLocation($nbt, $world), Human::parseSkinNBT($nbt), $nbt); }, ['Human']); PaintingMotive::init(); } /** * @phpstan-param class-string $baseClass * @phpstan-param \Closure(World, CompoundTag) : Entity $creationFunc */ private static function validateCreationFunc(string $baseClass, \Closure $creationFunc) : void{ $sig = new CallbackType( new ReturnType($baseClass), new ParameterType("world", World::class), new ParameterType("nbt", CompoundTag::class) ); if(!$sig->isSatisfiedBy($creationFunc)){ throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($creationFunc) . "` must be compatible with `" . $sig . "`"); } } /** * Registers an entity type into the index. * * @param string $className Class that extends Entity * @param \Closure $creationFunc * @param string[] $saveNames An array of save names which this entity might be saved under. Defaults to the short name of the class itself if empty. * @phpstan-param class-string $className * @phpstan-param \Closure(World $world, CompoundTag $nbt) : Entity $creationFunc * * NOTE: The first save name in the $saveNames array will be used when saving the entity to disk. The reflection * name of the class will be appended to the end and only used if no other save names are specified. * * @throws \InvalidArgumentException */ public function register(string $className, \Closure $creationFunc, array $saveNames, ?int $legacyMcpeSaveId = null) : void{ Utils::testValidInstance($className, Entity::class); self::validateCreationFunc($className, $creationFunc); $this->creationFuncs[$className] = $creationFunc; $shortName = (new \ReflectionClass($className))->getShortName(); if(!in_array($shortName, $saveNames, true)){ $saveNames[] = $shortName; } foreach($saveNames as $name){ $this->knownEntities[$name] = $className; } if($legacyMcpeSaveId !== null){ $this->knownEntities[$legacyMcpeSaveId] = $className; } $this->saveNames[$className] = $saveNames; } /** * Creates an entity from data stored on a chunk. * * @throws \RuntimeException * @internal */ public function createFromData(World $world, CompoundTag $nbt) : ?Entity{ $saveId = $nbt->getTag("id") ?? $nbt->getTag("identifier"); $baseClass = null; if($saveId instanceof StringTag){ $baseClass = $this->knownEntities[$saveId->getValue()] ?? null; }elseif($saveId instanceof IntTag){ //legacy MCPE format $baseClass = $this->knownEntities[$saveId->getValue() & 0xff] ?? null; } if($baseClass === null){ return null; } $func = $this->creationFuncs[$baseClass]; /** @var Entity $entity */ $entity = $func($world, $nbt); return $entity; } /** * @phpstan-param class-string $class */ public function getSaveId(string $class) : string{ if(isset($this->saveNames[$class])){ return reset($this->saveNames[$class]); } throw new \InvalidArgumentException("Entity $class is not registered"); } public static function parseLocation(CompoundTag $nbt, World $world) : Location{ $pos = self::parseVec3($nbt, "Pos", false); $yawPitch = $nbt->getTag("Rotation"); if(!($yawPitch instanceof ListTag) or $yawPitch->getTagType() !== NBT::TAG_Float){ throw new \UnexpectedValueException("'Rotation' should be a List"); } $values = $yawPitch->getValue(); if(count($values) !== 2){ throw new \UnexpectedValueException("Expected exactly 2 entries for 'Rotation'"); } return Location::fromObject($pos, $world, $values[0]->getValue(), $values[1]->getValue()); } public static function parseVec3(CompoundTag $nbt, string $tagName, bool $optional) : Vector3{ $pos = $nbt->getTag($tagName); if($pos === null and $optional){ return new Vector3(0, 0, 0); } if(!($pos instanceof ListTag) or $pos->getTagType() !== NBT::TAG_Double){ throw new \UnexpectedValueException("'$tagName' should be a List"); } /** @var DoubleTag[] $values */ $values = $pos->getValue(); if(count($values) !== 3){ throw new \UnexpectedValueException("Expected exactly 3 entries in '$tagName' tag"); } return new Vector3($values[0]->getValue(), $values[1]->getValue(), $values[2]->getValue()); } /** * Helper function which creates minimal NBT needed to spawn an entity. */ public static function createBaseNBT(Vector3 $pos, ?Vector3 $motion = null, float $yaw = 0.0, float $pitch = 0.0) : CompoundTag{ return CompoundTag::create() ->setTag("Pos", new ListTag([ new DoubleTag($pos->x), new DoubleTag($pos->y), new DoubleTag($pos->z) ])) ->setTag("Motion", new ListTag([ new DoubleTag($motion !== null ? $motion->x : 0.0), new DoubleTag($motion !== null ? $motion->y : 0.0), new DoubleTag($motion !== null ? $motion->z : 0.0) ])) ->setTag("Rotation", new ListTag([ new FloatTag($yaw), new FloatTag($pitch) ])); } }