Merge branch 'minor-next' into major-next

This commit is contained in:
Dylan K. Taylor
2025-09-28 00:27:40 +01:00
23 changed files with 823 additions and 187 deletions

View File

@@ -31,8 +31,8 @@ use function str_repeat;
final class VersionInfo{ final class VersionInfo{
public const NAME = "PocketMine-MP"; public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.34.0"; public const BASE_VERSION = "5.34.1";
public const IS_DEVELOPMENT_BUILD = false; public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "stable"; public const BUILD_CHANNEL = "stable";
/** /**

View File

@@ -82,7 +82,7 @@ final class Cauldron extends Transparent{
$this->fill(FillableCauldron::MAX_FILL_LEVEL, VanillaBlocks::LAVA_CAULDRON(), $item, VanillaItems::BUCKET(), $returnedItems); $this->fill(FillableCauldron::MAX_FILL_LEVEL, VanillaBlocks::LAVA_CAULDRON(), $item, VanillaItems::BUCKET(), $returnedItems);
}elseif($item->getTypeId() === ItemTypeIds::POWDER_SNOW_BUCKET){ }elseif($item->getTypeId() === ItemTypeIds::POWDER_SNOW_BUCKET){
//TODO: powder snow cauldron //TODO: powder snow cauldron
}elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion }elseif($item instanceof Potion || $item instanceof SplashPotion){
if($item->getType() === PotionType::WATER){ if($item->getType() === PotionType::WATER){
$this->fill(WaterCauldron::WATER_BOTTLE_FILL_AMOUNT, VanillaBlocks::WATER_CAULDRON(), $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); $this->fill(WaterCauldron::WATER_BOTTLE_FILL_AMOUNT, VanillaBlocks::WATER_CAULDRON(), $item, VanillaItems::GLASS_BOTTLE(), $returnedItems);
}else{ }else{

View File

@@ -124,7 +124,7 @@ final class WaterCauldron extends FillableCauldron{
$world->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronAddDyeSound()); $world->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronAddDyeSound());
$item->pop(); $item->pop();
}elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion }elseif($item instanceof Potion || $item instanceof SplashPotion){
if($item->getType() === PotionType::WATER){ if($item->getType() === PotionType::WATER){
$this->setCustomWaterColor(null)->addFillLevels(self::WATER_BOTTLE_FILL_AMOUNT, $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); $this->setCustomWaterColor(null)->addFillLevels(self::WATER_BOTTLE_FILL_AMOUNT, $item, VanillaItems::GLASS_BOTTLE(), $returnedItems);
}else{ }else{

View File

@@ -94,6 +94,7 @@ class Campfire extends Spawnable implements ContainerTile{
$listeners = $this->inventory->getListeners()->toArray(); $listeners = $this->inventory->getListeners()->toArray();
$this->inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization $this->inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
$baseErrorContext = "Campfire ($this->position)";
foreach([ foreach([
[0, self::TAG_FIRST_INPUT_ITEM, self::TAG_FIRST_COOKING_TIME], [0, self::TAG_FIRST_INPUT_ITEM, self::TAG_FIRST_COOKING_TIME],
[1, self::TAG_SECOND_INPUT_ITEM, self::TAG_SECOND_COOKING_TIME], [1, self::TAG_SECOND_INPUT_ITEM, self::TAG_SECOND_COOKING_TIME],
@@ -101,7 +102,7 @@ class Campfire extends Spawnable implements ContainerTile{
[3, self::TAG_FOURTH_INPUT_ITEM, self::TAG_FOURTH_COOKING_TIME], [3, self::TAG_FOURTH_INPUT_ITEM, self::TAG_FOURTH_COOKING_TIME],
] as [$slot, $itemTag, $cookingTimeTag]){ ] as [$slot, $itemTag, $cookingTimeTag]){
if(($tag = $nbt->getTag($itemTag)) instanceof CompoundTag){ if(($tag = $nbt->getTag($itemTag)) instanceof CompoundTag){
$items[$slot] = Item::nbtDeserialize($tag); $items[$slot] = Item::safeNbtDeserialize($tag, "$baseErrorContext slot $slot");
} }
if(($tag = $nbt->getTag($cookingTimeTag)) instanceof IntTag){ if(($tag = $nbt->getTag($cookingTimeTag)) instanceof IntTag){
$this->cookingTimes[$slot] = $tag->getValue(); $this->cookingTimes[$slot] = $tag->getValue();

View File

@@ -76,7 +76,6 @@ final class Cauldron extends Spawnable{
default => throw new AssumptionFailedError("Unexpected potion item type") default => throw new AssumptionFailedError("Unexpected potion item type")
}); });
//TODO: lingering potion
$type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null;
$nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type));
@@ -96,7 +95,7 @@ final class Cauldron extends Spawnable{
$this->potionItem = match($containerType){ $this->potionItem = match($containerType){
self::POTION_CONTAINER_TYPE_NORMAL => VanillaItems::POTION()->setType($potionType), self::POTION_CONTAINER_TYPE_NORMAL => VanillaItems::POTION()->setType($potionType),
self::POTION_CONTAINER_TYPE_SPLASH => VanillaItems::SPLASH_POTION()->setType($potionType), self::POTION_CONTAINER_TYPE_SPLASH => VanillaItems::SPLASH_POTION()->setType($potionType),
self::POTION_CONTAINER_TYPE_LINGERING => throw new SavedDataLoadingException("Not implemented"), self::POTION_CONTAINER_TYPE_LINGERING => VanillaItems::LINGERING_POTION()->setType($potionType),
default => throw new SavedDataLoadingException("Invalid potion container type ID $containerType") default => throw new SavedDataLoadingException("Invalid potion container type ID $containerType")
}; };
}else{ }else{
@@ -115,7 +114,6 @@ final class Cauldron extends Spawnable{
default => throw new AssumptionFailedError("Unexpected potion item type") default => throw new AssumptionFailedError("Unexpected potion item type")
}); });
//TODO: lingering potion
$type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null;
$nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type));

View File

@@ -26,7 +26,6 @@ namespace pocketmine\block\tile;
use pocketmine\block\utils\ChiseledBookshelfSlot; use pocketmine\block\utils\ChiseledBookshelfSlot;
use pocketmine\data\bedrock\item\SavedItemData; use pocketmine\data\bedrock\item\SavedItemData;
use pocketmine\data\bedrock\item\SavedItemStackData; use pocketmine\data\bedrock\item\SavedItemStackData;
use pocketmine\data\SavedDataLoadingException;
use pocketmine\inventory\SimpleInventory; use pocketmine\inventory\SimpleInventory;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\math\Vector3; use pocketmine\math\Vector3;
@@ -99,18 +98,13 @@ class ChiseledBookshelf extends Tile implements ContainerTile{
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
$newContents = []; $newContents = [];
$errorLogContext = "ChiseledBookshelf ($this->position)";
foreach($inventoryTag as $slot => $itemNBT){ foreach($inventoryTag as $slot => $itemNBT){
try{
$count = $itemNBT->getByte(SavedItemStackData::TAG_COUNT); $count = $itemNBT->getByte(SavedItemStackData::TAG_COUNT);
if($count === 0){ if($count === 0){
continue; continue;
} }
$newContents[$slot] = Item::nbtDeserialize($itemNBT); $newContents[$slot] = Item::safeNbtDeserialize($itemNBT, "$errorLogContext slot $slot");
}catch(SavedDataLoadingException $e){
//TODO: not the best solution
\GlobalLogger::get()->logException($e);
continue;
}
} }
$inventory->setContents($newContents); $inventory->setContents($newContents);

View File

@@ -24,7 +24,6 @@ declare(strict_types=1);
namespace pocketmine\block\tile; namespace pocketmine\block\tile;
use pocketmine\data\bedrock\item\SavedItemStackData; use pocketmine\data\bedrock\item\SavedItemStackData;
use pocketmine\data\SavedDataLoadingException;
use pocketmine\inventory\Inventory; use pocketmine\inventory\Inventory;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\nbt\NBT; use pocketmine\nbt\NBT;
@@ -56,14 +55,10 @@ trait ContainerTileTrait{
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
$newContents = []; $newContents = [];
$errorLogContext = "Container (" . $this->getPosition() . ")";
foreach($inventoryTag as $itemNBT){ foreach($inventoryTag as $itemNBT){
try{ $slotId = $itemNBT->getByte(SavedItemStackData::TAG_SLOT);
$newContents[$itemNBT->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($itemNBT); $newContents[$slotId] = Item::safeNbtDeserialize($itemNBT, "$errorLogContext slot $slotId");
}catch(SavedDataLoadingException $e){
//TODO: not the best solution
\GlobalLogger::get()->logException($e);
continue;
}
} }
$inventory->setContents($newContents); $inventory->setContents($newContents);

View File

@@ -51,7 +51,7 @@ class ItemFrame extends Spawnable{
public function readSaveData(CompoundTag $nbt) : void{ public function readSaveData(CompoundTag $nbt) : void{
if(($itemTag = $nbt->getCompoundTag(self::TAG_ITEM)) !== null){ if(($itemTag = $nbt->getCompoundTag(self::TAG_ITEM)) !== null){
$this->item = Item::nbtDeserialize($itemTag); $this->item = Item::safeNbtDeserialize($itemTag, "ItemFrame ($this->position) framed item");
} }
if($nbt->getTag(self::TAG_ITEM_ROTATION) instanceof FloatTag){ if($nbt->getTag(self::TAG_ITEM_ROTATION) instanceof FloatTag){
$this->itemRotation = (int) ($nbt->getFloat(self::TAG_ITEM_ROTATION, $this->itemRotation * 45) / 45); $this->itemRotation = (int) ($nbt->getFloat(self::TAG_ITEM_ROTATION, $this->itemRotation * 45) / 45);

View File

@@ -44,7 +44,7 @@ class Jukebox extends Spawnable{
public function readSaveData(CompoundTag $nbt) : void{ public function readSaveData(CompoundTag $nbt) : void{
if(($tag = $nbt->getCompoundTag(self::TAG_RECORD)) !== null){ if(($tag = $nbt->getCompoundTag(self::TAG_RECORD)) !== null){
$record = Item::nbtDeserialize($tag); $record = Item::safeNbtDeserialize($tag, "Jukebox ($this->position) record");
if($record instanceof Record){ if($record instanceof Record){
$this->record = $record; $this->record = $record;
} }

View File

@@ -45,7 +45,7 @@ class Lectern extends Spawnable{
public function readSaveData(CompoundTag $nbt) : void{ public function readSaveData(CompoundTag $nbt) : void{
$this->viewedPage = $nbt->getInt(self::TAG_PAGE, 0); $this->viewedPage = $nbt->getInt(self::TAG_PAGE, 0);
if(($itemTag = $nbt->getCompoundTag(self::TAG_BOOK)) !== null){ if(($itemTag = $nbt->getCompoundTag(self::TAG_BOOK)) !== null){
$book = Item::nbtDeserialize($itemTag); $book = Item::safeNbtDeserialize($itemTag, "Lectern ($this->position) book");
if($book instanceof WritableBookBase && !$book->isNull()){ if($book instanceof WritableBookBase && !$book->isNull()){
$this->book = $book; $this->book = $book;
} }

View File

@@ -519,6 +519,14 @@ final class ItemSerializerDeserializerRegistrar{
}, },
fn(GoatHorn $item) => GoatHornTypeIdMap::getInstance()->toId($item->getHornType()) fn(GoatHorn $item) => GoatHornTypeIdMap::getInstance()->toId($item->getHornType())
); );
$this->map1to1ItemWithMeta(
Ids::LINGERING_POTION,
Items::LINGERING_POTION(),
function(SplashPotion $item, int $meta) : void{
$item->setType(PotionTypeIdMap::getInstance()->fromId($meta) ?? throw new ItemTypeDeserializeException("Unknown potion type ID $meta"));
},
fn(SplashPotion $item) => PotionTypeIdMap::getInstance()->toId($item->getType())
);
$this->map1to1ItemWithMeta( $this->map1to1ItemWithMeta(
Ids::MEDICINE, Ids::MEDICINE,
Items::MEDICINE(), Items::MEDICINE(),

View File

@@ -29,6 +29,7 @@ use pocketmine\data\bedrock\PotionTypeIdMap;
use pocketmine\data\bedrock\PotionTypeIds; use pocketmine\data\bedrock\PotionTypeIds;
use pocketmine\data\SavedDataLoadingException; use pocketmine\data\SavedDataLoadingException;
use pocketmine\entity\EntityDataHelper as Helper; use pocketmine\entity\EntityDataHelper as Helper;
use pocketmine\entity\object\AreaEffectCloud;
use pocketmine\entity\object\EndCrystal; use pocketmine\entity\object\EndCrystal;
use pocketmine\entity\object\ExperienceOrb; use pocketmine\entity\object\ExperienceOrb;
use pocketmine\entity\object\FallingBlock; use pocketmine\entity\object\FallingBlock;
@@ -84,6 +85,10 @@ final class EntityFactory{
//define legacy save IDs first - use them for saving for maximum compatibility with Minecraft PC //define legacy save IDs first - use them for saving for maximum compatibility with Minecraft PC
//TODO: index them by version to allow proper multi-save compatibility //TODO: index them by version to allow proper multi-save compatibility
$this->register(AreaEffectCloud::class, function(World $world, CompoundTag $nbt) : AreaEffectCloud{
return new AreaEffectCloud(Helper::parseLocation($nbt, $world), $nbt);
}, ['AreaEffectCloud', 'minecraft:area_effect_cloud']);
$this->register(Arrow::class, function(World $world, CompoundTag $nbt) : Arrow{ $this->register(Arrow::class, function(World $world, CompoundTag $nbt) : Arrow{
return new Arrow(Helper::parseLocation($nbt, $world), null, $nbt->getByte(Arrow::TAG_CRIT, 0) === 1, $nbt); return new Arrow(Helper::parseLocation($nbt, $world), null, $nbt->getByte(Arrow::TAG_CRIT, 0) === 1, $nbt);
}, ['Arrow', 'minecraft:arrow']); }, ['Arrow', 'minecraft:arrow']);

View File

@@ -331,9 +331,11 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
if($slot >= 0 && $slot < 9){ //Hotbar if($slot >= 0 && $slot < 9){ //Hotbar
//Old hotbar saving stuff, ignore it //Old hotbar saving stuff, ignore it
}elseif($slot >= 100 && $slot < 104){ //Armor }elseif($slot >= 100 && $slot < 104){ //Armor
$armorInventoryItems[$slot - 100] = Item::nbtDeserialize($item); $armorSlot = $slot - 100;
$armorInventoryItems[$armorSlot] = Item::safeNbtDeserialize($item, "Human armor slot $armorSlot");
}elseif($slot >= 9 && $slot < $this->inventory->getSize() + 9){ }elseif($slot >= 9 && $slot < $this->inventory->getSize() + 9){
$inventoryItems[$slot - 9] = Item::nbtDeserialize($item); $inventorySlot = $slot - 9;
$inventoryItems[$inventorySlot] = Item::safeNbtDeserialize($item, "Human inventory slot $inventorySlot");
} }
} }
@@ -342,7 +344,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
} }
$offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM); $offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM);
if($offHand !== null){ if($offHand !== null){
$this->setOffHandItem(Item::nbtDeserialize($offHand)); $this->setOffHandItem(Item::safeNbtDeserialize($offHand, "Human off-hand item"));
} }
$this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent( $this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
$this->getViewers(), $this->getViewers(),
@@ -353,8 +355,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
if($enderChestInventoryTag !== null){ if($enderChestInventoryTag !== null){
$enderChestInventoryItems = []; $enderChestInventoryItems = [];
foreach($enderChestInventoryTag as $i => $item){ foreach($enderChestInventoryTag as $item){
$enderChestInventoryItems[$item->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($item); $slot = $item->getByte(SavedItemStackData::TAG_SLOT);
$enderChestInventoryItems[$slot] = Item::safeNbtDeserialize($item, "Human ender chest slot $slot");
} }
self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems); self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
} }

View File

@@ -0,0 +1,223 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity\effect;
use pocketmine\color\Color;
use pocketmine\utils\ObjectSet;
use pocketmine\utils\Utils;
use function abs;
use function count;
use function spl_object_id;
class EffectCollection{
/** @var EffectInstance[] */
protected array $effects = [];
/**
* @var \Closure[]|ObjectSet
* @phpstan-var ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void>
*/
protected ObjectSet $effectAddHooks;
/**
* @var \Closure[]|ObjectSet
* @phpstan-var ObjectSet<\Closure(EffectInstance) : void>
*/
protected ObjectSet $effectRemoveHooks;
protected Color $bubbleColor;
protected bool $onlyAmbientEffects = false;
/**
* Validates whether an effect will be used for bubbles color calculation.
*
* @phpstan-var \Closure(EffectInstance) : bool
*/
protected \Closure $effectFilterForBubbles;
public function __construct(){
$this->bubbleColor = new Color(0, 0, 0, 0);
$this->effectAddHooks = new ObjectSet();
$this->effectRemoveHooks = new ObjectSet();
$this->setEffectFilterForBubbles(static fn(EffectInstance $e) : bool => $e->isVisible() && $e->getType()->hasBubbles());
}
/**
* Returns all the effects in the collection, indexed by spl_object_id of the effect type.
* @return EffectInstance[]
*/
public function all() : array{
return $this->effects;
}
/**
* Removes all effects.
*/
public function clear() : void{
foreach($this->effects as $effect){
$this->remove($effect->getType());
}
}
/**
* Removes the effect with the specified ID.
*/
public function remove(Effect $effectType) : void{
$index = spl_object_id($effectType);
if(isset($this->effects[$index])){
$effect = $this->effects[$index];
unset($this->effects[$index]);
foreach($this->effectRemoveHooks as $hook){
$hook($effect);
}
$this->recalculateEffectColor();
}
}
/**
* Returns the effect instance active with the specified ID, or null if does not have the
* effect.
*/
public function get(Effect $effect) : ?EffectInstance{
return $this->effects[spl_object_id($effect)] ?? null;
}
/**
* Returns whether the specified effect is active.
*/
public function has(Effect $effect) : bool{
return isset($this->effects[spl_object_id($effect)]);
}
/**
* In the following cases it will return true:
* - if the effect type is not already applied
* - if an existing effect of the same type can be replaced (due to shorter duration or lower level)
*/
public function canAdd(EffectInstance $effect) : bool{
$index = spl_object_id($effect->getType());
if(isset($this->effects[$index])){
$oldEffect = $this->effects[$index];
if(
abs($effect->getAmplifier()) < $oldEffect->getAmplifier()
|| (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) && $effect->getDuration() < $oldEffect->getDuration())
){
return false;
}
}
return true;
}
/**
* Adds an effect to the collection.
* Existing effects of the same type will be replaced if {@see self::canAdd()} returns true.
*
* @return bool whether the effect has been successfully applied.
*/
public function add(EffectInstance $effect) : bool{
if($this->canAdd($effect)){
$index = spl_object_id($effect->getType());
$replacesOldEffect = isset($this->effects[$index]);
$this->effects[$index] = $effect;
foreach($this->effectAddHooks as $hook){
$hook($effect, $replacesOldEffect);
}
$this->recalculateEffectColor();
return true;
}
return false;
}
/**
* Sets the filter that determines which effects will be displayed in the bubbles.
*
* @phpstan-param \Closure(EffectInstance) : bool $filter
*/
public function setEffectFilterForBubbles(\Closure $filter) : void{
Utils::validateCallableSignature(fn(EffectInstance $e) : bool => false, $filter);
$this->effectFilterForBubbles = $filter;
}
/**
* Recalculates the potion bubbles colour based on the active effects.
*/
protected function recalculateEffectColor() : void{
/** @var Color[] $colors */
$colors = [];
$ambient = true;
foreach($this->effects as $effect){
if(($this->effectFilterForBubbles)($effect)){
$level = $effect->getEffectLevel();
$color = $effect->getColor();
for($i = 0; $i < $level; ++$i){
$colors[] = $color;
}
if(!$effect->isAmbient()){
$ambient = false;
}
}
}
if(count($colors) > 0){
$this->bubbleColor = Color::mix(...$colors);
$this->onlyAmbientEffects = $ambient;
}else{
$this->bubbleColor = new Color(0, 0, 0, 0);
$this->onlyAmbientEffects = false;
}
}
public function getBubbleColor() : Color{
return $this->bubbleColor;
}
public function hasOnlyAmbientEffects() : bool{
return $this->onlyAmbientEffects;
}
/**
* @return \Closure[]|ObjectSet
* @phpstan-return ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void>
*/
public function getEffectAddHooks() : ObjectSet{
return $this->effectAddHooks;
}
/**
* @return \Closure[]|ObjectSet
* @phpstan-return ObjectSet<\Closure(EffectInstance) : void>
*/
public function getEffectRemoveHooks() : ObjectSet{
return $this->effectRemoveHooks;
}
}

View File

@@ -23,56 +23,18 @@ declare(strict_types=1);
namespace pocketmine\entity\effect; namespace pocketmine\entity\effect;
use pocketmine\color\Color;
use pocketmine\entity\Living; use pocketmine\entity\Living;
use pocketmine\event\entity\EntityEffectAddEvent; use pocketmine\event\entity\EntityEffectAddEvent;
use pocketmine\event\entity\EntityEffectRemoveEvent; use pocketmine\event\entity\EntityEffectRemoveEvent;
use pocketmine\utils\ObjectSet;
use function abs;
use function count; use function count;
use function spl_object_id; use function spl_object_id;
class EffectManager{ class EffectManager extends EffectCollection{
/** @var EffectInstance[] */
protected array $effects = [];
protected Color $bubbleColor;
protected bool $onlyAmbientEffects = false;
/**
* @var \Closure[]|ObjectSet
* @phpstan-var ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void>
*/
protected ObjectSet $effectAddHooks;
/**
* @var \Closure[]|ObjectSet
* @phpstan-var ObjectSet<\Closure(EffectInstance) : void>
*/
protected ObjectSet $effectRemoveHooks;
public function __construct( public function __construct(
private Living $entity private Living $entity
){ ){
$this->bubbleColor = new Color(0, 0, 0, 0); parent::__construct();
$this->effectAddHooks = new ObjectSet();
$this->effectRemoveHooks = new ObjectSet();
}
/**
* Returns an array of Effects currently active on the mob.
* @return EffectInstance[]
*/
public function all() : array{
return $this->effects;
}
/**
* Removes all effects from the mob.
*/
public function clear() : void{
foreach($this->effects as $effect){
$this->remove($effect->getType());
}
} }
/** /**
@@ -91,55 +53,17 @@ class EffectManager{
return; return;
} }
unset($this->effects[$index]);
$effect->getType()->remove($this->entity, $effect); $effect->getType()->remove($this->entity, $effect);
foreach($this->effectRemoveHooks as $hook){ parent::remove($effectType);
$hook($effect);
}
$this->recalculateEffectColor();
} }
} }
/**
* Returns the effect instance active on this entity with the specified ID, or null if the mob does not have the
* effect.
*/
public function get(Effect $effect) : ?EffectInstance{
return $this->effects[spl_object_id($effect)] ?? null;
}
/**
* Returns whether the specified effect is active on the mob.
*/
public function has(Effect $effect) : bool{
return isset($this->effects[spl_object_id($effect)]);
}
/**
* Adds an effect to the mob.
* If a weaker effect of the same type is already applied, it will be replaced.
* If a weaker or equal-strength effect is already applied but has a shorter duration, it will be replaced.
*
* @return bool whether the effect has been successfully applied.
*/
public function add(EffectInstance $effect) : bool{ public function add(EffectInstance $effect) : bool{
$oldEffect = null;
$cancelled = false;
$index = spl_object_id($effect->getType()); $index = spl_object_id($effect->getType());
if(isset($this->effects[$index])){ $oldEffect = $this->effects[$index] ?? null;
$oldEffect = $this->effects[$index];
if(
abs($effect->getAmplifier()) < $oldEffect->getAmplifier()
|| (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) && $effect->getDuration() < $oldEffect->getDuration())
){
$cancelled = true;
}
}
$ev = new EntityEffectAddEvent($this->entity, $effect, $oldEffect); $ev = new EntityEffectAddEvent($this->entity, $effect, $oldEffect);
if($cancelled){ if(!$this->canAdd($effect)){
$ev->cancel(); $ev->cancel();
} }
@@ -153,53 +77,8 @@ class EffectManager{
} }
$effect->getType()->add($this->entity, $effect); $effect->getType()->add($this->entity, $effect);
foreach($this->effectAddHooks as $hook){
$hook($effect, $oldEffect !== null);
}
$this->effects[$index] = $effect; return parent::add($effect);
$this->recalculateEffectColor();
return true;
}
/**
* Recalculates the mob's potion bubbles colour based on the active effects.
*/
protected function recalculateEffectColor() : void{
/** @var Color[] $colors */
$colors = [];
$ambient = true;
foreach($this->effects as $effect){
if($effect->isVisible() && $effect->getType()->hasBubbles()){
$level = $effect->getEffectLevel();
$color = $effect->getColor();
for($i = 0; $i < $level; ++$i){
$colors[] = $color;
}
if(!$effect->isAmbient()){
$ambient = false;
}
}
}
if(count($colors) > 0){
$this->bubbleColor = Color::mix(...$colors);
$this->onlyAmbientEffects = $ambient;
}else{
$this->bubbleColor = new Color(0, 0, 0, 0);
$this->onlyAmbientEffects = false;
}
}
public function getBubbleColor() : Color{
return $this->bubbleColor;
}
public function hasOnlyAmbientEffects() : bool{
return $this->onlyAmbientEffects;
} }
public function tick(int $tickDiff = 1) : bool{ public function tick(int $tickDiff = 1) : bool{
@@ -216,20 +95,4 @@ class EffectManager{
return count($this->effects) > 0; return count($this->effects) > 0;
} }
/**
* @return \Closure[]|ObjectSet
* @phpstan-return ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void>
*/
public function getEffectAddHooks() : ObjectSet{
return $this->effectAddHooks;
}
/**
* @return \Closure[]|ObjectSet
* @phpstan-return ObjectSet<\Closure(EffectInstance) : void>
*/
public function getEffectRemoveHooks() : ObjectSet{
return $this->effectRemoveHooks;
}
} }

View File

@@ -0,0 +1,427 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity\object;
use pocketmine\data\bedrock\EffectIdMap;
use pocketmine\data\bedrock\PotionTypeIds;
use pocketmine\entity\effect\EffectCollection;
use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\InstantEffect;
use pocketmine\entity\Entity;
use pocketmine\entity\EntitySizeInfo;
use pocketmine\entity\Living;
use pocketmine\entity\Location;
use pocketmine\event\entity\AreaEffectCloudApplyEvent;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
use pocketmine\utils\Binary;
use pocketmine\world\particle\PotionSplashParticle;
use function count;
use function max;
use function round;
class AreaEffectCloud extends Entity{
public const DEFAULT_DURATION = 600; // in ticks
public const DEFAULT_DURATION_CHANGE_ON_USE = 0; // in ticks
public const UPDATE_DELAY = 10; // in ticks
public const REAPPLICATION_DELAY = 40; // in ticks
public const DEFAULT_RADIUS = 3.0; // in blocks
public const DEFAULT_RADIUS_CHANGE_ON_PICKUP = -0.5; // in blocks
public const DEFAULT_RADIUS_CHANGE_ON_USE = -0.5; // in blocks
public const DEFAULT_RADIUS_CHANGE_PER_TICK = -(self::DEFAULT_RADIUS / self::DEFAULT_DURATION); // in blocks
protected const TAG_POTION_ID = "PotionId"; //TAG_Short
protected const TAG_SPAWN_TICK = "SpawnTick"; //TAG_Long
protected const TAG_DURATION = "Duration"; //TAG_Int
protected const TAG_PICKUP_COUNT = "PickupCount"; //TAG_Int
protected const TAG_DURATION_ON_USE = "DurationOnUse"; //TAG_Int
protected const TAG_REAPPLICATION_DELAY = "ReapplicationDelay"; //TAG_Int
protected const TAG_INITIAL_RADIUS = "InitialRadius"; //TAG_Float
protected const TAG_RADIUS = "Radius"; //TAG_Float
protected const TAG_RADIUS_CHANGE_ON_PICKUP = "RadiusChangeOnPickup"; //TAG_Float
protected const TAG_RADIUS_ON_USE = "RadiusOnUse"; //TAG_Float
protected const TAG_RADIUS_PER_TICK = "RadiusPerTick"; //TAG_Float
protected const TAG_EFFECTS = "mobEffects"; //TAG_List
public static function getNetworkTypeId() : string{ return EntityIds::AREA_EFFECT_CLOUD; }
protected int $age = 0;
protected EffectCollection $effectCollection;
/** @var array<int, int> entity ID => expiration */
protected array $victims = [];
protected int $maxAge = self::DEFAULT_DURATION;
protected int $maxAgeChangeOnUse = self::DEFAULT_DURATION_CHANGE_ON_USE;
protected int $reapplicationDelay = self::REAPPLICATION_DELAY;
protected int $pickupCount = 0;
protected float $radiusChangeOnPickup = self::DEFAULT_RADIUS_CHANGE_ON_PICKUP;
protected float $initialRadius = self::DEFAULT_RADIUS;
protected float $radius = self::DEFAULT_RADIUS;
protected float $radiusChangeOnUse = self::DEFAULT_RADIUS_CHANGE_ON_USE;
protected float $radiusChangePerTick = self::DEFAULT_RADIUS_CHANGE_PER_TICK;
public function __construct(
Location $location,
?CompoundTag $nbt = null
){
parent::__construct($location, $nbt);
}
protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.5, $this->radius * 2); }
protected function getInitialDragMultiplier() : float{ return 0.0; }
protected function getInitialGravity() : float{ return 0.0; }
protected function initEntity(CompoundTag $nbt) : void{
parent::initEntity($nbt);
$this->effectCollection = new EffectCollection();
$this->effectCollection->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
$this->effectCollection->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
$this->effectCollection->setEffectFilterForBubbles(static fn(EffectInstance $e) : bool => $e->isVisible());
$worldTime = $this->getWorld()->getTime();
$this->age = max($worldTime - $nbt->getLong(self::TAG_SPAWN_TICK, $worldTime), 0);
$this->maxAge = $nbt->getInt(self::TAG_DURATION, self::DEFAULT_DURATION);
$this->maxAgeChangeOnUse = $nbt->getInt(self::TAG_DURATION_ON_USE, self::DEFAULT_DURATION_CHANGE_ON_USE);
$this->pickupCount = $nbt->getInt(self::TAG_PICKUP_COUNT, 0);
$this->reapplicationDelay = $nbt->getInt(self::TAG_REAPPLICATION_DELAY, self::REAPPLICATION_DELAY);
$this->initialRadius = $nbt->getFloat(self::TAG_INITIAL_RADIUS, self::DEFAULT_RADIUS);
$this->setRadius($nbt->getFloat(self::TAG_RADIUS, $this->initialRadius));
$this->radiusChangeOnPickup = $nbt->getFloat(self::TAG_RADIUS_CHANGE_ON_PICKUP, self::DEFAULT_RADIUS_CHANGE_ON_PICKUP);
$this->radiusChangeOnUse = $nbt->getFloat(self::TAG_RADIUS_ON_USE, self::DEFAULT_RADIUS_CHANGE_ON_USE);
$this->radiusChangePerTick = $nbt->getFloat(self::TAG_RADIUS_PER_TICK, self::DEFAULT_RADIUS_CHANGE_PER_TICK);
$effectsTag = $nbt->getListTag(self::TAG_EFFECTS, CompoundTag::class);
if($effectsTag !== null){
foreach($effectsTag as $e){
$effect = EffectIdMap::getInstance()->fromId($e->getByte("Id"));
if($effect === null){
continue;
}
$this->effectCollection->add(new EffectInstance(
$effect,
$e->getInt("Duration"),
Binary::unsignByte($e->getByte("Amplifier")),
$e->getByte("ShowParticles", 1) !== 0,
$e->getByte("Ambient", 0) !== 0
));
}
}
}
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setLong(self::TAG_SPAWN_TICK, $this->getWorld()->getTime() - $this->age);
$nbt->setShort(self::TAG_POTION_ID, PotionTypeIds::WATER); //not used, mobEffects is used exclusively in Bedrock
$nbt->setInt(self::TAG_DURATION, $this->maxAge);
$nbt->setInt(self::TAG_DURATION_ON_USE, $this->maxAgeChangeOnUse);
$nbt->setInt(self::TAG_PICKUP_COUNT, $this->pickupCount);
$nbt->setInt(self::TAG_REAPPLICATION_DELAY, $this->reapplicationDelay);
$nbt->setFloat(self::TAG_INITIAL_RADIUS, $this->initialRadius);
$nbt->setFloat(self::TAG_RADIUS, $this->radius);
$nbt->setFloat(self::TAG_RADIUS_CHANGE_ON_PICKUP, $this->radiusChangeOnPickup);
$nbt->setFloat(self::TAG_RADIUS_ON_USE, $this->radiusChangeOnUse);
$nbt->setFloat(self::TAG_RADIUS_PER_TICK, $this->radiusChangePerTick);
if(count($this->effectCollection->all()) > 0){
$effects = [];
foreach($this->effectCollection->all() as $effect){
$effects[] = CompoundTag::create()
->setByte("Id", EffectIdMap::getInstance()->toId($effect->getType()))
->setByte("Amplifier", Binary::signByte($effect->getAmplifier()))
->setInt("Duration", $effect->getDuration())
->setByte("Ambient", $effect->isAmbient() ? 1 : 0)
->setByte("ShowParticles", $effect->isVisible() ? 1 : 0);
}
$nbt->setTag(self::TAG_EFFECTS, new ListTag($effects));
}
return $nbt;
}
public function isFireProof() : bool{
return true;
}
public function canBeCollidedWith() : bool{
return false;
}
/**
* Returns the current age of the cloud (in ticks).
*/
public function getAge() : int{
return $this->age;
}
public function getEffects() : EffectCollection{
return $this->effectCollection;
}
/**
* Returns the initial radius (in blocks).
*/
public function getInitialRadius() : float{
return $this->initialRadius;
}
/**
* Returns the current radius (in blocks).
*/
public function getRadius() : float{
return $this->radius;
}
/**
* Sets the current radius (in blocks).
*/
protected function setRadius(float $radius) : void{
$this->radius = $radius;
$this->setSize($this->getInitialSizeInfo());
$this->networkPropertiesDirty = true;
}
/**
* Returns the amount that the radius of this cloud will add by when it is
* picked up (in blocks). Usually negative resulting in a radius reduction.
*
* Applied when getting dragon breath bottle.
*/
public function getRadiusChangeOnPickup() : float{
return $this->radiusChangeOnPickup;
}
/**
* Sets the amount that the radius of this cloud will add by when it is
* picked up (in blocks). Usually negative resulting in a radius reduction.
*
* Applied when getting dragon breath bottle.
*/
public function setRadiusChangeOnPickup(float $radiusChangeOnPickup) : void{
$this->radiusChangeOnPickup = $radiusChangeOnPickup;
}
/**
* Returns the amount that the radius of this cloud will add by when it
* applies an effect to an entity (in blocks). Usually negative resulting in a radius reduction.
*/
public function getRadiusChangeOnUse() : float{
return $this->radiusChangeOnUse;
}
/**
* Sets the amount that the radius of this cloud will add by when it
* applies an effect to an entity (in blocks).
*/
public function setRadiusChangeOnUse(float $radiusChangeOnUse) : void{
$this->radiusChangeOnUse = $radiusChangeOnUse;
}
/**
* Returns the amount that the radius of this cloud will add by when an update
* is performed (in blocks). Usually negative resulting in a radius reduction.
*/
public function getRadiusChangePerTick() : float{
return $this->radiusChangePerTick;
}
/**
* Sets the amount that the radius of this cloud will add by when an update is performed (in blocks).
*/
public function setRadiusChangePerTick(float $radiusChangePerTick) : void{
$this->radiusChangePerTick = $radiusChangePerTick;
}
/**
* Returns the age at which the cloud will despawn.
*/
public function getMaxAge() : int{
return $this->maxAge;
}
/**
* Sets the age at which the cloud will despawn.
*/
public function setMaxAge(int $maxAge) : void{
$this->maxAge = $maxAge;
}
/**
* Returns the amount that the max age of this cloud will change by when it
* applies an effect to an entity (in ticks).
*/
public function getMaxAgeChangeOnUse() : int{
return $this->maxAgeChangeOnUse;
}
/**
* Sets the amount that the max age of this cloud will change by when it
* applies an effect to an entity (in ticks).
*/
public function setMaxAgeChangeOnUse(int $maxAgeChangeOnUse) : void{
$this->maxAgeChangeOnUse = $maxAgeChangeOnUse;
}
/**
* Returns the time that an entity will be immune from subsequent exposure (in ticks).
*/
public function getReapplicationDelay() : int{
return $this->reapplicationDelay;
}
/**
* Sets the time that an entity will be immune from subsequent exposure (in ticks).
*/
public function setReapplicationDelay(int $delay) : void{
$this->reapplicationDelay = $delay;
}
protected function entityBaseTick(int $tickDiff = 1) : bool{
$hasUpdate = parent::entityBaseTick($tickDiff);
$this->age += $tickDiff;
$radius = $this->radius + ($this->radiusChangePerTick * $tickDiff);
if($radius < 0.5){
$this->flagForDespawn();
return true;
}
$this->setRadius($radius);
if($this->age >= self::UPDATE_DELAY && ($this->age % self::UPDATE_DELAY) === 0){
if($this->age > $this->maxAge){
$this->flagForDespawn();
return true;
}
foreach($this->victims as $entityId => $expiration){
if($this->age >= $expiration){
unset($this->victims[$entityId]);
}
}
$entities = [];
$radiusChange = 0.0;
$maxAgeChange = 0;
foreach($this->getWorld()->getCollidingEntities($this->getBoundingBox(), $this) as $entity){
if(!$entity instanceof Living || isset($this->victims[$entity->getId()])){
continue;
}
$entityPosition = $entity->getPosition();
$xDiff = $entityPosition->getX() - $this->location->getX();
$zDiff = $entityPosition->getZ() - $this->location->getZ();
if(($xDiff ** 2 + $zDiff ** 2) > $this->radius ** 2){
continue;
}
$entities[] = $entity;
if($this->radiusChangeOnUse !== 0.0){
$radiusChange += $this->radiusChangeOnUse;
if($this->radius + $radiusChange <= 0){
break;
}
}
if($this->maxAgeChangeOnUse !== 0){
$maxAgeChange += $this->maxAgeChangeOnUse;
if($this->maxAge + $maxAgeChange <= 0){
break;
}
}
}
if(count($entities) === 0){
return $hasUpdate;
}
$ev = new AreaEffectCloudApplyEvent($this, $entities);
$ev->call();
if($ev->isCancelled()){
return $hasUpdate;
}
foreach($ev->getAffectedEntities() as $entity){
foreach($this->effectCollection->all() as $effect){
$effect = clone $effect; //avoid accidental modification
if($effect->getType() instanceof InstantEffect){
$effect->getType()->applyEffect($entity, $effect, 0.5, $this);
}else{
$entity->getEffects()->add($effect->setDuration((int) round($effect->getDuration() / 4)));
}
}
if($this->reapplicationDelay !== 0){
$this->victims[$entity->getId()] = $this->age + $this->reapplicationDelay;
}
}
$radius = $this->radius + $radiusChange;
$maxAge = $this->maxAge + $maxAgeChange;
if($radius <= 0 || $maxAge <= 0){
$this->flagForDespawn();
return true;
}
$this->setRadius($radius);
$this->setMaxAge($maxAge);
$hasUpdate = true;
}
return $hasUpdate;
}
protected function syncNetworkData(EntityMetadataCollection $properties) : void{
parent::syncNetworkData($properties);
//visual properties
$properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS, $this->radius);
$properties->setInt(EntityMetadataProperties::POTION_COLOR, Binary::signInt((
count($this->effectCollection->all()) === 0 ? PotionSplashParticle::DEFAULT_COLOR() : $this->effectCollection->getBubbleColor()
)->toARGB()));
//these are properties the client expects, and are used for client-sided logic, which we don't want
$properties->setByte(EntityMetadataProperties::POTION_AMBIENT, 0);
$properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_DURATION, -1);
$properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS_CHANGE_ON_PICKUP, 0);
$properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS_PER_TICK, 0);
$properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_SPAWN_TIME, 0);
$properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_PICKUP_COUNT, 0);
$properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_WAITING, 0);
}
protected function destroyCycles() : void{
//wipe out callback refs
$this->effectCollection = new EffectCollection();
parent::destroyCycles();
}
}

View File

@@ -52,6 +52,8 @@ class EndCrystal extends Entity implements Explosive{
protected bool $showBase = false; protected bool $showBase = false;
protected ?Vector3 $beamTarget = null; protected ?Vector3 $beamTarget = null;
private bool $primed = false;
protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(2.0, 2.0); } protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(2.0, 2.0); }
protected function getInitialDragMultiplier() : float{ return 1.0; } protected function getInitialDragMultiplier() : float{ return 1.0; }
@@ -88,11 +90,9 @@ class EndCrystal extends Entity implements Explosive{
parent::attack($source); parent::attack($source);
if( if(
$source->getCause() !== EntityDamageEvent::CAUSE_VOID && $source->getCause() !== EntityDamageEvent::CAUSE_VOID &&
!$this->isFlaggedForDespawn() &&
!$source->isCancelled() !$source->isCancelled()
){ ){
$this->flagForDespawn(); $this->primed = true;
$this->explode();
} }
} }
@@ -125,6 +125,13 @@ class EndCrystal extends Entity implements Explosive{
return $nbt; return $nbt;
} }
protected function onDeathUpdate(int $tickDiff) : bool{
if($this->primed){
$this->explode();
}
return true;
}
public function explode() : void{ public function explode() : void{
$ev = new EntityPreExplodeEvent($this, 6); $ev = new EntityPreExplodeEvent($this, 6);
$ev->call(); $ev->call();

View File

@@ -32,10 +32,10 @@ use pocketmine\entity\effect\InstantEffect;
use pocketmine\entity\Entity; use pocketmine\entity\Entity;
use pocketmine\entity\Living; use pocketmine\entity\Living;
use pocketmine\entity\Location; use pocketmine\entity\Location;
use pocketmine\entity\object\AreaEffectCloud;
use pocketmine\event\entity\ProjectileHitBlockEvent; use pocketmine\event\entity\ProjectileHitBlockEvent;
use pocketmine\event\entity\ProjectileHitEntityEvent; use pocketmine\event\entity\ProjectileHitEntityEvent;
use pocketmine\event\entity\ProjectileHitEvent; use pocketmine\event\entity\ProjectileHitEvent;
use pocketmine\item\Potion;
use pocketmine\item\PotionType; use pocketmine\item\PotionType;
use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds; use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
@@ -96,8 +96,8 @@ class SplashPotion extends Throwable{
$this->getWorld()->addParticle($this->location, $particle); $this->getWorld()->addParticle($this->location, $particle);
$this->broadcastSound(new PotionSplashSound()); $this->broadcastSound(new PotionSplashSound());
if($hasEffects){
if(!$this->willLinger()){ if(!$this->willLinger()){
if($hasEffects){
foreach($this->getWorld()->getCollidingEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){ foreach($this->getWorld()->getCollidingEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){
if($entity instanceof Living){ if($entity instanceof Living){
$distanceSquared = $entity->getEyePos()->distanceSquared($this->location); $distanceSquared = $entity->getEyePos()->distanceSquared($this->location);
@@ -126,10 +126,18 @@ class SplashPotion extends Throwable{
} }
} }
} }
}else{
//TODO: lingering potions
} }
}elseif($event instanceof ProjectileHitBlockEvent && $this->getPotionType() === PotionType::WATER){ }else{
$entity = new AreaEffectCloud(Location::fromObject($this->location->floor()->add(0.5, 0.5, 0.5), $this->getWorld()));
foreach($this->potionType->getEffects() as $effect){
$entity->getEffects()->add($effect);
}
if(($owner = $this->getOwningEntity()) !== null && !$owner->isClosed()){
$entity->setOwningEntity($owner);
}
$entity->spawnToAll();
}
if(!$hasEffects && $event instanceof ProjectileHitBlockEvent && $this->getPotionType() === PotionType::WATER){
$blockIn = $event->getBlockHit()->getSide($event->getRayTraceResult()->getHitFace()); $blockIn = $event->getBlockHit()->getSide($event->getRayTraceResult()->getHitFace());
if($blockIn->hasTypeTag(BlockTypeTags::FIRE)){ if($blockIn->hasTypeTag(BlockTypeTags::FIRE)){

View File

@@ -0,0 +1,64 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\event\entity;
use pocketmine\entity\Living;
use pocketmine\entity\object\AreaEffectCloud;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
/**
* Called when an area effect cloud applies effects to entities.
*
* @phpstan-extends EntityEvent<AreaEffectCloud>
*/
class AreaEffectCloudApplyEvent extends EntityEvent implements Cancellable{
use CancellableTrait;
/**
* @param Living[] $affectedEntities
*/
public function __construct(
AreaEffectCloud $entity,
protected array $affectedEntities
){
$this->entity = $entity;
}
/**
* @return AreaEffectCloud
*/
public function getEntity(){
return $this->entity;
}
/**
* Returns the affected entities.
*
* @return Living[]
*/
public function getAffectedEntities() : array{
return $this->affectedEntities;
}
}

View File

@@ -779,6 +779,24 @@ class Item implements \JsonSerializable{
} }
} }
/**
* Same as nbtDeserialize(), but purposely suppresses data errors and returns AIR if deserialization fails.
* An error will be logged to the global logger if this happens.
*
* @param string $errorLogContext Used in log messages if deserialization fails to aid debugging (e.g. inventory owner, slot number, etc.)
*/
public static function safeNbtDeserialize(CompoundTag $tag, string $errorLogContext, ?\Logger $logger = null) : Item{
try{
return self::nbtDeserialize($tag);
}catch(SavedDataLoadingException $e){
//TODO: what if the intention was to suppress logging?
$logger ??= \GlobalLogger::get();
$logger->error("$errorLogContext: Error deserializing item (item will be replaced by AIR): " . $e->getMessage());
//no trace here, otherwise things could get very noisy
return VanillaItems::AIR();
}
}
public function __clone(){ public function __clone(){
$this->nbt = clone $this->nbt; $this->nbt = clone $this->nbt;
if($this->blockEntityTag !== null){ if($this->blockEntityTag !== null){

View File

@@ -33,6 +33,16 @@ class SplashPotion extends ProjectileItem{
private PotionType $potionType = PotionType::WATER; private PotionType $potionType = PotionType::WATER;
public function __construct(
ItemIdentifier $identifier,
string $name = "Splash Potion",
array $enchantmentTags = [],
private bool $linger = false
){
//TODO: remove unnecessary default parameters in PM6, they remain because backward compatibility
parent::__construct($identifier, $name, $enchantmentTags);
}
protected function describeState(RuntimeDataDescriber $w) : void{ protected function describeState(RuntimeDataDescriber $w) : void{
$w->enum($this->potionType); $w->enum($this->potionType);
} }
@@ -52,10 +62,19 @@ class SplashPotion extends ProjectileItem{
} }
protected function createEntity(Location $location, Player $thrower) : Throwable{ protected function createEntity(Location $location, Player $thrower) : Throwable{
return new SplashPotionEntity($location, $thrower, $this->potionType); $projectile = new SplashPotionEntity($location, $thrower, $this->potionType);
$projectile->setLinger($this->linger);
return $projectile;
} }
public function getThrowForce() : float{ public function getThrowForce() : float{
return 0.5; return 0.5;
} }
/**
* Returns whether this splash potion will create an area-effect cloud on impact.
*/
public function willLinger() : bool{
return $this->linger;
}
} }

View File

@@ -1226,6 +1226,7 @@ final class StringToItemParser extends StringToTParser{
$result->register($prefix("potion"), fn() => Items::POTION()->setType($potionType)); $result->register($prefix("potion"), fn() => Items::POTION()->setType($potionType));
$result->register($prefix("splash_potion"), fn() => Items::SPLASH_POTION()->setType($potionType)); $result->register($prefix("splash_potion"), fn() => Items::SPLASH_POTION()->setType($potionType));
$result->register($prefix("lingering_potion"), fn() => Items::LINGERING_POTION()->setType($potionType));
} }
} }

View File

@@ -222,6 +222,7 @@ use function strtolower;
* @method static Armor LEATHER_CAP() * @method static Armor LEATHER_CAP()
* @method static Armor LEATHER_PANTS() * @method static Armor LEATHER_PANTS()
* @method static Armor LEATHER_TUNIC() * @method static Armor LEATHER_TUNIC()
* @method static SplashPotion LINGERING_POTION()
* @method static Item MAGMA_CREAM() * @method static Item MAGMA_CREAM()
* @method static Boat MANGROVE_BOAT() * @method static Boat MANGROVE_BOAT()
* @method static HangingSign MANGROVE_HANGING_SIGN() * @method static HangingSign MANGROVE_HANGING_SIGN()
@@ -543,6 +544,7 @@ final class VanillaItems{
self::register("lapis_lazuli", fn(IID $id) => new Item($id, "Lapis Lazuli")); self::register("lapis_lazuli", fn(IID $id) => new Item($id, "Lapis Lazuli"));
self::register("lava_bucket", fn(IID $id) => new LiquidBucket($id, "Lava Bucket", Blocks::LAVA())); self::register("lava_bucket", fn(IID $id) => new LiquidBucket($id, "Lava Bucket", Blocks::LAVA()));
self::register("leather", fn(IID $id) => new Item($id, "Leather")); self::register("leather", fn(IID $id) => new Item($id, "Leather"));
self::register("lingering_potion", fn(IID $id) => new SplashPotion($id, "Lingering Potion", linger: true));
self::register("magma_cream", fn(IID $id) => new Item($id, "Magma Cream")); self::register("magma_cream", fn(IID $id) => new Item($id, "Magma Cream"));
self::register("mangrove_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::MANGROVE_SIGN(), Blocks::MANGROVE_WALL_SIGN())); self::register("mangrove_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::MANGROVE_SIGN(), Blocks::MANGROVE_WALL_SIGN()));
self::register("mangrove_hanging_sign", fn(IID $id) => new HangingSign($id, "Mangrove Hanging Sign", Blocks::MANGROVE_CEILING_CENTER_HANGING_SIGN(), Blocks::MANGROVE_CEILING_EDGES_HANGING_SIGN(), Blocks::MANGROVE_WALL_HANGING_SIGN())); self::register("mangrove_hanging_sign", fn(IID $id) => new HangingSign($id, "Mangrove Hanging Sign", Blocks::MANGROVE_CEILING_CENTER_HANGING_SIGN(), Blocks::MANGROVE_CEILING_EDGES_HANGING_SIGN(), Blocks::MANGROVE_WALL_HANGING_SIGN()));