skin === null and (!isset($nbt->Skin) or !isset($nbt->Skin->Data) or !Player::isValidSkin($nbt->Skin->Data->getValue()))){ throw new \InvalidStateException((new \ReflectionClass($this))->getShortName() . " must have a valid skin set"); } parent::__construct($level, $nbt); } /** * 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 strlen($skin) === 64 * 64 * 4 or strlen($skin) === 64 * 32 * 4; } /** * @return UUID|null */ public function getUniqueId(){ 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{ if(!$skin->isValid()){ throw new \InvalidStateException("Specified skin is not valid, must be 8KiB or 16KiB"); } $this->skin = $skin; $this->skin->debloatGeometryData(); } /** * 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(){ 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){ $attr = $this->attributeMap->getAttribute(Attribute::HUNGER); $old = $attr->getValue(); $attr->setValue($new); $reset = false; // 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)){ $reset = true; break; } } if($reset){ $this->foodTickTimer = 0; } } public function getMaxFood() : float{ return $this->attributeMap->getAttribute(Attribute::HUNGER)->getMaxValue(); } public function addFood(float $amount){ $attr = $this->attributeMap->getAttribute(Attribute::HUNGER); $amount += $attr->getValue(); $amount = max(min($amount, $attr->getMaxValue()), $attr->getMinValue()); $this->setFood($amount); } 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){ $this->attributeMap->getAttribute(Attribute::SATURATION)->setValue($saturation); } public function addSaturation(float $amount){ $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){ $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{ $this->server->getPluginManager()->callEvent($ev = new PlayerExhaustEvent($this, $amount, $cause)); 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($food); } } } $this->setExhaustion($exhaustion); return $ev->getAmount(); } public function getXpLevel() : int{ return (int) $this->attributeMap->getAttribute(Attribute::EXPERIENCE_LEVEL)->getValue(); } public function setXpLevel(int $level){ $this->attributeMap->getAttribute(Attribute::EXPERIENCE_LEVEL)->setValue($level); } public function getXpProgress() : float{ return $this->attributeMap->getAttribute(Attribute::EXPERIENCE)->getValue(); } public function setXpProgress(float $progress){ $this->attributeMap->getAttribute(Attribute::EXPERIENCE)->setValue($progress); } public function getTotalXp() : int{ return $this->totalXp; } public function getRemainderXp() : int{ return $this->getTotalXp() - self::getTotalXpForLevel($this->getXpLevel()); } public function recalculateXpProgress() : float{ $this->setXpProgress($progress = $this->getRemainderXp() / self::getTotalXpForLevel($this->getXpLevel())); return $progress; } public static function getTotalXpForLevel(int $level) : int{ if($level <= 16){ return $level ** 2 + $level * 6; }elseif($level < 32){ return $level ** 2 * 2.5 - 40.5 * $level + 360; } return $level ** 2 * 4.5 - 162.5 * $level + 2220; } public function getInventory(){ return $this->inventory; } /** * For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT. */ protected function initHumanData(){ if(isset($this->namedtag->NameTag)){ $this->setNameTag($this->namedtag["NameTag"]); } if(isset($this->namedtag->Skin) and $this->namedtag->Skin instanceof CompoundTag){ $this->setSkin(new Skin( $this->namedtag->Skin["Name"], $this->namedtag->Skin["Data"] )); } $this->uuid = UUID::fromData((string) $this->getId(), $this->skin->getSkinData(), $this->getNameTag()); } protected function initEntity(){ $this->setPlayerFlag(self::DATA_PLAYER_FLAG_SLEEP, false); $this->setDataProperty(self::DATA_PLAYER_BED_POSITION, self::DATA_TYPE_POS, [0, 0, 0], false); $this->inventory = new PlayerInventory($this); $this->initHumanData(); if(isset($this->namedtag->Inventory) and $this->namedtag->Inventory instanceof ListTag){ foreach($this->namedtag->Inventory as $i => $item){ if($item["Slot"] >= 0 and $item["Slot"] < 9){ //Hotbar //Old hotbar saving stuff, remove it (useless now) unset($this->namedtag->Inventory->{$i}); }elseif($item["Slot"] >= 100 and $item["Slot"] < 104){ //Armor $this->inventory->setItem($this->inventory->getSize() + $item["Slot"] - 100, ItemItem::nbtDeserialize($item)); }else{ $this->inventory->setItem($item["Slot"] - 9, ItemItem::nbtDeserialize($item)); } } } if(isset($this->namedtag->SelectedInventorySlot) and $this->namedtag->SelectedInventorySlot instanceof IntTag){ $this->inventory->setHeldItemIndex($this->namedtag->SelectedInventorySlot->getValue(), false); }else{ $this->inventory->setHeldItemIndex(0, false); } parent::initEntity(); if(!isset($this->namedtag->foodLevel) or !($this->namedtag->foodLevel instanceof IntTag)){ $this->namedtag->foodLevel = new IntTag("foodLevel", (int) $this->getFood()); }else{ $this->setFood((float) $this->namedtag["foodLevel"]); } if(!isset($this->namedtag->foodExhaustionLevel) or !($this->namedtag->foodExhaustionLevel instanceof FloatTag)){ $this->namedtag->foodExhaustionLevel = new FloatTag("foodExhaustionLevel", $this->getExhaustion()); }else{ $this->setExhaustion((float) $this->namedtag["foodExhaustionLevel"]); } if(!isset($this->namedtag->foodSaturationLevel) or !($this->namedtag->foodSaturationLevel instanceof FloatTag)){ $this->namedtag->foodSaturationLevel = new FloatTag("foodSaturationLevel", $this->getSaturation()); }else{ $this->setSaturation((float) $this->namedtag["foodSaturationLevel"]); } if(!isset($this->namedtag->foodTickTimer) or !($this->namedtag->foodTickTimer instanceof IntTag)){ $this->namedtag->foodTickTimer = new IntTag("foodTickTimer", $this->foodTickTimer); }else{ $this->foodTickTimer = $this->namedtag["foodTickTimer"]; } if(!isset($this->namedtag->XpLevel) or !($this->namedtag->XpLevel instanceof IntTag)){ $this->namedtag->XpLevel = new IntTag("XpLevel", $this->getXpLevel()); }else{ $this->setXpLevel((int) $this->namedtag["XpLevel"]); } if(!isset($this->namedtag->XpP) or !($this->namedtag->XpP instanceof FloatTag)){ $this->namedtag->XpP = new FloatTag("XpP", $this->getXpProgress()); } if(!isset($this->namedtag->XpTotal) or !($this->namedtag->XpTotal instanceof IntTag)){ $this->namedtag->XpTotal = new IntTag("XpTotal", $this->totalXp); }else{ $this->totalXp = $this->namedtag["XpTotal"]; } if(!isset($this->namedtag->XpSeed) or !($this->namedtag->XpSeed instanceof IntTag)){ $this->namedtag->XpSeed = new IntTag("XpSeed", $this->xpSeed ?? ($this->xpSeed = mt_rand(-0x80000000, 0x7fffffff))); }else{ $this->xpSeed = $this->namedtag["XpSeed"]; } } protected function addAttributes(){ 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)); } public function entityBaseTick(int $tickDiff = 1) : bool{ $hasUpdate = parent::entityBaseTick($tickDiff); $this->doFoodTick($tickDiff); return $hasUpdate; } public function doFoodTick(int $tickDiff = 1){ if($this->isAlive()){ $food = $this->getFood(); $health = $this->getHealth(); $difficulty = $this->level->getDifficulty(); $this->foodTickTimer += $tickDiff; if($this->foodTickTimer >= 80){ $this->foodTickTimer = 0; } if($difficulty === Level::DIFFICULTY_PEACEFUL and $this->foodTickTimer % 10 === 0){ if($food < 20){ $this->addFood(1.0); } 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 === 1 and $health > 10) or ($difficulty === 2 and $health > 1) or $difficulty === 3){ $this->attack(new EntityDamageEvent($this, EntityDamageEvent::CAUSE_STARVATION, 1)); } } } if($food <= 6){ if($this->isSprinting()){ $this->setSprinting(false); } } } } protected function doAirSupplyTick(int $tickDiff){ //TODO: allow this to apply to other mobs if(($ench = $this->inventory->getHelmet()->getEnchantment(Enchantment::RESPIRATION)) === null or lcg_value() <= (1 / ($ench->getLevel() + 1))){ parent::doAirSupplyTick($tickDiff); } } public function getName() : string{ return $this->getNameTag(); } public function getDrops() : array{ return $this->inventory !== null ? array_values($this->inventory->getContents()) : []; } public function saveNBT(){ parent::saveNBT(); $this->namedtag->foodLevel = new IntTag("foodLevel", (int) $this->getFood()); $this->namedtag->foodExhaustionLevel = new FloatTag("foodExhaustionLevel", $this->getExhaustion()); $this->namedtag->foodSaturationLevel = new FloatTag("foodSaturationLevel", $this->getSaturation()); $this->namedtag->foodTickTimer = new IntTag("foodTickTimer", $this->foodTickTimer); $this->namedtag->Inventory = new ListTag("Inventory", [], NBT::TAG_Compound); 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()){ $this->namedtag->Inventory[$slot] = $item->nbtSerialize($slot); } } //Armor for($slot = 100; $slot < 104; ++$slot){ $item = $this->inventory->getItem($this->inventory->getSize() + $slot - 100); if(!$item->isNull()){ $this->namedtag->Inventory[$slot] = $item->nbtSerialize($slot); } } $this->namedtag->SelectedInventorySlot = new IntTag("SelectedInventorySlot", $this->inventory->getHeldItemIndex()); } if($this->skin !== null){ $this->namedtag->Skin = new CompoundTag("Skin", [ //TODO: save cape & geometry new StringTag("Data", $this->skin->getSkinData()), new StringTag("Name", $this->skin->getSkinId()) ]); } } public function spawnTo(Player $player){ if($player !== $this){ parent::spawnTo($player); } } protected function sendSpawnPacket(Player $player) : void{ if(!$this->skin->isValid()){ throw new \InvalidStateException((new \ReflectionClass($this))->getShortName() . " must have a valid skin set"); } $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->dataProperties; $player->dataPacket($pk); $this->inventory->sendArmorContents($player); if(!($this instanceof Player)){ $this->sendSkin([$player]); } } public function close(){ if(!$this->closed){ if($this->inventory !== null){ $this->inventory->removeAllViewers(true); $this->inventory = null; } parent::close(); } } /** * 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(self::DATA_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){ $this->setDataFlag(self::DATA_PLAYER_FLAGS, $flagId, $value, self::DATA_TYPE_BYTE); } }