82 private const TAG_INVENTORY =
"Inventory";
83 private const TAG_OFF_HAND_ITEM =
"OffHandItem";
84 private const TAG_ENDER_CHEST_INVENTORY =
"EnderChestInventory";
85 private const TAG_SELECTED_INVENTORY_SLOT =
"SelectedInventorySlot";
86 private const TAG_FOOD_LEVEL =
"foodLevel";
87 private const TAG_FOOD_EXHAUSTION_LEVEL =
"foodExhaustionLevel";
88 private const TAG_FOOD_SATURATION_LEVEL =
"foodSaturationLevel";
89 private const TAG_FOOD_TICK_TIMER =
"foodTickTimer";
90 private const TAG_XP_LEVEL =
"XpLevel";
91 private const TAG_XP_PROGRESS =
"XpP";
92 private const TAG_LIFETIME_XP_TOTAL =
"XpTotal";
93 private const TAG_XP_SEED =
"XpSeed";
94 private const TAG_SKIN =
"Skin";
95 private const TAG_SKIN_NAME =
"Name";
96 private const TAG_SKIN_DATA =
"Data";
97 private const TAG_SKIN_CAPE_DATA =
"CapeData";
98 private const TAG_SKIN_GEOMETRY_NAME =
"GeometryName";
99 private const TAG_SKIN_GEOMETRY_DATA =
"GeometryData";
101 public function getNetworkTypeId() :
string{
return EntityIds::PLAYER; }
108 protected UuidInterface $uuid;
110 protected Skin $skin;
115 protected int $xpSeed;
119 parent::__construct($location, $nbt);
129 $skinTag = $nbt->getCompoundTag(self::TAG_SKIN);
130 if($skinTag ===
null){
134 $skinTag->getString(self::TAG_SKIN_NAME),
135 ($skinDataTag = $skinTag->getTag(self::TAG_SKIN_DATA)) instanceof
StringTag ? $skinDataTag->
getValue() : $skinTag->getByteArray(self::TAG_SKIN_DATA),
136 $skinTag->getByteArray(self::TAG_SKIN_CAPE_DATA,
""),
137 $skinTag->getString(self::TAG_SKIN_GEOMETRY_NAME,
""),
138 $skinTag->getByteArray(self::TAG_SKIN_GEOMETRY_DATA,
"")
142 public function getUniqueId() : UuidInterface{
167 public function sendSkin(?array $targets =
null) : void{
175 if($this->isSprinting()){
176 $this->hungerManager->exhaust(0.2, EntityExhaustEvent::CAUSE_SPRINT_JUMPING);
178 $this->hungerManager->exhaust(0.05, EntityExhaustEvent::CAUSE_JUMPING);
182 public function emote(
string $emoteId) : void{
183 NetworkBroadcastUtils::broadcastEntityEvent(
185 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onEmote($recipients, $this, $emoteId)
189 public function getHungerManager() : HungerManager{
190 return $this->hungerManager;
198 return $this->hungerManager->isHungry() || $this->getWorld()->getDifficulty() ===
World::DIFFICULTY_PEACEFUL;
202 if($consumable instanceof
FoodSource && $consumable->requiresHunger() && !$this->canEat()){
206 return parent::consumeObject($consumable);
211 $this->hungerManager->addFood($consumable->getFoodRestore());
212 $this->hungerManager->addSaturation($consumable->getSaturationRestore());
215 parent::applyConsumptionResults($consumable);
218 public function getXpManager() : ExperienceManager{
219 return $this->xpManager;
222 public function getEnchantmentSeed() : int{
223 return $this->xpSeed;
226 public function setEnchantmentSeed(
int $seed) : void{
227 $this->xpSeed = $seed;
230 public function regenerateEnchantmentSeed() : void{
231 $this->xpSeed = EnchantingHelper::generateSeed();
237 return min(100, 7 * $this->xpManager->getXpLevel());
240 public function getHotbar() :
Hotbar{
241 return $this->hotbar;
244 public function getInventory() : Inventory{
245 return $this->inventory;
248 public function getMainHandItem() : Item{
249 return $this->inventory->getItem($this->hotbar->getSelectedIndex());
252 public function setMainHandItem(Item $item) : void{
253 $this->inventory->setItem($this->hotbar->getSelectedIndex(), $item);
256 public function getOffHandItem() : Item{
257 return $this->offHandInventory->getItem(0);
260 public function setOffHandItem(Item $item) : void{
261 $this->offHandInventory->setItem(0, $item);
264 public function getOffHandInventory() : Inventory{ return $this->offHandInventory; }
266 public function getEnderInventory() : Inventory{
267 return $this->enderInventory;
270 public function getSneakOffset() : float{
279 $this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag());
286 private static function populateInventoryFromListTag(
Inventory $inventory, array $items) : void{
287 $listeners = $inventory->getListeners()->toArray();
295 protected function initEntity(
CompoundTag $nbt) : void{
296 parent::initEntity($nbt);
298 $this->hungerManager =
new HungerManager($this);
299 $this->xpManager =
new ExperienceManager($this);
301 $this->inventory =
new SimpleInventory(36);
302 $this->hotbar =
new Hotbar();
304 $syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent(
308 $this->inventory->getListeners()->add(
new CallbackInventoryListener(
309 function(Inventory $unused,
int $slot, Item $unused2) use ($syncHeldItem) :
void{
310 if($slot === $this->hotbar->getSelectedIndex()){
314 function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{
315 if(array_key_exists($this->hotbar->getSelectedIndex(), $oldItems)){
320 $this->offHandInventory =
new SimpleInventory(1);
321 $this->enderInventory =
new SimpleInventory(27);
322 $this->initHumanData($nbt);
324 $inventoryTag = $nbt->
getListTag(self::TAG_INVENTORY, CompoundTag::class);
325 if($inventoryTag !==
null){
326 $inventoryItems = [];
327 $armorInventoryItems = [];
329 foreach($inventoryTag as $i => $item){
330 $slot = $item->getByte(SavedItemStackData::TAG_SLOT);
331 if($slot >= 0 && $slot < 9){
333 }elseif($slot >= 100 && $slot < 104){
334 $armorSlot = $slot - 100;
335 $armorInventoryItems[$armorSlot] = Item::safeNbtDeserialize($item,
"Human armor slot $armorSlot");
336 }elseif($slot >= 9 && $slot < $this->inventory->getSize() + 9){
337 $inventorySlot = $slot - 9;
338 $inventoryItems[$inventorySlot] = Item::safeNbtDeserialize($item,
"Human inventory slot $inventorySlot");
342 self::populateInventoryFromListTag($this->inventory, $inventoryItems);
343 self::populateInventoryFromListTag($this->armorInventory, $armorInventoryItems);
346 if($offHand !==
null){
347 $this->setOffHandItem(Item::safeNbtDeserialize($offHand,
"Human off-hand item"));
349 $this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
351 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobOffHandItemChange($recipients, $this)
354 $enderChestInventoryTag = $nbt->
getListTag(self::TAG_ENDER_CHEST_INVENTORY, CompoundTag::class);
355 if($enderChestInventoryTag !==
null){
356 $enderChestInventoryItems = [];
358 foreach($enderChestInventoryTag as $item){
359 $slot = $item->getByte(SavedItemStackData::TAG_SLOT);
360 $enderChestInventoryItems[$slot] = Item::safeNbtDeserialize($item,
"Human ender chest slot $slot");
362 self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
365 $this->hotbar->setSelectedIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
367 $this->hotbar->getSelectedIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
369 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
372 $this->hungerManager->setFood((
float) $nbt->getInt(self::TAG_FOOD_LEVEL, (
int) $this->hungerManager->getFood()));
373 $this->hungerManager->setExhaustion($nbt->getFloat(self::TAG_FOOD_EXHAUSTION_LEVEL, $this->hungerManager->getExhaustion()));
374 $this->hungerManager->setSaturation($nbt->getFloat(self::TAG_FOOD_SATURATION_LEVEL, $this->hungerManager->getSaturation()));
375 $this->hungerManager->setFoodTickTimer($nbt->getInt(self::TAG_FOOD_TICK_TIMER, $this->hungerManager->getFoodTickTimer()));
377 $this->xpManager->setXpAndProgressNoEvent(
378 $nbt->getInt(self::TAG_XP_LEVEL, 0),
379 $nbt->getFloat(self::TAG_XP_PROGRESS, 0.0));
380 $this->xpManager->setLifetimeTotalXp($nbt->getInt(self::TAG_LIFETIME_XP_TOTAL, 0));
382 if(($xpSeedTag = $nbt->
getTag(self::TAG_XP_SEED)) instanceof IntTag){
383 $this->xpSeed = $xpSeedTag->getValue();
385 $this->xpSeed = EnchantingHelper::generateSeed();
389 protected function entityBaseTick(
int $tickDiff = 1) : bool{
390 $hasUpdate = parent::entityBaseTick($tickDiff);
392 $this->hungerManager->tick($tickDiff);
393 $this->xpManager->tick($tickDiff);
398 public function getName() : string{
399 return $this->getNameTag();
403 parent::applyDamageModifiers($source);
405 $type = $source->getCause();
406 if($type !== EntityDamageEvent::CAUSE_SUICIDE && $type !== EntityDamageEvent::CAUSE_VOID
407 && ($this->getMainHandItem() instanceof
Totem || $this->getOffHandItem() instanceof
Totem)){
409 $compensation = $this->getHealth() - $source->getFinalDamage() - 1;
410 if($compensation <= -1){
411 $source->setModifier($compensation, EntityDamageEvent::MODIFIER_TOTEM);
417 parent::applyPostDamageEffects($source);
418 $totemModifier = $source->getModifier(EntityDamageEvent::MODIFIER_TOTEM);
419 if($totemModifier < 0){
420 $this->effectManager->clear();
422 $this->effectManager->add(
new EffectInstance(VanillaEffects::REGENERATION(), 40 * 20, 1));
423 $this->effectManager->add(
new EffectInstance(VanillaEffects::FIRE_RESISTANCE(), 40 * 20, 1));
424 $this->effectManager->add(
new EffectInstance(VanillaEffects::ABSORPTION(), 5 * 20, 1));
429 $hand = $this->getMainHandItem();
430 if($hand instanceof
Totem){
432 $this->setMainHandItem($hand);
433 }elseif(($offHand = $this->getOffHandItem()) instanceof
Totem){
435 $this->setOffHandItem($offHand);
441 return array_filter(array_merge(
442 array_values($this->inventory->getContents()),
443 array_values($this->armorInventory->getContents()),
444 array_values($this->offHandInventory->getContents()),
445 ), function(
Item $item) : bool{ return !$item->hasEnchantment(
VanillaEnchantments::VANISHING()) && !$item->keepOnDeath(); });
449 $nbt = parent::saveNBT();
451 $nbt->
setInt(self::TAG_FOOD_LEVEL, (
int) $this->hungerManager->getFood());
452 $nbt->
setFloat(self::TAG_FOOD_EXHAUSTION_LEVEL, $this->hungerManager->getExhaustion());
453 $nbt->
setFloat(self::TAG_FOOD_SATURATION_LEVEL, $this->hungerManager->getSaturation());
454 $nbt->
setInt(self::TAG_FOOD_TICK_TIMER, $this->hungerManager->getFoodTickTimer());
456 $nbt->
setInt(self::TAG_XP_LEVEL, $this->xpManager->getXpLevel());
457 $nbt->
setFloat(self::TAG_XP_PROGRESS, $this->xpManager->getXpProgress());
458 $nbt->
setInt(self::TAG_LIFETIME_XP_TOTAL, $this->xpManager->getLifetimeTotalXp());
459 $nbt->
setInt(self::TAG_XP_SEED, $this->xpSeed);
461 $inventoryTag =
new ListTag([], NBT::TAG_Compound);
462 $nbt->
setTag(self::TAG_INVENTORY, $inventoryTag);
465 $slotCount = $this->inventory->getSize() + $this->hotbar->getSize();
466 for($slot = $this->hotbar->getSize(); $slot < $slotCount; ++$slot){
467 $item = $this->inventory->getItem($slot - 9);
468 if(!$item->isNull()){
469 $inventoryTag->push($item->nbtSerialize($slot));
474 for($slot = 100; $slot < 104; ++$slot){
475 $item = $this->armorInventory->getItem($slot - 100);
476 if(!$item->isNull()){
477 $inventoryTag->push($item->nbtSerialize($slot));
481 $nbt->
setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->hotbar->getSelectedIndex());
483 $offHandItem = $this->getOffHandItem();
484 if(!$offHandItem->isNull()){
485 $nbt->setTag(self::TAG_OFF_HAND_ITEM, $offHandItem->nbtSerialize());
491 $slotCount = $this->enderInventory->getSize();
492 for($slot = 0; $slot < $slotCount; ++$slot){
493 $item = $this->enderInventory->getItem($slot);
494 if(!$item->isNull()){
495 $items[] = $item->nbtSerialize($slot);
499 $nbt->
setTag(self::TAG_ENDER_CHEST_INVENTORY,
new ListTag($items, NBT::TAG_Compound));
501 $nbt->
setTag(self::TAG_SKIN, CompoundTag::create()
502 ->setString(self::TAG_SKIN_NAME, $this->skin->getSkinId())
503 ->setByteArray(self::TAG_SKIN_DATA, $this->skin->getSkinData())
504 ->setByteArray(self::TAG_SKIN_CAPE_DATA, $this->skin->getCapeData())
505 ->setString(self::TAG_SKIN_GEOMETRY_NAME, $this->skin->getGeometryName())
506 ->setByteArray(self::TAG_SKIN_GEOMETRY_DATA, $this->skin->getGeometryData())
512 public function spawnTo(Player $player) : void{
513 if($player !== $this){
514 parent::spawnTo($player);
519 $networkSession = $player->getNetworkSession();
520 $typeConverter = $networkSession->getTypeConverter();
521 if(!($this instanceof
Player)){
522 $networkSession->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($this->uuid, $this->
id, $this->getName(), $typeConverter->getSkinAdapter()->toSkinData($this->skin))]));
525 $networkSession->sendDataPacket(AddPlayerPacket::create(
526 $this->getUniqueId(),
530 $this->location->asVector3(),
532 $this->location->pitch,
533 $this->location->yaw,
534 $this->location->yaw,
535 ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getMainHandItem())),
537 $this->getAllNetworkData(),
539 UpdateAbilitiesPacket::create(
new AbilitiesData(CommandPermissions::NORMAL, PlayerPermissions::VISITOR, $this->getId() , [
541 AbilitiesLayer::LAYER_BASE,
542 array_fill(0, AbilitiesLayer::NUMBER_OF_ABILITIES,
false),
554 $this->sendData([$player], [EntityMetadataProperties::NAMETAG =>
new StringMetadataProperty($this->getNameTag())]);
556 $entityEventBroadcaster = $networkSession->getEntityEventBroadcaster();
557 $entityEventBroadcaster->onMobArmorChange([$networkSession], $this);
558 $entityEventBroadcaster->onMobOffHandItemChange([$networkSession], $this);
560 if(!($this instanceof
Player)){
561 $networkSession->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)]));
565 public function getOffsetPosition(Vector3 $vector3) : Vector3{
566 return $vector3->add(0, 1.621, 0);
570 $this->hotbar->getSelectedIndexChangeListeners()->clear();
571 $this->inventory->removeAllWindows();
572 $this->offHandInventory->removeAllWindows();
573 $this->enderInventory->removeAllWindows();
579 $this->hungerManager,
582 parent::destroyCycles();