skin === null){ $skinTag = $nbt->getCompoundTag("Skin"); if($skinTag === null){ throw new \InvalidStateException((new \ReflectionClass($this))->getShortName() . " must have a valid skin set"); } $this->skin = new Skin( //this throws if the skin is invalid $skinTag->getString("Name"), $skinTag->hasTag("Data", StringTag::class) ? $skinTag->getString("Data") : $skinTag->getByteArray("Data"), //old data (this used to be saved as a StringTag in older versions of PM) $skinTag->getByteArray("CapeData", ""), $skinTag->getString("GeometryName", ""), $skinTag->getByteArray("GeometryData", "") ); } parent::__construct($world, $nbt); } /** * @deprecated * * Checks the length of a supplied skin bitmap and returns whether the length is valid. * * @param string $skin * * @return bool */ public static function isValidSkin(string $skin) : bool{ return in_array(strlen($skin), Skin::ACCEPTED_SKIN_SIZES, true); } /** * @return UUID */ public function getUniqueId() : UUID{ return $this->uuid; } /** * @return string */ public function getRawUniqueId() : string{ return $this->rawUUID; } /** * Returns a Skin object containing information about this human's skin. * @return Skin */ public function getSkin() : Skin{ return $this->skin; } /** * Sets the human's skin. This will not send any update to viewers, you need to do that manually using * {@link sendSkin}. * * @param Skin $skin */ public function setSkin(Skin $skin) : void{ $this->skin = $skin; } /** * Sends the human's skin to the specified list of players. If null is given for targets, the skin will be sent to * all viewers. * * @param Player[]|null $targets */ public function sendSkin(?array $targets = null) : void{ $pk = new PlayerSkinPacket(); $pk->uuid = $this->getUniqueId(); $pk->skin = $this->skin; $this->server->broadcastPacket($targets ?? $this->hasSpawned, $pk); } public function jump() : void{ parent::jump(); if($this->isSprinting()){ $this->exhaust(0.8, PlayerExhaustEvent::CAUSE_SPRINT_JUMPING); }else{ $this->exhaust(0.2, PlayerExhaustEvent::CAUSE_JUMPING); } } public function getFood() : float{ return $this->attributeMap->getAttribute(Attribute::HUNGER)->getValue(); } /** * WARNING: This method does not check if full and may throw an exception if out of bounds. * Use {@link Human::addFood()} for this purpose * * @param float $new * * @throws \InvalidArgumentException */ public function setFood(float $new) : void{ $attr = $this->attributeMap->getAttribute(Attribute::HUNGER); $old = $attr->getValue(); $attr->setValue($new); // ranges: 18-20 (regen), 7-17 (none), 1-6 (no sprint), 0 (health depletion) foreach([17, 6, 0] as $bound){ if(($old > $bound) !== ($new > $bound)){ $this->foodTickTimer = 0; break; } } } public function getMaxFood() : float{ return $this->attributeMap->getAttribute(Attribute::HUNGER)->getMaxValue(); } public function addFood(float $amount) : void{ $attr = $this->attributeMap->getAttribute(Attribute::HUNGER); $amount += $attr->getValue(); $amount = max(min($amount, $attr->getMaxValue()), $attr->getMinValue()); $this->setFood($amount); } /** * Returns whether this Human may consume objects requiring hunger. * * @return bool */ public function isHungry() : bool{ return $this->getFood() < $this->getMaxFood(); } public function getSaturation() : float{ return $this->attributeMap->getAttribute(Attribute::SATURATION)->getValue(); } /** * WARNING: This method does not check if saturated and may throw an exception if out of bounds. * Use {@link Human::addSaturation()} for this purpose * * @param float $saturation * * @throws \InvalidArgumentException */ public function setSaturation(float $saturation) : void{ $this->attributeMap->getAttribute(Attribute::SATURATION)->setValue($saturation); } public function addSaturation(float $amount) : void{ $attr = $this->attributeMap->getAttribute(Attribute::SATURATION); $attr->setValue($attr->getValue() + $amount, true); } public function getExhaustion() : float{ return $this->attributeMap->getAttribute(Attribute::EXHAUSTION)->getValue(); } /** * WARNING: This method does not check if exhausted and does not consume saturation/food. * Use {@link Human::exhaust()} for this purpose. * * @param float $exhaustion */ public function setExhaustion(float $exhaustion) : void{ $this->attributeMap->getAttribute(Attribute::EXHAUSTION)->setValue($exhaustion); } /** * Increases a human's exhaustion level. * * @param float $amount * @param int $cause * * @return float the amount of exhaustion level increased */ public function exhaust(float $amount, int $cause = PlayerExhaustEvent::CAUSE_CUSTOM) : float{ $ev = new PlayerExhaustEvent($this, $amount, $cause); $ev->call(); if($ev->isCancelled()){ return 0.0; } $exhaustion = $this->getExhaustion(); $exhaustion += $ev->getAmount(); while($exhaustion >= 4.0){ $exhaustion -= 4.0; $saturation = $this->getSaturation(); if($saturation > 0){ $saturation = max(0, $saturation - 1.0); $this->setSaturation($saturation); }else{ $food = $this->getFood(); if($food > 0){ $food--; $this->setFood(max($food, 0)); } } } $this->setExhaustion($exhaustion); return $ev->getAmount(); } public function consumeObject(Consumable $consumable) : bool{ if($consumable instanceof FoodSource){ if($consumable->requiresHunger() and !$this->isHungry()){ return false; } $this->addFood($consumable->getFoodRestore()); $this->addSaturation($consumable->getSaturationRestore()); } return parent::consumeObject($consumable); } /** * Returns the player's experience level. * @return int */ public function getXpLevel() : int{ return (int) $this->attributeMap->getAttribute(Attribute::EXPERIENCE_LEVEL)->getValue(); } /** * Sets the player's experience level. This does not affect their total XP or their XP progress. * * @param int $level * * @return bool */ public function setXpLevel(int $level) : bool{ return $this->setXpAndProgress($level, null); } /** * Adds a number of XP levels to the player. * * @param int $amount * @param bool $playSound * * @return bool */ public function addXpLevels(int $amount, bool $playSound = true) : bool{ $oldLevel = $this->getXpLevel(); if($this->setXpLevel($oldLevel + $amount)){ if($playSound){ $newLevel = $this->getXpLevel(); if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){ $this->world->addSound($this, new XpLevelUpSound($newLevel)); } } return true; } return false; } /** * Subtracts a number of XP levels from the player. * * @param int $amount * * @return bool */ public function subtractXpLevels(int $amount) : bool{ return $this->addXpLevels(-$amount); } /** * Returns a value between 0.0 and 1.0 to indicate how far through the current level the player is. * @return float */ public function getXpProgress() : float{ return $this->attributeMap->getAttribute(Attribute::EXPERIENCE)->getValue(); } /** * Sets the player's progress through the current level to a value between 0.0 and 1.0. * * @param float $progress * * @return bool */ public function setXpProgress(float $progress) : bool{ return $this->setXpAndProgress(null, $progress); } /** * Returns the number of XP points the player has progressed into their current level. * @return int */ public function getRemainderXp() : int{ return (int) (ExperienceUtils::getXpToCompleteLevel($this->getXpLevel()) * $this->getXpProgress()); } /** * Returns the amount of XP points the player currently has, calculated from their current level and progress * through their current level. This will be reduced by enchanting deducting levels and is used to calculate the * amount of XP the player drops on death. * * @return int */ public function getCurrentTotalXp() : int{ return ExperienceUtils::getXpToReachLevel($this->getXpLevel()) + $this->getRemainderXp(); } /** * Sets the current total of XP the player has, recalculating their XP level and progress. * Note that this DOES NOT update the player's lifetime total XP. * * @param int $amount * * @return bool */ public function setCurrentTotalXp(int $amount) : bool{ $newLevel = ExperienceUtils::getLevelFromXp($amount); return $this->setXpAndProgress((int) $newLevel, $newLevel - ((int) $newLevel)); } /** * Adds an amount of XP to the player, recalculating their XP level and progress. XP amount will be added to the * player's lifetime XP. * * @param int $amount * @param bool $playSound Whether to play level-up and XP gained sounds. * * @return bool */ public function addXp(int $amount, bool $playSound = true) : bool{ $this->totalXp += $amount; $oldLevel = $this->getXpLevel(); $oldTotal = $this->getCurrentTotalXp(); if($this->setCurrentTotalXp($oldTotal + $amount)){ if($playSound){ $newLevel = $this->getXpLevel(); if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){ $this->world->addSound($this, new XpLevelUpSound($newLevel)); }elseif($this->getCurrentTotalXp() > $oldTotal){ $this->world->addSound($this, new XpCollectSound()); } } return true; } return false; } /** * Takes an amount of XP from the player, recalculating their XP level and progress. * * @param int $amount * * @return bool */ public function subtractXp(int $amount) : bool{ return $this->addXp(-$amount); } protected function setXpAndProgress(?int $level, ?float $progress) : bool{ if(!$this->justCreated){ $ev = new PlayerExperienceChangeEvent($this, $this->getXpLevel(), $this->getXpProgress(), $level, $progress); $ev->call(); if($ev->isCancelled()){ return false; } $level = $ev->getNewLevel(); $progress = $ev->getNewProgress(); } if($level !== null){ $this->getAttributeMap()->getAttribute(Attribute::EXPERIENCE_LEVEL)->setValue($level); } if($progress !== null){ $this->getAttributeMap()->getAttribute(Attribute::EXPERIENCE)->setValue($progress); } return true; } /** * Returns the total XP the player has collected in their lifetime. Resets when the player dies. * XP levels being removed in enchanting do not reduce this number. * * @return int */ public function getLifetimeTotalXp() : int{ return $this->totalXp; } /** * Sets the lifetime total XP of the player. This does not recalculate their level or progress. Used for player * score when they die. (TODO: add this when MCPE supports it) * * @param int $amount */ public function setLifetimeTotalXp(int $amount) : void{ if($amount < 0){ throw new \InvalidArgumentException("XP must be greater than 0"); } $this->totalXp = $amount; } /** * Returns whether the human can pickup XP orbs (checks cooldown time) * @return bool */ public function canPickupXp() : bool{ return $this->xpCooldown === 0; } public function onPickupXp(int $xpValue) : void{ static $mainHandIndex = -1; //TODO: replace this with a more generic equipment getting/setting interface /** @var Durable[] $equipment */ $equipment = []; if(($item = $this->inventory->getItemInHand()) instanceof Durable and $item->hasEnchantment(Enchantment::MENDING())){ $equipment[$mainHandIndex] = $item; } //TODO: check offhand foreach($this->armorInventory->getContents() as $k => $item){ if($item instanceof Durable and $item->hasEnchantment(Enchantment::MENDING())){ $equipment[$k] = $item; } } if(!empty($equipment)){ $repairItem = $equipment[$k = array_rand($equipment)]; if($repairItem->getDamage() > 0){ $repairAmount = min($repairItem->getDamage(), $xpValue * 2); $repairItem->setDamage($repairItem->getDamage() - $repairAmount); $xpValue -= (int) ceil($repairAmount / 2); if($k === $mainHandIndex){ $this->inventory->setItemInHand($repairItem); }else{ $this->armorInventory->setItem($k, $repairItem); } } } $this->addXp($xpValue); //this will still get fired even if the value is 0 due to mending, to play sounds $this->resetXpCooldown(); } /** * Sets the duration in ticks until the human can pick up another XP orb. * * @param int $value */ public function resetXpCooldown(int $value = 2) : void{ $this->xpCooldown = $value; } public function getXpDropAmount() : int{ //this causes some XP to be lost on death when above level 1 (by design), dropping at most enough points for //about 7.5 levels of XP. return (int) min(100, 7 * $this->getXpLevel()); } public function getInventory(){ return $this->inventory; } public function getEnderChestInventory() : EnderChestInventory{ return $this->enderChestInventory; } /** * For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT. * * @param CompoundTag $nbt */ protected function initHumanData(CompoundTag $nbt) : void{ if($nbt->hasTag("NameTag", StringTag::class)){ $this->setNameTag($nbt->getString("NameTag")); } $this->uuid = UUID::fromData((string) $this->getId(), $this->skin->getSkinData(), $this->getNameTag()); } protected function initEntity(CompoundTag $nbt) : void{ parent::initEntity($nbt); $this->setPlayerFlag(PlayerMetadataFlags::SLEEP, false); $this->propertyManager->setBlockPos(EntityMetadataProperties::PLAYER_BED_POSITION, null); $this->inventory = new PlayerInventory($this); $this->enderChestInventory = new EnderChestInventory(); $this->initHumanData($nbt); $inventoryTag = $nbt->getListTag("Inventory"); if($inventoryTag !== null){ $armorListeners = $this->armorInventory->getChangeListeners(); $this->armorInventory->removeChangeListeners(...$armorListeners); /** @var CompoundTag $item */ foreach($inventoryTag as $i => $item){ $slot = $item->getByte("Slot"); if($slot >= 0 and $slot < 9){ //Hotbar //Old hotbar saving stuff, ignore it }elseif($slot >= 100 and $slot < 104){ //Armor $this->armorInventory->setItem($slot - 100, Item::nbtDeserialize($item)); }else{ $this->inventory->setItem($slot - 9, Item::nbtDeserialize($item)); } } $this->armorInventory->addChangeListeners(...$armorListeners); } $enderChestInventoryTag = $nbt->getListTag("EnderChestInventory"); if($enderChestInventoryTag !== null){ /** @var CompoundTag $item */ foreach($enderChestInventoryTag as $i => $item){ $this->enderChestInventory->setItem($item->getByte("Slot"), Item::nbtDeserialize($item)); } } $this->inventory->setHeldItemIndex($nbt->getInt("SelectedInventorySlot", 0), false); $this->setFood((float) $nbt->getInt("foodLevel", (int) $this->getFood(), true)); $this->setExhaustion($nbt->getFloat("foodExhaustionLevel", $this->getExhaustion(), true)); $this->setSaturation($nbt->getFloat("foodSaturationLevel", $this->getSaturation(), true)); $this->foodTickTimer = $nbt->getInt("foodTickTimer", $this->foodTickTimer, true); $this->setXpLevel($nbt->getInt("XpLevel", $this->getXpLevel(), true)); $this->setXpProgress($nbt->getFloat("XpP", $this->getXpProgress(), true)); $this->totalXp = $nbt->getInt("XpTotal", $this->totalXp, true); if($nbt->hasTag("XpSeed", IntTag::class)){ $this->xpSeed = $nbt->getInt("XpSeed"); }else{ $this->xpSeed = random_int(INT32_MIN, INT32_MAX); } } protected function addAttributes() : void{ parent::addAttributes(); $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::SATURATION)); $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXHAUSTION)); $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::HUNGER)); $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXPERIENCE_LEVEL)); $this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXPERIENCE)); } protected function entityBaseTick(int $tickDiff = 1) : bool{ $hasUpdate = parent::entityBaseTick($tickDiff); $this->doFoodTick($tickDiff); if($this->xpCooldown > 0){ $this->xpCooldown--; } return $hasUpdate; } protected function doFoodTick(int $tickDiff = 1) : void{ if($this->isAlive()){ $food = $this->getFood(); $health = $this->getHealth(); $difficulty = $this->world->getDifficulty(); $this->foodTickTimer += $tickDiff; if($this->foodTickTimer >= 80){ $this->foodTickTimer = 0; } if($difficulty === World::DIFFICULTY_PEACEFUL and $this->foodTickTimer % 10 === 0){ if($food < $this->getMaxFood()){ $this->addFood(1.0); $food = $this->getFood(); } if($this->foodTickTimer % 20 === 0 and $health < $this->getMaxHealth()){ $this->heal(new EntityRegainHealthEvent($this, 1, EntityRegainHealthEvent::CAUSE_SATURATION)); } } if($this->foodTickTimer === 0){ if($food >= 18){ if($health < $this->getMaxHealth()){ $this->heal(new EntityRegainHealthEvent($this, 1, EntityRegainHealthEvent::CAUSE_SATURATION)); $this->exhaust(3.0, PlayerExhaustEvent::CAUSE_HEALTH_REGEN); } }elseif($food <= 0){ if(($difficulty === World::DIFFICULTY_EASY and $health > 10) or ($difficulty === World::DIFFICULTY_NORMAL and $health > 1) or $difficulty === World::DIFFICULTY_HARD){ $this->attack(new EntityDamageEvent($this, EntityDamageEvent::CAUSE_STARVATION, 1)); } } } if($food <= 6){ $this->setSprinting(false); } } } public function getName() : string{ return $this->getNameTag(); } public function applyDamageModifiers(EntityDamageEvent $source) : void{ parent::applyDamageModifiers($source); $type = $source->getCause(); if($type !== EntityDamageEvent::CAUSE_SUICIDE and $type !== EntityDamageEvent::CAUSE_VOID and $this->inventory->getItemInHand() instanceof Totem){ //TODO: check offhand as well (when it's implemented) $compensation = $this->getHealth() - $source->getFinalDamage() - 1; if($compensation < 0){ $source->setModifier($compensation, EntityDamageEvent::MODIFIER_TOTEM); } } } protected function applyPostDamageEffects(EntityDamageEvent $source) : void{ parent::applyPostDamageEffects($source); $totemModifier = $source->getModifier(EntityDamageEvent::MODIFIER_TOTEM); if($totemModifier < 0){ //Totem prevented death $this->removeAllEffects(); $this->addEffect(new EffectInstance(Effect::REGENERATION(), 40 * 20, 1)); $this->addEffect(new EffectInstance(Effect::FIRE_RESISTANCE(), 40 * 20, 1)); $this->addEffect(new EffectInstance(Effect::ABSORPTION(), 5 * 20, 1)); $this->broadcastEntityEvent(EntityEventPacket::CONSUME_TOTEM); $this->world->addSound($this->add(0, $this->eyeHeight, 0), new TotemUseSound()); $hand = $this->inventory->getItemInHand(); if($hand instanceof Totem){ $hand->pop(); //Plugins could alter max stack size $this->inventory->setItemInHand($hand); } } } public function getDrops() : array{ return array_filter(array_merge( $this->inventory !== null ? array_values($this->inventory->getContents()) : [], $this->armorInventory !== null ? array_values($this->armorInventory->getContents()) : [] ), function(Item $item) : bool{ return !$item->hasEnchantment(Enchantment::VANISHING()); }); } public function saveNBT() : CompoundTag{ $nbt = parent::saveNBT(); $nbt->setInt("foodLevel", (int) $this->getFood()); $nbt->setFloat("foodExhaustionLevel", $this->getExhaustion()); $nbt->setFloat("foodSaturationLevel", $this->getSaturation()); $nbt->setInt("foodTickTimer", $this->foodTickTimer); $nbt->setInt("XpLevel", $this->getXpLevel()); $nbt->setFloat("XpP", $this->getXpProgress()); $nbt->setInt("XpTotal", $this->totalXp); $nbt->setInt("XpSeed", $this->xpSeed); $inventoryTag = new ListTag([], NBT::TAG_Compound); $nbt->setTag("Inventory", $inventoryTag); if($this->inventory !== null){ //Normal inventory $slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize(); for($slot = $this->inventory->getHotbarSize(); $slot < $slotCount; ++$slot){ $item = $this->inventory->getItem($slot - 9); if(!$item->isNull()){ $inventoryTag->push($item->nbtSerialize($slot)); } } //Armor for($slot = 100; $slot < 104; ++$slot){ $item = $this->armorInventory->getItem($slot - 100); if(!$item->isNull()){ $inventoryTag->push($item->nbtSerialize($slot)); } } $nbt->setInt("SelectedInventorySlot", $this->inventory->getHeldItemIndex()); } if($this->enderChestInventory !== null){ /** @var CompoundTag[] $items */ $items = []; $slotCount = $this->enderChestInventory->getSize(); for($slot = 0; $slot < $slotCount; ++$slot){ $item = $this->enderChestInventory->getItem($slot); if(!$item->isNull()){ $items[] = $item->nbtSerialize($slot); } } $nbt->setTag("EnderChestInventory", new ListTag($items, NBT::TAG_Compound)); } if($this->skin !== null){ $nbt->setTag("Skin", CompoundTag::create() ->setString("Name", $this->skin->getSkinId()) ->setByteArray("Data", $this->skin->getSkinData()) ->setByteArray("CapeData", $this->skin->getCapeData()) ->setString("GeometryName", $this->skin->getGeometryName()) ->setByteArray("GeometryData", $this->skin->getGeometryData()) ); } return $nbt; } public function spawnTo(Player $player) : void{ if($player !== $this){ parent::spawnTo($player); } } protected function sendSpawnPacket(Player $player) : void{ if(!($this instanceof Player)){ /* we don't use Server->updatePlayerListData() because that uses batches, which could cause race conditions in async compression mode */ $player->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), $this->skin)])); } $pk = new AddPlayerPacket(); $pk->uuid = $this->getUniqueId(); $pk->username = $this->getName(); $pk->entityRuntimeId = $this->getId(); $pk->position = $this->asVector3(); $pk->motion = $this->getMotion(); $pk->yaw = $this->yaw; $pk->pitch = $this->pitch; $pk->item = $this->getInventory()->getItemInHand(); $pk->metadata = $this->propertyManager->getAll(); $player->sendDataPacket($pk); //TODO: Hack for MCPE 1.2.13: DATA_NAMETAG is useless in AddPlayerPacket, so it has to be sent separately $this->sendData($player, [EntityMetadataProperties::NAMETAG => [EntityMetadataTypes::STRING, $this->getNameTag()]]); $player->getNetworkSession()->onMobArmorChange($this); if(!($this instanceof Player)){ $player->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)])); } } protected function onDispose() : void{ $this->inventory->removeAllViewers(); $this->enderChestInventory->removeAllViewers(); parent::onDispose(); } protected function destroyCycles() : void{ $this->inventory = null; $this->enderChestInventory = null; parent::destroyCycles(); } /** * Wrapper around {@link Entity#getDataFlag} for player-specific data flag reading. * * @param int $flagId * * @return bool */ public function getPlayerFlag(int $flagId) : bool{ return $this->getDataFlag(EntityMetadataProperties::PLAYER_FLAGS, $flagId); } /** * Wrapper around {@link Entity#setDataFlag} for player-specific data flag setting. * * @param int $flagId * @param bool $value */ public function setPlayerFlag(int $flagId, bool $value = true) : void{ $this->setDataFlag(EntityMetadataProperties::PLAYER_FLAGS, $flagId, $value, EntityMetadataTypes::BYTE); } }