From 86ca0a78bc8661d47c056b2503f87fe484bd2d33 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:51:42 +0200 Subject: [PATCH] fix: whitelist activity pools eligible for reward multiplier --- .../config/ActivityConfiguration.java | 30 ++++++++++ .../activitytf/managers/ActivityManager.java | 10 +++- src/main/resources/config.yml | 16 ++++-- .../ActivityConfigurationRewardsTest.java | 20 +++++++ .../ActivityManagerRewardItemsTest.java | 55 +++++++++++++++---- 5 files changed, 112 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java b/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java index 533fc2f..d968be0 100644 --- a/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java +++ b/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java @@ -21,11 +21,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.TreeSet; import java.util.function.Predicate; @@ -63,6 +65,8 @@ public class ActivityConfiguration { private volatile int rewardMultiplier = 1; + private volatile Set multiplierPools = Set.of(); + private volatile Map milestoneDrops = Map.of(); private volatile Map dailyRewards = Map.of(); @@ -143,6 +147,7 @@ public void load() { rewardPools = loadRewardPools(config); rewardMultiplier = rewardMultiplier(config); + multiplierPools = loadMultiplierPools(config); barMax = Math.max(1, config.getInt("bar.max", 50)); dailyMax = Math.max(1, config.getInt("bar.daily-max", 10)); @@ -702,6 +707,31 @@ private int rewardAmount(Object raw, String at) { return (int) amount; } + Set loadMultiplierPools(ConfigurationSection config) { + String path = "rewards.multiplier-pools"; + Object raw = config.get(path); + if (raw == null) { + return Set.of(); + } + if (!(raw instanceof List entries)) { + plugin.getLogger().warning(path + " must be a list of pool names - no pools are multiplied."); + return Set.of(); + } + Set names = new HashSet<>(); + for (Object entry : entries) { + if (!(entry instanceof String name) || !isPoolName(name)) { + plugin.getLogger().warning(path + " contains an invalid pool name - ignored."); + continue; + } + names.add(poolName(name)); + } + return Set.copyOf(names); + } + + public Set multiplierPools() { + return multiplierPools; + } + private int rewardMultiplier(ConfigurationSection config) { Object raw = config.get(REWARDS_MULTIPLIER_PATH); if (raw == null) { diff --git a/src/main/java/net/tfminecraft/activitytf/managers/ActivityManager.java b/src/main/java/net/tfminecraft/activitytf/managers/ActivityManager.java index db4d062..45dc6ed 100644 --- a/src/main/java/net/tfminecraft/activitytf/managers/ActivityManager.java +++ b/src/main/java/net/tfminecraft/activitytf/managers/ActivityManager.java @@ -355,7 +355,8 @@ public int claim(Player player) { return 0; } - Payout payout = payMilestones(due, drops, config.rewardMultiplier(), pools, ActivityManager::draw, + Payout payout = payMilestones(due, drops, config.rewardMultiplier(), config.multiplierPools(), + pools, ActivityManager::draw, (milestone, drawn, itemMultiplier) -> { if (!dispatchRewards(player, drawn, "milestone " + milestone, itemMultiplier)) { return false; @@ -402,13 +403,14 @@ interface SpinPayer { } static Payout payMilestones(List due, Map drops, int multiplier, + Set multiplierPools, Function> pools, Function, RewardEntry> draw, SpinPayer pay, String who, Logger logger) { int paid = 0; for (int milestone : due) { int itemMultiplier = poolOf(drops.get(milestone)) == null ? multiplier : 1; - List spins = rewardFor(milestone, drops, multiplier, pools, draw); + List spins = rewardFor(milestone, drops, multiplier, multiplierPools, pools, draw); int spun = 0; for (RewardEntry drawn : spins) { if (drawn != null && pay.pay(milestone, drawn, itemMultiplier)) { @@ -450,6 +452,7 @@ private static String poolOf(RewardEntry drop) { } static List rewardFor(int milestone, Map drops, int multiplier, + Set multiplierPools, Function> pools, Function, RewardEntry> draw) { RewardEntry fixed = drops.get(milestone); @@ -459,7 +462,8 @@ static List rewardFor(int milestone, Map drop } List entries = pools.apply(pool); List spins = new ArrayList<>(); - for (int spin = 0; spin < multiplier; spin++) { + int spinCount = multiplierPools.contains(pool) ? multiplier : 1; + for (int spin = 0; spin < spinCount; spin++) { spins.add(draw.apply(entries)); } return spins; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 6fe9aa8..69976db 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -449,8 +449,8 @@ activities: # The reward pool. Clicking the weekly bar in /activity claims every # milestone reached and not yet claimed (see bar.milestones), drawing one -# entry from this pool at random per spin (rewards.multiplier spins per -# milestone), handing over that entry's items and running its commands. +# entry from this pool at random per spin (extra spins only for pools in +# rewards.multiplier-pools), handing over the entry's items and commands. # weight - how likely the entry is, relative to the others. Optional, # defaults to 1; an entry with weight 0 or less is dropped with a # warning and can never be drawn. @@ -468,8 +468,8 @@ activities: # Bedrock/Floodgate or other unsafe names, write reward commands using only # %uuid% (e.g. via a plugin that can look a player up by UUID). rewards: - # How many spins each pool milestone gets: at 2, claiming it draws from the - # pool twice, independently - the same entry can come up both times - and + # Spins per whitelisted pool milestone: at 2, it draws from the pool + # twice, independently - the same entry can come up both times - and # pays each draw's items and commands at the amounts written, with one chat # line per draw. A fixed drop has nothing to spin and hands over its amount # times this instead. Read at payout, so a reload changes what the next @@ -477,11 +477,17 @@ rewards: # reads as 1 rather than as "pay nothing". A total above one stack is # handed over as whole stacks of the item's own maximum size. multiplier: 1 + # Only these weekly reward pools get extra spins. Names are case-insensitive. + # Missing or empty means every pool draws once. Add only material pools, + # e.g. [pool_prologue]; leave skin/scroll pools out. Include 'pool' explicitly + # to multiply the default pool. Daily pool rewards still draw once; fixed + # item rewards keep their existing amount multiplier. + multiplier-pools: [] # What each milestone pays. N counts the bar.milestones that survive # loading - sorted, deduped, out-of-range values dropped - so drop_1 is the # lowest of those, drop_2 the next, and so on. Adding or removing a # milestone renumbers every drop_N above it. 'pool' spins the pool (once per - # multiplier) below; anything else is a fixed item, written as a block: + # multiplier when whitelisted); anything else is a fixed item block: # 'item:' (required, the same four forms as a pool item) and 'amount:' # (optional, 1-64, default 1). The one-line form ' [amount 1-64]' # (e.g. "m.material.steel 3") still works too. A fixed item is multiplied, diff --git a/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationRewardsTest.java b/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationRewardsTest.java index 473de40..d5a9563 100644 --- a/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationRewardsTest.java +++ b/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationRewardsTest.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -935,4 +936,23 @@ void missingNamedDropNeverFallsBackToDefaultPool() { assertFalse(ActivityConfiguration.poolNeededButEmpty(List.of(), drops, List.of(10, 20))); } + @Test + void multiplierWhitelistNormalizesNamesAndIgnoresInvalidEntries() { + ActivityConfiguration config = new ActivityConfiguration(stubPlugin()); + assertEquals(Set.of("pool", "pool_prologue"), config.loadMultiplierPools(yaml( + "rewards:\n multiplier-pools: [POOL, ' Pool_Prologue ', pool_prologue, '*', 12, null]\n"))); + assertTrue(loggedContains("invalid pool name")); + } + + @Test + void absentEmptyOrMalformedWhitelistNeverEnablesExtraPoolDraws() { + ActivityConfiguration config = new ActivityConfiguration(stubPlugin()); + assertTrue(config.multiplierPools().isEmpty()); + for (String value : List.of("", "rewards:\n multiplier-pools: []\n", + "rewards:\n multiplier-pools: pool_prologue\n")) { + assertTrue(config.loadMultiplierPools(yaml(value)).isEmpty()); + } + assertTrue(loggedContains("must be a list")); + } + } diff --git a/src/test/java/net/tfminecraft/activitytf/managers/ActivityManagerRewardItemsTest.java b/src/test/java/net/tfminecraft/activitytf/managers/ActivityManagerRewardItemsTest.java index 1584e55..1efa4fb 100644 --- a/src/test/java/net/tfminecraft/activitytf/managers/ActivityManagerRewardItemsTest.java +++ b/src/test/java/net/tfminecraft/activitytf/managers/ActivityManagerRewardItemsTest.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.Map; import java.util.UUID; import java.util.function.Function; @@ -530,7 +531,7 @@ private static Function> pools(RewardEntry... entries) @Test void aFixedDropIsPaidWithoutDrawingFromThePool() { - List spins = ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 1, pools(POOLED), NO_DRAW); + List spins = ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 1, Set.of("pool"), pools(POOLED), NO_DRAW); assertEquals(1, spins.size()); RewardEntry paid = spins.get(0); @@ -541,7 +542,7 @@ void aFixedDropIsPaidWithoutDrawingFromThePool() { @Test void aPoolMilestoneStillDrawsFromThePool() { List> drawnFrom = new ArrayList<>(); - List drawn = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 1, pools(POOLED), pool -> { + List drawn = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 1, Set.of("pool"), pools(POOLED), pool -> { drawnFrom.add(pool); return pool.get(0); }); @@ -553,14 +554,14 @@ void aPoolMilestoneStillDrawsFromThePool() { @Test void aFixedDropsChatLineCarriesTheMultipliedAmount() { assertEquals("#50d990x3 #b8906eDiamond", - ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 1, pools(), NO_DRAW).get(0).display()); + ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 1, Set.of("pool"), pools(), NO_DRAW).get(0).display()); assertEquals("#50d990x6 #b8906eDiamond", - ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 2, pools(), NO_DRAW).get(0).display()); + ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 2, Set.of("pool"), pools(), NO_DRAW).get(0).display()); } @Test void aFixedDropIsOnePayoutAtAnyMultiplier() { - List spins = ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 3, pools(POOLED), NO_DRAW); + List spins = ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 3, Set.of("pool"), pools(POOLED), NO_DRAW); assertEquals(1, spins.size()); assertEquals(DIAMONDS.items(), spins.get(0).items()); @@ -576,7 +577,7 @@ void aPoolMilestoneIsSpunMultiplierTimes() { List rolls = new ArrayList<>(List.of(STEEL_PICK, POOLED, STEEL_PICK)); List> drawnFrom = new ArrayList<>(); - List spins = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 3, name -> pool, p -> { + List spins = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 3, Set.of("pool"), name -> pool, p -> { drawnFrom.add(p); return rolls.remove(0); }); @@ -588,7 +589,7 @@ void aPoolMilestoneIsSpunMultiplierTimes() { @Test void aMultiplierOfOneSpinsThePoolOnce() { int[] draws = {0}; - List spins = ActivityManager.rewardFor(20, Map.of(), 1, pools(POOLED, STEEL_PICK), p -> { + List spins = ActivityManager.rewardFor(20, Map.of(), 1, Set.of("pool"), pools(POOLED, STEEL_PICK), p -> { draws[0]++; return p.get(1); }); @@ -599,7 +600,7 @@ void aMultiplierOfOneSpinsThePoolOnce() { @Test void spinsCanRepeatAnEntryAtItsOwnAmount() { - List spins = ActivityManager.rewardFor(20, Map.of(), 2, pools(DIAMONDS, POOLED), + List spins = ActivityManager.rewardFor(20, Map.of(), 2, Set.of("pool"), pools(DIAMONDS, POOLED), p -> p.get(0)); assertEquals(List.of(DIAMONDS, DIAMONDS), spins); @@ -629,7 +630,7 @@ private record PaidSpin(int milestone, RewardEntry entry, int multiplier) {} private ActivityManager.Payout payMilestones(List due, Map drops, int multiplier, List calls, boolean... answers) { int[] next = {0}; - return ActivityManager.payMilestones(due, drops, multiplier, pools(POOLED), FIRST, + return ActivityManager.payMilestones(due, drops, multiplier, Set.of("pool"), pools(POOLED), FIRST, (milestone, entry, m) -> { calls.add(new PaidSpin(milestone, entry, m)); return next[0] < answers.length ? answers[next[0]++] : true; @@ -724,14 +725,46 @@ void eachMilestoneDrawsOnlyItsNamedPoolWithIndependentSpins() { "pool_end", List.of(POOLED)); List calls = new ArrayList<>(); assertEquals(new ActivityManager.Payout(2, false), ActivityManager.payMilestones( - List.of(10, 20), drops, 2, name -> pools.getOrDefault(name, List.of()), FIRST, + List.of(10, 20), drops, 2, Set.of("pool_prologue", "pool_end"), name -> pools.getOrDefault(name, List.of()), FIRST, (milestone, reward, multiplier) -> { calls.add(new PaidSpin(milestone, reward, multiplier)); return true; }, "test", logger())); assertEquals(List.of(new PaidSpin(10, DIAMONDS, 1), new PaidSpin(10, DIAMONDS, 1), new PaidSpin(20, POOLED, 1), new PaidSpin(20, POOLED, 1)), calls); assertEquals(new ActivityManager.Payout(0, true), ActivityManager.payMilestones( - List.of(10), drops, 1, name -> List.of(), pool -> RewardEntry.pick(pool, 0), + List.of(10), drops, 1, Set.of(), name -> List.of(), pool -> RewardEntry.pick(pool, 0), (milestone, reward, multiplier) -> { throw new AssertionError("Missing pool paid"); }, "test", logger())); } + @Test + void onlyWhitelistedMaterialPoolsGetExtraSpinsWhileScrollsPayOnce() { + Map drops = Map.of( + 10, net.tfminecraft.activitytf.config.ActivityConfiguration.poolRef("pool_prologue"), + 20, net.tfminecraft.activitytf.config.ActivityConfiguration.poolRef("pool_prologue"), + 40, net.tfminecraft.activitytf.config.ActivityConfiguration.poolRef("pool_skin")); + RewardEntry scroll = new RewardEntry(1, "Skin scroll", List.of("say scroll"), + List.of(new RewardEntry.Item("m.loot.common_item_skin_scroll", 1))); + List calls = new ArrayList<>(); + assertEquals(new ActivityManager.Payout(3, false), ActivityManager.payMilestones( + List.of(10, 20, 40), drops, 2, Set.of("pool_prologue"), + name -> List.of(name.equals("pool_prologue") ? DIAMONDS : scroll), FIRST, + (milestone, reward, multiplier) -> { + calls.add(new PaidSpin(milestone, reward, multiplier)); + return true; + }, "test", logger())); + assertEquals(List.of(new PaidSpin(10, DIAMONDS, 1), new PaidSpin(10, DIAMONDS, 1), + new PaidSpin(20, DIAMONDS, 1), new PaidSpin(20, DIAMONDS, 1), + new PaidSpin(40, scroll, 1)), calls); + } + + @Test + void unlistedDefaultAndNamedPoolsDrawOnceEvenAtMaximumMultiplier() { + for (String name : List.of("pool", "pool_skin", "pool_ascended_skin", "pool_gilded_skin")) { + var drop = net.tfminecraft.activitytf.config.ActivityConfiguration.poolRef(name); + assertEquals(List.of(POOLED), ActivityManager.rewardFor(10, Map.of(10, drop), + 64, Set.of(), pools(POOLED), FIRST)); + } + assertEquals(List.of(POOLED), ActivityManager.rewardFor(10, Map.of(), + 64, Set.of("pool_prologue"), pools(POOLED), FIRST)); + } + }