Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,13 @@ public List<LevyEntry> getLevies() {
subjectLevies = subject.getMilitary().getLevies();
}
int total = 0;
int activeMembers = net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(subject.getMembers()).size();
for(Regiment r : subject.getMilitary().getRegiments()) {
if (r.isEquipment()) continue;
int count = r.getCurrentSlots();
count = (int) Math.round(count * (subject.getModifier(FactionModifiers.LEVY).getAmount()/100));
if(total+count >= subject.getMembers().size()) {
count = subject.getMembers().size() - total;
if(total+count >= activeMembers) {
count = activeMembers - total;
}
count = Math.max(0, count);
total += count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,15 @@ public static boolean eligible(Faction faction, String member) {
return member != null && (faction.isMember(member) || faction.isLeader(member));
}

/** An inactive Spymaster gives up the post; the leader keeps the default holding. */
private static boolean serving(Faction faction, String member) {
return eligible(faction, member)
&& (faction.isLeader(member) || !net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(member));
}

/** The leader's default holding follows the leadership; deliberate appointments stay. */
private static boolean validHolder(Faction faction, SpecialPositionAssignment holder) {
return holder != null && eligible(faction, holder.memberKey()) && !deadCharacter(holder)
return holder != null && serving(faction, holder.memberKey()) && !deadCharacter(holder)
&& !(holder.automatic && !faction.isLeader(holder.memberKey()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public boolean toggleRefuse(String name) {
public boolean canBeMember(String name, boolean ignoreSize, boolean ignoreRefuses) {
if(members.contains(name)) return false;
if(f.getLeader().equals(name)) return false;
if(net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(name)) return false;
if(!ignoreSize && getCurrentSize() >= getMaxSize()) return false;
if(!ignoreRefuses && refuses.contains(name)) return false;
if(f.getOrCreateMainGuild().isMember(name)) return true;
Expand All @@ -150,6 +151,9 @@ public boolean canRemainMember(String name) {
if (f.getLeader().equals(name)) {
return false;
}
if (net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(name)) {
return false;
}
if (f.getOrCreateMainGuild().isMember(name)) {
return true;
}
Expand Down Expand Up @@ -271,7 +275,9 @@ private void cleanupCouncil() {
}
}
for(String refuser : new ArrayList<>(refuses)) {
if(!canBeMember(refuser, true, true)) {
// An inactive member keeps their refusal for when they come back.
if(!canBeMember(refuser, true, true)
&& !net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(refuser)) {
refuses.remove(refuser);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ public boolean canBeCandidate(Candidate type, String player, boolean includeAlre
// Must be faction member (any guild)
if (!gov.getFaction().isMember(player))
return false;
if (net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(player))
return false;
Guild g = gov.getFaction().getGuild(player);
if (g != null && !g.isBase() && g.isLeader(player)) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ public void tick() {
}
checkSupporters();
checkForeignBackers();
if (hasLeader() && net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(leader)) leader = null;
if(causes.size() == 0) {
f.getGovernment().endMovement(this);
}
Expand Down Expand Up @@ -562,15 +563,17 @@ public List<Faction> getAllSupportingFactions() {

public double getPower() {
double base = 0;
double memberCount = supporters.getAllMembers().size();
// Inactive members count on neither side of the share.
double memberCount = net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(supporters.getAllMembers()).size();
for(Cause cause : causes) {
memberCount += cause.getPool().getAllMembers().size();
memberCount += net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(cause.getPool().getAllMembers()).size();
}
for(Faction backer : foreignBackers) {
memberCount += (backer.getMembers().size() + backer.getVassalMembers().size())/2.0; //backers count half
memberCount += (backer.getActiveMembers().size() + backer.getActiveVassalMembers().size())/2.0; //backers count half
}
if((f.getMembers().size()+f.getVassalMembers().size()) > 0) {
base += (100.0* (memberCount/(f.getMembers().size()+f.getVassalMembers().size())));
int realmMembers = f.getActiveMembers().size() + f.getActiveVassalMembers().size();
if(realmMembers > 0) {
base += (100.0* (memberCount/realmMembers));
}
if(f.getTotalTradePower() > 0) {
base += (30.0* (getTotalTradePower()/f.getTotalTradePower()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public void leave(Object o) {

public boolean canBeLeader(String p) {
if(p == null || !pool.getAllMembers().contains(p)) return false;
if(net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(p)) return false;
Member relation = movement.getFaction().getRelationToFaction(p);
if (relation == null) return false;
return switch (relation) {
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/net/tfminecraft/simplefactions/guild/Guild.java
Original file line number Diff line number Diff line change
Expand Up @@ -869,12 +869,18 @@ public double getRepressFavourCost() {
return Formatter.formatDouble(getStabilityEffect()*0.5);
}

/** Members not marked inactive. */
public List<String> getActiveMembers() {
return net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(members);
}

public double getMemberPercentage() {
int totalMembers = host.getMembers().size() + host.getVassalMembers().size();
int totalMembers = net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(host.getMembers()).size()
+ net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(host.getVassalMembers()).size();
if (totalMembers <= 0) {
return 0.0;
}
return (double) members.size() / (double) totalMembers;
return (double) getActiveMembers().size() / (double) totalMembers;
}

public double getStabilityEffect() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@

/**
* Inactivity is a mark, not a kick. After {@link #DAYS_UNTIL_INACTIVE} offline
* days a member stays in the guild and is shown as inactive. A guild whose
* days a member stays in the guild and is shown as inactive. An inactive
* member adds no prestige, does not count toward member-based limits and
* gives up council seats, candidacies, movement leadership and the Spymaster
* post. A realm leader stays leader, and a guild leader keeps the guild. A guild whose
* real members are all inactive loses 1% of upgrade output every
* {@link #DECAY_INTERVAL_MILLIS}. A faction in the same state loses 1% prestige
* on that cadence and, while it holds more land than its prestige pays for,
* sheds provinces in claim order. The capital is last, and it does go.
*/
public final class InactivityRules {
public static final int DAYS_UNTIL_INACTIVE = 21;
public static final int DAYS_UNTIL_INACTIVE = 28;
public static final long DECAY_INTERVAL_MILLIS = 4L * 60L * 60L * 1000L;
public static final int DECAY_STEP_PERCENT = 1;
public static final int DECAY_CAP_PERCENT = 100;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,19 @@ public static int prestigePercent(Faction faction) {

public static boolean isMemberInactive(String name) {
if (!InactivityRules.isTrackedMember(name)) return false;
if (isOnline(name)) return false;
return InactivityRules.isInactive(FactionCleanup.daysOffline(name));
// The day count is a map read; only a member already past the threshold needs the online lookup.
if (!InactivityRules.isInactive(FactionCleanup.daysOffline(name))) return false;
return !isOnline(name);
}

/** The members who still count. Inactive members stay in their guild but add nothing to counts. */
public static List<String> activeOnly(java.util.Collection<String> members) {
List<String> active = new ArrayList<>();
if (members == null) return active;
for (String member : members) {
if (!isMemberInactive(member)) active.add(member);
}
return active;
}

public static boolean isFullyInactive(List<String> members) {
Expand Down Expand Up @@ -143,7 +154,8 @@ public static void onLogin(Player player) {
Guild guild = faction.getGuild(name);
boolean outputCleared = guild != null && clearClock(guildClocks, guild.getId());
boolean prestigeCleared = clearClock(factionClocks, faction.getId());
if (prestigeCleared) faction.updatePrestige();
// The returning member counts toward prestige again, with or without a penalty to clear.
faction.updatePrestige();
if (outputCleared || prestigeCleared) {
recomputeEarliest();
save();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ public ItemStack createTitleItem(Player p, Faction f, Title t, List<Integer> pro
}

private static void addCountyLimitLore(Faction f, Tier county, List<String> lore) {
lore.add(StringFormatter.formatHex("#e07373County limit reached: #f5f5f5" + f.getTitles(county).size() + "/" + f.getMembers().size()
+ " #e07373(one per member)"));
lore.add(StringFormatter.formatHex("#767a77Recruit more members to hold another county."));
lore.add(StringFormatter.formatHex("#e07373County limit reached: #f5f5f5" + f.getTitles(county).size() + "/" + f.getActiveMembers().size()
+ " #e07373(one per active member)"));
lore.add(StringFormatter.formatHex("#767a77Recruit members, or bring inactive ones back, to hold another county."));
}

public int getNewTitleCost(Faction f, Tier t) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public void click(InventoryClickEvent e, Inventory inventory, Player p) {
if(tier.getId().equalsIgnoreCase("county")) {
current = f.getUntitledProvinces().size();
if(Title.countyLimitReached(f, tier)) {
p.sendMessage("§cYou need more members to form more counties!");
p.sendMessage("§cYou need more active members to form more counties!");
return;
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,13 @@ public void setName(String name) {
public List<String> getMembers() {
return guildHandler.getAllMembers();
}
/** Members not marked inactive. Inactive members stay in the realm but count for nothing. */
public List<String> getActiveMembers() {
return net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(getMembers());
}
public List<String> getActiveVassalMembers() {
return net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(getVassalMembers());
}
public List<String> getVassalMembers() {
List<String> members = new ArrayList<>();
for(Faction vassal : RelationManager.getSubjects(this)) {
Expand Down Expand Up @@ -644,6 +651,7 @@ public boolean isMember(org.bukkit.entity.Player player) {
public boolean canBecomeLeader(String member) {
if(isLeader(member)) return false;
if(!isMember(member)) return false;
if(net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(member)) return false;
for(Guild g : guildHandler.getGuilds()) {
if(g.isLeader(member)) return false;
}
Expand Down Expand Up @@ -779,7 +787,7 @@ public void updatePrestige() {

// Two halves: headcount, where each account is one member split across its
// characters by recent play, plus what each counted member's online time is worth.
var headcount = net.tfminecraft.simplefactions.prestige.MemberShare.count(guildHandler.getAllMembers());
var headcount = net.tfminecraft.simplefactions.prestige.MemberShare.count(getActiveMembers());
double members = Math.pow(headcount.heads()+4, 1.8)+5
+ net.tfminecraft.simplefactions.prestige.MemberPlaytime.totalFor(headcount.counted());

Expand Down Expand Up @@ -994,6 +1002,7 @@ public void ping() {
government.ping();
}
public boolean canVote(String p) {
if(net.tfminecraft.simplefactions.inactivity.InactivityService.isMemberInactive(p)) return false;
if(isMember(p)) return true;
if(hasFactionRule(Rules.VASSAL_VOTING_RIGHTS)) {
if(getVassalMembers().contains(p)) return true;
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/net/tfminecraft/simplefactions/tiers/Title.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,12 @@ public boolean canBeHeld(Faction f, List<Integer> ownedProvinces, List<Title> ow
return true;
}

/** A realm holds at most one county per member; at the limit it can't claim or form another. */
/**
* A realm forms at most one county per active member; at the limit it can't claim or form another.
* Counties it already holds are kept while it has one per member, active or not.
*/
public static boolean countyLimitReached(Faction f, Tier tier) {
return tier.getTier() == 2 && f.getTitles(tier).size() >= f.getMembers().size();
return tier.getTier() == 2 && f.getTitles(tier).size() >= f.getActiveMembers().size();
}

public boolean countyLimitReached(Faction f) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static int directLevyContribution(Faction source) {
return 0;
}
int total = 0;
int memberCap = source.getMembers() != null ? source.getMembers().size() : 0;
int memberCap = source.getMembers() != null ? net.tfminecraft.simplefactions.inactivity.InactivityService.activeOnly(source.getMembers()).size() : 0;
double levyPercent = source.getModifier(FactionModifiers.LEVY).getAmount();
for (Regiment regiment : source.getMilitary().getRegiments()) {
if (regiment.isLevy() || regiment.isEquipment()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package net.tfminecraft.simplefactions.espionage;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

import java.util.UUID;
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.simplefactions.inactivity.InactivityService;
import net.tfminecraft.simplefactions.objects.Faction;

class InactiveSpymasterTest {
private MockedStatic<InactivityService> inactivity;

@BeforeEach void setUp() {
inactivity = mockStatic(InactivityService.class);
inactivity.when(() -> InactivityService.isMemberInactive(anyString())).thenReturn(true);
}

@AfterEach void tearDown() {
inactivity.close();
}

private Faction faction(String spy, boolean leader) {
var faction = mock(Faction.class);
when(faction.getId()).thenReturn("realm");
when(faction.getEspionage()).thenReturn(new EspionageState());
var holder = new SpecialPositionAssignment();
EspionageTestFixtures.bind(holder, spy);
holder.playerId = UUID.randomUUID();
when(faction.isMember(EspionageTestFixtures.key(spy))).thenReturn(true);
when(faction.isLeader(EspionageTestFixtures.key(spy))).thenReturn(leader);
faction.getEspionage().appoint(holder, 50);
return faction;
}

@Test void anInactiveSpymasterNoLongerGuardsTheRealm() {
assertFalse(EspionageService.hasSpymaster(faction("Spy", false)));
}

@Test void anInactiveLeaderKeepsTheirOwnSpymasterPost() {
assertTrue(EspionageService.hasSpymaster(faction("Leader", true)));
}

@Test void anActiveSpymasterStillGuardsTheRealm() {
inactivity.when(() -> InactivityService.isMemberInactive(anyString())).thenReturn(false);
assertTrue(EspionageService.hasSpymaster(faction("Spy", false)));
}
}
Loading
Loading