diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java index 2abbc756..01985831 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java @@ -29,26 +29,20 @@ public SpawnerLootGenerator(SmartSpawner plugin) { } public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { - int mobCount = random.nextInt(maxMobs - minMobs + 1) + minMobs; int totalExperience = spawner.getEntityExperienceValue() * mobCount; - // Get valid items from the spawner's EntityLootConfig - List validItems = spawner.getValidLootItems(); + List validItems = spawner.getValidLootItems(); if (validItems.isEmpty()) { return new LootResult(Collections.emptyList(), totalExperience); } - // Use a Map to consolidate identical drops instead of List Map consolidatedLoot = new HashMap<>(); - // Process mobs in batch rather than individually for (LootItem lootItem : validItems) { - // Calculate the probability for the entire mob batch at once int successfulDrops = 0; - // Calculate binomial distribution - how many mobs will drop this item for (int i = 0; i < mobCount; i++) { if (random.nextDouble() * 100 <= lootItem.getChance()) { successfulDrops++; @@ -56,31 +50,26 @@ public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { } if (successfulDrops > 0) { - // Create item just once per loot type ItemStack prototype = lootItem.createItemStack(random); if (prototype != null) { - // Total amount across all mobs int totalAmount = 0; for (int i = 0; i < successfulDrops; i++) { totalAmount += lootItem.generateAmount(random); } if (totalAmount > 0) { - // Add to consolidated map consolidatedLoot.merge(prototype, totalAmount, Integer::sum); } } } } - // Convert consolidated map to item stacks List finalLoot = new ArrayList<>(consolidatedLoot.size()); for (Map.Entry entry : consolidatedLoot.entrySet()) { ItemStack item = entry.getKey().clone(); item.setAmount(Math.min(entry.getValue(), item.getMaxStackSize())); finalLoot.add(item); - // Handle amounts exceeding max stack size int remaining = entry.getValue() - item.getMaxStackSize(); while (remaining > 0) { ItemStack extraStack = item.clone(); @@ -94,18 +83,12 @@ public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { } public void spawnLootToSpawner(SpawnerData spawner) { - // Try to acquire the lock, but don't block if it's already locked - // This ensures we don't block the server thread while waiting for the lock boolean lockAcquired = spawner.getLootGenerationLock().tryLock(); if (!lockAcquired) { - // Lock is already held, which means another loot generation is happening - // Skip this loot generation cycle return; } try { - // Acquire dataLock to safely read spawn timing and configuration values - // Use tryLock with short timeout to avoid blocking boolean dataLockAcquired = false; try { dataLockAcquired = spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS); @@ -115,17 +98,13 @@ public void spawnLootToSpawner(SpawnerData spawner) { } if (!dataLockAcquired) { - // dataLock is held (likely stack size change), skip this cycle return; } - // Declare variables outside the try block so they're accessible in the async lambda final long currentTime = System.currentTimeMillis(); final long spawnTime; - final EntityType entityType; final int minMobs; final int maxMobs; - final String spawnerId; final AtomicInteger usedSlots; final AtomicInteger maxSlots; @@ -137,115 +116,78 @@ public void spawnLootToSpawner(SpawnerData spawner) { return; } - // Get exact inventory slot usage usedSlots = new AtomicInteger(spawner.getVirtualInventory().getUsedSlots()); maxSlots = new AtomicInteger(spawner.getMaxSpawnerLootSlots()); - // Check if both inventory and exp are full, only then skip loot generation if (usedSlots.get() >= maxSlots.get() && spawner.getSpawnerExp() >= spawner.getMaxStoredExp()) { if (!spawner.getIsAtCapacity()) { spawner.setIsAtCapacity(true); } - return; // Skip generation if both exp and inventory are full + return; } - // Important: Store the current values we need for async processing - entityType = spawner.getEntityType(); minMobs = spawner.getMinMobs(); maxMobs = spawner.getMaxMobs(); - spawnerId = spawner.getSpawnerId(); - // Store currentTime to update lastSpawnTime after successful loot addition spawnTime = currentTime; } finally { spawner.getDataLock().unlock(); } - // Run heavy calculations async and batch updates using the Scheduler Scheduler.runTaskAsync(() -> { - // Generate loot with full mob count LootResult loot = generateLoot(minMobs, maxMobs, spawner); - // Only proceed if we generated something if (loot.getItems().isEmpty() && loot.getExperience() == 0) { return; } - // Switch back to main thread for Bukkit API calls using location-aware scheduling Scheduler.runLocationTask(spawner.getSpawnerLocation(), () -> { - // Re-acquire the lock for the update phase - // This ensures the spawner hasn't been modified (like stack size changes) - // between our async calculations and now - boolean updateLockAcquired = spawner.getLootGenerationLock().tryLock(); - if (!updateLockAcquired) { - // Lock is held, stack size is changing, skip this update - return; - } + boolean changed = false; - try { - // Modified approach: Handle items and exp separately - boolean changed = false; + if (loot.getExperience() > 0 && spawner.getSpawnerExp() < spawner.getMaxStoredExp()) { + int currentExp = spawner.getSpawnerExp(); + int maxExp = spawner.getMaxStoredExp(); + int newExp = Math.min(currentExp + loot.getExperience(), maxExp); - // Process experience if there's any to add and not at max - if (loot.getExperience() > 0 && spawner.getSpawnerExp() < spawner.getMaxStoredExp()) { - int currentExp = spawner.getSpawnerExp(); - int maxExp = spawner.getMaxStoredExp(); - int newExp = Math.min(currentExp + loot.getExperience(), maxExp); - - if (newExp != currentExp) { - spawner.setSpawnerExp(newExp); - changed = true; - } + if (newExp != currentExp) { + spawner.setSpawnerExp(newExp); + changed = true; } + } - // Re-check max slots as it could have changed - maxSlots.set(spawner.getMaxSpawnerLootSlots()); - usedSlots.set(spawner.getVirtualInventory().getUsedSlots()); - - // Process items if there are any to add and inventory isn't completely full - if (!loot.getItems().isEmpty() && usedSlots.get() < maxSlots.get()) { - List itemsToAdd = new ArrayList<>(loot.getItems()); + maxSlots.set(spawner.getMaxSpawnerLootSlots()); + usedSlots.set(spawner.getVirtualInventory().getUsedSlots()); - // Get exact calculation of slots with the new items - int totalRequiredSlots = calculateRequiredSlots(itemsToAdd, spawner.getVirtualInventory()); + if (!loot.getItems().isEmpty() && usedSlots.get() < maxSlots.get()) { + List itemsToAdd = new ArrayList<>(loot.getItems()); - // If we'll exceed the limit, limit the items we're adding - if (totalRequiredSlots > maxSlots.get()) { - itemsToAdd = limitItemsToAvailableSlots(itemsToAdd, spawner); - } + int totalRequiredSlots = calculateRequiredSlots(itemsToAdd, spawner.getVirtualInventory()); - if (!itemsToAdd.isEmpty()) { - spawner.addItemsAndUpdateSellValue(itemsToAdd); - changed = true; - } + if (totalRequiredSlots > maxSlots.get()) { + itemsToAdd = limitItemsToAvailableSlots(itemsToAdd, spawner); } - if (!changed) { - return; + if (!itemsToAdd.isEmpty()) { + spawner.addItemsAndUpdateSellValue(itemsToAdd); + changed = true; } + } - // Update spawn time only after successful loot addition - // This prevents skipped spawns when the lock fails - // Must acquire dataLock to safely update lastSpawnTime - boolean updateDataLockAcquired = spawner.getDataLock().tryLock(); - if (updateDataLockAcquired) { - try { - spawner.setLastSpawnTime(spawnTime); - } finally { - spawner.getDataLock().unlock(); - } - } - - // Check if spawner is now at capacity and update status if needed - spawner.updateCapacityStatus(); - - // Handle GUI updates in batches - handleGuiUpdates(spawner); + if (!changed) { + return; + } - // Mark for saving only once - spawnerManager.markSpawnerModified(spawner.getSpawnerId()); - } finally { - spawner.getLootGenerationLock().unlock(); + boolean updateDataLockAcquired = spawner.getDataLock().tryLock(); + if (updateDataLockAcquired) { + try { + spawner.setLastSpawnTime(spawnTime); + } finally { + spawner.getDataLock().unlock(); + } } + + spawner.updateCapacityStatus(); + handleGuiUpdates(spawner); + spawnerManager.markSpawnerModified(spawner.getSpawnerId()); }); }); } finally { @@ -257,55 +199,43 @@ private List limitItemsToAvailableSlots(List items, Spawne VirtualInventory currentInventory = spawner.getVirtualInventory(); int maxSlots = spawner.getMaxSpawnerLootSlots(); - // If already full, return empty list if (currentInventory.getUsedSlots() >= maxSlots) { return Collections.emptyList(); } - // Create a simulation inventory Map simulatedInventory = new HashMap<>(currentInventory.getConsolidatedItems()); List acceptedItems = new ArrayList<>(); - // Sort items by priority (you can change this sorting strategy) items.sort(Comparator.comparing(item -> item.getType().name())); for (ItemStack item : items) { if (item == null || item.getAmount() <= 0) continue; - // Add to simulation and check slot count Map tempSimulation = new HashMap<>(simulatedInventory); VirtualInventory.ItemSignature sig = new VirtualInventory.ItemSignature(item); tempSimulation.merge(sig, (long) item.getAmount(), Long::sum); - // Calculate slots needed int slotsNeeded = calculateSlots(tempSimulation); - // If we still have room, accept this item if (slotsNeeded <= maxSlots) { acceptedItems.add(item); - simulatedInventory = tempSimulation; // Update simulation + simulatedInventory = tempSimulation; } else { - // Try to accept a partial amount of this item int maxStackSize = item.getMaxStackSize(); long currentAmount = simulatedInventory.getOrDefault(sig, 0L); - // Calculate how many we can add without exceeding slot limit int remainingSlots = maxSlots - calculateSlots(simulatedInventory); if (remainingSlots > 0) { - // Maximum items we can add in the remaining slots long maxAddAmount = remainingSlots * maxStackSize - (currentAmount % maxStackSize); if (maxAddAmount > 0) { - // Create a partial item ItemStack partialItem = item.clone(); partialItem.setAmount((int) Math.min(maxAddAmount, item.getAmount())); acceptedItems.add(partialItem); - // Update simulation simulatedInventory.merge(sig, (long) partialItem.getAmount(), Long::sum); } } - // We've filled all slots, stop processing break; } } @@ -314,27 +244,22 @@ private List limitItemsToAvailableSlots(List items, Spawne } private int calculateSlots(Map items) { - // Use a more efficient calculation approach return items.entrySet().stream() .mapToInt(entry -> { long amount = entry.getValue(); int maxStackSize = entry.getKey().getTemplateRef().getMaxStackSize(); - // Use integer division with ceiling function return (int) ((amount + maxStackSize - 1) / maxStackSize); }) .sum(); } private int calculateRequiredSlots(List items, VirtualInventory inventory) { - // Create a temporary map to simulate how items would stack Map simulatedItems = new HashMap<>(); - // First, get existing items if we need to account for them if (inventory != null) { simulatedItems.putAll(inventory.getConsolidatedItems()); } - // Add the new items to our simulation for (ItemStack item : items) { if (item == null || item.getAmount() <= 0) continue; @@ -342,12 +267,10 @@ private int calculateRequiredSlots(List items, VirtualInventory inven simulatedItems.merge(sig, (long) item.getAmount(), Long::sum); } - // Calculate exact slots needed return calculateSlots(simulatedItems); } private void handleGuiUpdates(SpawnerData spawner) { - // Show particles if needed if (plugin.getConfig().getBoolean("particle.spawner_generate_loot", true)) { Location loc = spawner.getSpawnerLocation(); World world = loc.getWorld(); diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java index 6f7a39d3..7a1c2d27 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java @@ -241,8 +241,6 @@ private void updateStackSize(int newStackSize, boolean restartHopper) { transferItemsToNewInventory(currentItems, newInventory); this.virtualInventory = newInventory; - // Reset lastSpawnTime to prevent exploit where players break spawners to trigger immediate loot - this.lastSpawnTime = System.currentTimeMillis(); updateHologramData(); // Invalidate GUI cache when stack size changes