PocketMine-MP 5.35.1 git-e32e836dad793a3a3c8ddd8927c00e112b1e576a
Loading...
Searching...
No Matches
Human.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\entity;
25
71use Ramsey\Uuid\Uuid;
72use Ramsey\Uuid\UuidInterface;
73use function array_fill;
74use function array_filter;
75use function array_key_exists;
76use function array_merge;
77use function array_values;
78use function min;
79
80class Human extends Living implements ProjectileSource, InventoryHolder{
81
82 private const TAG_INVENTORY = "Inventory"; //TAG_List<TAG_Compound>
83 private const TAG_OFF_HAND_ITEM = "OffHandItem"; //TAG_Compound
84 private const TAG_ENDER_CHEST_INVENTORY = "EnderChestInventory"; //TAG_List<TAG_Compound>
85 private const TAG_SELECTED_INVENTORY_SLOT = "SelectedInventorySlot"; //TAG_Int
86 private const TAG_FOOD_LEVEL = "foodLevel"; //TAG_Int
87 private const TAG_FOOD_EXHAUSTION_LEVEL = "foodExhaustionLevel"; //TAG_Float
88 private const TAG_FOOD_SATURATION_LEVEL = "foodSaturationLevel"; //TAG_Float
89 private const TAG_FOOD_TICK_TIMER = "foodTickTimer"; //TAG_Int
90 private const TAG_XP_LEVEL = "XpLevel"; //TAG_Int
91 private const TAG_XP_PROGRESS = "XpP"; //TAG_Float
92 private const TAG_LIFETIME_XP_TOTAL = "XpTotal"; //TAG_Int
93 private const TAG_XP_SEED = "XpSeed"; //TAG_Int
94 private const TAG_SKIN = "Skin"; //TAG_Compound
95 private const TAG_SKIN_NAME = "Name"; //TAG_String
96 private const TAG_SKIN_DATA = "Data"; //TAG_ByteArray
97 private const TAG_SKIN_CAPE_DATA = "CapeData"; //TAG_ByteArray
98 private const TAG_SKIN_GEOMETRY_NAME = "GeometryName"; //TAG_String
99 private const TAG_SKIN_GEOMETRY_DATA = "GeometryData"; //TAG_ByteArray
100
101 public function getNetworkTypeId() : string{ return EntityIds::PLAYER; }
102
103 protected Hotbar $hotbar;
104 protected Inventory $inventory;
105 protected Inventory $offHandInventory;
106 protected Inventory $enderInventory;
107
108 protected UuidInterface $uuid;
109
110 protected Skin $skin;
111
112 protected HungerManager $hungerManager;
113 protected ExperienceManager $xpManager;
114
115 protected int $xpSeed;
116
117 public function __construct(Location $location, Skin $skin, ?CompoundTag $nbt = null){
118 $this->skin = $skin;
119 parent::__construct($location, $nbt);
120 }
121
122 protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(1.8, 0.6, 1.62); }
123
128 public static function parseSkinNBT(CompoundTag $nbt) : Skin{
129 $skinTag = $nbt->getCompoundTag(self::TAG_SKIN);
130 if($skinTag === null){
131 throw new SavedDataLoadingException("Missing skin data");
132 }
133 return new Skin( //this throws if the skin is invalid
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), //old data (this used to be saved as a StringTag in older versions of PM)
136 $skinTag->getByteArray(self::TAG_SKIN_CAPE_DATA, ""),
137 $skinTag->getString(self::TAG_SKIN_GEOMETRY_NAME, ""),
138 $skinTag->getByteArray(self::TAG_SKIN_GEOMETRY_DATA, "")
139 );
140 }
141
142 public function getUniqueId() : UuidInterface{
143 return $this->uuid;
144 }
145
149 public function getSkin() : Skin{
150 return $this->skin;
151 }
152
157 public function setSkin(Skin $skin) : void{
158 $this->skin = $skin;
159 }
160
167 public function sendSkin(?array $targets = null) : void{
168 NetworkBroadcastUtils::broadcastPackets($targets ?? $this->hasSpawned, [
169 PlayerSkinPacket::create($this->getUniqueId(), "", "", TypeConverter::getInstance()->getSkinAdapter()->toSkinData($this->skin))
170 ]);
171 }
172
173 public function jump() : void{
174 parent::jump();
175 if($this->isSprinting()){
176 $this->hungerManager->exhaust(0.2, EntityExhaustEvent::CAUSE_SPRINT_JUMPING);
177 }else{
178 $this->hungerManager->exhaust(0.05, EntityExhaustEvent::CAUSE_JUMPING);
179 }
180 }
181
182 public function emote(string $emoteId) : void{
183 NetworkBroadcastUtils::broadcastEntityEvent(
184 $this->getViewers(),
185 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onEmote($recipients, $this, $emoteId)
186 );
187 }
188
189 public function getHungerManager() : HungerManager{
190 return $this->hungerManager;
191 }
192
197 public function canEat() : bool{
198 return $this->hungerManager->isHungry() || $this->getWorld()->getDifficulty() === World::DIFFICULTY_PEACEFUL;
199 }
200
201 public function consumeObject(Consumable $consumable) : bool{
202 if($consumable instanceof FoodSource && $consumable->requiresHunger() && !$this->canEat()){
203 return false;
204 }
205
206 return parent::consumeObject($consumable);
207 }
208
209 protected function applyConsumptionResults(Consumable $consumable) : void{
210 if($consumable instanceof FoodSource){
211 $this->hungerManager->addFood($consumable->getFoodRestore());
212 $this->hungerManager->addSaturation($consumable->getSaturationRestore());
213 }
214
215 parent::applyConsumptionResults($consumable);
216 }
217
218 public function getXpManager() : ExperienceManager{
219 return $this->xpManager;
220 }
221
222 public function getEnchantmentSeed() : int{
223 return $this->xpSeed;
224 }
225
226 public function setEnchantmentSeed(int $seed) : void{
227 $this->xpSeed = $seed;
228 }
229
230 public function regenerateEnchantmentSeed() : void{
231 $this->xpSeed = EnchantingHelper::generateSeed();
232 }
233
234 public function getXpDropAmount() : int{
235 //this causes some XP to be lost on death when above level 1 (by design), dropping at most enough points for
236 //about 7.5 levels of XP.
237 return min(100, 7 * $this->xpManager->getXpLevel());
238 }
239
240 public function getHotbar() : Hotbar{
241 return $this->hotbar;
242 }
243
244 public function getInventory() : Inventory{
245 return $this->inventory;
246 }
247
248 public function getMainHandItem() : Item{
249 return $this->inventory->getItem($this->hotbar->getSelectedIndex());
250 }
251
252 public function setMainHandItem(Item $item) : void{
253 $this->inventory->setItem($this->hotbar->getSelectedIndex(), $item);
254 }
255
256 public function getOffHandItem() : Item{
257 return $this->offHandInventory->getItem(0);
258 }
259
260 public function setOffHandItem(Item $item) : void{
261 $this->offHandInventory->setItem(0, $item);
262 }
263
264 public function getOffHandInventory() : Inventory{ return $this->offHandInventory; }
265
266 public function getEnderInventory() : Inventory{
267 return $this->enderInventory;
268 }
269
270 public function getSneakOffset() : float{
271 return 0.31;
272 }
273
277 protected function initHumanData(CompoundTag $nbt) : void{
278 //TODO: use of NIL UUID for namespace is a hack; we should provide a proper UUID for the namespace
279 $this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag());
280 }
281
286 private static function populateInventoryFromListTag(Inventory $inventory, array $items) : void{
287 $listeners = $inventory->getListeners()->toArray();
288 $inventory->getListeners()->clear();
289
290 $inventory->setContents($items);
291
292 $inventory->getListeners()->add(...$listeners);
293 }
294
295 protected function initEntity(CompoundTag $nbt) : void{
296 parent::initEntity($nbt);
297
298 $this->hungerManager = new HungerManager($this);
299 $this->xpManager = new ExperienceManager($this);
300
301 $this->inventory = new SimpleInventory(36);
302 $this->hotbar = new Hotbar();
303
304 $syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent(
305 $this->getViewers(),
306 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
307 );
308 $this->inventory->getListeners()->add(new CallbackInventoryListener(
309 function(Inventory $unused, int $slot, Item $unused2) use ($syncHeldItem) : void{
310 if($slot === $this->hotbar->getSelectedIndex()){
311 $syncHeldItem();
312 }
313 },
314 function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{
315 if(array_key_exists($this->hotbar->getSelectedIndex(), $oldItems)){
316 $syncHeldItem();
317 }
318 }
319 ));
320 $this->offHandInventory = new SimpleInventory(1);
321 $this->enderInventory = new SimpleInventory(27);
322 $this->initHumanData($nbt);
323
324 $inventoryTag = $nbt->getListTag(self::TAG_INVENTORY, CompoundTag::class);
325 if($inventoryTag !== null){
326 $inventoryItems = [];
327 $armorInventoryItems = [];
328
329 foreach($inventoryTag as $i => $item){
330 $slot = $item->getByte(SavedItemStackData::TAG_SLOT);
331 if($slot >= 0 && $slot < 9){ //Hotbar
332 //Old hotbar saving stuff, ignore it
333 }elseif($slot >= 100 && $slot < 104){ //Armor
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");
339 }
340 }
341
342 self::populateInventoryFromListTag($this->inventory, $inventoryItems);
343 self::populateInventoryFromListTag($this->armorInventory, $armorInventoryItems);
344 }
345 $offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM);
346 if($offHand !== null){
347 $this->setOffHandItem(Item::safeNbtDeserialize($offHand, "Human off-hand item"));
348 }
349 $this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
350 $this->getViewers(),
351 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobOffHandItemChange($recipients, $this)
352 )));
353
354 $enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY, CompoundTag::class);
355 if($enderChestInventoryTag !== null){
356 $enderChestInventoryItems = [];
357
358 foreach($enderChestInventoryTag as $item){
359 $slot = $item->getByte(SavedItemStackData::TAG_SLOT);
360 $enderChestInventoryItems[$slot] = Item::safeNbtDeserialize($item, "Human ender chest slot $slot");
361 }
362 self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
363 }
364
365 $this->hotbar->setSelectedIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
366 //TODO: cyclic reference
367 $this->hotbar->getSelectedIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
368 $this->getViewers(),
369 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
370 ));
371
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()));
376
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));
381
382 if(($xpSeedTag = $nbt->getTag(self::TAG_XP_SEED)) instanceof IntTag){
383 $this->xpSeed = $xpSeedTag->getValue();
384 }else{
385 $this->xpSeed = EnchantingHelper::generateSeed();
386 }
387 }
388
389 protected function entityBaseTick(int $tickDiff = 1) : bool{
390 $hasUpdate = parent::entityBaseTick($tickDiff);
391
392 $this->hungerManager->tick($tickDiff);
393 $this->xpManager->tick($tickDiff);
394
395 return $hasUpdate;
396 }
397
398 public function getName() : string{
399 return $this->getNameTag();
400 }
401
402 public function applyDamageModifiers(EntityDamageEvent $source) : void{
403 parent::applyDamageModifiers($source);
404
405 $type = $source->getCause();
406 if($type !== EntityDamageEvent::CAUSE_SUICIDE && $type !== EntityDamageEvent::CAUSE_VOID
407 && ($this->getMainHandItem() instanceof Totem || $this->getOffHandItem() instanceof Totem)){
408
409 $compensation = $this->getHealth() - $source->getFinalDamage() - 1;
410 if($compensation <= -1){
411 $source->setModifier($compensation, EntityDamageEvent::MODIFIER_TOTEM);
412 }
413 }
414 }
415
416 protected function applyPostDamageEffects(EntityDamageEvent $source) : void{
417 parent::applyPostDamageEffects($source);
418 $totemModifier = $source->getModifier(EntityDamageEvent::MODIFIER_TOTEM);
419 if($totemModifier < 0){ //Totem prevented death
420 $this->effectManager->clear();
421
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));
425
426 $this->broadcastAnimation(new TotemUseAnimation($this));
427 $this->broadcastSound(new TotemUseSound());
428
429 $hand = $this->getMainHandItem();
430 if($hand instanceof Totem){
431 $hand->pop(); //Plugins could alter max stack size
432 $this->setMainHandItem($hand);
433 }elseif(($offHand = $this->getOffHandItem()) instanceof Totem){
434 $offHand->pop();
435 $this->setOffHandItem($offHand);
436 }
437 }
438 }
439
440 public function getDrops() : array{
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(); });
446 }
447
448 public function saveNBT() : CompoundTag{
449 $nbt = parent::saveNBT();
450
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());
455
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);
460
461 $inventoryTag = new ListTag([], NBT::TAG_Compound);
462 $nbt->setTag(self::TAG_INVENTORY, $inventoryTag);
463
464 //Normal inventory
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));
470 }
471 }
472
473 //Armor
474 for($slot = 100; $slot < 104; ++$slot){
475 $item = $this->armorInventory->getItem($slot - 100);
476 if(!$item->isNull()){
477 $inventoryTag->push($item->nbtSerialize($slot));
478 }
479 }
480
481 $nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->hotbar->getSelectedIndex());
482
483 $offHandItem = $this->getOffHandItem();
484 if(!$offHandItem->isNull()){
485 $nbt->setTag(self::TAG_OFF_HAND_ITEM, $offHandItem->nbtSerialize());
486 }
487
489 $items = [];
490
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);
496 }
497 }
498
499 $nbt->setTag(self::TAG_ENDER_CHEST_INVENTORY, new ListTag($items, NBT::TAG_Compound));
500
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())
507 );
508
509 return $nbt;
510 }
511
512 public function spawnTo(Player $player) : void{
513 if($player !== $this){
514 parent::spawnTo($player);
515 }
516 }
517
518 protected function sendSpawnPacket(Player $player) : void{
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))]));
523 }
524
525 $networkSession->sendDataPacket(AddPlayerPacket::create(
526 $this->getUniqueId(),
527 $this->getName(),
528 $this->getId(),
529 "",
530 $this->location->asVector3(),
531 $this->getMotion(),
532 $this->location->pitch,
533 $this->location->yaw,
534 $this->location->yaw, //TODO: head yaw
535 ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getMainHandItem())),
536 GameMode::SURVIVAL,
537 $this->getAllNetworkData(),
538 new PropertySyncData([], []),
539 UpdateAbilitiesPacket::create(new AbilitiesData(CommandPermissions::NORMAL, PlayerPermissions::VISITOR, $this->getId() /* TODO: this should be unique ID */, [
540 new AbilitiesLayer(
541 AbilitiesLayer::LAYER_BASE,
542 array_fill(0, AbilitiesLayer::NUMBER_OF_ABILITIES, false),
543 0.0,
544 0.0,
545 0.0
546 )
547 ])),
548 [], //TODO: entity links
549 "", //device ID (we intentionally don't send this - secvuln)
550 DeviceOS::UNKNOWN //we intentionally don't send this (secvuln)
551 ));
552
553 //TODO: Hack for MCPE 1.2.13: DATA_NAMETAG is useless in AddPlayerPacket, so it has to be sent separately
554 $this->sendData([$player], [EntityMetadataProperties::NAMETAG => new StringMetadataProperty($this->getNameTag())]);
555
556 $entityEventBroadcaster = $networkSession->getEntityEventBroadcaster();
557 $entityEventBroadcaster->onMobArmorChange([$networkSession], $this);
558 $entityEventBroadcaster->onMobOffHandItemChange([$networkSession], $this);
559
560 if(!($this instanceof Player)){
561 $networkSession->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)]));
562 }
563 }
564
565 public function getOffsetPosition(Vector3 $vector3) : Vector3{
566 return $vector3->add(0, 1.621, 0); //TODO: +0.001 hack for MCPE falling underground
567 }
568
569 protected function onDispose() : void{
570 $this->hotbar->getSelectedIndexChangeListeners()->clear();
571 $this->inventory->removeAllWindows();
572 $this->offHandInventory->removeAllWindows();
573 $this->enderInventory->removeAllWindows();
574 parent::onDispose();
575 }
576
577 protected function destroyCycles() : void{
578 unset(
579 $this->hungerManager,
580 $this->xpManager
581 );
582 parent::destroyCycles();
583 }
584}
applyPostDamageEffects(EntityDamageEvent $source)
Definition Human.php:416
applyConsumptionResults(Consumable $consumable)
Definition Human.php:209
static parseSkinNBT(CompoundTag $nbt)
Definition Human.php:128
sendSkin(?array $targets=null)
Definition Human.php:167
sendSpawnPacket(Player $player)
Definition Human.php:518
initHumanData(CompoundTag $nbt)
Definition Human.php:277
setSkin(Skin $skin)
Definition Human.php:157
applyDamageModifiers(EntityDamageEvent $source)
Definition Human.php:402
consumeObject(Consumable $consumable)
Definition Human.php:201
setInt(string $name, int $value)
setTag(string $name, Tag $tag)
getListTag(string $name, string $tagClass=Tag::class)
setFloat(string $name, float $value)
onMobMainHandItemChange(array $recipients, Human $mob)