From 5b634c63304ea46621e50bad8cdc2da09d8edfea Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:04:11 +0000 Subject: [PATCH] Give hub tax and supply hubs their own law settings. Hub tax borrowed the tariff bracket and rule, so Free Trade and Decentralized, which switch tariffs off, forced it to zero. It now has its own bracket, set by each economy law: Free Trade 0-10%, Mercantilism 0-25%, Protectionism 0-40%, Isolationism 0-50%. Decentralized gets two new rules instead: it cannot collect hub taxes, and its guilds cannot build supply hubs. Hubs such a guild already has go dormant. With a laws.yml that predates this, every faction can build hubs and charge up to 10%. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../simplefactions/enums/Brackets.java | 1 + .../simplefactions/enums/Rules.java | 2 + .../simplefactions/guild/hub/HubNetwork.java | 8 +- .../guild/hub/HubTaxService.java | 2 +- .../guild/hub/SupplyHubCommands.java | 10 +- .../guild/hub/SupplyHubService.java | 34 ++++ .../guild/income/IncomePreviewContext.java | 53 +++++- .../simplefactions/guild/income/Ledger.java | 10 + .../simplefactions/laws/LawEffect.java | 4 +- .../managers/inventory/SupplyHubView.java | 5 +- .../simplefactions/objects/Faction.java | 19 ++ .../objects/handler/TaxHandler.java | 34 ++-- .../utils/BracketToTaxTarget.java | 2 + src/main/resources/laws.yml | 7 + .../guild/hub/HubTaxServiceTest.java | 2 + .../guild/hub/SupplyHubServiceTest.java | 42 +++++ .../guild/income/BranchIncomePreviewTest.java | 2 + .../guild/income/LedgerHubTaxTest.java | 53 ++++++ .../simplefactions/laws/HubLawTest.java | 172 ++++++++++++++++++ .../ProvinceManagerHubTransportTest.java | 34 ++++ .../objects/handler/TaxHandlerHubTaxTest.java | 58 +++++- 21 files changed, 514 insertions(+), 40 deletions(-) create mode 100644 src/test/java/net/tfminecraft/simplefactions/laws/HubLawTest.java diff --git a/src/main/java/net/tfminecraft/simplefactions/enums/Brackets.java b/src/main/java/net/tfminecraft/simplefactions/enums/Brackets.java index b71cac0b..27fce511 100644 --- a/src/main/java/net/tfminecraft/simplefactions/enums/Brackets.java +++ b/src/main/java/net/tfminecraft/simplefactions/enums/Brackets.java @@ -7,6 +7,7 @@ public enum Brackets { VASSAL_TAX("Vassal Tax"), DIVIDEND_TAX("Dividend Tax"), TARIFFS("Tariffs"), + HUB_TAX("Hub Tax"), VEHICLE_TAX("Vehicle Tax (% of upkeep)"), REGISTRATION_FEE("Registration Fee (x upkeep)"), TRANSFER_FEE("Transfer Fee (x upkeep)"); diff --git a/src/main/java/net/tfminecraft/simplefactions/enums/Rules.java b/src/main/java/net/tfminecraft/simplefactions/enums/Rules.java index 7d1defb9..9d54a4f7 100644 --- a/src/main/java/net/tfminecraft/simplefactions/enums/Rules.java +++ b/src/main/java/net/tfminecraft/simplefactions/enums/Rules.java @@ -14,6 +14,8 @@ public enum Rules { GUILD_TAX("Can Collect Guild Taxes", true), //Implemented DIVIDEND_TAX("Can Collect Dividend Taxes", true), //Implemented TARIFFS("Can Impose Tariffs", true), //Implemented + HUB_TAX("Can Collect Hub Taxes", true), //Implemented + SUPPLY_HUBS("Can Build Supply Hubs", true), //Implemented VEHICLE_TAX("Can Collect Vehicle Tax", true), //Implemented REGISTRATION_FEE("Can Charge Vehicle Registration Fees", true), //Implemented TRANSFER_FEE("Can Charge Vehicle Transfer Fees", true), //Implemented diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubNetwork.java b/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubNetwork.java index b552a696..562134cd 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubNetwork.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubNetwork.java @@ -51,7 +51,8 @@ public static List linksFor(Guild guild) { if (guild == null || guild.getId() == null) { return List.of(); } - return links.getOrDefault(guild.getId(), List.of()); + List cached = links.getOrDefault(guild.getId(), List.of()); + return cached.isEmpty() || SupplyHubService.allowsSupplyHubs(guild) ? cached : List.of(); } /** Rebuilds every guild's connections. Server thread only. */ @@ -179,9 +180,6 @@ private static List activeSites(Guild guild, List guilds) { List sites = new ArrayList<>(); String guildFactionId = guild.getFaction() == null ? null : guild.getFaction().getId(); for (SupplyHub hub : guild.getSupplyHubs()) { - if (SupplyHubService.beyondGuildLimit(guild, hub)) { - continue; - } Installation installation = SupplyHubService.findInstallation(hub.ownerFactionId(), hub.installationId()); if (installation == null) { @@ -190,7 +188,7 @@ private static List activeSites(Guild guild, List guilds) { Faction owner = FactionManager.getByString(hub.ownerFactionId()); boolean permit = owner != null && owner.hasHubPermit(guild.getId()); HubStanding standing = SupplyHubService.standing( - hub, + guild, hub, true, SupplyHubService.ownerAllows(guildFactionId, hub.ownerFactionId(), permit), InstallationConfigLoader.getHubSlots(installation.getKind(), installation.getLevel()), diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubTaxService.java b/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubTaxService.java index ab686a42..820d5aa6 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubTaxService.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/hub/HubTaxService.java @@ -49,7 +49,7 @@ public static HubTaxBreakdown assess(ProvinceManager provinces, Guild guild, Lis boolean allowed = SupplyHubService.ownerAllows( guild.getFaction().getId(), host.getId(), host.hasHubPermit(guild.getId())); if (!SupplyHubService.standing( - hub, true, allowed, + guild, hub, true, allowed, InstallationConfigLoader.getHubSlots(installation.getKind(), installation.getLevel()), SupplyHubService.atInstallation(host.getId(), installation.getId(), guilds)).active()) { continue; diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java b/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java index 02baf58f..fba455bc 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubCommands.java @@ -186,6 +186,10 @@ private static boolean build(Player player) { player.sendMessage("§cOnly the guild leader can build a supply hub"); return true; } + if (!SupplyHubService.allowsSupplyHubs(guild)) { + player.sendMessage(SupplyHubService.buildFailureMessage(BuildFailure.ECONOMY_DISALLOWS, null, 0)); + return true; + } List covering = covering(player.getLocation()); if (covering.isEmpty()) { player.sendMessage("§cYou are not standing inside an installation"); @@ -305,10 +309,6 @@ private static void listPermits(Player player, Faction faction) { } private static String statusOf(Guild guild, SupplyHub hub, Installation installation) { - if (SupplyHubService.beyondGuildLimit(guild, hub)) { - return SupplyHubService.statusText(new HubStanding( - false, SupplyHubService.DormantReason.BEYOND_GUILD_LIMIT)); - } String guildFactionId = guild.getFaction() == null ? null : guild.getFaction().getId(); Faction owner = FactionManager.getByString(hub.ownerFactionId()); boolean permit = owner != null && owner.hasHubPermit(guild.getId()); @@ -319,7 +319,7 @@ private static String statusOf(Guild guild, SupplyHub hub, Installation installa List atInstallation = SupplyHubService.atInstallation( hub.ownerFactionId(), hub.installationId(), SupplyHubService.allGuilds()); HubStanding standing = SupplyHubService.standing( - hub, + guild, hub, installation != null, SupplyHubService.ownerAllows(guildFactionId, hub.ownerFactionId(), permit), slots, diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubService.java b/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubService.java index ef08e87e..c8f089ab 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubService.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubService.java @@ -13,6 +13,8 @@ import net.tfminecraft.simplefactions.guild.Guild; import net.tfminecraft.simplefactions.guild.GuildModifierOverride; import net.tfminecraft.simplefactions.enums.GuildModifier; +import net.tfminecraft.simplefactions.enums.Rules; +import net.tfminecraft.simplefactions.guild.income.IncomePreviewContext; import net.tfminecraft.simplefactions.installation.Installation; import net.tfminecraft.simplefactions.installation.InstallationKind; import net.tfminecraft.simplefactions.managers.FactionManager; @@ -39,6 +41,7 @@ private SupplyHubService() { } public enum BuildFailure { + ECONOMY_DISALLOWS, KIND_DISALLOWS, ALREADY_HAS_HUB, HUB_LIMIT, @@ -48,6 +51,7 @@ public enum BuildFailure { } public enum DormantReason { + ECONOMY_DISALLOWS, INSTALLATION_GONE, NO_PERMIT, BEYOND_HUB_SLOTS, @@ -195,6 +199,8 @@ public static BuildFailure checkBuild( public static String buildFailureMessage(BuildFailure failure, String kindDisplayName, int limit) { String kind = kindDisplayName == null || kindDisplayName.isBlank() ? "installation" : kindDisplayName; switch (failure) { + case ECONOMY_DISALLOWS: + return "§cYour faction's economy does not allow supply hubs"; case KIND_DISALLOWS: return "§cA " + kind + " cannot host a supply hub"; case ALREADY_HAS_HUB: @@ -246,6 +252,32 @@ public static boolean togglePermit(List permits, String guildId) { return true; } + public static boolean allowsSupplyHubs(Guild guild) { + Faction faction = guild == null ? null : guild.getFaction(); + if (faction == null) { + return true; + } + IncomePreviewContext context = IncomePreviewContext.current(); + return context != null ? context.allowsHubRule(faction, Rules.SUPPLY_HUBS) + : faction.hasFactionRule(Rules.SUPPLY_HUBS); + } + + public static HubStanding standing( + Guild guild, + SupplyHub hub, + boolean installationExists, + boolean allowed, + int hubSlots, + List atInstallationOldestFirst) { + if (!allowsSupplyHubs(guild)) { + return new HubStanding(false, DormantReason.ECONOMY_DISALLOWS); + } + if (beyondGuildLimit(guild, hub)) { + return new HubStanding(false, DormantReason.BEYOND_GUILD_LIMIT); + } + return standing(hub, installationExists, allowed, hubSlots, atInstallationOldestFirst); + } + public static HubStanding standing( SupplyHub hub, boolean installationExists, @@ -278,6 +310,8 @@ public static String dormantReasonText(DormantReason reason) { return "dormant"; } switch (reason) { + case ECONOMY_DISALLOWS: + return "your faction's economy does not allow supply hubs"; case INSTALLATION_GONE: return "its installation no longer exists"; case NO_PERMIT: diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/income/IncomePreviewContext.java b/src/main/java/net/tfminecraft/simplefactions/guild/income/IncomePreviewContext.java index 9dcbac2e..f40cb21f 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/income/IncomePreviewContext.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/IncomePreviewContext.java @@ -249,7 +249,54 @@ private boolean replaces(TaxHandler handler, TaxTarget query, String id) { return true; } + public boolean allowsHubRule(Faction owner, Rules rule) { + if (!previewsLaw(owner)) { + return owner.hasFactionRule(rule); + } + LawEffect proposed = law.getScopedEffects().get(Scope.FACTION); + if (proposed != null && Boolean.FALSE.equals(proposed.getRules().get(rule))) { + return false; + } + boolean foundTrue = proposed != null && Boolean.TRUE.equals(proposed.getRules().get(rule)); + if (owner.getLawHandler() != null) { + for (Law current : owner.getLawHandler().getCurrentLaws()) { + if (current.getGroup().equals(law.getGroup())) continue; + LawEffect effect = current.getScopedEffects().get(Scope.FACTION); + if (effect != null && effect.getRules().containsKey(rule)) { + if (!effect.getRules().get(rule)) return false; + foundTrue = true; + } + } + } + if (foundTrue) return true; + Faction overlord = owner.getOverlord(); + return overlord == null || !Boolean.FALSE.equals(overlord.getExplicitRule(Scope.VASSALS, rule)); + } + + private Bracket hubTaxBracket() { + Bracket bracket = TaxHandler.DEFAULT_HUB_TAX_BRACKET; + boolean foundGroup = false; + if (faction.getLawHandler() != null) { + for (Law current : faction.getLawHandler().getCurrentLaws()) { + if (current.getGroup().equals(law.getGroup())) { + current = law; + foundGroup = true; + } + LawEffect effect = current.getScopedEffects().get(Scope.FACTION); + if (effect != null && effect.getBrackets().containsKey(Brackets.HUB_TAX)) { + bracket = effect.getBrackets().get(Brackets.HUB_TAX); + } + } + } + LawEffect proposed = law.getScopedEffects().get(Scope.FACTION); + return !foundGroup && proposed != null + ? proposed.getBrackets().getOrDefault(Brackets.HUB_TAX, bracket) : bracket; + } + private double clampForLaw(TaxTarget target, double rate) { + if (target == TaxTarget.HUB_TAX) { + return allowsHubRule(faction, Rules.HUB_TAX) ? clamp(rate, hubTaxBracket()) : 0; + } LawEffect effect = law.getScopedEffects().get(Scope.FACTION); if (effect == null) { return rate; @@ -261,8 +308,7 @@ private double clampForLaw(TaxTarget target, double rate) { } if (effect.hasBrackets()) { for (Map.Entry entry : effect.getBrackets().entrySet()) { - if ((BracketToTaxTarget.convert(entry.getKey()) == family - || (family == TaxTarget.HUB_TAX && entry.getKey() == Brackets.TARIFFS)) + if (BracketToTaxTarget.convert(entry.getKey()) == family && entry.getValue() != null) { rate = clamp(rate, entry.getValue()); } @@ -309,7 +355,8 @@ 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, HUB_TAX -> Rules.TARIFFS; + case TARIFFS, TARIFF_ID -> Rules.TARIFFS; + case HUB_TAX -> Rules.HUB_TAX; default -> null; }; } 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 fff714d3..12792495 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java @@ -33,6 +33,7 @@ import net.tfminecraft.simplefactions.utils.Formatter; import net.tfminecraft.simplefactions.utils.PostSettlementPayouts.PlayerUuidLookup; import net.tfminecraft.simplefactions.enums.FactionModifiers; +import net.tfminecraft.simplefactions.enums.Rules; import net.tfminecraft.simplefactions.government.proposal.TaxTarget; import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany; import net.tfminecraft.simplefactions.mercenary.contract.MercenaryEngagements; @@ -425,8 +426,17 @@ public Map getPayableHubTaxes() { if (hubTax == null || guild.getFaction() == null || skipsMoneyMovement()) { return Map.of(); } + IncomePreviewContext context = IncomePreviewContext.current(); + if (context != null && context.previewsLaw(guild.getFaction()) + && !context.allowsHubRule(guild.getFaction(), Rules.SUPPLY_HUBS)) { + return Map.of(); + } Map payable = new HashMap<>(); for (Map.Entry entry : hubTax.getTaxesByFaction().entrySet()) { + boolean allowed = context != null + ? context.allowsHubRule(entry.getKey(), Rules.HUB_TAX) + : entry.getKey().hasFactionRule(Rules.HUB_TAX); + if (!allowed) continue; Guild receiver = entry.getKey().getOrCreateMainGuild(); if (entry.getValue() > 0 && receiver != null && receiver.getLedger() != null && !receiver.getLedger().skipsMoneyMovement()) { diff --git a/src/main/java/net/tfminecraft/simplefactions/laws/LawEffect.java b/src/main/java/net/tfminecraft/simplefactions/laws/LawEffect.java index c2686455..ef5ff9c4 100644 --- a/src/main/java/net/tfminecraft/simplefactions/laws/LawEffect.java +++ b/src/main/java/net/tfminecraft/simplefactions/laws/LawEffect.java @@ -216,7 +216,9 @@ public boolean affectsEconomy() { for(Rules r : rules.keySet()) { if(r == Rules.VASSAL_TAX || r == Rules.GUILD_TAX - || r == Rules.TARIFFS) { + || r == Rules.TARIFFS + || r == Rules.HUB_TAX + || r == Rules.SUPPLY_HUBS) { return true; } } diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java index c979db37..42aa20fc 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/SupplyHubView.java @@ -268,9 +268,6 @@ private static boolean isLeader(Guild guild, Player player) { } private static HubStanding standing(Guild guild, SupplyHub hub) { - if (SupplyHubService.beyondGuildLimit(guild, hub)) { - return new HubStanding(false, SupplyHubService.DormantReason.BEYOND_GUILD_LIMIT); - } Installation installation = SupplyHubService.findInstallation( hub.ownerFactionId(), hub.installationId()); Faction owner = FactionManager.getByString(hub.ownerFactionId()); @@ -280,7 +277,7 @@ private static HubStanding standing(Guild guild, SupplyHub hub) { int slots = installation == null ? 0 : InstallationConfigLoader.getHubSlots(installation.getKind(), installation.getLevel()); - return SupplyHubService.standing(hub, installation != null, allowed, slots, + return SupplyHubService.standing(guild, hub, installation != null, allowed, slots, SupplyHubService.atInstallation( hub.ownerFactionId(), hub.installationId(), SupplyHubService.allGuilds())); } diff --git a/src/main/java/net/tfminecraft/simplefactions/objects/Faction.java b/src/main/java/net/tfminecraft/simplefactions/objects/Faction.java index ae619ab4..f69a542e 100644 --- a/src/main/java/net/tfminecraft/simplefactions/objects/Faction.java +++ b/src/main/java/net/tfminecraft/simplefactions/objects/Faction.java @@ -1042,6 +1042,7 @@ public void applyLaw(Law law, LawGroup group) { LawEffect effect = law.getScopedEffects().get(Scope.FACTION); if (effect == null) { + refreshHubTaxBracket(); cancelInvalidElections(); return; } @@ -1087,6 +1088,10 @@ public void applyLaw(Law law, LawGroup group) { if (!value) taxHandler.applyBracket(TaxTarget.TARIFFS, new Bracket(0, 0)); break; + case HUB_TAX: + if (!value) + taxHandler.applyBracket(TaxTarget.HUB_TAX, new Bracket(0, 0)); + break; case VEHICLE_TAX: case REGISTRATION_FEE: case TRANSFER_FEE: @@ -1099,6 +1104,8 @@ public void applyLaw(Law law, LawGroup group) { } } + refreshHubTaxBracket(); + // --- council structure --- if (effect.affectsCouncilSize() || effect.affectsCouncilType()) { government.getCouncil().reorganize(); @@ -1118,6 +1125,18 @@ public void applyLaw(Law law, LawGroup group) { } } + private void refreshHubTaxBracket() { + Bracket bracket = TaxHandler.DEFAULT_HUB_TAX_BRACKET; + for (Law current : lawHandler.getCurrentLaws()) { + LawEffect effect = current.getScopedEffects().get(Scope.FACTION); + if (effect != null && effect.getBrackets().containsKey(Brackets.HUB_TAX)) { + bracket = effect.getBrackets().get(Brackets.HUB_TAX); + } + } + taxHandler.applyBracket(TaxTarget.HUB_TAX, + hasFactionRule(Rules.HUB_TAX) ? bracket : new Bracket(0, 0)); + } + public void applyPoliticalAction(Cause cause, Proposal proposal) { if(!proposal.isPoliticalActionProposal()) return; PoliticalAction politicalAction = proposal.getPoliticalAction(); diff --git a/src/main/java/net/tfminecraft/simplefactions/objects/handler/TaxHandler.java b/src/main/java/net/tfminecraft/simplefactions/objects/handler/TaxHandler.java index 5f68aeec..5633d12d 100644 --- a/src/main/java/net/tfminecraft/simplefactions/objects/handler/TaxHandler.java +++ b/src/main/java/net/tfminecraft/simplefactions/objects/handler/TaxHandler.java @@ -15,6 +15,7 @@ import net.tfminecraft.simplefactions.enums.Rules; public class TaxHandler { + public static final Bracket DEFAULT_HUB_TAX_BRACKET = new Bracket(0, 10); private Faction f; private TaxSnapshot savedSnapshot; @@ -51,14 +52,15 @@ public double getHubTax() { } private double clampHubTax(double rate) { - return clampHubTax(rate, taxBrackets.get(TaxTarget.TARIFFS)); + return canCollectTax(TaxTarget.HUB_TAX) + ? clampHubTax(rate, getBracket(TaxTarget.HUB_TAX)) : 0; } private double clampHubTax(double rate, Bracket bracket) { if (!Double.isFinite(rate)) { return 0; } - double max = Math.min(Cache.supplyHubMaxTax, bracket == null ? 100.0 : bracket.getMax()); + double max = Math.min(Cache.supplyHubMaxTax, bracket == null ? DEFAULT_HUB_TAX_BRACKET.getMax() : bracket.getMax()); double min = Math.min(max, bracket == null ? 0.0 : bracket.getMin()); return Math.max(min, Math.min(max, rate)); } @@ -147,7 +149,7 @@ public double getTaxRate(TaxTarget target, String id, boolean effective) { double rate = switch (target) { case CITIZENS -> citizenTax; case DIVIDENDS -> dividendTax; - case HUB_TAX -> getHubTax(); + case HUB_TAX -> hubTax; case TARIFFS -> (id != null && hasSpecificTax(target, id)) ? getSpecificTax(target, id) : tariffs; @@ -173,10 +175,11 @@ public double getTaxRate(TaxTarget target, String id, boolean effective) { rate = context.adjustTax(f, this, target, id, rate); } if (target == TaxTarget.HUB_TAX) { - // adjustTax already applied the proposed law's bracket, as it does for tariffs. - Bracket bracket = context != null && context.previewsLaw(f) - ? null : taxBrackets.get(TaxTarget.TARIFFS); - rate = clampHubTax(rate, bracket); + if (context != null && context.previewsLaw(f)) { + rate = clampHubTax(rate, new Bracket(0, Cache.supplyHubMaxTax)); + } else { + rate = clampHubTax(rate); + } } return effective ? Formatter.formatDouble(rate * f.getGovernment().getTaxEfficiency()) : rate; } @@ -221,9 +224,12 @@ public void applyBracket(TaxTarget target, Bracket bracket) { dividendTax = applyBracket(dividendTax, bracket); break; + case HUB_TAX: + setHubTax(hubTax); + break; + case TARIFFS: tariffs = applyBracket(tariffs, bracket); - setHubTax(hubTax); applySpecificBracket(target, bracket); break; @@ -233,12 +239,13 @@ public void applyBracket(TaxTarget target, Bracket bracket) { } public Bracket getBracket(TaxTarget target) { - return taxBrackets.get(target); + return target == TaxTarget.HUB_TAX + ? taxBrackets.getOrDefault(target, DEFAULT_HUB_TAX_BRACKET) : taxBrackets.get(target); } public double getMin(TaxTarget target) { if(!canCollectTax(target)) return 0.0; - Bracket bracket = taxBrackets.get(target == TaxTarget.HUB_TAX ? TaxTarget.TARIFFS : target); + Bracket bracket = getBracket(target); if (bracket == null) return 0.0; if (target == TaxTarget.HUB_TAX) { return Math.min(bracket.getMin(), getMax(target)); @@ -248,9 +255,9 @@ public double getMin(TaxTarget target) { public double getMax(TaxTarget target) { if(!canCollectTax(target)) return 0.0; - Bracket bracket = taxBrackets.get(target == TaxTarget.HUB_TAX ? TaxTarget.TARIFFS : target); + Bracket bracket = getBracket(target); if (target == TaxTarget.HUB_TAX) { - return Math.min(Cache.supplyHubMaxTax, bracket == null ? 100.0 : bracket.getMax()); + return Math.min(Cache.supplyHubMaxTax, bracket == null ? DEFAULT_HUB_TAX_BRACKET.getMax() : bracket.getMax()); } if (bracket == null) return 100.0; return bracket.getMax(); @@ -270,8 +277,9 @@ public boolean canCollectTax(TaxTarget target) { return f.hasFactionRule(Rules.DIVIDEND_TAX); case TARIFFS: case TARIFF_ID: - case HUB_TAX: return f.hasFactionRule(Rules.TARIFFS); + case HUB_TAX: + return f.hasFactionRule(Rules.HUB_TAX); default: return false; } diff --git a/src/main/java/net/tfminecraft/simplefactions/utils/BracketToTaxTarget.java b/src/main/java/net/tfminecraft/simplefactions/utils/BracketToTaxTarget.java index e7aa77f8..14b611df 100644 --- a/src/main/java/net/tfminecraft/simplefactions/utils/BracketToTaxTarget.java +++ b/src/main/java/net/tfminecraft/simplefactions/utils/BracketToTaxTarget.java @@ -14,6 +14,8 @@ public static TaxTarget convert(Brackets bracket) { return TaxTarget.GUILDS; case TARIFFS: return TaxTarget.TARIFFS; + case HUB_TAX: + return TaxTarget.HUB_TAX; case VASSAL_TAX: return TaxTarget.VASSALS; default: diff --git a/src/main/resources/laws.yml b/src/main/resources/laws.yml index 5364d627..669cd868 100644 --- a/src/main/resources/laws.yml +++ b/src/main/resources/laws.yml @@ -664,6 +664,8 @@ economy: faction: rules: - tariffs false + - hub_tax false + - supply_hubs false foreign_guilds: our_territory: - trade_power(5.0) @@ -688,6 +690,7 @@ economy: faction: brackets: tariffs: "0-75" + hub_tax: "0-50" foreign_guilds: our_territory: - trade_power(-80.0) @@ -717,6 +720,7 @@ economy: faction: brackets: tariffs: "0-50" + hub_tax: "0-40" foreign_guilds: our_territory: - trade_power(-25.0) @@ -747,6 +751,7 @@ economy: faction: brackets: tariffs: "0-25" + hub_tax: "0-25" domestic_guilds: foreign_territory: - trade_power(10.0) @@ -766,6 +771,8 @@ economy: - decentralized 1 effects: faction: + brackets: + hub_tax: "0-10" rules: - tariffs false foreign_guilds: diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/hub/HubTaxServiceTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/hub/HubTaxServiceTest.java index cd88fbad..b63f049d 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/hub/HubTaxServiceTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/hub/HubTaxServiceTest.java @@ -18,6 +18,7 @@ import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.enums.GuildModifier; +import net.tfminecraft.simplefactions.enums.Rules; import net.tfminecraft.simplefactions.enums.Terrain; import net.tfminecraft.simplefactions.government.proposal.TaxTarget; import net.tfminecraft.simplefactions.guild.Guild; @@ -86,6 +87,7 @@ void snapshotsAssessForeignHubsWithoutChangingLiveDataOrTheNetwork() { Faction home = mock(Faction.class); Faction host = mock(Faction.class); when(home.getId()).thenReturn("home"); + when(home.hasFactionRule(Rules.SUPPLY_HUBS)).thenReturn(true); when(host.getId()).thenReturn("host"); when(host.hasHubPermit("guild")).thenReturn(true); when(host.getTaxRate(TaxTarget.HUB_TAX, null, true)).thenReturn(50.0); diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java index e731bd40..9bf216d0 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java @@ -5,6 +5,7 @@ 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.never; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -30,6 +31,7 @@ import net.tfminecraft.simplefactions.database.SupplyHubData; import net.tfminecraft.simplefactions.guild.Guild; import net.tfminecraft.simplefactions.enums.GuildModifier; +import net.tfminecraft.simplefactions.enums.Rules; import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.BuildFailure; import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.DormantReason; import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.HubCandidate; @@ -62,6 +64,46 @@ void restore() { Cache.supplyHubUpkeepGrowth = previousGrowth; } + @Test + void forbiddenEconomyRefusesBuildBeforeCheckingTheInstallation() { + boolean enabled = Cache.provincesEnabled; + Cache.provincesEnabled = true; + Guild guild = mock(Guild.class); + Faction faction = mock(Faction.class); + Player player = mock(Player.class); + when(player.getName()).thenReturn("leader"); + when(guild.getLeader()).thenReturn("leader"); + when(guild.getFaction()).thenReturn(faction); + when(faction.hasFactionRule(Rules.SUPPLY_HUBS)).thenReturn(false); + try (MockedStatic factions = mockStatic(FactionManager.class)) { + factions.when(() -> FactionManager.getGuildByMember("leader")).thenReturn(guild); + assertTrue(SupplyHubCommands.guild(player, new String[] {"hub", "build"})); + verify(player).sendMessage("§cYour faction's economy does not allow supply hubs"); + verify(player, never()).getLocation(); + } finally { + Cache.provincesEnabled = enabled; + } + } + + @Test + void forbiddenEconomyMakesExistingHubsDormantButKeepsUpkeepAndSlots() { + SupplyHub hub = hub("decentralized", "station", 1); + Guild guild = guildWith(hub); + Faction faction = mock(Faction.class); + when(guild.getFaction()).thenReturn(faction); + HubStanding standing = SupplyHubService.standing(guild, hub, true, true, 1, List.of(hub)); + assertFalse(standing.active()); + assertEquals(DormantReason.ECONOMY_DISALLOWS, standing.reason()); + assertEquals("§cDormant §7(your faction's economy does not allow supply hubs)", + SupplyHubService.statusText(standing)); + assertEquals(SupplyHubService.baseUpkeep(), SupplyHubService.dailyCost(guild)); + assertEquals(1, SupplyHubService.countAt("decentralized", "station", List.of(guild))); + + // The host's economy does not forbid a foreign guild that may build hubs. + when(faction.hasFactionRule(Rules.SUPPLY_HUBS)).thenReturn(true); + assertTrue(SupplyHubService.standing(guild, hub, true, true, 1, List.of(hub)).active()); + } + @Test void build_eachPreconditionFailsOnItsOwn() { assertEquals(BuildFailure.KIND_DISALLOWS, SupplyHubService.checkBuild(0, false, 0, 2, 0, 1, true)); diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java index 12c71f6c..f9194797 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java @@ -24,6 +24,7 @@ import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.enums.GuildModifier; +import net.tfminecraft.simplefactions.enums.Rules; import net.tfminecraft.simplefactions.enums.Terrain; import net.tfminecraft.simplefactions.government.proposal.TaxTarget; import net.tfminecraft.simplefactions.guild.Guild; @@ -67,6 +68,7 @@ void setUp() { live.start(Map.of(capital.getId(), capital, neighbour.getId(), neighbour)); faction = mock(Faction.class); + when(faction.hasFactionRule(Rules.SUPPLY_HUBS)).thenReturn(true); when(faction.getTaxRate(TaxTarget.GUILDS, "fields", true)).thenReturn(0.0); guild = mock(Guild.class); diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerHubTaxTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerHubTaxTest.java index 3d14a7a6..5182c1fc 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerHubTaxTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerHubTaxTest.java @@ -17,6 +17,11 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; +import net.tfminecraft.simplefactions.enums.Rules; +import net.tfminecraft.simplefactions.objects.Bracket; +import net.tfminecraft.simplefactions.laws.Law; +import net.tfminecraft.simplefactions.laws.LawGroup; +import org.bukkit.configuration.file.YamlConfiguration; import net.tfminecraft.simplefactions.database.GuildData; import net.tfminecraft.simplefactions.database.JsonUtil; import net.tfminecraft.simplefactions.government.Government; @@ -54,6 +59,8 @@ void setUp() { when(host.getGovernment()).thenReturn(government); when(government.getTaxEfficiency()).thenReturn(1.0); taxes = new TaxHandler(host, 0, 0, 0, 0, 0); + when(host.hasFactionRule(Rules.HUB_TAX)).thenReturn(true); + taxes.applyBracket(TaxTarget.HUB_TAX, new Bracket(0, 50)); taxes.setHubTax(20); when(host.getTaxHandler()).thenReturn(taxes); when(host.getTaxRate(any(TaxTarget.class), nullable(String.class), anyBoolean())) @@ -163,6 +170,52 @@ void ratePreviewUsesStoredBasesAndLeavesTheAssessmentAndRateAlone() { assertEquals(50, assessment.getTaxableIncome(host)); } + @Test + void forbiddenHostCollectsNothingEvenFromAnExistingAssessment() { + when(host.hasFactionRule(Rules.HUB_TAX)).thenReturn(false); + assertEquals(0, taxes.getHubTax()); + assertEquals(0, payer.getLedger().getIncome(Cashflow.HUB_TAX_PAYMENTS)); + assertEquals(0, receiver.getLedger().getIncome(Cashflow.HUB_TAX)); + assertEquals(200, payer.getLedger().getNetIncome()); + assertTrue(Ledger.collectHistoryDay(List.of(payer, receiver)).isEmpty()); + DailyGuildTransfers transfers = new DailyGuildTransfers(); + payer.getLedger().populateDailyTransfers(transfers); + assertTrue(transfers.getTransfers().isEmpty()); + } + + @Test + void lawPreviewUsesIndependentBracketAndRuleOnBothLedgerSides() { + YamlConfiguration config = new YamlConfiguration(); + config.set("effects.faction.rules", List.of("tariffs false")); + config.set("effects.faction.brackets.hub_tax", "0-10"); + LawGroup group = mock(LawGroup.class); + IncomePreviewContext.open(IncomePreviewContext.law(host, group, new Law("economy", "free_trade", config))); + assertEquals(-5, payer.getLedger().getIncome(Cashflow.HUB_TAX_PAYMENTS)); + assertEquals(5, receiver.getLedger().getIncome(Cashflow.HUB_TAX)); + IncomePreviewContext.clear(); + + config.set("effects.faction.rules", List.of("hub_tax false", "supply_hubs false")); + IncomePreviewContext.open(IncomePreviewContext.law(host, group, new Law("economy", "decentralized", config))); + assertEquals(0, payer.getLedger().getIncome(Cashflow.HUB_TAX_PAYMENTS)); + assertEquals(0, receiver.getLedger().getIncome(Cashflow.HUB_TAX)); + IncomePreviewContext.clear(); + assertEquals(20, taxes.getHubTax()); + assertEquals(10, assessment.getTotalTax()); + } + + @Test + void dormantHubLawPreviewRemovesPaymentsWithoutChangingTheStoredAssessment() { + YamlConfiguration config = new YamlConfiguration(); + config.set("effects.faction.rules", List.of("supply_hubs false")); + IncomePreviewContext.open(IncomePreviewContext.law(payer.getFaction(), mock(LawGroup.class), + new Law("economy", "decentralized", config))); + assertEquals(0, payer.getLedger().getIncome(Cashflow.HUB_TAX_PAYMENTS)); + assertEquals(0, receiver.getLedger().getIncome(Cashflow.HUB_TAX)); + IncomePreviewContext.clear(); + assertEquals(10, assessment.getTotalTax()); + assertEquals(-10, payer.getLedger().getIncome(Cashflow.HUB_TAX_PAYMENTS)); + } + private static Guild guild(Faction faction, boolean base) { Guild guild = mock(Guild.class); Bank bank = mock(Bank.class); diff --git a/src/test/java/net/tfminecraft/simplefactions/laws/HubLawTest.java b/src/test/java/net/tfminecraft/simplefactions/laws/HubLawTest.java new file mode 100644 index 00000000..d9249dd0 --- /dev/null +++ b/src/test/java/net/tfminecraft/simplefactions/laws/HubLawTest.java @@ -0,0 +1,172 @@ +package net.tfminecraft.simplefactions.laws; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.simplefactions.Cache; +import net.tfminecraft.simplefactions.enums.Brackets; +import net.tfminecraft.simplefactions.enums.Rules; +import net.tfminecraft.simplefactions.enums.Scope; +import net.tfminecraft.simplefactions.government.Government; +import net.tfminecraft.simplefactions.government.proposal.TaxTarget; +import net.tfminecraft.simplefactions.guild.income.IncomePreviewContext; +import net.tfminecraft.simplefactions.objects.Faction; +import net.tfminecraft.simplefactions.objects.handler.LawHandler; +import net.tfminecraft.simplefactions.objects.handler.TaxHandler; +import net.tfminecraft.simplefactions.utils.BracketToTaxTarget; +import net.tfminecraft.simplefactions.utils.LoreWriter; +import net.tfminecraft.tlibs.objects.api.subapi.StringFormatter; + +class HubLawTest { + private final double previousCap = Cache.supplyHubMaxTax; + private Faction faction; + private TaxHandler taxes; + private LawGroup group; + + @BeforeEach + void setUp() throws Exception { + Cache.supplyHubMaxTax = 50; + group = new LawGroup("economy", bundled().getConfigurationSection("economy")); + faction = mock(Faction.class); + LawHandler laws = mock(LawHandler.class); + when(faction.getLawHandler()).thenReturn(laws); + when(laws.getCurrentLaws()).thenAnswer(call -> List.of(group.getCurrent())); + when(faction.getGovernment()).thenReturn(mock(Government.class)); + doCallRealMethod().when(faction).hasFactionRule(any(Rules.class)); + doCallRealMethod().when(faction).getExplicitRule(any(Scope.class), any(Rules.class)); + doCallRealMethod().when(faction).applyLaw(any(Law.class), any(LawGroup.class)); + taxes = new TaxHandler(faction, 0, 0, 0, 0, 20); + setField("lawHandler", laws); + setField("taxHandler", taxes); + group.setCurrent(group.getLaw("isolationism")); + faction.applyLaw(group.getCurrent(), group); + } + + @AfterEach + void restore() { + IncomePreviewContext.clear(); + Cache.supplyHubMaxTax = previousCap; + } + + @Test + void bundledEconomyLawsHaveIndependentSettingsAndLore() { + assertEquals(TaxTarget.HUB_TAX, BracketToTaxTarget.convert(Brackets.HUB_TAX)); + for (Map.Entry setting : Map.of( + "free_trade", 10.0, "mercantilism", 25.0, + "protectionism", 40.0, "isolationism", 50.0).entrySet()) { + faction.applyLaw(group.getLaw(setting.getKey()), group); + LawEffect effect = group.getCurrent().getScopedEffects().get(Scope.FACTION); + assertEquals(0, effect.getBrackets().get(Brackets.HUB_TAX).getMin()); + assertEquals(setting.getValue(), taxes.getMax(TaxTarget.HUB_TAX)); + assertTrue(faction.hasFactionRule(Rules.HUB_TAX)); + assertTrue(faction.hasFactionRule(Rules.SUPPLY_HUBS)); + List lore = new ArrayList<>(); + LoreWriter.writeEffect(Scope.FACTION, effect, lore); + assertTrue(lore.stream().anyMatch(line -> line.contains("Hub Tax") && line.contains("Range:"))); + } + faction.applyLaw(group.getLaw("decentralized"), group); + LawEffect effect = group.getCurrent().getScopedEffects().get(Scope.FACTION); + assertFalse(effect.getRules().get(Rules.HUB_TAX)); + assertFalse(effect.getRules().get(Rules.SUPPLY_HUBS)); + assertTrue(effect.affectsEconomy()); + List lore = new ArrayList<>(); + LoreWriter.writeEffect(Scope.FACTION, effect, lore); + assertTrue(lore.contains(StringFormatter.formatHex(" #d65c5c✖ #d4c9aeCan Collect Hub Taxes"))); + assertTrue(lore.contains(StringFormatter.formatHex(" #d65c5c✖ #d4c9aeCan Build Supply Hubs"))); + } + + @Test + void freeTradeAllowsTenPercentHubTaxWhileTariffsStayOff() { + taxes.setHubTax(45); + taxes.setSpecificTax(TaxTarget.TARIFFS, "foreign", 35); + faction.applyLaw(group.getLaw("free_trade"), group); + assertEquals(10, taxes.getHubTax()); + assertTrue(taxes.canCollectTax(TaxTarget.HUB_TAX)); + assertEquals(0, taxes.getTariffs()); + assertEquals(0, taxes.getTaxRate(TaxTarget.TARIFFS, "foreign", false)); + assertFalse(taxes.canCollectTax(TaxTarget.TARIFFS)); + } + + @Test + void lawChangesPullDownTheSavedRateAndRestoreDefaultsAfterABan() { + taxes.setHubTax(50); + faction.applyLaw(group.getLaw("protectionism"), group); + assertEquals(40, taxes.getHubTax()); + faction.applyLaw(group.getLaw("mercantilism"), group); + assertEquals(25, taxes.getHubTax()); + faction.applyLaw(group.getLaw("decentralized"), group); + assertEquals(0, taxes.getHubTax()); + assertEquals(0, taxes.getMax(TaxTarget.HUB_TAX)); + assertFalse(faction.hasFactionRule(Rules.SUPPLY_HUBS)); + faction.applyLaw(group.getLaw("free_trade"), group); + assertEquals(10, taxes.getMax(TaxTarget.HUB_TAX)); + taxes.setHubTax(8); + assertEquals(8, taxes.getHubTax()); + assertTrue(faction.hasFactionRule(Rules.SUPPLY_HUBS)); + } + + @Test + void oldLawsFilesDefaultBothRulesToAllowedAndTheBracketToZeroToTen() { + YamlConfiguration config = bundled(); + for (String id : config.getConfigurationSection("economy.laws").getKeys(false)) { + String path = "economy.laws." + id + ".effects.faction"; + config.set(path + ".brackets.hub_tax", null); + List rules = new ArrayList<>(config.getStringList(path + ".rules")); + rules.removeIf(rule -> rule.startsWith("hub_tax ") || rule.startsWith("supply_hubs ")); + config.set(path + ".rules", rules); + } + group = new LawGroup("economy", config.getConfigurationSection("economy")); + for (Law law : group.getLaws().values()) { + faction.applyLaw(law, group); + assertTrue(faction.hasFactionRule(Rules.HUB_TAX), law.getId()); + assertTrue(faction.hasFactionRule(Rules.SUPPLY_HUBS), law.getId()); + assertEquals(0, taxes.getMin(TaxTarget.HUB_TAX)); + assertEquals(10, taxes.getMax(TaxTarget.HUB_TAX)); + taxes.setHubTax(50); + assertEquals(10, taxes.getHubTax()); + } + } + + @Test + void previewsUseProposedSettingsAndLeaveLiveLawsAndRateAlone() { + taxes.setHubTax(40); + IncomePreviewContext.open(IncomePreviewContext.law(faction, group, group.getLaw("free_trade"))); + assertEquals(10, taxes.getTaxRate(TaxTarget.HUB_TAX, null, false)); + IncomePreviewContext.clear(); + assertEquals(40, taxes.getHubTax()); + IncomePreviewContext.open(IncomePreviewContext.law(faction, group, group.getLaw("decentralized"))); + assertEquals(0, taxes.getTaxRate(TaxTarget.HUB_TAX, null, false)); + assertFalse(faction.hasFactionRule(Rules.SUPPLY_HUBS)); + IncomePreviewContext.clear(); + assertEquals("isolationism", group.getCurrent().getId()); + assertEquals(40, taxes.getHubTax()); + } + + private void setField(String name, Object value) throws Exception { + Field field = Faction.class.getDeclaredField(name); + field.setAccessible(true); + field.set(faction, value); + } + + private static YamlConfiguration bundled() { + return YamlConfiguration.loadConfiguration(new InputStreamReader( + HubLawTest.class.getResourceAsStream("/laws.yml"), StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/ProvinceManagerHubTransportTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/ProvinceManagerHubTransportTest.java index 7465b007..600b2f0f 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/ProvinceManagerHubTransportTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/ProvinceManagerHubTransportTest.java @@ -18,10 +18,15 @@ import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.enums.GuildModifier; +import net.tfminecraft.simplefactions.enums.Rules; import net.tfminecraft.simplefactions.diplomacy.DiplomacyHandler; import net.tfminecraft.simplefactions.enums.Terrain; import net.tfminecraft.simplefactions.government.Government; import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.income.IncomePreviewContext; +import net.tfminecraft.simplefactions.laws.Law; +import net.tfminecraft.simplefactions.laws.LawGroup; +import org.bukkit.configuration.file.YamlConfiguration; import net.tfminecraft.simplefactions.guild.hub.HubNetwork; import net.tfminecraft.simplefactions.guild.hub.HubTransport; import net.tfminecraft.simplefactions.guild.hub.HubTransport.Link; @@ -59,6 +64,7 @@ void setUp() { Faction host = mock(Faction.class); when(host.getId()).thenReturn("host"); + when(host.hasFactionRule(Rules.SUPPLY_HUBS)).thenReturn(true); guild = mock(Guild.class); when(guild.getId()).thenReturn("guild"); when(guild.getFaction()).thenReturn(host); @@ -80,6 +86,34 @@ void tearDown() { Cache.provincesEnabled = provincesWereEnabled; } + @Test + void forbiddenSupplyHubsCarryNothingAndLawPreviewSuppressesCachedLinks() { + link(new Link(1, 22, Mode.RAIL, 0, 0.7, 0.25)); + List cached = HubNetwork.linksFor(guild); + recalculate(); + assertEquals(14, trade(22), 1e-9); + YamlConfiguration config = new YamlConfiguration(); + config.set("effects.faction.rules", List.of("supply_hubs false")); + Law proposed = new Law("economy", "decentralized", config); + IncomePreviewContext.open(IncomePreviewContext.law(guild.getFaction(), mock(LawGroup.class), proposed)); + try { + ProvinceManager snapshot = provinces.createSnapshotShell(); + snapshot.copyAllDataFrom(provinces); + snapshot.recalculateForSingleGuild(guild, false); + assertTrue(HubNetwork.linksFor(guild).isEmpty()); + assertEquals(0, snapshot.get(22).getStoredGuildTrade(guild)); + assertEquals(0, snapshot.get(22).getGuildProduction(guild)); + assertEquals(14, trade(22), 1e-9); + } finally { + IncomePreviewContext.clear(); + } + assertEquals(cached, HubNetwork.linksFor(guild)); + when(guild.getFaction().hasFactionRule(Rules.SUPPLY_HUBS)).thenReturn(false); + recalculate(); + assertEquals(0, trade(22)); + assertEquals(0, production(22)); + } + @Test void withoutHubs_powerFadesAlongTheRoad() { recalculate(); diff --git a/src/test/java/net/tfminecraft/simplefactions/objects/handler/TaxHandlerHubTaxTest.java b/src/test/java/net/tfminecraft/simplefactions/objects/handler/TaxHandlerHubTaxTest.java index a022a7be..253fb09a 100644 --- a/src/test/java/net/tfminecraft/simplefactions/objects/handler/TaxHandlerHubTaxTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/objects/handler/TaxHandlerHubTaxTest.java @@ -41,6 +41,7 @@ void setUp() { faction = mock(Faction.class); Government government = mock(Government.class); when(faction.hasFactionRule(Rules.TARIFFS)).thenReturn(true); + when(faction.hasFactionRule(Rules.HUB_TAX)).thenReturn(true); when(faction.getGovernment()).thenReturn(government); when(government.getTaxEfficiency()).thenReturn(0.8); handler = new TaxHandler(faction, 0, 0, 0, 0, 15); @@ -56,38 +57,40 @@ void tearDown() { } @Test - void defaultsToZeroAndCapsAtFiftyWithTariffEfficiency() { + void defaultsToZeroAndCapsAtTenWithTaxEfficiency() { assertEquals(0, handler.getHubTax()); - assertEquals(50, handler.getMax(TaxTarget.HUB_TAX)); + assertEquals(10, handler.getMax(TaxTarget.HUB_TAX)); handler.setTaxRate(TaxTarget.HUB_TAX, null, 90); - assertEquals(50, handler.getTaxRate(TaxTarget.HUB_TAX, "any", false)); - assertEquals(40, handler.getTaxRate(TaxTarget.HUB_TAX, null, true)); + assertEquals(10, handler.getTaxRate(TaxTarget.HUB_TAX, "any", false)); + assertEquals(8, handler.getTaxRate(TaxTarget.HUB_TAX, null, true)); handler.setHubTax(-1); assertEquals(0, handler.getHubTax()); } @Test - void configuredCapAndTariffBracketsBothApply() { + void configuredCapAndHubTaxBracketsBothApply() { YamlConfiguration config = new YamlConfiguration(); config.set("supply-hubs.max-tax", 25); SupplyHubService.loadConfig(config); + handler.applyBracket(TaxTarget.HUB_TAX, new Bracket(0, 50)); handler.setHubTax(80); assertEquals(25, handler.getHubTax()); assertEquals(25, handler.getMax(TaxTarget.HUB_TAX)); - handler.applyBracket(TaxTarget.TARIFFS, new Bracket(5, 20)); + handler.applyBracket(TaxTarget.HUB_TAX, new Bracket(5, 20)); assertEquals(20, handler.getHubTax()); assertEquals(5, handler.getMin(TaxTarget.HUB_TAX)); assertEquals(20, handler.getMax(TaxTarget.HUB_TAX)); - handler.applyBracket(TaxTarget.TARIFFS, new Bracket(0, 0)); + handler.applyBracket(TaxTarget.HUB_TAX, new Bracket(0, 0)); assertEquals(0, handler.getHubTax()); } @Test - void lawPreviewUsesTheProposedTariffBracketAndConfiguredCap() { + void lawPreviewUsesTheProposedHubTaxBracketAndConfiguredCap() { handler.applyBracket(TaxTarget.TARIFFS, new Bracket(0, 10)); handler.setHubTax(5); YamlConfiguration config = new YamlConfiguration(); config.set("effects.FACTION.brackets.TARIFFS", "20-80"); + config.set("effects.FACTION.brackets.HUB_TAX", "20-40"); Law proposed = new Law("taxes", "proposed", config); LawGroup group = mock(LawGroup.class); @@ -105,8 +108,46 @@ void lawPreviewUsesTheProposedTariffBracketAndConfiguredCap() { assertEquals(10, handler.getTaxRate(TaxTarget.HUB_TAX, null, false)); } + @Test + void tariffsHaveNoEffectOnHubTaxAndForbiddenHubTaxCannotBeSet() { + handler.setHubTax(9); + handler.applyBracket(TaxTarget.TARIFFS, new Bracket(0, 0)); + when(faction.hasFactionRule(Rules.TARIFFS)).thenReturn(false); + assertEquals(9, handler.getHubTax()); + assertEquals(10, handler.getMax(TaxTarget.HUB_TAX)); + assertFalse(handler.canCollectTax(TaxTarget.TARIFFS)); + + when(faction.hasFactionRule(Rules.HUB_TAX)).thenReturn(false); + handler.setTaxRate(TaxTarget.HUB_TAX, null, 10); + assertEquals(0, handler.getHubTax()); + assertEquals(0, handler.getTaxRate(TaxTarget.HUB_TAX, null, true)); + assertEquals(0, handler.getMax(TaxTarget.HUB_TAX)); + assertEquals(0, handler.getMin(TaxTarget.HUB_TAX)); + assertFalse(handler.canCollectTax(TaxTarget.HUB_TAX)); + } + + @Test + void lawPreviewUsesHubTaxRuleAndDefaultsWithoutChangingLiveRate() { + handler.applyBracket(TaxTarget.HUB_TAX, new Bracket(0, 50)); + handler.setHubTax(40); + YamlConfiguration config = new YamlConfiguration(); + config.set("effects.faction.rules", java.util.List.of("hub_tax false")); + LawGroup group = mock(LawGroup.class); + IncomePreviewContext.open(IncomePreviewContext.law(faction, group, new Law("economy", "banned", config))); + assertEquals(0, handler.getTaxRate(TaxTarget.HUB_TAX, null, false)); + IncomePreviewContext.clear(); + assertEquals(40, handler.getHubTax()); + + config.set("effects.faction.rules", java.util.List.of("tariffs false")); + IncomePreviewContext.open(IncomePreviewContext.law(faction, group, new Law("economy", "legacy", config))); + assertEquals(10, handler.getTaxRate(TaxTarget.HUB_TAX, null, false)); + IncomePreviewContext.clear(); + assertEquals(40, handler.getHubTax()); + } + @Test void hubTaxCannotAcquireSpecificRatesAndRestoresItsSavedState() { + handler.applyBracket(TaxTarget.HUB_TAX, new Bracket(0, 50)); handler.setHubTax(12); handler.setSpecificTax(TaxTarget.HUB_TAX, "foreign", 30); assertFalse(handler.hasSpecificTax(TaxTarget.HUB_TAX, "foreign")); @@ -119,6 +160,7 @@ void hubTaxCannotAcquireSpecificRatesAndRestoresItsSavedState() { @Test void factionRateRoundTripsAndOldSavesDefaultToZero() { + handler.applyBracket(TaxTarget.HUB_TAX, new Bracket(0, 50)); handler.setHubTax(23.5); FactionData data = new FactionData(); data.hubTax = handler.getHubTax();