Adds a highly configurable option for deciding the base payout

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

View File

@ -5,11 +5,9 @@ plugin is a configurable bonus once players reach a specified total play-time on
configurable. You can alter the base payout per-group or per-player, you can adjust how often players are paid, how big
a percentage is paid to AFK players and whether players are alerted upon receiving a payout.
The logic for which payout is used is as follows:
- If a payout is set for a player, that payout will be used.
- If a payout is set for one or more of a player's group, the highest value will be used.
- If none of the above apply, the base pay is used.
You can configure how the base pay is combined from the four components of: the base payout, the highest group payout,
the sum of group payouts and the player payout. You can combine several, or have some as fallback if the others aren't
set.
Once a second, any players that have played longer than the specified payout delay are paid, and their internally
tracked playtime is reset. This is an improvement over plugins that simply pay players on a set delay, as that just ends
@ -30,7 +28,7 @@ up being a game of chance. If you are lucky, you are paid immediately after join
| Command | Arguments | Description |
|--------------------------------------|-------------------|------------------------------------------|
| /timeismoney:reload | | Reloads the configuration file from disk |
| /playerpayouts:reload | | Reloads the configuration file from disk |
| [/setgrouppayout](#setgrouppayout) | <group> <payout> | Sets the payout for a specific group |
| [/setplayerpayout](#setplayerpayout) | <player> <payout> | Sets the payout for a specific player |
@ -66,11 +64,24 @@ This command is used to override the payout for a specific group.
| hoursUntilBonus | number / -1 | The amount of hours a player must play until they start receiving a payout bonus, or -1 to disable the feature |
| bonusMultiplier | decimal number | A multiplier used to increase or decrease the time bonus ((hours played / hours until bonus) * bonusMultiplier) + payout |
| afkPercentage | percentage (0-100) | The percentage of their normal payout to pay AFK players |
| payoutRules | special string | The rules for how a player's base pay (before the bonus multiplier) is calculated. See [this section](#payoutrules) |
### payoutRules
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", which is read as "player override and sum
of group overrides + base payout". 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" (personal override and highest group override, or the
base payout). If the payout rule you set ends up giving 0 as the payment, it will fall back to the default of "p,hg,b" (
player override, or highest group override, or base payout)
## Permissions
| Permission | Description |
|--------------------|----------------------------------------|
| timeismoney.* | Grants all permissions |
| timeismoney.reload | Allows usage of the reload command |
| timeismoney.admin | Allows usage of configuration commands |
|----------------------|----------------------------------------|
| playerpayouts.* | Grants all permissions |
| playerpayouts.reload | Allows usage of the reload command |
| playerpayouts.admin | Allows usage of configuration commands |

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,23 +112,38 @@ 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;
@ -108,10 +154,44 @@ public class Configuration {
* 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) {
double maxPay = -1;
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 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