diff --git a/changelogs/5.21.md b/changelogs/5.21.md new file mode 100644 index 00000000000..b8131a3c8b9 --- /dev/null +++ b/changelogs/5.21.md @@ -0,0 +1,103 @@ +# 5.21.0 +Released 3rd November 2024. + +This is a minor feature release, including gameplay features and minor internals improvements. + +**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace. +Do not update plugin minimum API versions unless you need new features added in this release. + +**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.** +Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly. + +## Gameplay +- Added the following new blocks: + - Campfire + - Chiseled Copper + - Chiseled Tuff + - Chiseled Tuff Bricks + - Copper Bulb + - Copper Door + - Copper Grate + - Copper Trapdoor + - Polished Tuff, Slabs, Stairs and Walls + - Soul Campfire + - Tuff Bricks, Slabs, Stairs and Walls + - Tuff Slab, Stairs and Walls +- Added the following new types of painting: + - backyard + - baroque + - bouquet + - cavebird + - changing + - cotan + - endboss + - fern + - finding + - humble + - lowmist + - meditative + - orb + - owlemons + - passage + - pond + - prairie_ride + - sunflowers + - tides + - unpacked +- Armor slots are now properly restricted (on the server side) to only contain the appropriate type of armor or headwear. +- Implemented Aqua Affinity enchantment. Since the server doesn't currently enforce any movement restrictions in water, this enchantment works based on client-side behaviour only. + +## API +### `pocketmine\block` +- The following new API methods have been added: + - `public ChiseledBookshelf->getLastInteractedSlot() : ?ChiseledBookshelfSlot` + - `public ChiseledBookshelf->setLastInteractedSlot(?ChiseledBookshelfSlot $lastInteractedSlot) : $this` +- The following new classes have been added: + - `utils\CopperMaterial` - interface implemented by all copper-like blocks with oxidation and waxed properties + - `CopperBulb` + - `CopperDoor` + - `CopperGrate` + - `CopperTrapdoor` + - `SoulCampfire` + - `Campfire` +- The following enums have new cases: + - `utils\BannerPatternType` has new cases `FLOW` and `GUSTER` + +### `pocketmine\crafting` +- The following enums have new cases: + - `FurnaceType` has new cases `CAMPFIRE` and `SOUL_CAMPFIRE` + +### `pocketmine\event` +- The following new classes have been added: + - `block\CampfireCookEvent` - called when a campfire finishes cooking an item + +### `pocketmine\inventory` +- Added support for slot validators, which permit restricting the types of items a player can put into an inventory slot. + - The following new classes have been added: + - `transaction\action\SlotValidator` - interface + - `transaction\action\CallbackSlotValidator` - class allowing a closure to be used for slot content validation + - `SlotValidatedInventory` - implemented by inventories which support the use of slot validators + +### `pocketmine\item` +- The following new API methods have been added: + - `public Item->getCooldownTag() : ?string` - returns the cooldown group this item belongs to, used for ensuring that, for example, different types of goat horns all respect a general horn cooldown +- The following new classes have been added: + - `ItemCooldownTags` - list of cooldown group tags used by PocketMine-MP + +### `pocketmine\world\sound` +- The following new classes have been added + - `CampfireSound` - sound made by campfires while lit + +## Tools +- `tools/blockstate-upgrade-schema-utils.php` (formerly `generate-blockstate-upgrade-schema.php`) has several improvements: + - Support for generating `flattenedProperties` rules as per [BedrockBlockUpgradeSchema 5.0.0](https://github.com/pmmp/BedrockBlockUpgradeSchema/releases/tag/5.0.0) + - Improved criteria for flattened property selection to minimize the amount of rules required + - Several subcommands are now available: + - `generate` - generates a schema from provided data + - `update` - regenerates an existing schema in a newer format + - `update-all` - regenerates a folder of existing schemas in a newer format (useful for updating `BedrockBlockUpgradeSchema` en masse) + - `test` - verifies that a schema produces the results expected by provided data + +## Internals +- Fixed incorrect visibility of `createEntity` in spawn eggs. +- Added support for newer `BedrockBlockUpgradeSchema` in `BlockStateUpgrader`. diff --git a/composer.json b/composer.json index 9ecbad32fbe..ed5bb582f37 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "composer-runtime-api": "^2.0", "adhocore/json-comment": "~1.2.0", "pocketmine/netresearch-jsonmapper": "~v4.4.999", - "pocketmine/bedrock-block-upgrade-schema": "~4.5.0+bedrock-1.21.40", + "pocketmine/bedrock-block-upgrade-schema": "~5.0.0+bedrock-1.21.40", "pocketmine/bedrock-data": "~2.14.0+bedrock-1.21.40", "pocketmine/bedrock-item-upgrade-schema": "~1.13.0+bedrock-1.21.40", "pocketmine/bedrock-protocol": "~35.0.0+bedrock-1.21.40", diff --git a/composer.lock b/composer.lock index d4cb38ddcdc..b2f3e7858e2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5c5882370131d2ae3a043819c05e6f9c", + "content-hash": "b2fbf6e7a9d650341dc71fa4dd124681", "packages": [ { "name": "adhocore/json-comment", @@ -127,16 +127,16 @@ }, { "name": "pocketmine/bedrock-block-upgrade-schema", - "version": "4.5.0", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git", - "reference": "7943b894e050d68dd21b5c7fa609827a4e2e30f1" + "reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/7943b894e050d68dd21b5c7fa609827a4e2e30f1", - "reference": "7943b894e050d68dd21b5c7fa609827a4e2e30f1", + "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/20dd5c11e9915bacea4fe2cf649e1d23697a6e52", + "reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52", "shasum": "" }, "type": "library", @@ -147,9 +147,9 @@ "description": "Schemas describing how to upgrade saved block data in older Minecraft: Bedrock Edition world saves", "support": { "issues": "https://github.com/pmmp/BedrockBlockUpgradeSchema/issues", - "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/4.5.0" + "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.0.0" }, - "time": "2024-10-23T16:15:24+00:00" + "time": "2024-11-03T14:13:50+00:00" }, { "name": "pocketmine/bedrock-data", diff --git a/src/VersionInfo.php b/src/VersionInfo.php index 5de1f5a0593..4dc9ea7f9d3 100644 --- a/src/VersionInfo.php +++ b/src/VersionInfo.php @@ -31,8 +31,8 @@ final class VersionInfo{ public const NAME = "PocketMine-MP"; - public const BASE_VERSION = "5.20.2"; - public const IS_DEVELOPMENT_BUILD = true; + public const BASE_VERSION = "5.21.0"; + public const IS_DEVELOPMENT_BUILD = false; public const BUILD_CHANNEL = "stable"; /** diff --git a/src/block/BlockTypeIds.php b/src/block/BlockTypeIds.php index dec31eff11f..3914a4b74a1 100644 --- a/src/block/BlockTypeIds.php +++ b/src/block/BlockTypeIds.php @@ -745,8 +745,28 @@ private function __construct(){ public const PITCHER_PLANT = 10715; public const PITCHER_CROP = 10716; public const DOUBLE_PITCHER_CROP = 10717; + public const CAMPFIRE = 10718; + public const SOUL_CAMPFIRE = 10719; + public const TUFF_SLAB = 10720; + public const TUFF_STAIRS = 10721; + public const TUFF_WALL = 10722; + public const CHISELED_TUFF = 10723; + public const TUFF_BRICKS = 10724; + public const TUFF_BRICK_SLAB = 10725; + public const TUFF_BRICK_STAIRS = 10726; + public const TUFF_BRICK_WALL = 10727; + public const CHISELED_TUFF_BRICKS = 10728; + public const POLISHED_TUFF = 10729; + public const POLISHED_TUFF_SLAB = 10730; + public const POLISHED_TUFF_STAIRS = 10731; + public const POLISHED_TUFF_WALL = 10732; + public const COPPER_BULB = 10733; + public const COPPER_DOOR = 10734; + public const COPPER_TRAPDOOR = 10735; + public const CHISELED_COPPER = 10736; + public const COPPER_GRATE = 10737; - public const FIRST_UNUSED_BLOCK_ID = 10718; + public const FIRST_UNUSED_BLOCK_ID = 10738; private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID; diff --git a/src/block/Campfire.php b/src/block/Campfire.php new file mode 100644 index 00000000000..ce759ee87f7 --- /dev/null +++ b/src/block/Campfire.php @@ -0,0 +1,277 @@ + ticks + * @phpstan-var array + */ + protected array $cookingTimes = []; + + protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ + $this->encodeFacingState($w); + $this->encodeLitState($w); + } + + public function readStateFromWorld() : Block{ + parent::readStateFromWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileCampfire){ + $this->inventory = $tile->getInventory(); + $this->cookingTimes = $tile->getCookingTimes(); + }else{ + $this->inventory = new CampfireInventory($this->position); + } + + return $this; + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileCampfire){ + $tile->setCookingTimes($this->cookingTimes); + } + } + + public function hasEntityCollision() : bool{ + return true; + } + + public function getLightLevel() : int{ + return $this->lit ? 15 : 0; + } + + public function isAffectedBySilkTouch() : bool{ + return true; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaItems::CHARCOAL()->setCount(2) + ]; + } + + public function getSupportType(int $facing) : SupportType{ + return SupportType::NONE; + } + + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::UP, 9 / 16)]; + } + + public function getInventory() : CampfireInventory{ + return $this->inventory; + } + + protected function getFurnaceType() : FurnaceType{ + return FurnaceType::CAMPFIRE; + } + + protected function getEntityCollisionDamage() : int{ + return 1; + } + + /** + * Sets the number of ticks during the item in the given slot has been cooked. + */ + public function setCookingTime(int $slot, int $time) : void{ + if($slot < 0 || $slot > 3){ + throw new \InvalidArgumentException("Slot must be in range 0-3"); + } + if($time < 0 || $time > $this->getFurnaceType()->getCookDurationTicks()){ + throw new \InvalidArgumentException("CookingTime must be in range 0-" . $this->getFurnaceType()->getCookDurationTicks()); + } + $this->cookingTimes[$slot] = $time; + } + + /** + * Returns the number of ticks during the item in the given slot has been cooked. + */ + public function getCookingTime(int $slot) : int{ + return $this->cookingTimes[$slot] ?? 0; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + if($this->getSide(Facing::DOWN) instanceof Campfire){ + return false; + } + if($player !== null){ + $this->facing = $player->getHorizontalFacing(); + } + $this->lit = true; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + if(!$this->lit){ + if($item->getTypeId() === ItemTypeIds::FIRE_CHARGE){ + $item->pop(); + $this->ignite(); + $this->position->getWorld()->addSound($this->position, new BlazeShootSound()); + return true; + }elseif($item->getTypeId() === ItemTypeIds::FLINT_AND_STEEL || $item->hasEnchantment(VanillaEnchantments::FIRE_ASPECT())){ + if($item instanceof Durable){ + $item->applyDamage(1); + } + $this->ignite(); + return true; + } + }elseif($item instanceof Shovel){ + $item->applyDamage(1); + $this->extinguish(); + return true; + } + + if($this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($this->getFurnaceType())->match($item) !== null){ + $ingredient = clone $item; + $ingredient->setCount(1); + if(count($this->inventory->addItem($ingredient)) === 0){ + $item->pop(); + $this->position->getWorld()->addSound($this->position, new ItemFrameAddItemSound()); + return true; + } + } + return false; + } + + public function onNearbyBlockChange() : void{ + if($this->lit && $this->getSide(Facing::UP)->getTypeId() === BlockTypeIds::WATER){ + $this->extinguish(); + //TODO: Waterlogging + } + } + + public function onEntityInside(Entity $entity) : bool{ + if(!$this->lit){ + if($entity->isOnFire()){ + $this->ignite(); + return false; + } + }elseif($entity instanceof Living){ + $entity->attack(new EntityDamageByBlockEvent($this, $entity, EntityDamageEvent::CAUSE_FIRE, $this->getEntityCollisionDamage())); + } + return true; + } + + public function onProjectileHit(Projectile $projectile, RayTraceResult $hitResult) : void{ + if($this->lit && $projectile instanceof SplashPotion && $projectile->getPotionType() === PotionType::WATER){ + $this->extinguish(); + } + } + + public function onScheduledUpdate() : void{ + if($this->lit){ + $items = $this->inventory->getContents(); + $furnaceType = $this->getFurnaceType(); + $maxCookDuration = $furnaceType->getCookDurationTicks(); + foreach($items as $slot => $item){ + $this->setCookingTime($slot, min($maxCookDuration, $this->getCookingTime($slot) + self::UPDATE_INTERVAL_TICKS)); + if($this->getCookingTime($slot) >= $maxCookDuration){ + $result = + ($recipe = $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($furnaceType)->match($item)) instanceof FurnaceRecipe ? + $recipe->getResult() : + VanillaItems::AIR(); + + $ev = new CampfireCookEvent($this, $slot, $item, $result); + $ev->call(); + + if ($ev->isCancelled()){ + continue; + } + + $this->inventory->setItem($slot, VanillaItems::AIR()); + $this->setCookingTime($slot, 0); + $this->position->getWorld()->dropItem($this->position->add(0.5, 1, 0.5), $ev->getResult()); + } + } + if(count($items) > 0){ + $this->position->getWorld()->setBlock($this->position, $this); + } + if(mt_rand(1, 6) === 1){ + $this->position->getWorld()->addSound($this->position, $furnaceType->getCookSound()); + } + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, self::UPDATE_INTERVAL_TICKS); + } + } + + private function extinguish() : void{ + $this->position->getWorld()->addSound($this->position, new FireExtinguishSound()); + $this->position->getWorld()->setBlock($this->position, $this->setLit(false)); + } + + private function ignite() : void{ + $this->position->getWorld()->addSound($this->position, new FlintSteelSound()); + $this->position->getWorld()->setBlock($this->position, $this->setLit(true)); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, self::UPDATE_INTERVAL_TICKS); + } +} diff --git a/src/block/ChiseledBookshelf.php b/src/block/ChiseledBookshelf.php index 89340a8f39b..73c4861bf35 100644 --- a/src/block/ChiseledBookshelf.php +++ b/src/block/ChiseledBookshelf.php @@ -48,11 +48,32 @@ class ChiseledBookshelf extends Opaque{ */ private array $slots = []; + private ?ChiseledBookshelfSlot $lastInteractedSlot = null; + protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ $w->horizontalFacing($this->facing); $w->enumSet($this->slots, ChiseledBookshelfSlot::cases()); } + public function readStateFromWorld() : Block{ + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileChiseledBookshelf){ + $this->lastInteractedSlot = $tile->getLastInteractedSlot(); + }else{ + $this->lastInteractedSlot = null; + } + return $this; + } + + public function writeStateToWorld() : void{ + parent::writeStateToWorld(); + + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileChiseledBookshelf){ + $tile->setLastInteractedSlot($this->lastInteractedSlot); + } + } + /** * Returns whether the given slot is displayed as occupied. * This doesn't guarantee that there is or isn't a book in the bookshelf's inventory. @@ -92,6 +113,23 @@ public function getSlots() : array{ return $this->slots; } + /** + * Returns the last slot interacted by a player or null if no slot has been interacted with yet. + */ + public function getLastInteractedSlot() : ?ChiseledBookshelfSlot{ + return $this->lastInteractedSlot; + } + + /** + * Sets the last slot interacted by a player. + * + * @return $this + */ + public function setLastInteractedSlot(?ChiseledBookshelfSlot $lastInteractedSlot) : self{ + $this->lastInteractedSlot = $lastInteractedSlot; + return $this; + } + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($face !== $this->facing){ return false; @@ -112,10 +150,12 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $returnedItems[] = $inventory->getItem($slot->value); $inventory->clear($slot->value); $this->setSlot($slot, false); + $this->lastInteractedSlot = $slot; }elseif($item instanceof WritableBookBase || $item instanceof Book || $item instanceof EnchantedBook){ //TODO: type tags like blocks would be better for this $inventory->setItem($slot->value, $item->pop()); $this->setSlot($slot, true); + $this->lastInteractedSlot = $slot; }else{ return true; } diff --git a/src/block/Copper.php b/src/block/Copper.php index 1da253fa418..d285e6ec0ae 100644 --- a/src/block/Copper.php +++ b/src/block/Copper.php @@ -23,8 +23,9 @@ namespace pocketmine\block; +use pocketmine\block\utils\CopperMaterial; use pocketmine\block\utils\CopperTrait; -class Copper extends Opaque{ +class Copper extends Opaque implements CopperMaterial{ use CopperTrait; } diff --git a/src/block/CopperBulb.php b/src/block/CopperBulb.php new file mode 100644 index 00000000000..97fc209fee3 --- /dev/null +++ b/src/block/CopperBulb.php @@ -0,0 +1,69 @@ +encodeLitState($w); + $w->bool($this->powered); + } + + /** @return $this */ + public function togglePowered(bool $powered) : self{ + if($powered === $this->powered){ + return $this; + } + if ($powered) { + $this->setLit(!$this->lit); + } + $this->setPowered($powered); + return $this; + } + + public function getLightLevel() : int{ + if ($this->lit) { + return match($this->oxidation){ + CopperOxidation::NONE => 15, + CopperOxidation::EXPOSED => 12, + CopperOxidation::WEATHERED => 8, + CopperOxidation::OXIDIZED => 4, + }; + } + + return 0; + } +} diff --git a/src/block/CopperDoor.php b/src/block/CopperDoor.php new file mode 100644 index 00000000000..82a611206cf --- /dev/null +++ b/src/block/CopperDoor.php @@ -0,0 +1,53 @@ +isSneaking() && $this->onInteractCopper($item, $face, $clickVector, $player, $returnedItems)) { + //copy copper properties to other half + $other = $this->getSide($this->top ? Facing::DOWN : Facing::UP); + $world = $this->position->getWorld(); + if ($other instanceof CopperDoor) { + $other->setOxidation($this->oxidation); + $other->setWaxed($this->waxed); + $world->setBlock($other->position, $other); + } + return true; + } + + return parent::onInteract($item, $face, $clickVector, $player, $returnedItems); + } +} diff --git a/src/block/CopperGrate.php b/src/block/CopperGrate.php new file mode 100644 index 00000000000..d646d133342 --- /dev/null +++ b/src/block/CopperGrate.php @@ -0,0 +1,33 @@ +isSneaking() && $this->onInteractCopper($item, $face, $clickVector, $player, $returnedItems)) { + return true; + } + + return parent::onInteract($item, $face, $clickVector, $player, $returnedItems); + } +} diff --git a/src/block/SoulCampfire.php b/src/block/SoulCampfire.php new file mode 100644 index 00000000000..a9c8fc918f1 --- /dev/null +++ b/src/block/SoulCampfire.php @@ -0,0 +1,48 @@ +lit ? 10 : 0; + } + + public function getDropsForCompatibleTool(Item $item) : array{ + return [ + VanillaBlocks::SOUL_SOIL()->asItem() + ]; + } + + protected function getEntityCollisionDamage() : int{ + return 2; + } + + protected function getFurnaceType() : FurnaceType{ + return FurnaceType::SOUL_CAMPFIRE; + } +} diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index 9c0e7d3b7c7..60540dfb832 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -36,6 +36,7 @@ use pocketmine\block\tile\Bell as TileBell; use pocketmine\block\tile\BlastFurnace as TileBlastFurnace; use pocketmine\block\tile\BrewingStand as TileBrewingStand; +use pocketmine\block\tile\Campfire as TileCampfire; use pocketmine\block\tile\Cauldron as TileCauldron; use pocketmine\block\tile\Chest as TileChest; use pocketmine\block\tile\ChiseledBookshelf as TileChiseledBookshelf; @@ -154,6 +155,7 @@ * @method static CakeWithCandle CAKE_WITH_CANDLE() * @method static CakeWithDyedCandle CAKE_WITH_DYED_CANDLE() * @method static Opaque CALCITE() + * @method static Campfire CAMPFIRE() * @method static Candle CANDLE() * @method static Carpet CARPET() * @method static Carrot CARROTS() @@ -179,6 +181,7 @@ * @method static Wood CHERRY_WOOD() * @method static Chest CHEST() * @method static ChiseledBookshelf CHISELED_BOOKSHELF() + * @method static Copper CHISELED_COPPER() * @method static Opaque CHISELED_DEEPSLATE() * @method static Opaque CHISELED_NETHER_BRICKS() * @method static Opaque CHISELED_POLISHED_BLACKSTONE() @@ -186,6 +189,8 @@ * @method static Opaque CHISELED_RED_SANDSTONE() * @method static Opaque CHISELED_SANDSTONE() * @method static Opaque CHISELED_STONE_BRICKS() + * @method static Opaque CHISELED_TUFF() + * @method static Opaque CHISELED_TUFF_BRICKS() * @method static ChorusFlower CHORUS_FLOWER() * @method static ChorusPlant CHORUS_PLANT() * @method static Clay CLAY() @@ -205,7 +210,11 @@ * @method static Concrete CONCRETE() * @method static ConcretePowder CONCRETE_POWDER() * @method static Copper COPPER() + * @method static CopperBulb COPPER_BULB() + * @method static CopperDoor COPPER_DOOR() + * @method static CopperGrate COPPER_GRATE() * @method static CopperOre COPPER_ORE() + * @method static CopperTrapdoor COPPER_TRAPDOOR() * @method static Coral CORAL() * @method static CoralBlock CORAL_BLOCK() * @method static FloorCoralFan CORAL_FAN() @@ -607,6 +616,10 @@ * @method static Opaque POLISHED_GRANITE() * @method static Slab POLISHED_GRANITE_SLAB() * @method static Stair POLISHED_GRANITE_STAIRS() + * @method static Opaque POLISHED_TUFF() + * @method static Slab POLISHED_TUFF_SLAB() + * @method static Stair POLISHED_TUFF_STAIRS() + * @method static Wall POLISHED_TUFF_WALL() * @method static Flower POPPY() * @method static Potato POTATOES() * @method static PotionCauldron POTION_CAULDRON() @@ -685,6 +698,7 @@ * @method static Slab SMOOTH_STONE_SLAB() * @method static Snow SNOW() * @method static SnowLayer SNOW_LAYER() + * @method static SoulCampfire SOUL_CAMPFIRE() * @method static SoulFire SOUL_FIRE() * @method static Lantern SOUL_LANTERN() * @method static SoulSand SOUL_SAND() @@ -735,6 +749,13 @@ * @method static Tripwire TRIPWIRE() * @method static TripwireHook TRIPWIRE_HOOK() * @method static Opaque TUFF() + * @method static Opaque TUFF_BRICKS() + * @method static Slab TUFF_BRICK_SLAB() + * @method static Stair TUFF_BRICK_STAIRS() + * @method static Wall TUFF_BRICK_WALL() + * @method static Slab TUFF_SLAB() + * @method static Stair TUFF_STAIRS() + * @method static Wall TUFF_WALL() * @method static NetherVines TWISTING_VINES() * @method static UnderwaterTorch UNDERWATER_TORCH() * @method static Vine VINES() @@ -826,6 +847,11 @@ public function getBreakTime(Item $item) : float{ self::register("brown_mushroom", new BrownMushroom(new BID(Ids::BROWN_MUSHROOM), "Brown Mushroom", new Info(BreakInfo::instant(), [Tags::POTTABLE_PLANTS]))); self::register("cactus", new Cactus(new BID(Ids::CACTUS), "Cactus", new Info(new BreakInfo(0.4), [Tags::POTTABLE_PLANTS]))); self::register("cake", new Cake(new BID(Ids::CAKE), "Cake", new Info(new BreakInfo(0.5)))); + + $campfireBreakInfo = new Info(BreakInfo::axe(2.0)); + self::register("campfire", new Campfire(new BID(Ids::CAMPFIRE, TileCampfire::class), "Campfire", $campfireBreakInfo)); + self::register("soul_campfire", new SoulCampfire(new BID(Ids::SOUL_CAMPFIRE, TileCampfire::class), "Soul Campfire", $campfireBreakInfo)); + self::register("carrots", new Carrot(new BID(Ids::CARROTS), "Carrot Block", new Info(BreakInfo::instant()))); $chestBreakInfo = new Info(BreakInfo::axe(2.5)); @@ -1261,6 +1287,7 @@ public function getDropsForCompatibleTool(Item $item) : array{ self::registerBlocksR17(); self::registerBlocksR18(); self::registerMudBlocks(); + self::registerTuffBlocks(); self::registerCraftingTables(); self::registerChorusBlocks(); @@ -1568,7 +1595,6 @@ private static function registerBlocksR17() : void{ self::register("amethyst_cluster", new AmethystCluster(new BID(Ids::AMETHYST_CLUSTER), "Amethyst Cluster", $amethystInfo)); self::register("calcite", new Opaque(new BID(Ids::CALCITE), "Calcite", new Info(BreakInfo::pickaxe(0.75, ToolTier::WOOD)))); - self::register("tuff", new Opaque(new BID(Ids::TUFF), "Tuff", new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)))); self::register("raw_copper", new Opaque(new BID(Ids::RAW_COPPER), "Raw Copper Block", new Info(BreakInfo::pickaxe(5, ToolTier::STONE, 30.0)))); self::register("raw_gold", new Opaque(new BID(Ids::RAW_GOLD), "Raw Gold Block", new Info(BreakInfo::pickaxe(5, ToolTier::IRON, 30.0)))); @@ -1621,9 +1647,16 @@ public function isAffectedBySilkTouch() : bool{ self::register("lightning_rod", new LightningRod(new BID(Ids::LIGHTNING_ROD), "Lightning Rod", $copperBreakInfo)); self::register("copper", new Copper(new BID(Ids::COPPER), "Copper Block", $copperBreakInfo)); + self::register("chiseled_copper", new Copper(new BID(Ids::CHISELED_COPPER), "Chiseled Copper", $copperBreakInfo)); + self::register("copper_grate", new CopperGrate(new BID(Ids::COPPER_GRATE), "Copper Grate", $copperBreakInfo)); self::register("cut_copper", new Copper(new BID(Ids::CUT_COPPER), "Cut Copper Block", $copperBreakInfo)); self::register("cut_copper_slab", new CopperSlab(new BID(Ids::CUT_COPPER_SLAB), "Cut Copper Slab", $copperBreakInfo)); self::register("cut_copper_stairs", new CopperStairs(new BID(Ids::CUT_COPPER_STAIRS), "Cut Copper Stairs", $copperBreakInfo)); + self::register("copper_bulb", new CopperBulb(new BID(Ids::COPPER_BULB), "Copper Bulb", $copperBreakInfo)); + + $copperDoorBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE, 30.0)); + self::register("copper_door", new CopperDoor(new BID(Ids::COPPER_DOOR), "Copper Door", $copperDoorBreakInfo)); + self::register("copper_trapdoor", new CopperTrapdoor(new BID(Ids::COPPER_TRAPDOOR), "Copper Trapdoor", $copperDoorBreakInfo)); $candleBreakInfo = new Info(new BreakInfo(0.1)); self::register("candle", new Candle(new BID(Ids::CANDLE), "Candle", $candleBreakInfo)); @@ -1659,6 +1692,27 @@ private static function registerMudBlocks() : void{ self::register("mud_brick_wall", new Wall(new BID(Ids::MUD_BRICK_WALL), "Mud Brick Wall", $mudBricksBreakInfo)); } + private static function registerTuffBlocks() : void{ + $tuffBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)); + + self::register("tuff", new Opaque(new BID(Ids::TUFF), "Tuff", $tuffBreakInfo)); + self::register("tuff_slab", new Slab(new BID(Ids::TUFF_SLAB), "Tuff", $tuffBreakInfo)); + self::register("tuff_stairs", new Stair(new BID(Ids::TUFF_STAIRS), "Tuff Stairs", $tuffBreakInfo)); + self::register("tuff_wall", new Wall(new BID(Ids::TUFF_WALL), "Tuff Wall", $tuffBreakInfo)); + self::register("chiseled_tuff", new Opaque(new BID(Ids::CHISELED_TUFF), "Chiseled Tuff", $tuffBreakInfo)); + + self::register("tuff_bricks", new Opaque(new BID(Ids::TUFF_BRICKS), "Tuff Bricks", $tuffBreakInfo)); + self::register("tuff_brick_slab", new Slab(new BID(Ids::TUFF_BRICK_SLAB), "Tuff Brick", $tuffBreakInfo)); + self::register("tuff_brick_stairs", new Stair(new BID(Ids::TUFF_BRICK_STAIRS), "Tuff Brick Stairs", $tuffBreakInfo)); + self::register("tuff_brick_wall", new Wall(new BID(Ids::TUFF_BRICK_WALL), "Tuff Brick Wall", $tuffBreakInfo)); + self::register("chiseled_tuff_bricks", new Opaque(new BID(Ids::CHISELED_TUFF_BRICKS), "Chiseled Tuff Bricks", $tuffBreakInfo)); + + self::register("polished_tuff", new Opaque(new BID(Ids::POLISHED_TUFF), "Polished Tuff", $tuffBreakInfo)); + self::register("polished_tuff_slab", new Slab(new BID(Ids::POLISHED_TUFF_SLAB), "Polished Tuff", $tuffBreakInfo)); + self::register("polished_tuff_stairs", new Stair(new BID(Ids::POLISHED_TUFF_STAIRS), "Polished Tuff Stairs", $tuffBreakInfo)); + self::register("polished_tuff_wall", new Wall(new BID(Ids::POLISHED_TUFF_WALL), "Polished Tuff Wall", $tuffBreakInfo)); + } + private static function registerCauldronBlocks() : void{ $cauldronBreakInfo = new Info(BreakInfo::pickaxe(2, ToolTier::WOOD)); diff --git a/src/block/inventory/CampfireInventory.php b/src/block/inventory/CampfireInventory.php new file mode 100644 index 00000000000..ae762473e23 --- /dev/null +++ b/src/block/inventory/CampfireInventory.php @@ -0,0 +1,40 @@ +holder = $holder; + parent::__construct(4); + } + + public function getMaxStackSize() : int{ + return 1; + } +} diff --git a/src/block/tile/Campfire.php b/src/block/tile/Campfire.php new file mode 100644 index 00000000000..ad4a193d7b8 --- /dev/null +++ b/src/block/tile/Campfire.php @@ -0,0 +1,143 @@ + */ + private array $cookingTimes = []; + + public function __construct(World $world, Vector3 $pos){ + parent::__construct($world, $pos); + $this->inventory = new CampfireInventory($this->position); + $this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange( + static function(Inventory $unused) use ($world, $pos) : void{ + $block = $world->getBlock($pos); + if($block instanceof BlockCampfire){ + $world->setBlock($pos, $block); + } + }) + ); + } + + public function getInventory() : CampfireInventory{ + return $this->inventory; + } + + public function getRealInventory() : CampfireInventory{ + return $this->inventory; + } + + /** + * @return int[] + * @phpstan-return array + */ + public function getCookingTimes() : array{ + return $this->cookingTimes; + } + + /** + * @param int[] $cookingTimes + * @phpstan-param array $cookingTimes + */ + public function setCookingTimes(array $cookingTimes) : void{ + $this->cookingTimes = $cookingTimes; + } + + public function readSaveData(CompoundTag $nbt) : void{ + $items = []; + $listeners = $this->inventory->getListeners()->toArray(); + $this->inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization + + foreach([ + [0, self::TAG_FIRST_INPUT_ITEM, self::TAG_FIRST_COOKING_TIME], + [1, self::TAG_SECOND_INPUT_ITEM, self::TAG_SECOND_COOKING_TIME], + [2, self::TAG_THIRD_INPUT_ITEM, self::TAG_THIRD_COOKING_TIME], + [3, self::TAG_FOURTH_INPUT_ITEM, self::TAG_FOURTH_COOKING_TIME], + ] as [$slot, $itemTag, $cookingTimeTag]){ + if(($tag = $nbt->getTag($itemTag)) instanceof CompoundTag){ + $items[$slot] = Item::nbtDeserialize($tag); + } + if(($tag = $nbt->getTag($cookingTimeTag)) instanceof IntTag){ + $this->cookingTimes[$slot] = $tag->getValue(); + } + } + $this->inventory->setContents($items); + $this->inventory->getListeners()->add(...$listeners); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + foreach([ + [0, self::TAG_FIRST_INPUT_ITEM, self::TAG_FIRST_COOKING_TIME], + [1, self::TAG_SECOND_INPUT_ITEM, self::TAG_SECOND_COOKING_TIME], + [2, self::TAG_THIRD_INPUT_ITEM, self::TAG_THIRD_COOKING_TIME], + [3, self::TAG_FOURTH_INPUT_ITEM, self::TAG_FOURTH_COOKING_TIME], + ] as [$slot, $itemTag, $cookingTimeTag]){ + $item = $this->inventory->getItem($slot); + if(!$item->isNull()){ + $nbt->setTag($itemTag, $item->nbtSerialize()); + if(isset($this->cookingTimes[$slot])){ + $nbt->setInt($cookingTimeTag, $this->cookingTimes[$slot]); + } + } + } + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + foreach([ + 0 => self::TAG_FIRST_INPUT_ITEM, + 1 => self::TAG_SECOND_INPUT_ITEM, + 2 => self::TAG_THIRD_INPUT_ITEM, + 3 => self::TAG_FOURTH_INPUT_ITEM + ] as $slot => $tag){ + $item = $this->inventory->getItem($slot); + if(!$item->isNull()){ + $nbt->setTag($tag, TypeConverter::getInstance()->getItemTranslator()->toNetworkNbt($item)); + } + } + } +} diff --git a/src/block/tile/ChiseledBookshelf.php b/src/block/tile/ChiseledBookshelf.php index f9d61e7eb6d..06175e27f4d 100644 --- a/src/block/tile/ChiseledBookshelf.php +++ b/src/block/tile/ChiseledBookshelf.php @@ -40,8 +40,12 @@ class ChiseledBookshelf extends Tile implements Container{ use ContainerTrait; + private const TAG_LAST_INTERACTED_SLOT = "LastInteractedSlot"; //TAG_Int + private SimpleInventory $inventory; + private ?ChiseledBookshelfSlot $lastInteractedSlot = null; + public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); $this->inventory = new SimpleInventory(count(ChiseledBookshelfSlot::cases())); @@ -55,12 +59,30 @@ public function getRealInventory() : SimpleInventory{ return $this->inventory; } + public function getLastInteractedSlot() : ?ChiseledBookshelfSlot{ + return $this->lastInteractedSlot; + } + + public function setLastInteractedSlot(?ChiseledBookshelfSlot $lastInteractedSlot) : void{ + $this->lastInteractedSlot = $lastInteractedSlot; + } + public function readSaveData(CompoundTag $nbt) : void{ $this->loadItems($nbt); + + $lastInteractedSlot = $nbt->getInt(self::TAG_LAST_INTERACTED_SLOT, 0); + if($lastInteractedSlot !== 0){ + $this->lastInteractedSlot = ChiseledBookshelfSlot::tryFrom($lastInteractedSlot - 1); + } } - public function writeSaveData(CompoundTag $nbt) : void{ + protected function writeSaveData(CompoundTag $nbt) : void{ $this->saveItems($nbt); + + $nbt->setInt(self::TAG_LAST_INTERACTED_SLOT, $this->lastInteractedSlot !== null ? + $this->lastInteractedSlot->value + 1 : + 0 + ); } protected function loadItems(CompoundTag $tag) : void{ diff --git a/src/block/tile/TileFactory.php b/src/block/tile/TileFactory.php index 6e87b72acb9..515dd8c63c6 100644 --- a/src/block/tile/TileFactory.php +++ b/src/block/tile/TileFactory.php @@ -57,6 +57,7 @@ public function __construct(){ $this->register(Bell::class, ["Bell", "minecraft:bell"]); $this->register(BlastFurnace::class, ["BlastFurnace", "minecraft:blast_furnace"]); $this->register(BrewingStand::class, ["BrewingStand", "minecraft:brewing_stand"]); + $this->register(Campfire::class, ["Campfire", "minecraft:campfire"]); $this->register(Cauldron::class, ["Cauldron", "minecraft:cauldron"]); $this->register(Chest::class, ["Chest", "minecraft:chest"]); $this->register(ChiseledBookshelf::class, ["ChiseledBookshelf", "minecraft:chiseled_bookshelf"]); @@ -79,7 +80,6 @@ public function __construct(){ $this->register(MobHead::class, ["Skull", "minecraft:skull"]); $this->register(GlowingItemFrame::class, ["GlowItemFrame"]); - //TODO: Campfire //TODO: ChalkboardBlock //TODO: ChemistryTable //TODO: CommandBlock diff --git a/src/block/utils/BannerPatternType.php b/src/block/utils/BannerPatternType.php index cd63226a75b..9b196360331 100644 --- a/src/block/utils/BannerPatternType.php +++ b/src/block/utils/BannerPatternType.php @@ -81,10 +81,12 @@ enum BannerPatternType{ case DIAGONAL_RIGHT; case DIAGONAL_UP_LEFT; case DIAGONAL_UP_RIGHT; + case FLOW; case FLOWER; case GLOBE; case GRADIENT; case GRADIENT_UP; + case GUSTER; case HALF_HORIZONTAL; case HALF_HORIZONTAL_BOTTOM; case HALF_VERTICAL; diff --git a/src/block/utils/CopperMaterial.php b/src/block/utils/CopperMaterial.php new file mode 100644 index 00000000000..6df22620b70 --- /dev/null +++ b/src/block/utils/CopperMaterial.php @@ -0,0 +1,38 @@ + FurnaceType::FURNACE, "blast_furnace" => FurnaceType::BLAST_FURNACE, "smoker" => FurnaceType::SMOKER, - //TODO: campfire + "campfire" => FurnaceType::CAMPFIRE, + "soul_campfire" => FurnaceType::SOUL_CAMPFIRE, default => null }; if($furnaceType === null){ diff --git a/src/crafting/FurnaceType.php b/src/crafting/FurnaceType.php index 0ce5b72cea6..89834c82145 100644 --- a/src/crafting/FurnaceType.php +++ b/src/crafting/FurnaceType.php @@ -25,6 +25,7 @@ use pocketmine\utils\LegacyEnumShimTrait; use pocketmine\world\sound\BlastFurnaceSound; +use pocketmine\world\sound\CampfireSound; use pocketmine\world\sound\FurnaceSound; use pocketmine\world\sound\SmokerSound; use pocketmine\world\sound\Sound; @@ -35,8 +36,10 @@ * These are retained for backwards compatibility only. * * @method static FurnaceType BLAST_FURNACE() + * @method static FurnaceType CAMPFIRE() * @method static FurnaceType FURNACE() * @method static FurnaceType SMOKER() + * @method static FurnaceType SOUL_CAMPFIRE() * * @phpstan-type TMetadata array{0: int, 1: Sound} */ @@ -46,6 +49,8 @@ enum FurnaceType{ case FURNACE; case BLAST_FURNACE; case SMOKER; + case CAMPFIRE; + case SOUL_CAMPFIRE; /** * @phpstan-return TMetadata @@ -58,6 +63,7 @@ private function getMetadata() : array{ self::FURNACE => [200, new FurnaceSound()], self::BLAST_FURNACE => [100, new BlastFurnaceSound()], self::SMOKER => [100, new SmokerSound()], + self::CAMPFIRE, self::SOUL_CAMPFIRE => [600, new CampfireSound()] }; } diff --git a/src/data/bedrock/BannerPatternTypeIdMap.php b/src/data/bedrock/BannerPatternTypeIdMap.php index d1884350fa8..7d4353d4f21 100644 --- a/src/data/bedrock/BannerPatternTypeIdMap.php +++ b/src/data/bedrock/BannerPatternTypeIdMap.php @@ -56,9 +56,11 @@ public function __construct(){ BannerPatternType::DIAGONAL_UP_LEFT => "ld", BannerPatternType::DIAGONAL_UP_RIGHT => "rud", BannerPatternType::FLOWER => "flo", + BannerPatternType::FLOW => "flw", BannerPatternType::GLOBE => "glb", BannerPatternType::GRADIENT => "gra", BannerPatternType::GRADIENT_UP => "gru", + BannerPatternType::GUSTER => "gus", BannerPatternType::HALF_HORIZONTAL => "hh", BannerPatternType::HALF_HORIZONTAL_BOTTOM => "hhb", BannerPatternType::HALF_VERTICAL => "vh", diff --git a/src/data/bedrock/EnchantmentIdMap.php b/src/data/bedrock/EnchantmentIdMap.php index ae2460cfeb9..e3d652b19ce 100644 --- a/src/data/bedrock/EnchantmentIdMap.php +++ b/src/data/bedrock/EnchantmentIdMap.php @@ -43,6 +43,7 @@ private function __construct(){ $this->register(EnchantmentIds::PROJECTILE_PROTECTION, VanillaEnchantments::PROJECTILE_PROTECTION()); $this->register(EnchantmentIds::THORNS, VanillaEnchantments::THORNS()); $this->register(EnchantmentIds::RESPIRATION, VanillaEnchantments::RESPIRATION()); + $this->register(EnchantmentIds::AQUA_AFFINITY, VanillaEnchantments::AQUA_AFFINITY()); $this->register(EnchantmentIds::SHARPNESS, VanillaEnchantments::SHARPNESS()); //TODO: smite, bane of arthropods (these don't make sense now because their applicable mobs don't exist yet) diff --git a/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php b/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php index a7fee6efba0..aebfd67ffac 100644 --- a/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php +++ b/src/data/bedrock/block/convert/BlockObjectToStateSerializer.php @@ -43,6 +43,7 @@ use pocketmine\block\Cake; use pocketmine\block\CakeWithCandle; use pocketmine\block\CakeWithDyedCandle; +use pocketmine\block\Campfire; use pocketmine\block\Candle; use pocketmine\block\Carpet; use pocketmine\block\Carrot; @@ -57,8 +58,12 @@ use pocketmine\block\Concrete; use pocketmine\block\ConcretePowder; use pocketmine\block\Copper; +use pocketmine\block\CopperBulb; +use pocketmine\block\CopperDoor; +use pocketmine\block\CopperGrate; use pocketmine\block\CopperSlab; use pocketmine\block\CopperStairs; +use pocketmine\block\CopperTrapdoor; use pocketmine\block\Coral; use pocketmine\block\CoralBlock; use pocketmine\block\DaylightSensor; @@ -124,6 +129,7 @@ use pocketmine\block\Slab; use pocketmine\block\SmallDripleaf; use pocketmine\block\SnowLayer; +use pocketmine\block\SoulCampfire; use pocketmine\block\Sponge; use pocketmine\block\StainedGlass; use pocketmine\block\StainedGlassPane; @@ -791,6 +797,8 @@ private function registerSimpleSerializers() : void{ $this->mapSimple(Blocks::CHISELED_RED_SANDSTONE(), Ids::CHISELED_RED_SANDSTONE); $this->mapSimple(Blocks::CHISELED_SANDSTONE(), Ids::CHISELED_SANDSTONE); $this->mapSimple(Blocks::CHISELED_STONE_BRICKS(), Ids::CHISELED_STONE_BRICKS); + $this->mapSimple(Blocks::CHISELED_TUFF(), Ids::CHISELED_TUFF); + $this->mapSimple(Blocks::CHISELED_TUFF_BRICKS(), Ids::CHISELED_TUFF_BRICKS); $this->mapSimple(Blocks::CHORUS_PLANT(), Ids::CHORUS_PLANT); $this->mapSimple(Blocks::CLAY(), Ids::CLAY); $this->mapSimple(Blocks::COAL(), Ids::COAL_BLOCK); @@ -1014,6 +1022,7 @@ private function registerSimpleSerializers() : void{ $this->mapSimple(Blocks::POLISHED_DEEPSLATE(), Ids::POLISHED_DEEPSLATE); $this->mapSimple(Blocks::POLISHED_DIORITE(), Ids::POLISHED_DIORITE); $this->mapSimple(Blocks::POLISHED_GRANITE(), Ids::POLISHED_GRANITE); + $this->mapSimple(Blocks::POLISHED_TUFF(), Ids::POLISHED_TUFF); $this->mapSimple(Blocks::PRISMARINE(), Ids::PRISMARINE); $this->mapSimple(Blocks::PRISMARINE_BRICKS(), Ids::PRISMARINE_BRICKS); $this->mapSimple(Blocks::QUARTZ_BRICKS(), Ids::QUARTZ_BRICKS); @@ -1049,6 +1058,7 @@ private function registerSimpleSerializers() : void{ $this->mapSimple(Blocks::TINTED_GLASS(), Ids::TINTED_GLASS); $this->mapSimple(Blocks::TORCHFLOWER(), Ids::TORCHFLOWER); $this->mapSimple(Blocks::TUFF(), Ids::TUFF); + $this->mapSimple(Blocks::TUFF_BRICKS(), Ids::TUFF_BRICKS); $this->mapSimple(Blocks::WARPED_WART_BLOCK(), Ids::WARPED_WART_BLOCK); $this->mapSimple(Blocks::WARPED_ROOTS(), Ids::WARPED_ROOTS); $this->mapSimple(Blocks::WITHER_ROSE(), Ids::WITHER_ROSE); @@ -1187,6 +1197,11 @@ private function registerSerializers() : void{ return Writer::create(Ids::CAKE) ->writeInt(StateNames::BITE_COUNTER, $block->getBites()); }); + $this->map(Blocks::CAMPFIRE(), function(Campfire $block) : Writer{ + return Writer::create(Ids::CAMPFIRE) + ->writeCardinalHorizontalFacing($block->getFacing()) + ->writeBool(StateNames::EXTINGUISHED, !$block->isLit()); + }); $this->map(Blocks::CARROTS(), fn(Carrot $block) => Helper::encodeCrops($block, new Writer(Ids::CARROTS))); $this->map(Blocks::CARVED_PUMPKIN(), function(CarvedPumpkin $block) : Writer{ return Writer::create(Ids::CARVED_PUMPKIN) @@ -1238,6 +1253,40 @@ private function registerSerializers() : void{ Helper::selectCopperId($oxidation, Ids::COPPER_BLOCK, Ids::EXPOSED_COPPER, Ids::WEATHERED_COPPER, Ids::OXIDIZED_COPPER) ); }); + $this->map(Blocks::CHISELED_COPPER(), function(Copper $block) : Writer{ + $oxidation = $block->getOxidation(); + return new Writer($block->isWaxed() ? + Helper::selectCopperId($oxidation, + Ids::WAXED_CHISELED_COPPER, + Ids::WAXED_EXPOSED_CHISELED_COPPER, + Ids::WAXED_WEATHERED_CHISELED_COPPER, + Ids::WAXED_OXIDIZED_CHISELED_COPPER + ) : + Helper::selectCopperId($oxidation, + Ids::CHISELED_COPPER, + Ids::EXPOSED_CHISELED_COPPER, + Ids::WEATHERED_CHISELED_COPPER, + Ids::OXIDIZED_CHISELED_COPPER + ) + ); + }); + $this->map(Blocks::COPPER_GRATE(), function(CopperGrate $block) : Writer{ + $oxidation = $block->getOxidation(); + return new Writer($block->isWaxed() ? + Helper::selectCopperId($oxidation, + Ids::WAXED_COPPER_GRATE, + Ids::WAXED_EXPOSED_COPPER_GRATE, + Ids::WAXED_WEATHERED_COPPER_GRATE, + Ids::WAXED_OXIDIZED_COPPER_GRATE + ) : + Helper::selectCopperId($oxidation, + Ids::COPPER_GRATE, + Ids::EXPOSED_COPPER_GRATE, + Ids::WEATHERED_COPPER_GRATE, + Ids::OXIDIZED_COPPER_GRATE + ) + ); + }); $this->map(Blocks::CUT_COPPER(), function(Copper $block) : Writer{ $oxidation = $block->getOxidation(); return new Writer($block->isWaxed() ? @@ -1305,6 +1354,67 @@ private function registerSerializers() : void{ ) ); }); + $this->map(Blocks::COPPER_BULB(), function(CopperBulb $block) : Writer{ + $oxidation = $block->getOxidation(); + return Writer::create($block->isWaxed() ? + Helper::selectCopperId($oxidation, + Ids::WAXED_COPPER_BULB, + Ids::WAXED_EXPOSED_COPPER_BULB, + Ids::WAXED_WEATHERED_COPPER_BULB, + Ids::WAXED_OXIDIZED_COPPER_BULB) : + Helper::selectCopperId($oxidation, + Ids::COPPER_BULB, + Ids::EXPOSED_COPPER_BULB, + Ids::WEATHERED_COPPER_BULB, + Ids::OXIDIZED_COPPER_BULB + )) + ->writeBool(StateNames::LIT, $block->isLit()) + ->writeBool(StateNames::POWERED_BIT, $block->isPowered()); + }); + $this->map(Blocks::COPPER_DOOR(), function(CopperDoor $block) : Writer{ + $oxidation = $block->getOxidation(); + return Helper::encodeDoor( + $block, + new Writer($block->isWaxed() ? + Helper::selectCopperId( + $oxidation, + Ids::WAXED_COPPER_DOOR, + Ids::WAXED_EXPOSED_COPPER_DOOR, + Ids::WAXED_WEATHERED_COPPER_DOOR, + Ids::WAXED_OXIDIZED_COPPER_DOOR + ) : + Helper::selectCopperId( + $oxidation, + Ids::COPPER_DOOR, + Ids::EXPOSED_COPPER_DOOR, + Ids::WEATHERED_COPPER_DOOR, + Ids::OXIDIZED_COPPER_DOOR + ) + ) + ); + }); + $this->map(Blocks::COPPER_TRAPDOOR(), function(CopperTrapdoor $block) : Writer{ + $oxidation = $block->getOxidation(); + return Helper::encodeTrapdoor( + $block, + new Writer($block->isWaxed() ? + Helper::selectCopperId( + $oxidation, + Ids::WAXED_COPPER_TRAPDOOR, + Ids::WAXED_EXPOSED_COPPER_TRAPDOOR, + Ids::WAXED_WEATHERED_COPPER_TRAPDOOR, + Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR + ) : + Helper::selectCopperId( + $oxidation, + Ids::COPPER_TRAPDOOR, + Ids::EXPOSED_COPPER_TRAPDOOR, + Ids::WEATHERED_COPPER_TRAPDOOR, + Ids::OXIDIZED_COPPER_TRAPDOOR + ) + ) + ); + }); $this->map(Blocks::COCOA_POD(), function(CocoaBlock $block) : Writer{ return Writer::create(Ids::COCOA) ->writeInt(StateNames::AGE, $block->getAge()) @@ -1546,6 +1656,9 @@ private function registerSerializers() : void{ $this->mapStairs(Blocks::POLISHED_DIORITE_STAIRS(), Ids::POLISHED_DIORITE_STAIRS); $this->mapSlab(Blocks::POLISHED_GRANITE_SLAB(), Ids::POLISHED_GRANITE_SLAB, Ids::POLISHED_GRANITE_DOUBLE_SLAB); $this->mapStairs(Blocks::POLISHED_GRANITE_STAIRS(), Ids::POLISHED_GRANITE_STAIRS); + $this->mapSlab(Blocks::POLISHED_TUFF_SLAB(), Ids::POLISHED_TUFF_SLAB, Ids::POLISHED_TUFF_DOUBLE_SLAB); + $this->mapStairs(Blocks::POLISHED_TUFF_STAIRS(), Ids::POLISHED_TUFF_STAIRS); + $this->map(Blocks::POLISHED_TUFF_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::POLISHED_TUFF_WALL))); $this->map(Blocks::POTATOES(), fn(Potato $block) => Helper::encodeCrops($block, new Writer(Ids::POTATOES))); $this->map(Blocks::POWERED_RAIL(), function(PoweredRail $block) : Writer{ return Writer::create(Ids::GOLDEN_RAIL) @@ -1635,6 +1748,11 @@ private function registerSerializers() : void{ ->writeBool(StateNames::COVERED_BIT, false) ->writeInt(StateNames::HEIGHT, $block->getLayers() - 1); }); + $this->map(Blocks::SOUL_CAMPFIRE(), function(SoulCampfire $block) : Writer{ + return Writer::create(Ids::SOUL_CAMPFIRE) + ->writeCardinalHorizontalFacing($block->getFacing()) + ->writeBool(StateNames::EXTINGUISHED, !$block->isLit()); + }); $this->map(Blocks::SOUL_FIRE(), function() : Writer{ return Writer::create(Ids::SOUL_FIRE) ->writeInt(StateNames::AGE, 0); //useless for soul fire, we don't track it @@ -1694,6 +1812,12 @@ private function registerSerializers() : void{ ->writeBool(StateNames::POWERED_BIT, $block->isPowered()) ->writeLegacyHorizontalFacing($block->getFacing()); }); + $this->mapSlab(Blocks::TUFF_BRICK_SLAB(), Ids::TUFF_BRICK_SLAB, Ids::TUFF_BRICK_DOUBLE_SLAB); + $this->mapStairs(Blocks::TUFF_BRICK_STAIRS(), Ids::TUFF_BRICK_STAIRS); + $this->map(Blocks::TUFF_BRICK_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::TUFF_BRICK_WALL))); + $this->mapSlab(Blocks::TUFF_SLAB(), Ids::TUFF_SLAB, Ids::TUFF_DOUBLE_SLAB); + $this->mapStairs(Blocks::TUFF_STAIRS(), Ids::TUFF_STAIRS); + $this->map(Blocks::TUFF_WALL(), fn(Wall $block) => Helper::encodeWall($block, new Writer(Ids::TUFF_WALL))); $this->map(Blocks::TWISTING_VINES(), function(NetherVines $block) : Writer{ return Writer::create(Ids::TWISTING_VINES) ->writeInt(StateNames::TWISTING_VINES_AGE, $block->getAge()); diff --git a/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php b/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php index c51cda76886..fda0455aa38 100644 --- a/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php +++ b/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php @@ -26,9 +26,6 @@ use pocketmine\block\Block; use pocketmine\block\Button; use pocketmine\block\Candle; -use pocketmine\block\Copper; -use pocketmine\block\CopperSlab; -use pocketmine\block\CopperStairs; use pocketmine\block\Crops; use pocketmine\block\DaylightSensor; use pocketmine\block\Door; @@ -48,6 +45,7 @@ use pocketmine\block\Stair; use pocketmine\block\Stem; use pocketmine\block\Trapdoor; +use pocketmine\block\utils\CopperMaterial; use pocketmine\block\utils\CopperOxidation; use pocketmine\block\utils\SlabType; use pocketmine\block\Wall; @@ -99,24 +97,24 @@ public static function decodeComparator(RedstoneComparator $block, BlockStateRea } /** - * @phpstan-template TBlock of Copper|CopperSlab|CopperStairs + * @phpstan-template TBlock of CopperMaterial * * @phpstan-param TBlock $block * @phpstan-return TBlock */ - public static function decodeCopper(Copper|CopperSlab|CopperStairs $block, CopperOxidation $oxidation) : Copper|CopperSlab|CopperStairs{ + public static function decodeCopper(CopperMaterial $block, CopperOxidation $oxidation) : CopperMaterial{ $block->setOxidation($oxidation); $block->setWaxed(false); return $block; } /** - * @phpstan-template TBlock of Copper|CopperSlab|CopperStairs + * @phpstan-template TBlock of CopperMaterial * * @phpstan-param TBlock $block * @phpstan-return TBlock */ - public static function decodeWaxedCopper(Copper|CopperSlab|CopperStairs $block, CopperOxidation $oxidation) : Copper|CopperSlab|CopperStairs{ + public static function decodeWaxedCopper(CopperMaterial $block, CopperOxidation $oxidation) : CopperMaterial{ $block->setOxidation($oxidation); $block->setWaxed(true); return $block; diff --git a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php index 405f6e53e01..5c0a427cc75 100644 --- a/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php +++ b/src/data/bedrock/block/convert/BlockStateToObjectDeserializer.php @@ -722,6 +722,8 @@ private function registerSimpleDeserializers() : void{ $this->mapSimple(Ids::CHISELED_RED_SANDSTONE, fn() => Blocks::CHISELED_RED_SANDSTONE()); $this->mapSimple(Ids::CHISELED_SANDSTONE, fn() => Blocks::CHISELED_SANDSTONE()); $this->mapSimple(Ids::CHISELED_STONE_BRICKS, fn() => Blocks::CHISELED_STONE_BRICKS()); + $this->mapSimple(Ids::CHISELED_TUFF, fn() => Blocks::CHISELED_TUFF()); + $this->mapSimple(Ids::CHISELED_TUFF_BRICKS, fn() => Blocks::CHISELED_TUFF_BRICKS()); $this->mapSimple(Ids::CHORUS_PLANT, fn() => Blocks::CHORUS_PLANT()); $this->mapSimple(Ids::CLAY, fn() => Blocks::CLAY()); $this->mapSimple(Ids::COAL_BLOCK, fn() => Blocks::COAL()); @@ -940,6 +942,7 @@ private function registerSimpleDeserializers() : void{ $this->mapSimple(Ids::POLISHED_DEEPSLATE, fn() => Blocks::POLISHED_DEEPSLATE()); $this->mapSimple(Ids::POLISHED_DIORITE, fn() => Blocks::POLISHED_DIORITE()); $this->mapSimple(Ids::POLISHED_GRANITE, fn() => Blocks::POLISHED_GRANITE()); + $this->mapSimple(Ids::POLISHED_TUFF, fn() => Blocks::POLISHED_TUFF()); $this->mapSimple(Ids::PRISMARINE, fn() => Blocks::PRISMARINE()); $this->mapSimple(Ids::PRISMARINE_BRICKS, fn() => Blocks::PRISMARINE_BRICKS()); $this->mapSimple(Ids::QUARTZ_BRICKS, fn() => Blocks::QUARTZ_BRICKS()); @@ -977,6 +980,7 @@ private function registerSimpleDeserializers() : void{ $this->mapSimple(Ids::TINTED_GLASS, fn() => Blocks::TINTED_GLASS()); $this->mapSimple(Ids::TORCHFLOWER, fn() => Blocks::TORCHFLOWER()); $this->mapSimple(Ids::TUFF, fn() => Blocks::TUFF()); + $this->mapSimple(Ids::TUFF_BRICKS, fn() => Blocks::TUFF_BRICKS()); $this->mapSimple(Ids::UNDYED_SHULKER_BOX, fn() => Blocks::SHULKER_BOX()); $this->mapSimple(Ids::WARPED_WART_BLOCK, fn() => Blocks::WARPED_WART_BLOCK()); $this->mapSimple(Ids::WARPED_ROOTS, fn() => Blocks::WARPED_ROOTS()); @@ -1123,6 +1127,11 @@ private function registerDeserializers() : void{ return Blocks::CAKE() ->setBites($in->readBoundedInt(StateNames::BITE_COUNTER, 0, 6)); }); + $this->map(Ids::CAMPFIRE, function(Reader $in) : Block{ + return Blocks::CAMPFIRE() + ->setFacing($in->readCardinalHorizontalFacing()) + ->setLit(!$in->readBool(StateNames::EXTINGUISHED)); + }); $this->map(Ids::CARROTS, fn(Reader $in) => Helper::decodeCrops(Blocks::CARROTS(), $in)); $this->map(Ids::CARVED_PUMPKIN, function(Reader $in) : Block{ return Blocks::CARVED_PUMPKIN() @@ -1162,6 +1171,7 @@ private function registerDeserializers() : void{ return $block; }); + $this->map(Ids::CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::NONE)); $this->map(Ids::CHISELED_QUARTZ_BLOCK, function(Reader $in) : Block{ return Blocks::CHISELED_QUARTZ() ->setAxis($in->readPillarAxis()); @@ -1193,6 +1203,14 @@ private function registerDeserializers() : void{ ->setFacing(Facing::opposite($in->readLegacyHorizontalFacing())) ); $this->map(Ids::COPPER_BLOCK, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::NONE)); + $this->map(Ids::COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::NONE) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::NONE), $in)); + $this->map(Ids::COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::NONE)); + $this->map(Ids::COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::NONE), $in)); $this->map(Ids::CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::NONE)); $this->mapSlab(Ids::CUT_COPPER_SLAB, Ids::DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::NONE)); $this->mapStairs(Ids::CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::NONE)); @@ -1251,9 +1269,18 @@ private function registerDeserializers() : void{ ->setFacing($in->readCardinalHorizontalFacing()); }); $this->map(Ids::EXPOSED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::EXPOSED)); + $this->map(Ids::EXPOSED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::EXPOSED)); + $this->map(Ids::EXPOSED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::EXPOSED)); $this->map(Ids::EXPOSED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::EXPOSED)); $this->mapSlab(Ids::EXPOSED_CUT_COPPER_SLAB, Ids::EXPOSED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::EXPOSED)); $this->mapStairs(Ids::EXPOSED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::EXPOSED)); + $this->map(Ids::EXPOSED_COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::EXPOSED) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::EXPOSED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::EXPOSED), $in)); + $this->map(Ids::EXPOSED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::EXPOSED), $in)); $this->map(Ids::FARMLAND, function(Reader $in) : Block{ return Blocks::FARMLAND() ->setWetness($in->readBoundedInt(StateNames::MOISTURIZED_AMOUNT, 0, 7)); @@ -1407,9 +1434,18 @@ private function registerDeserializers() : void{ $this->mapStairs(Ids::NORMAL_STONE_STAIRS, fn() => Blocks::STONE_STAIRS()); $this->map(Ids::OCHRE_FROGLIGHT, fn(Reader $in) => Blocks::FROGLIGHT()->setFroglightType(FroglightType::OCHRE)->setAxis($in->readPillarAxis())); $this->map(Ids::OXIDIZED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::OXIDIZED)); + $this->map(Ids::OXIDIZED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::OXIDIZED)); + $this->map(Ids::OXIDIZED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::OXIDIZED)); $this->map(Ids::OXIDIZED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::OXIDIZED)); $this->mapSlab(Ids::OXIDIZED_CUT_COPPER_SLAB, Ids::OXIDIZED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::OXIDIZED)); $this->mapStairs(Ids::OXIDIZED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::OXIDIZED)); + $this->map(Ids::OXIDIZED_COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::OXIDIZED) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::OXIDIZED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::OXIDIZED), $in)); + $this->map(Ids::OXIDIZED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::OXIDIZED), $in)); $this->map(Ids::PEARLESCENT_FROGLIGHT, fn(Reader $in) => Blocks::FROGLIGHT()->setFroglightType(FroglightType::PEARLESCENT)->setAxis($in->readPillarAxis())); $this->mapSlab(Ids::PETRIFIED_OAK_SLAB, Ids::PETRIFIED_OAK_DOUBLE_SLAB, fn() => Blocks::FAKE_WOODEN_SLAB()); $this->map(Ids::PINK_PETALS, function(Reader $in) : Block{ @@ -1456,6 +1492,9 @@ private function registerDeserializers() : void{ $this->mapStairs(Ids::POLISHED_DIORITE_STAIRS, fn() => Blocks::POLISHED_DIORITE_STAIRS()); $this->mapSlab(Ids::POLISHED_GRANITE_SLAB, Ids::POLISHED_GRANITE_DOUBLE_SLAB, fn() => Blocks::POLISHED_GRANITE_SLAB()); $this->mapStairs(Ids::POLISHED_GRANITE_STAIRS, fn() => Blocks::POLISHED_GRANITE_STAIRS()); + $this->mapSlab(Ids::POLISHED_TUFF_SLAB, Ids::POLISHED_TUFF_DOUBLE_SLAB, fn() => Blocks::POLISHED_TUFF_SLAB()); + $this->mapStairs(Ids::POLISHED_TUFF_STAIRS, fn() => Blocks::POLISHED_TUFF_STAIRS()); + $this->map(Ids::POLISHED_TUFF_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::POLISHED_TUFF_WALL(), $in)); $this->map(Ids::PORTAL, function(Reader $in) : Block{ return Blocks::NETHER_PORTAL() ->setAxis(match($value = $in->readString(StateNames::PORTAL_AXIS)){ @@ -1566,6 +1605,11 @@ private function registerDeserializers() : void{ $in->ignored(StateNames::COVERED_BIT); //seems to be useless return Blocks::SNOW_LAYER()->setLayers($in->readBoundedInt(StateNames::HEIGHT, 0, 7) + 1); }); + $this->map(Ids::SOUL_CAMPFIRE, function(Reader $in) : Block{ + return Blocks::SOUL_CAMPFIRE() + ->setFacing($in->readCardinalHorizontalFacing()) + ->setLit(!$in->readBool(StateNames::EXTINGUISHED)); + }); $this->map(Ids::SOUL_FIRE, function(Reader $in) : Block{ $in->ignored(StateNames::AGE); //this is useless for soul fire, since it doesn't have the logic associated return Blocks::SOUL_FIRE(); @@ -1629,6 +1673,12 @@ private function registerDeserializers() : void{ ->setFacing($in->readLegacyHorizontalFacing()) ->setPowered($in->readBool(StateNames::POWERED_BIT)); }); + $this->mapSlab(Ids::TUFF_BRICK_SLAB, Ids::TUFF_BRICK_DOUBLE_SLAB, fn() => Blocks::TUFF_BRICK_SLAB()); + $this->mapStairs(Ids::TUFF_BRICK_STAIRS, fn() => Blocks::TUFF_BRICK_STAIRS()); + $this->map(Ids::TUFF_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::TUFF_BRICK_WALL(), $in)); + $this->mapSlab(Ids::TUFF_SLAB, Ids::TUFF_DOUBLE_SLAB, fn() => Blocks::TUFF_SLAB()); + $this->mapStairs(Ids::TUFF_STAIRS, fn() => Blocks::TUFF_STAIRS()); + $this->map(Ids::TUFF_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::TUFF_WALL(), $in)); $this->map(Ids::TWISTING_VINES, function(Reader $in) : Block{ return Blocks::TWISTING_VINES() ->setAge($in->readBoundedInt(StateNames::TWISTING_VINES_AGE, 0, 25)); @@ -1665,25 +1715,70 @@ private function registerDeserializers() : void{ }); $this->map(Ids::WATER, fn(Reader $in) => Helper::decodeStillLiquid(Blocks::WATER(), $in)); $this->map(Ids::WAXED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::NONE)); + $this->map(Ids::WAXED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::NONE)); + $this->map(Ids::WAXED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::NONE)); $this->map(Ids::WAXED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::NONE)); $this->mapSlab(Ids::WAXED_CUT_COPPER_SLAB, Ids::WAXED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::NONE)); $this->mapStairs(Ids::WAXED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::NONE)); + $this->map(Ids::WAXED_COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::NONE) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::WAXED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::NONE), $in)); + $this->map(Ids::WAXED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::NONE), $in)); $this->map(Ids::WAXED_EXPOSED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::EXPOSED)); + $this->map(Ids::WAXED_EXPOSED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::EXPOSED)); + $this->map(Ids::WAXED_EXPOSED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::EXPOSED)); $this->map(Ids::WAXED_EXPOSED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::EXPOSED)); $this->mapSlab(Ids::WAXED_EXPOSED_CUT_COPPER_SLAB, Ids::WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::EXPOSED)); $this->mapStairs(Ids::WAXED_EXPOSED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::EXPOSED)); + $this->map(Ids::WAXED_EXPOSED_COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::EXPOSED) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::WAXED_EXPOSED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::EXPOSED), $in)); + $this->map(Ids::WAXED_EXPOSED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::EXPOSED), $in)); $this->map(Ids::WAXED_OXIDIZED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::OXIDIZED)); + $this->map(Ids::WAXED_OXIDIZED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::OXIDIZED)); + $this->map(Ids::WAXED_OXIDIZED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::OXIDIZED)); $this->map(Ids::WAXED_OXIDIZED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::OXIDIZED)); $this->mapSlab(Ids::WAXED_OXIDIZED_CUT_COPPER_SLAB, Ids::WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::OXIDIZED)); $this->mapStairs(Ids::WAXED_OXIDIZED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::OXIDIZED)); + $this->map(Ids::WAXED_OXIDIZED_COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::OXIDIZED) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::WAXED_OXIDIZED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::OXIDIZED), $in)); + $this->map(Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::OXIDIZED), $in)); $this->map(Ids::WAXED_WEATHERED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::WEATHERED)); + $this->map(Ids::WAXED_WEATHERED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::WEATHERED)); + $this->map(Ids::WAXED_WEATHERED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::WEATHERED)); $this->map(Ids::WAXED_WEATHERED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::WEATHERED)); $this->mapSlab(Ids::WAXED_WEATHERED_CUT_COPPER_SLAB, Ids::WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::WEATHERED)); $this->mapStairs(Ids::WAXED_WEATHERED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::WEATHERED)); + $this->map(Ids::WAXED_WEATHERED_COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::WEATHERED) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::WAXED_WEATHERED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::WEATHERED), $in)); + $this->map(Ids::WAXED_WEATHERED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::WEATHERED), $in)); $this->map(Ids::WEATHERED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::WEATHERED)); + $this->map(Ids::WEATHERED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::WEATHERED)); + $this->map(Ids::WEATHERED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::WEATHERED)); $this->map(Ids::WEATHERED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::WEATHERED)); $this->mapSlab(Ids::WEATHERED_CUT_COPPER_SLAB, Ids::WEATHERED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::WEATHERED)); $this->mapStairs(Ids::WEATHERED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::WEATHERED)); + $this->map(Ids::WEATHERED_COPPER_BULB, function(Reader $in) : Block{ + return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::WEATHERED) + ->setLit($in->readBool(StateNames::LIT)) + ->setPowered($in->readBool(StateNames::POWERED_BIT)); + }); + $this->map(Ids::WEATHERED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::WEATHERED), $in)); + $this->map(Ids::WEATHERED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::WEATHERED), $in)); $this->map(Ids::WEEPING_VINES, function(Reader $in) : Block{ return Blocks::WEEPING_VINES() ->setAge($in->readBoundedInt(StateNames::WEEPING_VINES_AGE, 0, 25)); diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchema.php b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchema.php index 6d280ecf756..f8894cfd27c 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchema.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchema.php @@ -23,6 +23,7 @@ namespace pocketmine\data\bedrock\block\upgrade; +use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchemaFlattenInfo as FlattenInfo; use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchemaValueRemap as ValueRemap; use pocketmine\nbt\tag\Tag; use function count; @@ -58,6 +59,12 @@ final class BlockStateUpgradeSchema{ */ public array $remappedPropertyValues = []; + /** + * @var FlattenInfo[] + * @phpstan-var array + */ + public array $flattenedProperties = []; + /** * @var BlockStateUpgradeSchemaBlockRemap[][] * @phpstan-var array> @@ -93,6 +100,7 @@ public function isEmpty() : bool{ $this->removedProperties, $this->renamedProperties, $this->remappedPropertyValues, + $this->flattenedProperties, $this->remappedStates, ] as $list){ if(count($list) !== 0){ diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaBlockRemap.php b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaBlockRemap.php index 611ad04e25f..676afbaf4e5 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaBlockRemap.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaBlockRemap.php @@ -40,7 +40,7 @@ final class BlockStateUpgradeSchemaBlockRemap{ */ public function __construct( public array $oldState, - public string|BlockStateUpgradeSchemaFlattenedName $newName, + public string|BlockStateUpgradeSchemaFlattenInfo $newName, public array $newState, public array $copiedState ){} @@ -48,8 +48,8 @@ public function __construct( public function equals(self $that) : bool{ $sameName = $this->newName === $that->newName || ( - $this->newName instanceof BlockStateUpgradeSchemaFlattenedName && - $that->newName instanceof BlockStateUpgradeSchemaFlattenedName && + $this->newName instanceof BlockStateUpgradeSchemaFlattenInfo && + $that->newName instanceof BlockStateUpgradeSchemaFlattenInfo && $this->newName->equals($that->newName) ); if(!$sameName){ diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenedName.php b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenInfo.php similarity index 74% rename from src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenedName.php rename to src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenInfo.php index 1c95dd9c7f3..4a14a1291f0 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenedName.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaFlattenInfo.php @@ -23,20 +23,25 @@ namespace pocketmine\data\bedrock\block\upgrade; +use pocketmine\nbt\tag\ByteTag; +use pocketmine\nbt\tag\IntTag; +use pocketmine\nbt\tag\StringTag; use function ksort; use const SORT_STRING; -final class BlockStateUpgradeSchemaFlattenedName{ +final class BlockStateUpgradeSchemaFlattenInfo{ /** * @param string[] $flattenedValueRemaps * @phpstan-param array $flattenedValueRemaps + * @phpstan-param ?class-string $flattenedPropertyType */ public function __construct( public string $prefix, public string $flattenedProperty, public string $suffix, - public array $flattenedValueRemaps + public array $flattenedValueRemaps, + public ?string $flattenedPropertyType = null ){ ksort($this->flattenedValueRemaps, SORT_STRING); } @@ -45,6 +50,7 @@ public function equals(self $that) : bool{ return $this->prefix === $that->prefix && $this->flattenedProperty === $that->flattenedProperty && $this->suffix === $that->suffix && - $this->flattenedValueRemaps === $that->flattenedValueRemaps; + $this->flattenedValueRemaps === $that->flattenedValueRemaps && + $this->flattenedPropertyType === $that->flattenedPropertyType; } } diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php index 832631490a3..08eba89785d 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgradeSchemaUtils.php @@ -25,7 +25,7 @@ use pocketmine\data\bedrock\block\upgrade\model\BlockStateUpgradeSchemaModel; use pocketmine\data\bedrock\block\upgrade\model\BlockStateUpgradeSchemaModelBlockRemap; -use pocketmine\data\bedrock\block\upgrade\model\BlockStateUpgradeSchemaModelFlattenedName; +use pocketmine\data\bedrock\block\upgrade\model\BlockStateUpgradeSchemaModelFlattenInfo; use pocketmine\data\bedrock\block\upgrade\model\BlockStateUpgradeSchemaModelTag; use pocketmine\data\bedrock\block\upgrade\model\BlockStateUpgradeSchemaModelValueRemap; use pocketmine\nbt\tag\ByteTag; @@ -155,20 +155,24 @@ public static function fromJsonModel(BlockStateUpgradeSchemaModel $model, int $s } } + foreach(Utils::stringifyKeys($model->flattenedProperties ?? []) as $blockName => $flattenRule){ + $result->flattenedProperties[$blockName] = self::jsonModelToFlattenRule($flattenRule); + } + foreach(Utils::stringifyKeys($model->remappedStates ?? []) as $oldBlockName => $remaps){ foreach($remaps as $remap){ - if(isset($remap->newName) === isset($remap->newFlattenedName)){ + if(isset($remap->newName)){ + $remapName = $remap->newName; + }elseif(isset($remap->newFlattenedName)){ + $flattenRule = $remap->newFlattenedName; + $remapName = self::jsonModelToFlattenRule($flattenRule); + }else{ throw new \UnexpectedValueException("Expected exactly one of 'newName' or 'newFlattenedName' properties to be set"); } $result->remappedStates[$oldBlockName][] = new BlockStateUpgradeSchemaBlockRemap( array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->oldState ?? []), - $remap->newName ?? new BlockStateUpgradeSchemaFlattenedName( - $remap->newFlattenedName->prefix, - $remap->newFlattenedName->flattenedProperty, - $remap->newFlattenedName->suffix, - $remap->newFlattenedName->flattenedValueRemaps ?? [], - ), + $remapName, array_map(fn(BlockStateUpgradeSchemaModelTag $tag) => self::jsonModelToTag($tag), $remap->newState ?? []), $remap->copiedState ?? [] ); @@ -254,6 +258,36 @@ private static function buildRemappedValuesIndex(BlockStateUpgradeSchema $schema $model->remappedPropertyValues = $modelDedupMapping; } + private static function flattenRuleToJsonModel(BlockStateUpgradeSchemaFlattenInfo $flattenRule) : BlockStateUpgradeSchemaModelFlattenInfo{ + return new BlockStateUpgradeSchemaModelFlattenInfo( + $flattenRule->prefix, + $flattenRule->flattenedProperty, + $flattenRule->suffix, + $flattenRule->flattenedValueRemaps, + match($flattenRule->flattenedPropertyType){ + StringTag::class => null, //omit for TAG_String, as this is the common case + ByteTag::class => "byte", + IntTag::class => "int", + default => throw new \LogicException("Unexpected tag type " . $flattenRule->flattenedPropertyType . " in flattened property type") + } + ); + } + + private static function jsonModelToFlattenRule(BlockStateUpgradeSchemaModelFlattenInfo $flattenRule) : BlockStateUpgradeSchemaFlattenInfo{ + return new BlockStateUpgradeSchemaFlattenInfo( + $flattenRule->prefix, + $flattenRule->flattenedProperty, + $flattenRule->suffix, + $flattenRule->flattenedValueRemaps ?? [], + match ($flattenRule->flattenedPropertyType) { + "string", null => StringTag::class, + "int" => IntTag::class, + "byte" => ByteTag::class, + default => throw new \UnexpectedValueException("Unexpected flattened property type $flattenRule->flattenedPropertyType, expected 'string', 'int' or 'byte'") + } + ); + } + public static function toJsonModel(BlockStateUpgradeSchema $schema) : BlockStateUpgradeSchemaModel{ $result = new BlockStateUpgradeSchemaModel(); $result->maxVersionMajor = $schema->maxVersionMajor; @@ -292,19 +326,19 @@ public static function toJsonModel(BlockStateUpgradeSchema $schema) : BlockState self::buildRemappedValuesIndex($schema, $result); + foreach(Utils::stringifyKeys($schema->flattenedProperties) as $blockName => $flattenRule){ + $result->flattenedProperties[$blockName] = self::flattenRuleToJsonModel($flattenRule); + } + if(isset($result->flattenedProperties)){ + ksort($result->flattenedProperties); + } + foreach(Utils::stringifyKeys($schema->remappedStates) as $oldBlockName => $remaps){ $keyedRemaps = []; foreach($remaps as $remap){ $modelRemap = new BlockStateUpgradeSchemaModelBlockRemap( array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->oldState), - is_string($remap->newName) ? - $remap->newName : - new BlockStateUpgradeSchemaModelFlattenedName( - $remap->newName->prefix, - $remap->newName->flattenedProperty, - $remap->newName->suffix, - $remap->newName->flattenedValueRemaps - ), + is_string($remap->newName) ? $remap->newName : self::flattenRuleToJsonModel($remap->newName), array_map(fn(Tag $tag) => self::tagToJsonModel($tag), $remap->newState), $remap->copiedState ); diff --git a/src/data/bedrock/block/upgrade/BlockStateUpgrader.php b/src/data/bedrock/block/upgrade/BlockStateUpgrader.php index 4a305d8bc35..2dce762b8df 100644 --- a/src/data/bedrock/block/upgrade/BlockStateUpgrader.php +++ b/src/data/bedrock/block/upgrade/BlockStateUpgrader.php @@ -24,10 +24,14 @@ namespace pocketmine\data\bedrock\block\upgrade; use pocketmine\data\bedrock\block\BlockStateData; +use pocketmine\nbt\tag\ByteTag; +use pocketmine\nbt\tag\IntTag; use pocketmine\nbt\tag\StringTag; use pocketmine\nbt\tag\Tag; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; use function count; +use function get_class; use function is_string; use function ksort; use function max; @@ -79,6 +83,8 @@ public function upgrade(BlockStateData $blockStateData) : BlockStateData{ * version doesn't tell us which of the schemas have already been applied. * If there's only one schema for a version (the norm), we can safely assume it's already been applied if * the version is the same, and skip over it. + * TODO: this causes issues when testing isolated schemas since there will only be one schema for a version. + * The second check should be disabled for that case. */ if($version > $resultVersion || (count($schemaList) === 1 && $version === $resultVersion)){ continue; @@ -104,10 +110,21 @@ private function applySchema(BlockStateUpgradeSchema $schema, BlockStateData $bl } $oldName = $blockStateData->getName(); - $newName = $schema->renamedIds[$oldName] ?? null; + $states = $blockStateData->getStates(); + + if(isset($schema->renamedIds[$oldName]) && isset($schema->flattenedProperties[$oldName])){ + //TODO: this probably ought to be validated when the schema is constructed + throw new AssumptionFailedError("Both renamedIds and flattenedProperties are set for the same block ID \"$oldName\" - don't know what to do"); + } + if(isset($schema->renamedIds[$oldName])){ + $newName = $schema->renamedIds[$oldName] ?? null; + }elseif(isset($schema->flattenedProperties[$oldName])){ + [$newName, $states] = $this->applyPropertyFlattened($schema->flattenedProperties[$oldName], $oldName, $states); + }else{ + $newName = null; + } $stateChanges = 0; - $states = $blockStateData->getStates(); $states = $this->applyPropertyAdded($schema, $oldName, $states, $stateChanges); $states = $this->applyPropertyRemoved($schema, $oldName, $states, $stateChanges); @@ -140,15 +157,8 @@ private function applyStateRemapped(BlockStateUpgradeSchema $schema, BlockStateD if(is_string($remap->newName)){ $newName = $remap->newName; }else{ - $flattenedValue = $oldState[$remap->newName->flattenedProperty] ?? null; - if($flattenedValue instanceof StringTag){ - $embedValue = $remap->newName->flattenedValueRemaps[$flattenedValue->getValue()] ?? $flattenedValue->getValue(); - $newName = sprintf("%s%s%s", $remap->newName->prefix, $embedValue, $remap->newName->suffix); - unset($oldState[$remap->newName->flattenedProperty]); - }else{ - //flattened property is not a TAG_String, so this transformation is not applicable - continue; - } + //discard flatten modifications to state - the remap newState and copiedState will take care of it + [$newName, ] = $this->applyPropertyFlattened($remap->newName, $oldName, $oldState); } $newState = $remap->newState; @@ -266,4 +276,32 @@ private function applyPropertyValueChanged(BlockStateUpgradeSchema $schema, stri return $states; } + + /** + * @param Tag[] $states + * @phpstan-param array $states + * + * @return (string|Tag[])[] + * @phpstan-return array{0: string, 1: array} + */ + private function applyPropertyFlattened(BlockStateUpgradeSchemaFlattenInfo $flattenInfo, string $oldName, array $states) : array{ + $flattenedValue = $states[$flattenInfo->flattenedProperty] ?? null; + $expectedType = $flattenInfo->flattenedPropertyType; + if(!$flattenedValue instanceof $expectedType){ + //flattened property is not of the expected type, so this transformation is not applicable + return [$oldName, $states]; + } + $embedKey = match(get_class($flattenedValue)){ + StringTag::class => $flattenedValue->getValue(), + ByteTag::class => (string) $flattenedValue->getValue(), + IntTag::class => (string) $flattenedValue->getValue(), + //flattenedPropertyType is always one of these three types, but PHPStan doesn't know that + default => throw new AssumptionFailedError("flattenedPropertyType should be one of these three types, but have " . get_class($flattenedValue)), + }; + $embedValue = $flattenInfo->flattenedValueRemaps[$embedKey] ?? $embedKey; + $newName = sprintf("%s%s%s", $flattenInfo->prefix, $embedValue, $flattenInfo->suffix); + unset($states[$flattenInfo->flattenedProperty]); + + return [$newName, $states]; + } } diff --git a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModel.php b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModel.php index 1a4a14c87cf..7d91438e4f4 100644 --- a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModel.php +++ b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModel.php @@ -75,6 +75,12 @@ final class BlockStateUpgradeSchemaModel implements \JsonSerializable{ */ public array $remappedPropertyValuesIndex; + /** + * @var BlockStateUpgradeSchemaModelFlattenInfo[] + * @phpstan-var array + */ + public array $flattenedProperties; + /** * @var BlockStateUpgradeSchemaModelBlockRemap[][] * @phpstan-var array> diff --git a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelBlockRemap.php b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelBlockRemap.php index 0f518479e59..6accf1f0212 100644 --- a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelBlockRemap.php +++ b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelBlockRemap.php @@ -43,7 +43,7 @@ final class BlockStateUpgradeSchemaModelBlockRemap{ * Either this or newName must be present * Due to technical limitations of jsonmapper, we can't use a union type here */ - public BlockStateUpgradeSchemaModelFlattenedName $newFlattenedName; + public BlockStateUpgradeSchemaModelFlattenInfo $newFlattenedName; /** * @var BlockStateUpgradeSchemaModelTag[]|null @@ -67,9 +67,9 @@ final class BlockStateUpgradeSchemaModelBlockRemap{ * @phpstan-param array $newState * @phpstan-param list $copiedState */ - public function __construct(array $oldState, string|BlockStateUpgradeSchemaModelFlattenedName $newNameRule, array $newState, array $copiedState){ + public function __construct(array $oldState, string|BlockStateUpgradeSchemaModelFlattenInfo $newNameRule, array $newState, array $copiedState){ $this->oldState = count($oldState) === 0 ? null : $oldState; - if($newNameRule instanceof BlockStateUpgradeSchemaModelFlattenedName){ + if($newNameRule instanceof BlockStateUpgradeSchemaModelFlattenInfo){ $this->newFlattenedName = $newNameRule; }else{ $this->newName = $newNameRule; diff --git a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenedName.php b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenInfo.php similarity index 81% rename from src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenedName.php rename to src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenInfo.php index 001192f4789..6da590287b9 100644 --- a/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenedName.php +++ b/src/data/bedrock/block/upgrade/model/BlockStateUpgradeSchemaModelFlattenInfo.php @@ -25,12 +25,13 @@ use function count; -final class BlockStateUpgradeSchemaModelFlattenedName implements \JsonSerializable{ +final class BlockStateUpgradeSchemaModelFlattenInfo implements \JsonSerializable{ /** @required */ public string $prefix; /** @required */ public string $flattenedProperty; + public ?string $flattenedPropertyType = null; /** @required */ public string $suffix; /** @@ -43,11 +44,12 @@ final class BlockStateUpgradeSchemaModelFlattenedName implements \JsonSerializab * @param string[] $flattenedValueRemaps * @phpstan-param array $flattenedValueRemaps */ - public function __construct(string $prefix, string $flattenedProperty, string $suffix, array $flattenedValueRemaps){ + public function __construct(string $prefix, string $flattenedProperty, string $suffix, array $flattenedValueRemaps, ?string $flattenedPropertyType = null){ $this->prefix = $prefix; $this->flattenedProperty = $flattenedProperty; $this->suffix = $suffix; $this->flattenedValueRemaps = $flattenedValueRemaps; + $this->flattenedPropertyType = $flattenedPropertyType; } /** @@ -58,6 +60,9 @@ public function jsonSerialize() : array{ if(count($this->flattenedValueRemaps) === 0){ unset($result["flattenedValueRemaps"]); } + if($this->flattenedPropertyType === null){ + unset($result["flattenedPropertyType"]); + } return $result; } } diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index 0af17bc7333..86f6621df80 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -25,6 +25,8 @@ use pocketmine\block\Bed; use pocketmine\block\Block; +use pocketmine\block\CopperDoor; +use pocketmine\block\utils\CopperOxidation; use pocketmine\block\utils\DyeColor; use pocketmine\block\VanillaBlocks as Blocks; use pocketmine\data\bedrock\CompoundTypeIds; @@ -54,6 +56,7 @@ public function __construct( $this->register1to1BlockWithMetaMappings(); $this->register1to1ItemWithMetaMappings(); $this->register1ToNItemMappings(); + $this->registerMiscBlockMappings(); $this->registerMiscItemMappings(); } @@ -131,6 +134,7 @@ private function register1to1BlockMappings() : void{ $this->map1to1Block(Ids::BIRCH_DOOR, Blocks::BIRCH_DOOR()); $this->map1to1Block(Ids::BREWING_STAND, Blocks::BREWING_STAND()); $this->map1to1Block(Ids::CAKE, Blocks::CAKE()); + $this->map1to1Block(Ids::CAMPFIRE, Blocks::CAMPFIRE()); $this->map1to1Block(Ids::CAULDRON, Blocks::CAULDRON()); $this->map1to1Block(Ids::CHAIN, Blocks::CHAIN()); $this->map1to1Block(Ids::CHERRY_DOOR, Blocks::CHERRY_DOOR()); @@ -146,6 +150,7 @@ private function register1to1BlockMappings() : void{ $this->map1to1Block(Ids::MANGROVE_DOOR, Blocks::MANGROVE_DOOR()); $this->map1to1Block(Ids::NETHER_WART, Blocks::NETHER_WART()); $this->map1to1Block(Ids::REPEATER, Blocks::REDSTONE_REPEATER()); + $this->map1to1Block(Ids::SOUL_CAMPFIRE, Blocks::SOUL_CAMPFIRE()); $this->map1to1Block(Ids::SPRUCE_DOOR, Blocks::SPRUCE_DOOR()); $this->map1to1Block(Ids::SUGAR_CANE, Blocks::SUGARCANE()); $this->map1to1Block(Ids::WARPED_DOOR, Blocks::WARPED_DOOR()); @@ -526,4 +531,29 @@ private function registerMiscItemMappings() : void{ } $this->serializer?->map(Items::DYE(), fn(Dye $item) => new Data(DyeColorIdMap::getInstance()->toItemId($item->getColor()))); } + + /** + * Registers serializers and deserializers for PocketMine-MP blockitems that don't fit any other pattern. + * Ideally we want to get rid of this completely, if possible. + * + * Most of these are single PocketMine-MP blocks which map to multiple IDs depending on their properties, which is + * complex to implement in a generic way. + */ + private function registerMiscBlockMappings() : void{ + $copperDoorStateIdMap = []; + foreach ([ + [Ids::COPPER_DOOR, CopperOxidation::NONE, false], + [Ids::EXPOSED_COPPER_DOOR, CopperOxidation::EXPOSED, false], + [Ids::WEATHERED_COPPER_DOOR, CopperOxidation::WEATHERED, false], + [Ids::OXIDIZED_COPPER_DOOR, CopperOxidation::OXIDIZED, false], + [Ids::WAXED_COPPER_DOOR, CopperOxidation::NONE, true], + [Ids::WAXED_EXPOSED_COPPER_DOOR, CopperOxidation::EXPOSED, true], + [Ids::WAXED_WEATHERED_COPPER_DOOR, CopperOxidation::WEATHERED, true], + [Ids::WAXED_OXIDIZED_COPPER_DOOR, CopperOxidation::OXIDIZED, true] + ] as [$id, $oxidation, $waxed]) { + $copperDoorStateIdMap[$oxidation->value][$waxed ? 1 : 0] = $id; + $this->deserializer?->mapBlock($id, fn() => Blocks::COPPER_DOOR()->setOxidation($oxidation)->setWaxed($waxed)); + } + $this->serializer?->mapBlock(Blocks::COPPER_DOOR(), fn(CopperDoor $block) => new Data($copperDoorStateIdMap[$block->getOxidation()->value][$block->isWaxed() ? 1 : 0])); + } } diff --git a/src/entity/object/PaintingMotive.php b/src/entity/object/PaintingMotive.php index a456630fca3..00114c508be 100644 --- a/src/entity/object/PaintingMotive.php +++ b/src/entity/object/PaintingMotive.php @@ -37,9 +37,11 @@ public static function init() : void{ new PaintingMotive(1, 1, "Aztec2"), new PaintingMotive(1, 1, "Bomb"), new PaintingMotive(1, 1, "Kebab"), + new PaintingMotive(1, 1, "meditative"), new PaintingMotive(1, 1, "Plant"), new PaintingMotive(1, 1, "Wasteland"), new PaintingMotive(1, 2, "Graham"), + new PaintingMotive(1, 2, "prairie_ride"), new PaintingMotive(1, 2, "Wanderer"), new PaintingMotive(2, 1, "Courbet"), new PaintingMotive(2, 1, "Creebet"), @@ -47,8 +49,10 @@ public static function init() : void{ new PaintingMotive(2, 1, "Sea"), new PaintingMotive(2, 1, "Sunset"), new PaintingMotive(2, 2, "Bust"), + new PaintingMotive(2, 2, "baroque"), new PaintingMotive(2, 2, "Earth"), new PaintingMotive(2, 2, "Fire"), + new PaintingMotive(2, 2, "humble"), new PaintingMotive(2, 2, "Match"), new PaintingMotive(2, 2, "SkullAndRoses"), new PaintingMotive(2, 2, "Stage"), @@ -56,12 +60,28 @@ public static function init() : void{ new PaintingMotive(2, 2, "Water"), new PaintingMotive(2, 2, "Wind"), new PaintingMotive(2, 2, "Wither"), + new PaintingMotive(3, 3, "bouquet"), + new PaintingMotive(3, 3, "cavebird"), + new PaintingMotive(3, 3, "cotan"), + new PaintingMotive(3, 3, "endboss"), + new PaintingMotive(3, 3, "fern"), + new PaintingMotive(3, 3, "owlemons"), + new PaintingMotive(3, 3, "sunflowers"), + new PaintingMotive(3, 3, "tides"), + new PaintingMotive(3, 4, "backyard"), + new PaintingMotive(3, 4, "pond"), + new PaintingMotive(4, 2, "changing"), new PaintingMotive(4, 2, "Fighters"), + new PaintingMotive(4, 2, "finding"), + new PaintingMotive(4, 2, "lowmist"), + new PaintingMotive(4, 2, "passage"), new PaintingMotive(4, 3, "DonkeyKong"), new PaintingMotive(4, 3, "Skeleton"), new PaintingMotive(4, 4, "BurningSkull"), + new PaintingMotive(4, 4, "orb"), new PaintingMotive(4, 4, "Pigscene"), - new PaintingMotive(4, 4, "Pointer") + new PaintingMotive(4, 4, "Pointer"), + new PaintingMotive(4, 4, "unpacked") ] as $motive){ self::registerMotive($motive); } diff --git a/src/event/block/CampfireCookEvent.php b/src/event/block/CampfireCookEvent.php new file mode 100644 index 00000000000..3762f5848f1 --- /dev/null +++ b/src/event/block/CampfireCookEvent.php @@ -0,0 +1,63 @@ +input = clone $input; + } + + public function getCampfire() : Campfire{ + return $this->campfire; + } + + public function getSlot() : int{ + return $this->slot; + } + + public function getInput() : Item{ + return $this->input; + } + + public function getResult() : Item{ + return $this->result; + } + + public function setResult(Item $result) : void{ + $this->result = $result; + } +} diff --git a/src/inventory/ArmorInventory.php b/src/inventory/ArmorInventory.php index dcb3c04cb2e..0b3ae5b7bcc 100644 --- a/src/inventory/ArmorInventory.php +++ b/src/inventory/ArmorInventory.php @@ -23,8 +23,13 @@ namespace pocketmine\inventory; +use pocketmine\block\BlockTypeIds; use pocketmine\entity\Living; +use pocketmine\inventory\transaction\action\validator\CallbackSlotValidator; +use pocketmine\inventory\transaction\TransactionValidationException; +use pocketmine\item\Armor; use pocketmine\item\Item; +use pocketmine\item\ItemBlock; class ArmorInventory extends SimpleInventory{ public const SLOT_HEAD = 0; @@ -36,6 +41,8 @@ public function __construct( protected Living $holder ){ parent::__construct(4); + + $this->validators->add(new CallbackSlotValidator($this->validate(...))); } public function getHolder() : Living{ @@ -73,4 +80,20 @@ public function setLeggings(Item $leggings) : void{ public function setBoots(Item $boots) : void{ $this->setItem(self::SLOT_FEET, $boots); } + + private function validate(Inventory $inventory, Item $item, int $slot) : ?TransactionValidationException{ + if($item instanceof Armor){ + if($item->getArmorSlot() !== $slot){ + return new TransactionValidationException("Armor item is in wrong slot"); + } + }else{ + if(!($slot === ArmorInventory::SLOT_HEAD && $item instanceof ItemBlock && ( + $item->getBlock()->getTypeId() === BlockTypeIds::CARVED_PUMPKIN || + $item->getBlock()->getTypeId() === BlockTypeIds::MOB_HEAD + ))){ + return new TransactionValidationException("Item is not accepted in an armor slot"); + } + } + return null; + } } diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php index 254e44b1ea0..522c827a4b2 100644 --- a/src/inventory/BaseInventory.php +++ b/src/inventory/BaseInventory.php @@ -36,8 +36,10 @@ /** * This class provides everything needed to implement an inventory, minus the underlying storage system. + * + * @phpstan-import-type SlotValidators from SlotValidatedInventory */ -abstract class BaseInventory implements Inventory{ +abstract class BaseInventory implements Inventory, SlotValidatedInventory{ protected int $maxStackSize = Inventory::MAX_STACK; /** @var Player[] */ protected array $viewers = []; @@ -46,9 +48,12 @@ abstract class BaseInventory implements Inventory{ * @phpstan-var ObjectSet */ protected ObjectSet $listeners; + /** @phpstan-var SlotValidators */ + protected ObjectSet $validators; public function __construct(){ $this->listeners = new ObjectSet(); + $this->validators = new ObjectSet(); } public function getMaxStackSize() : int{ @@ -398,4 +403,8 @@ public function slotExists(int $slot) : bool{ public function getListeners() : ObjectSet{ return $this->listeners; } + + public function getSlotValidators() : ObjectSet{ + return $this->validators; + } } diff --git a/src/inventory/SlotValidatedInventory.php b/src/inventory/SlotValidatedInventory.php new file mode 100644 index 00000000000..f30ebf8a011 --- /dev/null +++ b/src/inventory/SlotValidatedInventory.php @@ -0,0 +1,46 @@ + + */ +interface SlotValidatedInventory{ + /** + * Returns a set of validators that will be used to determine whether an item can be placed in a particular slot. + * All validators need to return null for the transaction to be allowed. + * If one of the validators returns an exception, the transaction will be cancelled. + * + * There is no guarantee that the validators will be called in any particular order. + * + * @phpstan-return SlotValidators + */ + public function getSlotValidators() : ObjectSet; +} diff --git a/src/inventory/transaction/action/SlotChangeAction.php b/src/inventory/transaction/action/SlotChangeAction.php index 453f0c4d228..68c3dba1b32 100644 --- a/src/inventory/transaction/action/SlotChangeAction.php +++ b/src/inventory/transaction/action/SlotChangeAction.php @@ -24,6 +24,7 @@ namespace pocketmine\inventory\transaction\action; use pocketmine\inventory\Inventory; +use pocketmine\inventory\SlotValidatedInventory; use pocketmine\inventory\transaction\InventoryTransaction; use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Item; @@ -74,6 +75,14 @@ public function validate(Player $source) : void{ if($this->targetItem->getCount() > $this->inventory->getMaxStackSize()){ throw new TransactionValidationException("Target item exceeds inventory max stack size"); } + if($this->inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){ + foreach($this->inventory->getSlotValidators() as $validator){ + $ret = $validator->validate($this->inventory, $this->targetItem, $this->inventorySlot); + if($ret !== null){ + throw new TransactionValidationException("Target item is not accepted by the inventory at slot #" . $this->inventorySlot . ": " . $ret->getMessage(), 0, $ret); + } + } + } } /** diff --git a/src/inventory/transaction/action/validator/CallbackSlotValidator.php b/src/inventory/transaction/action/validator/CallbackSlotValidator.php new file mode 100644 index 00000000000..1670dc6232a --- /dev/null +++ b/src/inventory/transaction/action/validator/CallbackSlotValidator.php @@ -0,0 +1,44 @@ +validate)($inventory, $item, $slot); + } +} diff --git a/src/inventory/transaction/action/validator/SlotValidator.php b/src/inventory/transaction/action/validator/SlotValidator.php new file mode 100644 index 00000000000..1b78c91f7f4 --- /dev/null +++ b/src/inventory/transaction/action/validator/SlotValidator.php @@ -0,0 +1,38 @@ + false, "waxed_" => true] as $waxedPrefix => $waxed){ $register = fn(string $name, \Closure $callback) => $result->registerBlock($waxedPrefix . $oxPrefix . $name, $callback); $register("copper_block", fn() => Blocks::COPPER()->setOxidation($oxidation)->setWaxed($waxed)); + $register("chiseled_copper", fn() => Blocks::CHISELED_COPPER()->setOxidation($oxidation)->setWaxed($waxed)); + $register("copper_grate", fn() => Blocks::COPPER_GRATE()->setOxidation($oxidation)->setWaxed($waxed)); $register("cut_copper_block", fn() => Blocks::CUT_COPPER()->setOxidation($oxidation)->setWaxed($waxed)); $register("cut_copper_stairs", fn() => Blocks::CUT_COPPER_STAIRS()->setOxidation($oxidation)->setWaxed($waxed)); $register("cut_copper_slab", fn() => Blocks::CUT_COPPER_SLAB()->setOxidation($oxidation)->setWaxed($waxed)); + $register("copper_bulb", fn() => Blocks::COPPER_BULB()->setOxidation($oxidation)->setWaxed($waxed)); + $register("copper_door", fn() => Blocks::COPPER_DOOR()->setOxidation($oxidation)->setWaxed($waxed)); + $register("copper_trapdoor", fn() => Blocks::COPPER_TRAPDOOR()->setOxidation($oxidation)->setWaxed($waxed)); } } @@ -205,6 +210,7 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("cake", fn() => Blocks::CAKE()); $result->registerBlock("cake_block", fn() => Blocks::CAKE()); $result->registerBlock("calcite", fn() => Blocks::CALCITE()); + $result->registerBlock("campfire", fn() => Blocks::CAMPFIRE()); $result->registerBlock("candle", fn() => Blocks::CANDLE()); $result->registerBlock("carpet", fn() => Blocks::CARPET()); $result->registerBlock("carrot_block", fn() => Blocks::CARROTS()); @@ -239,6 +245,8 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("chiseled_red_sandstone", fn() => Blocks::CHISELED_RED_SANDSTONE()); $result->registerBlock("chiseled_sandstone", fn() => Blocks::CHISELED_SANDSTONE()); $result->registerBlock("chiseled_stone_bricks", fn() => Blocks::CHISELED_STONE_BRICKS()); + $result->registerBlock("chiseled_tuff", fn() => Blocks::CHISELED_TUFF()); + $result->registerBlock("chiseled_tuff_bricks", fn() => Blocks::CHISELED_TUFF_BRICKS()); $result->registerBlock("chorus_flower", fn() => Blocks::CHORUS_FLOWER()); $result->registerBlock("chorus_plant", fn() => Blocks::CHORUS_PLANT()); $result->registerBlock("clay_block", fn() => Blocks::CLAY()); @@ -897,6 +905,10 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("polished_granite", fn() => Blocks::POLISHED_GRANITE()); $result->registerBlock("polished_granite_slab", fn() => Blocks::POLISHED_GRANITE_SLAB()); $result->registerBlock("polished_granite_stairs", fn() => Blocks::POLISHED_GRANITE_STAIRS()); + $result->registerBlock("polished_tuff", fn() => Blocks::POLISHED_TUFF()); + $result->registerBlock("polished_tuff_slab", fn() => Blocks::POLISHED_TUFF_SLAB()); + $result->registerBlock("polished_tuff_stairs", fn() => Blocks::POLISHED_TUFF_STAIRS()); + $result->registerBlock("polished_tuff_wall", fn() => Blocks::POLISHED_TUFF_WALL()); $result->registerBlock("poppy", fn() => Blocks::POPPY()); $result->registerBlock("portal", fn() => Blocks::NETHER_PORTAL()); $result->registerBlock("portal_block", fn() => Blocks::NETHER_PORTAL()); @@ -1003,6 +1015,7 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("snow", fn() => Blocks::SNOW()); $result->registerBlock("snow_block", fn() => Blocks::SNOW()); $result->registerBlock("snow_layer", fn() => Blocks::SNOW_LAYER()); + $result->registerBlock("soul_campfire", fn() => Blocks::SOUL_CAMPFIRE()); $result->registerBlock("soul_lantern", fn() => Blocks::SOUL_LANTERN()); $result->registerBlock("soul_sand", fn() => Blocks::SOUL_SAND()); $result->registerBlock("soul_soil", fn() => Blocks::SOUL_SOIL()); @@ -1096,6 +1109,13 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("trunk", fn() => Blocks::OAK_PLANKS()); $result->registerBlock("trunk2", fn() => Blocks::ACACIA_LOG()->setStripped(false)); $result->registerBlock("tuff", fn() => Blocks::TUFF()); + $result->registerBlock("tuff_bricks", fn() => Blocks::TUFF_BRICKS()); + $result->registerBlock("tuff_brick_slab", fn() => Blocks::TUFF_BRICK_SLAB()); + $result->registerBlock("tuff_brick_stairs", fn() => Blocks::TUFF_BRICK_STAIRS()); + $result->registerBlock("tuff_brick_wall", fn() => Blocks::TUFF_BRICK_WALL()); + $result->registerBlock("tuff_slab", fn() => Blocks::TUFF_SLAB()); + $result->registerBlock("tuff_stairs", fn() => Blocks::TUFF_STAIRS()); + $result->registerBlock("tuff_wall", fn() => Blocks::TUFF_WALL()); $result->registerBlock("twisting_vines", fn() => Blocks::TWISTING_VINES()); $result->registerBlock("underwater_tnt", fn() => Blocks::TNT()->setWorksUnderwater(true)); $result->registerBlock("underwater_torch", fn() => Blocks::UNDERWATER_TORCH()); diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index bbd0dfc01b3..5115ee48a8b 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -591,12 +591,12 @@ protected function createEntity(World $world, Vector3 $pos, float $yaw, float $p } }); self::register("squid_spawn_egg", new class(new IID(Ids::SQUID_SPAWN_EGG), "Squid Spawn Egg") extends SpawnEgg{ - public function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ + protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ return new Squid(Location::fromObject($pos, $world, $yaw, $pitch)); } }); self::register("villager_spawn_egg", new class(new IID(Ids::VILLAGER_SPAWN_EGG), "Villager Spawn Egg") extends SpawnEgg{ - public function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ + protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{ return new Villager(Location::fromObject($pos, $world, $yaw, $pitch)); } }); diff --git a/src/item/enchantment/AvailableEnchantmentRegistry.php b/src/item/enchantment/AvailableEnchantmentRegistry.php index 2c6f421ede0..cae94c666f5 100644 --- a/src/item/enchantment/AvailableEnchantmentRegistry.php +++ b/src/item/enchantment/AvailableEnchantmentRegistry.php @@ -56,6 +56,7 @@ private function __construct(){ $this->register(Enchantments::PROJECTILE_PROTECTION(), [Tags::ARMOR], []); $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::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 cd57eb203a5..47a750ff27d 100644 --- a/src/item/enchantment/StringToEnchantmentParser.php +++ b/src/item/enchantment/StringToEnchantmentParser.php @@ -52,6 +52,7 @@ private static function make() : self{ $result->register("protection", fn() => VanillaEnchantments::PROTECTION()); $result->register("punch", fn() => VanillaEnchantments::PUNCH()); $result->register("respiration", fn() => VanillaEnchantments::RESPIRATION()); + $result->register("aqua_affinity", fn() => VanillaEnchantments::AQUA_AFFINITY()); $result->register("sharpness", fn() => VanillaEnchantments::SHARPNESS()); $result->register("silk_touch", fn() => VanillaEnchantments::SILK_TOUCH()); $result->register("swift_sneak", fn() => VanillaEnchantments::SWIFT_SNEAK()); diff --git a/src/item/enchantment/VanillaEnchantments.php b/src/item/enchantment/VanillaEnchantments.php index 779098c770e..19ce3971633 100644 --- a/src/item/enchantment/VanillaEnchantments.php +++ b/src/item/enchantment/VanillaEnchantments.php @@ -33,6 +33,7 @@ * @see build/generate-registry-annotations.php * @generate-registry-docblock * + * @method static Enchantment AQUA_AFFINITY() * @method static ProtectionEnchantment BLAST_PROTECTION() * @method static Enchantment EFFICIENCY() * @method static ProtectionEnchantment FEATHER_FALLING() @@ -144,6 +145,15 @@ protected static function setup() : void{ fn(int $level) : int => 10 * $level, 30 )); + self::register("AQUA_AFFINITY", new Enchantment( + KnownTranslationFactory::enchantment_waterWorker(), + Rarity::RARE, + 0, + 0, + 1, + null, + 40 + )); self::register("SHARPNESS", new SharpnessEnchantment( KnownTranslationFactory::enchantment_damage_all(), diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index c0969b61b2e..e4c303121f7 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -364,6 +364,7 @@ protected static function createContainerOpen(int $id, Inventory $inv) : ?array{ FurnaceType::FURNACE => WindowTypes::FURNACE, FurnaceType::BLAST_FURNACE => WindowTypes::BLAST_FURNACE, FurnaceType::SMOKER => WindowTypes::SMOKER, + FurnaceType::CAMPFIRE, FurnaceType::SOUL_CAMPFIRE => throw new \LogicException("Campfire inventory cannot be displayed to a player") }, $inv instanceof EnchantInventory => WindowTypes::ENCHANTMENT, $inv instanceof BrewingStandInventory => WindowTypes::BREWING_STAND, diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index 2dce5bbb8b5..0eee71e0eec 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -30,6 +30,7 @@ use pocketmine\event\server\DataPacketReceiveEvent; use pocketmine\event\server\DataPacketSendEvent; use pocketmine\form\Form; +use pocketmine\item\Item; use pocketmine\lang\KnownTranslationFactory; use pocketmine\lang\Translatable; use pocketmine\math\Vector3; @@ -65,6 +66,7 @@ use pocketmine\network\mcpe\protocol\PacketDecodeException; use pocketmine\network\mcpe\protocol\PacketPool; use pocketmine\network\mcpe\protocol\PlayerListPacket; +use pocketmine\network\mcpe\protocol\PlayerStartItemCooldownPacket; use pocketmine\network\mcpe\protocol\PlayStatusPacket; use pocketmine\network\mcpe\protocol\ProtocolInfo; use pocketmine\network\mcpe\protocol\serializer\PacketBatch; @@ -111,6 +113,7 @@ use pocketmine\utils\BinaryStream; use pocketmine\utils\ObjectSet; use pocketmine\utils\TextFormat; +use pocketmine\world\format\io\GlobalItemDataHandlers; use pocketmine\world\Position; use pocketmine\YmlServerProperties; use function array_map; @@ -1289,6 +1292,13 @@ public function onOpenSignEditor(Vector3 $signPosition, bool $frontSide) : void{ $this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide)); } + public function onItemCooldownChanged(Item $item, int $ticks) : void{ + $this->sendDataPacket(PlayerStartItemCooldownPacket::create( + GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName(), + $ticks + )); + } + public function tick() : void{ if(!$this->isConnected()){ $this->dispose(); diff --git a/src/network/mcpe/cache/CraftingDataCache.php b/src/network/mcpe/cache/CraftingDataCache.php index 29f37590e3b..14523f74c7f 100644 --- a/src/network/mcpe/cache/CraftingDataCache.php +++ b/src/network/mcpe/cache/CraftingDataCache.php @@ -130,6 +130,8 @@ private function buildCraftingDataCache(CraftingManager $manager) : CraftingData FurnaceType::FURNACE => FurnaceRecipeBlockName::FURNACE, FurnaceType::BLAST_FURNACE => FurnaceRecipeBlockName::BLAST_FURNACE, FurnaceType::SMOKER => FurnaceRecipeBlockName::SMOKER, + FurnaceType::CAMPFIRE => FurnaceRecipeBlockName::CAMPFIRE, + FurnaceType::SOUL_CAMPFIRE => FurnaceRecipeBlockName::SOUL_CAMPFIRE }; foreach($manager->getFurnaceRecipeManager($furnaceType)->getAll() as $recipe){ $input = $converter->coreRecipeIngredientToNet($recipe->getInput())->getDescriptor(); diff --git a/src/player/Player.php b/src/player/Player.php index d442c6a3b20..8ae206e1a4a 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -283,7 +283,11 @@ public static function isValidUserName(?string $name) : bool{ protected string $locale = "en_US"; protected int $startAction = -1; - /** @var int[] ID => ticks map */ + + /** + * @phpstan-var array + * @var int[] stateId|cooldownTag => ticks map + */ protected array $usedItemsCooldown = []; private int $lastEmoteTick = 0; @@ -697,7 +701,7 @@ public function getItemUseDuration() : int{ */ public function getItemCooldownExpiry(Item $item) : int{ $this->checkItemCooldowns(); - return $this->usedItemsCooldown[$item->getStateId()] ?? 0; + return $this->usedItemsCooldown[$item->getCooldownTag() ?? $item->getStateId()] ?? 0; } /** @@ -705,7 +709,7 @@ public function getItemCooldownExpiry(Item $item) : int{ */ public function hasItemCooldown(Item $item) : bool{ $this->checkItemCooldowns(); - return isset($this->usedItemsCooldown[$item->getStateId()]); + return isset($this->usedItemsCooldown[$item->getCooldownTag() ?? $item->getStateId()]); } /** @@ -714,7 +718,8 @@ public function hasItemCooldown(Item $item) : bool{ public function resetItemCooldown(Item $item, ?int $ticks = null) : void{ $ticks = $ticks ?? $item->getCooldownTicks(); if($ticks > 0){ - $this->usedItemsCooldown[$item->getStateId()] = $this->server->getTick() + $ticks; + $this->usedItemsCooldown[$item->getCooldownTag() ?? $item->getStateId()] = $this->server->getTick() + $ticks; + $this->getNetworkSession()->onItemCooldownChanged($item, $ticks); } } diff --git a/src/world/sound/CampfireSound.php b/src/world/sound/CampfireSound.php new file mode 100644 index 00000000000..7e01342efe6 --- /dev/null +++ b/src/world/sound/CampfireSound.php @@ -0,0 +1,35 @@ +, int given\\.$#" - count: 1 - path: ../../../src/resourcepacks/ZippedResourcePack.php - - message: "#^Property pocketmine\\\\resourcepacks\\\\ZippedResourcePack\\:\\:\\$fileResource \\(resource\\) does not accept resource\\|false\\.$#" count: 1 @@ -1035,11 +1030,6 @@ parameters: count: 1 path: ../../../src/world/format/io/region/RegionLoader.php - - - message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int given\\.$#" - count: 1 - path: ../../../src/world/format/io/region/RegionLoader.php - - message: "#^Parameter \\#2 \\$size of function ftruncate expects int\\<0, max\\>, int given\\.$#" count: 1 @@ -1190,18 +1180,3 @@ parameters: count: 1 path: ../../../src/world/light/SkyLightUpdate.php - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 1 - path: ../../phpunit/block/BlockTest.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 1 - path: ../../phpunit/block/regenerate_consistency_check.php - - - - message: "#^Parameter \\#1 \\$logFile of class pocketmine\\\\utils\\\\MainLogger constructor expects string, string\\|false given\\.$#" - count: 1 - path: ../../phpunit/scheduler/AsyncPoolTest.php - diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon index de38903bd39..0fc3defda78 100644 --- a/tests/phpstan/configs/phpstan-bugs.neon +++ b/tests/phpstan/configs/phpstan-bugs.neon @@ -5,6 +5,16 @@ parameters: count: 1 path: ../../../src/block/CakeWithCandle.php + - + message: "#^Method pocketmine\\\\block\\\\CopperDoor\\:\\:onInteractCopper\\(\\) has parameter \\$returnedItems with no value type specified in iterable type array\\.$#" + count: 1 + path: ../../../src/block/CopperDoor.php + + - + message: "#^Method pocketmine\\\\block\\\\CopperTrapdoor\\:\\:onInteractCopper\\(\\) has parameter \\$returnedItems with no value type specified in iterable type array\\.$#" + count: 1 + path: ../../../src/block/CopperTrapdoor.php + - message: "#^Method pocketmine\\\\block\\\\DoubleTallGrass\\:\\:traitGetDropsForIncompatibleTool\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 diff --git a/tests/phpunit/block/block_factory_consistency_check.json b/tests/phpunit/block/block_factory_consistency_check.json index 27bf831841b..79804d8cbd8 100644 --- a/tests/phpunit/block/block_factory_consistency_check.json +++ b/tests/phpunit/block/block_factory_consistency_check.json @@ -79,6 +79,7 @@ "CAKE_WITH_CANDLE": 2, "CAKE_WITH_DYED_CANDLE": 32, "CALCITE": 1, + "CAMPFIRE": 8, "CANDLE": 8, "CARPET": 16, "CARROTS": 8, @@ -104,6 +105,7 @@ "CHERRY_WOOD": 6, "CHEST": 4, "CHISELED_BOOKSHELF": 256, + "CHISELED_COPPER": 8, "CHISELED_DEEPSLATE": 1, "CHISELED_NETHER_BRICKS": 1, "CHISELED_POLISHED_BLACKSTONE": 1, @@ -111,6 +113,8 @@ "CHISELED_RED_SANDSTONE": 1, "CHISELED_SANDSTONE": 1, "CHISELED_STONE_BRICKS": 1, + "CHISELED_TUFF": 1, + "CHISELED_TUFF_BRICKS": 1, "CHORUS_FLOWER": 6, "CHORUS_PLANT": 1, "CLAY": 1, @@ -130,7 +134,11 @@ "CONCRETE": 16, "CONCRETE_POWDER": 16, "COPPER": 8, + "COPPER_BULB": 32, + "COPPER_DOOR": 256, + "COPPER_GRATE": 8, "COPPER_ORE": 1, + "COPPER_TRAPDOOR": 128, "CORAL": 10, "CORAL_BLOCK": 10, "CORAL_FAN": 20, @@ -532,6 +540,10 @@ "POLISHED_GRANITE": 1, "POLISHED_GRANITE_SLAB": 3, "POLISHED_GRANITE_STAIRS": 8, + "POLISHED_TUFF": 1, + "POLISHED_TUFF_SLAB": 3, + "POLISHED_TUFF_STAIRS": 8, + "POLISHED_TUFF_WALL": 162, "POPPY": 1, "POTATOES": 8, "POTION_CAULDRON": 6, @@ -610,6 +622,7 @@ "SMOOTH_STONE_SLAB": 3, "SNOW": 1, "SNOW_LAYER": 8, + "SOUL_CAMPFIRE": 8, "SOUL_FIRE": 1, "SOUL_LANTERN": 2, "SOUL_SAND": 1, @@ -660,6 +673,13 @@ "TRIPWIRE": 16, "TRIPWIRE_HOOK": 16, "TUFF": 1, + "TUFF_BRICKS": 1, + "TUFF_BRICK_SLAB": 3, + "TUFF_BRICK_STAIRS": 8, + "TUFF_BRICK_WALL": 162, + "TUFF_SLAB": 3, + "TUFF_STAIRS": 8, + "TUFF_WALL": 162, "TWISTING_VINES": 26, "UNDERWATER_TORCH": 5, "VINES": 16, diff --git a/tests/phpunit/data/bedrock/block/upgrade/BlockStateUpgraderTest.php b/tests/phpunit/data/bedrock/block/upgrade/BlockStateUpgraderTest.php index 4d4d321ec0d..91afd8ed929 100644 --- a/tests/phpunit/data/bedrock/block/upgrade/BlockStateUpgraderTest.php +++ b/tests/phpunit/data/bedrock/block/upgrade/BlockStateUpgraderTest.php @@ -24,8 +24,10 @@ namespace pocketmine\data\bedrock\block\upgrade; use PHPUnit\Framework\TestCase; +use pocketmine\block\Block; use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\nbt\tag\IntTag; +use pocketmine\nbt\tag\StringTag; use const PHP_INT_MAX; class BlockStateUpgraderTest extends TestCase{ @@ -210,6 +212,23 @@ public function testRemapAndRenameProperty(\Closure $getStateData, ?int $valueAf self::assertSame($upgradedStateData->getState(self::TEST_PROPERTY_2)?->getValue(), $valueAfter); } + public function testFlattenProperty() : void{ + $schema = $this->getNewSchema(); + $schema->flattenedProperties[self::TEST_BLOCK] = new BlockStateUpgradeSchemaFlattenInfo( + "minecraft:", + "test", + "_suffix", + [], + StringTag::class + ); + + $stateData = new BlockStateData(self::TEST_BLOCK, ["test" => new StringTag("value1")], 0); + $upgradedStateData = $this->upgrade($stateData, fn() => $stateData); + + self::assertSame("minecraft:value1_suffix", $upgradedStateData->getName()); + self::assertEmpty($upgradedStateData->getStates()); + } + /** * @phpstan-return \Generator */ diff --git a/tools/generate-blockstate-upgrade-schema.php b/tools/blockstate-upgrade-schema-utils.php similarity index 59% rename from tools/generate-blockstate-upgrade-schema.php rename to tools/blockstate-upgrade-schema-utils.php index 741098f7aa1..b7a9a4169dc 100644 --- a/tools/generate-blockstate-upgrade-schema.php +++ b/tools/blockstate-upgrade-schema-utils.php @@ -21,36 +21,49 @@ declare(strict_types=1); -namespace pocketmine\tools\generate_blockstate_upgrade_schema; +namespace pocketmine\tools\blockstate_upgrade_schema_utils; use pocketmine\data\bedrock\block\BlockStateData; +use pocketmine\data\bedrock\block\upgrade\BlockStateUpgrader; use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchema; use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchemaBlockRemap; -use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchemaFlattenedName; +use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchemaFlattenInfo; use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchemaUtils; use pocketmine\data\bedrock\block\upgrade\BlockStateUpgradeSchemaValueRemap; use pocketmine\nbt\LittleEndianNbtSerializer; +use pocketmine\nbt\tag\ByteTag; +use pocketmine\nbt\tag\IntTag; use pocketmine\nbt\tag\StringTag; use pocketmine\nbt\tag\Tag; use pocketmine\nbt\TreeRoot; +use pocketmine\network\mcpe\convert\BlockStateDictionary; use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Filesystem; use pocketmine\utils\Utils; +use Symfony\Component\Filesystem\Path; use function array_key_first; use function array_key_last; use function array_keys; use function array_map; use function array_shift; +use function array_unique; use function array_values; use function count; use function dirname; +use function file_exists; use function file_put_contents; use function fwrite; +use function get_class; +use function get_debug_type; use function implode; +use function is_dir; +use function is_numeric; use function json_encode; use function ksort; use function min; +use function preg_match; +use function scandir; use function sort; use function strlen; use function strrev; @@ -83,18 +96,18 @@ function encodeProperty(Tag $tag) : string{ } /** + * @param TreeRoot[] $oldNewStateList + * @phpstan-param list $oldNewStateList + * * @return BlockStateMapping[][] * @phpstan-return array> */ -function loadUpgradeTable(string $file, bool $reverse) : array{ - $contents = Filesystem::fileGetContents($file); - $data = (new NetworkNbtSerializer())->readMultiple($contents); - +function buildUpgradeTableFromData(array $oldNewStateList, bool $reverse) : array{ $result = []; - for($i = 0; isset($data[$i]); $i += 2){ - $oldTag = $data[$i]->mustGetCompoundTag(); - $newTag = $data[$i + 1]->mustGetCompoundTag(); + for($i = 0; isset($oldNewStateList[$i]); $i += 2){ + $oldTag = $oldNewStateList[$i]->mustGetCompoundTag(); + $newTag = $oldNewStateList[$i + 1]->mustGetCompoundTag(); $old = BlockStateData::fromNbt($reverse ? $newTag : $oldTag); $new = BlockStateData::fromNbt($reverse ? $oldTag : $newTag); @@ -107,6 +120,17 @@ function loadUpgradeTable(string $file, bool $reverse) : array{ return $result; } +/** + * @return BlockStateMapping[][] + * @phpstan-return array> + */ +function loadUpgradeTableFromFile(string $file, bool $reverse) : array{ + $contents = Filesystem::fileGetContents($file); + $data = (new NetworkNbtSerializer())->readMultiple($contents); + + return buildUpgradeTableFromData($data, $reverse); +} + /** * @param BlockStateData[] $states * @phpstan-param array $states @@ -159,6 +183,11 @@ function processStateGroup(string $oldName, array $upgradeTable, BlockStateUpgra $removedProperties = []; $renamedProperties = []; + $uniqueNewIds = []; + foreach($upgradeTable as $pair){ + $uniqueNewIds[$pair->new->getName()] = $pair->new->getName(); + } + foreach(Utils::stringifyKeys($newProperties) as $newPropertyName => $newPropertyValues){ if(count($newPropertyValues) === 1){ $newPropertyValue = $newPropertyValues[array_key_first($newPropertyValues)]; @@ -254,6 +283,45 @@ function processStateGroup(string $oldName, array $upgradeTable, BlockStateUpgra } } + if(count($uniqueNewIds) > 1){ + //detect possible flattening + $flattenedProperty = null; + $flattenedPropertyType = null; + $flattenedPropertyMap = []; + foreach($removedProperties as $removedProperty){ + $valueMap = []; + foreach($upgradeTable as $pair){ + $oldValue = $pair->old->getState($removedProperty); + if($oldValue === null){ + throw new AssumptionFailedError("We already checked that all states had consistent old properties"); + } + if(!checkFlattenPropertySuitability($oldValue, $flattenedPropertyType, $pair->new->getName(), $valueMap)){ + continue 2; + } + } + + if($flattenedProperty !== null){ + //found multiple candidates for flattening - fallback to remappedStates + return false; + } + //we found a suitable candidate + $flattenedProperty = $removedProperty; + $flattenedPropertyMap = $valueMap; + break; + } + + if($flattenedProperty === null){ + //can't figure out how the new IDs are related to the old states - fallback to remappedStates + return false; + } + if($flattenedPropertyType === null){ + throw new AssumptionFailedError("This should never happen at this point"); + } + + $result->flattenedProperties[$oldName] = buildFlattenPropertyRule($flattenedPropertyMap, $flattenedProperty, $flattenedPropertyType); + unset($removedProperties[$flattenedProperty]); + } + //finally, write the results to the schema if(count($remappedPropertyValues) !== 0){ @@ -308,43 +376,100 @@ function findCommonSuffix(array $strings) : string{ return strrev(findCommonPrefix($reversed)); } +/** + * @param string[] $valueToIdMap + * @phpstan-param ?class-string $expectedType + * @phpstan-param-out class-string $expectedType + * @phpstan-param array $valueToIdMap + * @phpstan-param-out array $valueToIdMap + */ +function checkFlattenPropertySuitability(Tag $oldValue, ?string &$expectedType, string $actualNewId, array &$valueToIdMap) : bool{ + //TODO: lots of similar logic to the remappedStates builder below + if(!$oldValue instanceof ByteTag && !$oldValue instanceof IntTag && !$oldValue instanceof StringTag){ + //unknown property type - bad candidate for flattening + return false; + } + if($expectedType === null){ + $expectedType = get_class($oldValue); + }elseif(!$oldValue instanceof $expectedType){ + //property type mismatch - bad candidate for flattening + return false; + } + + $rawValue = (string) $oldValue->getValue(); + $existingNewId = $valueToIdMap[$rawValue] ?? null; + if($existingNewId !== null && $existingNewId !== $actualNewId){ + //this property value is associated with multiple new IDs - bad candidate for flattening + return false; + } + $valueToIdMap[$rawValue] = $actualNewId; + + return true; +} + +/** + * @param string[] $valueToId + * @phpstan-param array $valueToId + * @phpstan-param class-string $propertyType + */ +function buildFlattenPropertyRule(array $valueToId, string $propertyName, string $propertyType) : BlockStateUpgradeSchemaFlattenInfo{ + $ids = array_values($valueToId); + + //TODO: this is a bit too enthusiastic. For example, when flattening the old "stone", it will see that + //"granite", "andesite", "stone" etc all have "e" as a common suffix, which works, but looks a bit daft. + //This also causes more remaps to be generated than necessary, since some of the values are already + //contained in the new ID. + $idPrefix = findCommonPrefix($ids); + $idSuffix = findCommonSuffix($ids); + if(strlen($idSuffix) < 2){ + $idSuffix = ""; + } + + $valueMap = []; + foreach(Utils::stringifyKeys($valueToId) as $value => $newId){ + $newValue = substr($newId, strlen($idPrefix), $idSuffix !== "" ? -strlen($idSuffix) : null); + if($newValue !== $value){ + $valueMap[$value] = $newValue; + } + } + + $allNumeric = true; + if(count($valueMap) > 0){ + foreach(Utils::stringifyKeys($valueMap) as $value => $newValue){ + if(!is_numeric($value)){ + $allNumeric = false; + break; + } + } + if($allNumeric){ + //add a dummy key to force the JSON to be an object and not a list + $valueMap["dummy"] = "map_not_list"; + } + } + + return new BlockStateUpgradeSchemaFlattenInfo( + $idPrefix, + $propertyName, + $idSuffix, + $valueMap, + $propertyType, + ); +} + /** * @param string[][][] $candidateFlattenedValues * @phpstan-param array>> $candidateFlattenedValues + * @param string[] $candidateFlattenPropertyTypes + * @phpstan-param array> $candidateFlattenPropertyTypes * - * @return BlockStateUpgradeSchemaFlattenedName[][] - * @phpstan-return array> + * @return BlockStateUpgradeSchemaFlattenInfo[][] + * @phpstan-return array> */ -function buildFlattenPropertyRules(array $candidateFlattenedValues) : array{ +function buildFlattenPropertyRules(array $candidateFlattenedValues, array $candidateFlattenPropertyTypes) : array{ $flattenPropertyRules = []; foreach(Utils::stringifyKeys($candidateFlattenedValues) as $propertyName => $filters){ foreach(Utils::stringifyKeys($filters) as $filter => $valueToId){ - $ids = array_values($valueToId); - - //TODO: this is a bit too enthusiastic. For example, when flattening the old "stone", it will see that - //"granite", "andesite", "stone" etc all have "e" as a common suffix, which works, but looks a bit daft. - //This also causes more remaps to be generated than necessary, since some of the values are already - //contained in the new ID. - $idPrefix = findCommonPrefix($ids); - $idSuffix = findCommonSuffix($ids); - if(strlen($idSuffix) < 2){ - $idSuffix = ""; - } - - $valueMap = []; - foreach(Utils::stringifyKeys($valueToId) as $value => $newId){ - $newValue = substr($newId, strlen($idPrefix), $idSuffix !== "" ? -strlen($idSuffix) : null); - if($newValue !== $value){ - $valueMap[$value] = $newValue; - } - } - - $flattenPropertyRules[$propertyName][$filter] = new BlockStateUpgradeSchemaFlattenedName( - $idPrefix, - $propertyName, - $idSuffix, - $valueMap - ); + $flattenPropertyRules[$propertyName][$filter] = buildFlattenPropertyRule($valueToId, $propertyName, $candidateFlattenPropertyTypes[$propertyName]); } } ksort($flattenPropertyRules, SORT_STRING); @@ -406,56 +531,54 @@ function processRemappedStates(array $upgradeTable) : array{ $notFlattenedProperties = []; $candidateFlattenedValues = []; + $candidateFlattenedPropertyTypes = []; foreach($upgradeTable as $pair){ foreach(Utils::stringifyKeys($pair->old->getStates()) as $propertyName => $propertyValue){ if(isset($notFlattenedProperties[$propertyName])){ continue; } - if(!$propertyValue instanceof StringTag){ - $notFlattenedProperties[$propertyName] = true; - continue; - } - $rawValue = $propertyValue->getValue(); - if($rawValue === ""){ - $notFlattenedProperties[$propertyName] = true; - continue; - } $filter = $pair->old->getStates(); foreach($unchangedStatesByNewName[$pair->new->getName()] as $unchangedPropertyName){ + if($unchangedPropertyName === $propertyName){ + $notFlattenedProperties[$propertyName] = true; + continue 2; + } unset($filter[$unchangedPropertyName]); } unset($filter[$propertyName]); $rawFilter = encodeOrderedProperties($filter); - if(isset($candidateFlattenedValues[$propertyName][$rawFilter])){ - $valuesToIds = $candidateFlattenedValues[$propertyName][$rawFilter]; - $existingNewId = $valuesToIds[$rawValue] ?? null; - if($existingNewId !== null && $existingNewId !== $pair->new->getName()){ - //this old value is associated with multiple new IDs - bad candidate for flattening - $notFlattenedProperties[$propertyName] = true; - continue; - } - foreach(Utils::stringifyKeys($valuesToIds) as $otherRawValue => $otherNewId){ - if($otherRawValue === $rawValue){ - continue; - } - if($otherNewId === $pair->new->getName()){ - //this old value maps to the same new ID as another old value - bad candidate for flattening - $notFlattenedProperties[$propertyName] = true; - continue 2; - } - } + $candidateFlattenedValues[$propertyName][$rawFilter] ??= []; + $expectedType = $candidateFlattenedPropertyTypes[$propertyName] ?? null; + if(!checkFlattenPropertySuitability($propertyValue, $expectedType, $pair->new->getName(), $candidateFlattenedValues[$propertyName][$rawFilter])){ + $notFlattenedProperties[$propertyName] = true; + continue; + } + $candidateFlattenedPropertyTypes[$propertyName] = $expectedType; + } + } + foreach(Utils::stringifyKeys($candidateFlattenedValues) as $propertyName => $filters){ + foreach($filters as $valuesToIds){ + if(count(array_unique($valuesToIds)) === 1){ + //this property doesn't influence the new ID + $notFlattenedProperties[$propertyName] = true; + continue 2; } - $candidateFlattenedValues[$propertyName][$rawFilter][$rawValue] = $pair->new->getName(); } } foreach(Utils::stringifyKeys($notFlattenedProperties) as $propertyName => $_){ unset($candidateFlattenedValues[$propertyName]); } - $flattenedProperties = buildFlattenPropertyRules($candidateFlattenedValues); + $flattenedProperties = buildFlattenPropertyRules($candidateFlattenedValues, $candidateFlattenedPropertyTypes); $flattenProperty = array_key_first($flattenedProperties); + //Properties with fewer rules take up less space for the same result + foreach(Utils::stringifyKeys($flattenedProperties) as $propertyName => $rules){ + if(count($rules) < count($flattenedProperties[$flattenProperty])){ + $flattenProperty = $propertyName; + } + } $list = []; @@ -475,8 +598,8 @@ function processRemappedStates(array $upgradeTable) : array{ ksort($cleanedNewState); if($flattenProperty !== null){ $flattenedValue = $cleanedOldState[$flattenProperty] ?? null; - if(!$flattenedValue instanceof StringTag){ - throw new AssumptionFailedError("This should always be a TAG_String ($newName $flattenProperty)"); + if(!$flattenedValue instanceof StringTag && !$flattenedValue instanceof IntTag && !$flattenedValue instanceof ByteTag){ + throw new AssumptionFailedError("Non-flattenable type of tag ($newName $flattenProperty) but have " . get_debug_type($flattenedValue)); } unset($cleanedOldState[$flattenProperty]); } @@ -583,10 +706,15 @@ function generateBlockStateUpgradeSchema(array $upgradeTable) : BlockStateUpgrad throw new \RuntimeException("States with the same ID should be fully consistent"); } }else{ - //block mapped to multiple different new IDs; we can't guess these, so we just do a plain old remap - //even if some of the states stay under the same ID, the compression techniques used by this function - //implicitly rely on knowing the full set of old states and their new transformations - $result->remappedStates[$oldName] = processRemappedStates($blockStateMappings); + //try processing this as a regular state group first + //if a property was flattened into the ID, the remaining states will normally be consistent + //if not we fall back to remap states and state filters + if(!processStateGroup($oldName, $blockStateMappings, $result)){ + //block mapped to multiple different new IDs; we can't guess these, so we just do a plain old remap + //even if some of the states stay under the same ID, the compression techniques used by this function + //implicitly rely on knowing the full set of old states and their new transformations + $result->remappedStates[$oldName] = processRemappedStates($blockStateMappings); + } } } @@ -594,18 +722,42 @@ function generateBlockStateUpgradeSchema(array $upgradeTable) : BlockStateUpgrad } /** - * @param string[] $argv + * @param BlockStateMapping[][] $upgradeTable + * @phpstan-param array> $upgradeTable */ -function main(array $argv) : int{ - if(count($argv) !== 3){ - fwrite(STDERR, "Required arguments: input file path, output file path\n"); - return 1; +function testBlockStateUpgradeSchema(array $upgradeTable, BlockStateUpgradeSchema $schema) : bool{ + //TODO: HACK! + //the upgrader won't apply the schema if it's the same version and there's only one schema with a matching version + //ID (for performance reasons), which is a problem for testing isolated schemas + //add a dummy schema to bypass this optimization + $dummySchema = new BlockStateUpgradeSchema($schema->maxVersionMajor, $schema->maxVersionMinor, $schema->maxVersionPatch, $schema->maxVersionRevision, $schema->getSchemaId() + 1); + $upgrader = new BlockStateUpgrader([$schema, $dummySchema]); + + foreach($upgradeTable as $mappingsByOldName){ + foreach($mappingsByOldName as $mapping){ + $expectedNewState = $mapping->new; + + $actualNewState = $upgrader->upgrade($mapping->old); + + if(!$expectedNewState->equals($actualNewState)){ + \GlobalLogger::get()->error("Expected: " . $expectedNewState->toNbt()); + \GlobalLogger::get()->error("Actual: " . $actualNewState->toNbt()); + return false; + } + } } - $input = $argv[1]; - $output = $argv[2]; + return true; +} - $table = loadUpgradeTable($input, false); +/** + * @param string[] $argv + */ +function cmdGenerate(array $argv) : int{ + $upgradeTableFile = $argv[2]; + $schemaFile = $argv[3]; + + $table = loadUpgradeTableFromFile($upgradeTableFile, false); ksort($table, SORT_STRING); @@ -614,13 +766,148 @@ function main(array $argv) : int{ \GlobalLogger::get()->warning("All states appear to be the same! No schema generated."); return 0; } + + if(!testBlockStateUpgradeSchema($table, $diff)){ + \GlobalLogger::get()->error("Generated schema does not produce the results expected by $upgradeTableFile"); + \GlobalLogger::get()->error("This is probably a bug in the schema generation code. Please report this to the developers."); + return 1; + } + file_put_contents( - $output, + $schemaFile, json_encode(BlockStateUpgradeSchemaUtils::toJsonModel($diff), JSON_PRETTY_PRINT) . "\n" ); - \GlobalLogger::get()->info("Schema file $output generated successfully."); + \GlobalLogger::get()->info("Schema file $schemaFile generated successfully."); + return 0; +} + +/** + * @param string[] $argv + */ +function cmdTest(array $argv) : int{ + $upgradeTableFile = $argv[2]; + $schemaFile = $argv[3]; + + $table = loadUpgradeTableFromFile($upgradeTableFile, false); + + ksort($table, SORT_STRING); + + $schema = BlockStateUpgradeSchemaUtils::loadSchemaFromString(Filesystem::fileGetContents($schemaFile), 0); + if(!testBlockStateUpgradeSchema($table, $schema)){ + \GlobalLogger::get()->error("Schema $schemaFile does not produce the results predicted by $upgradeTableFile"); + return 1; + } + \GlobalLogger::get()->info("Schema $schemaFile is valid according to $upgradeTableFile"); + + return 0; +} + +/** + * @param string[] $argv + */ +function cmdUpdate(array $argv) : int{ + [, , $oldSchemaFile, $oldPaletteFile, $newSchemaFile] = $argv; + + $palette = BlockStateDictionary::loadPaletteFromString(Filesystem::fileGetContents($oldPaletteFile)); + $schema = BlockStateUpgradeSchemaUtils::loadSchemaFromString(Filesystem::fileGetContents($oldSchemaFile), 0); + //TODO: HACK! + //the upgrader won't apply the schema if it's the same version and there's only one schema with a matching version + //ID (for performance reasons), which is a problem for testing isolated schemas + //add a dummy schema to bypass this optimization + $dummySchema = new BlockStateUpgradeSchema($schema->maxVersionMajor, $schema->maxVersionMinor, $schema->maxVersionPatch, $schema->maxVersionRevision, $schema->getSchemaId() + 1); + $upgrader = new BlockStateUpgrader([$schema, $dummySchema]); + + $tags = []; + foreach($palette as $stateData){ + $tags[] = new TreeRoot($stateData->toNbt()); + $tags[] = new TreeRoot($upgrader->upgrade($stateData)->toNbt()); + } + + $upgradeTable = buildUpgradeTableFromData($tags, false); + $newSchema = generateBlockStateUpgradeSchema($upgradeTable); + + if(!testBlockStateUpgradeSchema($upgradeTable, $newSchema)){ + \GlobalLogger::get()->error("Updated schema does not produce the expected results!"); + \GlobalLogger::get()->error("This is probably a bug in the schema generation code. Please report this to the developers."); + return 1; + } + + file_put_contents( + $newSchemaFile, + json_encode(BlockStateUpgradeSchemaUtils::toJsonModel($newSchema), JSON_PRETTY_PRINT) . "\n" + ); + \GlobalLogger::get()->info("Schema file $newSchemaFile updated to new format (from $oldSchemaFile) successfully."); + return 0; +} +/** + * @param string[] $argv + */ +function cmdUpdateAll(array $argv) : int{ + $oldPaletteFilenames = [ + '1.9.0' => '1.09.0', + '1.19.50' => '1.19.50.23_beta', + '1.19.60' => '1.19.60.26_beta', + '1.19.70' => '1.19.70.26_beta', + '1.19.80' => '1.19.80.24_beta', + ]; + $schemaDir = $argv[2]; + $paletteArchiveDir = $argv[3]; + + $schemaFileNames = scandir($schemaDir); + if($schemaFileNames === false){ + \GlobalLogger::get()->error("Failed to read schema directory $schemaDir"); + return 1; + } + foreach($schemaFileNames as $file){ + $schemaFile = Path::join($schemaDir, $file); + if(!file_exists($schemaFile) || is_dir($schemaFile)){ + continue; + } + + if(preg_match('/^\d{4}_(.+?)_to_(.+?).json/', $file, $matches) !== 1){ + continue; + } + $oldPaletteFile = Path::join($paletteArchiveDir, ($oldPaletteFilenames[$matches[1]] ?? $matches[1]) . '.nbt'); + + //a bit clunky but it avoids having to make yet another function + //TODO: perhaps in the future we should write the result to a tmpfile until all schemas are updated, + //and then copy the results into place at the end + if(cmdUpdate([$argv[0], "update", $schemaFile, $oldPaletteFile, $schemaFile]) !== 0){ + return 1; + } + } + + \GlobalLogger::get()->info("All schemas updated successfully."); return 0; } +/** + * @param string[] $argv + */ +function main(array $argv) : int{ + $options = [ + "generate" => [["palette upgrade table file", "schema output file"], cmdGenerate(...)], + "test" => [["palette upgrade table file", "schema output file"], cmdTest(...)], + "update" => [["schema input file", "old palette file", "updated schema output file"], cmdUpdate(...)], + "update-all" => [["schema folder", "path to BlockPaletteArchive"], cmdUpdateAll(...)] + ]; + + $selected = $argv[1] ?? null; + if($selected === null || !isset($options[$selected])){ + fwrite(STDERR, "Available commands:\n"); + foreach($options as $command => [$args, $callback]){ + fwrite(STDERR, " - $command " . implode(" ", array_map(fn(string $a) => "<$a>", $args)) . "\n"); + } + return 1; + } + + $callback = $options[$selected][1]; + if(count($argv) !== count($options[$selected][0]) + 2){ + fwrite(STDERR, "Usage: {$argv[0]} $selected " . implode(" ", array_map(fn(string $a) => "<$a>", $options[$selected][0])) . "\n"); + return 1; + } + return $callback($argv); +} + exit(main($argv));