diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index 7fae36c..1fa0e18 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -159,24 +159,24 @@ public void run() { validate(); for(Node n : nodes) { + try { if(!n.getIsActive()) continue; if(!n.hasGuild()) continue; if(n.getLoc().getChunk().isForceLoaded() == false) { n.getLoc().getChunk().setForceLoaded(true); } + if(n.getTimeLeft() <= 0) { + if(!new ItemDropper().dropItems(n)) continue; + n.setTimeLeft(n.getModifiedTime()); + n.setInputCounter(0); + } n.tickCycle(); if(n.getCycleTime().equals(Cache.cycleLength)) { n.setCycleTime(0); n.input(); } - if(n.getTimeLeft() > 0) { - n.tick(); - } else { - ItemDropper dropper = new ItemDropper(); - dropper.dropItems(n); - n.setTimeLeft(n.getModifiedTime()); - n.setInputCounter(0); - } + if(!n.getIsActive()) continue; + n.tick(); for(Player p : Bukkit.getOnlinePlayers()) { if(currentNode.containsKey(p)) { if(currentNode.get(p).getId().equals(n.getId())) { @@ -190,6 +190,10 @@ public void run() } } } + } catch(RuntimeException ex) { + DowsingMain.plugin.getLogger().log(java.util.logging.Level.SEVERE, + "Failed to process node " + n.getId() + " at " + n.getLoc(), ex); + } } } }.runTaskTimer(DowsingMain.plugin, 0L, 1200L); diff --git a/src/main/java/net/tfminecraft/dowsing/utils/ItemDropper.java b/src/main/java/net/tfminecraft/dowsing/utils/ItemDropper.java index 53f2bfb..ed6e2cb 100644 --- a/src/main/java/net/tfminecraft/dowsing/utils/ItemDropper.java +++ b/src/main/java/net/tfminecraft/dowsing/utils/ItemDropper.java @@ -1,5 +1,12 @@ package net.tfminecraft.dowsing.utils; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.function.Function; +import java.util.function.Consumer; + import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.entity.Item; @@ -9,8 +16,36 @@ import net.tfminecraft.dowsing.objects.Node; public class ItemDropper { - public void dropItems(Node n) { - n.getLastResult().clear(); + private static final Set unavailableRewardsLogged = new HashSet<>(); + private final Function itemFactory; + private final Consumer warning; + + public ItemDropper() { + this(new ItemCreator()::getItemFromPath, message -> org.bukkit.Bukkit.getLogger().warning(message)); + } + + ItemDropper(Function itemFactory, Consumer warning) { + this.itemFactory = itemFactory; + this.warning = warning; + } + + /** Returns false when a configured reward cannot be created, so the cycle can retry. */ + public boolean dropItems(Node n) { + Map items = new HashMap<>(); + for(String key : n.getCompleteDrop().keySet()) { + if(key.equalsIgnoreCase("nothing")) continue; + ItemStack item = itemFactory.apply(key); + if(item == null) { + if(unavailableRewardsLogged.add(key)) { + warning.accept("[Dowsing] Cannot complete node " + n.getId() + + ": reward item is unavailable: " + key); + } + return false; + } + unavailableRewardsLogged.remove(key); + items.put(key, item); + } + Map result = new HashMap<>(); Double maxWeight = 0.0; for(String key : n.getCompleteDrop().keySet()) { maxWeight = maxWeight+n.getCompleteDrop().get(key); @@ -30,15 +65,7 @@ public void dropItems(Node n) { if(random <= max && random > previous) { dropped++; if(!key.equalsIgnoreCase("nothing")) { - Integer a = 1; - if(n.getLastResult().containsKey(key)) { - a = 1+n.getLastResult().get(key); - } - n.getLastResult().put(key, a); - Location loc = new Location(n.getLoc().getWorld(), n.getLoc().getX(), n.getLoc().getY(), n.getLoc().getZ()); - loc.add(0.5,2,0.5); - dropItem(loc, key, true); - loc.getWorld().playSound(loc, Sound.ENTITY_ITEM_PICKUP, 0.5f, 1f); + result.merge(key, 1, Integer::sum); } break; } @@ -46,6 +73,16 @@ public void dropItems(Node n) { } } n.update(); + Location loc = n.getLoc().clone().add(0.5, 2, 0.5); + for(Map.Entry entry : result.entrySet()) { + for(int count = 0; count < entry.getValue(); count++) { + Item e = loc.getWorld().dropItem(loc, items.get(entry.getKey()).clone()); + e.setVelocity(new Vector()); + loc.getWorld().playSound(loc, Sound.ENTITY_ITEM_PICKUP, 0.5f, 1f); + } + } + n.setLastResult(result); + return true; } public void dropItem(Location loc, String path, Boolean noV) { ItemCreator ic = new ItemCreator(); diff --git a/src/test/java/net/tfminecraft/dowsing/utils/ItemDropperTest.java b/src/test/java/net/tfminecraft/dowsing/utils/ItemDropperTest.java new file mode 100644 index 0000000..11649d3 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/utils/ItemDropperTest.java @@ -0,0 +1,41 @@ +package net.tfminecraft.dowsing.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.bukkit.inventory.ItemStack; + +import net.tfminecraft.dowsing.objects.Node; + +class ItemDropperTest { + @Test + void unavailableRewardKeepsCyclePendingAndLastResultIntact() { + Node node = mock(Node.class); + when(node.getId()).thenReturn(UUID.randomUUID()); + Map rewards = new LinkedHashMap<>(); + rewards.put("first.item", 100.0); + rewards.put("missing.item", 100.0); + when(node.getCompleteDrop()).thenReturn(rewards); + Map previous = new HashMap<>(Map.of("old.item", 2)); + when(node.getLastResult()).thenReturn(previous); + + ItemStack first = mock(ItemStack.class); + assertFalse(new ItemDropper(path -> path.equals("first.item") ? first : null, + message -> {}).dropItems(node)); + assertEquals(Map.of("old.item", 2), previous); + verify(node, never()).getLoc(); + verify(node, never()).update(); + verify(node, never()).setLastResult(any()); + } +}