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 d52e2db..1c8ffff 100644 --- a/src/david/itemStacker/EventListener.php +++ b/src/david/itemStacker/EventListener.php @@ -1,50 +1,128 @@ -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); - } - } -} +plugin = $plugin; + } + + public function onEntitySpawn(EntitySpawnEvent $event) : void { + $entity = $event->getEntity(); + + if (!$entity instanceof ItemEntity) { + return; + } + + $originalItem = $entity->getItem(); + + $world = $entity->getWorld(); + $position = $entity->getPosition(); + + $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, + $position->x + $halfRadius, + $position->y + $halfRadius, + $position->z + $halfRadius + ); + + /** @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) { + $nearbyItem = $nearbyEntity->getItem(); + + if ($nearbyItem->equals($originalItem)) { + $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; + } + + $tempStack->setCount($currentCount + $nearbyCount); + } + } + } + + foreach ($entitiesToDespawn as $nearbyEntity) { + $nearbyEntity->flagForDespawn(); + } + + $entity->setStackSize($tempStack->getCount()); + + $entity->setDespawnDelay(ItemEntity::DEFAULT_DESPAWN_DELAY); + + $finalCount = $tempStack->getCount(); + + if ($finalCount > 1 || $displaySingleStackTag) { + $this->updateNameTag($entity, $tempStack->getName(), $finalCount); + } else { + $entity->setNameTag(''); + } + + 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; + } +}