@@ -55,6 +55,8 @@ public class SpawnerStackerHandler implements Listener {
5555 private static final int [] INCREASE_SLOTS = {17 , 16 , 15 };
5656 private static final int SPAWNER_INFO_SLOT = 13 ;
5757 private static final int [] STACK_AMOUNTS = {64 , 10 , 1 };
58+ private static final int REMOVE_ALL_SLOT = 22 ;
59+ private static final int ADD_ALL_SLOT = 4 ;
5860
5961 // Player interaction tracking - using more efficient data structures
6062 private final Map <UUID , Long > lastClickTime = new ConcurrentHashMap <>(16 , 0.75f , 2 );
@@ -135,6 +137,29 @@ public void onInventoryClick(InventoryClickEvent event) {
135137
136138 // Process stack modification
137139 int slotIndex = event .getRawSlot ();
140+
141+ // Handle "all" buttons
142+ if (slotIndex == ADD_ALL_SLOT ) {
143+ lastClickTime .put (playerId , System .currentTimeMillis ());
144+ handleAddAll (player , spawner );
145+ String spawnerId = spawner .getSpawnerId ();
146+ Set <UUID > viewers = activeViewers .get (spawnerId );
147+ if (viewers != null && !viewers .isEmpty ()) {
148+ scheduleViewersUpdate (spawner );
149+ }
150+ return ;
151+ }
152+ if (slotIndex == REMOVE_ALL_SLOT ) {
153+ lastClickTime .put (playerId , System .currentTimeMillis ());
154+ handleRemoveAll (player , spawner );
155+ String spawnerId = spawner .getSpawnerId ();
156+ Set <UUID > viewers = activeViewers .get (spawnerId );
157+ if (viewers != null && !viewers .isEmpty ()) {
158+ scheduleViewersUpdate (spawner );
159+ }
160+ return ;
161+ }
162+
138163 int changeAmount = determineChangeAmount (slotIndex );
139164
140165 if (changeAmount != 0 ) {
@@ -157,7 +182,7 @@ public void onInventoryDrag(InventoryDragEvent event) {
157182 if (!(event .getInventory ().getHolder (false ) instanceof SpawnerStackerHolder holder )) {
158183 return ;
159184 }
160-
185+
161186 event .setCancelled (true );
162187 }
163188
@@ -419,6 +444,167 @@ private void handleStackIncrease(Player player, SpawnerData spawner, int changeA
419444 player .playSound (player .getLocation (), STACK_SOUND , SOUND_VOLUME , SOUND_PITCH );
420445 }
421446
447+ private void handleAddAll (Player player , SpawnerData spawner ) {
448+ int currentSize = spawner .getStackSize ();
449+ int maxStackSize = spawner .getMaxStackSize ();
450+ int spaceLeft = maxStackSize - currentSize ;
451+
452+ if (spaceLeft <= 0 ) {
453+ Map <String , String > placeholders = new HashMap <>(2 );
454+ placeholders .put ("max" , String .valueOf (maxStackSize ));
455+ messageService .sendMessage (player , "spawner_stack_full" , placeholders );
456+ return ;
457+ }
458+
459+ // Scan inventory for matching spawners
460+ InventoryScanResult scanResult ;
461+ if (spawner .isItemSpawner ()) {
462+ scanResult = scanPlayerInventoryForItemSpawner (player , spawner .getSpawnedItemMaterial ());
463+ } else {
464+ scanResult = scanPlayerInventory (player , spawner .getEntityType ());
465+ }
466+
467+ if (scanResult .availableSpawners == 0 && scanResult .hasDifferentType ) {
468+ messageService .sendMessage (player , "spawner_different" );
469+ return ;
470+ }
471+
472+ if (scanResult .availableSpawners == 0 ) {
473+ Map <String , String > placeholders = new HashMap <>(4 );
474+ placeholders .put ("amountChange" , String .valueOf (spaceLeft ));
475+ placeholders .put ("amountAvailable" , "0" );
476+ messageService .sendMessage (player , "spawner_insufficient_quantity" , placeholders );
477+ return ;
478+ }
479+
480+ int actualChange = Math .min (spaceLeft , scanResult .availableSpawners );
481+
482+ if (SpawnerStackEvent .getHandlerList ().getRegisteredListeners ().length != 0 ) {
483+ SpawnerStackEvent e = new SpawnerStackEvent (player , spawner .getSpawnerLocation (), spawner .getStackSize (),
484+ spawner .getStackSize () + actualChange , SpawnerStackEvent .StackSource .GUI );
485+ Bukkit .getPluginManager ().callEvent (e );
486+ if (e .isCancelled ())
487+ return ;
488+ }
489+
490+ // Update stack size first and ensure data is marked as modified
491+ spawner .setStackSize (currentSize + actualChange );
492+ spawnerManager .markSpawnerModified (spawner .getSpawnerId ());
493+
494+ if (spawner .isInteracted ()) {
495+ player .playSound (player .getLocation (), STACK_SOUND , SOUND_VOLUME , SOUND_PITCH );
496+ return ;
497+ }
498+
499+ if (spawner .isItemSpawner ()) {
500+ removeValidItemSpawnersFromInventory (player , spawner .getSpawnedItemMaterial (), actualChange ,
501+ scanResult .spawnerSlots );
502+ } else {
503+ removeValidSpawnersFromInventory (player , spawner .getEntityType (), actualChange ,
504+ scanResult .spawnerSlots );
505+ }
506+
507+ player .playSound (player .getLocation (), STACK_SOUND , SOUND_VOLUME , SOUND_PITCH );
508+ }
509+
510+ private void handleRemoveAll (Player player , SpawnerData spawner ) {
511+ Location location = spawner .getSpawnerLocation ();
512+
513+ if (!locationLockManager .tryLock (location )) {
514+ messageService .sendMessage (player , "action_in_progress" );
515+ return ;
516+ }
517+
518+ try {
519+ int currentSize = spawner .getStackSize ();
520+
521+ if (currentSize == 1 ) {
522+ messageService .sendMessage (player , "spawner_cannot_remove_last" );
523+ return ;
524+ }
525+
526+ int actualChange = currentSize - 1 ;
527+
528+ // Cap at available inventory capacity
529+ int inventoryCapacity = countAvailableSpawnerCapacity (player , spawner );
530+ if (inventoryCapacity <= 0 ) {
531+ messageService .sendMessage (player , "inventory_full" );
532+ return ;
533+ }
534+ actualChange = Math .min (actualChange , inventoryCapacity );
535+ int newStackSize = currentSize - actualChange ;
536+
537+ if (SpawnerRemoveEvent .getHandlerList ().getRegisteredListeners ().length != 0 ) {
538+ SpawnerRemoveEvent e = new SpawnerRemoveEvent (player , spawner .getSpawnerLocation (), newStackSize ,
539+ actualChange );
540+ Bukkit .getPluginManager ().callEvent (e );
541+ if (e .isCancelled ())
542+ return ;
543+ }
544+
545+ spawner .setStackSize (newStackSize );
546+ spawnerManager .markSpawnerModified (spawner .getSpawnerId ());
547+
548+ if (spawner .isItemSpawner ()) {
549+ giveItemSpawnersToPlayer (player , actualChange , spawner .getSpawnedItemMaterial ());
550+ } else {
551+ giveSpawnersToPlayer (player , actualChange , spawner .getEntityType ());
552+ }
553+
554+ if (plugin .getSpawnerActionLogger () != null ) {
555+ final int logActualChange = actualChange ;
556+ final int logNewStackSize = newStackSize ;
557+ plugin .getSpawnerActionLogger ().log (
558+ github .nighter .smartspawner .logging .SpawnerEventType .SPAWNER_DESTACK_GUI ,
559+ builder -> builder .player (player .getName (), player .getUniqueId ())
560+ .location (spawner .getSpawnerLocation ())
561+ .entityType (spawner .getEntityType ())
562+ .metadata ("amount_removed" , logActualChange )
563+ .metadata ("old_stack_size" , currentSize )
564+ .metadata ("new_stack_size" , logNewStackSize ));
565+ }
566+
567+ player .playSound (player .getLocation (), STACK_SOUND , SOUND_VOLUME , SOUND_PITCH );
568+ } finally {
569+ locationLockManager .unlock (location );
570+ }
571+ }
572+
573+ /**
574+ * Counts how many spawners of the given type the player's inventory can accept.
575+ * Considers both empty slots and partial stacks of matching spawners.
576+ */
577+ private int countAvailableSpawnerCapacity (Player player , SpawnerData spawner ) {
578+ final int MAX_STACK_SIZE = 64 ;
579+ int capacity = 0 ;
580+ ItemStack [] contents = player .getInventory ().getContents ();
581+
582+ for (int i = 0 ; i < 36 ; i ++) { // Main inventory slots 0-35
583+ ItemStack item = contents [i ];
584+ if (item == null || item .getType () == Material .AIR ) {
585+ capacity += MAX_STACK_SIZE ;
586+ continue ;
587+ }
588+
589+ if (item .getType () == Material .SPAWNER && !SpawnerTypeChecker .isVanillaSpawner (item )) {
590+ Optional <EntityType > itemEntityType = getSpawnerEntityTypeCached (item );
591+ boolean matches ;
592+ if (spawner .isItemSpawner ()) {
593+ // For item spawners, match by spawned item material
594+ matches = false ; // Item spawners use separate give method; empty slots cover them
595+ } else {
596+ matches = itemEntityType .isPresent () && itemEntityType .get () == spawner .getEntityType ();
597+ }
598+
599+ if (matches && item .getAmount () < MAX_STACK_SIZE ) {
600+ capacity += MAX_STACK_SIZE - item .getAmount ();
601+ }
602+ }
603+ }
604+
605+ return capacity ;
606+ }
607+
422608 private void scheduleViewersUpdate (SpawnerData spawner ) {
423609 String spawnerId = spawner .getSpawnerId ();
424610 Set <UUID > viewers = activeViewers .get (spawnerId );
@@ -471,6 +657,10 @@ private void updateGui(Player player, SpawnerData spawner) {
471657 updateActionButton (inv , "add" , STACK_AMOUNTS [i ], INCREASE_SLOTS [i ], basePlaceholders );
472658 }
473659
660+ // Update "all" buttons
661+ updateAllActionButton (inv , "remove_all" , REMOVE_ALL_SLOT , basePlaceholders );
662+ updateAllActionButton (inv , "add_all" , ADD_ALL_SLOT , basePlaceholders );
663+
474664 // Force client refresh
475665 player .updateInventory ();
476666 }
@@ -525,6 +715,23 @@ private void updateActionButton(Inventory inventory, String action, int amount,
525715 button .setItemMeta (meta );
526716 }
527717
718+ private void updateAllActionButton (Inventory inventory , String action , int slot ,
719+ Map <String , String > basePlaceholders ) {
720+ ItemStack button = inventory .getItem (slot );
721+ if (button == null || !button .hasItemMeta ())
722+ return ;
723+
724+ ItemMeta meta = button .getItemMeta ();
725+ Map <String , String > placeholders = new HashMap <>(basePlaceholders );
726+
727+ String name = languageManager .getGuiItemName ("button_" + action + ".name" , placeholders );
728+ String [] lore = languageManager .getGuiItemLore ("button_" + action + ".lore" , placeholders );
729+
730+ meta .setDisplayName (name );
731+ meta .setLore (Arrays .asList (lore ));
732+ button .setItemMeta (meta );
733+ }
734+
528735 // Combined inventory scan that collects all required data in one pass
529736 private InventoryScanResult scanPlayerInventory (Player player , EntityType requiredType ) {
530737 int count = 0 ;
0 commit comments