diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4ab347ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# http://editorconfig.org/ +root = yes + +[*] +indent_size = 4 +indent_style = tab +charset = utf-8 +end_of_line = LF +insert_final_newline = true +trim_trailing_whitespace = true + +[composer.json] +indent_size = 4 + +[{*.json,*.yml,*.yaml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..ce1c9324 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Auto detect text files and perform LF normalization +* text=auto +*.php text eol=lf +*.txt text eol=lf \ No newline at end of file diff --git a/README.md b/README.md index 621c5969..b850ded8 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,15 @@ A PocketMine-MP plugin that implements support for custom blocks, items and enti Discord -Official Discord community chat for socializing, receiving help with the plugin, and sharing creations. Join in on the -fun! +Official Discord community chat for socializing, receiving help with the plugin, and sharing creations. Join in on the fun! ## Usage The usage guides have been moved to the [Customies Wiki](https://github.com/CustomiesDevs/Customies/wiki)! +[![Mojang Item Docs](https://img.shields.io/badge/📖-Microsoft_Docs-blue)](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/itemreference/examples/itemcomponentlist?view=minecraft-bedrock-stable) +[![Mojang Block Docs](https://img.shields.io/badge/📖-Microsoft_Docs-blue)](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/blockreference/examples/blockcomponents/blockcomponentslist?view=minecraft-bedrock-stable) + ## Important Contributors | Name | Contribution | diff --git a/plugin.yml b/plugin.yml index e1d59501..c5abfcfa 100644 --- a/plugin.yml +++ b/plugin.yml @@ -3,8 +3,8 @@ description: A PocketMine-MP plugin that implements support for custom blocks, i main: customiesdevs\customies\Customies src-namespace-prefix: customiesdevs\customies -version: 1.4.0 -api: 5.1.3 +version: 1.5.0 +api: 5.37.0 authors: - DenielWorld @@ -12,4 +12,4 @@ authors: contributors: - JackNoordhuis - Unickorn - - abimek + - abimek \ No newline at end of file diff --git a/src/Customies.php b/src/Customies.php index 4ce46398..aba7d870 100644 --- a/src/Customies.php +++ b/src/Customies.php @@ -6,17 +6,22 @@ use customiesdevs\customies\block\CustomiesBlockFactory; use pocketmine\plugin\PluginBase; use pocketmine\scheduler\ClosureTask; +use pocketmine\utils\SingletonTrait; final class Customies extends PluginBase { + use SingletonTrait; + + public function onLoad(): void{ + self::setInstance($this); + } protected function onEnable(): void { $this->getServer()->getPluginManager()->registerEvents(new CustomiesListener(), $this); - $cachePath = $this->getDataFolder() . "idcache"; - $this->getScheduler()->scheduleDelayedTask(new ClosureTask(static function () use ($cachePath): void { + $this->getScheduler()->scheduleDelayedTask(new ClosureTask(static function (): void { // This task is scheduled with a 0-tick delay so it runs as soon as the server has started. Plugins should - // register their custom blocks and entities in onEnable() before this is executed. - CustomiesBlockFactory::getInstance()->addWorkerInitHook($cachePath); + // register their custom blocks and entities in onEnable() before this is executed + CustomiesBlockFactory::getInstance()->addWorkerInitHook(); }), 0); } -} +} \ No newline at end of file diff --git a/src/CustomiesListener.php b/src/CustomiesListener.php index 4ade1464..fbdfcebd 100644 --- a/src/CustomiesListener.php +++ b/src/CustomiesListener.php @@ -13,31 +13,33 @@ use function count; final class CustomiesListener implements Listener { + /** @var BlockPaletteEntry[] */ private array $cachedBlockPalette = []; private Experiments $experiments; public function __construct() { $this->experiments = new Experiments([ - // "data_driven_items" is required for custom blocks to render in-game. With this disabled, they will be - // shown as the UPDATE texture block. + // "data_driven_items" is required for custom blocks to render in-game. + // With this disabled, custom blocks will appear as the UPDATE texture block. "data_driven_items" => true, + "upcoming_creator_features" => true ], true); } public function onDataPacketSend(DataPacketSendEvent $event): void { foreach($event->getPackets() as $packet){ - if($packet instanceof StartGamePacket) { - if(count($this->cachedBlockPalette) === 0) { + if($packet instanceof StartGamePacket){ + if(count($this->cachedBlockPalette) === 0){ // Wait for the data to be needed before it is actually cached. Allows for all blocks and items to be // registered before they are cached for the rest of the runtime. $this->cachedBlockPalette = CustomiesBlockFactory::getInstance()->getBlockPaletteEntries(); } $packet->levelSettings->experiments = $this->experiments; $packet->blockPalette = $this->cachedBlockPalette; - } elseif($packet instanceof ResourcePackStackPacket) { + }elseif($packet instanceof ResourcePackStackPacket) { $packet->experiments = $this->experiments; } } } -} +} \ No newline at end of file diff --git a/src/block/BlockComponentsTrait.php b/src/block/BlockComponentsTrait.php deleted file mode 100644 index fef4fa63..00000000 --- a/src/block/BlockComponentsTrait.php +++ /dev/null @@ -1,67 +0,0 @@ -components[$component->getName()] = $component; - } - - public function hasComponent(string $name): bool { - return isset($this->components[$name]); - } - - /** - * @return BlockComponent[] - */ - public function getComponents(): array { - return $this->components; - } - - /** - * Initialises a block's components with default values inferred from existing properties. - * @todo Work on more default values depending on different pm classes similar to items - * @param string $texture Texture name for the material. - * @param bool $useGeometry Check if geometry component should be used, default is set to `true` - */ - protected function initComponent(string $texture, bool $useGeometry = true): void { - $this->addComponent(new BreathabilityComponent()); - $this->addComponent(new DestructibleByExplosionComponent()); - $this->addComponent(new DestructibleByMiningComponent($this->getBreakInfo()->getHardness())); - $this->addComponent(new LightEmissionComponent($this->getLightLevel())); - $this->addComponent(new LightDampeningComponent($this->getLightFilter())); - $this->addComponent(new FrictionComponent($this->getFrictionFactor())); - if ($useGeometry){ - $this->addComponent(new GeometryComponent()); - } - $this->addComponent(new SelectionBoxComponent()); - if($this->hasEntityCollision()){ - $this->addComponent(new CollisionBoxComponent()); - } - if($this->getFlammability() > 0){ - $this->addComponent(new FlammableComponent($this->getFlameEncouragement())); - } - if($this->getName() !== "Unknown") { - $this->addComponent(new DisplayNameComponent($this->getName())); - } - $this->addComponent(new MaterialInstancesComponent([new Material(Material::TARGET_ALL, $texture, Material::RENDER_METHOD_OPAQUE)])); - } -} \ No newline at end of file diff --git a/src/block/BlockPalette.php b/src/block/BlockPalette.php index 0e037484..3df7c192 100644 --- a/src/block/BlockPalette.php +++ b/src/block/BlockPalette.php @@ -45,6 +45,7 @@ public function __construct() { } /** + * Returns all block states in the palette. * @return BlockStateDictionaryEntry[] */ public function getStates(): array { @@ -52,6 +53,7 @@ public function getStates(): array { } /** + * Returns all custom block states. * @return BlockStateDictionaryEntry[] */ public function getCustomStates(): array { @@ -60,6 +62,9 @@ public function getCustomStates(): array { /** * Inserts the provided state in to the correct position of the palette. + * @param CompoundTag $state + * @param int $meta + * @return void */ public function insertState(CompoundTag $state, int $meta = 0): void { if(($name = $state->getString(BlockStateData::TAG_NAME, "")) === "") { @@ -74,6 +79,8 @@ public function insertState(CompoundTag $state, int $meta = 0): void { /** * Sorts the palette's block states in the correct order, also adding the provided state to the array. + * @param BlockStateDictionaryEntry $newState + * @return void */ private function sortWith(BlockStateDictionaryEntry $newState): void { // To sort the block palette we first have to split the palette up in to groups of states. We only want to sort @@ -112,4 +119,4 @@ private function sortWith(BlockStateDictionaryEntry $newState): void { throw new AssumptionFailedError(BlockTypeNames::INFO_UPDATE . " should always exist") ); } -} +} \ No newline at end of file diff --git a/src/block/CustomiesBlockFactory.php b/src/block/CustomiesBlockFactory.php index ff4aa006..7a2ff9b5 100644 --- a/src/block/CustomiesBlockFactory.php +++ b/src/block/CustomiesBlockFactory.php @@ -4,8 +4,9 @@ namespace customiesdevs\customies\block; use Closure; -use customiesdevs\customies\block\permutations\Permutable; -use customiesdevs\customies\block\permutations\Permutation; +use customiesdevs\customies\block\component\BlockComponents; +use customiesdevs\customies\block\permutations\BlockPermutation; +use customiesdevs\customies\block\permutations\BlockPermutations; use customiesdevs\customies\block\permutations\Permutations; use customiesdevs\customies\item\CreativeInventoryInfo; use customiesdevs\customies\item\CustomiesItemFactory; @@ -17,18 +18,14 @@ use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\block\convert\BlockStateReader; use pocketmine\data\bedrock\block\convert\BlockStateWriter; -use pocketmine\inventory\CreativeCategory; -use pocketmine\inventory\CreativeGroup; -use pocketmine\inventory\CreativeInventory; -use pocketmine\lang\Translatable; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; use pocketmine\network\mcpe\protocol\types\BlockPaletteEntry; use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\Server; -use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; use pocketmine\world\format\io\GlobalBlockStateHandlers; +use RuntimeException; use function array_map; use function array_reverse; use function hash; @@ -45,43 +42,33 @@ final class CustomiesBlockFactory { private array $blockFuncs = []; /** @var BlockPaletteEntry[] */ private array $blockPaletteEntries = []; - /** @var array */ + /** @var array Map of block identifiers to block instances */ private array $customBlocks = []; - private array $groups = []; /** * Adds a worker initialize hook to the async pool to sync the BlockFactory for every thread worker that is created. * It is especially important for the workers that deal with chunk encoding, as using the wrong runtime ID mappings * can result in massive issues with almost every block showing as the wrong thing and causing lag to clients. */ - public function addWorkerInitHook(string $cachePath): void { + public function addWorkerInitHook(): void { $server = Server::getInstance(); $blocks = $this->blockFuncs; - $server->getAsyncPool()->addWorkerStartHook(static function (int $worker) use ($cachePath, $server, $blocks): void { - $server->getAsyncPool()->submitTaskToWorker(new AsyncRegisterBlocksTask($cachePath, $blocks), $worker); + $server->getAsyncPool()->addWorkerStartHook(static function (int $worker) use ($server, $blocks): void { + $server->getAsyncPool()->submitTaskToWorker(new AsyncRegisterBlocksTask($blocks), $worker); }); } /** * Get a custom block from its identifier. An exception will be thrown if the block is not registered. + * @param string $identifier Unique block identifier (e.g. "namespace:block_name") + * @return Block A clone of the registered block. + * @throws InvalidArgumentException If the block is not registered */ public function get(string $identifier): Block { - return clone ( - $this->customBlocks[$identifier] ?? - throw new InvalidArgumentException("Custom block $identifier is not registered") - ); - } - - private function loadGroups() : void { - if($this->groups !== []){ - return; - } - foreach(CreativeInventory::getInstance()->getAllEntries() as $entry){ - $group = $entry->getGroup(); - if($group !== null){ - $this->groups[$group->getName()->getText()] = $group; - } + if(!isset($this->customBlocks[$identifier])){ + throw new InvalidArgumentException("Custom block $identifier is not registered"); } + return clone $this->customBlocks[$identifier]; } /** @@ -95,13 +82,22 @@ public function getBlockPaletteEntries(): array { /** * Register a block to the BlockFactory and all the required mappings. A custom stateReader and stateWriter can be * provided to allow for custom block state serialization. - * @phpstan-param (Closure(): Block) $blockFunc - * @phpstan-param null|(Closure(BlockStateWriter): Block) $serializer - * @phpstan-param null|(Closure(Block): BlockStateReader) $deserializer + * @param Closure $blockFunc A closure that returns a new instance of the block to register. + * @param string $identifier The unique identifier for the block (e.g. "namespace:block_name"). + * @param CreativeInventoryInfo $creativeInfo Creative inventory information for the block. Default set to `Equipment` Category. + * @param (Closure(BlockStateWriter): Block)|null $serializer Optional closure that takes a BlockStateWriter and returns it after writing the block state. + * @param (Closure(Block): BlockStateReader)|null $deserializer Optional closure that takes a BlockStateReader and returns a new instance of the block after reading the state. + * @throws InvalidArgumentException If the blockFunc does not return a Block instance. */ - public function registerBlock(Closure $blockFunc, string $identifier, ?CreativeInventoryInfo $creativeInfo = null, ?Closure $serializer = null, ?Closure $deserializer = null): void { + public function registerBlock( + Closure $blockFunc, + string $identifier, + CreativeInventoryInfo $creativeInfo = new CreativeInventoryInfo(CreativeInventoryInfo::CATEGORY_EQUIPMENT), + ?Closure $serializer = null, + ?Closure $deserializer = null + ): void { $block = $blockFunc(); - if(!$block instanceof Block) { + if(!$block instanceof Block){ throw new InvalidArgumentException("Class returned from closure is not a Block"); } @@ -109,116 +105,91 @@ public function registerBlock(Closure $blockFunc, string $identifier, ?CreativeI CustomiesItemFactory::getInstance()->registerBlockItem($identifier, $block); $this->customBlocks[$identifier] = $block; - $propertiesTag = CompoundTag::create(); - $components = CompoundTag::create(); - if($block instanceof BlockComponents) { - foreach ($block->getComponents() as $component) { - $components->setTag($component->getName(), $component->getValue()); + $nbtTag = CompoundTag::create(); + $componentsTag = CompoundTag::create(); + // Adds Components to Block + if($block instanceof BlockComponents){ + foreach($block->getComponents() as $component){ + $tag = NBT::getTagType($component->getValue()) ?? throw new RuntimeException("Failed to get tag type for component: " . $component->getName()); + $componentsTag->setTag($component->getName(), $tag); } } - - if($block instanceof Permutable) { - $blockPropertyNames = $blockPropertyValues = $blockProperties = []; - foreach($block->getBlockProperties() as $blockProperty){ - $blockPropertyNames[] = $blockProperty->getName(); - $blockPropertyValues[] = $blockProperty->getValues(); - $blockProperties[] = $blockProperty->toNBT(); + // Creative NBT + $nbtTag->setTag("menu_category", + CompoundTag::create() + ->setString("category", $creativeInfo->getCategory()) + ->setString("group", $creativeInfo->getGroup()) + ->setByte("is_hidden_in_commands", 0) + ); + // Adds States/Permutation to Block + if($block instanceof BlockPermutations){ + $blockNames = $blockValues = $blockProperties = []; + foreach($block->getStates() as $state){ + $blockNames[] = $state->getName(); + $blockValues[] = $state->getValues(); + $blockProperties[] = NBT::getTagType($state->getValue()); } - $permutations = array_map(static fn(Permutation $permutation) => $permutation->toNBT(), $block->getPermutations()); - - // The 'minecraft:on_player_placing' component is required for the client to predict block placement, making - // it a smoother experience for the end-user. - $components->setTag("minecraft:on_player_placing", CompoundTag::create()); - $propertiesTag - ->setTag("permutations", new ListTag($permutations)) - ->setTag("properties", new ListTag(array_reverse($blockProperties))); // fix client-side order - - foreach(Permutations::getCartesianProduct($blockPropertyValues) as $meta => $permutations){ + $nbtTag->setTag("permutations", new ListTag(array_map( + static fn(BlockPermutation $p) => NBT::getTagType($p->toArray()), + $block->getPermutations() + ))); + $nbtTag->setTag("properties", new ListTag(array_reverse($blockProperties))); + foreach(Permutations::getCartesianProduct($blockValues) as $meta => $stateValues){ + $stateTag = CompoundTag::create(); // We need to insert states for every possible permutation to allow for all blocks to be used and to // keep in sync with the client's block palette. - $states = CompoundTag::create(); - foreach($permutations as $i => $value){ - $states->setTag($blockPropertyNames[$i], NBT::getTagType($value)); + foreach($stateValues as $i => $value){ + $stateTag->setTag($blockNames[$i], NBT::getTagType($value)); } - $blockState = CompoundTag::create() - ->setString(BlockStateData::TAG_NAME, $identifier) - ->setTag(BlockStateData::TAG_STATES, $states); - BlockPalette::getInstance()->insertState($blockState, $meta); + BlockPalette::getInstance()->insertState( + CompoundTag::create() + ->setString(BlockStateData::TAG_NAME, $identifier) + ->setTag(BlockStateData::TAG_STATES, $stateTag), + $meta + ); } - - $serializer ??= static function (Permutable $block) use ($identifier, $blockPropertyNames) : BlockStateWriter { - $b = BlockStateWriter::create($identifier); - $block->serializeState($b); - return $b; + $serializer ??= static function (BlockPermutations $b) use ($identifier): BlockStateWriter { + $writer = BlockStateWriter::create($identifier); + $b->serializeState($writer); + return $writer; }; - $deserializer ??= static function (BlockStateReader $in) use ($block, $identifier, $blockPropertyNames) : Permutable { + $deserializer ??= static function (BlockStateReader $in) use ($identifier): BlockPermutations { $b = CustomiesBlockFactory::getInstance()->get($identifier); - assert($b instanceof Permutable); + assert($b instanceof BlockPermutations); $b->deserializeState($in); return $b; }; - } else { + }else{ // If a block does not contain any permutations we can just insert the one state. - $blockState = CompoundTag::create() - ->setString(BlockStateData::TAG_NAME, $identifier) - ->setTag(BlockStateData::TAG_STATES, CompoundTag::create()); - BlockPalette::getInstance()->insertState($blockState); + BlockPalette::getInstance()->insertState( + CompoundTag::create() + ->setString(BlockStateData::TAG_NAME, $identifier) + ->setTag(BlockStateData::TAG_STATES, CompoundTag::create()) + ); $serializer ??= static fn() => new BlockStateWriter($identifier); $deserializer ??= static fn(BlockStateReader $in) => $block; } GlobalBlockStateHandlers::getSerializer()->map($block, $serializer); GlobalBlockStateHandlers::getDeserializer()->map($identifier, $deserializer); - - $creativeInfo ??= CreativeInventoryInfo::DEFAULT(); - $propertiesTag - ->setTag("components", - $components->setTag("minecraft:creative_category", CompoundTag::create() - ->setString("category", $creativeInfo->getCategory()) - ->setString("group", $creativeInfo->getGroup()))) - ->setTag("menu_category", CompoundTag::create() - ->setString("category", $creativeInfo->getCategory() ?? "") - ->setString("group", $creativeInfo->getGroup() ?? "")) - ->setInt("molangVersion", 1); - - if($creativeInfo !== null){ - $this->loadGroups(); - if($creativeInfo->getCategory() === CreativeInventoryInfo::CATEGORY_ALL || $creativeInfo->getCategory() === CreativeInventoryInfo::CATEGORY_COMMANDS){ - return; - } - - $group = $this->groups[$creativeInfo->getGroup()] ?? ($creativeInfo->getGroup() !== "" && $creativeInfo->getGroup() !== CreativeInventoryInfo::NONE ? new CreativeGroup( - new Translatable($creativeInfo->getGroup()), - $block->asItem() - ) : null); - - if($group !== null){ - $this->groups[$group->getName()->getText()] = $group; - } - - $category = match ($creativeInfo->getCategory()) { - CreativeInventoryInfo::CATEGORY_CONSTRUCTION => CreativeCategory::CONSTRUCTION, - CreativeInventoryInfo::CATEGORY_ITEMS => CreativeCategory::ITEMS, - CreativeInventoryInfo::CATEGORY_NATURE => CreativeCategory::NATURE, - CreativeInventoryInfo::CATEGORY_EQUIPMENT => CreativeCategory::EQUIPMENT, - default => throw new AssumptionFailedError("Unknown category") - }; - - CreativeInventory::getInstance()->add($block->asItem(), $category, $group); - } - - $this->blockPaletteEntries[] = new BlockPaletteEntry($identifier, new CacheableNbt($propertiesTag)); + // The 'minecraft:on_player_placing' component is required for the client to predict block placement, making + // it a smoother experience for the end-user. + $componentsTag->setTag("minecraft:on_player_placing", CompoundTag::create()); + $nbtTag->setTag("blockTags", new ListTag()); + $nbtTag->setTag("components", $componentsTag); + $nbtTag->setInt("molangVersion", 13); + // Registers the block to creative inventory + CreativeInventoryInfo::registerCreativeInfo($block, $creativeInfo); + $this->blockPaletteEntries[] = new BlockPaletteEntry($identifier, new CacheableNbt($nbtTag)); $this->blockFuncs[$identifier] = [$blockFunc, $serializer, $deserializer]; - // 1.20.60 added a new "block_id" field which depends on the order of the block palette entries. Every time we // insert a new block, we need to re-sort the block palette entries to keep in sync with the client. usort($this->blockPaletteEntries, static function(BlockPaletteEntry $a, BlockPaletteEntry $b): int { return strcmp(hash("fnv164", $a->getName()), hash("fnv164", $b->getName())); }); - foreach($this->blockPaletteEntries as $i => $entry) { - $root = $entry->getStates()->getRoot() - ->setTag("vanilla_block_data", CompoundTag::create() - ->setInt("block_id", 10000 + $i)); + foreach($this->blockPaletteEntries as $i => $entry){ + $root = $entry->getStates()->getRoot(); + $root->setTag("vanilla_block_data", CompoundTag::create()->setInt("block_id", 10000 + $i)); $this->blockPaletteEntries[$i] = new BlockPaletteEntry($entry->getName(), new CacheableNbt($root)); } } -} +} \ No newline at end of file diff --git a/src/block/Material.php b/src/block/Material.php deleted file mode 100644 index 59db55d2..00000000 --- a/src/block/Material.php +++ /dev/null @@ -1,48 +0,0 @@ -target; - } - - /** - * Returns the material in the correct NBT format supported by the client. - */ - public function toNBT(): CompoundTag { - return CompoundTag::create() - ->setString("texture", $this->texture) - ->setString("render_method", $this->renderMethod) - ->setByte("face_dimming", $this->faceDimming ? 1 : 0) - ->setByte("ambient_occlusion", $this->ambientOcclusion ? 1 : 0); - } -} diff --git a/src/block/component/BlockComponent.php b/src/block/component/BlockComponent.php index 76fb4e1d..c9790da2 100644 --- a/src/block/component/BlockComponent.php +++ b/src/block/component/BlockComponent.php @@ -3,19 +3,17 @@ namespace customiesdevs\customies\block\component; -use pocketmine\nbt\tag\CompoundTag; - interface BlockComponent { /** - * Returns the name of the component + * The component identifier, e.g. "minecraft:collision_box" * @return string */ public function getName(): string; /** - * Returns the value of the component - * @return CompoundTag + * The value of this component, as it would appear in a block JSON. + * @return mixed */ - public function getValue(): CompoundTag; + public function getValue(): mixed; } \ No newline at end of file diff --git a/src/block/BlockComponents.php b/src/block/component/BlockComponents.php similarity index 68% rename from src/block/BlockComponents.php rename to src/block/component/BlockComponents.php index 8994af03..befac4bc 100644 --- a/src/block/BlockComponents.php +++ b/src/block/component/BlockComponents.php @@ -1,6 +1,7 @@ + */ + private array $components; + + /** + * Adds or replaces a block component. + * If a component with the same name already exists, it will be overwritten. + * @param BlockComponent $component The component to add. + */ + public function addComponent(BlockComponent $component): void { + $this->components[$component->getName()] = $component; + } + + /** + * Checks whether the block has a component with the given name. + * + * @param string $name Component identifier (e.g. "minecraft:flammable") + * @return bool True if the component exists, false otherwise. + */ + public function hasComponent(string $name): bool { + return isset($this->components[$name]); + } + + /** + * Retrieves a component by its name. + * + * @param string $name Component identifier. + * @return BlockComponent|null The component if present, otherwise null. + */ + public function getComponent(string $name): ?BlockComponent { + return $this->components[$name] ?? null; + } + + /** + * Returns all registered block components. + * + * @return array + */ + public function getComponents(): array { + return $this->components; + } + + /** + * @todo + * Initializes the default components for a block with the given texture and name. + * Adds geometry, material instances, and display name components. + * + * @param string $texture The texture identifier + * @param string $name The display name of the block + */ + protected function initComponents(string $texture, string $name): void { + // Only initialize if no components are set yet + if($this->getComponents() !== []){ + return; + } + $this->addComponent(new GeometryComponent()); + $this->addComponent(new MaterialInstancesComponent([new Material("*", $texture)])); + $this->addComponent(new DisplayNameComponent($name)); + } +} \ No newline at end of file diff --git a/src/block/component/BreathabilityComponent.php b/src/block/component/BreathabilityComponent.php deleted file mode 100644 index d8b8eb19..00000000 --- a/src/block/component/BreathabilityComponent.php +++ /dev/null @@ -1,30 +0,0 @@ -breathability = $breathability; - } - - public function getName(): string { - return "minecraft:breathability"; - } - - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setString("value", $this->breathability); - } -} \ No newline at end of file diff --git a/src/block/component/CollisionBoxComponent.php b/src/block/component/CollisionBoxComponent.php index e0e641af..d0556415 100644 --- a/src/block/component/CollisionBoxComponent.php +++ b/src/block/component/CollisionBoxComponent.php @@ -1,46 +1,78 @@ useCollisionBox = $useCollisionBox; - $this->origin = $origin; - $this->size = $size; + public function __construct(bool $enabled = true) { + $this->enabled = $enabled; } public function getName(): string { - return "minecraft:collision_box"; + return 'minecraft:collision_box'; } - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setByte("enabled", $this->useCollisionBox ? 1 : 0) - ->setTag("origin", new ListTag([ - new FloatTag($this->origin->getX()), - new FloatTag($this->origin->getY()), - new FloatTag($this->origin->getZ()) - ])) - ->setTag("size", new ListTag([ - new FloatTag($this->size->getX()), - new FloatTag($this->size->getY()), - new FloatTag($this->size->getZ()) - ])); + public function getValue(): array { + $boxes = []; + foreach($this->boxes as $box){ + $boxes[] = $box->toNbtArray(); + } + // if no boxes are defined we add a default full block box + if($this->enabled && empty($boxes)){ + $boxes[] = Box::defaultBox()->toNbtArray(); + } + // no collision + if(!$this->enabled){ + $boxes[] = []; + } + return [ + "boxes" => $boxes, + "enabled" => $this->enabled + ]; + } + + /** + * Adds a single collision box. + * @param Box $box + * The collision box to add. + * @return $this + */ + public function addBox(Box $box): self { + if(count($this->boxes) === 1){ + $this->boxes = []; + } + $this->boxes[] = $box; + return $this; + } + + /** + * Adds multiple collision boxes. + * @param Box[] $boxes + * An array of collision boxes to add. + * @return $this + * @throws \InvalidArgumentException If any element in the array is not an instance of Box. + */ + public function addBoxes(array $boxes): self { + if(count($this->boxes) === 1){ + $this->boxes = []; + } + foreach($boxes as $box){ + if(!$box instanceof Box){ + throw new \InvalidArgumentException("All boxes must be instances of " . Box::class); + } + $this->boxes[] = $box; + } + return $this; } } \ No newline at end of file diff --git a/src/block/component/DestructibleByExplosionComponent.php b/src/block/component/DestructibleByExplosionComponent.php index dd55d972..52737e94 100644 --- a/src/block/component/DestructibleByExplosionComponent.php +++ b/src/block/component/DestructibleByExplosionComponent.php @@ -2,9 +2,7 @@ namespace customiesdevs\customies\block\component; -use pocketmine\nbt\tag\CompoundTag; - -class DestructibleByExplosionComponent implements BlockComponent { +final class DestructibleByExplosionComponent implements BlockComponent { private float $explosionResistance; @@ -17,11 +15,12 @@ public function __construct(float $explosionResistance = 0.0) { } public function getName(): string { - return "minecraft:destructible_by_explosion"; + return 'minecraft:destructible_by_explosion'; } - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setFloat("value", $this->explosionResistance); + public function getValue(): array { + return [ + "value" => $this->explosionResistance + ]; } } \ No newline at end of file diff --git a/src/block/component/DestructibleByMiningComponent.php b/src/block/component/DestructibleByMiningComponent.php index 458a44cb..db86b695 100644 --- a/src/block/component/DestructibleByMiningComponent.php +++ b/src/block/component/DestructibleByMiningComponent.php @@ -2,26 +2,87 @@ namespace customiesdevs\customies\block\component; -use pocketmine\nbt\tag\CompoundTag; +use pocketmine\nbt\tag\ShortTag; -class DestructibleByMiningComponent implements BlockComponent { +final class DestructibleByMiningComponent implements BlockComponent { + /** Seconds to destroy with base equipment */ private float $secondsToDestroy; + /** + * @var array + */ + private array $itemSpecificSpeeds = []; /** * Describes the destructible by mining properties for this block. If set to true, the block will take the default number of seconds to destroy. If set to false, this block is indestructible by mining. If the component is omitted, the block will take the default number of seconds to destroy. * @param float $secondsToDestroy Sets the number of seconds it takes to destroy the block with base equipment. Greater numbers result in greater mining times. */ public function __construct(float $secondsToDestroy = 0.0) { + if($secondsToDestroy < 0){ + throw new \InvalidArgumentException("secondsToDestroy must be >= 0"); + } $this->secondsToDestroy = $secondsToDestroy; } public function getName(): string { - return "minecraft:destructible_by_mining"; + return 'minecraft:destructible_by_mining'; + } + + public function getValue(): array { + $data = [ + "value" => $this->secondsToDestroy + ]; + if($this->itemSpecificSpeeds !== []){ + $data["item_specific_speeds"] = $this->itemSpecificSpeeds; + } + return $data; + } + + /** + * Adds an item-specific destroy speed using item tags (Molang). + * @param float $destroySpeed + * @param string $tags Molang tag expression + * @param int $molangVersion + */ + public function addItemSpeedByTags( + float $destroySpeed, + string $tags, + ): self { + if($destroySpeed <= 0){ + throw new \InvalidArgumentException("destroy_speed must be > 0"); + } + $this->itemSpecificSpeeds[] = [ + "destroy_speed" => $destroySpeed, + "item" => [ + "MolangVersion" => new ShortTag(13), + "Tags" => $tags + ] + ]; + return $this; } - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setFloat("value", $this->secondsToDestroy); + /** + * Adds an item-specific destroy speed using an item identifier. + * @param float $destroySpeed + * @param string $itemId + */ + public function addItemSpeedByItem( + float $destroySpeed, + string $itemId + ): self { + if($destroySpeed <= 0){ + throw new \InvalidArgumentException("destroy_speed must be > 0"); + } + $this->itemSpecificSpeeds[] = [ + "destroy_speed" => $destroySpeed, + "item" => $itemId + ]; + return $this; } } \ No newline at end of file diff --git a/src/block/component/DestructionParticlesComponent.php b/src/block/component/DestructionParticlesComponent.php new file mode 100644 index 00000000..fdf959a1 --- /dev/null +++ b/src/block/component/DestructionParticlesComponent.php @@ -0,0 +1,40 @@ +particleCount = max(0, min(255, $particleCount)); + $this->texture = $texture; + $this->tintMethod = $tintMethod; + } + + public function getName(): string { + return 'minecraft:destruction_particles'; + } + + public function getValue(): array { + return [ + "particle_count" => $this->particleCount, + "texture" => $this->texture, + "tint_method" => $this->tintMethod->value + ]; + } +} \ No newline at end of file diff --git a/src/block/component/DisplayNameComponent.php b/src/block/component/DisplayNameComponent.php index b145b0a2..0637a037 100644 --- a/src/block/component/DisplayNameComponent.php +++ b/src/block/component/DisplayNameComponent.php @@ -2,9 +2,7 @@ namespace customiesdevs\customies\block\component; -use pocketmine\nbt\tag\CompoundTag; - -class DisplayNameComponent implements BlockComponent { +final class DisplayNameComponent implements BlockComponent { private string $displayName; @@ -20,11 +18,12 @@ public function __construct(string $displayName) { } public function getName(): string { - return "minecraft:display_name"; + return 'minecraft:display_name'; } - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setString("value", $this->displayName); + public function getValue(): array { + return [ + "value" => $this->displayName + ]; } } \ No newline at end of file diff --git a/src/block/component/EmbeddedVisualComponent.php b/src/block/component/EmbeddedVisualComponent.php new file mode 100644 index 00000000..dc50a0e3 --- /dev/null +++ b/src/block/component/EmbeddedVisualComponent.php @@ -0,0 +1,38 @@ +materials as $material){ + $materials[$material->getTarget()] = [ + "alpha_masked_tint" => false, + "face_dimming" => true, + "isotropic" => false, + ...$material->toArray() + ]; + } + return [ + "geometry" => $this->geometry->getValue(), + "material_instances" => $materials + ]; + } +} \ No newline at end of file diff --git a/src/block/component/FlammableComponent.php b/src/block/component/FlammableComponent.php index 537cb48a..d707f729 100644 --- a/src/block/component/FlammableComponent.php +++ b/src/block/component/FlammableComponent.php @@ -2,9 +2,7 @@ namespace customiesdevs\customies\block\component; -use pocketmine\nbt\tag\CompoundTag; - -class FlammableComponent implements BlockComponent { +final class FlammableComponent implements BlockComponent { private int $catchChanceModifier; private int $destroyChanceModifier; @@ -20,12 +18,13 @@ public function __construct(int $catchChanceModifier = 5, int $destroyChanceModi } public function getName(): string { - return "minecraft:flammable"; + return 'minecraft:flammable'; } - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setInt("catch_chance_modifier", $this->catchChanceModifier) - ->setInt("destroy_chance_modifier", $this->destroyChanceModifier); + public function getValue(): array { + return [ + "catch_chance_modifier" => $this->catchChanceModifier, + "destroy_chance_modifier" => $this->destroyChanceModifier + ]; } } \ No newline at end of file diff --git a/src/block/component/FlowerPottableComponent.php b/src/block/component/FlowerPottableComponent.php new file mode 100644 index 00000000..35e40450 --- /dev/null +++ b/src/block/component/FlowerPottableComponent.php @@ -0,0 +1,16 @@ +setFloat("value", $this->friction); + public function getValue(): array { + return [ + "value" => $this->friction + ]; } } \ No newline at end of file diff --git a/src/block/component/GeometryComponent.php b/src/block/component/GeometryComponent.php index aee3999e..bc03630c 100644 --- a/src/block/component/GeometryComponent.php +++ b/src/block/component/GeometryComponent.php @@ -2,41 +2,51 @@ namespace customiesdevs\customies\block\component; -use pocketmine\nbt\tag\CompoundTag; +final class GeometryComponent implements BlockComponent { -class GeometryComponent implements BlockComponent { - - private string $geometry; - private CompoundTag $boneVisibility; + private string $identifier; + private array $boneVisibility = []; + private string $culling; + private string $cullingLayer; + private array|bool $uvLock; /** * The description identifier of the geometry to use to render this block. This identifier must either match an existing geometry identifier in any of the loaded resource packs or be one of the currently supported Vanilla identifiers: "minecraft:geometry.full_block" or "minecraft:geometry.cross". - * @param string $geometry + * @param string $identifier Specifies the geometry description identifier to use to render this block. This identifier must match an existing geometry identifier in any of the currently loaded resource packs. + * @param array $boneVisibility An optional list of true/false values that define the visibility of individual bones in the geometry file. In order to set up 'bone_visibility', the geometry file name must be entered as an identifier. After the identifier has been specified, bone_visibility can be defined based on the names of the bones in the specified geometry file on a true/false basis. Note that all bones default to 'true,' so bones should only be defined if they are being set to 'false.' Including bones set to 'true' will work the same as the default. + * @param string $culling An optional identifer of a culling definition. The culling definition is used to determine which faces of the block should be culled when rendering. The culling definition can be used to optimize rendering performance by reducing the number of faces that need to be rendered. This identifier must match an existing culling definition in any of the currently loaded resource packs. + * @param string $cullingLayer [Experimental] - A string that allows culling rule to group multiple blocks together when comparing them. When using the minecraft namespace, the only allowed culling layer identifiers are : "minecraft:culling_layer.undefined" or "minecraft:culling_layer.leaves". Additionally, the feature is currently only usable behind the "upcoming creator features" toggle. When using no namespaces or a custom one, the names must start and end with an alpha-numeric character. + * @param array|bool $uvLock A Boolean locking UV orientation of all bones in the geometry, or an array of strings locking UV orientation of specific bones in the geometry. For performance reasons it is recommended to use the Boolean. Note that for cubes using Box UVs, rather than Per-face UVs, 'uv_lock' is only supported if the cube faces are square. */ - public function __construct(string $geometry = "minecraft:geometry.full_block") { - $this->geometry = $geometry; - $this->boneVisibility = CompoundTag::create(); + public function __construct( + string $identifier = "minecraft:geometry.full_block", + array $boneVisibility = [], + string $culling = "", + string $cullingLayer = "minecraft:culling_layer.undefined", + array|bool $uvLock = false + ) { + $this->identifier = $identifier; + $this->culling = $culling; + $this->cullingLayer = $cullingLayer; + $this->uvLock = $uvLock; + $this->boneVisibility = $boneVisibility; } public function getName(): string { - return "minecraft:geometry"; - } - - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setTag("bone_visibility", $this->boneVisibility) - ->setString("culling", "") - ->setString("identifier", $this->geometry); + return 'minecraft:geometry'; } - public function addBoneVisibility(string $boneName, bool|string $visibility): self { - if(is_string($visibility) && !is_bool($visibility)){ - $this->boneVisibility->setTag($boneName, CompoundTag::create() - ->setString("expression", $visibility) - ->setShort("version", 12)); - } elseif(is_bool($visibility)){ - $this->boneVisibility->setFloat($boneName, $visibility ? 1 : 0); - } - return $this; + public function getValue(): array { + return [ + "bone_visibility" => $this->boneVisibility, + "culling" => $this->culling, + "culling_layer" => $this->cullingLayer, + "identifier" => $this->identifier, + "uv_lock" => $this->uvLock, + // no reason as to why these 3 exist, but its what minecraft is outputting + "ignoreGeometryForIsSolid" => false, + "needsLegacyTopRotation" => false, + "useBlockTypeLightAbsorption" => false + ]; } } \ No newline at end of file diff --git a/src/block/component/ItemVisualComponent.php b/src/block/component/ItemVisualComponent.php new file mode 100644 index 00000000..2ade002a --- /dev/null +++ b/src/block/component/ItemVisualComponent.php @@ -0,0 +1,40 @@ +materials as $material){ + $materials[$material->getTarget()] = [ + ...$material->toArray(), + "packed_bools" => new ByteTag(Material::FACE_DIMMING) + ]; + } + return [ + "geometryDescription" => $this->geometry->getValue(), + "materialInstancesDescription" => [ + "mappings" => [], + "materials" => $materials + ] + ]; + } +} \ No newline at end of file diff --git a/src/block/component/LightDampeningComponent.php b/src/block/component/LightDampeningComponent.php index 4cfc704d..cbc35c30 100644 --- a/src/block/component/LightDampeningComponent.php +++ b/src/block/component/LightDampeningComponent.php @@ -4,7 +4,7 @@ use pocketmine\nbt\tag\CompoundTag; -class LightDampeningComponent implements BlockComponent { +final class LightDampeningComponent implements BlockComponent { private int $dampening; @@ -17,7 +17,7 @@ public function __construct(int $dampening = 15) { } public function getName(): string { - return "minecraft:light_dampening"; + return 'minecraft:light_dampening'; } public function getValue(): CompoundTag { diff --git a/src/block/component/LightEmissionComponent.php b/src/block/component/LightEmissionComponent.php index 2ad043b7..b84bae4f 100644 --- a/src/block/component/LightEmissionComponent.php +++ b/src/block/component/LightEmissionComponent.php @@ -4,7 +4,7 @@ use pocketmine\nbt\tag\CompoundTag; -class LightEmissionComponent implements BlockComponent { +final class LightEmissionComponent implements BlockComponent { private int $emission; @@ -17,7 +17,7 @@ public function __construct(int $emission = 0) { } public function getName(): string { - return "minecraft:light_emission"; + return 'minecraft:light_emission'; } public function getValue(): CompoundTag { diff --git a/src/block/component/LiquidDetectionComponent.php b/src/block/component/LiquidDetectionComponent.php new file mode 100644 index 00000000..cba8edb5 --- /dev/null +++ b/src/block/component/LiquidDetectionComponent.php @@ -0,0 +1,74 @@ +liquidType = $liquidType; + $this->canContainLiquid = $canContainLiquid; + $this->onLiquidTouches = $onLiquidTouches; + $this->stopsLiquidFlowingFromDirection = $stopsLiquidFlowingFromDirection; + } + + public function getName(): string { + return 'minecraft:liquid_detection'; + } + + public function getValue(): array { + return [ + "detectionRules" => [ + [ + "liquidType" => $this->liquidType, + "canContainLiquid" => $this->canContainLiquid, + "onLiquidTouches" => $this->onLiquidTouches, + "stopsLiquidFromDirection" => $this->stopsLiquidFlowingFromDirection, + ] + ] + ]; + } +} \ No newline at end of file diff --git a/src/block/component/MapColorComponent.php b/src/block/component/MapColorComponent.php new file mode 100644 index 00000000..fd095553 --- /dev/null +++ b/src/block/component/MapColorComponent.php @@ -0,0 +1,26 @@ +color = $color; + } + + public function getName(): string { + return 'minecraft:map_color'; + } + + public function getValue(): array { + return [ + "color" => $this->color + ]; + } +} \ No newline at end of file diff --git a/src/block/component/MaterialInstancesComponent.php b/src/block/component/MaterialInstancesComponent.php index 72dd6383..8239526c 100644 --- a/src/block/component/MaterialInstancesComponent.php +++ b/src/block/component/MaterialInstancesComponent.php @@ -2,30 +2,30 @@ namespace customiesdevs\customies\block\component; -use customiesdevs\customies\block\Material; -use pocketmine\nbt\tag\CompoundTag; +use customiesdevs\customies\block\properties\Material; -class MaterialInstancesComponent implements BlockComponent { +final class MaterialInstancesComponent implements BlockComponent { - /** @var Material[] */ - private array $materials; - - public function __construct(array $materials) { - $this->materials = $materials; + /** + * The material instances for a block. Maps face or material_instance names in a geometry file to an actual material instance. You can assign a material instance object to any of these faces: "up", "down", "north", "south", "east", "west", or "*". You can also give an instance the name of your choosing such as "my_instance", and then assign it to a face by doing "north":"my_instance". + * @param Material[] $materials + */ + public function __construct(private readonly array $materials = []) { + Material::validMaterials($materials); } public function getName(): string { - return "minecraft:material_instances"; + return 'minecraft:material_instances'; } - public function getValue(): CompoundTag { - $materials = CompoundTag::create(); + public function getValue(): array { + $materials = []; foreach($this->materials as $material){ - $materials->setTag($material->getTarget(), $material->toNBT()); + $materials[$material->getTarget()] = $material->toArray(); } - - return CompoundTag::create() - ->setTag("mappings", CompoundTag::create()) - ->setTag("materials", $materials); + return [ + "mappings" => [], + "materials" => $materials + ]; } } \ No newline at end of file diff --git a/src/block/component/PlacementFilterComponent.php b/src/block/component/PlacementFilterComponent.php new file mode 100644 index 00000000..2b8a0684 --- /dev/null +++ b/src/block/component/PlacementFilterComponent.php @@ -0,0 +1,43 @@ + 64){ + throw new InvalidArgumentException("Placement filter may not exceed 64 conditions"); + } + $this->conditions = $conditions; + } + + public function getName(): string { + return 'minecraft:placement_filter'; + } + + public function getValue(): array { + return [ + "conditions" => array_map( + static fn(PlacementCondition $c) => $c->toArray(), + $this->conditions + ) + ]; + } + + public function addCondition(PlacementCondition $condition): self { + if(count($this->conditions) >= 64){ + throw new InvalidArgumentException("Placement filter may not exceed 64 conditions"); + } + $this->conditions[] = $condition; + return $this; + } +} \ No newline at end of file diff --git a/src/block/component/RandomOffsetComponent.php b/src/block/component/RandomOffsetComponent.php new file mode 100644 index 00000000..d0631947 --- /dev/null +++ b/src/block/component/RandomOffsetComponent.php @@ -0,0 +1,84 @@ +min = $min; + $this->max = $max; + $this->steps = $steps; + } + + public function getName(): string { + return 'minecraft:random_offset'; + } + + public function getValue(): array { + return [ + "x" => [ + "steps" => (int) $this->steps->x, + "range" => ["min" => $this->min->x, "max" => $this->max->x] + ], + "y" => [ + "steps" => (int) $this->steps->y, + "range" => ["min" => $this->min->y, "max" => $this->max->y] + ], + "z" => [ + "steps" => (int) $this->steps->z, + "range" => ["min" => $this->min->z, "max" => $this->max->z] + ], + ]; + } + + public function setMin(Vector3 $min): self { + $this->min = $min; + return $this; + } + + public function setMax(Vector3 $max): self { + $this->max = $max; + return $this; + } + + public function setSteps(Vector3 $steps): self { + $this->steps = $steps; + return $this; + } + + public function setX(float $min, float $max, int $steps = 0): self { + $this->min->x = $min; + $this->max->x = $max; + $this->steps->x = $steps; + return $this; + } + + public function setY(float $min, float $max, int $steps = 0): self { + $this->min->y = $min; + $this->max->y = $max; + $this->steps->y = $steps; + return $this; + } + + public function setZ(float $min, float $max, int $steps = 0): self { + $this->min->z = $min; + $this->max->z = $max; + $this->steps->z = $steps; + return $this; + } +} \ No newline at end of file diff --git a/src/block/component/SelectionBoxComponent.php b/src/block/component/SelectionBoxComponent.php index 2f0ebce6..4d822e21 100644 --- a/src/block/component/SelectionBoxComponent.php +++ b/src/block/component/SelectionBoxComponent.php @@ -3,11 +3,8 @@ namespace customiesdevs\customies\block\component; use pocketmine\math\Vector3; -use pocketmine\nbt\tag\CompoundTag; -use pocketmine\nbt\tag\FloatTag; -use pocketmine\nbt\tag\ListTag; -class SelectionBoxComponent implements BlockComponent { +final class SelectionBoxComponent implements BlockComponent { private bool $useSelectionBox; private Vector3 $origin; @@ -26,21 +23,22 @@ public function __construct(bool $useSelectionBox = true, ?Vector3 $origin = new } public function getName(): string { - return "minecraft:selection_box"; + return 'minecraft:selection_box'; } - public function getValue(): CompoundTag { - return CompoundTag::create() - ->setByte("enabled", $this->useSelectionBox ? 1 : 0) - ->setTag("origin", new ListTag([ - new FloatTag($this->origin->getX()), - new FloatTag($this->origin->getY()), - new FloatTag($this->origin->getZ()) - ])) - ->setTag("size", new ListTag([ - new FloatTag($this->size->getX()), - new FloatTag($this->size->getY()), - new FloatTag($this->size->getZ()) - ])); + public function getValue(): array { + return [ + "enabled" => $this->useSelectionBox, + "origin" => [ + $this->origin->getX(), + $this->origin->getY(), + $this->origin->getZ() + ], + "size" => [ + $this->size->getX(), + $this->size->getY(), + $this->size->getZ() + ] + ]; } } \ No newline at end of file diff --git a/src/block/component/SupportComponent.php b/src/block/component/SupportComponent.php new file mode 100644 index 00000000..88682ac6 --- /dev/null +++ b/src/block/component/SupportComponent.php @@ -0,0 +1,28 @@ +shape = $shape; + } + + public function getName(): string { + return 'minecraft:support'; + } + + public function getValue(): array { + return [ + "shape" => $this->shape + ]; + } +} \ No newline at end of file diff --git a/src/block/component/TransformationComponent.php b/src/block/component/TransformationComponent.php new file mode 100644 index 00000000..b064d237 --- /dev/null +++ b/src/block/component/TransformationComponent.php @@ -0,0 +1,63 @@ + (int) self::rotationToIndex($this->rotation->x), + "RY" => (int) self::rotationToIndex($this->rotation->y), + "RZ" => (int) self::rotationToIndex($this->rotation->z), + "RXP" => (float) $this->rotationPivot->x, + "RYP" => (float) $this->rotationPivot->y, + "RZP" => (float) $this->rotationPivot->z, + "SX" => (float) $this->scale->x, + "SY" => (float) $this->scale->y, + "SZ" => (float) $this->scale->z, + "SXP" => (float) $this->scalePivot->x, + "SYP" => (float) $this->scalePivot->y, + "SZP" => (float) $this->scalePivot->z, + "TX" => (float) $this->translation->x, + "TY" => (float) $this->translation->y, + "TZ" => (float) $this->translation->z, + "hasJsonVersionBeforeValidation" => false + ]; + } + + private static function rotationToIndex(float $d): int { + $d = ((int) $d) % 360; + if($d < 0){ + $d += 360; + } + return match($d){ + 0 => 0, // North + 90 => 1, // West + 180 => 2, // South + 270, -90 => 3, // East + default => 0 // North By Default + }; + } +} \ No newline at end of file diff --git a/src/block/permutations/BlockPermutation.php b/src/block/permutations/BlockPermutation.php new file mode 100644 index 00000000..652d00c3 --- /dev/null +++ b/src/block/permutations/BlockPermutation.php @@ -0,0 +1,44 @@ +condition; + } + + /** + * Gets the components associated with this permutation. + */ + public function getComponents(): BlockComponent { + return $this->components; + } + + /** + * Converts the BlockPermutation to an array format. + */ + public function toArray(): array { + return [ + "condition" => $this->condition, + "components" => [ + $this->components->getName() => $this->components->getValue() + ] + ]; + } +} \ No newline at end of file diff --git a/src/block/permutations/BlockPermutations.php b/src/block/permutations/BlockPermutations.php new file mode 100644 index 00000000..c396de87 --- /dev/null +++ b/src/block/permutations/BlockPermutations.php @@ -0,0 +1,27 @@ +blockPermutations[] = $permutation; + } + + /** + * Adds multiple permutations at once. + * @param BlockPermutation[] $permutations + */ + public function addPermutations(array $permutations): void { + foreach($permutations as $permutation) { + $this->addPermutation($permutation); + } + } + + /** + * Returns all registered block permutations. + * @return BlockPermutation[] + */ + public function getPermutations(): array { + return $this->blockPermutations; + } +} \ No newline at end of file diff --git a/src/block/permutations/BlockProperty.php b/src/block/permutations/BlockProperty.php deleted file mode 100644 index 6550c432..00000000 --- a/src/block/permutations/BlockProperty.php +++ /dev/null @@ -1,38 +0,0 @@ -name; - } - - /** - * Returns the array of possible values of the block property provided in the constructor. - */ - public function getValues(): array { - return $this->values; - } - - /** - * Returns the block property in the correct NBT format supported by the client. - */ - public function toNBT(): CompoundTag { - $values = array_map(static fn($value) => NBT::getTagType($value), $this->values); - return CompoundTag::create() - ->setString("name", $this->name) - ->setTag("enum", new ListTag($values)); - } -} \ No newline at end of file diff --git a/src/block/permutations/Permutation.php b/src/block/permutations/Permutation.php deleted file mode 100644 index f9ff12a3..00000000 --- a/src/block/permutations/Permutation.php +++ /dev/null @@ -1,33 +0,0 @@ -components = CompoundTag::create(); - } - - /** - * Returns the permutation with the provided component added to the current list of components. - */ - public function withComponent(string $component, mixed $value) : self { - $this->components->setTag($component, NBT::getTagType($value)); - return $this; - } - - /** - * Returns the permutation in the correct NBT format supported by the client. - */ - public function toNBT(): CompoundTag { - return CompoundTag::create() - ->setString("condition", $this->condition) - ->setTag("components", $this->components); - } -} \ No newline at end of file diff --git a/src/block/permutations/Permutations.php b/src/block/permutations/Permutations.php index 96d7b2f0..ef736357 100644 --- a/src/block/permutations/Permutations.php +++ b/src/block/permutations/Permutations.php @@ -3,6 +3,7 @@ namespace customiesdevs\customies\block\permutations; +use customiesdevs\customies\block\states\BlockState; use Exception; use function array_map; use function count; @@ -17,9 +18,9 @@ class Permutations { * of the block. An exception is thrown if the meta value does not match any combinations of all the block * properties. */ - public static function fromMeta(Permutable $block, int $meta): array { + public static function fromMeta(BlockPermutations $block, int $meta): array { $properties = self::getCartesianProduct( - array_map(static fn(BlockProperty $blockProperty) => $blockProperty->getValues(), $block->getBlockProperties()) + array_map(static fn(BlockState $blockProperty) => $blockProperty->getValues(), $block->getStates()) )[$meta] ?? null; if($properties === null) { throw new Exception("Unable to calculate permutations from block meta: " . $meta); @@ -31,12 +32,12 @@ public static function fromMeta(Permutable $block, int $meta): array { * Attempts to convert the block in to a meta value based on the possible permutations of the block. An exception is * thrown if the state of the block is not a possible combination of all the block properties. */ - public static function toMeta(Permutable $block): int { + public static function toMeta(BlockPermutations $block): int { $properties = self::getCartesianProduct( - array_map(static fn(BlockProperty $blockProperty) => $blockProperty->getValues(), $block->getBlockProperties()) + array_map(static fn(BlockState $blockProperty) => $blockProperty->getValues(), $block->getStates()) ); foreach($properties as $meta => $permutations){ - if($permutations === $block->getCurrentBlockProperties()) { + if($permutations === $block->getCurrentStates()) { return $meta; } } @@ -46,8 +47,8 @@ public static function toMeta(Permutable $block): int { /** * Returns the number of bits required to represent all the possible permutations of the block. */ - public static function getStateBitmask(Permutable $block): int { - $possibleValues = array_map(static fn(BlockProperty $blockProperty) => $blockProperty->getValues(), $block->getBlockProperties()); + public static function getStateBitmask(BlockPermutations $block): int { + $possibleValues = array_map(static fn(BlockState $blockProperty) => $blockProperty->getValues(), $block->getStates()); return count(self::getCartesianProduct($possibleValues)) - 1; } @@ -56,13 +57,20 @@ public static function getStateBitmask(Permutable $block): int { * product (https://en.wikipedia.org/wiki/Cartesian_product). */ public static function getCartesianProduct(array $arrays): array { + if($arrays === []){ + return [[]]; + } $result = []; $count = count($arrays) - 1; $combinations = array_product(array_map(static fn(array $array) => count($array), $arrays)); for($i = 0; $i < $combinations; $i++){ - $result[] = array_map(static fn(array $array) => current($array), $arrays); + $row = []; + foreach($arrays as $index => $_){ + $row[] = current($arrays[$index]); + } + $result[] = $row; for($j = $count; $j >= 0; $j--){ - if(next($arrays[$j])) { + if(next($arrays[$j]) !== false){ break; } reset($arrays[$j]); diff --git a/src/block/permutations/RotatableTrait.php b/src/block/permutations/RotatableTrait.php deleted file mode 100644 index 5e16d741..00000000 --- a/src/block/permutations/RotatableTrait.php +++ /dev/null @@ -1,112 +0,0 @@ -withComponent("minecraft:transformation", CompoundTag::create() - ->setInt("RX", 0) - ->setInt("RY", 0) - ->setInt("RZ", 0) - ->setFloat("SX", 1.0) - ->setFloat("SY", 1.0) - ->setFloat("SZ", 1.0) - ->setFloat("TX", 0.0) - ->setFloat("TY", 0.0) - ->setFloat("TZ", 0.0)), - (new Permutation("q.block_property('customies:rotation') == 3")) - ->withComponent("minecraft:transformation", CompoundTag::create() - ->setInt("RX", 0) - ->setInt("RY", 2) - ->setInt("RZ", 0) - ->setFloat("SX", 1.0) - ->setFloat("SY", 1.0) - ->setFloat("SZ", 1.0) - ->setFloat("TX", 0.0) - ->setFloat("TY", 0.0) - ->setFloat("TZ", 0.0)), - (new Permutation("q.block_property('customies:rotation') == 4")) - ->withComponent("minecraft:transformation", CompoundTag::create() - ->setInt("RX", 0) - ->setInt("RY", 1) - ->setInt("RZ", 0) - ->setFloat("SX", 1.0) - ->setFloat("SY", 1.0) - ->setFloat("SZ", 1.0) - ->setFloat("TX", 0.0) - ->setFloat("TY", 0.0) - ->setFloat("TZ", 0.0)), - (new Permutation("q.block_property('customies:rotation') == 5")) - ->withComponent("minecraft:transformation", CompoundTag::create() - ->setInt("RX", 0) - ->setInt("RY", 3) - ->setInt("RZ", 0) - ->setFloat("SX", 1.0) - ->setFloat("SY", 1.0) - ->setFloat("SZ", 1.0) - ->setFloat("TX", 0.0) - ->setFloat("TY", 0.0) - ->setFloat("TZ", 0.0)), - ]; - } - - public function getCurrentBlockProperties(): array { - return [$this->facing]; - } - - protected function writeStateToMeta(): int { - return Permutations::toMeta($this); - } - - public function readStateFromData(int $id, int $stateMeta): void { - $blockProperties = Permutations::fromMeta($this, $stateMeta); - $this->facing = $blockProperties[0] ?? Facing::NORTH; - } - - public function getStateBitmask(): int { - return Permutations::getStateBitmask($this); - } - - public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null): bool { - if($player !== null) { - $this->facing = $player->getHorizontalFacing(); - } - return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); - } - - public function serializeState(BlockStateWriter $out): void { - $out->writeInt("customies:rotation", $this->facing); - } - - public function deserializeState(BlockStateReader $in): void { - $this->facing = $in->readInt("customies:rotation"); - } -} \ No newline at end of file diff --git a/src/block/permutations/traits/BlockFaceRotationTrait.php b/src/block/permutations/traits/BlockFaceRotationTrait.php new file mode 100644 index 00000000..d354e5cb --- /dev/null +++ b/src/block/permutations/traits/BlockFaceRotationTrait.php @@ -0,0 +1,97 @@ +addState(new BlockState("minecraft:block_face", + ["down", "up", "north", "south", "east", "west"] + )); + } + + protected function initPermutations(): void { + $this->addPermutations([ + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'down'", + new TransformationComponent(new Vector3(90, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'up'", + new TransformationComponent(new Vector3(-90, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'north'", + new TransformationComponent(new Vector3(0, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'south'", + new TransformationComponent(new Vector3(0, 180, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'west'", + new TransformationComponent(new Vector3(0, 90, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'east'", + new TransformationComponent(new Vector3(0, -90, 0)) + ), + ]); + } + + public function getCurrentStates(): array { + return [$this->facing]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null): bool { + $this->facing = $face; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function serializeState(BlockStateWriter $out): void { + $out->writeString( + "minecraft:block_face", + match ($this->facing) { + Facing::DOWN => "down", + Facing::UP => "up", + Facing::NORTH => "north", + Facing::SOUTH => "south", + Facing::WEST => "west", + Facing::EAST => "east", + } + ); + } + + public function deserializeState(BlockStateReader $in): void { + $this->facing = match ($in->readString("minecraft:block_face")) { + "down" => Facing::UP, + "up" => Facing::DOWN, + "north" => Facing::NORTH, + "south" => Facing::SOUTH, + "west" => Facing::WEST, + "east" => Facing::EAST, + }; + } +} diff --git a/src/block/permutations/traits/CardinalDirectionRotationTrait.php b/src/block/permutations/traits/CardinalDirectionRotationTrait.php new file mode 100644 index 00000000..3f06e541 --- /dev/null +++ b/src/block/permutations/traits/CardinalDirectionRotationTrait.php @@ -0,0 +1,91 @@ +addState(new BlockState("minecraft:cardinal_direction", + ["north", "south", "west", "east"] + )); + } + + protected function initPermutations(): void { + $this->addPermutations([ + new BlockPermutation( + "q.block_state('minecraft:cardinal_direction') == 'north'", + new TransformationComponent(new Vector3(0, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:cardinal_direction') == 'south'", + new TransformationComponent(new Vector3(0, 180, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:cardinal_direction') == 'west'", + new TransformationComponent(new Vector3(0, 90, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:cardinal_direction') == 'east'", + new TransformationComponent(new Vector3(0, -90, 0)) + ), + ]); + } + + public function getCurrentStates(): array { + return [$this->facing]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null): bool { + $this->facing = match($face) { + Facing::NORTH => Facing::SOUTH, + Facing::SOUTH => Facing::NORTH, + Facing::WEST => Facing::EAST, + Facing::EAST => Facing::WEST, + default => Facing::opposite($player?->getHorizontalFacing() ?? Facing::NORTH) + }; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function serializeState(BlockStateWriter $out): void { + $out->writeString( + "minecraft:cardinal_direction", + match($this->facing) { + Facing::NORTH => "north", + Facing::SOUTH => "south", + Facing::WEST => "west", + Facing::EAST => "east", + } + ); + } + + public function deserializeState(BlockStateReader $in): void { + $this->facing = match($in->readString("minecraft:cardinal_direction")) { + "north" => Facing::NORTH, + "south" => Facing::SOUTH, + "west" => Facing::WEST, + "east" => Facing::EAST, + }; + } +} diff --git a/src/block/permutations/traits/FacingDirectionRotationTrait.php b/src/block/permutations/traits/FacingDirectionRotationTrait.php new file mode 100644 index 00000000..c605ee50 --- /dev/null +++ b/src/block/permutations/traits/FacingDirectionRotationTrait.php @@ -0,0 +1,97 @@ +addState(new BlockState("minecraft:facing_direction", + ["down", "up", "north", "south", "east", "west"] + )); + } + + protected function initPermutations(): void { + $this->addPermutations([ + new BlockPermutation( + "q.block_state('minecraft:facing_direction') == 'down'", + new TransformationComponent(new Vector3(90, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:facing_direction') == 'up'", + new TransformationComponent(new Vector3(-90, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:facing_direction') == 'north'", + new TransformationComponent(new Vector3(0, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:facing_direction') == 'south'", + new TransformationComponent(new Vector3(0, 180, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:facing_direction') == 'west'", + new TransformationComponent(new Vector3(0, 90, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:facing_direction') == 'east'", + new TransformationComponent(new Vector3(0, -90, 0)) + ), + ]); + } + + public function getCurrentStates(): array { + return [$this->facing]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null): bool { + $this->facing = Facing::opposite($face); + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function serializeState(BlockStateWriter $out): void { + $out->writeString( + "minecraft:facing_direction", + match ($this->facing) { + Facing::DOWN => "down", + Facing::UP => "up", + Facing::NORTH => "north", + Facing::SOUTH => "south", + Facing::WEST => "west", + Facing::EAST => "east", + } + ); + } + + public function deserializeState(BlockStateReader $in): void { + $this->facing = match ($in->readString("minecraft:facing_direction")) { + "down" => Facing::UP, + "up" => Facing::DOWN, + "north" => Facing::NORTH, + "south" => Facing::SOUTH, + "west" => Facing::WEST, + "east" => Facing::EAST, + }; + } +} diff --git a/src/block/permutations/traits/LogRotationTrait.php b/src/block/permutations/traits/LogRotationTrait.php new file mode 100644 index 00000000..33ec3bd8 --- /dev/null +++ b/src/block/permutations/traits/LogRotationTrait.php @@ -0,0 +1,84 @@ +addState(new BlockState("minecraft:block_face", + ["down", "up","north", "south", "east", "west"] + )); + } + + protected function initPermutations(): void { + $this->addPermutations([ + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'west' || q.block_state('minecraft:block_face') == 'east'", + new TransformationComponent(new Vector3(0, 0, 90)) + ), + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'down' || q.block_state('minecraft:block_face') == 'up'", + new TransformationComponent(new Vector3(0, 0, 0)) + ), + new BlockPermutation( + "q.block_state('minecraft:block_face') == 'north' || q.block_state('minecraft:block_face') == 'south'", + new TransformationComponent(new Vector3(90, 0, 0)) + ) + ]); + } + + public function getCurrentStates(): array { + return [$this->axis]; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null): bool { + $this->axis = match($face) { + 0, 1 => Axis::Y, // down, up + 2, 3 => Axis::Z, // north, south + 4, 5 => Axis::X, // west, east + default => Axis::Y + }; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function serializeState(BlockStateWriter $out): void { + $rotation = match($this->axis) { + Axis::X => "east", + Axis::Y => "up", + Axis::Z => "north", + default => "down" + }; + $out->writeString("minecraft:block_face", $rotation); + } + + public function deserializeState(BlockStateReader $in): void { + $this->axis = match($in->readString("minecraft:block_face")) { + "east", "west" => Axis::X, + "up", "down" => Axis::Y, + "north", "south" => Axis::Z, + default => Axis::Y + }; + } +} diff --git a/src/block/properties/AllowedFace.php b/src/block/properties/AllowedFace.php new file mode 100644 index 00000000..fa872a62 --- /dev/null +++ b/src/block/properties/AllowedFace.php @@ -0,0 +1,18 @@ + */ + private array $states = []; + /** @var string|null */ + private ?string $tags; + + /** + * @param string|null $name Block identifier (e.g. minecraft:dirt) + * @param array $states + * @param string|null $tags Molang tag query + */ + public function __construct( + ?string $name = null, + array $states = [], + ?string $tags = null + ) { + $this->name = $name; + $this->states = $states; + $this->tags = $tags; + } + + /** + * Converts descriptor to Bedrock format. + */ + public function toArray(): array { + $data = []; + if($this->name !== null){ + $data["name"] = $this->name; + } + if(!empty($this->states)){ + $data["states"] = $this->states; + } + if($this->tags !== null){ + $data["tags"] = $this->tags; + $data["tags_version"] = (int) 13; + } + return $data; + } + + /** + * Creates a BlockDescriptor from an array. + */ + public static function fromArray(array $data): self { + return new self( + $data["name"] ?? null, + $data["states"] ?? [], + $data["tags"] ?? null + ); + } +} \ No newline at end of file diff --git a/src/block/properties/Box.php b/src/block/properties/Box.php new file mode 100644 index 00000000..9a762735 --- /dev/null +++ b/src/block/properties/Box.php @@ -0,0 +1,131 @@ +x)); + $originY = max(0, min(23, $origin->y)); + $originZ = max(-8, min(7, $origin->z)); + + // Clamp size + $sizeX = max(1, min(16, $size->x)); + $sizeY = max(1, min(24, $size->y)); + $sizeZ = max(1, min(16, $size->z)); + + // Clamp to ensure origin + size is valid + $sizeX = min($sizeX, 8 - $originX); + $sizeY = min($sizeY, 24 - $originY); + $sizeZ = min($sizeZ, 8 - $originZ); + + $this->origin = new Vector3($originX, $originY, $originZ); + $this->size = new Vector3($sizeX, $sizeY, $sizeZ); + } + + /** + * Creates a box from an {@see AxisAlignedBB}. + * @param AxisAlignedBB $bb The bounding box to convert. + * @return self + */ + public static function fromAABB(AxisAlignedBB $bb): self { + return new self( + new Vector3($bb->minX, $bb->minY, $bb->minZ), + new Vector3($bb->maxX - $bb->minX, $bb->maxY - $bb->minY, $bb->maxZ - $bb->minZ) + ); + } + + /** + * Returns the origin (minimum corner) of the box. + * @return Vector3 + */ + public function getOrigin(): Vector3 { return $this->origin; } + + /** + * Returns the size of the box. + * @return Vector3 + */ + public function getSize(): Vector3 { return $this->size; } + + /** + * Returns the maximum corner of the box (origin + size). + * @return Vector3 + */ + public function getMax(): Vector3 { return $this->origin->addVector($this->size); } + + /** + * Converts the box into the Bedrock NBT array format. + * + * Coordinates are converted from block-relative space into + * client-expected values (X and Z shifted by +8). + * @return array{ + * minX: float, + * minY: float, + * minZ: float, + * maxX: float, + * maxY: float, + * maxZ: float + * } + */ + public function toNbtArray(): array { + $max = $this->getMax(); + return [ + "minX" => (float) ($this->origin->x + 8), + "minY" => (float) $this->origin->y, + "minZ" => (float) ($this->origin->z + 8), + "maxX" => (float) ($max->x + 8), + "maxY" => (float) $max->y, + "maxZ" => (float) ($max->z + 8), + ]; + } + + /** + * Converts this box into an {@see AxisAlignedBB}. + * @return AxisAlignedBB + */ + public function toAxisAlignedBB(): AxisAlignedBB { + $max = $this->getMax(); + return new AxisAlignedBB( + $this->origin->x, $this->origin->y, $this->origin->z, + $max->x, $max->y, $max->z + ); + } + + /** + * Returns a default full block collision box. + * @return Box + */ + public static function defaultBox(): Box { + return new self(new Vector3(-8.0, 0.0, -8.0), new Vector3(16.0, 16.0, 16.0)); + } +} \ No newline at end of file diff --git a/src/block/properties/Material.php b/src/block/properties/Material.php new file mode 100644 index 00000000..cb195370 --- /dev/null +++ b/src/block/properties/Material.php @@ -0,0 +1,157 @@ +packed_bools = self::SUPPORTS_TEXTURE_VARIATION + | ($face_dimming ? self::FACE_DIMMING : 0) + | ($isotropic ? self::RANDOMIZE_UV_ROTATION : 0); + } + + /** + * Returns the targeted face for the material. + * @return string The targeted face for the material. + */ + public function getTarget(): string { + return $this->target; + } + + /** + * Returns the material flag bitmask. + * @return int Bitmask composed of FLAG_* constants. + */ + public function getBitSet(): int { + return $this->packed_bools; + } + + /** + * Creates a Material instance from a decoded material definition. + * @param string $target Targeted face for the material. + * @param array{ + * texture: string, + * render_method?: string, + * tint_method?: string, + * ambient_occlusion?: float|int, + * face_dimming?: bool, + * isotropic?: bool + * } $data + * @return self + * @throws \InvalidArgumentException if required fields are missing or invalid. + */ + public static function fromArray(string $target, array $data): self { + if(!isset($data['texture'])){ + throw new \InvalidArgumentException('Material texture is required'); + } + return new self( + $target, + $data["texture"], + RenderMethod::tryFrom($data["render_method"] ?? "") ?? RenderMethod::OPAQUE, + TintMethod::tryFrom($data["tint_method"] ?? "") ?? TintMethod::NONE, + (float) ($data["ambient_occlusion"] ?? 1.0), + $data["face_dimming"] ?? true, + $data["isotropic"] ?? false + ); + } + + /** + * Converts the material into Bedrock-compatible NBT format. + * @return array{ + * ambient_occlusion: float, + * packed_bools: ByteTag, + * render_method: string, + * texture: string, + * tint_method: string, + * } + */ + public function toArray(): array { + return [ + "texture" => $this->texture, + "render_method" => $this->renderMethod->value, + "tint_method" => $this->tintMethod->value, + "ambient_occlusion" => $this->ambientOcclusion, + "packed_bools" => new ByteTag($this->packed_bools) + ]; + } + + /** + * Validates an array of materials. + * @param Material[] $materials + * @throws \InvalidArgumentException if the array is empty or contains invalid entries. + */ + public static function validMaterials(array $materials): void{ + if($materials === []){ + throw new \InvalidArgumentException('At least one material must be defined'); + } + foreach($materials as $material){ + if(!$material instanceof Material){ + throw new \InvalidArgumentException('All materials must be instances of ' . Material::class); + } + } + } +} \ No newline at end of file diff --git a/src/block/properties/PlacementCondition.php b/src/block/properties/PlacementCondition.php new file mode 100644 index 00000000..6b1f53b3 --- /dev/null +++ b/src/block/properties/PlacementCondition.php @@ -0,0 +1,62 @@ + 6){ + throw new InvalidArgumentException("Placement condition may not exceed 6 allowed faces"); + } + if(count($blockFilters) > 64){ + throw new InvalidArgumentException("Placement condition may not exceed 64 block filters"); + } + $this->allowedFaces = $allowedFaces; + $this->blockFilters = $blockFilters; + } + + /** + * Converts the placement condition to Bedrock format. + */ + public function toArray(): array { + return [ + "allowed_faces" => array_map( + static fn(AllowedFace $f) => $f->value, + $this->allowedFaces + ), + "block_filter" => array_map( + static fn(BlockDescriptor $b) => $b->toArray(), + $this->blockFilters + ) + ]; + } + + /** + * Creates a PlacementCondition from an array. + */ + public static function fromArray(array $data): self { + return new self( + array_map( + static fn(string $f) => AllowedFace::from($f), + $data["allowed_faces"] ?? [] + ), + array_map( + static fn(array $b) => BlockDescriptor::fromArray($b), + $data["block_filter"] ?? [] + ) + ); + } +} \ No newline at end of file diff --git a/src/block/properties/RenderMethod.php b/src/block/properties/RenderMethod.php new file mode 100644 index 00000000..89695a57 --- /dev/null +++ b/src/block/properties/RenderMethod.php @@ -0,0 +1,16 @@ +currentValue = $values[0] ?? null; + } + + /** + * Returns the state property name. + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * Returns the possible values array. + * @return array + */ + public function getValues(): array { + return $this->values; + } + + /** + * Returns the NBT value definition for client (enum + name). + * @return array + */ + public function getValue(): array { + return [ + "enum" => $this->values, + "name" => $this->name + ]; + } +} \ No newline at end of file diff --git a/src/block/permutations/Permutable.php b/src/block/states/BlockStates.php similarity index 52% rename from src/block/permutations/Permutable.php rename to src/block/states/BlockStates.php index 60637fc1..5852ba1c 100644 --- a/src/block/permutations/Permutable.php +++ b/src/block/states/BlockStates.php @@ -1,33 +1,46 @@ states[$state->getName()] = $state; + } + + /** + * Checks whether the block has a state with the given name. + * @param string $name + * @return bool + */ + public function hasState(string $name): bool { + return isset($this->states[$name]); + } + + /** + * Retrieves a state by its name. + * @param string $name + * @return BlockState|null + */ + public function getState(string $name): ?BlockState { + return $this->states[$name] ?? null; + } + + /** + * Returns all registered block states. + * @return BlockState[] + */ + public function getStates(): array { + return $this->states; + } +} \ No newline at end of file diff --git a/src/entity/CustomiesEntityFactory.php b/src/entity/CustomiesEntityFactory.php index e6da13ac..5c0a9ea3 100644 --- a/src/entity/CustomiesEntityFactory.php +++ b/src/entity/CustomiesEntityFactory.php @@ -16,7 +16,7 @@ use pocketmine\world\World; use ReflectionClass; -class CustomiesEntityFactory { +final class CustomiesEntityFactory { use SingletonTrait; /** @@ -32,6 +32,12 @@ public function registerEntity(string $className, string $identifier, ?Closure $ $this->updateStaticPacketCache($identifier, $behaviourId); } + /** + * Updates the AvailableActorIdentifiersPacket to include the new entity. + * @param string $identifier example: "customies:my_entity" + * @param string $behaviourId + * @return void + */ private function updateStaticPacketCache(string $identifier, string $behaviourId): void { $instance = StaticPacketCache::getInstance(); $property = (new ReflectionClass($instance))->getProperty("availableActorIdentifiers"); @@ -44,4 +50,4 @@ private function updateStaticPacketCache(string $identifier, string $behaviourId ->setString("bid", $behaviourId)); $packet->identifiers = new CacheableNbt($root); } -} +} \ No newline at end of file diff --git a/src/item/CreativeInventoryInfo.php b/src/item/CreativeInventoryInfo.php index 9b1eb5da..487508a8 100644 --- a/src/item/CreativeInventoryInfo.php +++ b/src/item/CreativeInventoryInfo.php @@ -2,8 +2,21 @@ namespace customiesdevs\customies\item; +use pocketmine\block\Block; +use pocketmine\inventory\CreativeCategory; +use pocketmine\inventory\CreativeGroup; +use pocketmine\inventory\CreativeInventory; +use pocketmine\item\Item; +use pocketmine\lang\Translatable; +use pocketmine\utils\AssumptionFailedError; + final class CreativeInventoryInfo { + /** @var array|null */ + private static ?array $groups = null; + + const NONE = "none"; + const CATEGORY_ALL = "all"; const CATEGORY_COMMANDS = "commands"; const CATEGORY_CONSTRUCTION = "construction"; @@ -11,18 +24,22 @@ final class CreativeInventoryInfo { const CATEGORY_ITEMS = "items"; const CATEGORY_NATURE = "nature"; - const NONE = "none"; const GROUP_ANVIL = "itemGroup.name.anvil"; const GROUP_ARROW = "itemGroup.name.arrow"; const GROUP_AXE = "itemGroup.name.axe"; const GROUP_BANNER = "itemGroup.name.banner"; const GROUP_BANNER_PATTERN = "itemGroup.name.banner_pattern"; + const GROUP_BAR = "itemGroup.name.bars"; const GROUP_BED = "itemGroup.name.bed"; const GROUP_BOAT = "itemGroup.name.boat"; const GROUP_BOOTS = "itemGroup.name.boots"; + const GROUP_BUNDLES = "itemGroup.name.bundles"; const GROUP_BUTTONS = "itemGroup.name.buttons"; + const GROUP_CANDLES = "itemGroup.name.candles"; + const GROUP_CHAINS = "itemGroup.name.chains"; const GROUP_CHALKBOARD = "itemGroup.name.chalkboard"; const GROUP_CHEST = "itemGroup.name.chest"; + const GROUP_CHEST_BOAT = "itemGroup.name.chestboat"; const GROUP_CHESTPLATE = "itemGroup.name.chestplate"; const GROUP_CONCRETE = "itemGroup.name.concrete"; const GROUP_CONCRETE_POWDER = "itemGroup.name.concretePowder"; @@ -42,12 +59,18 @@ final class CreativeInventoryInfo { const GROUP_GLASS = "itemGroup.name.glass"; const GROUP_GLASS_PANE = "itemGroup.name.glassPane"; const GROUP_GLAZED_TERRACOTTA = "itemGroup.name.glazedTerracotta"; + const GROUP_GOAT_HORN = "itemGroup.name.goatHorn"; + const GROUP_GOLEM_STATUE = "itemGroup.name.copper_golem_statue"; const GROUP_GRASS = "itemGroup.name.grass"; + const GROUP_HANGING_SIGN = "itemGroup.name.hanging_sign"; + const GROUP_HARNESSES = "itemGroup.name.harnesses"; const GROUP_HELMET = "itemGroup.name.helmet"; const GROUP_HOE = "itemGroup.name.hoe"; const GROUP_HORSE_ARMOR = "itemGroup.name.horseArmor"; + const GROUP_LANTERNS = "itemGroup.name.lanterns"; const GROUP_LEAVES = "itemGroup.name.leaves"; const GROUP_LEGGINGS = "itemGroup.name.leggings"; + const GROUP_LIGHTNING_ROD = "itemGroup.name.lightning_rod"; const GROUP_LINGERING_POTION = "itemGroup.name.lingeringPotion"; const GROUP_LOG = "itemGroup.name.log"; const GROUP_MINECRAFT = "itemGroup.name.minecart"; @@ -55,25 +78,32 @@ final class CreativeInventoryInfo { const GROUP_MOB_EGGS = "itemGroup.name.mobEgg"; const GROUP_MONSTER_STONE_EGG = "itemGroup.name.monsterStoneEgg"; const GROUP_MUSHROOM = "itemGroup.name.mushroom"; + const GROUP_NAUTILUS_ARMOR = "itemGroup.name.nautilus_armor"; const GROUP_NETHERWART_BLOCK = "itemGroup.name.netherWartBlock"; + const GROUP_OMINOUS_BOTTLE = "itemGroup.name.ominousBottle"; const GROUP_ORE = "itemGroup.name.ore"; const GROUP_PERMISSION = "itemGroup.name.permission"; const GROUP_PICKAXE = "itemGroup.name.pickaxe"; const GROUP_PLANKS = "itemGroup.name.planks"; const GROUP_POTION = "itemGroup.name.potion"; + const GROUP_POTTERY_SHERDS = "itemGroup.name.potterySherds"; const GROUP_PRESSURE_PLATE = "itemGroup.name.pressurePlate"; const GROUP_RAIL = "itemGroup.name.rail"; const GROUP_RAW_FOOD = "itemGroup.name.rawFood"; const GROUP_RECORD = "itemGroup.name.record"; const GROUP_SANDSTONE = "itemGroup.name.sandstone"; const GROUP_SAPLING = "itemGroup.name.sapling"; + const GROUP_SCULK = "itemGroup.name.sculk"; const GROUP_SEED = "itemGroup.name.seed"; + const GROUP_SHELF = "itemGroup.name.shelf"; const GROUP_SHOVEL = "itemGroup.name.shovel"; const GROUP_SHULKER_BOX = "itemGroup.name.shulkerBox"; const GROUP_SIGN = "itemGroup.name.sign"; const GROUP_SKULL = "itemGroup.name.skull"; const GROUP_SLAB = "itemGroup.name.slab"; const GROUP_SLASH_POTION = "itemGroup.name.splashPotion"; + const GROUP_SMITHING_TEMPLATES = "itemGroup.name.smithing_templates"; + const GROUP_SPEAR = "itemGroup.name.spear"; const GROUP_STAINED_CLAY = "itemGroup.name.stainedClay"; const GROUP_STAIRS = "itemGroup.name.stairs"; const GROUP_STONE = "itemGroup.name.stone"; @@ -84,42 +114,132 @@ final class CreativeInventoryInfo { const GROUP_WOOD = "itemGroup.name.wood"; const GROUP_WOOL = "itemGroup.name.wool"; const GROUP_WOOL_CARPET = "itemGroup.name.woolCarpet"; - const GROUP_CANDLES = "itemGroup.name.candles"; - const GROUP_GOAT_HORN = "itemGroup.name.goatHorn"; /** - * Returns a default type which puts the item in to the all category and no sub group. + * Returns a default CreativeInventoryInfo instance (all category, no group) + * @return self */ public static function DEFAULT(): self { return new self(self::CATEGORY_ALL, self::NONE); } - public function __construct(private readonly string $category = self::NONE, private readonly string $group = self::NONE) { } + /** + * @param string $category The category this item belongs to + * @param string $group The group this item belongs to (optional) + */ + public function __construct( + private readonly string $category = self::NONE, + private readonly string $group = self::NONE + ) {} /** - * Returns the category the item is part of. + * Returns the creative inventory category. + * @return string */ public function getCategory(): string { return $this->category; } /** - * Returns the numeric representation of the category the item is part of. + * Returns the numeric representation of the category. + * 0 = all, 1 = construction, 2 = nature, 3 = equipment, 4 = items + * @return int */ public function getNumericCategory(): int { - return match ($this->getCategory()) { + return match ($this->category) { self::CATEGORY_CONSTRUCTION => 1, self::CATEGORY_NATURE => 2, self::CATEGORY_EQUIPMENT => 3, self::CATEGORY_ITEMS => 4, - default => 0 + default => 0, }; } /** - * Returns the group the item is part of, if any. + * Returns the creative inventory group. + * @return string */ public function getGroup(): string { return $this->group; } + + /** + * Loads all existing creative groups from the Creative Inventory. + * @return void + */ + public static function load(): void { + if(self::$groups !== null){ + return; + } + $groups = []; + foreach(CreativeInventory::getInstance()->getAllEntries() as $entry){ + $group = $entry->getGroup(); + if($group !== null){ + $groups[$group->getName()->getText()] = $group; + } + } + self::$groups = $groups; + } + + /** + * Returns the CreativeGroup instance for the given name, or null if it does not exist. + * @param string $name + * @return CreativeGroup|null + */ + public static function get(string $name): ?CreativeGroup { + self::load(); + return self::$groups[$name] ?? null; + } + + /** + * Sets a CreativeGroup instance in the internal list. + * @param CreativeGroup $group + * @return void + */ + public static function set(CreativeGroup $group): void { + self::load(); + self::$groups[$group->getName()->getText()] = $group; + } + + /** + * Returns all loaded CreativeGroup instances. + * @return CreativeGroup[] + */ + public static function all(): array { + self::load(); + return self::$groups; + } + + /** + * Registers the Item/Bloxk in the creative inventory based on the provided CreativeInventoryInfo. + * @param Item|Block $type The item/block to register + * @param CreativeInventoryInfo $creativeInfo The creative inventory information + */ + public static function registerCreativeInfo( + Item|Block $type, + CreativeInventoryInfo $creativeInfo + ): void { + if( + $creativeInfo->getCategory() === self::CATEGORY_ALL || + $creativeInfo->getCategory() === self::CATEGORY_COMMANDS + ){ + return; + } + $group = null; + if($creativeInfo->getGroup() !== CreativeInventoryInfo::NONE){ + $group = CreativeInventoryInfo::get($creativeInfo->getGroup()) + ?? new CreativeGroup( + new Translatable($creativeInfo->getGroup()), + $type instanceof Block ? $type->asItem() : $type + ); + } + $category = match($creativeInfo->getCategory()){ + CreativeInventoryInfo::CATEGORY_CONSTRUCTION => CreativeCategory::CONSTRUCTION, + CreativeInventoryInfo::CATEGORY_ITEMS => CreativeCategory::ITEMS, + CreativeInventoryInfo::CATEGORY_NATURE => CreativeCategory::NATURE, + CreativeInventoryInfo::CATEGORY_EQUIPMENT => CreativeCategory::EQUIPMENT, + default => throw new AssumptionFailedError("Unknown Creative Category: " . $creativeInfo->getCategory()), + }; + CreativeInventory::getInstance()->add($type instanceof Block ? $type->asItem() : $type, $category, $group); + } } \ No newline at end of file diff --git a/src/item/CustomiesItemFactory.php b/src/item/CustomiesItemFactory.php index 69275125..cd381f0e 100644 --- a/src/item/CustomiesItemFactory.php +++ b/src/item/CustomiesItemFactory.php @@ -9,35 +9,72 @@ use pocketmine\block\Block; use pocketmine\data\bedrock\item\BlockItemIdMap; use pocketmine\data\bedrock\item\SavedItemData; -use pocketmine\inventory\CreativeCategory; -use pocketmine\inventory\CreativeGroup; -use pocketmine\inventory\CreativeInventory; use pocketmine\item\Item; use pocketmine\item\StringToItemParser; -use pocketmine\lang\Translatable; use pocketmine\nbt\tag\CompoundTag; use pocketmine\network\mcpe\convert\TypeConverter; use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; -use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; use pocketmine\world\format\io\GlobalItemDataHandlers; use ReflectionClass; -use RuntimeException; - use function array_values; final class CustomiesItemFactory { use SingletonTrait; - /** - * @var ItemTypeEntry[] - */ + /** Default values for item_properties */ + private const PROPERTY_DEFAULTS = [ + 'allow_off_hand' => false, // Byte + 'can_destroy_in_creative' => true, // Byte + 'damage' => 0, // Int + 'enchantable_slot' => 'none', // String + 'enchantable_value' => 0, // Int + 'foil' => false, // Byte + 'frame_count' => 1, // Int + 'hand_equipped' => false, // Byte + 'liquid_clipped' => false, // Byte + 'max_stack_size' => 64, // Int + 'mining_speed' => 1.0, // Float + 'should_despawn' => true, // Byte + 'stacked_by_data' => false, // Byte + 'use_animation' => 0, // Int + 'use_duration' => 0, // Int + ]; + + /** Order in which properties should appear in item_properties */ + private const PROPERTY_ORDER = [ + 'allow_off_hand', + 'can_destroy_in_creative', + 'creative_category', + 'creative_group', + 'damage', + 'enchantable_slot', + 'enchantable_value', + 'foil', + 'frame_count', + 'hand_equipped', + 'hidden_in_commands', + 'liquid_clipped', + 'max_stack_size', + 'minecraft:icon', + 'mining_speed', + 'should_despawn', + 'stacked_by_data', + 'use_animation', + 'use_duration', + ]; + + /** @var ItemTypeEntry[] */ private array $itemTableEntries = []; - private array $groups = []; /** * Get a custom item from its identifier. An exception will be thrown if the item is not registered. + * + * @param string $identifier The string identifier for the item, usually in the format "namespace:item_name" + * @param int $amount The amount of the item to be returned + * @return Item The item instance + * @throws InvalidArgumentException if the item is not registered */ public function get(string $identifier, int $amount = 1): Item { $item = StringToItemParser::getInstance()->parse($identifier); @@ -47,20 +84,9 @@ public function get(string $identifier, int $amount = 1): Item { return $item->setCount($amount); } - private function loadGroups() : void { - if($this->groups !== []){ - return; - } - foreach(CreativeInventory::getInstance()->getAllEntries() as $entry){ - $group = $entry->getGroup(); - if($group !== null){ - $this->groups[$group->getName()->getText()] = $group; - } - } - } - /** - * Returns custom item entries + * Returns all registered item table entries. + * * @return ItemTypeEntry[] */ public function getItemTableEntries(): array { @@ -70,9 +96,17 @@ public function getItemTableEntries(): array { /** * Registers the item to the item factory and assigns it an ID. It also updates the required mappings and stores the * item components if present. - * @phpstan-param class-string $className + * + * @param Closure $itemFunc A closure that returns an instance of the item to be registered + * @param string $identifier The string identifier for the item, usually in the format "namespace:item_name" + * @param CreativeInventoryInfo $creativeInfo The creative inventory info for the item, if any + * @throws InvalidArgumentException if the closure does not return an Item instance */ - public function registerItem(Closure $itemFunc, string $identifier, ?CreativeInventoryInfo $creativeInfo = null): void { + public function registerItem( + Closure $itemFunc, + string $identifier, + CreativeInventoryInfo $creativeInfo = new CreativeInventoryInfo(CreativeInventoryInfo::CATEGORY_EQUIPMENT) + ): void { $item = $itemFunc(); if(!$item instanceof Item) { throw new InvalidArgumentException("Class returned from closure is not a Item"); @@ -81,77 +115,92 @@ public function registerItem(Closure $itemFunc, string $identifier, ?CreativeInv GlobalItemDataHandlers::getDeserializer()->map($identifier, fn() => clone $item); GlobalItemDataHandlers::getSerializer()->map($item, fn() => new SavedItemData($identifier)); - StringToItemParser::getInstance()->register($identifier, fn() => clone $item); - // This is where the components are added to the item + // Adding item components $componentBased = $item instanceof ItemComponents; + // Registers the item to creative inventory + CreativeInventoryInfo::registerCreativeInfo($item, $creativeInfo); + // Create the NBT data for the item $nbt = $this->createItemNbt($item, $identifier, $itemId, $creativeInfo); - - if($creativeInfo !== null){ - $this->loadGroups(); - if($creativeInfo->getCategory() === CreativeInventoryInfo::CATEGORY_ALL || $creativeInfo->getCategory() === CreativeInventoryInfo::CATEGORY_COMMANDS){ - return; - } - - $group = $this->groups[$creativeInfo->getGroup()] ?? ($creativeInfo->getGroup() !== "" && $creativeInfo->getGroup() !== CreativeInventoryInfo::NONE ? new CreativeGroup( - new Translatable($creativeInfo->getGroup()), - $item - ) : null); - - if($group !== null){ - $this->groups[$group->getName()->getText()] = $group; - } - - $category = match ($creativeInfo->getCategory()) { - CreativeInventoryInfo::CATEGORY_CONSTRUCTION => CreativeCategory::CONSTRUCTION, - CreativeInventoryInfo::CATEGORY_ITEMS => CreativeCategory::ITEMS, - CreativeInventoryInfo::CATEGORY_NATURE => CreativeCategory::NATURE, - CreativeInventoryInfo::CATEGORY_EQUIPMENT => CreativeCategory::EQUIPMENT, - default => throw new AssumptionFailedError("Unknown category") - }; - - CreativeInventory::getInstance()->add($item, $category, $group); - } - - $this->itemTableEntries[$identifier] = $entry = new ItemTypeEntry($identifier, $itemId, $componentBased, $componentBased ? 1 : 0, new CacheableNbt($nbt)); + $entry = new ItemTypeEntry( + $identifier, + $itemId, + $componentBased, + $componentBased ? 1 : 0, + new CacheableNbt($nbt) + ); + $this->itemTableEntries[$identifier] = $entry; $this->registerCustomItemMapping($identifier, $itemId, $entry); } /** - * Creates the NBT data for the item. + * Creates the CompoundTag for an item, including components and default properties. */ - private function createItemNbt(Item $item, string $identifier, int $itemId, ?CreativeInventoryInfo $creativeInfo): CompoundTag { - $components = CompoundTag::create(); - $properties = CompoundTag::create(); - - if ($item instanceof ItemComponents) { - foreach ($item->getComponents() as $component) { - $tag = NBT::getTagType($component->getValue()); - if ($tag === null) { - throw new RuntimeException("Failed to get tag type for component " . $component->getName()); - } - if ($component->isProperty()) { - $properties->setTag($component->getName(), $tag); - continue; - } - $components->setTag($component->getName(), $tag); + private function createItemNbt(Item $item, string $identifier, int $itemId, CreativeInventoryInfo $creativeInfo): CompoundTag { + if(!($item instanceof ItemComponents)) { + return CompoundTag::create(); + } + // Initialize item_properties with defaults + $propertiesTag = CompoundTag::create(); + foreach(self::PROPERTY_DEFAULTS as $name => $default) { + $propertiesTag + ->setTag($name, NBT::getTagType($default)); + } + // Set creative info + $propertiesTag->setTag('creative_category', NBT::getTagType((int) $creativeInfo->getNumericCategory())); + $propertiesTag->setTag('creative_group', NBT::getTagType((string) $creativeInfo->getGroup())); + $propertiesTag->setByte("hidden_in_commands", 2); + $tags = []; + $componentsTag = CompoundTag::create(); + // Process each component + foreach($item->getComponents() as $component) { + $name = $component->getName(); + $value = $component->getValue(); + $tag = NBT::getTagType($value); + // Icon goes to item_properties + if($name === 'minecraft:icon') { + $propertiesTag->setTag('minecraft:icon', $tag); + continue; } - if ($creativeInfo !== null) { - $properties->setTag("creative_category", NBT::getTagType($creativeInfo->getNumericCategory())); - $properties->setTag("creative_group", NBT::getTagType($creativeInfo->getGroup())); + // Tags go to item_tags + if($name === 'minecraft:tags') { + $tags = $value['tags'] ?? []; + $componentsTag->setTag($name, $tag); + continue; + } + // Components with property mappings also update item_properties + $mapping = $component->getPropertyMapping(); + if($mapping !== null) { + foreach($mapping as $prop => $propValue) { + if($prop === "use_duration"){ + $propertiesTag->setTag("use_duration", NBT::getTagType((int) round($propValue * 20))); + continue; + } + $propertiesTag->setTag($prop, NBT::getTagType($propValue)); + } } - $components->setTag("item_properties", $properties); - return CompoundTag::create() - ->setTag("components", $components) - ->setInt("id", $itemId) - ->setString("name", $identifier); + // All components go to components tag + $componentsTag->setTag($name, $tag); } - return CompoundTag::create(); + $propertiesTag = NBT::sortCompoundTag($propertiesTag, self::PROPERTY_ORDER); + $components = CompoundTag::create() + ->setTag('item_properties', $propertiesTag) + ->setTag('item_tags', NBT::getTagType($tags)) + ->merge($componentsTag); + + return CompoundTag::create() + ->setTag('components', $components) + ->setInt('id', $itemId) + ->setString('name', $identifier); } /** * Registers a custom item ID to the required mappings in the global ItemTypeDictionary instance. + * This allows the item to be recognized and used within the game. + * @param string $identifier The string identifier for the item, usually in the format "namespace:item_name" + * @param int $itemId The numerical ID to be assigned to the item + * @param ItemTypeEntry $entry The ItemTypeEntry instance representing the item */ private function registerCustomItemMapping(string $identifier, int $itemId, ItemTypeEntry $entry): void { $dictionary = TypeConverter::getInstance()->getItemTypeDictionary(); @@ -176,11 +225,14 @@ private function registerCustomItemMapping(string $identifier, int $itemId, Item /** * Registers the required mappings for the block to become an item that can be placed etc. It is assigned an ID that * correlates to its block ID. + * @param string $identifier The string identifier for the block item, usually in the format "namespace:block_name" + * @param Block $block The block instance to be registered as an item */ public function registerBlockItem(string $identifier, Block $block): void { $itemId = $block->getIdInfo()->getBlockTypeId(); StringToItemParser::getInstance()->registerBlock($identifier, fn() => clone $block); - $this->itemTableEntries[] = $entry = new ItemTypeEntry($identifier, $itemId, false, 2, new CacheableNbt(CompoundTag::create())); + $entry = new ItemTypeEntry($identifier, $itemId, false, 2, new CacheableNbt(CompoundTag::create())); + $this->itemTableEntries[] = $entry; $this->registerCustomItemMapping($identifier, $itemId, $entry); $blockItemIdMap = BlockItemIdMap::getInstance(); @@ -191,4 +243,4 @@ public function registerBlockItem(string $identifier, Block $block): void { $value = $itemToBlockId->getValue($blockItemIdMap); $itemToBlockId->setValue($blockItemIdMap, $value + [$identifier => $identifier]); } -} +} \ No newline at end of file diff --git a/src/item/ItemComponents.php b/src/item/ItemComponents.php index cb3c60d7..e9696640 100644 --- a/src/item/ItemComponents.php +++ b/src/item/ItemComponents.php @@ -8,8 +8,8 @@ interface ItemComponents { /** - * Add component adds a component to the item that can be returned in the getComponents() method to be sent over - * the network. + * Adds a component to the item + * * @param ItemComponent $component * @return void */ @@ -17,14 +17,24 @@ public function addComponent(ItemComponent $component): void; /** * Returns if the item has the component with the provided name. + * * @param string $name * @return bool */ public function hasComponent(string $name): bool; /** - * Returns the fully-structured CompoundTag that can be sent to a client in the ItemComponentsPacket. - * @return ItemComponent[] + * Returns the component with the provided name, or null if it does not exist. + * + * @param string $name + * @return ItemComponent|null + */ + public function getComponent(string $name): ?ItemComponent; + + /** + * Returns all components of the item. + * + * @return ItemComponent[] Array of all components */ public function getComponents(): array; } diff --git a/src/item/ItemComponentsTrait.php b/src/item/ItemComponentsTrait.php index 2dbacfe0..b9470059 100644 --- a/src/item/ItemComponentsTrait.php +++ b/src/item/ItemComponentsTrait.php @@ -3,102 +3,70 @@ namespace customiesdevs\customies\item; -use customiesdevs\customies\item\component\CanDestroyInCreativeComponent; -use customiesdevs\customies\item\component\DamageComponent; use customiesdevs\customies\item\component\DisplayNameComponent; -use customiesdevs\customies\item\component\DurabilityComponent; -use customiesdevs\customies\item\component\FoodComponent; -use customiesdevs\customies\item\component\FuelComponent; -use customiesdevs\customies\item\component\HandEquippedComponent; use customiesdevs\customies\item\component\IconComponent; use customiesdevs\customies\item\component\ItemComponent; -use customiesdevs\customies\item\component\MaxStackSizeComponent; -use customiesdevs\customies\item\component\ProjectileComponent; -use customiesdevs\customies\item\component\ThrowableComponent; -use customiesdevs\customies\item\component\UseAnimationComponent; -use customiesdevs\customies\item\component\WearableComponent; -use pocketmine\entity\Consumable; -use pocketmine\inventory\ArmorInventory; -use pocketmine\item\Armor; -use pocketmine\item\Durable; -use pocketmine\item\Food; -use pocketmine\item\ProjectileItem; -use pocketmine\item\Sword; -use pocketmine\item\Tool; trait ItemComponentsTrait { - /** @var ItemComponent[] */ + /** + * Registered item components indexed by component name. + * + * @var array + */ private array $components; + /** + * Adds a component to the item. + * + * @param ItemComponent $component The component to add + */ public function addComponent(ItemComponent $component): void { $this->components[$component->getName()] = $component; } + /** + * Checks if the item has a component by its name. + * + * @param string $name The name of the component + * @return bool True if the component exists, false otherwise + */ public function hasComponent(string $name): bool { return isset($this->components[$name]); } /** - * @return ItemComponent[] + * Retrieves a component by its name. + * + * @param string $name The name of the component + * @return ItemComponent|null The component if found, null otherwise + */ + public function getComponent(string $name): ?ItemComponent { + return $this->components[$name] ?? null; + } + + /** + * Returns all components of the item. + * + * @return ItemComponent[] Array of all components */ public function getComponents(): array { return $this->components; } /** - * Initializes the item with default components that are required for the item to function correctly. + * @todo + * Initializes common item components. + * + * @param string $texture The texture identifier for the icon + * @param string $name The display name of the item */ - protected function initComponent(string $texture): void { - $this->addComponent(new IconComponent($texture)); - $this->addComponent(new CanDestroyInCreativeComponent()); - $this->addComponent(new MaxStackSizeComponent($this->getMaxStackSize())); - - if($this instanceof Armor) { - $slot = match ($this->getArmorSlot()) { - ArmorInventory::SLOT_HEAD => WearableComponent::SLOT_ARMOR_HEAD, - ArmorInventory::SLOT_CHEST => WearableComponent::SLOT_ARMOR_CHEST, - ArmorInventory::SLOT_LEGS => WearableComponent::SLOT_ARMOR_LEGS, - ArmorInventory::SLOT_FEET => WearableComponent::SLOT_ARMOR_FEET, - default => WearableComponent::SLOT_ARMOR - }; - $this->addComponent(new WearableComponent($slot, $this->getDefensePoints())); - } - - if($this instanceof Consumable) { - if(($food = $this instanceof Food)) { - $this->addComponent(new FoodComponent(!$this->requiresHunger())); - } - $this->addComponent(new UseAnimationComponent($food ? UseAnimationComponent::ANIMATION_EAT : UseAnimationComponent::ANIMATION_DRINK)); - $this->setUseDuration(20); - } - - if($this instanceof Durable) { - $this->addComponent(new DurabilityComponent($this->getMaxDurability())); - } - - if($this instanceof ProjectileItem) { - $this->addComponent(new ProjectileComponent(1.25, "projectile")); - $this->addComponent(new ThrowableComponent(true)); - } - - if($this->getName() !== "Unknown") { - $this->addComponent(new DisplayNameComponent($this->getName())); - } - - if($this->getFuelTime() > 0) { - $this->addComponent(new FuelComponent($this->getFuelTime())); - } - - if($this->getAttackPoints() > 0) { - $this->addComponent(new DamageComponent($this->getAttackPoints())); - } - - if($this instanceof Tool) { - $this->addComponent(new HandEquippedComponent()); - if ($this instanceof Sword) { - $this->addComponent(new CanDestroyInCreativeComponent(false)); - } + protected function initComponent(string $texture, string $name): void { + // Only initialize if no components are set yet + if($this->getComponents() !== []){ + return; } + $this->addComponent(new IconComponent($texture)); + $this->addComponent(new DisplayNameComponent($name)); } -} +} \ No newline at end of file diff --git a/src/item/component/AllowOffHandComponent.php b/src/item/component/AllowOffHandComponent.php index 337f8a02..effb379c 100644 --- a/src/item/component/AllowOffHandComponent.php +++ b/src/item/component/AllowOffHandComponent.php @@ -16,14 +16,16 @@ public function __construct(bool $offHand = true) { } public function getName(): string { - return "allow_off_hand"; + return 'minecraft:allow_off_hand'; } - public function getValue(): bool { - return $this->offHand; + public function getValue(): array { + return [ + "value" => $this->offHand + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['allow_off_hand' => $this->offHand]; } } \ No newline at end of file diff --git a/src/item/component/BlockPlacerComponent.php b/src/item/component/BlockPlacerComponent.php deleted file mode 100644 index 2a0953ba..00000000 --- a/src/item/component/BlockPlacerComponent.php +++ /dev/null @@ -1,49 +0,0 @@ -block = $block; - } - - public function getName(): string { - return "minecraft:block_placer"; - } - - public function getValue(): array { - return [ - "block" => GlobalBlockStateHandlers::getSerializer()->serialize($this->block->getStateId())->getName(), - "use_on" => $this->useOn - ]; - } - - public function isProperty(): bool { - return false; - } - - /** - * Add blocks to the `use_on` array in the required format. - * @param Block ...$blocks - */ - public function useOn(Block ...$blocks): self{ - foreach($blocks as $block){ - $this->useOn[] = [ - "name" => GlobalBlockStateHandlers::getSerializer()->serialize($block->getStateId())->getName() - ]; - } - return $this; - } -} \ No newline at end of file diff --git a/src/item/component/BundleInteractionComponent.php b/src/item/component/BundleInteractionComponent.php index 79e446fc..1b7b0df7 100644 --- a/src/item/component/BundleInteractionComponent.php +++ b/src/item/component/BundleInteractionComponent.php @@ -8,16 +8,20 @@ final class BundleInteractionComponent implements ItemComponent { private int $numViewableSlots; /** - * `minecraft:bundle_interaction` enables the bundle-specific interaction scheme and tooltip for an item. + * Enables the bundle-specific interaction scheme and tooltip for an item. * To use this component, the item must have a `minecraft:storage_item` item component defined. - * @param int $numViewableSlots The number of slots that are viewable in the bundle. + * @param int $numViewableSlots The maximum number of slots in the bundle viewable by the player. Can be from 1 to 64. Default is 12. + * @throws \InvalidArgumentException if the number of viewable slots is not between 1 and 64. */ - public function __construct(int $numViewableSlots) { + public function __construct(int $numViewableSlots = 12) { + if($numViewableSlots < 1 || $numViewableSlots > 64) { + throw new \InvalidArgumentException("Number of viewable-slots must be between 1 and 64, $numViewableSlots given"); + } $this->numViewableSlots = $numViewableSlots; } public function getName(): string { - return "minecraft:bundle_interaction"; + return 'minecraft:bundle_interaction'; } public function getValue(): array { @@ -26,7 +30,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/CanDestroyInCreativeComponent.php b/src/item/component/CanDestroyInCreativeComponent.php index 0c7c6d5f..bb904355 100644 --- a/src/item/component/CanDestroyInCreativeComponent.php +++ b/src/item/component/CanDestroyInCreativeComponent.php @@ -16,14 +16,16 @@ public function __construct(bool $canDestroyInCreative = true) { } public function getName(): string { - return "can_destroy_in_creative"; + return 'minecraft:can_destroy_in_creative'; } - public function getValue(): bool { - return $this->canDestroyInCreative; + public function getValue(): array { + return [ + "value" => $this->canDestroyInCreative + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['can_destroy_in_creative' => $this->canDestroyInCreative]; } -} +} \ No newline at end of file diff --git a/src/item/component/CompostableComponent.php b/src/item/component/CompostableComponent.php new file mode 100644 index 00000000..3b42d248 --- /dev/null +++ b/src/item/component/CompostableComponent.php @@ -0,0 +1,37 @@ + 100) { + throw new \InvalidArgumentException("Composting chance must be between 1 and 100, $compostingChance given"); + } + $this->compostingChance = $compostingChance; + } + + public function getName(): string { + return 'minecraft:compostable'; + } + + public function getValue(): array { + return [ + "composting_chance" => new ByteTag($this->compostingChance) + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/CooldownComponent.php b/src/item/component/CooldownComponent.php index 54f025a4..99341f21 100644 --- a/src/item/component/CooldownComponent.php +++ b/src/item/component/CooldownComponent.php @@ -5,38 +5,52 @@ final class CooldownComponent implements ItemComponent { - public const CATEGORY_SHIELD = "minecraft:shield"; - public const CATEGORY_PEARL = "minecraft:ender_pearl"; - public const CATEGORY_HORN = "minecraft:goat_horn"; - public const CATEGORY_WINDCHARGE = "minecraft:wind_charge"; - public const CATEGORY_CHORUS = "minecraft:chorusfruit"; + public const CATEGORY_SHIELD = "shield"; + public const CATEGORY_PEARL = "ender_pearl"; + public const CATEGORY_HORN = "goat_horn"; + public const CATEGORY_WINDCHARGE = "wind_charge"; + public const CATEGORY_CHORUS = "chorusfruit"; + + /** + * Causes the cooldown to start when the player attacks while holding the item and + * prevents the item from being used to attack while the cooldown is active. + */ + public const TYPE_ATTACK = "attack"; + /** + * Causes the cooldown to start when the item is used and + * prevents the item from being used while the cooldown is active. + */ + public const TYPE_USE = "use"; private string $category; private float $duration; + private string $type; /** - * Sets an item's "Cooldown" time. - * After using an item, it becomes unusable for the duration specified by the `duration` setting of this component. - * @param string $category The type of cool down for this item. All items with a cool down component with the same category are put on cool down when one is used - * @param float $duration The duration of time (in seconds) items with a matching category will spend cooling down before becoming usable again + * The duration of time (in seconds) items with a matching category will spend cooling down before becoming usable again. + * @param string $category All items with the same "category" are put on cooldown when one is used. + * @param float $duration How long the item is on cooldown before being able to be used again. + * @param string $type The type of cooldown (e.g., "use", "attack"). Default is "use". */ - public function __construct(string $category, float $duration) { + public function __construct(string $category, float $duration, string $type = self::TYPE_USE) { $this->category = $category; $this->duration = $duration; + $this->type = $type; } public function getName(): string { - return "minecraft:cooldown"; + return 'minecraft:cooldown'; } public function getValue(): array { return [ "category" => $this->category, - "duration" => $this->duration + "duration" => $this->duration, + "type" => $this->type, ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/DamageAbsorptionComponent.php b/src/item/component/DamageAbsorptionComponent.php index 0acd6270..41e68715 100644 --- a/src/item/component/DamageAbsorptionComponent.php +++ b/src/item/component/DamageAbsorptionComponent.php @@ -3,30 +3,60 @@ namespace customiesdevs\customies\item\component; +use customiesdevs\customies\item\properties\DamageCause; + final class DamageAbsorptionComponent implements ItemComponent { - private array $absorbableCauses; + /** + * List of damage causes that can be absorbed by the item. + * @var DamageCause[] + */ + private array $absorbableCauses = []; /** - * Causes the item to absorb damage that would otherwise be dealt to its wearer. - * For this to happen, the item needs to have the durability component and be equipped in an armor slot. - * @param array $absorbableCauses List of damage causes (such as entity_attack and magma) that can be absorbed by the item. + * It allows an item to absorb damage that would otherwise be dealt to its wearer. + * For this to happen, the item needs to be equipped in an armor slot. + * The absorbed damage reduces the item's durability, with any excess damage being ignored. + * Because of this, the item also needs a `minecraft:durability` component. + * @param array $absorbableCauses List of damage causes that can be absorbed by the item. By default, no damage cause is absorbed. */ - public function __construct(array $absorbableCauses) { - $this->absorbableCauses = $absorbableCauses; + public function __construct(array $absorbableCauses = []) { + foreach($absorbableCauses as $cause){ + $this->addCause($cause); + } } public function getName(): string { - return "minecraft:damage_absorption"; + return 'minecraft:damage_absorption'; } public function getValue(): array { return [ - "absorbable_causes" => $this->absorbableCauses + 'absorbable_causes' => array_map( + static fn (DamageCause $cause) => $cause->value, + $this->absorbableCauses + ) ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; + } + + /** + * Adds a damage cause to the absorbable list. + * @param DamageCause|string $cause + */ + public function addCause(DamageCause|string $cause): self { + if(is_string($cause)){ + $cause = DamageCause::tryFrom($cause); + if($cause === null){ + throw new \InvalidArgumentException("Invalid damage cause: {$cause}"); + } + } + if(!in_array($cause, $this->absorbableCauses, true)){ + $this->absorbableCauses[] = $cause; + } + return $this; } } \ No newline at end of file diff --git a/src/item/component/DamageComponent.php b/src/item/component/DamageComponent.php index 95a22a6e..1038f7d8 100644 --- a/src/item/component/DamageComponent.php +++ b/src/item/component/DamageComponent.php @@ -3,27 +3,36 @@ namespace customiesdevs\customies\item\component; +use pocketmine\nbt\tag\ByteTag; + final class DamageComponent implements ItemComponent { private int $damage; /** - * Determines how much extra damage an item does on attack. Note that this must be a positive value. - * @param int $damage Should be a Intger above `0` + * Determines how much extra damage the item does on attack. + * Note that this must be a positive value. + * @param int $damage Specifies how much extra damage the item does, must be a positive number. + * @throws \InvalidArgumentException if the damage value is negative. */ public function __construct(int $damage) { + if($damage < 0) { + throw new \InvalidArgumentException("Damage value must be a positive number, $damage given"); + } $this->damage = $damage; } public function getName(): string { - return "damage"; + return 'minecraft:damage'; } - public function getValue(): int { - return $this->damage; + public function getValue(): array { + return [ + "value" => new ByteTag($this->damage) + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['damage' => $this->damage]; } } \ No newline at end of file diff --git a/src/item/component/DiggerComponent.php b/src/item/component/DiggerComponent.php index 2f2e761c..f59ba488 100644 --- a/src/item/component/DiggerComponent.php +++ b/src/item/component/DiggerComponent.php @@ -10,19 +10,36 @@ final class DiggerComponent implements ItemComponent { - private array $destroySpeeds; + /** + * @var array + */ + private array $destroySpeeds = []; private bool $useEfficiency; /** * Allows a creator to determine how quickly an item can dig specific blocks. - * @param bool $useEfficiency Determines whether the item should be impacted if the `efficiency` enchant is applied to it. + * @param bool $useEfficiency Determines whether the item should be impacted by the Efficiency enchantment. + * @param array $destroySpeeds An array of blocks/tags and their corresponding digging speeds. */ - public function __construct(bool $useEfficiency) { + public function __construct(bool $useEfficiency = false, array $destroySpeeds = []) { $this->useEfficiency = $useEfficiency; + $this->destroySpeeds = $destroySpeeds; } public function getName(): string { - return "minecraft:digger"; + return 'minecraft:digger'; } public function getValue(): array { @@ -32,16 +49,16 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } - /** - * Add blocks to the `destroy_speeds` array in the required format. + /** + * Adds blocks to the `destroy_speeds` array with a specified speed. * @param int $speed Digging speed for the correlating block(s) - * @param Block ...$blocks A list of blocks to dig with correlating speeds of digging + * @param Block ...$blocks A list of blocks to dig with the given speed */ - public function withBlocks(int $speed, Block ...$blocks): DiggerComponent { + public function withBlocks(int $speed, Block ...$blocks): self { foreach($blocks as $block){ $this->destroySpeeds[] = [ "block" => [ @@ -54,18 +71,43 @@ public function withBlocks(int $speed, Block ...$blocks): DiggerComponent { } /** - * Add blocks to the `destroy_speeds` array in the required format. - * @param int $speed Digging speed for the correlating block(s) - * @param string ...$tags A list of blocks to dig with correlating speeds of digging + * Adds block tags to the `destroy_speeds` array with a specified speed. + * @param int $speed Digging speed for the correlating blocks + * @param string ...$tags A list of block tags */ - public function withTags(int $speed, string ...$tags): DiggerComponent { - $query = implode(",", array_map(fn($tag) => "'" . $tag . "'", $tags)); + public function withTags(int $speed, string ...$tags): self { + $query = implode(", ", array_map(fn(string $tag) => "'" . $tag . "'", $tags)); $this->destroySpeeds[] = [ "block" => [ - "tags" => "query.any_tag(" . $query . ")" + "tags" => "query.any_tag($query)" ], "speed" => $speed ]; return $this; } + + /** @todo */ + private function withStates(int $speed, array ...$states): self { + foreach($states as $state){ + $stateArray = []; + foreach($state as $key => $value){ + $stateArray[$key] = $value; + } + $this->destroySpeeds[] = [ + "block" => [ + "states" => $stateArray, + ], + "speed" => $speed + ]; + } + return $this; + } + + /** + * Returns the array of destroy speeds. + * @return array The destroy speeds array. + */ + public function getDestroySpeeds(): array { + return $this->destroySpeeds; + } } \ No newline at end of file diff --git a/src/item/component/DisplayNameComponent.php b/src/item/component/DisplayNameComponent.php index 28ad340b..4236190c 100644 --- a/src/item/component/DisplayNameComponent.php +++ b/src/item/component/DisplayNameComponent.php @@ -17,7 +17,7 @@ public function __construct(string $name) { } public function getName(): string { - return "minecraft:display_name"; + return 'minecraft:display_name'; } public function getValue(): array { @@ -26,7 +26,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/DurabilityComponent.php b/src/item/component/DurabilityComponent.php index 3cc9847e..a71e28f8 100644 --- a/src/item/component/DurabilityComponent.php +++ b/src/item/component/DurabilityComponent.php @@ -14,15 +14,24 @@ final class DurabilityComponent implements ItemComponent { * @param int $maxDurability Max durability is the amount of damage that this item can take before breaking * @param int $minDamageChance Specifies the percentage minimum chance for durability to take damage. Range: [0, 100]. Default is set to `100` * @param int $maxDamageChance Specifies the percentage maximum chance for durability to take damage. Range: [0, 100]. Default is set to `100` + * @throws \InvalidArgumentException if the min or max damage chance is not between 0 and 100, or if minDamageChance is greater than maxDamageChance, or if maxDurability is not between 0 and 2147483647. */ public function __construct(int $maxDurability, int $minDamageChance = 100, int $maxDamageChance = 100) { + self::validate($minDamageChance, 'Minimum'); + self::validate($maxDamageChance, 'Maximum'); + if($minDamageChance > $maxDamageChance){ + throw new \InvalidArgumentException("Minimum damage chance ($minDamageChance) cannot be greater than maximum damage chance ($maxDamageChance)"); + } + if($maxDurability < 0 || $maxDurability > 2147483647){ + throw new \InvalidArgumentException("Max durability must be between 0 and 2147483647, $maxDurability given"); + } $this->maxDurability = $maxDurability; $this->minDamageChance = $minDamageChance; $this->maxDamageChance = $maxDamageChance; } public function getName(): string { - return "minecraft:durability"; + return 'minecraft:durability'; } public function getValue(): array { @@ -35,7 +44,13 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; + } + + private static function validate(int $value, string $type): void { + if($value < 0 || $value > 100){ + throw new \InvalidArgumentException("$type damage chance must be between 0 and 100, $value given"); + } } } \ No newline at end of file diff --git a/src/item/component/DurabilitySensorComponent.php b/src/item/component/DurabilitySensorComponent.php new file mode 100644 index 00000000..ae26f30d --- /dev/null +++ b/src/item/component/DurabilitySensorComponent.php @@ -0,0 +1,90 @@ + + */ + private array $durabilityThresholds = []; + + /** + * Enables an item to emit effects when it receives damage. Because of this, the item also needs a `minecraft:durability` component. + * @param array $durabilityThresholds + */ + public function __construct(array $durabilityThresholds = []) { + if(isset($durabilityThresholds['durability'])){ + $durabilityThresholds = [$durabilityThresholds]; + } + foreach($durabilityThresholds as $threshold){ + if(!is_array($threshold)){ + throw new \InvalidArgumentException("Durability threshold must be an array"); + } + $this->addDurabilityThreshold( + $threshold['durability'], + $threshold['particle_type'] ?? null, + $threshold['sound_event'] ?? null + ); + } + } + + public function getName(): string { + return 'minecraft:durability_sensor'; + } + + public function getValue(): array { + return [ + "durability_thresholds" => array_map( + fn(array $threshold) => [ + "durability" => $threshold["durability"], + "particle_type" => $threshold["particle_type"], + "sound_event" => $threshold["sound_event"] + ], + $this->durabilityThresholds + ) + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } + + /** + * Adds a durability threshold that triggers effects when the item's durability reaches the specified value. + * @param int $durability The durability threshold at which the effects are triggered. Must be >= 0. + * @param ParticleType|null $particleType The type of particle effect to emit when the threshold is reached. If null, no particle effect is emitted. + * @param SoundEvent|null $soundEvent The sound event to play when the threshold is reached. If null, no sound is played. + * @return $this + */ + public function addDurabilityThreshold( + int $durability, + ?ParticleType $particleType = null, + ?SoundEvent $soundEvent = null + ): self { + if($durability < 0){ + throw new \InvalidArgumentException("Durability threshold must be >= 0, $durability given"); + } + if($particleType === null && $soundEvent === null){ + throw new \InvalidArgumentException("At least one of particle_type or sound_event must be specified"); + } + $this->durabilityThresholds[] = [ + "durability" => $durability, + "particle_type" => $particleType->value, + "sound_event" => $soundEvent->value + ]; + return $this; + } +} \ No newline at end of file diff --git a/src/item/component/DyeableComponent.php b/src/item/component/DyeableComponent.php index 563c0ec8..9217b622 100644 --- a/src/item/component/DyeableComponent.php +++ b/src/item/component/DyeableComponent.php @@ -5,27 +5,49 @@ final class DyeableComponent implements ItemComponent { - private string $hex; + /** + * RGB color values as an array of three integers [R, G, B]. + * @var int[] + */ + private array $rgb = []; /** * Allows the item to be dyed by cauldron water. Once dyed, the item will display the `dyed` texture defined in the `minecraft:icon` component rather than `default`. - * @param string $hex The hex color code (e.g "#47ff5a") + * @param string $hex Hex color code (e.g. "#175882") + * @throws \InvalidArgumentException If the hex code is invalid */ public function __construct(string $hex) { - $this->hex = $hex; + $this->rgb = self::hexToRgb($hex); } public function getName(): string { - return "minecraft:dyeable"; + return 'minecraft:dyeable'; } public function getValue(): array { return [ - "default_color" => $this->hex + "default_color" => $this->rgb ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; + } + + /** + * Converts a hex color string to an RGB array. + * + * @param string $hex Hex color string (e.g. "#175882") + * @return int[] Array of RGB values [R, G, B] + * @throws \InvalidArgumentException If the hex code is invalid + */ + private static function hexToRgb(string $hex): array { + $hex = ltrim($hex, '#'); + if(strlen($hex) !== 6) throw new \InvalidArgumentException("Invalid hex color: {$hex}"); + return [ + hexdec(substr($hex, 0, 2)), + hexdec(substr($hex, 2, 2)), + hexdec(substr($hex, 4, 2)) + ]; } } \ No newline at end of file diff --git a/src/item/component/EnchantableComponent.php b/src/item/component/EnchantableComponent.php new file mode 100644 index 00000000..f3433404 --- /dev/null +++ b/src/item/component/EnchantableComponent.php @@ -0,0 +1,89 @@ + 32767){ + throw new \InvalidArgumentException("Enchantable value must be between 0 and 32767, $value given"); + } + $this->slot = $slot; + $this->value = $value; + } + + public function getName(): string { + return 'minecraft:enchantable'; + } + + public function getValue(): array { + return [ + "slot" => $this->slot, + "value" => new ByteTag($this->value) + ]; + } + + public function getPropertyMapping(): ?array { + return ['enchantable_slot' => $this->slot, 'enchantable_value' => $this->value]; + } +} \ No newline at end of file diff --git a/src/item/component/EnchantableSlotComponent.php b/src/item/component/EnchantableSlotComponent.php deleted file mode 100644 index f97b520b..00000000 --- a/src/item/component/EnchantableSlotComponent.php +++ /dev/null @@ -1,48 +0,0 @@ -slot = $slot; - } - - public function getName(): string { - return "enchantable_slot"; - } - - public function getValue(): string { - return $this->slot; - } - - public function isProperty(): bool { - return true; - } -} \ No newline at end of file diff --git a/src/item/component/EnchantableValueComponent.php b/src/item/component/EnchantableValueComponent.php deleted file mode 100644 index 80963464..00000000 --- a/src/item/component/EnchantableValueComponent.php +++ /dev/null @@ -1,47 +0,0 @@ -value = $value; - } - - public function getName(): string { - return "enchantable_value"; - } - - public function getValue(): int { - return $this->value; - } - - public function isProperty(): bool { - return true; - } -} \ No newline at end of file diff --git a/src/item/component/FireResistantComponent.php b/src/item/component/FireResistantComponent.php new file mode 100644 index 00000000..031e32d9 --- /dev/null +++ b/src/item/component/FireResistantComponent.php @@ -0,0 +1,31 @@ +fireResistant = $fireResistant; + } + + public function getName(): string { + return 'minecraft:fire_resistant'; + } + + public function getValue(): array { + return [ + "value" => $this->fireResistant + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/FoodComponent.php b/src/item/component/FoodComponent.php index a06a855b..b3decc11 100644 --- a/src/item/component/FoodComponent.php +++ b/src/item/component/FoodComponent.php @@ -3,21 +3,69 @@ namespace customiesdevs\customies\item\component; +use customiesdevs\customies\item\properties\EffectType; + final class FoodComponent implements ItemComponent { + /** Default eating behavior */ + public const USE_ACTION_NORMAL = -1; + /** Chorus fruit teleport */ + public const USE_ACTION_CHORUS_TELEPORT = 0; + /** Suspicious stew effect handler */ + public const USE_ACTION_SUSPICIOUS_STEW_EFFECT = 1; + + public const MODIFIER_POOR = 0.1; + public const MODIFIER_LOW = 0.3; + public const MODIFIER_NORMAL = 0.6; + public const MODIFIER_GOOD = 0.8; + public const MODIFIER_SUPERNATURAL = 1.2; + + /** No cooldown type */ + public const COOLDOWN_DEFAULT = ""; + /** Chorus fruit cooldown type (forces 20 ticks) */ + public const COOLDOWN_CHORUSFRUIT = "chorusfruit"; + private bool $canAlwaysEat; private int $nutrition; private float $saturationModifier; private string $usingConvertsTo; + private ?int $cooldownTime = null; + private ?string $cooldownType = null; + private ?int $onUseAction = null; + /** @var array{0: float, 1: float, 2: float}|null */ + private ?array $onUseRange = null; + /** + * Potion effects applied when the food is eaten. + * + * @var array + */ + private array $effects = []; + /** + * List of potion effect IDs to remove when the food is consumed. + * @var int[] + */ + private array $removeEffects = []; /** * Sets the item as a food component, allowing it to be edible to the player. - * @param bool $canAlwaysEat If true you can always eat this item (even when not hungry) - * @param int $nutrition Value that is added to the entity's nutrition when the item is used - * @param float $saturationModifier - * @param string $usingConvertsTo When used, converts to the item specified by the string in this field. Default does not convert item + * @param bool $canAlwaysEat Whether the player can always eat this food, even when not hungry. Default is false. + * @param int $nutrition The amount of hunger points this food item restores when eaten. Default is 0. + * @param float $saturationModifier The saturation modifier for this food item. Default is `MODIFIER_NORMAL` (0.6). + * @param string $usingConvertsTo The item this food converts to after being consumed. Default is an empty string. */ - public function __construct(bool $canAlwaysEat = false, int $nutrition = 0, float $saturationModifier = 0.6, string $usingConvertsTo = "") { + public function __construct( + bool $canAlwaysEat = false, + int $nutrition = 0, + float $saturationModifier = self::MODIFIER_NORMAL, + string $usingConvertsTo = "" + ) { $this->canAlwaysEat = $canAlwaysEat; $this->nutrition = $nutrition; $this->saturationModifier = $saturationModifier; @@ -25,21 +73,116 @@ public function __construct(bool $canAlwaysEat = false, int $nutrition = 0, floa } public function getName(): string { - return "minecraft:food"; + return 'minecraft:food'; } public function getValue(): array { - return [ + $data = [ "can_always_eat" => $this->canAlwaysEat, "nutrition" => $this->nutrition, "saturation_modifier" => $this->saturationModifier, - "using_converts_to" => [ - "name" => $this->usingConvertsTo - ] + "using_converts_to" => $this->usingConvertsTo ]; + if($this->cooldownTime !== null){ + $data['cooldown_time'] = $this->cooldownTime; + $data['cooldown_type'] = $this->cooldownType ?? self::COOLDOWN_DEFAULT; + } + if($this->onUseAction !== null){ + $data["on_use_action"] = $this->onUseAction; + $data["on_use_range"] = $this->onUseRange ?? [8.0, 8.0, 8.0]; + } + if($this->effects !== []){ + $data["effects"] = $this->effects; + } + if($this->removeEffects !== []){ + $data['remove_effects'] = $this->removeEffects; + } + return $data; + } + + public function getPropertyMapping(): ?array { + return null; } - public function isProperty(): bool { - return false; + /** + * Sets a cooldown after eating. + * If `COOLDOWN_CHORUSFRUIT` is used, cooldown time is forced to 20 ticks. + * @param int $time Cooldown duration in ticks + * @param string $type Cooldown type + */ + public function addCooldown(int $time = 0, string $type = self::COOLDOWN_DEFAULT): self { + if($time < 0){ + throw new \InvalidArgumentException("Cooldown time must be >= 0"); + } + $this->cooldownType = $type; + $this->cooldownTime = ($type === self::COOLDOWN_CHORUSFRUIT) ? 20 : $time; + return $this; + } + + /** + * Defines an on-use event action. + * @param int $action One of the USE_ACTION_* constants + * @param float $x Effect range X + * @param float $y Effect range Y + * @param float $z Effect range Z + */ + public function onUseEvent( + int $action = self::USE_ACTION_NORMAL, + float $x = 8.0, + float $y = 8.0, + float $z = 8.0 + ): self { + if(!in_array($action, [ + self::USE_ACTION_NORMAL, + self::USE_ACTION_CHORUS_TELEPORT, + self::USE_ACTION_SUSPICIOUS_STEW_EFFECT + ], true)){ + throw new \InvalidArgumentException("Invalid on_use_action: $action"); + } + $this->onUseAction = $action; + $this->onUseRange = [$x, $y, $z]; + return $this; + } + + /** + * Adds a potion effect applied when the food is consumed. + * @param EffectType $effect Potion effect type + * @param int $duration Effect duration in seconds + * @param int $amplifier Effect strength (0 = level I) + * @param float $chance Chance to apply the effect) + */ + public function addEffect( + EffectType $effect, + int $duration, + int $amplifier = 0, + float $chance = 1.0 + ): self { + if($duration <= 0){ + throw new \InvalidArgumentException("Effect duration must be > 0"); + } + if($amplifier < 0){ + throw new \InvalidArgumentException("Effect amplifier must be >= 0"); + } + $this->effects[] = [ + "name" => $effect->getName(), + "id" => $effect->getId(), + "descriptionId" => $effect->getDescriptionId(), + "duration" => $duration, + "amplifier" => $amplifier, + "chance" => $chance + ]; + return $this; + } + + /** + * Removes one or more potion effects when the food is consumed. + * @param EffectType ...$effects Effects to remove + */ + public function removeEffects(EffectType ...$effects): self { + foreach($effects as $effect){ + $this->removeEffects[] = $effect->getId(); + } + $this->removeEffects = array_values(array_unique($this->removeEffects)); + return $this; } } \ No newline at end of file diff --git a/src/item/component/FuelComponent.php b/src/item/component/FuelComponent.php index f86e8554..8b40cd5b 100644 --- a/src/item/component/FuelComponent.php +++ b/src/item/component/FuelComponent.php @@ -9,14 +9,18 @@ final class FuelComponent implements ItemComponent { /** * Allows this item to be used as fuel in a furnace to 'cook' other items. - * @param float $duration Amount of time, in seconds, this fuel will cook items + * @param float $duration Amount of time, in seconds, this fuel will cook items. + * @throws \InvalidArgumentException if the fuel duration is less than 0.05 */ public function __construct(float $duration) { + if($duration < 0.05){ + throw new \InvalidArgumentException("Fuel duration must be at least 0.05 seconds, $duration given"); + } $this->duration = $duration; } public function getName(): string { - return "minecraft:fuel"; + return 'minecraft:fuel'; } public function getValue(): array { @@ -25,7 +29,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/GlintComponent.php b/src/item/component/GlintComponent.php index 5a9b2c19..e766a5c0 100644 --- a/src/item/component/GlintComponent.php +++ b/src/item/component/GlintComponent.php @@ -16,14 +16,16 @@ public function __construct(bool $glint = true) { } public function getName(): string { - return "foil"; + return 'minecraft:glint'; } - public function getValue(): bool { - return $this->glint; + public function getValue(): array { + return [ + "value" => $this->glint + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['foil' => $this->glint]; } } \ No newline at end of file diff --git a/src/item/component/HandEquippedComponent.php b/src/item/component/HandEquippedComponent.php index dad02413..b67574b5 100644 --- a/src/item/component/HandEquippedComponent.php +++ b/src/item/component/HandEquippedComponent.php @@ -16,14 +16,16 @@ public function __construct(bool $handEquipped = true) { } public function getName(): string { - return "hand_equipped"; + return 'minecraft:hand_equipped'; } - public function getValue(): bool { - return $this->handEquipped; + public function getValue(): array { + return [ + "value" => $this->handEquipped + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['hand_equipped' => $this->handEquipped]; } } \ No newline at end of file diff --git a/src/item/component/HoverTextColorComponent.php b/src/item/component/HoverTextColorComponent.php index 2947145f..0761ed2f 100644 --- a/src/item/component/HoverTextColorComponent.php +++ b/src/item/component/HoverTextColorComponent.php @@ -17,14 +17,16 @@ public function __construct(string $hoverTextColor) { } public function getName(): string { - return "hover_text_color"; + return 'minecraft:hover_text_color'; } - public function getValue(): string { - return $this->hoverTextColor; + public function getValue(): array { + return [ + "value" => $this->hoverTextColor + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['hover_text_color' => $this->hoverTextColor]; } } \ No newline at end of file diff --git a/src/item/component/IconComponent.php b/src/item/component/IconComponent.php index dd92a950..a28f5bb3 100644 --- a/src/item/component/IconComponent.php +++ b/src/item/component/IconComponent.php @@ -8,35 +8,45 @@ final class IconComponent implements ItemComponent { private string $default_texture; private string $dyed_texture; private string $trim_texture; + private string $bundle_open_back_texture; + private string $bundle_open_front_texture; /** * Determines the icon to represent the item in the UI and elsewhere. * @param string $default_texture the texture name should same as the `resource_pack/textures/item_texture.json` `texture_data` * @param string $dyed_texture Default is set to `None` * @param string $trim_texture Default is set to `None` + * @param string $bundle_open_back_texture Default is set to `None` + * @param string $bundle_open_front_texture Default is set to `None` */ - public function __construct(string $default_texture, string $dyed_texture = "", string $trim_texture = "") { + public function __construct( + string $default_texture, + string $dyed_texture = "", + string $trim_texture = "", + string $bundle_open_back_texture = "", + string $bundle_open_front_texture = "" + ) { $this->default_texture = $default_texture; $this->dyed_texture = $dyed_texture; $this->trim_texture = $trim_texture; + $this->bundle_open_back_texture = $bundle_open_back_texture; + $this->bundle_open_front_texture = $bundle_open_front_texture; } public function getName(): string { - return "minecraft:icon"; + return 'minecraft:icon'; } public function getValue(): array { - return [ - "texture" => $this->default_texture, - "textures" => [ - "default" => $this->default_texture, - "dyed" => $this->dyed_texture, - "icon_trim" => $this->trim_texture - ] - ]; + $textures = ["default" => $this->default_texture]; + if($this->dyed_texture !== "") $textures["dyed"] = $this->dyed_texture; + if($this->trim_texture !== "") $textures["icon_trim"] = $this->trim_texture; + if($this->bundle_open_back_texture !== "") $textures["bundle_open_back"] = $this->bundle_open_back_texture; + if($this->bundle_open_front_texture !== "") $textures["bundle_open_front"] = $this->bundle_open_front_texture; + return ["textures" => $textures]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/InteractButtonComponent.php b/src/item/component/InteractButtonComponent.php index fa24a019..3e335acf 100644 --- a/src/item/component/InteractButtonComponent.php +++ b/src/item/component/InteractButtonComponent.php @@ -5,7 +5,7 @@ final class InteractButtonComponent implements ItemComponent { - private bool|string $interactButton; + private string $interactButton; /** * Ineract Button is a boolean or string that determines if the interact button is shown in touch controls, and what text is displayed on the button. When set to 'true', the default 'Use Item' text will be used. @@ -20,17 +20,17 @@ public function __construct(bool|string $interactButton) { } public function getName(): string { - return "minecraft:interact_button"; + return 'minecraft:interact_button'; } public function getValue(): array { return [ - "interact_text" => (string) $this->interactButton, - "requires_interact" => 1 + "interact_text" => $this->interactButton, + "requires_interact" => true ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/ItemComponent.php b/src/item/component/ItemComponent.php index 4f50fa56..3b2512cc 100644 --- a/src/item/component/ItemComponent.php +++ b/src/item/component/ItemComponent.php @@ -5,9 +5,21 @@ interface ItemComponent { + /** + * The component identifier, e.g. "minecraft:display_name" + * @return string + */ public function getName(): string; + /** + * The value of this component, as it would appear in an item JSON. + * @return mixed + */ public function getValue(): mixed; - public function isProperty(): bool; + /** + * Returns property mappings if this component maps to item_properties. + * @return array|null [propertyName => propertyValue] or null if not a property + */ + public function getPropertyMapping(): ?array; } \ No newline at end of file diff --git a/src/item/component/KineticWeaponComponent.php b/src/item/component/KineticWeaponComponent.php new file mode 100644 index 00000000..d9ddfb1c --- /dev/null +++ b/src/item/component/KineticWeaponComponent.php @@ -0,0 +1,126 @@ + 2.0, 'max' => 7.5], + float $damageModifier = 0.0, + float $damageMultiplier = 0.7, + int $delay = 15, + array $damageConditions = [ + 'min_speed' => 0.0, + 'min_relative_speed' => 4.6, + 'max_duration' => 200 + ], + array $dismountConditions = [ + 'min_speed' => 14.0, + 'min_relative_speed' => 0.0, + 'max_duration' => 100 + ], + float $hitboxMargin = 0.25, + array $knockbackConditions = [ + 'min_speed' => 5.1, + 'min_relative_speed' => 0.0, + 'max_duration' => 120 + ], + array $reach = ['min' => 2.0, 'max' => 4.5] + ) { + $this->creativeReach = self::validateRange($creativeReach, 'creative_reach'); + $this->reach = self::validateRange($reach, 'reach'); + $this->damageModifier = $damageModifier; + $this->damageMultiplier = $damageMultiplier; + $this->delay = $delay; + $this->damageConditions = self::validateConditions($damageConditions, 'damage_conditions'); + $this->dismountConditions = self::validateConditions($dismountConditions, 'dismount_conditions'); + $this->knockbackConditions = self::validateConditions($knockbackConditions, 'knockback_conditions'); + $this->hitboxMargin = $hitboxMargin; + } + public function getName(): string { + return 'minecraft:kinetic_weapon'; + } + + public function getValue(): array { + return [ + "minecraft:kinetic_weapon" => [ + "creative_reach" => self::rangeToArray($this->creativeReach), + "damage_conditions" => self::conditionsToArray($this->damageConditions), + "damage_modifier" => $this->damageModifier, + "damage_multiplier" => $this->damageMultiplier, + "delay" => new ShortTag($this->delay), + "dismount_conditions" => self::conditionsToArray($this->dismountConditions), + "hitbox_margin" => $this->hitboxMargin, + "knockback_conditions" => self::conditionsToArray($this->knockbackConditions), + "reach" => self::rangeToArray($this->reach), + ] + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } + + private static function validateRange(array $range, string $name): array { + if(!isset($range['min'], $range['max'])){ + throw new \InvalidArgumentException("$name must contain min and max"); + } + return [ + 'min' => (float) $range['min'], + 'max' => (float) $range['max'] + ]; + } + + private static function validateConditions(array $conditions, string $name): array { + foreach(['min_speed', 'min_relative_speed', 'max_duration'] as $key){ + if(!array_key_exists($key, $conditions)){ + throw new \InvalidArgumentException("$name missing $key"); + } + } + return [ + 'min_speed' => (float) $conditions['min_speed'], + 'min_relative_speed' => (float) $conditions['min_relative_speed'], + 'max_duration' => (int) $conditions['max_duration'] + ]; + } + + private static function rangeToArray(array $range): array { + return [ + "min" => $range['min'], + "max" => $range['max'] + ]; + } + + private static function conditionsToArray(array $conditions): array { + return [ + "min_speed" => $conditions['min_speed'], + "min_relative_speed" => $conditions['min_relative_speed'], + "max_duration" => new ShortTag($conditions['max_duration']) + ]; + } +} \ No newline at end of file diff --git a/src/item/component/LiquidClippedComponent.php b/src/item/component/LiquidClippedComponent.php index f80160d7..914be38d 100644 --- a/src/item/component/LiquidClippedComponent.php +++ b/src/item/component/LiquidClippedComponent.php @@ -16,14 +16,16 @@ public function __construct(bool $liquidClipped = true) { } public function getName(): string { - return "liquid_clipped"; + return 'minecraft:liquid_clipped'; } - public function getValue(): bool { - return $this->liquidClipped; + public function getValue(): array { + return [ + "value" => $this->liquidClipped + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['liquid_clipped' => $this->liquidClipped]; } } \ No newline at end of file diff --git a/src/item/component/MaxStackSizeComponent.php b/src/item/component/MaxStackSizeComponent.php index 4d2327a1..335fd485 100644 --- a/src/item/component/MaxStackSizeComponent.php +++ b/src/item/component/MaxStackSizeComponent.php @@ -3,6 +3,8 @@ namespace customiesdevs\customies\item\component; +use pocketmine\nbt\tag\ByteTag; + final class MaxStackSizeComponent implements ItemComponent { private int $maxStackSize; @@ -12,18 +14,23 @@ final class MaxStackSizeComponent implements ItemComponent { * @param int $maxStackSize Max Size, Default is set to `64` */ public function __construct(int $maxStackSize = 64) { + if($maxStackSize < 1 || $maxStackSize > 64) { + throw new \InvalidArgumentException("Max stack size must be between 1 and 64, $maxStackSize given."); + } $this->maxStackSize = $maxStackSize; } public function getName(): string { - return "max_stack_size"; + return 'minecraft:max_stack_size'; } - public function getValue(): int { - return $this->maxStackSize; + public function getValue(): array { + return [ + "value" => new ByteTag($this->maxStackSize) + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['max_stack_size' => (int) $this->maxStackSize]; } } \ No newline at end of file diff --git a/src/item/component/PiercingWeaponComponent.php b/src/item/component/PiercingWeaponComponent.php new file mode 100644 index 00000000..6e109141 --- /dev/null +++ b/src/item/component/PiercingWeaponComponent.php @@ -0,0 +1,54 @@ + 2.0, 'max' => 7.5], + float $hitboxMargin = 0.25, + array $reach = ['min' => 2.0, 'max' => 4.5] + ) { + $this->creativeReach = self::validateRange($creativeReach, 'creative_reach'); + $this->reach = self::validateRange($reach, 'reach'); + $this->hitboxMargin = $hitboxMargin; + } + + public function getName(): string { + return 'minecraft:piercing_weapon'; + } + + public function getValue(): array { + return [ + "creative_reach" => self::rangeToArray($this->creativeReach), + "hitbox_margin" => $this->hitboxMargin, + "reach" => self::rangeToArray($this->reach) + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } + + private static function validateRange(array $range, string $name): array { + if(!isset($range['min'], $range['max'])){ + throw new \InvalidArgumentException("$name must contain min and max values"); + } + return [ + 'min' => (float) $range['min'], + 'max' => (float) $range['max'] + ]; + } + + private static function rangeToArray(array $range): array { + return [ + "min" => $range['min'], + "max" => $range['max'] + ]; + } +} \ No newline at end of file diff --git a/src/item/component/ProjectileComponent.php b/src/item/component/ProjectileComponent.php index 215ad273..2e371bb0 100644 --- a/src/item/component/ProjectileComponent.php +++ b/src/item/component/ProjectileComponent.php @@ -5,6 +5,9 @@ final class ProjectileComponent implements ItemComponent { + public const ENTITY_ARROW = "minecraft:arrow<>"; + public const ENTITY_WINDCHARGE = "minecraft:wind_charge_projectile<>"; + private float $minimumCriticalPower; private string $projectileEntity; @@ -15,13 +18,13 @@ final class ProjectileComponent implements ItemComponent { * @param float $minimumCriticalPower Specifies how long a player must charge a projectile for it to critically hit * @param string $projectileEntity Which entity is to be fired as a projectile */ - public function __construct(float $minimumCriticalPower, string $projectileEntity) { + public function __construct(float $minimumCriticalPower = 0, string $projectileEntity) { $this->minimumCriticalPower = $minimumCriticalPower; $this->projectileEntity = $projectileEntity; } public function getName(): string { - return "minecraft:projectile"; + return 'minecraft:projectile'; } public function getValue(): array { @@ -31,7 +34,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/RarityComponent.php b/src/item/component/RarityComponent.php index 7ee3a185..f5bfb27f 100644 --- a/src/item/component/RarityComponent.php +++ b/src/item/component/RarityComponent.php @@ -22,7 +22,7 @@ public function __construct(string $rarity = self::COMMON) { } public function getName(): string { - return "minecraft:rarity"; + return 'minecraft:rarity'; } public function getValue(): array { @@ -31,7 +31,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/RecordComponent.php b/src/item/component/RecordComponent.php index 7728d9d4..3795f427 100644 --- a/src/item/component/RecordComponent.php +++ b/src/item/component/RecordComponent.php @@ -22,7 +22,7 @@ public function __construct(int $comparatorSignal, float $duration, string $soun } public function getName(): string { - return "minecraft:record"; + return 'minecraft:record'; } public function getValue(): array { @@ -33,7 +33,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/RepairableComponent.php b/src/item/component/RepairableComponent.php new file mode 100644 index 00000000..89bf1282 --- /dev/null +++ b/src/item/component/RepairableComponent.php @@ -0,0 +1,35 @@ +repairItems as $repairItem) { + $repairItems[] = $repairItem->toArray(); + } + return [ + "repair_items" => $repairItems + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/ShooterComponent.php b/src/item/component/ShooterComponent.php index 14f0e813..3fd0bf42 100644 --- a/src/item/component/ShooterComponent.php +++ b/src/item/component/ShooterComponent.php @@ -25,7 +25,15 @@ final class ShooterComponent implements ItemComponent { * @param float $maxDrawDuration Draw Duration. Default is set to 0 * @param bool $scalePowerByDrawDuration Scale power by draw duration? Default is set to false */ - public function __construct(string $item, bool $useOffhand = false, bool $searchInventory = false, bool $useInCreative = false, bool $chargeOnDraw = false, float $maxDrawDuration = 0.0, bool $scalePowerByDrawDuration = false) { + public function __construct( + string $item, + bool $useOffhand = false, + bool $searchInventory = false, + bool $useInCreative = false, + bool $chargeOnDraw = false, + float $maxDrawDuration = 0.0, + bool $scalePowerByDrawDuration = false + ) { $this->item = $item; $this->useOffhand = $useOffhand; $this->searchInventory = $searchInventory; @@ -36,14 +44,16 @@ public function __construct(string $item, bool $useOffhand = false, bool $search } public function getName(): string { - return "minecraft:shooter"; + return 'minecraft:shooter'; } public function getValue(): array { return [ "ammunition" => [ [ - "item" => $this->item, + "item" => [ + "name" => $this->item + ], "use_offhand" => $this->useOffhand, "search_inventory" => $this->searchInventory, "use_in_creative" => $this->useInCreative @@ -55,7 +65,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/ShouldDespawnComponent.php b/src/item/component/ShouldDespawnComponent.php index 126f8d54..1ff046c0 100644 --- a/src/item/component/ShouldDespawnComponent.php +++ b/src/item/component/ShouldDespawnComponent.php @@ -16,14 +16,16 @@ public function __construct(bool $shouldDespawn = true) { } public function getName(): string { - return "should_despawn"; + return 'minecraft:should_despawn'; } - public function getValue(): bool { - return $this->shouldDespawn; + public function getValue(): array { + return [ + "value" => $this->shouldDespawn + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['should_despawn' => $this->shouldDespawn]; } } \ No newline at end of file diff --git a/src/item/component/StackedByDataComponent.php b/src/item/component/StackedByDataComponent.php index 1eddb16f..f5888204 100644 --- a/src/item/component/StackedByDataComponent.php +++ b/src/item/component/StackedByDataComponent.php @@ -17,14 +17,16 @@ public function __construct(bool $stackedByData = true) { } public function getName(): string { - return "stacked_by_data"; + return 'minecraft:stacked_by_data'; } - public function getValue(): bool { - return $this->stackedByData; + public function getValue(): array { + return [ + "value" => $this->stackedByData + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['stacked_by_data' => $this->stackedByData]; } } \ No newline at end of file diff --git a/src/item/component/StorageItemComponent.php b/src/item/component/StorageItemComponent.php new file mode 100644 index 00000000..9d2c3cdc --- /dev/null +++ b/src/item/component/StorageItemComponent.php @@ -0,0 +1,94 @@ += 0. + */ + public function __construct( + bool $allowNestedStorageItems = true, + int $maxSlots = 64 + ) { + $this->allowNestedStorageItems = $allowNestedStorageItems; + $this->maxSlots = $maxSlots; + } + + public function getName(): string { + return 'minecraft:storage_item'; + } + + public function getValue(): array { + return [ + "allow_nested_storage_items" => $this->allowNestedStorageItems, + "allowed_items" => $this->allowedItems, + "banned_items" => $this->bannedItems, + "max_slots" => $this->maxSlots + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } + + /** + * List of items that are exclusively allowed in this Storage Item. + * If empty, all items are allowed. + * @param Item|Item[] $items + */ + public function allowItem(Item|array $items): self { + foreach($this->normalizeItems($items) as $name){ + if(!$this->containsItem($this->allowedItems, $name)){ + $this->allowedItems[] = ["name" => $name]; + } + } + return $this; + } + + /** + * List of items that are NOT allowed in this Storage Item. + * @param Item|Item[] $items + */ + public function banItem(Item|array $items): self { + foreach($this->normalizeItems($items) as $name){ + if(!$this->containsItem($this->bannedItems, $name)){ + $this->bannedItems[] = ["name" => $name]; + } + } + return $this; + } + + /** + * @return string[] + */ + private function normalizeItems(Item|array $items): array { + $items = is_array($items) ? $items : [$items]; + $names = []; + foreach($items as $item){ + if(!$item instanceof Item) continue; + $names[] = $item->nbtSerialize()->getString("Name", "unknown"); + } + return array_unique($names); + } + + private function containsItem(array $list, string $name): bool { + foreach($list as $entry){ + if(($entry["name"] ?? null) === $name){ + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/item/component/StorageWeightLimitComponent.php b/src/item/component/StorageWeightLimitComponent.php new file mode 100644 index 00000000..a3540369 --- /dev/null +++ b/src/item/component/StorageWeightLimitComponent.php @@ -0,0 +1,31 @@ += 0. + */ + public function __construct(int $maxWeightLimit = 64) { + $this->maxWeightLimit = $maxWeightLimit; + } + + public function getName(): string { + return 'minecraft:storage_weight_limit'; + } + + public function getValue(): array { + return [ + "max_weight_limit" => $this->maxWeightLimit + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/StorageWeightModifierComponent.php b/src/item/component/StorageWeightModifierComponent.php new file mode 100644 index 00000000..26d20170 --- /dev/null +++ b/src/item/component/StorageWeightModifierComponent.php @@ -0,0 +1,31 @@ +weightInStorageItem = $weightInStorageItem; + } + + public function getName(): string { + return 'minecraft:storage_weight_modifier'; + } + + public function getValue(): array { + return [ + "weight_in_storage_item" => $this->weightInStorageItem + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/SwingDurationComponent.php b/src/item/component/SwingDurationComponent.php new file mode 100644 index 00000000..bc00698f --- /dev/null +++ b/src/item/component/SwingDurationComponent.php @@ -0,0 +1,31 @@ +swingDuration = $swingDuration; + } + + public function getName(): string { + return 'minecraft:swing_duration'; + } + + public function getValue(): array { + return [ + "value" => $this->swingDuration + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/SwingSoundsComponent.php b/src/item/component/SwingSoundsComponent.php new file mode 100644 index 00000000..4b69e11c --- /dev/null +++ b/src/item/component/SwingSoundsComponent.php @@ -0,0 +1,31 @@ + $this->critical->value, + "attack_hit" => $this->hit->value, + "attack_miss" => $this->miss->value, + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/TagsComponent.php b/src/item/component/TagsComponent.php new file mode 100644 index 00000000..5c588196 --- /dev/null +++ b/src/item/component/TagsComponent.php @@ -0,0 +1,37 @@ +tags = $tags; + } + + public function getName(): string { + return 'minecraft:tags'; + } + + public function getValue(): array { + return [ + "tags" => $this->tags + ]; + } + + public function getPropertyMapping(): ?array { + return null; + } +} \ No newline at end of file diff --git a/src/item/component/ThrowableComponent.php b/src/item/component/ThrowableComponent.php index 8edcc94d..ed082924 100644 --- a/src/item/component/ThrowableComponent.php +++ b/src/item/component/ThrowableComponent.php @@ -14,14 +14,21 @@ final class ThrowableComponent implements ItemComponent { /** * Sets the throwable item component. - * @param bool $doSwingAnimation Determines whether the item should use the swing animation when thrown - * @param float $launchPowerScale The scale at which the power of the throw increases - * @param float $maxDrawDuration The maximum duration to draw a throwable item - * @param float $maxLaunchPower The maximum power to launch the throwable item - * @param float $minDrawDuration The minimum duration to draw a throwable item - * @param bool $scalePowerByDrawDuration Whether or not the power of the throw increases with duration charged + * @param bool $doSwingAnimation Determines whether the item should use the swing animation when thrown. Default is set to false. + * @param float $launchPowerScale The scale at which the power of the throw increases. Default is set to 1.0. + * @param float $maxDrawDuration The maximum duration to draw a throwable item. Default is set to 0.0. + * @param float $maxLaunchPower The maximum power to launch the throwable item. Default is set to 1.0. + * @param float $minDrawDuration The minimum duration to draw a throwable item. Default is set to 0.0. + * @param bool $scalePowerByDrawDuration Whether or not the power of the throw increases with duration charged. Default is set to false. */ - public function __construct(bool $doSwingAnimation = false, float $launchPowerScale = 1.0, float $maxDrawDuration = 0.0, float $maxLaunchPower = 1.0, float $minDrawDuration = 0.0, bool $scalePowerByDrawDuration = false) { + public function __construct( + bool $doSwingAnimation = false, + float $launchPowerScale = 1.0, + float $maxDrawDuration = 0.0, + float $maxLaunchPower = 1.0, + float $minDrawDuration = 0.0, + bool $scalePowerByDrawDuration = false + ) { $this->doSwingAnimation = $doSwingAnimation; $this->launchPowerScale = $launchPowerScale; $this->maxDrawDuration = $maxDrawDuration; @@ -31,7 +38,7 @@ public function __construct(bool $doSwingAnimation = false, float $launchPowerSc } public function getName(): string { - return "minecraft:throwable"; + return 'minecraft:throwable'; } public function getValue(): array { @@ -45,7 +52,7 @@ public function getValue(): array { ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/component/UseAnimationComponent.php b/src/item/component/UseAnimationComponent.php index deafdffc..cd7530b3 100644 --- a/src/item/component/UseAnimationComponent.php +++ b/src/item/component/UseAnimationComponent.php @@ -5,36 +5,51 @@ final class UseAnimationComponent implements ItemComponent { - public const ANIMATION_NONE = 0; - public const ANIMATION_EAT = 1; - public const ANIMATION_DRINK = 2; - public const ANIMATION_BLOCK = 3; - public const ANIMATION_BOW = 4; - public const ANIMATION_CAMERA = 5; - public const ANIMATION_SPEAR = 6; - public const ANIMATION_CROSSBOW = 9; - public const ANIMATION_SPYGLASS = 10; - public const ANIMATION_BRUSH = 12; - - private int $animation; + public const ANIMATION_NONE = 'none'; + public const ANIMATION_EAT = 'eat'; + public const ANIMATION_DRINK = 'drink'; + public const ANIMATION_BLOCK = 'block'; + public const ANIMATION_BOW = 'bow'; + public const ANIMATION_CAMERA = 'camera'; + public const ANIMATION_SPEAR = 'spear'; + public const ANIMATION_CROSSBOW = 'crossbow'; + public const ANIMATION_SPYGLASS = 'spyglass'; + public const ANIMATION_BRUSH = 'brush'; + + private const STRING_TO_INT = [ + self::ANIMATION_NONE => 0, + self::ANIMATION_EAT => 1, + self::ANIMATION_DRINK => 2, + self::ANIMATION_BLOCK => 3, + self::ANIMATION_BOW => 4, + self::ANIMATION_CAMERA => 5, + self::ANIMATION_SPEAR => 6, + self::ANIMATION_CROSSBOW => 9, + self::ANIMATION_SPYGLASS => 10, + self::ANIMATION_BRUSH => 12, + ]; + + private string $animation; /** * Determines which animation plays when using an item. - * @param int $animation Specifies which animation to play when the the item is used, Default is set to `0` + * @param string $animation Specifies which animation to play when the the item is used. */ - public function __construct(int $animation) { + public function __construct(string $animation = self::ANIMATION_NONE) { $this->animation = $animation; } public function getName(): string { - return "use_animation"; + return 'minecraft:use_animation'; } - public function getValue(): int { - return $this->animation; + public function getValue(): array { + return [ + "value" => $this->animation + ]; } - public function isProperty(): bool { - return true; + public function getPropertyMapping(): ?array { + return ['use_animation' => (int) self::STRING_TO_INT[$this->animation] ?? 0]; } } \ No newline at end of file diff --git a/src/item/component/UseDurationComponent.php b/src/item/component/UseDurationComponent.php deleted file mode 100644 index 5c8b654e..00000000 --- a/src/item/component/UseDurationComponent.php +++ /dev/null @@ -1,29 +0,0 @@ -duration = $duration; - } - - public function getName(): string { - return "use_duration"; - } - - public function getValue(): int { - return $this->duration; - } - - public function isProperty(): bool { - return true; - } -} \ No newline at end of file diff --git a/src/item/component/UseModifiersComponent.php b/src/item/component/UseModifiersComponent.php index a4a6b11a..8011e412 100644 --- a/src/item/component/UseModifiersComponent.php +++ b/src/item/component/UseModifiersComponent.php @@ -3,33 +3,51 @@ namespace customiesdevs\customies\item\component; +use customiesdevs\customies\item\properties\SoundEvent; + final class UseModifiersComponent implements ItemComponent { private float $useDuration; private float $movementModifier; + private bool $emitVibrations; + private ?string $startSound; /** - * Determines how long an item takes to use in combination with components such as Shooter, Throwable, or Food. - * @param float $useDuration How long the item takes to use in seconds - * @param float $movementModifier Modifier value to scale the players movement speed when item is in use + * Determines how an item behaves while being used. + * @param float $movementModifier Modifier applied to player movement speed + * @param float $useDuration How long the item takes to use (seconds) + * @param bool $emitVibrations Whether the item emits vibration events + * @param SoundEvent|string|null $startSound Sound played when use starts */ - public function __construct(float $movementModifier, float $useDuration = 0) { - $this->useDuration = $useDuration; + public function __construct( + float $movementModifier = 1.0, + float $useDuration = 0.0, + bool $emitVibrations = false, + SoundEvent|string|null $startSound = null + ) { $this->movementModifier = $movementModifier; + $this->useDuration = $useDuration; + $this->emitVibrations = $emitVibrations; + $this->startSound = $startSound instanceof SoundEvent ? $startSound->value : $startSound; } public function getName(): string { - return "minecraft:use_modifiers"; + return 'minecraft:use_modifiers'; } public function getValue(): array { - return [ + $value = [ "movement_modifier" => $this->movementModifier, - "use_duration" => $this->useDuration + "use_duration" => $this->useDuration, + "emit_vibrations" => $this->emitVibrations ]; + if($this->startSound !== null){ + $value['start_sound'] = $this->startSound; + } + return $value; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return ['use_duration' => $this->useDuration]; } } \ No newline at end of file diff --git a/src/item/component/WearableComponent.php b/src/item/component/WearableComponent.php index 29a25ae8..f9069e0f 100644 --- a/src/item/component/WearableComponent.php +++ b/src/item/component/WearableComponent.php @@ -5,50 +5,51 @@ final class WearableComponent implements ItemComponent { - public const SLOT_ARMOR = "slot.armor"; - public const SLOT_ARMOR_CHEST = "slot.armor.chest"; - public const SLOT_ARMOR_FEET = "slot.armor.feet"; public const SLOT_ARMOR_HEAD = "slot.armor.head"; + public const SLOT_ARMOR_CHEST = "slot.armor.chest"; public const SLOT_ARMOR_LEGS = "slot.armor.legs"; - public const SLOT_CHEST = "slot.chest"; - public const SLOT_ENDERCHEST = "slot.enderchest"; - public const SLOT_EQUIPPABLE = "slot.equippable"; + public const SLOT_ARMOR_FEET = "slot.armor.feet"; + public const SLOT_BODY = "slot.armor.body"; + public const SLOT_WEAPON_MAIN_HAND = "slot.weapon.mainhand"; + public const SLOT_WEAPON_OFF_HAND = "slot.weapon.offhand"; + public const SLOT_HOTBAR = "slot.hotbar"; public const SLOT_INVENTORY = "slot.inventory"; - public const SLOT_NONE = "none"; + public const SLOT_ENDERCHEST = "slot.enderchest"; public const SLOT_SADDLE = "slot.saddle"; - public const SLOT_WEAPON_MAIN_HAND = "slot.weapon.mainhand"; - public const SLOT_WEAPON_OFF_HAND = "slot.weapon.offhand"; + public const SLOT_ARMOR = "slot.armor"; + public const SLOT_CHEST = "slot.chest"; + public const SLOT_EQUIPPABLE = "slot.equippable"; private string $slot; private int $protection; - private bool $dispensable; + private bool $hidePlayerLocation; /** * Sets the wearable item component. - * @param string $slot Specifies where the item can be worn - * @param int $protection How much protection the wearable item provides - * @param bool $dispensable Whether the wearable item can be dispensed + * @param string $slot Specifies where the item can be worn. If any non-hand slot is chosen, the max stack size is set to 1. + * @param int $protection How much protection the wearable item provides. Default is set to 0. + * @param bool $hidePlayerLocation Determines whether the Player's location is hidden on Locator Maps and the Locator Bar when the wearable item is worn. Default is false. */ - public function __construct(string $slot, int $protection = 0, bool $dispensable = true) { + public function __construct(string $slot, int $protection = 0, bool $hidePlayerLocation = false) { $this->slot = $slot; $this->protection = $protection; - $this->dispensable = $dispensable; + $this->hidePlayerLocation = $hidePlayerLocation; } public function getName(): string { - return "minecraft:wearable"; + return 'minecraft:wearable'; } public function getValue(): array { return [ "slot" => $this->slot, "protection" => $this->protection, - "dispensable" => $this->dispensable + "hides_player_location" => $this->hidePlayerLocation ]; } - public function isProperty(): bool { - return false; + public function getPropertyMapping(): ?array { + return null; } } \ No newline at end of file diff --git a/src/item/properties/DamageCause.php b/src/item/properties/DamageCause.php new file mode 100644 index 00000000..7b41e867 --- /dev/null +++ b/src/item/properties/DamageCause.php @@ -0,0 +1,46 @@ +name); + } + + public function getDescriptionId(): string { + return match($this){ + self::REGENERATION => KnownTranslationKeys::POTION_REGENERATION, + self::ABSORPTION => KnownTranslationKeys::POTION_ABSORPTION, + self::RESISTANCE => KnownTranslationKeys::POTION_RESISTANCE, + self::FIRE_RESISTANCE => KnownTranslationKeys::POTION_FIRERESISTANCE, + self::POISON, self::FATAL_POISON => KnownTranslationKeys::POTION_POISON, + self::WITHER => KnownTranslationKeys::POTION_WITHER, + self::SPEED => KnownTranslationKeys::POTION_MOVESPEED, + self::SLOWNESS => KnownTranslationKeys::POTION_MOVESLOWDOWN, + self::HASTE => KnownTranslationKeys::POTION_DIGSPEED, + self::MINING_FATIGUE => KnownTranslationKeys::POTION_DIGSLOWDOWN, + self::STRENGTH => KnownTranslationKeys::POTION_DAMAGEBOOST, + self::INSTANT_HEALTH => KnownTranslationKeys::POTION_HEAL, + self::INSTANT_DAMAGE => KnownTranslationKeys::POTION_HARM, + self::JUMP_BOOST => KnownTranslationKeys::POTION_JUMP, + self::NAUSEA => KnownTranslationKeys::POTION_CONFUSION, + self::BLINDNESS => KnownTranslationKeys::POTION_BLINDNESS, + self::NIGHT_VISION => KnownTranslationKeys::POTION_NIGHTVISION, + self::HUNGER => KnownTranslationKeys::POTION_HUNGER, + self::WEAKNESS => KnownTranslationKeys::POTION_WEAKNESS, + self::HEALTH_BOOST => KnownTranslationKeys::POTION_HEALTHBOOST, + self::SATURATION => KnownTranslationKeys::POTION_SATURATION, + self::LEVITATION => KnownTranslationKeys::POTION_LEVITATION, + self::CONDUIT_POWER => KnownTranslationKeys::POTION_CONDUITPOWER, + self::SLOW_FALLING => KnownTranslationKeys::POTION_SLOWFALLING, + self::BAD_OMEN => "effect.badOmen", + self::VILLAGE_HERO => "effect.villageHero", + self::DARKNESS => KnownTranslationKeys::EFFECT_DARKNESS, + self::WATER_BREATHING => KnownTranslationKeys::POTION_WATERBREATHING, + }; + } + + public function getId(): int { + return $this->value; + } +} \ No newline at end of file diff --git a/src/item/properties/ParticleType.php b/src/item/properties/ParticleType.php new file mode 100644 index 00000000..c38546e5 --- /dev/null +++ b/src/item/properties/ParticleType.php @@ -0,0 +1,101 @@ +numeric !== null){ + return $this->numeric; + } + return [ + "expression" => $this->expression, + "version" => new ShortTag(13) + ]; + } +} diff --git a/src/item/properties/RepairItems.php b/src/item/properties/RepairItems.php new file mode 100644 index 00000000..dad2301c --- /dev/null +++ b/src/item/properties/RepairItems.php @@ -0,0 +1,45 @@ +, + * repair_amount: int|float|array + * } + */ + public function toArray(): array { + return [ + "items" => array_map( + static fn(string $item) => ["name" => $item], + $this->items + ), + "repair_amount" => $this->repairAmount->toArray() + ]; + } + + /** @return string[] */ + public function getItems(): array { + return $this->items; + } + + public function getRepairAmount(): RepairAmount { + return $this->repairAmount; + } +} \ No newline at end of file diff --git a/src/item/properties/SoundEvent.php b/src/item/properties/SoundEvent.php new file mode 100644 index 00000000..5c2ba6df --- /dev/null +++ b/src/item/properties/SoundEvent.php @@ -0,0 +1,228 @@ + $blockFuncs + * @param Closure[] $blockFuncs Array of callbacks used for block registration + * @phpstan-param array $blockFuncs Associative array where the key is the block identifier and the value is a triple of: + * - block creation function + * - serializer function + * - deserializer function */ - public function __construct(private string $cachePath, array $blockFuncs) { + public function __construct(array $blockFuncs) { $this->blockFuncs = new ThreadSafeArray(); $this->serializer = new ThreadSafeArray(); $this->deserializer = new ThreadSafeArray(); - foreach($blockFuncs as $identifier => [$blockFunc, $serializer, $deserializer]){ $this->blockFuncs[$identifier] = $blockFunc; $this->serializer[$identifier] = $serializer; @@ -34,9 +41,12 @@ public function __construct(private string $cachePath, array $blockFuncs) { public function onRun(): void { foreach($this->blockFuncs as $identifier => $blockFunc){ - // We do not care about the model or creative inventory data in other threads since it is unused outside of - // the main thread. - CustomiesBlockFactory::getInstance()->registerBlock($blockFunc, $identifier, serializer: $this->serializer[$identifier], deserializer: $this->deserializer[$identifier]); + CustomiesBlockFactory::getInstance()->registerBlock( + $blockFunc, + (string) $identifier, + serializer: $this->serializer[$identifier], + deserializer: $this->deserializer[$identifier] + ); } } -} +} \ No newline at end of file diff --git a/src/util/NBT.php b/src/util/NBT.php index 0ef9cadc..e01414ba 100644 --- a/src/util/NBT.php +++ b/src/util/NBT.php @@ -20,34 +20,76 @@ use function is_string; use function range; -class NBT { +final class NBT { /** - * Attempts to return the correct Tag for the provided type. + * Attempts to return the correct NBT Tag for the provided PHP value. + * Supported conversions: + * - array → ListTag or CompoundTag + * - bool → ByteTag + * - float → FloatTag + * - int → IntTag + * - string → StringTag + * - Tag → Returned as-is + * + * @param mixed $type The value to convert into an NBT Tag + * @return Tag|null Returns the corresponding Tag instance, or null if the + * type cannot be converted. */ public static function getTagType($type): ?Tag { - return match (true) { + return match (true){ + $type instanceof Tag => $type, is_array($type) => self::getArrayTag($type), is_bool($type) => new ByteTag($type ? 1 : 0), is_float($type) => new FloatTag($type), is_int($type) => new IntTag($type), is_string($type) => new StringTag($type), - $type instanceof CompoundTag => $type, default => null, }; } /** - * Creates a Tag that is either a ListTag or CompoundTag based on the data types of the keys in the provided array. + * Creates an NBT Tag from an array. + * - If the array uses sequential numeric keys (0..n), a ListTag is created. + * - Otherwise, a CompoundTag is created with each key mapped to a Tag. + * @param array $array The array to convert into an NBT Tag + * @return Tag Returns either a ListTag or CompoundTag depending on the array structure + * @throws \InvalidArgumentException If any value cannot be converted to a Tag */ private static function getArrayTag(array $array): Tag { - if(array_keys($array) === range(0, count($array) - 1)) { - return new ListTag(array_map(fn($value) => self::getTagType($value), $array)); + if(array_keys($array) === range(0, count($array) - 1)){ + return new ListTag(array_map(function($value){ + $tag = self::getTagType($value); + if($tag === null) { + throw new \InvalidArgumentException("Cannot convert value of type " . get_debug_type($value) . " to NBT Tag"); + } + return $tag; + }, $array)); } $tag = CompoundTag::create(); foreach($array as $key => $value){ - $tag->setTag($key, self::getTagType($value)); + $valueTag = self::getTagType($value); + if($valueTag === null) { + throw new \InvalidArgumentException("Cannot convert value of type " . get_debug_type($value) . " for key '$key' to NBT Tag"); + } + $tag->setTag((string) $key, $valueTag); } return $tag; } + + public static function sortCompoundTag(CompoundTag $tag, array $order): CompoundTag { + $sorted = CompoundTag::create(); + foreach($order as $key){ + $existing = $tag->getTag($key); + if($existing !== null){ + $sorted->setTag($key, $existing); + } + } + foreach($tag->getValue() as $key => $value){ + if($sorted->getTag((string) $key) === null){ + $sorted->setTag((string) $key, $value); + } + } + return $sorted; + } } \ No newline at end of file