Dylan K. Taylor dec6c9f49b Removed EntityInventoryChangeEvent and EntityArmorChangeEvent
there is nothing that these events do that can't be fulfilled by transactions. They complicate the internal implementation and produce unexpected behaviour for plugins when cancelled.

TL;DR: Use transactions. That's what they are there for.
2019-05-19 16:02:03 +01:00

912 lines
27 KiB
PHP

<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity;
use pocketmine\entity\effect\Effect;
use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\projectile\ProjectileSource;
use pocketmine\entity\utils\ExperienceUtils;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityRegainHealthEvent;
use pocketmine\event\player\PlayerExhaustEvent;
use pocketmine\event\player\PlayerExperienceChangeEvent;
use pocketmine\inventory\EnderChestInventory;
use pocketmine\inventory\InventoryHolder;
use pocketmine\inventory\PlayerInventory;
use pocketmine\item\Consumable;
use pocketmine\item\Durable;
use pocketmine\item\enchantment\Enchantment;
use pocketmine\item\FoodSource;
use pocketmine\item\Item;
use pocketmine\item\Totem;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\network\mcpe\protocol\AddPlayerPacket;
use pocketmine\network\mcpe\protocol\EntityEventPacket;
use pocketmine\network\mcpe\protocol\PlayerListPacket;
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
use pocketmine\network\mcpe\protocol\types\EntityMetadataProperties;
use pocketmine\network\mcpe\protocol\types\EntityMetadataTypes;
use pocketmine\network\mcpe\protocol\types\PlayerListEntry;
use pocketmine\network\mcpe\protocol\types\PlayerMetadataFlags;
use pocketmine\Player;
use pocketmine\utils\UUID;
use pocketmine\world\sound\TotemUseSound;
use pocketmine\world\sound\XpCollectSound;
use pocketmine\world\sound\XpLevelUpSound;
use pocketmine\world\World;
use function array_filter;
use function array_merge;
use function array_rand;
use function array_values;
use function ceil;
use function in_array;
use function max;
use function min;
use function random_int;
use function strlen;
use const INT32_MAX;
use const INT32_MIN;
class Human extends Creature implements ProjectileSource, InventoryHolder{
/** @var PlayerInventory */
protected $inventory;
/** @var EnderChestInventory */
protected $enderChestInventory;
/** @var UUID */
protected $uuid;
protected $rawUUID;
public $width = 0.6;
public $height = 1.8;
public $eyeHeight = 1.62;
/** @var Skin */
protected $skin;
protected $foodTickTimer = 0;
protected $totalXp = 0;
protected $xpSeed;
protected $xpCooldown = 0;
protected $baseOffset = 1.62;
public function __construct(World $world, CompoundTag $nbt){
if($this->skin === null){
$skinTag = $nbt->getCompoundTag("Skin");
if($skinTag === null){
throw new \InvalidStateException((new \ReflectionClass($this))->getShortName() . " must have a valid skin set");
}
$this->skin = self::deserializeSkinNBT($skinTag); //this throws if the skin is invalid
}
parent::__construct($world, $nbt);
}
/**
* @param CompoundTag $skinTag
*
* @return Skin
* @throws \InvalidArgumentException
*/
protected static function deserializeSkinNBT(CompoundTag $skinTag) : Skin{
$skin = new Skin(
$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", "")
);
$skin->validate();
return $skin;
}
/**
* @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{
$skin->validate();
$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() : 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){
$armorListener = $this->armorInventory->getSlotChangeListener();
$this->armorInventory->setSlotChangeListener(null);
/** @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->setSlotChangeListener($armorListener);
}
$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{
$this->skin->validate();
if(!($this instanceof Player)){
/* we don't use Server->updatePlayerListData() because that uses batches, which could cause race conditions in async compression mode */
$pk = new PlayerListPacket();
$pk->type = PlayerListPacket::TYPE_ADD;
$pk->entries = [PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), $this->skin)];
$player->sendDataPacket($pk);
}
$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()]]);
$this->armorInventory->sendContents($player);
if(!($this instanceof Player)){
$pk = new PlayerListPacket();
$pk->type = PlayerListPacket::TYPE_REMOVE;
$pk->entries = [PlayerListEntry::createRemovalEntry($this->uuid)];
$player->sendDataPacket($pk);
}
}
protected function onDispose() : void{
$this->inventory->removeAllViewers(true);
$this->enderChestInventory->removeAllViewers(true);
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);
}
}