creator function * @phpstan-var array */ private $creationFuncs = []; /** * @var string[] * @phpstan-var array, string> */ 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(Helper::parseLocation($nbt, $world), null, $nbt->getByte(Arrow::TAG_CRIT, 0) === 1, $nbt); }, ['Arrow', 'minecraft:arrow'], LegacyIds::ARROW); $this->register(Egg::class, function(World $world, CompoundTag $nbt) : Egg{ return new Egg(Helper::parseLocation($nbt, $world), null, $nbt); }, ['Egg', 'minecraft:egg'], LegacyIds::EGG); $this->register(EnderPearl::class, function(World $world, CompoundTag $nbt) : EnderPearl{ return new EnderPearl(Helper::parseLocation($nbt, $world), null, $nbt); }, ['ThrownEnderpearl', 'minecraft:ender_pearl'], LegacyIds::ENDER_PEARL); $this->register(ExperienceBottle::class, function(World $world, CompoundTag $nbt) : ExperienceBottle{ return new ExperienceBottle(Helper::parseLocation($nbt, $world), null, $nbt); }, ['ThrownExpBottle', 'minecraft:xp_bottle'], LegacyIds::XP_BOTTLE); $this->register(ExperienceOrb::class, function(World $world, CompoundTag $nbt) : ExperienceOrb{ $value = 1; if(($valuePcTag = $nbt->getTag(ExperienceOrb::TAG_VALUE_PC)) instanceof ShortTag){ //PC $value = $valuePcTag->getValue(); }elseif(($valuePeTag = $nbt->getTag(ExperienceOrb::TAG_VALUE_PE)) instanceof IntTag){ //PE save format $value = $valuePeTag->getValue(); } return new ExperienceOrb(Helper::parseLocation($nbt, $world), $value, $nbt); }, ['XPOrb', 'minecraft:xp_orb'], LegacyIds::XP_ORB); $this->register(FallingBlock::class, function(World $world, CompoundTag $nbt) : FallingBlock{ return new FallingBlock(Helper::parseLocation($nbt, $world), FallingBlock::parseBlockNBT(BlockFactory::getInstance(), $nbt), $nbt); }, ['FallingSand', 'minecraft:falling_block'], LegacyIds::FALLING_BLOCK); $this->register(ItemEntity::class, function(World $world, CompoundTag $nbt) : ItemEntity{ $itemTag = $nbt->getCompoundTag("Item"); if($itemTag === null){ throw new SavedDataLoadingException("Expected \"Item\" NBT tag not found"); } $item = Item::nbtDeserialize($itemTag); if($item->isNull()){ throw new SavedDataLoadingException("Item is invalid"); } return new ItemEntity(Helper::parseLocation($nbt, $world), $item, $nbt); }, ['Item', 'minecraft:item'], LegacyIds::ITEM); $this->register(Painting::class, function(World $world, CompoundTag $nbt) : Painting{ $motive = PaintingMotive::getMotiveByName($nbt->getString("Motive")); if($motive === null){ throw new SavedDataLoadingException("Unknown painting motive"); } $blockIn = new Vector3($nbt->getInt("TileX"), $nbt->getInt("TileY"), $nbt->getInt("TileZ")); if(($directionTag = $nbt->getTag("Direction")) instanceof ByteTag){ $facing = Painting::DATA_TO_FACING[$directionTag->getValue()] ?? Facing::NORTH; }elseif(($facingTag = $nbt->getTag("Facing")) instanceof ByteTag){ $facing = Painting::DATA_TO_FACING[$facingTag->getValue()] ?? Facing::NORTH; }else{ throw new SavedDataLoadingException("Missing facing info"); } return new Painting(Helper::parseLocation($nbt, $world), $blockIn, $facing, $motive, $nbt); }, ['Painting', 'minecraft:painting'], LegacyIds::PAINTING); $this->register(PrimedTNT::class, function(World $world, CompoundTag $nbt) : PrimedTNT{ return new PrimedTNT(Helper::parseLocation($nbt, $world), $nbt); }, ['PrimedTnt', 'PrimedTNT', 'minecraft:tnt'], LegacyIds::TNT); $this->register(Snowball::class, function(World $world, CompoundTag $nbt) : Snowball{ return new Snowball(Helper::parseLocation($nbt, $world), null, $nbt); }, ['Snowball', 'minecraft:snowball'], LegacyIds::SNOWBALL); $this->register(SplashPotion::class, function(World $world, CompoundTag $nbt) : SplashPotion{ $potionType = PotionTypeIdMap::getInstance()->fromId($nbt->getShort("PotionId", PotionTypeIds::WATER)); if($potionType === null){ throw new SavedDataLoadingException("No such potion type"); } return new SplashPotion(Helper::parseLocation($nbt, $world), null, $potionType, $nbt); }, ['ThrownPotion', 'minecraft:potion', 'thrownpotion'], LegacyIds::SPLASH_POTION); $this->register(Squid::class, function(World $world, CompoundTag $nbt) : Squid{ return new Squid(Helper::parseLocation($nbt, $world), $nbt); }, ['Squid', 'minecraft:squid'], LegacyIds::SQUID); $this->register(Villager::class, function(World $world, CompoundTag $nbt) : Villager{ return new Villager(Helper::parseLocation($nbt, $world), $nbt); }, ['Villager', 'minecraft:villager'], LegacyIds::VILLAGER); $this->register(Zombie::class, function(World $world, CompoundTag $nbt) : Zombie{ return new Zombie(Helper::parseLocation($nbt, $world), $nbt); }, ['Zombie', 'minecraft:zombie'], LegacyIds::ZOMBIE); $this->register(Human::class, function(World $world, CompoundTag $nbt) : Human{ return new Human(Helper::parseLocation($nbt, $world), Human::parseSkinNBT($nbt), $nbt); }, ['Human']); } /** * @phpstan-param \Closure(World, CompoundTag) : Entity $creationFunc */ private static function validateCreationFunc(\Closure $creationFunc) : void{ $sig = new CallbackType( new ReturnType(Entity::class), 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 string[] $saveNames An array of save names which this entity might be saved under. * @phpstan-param class-string $className * @phpstan-param list $saveNames * @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. * * @throws \InvalidArgumentException */ public function register(string $className, \Closure $creationFunc, array $saveNames, ?int $legacyMcpeSaveId = null) : void{ if(count($saveNames) === 0){ throw new \InvalidArgumentException("At least one save name must be provided"); } Utils::testValidInstance($className, Entity::class); self::validateCreationFunc($creationFunc); foreach($saveNames as $name){ $this->creationFuncs[$name] = $creationFunc; } if($legacyMcpeSaveId !== null){ $this->creationFuncs[$legacyMcpeSaveId] = $creationFunc; } $this->saveNames[$className] = reset($saveNames); } /** * Creates an entity from data stored on a chunk. * * @throws SavedDataLoadingException * @internal */ public function createFromData(World $world, CompoundTag $nbt) : ?Entity{ try{ $saveId = $nbt->getTag("id") ?? $nbt->getTag("identifier"); $func = null; if($saveId instanceof StringTag){ $func = $this->creationFuncs[$saveId->getValue()] ?? null; }elseif($saveId instanceof IntTag){ //legacy MCPE format $func = $this->creationFuncs[$saveId->getValue() & 0xff] ?? null; } if($func === null){ return null; } /** @var Entity $entity */ $entity = $func($world, $nbt); return $entity; }catch(NbtException $e){ throw new SavedDataLoadingException($e->getMessage(), 0, $e); } } public function injectSaveId(string $class, CompoundTag $saveData) : void{ if(isset($this->saveNames[$class])){ $saveData->setTag("id", new StringTag($this->saveNames[$class])); }else{ throw new \InvalidArgumentException("Entity $class is not registered"); } } /** * @phpstan-param class-string $class */ public function getSaveId(string $class) : string{ if(isset($this->saveNames[$class])){ return $this->saveNames[$class]; } throw new \InvalidArgumentException("Entity $class is not registered"); } }