From c90f6acc23d5de4b8678edf22e0ba3cfc59e0ee0 Mon Sep 17 00:00:00 2001 From: AIPTU Date: Sun, 26 Oct 2025 19:36:44 +0700 Subject: [PATCH 1/2] refactor: Improve item stacking performance and code strictness Updates the EventListener to enhance efficiency by: - Using a calculated AxisAlignedBoundingBox for faster entity lookups. - Cloning the item before mutation to adhere to better state management. --- src/david/itemStacker/EventListener.php | 126 ++++++++++++++---------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/src/david/itemStacker/EventListener.php b/src/david/itemStacker/EventListener.php index d52e2db..41b3c4f 100644 --- a/src/david/itemStacker/EventListener.php +++ b/src/david/itemStacker/EventListener.php @@ -1,50 +1,76 @@ -getEntity(); - - if (!$entity instanceof ItemEntity) { - return; - } - - $position = $entity->getPosition(); - $world = $position->getWorld(); - $entities = $world->getNearbyEntities($entity->getBoundingBox()->expandedCopy(5, 5, 5)); - - if (empty($entities)) { - return; - } - - $originalItem = $entity->getItem(); - $totalItemCount = $originalItem->getCount(); - - foreach ($entities as $e) { - if ($e instanceof ItemEntity && $entity !== $e) { - $itemE = $e->getItem(); - if ($itemE->equals($originalItem)) { - $e->flagForDespawn(); - $originalItem->setCount($originalItem->getCount() + $itemE->getCount()); - $totalItemCount += $itemE->getCount(); - } - } - } - - if ($totalItemCount > 1) { - $itemName = $originalItem->getName(); - $tag = "§7" . $itemName . " §7x§b" . $totalItemCount; - $entity->setNameTag($tag); - $entity->setNameTagAlwaysVisible(true); - } - } -} +getEntity(); + + if (!$entity instanceof ItemEntity) { + return; + } + + $originalItem = $entity->getItem(); + $totalItemCount = $originalItem->getCount(); + + $world = $entity->getWorld(); + + $position = $entity->getPosition(); + $halfRadius = self::STACK_RADIUS / 2; + + $aabb = new AxisAlignedBoundingBox( + $position->x - $halfRadius, + $position->y - $halfRadius, + $position->z - $halfRadius, + $position->x + $halfRadius, + $position->y + $halfRadius, + $position->z + $halfRadius + ); + + $entities = $world->getNearbyEntities($aabb); + + if (count($entities) === 0) { + return; + } + + $tempStack = clone $originalItem; + + foreach ($entities as $nearbyEntity) { + if ($nearbyEntity instanceof ItemEntity && $entity !== $nearbyEntity) { + $nearbyItem = $nearbyEntity->getItem(); + + if ($nearbyItem->equals($originalItem)) { + $tempStack->setCount($tempStack->getCount() + $nearbyItem->getCount()); + + $nearbyEntity->flagForDespawn(); + } + } + } + + $finalCount = $tempStack->getCount(); + + if ($finalCount > 1) { + // Only update the entity's item if stacking actually increased the count + if ($finalCount > $totalItemCount) { + $entity->setItem($tempStack); + } + + $itemName = $tempStack->getName(); + $tag = '§7' . $itemName . ' §7x§b' . $finalCount; + + $entity->setNameTag($tag); + $entity->setNameTagAlwaysVisible(true); + } + } +} From 9510c060ab3daba45153e3217fac437b043b8f1d Mon Sep 17 00:00:00 2001 From: AIPTU Date: Sun, 26 Oct 2025 20:03:11 +0700 Subject: [PATCH 2/2] feat(item-stacker): add configurable stacking with validation and improved logic - Add config.yml support with stacking_radius, max_global_stack_size, tag_format, and display_single_stack_tag options - Implement strict config validation with fallback defaults and warning logs - Replace hardcoded 5-block radius with configurable stackRadius - Add max_global_stack_size to enforce stack limits beyond vanilla max stack - Support custom name tag formatting with {COUNT} and {ITEM_NAME} placeholders - Add display_single_stack_tag option to show/hide tags for single items - Implement proper stack overflow handling with remainder item spawning - Fix entity stacking logic to respect max stack sizes and prevent item loss - Add AxisAlignedBB for precise radius-based entity detection - Improve name tag display logic with conditional visibility - Reset despawn delay on stacked items to prevent premature despawning - Add proper item cloning to prevent reference issues - Implement getter methods for config access in Loader class BREAKING CHANGE: Stacking behavior now configurable via config.yml, hardcoded values removed --- resources/config.yml | 20 +++++ src/david/itemStacker/EventListener.php | 94 ++++++++++++++++----- src/david/itemStacker/Loader.php | 104 ++++++++++++++++++++---- 3 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 resources/config.yml diff --git a/resources/config.yml b/resources/config.yml new file mode 100644 index 0000000..6d1117a --- /dev/null +++ b/resources/config.yml @@ -0,0 +1,20 @@ +# Configuration for ItemStacker + +# 1. Configuration for Stack Radius +stacking_radius: 5.0 + +# 2. Configuration for Max Stack Size Enforcement +# If set to 0, no global limit is applied (uses item's default max stack size). +max_global_stack_size: 1024 + +# 3. Custom Tag Display Format +# Define the format for the item stack name tag. +# Available placeholders: +# - {COUNT}: The total number of stacked items. +# - {ITEM_NAME}: The item's default name (e.g., "Stone"). +# You can use Minecraft color codes (e.g., §r, §l, §a) +tag_format: "§r§l§7{ITEM_NAME} §r§7x§b{COUNT}" + +# 4. Default Name Tag Display (Optional fallback) +# If true, the name tag will still be set even if the stack size is 1. +display_single_stack_tag: false diff --git a/src/david/itemStacker/EventListener.php b/src/david/itemStacker/EventListener.php index 41b3c4f..1c8ffff 100644 --- a/src/david/itemStacker/EventListener.php +++ b/src/david/itemStacker/EventListener.php @@ -4,15 +4,20 @@ namespace david\itemStacker; +use pocketmine\entity\Entity; use pocketmine\entity\object\ItemEntity; use pocketmine\event\entity\EntitySpawnEvent; use pocketmine\event\Listener; -use pocketmine\item\Item; -use pocketmine\math\AxisAlignedBoundingBox; +use pocketmine\math\AxisAlignedBB; use function count; +use function str_replace; class EventListener implements Listener { - private const STACK_RADIUS = 5.0; + private Loader $plugin; + + public function __construct(Loader $plugin) { + $this->plugin = $plugin; + } public function onEntitySpawn(EntitySpawnEvent $event) : void { $entity = $event->getEntity(); @@ -22,14 +27,17 @@ public function onEntitySpawn(EntitySpawnEvent $event) : void { } $originalItem = $entity->getItem(); - $totalItemCount = $originalItem->getCount(); $world = $entity->getWorld(); - $position = $entity->getPosition(); - $halfRadius = self::STACK_RADIUS / 2; - $aabb = new AxisAlignedBoundingBox( + $stackRadius = $this->plugin->getStackRadius(); + $maxGlobalStackSize = $this->plugin->getMaxGlobalStackSize(); + $displaySingleStackTag = $this->plugin->displaySingleStackTag(); + + $halfRadius = $stackRadius / 2; + + $aabb = new AxisAlignedBB( $position->x - $halfRadius, $position->y - $halfRadius, $position->z - $halfRadius, @@ -38,39 +46,83 @@ public function onEntitySpawn(EntitySpawnEvent $event) : void { $position->z + $halfRadius ); - $entities = $world->getNearbyEntities($aabb); + /** @var array $entities */ + $entities = $world->getNearbyEntities($aabb, $entity); if (count($entities) === 0) { + $finalCount = $originalItem->getCount(); + if ($finalCount > 0 && $displaySingleStackTag) { + $this->updateNameTag($entity, $originalItem->getName(), $finalCount); + } + return; } $tempStack = clone $originalItem; + $itemsToSpawn = []; + $entitiesToDespawn = []; + + $maxItemStack = $maxGlobalStackSize > 0 ? $maxGlobalStackSize : $originalItem->getMaxStackSize(); foreach ($entities as $nearbyEntity) { - if ($nearbyEntity instanceof ItemEntity && $entity !== $nearbyEntity) { + if ($nearbyEntity instanceof ItemEntity) { $nearbyItem = $nearbyEntity->getItem(); if ($nearbyItem->equals($originalItem)) { - $tempStack->setCount($tempStack->getCount() + $nearbyItem->getCount()); + $nearbyCount = $nearbyItem->getCount(); + $entitiesToDespawn[] = $nearbyEntity; + + $currentCount = $tempStack->getCount(); + + if ($currentCount + $nearbyCount > $maxItemStack) { + $canAdd = $maxItemStack - $currentCount; + $remainder = $nearbyCount - $canAdd; + + $tempStack->setCount($maxItemStack); + + $spillItem = clone $originalItem; + $spillItem->setCount($remainder); + $itemsToSpawn[] = $spillItem; + + break; + } - $nearbyEntity->flagForDespawn(); + $tempStack->setCount($currentCount + $nearbyCount); } } } - $finalCount = $tempStack->getCount(); + foreach ($entitiesToDespawn as $nearbyEntity) { + $nearbyEntity->flagForDespawn(); + } - if ($finalCount > 1) { - // Only update the entity's item if stacking actually increased the count - if ($finalCount > $totalItemCount) { - $entity->setItem($tempStack); - } + $entity->setStackSize($tempStack->getCount()); + + $entity->setDespawnDelay(ItemEntity::DEFAULT_DESPAWN_DELAY); + + $finalCount = $tempStack->getCount(); - $itemName = $tempStack->getName(); - $tag = '§7' . $itemName . ' §7x§b' . $finalCount; + if ($finalCount > 1 || $displaySingleStackTag) { + $this->updateNameTag($entity, $tempStack->getName(), $finalCount); + } else { + $entity->setNameTag(''); + } - $entity->setNameTag($tag); - $entity->setNameTagAlwaysVisible(true); + foreach ($itemsToSpawn as $spillItem) { + $world->dropItem($position, $spillItem); } } + + private function updateNameTag(ItemEntity $entity, string $itemName, int $count) : void { + $tagFormat = $this->plugin->getTagFormat(); + + $tag = str_replace( + ['{COUNT}', '{ITEM_NAME}'], + [(string) $count, $itemName], + $tagFormat + ); + + $entity->setNameTag($tag); + $entity->setNameTagAlwaysVisible(true); + } } diff --git a/src/david/itemStacker/Loader.php b/src/david/itemStacker/Loader.php index 64c75fd..f3cfa91 100644 --- a/src/david/itemStacker/Loader.php +++ b/src/david/itemStacker/Loader.php @@ -1,18 +1,86 @@ -getServer()->getPluginManager()->registerEvents(new EventListener(), $this); - - libPiggyUpdateChecker::init($this); - } -} +saveDefaultConfig(); + $this->validateConfig($this->getConfig()->getAll()); + + $this->getServer()->getPluginManager()->registerEvents(new EventListener($this), $this); + + libPiggyUpdateChecker::init($this); + } + + /** + * Performs strict validation and sets class properties. + * + * @param array $config + */ + private function validateConfig(array $config) : void { + $logger = $this->getLogger(); + + $radius = $config['stacking_radius'] ?? 5.0; + if (!is_numeric($radius) || (float) $radius <= 0.0) { + $radius = 5.0; + $logger->warning("Invalid 'stacking_radius' in config.yml. Must be a positive number. Using default: 5.0"); + } + + $this->stackRadius = (float) $radius; + + $maxSize = $config['max_global_stack_size'] ?? 1024; + if (!is_int($maxSize) || $maxSize < 0) { + $maxSize = 1024; + $logger->warning("Invalid 'max_global_stack_size' in config.yml. Must be a non-negative integer. Using default: 1024"); + } + + $this->maxGlobalStackSize = $maxSize; + + $format = $config['tag_format'] ?? '§r§7{ITEM_NAME} §r§7x§b{COUNT}'; + if (!is_string($format) || trim($format) === '') { + $format = '§r§7{ITEM_NAME} §r§7x§b{COUNT}'; + $logger->warning("Invalid 'tag_format' in config.yml. Must be a non-empty string. Using default format."); + } + + $this->tagFormat = $format; + + $displaySingle = $config['display_single_stack_tag'] ?? false; + if (!is_bool($displaySingle)) { + $displaySingle = false; + $logger->warning("Invalid 'display_single_stack_tag' in config.yml. Must be a boolean (true/false). Using default: false"); + } + + $this->displaySingleStackTag = $displaySingle; + } + + public function getStackRadius() : float { + return $this->stackRadius; + } + + public function getMaxGlobalStackSize() : int { + return $this->maxGlobalStackSize; + } + + public function getTagFormat() : string { + return $this->tagFormat; + } + + public function displaySingleStackTag() : bool { + return $this->displaySingleStackTag; + } +}