diff --git a/pom.xml b/pom.xml index cfab5a0..74e0166 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,12 @@ + + org.junit.jupiter + junit-jupiter + 5.14.0 + test + @@ -93,6 +99,11 @@ maven-compiler-plugin 3.14.1 + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + diff --git a/src/main/java/net/tfminecraft/marketblock/Cache.java b/src/main/java/net/tfminecraft/marketblock/Cache.java index 076c752..8b93dbf 100644 --- a/src/main/java/net/tfminecraft/marketblock/Cache.java +++ b/src/main/java/net/tfminecraft/marketblock/Cache.java @@ -7,11 +7,13 @@ import org.bukkit.block.Block; +import net.tfminecraft.marketblock.util.DemandSchedule; import net.tfminecraft.tlibs.TLibs; public class Cache { public static List slots = new ArrayList<>(); public static String marketBlock; + public static double demandRecoveryHours = DemandSchedule.DEFAULT_HOURS; public static final Map freshnessPrice = new LinkedHashMap<>(); public static boolean blockIsMarketBlock(Block b) { diff --git a/src/main/java/net/tfminecraft/marketblock/MarketBlock.java b/src/main/java/net/tfminecraft/marketblock/MarketBlock.java index bc2cbb5..4da38fb 100644 --- a/src/main/java/net/tfminecraft/marketblock/MarketBlock.java +++ b/src/main/java/net/tfminecraft/marketblock/MarketBlock.java @@ -86,5 +86,6 @@ public void reload(Player p) { public void reload() { loadConfigs(); + tradeManager.rescheduleDemandCycle(); } } diff --git a/src/main/java/net/tfminecraft/marketblock/loader/ConfigLoader.java b/src/main/java/net/tfminecraft/marketblock/loader/ConfigLoader.java index f849f01..04b41a7 100644 --- a/src/main/java/net/tfminecraft/marketblock/loader/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/marketblock/loader/ConfigLoader.java @@ -7,8 +7,9 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import net.tfminecraft.tlibs.interfaces.LoaderInterface; import net.tfminecraft.marketblock.Cache; +import net.tfminecraft.marketblock.util.DemandSchedule; +import net.tfminecraft.tlibs.interfaces.LoaderInterface; public class ConfigLoader implements LoaderInterface{ @@ -22,6 +23,8 @@ public void load(File configFile) { } Cache.marketBlock = config.getString("market-block"); + Cache.demandRecoveryHours = DemandSchedule.normalizeHours( + config.getDouble("demand-recovery-hours", DemandSchedule.DEFAULT_HOURS)); Cache.slots = config.getIntegerList("slots"); diff --git a/src/main/java/net/tfminecraft/marketblock/manager/TradeManager.java b/src/main/java/net/tfminecraft/marketblock/manager/TradeManager.java index 63ddf3b..d9e90c9 100644 --- a/src/main/java/net/tfminecraft/marketblock/manager/TradeManager.java +++ b/src/main/java/net/tfminecraft/marketblock/manager/TradeManager.java @@ -14,6 +14,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; import net.tfminecraft.marketblock.Cache; import net.tfminecraft.marketblock.MarketBlock; @@ -25,12 +26,14 @@ import net.tfminecraft.marketblock.loader.TradeLoader; import net.tfminecraft.marketblock.trade.Category; import net.tfminecraft.marketblock.trade.Trade; +import net.tfminecraft.marketblock.util.DemandSchedule; import net.tfminecraft.marketblock.util.InventoryUtils; import net.tfminecraft.marketblock.util.PriceCalculator; import net.tfminecraft.marketblock.util.SaleTake; public class TradeManager implements Listener { InventoryManager inv = new InventoryManager(); + private BukkitTask demandTask; public void update() { for(Player p : Bukkit.getOnlinePlayers()) { @@ -61,19 +64,32 @@ private String getItemId(MBGUI type, ItemStack i) { } public void start() { - demandCycle(); + scheduleDemand(0L); } - public void demandCycle() { - new BukkitRunnable() { + public void rescheduleDemandCycle() { + scheduleDemand(DemandSchedule.periodTicks(Cache.demandRecoveryHours)); + } + + private void scheduleDemand(long delayTicks) { + if (demandTask != null) { + demandTask.cancel(); + demandTask = null; + } + long period = DemandSchedule.periodTicks(Cache.demandRecoveryHours); + demandTask = new BukkitRunnable() { @Override public void run() { - for(Trade t : TradeLoader.getTrades().values()) { - t.demand(); - } - update(); + recoverDemand(); } - }.runTaskTimer(MarketBlock.plugin, 0, 60*60*20L); + }.runTaskTimer(MarketBlock.plugin, delayTicks, period); + } + + public void recoverDemand() { + for (Trade t : TradeLoader.getTrades().values()) { + t.demand(); + } + update(); } @EventHandler diff --git a/src/main/java/net/tfminecraft/marketblock/util/DemandSchedule.java b/src/main/java/net/tfminecraft/marketblock/util/DemandSchedule.java new file mode 100644 index 0000000..bdbe06e --- /dev/null +++ b/src/main/java/net/tfminecraft/marketblock/util/DemandSchedule.java @@ -0,0 +1,24 @@ +package net.tfminecraft.marketblock.util; + +public final class DemandSchedule { + public static final double DEFAULT_HOURS = 4.0; + public static final long TICKS_PER_HOUR = 60L * 60L * 20L; + + private DemandSchedule() { + } + + public static double normalizeHours(double configuredHours) { + if (!Double.isFinite(configuredHours) || configuredHours <= 0.0) { + return DEFAULT_HOURS; + } + return configuredHours; + } + + public static long periodTicks(double configuredHours) { + double ticks = normalizeHours(configuredHours) * TICKS_PER_HOUR; + if (!Double.isFinite(ticks) || ticks >= Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + return Math.max(1L, Math.round(ticks)); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b0679f9..c505265 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,5 +1,7 @@ slots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43] market-block: iaf(tfmc:market_block) +# Hours between demand recovery ticks. Each tick raises every item's demand. +demand-recovery-hours: 4 freshness-price: fresh: 1.0 stale: 0.5 diff --git a/src/test/java/net/tfminecraft/marketblock/loader/ConfigLoaderTest.java b/src/test/java/net/tfminecraft/marketblock/loader/ConfigLoaderTest.java new file mode 100644 index 0000000..c07f48d --- /dev/null +++ b/src/test/java/net/tfminecraft/marketblock/loader/ConfigLoaderTest.java @@ -0,0 +1,55 @@ +package net.tfminecraft.marketblock.loader; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.nio.file.Files; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import net.tfminecraft.marketblock.Cache; +import net.tfminecraft.marketblock.util.DemandSchedule; + +class ConfigLoaderTest { + private final ConfigLoader loader = new ConfigLoader(); + + @AfterEach + void restoreDefault() { + Cache.demandRecoveryHours = DemandSchedule.DEFAULT_HOURS; + } + + @Test + void loadsDemandRecoveryHours(@TempDir File dir) throws Exception { + File config = write(dir, "demand-recovery-hours: 4\n"); + + loader.load(config); + + assertEquals(4.0, Cache.demandRecoveryHours); + } + + @Test + void missingKeyDefaultsToFourHours(@TempDir File dir) throws Exception { + File config = write(dir, "market-block: iaf(tfmc:market_block)\n"); + + loader.load(config); + + assertEquals(DemandSchedule.DEFAULT_HOURS, Cache.demandRecoveryHours); + } + + @Test + void nonPositiveHoursFallBackToFour(@TempDir File dir) throws Exception { + loader.load(write(dir, "demand-recovery-hours: 0\n")); + assertEquals(4.0, Cache.demandRecoveryHours); + + loader.load(write(dir, "demand-recovery-hours: -1\n")); + assertEquals(4.0, Cache.demandRecoveryHours); + } + + private static File write(File dir, String yaml) throws Exception { + File config = new File(dir, "config.yml"); + Files.writeString(config.toPath(), yaml); + return config; + } +} diff --git a/src/test/java/net/tfminecraft/marketblock/trade/TradeDemandTest.java b/src/test/java/net/tfminecraft/marketblock/trade/TradeDemandTest.java new file mode 100644 index 0000000..712342d --- /dev/null +++ b/src/test/java/net/tfminecraft/marketblock/trade/TradeDemandTest.java @@ -0,0 +1,47 @@ +package net.tfminecraft.marketblock.trade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class TradeDemandTest { + + @Test + void eachRecoveryTickRaisesDemandByOneUpToJustUnderSeven() { + Trade trade = trade(1_000); + trade.setDemand(10); + + for (int i = 0; i < 20; i++) { + double before = trade.getDemand(); + trade.demand(); + double increase = trade.getDemand() - before; + assertTrue(increase + 1e-9 >= 1.0, "increase was " + increase); + assertTrue(increase < 7.0, "increase was " + increase); + } + } + + @Test + void recoveryStopsAtTheDemandLimit() { + Trade trade = trade(20); + trade.setDemand(19.5); + + trade.demand(); + + assertEquals(20.0, trade.getDemand()); + } + + @Test + void sellingCannotDropDemandBelowOne() { + Trade trade = trade(20); + trade.setDemand(1); + + trade.sell(); + + assertEquals(1.0, trade.getDemand()); + } + + private static Trade trade(double limit) { + return new Trade("oak", new Category(), limit / 2, limit, 1, "v.oak_log", 2, 64, 0); + } +} diff --git a/src/test/java/net/tfminecraft/marketblock/util/DemandScheduleTest.java b/src/test/java/net/tfminecraft/marketblock/util/DemandScheduleTest.java new file mode 100644 index 0000000..99cb122 --- /dev/null +++ b/src/test/java/net/tfminecraft/marketblock/util/DemandScheduleTest.java @@ -0,0 +1,34 @@ +package net.tfminecraft.marketblock.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class DemandScheduleTest { + + @Test + void fourHoursIsTheDefaultPeriod() { + assertEquals(4.0 * DemandSchedule.TICKS_PER_HOUR, DemandSchedule.periodTicks(4)); + assertEquals(288_000L, DemandSchedule.periodTicks(DemandSchedule.DEFAULT_HOURS)); + } + + @Test + void oneHourMatchesThePreviousHardcodedInterval() { + assertEquals(72_000L, DemandSchedule.periodTicks(1)); + } + + @Test + void fractionalHoursRoundToTheNearestTick() { + assertEquals(36_000L, DemandSchedule.periodTicks(0.5)); + assertEquals(1L, DemandSchedule.periodTicks(1.0 / DemandSchedule.TICKS_PER_HOUR / 10)); + } + + @Test + void missingOrInvalidHoursFallBackToFour() { + assertEquals(DemandSchedule.DEFAULT_HOURS, DemandSchedule.normalizeHours(0)); + assertEquals(DemandSchedule.DEFAULT_HOURS, DemandSchedule.normalizeHours(-2)); + assertEquals(DemandSchedule.DEFAULT_HOURS, DemandSchedule.normalizeHours(Double.NaN)); + assertEquals(DemandSchedule.DEFAULT_HOURS, DemandSchedule.normalizeHours(Double.POSITIVE_INFINITY)); + assertEquals(288_000L, DemandSchedule.periodTicks(0)); + } +}