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 @@ -404,7 +404,7 @@ private void handleAdd(CommandSender sender, String[] args) {
Added added = add(target.getUniqueId(), args[2], count, forced(args));
Outcome outcome = added.outcome();
sender.sendMessage(switch (outcome) {
case ADDED, CAPPED_ACTIVITY, CAPPED_DAILY, CAPPED_WEEKLY, CLAMPED_WEEKLY ->
case ADDED, CAPPED_ACTIVITY, CAPPED_DAILY, CAPPED_VOTE_SHARE, CAPPED_WEEKLY, CLAMPED_WEEKLY ->
messages().get(
outcome == Outcome.ADDED && added.points() > 0 ? ADD_DONE_POINTS : outcome.messageKey(),
"%count%", count, "%activity%", args[2], "%player%", name(target, args[1]),
Expand Down Expand Up @@ -476,6 +476,7 @@ enum Outcome {
NOT_A_TASK("admin.add-not-a-task"),
CAPPED_ACTIVITY("admin.add-capped-activity"),
CAPPED_DAILY("admin.add-capped-daily"),
CAPPED_VOTE_SHARE("admin.add-capped-vote-share"),
CAPPED_WEEKLY("admin.add-capped-weekly"),
// Forced only: some points landed and bar.max swallowed the rest
CLAMPED_WEEKLY("admin.add-clamped-weekly"),
Expand Down Expand Up @@ -509,6 +510,7 @@ Added add(UUID uuid, String activityId, int count, boolean force) {
case NOT_A_TASK -> Outcome.NOT_A_TASK;
case ACTIVITY_CAP -> Outcome.CAPPED_ACTIVITY;
case DAILY_MAX -> Outcome.CAPPED_DAILY;
case VOTE_SHARE -> Outcome.CAPPED_VOTE_SHARE;
case WEEKLY_MAX -> Outcome.CAPPED_WEEKLY;
case WEEKLY_CLAMPED -> Outcome.CLAMPED_WEEKLY;
case UNKNOWN -> Outcome.UNKNOWN_ACTIVITY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ public class ActivityConfiguration {

private volatile int barMax;
private volatile int dailyMax;
// Percent of dailyMax only the vote activity can fill, 0..100
private volatile int voteShare;
// Ascending, deduped, every value inside 1..barMax. Never empty.
private volatile List<Integer> milestones = List.of();
private volatile int barLength;
Expand Down Expand Up @@ -219,6 +221,8 @@ public void load() {

barMax = Math.max(1, config.getInt("bar.max", 50));
dailyMax = Math.max(1, config.getInt("bar.daily-max", 10));
voteShare = parseVoteShare(config);
warnIfVoteShareMisfits();
// After barMax: every milestone is validated against it
milestones = loadMilestones(config.getIntegerList("bar.milestones"));
// After milestones: drop_N names the Nth of them. Before the path
Expand Down Expand Up @@ -1151,6 +1155,32 @@ private List<Integer> loadMilestones(List<Integer> raw) {
return List.copyOf(milestones);
}

// ====================================
// bar.vote-share only makes sense when voting can actually fill what it
// keeps back: a vote activity every player is handed each day, with a
// daily-cap that leaves room for the whole reserved share.
// ====================================
private void warnIfVoteShareMisfits() {
if (voteShare == 0) {
return;
}
ActivityDef vote = activities.get("vote");
if (vote == null) {
plugin.getLogger().warning("bar.vote-share is " + voteShare + " but there is no 'vote' activity"
+ " - the share is not applied.");
return;
}
if (!guaranteedActivities.contains("vote")) {
plugin.getLogger().warning("bar.vote-share is " + voteShare + " but 'vote' is not daily-guaranteed"
+ " - on a day it is not drawn, the reserved share cannot be earned.");
}
int reserved = dailyMax - nonVoteDailyMax();
if (vote.dailyCap() > 0 && vote.dailyCap() < reserved) {
plugin.getLogger().warning("activities.vote.daily-cap " + vote.dailyCap() + " is below the "
+ reserved + " points bar.vote-share keeps for voting - the rest of that share is never earned.");
}
}

// ====================================
// A player can only earn from the TASKS_PER_DAY activities drawn for them,
// so the daily ceiling is the worst draw they can get: the lowest
Expand All @@ -1171,9 +1201,14 @@ private void warnIfBarUnreachable() {
}
}

String warning = unreachableWarning(caps, activities.size(), dailyMax, milestones.get(0));
// Counted for a player who never votes: with a vote activity loaded,
// bar.vote-share keeps part of the day out of their reach
boolean shared = voteShare > 0 && activities.containsKey("vote");
String warning = unreachableWarning(caps, activities.size(), shared ? nonVoteDailyMax() : dailyMax,
milestones.get(0));
if (warning != null) {
plugin.getLogger().warning(warning);
plugin.getLogger().warning(shared ? warning + " This is without voting: bar.vote-share keeps "
+ (dailyMax - nonVoteDailyMax()) + " of bar.daily-max for it." : warning);
}
}

Expand Down Expand Up @@ -1435,6 +1470,22 @@ public int dailyMax() {
return dailyMax;
}

// Most points every activity but vote can add together in one day
public int nonVoteDailyMax() {
return nonVoteDailyMax(dailyMax, voteShare);
}

// Rounded down, so the share kept for voting rounds up
static int nonVoteDailyMax(int dailyMax, int voteShare) {
return dailyMax * (100 - voteShare) / 100;
}

// bar.vote-share, clamped to 0-100 with a warning. Package-private for
// the test, which has no server to run load() on.
int parseVoteShare(ConfigurationSection config) {
return clamped(config, "bar.vote-share", 50, 0, 100);
}

public List<Integer> milestones() {
return milestones;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,11 @@ private RecordResult record(UUID uuid, String activityId, int amount, boolean ga
}

RecordResult result = gated
? data.record(amount, def, config.barMax(), config.dailyMax(), config.milestones())
// No vote activity loaded: nothing could fill the reserved share,
// so there is no share to keep
? data.record(amount, def, config.barMax(), config.dailyMax(),
config.activity("vote") == null ? config.dailyMax() : config.nonVoteDailyMax(),
config.milestones())
: data.recordForced(amount, def, config.barMax(), config.milestones());
return credited(uuid, data, result, Utils.colorize(def.display()));
}
Expand Down
42 changes: 40 additions & 2 deletions src/main/java/tfmc/justin/activity/models/PlayerData.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public class PlayerData {
// Points accepted today across all activities; anything past dailyMax is
// dropped and never reaches the weekly total
private volatile int dailyPoints;
// The part of dailyPoints the vote activity put in through the gated
// path - what bar.vote-share leaves out of the non-vote share. Never above
// dailyPoints. A forced vote does not raise it, as it does not raise
// dailyPoints either.
private volatile int votePoints;
private volatile String weekKey;
private volatile String dayKey;
// ====================================
Expand Down Expand Up @@ -110,6 +115,7 @@ public boolean roll(String weekKey, String dayKey) {
points = 0;
claimedPoints = 0;
dailyPoints = 0;
votePoints = 0;
daily.clear();
clearTasks();
rerolls = 0;
Expand All @@ -120,6 +126,7 @@ public boolean roll(String weekKey, String dayKey) {

if (!this.dayKey.equals(dayKey)) {
dailyPoints = 0;
votePoints = 0;
daily.clear();
clearTasks();
rerolls = 0;
Expand All @@ -138,6 +145,16 @@ public boolean roll(String weekKey, String dayKey) {
// day by the difference; acceptable
// ====================================
public RecordResult record(int amount, ActivityDef def, int max, int dailyMax, List<Integer> milestones) {
return record(amount, def, max, dailyMax, dailyMax, milestones);
}

// ====================================
// Same, with bar.vote-share: every activity but vote shares nonVoteMax of
// the day, and today's non-vote points are dailyPoints minus votePoints.
// A nonVoteMax equal to dailyMax is no share at all.
// ====================================
public RecordResult record(int amount, ActivityDef def, int max, int dailyMax, int nonVoteMax,
List<Integer> milestones) {
int before = daily.getOrDefault(def.id(), 0);
// Saturate: a bogus /activity add 2000000000 twice must not wrap the
// counter negative and hand out awards all over again
Expand All @@ -147,9 +164,12 @@ public RecordResult record(int amount, ActivityDef def, int max, int dailyMax, L
int earned = def.worth(after) - def.worth(before);
// Everything past today's budget is lost outright - it must not reach
// the weekly bar, today or later
int budget = Math.max(0, dailyMax - dailyPoints);
int dailyBudget = Math.max(0, dailyMax - dailyPoints);
boolean isVote = def.id().equals("vote");
int budget = isVote ? dailyBudget
: Math.min(dailyBudget, Math.max(0, nonVoteMax - (dailyPoints - votePoints)));
if (earned > 0 && budget <= 0) {
return new RecordResult(0, 0, Recorded.DAILY_MAX);
return new RecordResult(0, 0, dailyBudget <= 0 ? Recorded.DAILY_MAX : Recorded.VOTE_SHARE);
}
earned = Math.min(earned, budget);
if (earned <= 0) {
Expand All @@ -162,6 +182,9 @@ public RecordResult record(int amount, ActivityDef def, int max, int dailyMax, L
int pointsBefore = points;
addPoints(earned, max);
dailyPoints += points - pointsBefore;
if (isVote) {
votePoints += points - pointsBefore;
}
if (points == pointsBefore) {
return new RecordResult(0, 0, Recorded.WEEKLY_MAX);
}
Expand Down Expand Up @@ -260,6 +283,7 @@ public void reset(String weekKey, String dayKey) {
points = 0;
claimedPoints = 0;
dailyPoints = 0;
votePoints = 0;
daily.clear();
clearTasks();
rerolls = 0;
Expand Down Expand Up @@ -300,7 +324,11 @@ public void reroll(List<String> newTasks, int max) {
// lowered and then raised back), before - points is negative and
// must not be allowed to increase dailyPoints instead.
int refund = Math.max(0, before - points);
int dailyBefore = dailyPoints;
dailyPoints = Math.max(0, dailyPoints - refund);
// The carried points keep the day's vote/non-vote mix, so votes carried
// past a reroll still do not count against the non-vote share
votePoints = dailyBefore == 0 ? 0 : (int) ((long) votePoints * dailyPoints / dailyBefore);
daily.clear();
clearTasks();
tasks.addAll(newTasks);
Expand Down Expand Up @@ -460,6 +488,16 @@ public int dailyPoints() {
return dailyPoints;
}

public int votePoints() {
return votePoints;
}

// The store's, for the persisted value; clamped to 0..dailyPoints so the
// non-vote share can never read as negative or past the day
public void setVotePoints(int votePoints) {
this.votePoints = Math.max(0, Math.min(votePoints, dailyPoints));
}

public int count(String id) {
return daily.getOrDefault(id, 0);
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/tfmc/justin/activity/models/Recorded.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public enum Recorded {
// Today's point budget (bar.daily-max) was already spent
DAILY_MAX,

// Not vote, and every other activity together has already added its
// share of the day (bar.daily-max less bar.vote-share); voting still can
VOTE_SHARE,

// The weekly bar (bar.max) was already full
WEEKLY_MAX,

Expand Down
21 changes: 18 additions & 3 deletions src/main/java/tfmc/justin/activity/store/PlayerStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import tfmc.justin.activity.config.ActivityConfiguration;
import tfmc.justin.activity.models.ActivityDef;
import tfmc.justin.activity.models.PlayerData;

import java.io.File;
Expand Down Expand Up @@ -142,7 +143,8 @@ private void readEntry(ConfigurationSection root, String key) {
return;
}

players.put(uuid, parse(entry, config.barMax(), config.dailyMax(), id -> config.activity(id) != null));
players.put(uuid, parse(entry, config.barMax(), config.dailyMax(), id -> config.activity(id) != null,
config.activity("vote")));
}

// ====================================
Expand All @@ -153,6 +155,13 @@ private void readEntry(ConfigurationSection root, String key) {
// ====================================
static PlayerData readEntry(ConfigurationSection root, String key, int barMax, int dailyMax,
Predicate<String> known) {
return readEntry(root, key, barMax, dailyMax, known, null);
}

// Same, with the loaded vote activity (or null) for a row saved before
// vote-points existed
static PlayerData readEntry(ConfigurationSection root, String key, int barMax, int dailyMax,
Predicate<String> known, ActivityDef vote) {
ConfigurationSection entry = root.getConfigurationSection(key);
if (entry == null) {
return null;
Expand All @@ -164,14 +173,14 @@ static PlayerData readEntry(ConfigurationSection root, String key, int barMax, i
return null;
}

return parse(entry, barMax, dailyMax, known);
return parse(entry, barMax, dailyMax, known, vote);
}

// ====================================
// One entry's values, with the key already dealt with by the caller.
// ====================================
private static PlayerData parse(ConfigurationSection entry, int barMax, int dailyMax,
Predicate<String> known) {
Predicate<String> known, ActivityDef vote) {
Map<String, Integer> daily = new HashMap<>();
ConfigurationSection dailySection = entry.getConfigurationSection("daily");
if (dailySection != null) {
Expand Down Expand Up @@ -207,6 +216,11 @@ private static PlayerData parse(ConfigurationSection entry, int barMax, int dail
entry.getString("day", ""), claimedPoints, daily, tasks, entry.getStringList("revealed"), rerolls);
// A row written before daily-reward existed has no key: not claimed
data.setDailyRewardClaimed(entry.getBoolean("daily-reward-claimed", false));
// A row written before vote-share existed has no key: today's vote
// count's worth is the best guess of what voting put in. setVotePoints
// clamps it to dailyPoints, and nothing already earned is taken back.
data.setVotePoints(entry.contains("vote-points") ? entry.getInt("vote-points")
: vote == null ? 0 : vote.worth(daily.getOrDefault(vote.id(), 0)));
return data;
}

Expand Down Expand Up @@ -392,6 +406,7 @@ static YamlConfiguration snapshot(Map<UUID, PlayerData> players) {
yaml.set(path + ".day", data.dayKey());
yaml.set(path + ".claimed-points", data.claimedPoints());
yaml.set(path + ".daily-points", data.dailyPoints());
yaml.set(path + ".vote-points", data.votePoints());
yaml.set(path + ".rerolls", data.rerolls());
yaml.set(path + ".daily-reward-claimed", data.dailyRewardClaimed());
yaml.set(path + ".daily", new LinkedHashMap<>(data.daily()));
Expand Down
5 changes: 4 additions & 1 deletion src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ reset:
# /activity; a task earns nothing until the player clicks it to reveal it.
# Every 'every' actions → earn 'points'. 'daily-cap' = most points one
# activity can add per day (0 = no cap). Bar caps at bar.max; bar.daily-max
# caps what can reach it on any single day, first. Both are bypassed by
# caps what can reach it on any single day, first, and bar.vote-share keeps part
# of that day for voting only. All are bypassed by
# /activity add <player> <activity> <count> --force, which is staff-only and
# credits the full worth of the count; only bar.max still holds it back.
# daily-guaranteed: optional, default false. An activity marked true is in
Expand Down Expand Up @@ -558,6 +559,8 @@ gui:
bar:
max: 50 # points to fill the weekly bar
daily-max: 10 # most points one day can add - anything past it is lost and never reaches the weekly bar
vote-share: 50 # percent of daily-max only voting can fill (0-100, 0 = off). Every other activity
# together adds at most the rest - 5 of 10 here - and anything past that is lost
milestones: [10, 20] # point totals a reward can be claimed at. The bar keeps filling past the
# last one but pays nothing more until the week resets. A change applies
# at once on reload: a new milestone above a player's claimed total is
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ admin:
add-not-a-task: "&cAdded nothing: %activity% is not one of %player%'s revealed tasks today. Use --force to credit it anyway."
add-capped-activity: "&eAdded %count% to %activity% for %player%, but no points: %activity% has already given all it can today."
add-capped-daily: "&eAdded %count% to %activity% for %player%, but no points: %player% has already earned today's maximum."
add-capped-vote-share: "&eAdded %count% to %activity% for %player%, but no points: %player% has used today's non-vote share - voting can still add the rest."
add-capped-weekly: "&eAdded %count% to %activity% for %player%, but no points: %player%'s weekly bar is already full."
add-clamped-weekly: "&eAdded %count% to %activity% for %player%, but only &f%points%&e points fit: %player%'s weekly bar hit its maximum of %max%."
addpoints-done: "&aAdded &f%points%&a points to %player%."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ void everyOutcomeHasAShippedMessage() {
assertEquals("admin.add-not-a-task", Outcome.NOT_A_TASK.messageKey());
assertEquals("admin.add-capped-activity", Outcome.CAPPED_ACTIVITY.messageKey());
assertEquals("admin.add-capped-daily", Outcome.CAPPED_DAILY.messageKey());
assertEquals("admin.add-capped-vote-share", Outcome.CAPPED_VOTE_SHARE.messageKey());
assertEquals("admin.add-capped-weekly", Outcome.CAPPED_WEEKLY.messageKey());
assertEquals("admin.add-clamped-weekly", Outcome.CLAMPED_WEEKLY.messageKey());
assertEquals("admin.unknown-activity", Outcome.UNKNOWN_ACTIVITY.messageKey());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package tfmc.justin.activity.config;

import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.Test;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

// ====================================
// bar.vote-share: the clamp on the value read and the non-vote share of
// bar.daily-max it leaves. load() needs a live server, so the parser and the
// arithmetic are driven directly, with the plugin's logger captured.
// ====================================
class ActivityConfigurationVoteShareTest {

private final List<String> logged = new ArrayList<>();

private int nonVoteDailyMax(int dailyMax, String voteShareYaml) {
ActivityConfiguration config = new ActivityConfiguration(TestPlugins.capturing(logged));
int share = config.parseVoteShare(
YamlConfiguration.loadConfiguration(new StringReader("bar:\n vote-share: " + voteShareYaml + "\n")));
return ActivityConfiguration.nonVoteDailyMax(dailyMax, share);
}

@Test
void theNonVoteShareRoundsDown() {
assertEquals(3, nonVoteDailyMax(7, "50"));
assertTrue(logged.isEmpty(), logged.toString());
}

@Test
void aNegativeShareClampsToZero() {
assertEquals(7, nonVoteDailyMax(7, "-5"));
assertTrue(logged.stream().anyMatch(line -> line.contains("bar.vote-share -5 is outside 0-100")),
logged.toString());
}

@Test
void aShareOverAHundredClampsToAHundred() {
assertEquals(0, nonVoteDailyMax(7, "150"));
assertTrue(logged.stream().anyMatch(line -> line.contains("bar.vote-share 150 is outside 0-100")),
logged.toString());
}
}
Loading
Loading