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
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 @@ -168,6 +168,7 @@ public static boolean requireProvinces(org.bukkit.command.CommandSender sender)
public static int supplyHubBaseLimit = 2;
public static double supplyHubBaseUpkeep = 15.0;
public static double supplyHubUpkeepGrowth = 1.5;
public static double supplyHubMaxTax = 50.0;

public static Map<Scope, LawEffect> baseEffects = new HashMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ public void onEnable() {
if (Cache.provincesEnabled) {
provinceSnapshot = provinceManager.createSnapshotShell();
provinceManager.recalculate();
// Hub tax is assessed at the day change; do it once now so menus are right before then.
net.tfminecraft.simplefactions.guild.hub.HubTaxService.refresh(provinceManager);
}
inventoryManager.start();
vehicleMaintenanceDecayTask.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ public void loadFactions() {
data.laws,
data.governmentData
);
f.getTaxHandler().setHubTax(data.hubTax == null ? 0.0 : data.hubTax);
f.getVehicleFeeHandler().load(data.vehicleFees, data.vehicleTypeFees);
if (data.lawChangedAt != null) {
for (Map.Entry<String, Long> entry : data.lawChangedAt.entrySet()) {
Expand Down Expand Up @@ -329,6 +330,7 @@ public void saveFaction(Faction f) {
data.vassalTax = f.getTaxHandler().getVassalTax();
data.dividendTax = f.getTaxHandler().getDividendTax();
data.tariffs = f.getTaxHandler().getTariffs();
data.hubTax = f.getTaxHandler().getHubTax();
data.specificTaxes = serializeSpecificTaxes(f.getTaxHandler());
data.vehicleFees = new HashMap<>(f.getVehicleFeeHandler().serializeRates());
data.vehicleTypeFees = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public class FactionData {

public Double tariffs;

@SerializedName("hub tax")
public Double hubTax;

@SerializedName("specific taxes")
public HashMap<String, HashMap<String, Double>> specificTaxes = new HashMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public enum TaxTarget {
VASSALS("Vassals"),
DIVIDENDS("Dividends"),
TARIFFS("Tariffs"),
HUB_TAX("Hub Tax"),
TARIFF_ID("Faction Specific Tariffs"),
GUILD_ID("Guild Specific"),
VASSAL_ID("Vassal Specific");
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/tfminecraft/simplefactions/guild/Guild.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import net.tfminecraft.simplefactions.guild.income.IncomePreviewContext;
import net.tfminecraft.simplefactions.guild.income.Ledger;
import net.tfminecraft.simplefactions.guild.income.TradeBreakdown;
import net.tfminecraft.simplefactions.guild.hub.HubTaxBreakdown;
import net.tfminecraft.simplefactions.guild.loans.LoanHandler;
import net.tfminecraft.simplefactions.guild.upgrade.Upgrade;
import net.tfminecraft.simplefactions.guild.upgrade.UpgradeExpansion;
Expand Down Expand Up @@ -91,6 +92,7 @@ public class Guild {
private List<Modifier> wealthModifiers = new ArrayList<>();

private TradeBreakdown breakdown = new TradeBreakdown();
private volatile HubTaxBreakdown hubTaxBreakdown = HubTaxBreakdown.empty();

private Stance stance;

Expand Down Expand Up @@ -703,6 +705,14 @@ public TradeBreakdown getTradeBreakdown() {
}
public void setTradeBreakdown(TradeBreakdown breakdown) { this.breakdown = breakdown; }

public HubTaxBreakdown getHubTaxBreakdown() {
return hubTaxBreakdown;
}

public void setHubTaxBreakdown(HubTaxBreakdown hubTaxBreakdown) {
this.hubTaxBreakdown = hubTaxBreakdown;
}

public double getRepressFavourCost() {
return Formatter.formatDouble(getStabilityEffect()*0.5);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package net.tfminecraft.simplefactions.guild.hub;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import net.tfminecraft.simplefactions.government.proposal.TaxTarget;
import net.tfminecraft.simplefactions.guild.income.IncomePreviewContext;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.utils.Formatter;

/** Immutable daily assessment. Ordinary trade recalculations do not replace it. */
public final class HubTaxBreakdown {
public record Assessment(double taxableIncome, double tax) {
}

private static final HubTaxBreakdown EMPTY = new HubTaxBreakdown(Map.of(), Map.of(), Map.of(), Set.of());

private final Set<Faction> exemptHosts;
private final Map<SupplyHub, Assessment> hubs;
private final Map<Faction, Double> taxableByFaction;
private final Map<Faction, Double> taxesByFaction;

public HubTaxBreakdown(
Map<SupplyHub, Assessment> hubs,
Map<Faction, Double> taxableByFaction,
Map<Faction, Double> taxesByFaction,
Set<Faction> exemptHosts) {
this.exemptHosts = Set.copyOf(exemptHosts);
this.hubs = Map.copyOf(hubs);
this.taxableByFaction = Map.copyOf(taxableByFaction);
this.taxesByFaction = Map.copyOf(taxesByFaction);
}

public static HubTaxBreakdown empty() {
return EMPTY;
}

public Assessment forHub(SupplyHub hub) {
return hubs.getOrDefault(hub, new Assessment(0, 0));
}

public double getTaxableIncome(Faction host) {
return taxableByFaction.getOrDefault(host, 0.0);
}

public Map<Faction, Double> getTaxesByFaction() {
IncomePreviewContext context = IncomePreviewContext.current();
if (context == null) {
return taxesByFaction;
}
Map<Faction, Double> preview = new HashMap<>(taxesByFaction);
for (Map.Entry<Faction, Double> entry : taxableByFaction.entrySet()) {
Faction host = entry.getKey();
if (!exemptHosts.contains(host) && context.affectsHubTax(host)) {
preview.put(host, Formatter.formatDouble(
entry.getValue() * host.getTaxRate(TaxTarget.HUB_TAX, null, true) / 100.0));
}
}
return preview;
}

public double getTax(Faction host) {
return getTaxesByFaction().getOrDefault(host, 0.0);
}

public double getTotalTax() {
double total = 0;
for (double tax : getTaxesByFaction().values()) {
total += tax;
}
return Formatter.formatDouble(total);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package net.tfminecraft.simplefactions.guild.hub;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.tfminecraft.simplefactions.government.proposal.TaxTarget;
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.guild.hub.HubTaxBreakdown.Assessment;
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Link;
import net.tfminecraft.simplefactions.installation.Installation;
import net.tfminecraft.simplefactions.loaders.InstallationConfigLoader;
import net.tfminecraft.simplefactions.managers.FactionManager;
import net.tfminecraft.simplefactions.managers.ProvinceManager;
import net.tfminecraft.simplefactions.managers.RelationManager;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.utils.Formatter;

/** Computes hub tax once per day on copies of the province data. Server thread only. */
public final class HubTaxService {
private HubTaxService() {
}

public static void refresh(ProvinceManager provinces) {
List<Guild> guilds = SupplyHubService.allGuilds();
for (Guild guild : guilds) {
guild.setHubTaxBreakdown(assess(provinces, guild, guilds));
}
}

public static HubTaxBreakdown assess(ProvinceManager provinces, Guild guild, List<Guild> guilds) {
List<Link> links = HubNetwork.linksFor(guild);
if (links.isEmpty() || !guild.hasCapital()) {
return HubTaxBreakdown.empty();
}
double base = incomeWith(provinces, guild, List.of());
double full = incomeWith(provinces, guild, links);
Map<SupplyHub, Double> without = new HashMap<>();
Map<SupplyHub, Faction> hosts = new HashMap<>();
for (SupplyHub hub : guild.getSupplyHubs()) {
Installation installation = SupplyHubService.findInstallation(
hub.ownerFactionId(), hub.installationId());
Faction host = FactionManager.getByString(hub.ownerFactionId());
if (installation == null || host == null) {
continue;
}
boolean allowed = SupplyHubService.ownerAllows(
guild.getFaction().getId(), host.getId(), host.hasHubPermit(guild.getId()));
if (!SupplyHubService.standing(
hub, true, allowed,
InstallationConfigLoader.getHubSlots(installation.getKind(), installation.getLevel()),
SupplyHubService.atInstallation(host.getId(), installation.getId(), guilds)).active()) {
continue;
}
List<Link> remaining = links.stream()
.filter(link -> !touches(link, installation))
.toList();
without.put(hub, incomeWith(provinces, guild, remaining));
hosts.put(hub, host);
}
Map<SupplyHub, Double> taxable = taxableIncome(base, full, without);
Map<SupplyHub, Assessment> assessments = new HashMap<>();
Set<Faction> exemptHosts = new HashSet<>();
Map<Faction, Double> taxableByFaction = new HashMap<>();
Map<Faction, Double> taxesByFaction = new HashMap<>();
for (Map.Entry<SupplyHub, Double> entry : taxable.entrySet()) {
Faction host = hosts.get(entry.getKey());
double income = entry.getValue();
double tax = 0;
taxableByFaction.merge(host, income, Double::sum);
if (RelationManager.sameRealm(host, guild.getFaction())) {
exemptHosts.add(host);
} else {
tax = income * host.getTaxRate(TaxTarget.HUB_TAX, null, true) / 100.0;
taxesByFaction.merge(host, tax, Double::sum);
}
assessments.put(entry.getKey(), new Assessment(income, tax));
}
taxesByFaction.replaceAll((host, tax) -> Formatter.formatDouble(tax));
return new HubTaxBreakdown(assessments, taxableByFaction, taxesByFaction, exemptHosts);
}

/** Positive marginal gains, scaled only when their sum exceeds the total hub gain. */
public static <T> Map<T, Double> taxableIncome(double base, double full, Map<T, Double> without) {
double gain = Math.max(0, full - base);
Map<T, Double> marginal = new HashMap<>();
double total = 0;
for (Map.Entry<T, Double> entry : without.entrySet()) {
double amount = Math.max(0, full - entry.getValue());
marginal.put(entry.getKey(), amount);
total += amount;
}
if (total > gain) {
double scale = gain / total;
marginal.replaceAll((hub, amount) -> amount * scale);
}
return marginal;
}

static boolean touches(Link link, Installation installation) {
return link.mode().getKind() == installation.getKind()
&& (link.fromProvince() == installation.getProvince()
|| link.toProvince() == installation.getProvince());
}

private static double incomeWith(ProvinceManager source, Guild guild, List<Link> links) {
ProvinceManager snapshot = source.createSnapshotShell();
snapshot.copyAllDataFrom(source);
snapshot.setHubLinksOverride(Map.of(guild.getId(), links));
snapshot.recalculateForSingleGuild(guild, false);
return snapshot.getGrossTradeIncome(guild);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ private static boolean list(Player player) {
hub, guild.getSupplyHubs(), SupplyHubService.baseUpkeep(), SupplyHubService.upkeepGrowth());
player.sendMessage("§f" + name + " §7(" + kind + ") §7owned by §f" + ownerName);
player.sendMessage("§7Upkeep: §e" + Formatter.formatMoney(cost) + "d/day §7" + statusOf(guild, hub, installation));
HubTaxBreakdown.Assessment assessment = guild.getHubTaxBreakdown().forHub(hub);
player.sendMessage("§7Taxable income: §e" + Formatter.formatMoney(assessment.taxableIncome())
+ "d/day §7Hub Tax: §e" + Formatter.formatMoney(assessment.tax()) + "d/day");
for (String line : connectionLines(guild, installation)) {
player.sendMessage(line);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public static void loadConfig(FileConfiguration config) {
}
}
}
double maxTax = config == null ? 50.0 : config.getDouble("supply-hubs.max-tax", 50.0);
Cache.supplyHubMaxTax = Double.isFinite(maxTax) ? Math.max(0, Math.min(100, maxTax)) : 50.0;
Cache.supplyHubBaseLimit = limit;
Cache.supplyHubBaseUpkeep = upkeep;
Cache.supplyHubUpkeepGrowth = growth;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public enum Cashflow {
CITIZENS("#c7bf85From tax on #94b572Citizens", false, true),
TARIFF_PAYMENTS("#5cc46aTariffs", false, false),
TARIFFS("#5cc46aTariffs", false, true),
HUB_TAX_PAYMENTS("#5cc46aHub Tax", false, false),
HUB_TAX("#5cc46aHub Tax", false, true),
LOAN_PAYMENTS("#e6bb57Loans", false, false),
LOANS("#e6bb57Loans", false, false),
INTEREST_PAYMENTS("#c25140Interest Payments", false, false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ public boolean affects(Faction owner) {
return owner != null && faction == owner && (law != null || taxRate != null);
}

public boolean affectsHubTax(Faction owner) {
return owner != null && faction == owner && (law != null || taxTarget == TaxTarget.HUB_TAX);
}

public boolean previewsLaw(Faction owner) {
return owner != null && faction == owner && law != null;
}

private boolean replaces(TaxHandler handler, TaxTarget query, String id) {
if (taxTarget == null || query == null) {
return false;
Expand Down Expand Up @@ -253,7 +261,9 @@ private double clampForLaw(TaxTarget target, double rate) {
}
if (effect.hasBrackets()) {
for (Map.Entry<Brackets, Bracket> entry : effect.getBrackets().entrySet()) {
if (BracketToTaxTarget.convert(entry.getKey()) == family && entry.getValue() != null) {
if ((BracketToTaxTarget.convert(entry.getKey()) == family
|| (family == TaxTarget.HUB_TAX && entry.getKey() == Brackets.TARIFFS))
&& entry.getValue() != null) {
rate = clamp(rate, entry.getValue());
}
}
Expand Down Expand Up @@ -299,7 +309,7 @@ private static Rules ruleFor(TaxTarget target) {
case GUILDS, GUILD_ID -> Rules.GUILD_TAX;
case VASSALS, VASSAL_ID -> Rules.VASSAL_TAX;
case DIVIDENDS -> Rules.DIVIDEND_TAX;
case TARIFFS, TARIFF_ID -> Rules.TARIFFS;
case TARIFFS, TARIFF_ID, HUB_TAX -> Rules.TARIFFS;
default -> null;
};
}
Expand Down
Loading
Loading