From 12214792b32a63a0668a2ecbc175f148bd7fe222 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 1 Dec 2024 17:42:26 +0000 Subject: [PATCH 1/3] Allow eating in creative & peaceful closes #5923 closes #6056 --- src/entity/FoodSource.php | 1 + src/entity/Human.php | 11 ++++++++++- src/entity/HungerManager.php | 3 ++- src/item/Food.php | 2 +- src/player/Player.php | 4 ++++ 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/entity/FoodSource.php b/src/entity/FoodSource.php index 98478b4a1..028c76783 100644 --- a/src/entity/FoodSource.php +++ b/src/entity/FoodSource.php @@ -34,6 +34,7 @@ interface FoodSource extends Consumable{ /** * Returns whether a Human eating this FoodSource must have a non-full hunger bar. + * This is ignored in creative mode and in peaceful difficulty. */ public function requiresHunger() : bool; } diff --git a/src/entity/Human.php b/src/entity/Human.php index f2c4c7a74..833196651 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -68,6 +68,7 @@ use pocketmine\network\mcpe\protocol\types\PlayerPermissions; use pocketmine\network\mcpe\protocol\UpdateAbilitiesPacket; use pocketmine\player\Player; use pocketmine\world\sound\TotemUseSound; +use pocketmine\world\World; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use function array_fill; @@ -189,8 +190,16 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ return $this->hungerManager; } + /** + * Returns whether the Human can eat food. This may return a different result than {@link HungerManager::isHungry()}, + * as HungerManager only handles the hunger bar. + */ + public function canEat() : bool{ + return $this->hungerManager->isHungry() || $this->getWorld()->getDifficulty() === World::DIFFICULTY_PEACEFUL; + } + public function consumeObject(Consumable $consumable) : bool{ - if($consumable instanceof FoodSource && $consumable->requiresHunger() && !$this->hungerManager->isHungry()){ + if($consumable instanceof FoodSource && $consumable->requiresHunger() && !$this->canEat()){ return false; } diff --git a/src/entity/HungerManager.php b/src/entity/HungerManager.php index a31855891..7e3b40e74 100644 --- a/src/entity/HungerManager.php +++ b/src/entity/HungerManager.php @@ -88,7 +88,8 @@ class HungerManager{ } /** - * Returns whether this Human may consume objects requiring hunger. + * Returns whether the food level is below the maximum. + * This doesn't decide if the entity can eat food. Use {@link Human::canEat()} for that. */ public function isHungry() : bool{ return $this->getFood() < $this->getMaxFood(); diff --git a/src/item/Food.php b/src/item/Food.php index 1950c4b14..d01ce9e18 100644 --- a/src/item/Food.php +++ b/src/item/Food.php @@ -44,6 +44,6 @@ abstract class Food extends Item implements FoodSourceItem{ } public function canStartUsingItem(Player $player) : bool{ - return !$this->requiresHunger() || $player->getHungerManager()->isHungry(); + return !$this->requiresHunger() || $player->canEat(); } } diff --git a/src/player/Player.php b/src/player/Player.php index e4c74c4ef..f20945b9a 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -1471,6 +1471,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ return true; } + public function canEat() : bool{ + return $this->isCreative() || parent::canEat(); + } + public function canBreathe() : bool{ return $this->isCreative() || parent::canBreathe(); } From f3763ae691ab73db8ca53d44db32b7b4e7439b90 Mon Sep 17 00:00:00 2001 From: IvanCraft623 <57236932+IvanCraft623@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:25:45 -0500 Subject: [PATCH 2/3] Implement Recovery compass (#5502) Co-authored-by: Dylan K. Taylor --- .../ItemSerializerDeserializerRegistrar.php | 1 + src/item/ItemTypeIds.php | 3 +- src/item/StringToItemParser.php | 1 + src/item/VanillaItems.php | 2 + src/player/Player.php | 55 +++++++++++++++++++ 5 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index 8240bf063..df1db4211 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -352,6 +352,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::RAW_COPPER, Items::RAW_COPPER()); $this->map1to1Item(Ids::RAW_GOLD, Items::RAW_GOLD()); $this->map1to1Item(Ids::RAW_IRON, Items::RAW_IRON()); + $this->map1to1Item(Ids::RECOVERY_COMPASS, Items::RECOVERY_COMPASS()); $this->map1to1Item(Ids::REDSTONE, Items::REDSTONE_DUST()); $this->map1to1Item(Ids::RIB_ARMOR_TRIM_SMITHING_TEMPLATE, Items::RIB_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::ROTTEN_FLESH, Items::ROTTEN_FLESH()); diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index f3ad406a6..c93c23e81 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -327,8 +327,9 @@ final class ItemTypeIds{ public const GOAT_HORN = 20288; public const END_CRYSTAL = 20289; public const ICE_BOMB = 20290; + public const RECOVERY_COMPASS = 20291; - public const FIRST_UNUSED_ITEM_ID = 20291; + public const FIRST_UNUSED_ITEM_ID = 20292; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 4cc9d29eb..09c93d5d9 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -1481,6 +1481,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("record_strad", fn() => Items::RECORD_STRAD()); $result->register("record_wait", fn() => Items::RECORD_WAIT()); $result->register("record_ward", fn() => Items::RECORD_WARD()); + $result->register("recovery_compass", fn() => Items::RECOVERY_COMPASS()); $result->register("redstone", fn() => Items::REDSTONE_DUST()); $result->register("redstone_dust", fn() => Items::REDSTONE_DUST()); $result->register("rib_armor_trim_smithing_template", fn() => Items::RIB_ARMOR_TRIM_SMITHING_TEMPLATE()); diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index dcf59daf6..6768ed8f0 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -284,6 +284,7 @@ use function strtolower; * @method static Record RECORD_STRAD() * @method static Record RECORD_WAIT() * @method static Record RECORD_WARD() + * @method static Item RECOVERY_COMPASS() * @method static Redstone REDSTONE_DUST() * @method static Item RIB_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static RottenFlesh ROTTEN_FLESH() @@ -574,6 +575,7 @@ final class VanillaItems{ self::register("record_strad", fn(IID $id) => new Record($id, RecordType::DISK_STRAD, "Record Strad")); self::register("record_wait", fn(IID $id) => new Record($id, RecordType::DISK_WAIT, "Record Wait")); self::register("record_ward", fn(IID $id) => new Record($id, RecordType::DISK_WARD, "Record Ward")); + self::register("recovery_compass", fn(IID $id) => new Item($id, "Recovery Compass")); self::register("redstone_dust", fn(IID $id) => new Redstone($id, "Redstone")); self::register("rotten_flesh", fn(IID $id) => new RottenFlesh($id, "Rotten Flesh")); self::register("scute", fn(IID $id) => new Item($id, "Scute")); diff --git a/src/player/Player.php b/src/player/Player.php index f20945b9a..bf911a2ff 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -110,6 +110,7 @@ use pocketmine\network\mcpe\protocol\AnimatePacket; use pocketmine\network\mcpe\protocol\MovePlayerPacket; use pocketmine\network\mcpe\protocol\SetActorMotionPacket; use pocketmine\network\mcpe\protocol\types\BlockPosition; +use pocketmine\network\mcpe\protocol\types\DimensionIds; use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags; use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties; @@ -191,6 +192,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ private const TAG_SPAWN_X = "SpawnX"; //TAG_Int private const TAG_SPAWN_Y = "SpawnY"; //TAG_Int private const TAG_SPAWN_Z = "SpawnZ"; //TAG_Int + private const TAG_DEATH_WORLD = "DeathLevel"; //TAG_String + private const TAG_DEATH_X = "DeathPositionX"; //TAG_Int + private const TAG_DEATH_Y = "DeathPositionY"; //TAG_Int + private const TAG_DEATH_Z = "DeathPositionZ"; //TAG_Int public const TAG_LEVEL = "Level"; //TAG_String public const TAG_LAST_KNOWN_XUID = "LastKnownXUID"; //TAG_String @@ -273,6 +278,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ private bool $respawnLocked = false; + private ?Position $deathPosition = null; + //TODO: Abilities protected bool $autoJump = true; protected bool $allowFlight = false; @@ -391,6 +398,9 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ if(($world = $this->server->getWorldManager()->getWorldByName($nbt->getString(self::TAG_SPAWN_WORLD, ""))) instanceof World){ $this->spawnPosition = new Position($nbt->getInt(self::TAG_SPAWN_X), $nbt->getInt(self::TAG_SPAWN_Y), $nbt->getInt(self::TAG_SPAWN_Z), $world); } + if(($world = $this->server->getWorldManager()->getWorldByName($nbt->getString(self::TAG_DEATH_WORLD, ""))) instanceof World){ + $this->deathPosition = new Position($nbt->getInt(self::TAG_DEATH_X), $nbt->getInt(self::TAG_DEATH_Y), $nbt->getInt(self::TAG_DEATH_Z), $world); + } } public function getLeaveMessage() : Translatable|string{ @@ -1032,6 +1042,30 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ } } + public function getDeathPosition() : ?Position{ + if($this->deathPosition !== null && !$this->deathPosition->isValid()){ + $this->deathPosition = null; + } + return $this->deathPosition; + } + + /** + * @param Vector3|Position|null $pos + */ + public function setDeathPosition(?Vector3 $pos) : void{ + if($pos !== null){ + if($pos instanceof Position && $pos->world !== null){ + $world = $pos->world; + }else{ + $world = $this->getWorld(); + } + $this->deathPosition = new Position($pos->x, $pos->y, $pos->z, $world); + }else{ + $this->deathPosition = null; + } + $this->networkPropertiesDirty = true; + } + /** * @return Position */ @@ -2336,6 +2370,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ unset($this->cursorInventory); unset($this->craftingGrid); $this->spawnPosition = null; + $this->deathPosition = null; $this->blockBreakHandler = null; parent::destroyCycles(); } @@ -2377,6 +2412,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $nbt->setInt(self::TAG_SPAWN_Z, $spawn->getFloorZ()); } + if($this->deathPosition !== null && $this->deathPosition->isValid()){ + $nbt->setString(self::TAG_DEATH_WORLD, $this->deathPosition->getWorld()->getFolderName()); + $nbt->setInt(self::TAG_DEATH_X, $this->deathPosition->getFloorX()); + $nbt->setInt(self::TAG_DEATH_Y, $this->deathPosition->getFloorY()); + $nbt->setInt(self::TAG_DEATH_Z, $this->deathPosition->getFloorZ()); + } + $nbt->setInt(self::TAG_GAME_MODE, GameModeIdMap::getInstance()->toId($this->gamemode)); $nbt->setLong(self::TAG_FIRST_PLAYED, $this->firstPlayed); $nbt->setLong(self::TAG_LAST_PLAYED, (int) floor(microtime(true) * 1000)); @@ -2396,6 +2438,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ //main inventory and drops the rest on the ground. $this->removeCurrentWindow(); + $this->setDeathPosition($this->getPosition()); + $ev = new PlayerDeathEvent($this, $this->getDrops(), $this->getXpDropAmount(), null); $ev->call(); @@ -2524,6 +2568,17 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $properties->setPlayerFlag(PlayerMetadataFlags::SLEEP, $this->sleeping !== null); $properties->setBlockPos(EntityMetadataProperties::PLAYER_BED_POSITION, $this->sleeping !== null ? BlockPosition::fromVector3($this->sleeping) : new BlockPosition(0, 0, 0)); + + if($this->deathPosition !== null && $this->deathPosition->world === $this->location->world){ + $properties->setBlockPos(EntityMetadataProperties::PLAYER_DEATH_POSITION, BlockPosition::fromVector3($this->deathPosition)); + //TODO: this should be updated when dimensions are implemented + $properties->setInt(EntityMetadataProperties::PLAYER_DEATH_DIMENSION, DimensionIds::OVERWORLD); + $properties->setByte(EntityMetadataProperties::PLAYER_HAS_DIED, 1); + }else{ + $properties->setBlockPos(EntityMetadataProperties::PLAYER_DEATH_POSITION, new BlockPosition(0, 0, 0)); + $properties->setInt(EntityMetadataProperties::PLAYER_DEATH_DIMENSION, DimensionIds::OVERWORLD); + $properties->setByte(EntityMetadataProperties::PLAYER_HAS_DIED, 0); + } } public function sendData(?array $targets, ?array $data = null) : void{ From f1a3b42620c11a6e9be99f9d014084f1bd6fe490 Mon Sep 17 00:00:00 2001 From: IvanCraft623 <57236932+IvanCraft623@users.noreply.github.com> Date: Sun, 1 Dec 2024 14:46:38 -0500 Subject: [PATCH 3/3] Implement frost walker enchantment (#5497) Co-authored-by: Dylan T. --- src/block/Magma.php | 2 +- src/data/bedrock/EnchantmentIdMap.php | 2 + src/entity/Living.php | 55 +++++++++++++++++++ .../AvailableEnchantmentRegistry.php | 1 + .../enchantment/StringToEnchantmentParser.php | 1 + src/item/enchantment/VanillaEnchantments.php | 11 ++++ 6 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/block/Magma.php b/src/block/Magma.php index d2f309325..7b3fa5229 100644 --- a/src/block/Magma.php +++ b/src/block/Magma.php @@ -39,7 +39,7 @@ class Magma extends Opaque{ } public function onEntityInside(Entity $entity) : bool{ - if($entity instanceof Living && !$entity->isSneaking()){ + if($entity instanceof Living && !$entity->isSneaking() && $entity->getFrostWalkerLevel() === 0){ $ev = new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_FIRE, 1); $entity->attack($ev); } diff --git a/src/data/bedrock/EnchantmentIdMap.php b/src/data/bedrock/EnchantmentIdMap.php index e3d652b19..90a10dc20 100644 --- a/src/data/bedrock/EnchantmentIdMap.php +++ b/src/data/bedrock/EnchantmentIdMap.php @@ -66,5 +66,7 @@ final class EnchantmentIdMap{ $this->register(EnchantmentIds::VANISHING, VanillaEnchantments::VANISHING()); $this->register(EnchantmentIds::SWIFT_SNEAK, VanillaEnchantments::SWIFT_SNEAK()); + + $this->register(EnchantmentIds::FROST_WALKER, VanillaEnchantments::FROST_WALKER()); } } diff --git a/src/entity/Living.php b/src/entity/Living.php index 1f27a5cac..852344784 100644 --- a/src/entity/Living.php +++ b/src/entity/Living.php @@ -25,6 +25,8 @@ namespace pocketmine\entity; use pocketmine\block\Block; use pocketmine\block\BlockTypeIds; +use pocketmine\block\VanillaBlocks; +use pocketmine\block\Water; use pocketmine\data\bedrock\EffectIdMap; use pocketmine\entity\animation\DeathAnimation; use pocketmine\entity\animation\HurtAnimation; @@ -44,6 +46,7 @@ use pocketmine\item\Durable; use pocketmine\item\enchantment\Enchantment; use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\item\Item; +use pocketmine\math\AxisAlignedBB; use pocketmine\math\Vector3; use pocketmine\math\VoxelRayTrace; use pocketmine\nbt\tag\CompoundTag; @@ -64,6 +67,7 @@ use pocketmine\world\sound\EntityLandSound; use pocketmine\world\sound\EntityLongFallSound; use pocketmine\world\sound\EntityShortFallSound; use pocketmine\world\sound\ItemBreakSound; +use function abs; use function array_shift; use function atan2; use function ceil; @@ -128,6 +132,8 @@ abstract class Living extends Entity{ protected bool $gliding = false; protected bool $swimming = false; + private ?int $frostWalkerLevel = null; + protected function getInitialDragMultiplier() : float{ return 0.02; } protected function getInitialGravity() : float{ return 0.08; } @@ -151,6 +157,14 @@ abstract class Living extends Entity{ $this->getViewers(), fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobArmorChange($recipients, $this) ))); + $this->armorInventory->getListeners()->add(new CallbackInventoryListener( + onSlotChange: function(Inventory $inventory, int $slot) : void{ + if($slot === ArmorInventory::SLOT_FEET){ + $this->frostWalkerLevel = null; + } + }, + onContentChange: function() : void{ $this->frostWalkerLevel = null; } + )); $health = $this->getMaxHealth(); @@ -687,6 +701,47 @@ abstract class Living extends Entity{ return $hasUpdate; } + protected function move(float $dx, float $dy, float $dz) : void{ + $oldX = $this->location->x; + $oldZ = $this->location->z; + + parent::move($dx, $dy, $dz); + + $frostWalkerLevel = $this->getFrostWalkerLevel(); + if($frostWalkerLevel > 0 && (abs($this->location->x - $oldX) > self::MOTION_THRESHOLD || abs($this->location->z - $oldZ) > self::MOTION_THRESHOLD)){ + $this->applyFrostWalker($frostWalkerLevel); + } + } + + protected function applyFrostWalker(int $level) : void{ + $radius = $level + 2; + $world = $this->getWorld(); + + $baseX = $this->location->getFloorX(); + $y = $this->location->getFloorY() - 1; + $baseZ = $this->location->getFloorZ(); + + $frostedIce = VanillaBlocks::FROSTED_ICE(); + for($x = $baseX - $radius; $x <= $baseX + $radius; $x++){ + for($z = $baseZ - $radius; $z <= $baseZ + $radius; $z++){ + $block = $world->getBlockAt($x, $y, $z); + if( + !$block instanceof Water || + !$block->isSource() || + $world->getBlockAt($x, $y + 1, $z)->getTypeId() !== BlockTypeIds::AIR || + count($world->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z))) !== 0 + ){ + continue; + } + $world->setBlockAt($x, $y, $z, $frostedIce); + } + } + } + + public function getFrostWalkerLevel() : int{ + return $this->frostWalkerLevel ??= $this->armorInventory->getBoots()->getEnchantmentLevel(VanillaEnchantments::FROST_WALKER()); + } + /** * Ticks the entity's air supply, consuming it when underwater and regenerating it when out of water. */ diff --git a/src/item/enchantment/AvailableEnchantmentRegistry.php b/src/item/enchantment/AvailableEnchantmentRegistry.php index cae94c666..eed7bff52 100644 --- a/src/item/enchantment/AvailableEnchantmentRegistry.php +++ b/src/item/enchantment/AvailableEnchantmentRegistry.php @@ -57,6 +57,7 @@ final class AvailableEnchantmentRegistry{ $this->register(Enchantments::THORNS(), [Tags::CHESTPLATE], [Tags::HELMET, Tags::LEGGINGS, Tags::BOOTS]); $this->register(Enchantments::RESPIRATION(), [Tags::HELMET], []); $this->register(Enchantments::AQUA_AFFINITY(), [Tags::HELMET], []); + $this->register(Enchantments::FROST_WALKER(), [/* no primary items */], [Tags::BOOTS]); $this->register(Enchantments::SHARPNESS(), [Tags::SWORD, Tags::AXE], []); $this->register(Enchantments::KNOCKBACK(), [Tags::SWORD], []); $this->register(Enchantments::FIRE_ASPECT(), [Tags::SWORD], []); diff --git a/src/item/enchantment/StringToEnchantmentParser.php b/src/item/enchantment/StringToEnchantmentParser.php index 47a750ff2..b6763e491 100644 --- a/src/item/enchantment/StringToEnchantmentParser.php +++ b/src/item/enchantment/StringToEnchantmentParser.php @@ -44,6 +44,7 @@ final class StringToEnchantmentParser extends StringToTParser{ $result->register("fire_protection", fn() => VanillaEnchantments::FIRE_PROTECTION()); $result->register("flame", fn() => VanillaEnchantments::FLAME()); $result->register("fortune", fn() => VanillaEnchantments::FORTUNE()); + $result->register("frost_walker", fn() => VanillaEnchantments::FROST_WALKER()); $result->register("infinity", fn() => VanillaEnchantments::INFINITY()); $result->register("knockback", fn() => VanillaEnchantments::KNOCKBACK()); $result->register("mending", fn() => VanillaEnchantments::MENDING()); diff --git a/src/item/enchantment/VanillaEnchantments.php b/src/item/enchantment/VanillaEnchantments.php index 19ce39716..1132dc9c6 100644 --- a/src/item/enchantment/VanillaEnchantments.php +++ b/src/item/enchantment/VanillaEnchantments.php @@ -41,6 +41,7 @@ use pocketmine\utils\RegistryTrait; * @method static ProtectionEnchantment FIRE_PROTECTION() * @method static Enchantment FLAME() * @method static Enchantment FORTUNE() + * @method static Enchantment FROST_WALKER() * @method static Enchantment INFINITY() * @method static KnockbackEnchantment KNOCKBACK() * @method static Enchantment MENDING() @@ -145,6 +146,16 @@ final class VanillaEnchantments{ fn(int $level) : int => 10 * $level, 30 )); + + self::register("FROST_WALKER", new Enchantment( + KnownTranslationFactory::enchantment_frostwalker(), + Rarity::RARE, + 0, + 0, + 2, + fn(int $level) : int => 10 * $level, + 15 + )); self::register("AQUA_AFFINITY", new Enchantment( KnownTranslationFactory::enchantment_waterWorker(), Rarity::RARE,