48 private const TAG_HEALTH =
"Health";
49 private const TAG_AGE =
"Age";
50 private const TAG_PICKUP_DELAY =
"PickupDelay";
51 private const TAG_OWNER =
"Owner";
52 private const TAG_THROWER =
"Thrower";
53 public const TAG_ITEM =
"Item";
55 public static function getNetworkTypeId() :
string{
return EntityIds::ITEM; }
57 public const MERGE_CHECK_PERIOD = 2;
58 public const DEFAULT_DESPAWN_DELAY = 6000;
59 public const NEVER_DESPAWN = -1;
60 public const MAX_DESPAWN_DELAY = 32767 + self::DEFAULT_DESPAWN_DELAY;
62 protected string $owner =
"";
63 protected string $thrower =
"";
64 protected int $pickupDelay = 0;
65 protected int $despawnDelay = self::DEFAULT_DESPAWN_DELAY;
70 throw new \InvalidArgumentException(
"Item entity must have a non-air item with a count of at least 1");
72 $this->item = clone $item;
73 parent::__construct($location, $nbt);
82 protected function initEntity(
CompoundTag $nbt) : void{
83 parent::initEntity($nbt);
85 $this->setMaxHealth(5);
86 $this->setHealth($nbt->getShort(self::TAG_HEALTH, (
int) $this->getHealth()));
88 $age = $nbt->getShort(self::TAG_AGE, 0);
90 $this->despawnDelay = self::NEVER_DESPAWN;
92 $this->despawnDelay = max(0, self::DEFAULT_DESPAWN_DELAY - $age);
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);
101 parent::onFirstUpdate($currentTick);
104 protected function entityBaseTick(
int $tickDiff = 1) : bool{
109 Timings::$itemEntityBaseTick->startTiming();
112 $hasUpdate = parent::entityBaseTick($tickDiff);
114 if($this->isFlaggedForDespawn()){
118 if($this->pickupDelay !== self::NEVER_DESPAWN && $this->pickupDelay > 0){
120 $this->pickupDelay -= $tickDiff;
121 if($this->pickupDelay < 0){
122 $this->pickupDelay = 0;
126 if($this->hasMovementUpdate() && $this->isMergeCandidate() && $this->despawnDelay % self::MERGE_CHECK_PERIOD === 0){
127 $mergeable = [$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()){
134 if($entity->isMergeable($this)){
135 $mergeable[] = $entity;
136 if($entity->item->getCount() > $mergeTarget->item->getCount()){
137 $mergeTarget = $entity;
141 foreach($mergeable as $itemEntity){
142 if($itemEntity !== $mergeTarget){
143 $itemEntity->tryMergeInto($mergeTarget);
148 if(!$this->isFlaggedForDespawn() && $this->despawnDelay !== self::NEVER_DESPAWN){
150 $this->despawnDelay -= $tickDiff;
151 if($this->despawnDelay <= 0){
152 $ev =
new ItemDespawnEvent($this);
154 if($ev->isCancelled()){
155 $this->despawnDelay = self::DEFAULT_DESPAWN_DELAY;
157 $this->flagForDespawn();
164 Timings::$itemEntityBaseTick->stopTiming();
168 private function isMergeCandidate() : bool{
169 return $this->pickupDelay !== self::NEVER_DESPAWN && $this->item->getCount() < $this->item->getMaxStackSize();
176 if(!$this->isMergeCandidate() || !$entity->isMergeCandidate()){
179 $item = $entity->item;
180 return $entity !== $this && $item->
canStackWith($this->item) && $item->getCount() + $this->item->getCount() <= $item->
getMaxStackSize();
187 if(!$this->isMergeable($consumer)){
194 if($ev->isCancelled()){
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);
206 protected function tryChangeMovement() : void{
207 $this->checkObstruction($this->location->x, $this->location->y, $this->location->z);
208 parent::tryChangeMovement();
211 protected function applyDragBeforeGravity() : bool{
216 return !$this->item->isNull() && parent::canSaveWithChunk();
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){
226 $age = self::DEFAULT_DESPAWN_DELAY - $this->despawnDelay;
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);
236 public function getItem() :
Item{
240 public function isFireProof() : bool{
241 return $this->item->isFireProof();
244 public function canCollideWith(Entity $entity) : bool{
248 public function canBeCollidedWith() : bool{
252 public function getPickupDelay() : int{
253 return $this->pickupDelay;
256 public function setPickupDelay(
int $delay) : void{
257 $this->pickupDelay = $delay;
264 return $this->despawnDelay;
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");
274 $this->despawnDelay = $despawnDelay;
277 public function getOwner() : string{
281 public function setOwner(
string $owner) : void{
282 $this->owner = $owner;
285 public function getThrower() : string{
286 return $this->thrower;
289 public function setThrower(
string $thrower) : void{
290 $this->thrower = $thrower;
294 $networkSession = $player->getNetworkSession();
295 $networkSession->sendDataPacket(AddItemActorPacket::create(
298 ItemStackWrapper::legacy($networkSession->getTypeConverter()->coreItemStackToNet($this->getItem())),
299 $this->location->asVector3(),
301 $this->getAllNetworkData(),
306 public function setStackSize(
int $newCount) : void{
308 throw new \InvalidArgumentException(
"Stack size must be at least 1");
310 $this->item->setCount($newCount);
311 $this->broadcastAnimation(
new ItemEntityStackSizeChangeAnimation($this, $newCount));
314 public function getOffsetPosition(Vector3 $vector3) : Vector3{
315 return $vector3->add(0, 0.125, 0);
318 public function onCollideWithPlayer(Player $player) : void{
319 if($this->getPickupDelay() !== 0){
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(),
330 $ev =
new EntityItemPickupEvent($player, $this, $item, $playerInventory);
336 if($ev->isCancelled()){
340 NetworkBroadcastUtils::broadcastEntityEvent(
342 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $player, $this)
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));
351 $this->flagForDespawn();