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
1 change: 1 addition & 0 deletions src/main/java/net/tfminecraft/simplefactions/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class Cache {
public static int maxUntitledProvinces;
public static int maxFreeTitles;
public static double deJureRequirement;
public static double lawSwitchLockDays = 3.0;
public static boolean mapEnabled;
public static boolean provincesEnabled = true;
public static boolean chronicleEnabled = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ public void loadFactions() {
data.laws,
data.governmentData
);
if (data.lawChangedAt != null) {
for (Map.Entry<String, Long> entry : data.lawChangedAt.entrySet()) {
LawGroup group = f.getLawHandler().getGroup(entry.getKey());
if (group != null && entry.getValue() != null) group.setChangedAt(entry.getValue());
}
}

// --- Rank / founding ---
// Rank is derived state that only climbs one level per updatePrestige, so it
Expand Down Expand Up @@ -452,6 +458,9 @@ public void saveFaction(Faction f) {
if (current != null) {
data.laws.add(group.getId() + ":" + current.getId());
}
if (group.getChangedAt() > 0) {
data.lawChangedAt.put(group.getId(), group.getChangedAt());
}
}

data.overlord = RelationManager.getOverlord(f);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

import com.google.gson.annotations.SerializedName;
Expand Down Expand Up @@ -84,6 +85,9 @@ public class FactionData {

public List<String> laws = new ArrayList<>();

@SerializedName("law changed at")
public Map<String, Long> lawChangedAt = new HashMap<>();

@SerializedName("war reparations")
public List<WarReparationsObligationData> warReparationsObligations = new ArrayList<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.utils.EconomicImpact;
import net.tfminecraft.simplefactions.utils.EconomicImpactService;
import net.tfminecraft.simplefactions.laws.CanHaveLaw;
import net.tfminecraft.simplefactions.laws.LawGroup;

import net.tfminecraft.simplefactions.war.core.War;
Expand Down Expand Up @@ -42,6 +43,11 @@ public String getProposer() {

public void apply(Cause cause) {
if (isLawProposal()) {
// The group may have been switched by a coup, war or movement since this was proposed.
// Movements (cause != null) override the lock.
if (cause == null && CanHaveLaw.lockReason(gov.getFaction(), law, System.currentTimeMillis()) != null) {
return;
}
LawGroup group = gov.getFaction().getLawHandler().getGroup(law.getGroup());
gov.getFaction().applyLaw(law, group);
} else if (isTaxProposal()) {
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/net/tfminecraft/simplefactions/laws/CanHaveLaw.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,46 @@ public static String blockReason(Faction faction, Law law) {
if (isCurrentInGroup(faction, law)) {
return null;
}
String lockReason = lockReason(faction, law, System.currentTimeMillis());
if (lockReason != null) {
return lockReason;
}
String requirementReason = requirementsReason(faction, law);
if (requirementReason != null) {
return requirementReason;
}
return compatibilityReason(faction, law);
}

/** Why the law's group cannot be switched yet, or null if it can. */
public static String lockReason(Faction faction, Law law, long now) {
if (faction == null || faction.getLawHandler() == null || law == null || law.getGroup() == null) {
return null;
}
if (isCurrentInGroup(faction, law)) {
return null;
}
LawGroup group = faction.getLawHandler().getGroup(law.getGroup());
if (group == null) {
return null;
}
long remaining = group.lockRemaining(now);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (remaining <= 0) {
return null;
}
return "§cThis law was changed recently. It can change again in " + formatRemaining(remaining) + ".";
}

static String formatRemaining(long millis) {
long minutes = Math.max(1, (millis + 59_999) / 60_000);
long days = minutes / 1440;
long hours = minutes % 1440 / 60;
long mins = minutes % 60;
if (days > 0) return days + "d " + hours + "h";
if (hours > 0) return hours + "h " + mins + "m";
return mins + "m";
}

private static boolean isCurrentInGroup(Faction faction, Law law) {
Law current = currentInGroup(faction, law);
return current != null && current.getId() != null && current.getId().equalsIgnoreCase(law.getId());
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/net/tfminecraft/simplefactions/laws/LawGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import org.bukkit.configuration.ConfigurationSection;

import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.guild.income.IncomePreviewContext;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.objects.handler.TaxHandler;
Expand All @@ -17,6 +18,7 @@ public class LawGroup {
private String id;
private String name;
private Law current;
private long changedAt; // epoch millis of the last switch, 0 if never
private Map<String, Law> laws = new LinkedHashMap<>();
private List<String> description = new ArrayList<>();

Expand Down Expand Up @@ -68,4 +70,21 @@ public void setCurrent(Law law) {
if(!laws.containsValue(law)) return;
current = law;
}

/** Sets the law and, if it differs from the current one, starts the switch lock. */
public void switchTo(Law law, long now) {
if(!laws.containsValue(law)) return;
if(law != current) changedAt = now;
current = law;
}

public long getChangedAt() { return changedAt; }
public void setChangedAt(long changedAt) { this.changedAt = changedAt; }

/** Millis left before this group may be switched again, 0 if it may be now. */
public long lockRemaining(long now) {
if(changedAt <= 0 || Cache.lawSwitchLockDays <= 0) return 0;
long until = changedAt + (long) (Cache.lawSwitchLockDays * 86_400_000L);
return Math.max(0, until - now);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void loadConfig(File configFile) {
Cache.maxExtraNodeCapacity = config.getInt("max-extra-node-capacity", 0);

Cache.deJureRequirement = config.getDouble("de-jure-requirement", 100.0);
Cache.lawSwitchLockDays = config.getDouble("law-switch-lock-days", 3.0);
Cache.maxUntitledProvinces = config.getInt("max-untitled-provinces", 5);
Cache.maxFreeTitles = config.getInt("max-free-titles", 3);
Cache.mapEnabled = config.getBoolean("enable-map", false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ public boolean canVote(String p) {
public LawHandler getLawHandler() { return lawHandler; }

public void applyLaw(Law law, LawGroup group) {
group.setCurrent(law);
group.switchTo(law, System.currentTimeMillis());

LawEffect effect = law.getScopedEffects().get(Scope.FACTION);
if (effect == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ static String applyConfiguredVassalage(Faction rebels) {
if (law.getScopedEffects() != null && law.getScopedEffects().get(Scope.FACTION) != null) {
rebels.applyLaw(law, group);
} else {
group.setCurrent(law);
group.switchTo(law, System.currentTimeMillis());
}
return null;
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ starting-year: "372 AE"

max-extra-node-capacity: 0
de-jure-requirement: 75.0
# Days after a law group changes before the faction can switch it again.
# Coups, movements, war outcomes and admin commands ignore it. 0 disables it.
law-switch-lock-days: 3

# Map platform (step 42) — requires TFMCWeb softdepend + api.base-url configured
enable-map: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package net.tfminecraft.simplefactions.laws;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.List;

import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.objects.handler.LawHandler;

class LawSwitchLockTest {

private static final long DAY = 86_400_000L;
private static final long NOW = 1_000L * DAY;

private final double savedDays = Cache.lawSwitchLockDays;

@AfterEach
void restore() {
Cache.lawSwitchLockDays = savedDays;
}

@Test
void neverSwitched_notLocked() {
Cache.lawSwitchLockDays = 3;
assertEquals(0, lawGroup().lockRemaining(NOW));
}

@Test
void switching_startsLockForConfiguredDays() {
Cache.lawSwitchLockDays = 3;
LawGroup group = lawGroup();
group.switchTo(group.getLaw("high"), NOW);

assertEquals(3 * DAY, group.lockRemaining(NOW));
assertEquals(DAY, group.lockRemaining(NOW + 2 * DAY));
assertEquals(0, group.lockRemaining(NOW + 3 * DAY));
}

@Test
void reapplyingSameLaw_doesNotRestartLock() {
Cache.lawSwitchLockDays = 3;
LawGroup group = lawGroup();
group.switchTo(group.getLaw("high"), NOW);
group.switchTo(group.getLaw("high"), NOW + 2 * DAY);

assertEquals(NOW, group.getChangedAt());
}

@Test
void setCurrent_doesNotStartLock() {
Cache.lawSwitchLockDays = 3;
LawGroup group = lawGroup();
group.setCurrent(group.getLaw("high"));

assertEquals(0, group.lockRemaining(NOW));
}

@Test
void zeroDays_disablesLock() {
Cache.lawSwitchLockDays = 0;
LawGroup group = lawGroup();
group.switchTo(group.getLaw("high"), NOW);

assertEquals(0, group.lockRemaining(NOW));
}

@Test
void lockReason_blocksOtherLawsInLockedGroup() {
Cache.lawSwitchLockDays = 3;
LawGroup group = lawGroup();
group.switchTo(group.getLaw("high"), NOW);
Faction faction = factionWith(group);

String reason = CanHaveLaw.lockReason(faction, group.getLaw("low"), NOW + DAY);
assertNotNull(reason);
assertTrue(reason.contains("2d 0h"), reason);
assertNull(CanHaveLaw.lockReason(faction, group.getLaw("low"), NOW + 3 * DAY));
}

@Test
void lockReason_exemptsCurrentLaw() {
Cache.lawSwitchLockDays = 3;
LawGroup group = lawGroup();
group.switchTo(group.getLaw("high"), NOW);

assertNull(CanHaveLaw.lockReason(factionWith(group), group.getLaw("high"), NOW + DAY));
}

@Test
void blockReason_currentLawStillAvailableWhileLocked() {
Cache.lawSwitchLockDays = 3;
LawGroup group = lawGroup();
group.switchTo(group.getLaw("high"), System.currentTimeMillis());
Faction faction = factionWith(group);

assertNull(CanHaveLaw.blockReason(faction, group.getLaw("high")));
assertNotNull(CanHaveLaw.blockReason(faction, group.getLaw("low")));
}

@Test
void formatRemaining_roundsUpToMinutes() {
assertEquals("2d 5h", CanHaveLaw.formatRemaining(2 * DAY + 5 * 3_600_000L + 1));
assertEquals("3h 1m", CanHaveLaw.formatRemaining(3 * 3_600_000L + 1));
assertEquals("1m", CanHaveLaw.formatRemaining(1));
}

private static Faction factionWith(LawGroup group) {
Faction faction = mock(Faction.class);
LawHandler handler = mock(LawHandler.class);
when(faction.getLawHandler()).thenReturn(handler);
when(handler.getGroup("taxes")).thenReturn(group);
when(handler.getCurrentLaws()).thenReturn(List.of(group.getCurrent()));
return faction;
}

private static LawGroup lawGroup() {
YamlConfiguration config = new YamlConfiguration();
config.set("name", "Taxes");
for (String id : List.of("low", "high")) {
config.createSection("laws." + id);
config.set("laws." + id + ".name", id);
}
LawGroup group = new LawGroup("taxes", config);
group.setCurrent(group.getLaw("low"));
return group;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ void applyConfiguredVassalage_setsLawFromCacheIds() {
Faction rebels = mock(Faction.class);
stubRebelVassalage(rebels);
assertNull(CivilWarStartService.applyConfiguredVassalage(rebels));
verify(rebels.getLawHandler().getGroup("vassalage")).setCurrent(org.mockito.ArgumentMatchers.any());
verify(rebels.getLawHandler().getGroup("vassalage")).switchTo(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyLong());
}

private static Movement baseMovement(Action action, Member leaderRelation) {
Expand Down