PocketMine-MP 5.14.2 git-50e2c469a547a16a23b2dc691e70a51d34e29395
ItemEntity.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\object;
25
44use function max;
45
46class ItemEntity extends Entity{
47
48 private const TAG_HEALTH = "Health"; //TAG_Short
49 private const TAG_AGE = "Age"; //TAG_Short
50 private const TAG_PICKUP_DELAY = "PickupDelay"; //TAG_Short
51 private const TAG_OWNER = "Owner"; //TAG_String
52 private const TAG_THROWER = "Thrower"; //TAG_String
53 public const TAG_ITEM = "Item"; //TAG_Compound
54
55 public static function getNetworkTypeId() : string{ return EntityIds::ITEM; }
56
57 public const MERGE_CHECK_PERIOD = 2; //0.1 seconds
58 public const DEFAULT_DESPAWN_DELAY = 6000; //5 minutes
59 public const NEVER_DESPAWN = -1;
60 public const MAX_DESPAWN_DELAY = 32767 + self::DEFAULT_DESPAWN_DELAY; //max value storable by mojang NBT :(
61
62 protected string $owner = "";
63 protected string $thrower = "";
64 protected int $pickupDelay = 0;
65 protected int $despawnDelay = self::DEFAULT_DESPAWN_DELAY;
66 protected Item $item;
67
68 public function __construct(Location $location, Item $item, ?CompoundTag $nbt = null){
69 if($item->isNull()){
70 throw new \InvalidArgumentException("Item entity must have a non-air item with a count of at least 1");
71 }
72 $this->item = clone $item;
73 parent::__construct($location, $nbt);
74 }
75
76 protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); }
77
78 protected function getInitialDragMultiplier() : float{ return 0.02; }
79
80 protected function getInitialGravity() : float{ return 0.04; }
81
82 protected function initEntity(CompoundTag $nbt) : void{
83 parent::initEntity($nbt);
84
85 $this->setMaxHealth(5);
86 $this->setHealth($nbt->getShort(self::TAG_HEALTH, (int) $this->getHealth()));
87
88 $age = $nbt->getShort(self::TAG_AGE, 0);
89 if($age === -32768){
90 $this->despawnDelay = self::NEVER_DESPAWN;
91 }else{
92 $this->despawnDelay = max(0, self::DEFAULT_DESPAWN_DELAY - $age);
93 }
94 $this->pickupDelay = $nbt->getShort(self::TAG_PICKUP_DELAY, $this->pickupDelay);
95 $this->owner = $nbt->getString(self::TAG_OWNER, $this->owner);
96 $this->thrower = $nbt->getString(self::TAG_THROWER, $this->thrower);
97 }
98
99 protected function onFirstUpdate(int $currentTick) : void{
100 (new ItemSpawnEvent($this))->call(); //this must be called before EntitySpawnEvent, to maintain backwards compatibility
101 parent::onFirstUpdate($currentTick);
102 }
103
104 protected function entityBaseTick(int $tickDiff = 1) : bool{
105 if($this->closed){
106 return false;
107 }
108
109 Timings::$itemEntityBaseTick->startTiming();
110 try{
111
112 $hasUpdate = parent::entityBaseTick($tickDiff);
113
114 if($this->isFlaggedForDespawn()){
115 return $hasUpdate;
116 }
117
118 if($this->pickupDelay !== self::NEVER_DESPAWN && $this->pickupDelay > 0){ //Infinite delay
119 $hasUpdate = true;
120 $this->pickupDelay -= $tickDiff;
121 if($this->pickupDelay < 0){
122 $this->pickupDelay = 0;
123 }
124 }
125
126 if($this->hasMovementUpdate() && $this->isMergeCandidate() && $this->despawnDelay % self::MERGE_CHECK_PERIOD === 0){
127 $mergeable = [$this]; //in case the merge target ends up not being this
128 $mergeTarget = $this;
129 foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(0.5, 0.5, 0.5), $this) as $entity){
130 if(!$entity instanceof ItemEntity || $entity->isFlaggedForDespawn()){
131 continue;
132 }
133
134 if($entity->isMergeable($this)){
135 $mergeable[] = $entity;
136 if($entity->item->getCount() > $mergeTarget->item->getCount()){
137 $mergeTarget = $entity;
138 }
139 }
140 }
141 foreach($mergeable as $itemEntity){
142 if($itemEntity !== $mergeTarget){
143 $itemEntity->tryMergeInto($mergeTarget);
144 }
145 }
146 }
147
148 if(!$this->isFlaggedForDespawn() && $this->despawnDelay !== self::NEVER_DESPAWN){
149 $hasUpdate = true;
150 $this->despawnDelay -= $tickDiff;
151 if($this->despawnDelay <= 0){
152 $ev = new ItemDespawnEvent($this);
153 $ev->call();
154 if($ev->isCancelled()){
155 $this->despawnDelay = self::DEFAULT_DESPAWN_DELAY;
156 }else{
157 $this->flagForDespawn();
158 }
159 }
160 }
161
162 return $hasUpdate;
163 }finally{
164 Timings::$itemEntityBaseTick->stopTiming();
165 }
166 }
167
168 private function isMergeCandidate() : bool{
169 return $this->pickupDelay !== self::NEVER_DESPAWN && $this->item->getCount() < $this->item->getMaxStackSize();
170 }
171
175 public function isMergeable(ItemEntity $entity) : bool{
176 if(!$this->isMergeCandidate() || !$entity->isMergeCandidate()){
177 return false;
178 }
179 $item = $entity->item;
180 return $entity !== $this && $item->canStackWith($this->item) && $item->getCount() + $this->item->getCount() <= $item->getMaxStackSize();
181 }
182
186 public function tryMergeInto(ItemEntity $consumer) : bool{
187 if(!$this->isMergeable($consumer)){
188 return false;
189 }
190
191 $ev = new ItemMergeEvent($this, $consumer);
192 $ev->call();
193
194 if($ev->isCancelled()){
195 return false;
196 }
197
198 $consumer->setStackSize($consumer->item->getCount() + $this->item->getCount());
199 $this->flagForDespawn();
200 $consumer->pickupDelay = max($consumer->pickupDelay, $this->pickupDelay);
201 $consumer->despawnDelay = max($consumer->despawnDelay, $this->despawnDelay);
202
203 return true;
204 }
205
206 protected function tryChangeMovement() : void{
207 $this->checkObstruction($this->location->x, $this->location->y, $this->location->z);
208 parent::tryChangeMovement();
209 }
210
211 protected function applyDragBeforeGravity() : bool{
212 return true;
213 }
214
215 public function canSaveWithChunk() : bool{
216 return !$this->item->isNull() && parent::canSaveWithChunk();
217 }
218
219 public function saveNBT() : CompoundTag{
220 $nbt = parent::saveNBT();
221 $nbt->setTag(self::TAG_ITEM, $this->item->nbtSerialize());
222 $nbt->setShort(self::TAG_HEALTH, (int) $this->getHealth());
223 if($this->despawnDelay === self::NEVER_DESPAWN){
224 $age = -32768;
225 }else{
226 $age = self::DEFAULT_DESPAWN_DELAY - $this->despawnDelay;
227 }
228 $nbt->setShort(self::TAG_AGE, $age);
229 $nbt->setShort(self::TAG_PICKUP_DELAY, $this->pickupDelay);
230 $nbt->setString(self::TAG_OWNER, $this->owner);
231 $nbt->setString(self::TAG_THROWER, $this->thrower);
232
233 return $nbt;
234 }
235
236 public function getItem() : Item{
237 return $this->item;
238 }
239
240 public function isFireProof() : bool{
241 return $this->item->isFireProof();
242 }
243
244 public function canCollideWith(Entity $entity) : bool{
245 return false;
246 }
247
248 public function canBeCollidedWith() : bool{
249 return false;
250 }
251
252 public function getPickupDelay() : int{
253 return $this->pickupDelay;
254 }
255
256 public function setPickupDelay(int $delay) : void{
257 $this->pickupDelay = $delay;
258 }
259
263 public function getDespawnDelay() : int{
264 return $this->despawnDelay;
265 }
266
270 public function setDespawnDelay(int $despawnDelay) : void{
271 if(($despawnDelay < 0 || $despawnDelay > self::MAX_DESPAWN_DELAY) && $despawnDelay !== self::NEVER_DESPAWN){
272 throw new \InvalidArgumentException("Despawn ticker must be in range 0 ... " . self::MAX_DESPAWN_DELAY . " or " . self::NEVER_DESPAWN . ", got $despawnDelay");
273 }
274 $this->despawnDelay = $despawnDelay;
275 }
276
277 public function getOwner() : string{
278 return $this->owner;
279 }
280
281 public function setOwner(string $owner) : void{
282 $this->owner = $owner;
283 }
284
285 public function getThrower() : string{
286 return $this->thrower;
287 }
288
289 public function setThrower(string $thrower) : void{
290 $this->thrower = $thrower;
291 }
292
293 protected function sendSpawnPacket(Player $player) : void{
294 $networkSession = $player->getNetworkSession();
295 $networkSession->sendDataPacket(AddItemActorPacket::create(
296 $this->getId(), //TODO: entity unique ID
297 $this->getId(),
298 ItemStackWrapper::legacy($networkSession->getTypeConverter()->coreItemStackToNet($this->getItem())),
299 $this->location->asVector3(),
300 $this->getMotion(),
301 $this->getAllNetworkData(),
302 false //TODO: I have no idea what this is needed for, but right now we don't support fishing anyway
303 ));
304 }
305
306 public function setStackSize(int $newCount) : void{
307 if($newCount <= 0){
308 throw new \InvalidArgumentException("Stack size must be at least 1");
309 }
310 $this->item->setCount($newCount);
311 $this->broadcastAnimation(new ItemEntityStackSizeChangeAnimation($this, $newCount));
312 }
313
314 public function getOffsetPosition(Vector3 $vector3) : Vector3{
315 return $vector3->add(0, 0.125, 0);
316 }
317
318 public function onCollideWithPlayer(Player $player) : void{
319 if($this->getPickupDelay() !== 0){
320 return;
321 }
322
323 $item = $this->getItem();
324 $playerInventory = match(true){
325 $player->getOffHandInventory()->getItem(0)->canStackWith($item) && $player->getOffHandInventory()->getAddableItemQuantity($item) > 0 => $player->getOffHandInventory(),
326 $player->getInventory()->getAddableItemQuantity($item) > 0 => $player->getInventory(),
327 default => null
328 };
329
330 $ev = new EntityItemPickupEvent($player, $this, $item, $playerInventory);
331 if($player->hasFiniteResources() && $playerInventory === null){
332 $ev->cancel();
333 }
334
335 $ev->call();
336 if($ev->isCancelled()){
337 return;
338 }
339
340 NetworkBroadcastUtils::broadcastEntityEvent(
341 $this->getViewers(),
342 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $player, $this)
343 );
344
345 $inventory = $ev->getInventory();
346 if($inventory !== null){
347 foreach($inventory->addItem($ev->getItem()) as $remains){
348 $this->getWorld()->dropItem($this->location, $remains, new Vector3(0, 0, 0));
349 }
350 }
351 $this->flagForDespawn();
352 }
353}
setDespawnDelay(int $despawnDelay)
Definition: ItemEntity.php:270
tryMergeInto(ItemEntity $consumer)
Definition: ItemEntity.php:186
canStackWith(Item $other)
Definition: Item.php:671
setString(string $name, string $value)
setTag(string $name, Tag $tag)
setShort(string $name, int $value)