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 @@ -12,8 +12,6 @@
import net.tfminecraft.simplefactions.database.Database;
import net.tfminecraft.simplefactions.database.DurableJson;
import net.tfminecraft.simplefactions.government.movement.Movement;
import net.tfminecraft.simplefactions.installation.InstallationKind;
import net.tfminecraft.simplefactions.loaders.InstallationConfigLoader;
import net.tfminecraft.simplefactions.managers.FactionManager;
import net.tfminecraft.simplefactions.managers.TitleManager;
import net.tfminecraft.simplefactions.objects.Faction;
Expand All @@ -23,12 +21,11 @@
/** Replays the nonfinancial preparation of a day before its monetary plan is published. */
public final class DailyPreparationJournal {
private record Removal(String faction, UUID incarnation, Integer province) {}
public record Prepared(long day, long timestamp, List<FactionUpkeepPlan> upkeep) {}
public record Prepared(long day, long timestamp) {}
private static final class Saved {
long day;
long timestamp;
List<Removal> removals;
List<FactionUpkeepPlan> upkeep;
Map<String, Map<String, Double>> organization;
Map<String, Integer> offlineDays;
boolean prepared;
Expand Down Expand Up @@ -62,10 +59,8 @@ public Prepared prepare(long day, long now) throws IOException {
saved.day = day;
saved.timestamp = now;
saved.removals = new ArrayList<>();
saved.upkeep = new ArrayList<>();
for (Faction faction : FactionManager.factions.stream().sorted(Comparator.comparing(Faction::getId)).toList()) {
saved.removals.add(new Removal(faction.getId(), SettlementGuilds.capital(faction).getIncarnationId(), removal(faction)));
saved.upkeep.add(FactionUpkeepPlan.calculate(faction));
}
DurableJson.write(path, saved);
}
Expand All @@ -89,8 +84,6 @@ public Prepared prepare(long day, long now) throws IOException {
saved.organization.put(faction.getId(), values);
}
saved.offlineDays = FactionCleanup.planOfflineDays(FactionManager.factions);
List<Removal> removals = saved.removals;
saved.upkeep = saved.upkeep.stream().map(plan -> afterProvinceLoss(plan, removals)).toList();
DurableJson.write(path, saved);
}
for (Removal removal : saved.removals) {
Expand All @@ -106,7 +99,7 @@ public Prepared prepare(long day, long now) throws IOException {
saved.prepared = true;
DurableJson.write(path, saved);
}
return new Prepared(saved.day, saved.timestamp, saved.upkeep);
return new Prepared(saved.day, saved.timestamp);
}

public void complete(long day) throws IOException {
Expand All @@ -116,17 +109,6 @@ public void complete(long day) throws IOException {
DurableJson.write(path, saved);
}

private static FactionUpkeepPlan afterProvinceLoss(FactionUpkeepPlan plan, List<Removal> removals) {
Integer lost = removals.stream().filter(r -> r.faction().equals(plan.faction())).findFirst().orElseThrow().province();
if (lost == null) return plan;
var installations = plan.installations().stream().filter(i -> !lost.equals(i.province)).toList();
var construction = plan.construction();
if (construction != null && lost.equals(construction.province)) construction = null;
double cost = installations.stream().mapToDouble(i -> InstallationConfigLoader.getDailyUpkeep(InstallationKind.fromCommand(i.kind), i.level)).sum();
return new FactionUpkeepPlan(plan.faction(), plan.regimentSlots(), installations, construction,
plan.charge(), plan.armyLedgerCost(), cost);
}

private static Integer removal(Faction faction) {
List<Integer> provinces = faction.getProvinces();
if (CivilWarBorderLock.isLocked(faction) || net.tfminecraft.simplefactions.guild.HeldRealms.isHeld(faction)
Expand All @@ -151,7 +133,7 @@ private void save(Faction faction) throws IOException {
private static Saved read(Path path) throws IOException {
Saved saved = DurableJson.read(path, Saved.class);
if (!path.getFileName().toString().equals(saved.day + ".json") || saved.timestamp <= 0
|| saved.removals == null || saved.upkeep == null || (saved.prepared && saved.organization == null))
|| saved.removals == null || (saved.prepared && saved.organization == null))
throw new IOException("Invalid daily preparation: " + path);
return saved;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,22 @@
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.utils.Formatter;

/** The pre-income army and installation decisions, calculated without changing the faction. */
/** The army and installation decisions, calculated without changing the faction. */
public record FactionUpkeepPlan(String faction, Map<String, Integer> regimentSlots,
List<InstallationData> installations, InstallationConstructionData construction,
long charge, double armyLedgerCost, double installationLedgerCost) {

/** Plans against the ruling guild's balance as it stands now. */
public static FactionUpkeepPlan calculate(Faction faction) {
var bank = SettlementGuilds.capital(faction).getBank();
return calculate(faction, bank == null ? 0 : bank.getWealth());
}

/**
* Plans against {@code available}: during settlement, the ruling guild's balance once the
* day's income and other transfers have arrived, so upkeep is paid from that income.
*/
public static FactionUpkeepPlan calculate(Faction faction, double available) {
if (net.tfminecraft.simplefactions.guild.HeldRealms.isHeld(faction)) return paused(faction);
List<Regiment> regiments = new ArrayList<>();
if (faction.getMilitary() != null) {
Expand All @@ -29,7 +39,7 @@ public static FactionUpkeepPlan calculate(Faction faction) {
}
}
boolean bank = SettlementGuilds.capital(faction).getBank() != null;
double wealth = bank ? SettlementGuilds.capital(faction).getBank().getWealth() : 0;
double wealth = bank ? available : 0;
double factor = 1 + faction.getModifier(FactionModifiers.MILITARY_UPKEEP).getAmount() / 100;
double army = armyCost(regiments, factor);
if (!bank && army > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public interface Vehicles {
private final long now;
private final List<Guild> guilds;
private final List<Claim> claims;
private List<FactionUpkeepPlan> preparedUpkeep;
private final Map<Guild, Ledger> ledgers = new LinkedHashMap<>();
private final Map<Guild, SettlementLedgerView> views = new LinkedHashMap<>();
private final Map<Guild, MercenaryCompany> companies = new LinkedHashMap<>();
Expand All @@ -67,29 +66,29 @@ public RealmSettlementCalculation(long day, long now, Collection<Guild> guilds,
this.claims = List.copyOf(claims);
}

public RealmSettlementCalculation withPreparedUpkeep(List<FactionUpkeepPlan> upkeep) {
this.preparedUpkeep = List.copyOf(upkeep);
return this;
}

public SettlementJournal.Plan calculate(Vehicles vehicles) {
capture();
accrue();
var history = Ledger.collectHistoryDay(guilds, ledgers::get);
Map<String, Long> beforeIncome = new LinkedHashMap<>(balances);
// Income and the day's other payments land first, so upkeep and loans are paid from them.
DailyGuildTransfers buffer = new DailyGuildTransfers();
for (Guild guild : guilds) ledgers.get(guild).populateDailyTransfers(buffer);
for (var from : buffer.getTransfers().entrySet()) {
for (var to : from.getValue().entrySet()) transfer(account(from.getKey()), account(to.getKey()), Money.cents(to.getValue()));
}
for (var entry : buffer.getExternalDeltas().entrySet()) transfer(Denars.MINT, account(entry.getKey()), Money.cents(entry.getValue()));
for (Guild guild : guilds) ledgers.get(guild).populateTransfersBeforeLoans(buffer);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
apply(buffer);
for (Guild guild : guilds) {
if (canMove(guild) && guild.isBase()) {
// CITIZENS in the transfer buffer is backed by held tax, never newly minted.
long held = Money.cents(ledgers.get(guild).getIncome(Cashflow.CITIZENS));
transfer(Denars.guildTax(guild.getIncarnationId()), Denars.MINT, held);
}
}
chargeUpkeep();
DailyGuildTransfers loanPayments = new DailyGuildTransfers();
for (Guild guild : guilds) {
long left = balances.getOrDefault(account(guild), 0L);
ledgers.get(guild).populateLoanTransfers(loanPayments, Money.denars(Math.max(0, left)));
}
apply(loanPayments);
for (Guild guild : guilds) liquidate(guild, !java.util.Objects.equals(beforeIncome.get(account(guild)), balances.get(account(guild))));
for (Guild guild : guilds) payouts(guild, buffer);
payOrphanClaims();
Expand All @@ -114,8 +113,24 @@ public SettlementJournal.Plan calculate(Vehicles vehicles) {
return new SettlementJournal.Plan(day, posting, JsonUtil.GSON.toJsonTree(changes).getAsJsonObject());
}

private void apply(DailyGuildTransfers buffer) {
for (var from : buffer.getTransfers().entrySet()) {
for (var to : from.getValue().entrySet()) transfer(account(from.getKey()), account(to.getKey()), Money.cents(to.getValue()));
}
for (var entry : buffer.getExternalDeltas().entrySet()) transfer(Denars.MINT, account(entry.getKey()), Money.cents(entry.getValue()));
}

/** Army and installation upkeep, planned against each ruling guild's balance after the day's income. */
private void chargeUpkeep() {
for (Faction faction : guilds.stream().map(Guild::getFaction).distinct().toList()) {
Guild capital = SettlementGuilds.capital(faction);
FactionUpkeepPlan plan = FactionUpkeepPlan.calculate(faction, Money.denars(balances.get(account(capital))));
changes.upkeep.add(plan);
transfer(account(capital), Denars.MINT, plan.charge());
}
}

private void capture() {
Map<Faction, FactionUpkeepPlan> upkeep = new LinkedHashMap<>();
for (Guild guild : guilds) {
balances.put(account(guild), guild.getBank() == null ? 0 : Money.cents(guild.getBank().getWealth()));
initial.put(guild, guild.getBank() == null ? 0 : guild.getBank().getWealth());
Expand All @@ -128,15 +143,6 @@ private void capture() {
companies.put(guild, copy);
}
}
for (Faction faction : guilds.stream().map(Guild::getFaction).distinct().toList()) {
FactionUpkeepPlan plan = preparedUpkeep == null ? FactionUpkeepPlan.calculate(faction)
: preparedUpkeep.stream().filter(p -> p.faction().equals(faction.getId())).findFirst().orElseThrow();
changes.upkeep.add(plan);
upkeep.put(faction, plan);
Guild capital = SettlementGuilds.capital(faction);
transfer(account(capital), Denars.MINT, plan.charge());
initial.put(capital, Money.denars(balances.get(account(capital))));
}
for (Guild guild : guilds) {
for (Loan original : guild.getLoanHandler().getLoansGiven()) {
if (!credits.containsKey(original.getBorrower()) || original.getIssuer() != guild)
Expand All @@ -148,10 +154,6 @@ private void capture() {
for (Guild guild : guilds) {
Map<Cashflow, Double> income = new EnumMap<>(Cashflow.class);
if (guild.isBase()) {
FactionUpkeepPlan costs = upkeep.get(guild.getFaction());
MercenaryCompany company = companies.get(guild);
income.put(Cashflow.MILITARY_UPKEEP, -costs.armyLedgerCost() - (company == null || !company.isFormed() ? 0 : company.getSlotUpkeep()));
income.put(Cashflow.INSTALLATIONS, -costs.installationLedgerCost());
income.put(Cashflow.CITIZENS, Money.denars(Denars.balance(Denars.guildTax(guild.getIncarnationId()))));
}
SettlementLedgerView view = new SettlementLedgerView(ledgers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void settle(long day) throws IOException {
var claims = CharacterDeaths.pendingPayments().stream().map(payment -> new RealmSettlementCalculation.Claim(
payment.id(), payment.guildIncarnation(), payment.characterId(), payment.cents(), payment.kind())).toList();
return new RealmSettlementCalculation(day, prepared.timestamp(), FactionManager.getAllGuilds(), claims)
.withPreparedUpkeep(prepared.upkeep()).calculate((balances, legs, changes) -> VehicleSettlement.calculate(balances, legs, changes, prepared.timestamp()));
.calculate((balances, legs, changes) -> VehicleSettlement.calculate(balances, legs, changes, prepared.timestamp()));
}, effects);
if (result != Result.APPLIED) throw new IOException("Realm settlement " + day + " remains " + result);
preparation.complete(day);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,26 @@ private double loanBudget(DailyGuildTransfers buffer) {
}

public void populateDailyTransfers(DailyGuildTransfers buffer) {
populateTransfersBeforeLoans(buffer);
populateLoanTransfers(buffer, loanBudget(buffer));
if (settlement != null) return;
if (!skipsMoneyMovement()) {
citizenTaxes.clear();
}
loanPayments.clear();
interestPayments.clear();
// Taxed once, on the day it was won.
casinoProfit = 0;
vehicleFeeIncome = 0;
// Rebuilt from the persisted buckets by every pre-pass, so clearing them for a
// bankrupt hirer too keeps yesterday's bill from being paid twice.
mercenaryPayments.clear();
refunds.clear();
clearSettledContractBuckets();
}

/** Every daily payment except the automatic loan payments, which come last. */
public void populateTransfersBeforeLoans(DailyGuildTransfers buffer) {
// Drop the gift before anything else is committed. Other costs may then still fit,
// so a guild that was short only because of the donation stays solvent.
cancelDonationIfUnaffordable();
Expand All @@ -1027,30 +1047,21 @@ public void populateDailyTransfers(DailyGuildTransfers buffer) {
if (cf == Cashflow.LOAN_PAYMENTS || cf == Cashflow.INTEREST_PAYMENTS) continue;
applySettlementFor(cf, buffer);
}
// Loans are paid last, from what is left after today's other payments. The lender is
// credited in full, so an unfunded payment would create money; any shortfall stays owed.
}

/**
* Loans are paid last, from {@code budget}: what is left after today's other payments. The
* lender is credited in full, so an unfunded payment would create money; any shortfall stays owed.
*/
public void populateLoanTransfers(DailyGuildTransfers buffer, double budget) {
loanPlan = guild.getLoanHandler() == null ? Map.of()
: LoanFunding.plan(loansTaken(), loanBudget(buffer));
: LoanFunding.plan(loansTaken(), Math.max(0.0, budget));
try {
applySettlementFor(Cashflow.LOAN_PAYMENTS, buffer);
applySettlementFor(Cashflow.INTEREST_PAYMENTS, buffer);
} finally {
loanPlan = null;
}
if (settlement != null) return;
if (!skipsMoneyMovement()) {
citizenTaxes.clear();
}
loanPayments.clear();
interestPayments.clear();
// Taxed once, on the day it was won.
casinoProfit = 0;
vehicleFeeIncome = 0;
// Rebuilt from the persisted buckets by every pre-pass, so clearing them for a
// bankrupt hirer too keeps yesterday's bill from being paid twice.
mercenaryPayments.clear();
refunds.clear();
clearSettledContractBuckets();
}

/**
Expand Down Expand Up @@ -1113,7 +1124,7 @@ private void applySettlementFor(Cashflow cf, DailyGuildTransfers buffer) {
case TRADE_UPKEEP:
buffer.addExternalDelta(guild, getIncome(cf));
return;
// INSTALLATIONS: charged by the pre-income upkeep plan, so getIncome() is ledger GUI display only.
// INSTALLATIONS: charged by the upkeep plan after income, so getIncome() is ledger GUI display only.
// VEHICLE_UPKEEP: withdrawn in VehicleUpkeepService for pool and installation vehicles.
case UPGRADES_UPKEEP:
case PENALTIES:
Expand All @@ -1131,7 +1142,7 @@ private void applySettlementFor(Cashflow cf, DailyGuildTransfers buffer) {
case VEHICLE_FEES:
return;

// The faction share of military upkeep is charged by the pre-income upkeep plan;
// The faction share of military upkeep is charged by the upkeep plan after income;
// only the company's slot upkeep settles here.
case MILITARY_UPKEEP: {
double slots = getCompanySlotUpkeep();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ class DailyPreparationJournalTest {
net.tfminecraft.simplefactions.guild.HeldRealms.hold(faction, leader, 1);
var database = mock(Database.class);
when(database.saveFactionDurably(faction)).thenReturn(true);
var prepared = new DailyPreparationJournal(disk.root.resolve("day-preparation"), database).prepare(2, 1000);
new DailyPreparationJournal(disk.root.resolve("day-preparation"), database).prepare(2, 1000);
assertEquals(List.of(1, 2), faction.getProvinces());
var upkeep = prepared.upkeep().getFirst();
var upkeep = FactionUpkeepPlan.calculate(faction, 0);
assertEquals(0, upkeep.charge());
assertEquals(Map.of("guard", 6), upkeep.regimentSlots());
}
Expand Down
Loading
Loading