From 0ed280f79f20a4f84bd17d23326d69065d5ff28f Mon Sep 17 00:00:00 2001
From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com>
Date: Sat, 17 Oct 2026 14:55:19 +0000
Subject: [PATCH 1/2] fix: show character contributors in daily citizen tax
ledgers
---
pom.xml | 2 +-
.../simplefactions/economy/FrozenReads.java | 17 +++++++++--
.../simplefactions/guild/income/Ledger.java | 9 ++++--
.../economy/FrozenReadsTest.java | 30 ++++++++++++++-----
.../RealmSettlementCalculationTest.java | 19 ++++++++++++
.../LedgerCitizenTaxPersistenceTest.java | 16 ++++++++++
.../inventory/GuildMenusCoverageTest.java | 30 +++++++++++++++++++
7 files changed, 109 insertions(+), 14 deletions(-)
diff --git a/pom.xml b/pom.xml
index 02814dd9..7052c262 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
21
UTF-8
full
- 0.6.0
+ 0.7.0
2.16.0
2.6.0
2.13.0
diff --git a/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java b/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java
index 90537d3d..c96133d7 100644
--- a/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java
+++ b/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java
@@ -24,12 +24,15 @@ public final class FrozenReads {
private static final ThreadLocal CURRENT = new ThreadLocal<>();
private final Map cents;
+ private final Map> taxContributors;
private final Map nodeUpkeep;
private final Map> espionage;
private FrozenReads(
- Map cents, Map nodeUpkeep, Map> espionage) {
+ Map cents, Map> taxContributors,
+ Map nodeUpkeep, Map> espionage) {
this.cents = Map.copyOf(cents);
+ this.taxContributors = Map.copyOf(taxContributors);
this.nodeUpkeep = Map.copyOf(nodeUpkeep);
this.espionage = Map.copyOf(espionage);
}
@@ -49,6 +52,7 @@ public static FrozenReads capture() {
}
}
Map cents = new HashMap<>();
+ Map> taxContributors = new HashMap<>();
Map nodeUpkeep = new HashMap<>();
for (Guild guild : FactionManager.getAllGuilds()) {
Bank bank = guild == null ? null : guild.getBank();
@@ -60,10 +64,11 @@ public static FrozenReads capture() {
if (incarnation != null) {
String taxAccount = Denars.guildTax(incarnation);
cents.put(taxAccount, Denars.balance(taxAccount));
+ taxContributors.put(incarnation, Map.copyOf(Denars.taxContributors(incarnation)));
nodeUpkeep.put(incarnation, Ledger.liveNodeUpkeep(guild));
}
}
- return new FrozenReads(cents, nodeUpkeep, espionage);
+ return new FrozenReads(cents, taxContributors, nodeUpkeep, espionage);
}
/** Runs the work with these reads standing in for the live state on this thread. */
@@ -90,6 +95,14 @@ public static long balance(String account) {
return Denars.balance(account);
}
+ /** Held tax by earning character, frozen with the balance for async previews and settlement. */
+ public static Map taxContributors(UUID incarnation) {
+ if (incarnation == null) return Map.of();
+ FrozenReads current = CURRENT.get();
+ return current == null ? Denars.taxContributors(incarnation)
+ : current.taxContributors.getOrDefault(incarnation, Map.of());
+ }
+
/** The frozen node upkeep inside {@link #read}, otherwise the live upkeep. */
public static double nodeUpkeep(Guild guild) {
FrozenReads current = CURRENT.get();
diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java
index d521bfb2..6879d607 100644
--- a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java
+++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java
@@ -144,15 +144,18 @@ public Map getCitizenTaxesCopy() {
return new HashMap<>(citizenTaxes);
}
- /** Held tax is authoritative; legacy contributor rows describe only part of it. */
+ /** Held tax is authoritative; combine transaction attribution with legacy contributor rows. */
public Map getCitizenTaxBreakdown() {
double collected = getCollectedCitizenTax();
if (collected <= 0) return Map.of();
- if (!Double.isFinite(LedgerHistory.total(citizenTaxes))) {
+ Map taxes = new HashMap<>(citizenTaxes);
+ FrozenReads.taxContributors(guild.getIncarnationId()).forEach((character, cents) ->
+ taxes.merge(character.toString(), cents / 100.0, Double::sum));
+ if (!Double.isFinite(LedgerHistory.total(taxes))) {
return Map.of("Other citizen taxes", collected);
}
Map amounts = new HashMap<>();
- citizenTaxes.forEach((name, value) -> {
+ taxes.forEach((name, value) -> {
double amount = Formatter.formatDouble(value);
if (amount > 0) amounts.put(name, amount);
});
diff --git a/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java b/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java
index 5d2902d1..6bdea968 100644
--- a/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java
+++ b/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java
@@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -50,6 +51,12 @@ class FrozenReadsTest {
private final List savedFactions = new ArrayList<>();
private Guild guild;
+ private org.mockito.MockedStatic mockDenars() {
+ var denars = mockStatic(Denars.class, CALLS_REAL_METHODS);
+ denars.when(() -> Denars.taxContributors(any())).thenReturn(Map.of());
+ return denars;
+ }
+
@BeforeEach
void setUp() {
savedFactions.addAll(FactionManager.factions);
@@ -74,7 +81,7 @@ void tearDown() {
@Test
void asyncReadsUseBalancesCapturedOnTheMainThread() throws Exception {
FrozenReads frozen;
- try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
+ try (var denars = mockDenars()) {
denars.when(() -> Denars.balance(anyString())).thenReturn(1234L);
frozen = FrozenReads.capture();
}
@@ -82,6 +89,8 @@ void asyncReadsUseBalancesCapturedOnTheMainThread() throws Exception {
// Static mocks stay on the test thread, so only the frozen balance can give the worker 12.34.
assertEquals(12.34, CompletableFuture.supplyAsync(() -> frozen.read(() -> guild.getBank().getWealth())).get());
assertEquals(0L, frozen.read(() -> FrozenReads.balance("unknown")));
+ assertEquals(Map.of(), frozen.read(() -> FrozenReads.taxContributors(UUID.randomUUID())));
+ assertEquals(Map.of(), FrozenReads.taxContributors(null));
}
@Test
@@ -91,22 +100,27 @@ void asyncCitizenTaxUsesTheHoldingBalanceCapturedOnTheMainThread() throws Except
Ledger ledger = new Ledger(guild);
String holding = Denars.guildTax(guild.getIncarnationId());
FrozenReads frozen;
- try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
+ UUID contributor = UUID.randomUUID();
+ var contributors = new java.util.HashMap<>(Map.of(contributor, 1000L));
+ try (var denars = mockDenars()) {
denars.when(() -> Denars.balance(anyString())).thenReturn(0L);
denars.when(() -> Denars.balance(holding)).thenReturn(1234L);
+ denars.when(() -> Denars.taxContributors(guild.getIncarnationId())).thenReturn(contributors);
frozen = FrozenReads.capture();
denars.when(() -> Denars.balance(holding)).thenReturn(5000L);
+ contributors.put(contributor, 2000L);
assertEquals(50.0, ledger.getIncome(Cashflow.CITIZENS));
+ assertEquals(Map.of(contributor.toString(), 20.0, "Other citizen taxes", 30.0), ledger.getCitizenTaxBreakdown());
}
assertEquals(12.34, CompletableFuture.supplyAsync(() -> frozen.read(() -> ledger.getIncome(Cashflow.CITIZENS))).get());
- assertEquals(Map.of("Other citizen taxes", 12.34), CompletableFuture
+ assertEquals(Map.of(contributor.toString(), 10.0, "Other citizen taxes", 2.34), CompletableFuture
.supplyAsync(() -> frozen.read(ledger::getCitizenTaxBreakdown)).get());
}
@Test
void readRestoresTheLiveLedgerAfterwards() {
- try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
+ try (var denars = mockDenars()) {
denars.when(() -> Denars.balance(anyString())).thenReturn(500L);
FrozenReads frozen = FrozenReads.capture();
denars.when(() -> Denars.balance(anyString())).thenReturn(700L);
@@ -118,7 +132,7 @@ void readRestoresTheLiveLedgerAfterwards() {
@Test
void nestedReadsRestoreTheOuterBalances() {
- try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
+ try (var denars = mockDenars()) {
denars.when(() -> Denars.balance(anyString())).thenReturn(100L);
FrozenReads outer = FrozenReads.capture();
denars.when(() -> Denars.balance(anyString())).thenReturn(200L);
@@ -135,7 +149,7 @@ void nestedReadsRestoreTheOuterBalances() {
void previewCopiesCarryTheBalancesFromWhenTheyWereMade() {
ProvinceManager live = new ProvinceManager();
live.start(Map.of());
- try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
+ try (var denars = mockDenars()) {
denars.when(() -> Denars.balance(anyString())).thenReturn(-300L);
EconomicPreview.Prepared economic = EconomicPreview.prepare(live);
BranchIncomePreview.Prepared branch = BranchIncomePreview.prepare(live);
@@ -168,7 +182,7 @@ void asyncStabilityUsesSpymasterDeathsCapturedOnTheMainThread() throws Exception
assertInstanceOf(IllegalStateException.class, live.getCause());
FrozenReads frozen;
- try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
+ try (var denars = mockDenars()) {
denars.when(() -> Denars.balance(anyString())).thenReturn(0L);
frozen = FrozenReads.capture();
}
@@ -200,7 +214,7 @@ void asyncLedgerUsesNodeUpkeepCapturedOnTheMainThread() throws Exception {
assertInstanceOf(IllegalStateException.class, live.getCause());
FrozenReads frozen;
- try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
+ try (var denars = mockDenars()) {
denars.when(() -> Denars.balance(anyString())).thenReturn(0L);
frozen = FrozenReads.capture();
}
diff --git a/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java b/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java
index caaa2472..449b32ab 100644
--- a/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java
+++ b/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java
@@ -3,8 +3,11 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import net.tfminecraft.denareconomy.DenarEconomy;
import net.tfminecraft.denareconomy.api.Denars;
import net.tfminecraft.denareconomy.api.Leg;
+import net.tfminecraft.denareconomy.api.Result;
+import net.tfminecraft.denareconomy.api.TaxLeg;
import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.database.JsonUtil;
import net.tfminecraft.simplefactions.database.LoanData;
@@ -95,6 +98,22 @@ RealmSettlementChanges changes(SettlementJournal.Plan plan) {
assertTrue(capital.getLedger().getCitizenTaxesCopy().isEmpty());
}
+ @Test void postedTaxContributorsBecomeLastDayAndLifetimeWithoutChangingSettlementMoney() {
+ String holding = Denars.guildTax(capital.getIncarnationId());
+ assertEquals(Result.APPLIED, DenarEconomy.getLedger().postEarning("tax", "earn",
+ List.of(new Leg(Denars.MINT, -2000), new Leg("char:" + character + ":bank", 766),
+ new Leg(holding, 1234)), character, new TaxLeg(holding, 1234)));
+ assertEquals(Map.of(character.toString(), 12.34), capital.getLedger().getCitizenTaxBreakdown());
+ var plan = plan(List.of());
+ assertEquals(-1234, net(plan, holding));
+ assertEquals(1234, net(plan, capital.getBank().accountId()));
+ var saved = changes(plan).guilds.stream()
+ .filter(c -> c.incarnation.equals(capital.getIncarnationId())).findFirst().orElseThrow();
+ assertEquals(Map.of(character.toString(), 12.34), saved.ledgerLastDay.get("CITIZENS"));
+ assertEquals(Map.of(character.toString(), 12.34), saved.ledgerLifetime.get("CITIZENS"));
+ assertTrue(capital.getLedger().getCitizenTaxesCopy().isEmpty());
+ }
+
@Test void debtIsPreservedAndDailyShortfallsLiquidateWithoutTouchingLiveBranches() {
guild.getBranch(0).levelUp();
guild.getBranch(0).levelUp();
diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java
index f79903d6..8335a07f 100644
--- a/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java
+++ b/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java
@@ -24,6 +24,22 @@
class LedgerCitizenTaxPersistenceTest {
+ @Test
+ void transactionContributorsCombineWithLegacyRowsWithoutChangingSavedAmounts() {
+ Guild guild = mock(Guild.class);
+ UUID incarnation = UUID.randomUUID();
+ UUID character = UUID.randomUUID();
+ when(guild.getIncarnationId()).thenReturn(incarnation);
+ Ledger ledger = new Ledger(guild);
+ ledger.setCitizenTaxes(Map.of(character.toString(), 0.50));
+ try (var reads = mockStatic(FrozenReads.class)) {
+ reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(1234L);
+ reads.when(() -> FrozenReads.taxContributors(incarnation)).thenReturn(Map.of(character, 1000L));
+ assertEquals(Map.of(character.toString(), 10.50, "Other citizen taxes", 1.84), ledger.getCitizenTaxBreakdown());
+ assertEquals(Map.of(character.toString(), 0.50), ledger.getCitizenTaxesCopy());
+ }
+ }
+
@Test
void heldTaxIsVisibleWithoutLegacyContributorEntries() {
Guild guild = mock(Guild.class);
diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java
index bc9c060b..49253dd0 100644
--- a/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java
+++ b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java
@@ -10,7 +10,13 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import java.util.function.Consumer;
+import net.tfminecraft.denareconomy.DenarEconomy;
+import net.tfminecraft.denareconomy.api.Denars;
+import net.tfminecraft.denareconomy.api.Leg;
+import net.tfminecraft.denareconomy.api.Result;
+import net.tfminecraft.denareconomy.api.TaxLeg;
import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.database.GuildData;
import net.tfminecraft.simplefactions.diplomacy.RelationType;
@@ -39,6 +45,7 @@
import net.tfminecraft.simplefactions.guild.upgrade.Upgrade;
import net.tfminecraft.simplefactions.guild.upgrade.UpgradeExpansion;
import net.tfminecraft.simplefactions.inactivity.InactivityService;
+import net.tfminecraft.simplefactions.identity.Members;
import net.tfminecraft.simplefactions.keys.Keys;
import net.tfminecraft.simplefactions.loaders.BranchLoader;
import net.tfminecraft.simplefactions.loaders.RelationLoader;
@@ -641,6 +648,29 @@ void citizenTooltipShowsHeldTaxWithoutLegacyContributorRows() {
}
}
+ @Test
+ void citizenTooltipNamesTodaysContributorAndRetainsItAfterRollover() {
+ var character = UUID.randomUUID();
+ var holding = Denars.guildTax(guild.getIncarnationId());
+ assertEquals(Result.APPLIED, DenarEconomy.getLedger().postEarning("daily-tax", "earn",
+ List.of(new Leg(Denars.MINT, -1234), new Leg(holding, 1234)), character, new TaxLeg(holding, 1234)));
+ try (var members = mockStatic(Members.class, CALLS_REAL_METHODS)) {
+ members.when(() -> Members.display(character.toString())).thenReturn("Daily Citizen");
+ ItemStack today = creator.createLedgerCitizensItem(guild);
+ assertLore(today, "Today: +12.34d", true);
+ assertLore(today, "Daily Citizen: +12.34d", true);
+ assertLore(today, "Other citizen taxes", false);
+ guild.getLedger().getHistory().closeDay(Map.of(LedgerHistory.Source.CITIZENS, guild.getLedger().getCitizenTaxBreakdown()));
+ assertEquals(Result.APPLIED, Denars.transfer("daily-settle", "settle", holding, guild.getBank().accountId(), 1234));
+ ItemStack yesterday = creator.createLedgerCitizensItem(guild);
+ assertLore(yesterday, "Today: +0.00d", true);
+ assertLore(yesterday, "Last day: +12.34d", true);
+ assertLore(yesterday, "Lifetime: +12.34d", true);
+ assertEquals(2, yesterday.getItemMeta().getLore().stream()
+ .filter(line -> ChatColor.stripColor(line).equals("Daily Citizen: +12.34d")).count());
+ }
+ }
+
@Test
void ledgerSourcesSortCapAndSeparateTodayFromHistoricalTotals() {
Guild display = displayGuild(guild);
From 6f409a747ff98a2018db38e5fa65d39bb991693e Mon Sep 17 00:00:00 2001
From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com>
Date: Sat, 17 Oct 2026 15:03:42 +0000
Subject: [PATCH 2/2] build: retain snapshot version for tagged releases
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 7052c262..d9641926 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
net.tfminecraft
simplefactions
- 4.2.0
+ main-SNAPSHOT