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 @@ -16,7 +16,7 @@
import net.tfminecraft.simplefactions.objects.Faction;

/**
* Guild treasuries, node upkeep and espionage stability read once on the main thread.
* Guild treasuries, held tax, node upkeep and espionage stability read once on the main thread.
* DenarEconomy, Dowsing and TLibs character identity refuse reads from other threads, so
* income previews that run async read these instead.
*/
Expand All @@ -34,7 +34,7 @@ private FrozenReads(
this.espionage = Map.copyOf(espionage);
}

/** Reads every guild treasury and node upkeep and every realm espionage modifier now. Call on the main thread. */
/** Reads guild treasuries, held tax and node upkeep and realm espionage modifiers now. Call on the main thread. */
public static FrozenReads capture() {
long now = System.currentTimeMillis();
Map<String, List<StabilityModifier>> espionage = new HashMap<>();
Expand All @@ -58,6 +58,8 @@ public static FrozenReads capture() {
}
UUID incarnation = guild == null ? null : guild.getIncarnationId();
if (incarnation != null) {
String taxAccount = Denars.guildTax(incarnation);
cents.put(taxAccount, Denars.balance(taxAccount));
nodeUpkeep.put(incarnation, Ledger.liveNodeUpkeep(guild));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import java.util.Set;
import java.util.function.ToDoubleFunction;

import net.tfminecraft.denareconomy.api.Denars;
import net.tfminecraft.simplefactions.economy.FrozenReads;
import net.tfminecraft.simplefactions.economy.Money;

import net.tfminecraft.simplefactions.vehicles.pool.FactionVehiclePoolService;

Expand Down Expand Up @@ -142,6 +144,25 @@ public Map<String, Double> getCitizenTaxesCopy() {
return new HashMap<>(citizenTaxes);
}

/** Held tax is authoritative; legacy contributor rows describe only part of it. */
public Map<String, Double> getCitizenTaxBreakdown() {
double collected = getCollectedCitizenTax();
if (collected <= 0) return Map.of();
if (!Double.isFinite(LedgerHistory.total(citizenTaxes))) {
return Map.of("Other citizen taxes", collected);
}
Map<String, Double> amounts = new HashMap<>();
citizenTaxes.forEach((name, value) -> {
double amount = Formatter.formatDouble(value);
if (amount > 0) amounts.put(name, amount);
});
double attributed = Formatter.formatDouble(LedgerHistory.total(amounts));
if (attributed > collected) return Map.of("Other citizen taxes", collected);
double remaining = Formatter.formatDouble(collected - attributed);
if (remaining > 0) amounts.merge("Other citizen taxes", remaining, Double::sum);
return amounts;
}

Comment thread
ryanbarlow97 marked this conversation as resolved.
/** Seeded from disk at load, so an unsettled day survives a restart. */
public void setCitizenTaxes(Map<String, Double> taxes) {
citizenTaxes.clear();
Expand Down Expand Up @@ -286,7 +307,7 @@ public double getIncome(Cashflow cashflow) {
break;
case CITIZENS:
if(!guild.isBase()) return 0;
amount = getAggregatedCitizenTax();
amount = getCollectedCitizenTax();
break;
case TARIFFS:
if(!guild.isBase()) return 0;
Expand Down Expand Up @@ -466,18 +487,18 @@ public double getTotalTariffsEarned() {
}

public List<Map.Entry<String, Double>> getCitizenTaxEntriesDescending() {
return citizenTaxes.entrySet().stream()
return getCitizenTaxBreakdown().entrySet().stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.toList();
}


private double getAggregatedCitizenTax() {
double total = 0;
for(Double d : citizenTaxes.values()) {
total+=d;
private double getCollectedCitizenTax() {
if (settlement != null && settlement.income.containsKey(Cashflow.CITIZENS)) {
return settlement.income.get(Cashflow.CITIZENS);
}
return total;
var incarnation = guild.getIncarnationId();
return incarnation == null ? 0.0 : Money.denars(FrozenReads.balance(Denars.guildTax(incarnation)));
}

private double getAggregatedLoanPayments() {
Expand Down Expand Up @@ -894,7 +915,7 @@ public static Map<Guild, Map<LedgerHistory.Source, Map<String, Double>>> collect
if (f == null || ledger.skipsMoneyMovement()) continue;

if (payer.isBase()) {
ledger.citizenTaxes.forEach((name, tax) ->
ledger.getCitizenTaxBreakdown().forEach((name, tax) ->
recordHistory(out, payer, LedgerHistory.Source.CITIZENS, name, tax));

Faction overlord = f.getOverlord();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ public ItemStack createLedgerCitizensItem(Guild g) {
m.addItemFlags(ItemFlag.HIDE_ADDITIONAL_TOOLTIP);
m.setDisplayName(StringFormatter.formatHex("#94b572Citizens"));
m.setLore(ledgerSourceLore(g, LedgerHistory.Source.CITIZENS, "Top contributors",
g.getLedger().getCitizenTaxesCopy(), "No citizen taxes."));
g.getLedger().getCitizenTaxBreakdown(), "No citizen taxes."));
i.setItemMeta(m);
return i;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ void asyncReadsUseBalancesCapturedOnTheMainThread() throws Exception {
assertEquals(0L, frozen.read(() -> FrozenReads.balance("unknown")));
}

@Test
void asyncCitizenTaxUsesTheHoldingBalanceCapturedOnTheMainThread() throws Exception {
when(guild.isBase()).thenReturn(true);
when(guild.getFaction()).thenReturn(FactionManager.factions.getFirst());
Ledger ledger = new Ledger(guild);
String holding = Denars.guildTax(guild.getIncarnationId());
FrozenReads frozen;
try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
denars.when(() -> Denars.balance(anyString())).thenReturn(0L);
denars.when(() -> Denars.balance(holding)).thenReturn(1234L);
frozen = FrozenReads.capture();
denars.when(() -> Denars.balance(holding)).thenReturn(5000L);
assertEquals(50.0, ledger.getIncome(Cashflow.CITIZENS));
}

assertEquals(12.34, CompletableFuture.supplyAsync(() -> frozen.read(() -> ledger.getIncome(Cashflow.CITIZENS))).get());
assertEquals(Map.of("Other citizen taxes", 12.34), CompletableFuture
.supplyAsync(() -> frozen.read(ledger::getCitizenTaxBreakdown)).get());
}

@Test
void readRestoresTheLiveLedgerAfterwards() {
try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ RealmSettlementChanges changes(SettlementJournal.Plan plan) {
assertTrue(changes(plan).guilds.stream().filter(c -> c.incarnation.equals(capital.getIncarnationId())).findFirst().orElseThrow().citizenTaxes.isEmpty());
}

@Test void heldTaxWithoutLegacyEntriesIsIncludedInSettlementHistory() {
denars.when(() -> Denars.balance(Denars.guildTax(capital.getIncarnationId()))).thenReturn(1234L);
var plan = plan(List.of());
assertEquals(-1234, net(plan, Denars.guildTax(capital.getIncarnationId())));
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("Other citizen taxes", 12.34), saved.ledgerLastDay.get("CITIZENS"));
assertEquals(Map.of("Other citizen taxes", 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 @@ -4,21 +4,90 @@
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.mockStatic;
import static org.mockito.Mockito.when;

import java.util.Collections;
import java.util.Map;
import java.util.UUID;

import org.junit.jupiter.api.Test;

import net.tfminecraft.simplefactions.database.GuildData;
import net.tfminecraft.simplefactions.database.JsonUtil;
import net.tfminecraft.simplefactions.economy.FrozenReads;
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.guild.loans.LoanHandler;
import net.tfminecraft.simplefactions.objects.Bank;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.utils.DailyGuildTransfers;

class LedgerCitizenTaxPersistenceTest {

@Test
void heldTaxIsVisibleWithoutLegacyContributorEntries() {
Guild guild = mock(Guild.class);
UUID incarnation = UUID.randomUUID();
when(guild.getIncarnationId()).thenReturn(incarnation);
when(guild.isBase()).thenReturn(true);
when(guild.getBank()).thenReturn(mock(Bank.class));
when(guild.getFaction()).thenReturn(mock(Faction.class));
Ledger ledger = new Ledger(guild);
try (var reads = mockStatic(FrozenReads.class)) {
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(1234L);
assertTrue(ledger.getCitizenTaxesCopy().isEmpty());
assertEquals(12.34, ledger.getIncome(Cashflow.CITIZENS));
assertEquals(Map.of("Other citizen taxes", 12.34), ledger.getCitizenTaxBreakdown());
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(0L);
assertEquals(0.0, ledger.getIncome(Cashflow.CITIZENS));
assertTrue(ledger.getCitizenTaxBreakdown().isEmpty());
}
}

@Test
void fractionalLegacyContributorsAreRoundedBeforeReconcilingHeldTax() {
Guild guild = mock(Guild.class);
UUID incarnation = UUID.randomUUID();
when(guild.getIncarnationId()).thenReturn(incarnation);
Ledger ledger = new Ledger(guild);
ledger.setCitizenTaxes(Map.of("Alice", 0.005));
try (var reads = mockStatic(FrozenReads.class)) {
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(1L);
assertEquals(Map.of("Alice", 0.01), ledger.getCitizenTaxBreakdown());
assertEquals(Map.of("Alice", 0.005), ledger.getCitizenTaxesCopy());
ledger.setCitizenTaxes(Map.of("Alice", 0.005, "Bob", 0.005));
assertEquals(Map.of("Other citizen taxes", 0.01), ledger.getCitizenTaxBreakdown());
ledger.setCitizenTaxes(Map.of("Alice", 0.004, "Bob", 0.004));
assertEquals(Map.of("Other citizen taxes", 0.01), ledger.getCitizenTaxBreakdown());
}
}

@Test
void contributorBreakdownReconcilesToHeldTaxAndDoesNotChangeSavedEntries() {
Guild guild = mock(Guild.class);
UUID incarnation = UUID.randomUUID();
when(guild.getIncarnationId()).thenReturn(incarnation);
Ledger ledger = new Ledger(guild);
ledger.setCitizenTaxes(Map.of("Alice", 0.20, "Bob", 0.10));
try (var reads = mockStatic(FrozenReads.class)) {
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(80L);
assertEquals(Map.of("Alice", 0.20, "Bob", 0.10, "Other citizen taxes", 0.50), ledger.getCitizenTaxBreakdown());
assertEquals("Other citizen taxes", ledger.getCitizenTaxEntriesDescending().getFirst().getKey());
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(30L);
Map<String, Double> displayed = ledger.getCitizenTaxBreakdown();
assertEquals(Map.of("Alice", 0.20, "Bob", 0.10), displayed);
displayed.clear();
assertEquals(Map.of("Alice", 0.20, "Bob", 0.10), ledger.getCitizenTaxesCopy());
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(15L);
assertEquals(Map.of("Other citizen taxes", 0.15), ledger.getCitizenTaxBreakdown());
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(0L);
assertTrue(ledger.getCitizenTaxBreakdown().isEmpty());
ledger.setCitizenTaxes(Map.of("Alice", Double.NaN));
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(30L);
assertEquals(Map.of("Other citizen taxes", 0.30), ledger.getCitizenTaxBreakdown());
}
}

@Test
void oldJsonWithoutCitizenTaxes_loadsEmpty() {
GuildData data = JsonUtil.GSON.fromJson("{\"id\":\"capital\"}", GuildData.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.junit.jupiter.api.Test;

import net.tfminecraft.simplefactions.database.GuildData;
import net.tfminecraft.simplefactions.database.JsonUtil;
import net.tfminecraft.simplefactions.economy.FrozenReads;
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.objects.Bank;
import net.tfminecraft.simplefactions.objects.Faction;
Expand Down Expand Up @@ -100,11 +103,15 @@ void collectHistoryDay_recordsCitizensAndTariffsForTheReceivingCapital() {
when(trade.getTariffsByFactionMap()).thenReturn(new java.util.HashMap<>(Map.of(rome, 4.0)));
when(gaulCapital.getTradeBreakdown()).thenReturn(trade);

var day = Ledger.collectHistoryDay(List.of(romeCapital, gaulCapital));

assertEquals(Map.of("Alice", 2.0), day.get(romeCapital).get(LedgerHistory.Source.CITIZENS));
assertEquals(Map.of("Gaul", 4.0), day.get(romeCapital).get(LedgerHistory.Source.TARIFFS));
assertFalse(day.containsKey(gaulCapital));
UUID incarnation = UUID.randomUUID();
when(romeCapital.getIncarnationId()).thenReturn(incarnation);
try (var reads = mockStatic(FrozenReads.class)) {
reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(200L);
var day = Ledger.collectHistoryDay(List.of(romeCapital, gaulCapital));
assertEquals(Map.of("Alice", 2.0), day.get(romeCapital).get(LedgerHistory.Source.CITIZENS));
assertEquals(Map.of("Gaul", 4.0), day.get(romeCapital).get(LedgerHistory.Source.TARIFFS));
assertFalse(day.containsKey(gaulCapital));
}
// Collecting is a read: settlement still clears the citizen taxes afterwards.
assertEquals(2.0, romeCapital.getLedger().getCitizenTaxesCopy().get("Alice"), 1e-9);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ void independentDailyEntriesAccumulateAndSettlementClearsThemExactlyOnce() {
ledger.setCitizenTaxes(saved);
ledger.addCitizenTaxEntry("Alice", 3.0);
ledger.addCitizenTaxEntry("Bob", 9.0);
String holding = net.tfminecraft.denareconomy.api.Denars.guildTax(capital.getIncarnationId());
net.tfminecraft.simplefactions.testsupport.TestTreasuries.set(holding, 1400L);
assertEquals(Map.of("Alice", 5.0, "Bob", 9.0), ledger.getCitizenTaxesCopy());
assertEquals(
List.of("Bob", "Alice"),
Expand Down Expand Up @@ -104,6 +106,8 @@ void independentDailyEntriesAccumulateAndSettlementClearsThemExactlyOnce() {
assertEquals(33, first.getExternalDeltas().get(capital));
assertEquals(0, ledger.getCasinoProfit());
assertTrue(ledger.getCitizenTaxesCopy().isEmpty());
// The legacy transfer buffer does not post money; simulate its holding-account sweep.
net.tfminecraft.simplefactions.testsupport.TestTreasuries.set(holding, 0L);
for (Cashflow kind :
List.of(Cashflow.LOANS, Cashflow.INTEREST, Cashflow.MERCENARY_PAYMENTS, Cashflow.REFUNDS))
assertEquals(0, ledger.getIncome(kind));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,20 @@ void iconOverridesRetainMenuContentAndInactivityDoesNotHideUpgradeLevels() {
assertLore(creator.createUpgradesItem(fixture.player, guild), "Inactive output -40%", true);
}

@Test
void citizenTooltipShowsHeldTaxWithoutLegacyContributorRows() {
var incarnation = guild.getIncarnationId();
try (var reads = mockStatic(net.tfminecraft.simplefactions.economy.FrozenReads.class)) {
reads.when(() -> net.tfminecraft.simplefactions.economy.FrozenReads.balance("guildtax:" + incarnation))
.thenReturn(1234L);
assertTrue(guild.getLedger().getCitizenTaxesCopy().isEmpty());
ItemStack citizens = creator.createLedgerCitizensItem(guild);
assertLore(citizens, "Today: +12.34d", true);
assertLore(citizens, "Other citizen taxes: +12.34d", true);
assertLore(citizens, "No citizen taxes", false);
}
}

@Test
void ledgerSourcesSortCapAndSeparateTodayFromHistoricalTotals() {
Guild display = displayGuild(guild);
Expand All @@ -586,7 +600,7 @@ void ledgerSourcesSortCapAndSeparateTodayFromHistoricalTotals() {
Map<String, Double> taxes = new LinkedHashMap<>();
for (int n = 0; n < 7; n++) taxes.put("Citizen" + n, (double) n);
taxes.put("Negative", -2.0);
when(ledger.getCitizenTaxesCopy()).thenReturn(taxes);
when(ledger.getCitizenTaxBreakdown()).thenReturn(taxes);
history.closeDay(Map.of(LedgerHistory.Source.CITIZENS, Map.of("Yesterday", 15.0)));
history.addDeposit("Leader", 25);
history.addDeposit("Alice", 10);
Expand All @@ -596,7 +610,7 @@ void ledgerSourcesSortCapAndSeparateTodayFromHistoricalTotals() {
assertLore(citizens, "Citizen1", false);
assertLore(citizens, "Yesterday", true);
assertLore(citizens, "Negative", false);
when(ledger.getCitizenTaxesCopy()).thenReturn(Map.of("Zero", 0.0));
when(ledger.getCitizenTaxBreakdown()).thenReturn(Map.of("Zero", 0.0));
assertLore(creator.createLedgerCitizensItem(display), "No citizen taxes", true);
Guild paying = displayGuild(fixture.guild("guild", 0));
when(paying.getLedger().getIncome(Cashflow.GUILD_PAYMENTS)).thenReturn(-12.0);
Expand Down
Loading