Skip to content

Commit 7b345be

Browse files
authored
Merge pull request #160 from alexandrosmagos/Race_conditions_fix_and_addAll_removeAll_implementation
Stacker UI Improvements + Critical Exploit Fixes
2 parents fa1044d + 6261048 commit 7b345be

10 files changed

Lines changed: 342 additions & 27 deletions

File tree

core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmListener.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ private void handleCancel(Player player, SpawnerData spawner, SpawnerSellConfirm
7070
// Play sound instead of sending message
7171
player.playSound(player.getLocation(), org.bukkit.Sound.UI_BUTTON_CLICK, 1.0f, 1.0f);
7272

73+
// Clear interaction state
74+
spawner.clearInteracted();
75+
7376
// Reopen previous GUI
7477
reopenPreviousGui(player, spawner, previousGui);
7578
}
@@ -80,6 +83,9 @@ private void handleConfirm(Player player, SpawnerData spawner, SpawnerSellConfir
8083
plugin.getSpawnerMenuAction().handleExpBottleClick(player, spawner, true);
8184
}
8285

86+
// Clear interaction state
87+
spawner.clearInteracted();
88+
8389
// Trigger the actual sell operation
8490
plugin.getSpawnerSellManager().sellAllItems(player, spawner);
8591

core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ public void openSellConfirmGui(Player player, SpawnerData spawner, PreviousGui p
4242
return;
4343
}
4444

45+
// Check if there are items to sell before opening
46+
if (spawner.getVirtualInventory().getUsedSlots() == 0) {
47+
plugin.getMessageService().sendMessage(player, "no_items");
48+
return;
49+
}
50+
51+
// Mark spawner as interacted to lock state during transaction
52+
spawner.markInteracted();
53+
4554
// Cache title - no placeholders needed
4655
String title = languageManager.getGuiTitle("gui_title_sell_confirm", null);
4756
Inventory gui = Bukkit.createInventory(new SpawnerSellConfirmHolder(spawner, previousGui, collectExp), GUI_SIZE, title);

core/src/main/java/github/nighter/smartspawner/spawner/gui/stacker/SpawnerStackerHandler.java

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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;

core/src/main/java/github/nighter/smartspawner/spawner/gui/stacker/SpawnerStackerUI.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public class SpawnerStackerUI {
2222
private static final int[] INCREASE_SLOTS = {17, 16, 15};
2323
private static final int SPAWNER_INFO_SLOT = 13;
2424
private static final int[] STACK_AMOUNTS = {64, 10, 1};
25+
private static final int REMOVE_ALL_SLOT = 22;
26+
private static final int ADD_ALL_SLOT = 4;
2527

2628
private final SmartSpawner plugin;
2729
private final LanguageManager languageManager;
@@ -58,6 +60,8 @@ private void populateStackerGui(Inventory gui, SpawnerData spawner) {
5860
gui.setItem(INCREASE_SLOTS[i], createActionButton("add", spawner, STACK_AMOUNTS[i]));
5961
}
6062
gui.setItem(SPAWNER_INFO_SLOT, createSpawnerInfoButton(spawner));
63+
gui.setItem(REMOVE_ALL_SLOT, createAllActionButton("remove_all", spawner));
64+
gui.setItem(ADD_ALL_SLOT, createAllActionButton("add_all", spawner));
6165
}
6266

6367
private ItemStack createActionButton(String action, SpawnerData spawner, int amount) {
@@ -77,6 +81,15 @@ private ItemStack createSpawnerInfoButton(SpawnerData spawner) {
7781
return createButton(Material.SPAWNER, name, lore);
7882
}
7983

84+
private ItemStack createAllActionButton(String action, SpawnerData spawner) {
85+
Map<String, String> placeholders = createPlaceholders(spawner, 0);
86+
String name = languageManager.getGuiItemName("button_" + action + ".name", placeholders);
87+
String[] lore = languageManager.getGuiItemLore("button_" + action + ".lore", placeholders);
88+
Material material = action.equals("add_all") ? Material.LIME_STAINED_GLASS_PANE
89+
: Material.RED_STAINED_GLASS_PANE;
90+
return createButton(material, name, lore);
91+
}
92+
8093
private Map<String, String> createPlaceholders(SpawnerData spawner, int amount) {
8194
Map<String, String> placeholders = new HashMap<>();
8295
placeholders.put("amount", String.valueOf(amount));

0 commit comments

Comments
 (0)