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
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

<groupId>net.tfminecraft</groupId>
<artifactId>simplefactions</artifactId>
<version>4.2.0</version>
<version>main-SNAPSHOT</version>

<properties>
<argLine></argLine>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.proc>full</maven.compiler.proc>
<denareconomy.version>0.6.0</denareconomy.version>
<denareconomy.version>0.7.0</denareconomy.version>
<rpcharacters.version>2.16.0</rpcharacters.version>
<tlibs.version>2.6.0</tlibs.version>
<vehicleframework.version>2.13.0</vehicleframework.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ public final class FrozenReads {
private static final ThreadLocal<FrozenReads> CURRENT = new ThreadLocal<>();

private final Map<String, Long> cents;
private final Map<UUID, Map<UUID, Long>> taxContributors;
private final Map<UUID, Double> nodeUpkeep;
private final Map<String, List<StabilityModifier>> espionage;

private FrozenReads(
Map<String, Long> cents, Map<UUID, Double> nodeUpkeep, Map<String, List<StabilityModifier>> espionage) {
Map<String, Long> cents, Map<UUID, Map<UUID, Long>> taxContributors,
Map<UUID, Double> nodeUpkeep, Map<String, List<StabilityModifier>> espionage) {
this.cents = Map.copyOf(cents);
this.taxContributors = Map.copyOf(taxContributors);
this.nodeUpkeep = Map.copyOf(nodeUpkeep);
this.espionage = Map.copyOf(espionage);
}
Expand All @@ -49,6 +52,7 @@ public static FrozenReads capture() {
}
}
Map<String, Long> cents = new HashMap<>();
Map<UUID, Map<UUID, Long>> taxContributors = new HashMap<>();
Map<UUID, Double> nodeUpkeep = new HashMap<>();
for (Guild guild : FactionManager.getAllGuilds()) {
Bank bank = guild == null ? null : guild.getBank();
Expand All @@ -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. */
Expand All @@ -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<UUID, Long> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,18 @@ public Map<String, Double> 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<String, Double> getCitizenTaxBreakdown() {
double collected = getCollectedCitizenTax();
if (collected <= 0) return Map.of();
if (!Double.isFinite(LedgerHistory.total(citizenTaxes))) {
Map<String, Double> 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<String, Double> amounts = new HashMap<>();
citizenTaxes.forEach((name, value) -> {
taxes.forEach((name, value) -> {
double amount = Formatter.formatDouble(value);
if (amount > 0) amounts.put(name, amount);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +51,12 @@ class FrozenReadsTest {
private final List<Faction> savedFactions = new ArrayList<>();
private Guild guild;

private org.mockito.MockedStatic<Denars> 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);
Expand All @@ -74,14 +81,16 @@ 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();
}

// 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
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading