From f8567c14d47989fb9b78b3fca576fc924b0f7459 Mon Sep 17 00:00:00 2001 From: XxFran10xX Date: Wed, 28 Oct 2026 16:12:27 +0100 Subject: [PATCH 1/3] perf: count a guild's nodes once per upkeep total NodeManager.getTotalUpkeep(g) asked every node of g for its daily upkeep, and each node worked out its cost increase by counting g's nodes through a walk of every node: another nodes x nodes guild lookup, run whenever a ledger reads node upkeep (SimpleFactions income and the 5-minute chronicle export on Main). getTotalUpkeep now counts g's nodes once and passes the count to the new Node.getDailyUpkeep(int), which uses the same cost formula. Every node it adds belongs to g, so the count is the one each node would have found. getDailyUpkeep() keeps working on its own for single-node callers. Co-Authored-By: Claude Opus 5.5 --- .../tfminecraft/dowsing/managers/NodeManager.java | 4 +++- .../java/net/tfminecraft/dowsing/objects/Node.java | 12 +++++++++++- .../dowsing/managers/NodeManagerCyclesTest.java | 2 +- .../dowsing/objects/NodeBehaviorTest.java | 3 +++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index 73782a7..00c2dd1 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -106,10 +106,12 @@ public static boolean canPurchaseCapacity(Guild g) { public static double getTotalUpkeep(Guild g) { if (g == null || Node.held(g)) return 0; double total = 0; + // Every node of g pays the same cost increase, so count g's nodes once here. + int amount = getNodeAmount(g); for (Node n : nodes) { if (!n.getIsActive() || !n.hasGuild()) continue; if (!n.getGuild().getId().equalsIgnoreCase(g.getId())) continue; - total += n.getDailyUpkeep(); + total += n.getDailyUpkeep(amount); } return total; } diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index 3459489..a57aa76 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -81,9 +81,19 @@ public void setUpkeep(Double upkeep) { * since members joining or capacity being bought does not refresh every node. */ public double getDailyUpkeep() { + return dailyUpkeep(currentCostIncrease()); + } + /** + * {@link #getDailyUpkeep()} for a node whose guild is already known to hold {@code nodeAmount} + * nodes, so a guild total counts its nodes once instead of once per node. + */ + public double getDailyUpkeep(int nodeAmount) { + return dailyUpkeep(costIncreaseFor(nodeAmount, getCapacity())); + } + private double dailyUpkeep(double costIncrease) { double base = upkeep == null ? 0.0 : upkeep; return BigDecimal.valueOf(base) - .multiply(BigDecimal.valueOf(currentCostIncrease())) + .multiply(BigDecimal.valueOf(costIncrease)) .setScale(2, RoundingMode.HALF_UP) .doubleValue(); } diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java index 18d589b..ed461b3 100644 --- a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java @@ -75,7 +75,7 @@ void countsCapacityAndUpkeepRespectOwnershipSpecialNodesAndPurchasedLimits() { assertEquals(0, NodeManager.getExtraCapacity(null)); assertFalse(NodeManager.canPurchaseCapacity(null)); assertEquals(0, NodeManager.getTotalUpkeep(null)); assertEquals(0, NodeManager.getNodeAmount(null)); assertEquals(1, NodeManager.getNodeCapacity(null)); assertEquals(1, NodeManager.getNodeCapacity(f.guild)); - NodeManager.nodes.add(f.node); when(f.node.getIsActive()).thenReturn(true); when(f.node.getDailyUpkeep()).thenReturn(2.25); + NodeManager.nodes.add(f.node); when(f.node.getIsActive()).thenReturn(true); when(f.node.getDailyUpkeep(anyInt())).thenReturn(2.25); Node inactive = f.nodeAt(f.location), unowned = f.nodeAt(f.location), other = f.nodeAt(f.location); when(unowned.hasGuild()).thenReturn(false); when(unowned.getIsActive()).thenReturn(true); Guild otherGuild = mock(Guild.class); when(otherGuild.getId()).thenReturn("other"); when(other.getGuild()).thenReturn(otherGuild); when(other.getIsActive()).thenReturn(true); diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java index a2e2e66..c0286bb 100644 --- a/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java @@ -237,8 +237,11 @@ void upkeepRoundsHalfUpAndRecalculatesFromLiveGuildCapacity() { node.setUpkeep(0.29); node.setCostIncrease(1.5); assertEquals(0.44, node.getDailyUpkeep()); node.setGuild(f.guild); NodeManager.nodes.add(node); NodeManager.nodes.add(f.owned()); assertEquals(1.5, node.currentCostIncrease()); assertEquals(0.44, node.getDailyUpkeep()); + assertEquals(node.getDailyUpkeep(), node.getDailyUpkeep(NodeManager.getNodeAmount(f.guild))); + assertEquals(0.58, node.getDailyUpkeep(3), "a counted amount gives the same cost formula"); Cache.extraCapacity = true; Cache.membersPerCapacity = 2; Cache.maxMemberCapacity = 5; assertEquals(2, node.getCapacity()); assertEquals(0.29, node.getDailyUpkeep()); + assertEquals(node.getDailyUpkeep(), node.getDailyUpkeep(NodeManager.getNodeAmount(f.guild))); Cache.extraCapacityCost = 12.345; NodeManager.extraCapacityByGuild.put("guild", 2); assertEquals(37.04, node.getNodeCapacityCost()); From 12ffdee23dd18f52bbe3f909f9de44a1a5cf7318 Mon Sep 17 00:00:00 2001 From: XxFran10xX Date: Wed, 28 Oct 2026 16:16:48 +0100 Subject: [PATCH 2/3] fix: count lazily, and keep the stored cost for unresolved guilds The full build caught NodeUpkeepTest: getTotalUpkeep counted g's nodes before the loop, even when no node of g needed the count, and getDailyUpkeep(int) always used the counted cost increase. The old per-node path used the node's stored cost increase when its guild does not resolve and only counted for nodes that reached it. Node.getDailyUpkeep(IntSupplier) now falls back to getDailyUpkeep() when the guild does not resolve, and getTotalUpkeep passes a memoised count that walks the nodes at most once, on first use. NodeUpkeepTest passes unchanged. Co-Authored-By: Claude Opus 5.5 --- .../net/tfminecraft/dowsing/managers/NodeManager.java | 8 +++++--- .../java/net/tfminecraft/dowsing/objects/Node.java | 11 +++++++---- .../dowsing/managers/NodeManagerCyclesTest.java | 2 +- .../tfminecraft/dowsing/objects/NodeBehaviorTest.java | 6 +++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index 00c2dd1..1caebec 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -106,12 +106,14 @@ public static boolean canPurchaseCapacity(Guild g) { public static double getTotalUpkeep(Guild g) { if (g == null || Node.held(g)) return 0; double total = 0; - // Every node of g pays the same cost increase, so count g's nodes once here. - int amount = getNodeAmount(g); + // Every node of g pays the same cost increase, so count g's nodes at most once, and only + // when a node first needs the count. + int[] amount = {-1}; + java.util.function.IntSupplier count = () -> amount[0] >= 0 ? amount[0] : (amount[0] = getNodeAmount(g)); for (Node n : nodes) { if (!n.getIsActive() || !n.hasGuild()) continue; if (!n.getGuild().getId().equalsIgnoreCase(g.getId())) continue; - total += n.getDailyUpkeep(amount); + total += n.getDailyUpkeep(count); } return total; } diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index a57aa76..2fa348d 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.IntSupplier; import org.apache.commons.lang.WordUtils; import org.bukkit.Bukkit; @@ -84,11 +85,13 @@ public double getDailyUpkeep() { return dailyUpkeep(currentCostIncrease()); } /** - * {@link #getDailyUpkeep()} for a node whose guild is already known to hold {@code nodeAmount} - * nodes, so a guild total counts its nodes once instead of once per node. + * {@link #getDailyUpkeep()} with the guild's node count supplied by the caller, so a guild + * total counts its nodes once instead of once per node. The count is only asked for when this + * node's guild resolves, exactly when {@link #currentCostIncrease()} would count. */ - public double getDailyUpkeep(int nodeAmount) { - return dailyUpkeep(costIncreaseFor(nodeAmount, getCapacity())); + public double getDailyUpkeep(IntSupplier nodeAmount) { + if(resolveGuild() == null) return getDailyUpkeep(); + return dailyUpkeep(costIncreaseFor(nodeAmount.getAsInt(), getCapacity())); } private double dailyUpkeep(double costIncrease) { double base = upkeep == null ? 0.0 : upkeep; diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java index ed461b3..3a64c60 100644 --- a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerCyclesTest.java @@ -75,7 +75,7 @@ void countsCapacityAndUpkeepRespectOwnershipSpecialNodesAndPurchasedLimits() { assertEquals(0, NodeManager.getExtraCapacity(null)); assertFalse(NodeManager.canPurchaseCapacity(null)); assertEquals(0, NodeManager.getTotalUpkeep(null)); assertEquals(0, NodeManager.getNodeAmount(null)); assertEquals(1, NodeManager.getNodeCapacity(null)); assertEquals(1, NodeManager.getNodeCapacity(f.guild)); - NodeManager.nodes.add(f.node); when(f.node.getIsActive()).thenReturn(true); when(f.node.getDailyUpkeep(anyInt())).thenReturn(2.25); + NodeManager.nodes.add(f.node); when(f.node.getIsActive()).thenReturn(true); when(f.node.getDailyUpkeep(any(java.util.function.IntSupplier.class))).thenReturn(2.25); Node inactive = f.nodeAt(f.location), unowned = f.nodeAt(f.location), other = f.nodeAt(f.location); when(unowned.hasGuild()).thenReturn(false); when(unowned.getIsActive()).thenReturn(true); Guild otherGuild = mock(Guild.class); when(otherGuild.getId()).thenReturn("other"); when(other.getGuild()).thenReturn(otherGuild); when(other.getIsActive()).thenReturn(true); diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java index c0286bb..4a76b36 100644 --- a/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java @@ -237,11 +237,11 @@ void upkeepRoundsHalfUpAndRecalculatesFromLiveGuildCapacity() { node.setUpkeep(0.29); node.setCostIncrease(1.5); assertEquals(0.44, node.getDailyUpkeep()); node.setGuild(f.guild); NodeManager.nodes.add(node); NodeManager.nodes.add(f.owned()); assertEquals(1.5, node.currentCostIncrease()); assertEquals(0.44, node.getDailyUpkeep()); - assertEquals(node.getDailyUpkeep(), node.getDailyUpkeep(NodeManager.getNodeAmount(f.guild))); - assertEquals(0.58, node.getDailyUpkeep(3), "a counted amount gives the same cost formula"); + assertEquals(node.getDailyUpkeep(), node.getDailyUpkeep(() -> NodeManager.getNodeAmount(f.guild))); + assertEquals(0.58, node.getDailyUpkeep(() -> 3), "a counted amount gives the same cost formula"); Cache.extraCapacity = true; Cache.membersPerCapacity = 2; Cache.maxMemberCapacity = 5; assertEquals(2, node.getCapacity()); assertEquals(0.29, node.getDailyUpkeep()); - assertEquals(node.getDailyUpkeep(), node.getDailyUpkeep(NodeManager.getNodeAmount(f.guild))); + assertEquals(node.getDailyUpkeep(), node.getDailyUpkeep(() -> NodeManager.getNodeAmount(f.guild))); Cache.extraCapacityCost = 12.345; NodeManager.extraCapacityByGuild.put("guild", 2); assertEquals(37.04, node.getNodeCapacityCost()); From 89cf740a6cafa0cc909514b27bba8ecf25565b13 Mon Sep 17 00:00:00 2001 From: XxFran10xX Date: Wed, 28 Oct 2026 16:19:21 +0100 Subject: [PATCH 3/3] fix: read the base upkeep before the cost increase, as before Sol review: the counted upkeep evaluated the node count before turning the base upkeep into a BigDecimal, so a bad upkeep value (NaN) with a failing count would surface a different exception than before. Both upkeep methods now take the base first, then resolve the guild, then count, which is the old order, and the unresolved fallback uses the stored cost increase directly so the guild is resolved once. Co-Authored-By: Claude Opus 5.5 --- .../net/tfminecraft/dowsing/objects/Node.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index 2fa348d..f9c2a96 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -82,28 +82,34 @@ public void setUpkeep(Double upkeep) { * since members joining or capacity being bought does not refresh every node. */ public double getDailyUpkeep() { - return dailyUpkeep(currentCostIncrease()); + BigDecimal base = baseUpkeep(); + return dailyUpkeep(base, currentCostIncrease()); } /** * {@link #getDailyUpkeep()} with the guild's node count supplied by the caller, so a guild - * total counts its nodes once instead of once per node. The count is only asked for when this - * node's guild resolves, exactly when {@link #currentCostIncrease()} would count. + * total counts its nodes once instead of once per node. Same steps in the same order: the + * base upkeep, then the guild, then the count, which is only asked for when the guild resolves. */ public double getDailyUpkeep(IntSupplier nodeAmount) { - if(resolveGuild() == null) return getDailyUpkeep(); - return dailyUpkeep(costIncreaseFor(nodeAmount.getAsInt(), getCapacity())); + BigDecimal base = baseUpkeep(); + if(resolveGuild() == null) return dailyUpkeep(base, storedCostIncrease()); + return dailyUpkeep(base, costIncreaseFor(nodeAmount.getAsInt(), getCapacity())); } - private double dailyUpkeep(double costIncrease) { - double base = upkeep == null ? 0.0 : upkeep; - return BigDecimal.valueOf(base) - .multiply(BigDecimal.valueOf(costIncrease)) + private BigDecimal baseUpkeep() { + return BigDecimal.valueOf(upkeep == null ? 0.0 : upkeep); + } + private static double dailyUpkeep(BigDecimal base, double costIncrease) { + return base.multiply(BigDecimal.valueOf(costIncrease)) .setScale(2, RoundingMode.HALF_UP) .doubleValue(); } double currentCostIncrease() { - if(resolveGuild() == null) return costIncrease == null ? 1.0 : costIncrease; + if(resolveGuild() == null) return storedCostIncrease(); return costIncreaseFor(NodeManager.getNodeAmount(this.guild), getCapacity()); } + private double storedCostIncrease() { + return costIncrease == null ? 1.0 : costIncrease; + } static double costIncreaseFor(int nodeAmount, int capacity) { int multiplier = Math.max(1, 1+nodeAmount-capacity); return 1.0+((multiplier-1.0)*0.5);