10 Commits

Author SHA1 Message Date
b1aefdd9d9 Updates README and version to 0.6.0 2022-03-15 14:31:25 +01:00
c44ff5c890 Fixes a bug preventing default option states for paid sign conditions 2022-03-15 14:20:14 +01:00
c6d3a771c3 Fixes some bugs in the edit command
Adds missing super call to EditTabCompleter
Adds missing super call to EditCommand
Fixes the index of an argument during tab completion
Fixes a bug that caused conditions to be lost when a paid sign is changed
2022-03-14 20:20:04 +01:00
3e31c8c648 Implements the untested edit command 2022-03-14 19:00:51 +01:00
9c6921b4cd Performs some necessary work required for the edit command
Adds an implementation for the edit tab completer
Adds an enum representing a paid sign property
Adds an enum representing a paid sign condition property
Adds aliases to all commands
2022-03-14 16:20:42 +01:00
3b5218cb98 Adds a notice about payment exempt OP players to the README 2022-03-03 11:52:25 +01:00
a1c9624fe1 Updates version to 0.5.0-ALPHA 2022-03-03 11:34:09 +01:00
a7d1da7f8a Fixes a bug in refunding 2022-03-03 11:33:50 +01:00
c2ffe5e903 Adds increased detection and optional refunding of signs broken by non-players 2022-03-02 14:05:20 +01:00
3d83458b9c Makes refundPercentage use short instead of int 2022-03-02 13:02:34 +01:00
20 changed files with 798 additions and 162 deletions

View File

@ -3,6 +3,9 @@
The paid-signs plugin is a plugin that lets you add costs for creating plugin signs. This allows restricting usage of
signs such as CraftBook's gate/lift and other signs while still allowing every player to use the signs.
Note: OP players, and players with the '*' permission will have the `paidsigns.paymentexempt` permission and will not
see any payment messages. For testing, you'll need to un-set the `paidsigns.paymentexempt` permission.
## Limitations
As this plugin only listens to sign change events, there are some limitations:
@ -17,6 +20,7 @@ As this plugin only listens to sign change events, there are some limitations:
* /addpaidsigncondition <name (of a paid sign)> <line number> <string to match> \[executeRegEx] \[ignoreCase]
\[ignoreColor]
* /listpaidsigns \[name (of a paid sign)] \[line number]
* /editpaidsign <sign name> <property>/<line number> \[new value]/<property> \[new value]
* /removepaidsigncondition <name (of a paid sign)> <line number>
* /removepaidsign <name (of a paid sign)>
* /reload
@ -59,6 +63,14 @@ This lists registered paid signs and paid sign conditions. No arguments will pri
* name - The name of the paid sign to see information about
* line number - The line number of the condition to see information about
### /editpaidsign
* name - The name of the paid sign to edit
* property/line number - The property to edit for the sign, or the line of the condition to edit
* new value/property - The new property value if a property was specified in the second argument, or a condition
property if a line number was specified in the second argument
* new value - The new property value of the condition property specified in the third argument
### /removepaidsigncondition
Removes a paid sign condition from a sign
@ -88,6 +100,8 @@ Removes a registered paid sign
* ignoreColor - Whether to ignore any color or formatting applied to the text when trying to match a paid sign's text.
The option can be set on a per-sign basis, but this value is used if not specified. The correct value depends on
whether the plugin signs it should match allow coloring or not.
* enableRefunds - Whether to enable refunds to the sign creator when a sign detected as a paid sign is broken (payment
* refundsEnabled - Whether to enable refunds to the sign creator when a sign detected as a paid sign is broken (payment
will always go to the original creator)
* refundPercentage - The percentage of the paid sign cost to refund (0-100)
* refundPercentage - The percentage of the paid sign cost to refund (0-100)
* refundAlways - Whether to refund when signs that players have paid for are broken by anything. This includes tnt,
creepers, pistons and similar

View File

@ -6,7 +6,7 @@
<groupId>net.knarcraft</groupId>
<artifactId>paidsigns</artifactId>
<version>0.4.0-ALPHA</version>
<version>0.6.0-ALPHA</version>
<packaging>jar</packaging>
<name>Paid Signs</name>
@ -25,8 +25,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>16</source>
<target>16</target>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
@ -85,7 +85,7 @@
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>22.0.0</version>
<version>23.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@ -4,6 +4,8 @@ import net.knarcraft.paidsigns.command.AddCommand;
import net.knarcraft.paidsigns.command.AddConditionCommand;
import net.knarcraft.paidsigns.command.AddConditionTabCompleter;
import net.knarcraft.paidsigns.command.AddTabCompleter;
import net.knarcraft.paidsigns.command.EditCommand;
import net.knarcraft.paidsigns.command.EditTabCompleter;
import net.knarcraft.paidsigns.command.ListCommand;
import net.knarcraft.paidsigns.command.ListTabCompleter;
import net.knarcraft.paidsigns.command.ReloadTabCommand;
@ -11,7 +13,7 @@ import net.knarcraft.paidsigns.command.RemoveConditionCommand;
import net.knarcraft.paidsigns.command.RemoveConditionTabCompleter;
import net.knarcraft.paidsigns.command.RemoveTabCommand;
import net.knarcraft.paidsigns.formatting.Translator;
import net.knarcraft.paidsigns.listener.BlockBreakListener;
import net.knarcraft.paidsigns.listener.SignBreakListener;
import net.knarcraft.paidsigns.listener.SignListener;
import net.knarcraft.paidsigns.manager.EconomyManager;
import net.knarcraft.paidsigns.manager.PaidSignManager;
@ -37,8 +39,9 @@ public final class PaidSigns extends JavaPlugin {
private String language;
private boolean ignoreCase;
private boolean ignoreColor;
private boolean enableRefunds;
private int refundPercentage;
private boolean refundsEnabled;
private short refundPercentage;
private boolean refundAlways;
/**
* Instantiates a new paid signs object
@ -67,7 +70,7 @@ public final class PaidSigns extends JavaPlugin {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new SignListener(), this);
pluginManager.registerEvents(new BlockBreakListener(), this);
pluginManager.registerEvents(new SignBreakListener(), this);
registerCommands();
}
@ -120,7 +123,7 @@ public final class PaidSigns extends JavaPlugin {
* @return <p>Whether refunds are currently enabled</p>
*/
public boolean areRefundsEnabled() {
return this.enableRefunds;
return this.refundsEnabled;
}
/**
@ -128,7 +131,7 @@ public final class PaidSigns extends JavaPlugin {
*
* @return <p>The percentage of the cost to refund</p>
*/
public int getRefundPercentage() {
public short getRefundPercentage() {
if (this.refundPercentage < 0) {
return 0;
} else if (refundPercentage > 100) {
@ -137,6 +140,15 @@ public final class PaidSigns extends JavaPlugin {
return this.refundPercentage;
}
/**
* Gets whether refunds should always happen, even if signs are not broken by players
*
* @return <p>True if refunds should always happen</p>
*/
public boolean refundAlways() {
return this.refundAlways;
}
/**
* Registers the commands used by this plugin
*/
@ -144,7 +156,9 @@ public final class PaidSigns extends JavaPlugin {
registerCommand("addPaidSign", new AddCommand(), new AddTabCompleter());
registerCommand("listPaidSigns", new ListCommand(), new ListTabCompleter());
registerCommand("addPaidSignCondition", new AddConditionCommand(), new AddConditionTabCompleter());
registerCommand("removePaidSignCondition", new RemoveConditionCommand(), new RemoveConditionTabCompleter());
registerCommand("removePaidSignCondition", new RemoveConditionCommand(),
new RemoveConditionTabCompleter());
registerCommand("editPaidSign", new EditCommand(), new EditTabCompleter());
TabExecutor removeTabExecutor = new RemoveTabCommand();
registerCommand("removePaidSign", removeTabExecutor, removeTabExecutor);
@ -175,11 +189,12 @@ public final class PaidSigns extends JavaPlugin {
config.options().copyDefaults(true);
this.saveDefaultConfig();
this.saveConfig();
language = config.getString("language", "en");
ignoreCase = config.getBoolean("ignoreCase", true);
ignoreColor = config.getBoolean("ignoreColor", false);
enableRefunds = config.getBoolean("enableRefunds", true);
refundPercentage = config.getInt("refundPercentage", 100);
language = config.getString("language", "en");
refundsEnabled = config.getBoolean("refundsEnabled", true);
refundPercentage = (short) config.getInt("refundPercentage", 100);
refundAlways = config.getBoolean("refundAlways", false);
}
/**

View File

@ -11,8 +11,6 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A representation of the command for adding a new match condition for a sign
@ -43,7 +41,7 @@ public class AddConditionCommand extends TokenizedCommand {
boolean executeRegEx = false;
if (argumentSize > 3) {
executeRegEx = Boolean.parseBoolean(arguments.get(3));
if (executeRegEx && !testRegEx(sender, stringToMatch)) {
if (executeRegEx && isRegExInvalid(sender, stringToMatch)) {
return false;
}
}
@ -58,24 +56,6 @@ public class AddConditionCommand extends TokenizedCommand {
return addCondition(name, lineNumber, stringToMatch, executeRegEx, ignoreCase, ignoreColor, sender);
}
/**
* Tests whether the given regular expression is valid
*
* @param sender <p>The command sender to notify if the regular expression is invalid</p>
* @param regularExpression <p>The regular expression to test</p>
* @return <p>True if the regular expression is valid</p>
*/
private boolean testRegEx(CommandSender sender, String regularExpression) {
try {
Pattern.compile(regularExpression);
return true;
} catch (PatternSyntaxException exception) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.ERROR_INVALID_REGULAR_EXPRESSION));
return false;
}
}
/**
* Uses the given input to add a paid sign condition
*

View File

@ -1,18 +1,13 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.utility.TabCompleteHelper;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.permissions.Permission;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
/**
* The tab completer for the add paid sign command
@ -21,8 +16,6 @@ public class AddTabCompleter extends TokenizedTabCompleter {
private static List<String> names;
private static List<String> costs;
private static List<String> plugins;
private static Map<String, List<String>> permissions;
private static List<String> options;
private static List<String> booleans;
@ -33,7 +26,6 @@ public class AddTabCompleter extends TokenizedTabCompleter {
super.onTabComplete(sender, command, alias, args);
if (names == null) {
initializeValues();
loadAvailablePermissions();
}
if (argumentSize == 1) {
@ -41,7 +33,7 @@ public class AddTabCompleter extends TokenizedTabCompleter {
} else if (argumentSize == 2) {
return TabCompleteHelper.filterMatchingStartsWith(costs, arguments.get(1));
} else if (argumentSize == 3) {
return tabCompletePermission(arguments.get(arguments.size() - 1));
return TabCompleteHelper.tabCompletePermission(arguments.get(arguments.size() - 1));
} else if (argumentSize == 4) {
return TabCompleteHelper.filterMatchingStartsWith(options, arguments.get(3));
} else if (argumentSize == 5) {
@ -52,81 +44,6 @@ public class AddTabCompleter extends TokenizedTabCompleter {
return new ArrayList<>();
}
/**
* Gets the tab complete value for the permission typed
*
* @param typedNode <p>The full permission node typed by the player</p>
* @return <p>All known valid auto-complete options</p>
*/
private List<String> tabCompletePermission(String typedNode) {
List<String> output;
if (typedNode.contains(".")) {
List<String> matchingPermissions = permissions.get(typedNode.substring(0, typedNode.lastIndexOf(".")));
if (matchingPermissions == null) {
output = new ArrayList<>();
} else {
//Filter by the typed text
output = filterMatching(matchingPermissions, typedNode);
}
} else {
output = plugins;
}
//Add previous permissions in the comma-separated lists as a prefix
return output;
}
/**
* Find completable strings which match the text typed by the command's sender
*
* @param values <p>The values to filter</p>
* @param typedText <p>The text the player has started typing</p>
* @return <p>The given string values which start with the player's typed text</p>
*/
private List<String> filterMatching(List<String> values, String typedText) {
List<String> configValues = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase().startsWith(typedText.toLowerCase())) {
configValues.add(value);
}
}
return configValues;
}
/**
* Loads all permissions available from bukkit plugins
*/
private static void loadAvailablePermissions() {
plugins = new ArrayList<>();
permissions = new HashMap<>();
for (Permission permission : Bukkit.getPluginManager().getPermissions()) {
loadPermission(permission.getName());
}
}
/**
* Loads a given permission into the proper lists and maps
*
* @param permissionName <p>The permission to load</p>
*/
private static void loadPermission(String permissionName) {
String[] permissionParts = permissionName.split("\\.");
if (permissionParts.length == 1 && !plugins.contains(permissionParts[0])) {
plugins.add(permissionParts[0]);
} else if (permissionParts.length > 1) {
StringJoiner pathJoiner = new StringJoiner(".");
for (int j = 0; j < permissionParts.length - 1; j++) {
pathJoiner.add(permissionParts[j]);
}
String path = pathJoiner.toString();
List<String> permissionList = permissions.computeIfAbsent(path, k -> new ArrayList<>());
permissionList.add(permissionName);
loadPermission(path);
}
}
/**
* Initializes the values available for tab completion
*/

View File

@ -0,0 +1,186 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.container.PaidSignCondition;
import net.knarcraft.paidsigns.formatting.StringFormatter;
import net.knarcraft.paidsigns.formatting.TranslatableMessage;
import net.knarcraft.paidsigns.manager.PaidSignManager;
import net.knarcraft.paidsigns.property.OptionState;
import net.knarcraft.paidsigns.property.PaidSignConditionProperty;
import net.knarcraft.paidsigns.property.PaidSignProperty;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Map;
/**
* A representation of the command for editing a new paid sign
*/
public class EditCommand extends TokenizedCommand {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
super.onCommand(sender, command, label, args);
if (argumentSize < 3) {
return false;
}
PaidSign sign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (sign == null) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND));
return false;
}
try {
try {
//First, assume a condition is changed
return parseGivenConditionLine(sign, sender);
} catch (NumberFormatException exception) {
//Fall back to assume a sign is changed
return parseGivenProperty(sign, sender);
}
} catch (IOException e) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_EXCEPTION_OCCURRED));
return false;
}
}
/**
* Parses the given condition line and the rest of the input
*
* @param sign <p>The paid sign the user is trying to edit</p>
* @param sender <p>The command sender to notify of any errors</p>
* @return <p>True if the command was executed successfully</p>
* @throws NumberFormatException <p>If the given argument is not a number</p>
*/
private boolean parseGivenConditionLine(@NotNull PaidSign sign,
@NotNull CommandSender sender) throws NumberFormatException {
short signLine = (short) (Short.parseShort(arguments.get(1)) - 1);
if (signLine < 0 || signLine > 3 || sign.getConditions().get(signLine) == null) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_NO_SUCH_CONDITION));
return false;
}
if (argumentSize < 4) {
return false;
}
PaidSignConditionProperty conditionProperty = PaidSignConditionProperty.getFromString(arguments.get(2));
if (conditionProperty == null) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.ERROR_PROPERTY_NOT_RECOGNIZED));
return false;
}
String value = arguments.get(3);
return updateConditionProperty(sender, sign, signLine, conditionProperty, value);
}
/**
* Parses the given paid sign property and the rest of the input
*
* @param sign <p>The paid sign the user is trying to edit</p>
* @param sender <p>The command sender to notify of any errors</p>
* @return <p>True if the command was executed successfully</p>
* @throws IOException <p>If unable to remove or save the sign</p>
*/
private boolean parseGivenProperty(@NotNull PaidSign sign,
@NotNull CommandSender sender) throws IOException {
PaidSignProperty property = PaidSignProperty.getFromString(arguments.get(1));
if (property == null) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.ERROR_PROPERTY_NOT_RECOGNIZED));
return false;
}
String value = arguments.get(2);
try {
updateProperty(sender, sign, property, value);
return true;
} catch (NumberFormatException exception) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_INVALID_NUMBER));
return false;
}
}
/**
* Updates a property for a paid sign
*
* @param sender <p>The command sender to notify of any errors or success</p>
* @param sign <p>The sign to be updated</p>
* @param property <p>The property to update</p>
* @param newValue <p>The new value of the property</p>
* @throws IOException <p>If unable to remove or save the sign</p>
*/
private void updateProperty(CommandSender sender, PaidSign sign, PaidSignProperty property,
String newValue) throws IOException {
String signName = property == PaidSignProperty.NAME ? newValue : sign.getName();
OptionState ignoreCase = property == PaidSignProperty.IGNORE_CASE ? OptionState.fromString(newValue) :
OptionState.getFromBoolean(sign.getIgnoreCase());
OptionState ignoreColor = property == PaidSignProperty.IGNORE_COLOR ? OptionState.fromString(newValue) :
OptionState.getFromBoolean(sign.getIgnoreColor());
boolean matchAnyCondition = property == PaidSignProperty.MATCH_ANY_CONDITION ? Boolean.parseBoolean(newValue) :
sign.matchAnyCondition();
double cost = property == PaidSignProperty.COST ? Double.parseDouble(newValue) : sign.getCost();
String permission = property == PaidSignProperty.PERMISSION ? newValue : sign.getPermission();
Map<Short, PaidSignCondition> conditions = sign.getConditions();
PaidSignManager manager = PaidSigns.getInstance().getSignManager();
PaidSign updatedSign = new PaidSign(signName, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
for (short line : conditions.keySet()) {
PaidSignCondition condition = conditions.get(line);
updatedSign.addCondition(line, condition.getStringToMatch(), condition.executeRegex(),
OptionState.getFromBoolean(condition.ignoreCase()),
OptionState.getFromBoolean(condition.ignoreColor()));
}
manager.removePaidSign(sign.getName());
manager.addPaidSign(updatedSign);
sender.sendMessage(StringFormatter.getTranslatedInfoMessage(TranslatableMessage.SUCCESS_UPDATED_PAID_SIGN));
}
/**
* Updates a property of a condition of a paid sign
*
* @param sender <p>The command sender to notify of any errors or success</p>
* @param sign <p>The sign the condition belongs to</p>
* @param conditionIndex <p>The line index that identifies the sign condition</p>
* @param property <p>The condition property to update</p>
* @param newValue <p>The new value of the property</p>
* @return <p>True if the property was successfully changed</p>
*/
private boolean updateConditionProperty(CommandSender sender, PaidSign sign, short conditionIndex,
PaidSignConditionProperty property, String newValue) {
PaidSignCondition condition = sign.getConditions().get(conditionIndex);
String stringToMatch = property == PaidSignConditionProperty.STRING_TO_MATCH ? newValue :
condition.getStringToMatch();
boolean executeRegEx = property == PaidSignConditionProperty.EXECUTE_REG_EX ? Boolean.parseBoolean(newValue) :
condition.executeRegex();
boolean ignoreCase = property == PaidSignConditionProperty.IGNORE_CASE ? OptionState.getBooleanValue(
OptionState.fromString(newValue), sign.getIgnoreCase()) : condition.ignoreCase();
boolean ignoreColor = property == PaidSignConditionProperty.IGNORE_COLOR ? OptionState.getBooleanValue(
OptionState.fromString(newValue), sign.getIgnoreColor()) : condition.ignoreColor();
//Make sure to test the regular expression in case anything changed
if (executeRegEx && isRegExInvalid(sender, stringToMatch)) {
return false;
}
sign.addCondition(conditionIndex, stringToMatch, executeRegEx, OptionState.getFromBoolean(ignoreCase),
OptionState.getFromBoolean(ignoreColor));
try {
PaidSigns.getInstance().getSignManager().saveSigns();
} catch (IOException e) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.ERROR_EXCEPTION_OCCURRED));
return false;
}
sender.sendMessage(StringFormatter.getTranslatedInfoMessage(
TranslatableMessage.SUCCESS_UPDATED_PAID_SIGN_CONDITION));
return true;
}
}

View File

@ -0,0 +1,176 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.property.PaidSignConditionProperty;
import net.knarcraft.paidsigns.property.PaidSignProperty;
import net.knarcraft.paidsigns.utility.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The tab completer for the edit paid sign command
*/
public class EditTabCompleter extends TokenizedTabCompleter {
private static Map<PaidSignProperty, List<String>> propertyExampleValues;
private static Map<PaidSignConditionProperty, List<String>> conditionPropertyExampleValues;
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
super.onTabComplete(sender, command, label, args);
if (propertyExampleValues == null) {
initializePropertyExampleValues();
initializeConditionPropertyExampleValues();
}
if (argumentSize == 1) {
return TabCompleteHelper.filterMatchingStartsWith(TabCompleteHelper.getPaidSignNames(), arguments.get(0));
} else if (argumentSize >= 2) {
PaidSign sign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (sign != null) {
return tabCompleteForSign(sign);
}
}
return new ArrayList<>();
}
/**
* Returns tab completions for the given, non-null sign
*
* @param sign <p>The sign to tab complete for</p>
* @return <p>The tab complete options to give to users</p>
*/
private List<String> tabCompleteForSign(@NotNull PaidSign sign) {
if (argumentSize == 2) {
List<String> conditions = getAvailableSignConditions(sign);
conditions.addAll(getPaidSignProperties());
return TabCompleteHelper.filterMatchingStartsWith(conditions, arguments.get(1));
} else if (argumentSize >= 3) {
try {
return tabCompleteSignLine(sign);
} catch (NumberFormatException exception) {
if (argumentSize == 3) {
return tabCompleteProperty();
}
}
}
return new ArrayList<>();
}
/**
* Returns tab completions for the selected sign line
*
* @param sign <p>The paid sign the line belongs to</p>
* @return <p>The tab complete options to give to users</p>
*/
private List<String> tabCompleteSignLine(@NotNull PaidSign sign) {
short signLine = (short) (Short.parseShort(arguments.get(1)) - 1);
//Refuse to autocomplete if invalid input is given
if (signLine < 0 || signLine > 3 || sign.getConditions().get(signLine) == null) {
return new ArrayList<>();
} else if (argumentSize == 3) {
return TabCompleteHelper.filterMatchingStartsWith(getPaidSignConditionProperties(), arguments.get(2));
} else if (argumentSize == 4) {
PaidSignConditionProperty property = PaidSignConditionProperty.getFromString(arguments.get(2));
if (property != null) {
return TabCompleteHelper.filterMatchingStartsWith(conditionPropertyExampleValues.get(property),
arguments.get(3));
}
}
return new ArrayList<>();
}
/**
* Returns tab completions for the selected property
*
* @return <p>The tab complete options to give to users</p>
*/
private List<String> tabCompleteProperty() {
PaidSignProperty paidSignProperty = PaidSignProperty.getFromString(arguments.get(1));
if (paidSignProperty != null) {
if (paidSignProperty == PaidSignProperty.PERMISSION) {
return TabCompleteHelper.tabCompletePermission(arguments.get(2));
} else {
return TabCompleteHelper.filterMatchingStartsWith(propertyExampleValues.get(paidSignProperty),
arguments.get(2));
}
} else {
return new ArrayList<>();
}
}
/**
* Gets all paid sign condition properties
*
* @return <p>All paid sign condition properties</p>
*/
private List<String> getPaidSignConditionProperties() {
List<String> properties = new ArrayList<>();
for (PaidSignConditionProperty property : PaidSignConditionProperty.values()) {
properties.add(property.getStringRepresentation());
}
return properties;
}
/**
* Gets all paid sign properties
*
* @return <p>All paid sign properties</p>
*/
private List<String> getPaidSignProperties() {
List<String> properties = new ArrayList<>();
for (PaidSignProperty property : PaidSignProperty.values()) {
properties.add(property.getStringRepresentation());
}
return properties;
}
/**
* Gets all sign conditions available for the given sign
*
* @param sign <p>The sign to get available sign conditions for</p>
* @return <p>The available sign conditions</p>
*/
private List<String> getAvailableSignConditions(PaidSign sign) {
List<String> availableConditions = new ArrayList<>();
for (Short signLine : sign.getConditions().keySet()) {
availableConditions.add(String.valueOf(signLine + 1));
}
return availableConditions;
}
/**
* Initializes the map for paid sign property example tab completions
*/
private void initializePropertyExampleValues() {
propertyExampleValues = new HashMap<>();
propertyExampleValues.put(PaidSignProperty.COST, TabCompleteHelper.getCosts());
propertyExampleValues.put(PaidSignProperty.NAME, TabCompleteHelper.getPaidSignNames());
propertyExampleValues.put(PaidSignProperty.IGNORE_CASE, TabCompleteHelper.getOptionStates());
propertyExampleValues.put(PaidSignProperty.IGNORE_COLOR, TabCompleteHelper.getOptionStates());
propertyExampleValues.put(PaidSignProperty.MATCH_ANY_CONDITION, TabCompleteHelper.getBooleans());
}
/**
* Initializes the map for paid sign condition property example tab completions
*/
private void initializeConditionPropertyExampleValues() {
conditionPropertyExampleValues = new HashMap<>();
conditionPropertyExampleValues.put(PaidSignConditionProperty.STRING_TO_MATCH,
TabCompleteHelper.getExampleConditionStrings());
conditionPropertyExampleValues.put(PaidSignConditionProperty.IGNORE_COLOR, TabCompleteHelper.getOptionStates());
conditionPropertyExampleValues.put(PaidSignConditionProperty.IGNORE_CASE, TabCompleteHelper.getOptionStates());
conditionPropertyExampleValues.put(PaidSignConditionProperty.EXECUTE_REG_EX, TabCompleteHelper.getBooleans());
}
}

View File

@ -1,9 +1,9 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.formatting.StringFormatter;
import net.knarcraft.paidsigns.formatting.TranslatableMessage;
import net.knarcraft.paidsigns.utility.TabCompleteHelper;
import net.knarcraft.paidsigns.utility.Tokenizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -14,7 +14,6 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A representation of the command for removing a paid sign
@ -53,12 +52,7 @@ public class RemoveTabCommand implements TabExecutor {
int argumentSize = args[args.length - 1].isEmpty() ? arguments.size() : arguments.size() - 1;
if (argumentSize < 1) {
Map<String, PaidSign> allPaidSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns();
List<String> signNames = new ArrayList<>();
for (String name : allPaidSigns.keySet()) {
signNames.add("\"" + name + "\"");
}
return signNames;
return TabCompleteHelper.getPaidSignNames();
} else {
return new ArrayList<>();
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.formatting.StringFormatter;
import net.knarcraft.paidsigns.formatting.TranslatableMessage;
import net.knarcraft.paidsigns.utility.Tokenizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -7,6 +9,8 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A command executor with tokenized arguments
@ -23,4 +27,22 @@ public class TokenizedCommand implements CommandExecutor {
return true;
}
/**
* Tests whether the given regular expression is valid
*
* @param sender <p>The command sender to notify if the regular expression is invalid</p>
* @param regularExpression <p>The regular expression to test</p>
* @return <p>True if the regular expression is invalid</p>
*/
protected boolean isRegExInvalid(CommandSender sender, String regularExpression) {
try {
Pattern.compile(regularExpression);
return false;
} catch (PatternSyntaxException exception) {
sender.sendMessage(StringFormatter.getTranslatedErrorMessage(
TranslatableMessage.ERROR_INVALID_REGULAR_EXPRESSION));
return true;
}
}
}

View File

@ -20,6 +20,16 @@ public enum TranslatableMessage {
*/
SUCCESS_ADDED_PAID_SIGN_CONDITION,
/**
* The message to display when a paid sign is successfully updated
*/
SUCCESS_UPDATED_PAID_SIGN,
/**
* The message to display when a paid sign condition is successfully updated
*/
SUCCESS_UPDATED_PAID_SIGN_CONDITION,
/**
* The message to display when a paid sign has been successfully removed
*/
@ -120,4 +130,9 @@ public enum TranslatableMessage {
*/
ERROR_INVALID_REGULAR_EXPRESSION,
/**
* The error to display if an invalid property is given to the edit command
*/
ERROR_PROPERTY_NOT_RECOGNIZED,
}

View File

@ -1,27 +0,0 @@
package net.knarcraft.paidsigns.listener;
import net.knarcraft.paidsigns.manager.TrackedSignManager;
import org.bukkit.block.Sign;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import java.io.IOException;
/**
* A listener that listens for any tracked signs being broken
*/
public class BlockBreakListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onBlockBreak(BlockBreakEvent event) {
if (event.getBlock().getState() instanceof Sign) {
try {
TrackedSignManager.removeTrackedSign(event.getBlock().getLocation());
} catch (IOException ignored) {
}
}
}
}

View File

@ -0,0 +1,103 @@
package net.knarcraft.paidsigns.listener;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.manager.TrackedSignManager;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockPistonExtendEvent;
import org.bukkit.event.block.BlockPistonRetractEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import java.io.IOException;
import java.util.List;
/**
* A listener that listens for any tracked signs being broken
*/
public class SignBreakListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onBlockBreak(BlockBreakEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSign(event.getBlock(), true);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onExplosion(BlockExplodeEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.blockList());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityExplosion(EntityExplodeEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.blockList());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPistonPush(BlockPistonExtendEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.getBlocks());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPistonPull(BlockPistonRetractEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.getBlocks());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityBlockChange(EntityChangeBlockEvent event) {
if (event.isCancelled()) {
return;
}
if (event.getEntity() instanceof Player) {
return;
}
removeTrackedSign(event.getBlock(), PaidSigns.getInstance().refundAlways());
}
/**
* Removes all tracked signs from the given blocks
*
* @param blocks <p>The blocks to search for tracked signs</p>
*/
private void removeTrackedSigns(List<Block> blocks) {
for (Block block : blocks) {
removeTrackedSign(block, PaidSigns.getInstance().refundAlways());
}
}
/**
* Tries to remove any tracked sign at the given block
*
* @param block <p>The block that might be a sign</p>
* @param refund <p>Whether to perform a refund after un-tracking the sign</p>
*/
private void removeTrackedSign(Block block, boolean refund) {
if (block.getState() instanceof Sign) {
try {
TrackedSignManager.removeTrackedSign(block.getLocation(), refund);
} catch (IOException ignored) {
}
}
}
}

View File

@ -58,4 +58,14 @@ public final class EconomyManager {
economy.withdrawPlayer(player, cost);
}
/**
* Deposits a given sum into the given player's account
*
* @param player <p>The player to deposit money to</p>
* @param sum <p>The amount of money to deposit</p>
*/
public static void deposit(OfflinePlayer player, double sum) {
economy.depositPlayer(player, sum);
}
}

View File

@ -49,21 +49,22 @@ public final class TrackedSignManager {
* Removes a tracked sign from the manager
*
* @param signLocation <p>The location the sign was removed from</p>
* @param refund <p>Whether to perform a refund after un-tracking the sign</p>
* @throws IOException <p>If unable to save the tracked signs</p>
*/
public static void removeTrackedSign(Location signLocation) throws IOException {
public static void removeTrackedSign(Location signLocation, boolean refund) throws IOException {
if (!trackedSigns.containsKey(signLocation)) {
return;
}
TrackedSign trackedSign = trackedSigns.get(signLocation);
trackedSigns.remove(signLocation);
saveTrackedSigns();
if (!PaidSigns.getInstance().areRefundsEnabled()) {
if (!PaidSigns.getInstance().areRefundsEnabled() || !refund) {
return;
}
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(trackedSign.getPlayerId());
double refundSum = trackedSign.getCost() / 100 * PaidSigns.getInstance().getRefundPercentage();
EconomyManager.withdraw(offlinePlayer, -refundSum);
EconomyManager.deposit(offlinePlayer, refundSum);
if (offlinePlayer instanceof Player player) {
player.sendMessage(String.format(StringFormatter.replacePlaceholders(
StringFormatter.getTranslatedInfoMessage(TranslatableMessage.SUCCESS_REFUNDED),

View File

@ -0,0 +1,63 @@
package net.knarcraft.paidsigns.property;
/**
* A representation of a paid sign condition's editable properties
*/
public enum PaidSignConditionProperty {
/**
* The string used to trigger the paid sign condition
*/
STRING_TO_MATCH("stringToMatch"),
/**
* Whether to execute the match string as a regular expression
*/
EXECUTE_REG_EX("executeRegEx"),
/**
* Whether to ignore case for the sign condition
*/
IGNORE_CASE("ignoreCase"),
/**
* Whether to ignore color for the sign condition
*/
IGNORE_COLOR("ignoreColor");
private final String stringRepresentation;
/**
* Instantiates a new paid sign condition property
*
* @param stringRepresentation <p>The string representation of the paid sign condition property</p>
*/
PaidSignConditionProperty(String stringRepresentation) {
this.stringRepresentation = stringRepresentation;
}
/**
* Gets the string representation of this paid sign condition property
*
* @return <p>The string representation of this paid sign condition property</p>
*/
public String getStringRepresentation() {
return this.stringRepresentation;
}
/**
* Gets the paid sign property matching the given string
*
* @param propertyString <p>The string representing a paid sign condition property</p>
* @return <p>The matching paid sign condition property, or null if no such property exists</p>
*/
public static PaidSignConditionProperty getFromString(String propertyString) {
for (PaidSignConditionProperty paidSignProperty : PaidSignConditionProperty.values()) {
if (paidSignProperty.getStringRepresentation().equalsIgnoreCase(propertyString)) {
return paidSignProperty;
}
}
return null;
}
}

View File

@ -0,0 +1,73 @@
package net.knarcraft.paidsigns.property;
/**
* A representation of a paid sign's editable properties
*/
public enum PaidSignProperty {
/**
* The name of a paid sign
*/
NAME("name"),
/**
* The cost of creating a sign matching the paid sign
*/
COST("cost"),
/**
* The permission required to create the plugin sign matching the paid sign
*/
PERMISSION("permission"),
/**
* Whether to ignore case for any sign conditions added to the sign
*/
IGNORE_CASE("ignoreCase"),
/**
* Whether to ignore color for any sign conditions added to the sign
*/
IGNORE_COLOR("ignoreColor"),
/**
* Whether to treat a single condition match as a full match
*/
MATCH_ANY_CONDITION("matchAnyCondition");
private final String stringRepresentation;
/**
* Instantiates a new paid sign property
*
* @param stringRepresentation <p>The string representation of the paid sign property</p>
*/
PaidSignProperty(String stringRepresentation) {
this.stringRepresentation = stringRepresentation;
}
/**
* Gets the string representation of this paid sign property
*
* @return <p>The string representation of this paid sign property</p>
*/
public String getStringRepresentation() {
return this.stringRepresentation;
}
/**
* Gets the paid sign property matching the given string
*
* @param propertyString <p>The string representing a paid sign property</p>
* @return <p>The matching paid sign property, or null if no such property exists</p>
*/
public static PaidSignProperty getFromString(String propertyString) {
for (PaidSignProperty paidSignProperty : PaidSignProperty.values()) {
if (paidSignProperty.getStringRepresentation().equalsIgnoreCase(propertyString)) {
return paidSignProperty;
}
}
return null;
}
}

View File

@ -1,16 +1,24 @@
package net.knarcraft.paidsigns.utility;
import net.knarcraft.paidsigns.PaidSigns;
import org.bukkit.Bukkit;
import org.bukkit.permissions.Permission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
/**
* A helper class for providing common tab complete options
*/
public final class TabCompleteHelper {
private static List<String> plugins;
private static Map<String, List<String>> permissions;
private TabCompleteHelper() {
}
@ -122,4 +130,65 @@ public final class TabCompleteHelper {
return conditionStrings;
}
/**
* Gets the tab complete value for the permission typed
*
* @param typedNode <p>The full permission node typed by the player</p>
* @return <p>All known valid auto-complete options</p>
*/
public static List<String> tabCompletePermission(String typedNode) {
if (plugins == null) {
loadAvailablePermissions();
}
List<String> output;
if (typedNode.contains(".")) {
List<String> matchingPermissions = permissions.get(typedNode.substring(0, typedNode.lastIndexOf(".")));
if (matchingPermissions == null) {
output = new ArrayList<>();
} else {
//Filter by the typed text
output = TabCompleteHelper.filterMatchingStartsWith(matchingPermissions, typedNode);
}
} else {
output = plugins;
}
//Add previous permissions in the comma-separated lists as a prefix
return output;
}
/**
* Loads all permissions available from bukkit plugins
*/
private static void loadAvailablePermissions() {
plugins = new ArrayList<>();
permissions = new HashMap<>();
for (Permission permission : Bukkit.getPluginManager().getPermissions()) {
loadPermission(permission.getName());
}
}
/**
* Loads a given permission into the proper lists and maps
*
* @param permissionName <p>The permission to load</p>
*/
private static void loadPermission(String permissionName) {
String[] permissionParts = permissionName.split("\\.");
if (permissionParts.length == 1 && !plugins.contains(permissionParts[0])) {
plugins.add(permissionParts[0]);
} else if (permissionParts.length > 1) {
StringJoiner pathJoiner = new StringJoiner(".");
for (int j = 0; j < permissionParts.length - 1; j++) {
pathJoiner.add(permissionParts[j]);
}
String path = pathJoiner.toString();
List<String> permissionList = permissions.computeIfAbsent(path, k -> new ArrayList<>());
permissionList.add(permissionName);
loadPermission(path);
}
}
}

View File

@ -13,7 +13,11 @@ ignoreColor: false
# Whether to enable refunds to the sign creator when a sign detected as a paid sign is broken (payment will always go
# to the original creator)
enableRefunds: true
refundsEnabled: true
# The percentage of the paid sign cost to refund (0-100)
refundPercentage: 100
refundPercentage: 100
# Whether to refund when signs that players have paid for are broken by anything. This includes tnt, creepers, pistons
# and similar
refundAlways: false

View File

@ -9,26 +9,44 @@ description: Add costs for creating plugin signs
website: https://git.knarcraft.net/EpicKnarvik97/PaidSigns
commands:
addpaidsign:
aliases:
- aps
description: Used to add a new paid sign
usage: /<command> <name> <cost> [permission] [ignore case] [ignore color] [match any condition]
permission: paidsigns.manage
addpaidsigncondition:
aliases:
- apsc
description: Used to add a new match condition for a paid sign
usage: /<command> <name (of a paid sign)> <line number> <string to match> [executeRegEx] [ignoreCase] [ignoreColor]
permission: paidsigns.manage
listpaidsigns:
aliases:
- lps
description: Lists all previously added paid signs
usage: /<command> [sign name] [line number]
permission: paidsigns.manage
editpaidsign:
aliases:
- eps
description: Edits a property of a paid sign or a paid sign condition
usage: /<command> <sign name> <property>/<line number> [new value]/<property> [new value]
permission: paidsigns.manage
removepaidsigncondition:
aliases:
- rpsc
description: Used to remove a match condition from a paid sign
usage: /<command> <name (of a paid sign)> <line number>
permission: paidsigns.manage
removepaidsign:
aliases:
- rps
description: Used to remove a paid sign
usage: /<command> <name (of a paid sign)>
permission: paidsigns.manage
reload:
aliases:
- r
description: Reloads paid signs from disk
usage: /<command>
permision: paidsigns.reload

View File

@ -2,6 +2,8 @@ en:
PREFIX: "[PaidSigns]"
SUCCESS_ADDED_PAID_SIGN: "&bSuccessfully added new paid sign"
SUCCESS_ADDED_PAID_SIGN_CONDITION: "&bSuccessfully added new paid sign condition"
SUCCESS_UPDATED_PAID_SIGN: "&bSuccessfully updated the paid sign property"
SUCCESS_UPDATED_PAID_SIGN_CONDITION: "&bSuccessfully updated the paid sign condition property"
SUCCESS_REMOVED_PAID_SIGN: "&bSuccessfully removed paid sign"
SUCCESS_REMOVED_CONDITION: "&bSuccessfully removed paid sign condition"
SUCCESS_RELOADED: "&bSuccessfully reloaded configuration"
@ -21,4 +23,5 @@ en:
ERROR_PAID_SIGN_NOT_FOUND: "&bNo such paid sign exists"
ERROR_NO_SUCH_CONDITION: "&bThe paid sign you specified has no condition for line {line}"
ERROR_CANNOT_AFFORD: "&bYou cannot afford to create this sign"
ERROR_INVALID_REGULAR_EXPRESSION: "&bThe provided regular expression is invalid"
ERROR_INVALID_REGULAR_EXPRESSION: "&bThe provided regular expression is invalid"
ERROR_PROPERTY_NOT_RECOGNIZED: "&bThe property you tried to change was not recognized"