diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index 17d5502..73782a7 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; import org.apache.commons.lang.WordUtils; import org.bukkit.Bukkit; @@ -116,6 +117,7 @@ public static double getTotalUpkeep(Guild g) { public static Integer getNodeAmount(Guild g) { Integer i = 0; if(g == null) return i; + if(passCounts != null) return g.getId() == null ? i : passCounts.getOrDefault(g.getId(), i); for(Node n : nodes) { if(!n.hasGuild()) continue; if(n.getBlock().isSpecial()) continue; @@ -123,6 +125,34 @@ public static Integer getNodeAmount(Guild g) { } return i; } + /** + * Claimed-node counts per guild for one particle pass. The pass checks every claimed node four + * times a second, and each check counted its guild's nodes by walking every node, so a pass cost + * nodes x nodes guild lookups. Counted once per pass instead, and kept right by {@link #released}. + */ + private static Map passCounts; + + static void beginCountPass() { + Map counts = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for(Node n : nodes) { + if(!n.hasGuild()) continue; + if(n.getBlock().isSpecial()) continue; + String id = n.getGuild().getId(); + if(id != null) counts.merge(id, 1, Integer::sum); + } + passCounts = counts; + } + + static void endCountPass() { + passCounts = null; + } + + /** A node of {@code g} was just let go during a particle pass, so its guild holds one fewer. */ + public static void released(Guild g, Node n) { + if(passCounts == null || g == null || g.getId() == null || n.getBlock().isSpecial()) return; + passCounts.computeIfPresent(g.getId(), (id, count) -> count - 1); + } + public static Integer getNodeCapacity(Guild g) { int capacity = 1; if(g == null) return capacity; @@ -211,14 +241,19 @@ public void particleCycle(){ public void run() { if (!DowsingMain.plugin.nodesReady()) return; - for(Node n : nodes){ - if(n.isClaimable()){ - Location loc = n.getLoc().clone().add(0.5, 1, 0.5); - loc.getWorld().spawnParticle(Particle.HAPPY_VILLAGER, loc, 10); - } else { - n.check(); + beginCountPass(); + try { + for(Node n : nodes){ + if(n.isClaimable()){ + Location loc = n.getLoc().clone().add(0.5, 1, 0.5); + loc.getWorld().spawnParticle(Particle.HAPPY_VILLAGER, loc, 10); + } else { + n.check(); + } } - } + } finally { + endCountPass(); + } } }.runTaskTimer(DowsingMain.plugin, 0L, 5L); } diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index 6eef8e3..3459489 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -403,6 +403,7 @@ public void check() { p.closeInventory(); } guild = null; + NodeManager.released(g, this); NodeManager.requestNodeBenefitSync(); } diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java index a8b9322..18d589b 100644 --- a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java @@ -159,6 +159,53 @@ void cachingReloadsBlockTypeAndPreviouslySelectedProductionMethods() { NodeManager.nodes.clear(); f.manager.cacheNodes(); assertTrue(f.manager.cached.isEmpty()); } + @Test + void aParticlePassCountsEachGuildOnceAndTracksNodesLetGo() { + Node second = f.nodeAt(f.location), unowned = f.nodeAt(f.location), other = f.nodeAt(f.location), special = f.nodeAt(f.location), noId = f.nodeAt(f.location); + when(unowned.hasGuild()).thenReturn(false); + Guild otherGuild = mock(Guild.class); when(otherGuild.getId()).thenReturn("OTHER"); when(other.getGuild()).thenReturn(otherGuild); + NodeBlock specialBlock = mock(NodeBlock.class); when(specialBlock.isSpecial()).thenReturn(true); when(special.getBlock()).thenReturn(specialBlock); + Guild idless = mock(Guild.class); when(noId.getGuild()).thenReturn(idless); + NodeManager.nodes.addAll(List.of(f.node, second, unowned, other, special)); + assertEquals(2, NodeManager.getNodeAmount(f.guild)); + NodeManager.nodes.add(noId); // the live walk throws on a guild without an id; a pass skips it + NodeManager.beginCountPass(); + try { + assertEquals(2, NodeManager.getNodeAmount(f.guild)); assertEquals(1, NodeManager.getNodeAmount(otherGuild)); + assertEquals(0, NodeManager.getNodeAmount(idless)); assertEquals(0, NodeManager.getNodeAmount(null)); + when(f.node.hasGuild()).thenReturn(false); + assertEquals(2, NodeManager.getNodeAmount(f.guild), "counted once at the start of the pass"); + NodeManager.released(f.guild, f.node); assertEquals(1, NodeManager.getNodeAmount(f.guild)); + NodeManager.released(f.guild, special); NodeManager.released(null, f.node); NodeManager.released(idless, f.node); + Guild unknown = mock(Guild.class); when(unknown.getId()).thenReturn("unknown"); NodeManager.released(unknown, f.node); + assertEquals(1, NodeManager.getNodeAmount(f.guild)); assertEquals(0, NodeManager.getNodeAmount(unknown)); + } finally { + NodeManager.endCountPass(); + } + NodeManager.nodes.remove(noId); NodeManager.released(f.guild, second); + assertEquals(1, NodeManager.getNodeAmount(f.guild), "live again once the pass ends"); + } + + @Test + void aRealCheckThatLetsANodeGoLowersItsGuildsCountForTheRestOfThePass() { + Cache.extraCapacity = false; + Node first = new Node(UUID.randomUUID(), f.block, f.location, f.guild, true, 1, 0, f.type, 60, 0, 25); + Node second = new Node(UUID.randomUUID(), f.block, f.location, f.guild, true, 1, 0, f.type, 60, 0, 25); + NodeManager.nodes.addAll(List.of(first, second)); + assertEquals(1, NodeManager.getNodeCapacity(f.guild)); + NodeManager.beginCountPass(); + try { + assertEquals(2, NodeManager.getNodeAmount(f.guild)); + first.check(); + assertTrue(first.isClaimable(), "over capacity, so the first node is let go"); + assertEquals(1, NodeManager.getNodeAmount(f.guild), "Node.check lowers the pass count"); + second.check(); + assertFalse(second.isClaimable(), "back within capacity, so the second node stays"); + } finally { + NodeManager.endCountPass(); + } + } + @Test void timersUseTheirIntendedCadenceAndRunParticlesChecksAndEfficiencyGrowth() { Node claimed = f.node, unclaimed = f.nodeAt(f.location);