Adds a highly configurable option for deciding the base payout

This commit is contained in:
2024-01-09 18:22:50 +01:00
parent db922f7351
commit 0b601d9bb7
13 changed files with 455 additions and 66 deletions

View File

@ -111,7 +111,7 @@ public final class PlayerPayouts extends JavaPlugin {
public static void reload() {
playerPayouts.reloadConfig();
playerPayouts.saveConfig();
playerPayouts.configuration = new Configuration(playerPayouts.getConfig());
playerPayouts.configuration.load(playerPayouts.getConfig());
}
/**

View File

@ -7,7 +7,6 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A command for overriding payments for specific groups
@ -41,11 +40,11 @@ public class SetGroupPaymentCommand implements CommandExecutor {
try {
String group = arguments[0];
if (StringHelper.isNonValue(arguments[1])) {
setPayout(group, null);
configuration.setGroupPayout(group, null);
commandSender.sendMessage(String.format("Group payout for group %s has been cleared", group));
} else {
Double payout = Double.parseDouble(arguments[1]);
setPayout(group, payout);
configuration.setGroupPayout(group, payout);
commandSender.sendMessage(String.format("Group payout for group %s has been set to %s", group, payout));
}
return true;
@ -55,15 +54,4 @@ public class SetGroupPaymentCommand implements CommandExecutor {
}
}
/**
* Sets the payout for the given group
*
* @param group <p>The group to set payout for</p>
* @param payout <p>The payout to set</p>
*/
private void setPayout(@NotNull String group, @Nullable Double payout) {
configuration.setGroupPayout(group, payout);
configuration.save();
}
}

View File

@ -9,7 +9,6 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
@ -66,11 +65,11 @@ public class SetPlayerPaymentCommand implements CommandExecutor {
// Parse the payout value
try {
if (StringHelper.isNonValue(arguments[1])) {
setPayout(playerId, null);
configuration.setPlayerPayout(playerId, null);
commandSender.sendMessage(String.format("Player payout for player %s has been cleared", playerName));
} else {
Double payout = Double.parseDouble(arguments[1]);
setPayout(playerId, payout);
configuration.setPlayerPayout(playerId, payout);
commandSender.sendMessage(String.format("Player payout for player %s has been set to %s", playerName,
payout));
}
@ -81,15 +80,4 @@ public class SetPlayerPaymentCommand implements CommandExecutor {
}
}
/**
* Sets the payout for the given player
*
* @param playerId <p>The player to set payout for</p>
* @param payout <p>The payout to set</p>
*/
private void setPayout(@NotNull UUID playerId, @Nullable Double payout) {
configuration.setPlayerPayout(playerId, payout);
configuration.save();
}
}

View File

@ -1,32 +1,41 @@
package net.knarcraft.playerpayouts.config;
import net.knarcraft.playerpayouts.PlayerPayouts;
import net.knarcraft.playerpayouts.config.payout.PayoutAction;
import net.knarcraft.playerpayouts.config.payout.PayoutActionParser;
import net.knarcraft.playerpayouts.config.payout.PayoutComponent;
import net.knarcraft.playerpayouts.config.payout.PayoutDelimiter;
import net.knarcraft.playerpayouts.config.payout.PayoutTarget;
import net.knarcraft.playerpayouts.manager.PermissionManager;
import org.apache.commons.lang.NotImplementedException;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* This plugin's configuration
*/
public class Configuration {
private final FileConfiguration fileConfiguration;
private final Map<String, Double> groupPayouts;
private final Map<UUID, Double> playerPayouts;
private final double defaultPayout;
private final int hoursUntilBonus;
private final double bonusMultiplier;
private final int payoutDelay;
private final double afkPercentage;
private final boolean displayPaymentMessage;
private FileConfiguration fileConfiguration;
private Map<String, Double> groupPayouts;
private Map<UUID, Double> playerPayouts;
private double defaultPayout;
private int hoursUntilBonus;
private double bonusMultiplier;
private int payoutDelay;
private double afkPercentage;
private boolean displayPaymentMessage;
private PayoutComponent payoutComponent;
/**
* Instantiates a new configuration
@ -34,6 +43,15 @@ public class Configuration {
* @param fileConfiguration <p>The file configuration to read values from</p>
*/
public Configuration(@NotNull FileConfiguration fileConfiguration) {
load(fileConfiguration);
}
/**
* Loads the configuration specified in the given file configuration
*
* @param fileConfiguration <p>The file configuration to load</p>
*/
public void load(@NotNull FileConfiguration fileConfiguration) {
groupPayouts = new HashMap<>();
playerPayouts = new HashMap<>();
this.fileConfiguration = fileConfiguration;
@ -57,6 +75,19 @@ public class Configuration {
this.payoutDelay = fileConfiguration.getInt(ConfigurationKey.PAYOUT_DELAY.getPath(), 60);
this.afkPercentage = fileConfiguration.getDouble(ConfigurationKey.AFK_PERCENTAGE.getPath(), 0);
this.displayPaymentMessage = fileConfiguration.getBoolean(ConfigurationKey.DISPLAY_PAYMENT_MESSAGE.getPath(), true);
try {
this.payoutComponent = PayoutActionParser.matchPayoutComponent(fileConfiguration.getString(
ConfigurationKey.PAYOUT_RULES.getPath(), "p,hg,b"));
} catch (ParseException exception) {
PlayerPayouts.getInstance().getLogger().log(Level.SEVERE, "Unable to parse your payout rules! Please " +
"check your configuration file! Falling back to default!");
PlayerPayouts.getInstance().getLogger().log(Level.SEVERE, exception.getMessage());
try {
this.payoutComponent = PayoutActionParser.matchPayoutComponent("p,hg,b");
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
/**
@ -81,37 +112,86 @@ public class Configuration {
return bonusMultiplier;
}
/**
* Gets the base pay for the given player
*
* @param player <p>The player to get the base pay for</p>
* @return <p>The base pay for the player</p>
*/
public double getBasePay(@NotNull Player player) {
double pay = calculatePayoutFromComponent(payoutComponent, player);
if (pay != 0) {
return pay;
} else {
return getDefaultBasePay(player);
}
}
/**
* Gets the base payout to pay the given player
*
* @param player <p>The player to get the base payout for</p>
* @return <p>The player's base pay</p>
*/
public double getBasePay(@NotNull Player player) {
public double getDefaultBasePay(@NotNull Player player) {
if (playerPayouts.get(player.getUniqueId()) != null) {
return playerPayouts.get(player.getUniqueId());
}
double groupPayout = -1;
double groupPayout = 0;
if (PermissionManager.isInitialized()) {
groupPayout = getGroupPayout(player);
groupPayout = getGroupPayout(player, true);
}
if (groupPayout == -1) {
if (groupPayout == 0) {
return defaultPayout;
} else {
return groupPayout;
}
}
/**
* Gets the max payout of the given player's permission groups
*
* @param player <p>The player to get the group payout for</p>
* @param highest <p>Whether to just get the highest value, instead of the sum</p>
* @return <p>The group payout, or -1 if no groups has a set payout</p>
*/
private double getGroupPayout(@NotNull Player player, boolean highest) {
if (highest) {
return getHighestGroupPayout(player);
} else {
return getSumGroupPayout(player);
}
}
/**
* Gets the max payout of the given player's permission groups
*
* @param player <p>The player to get the group payout for</p>
* @return <p>The group payout, or -1 if no groups has a set payout</p>
*/
private double getGroupPayout(@NotNull Player player) {
double maxPay = -1;
private double getSumGroupPayout(@NotNull Player player) {
double total = 0;
for (String group : PermissionManager.getPlayerGroups(player)) {
if (groupPayouts.containsKey(group)) {
Double groupPayout = groupPayouts.get(group);
if (groupPayout != null) {
total += groupPayout;
}
}
}
return total;
}
/**
* Gets the max payout of the given player's permission groups
*
* @param player <p>The player to get the group payout for</p>
* @return <p>The group payout, or -1 if no groups has a set payout</p>
*/
private double getHighestGroupPayout(@NotNull Player player) {
double maxPay = 0;
for (String group : PermissionManager.getPlayerGroups(player)) {
if (groupPayouts.containsKey(group)) {
Double groupPayout = groupPayouts.get(group);
@ -162,6 +242,7 @@ public class Configuration {
} else {
this.playerPayouts.put(playerId, payout);
}
this.save();
}
/**
@ -176,6 +257,7 @@ public class Configuration {
} else {
this.groupPayouts.put(groupName, payout);
}
this.save();
}
/**
@ -188,7 +270,8 @@ public class Configuration {
fileConfiguration.set(ConfigurationKey.BONUS_MULTIPLIER.getPath(), this.bonusMultiplier);
fileConfiguration.setComments(ConfigurationKey.BONUS_MULTIPLIER.getPath(),
List.of("A multiplier used to increase or decrease the time bonus ((hours played / hours until bonus) * bonusMultiplier) + payout"));
List.of("A multiplier used to increase or decrease the time bonus ((hours played / hours until " +
"bonus) * bonusMultiplier) + payout"));
fileConfiguration.set(ConfigurationKey.DEFAULT_PAYOUT.getPath(), this.defaultPayout);
fileConfiguration.setComments(ConfigurationKey.DEFAULT_PAYOUT.getPath(),
@ -224,9 +307,18 @@ public class Configuration {
fileConfiguration.setComments(ConfigurationKey.GROUP_PAYOUTS.getPath(),
List.of("Overrides for specific groups"));
fileConfiguration.setComments(ConfigurationKey.PAYOUT_RULES.getPath(), List.of("The rules for how the base payout " +
"is calculated. \",\" = OR, \"+\" = AND. \"p\" or \"player\" is the override for a specific player. \"g\"",
" or \"groups\" is the sum of all group overrides for a specific player. \"hg\" or \"HighestGroup\" " +
"is the highest sum of all", " of a specific player's groups. \"b\" or \"base\" is the default" +
" base payment.", "", "If you wanted to give players the sum of everything, you'd set it to " +
"\"p+g+b\".", "If you wanted to give players the sum of their personal override and their " +
"highest group, but fall back to the base", " pay, you'd set it to: \"p+hg,b\"", "If the payout" +
" rule you set ends up giving 0 as the payment, it will fall back to the default of \"p,hg,b\""));
PlayerPayouts.getInstance().saveConfig();
// Null values are necessary for updating removed keys, but should not appear otherwise
// Remove any null values, as those aren't useful except for clearing values
for (Map.Entry<String, Double> entry : groupPayouts.entrySet()) {
if (entry.getValue() == null) {
groupPayouts.remove(entry.getKey());
@ -239,4 +331,44 @@ public class Configuration {
}
}
/**
* Calculates payment from the given payout component
*
* @param payoutComponent <p>The payout component to calculate from</p>
* @param player <p>The player to calculate payment for</p>
* @return <p></p>
*/
private double calculatePayoutFromComponent(@NotNull PayoutComponent payoutComponent, @NotNull Player player) {
if (payoutComponent instanceof PayoutTarget target) {
// Get the value of the payout target
return switch (target) {
case BASE -> defaultPayout;
case PLAYER -> playerPayouts.getOrDefault(player.getUniqueId(), 0d);
case GROUPS -> getGroupPayout(player, false);
case HIGHEST_GROUP -> getGroupPayout(player, true);
};
} else if (payoutComponent instanceof PayoutAction action) {
PayoutDelimiter delimiter = action.delimiter();
if (delimiter == PayoutDelimiter.ADD) {
// Return the sum of the two components
return calculatePayoutFromComponent(action.component1(), player) +
calculatePayoutFromComponent(action.component2(), player);
} else if (delimiter == PayoutDelimiter.OR) {
// Get the result of the first component, or the second one if the first is zero
double value = calculatePayoutFromComponent(action.component1(), player);
if (value > 0) {
return value;
} else {
return calculatePayoutFromComponent(action.component2(), player);
}
} else {
throw new NotImplementedException("The encountered payout delimiter is not implemented. " +
"Please inform the developer!");
}
} else {
throw new NotImplementedException("An unknown type of payout component was encountered." +
"Please inform the developer!");
}
}
}

View File

@ -34,6 +34,11 @@ public enum ConfigurationKey {
*/
DISPLAY_PAYMENT_MESSAGE("displayPaymentMessage"),
/**
* The rules for how the base payouts are calculated
*/
PAYOUT_RULES("payoutRules"),
/**
* Payout overrides for each group
*/

View File

@ -0,0 +1,15 @@
package net.knarcraft.playerpayouts.config.payout;
import org.jetbrains.annotations.NotNull;
/**
* Instantiates a new payout action
*
* @param component1 <p>The component "to the left of" the delimiter</p>
* @param delimiter <p>The delimiter between the two components</p>
* @param component2 <p>The component "to the right of" the delimiter</p>
*/
public record PayoutAction(@NotNull PayoutComponent component1, @NotNull PayoutDelimiter delimiter,
@NotNull PayoutComponent component2) implements PayoutComponent {
}

View File

@ -0,0 +1,127 @@
package net.knarcraft.playerpayouts.config.payout;
import org.apache.commons.lang.NotImplementedException;
import org.jetbrains.annotations.NotNull;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
/**
* A parser for the payout mode string
*/
public class PayoutActionParser {
/**
* Matches a payout component from the input string
*
* @param toMatch <p>The string to match</p>
* @return <p>A payout component describing how to handle payouts</p>
* @throws ParseException <p>If unable to parse the input string</p>
*/
public static PayoutComponent matchPayoutComponent(@NotNull String toMatch) throws ParseException {
List<String> parts = tokenize(toMatch);
return parsePayoutComponent(parts, 0);
}
/**
* Parses a payout component from the given input
*
* @param parts <p>The list of components to parse</p>
* @param index <p>The index of the list to start at</p>
* @return <p>The parsed payout component</p>
* @throws ParseException <p>If unable to parse the input</p>
*/
private static @NotNull PayoutComponent parsePayoutComponent(@NotNull List<String> parts,
int index) throws ParseException {
PayoutComponent payoutComponent = null;
if (parts.size() <= index) {
throw new ParseException("Found no payout mode information to parse", 0);
}
for (int i = index; i < parts.size(); i++) {
if (payoutComponent == null) {
String part = parts.get(i);
PayoutTarget target = PayoutTarget.match(part);
if (target == null) {
throw new ParseException("Unable to parse payout mode string component " + part + "!", i);
}
payoutComponent = target;
if (parts.size() == 1) {
return payoutComponent;
}
}
// Stop here, as the last component has already been parsed
if (i + 1 >= parts.size()) {
return payoutComponent;
}
PayoutDelimiter delimiter = PayoutDelimiter.match(parts.get(++i).charAt(0));
if (delimiter == null) {
throw new ParseException("Unable to parse payout mode string delimiter " + parts.get(i) + "!", i);
}
if (delimiter == PayoutDelimiter.ADD) {
PayoutTarget target2 = PayoutTarget.match(parts.get(++i));
if (target2 == null) {
throw new ParseException("Unable to parse payout mode string component " + parts.get(i) + "!", i);
}
payoutComponent = new PayoutAction(payoutComponent, delimiter, target2);
// The index has to be held back to prevent the parser from skipping the delimiter
i--;
} else if (delimiter == PayoutDelimiter.OR) {
return new PayoutAction(payoutComponent, delimiter, parsePayoutComponent(parts, i + 1));
} else {
throw new NotImplementedException("That payout delimiter has not been implemented! Alert the developer!");
}
}
return payoutComponent;
}
/**
* Tokenizes a string
*
* @param input <p>A string.</p>
* @return <p>A list of tokens.</p>
*/
private static @NotNull List<String> tokenize(@NotNull String input) {
List<String> tokens = new ArrayList<>();
StringBuilder currentToken = new StringBuilder();
for (int index = 0; index < input.length(); index++) {
char character = input.charAt(index);
if (PayoutDelimiter.match(character) != null) {
if (!currentToken.isEmpty()) {
tokens.add(currentToken.toString());
}
tokens.add(String.valueOf(character));
currentToken = new StringBuilder();
} else if (Character.isLetter(character)) {
tokenizeNormalCharacter(currentToken, character, input.length(), index, tokens);
}
// Unrecognized tokens are ignored
}
return tokens;
}
/**
* Adds a normal character to the token. Adds the current token to tokens if at the end of the input
*
* @param currentToken <p>The string builder containing the current token.</p>
* @param character <p>The character found in the input.</p>
* @param inputLength <p>The length of the given input</p>
* @param index <p>The index of the read character.</p>
* @param tokens <p>The list of processed tokens.</p>
*/
private static void tokenizeNormalCharacter(@NotNull StringBuilder currentToken, char character, int inputLength,
int index, @NotNull List<String> tokens) {
currentToken.append(character);
if (index == inputLength - 1) {
tokens.add(currentToken.toString());
}
}
}

View File

@ -0,0 +1,7 @@
package net.knarcraft.playerpayouts.config.payout;
/**
* Interface necessary to treat both payout targets and payout actions as components
*/
public interface PayoutComponent {
}

View File

@ -0,0 +1,45 @@
package net.knarcraft.playerpayouts.config.payout;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public enum PayoutDelimiter {
/**
* Add a payout component to another
*/
ADD('+'),
/**
* Choose one if possible, and the other if not
*/
OR(','),
;
private final Character character;
/**
* Instantiates a new payout delimiter
*
* @param character <p>The character used to specify the delimiter</p>
*/
PayoutDelimiter(@NotNull Character character) {
this.character = character;
}
/**
* Tries to match the payout delimiter specified
*
* @param toMatch <p>The character to match to a payout delimiter</p>
* @return <p>The matched delimiter, or null if a match was not found</p>
*/
public static @Nullable PayoutDelimiter match(@NotNull Character toMatch) {
for (PayoutDelimiter payoutDelimiter : PayoutDelimiter.values()) {
if (payoutDelimiter.character.equals(toMatch)) {
return payoutDelimiter;
}
}
return null;
}
}

View File

@ -0,0 +1,62 @@
package net.knarcraft.playerpayouts.config.payout;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* Components used to specify a payout mode
*/
public enum PayoutTarget implements PayoutComponent {
/**
* The payout set for the specific player
*/
PLAYER(List.of("p", "player")),
/**
* The payout of all of a player's groups
*/
GROUPS(List.of("g", "group", "groups")),
/**
* The payout of a player's highest group
*/
HIGHEST_GROUP(List.of("hg", "highestGroup")),
/**
* The base payout
*/
BASE(List.of("b", "base")),
;
private final List<String> stringRepresentations;
/**
* Instantiates a new payout component
*
* @param stringRepresentations <p>The possible string representations of the payout component</p>
*/
PayoutTarget(List<String> stringRepresentations) {
this.stringRepresentations = stringRepresentations;
}
/**
* Tries to find a payout component matching the given string
*
* @param toMatch <p>A string that specifies a payout component</p>
* @return <p>The payout component matching the input, or null if no match was found</p>
*/
public static @Nullable PayoutTarget match(@NotNull String toMatch) {
for (PayoutTarget payoutTarget : PayoutTarget.values()) {
for (String representation : payoutTarget.stringRepresentations) {
if (representation.equalsIgnoreCase(toMatch)) {
return payoutTarget;
}
}
}
return null;
}
}

View File

@ -10,6 +10,15 @@ hoursUntilBonus: 100
bonusMultiplier: 1
# The percentage of their normal payout to pay AFK players
afkPercentage: 0
# The rules for how the base payout is calculated. "," = OR, "+" = AND. "p" or "player" is the override for a specific player.
# "g" or "groups" is the sum of all group overrides for a specific player. "hg" or "HighestGroup" is the highest sum of all
# of a specific player's groups. "b" or "base" is the default base payment.
#
# If you wanted to give players the sum of everything, you'd set it to "p+g+b".
# If you wanted to give players the sum of their personal override and their highest group, but fall back to the base
# pay, you'd set it to: "p+hg,b"
# If the payout rule you set ends up giving 0 as the payment, it will fall back to the default of "p,hg,b"
payoutRules: "player,HighestGroup,base"
# Overrides for specific groups. Use /setgrouppayout
groupPayouts: [ ]
# Overrides for specific players. Use /setplayerpayout

View File

@ -11,28 +11,28 @@ softdepend:
commands:
reload:
permission: timeismoney.reload
permission: playerpayouts.reload
description: Reloads the plugin
usage: /<command>
setgrouppayout:
permission: timeismoney.admin
permission: playerpayouts.admin
description: Sets the payout for a permission group
usage: /<command> <group name> <payout>
setplayerpayout:
permission: timeismoney.admin
permission: playerpayouts.admin
description: Sets the payout for a player
usage: /<command> <player name/uuid> <payout>
permissions:
timeismoney.*:
playerpayouts.*:
description: Allows usage of all commands
default: false
default: op
children:
- timeismoney.reload
- timeismoney.admin
timeismoney.reload:
- playerpayouts.reload
- playerpayouts.admin
playerpayouts.reload:
description: Allows usage of the /reload command
default: false
timeismoney.admin:
playerpayouts.admin:
description: Allows usage of configuration commands
default: false