Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 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,43 @@ 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;
}
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,128 @@
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 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