From 7918b2b7d031c7c34af3f1969e4c9c8a5f53208a Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:32:49 +0000 Subject: [PATCH 1/2] Charge node upkeep daily and log manual node toggles Activation used to withdraw upkeep once and deactivation refunded it once per input taken, so a long cycle could be switched off for more than was paid. Upkeep is now charged by the SimpleFactions daily settlement for each active node, including the over-capacity multiplier the GUI already showed, and deactivation refunds only the inputs. Manual activations and deactivations are logged to the console and to node-toggles.log with the time left until the new day, as a warning within an hour of it, so switching nodes off over the new day can be spotted. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../dowsing/managers/InventoryManager.java | 3 +- .../dowsing/managers/NodeManager.java | 9 +- .../net/tfminecraft/dowsing/objects/Node.java | 9 +- .../dowsing/utils/ItemCreator.java | 2 +- .../tfminecraft/dowsing/utils/NodeEngine.java | 3 - .../dowsing/utils/NodeToggleLog.java | 82 +++++++++++++++++++ .../dowsing/managers/NodeUpkeepTest.java | 74 +++++++++++++++++ .../dowsing/utils/NodeToggleLogTest.java | 16 ++++ 8 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 src/main/java/net/tfminecraft/dowsing/utils/NodeToggleLog.java create mode 100644 src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java create mode 100644 src/test/java/net/tfminecraft/dowsing/utils/NodeToggleLogTest.java diff --git a/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java b/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java index 4b45d2b..2d99a3a 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java @@ -357,7 +357,8 @@ ItemStack createStatus(Node n) { lore.add(" "); lore.add("§4WARNING!"); lore.add("§cDeactivating resets the current cycle!"); - lore.add("§cThe cost of the current cycle will be refunded."); + lore.add("§cThe inputs of the current cycle will be refunded."); + lore.add("§7Upkeep is charged at each new day while active."); m.setLore(lore); i.setItemMeta(m); } diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index 0b9e595..7fae36c 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -43,6 +43,7 @@ import net.tfminecraft.dowsing.utils.ItemDropper; import net.tfminecraft.dowsing.utils.NodeEngine; import net.tfminecraft.dowsing.utils.NodeReloader; +import net.tfminecraft.dowsing.utils.NodeToggleLog; import net.tfminecraft.dowsing.enums.ConfirmType; import net.tfminecraft.simplefactions.guild.Guild; import net.tfminecraft.simplefactions.managers.FactionManager; @@ -101,10 +102,9 @@ public static double getTotalUpkeep(Guild g) { if (g == null) return 0; double total = 0; for (Node n : nodes) { - if (!n.hasGuild()) continue; + if (!n.getIsActive() || !n.hasGuild()) continue; if (!n.getGuild().getId().equalsIgnoreCase(g.getId())) continue; - Double upkeep = n.getUpkeep(); - if (upkeep != null) total += upkeep; + total += n.getDailyUpkeep(); } return total; } @@ -233,7 +233,9 @@ public void validate() { public void confirmClick(Player p, Node n, ConfirmType t) { if(t != ConfirmType.DEACTIVATE && blockPendingRefund(p, n)) return; if(t.equals(ConfirmType.DEACTIVATE)) { + boolean wasActive = n.getIsActive(); n.deActivate(); + if(wasActive) NodeToggleLog.deactivated(p, n); InventoryManager inv = new InventoryManager(); inv.nodeView(p, n); currentNode.put(p, n); @@ -452,6 +454,7 @@ public void invenClick(InventoryClickEvent e) { } else { n.activate(); if(n.getIsActive()) { + NodeToggleLog.activated(p, n); p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f); } else { p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f); diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index d1a86ee..9524d80 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -67,6 +67,12 @@ public Double getUpkeep() { public void setUpkeep(Double upkeep) { this.upkeep = upkeep; } + /** Charged by the SimpleFactions daily settlement for each day the node is active. */ + public double getDailyUpkeep() { + double base = upkeep == null ? 0.0 : upkeep; + double increase = costIncrease == null ? 1.0 : costIncrease; + return Math.round(base * increase * 100.0) / 100.0; + } public UUID getId() { return id; } @@ -432,7 +438,7 @@ public void activate() { this.errors.add("§7No bank"); } if(this.guild != null && this.guild.getBank() != null) { - if(this.guild.isBankrupt() || this.guild.getBank().getWealth() < this.upkeep) { + if(this.guild.isBankrupt() || this.guild.getBank().getWealth() < getDailyUpkeep()) { failed = true; this.errors.add("§7Lacking upkeep"); } @@ -453,7 +459,6 @@ public void activate() { if(failed) { return; } - this.guild.getBank().withdraw(this.upkeep); this.isActive = true; this.timeLeft = this.modifiedTime; this.cycleTime = 0; diff --git a/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java b/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java index bd02700..030d949 100644 --- a/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java +++ b/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java @@ -137,7 +137,7 @@ public ItemStack createTypeItemNode(Node n, NodeType t, boolean gui) { } lore.add("§eYield: §a"+n.getYield()); Formatter format = new Formatter(); - lore.add("§eTotal Upkeep: §f"+format.formatDouble(n.getUpkeep()*n.getCostIncrease())+"d"); + lore.add("§eTotal Upkeep: §f"+format.formatDouble(n.getDailyUpkeep())+"d/day"); lore.add("§7Possible Drops:"); Double maxWeight = 0.0; for(String s : n.getCompleteDrop().keySet()) { diff --git a/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java b/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java index 9aa3697..5c30ff8 100644 --- a/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java +++ b/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java @@ -92,9 +92,6 @@ public void refund(Node n) { inputs.put(key, amount); } } - if(n.getGuild() != null && n.getGuild().getBank() != null) { - n.getGuild().getBank().deposit(n.getUpkeep()); - } n.setInputCounter(n.getInputCounter()-1); for(String key : inputs.keySet()) { addItem(key, inputs.get(key), i); diff --git a/src/main/java/net/tfminecraft/dowsing/utils/NodeToggleLog.java b/src/main/java/net/tfminecraft/dowsing/utils/NodeToggleLog.java new file mode 100644 index 0000000..30fde03 --- /dev/null +++ b/src/main/java/net/tfminecraft/dowsing/utils/NodeToggleLog.java @@ -0,0 +1,82 @@ +package net.tfminecraft.dowsing.utils; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import net.tfminecraft.dowsing.DowsingMain; +import net.tfminecraft.dowsing.objects.Node; +import net.tfminecraft.simplefactions.managers.FactionManager; + +/** + * Upkeep is charged for the nodes active at the new day, so switching a node off just + * before it and on again after skips a day's upkeep. Every manual toggle is recorded with + * the time left until the new day, and one close to it is logged as a warning. + */ +public final class NodeToggleLog { + public static final int WARN_SECONDS_BEFORE_NEW_DAY = 60 * 60; + private static final String FILE_NAME = "node-toggles.log"; + private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private NodeToggleLog() {} + + public static void activated(Player player, Node node) { + record(player, node, "activated"); + } + + public static void deactivated(Player player, Node node) { + record(player, node, "deactivated"); + } + + private static void record(Player player, Node node, String action) { + if (player == null || node == null) return; + int secondsUntilNewDay = FactionManager.getSecondsUntilNewDay(); + String line = format(player.getName(), action, node, secondsUntilNewDay); + Logger logger = DowsingMain.plugin.getLogger(); + if (secondsUntilNewDay <= WARN_SECONDS_BEFORE_NEW_DAY) { + logger.warning(line); + } else { + logger.info(line); + } + append(LocalDateTime.now().format(TIMESTAMP) + " " + line); + } + + static String format(String player, String action, Node node, int secondsUntilNewDay) { + String guild = node.getGuild() == null ? "none" : node.getGuild().getId(); + String resource = node.getBlock() == null ? "unknown" : node.getBlock().getResource(); + return String.format(Locale.ROOT, "[NodeToggle] %s %s %s node %s of guild %s at %s, upkeep %.2fd/day, %s until new day", + player, action, resource, node.getId(), guild, location(node.getLoc()), + node.getDailyUpkeep(), duration(secondsUntilNewDay)); + } + + static String duration(int seconds) { + int clamped = Math.max(0, seconds); + return String.format(Locale.ROOT, "%dh %02dm", clamped / 3600, (clamped % 3600) / 60); + } + + private static String location(Location loc) { + if (loc == null) return "unknown"; + String world = loc.getWorld() == null ? "?" : loc.getWorld().getName(); + return world + " " + loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ(); + } + + private static void append(String line) { + File file = new File(DowsingMain.plugin.getDataFolder(), FILE_NAME); + try { + Files.writeString(file.toPath(), line + System.lineSeparator(), StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } catch (IOException e) { + DowsingMain.plugin.getLogger().log(Level.WARNING, "Could not write " + FILE_NAME, e); + } + } +} diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java new file mode 100644 index 0000000..bfcf783 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java @@ -0,0 +1,74 @@ +package net.tfminecraft.dowsing.managers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.dowsing.objects.Node; +import net.tfminecraft.simplefactions.guild.Guild; + +class NodeUpkeepTest { + private List previousNodes; + private Guild guild; + + @BeforeEach + void setUp() { + previousNodes = NodeManager.nodes; + NodeManager.nodes = new ArrayList<>(); + guild = mock(Guild.class); + when(guild.getId()).thenReturn("The_Chisels"); + } + + @AfterEach + void tearDown() { + NodeManager.nodes = previousNodes; + } + + @Test + void onlyActiveNodesOweUpkeep() { + NodeManager.nodes.add(node(guild, true, 40.0, 1.0)); + NodeManager.nodes.add(node(guild, false, 25.0, 1.0)); + + assertEquals(40.0, NodeManager.getTotalUpkeep(guild)); + } + + @Test + void overCapacityMultiplierIsCharged() { + NodeManager.nodes.add(node(guild, true, 40.0, 1.5)); + + assertEquals(60.0, NodeManager.getTotalUpkeep(guild)); + } + + @Test + void otherGuildsNodesAreIgnored() { + Guild other = mock(Guild.class); + when(other.getId()).thenReturn("Other"); + NodeManager.nodes.add(node(other, true, 40.0, 1.0)); + + assertEquals(0.0, NodeManager.getTotalUpkeep(guild)); + } + + private static Node node(Guild owner, boolean active, double upkeep, double costIncrease) { + Node node = new Node(java.util.UUID.randomUUID(), null, null, null, active, 1, 0, null, 10, 0, 50) { + @Override + public Boolean hasGuild() { + return true; + } + + @Override + public Guild getGuild() { + return owner; + } + }; + node.setUpkeep(upkeep); + node.setCostIncrease(costIncrease); + return node; + } +} diff --git a/src/test/java/net/tfminecraft/dowsing/utils/NodeToggleLogTest.java b/src/test/java/net/tfminecraft/dowsing/utils/NodeToggleLogTest.java new file mode 100644 index 0000000..06368e8 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/utils/NodeToggleLogTest.java @@ -0,0 +1,16 @@ +package net.tfminecraft.dowsing.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class NodeToggleLogTest { + + @Test + void durationShowsHoursAndMinutes() { + assertEquals("0h 00m", NodeToggleLog.duration(0)); + assertEquals("0h 59m", NodeToggleLog.duration(3599)); + assertEquals("23h 30m", NodeToggleLog.duration(23 * 3600 + 30 * 60)); + assertEquals("0h 00m", NodeToggleLog.duration(-5)); + } +} From 50d2fd98f9420baf71ad7b1c8858718f0ff6a1a5 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:49:29 +0000 Subject: [PATCH 2/2] Work out the node upkeep multiplier when it is charged Capacity changes when members join or capacity is bought, which does not refresh every node, so the daily upkeep now uses the guild's current node count and capacity. Round it to the cent with BigDecimal half-up. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../net/tfminecraft/dowsing/objects/Node.java | 32 +++++++++++++------ .../dowsing/managers/NodeUpkeepTest.java | 7 ++++ .../dowsing/objects/NodeCostIncreaseTest.java | 25 +++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 src/test/java/net/tfminecraft/dowsing/objects/NodeCostIncreaseTest.java diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index 9524d80..dd05a41 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -1,6 +1,8 @@ package net.tfminecraft.dowsing.objects; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -67,11 +69,25 @@ public Double getUpkeep() { public void setUpkeep(Double upkeep) { this.upkeep = upkeep; } - /** Charged by the SimpleFactions daily settlement for each day the node is active. */ + /** + * Charged by the SimpleFactions daily settlement for each day the node is active. The + * over-capacity multiplier is worked out from the guild's current nodes and capacity, + * since members joining or capacity being bought does not refresh every node. + */ public double getDailyUpkeep() { double base = upkeep == null ? 0.0 : upkeep; - double increase = costIncrease == null ? 1.0 : costIncrease; - return Math.round(base * increase * 100.0) / 100.0; + return BigDecimal.valueOf(base) + .multiply(BigDecimal.valueOf(currentCostIncrease())) + .setScale(2, RoundingMode.HALF_UP) + .doubleValue(); + } + double currentCostIncrease() { + if(resolveGuild() == null) return costIncrease == null ? 1.0 : costIncrease; + return costIncreaseFor(NodeManager.getNodeAmount(this.guild), getCapacity()); + } + static double costIncreaseFor(int nodeAmount, int capacity) { + int multiplier = Math.max(1, 1+nodeAmount-capacity); + return 1.0+((multiplier-1.0)*0.5); } public UUID getId() { return id; @@ -570,13 +586,11 @@ public void update() { updateEfficiencyTime(); if(timeLeft > modifiedTime) timeLeft = modifiedTime; setCompleteDrops(); - int newMultipler = 1+NodeManager.getNodeAmount(this.guild)-getCapacity(); - if(newMultipler < 1) { - newMultipler = 1; - } - this.multiplier = newMultipler; + int nodeAmount = NodeManager.getNodeAmount(this.guild); + int capacity = getCapacity(); + this.multiplier = Math.max(1, 1+nodeAmount-capacity); Formatter format = new Formatter(); - this.costIncrease = format.formatDouble(1.0+((multiplier-1.0)*0.5)); + this.costIncrease = format.formatDouble(costIncreaseFor(nodeAmount, capacity)); this.upkeep = format.formatDouble(this.upkeep); } Double getAddedPrestige(String e) { diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java index bfcf783..c9861dd 100644 --- a/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java @@ -46,6 +46,13 @@ void overCapacityMultiplierIsCharged() { assertEquals(60.0, NodeManager.getTotalUpkeep(guild)); } + @Test + void upkeepRoundsHalfUpToTheCent() { + NodeManager.nodes.add(node(guild, true, 0.29, 1.5)); + + assertEquals(0.44, NodeManager.getTotalUpkeep(guild)); + } + @Test void otherGuildsNodesAreIgnored() { Guild other = mock(Guild.class); diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeCostIncreaseTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeCostIncreaseTest.java new file mode 100644 index 0000000..1c945f7 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeCostIncreaseTest.java @@ -0,0 +1,25 @@ +package net.tfminecraft.dowsing.objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class NodeCostIncreaseTest { + + @Test + void withinCapacityCostsTheBaseAmount() { + assertEquals(1.0, Node.costIncreaseFor(1, 1)); + assertEquals(1.0, Node.costIncreaseFor(0, 3)); + } + + @Test + void eachNodeOverCapacityAddsHalf() { + assertEquals(1.5, Node.costIncreaseFor(2, 1)); + assertEquals(2.0, Node.costIncreaseFor(3, 1)); + } + + @Test + void buyingCapacityLowersTheMultiplier() { + assertEquals(1.5, Node.costIncreaseFor(3, 2)); + } +}