Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -63,6 +65,8 @@ public class ActivityConfiguration {

private volatile int rewardMultiplier = 1;

private volatile Set<String> multiplierPools = Set.of();

private volatile Map<Integer, RewardEntry> milestoneDrops = Map.of();

private volatile Map<String, RewardEntry> dailyRewards = Map.of();
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -702,6 +707,31 @@ private int rewardAmount(Object raw, String at) {
return (int) amount;
}

Set<String> 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<String> 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<String> multiplierPools() {
return multiplierPools;
}

private int rewardMultiplier(ConfigurationSection config) {
Object raw = config.get(REWARDS_MULTIPLIER_PATH);
if (raw == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -402,13 +403,14 @@ interface SpinPayer {
}

static Payout payMilestones(List<Integer> due, Map<Integer, RewardEntry> drops, int multiplier,
Set<String> multiplierPools,
Function<String, List<RewardEntry>> pools,
Function<List<RewardEntry>, 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<RewardEntry> spins = rewardFor(milestone, drops, multiplier, pools, draw);
List<RewardEntry> spins = rewardFor(milestone, drops, multiplier, multiplierPools, pools, draw);
int spun = 0;
for (RewardEntry drawn : spins) {
if (drawn != null && pay.pay(milestone, drawn, itemMultiplier)) {
Expand Down Expand Up @@ -450,6 +452,7 @@ private static String poolOf(RewardEntry drop) {
}

static List<RewardEntry> rewardFor(int milestone, Map<Integer, RewardEntry> drops, int multiplier,
Set<String> multiplierPools,
Function<String, List<RewardEntry>> pools,
Function<List<RewardEntry>, RewardEntry> draw) {
RewardEntry fixed = drops.get(milestone);
Expand All @@ -459,7 +462,8 @@ static List<RewardEntry> rewardFor(int milestone, Map<Integer, RewardEntry> drop
}
List<RewardEntry> entries = pools.apply(pool);
List<RewardEntry> 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;
Expand Down
16 changes: 11 additions & 5 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -468,20 +468,26 @@ 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
# claim pays. 1-64; anything outside that is clamped, and 0 or a negative
# 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 '<item path> [amount 1-64]'
# (e.g. "m.material.steel 3") still works too. A fixed item is multiplied,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -530,7 +531,7 @@ private static Function<String, List<RewardEntry>> pools(RewardEntry... entries)

@Test
void aFixedDropIsPaidWithoutDrawingFromThePool() {
List<RewardEntry> spins = ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 1, pools(POOLED), NO_DRAW);
List<RewardEntry> 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);
Expand All @@ -541,7 +542,7 @@ void aFixedDropIsPaidWithoutDrawingFromThePool() {
@Test
void aPoolMilestoneStillDrawsFromThePool() {
List<List<RewardEntry>> drawnFrom = new ArrayList<>();
List<RewardEntry> drawn = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 1, pools(POOLED), pool -> {
List<RewardEntry> drawn = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 1, Set.of("pool"), pools(POOLED), pool -> {
drawnFrom.add(pool);
return pool.get(0);
});
Expand All @@ -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<RewardEntry> spins = ActivityManager.rewardFor(10, Map.of(10, DIAMONDS), 3, pools(POOLED), NO_DRAW);
List<RewardEntry> 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());
Expand All @@ -576,7 +577,7 @@ void aPoolMilestoneIsSpunMultiplierTimes() {
List<RewardEntry> rolls = new ArrayList<>(List.of(STEEL_PICK, POOLED, STEEL_PICK));
List<List<RewardEntry>> drawnFrom = new ArrayList<>();

List<RewardEntry> spins = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 3, name -> pool, p -> {
List<RewardEntry> spins = ActivityManager.rewardFor(20, Map.of(10, DIAMONDS), 3, Set.of("pool"), name -> pool, p -> {
drawnFrom.add(p);
return rolls.remove(0);
});
Expand All @@ -588,7 +589,7 @@ void aPoolMilestoneIsSpunMultiplierTimes() {
@Test
void aMultiplierOfOneSpinsThePoolOnce() {
int[] draws = {0};
List<RewardEntry> spins = ActivityManager.rewardFor(20, Map.of(), 1, pools(POOLED, STEEL_PICK), p -> {
List<RewardEntry> spins = ActivityManager.rewardFor(20, Map.of(), 1, Set.of("pool"), pools(POOLED, STEEL_PICK), p -> {
draws[0]++;
return p.get(1);
});
Expand All @@ -599,7 +600,7 @@ void aMultiplierOfOneSpinsThePoolOnce() {

@Test
void spinsCanRepeatAnEntryAtItsOwnAmount() {
List<RewardEntry> spins = ActivityManager.rewardFor(20, Map.of(), 2, pools(DIAMONDS, POOLED),
List<RewardEntry> spins = ActivityManager.rewardFor(20, Map.of(), 2, Set.of("pool"), pools(DIAMONDS, POOLED),
p -> p.get(0));

assertEquals(List.of(DIAMONDS, DIAMONDS), spins);
Expand Down Expand Up @@ -629,7 +630,7 @@ private record PaidSpin(int milestone, RewardEntry entry, int multiplier) {}
private ActivityManager.Payout payMilestones(List<Integer> due, Map<Integer, RewardEntry> drops,
int multiplier, List<PaidSpin> 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;
Expand Down Expand Up @@ -724,14 +725,46 @@ void eachMilestoneDrawsOnlyItsNamedPoolWithIndependentSpins() {
"pool_end", List.of(POOLED));
List<PaidSpin> 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<Integer, RewardEntry> 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<PaidSpin> 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));
}

}