diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index a1db0ff..78717b9 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -208,11 +208,20 @@ public static void released(Guild g, Node n) { passCounts.computeIfPresent(g.getId(), (id, count) -> count - 1); } + /** Room for new nodes, from the guild's active members. */ public static Integer getNodeCapacity(Guild g) { + return capacityFor(g, net.tfminecraft.dowsing.utils.ActiveMembers.count(g)); + } + + /** Room to keep the nodes a guild already holds, from all its members, so going inactive does not cost a node. */ + public static Integer getHeldNodeCapacity(Guild g) { + return capacityFor(g, g == null || g.getMembers() == null ? 0 : g.getMembers().size()); + } + + private static Integer capacityFor(Guild g, int members) { int capacity = 1; if(g == null) return capacity; if(Cache.extraCapacity) { - int members = g.getMembers().size(); int added = (int) Math.floorDiv(members, Cache.membersPerCapacity); if(added > Cache.maxMemberCapacity) { added = Cache.maxMemberCapacity; @@ -450,8 +459,8 @@ private void tryCreateNode(Player p, Location loc, NodeBlock b, Cancellable e) { e.setCancelled(true); return; } - if(g.getMembers().size() < Cache.minMembersForNode && !b.isSpecial()){ - p.sendMessage("§cYou need at least "+Cache.minMembersForNode+" members in your guild to have a node!"); + if(net.tfminecraft.dowsing.utils.ActiveMembers.count(g) < Cache.minMembersForNode && !b.isSpecial()){ + p.sendMessage("§cYou need at least "+Cache.minMembersForNode+" active members in your guild to have a node!"); e.setCancelled(true); return; } diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index f9c2a96..c16bdbd 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -307,8 +307,9 @@ public boolean canHold(Guild g){ if(held(g)) return true; Player p = Members.player(g.getLeader()); if(p != null && p.isOnline() && Permissions.isAdmin(p)) return true; + // Holding counts every member, active or not; only new claims need active ones. if(g.getMembers().size() < Cache.minMembersForNode) return false; - if(NodeManager.getNodeAmount(g)-NodeManager.getNodeCapacity(g) > 0) return false; + if(NodeManager.getNodeAmount(g)-NodeManager.getHeldNodeCapacity(g) > 0) return false; return true; } @@ -328,8 +329,8 @@ public static boolean held(Guild g) { public boolean canClaim(Player p, Guild g){ if(Permissions.isAdmin(p)) return true; if(!isClaimable()) return false; - if(g.getMembers().size() < Cache.minMembersForNode){ - p.sendMessage("§cYou need at least "+Cache.minMembersForNode+" members in your guild to claim this node!"); + if(net.tfminecraft.dowsing.utils.ActiveMembers.count(g) < Cache.minMembersForNode){ + p.sendMessage("§cYou need at least "+Cache.minMembersForNode+" active members in your guild to claim this node!"); return false; } if(NodeManager.getNodeAmount(g)-NodeManager.getNodeCapacity(g) >= 0 && !block.isSpecial()) { @@ -584,14 +585,14 @@ public void deActivate() { public void growEfficiency() { // A held realm has no members, which would cap its nodes' efficiency at 0 until it is taken on. if(resolveGuild() == null || held(guild)) return; - int members = guild.getMembers().size(); + int members = net.tfminecraft.dowsing.utils.ActiveMembers.count(guild); double growth = Math.min(1.0, members*Cache.efficiencyGrowthPerMember); updateEfficiency(growth); } public double getMaxEfficiency() { if(resolveGuild() == null) return 0; - return Math.min(100.0, guild.getMembers().size()*Cache.maxEfficiencyPerMember); + return Math.min(100.0, net.tfminecraft.dowsing.utils.ActiveMembers.count(guild)*Cache.maxEfficiencyPerMember); } public void updateEfficiency(double eff) { diff --git a/src/main/java/net/tfminecraft/dowsing/utils/ActiveMembers.java b/src/main/java/net/tfminecraft/dowsing/utils/ActiveMembers.java new file mode 100644 index 0000000..615da84 --- /dev/null +++ b/src/main/java/net/tfminecraft/dowsing/utils/ActiveMembers.java @@ -0,0 +1,31 @@ +package net.tfminecraft.dowsing.utils; + +import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.inactivity.InactivityService; + +/** + * The guild members who count toward nodes. SimpleFactions marks a member inactive after + * weeks offline; they stay in the guild but add no node capacity, do not meet the minimum + * member count and do not raise node efficiency until they log in again. + */ +public final class ActiveMembers { + private ActiveMembers() {} + + public static int count(Guild guild) { + if (guild == null || guild.getMembers() == null) return 0; + int active = 0; + for (String member : guild.getMembers()) { + if (!inactive(member)) active++; + } + return active; + } + + /** A member whose inactivity cannot be read counts, as before inactivity was checked. */ + static boolean inactive(String member) { + try { + return InactivityService.isMemberInactive(member); + } catch (LinkageError | RuntimeException unreadable) { + return false; + } + } +} diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerEventsTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerEventsTest.java index 4dbfa0e..487a829 100644 --- a/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerEventsTest.java +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeManagerEventsTest.java @@ -97,7 +97,7 @@ void placementRequiresGuildMembershipMinimumMembersAndAvailableCapacity() { verify(f.player).sendMessage(contains("need to have a guild")); f.factions.when(() -> FactionManager.getGuildByMember(f.player)).thenReturn(f.guild); Cache.minMembersForNode = 3; event = f.place(); f.manager.placeVanillaNode(event); - verify(event).setCancelled(true); verify(f.player).sendMessage(contains("at least 3 members")); + verify(event).setCancelled(true); verify(f.player).sendMessage(contains("at least 3 active members")); Cache.minMembersForNode = 1; NodeManager.nodes.add(f.nodeAt(f.remoteLocation())); event = f.place(); f.manager.placeVanillaNode(event); verify(event).setCancelled(true); verify(f.player).sendMessage(contains("filled its node capacity")); diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java index 4a76b36..d9e6511 100644 --- a/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeBehaviorTest.java @@ -138,7 +138,7 @@ void claimAndRetentionEnforceMembershipAndCapacityWithAdminAndSpecialExceptions( Cache.minMembersForNode = 3; assertFalse(node.canHold(null)); assertFalse(node.canHold(f.guild)); assertFalse(node.canClaim(player, f.guild)); - verify(player).sendMessage(contains("at least 3 members")); + verify(player).sendMessage(contains("at least 3 active members")); Cache.minMembersForNode = 1; assertTrue(node.canHold(f.guild)); assertTrue(node.canClaim(player, f.guild)); Node owned = f.owned(); NodeManager.nodes.add(owned); diff --git a/src/test/java/net/tfminecraft/dowsing/utils/ActiveMembersTest.java b/src/test/java/net/tfminecraft/dowsing/utils/ActiveMembersTest.java new file mode 100644 index 0000000..fd68d07 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/utils/ActiveMembersTest.java @@ -0,0 +1,80 @@ +package net.tfminecraft.dowsing.utils; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import net.tfminecraft.dowsing.Cache; +import net.tfminecraft.dowsing.managers.NodeManager; +import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.inactivity.InactivityService; + +class ActiveMembersTest { + private MockedStatic inactivity; + private Boolean previousExtra; + private Integer previousPer; + private int previousMax; + private Map previousBought; + + @BeforeEach void setUp() { + inactivity = mockStatic(InactivityService.class); + inactivity.when(() -> InactivityService.isMemberInactive(anyString())) + .thenAnswer(call -> call.getArgument(0, String.class).startsWith("away")); + previousExtra = Cache.extraCapacity; previousPer = Cache.membersPerCapacity; previousMax = Cache.maxMemberCapacity; + previousBought = new HashMap<>(NodeManager.extraCapacityByGuild); NodeManager.extraCapacityByGuild.clear(); + } + + @AfterEach void tearDown() { + inactivity.close(); + Cache.extraCapacity = previousExtra; Cache.membersPerCapacity = previousPer; Cache.maxMemberCapacity = previousMax; + NodeManager.extraCapacityByGuild.clear(); NodeManager.extraCapacityByGuild.putAll(previousBought); + } + + private static Guild guild(String... members) { + Guild guild = mock(Guild.class); + when(guild.getId()).thenReturn("guild"); + when(guild.getMembers()).thenReturn(new ArrayList<>(List.of(members))); + return guild; + } + + @Test + void inactiveMembersAreLeftOutOfTheCount() { + assertEquals(2, ActiveMembers.count(guild("ann", "away1", "bob", "away2"))); + assertEquals(0, ActiveMembers.count(guild("away1"))); + assertEquals(0, ActiveMembers.count(null)); + Guild noRoster = mock(Guild.class); + assertEquals(0, ActiveMembers.count(noRoster)); + } + + @Test + void aMemberWhoseInactivityCannotBeReadStillCounts() { + inactivity.when(() -> InactivityService.isMemberInactive("broken")).thenThrow(new IllegalStateException("unreadable")); + inactivity.when(() -> InactivityService.isMemberInactive("old")).thenThrow(new NoSuchMethodError("older SimpleFactions")); + assertEquals(3, ActiveMembers.count(guild("broken", "old", "ann", "away1"))); + } + + @Test + void nodeCapacityGrowsOnlyWithActiveMembers() { + Cache.extraCapacity = true; Cache.membersPerCapacity = 2; Cache.maxMemberCapacity = 10; + assertEquals(2, NodeManager.getNodeCapacity(guild("ann", "bob", "away1", "away2", "away3"))); + assertEquals(3, NodeManager.getNodeCapacity(guild("ann", "bob", "cid", "dan", "away1"))); + } + + @Test + void nodesAlreadyHeldAreKeptOnEveryMember() { + Cache.extraCapacity = true; Cache.membersPerCapacity = 2; Cache.maxMemberCapacity = 10; + assertEquals(3, NodeManager.getHeldNodeCapacity(guild("ann", "bob", "away1", "away2", "away3"))); + assertEquals(1, NodeManager.getHeldNodeCapacity(null)); + assertEquals(1, NodeManager.getHeldNodeCapacity(mock(Guild.class))); + } +}