PocketMine-MP 5.12.1 git-72f3c0b4b9a4c6b5d1bc79a3b31d0568ccc2baa9
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 static function getNetworkTypeId() : string{ return EntityIds::PLAYER; }
102
103 protected PlayerInventory $inventory;
104 protected PlayerOffHandInventory $offHandInventory;
105 protected PlayerEnderInventory $enderInventory;
106
107 protected UuidInterface $uuid;
108
109 protected Skin $skin;
110
111 protected HungerManager $hungerManager;
112 protected ExperienceManager $xpManager;
113
114 protected int $xpSeed;
115
116 public function __construct(Location $location, Skin $skin, ?CompoundTag $nbt = null){
117 $this->skin = $skin;
118 parent::__construct($location, $nbt);
119 }
120
121 protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(1.8, 0.6, 1.62); }
122
127 public static function parseSkinNBT(CompoundTag $nbt) : Skin{
128 $skinTag = $nbt->getCompoundTag(self::TAG_SKIN);
129 if($skinTag === null){
130 throw new SavedDataLoadingException("Missing skin data");
131 }
132 return new Skin( //this throws if the skin is invalid
133 $skinTag->getString(self::TAG_SKIN_NAME),
134 ($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)
135 $skinTag->getByteArray(self::TAG_SKIN_CAPE_DATA, ""),
136 $skinTag->getString(self::TAG_SKIN_GEOMETRY_NAME, ""),
137 $skinTag->getByteArray(self::TAG_SKIN_GEOMETRY_DATA, "")
138 );
139 }
140
141 public function getUniqueId() : UuidInterface{
142 return $this->uuid;
143 }
144
148 public function getSkin() : Skin{
149 return $this->skin;
150 }
151
156 public function setSkin(Skin $skin) : void{
157 $this->skin = $skin;
158 }
159
166 public function sendSkin(?array $targets = null) : void{
167 NetworkBroadcastUtils::broadcastPackets($targets ?? $this->hasSpawned, [
168 PlayerSkinPacket::create($this->getUniqueId(), "", "", TypeConverter::getInstance()->getSkinAdapter()->toSkinData($this->skin))
169 ]);
170 }
171
172 public function jump() : void{
173 parent::jump();
174 if($this->isSprinting()){
175 $this->hungerManager->exhaust(0.2, PlayerExhaustEvent::CAUSE_SPRINT_JUMPING);
176 }else{
177 $this->hungerManager->exhaust(0.05, PlayerExhaustEvent::CAUSE_JUMPING);
178 }
179 }
180
181 public function emote(string $emoteId) : void{
182 NetworkBroadcastUtils::broadcastEntityEvent(
183 $this->getViewers(),
184 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onEmote($recipients, $this, $emoteId)
185 );
186 }
187
188 public function getHungerManager() : HungerManager{
189 return $this->hungerManager;
190 }
191
192 public function consumeObject(Consumable $consumable) : bool{
193 if($consumable instanceof FoodSource && $consumable->requiresHunger() && !$this->hungerManager->isHungry()){
194 return false;
195 }
196
197 return parent::consumeObject($consumable);
198 }
199
200 protected function applyConsumptionResults(Consumable $consumable) : void{
201 if($consumable instanceof FoodSource){
202 $this->hungerManager->addFood($consumable->getFoodRestore());
203 $this->hungerManager->addSaturation($consumable->getSaturationRestore());
204 }
205
206 parent::applyConsumptionResults($consumable);
207 }
208
209 public function getXpManager() : ExperienceManager{
210 return $this->xpManager;
211 }
212
213 public function getEnchantmentSeed() : int{
214 return $this->xpSeed;
215 }
216
217 public function setEnchantmentSeed(int $seed) : void{
218 $this->xpSeed = $seed;
219 }
220
221 public function regenerateEnchantmentSeed() : void{
222 $this->xpSeed = EnchantingHelper::generateSeed();
223 }
224
225 public function getXpDropAmount() : int{
226 //this causes some XP to be lost on death when above level 1 (by design), dropping at most enough points for
227 //about 7.5 levels of XP.
228 return min(100, 7 * $this->xpManager->getXpLevel());
229 }
230
231 public function getInventory() : PlayerInventory{
232 return $this->inventory;
233 }
234
235 public function getOffHandInventory() : PlayerOffHandInventory{ return $this->offHandInventory; }
236
237 public function getEnderInventory() : PlayerEnderInventory{
238 return $this->enderInventory;
239 }
240
244 protected function initHumanData(CompoundTag $nbt) : void{
245 //TODO: use of NIL UUID for namespace is a hack; we should provide a proper UUID for the namespace
246 $this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag());
247 }
248
253 private static function populateInventoryFromListTag(Inventory $inventory, array $items) : void{
254 $listeners = $inventory->getListeners()->toArray();
255 $inventory->getListeners()->clear();
256
257 $inventory->setContents($items);
258
259 $inventory->getListeners()->add(...$listeners);
260 }
261
262 protected function initEntity(CompoundTag $nbt) : void{
263 parent::initEntity($nbt);
264
265 $this->hungerManager = new HungerManager($this);
266 $this->xpManager = new ExperienceManager($this);
267
268 $this->inventory = new PlayerInventory($this);
269 $syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent(
270 $this->getViewers(),
271 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
272 );
273 $this->inventory->getListeners()->add(new CallbackInventoryListener(
274 function(Inventory $unused, int $slot, Item $unused2) use ($syncHeldItem) : void{
275 if($slot === $this->inventory->getHeldItemIndex()){
276 $syncHeldItem();
277 }
278 },
279 function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{
280 if(array_key_exists($this->inventory->getHeldItemIndex(), $oldItems)){
281 $syncHeldItem();
282 }
283 }
284 ));
285 $this->offHandInventory = new PlayerOffHandInventory($this);
286 $this->enderInventory = new PlayerEnderInventory($this);
287 $this->initHumanData($nbt);
288
289 $inventoryTag = $nbt->getListTag(self::TAG_INVENTORY);
290 if($inventoryTag !== null){
291 $inventoryItems = [];
292 $armorInventoryItems = [];
293
295 foreach($inventoryTag as $i => $item){
296 $slot = $item->getByte(SavedItemStackData::TAG_SLOT);
297 if($slot >= 0 && $slot < 9){ //Hotbar
298 //Old hotbar saving stuff, ignore it
299 }elseif($slot >= 100 && $slot < 104){ //Armor
300 $armorInventoryItems[$slot - 100] = Item::nbtDeserialize($item);
301 }elseif($slot >= 9 && $slot < $this->inventory->getSize() + 9){
302 $inventoryItems[$slot - 9] = Item::nbtDeserialize($item);
303 }
304 }
305
306 self::populateInventoryFromListTag($this->inventory, $inventoryItems);
307 self::populateInventoryFromListTag($this->armorInventory, $armorInventoryItems);
308 }
309 $offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM);
310 if($offHand !== null){
311 $this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand));
312 }
313 $this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
314 $this->getViewers(),
315 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobOffHandItemChange($recipients, $this)
316 )));
317
318 $enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY);
319 if($enderChestInventoryTag !== null){
320 $enderChestInventoryItems = [];
321
323 foreach($enderChestInventoryTag as $i => $item){
324 $enderChestInventoryItems[$item->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($item);
325 }
326 self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
327 }
328
329 $this->inventory->setHeldItemIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
330 $this->inventory->getHeldItemIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
331 $this->getViewers(),
332 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
333 ));
334
335 $this->hungerManager->setFood((float) $nbt->getInt(self::TAG_FOOD_LEVEL, (int) $this->hungerManager->getFood()));
336 $this->hungerManager->setExhaustion($nbt->getFloat(self::TAG_FOOD_EXHAUSTION_LEVEL, $this->hungerManager->getExhaustion()));
337 $this->hungerManager->setSaturation($nbt->getFloat(self::TAG_FOOD_SATURATION_LEVEL, $this->hungerManager->getSaturation()));
338 $this->hungerManager->setFoodTickTimer($nbt->getInt(self::TAG_FOOD_TICK_TIMER, $this->hungerManager->getFoodTickTimer()));
339
340 $this->xpManager->setXpAndProgressNoEvent(
341 $nbt->getInt(self::TAG_XP_LEVEL, 0),
342 $nbt->getFloat(self::TAG_XP_PROGRESS, 0.0));
343 $this->xpManager->setLifetimeTotalXp($nbt->getInt(self::TAG_LIFETIME_XP_TOTAL, 0));
344
345 if(($xpSeedTag = $nbt->getTag(self::TAG_XP_SEED)) instanceof IntTag){
346 $this->xpSeed = $xpSeedTag->getValue();
347 }else{
348 $this->xpSeed = EnchantingHelper::generateSeed();
349 }
350 }
351
352 protected function entityBaseTick(int $tickDiff = 1) : bool{
353 $hasUpdate = parent::entityBaseTick($tickDiff);
354
355 $this->hungerManager->tick($tickDiff);
356 $this->xpManager->tick($tickDiff);
357
358 return $hasUpdate;
359 }
360
361 public function getName() : string{
362 return $this->getNameTag();
363 }
364
365 public function applyDamageModifiers(EntityDamageEvent $source) : void{
366 parent::applyDamageModifiers($source);
367
368 $type = $source->getCause();
369 if($type !== EntityDamageEvent::CAUSE_SUICIDE && $type !== EntityDamageEvent::CAUSE_VOID
370 && ($this->inventory->getItemInHand() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){
371
372 $compensation = $this->getHealth() - $source->getFinalDamage() - 1;
373 if($compensation <= -1){
374 $source->setModifier($compensation, EntityDamageEvent::MODIFIER_TOTEM);
375 }
376 }
377 }
378
379 protected function applyPostDamageEffects(EntityDamageEvent $source) : void{
380 parent::applyPostDamageEffects($source);
381 $totemModifier = $source->getModifier(EntityDamageEvent::MODIFIER_TOTEM);
382 if($totemModifier < 0){ //Totem prevented death
383 $this->effectManager->clear();
384
385 $this->effectManager->add(new EffectInstance(VanillaEffects::REGENERATION(), 40 * 20, 1));
386 $this->effectManager->add(new EffectInstance(VanillaEffects::FIRE_RESISTANCE(), 40 * 20, 1));
387 $this->effectManager->add(new EffectInstance(VanillaEffects::ABSORPTION(), 5 * 20, 1));
388
389 $this->broadcastAnimation(new TotemUseAnimation($this));
390 $this->broadcastSound(new TotemUseSound());
391
392 $hand = $this->inventory->getItemInHand();
393 if($hand instanceof Totem){
394 $hand->pop(); //Plugins could alter max stack size
395 $this->inventory->setItemInHand($hand);
396 }elseif(($offHand = $this->offHandInventory->getItem(0)) instanceof Totem){
397 $offHand->pop();
398 $this->offHandInventory->setItem(0, $offHand);
399 }
400 }
401 }
402
403 public function getDrops() : array{
404 return array_filter(array_merge(
405 array_values($this->inventory->getContents()),
406 array_values($this->armorInventory->getContents()),
407 array_values($this->offHandInventory->getContents()),
408 ), function(Item $item) : bool{ return !$item->hasEnchantment(VanillaEnchantments::VANISHING()) && !$item->keepOnDeath(); });
409 }
410
411 public function saveNBT() : CompoundTag{
412 $nbt = parent::saveNBT();
413
414 $nbt->setInt(self::TAG_FOOD_LEVEL, (int) $this->hungerManager->getFood());
415 $nbt->setFloat(self::TAG_FOOD_EXHAUSTION_LEVEL, $this->hungerManager->getExhaustion());
416 $nbt->setFloat(self::TAG_FOOD_SATURATION_LEVEL, $this->hungerManager->getSaturation());
417 $nbt->setInt(self::TAG_FOOD_TICK_TIMER, $this->hungerManager->getFoodTickTimer());
418
419 $nbt->setInt(self::TAG_XP_LEVEL, $this->xpManager->getXpLevel());
420 $nbt->setFloat(self::TAG_XP_PROGRESS, $this->xpManager->getXpProgress());
421 $nbt->setInt(self::TAG_LIFETIME_XP_TOTAL, $this->xpManager->getLifetimeTotalXp());
422 $nbt->setInt(self::TAG_XP_SEED, $this->xpSeed);
423
424 $inventoryTag = new ListTag([], NBT::TAG_Compound);
425 $nbt->setTag(self::TAG_INVENTORY, $inventoryTag);
426
427 //Normal inventory
428 $slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize();
429 for($slot = $this->inventory->getHotbarSize(); $slot < $slotCount; ++$slot){
430 $item = $this->inventory->getItem($slot - 9);
431 if(!$item->isNull()){
432 $inventoryTag->push($item->nbtSerialize($slot));
433 }
434 }
435
436 //Armor
437 for($slot = 100; $slot < 104; ++$slot){
438 $item = $this->armorInventory->getItem($slot - 100);
439 if(!$item->isNull()){
440 $inventoryTag->push($item->nbtSerialize($slot));
441 }
442 }
443
444 $nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->inventory->getHeldItemIndex());
445
446 $offHandItem = $this->offHandInventory->getItem(0);
447 if(!$offHandItem->isNull()){
448 $nbt->setTag(self::TAG_OFF_HAND_ITEM, $offHandItem->nbtSerialize());
449 }
450
452 $items = [];
453
454 $slotCount = $this->enderInventory->getSize();
455 for($slot = 0; $slot < $slotCount; ++$slot){
456 $item = $this->enderInventory->getItem($slot);
457 if(!$item->isNull()){
458 $items[] = $item->nbtSerialize($slot);
459 }
460 }
461
462 $nbt->setTag(self::TAG_ENDER_CHEST_INVENTORY, new ListTag($items, NBT::TAG_Compound));
463
464 $nbt->setTag(self::TAG_SKIN, CompoundTag::create()
465 ->setString(self::TAG_SKIN_NAME, $this->skin->getSkinId())
466 ->setByteArray(self::TAG_SKIN_DATA, $this->skin->getSkinData())
467 ->setByteArray(self::TAG_SKIN_CAPE_DATA, $this->skin->getCapeData())
468 ->setString(self::TAG_SKIN_GEOMETRY_NAME, $this->skin->getGeometryName())
469 ->setByteArray(self::TAG_SKIN_GEOMETRY_DATA, $this->skin->getGeometryData())
470 );
471
472 return $nbt;
473 }
474
475 public function spawnTo(Player $player) : void{
476 if($player !== $this){
477 parent::spawnTo($player);
478 }
479 }
480
481 protected function sendSpawnPacket(Player $player) : void{
482 $networkSession = $player->getNetworkSession();
483 $typeConverter = $networkSession->getTypeConverter();
484 if(!($this instanceof Player)){
485 $networkSession->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($this->uuid, $this->id, $this->getName(), $typeConverter->getSkinAdapter()->toSkinData($this->skin))]));
486 }
487
488 $networkSession->sendDataPacket(AddPlayerPacket::create(
489 $this->getUniqueId(),
490 $this->getName(),
491 $this->getId(),
492 "",
493 $this->location->asVector3(),
494 $this->getMotion(),
495 $this->location->pitch,
496 $this->location->yaw,
497 $this->location->yaw, //TODO: head yaw
498 ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getInventory()->getItemInHand())),
499 GameMode::SURVIVAL,
500 $this->getAllNetworkData(),
501 new PropertySyncData([], []),
502 UpdateAbilitiesPacket::create(new AbilitiesData(CommandPermissions::NORMAL, PlayerPermissions::VISITOR, $this->getId() /* TODO: this should be unique ID */, [
503 new AbilitiesLayer(
504 AbilitiesLayer::LAYER_BASE,
505 array_fill(0, AbilitiesLayer::NUMBER_OF_ABILITIES, false),
506 0.0,
507 0.0
508 )
509 ])),
510 [], //TODO: entity links
511 "", //device ID (we intentionally don't send this - secvuln)
512 DeviceOS::UNKNOWN //we intentionally don't send this (secvuln)
513 ));
514
515 //TODO: Hack for MCPE 1.2.13: DATA_NAMETAG is useless in AddPlayerPacket, so it has to be sent separately
516 $this->sendData([$player], [EntityMetadataProperties::NAMETAG => new StringMetadataProperty($this->getNameTag())]);
517
518 $entityEventBroadcaster = $networkSession->getEntityEventBroadcaster();
519 $entityEventBroadcaster->onMobArmorChange([$networkSession], $this);
520 $entityEventBroadcaster->onMobOffHandItemChange([$networkSession], $this);
521
522 if(!($this instanceof Player)){
523 $networkSession->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)]));
524 }
525 }
526
527 public function getOffsetPosition(Vector3 $vector3) : Vector3{
528 return $vector3->add(0, 1.621, 0); //TODO: +0.001 hack for MCPE falling underground
529 }
530
531 protected function onDispose() : void{
532 $this->inventory->removeAllViewers();
533 $this->inventory->getHeldItemIndexChangeListeners()->clear();
534 $this->offHandInventory->removeAllViewers();
535 $this->enderInventory->removeAllViewers();
536 parent::onDispose();
537 }
538
539 protected function destroyCycles() : void{
540 unset(
541 $this->inventory,
542 $this->offHandInventory,
543 $this->enderInventory,
544 $this->hungerManager,
545 $this->xpManager
546 );
547 parent::destroyCycles();
548 }
549}
applyPostDamageEffects(EntityDamageEvent $source)
Definition: Human.php:379
applyConsumptionResults(Consumable $consumable)
Definition: Human.php:200
static parseSkinNBT(CompoundTag $nbt)
Definition: Human.php:127
sendSkin(?array $targets=null)
Definition: Human.php:166
sendSpawnPacket(Player $player)
Definition: Human.php:481
initHumanData(CompoundTag $nbt)
Definition: Human.php:244
setSkin(Skin $skin)
Definition: Human.php:156
applyDamageModifiers(EntityDamageEvent $source)
Definition: Human.php:365
consumeObject(Consumable $consumable)
Definition: Human.php:192
setInt(string $name, int $value)
setTag(string $name, Tag $tag)
setFloat(string $name, float $value)
onMobMainHandItemChange(array $recipients, Human $mob)